Revision history for SimpleFlow


0.16 2026-08-28 (Claude Opus 5 helped)

 [Fixed]

 - **`die => 0` never reported a failure.** The `will.do => "FAILED"` assignment
     sat inside the `if ($r{die})` branch, so it could only run on the path that
     immediately died. Under `die => 0` — the mode in which the caller is meant
     to read `will.do` — a command that exited non-zero was reported as `"done"`,
     and nothing warned. `will.do` is now `"FAILED"` for a non-zero exit, a
     timeout, or a missing output file regardless of `die`, and `die => 0` emits
     a warning naming the exit code.
 - **The log lost the record of the task that killed the run.** The log
     filehandle was never autoflushed. Measured with a `SIGKILL` part-way through
     a pipeline (the shape of an OOM kill or a scheduler eviction), a log holding
     862 bytes on a clean exit held 139 bytes after the kill: everything written
     after the last command started — its exit code, duration and captured output
     — was still in stdio's buffer. `task` and `say2` now switch the handle to
     autoflush.
 - **An undefined filename still crashed.** 0.14 added a `defined` guard to the
     0-length check, but the `-f -r` filetest ran first, so an `undef` element of
     an `input.files` array died as `Use of uninitialized value $_ in -r` under
     `warnings FATAL => 'all'`. Names are now validated before anything is
     filetested.
 - **The 0-length `input.files` check was unreachable.** `''` fails `-f`, so an
     empty input filename was reported as `"missing or unreadable"` and the
     0-length check below it could never fire. Both undefined and 0-length names
     are now reported as what they are, and the message names the offending index.
 - **`cmd` was not type-checked.** Only definedness was checked, so any reference
     was stringified straight into the shell: `task(cmd => ['echo','hi'])` ran the
     literal command `ARRAY(0x5ed9d076e618)`. `cmd` must now be a non-empty string
     or a non-empty array ref of defined values.
 - **Skip detection and the post-run check disagreed.** Skipping tested a bare
     `-f` while the post-run check tested `-f -r`, so an output file that existed
     but could not be read counted as already done. Both use `-f -r` now.
 - **The result record changed shape between paths.** `exit`, `signal`, `stdout`
     and `stderr` were absent after a skip or a dry run, so a caller running under
     the `warnings FATAL => 'all'` this module recommends died just by reading
     `$t->{'exit'}`. They are now always present, holding their empty values.
 - **`string_max` was uncapped**, so a chatty command had its whole capture echoed
     to the terminal and written to the log — a measured 3 MB stdout wrote
     3,002,832 bytes to each. It is now capped at 4096 characters; Data::Printer
     marks what it drops. The full capture is still on the result hash.
 - **Loading SimpleFlow polluted `main::`.** `use DDP` and `use Cwd 'getcwd'` sat
     above the `package` statement, so `p`, `np` and `getcwd` were imported into
     every program that loaded the module. The `package` statement now comes
     first, and the duplicated `use` lines are gone.
 - **Unbalanced parenthesis** in the 0-length `output.files` error message.

 [Added]

 - **`stale`**: also re-run when an input file is newer than an output file, the
     rule `make` and `snakemake` use. Off by default, so existing pipelines are
     unaffected. The result carries `out.of.date`.
 - **`timeout`**: a wall-clock budget in whole seconds. The command runs in its
     own process group and the whole group is killed if the budget is exceeded,
     so a wedged pipeline does not leave orphans behind. The result carries
     `timed.out`. POSIX only.
 - **An array-ref `cmd`** runs the command without a shell, so arguments coming
     from data need no quoting.
 - **`quiet`**: suppress the record printed to the terminal without silencing the
     log or `STDERR`.
 - **`input.file`**, the single-file convenience form of `input.files`, matching
     `output.file`.

 [Changed]

 - `$VERSION` is now a quoted string. As a bare number it was stringified through
     `%g`, so a future `0.20` would have become `"0.2"` and compared as older than
     `"0.15"` on CPAN.
 - **Incompatible:** `input.files` on the result is now always an array ref, as
     `output.files` always was. A scalar argument used to be stored raw.
 - `POSIX` (core) is now a dependency, for `_exit` in the timeout child.

0.15 2026-07-17 (Claude Opus 4.8 helped)

 - addition of `output.file`, a single-file convenience form of `output.files`. It
   takes one plain filename, cannot be combined with `output.files`, and dies if
   given a reference or an empty name.

 - removal of Term::ANSIColor dependency

 - improved coverage testing

0.14 2026-06-29 (Claude Opus 4.8 helped)

 [`task`]
 - **New:** accepts a flat key/value list as well as a hash ref —
     `task(cmd => ...)` and `task( cmd => ... )` are now equivalent. A lone
     non-hashref scalar or any odd-length argument list is fatal.
 - **Bug fix:** the default `die => 1` was ignored when checking for missing
     `output.files`. The block tested the raw `$args->{'die'}` (undef when the
     caller omitted it) instead of the resolved `$r{'die'}`, so a command that
     failed to produce its declared outputs only warned instead of dying. Now
     consistent with the exit-code check.
 - **Bug fix:** removed a stray `)` (and an extraneous leading space) from the
     "command is" line written to the log file; it now matches the on-screen form.
 - **Bug fix:** `length $_ == 0` could throw a fatal uninitialized-value warning
     (under `warnings FATAL => 'all'`) on an undef element of the `input.files`
     array branch and the `output.files` empty-name check. Both now guard with
     `(defined $_) && (length $_ == 0)`, matching the `input.files` scalar branch.

0.13 2026-06-11

 [Fixed (Claude Opus 4.8 helped)]

 - **Exit status and signal are now decoded correctly.** `task()` previously
     computed the exit code (`$status >> 8`) and *then* derived the signal as
     `$exit & 127`. Because the signal lives in the low byte of the raw wait
     status, which `>> 8` discards the `signal` field was always wrong: a clean
     `exit 42` was reported as `signal 42`, and a process actually killed by a
     signal reported `signal 0`. The signal is now read from the raw status before
     shifting, so `exit` and `signal` are independent and accurate.

 - **No longer dies on a missing output file when `die => 0`.** The zero-size
     check did `(-s $file) == 0`, which is `undef == 0` when a declared output file
     is absent. Under `use warnings FATAL => 'all'` that "uninitialized value"
     warning was fatal, so a task that was meant to *warn* about missing output
     (with `die => 0`) crashed instead. Missing sizes are now treated as `0`, so
     the task warns and returns its result hash as intended.

 - **The "already done" result is now logged with its `duration`.** In the
     short-circuit path (output files already exist), `duration` was set *after*
     the record was written to the log, so the logged hash was missing it; the
     duplicate `done => 'before'` assignment was also removed.

 [Changed / Windows support]

 - **Portable exit-status handling.** Decoding now branches on `$^O`: Windows has
     no POSIX signals (`signal` is reported as `0` there), and a `system()` that
     fails to launch the command (`-1`) yields `exit => -1` instead of a garbage
     value from shifting `-1`.

 - **ANSI colour is disabled on the legacy Windows console.** `Term::ANSIColor`
     output is suppressed on `MSWin32` unless an ANSI-capable terminal is detected
     (Windows Terminal, ConEmu, or ANSICON), so `cmd.exe` no longer prints raw
     escape sequences and redirected logs stay clean. Unix and modern Windows
     terminals are unaffected.

 [Tests]

 - Rewrote `t/01.t` to be cross-platform: shell commands now invoke the running
     Perl interpreter (`"$^X" -e ...`) instead of Unix-only tools (`which`, `ls`,
     `ln`, `cp`), and temp files use the system temp directory instead of a
     hard-coded `/tmp`.
 - Added regression tests for both fixed bugs (exit/signal decoding; surviving a
     missing output file with `die => 0`).
 - Added coverage for the `note` field, the `input.file.size` / `output.file.size`
     hashes, scalar-vs-array normalisation of `input.files` / `output.files`, the
     `dir` / `source.file` / `source.line` metadata, captured `stdout` / `stderr`
     (including trailing-whitespace stripping), and argument validation (missing
     `cmd`, unknown keys, bad `log.fh`, missing input files).

0.12 2026-02-14

 - exit code now matches what shell would show it as; signal now appears

0.11 2026-01-13

 - max string length now corresponds to max of output strings, no more truncated output
   added List::Util dependency for string length maxes
   memory size now shows when output
   directory is now output during dry runs

