Revision history for Proc-Spawner (EXPERIMENTAL)

0.12  2026-11-13
      - Since 0.11: X27's steps 2 and 3 (large-subset-environment.md,
        sections 10 to 12): every undecided sample's dependences named and
        tried in example environments (2,902 samples of step 2; step 3's
        classes: pass and split almost strict under passthrough and
        splitglob, LV1 rows of their own; control and affects-shell run in
        a root of their own; the 93 left out classified by reading, AI-);
        the match of dash, bash and ours over every environment, messages
        counted nowhere, each difference with the ruling that allows it
        (section 11: alike 38,603, legalized 1,620, awaiting A12 88, chance
        40, not legalized 45); the runner waits for background jobs (A18);
        every run result redone from nothing on 2026-11-13.  Bugs of ours
        fixed: X39 to X55 (. of an empty file, [[:digit:]] in pathnames,
        "${v#$x}", exit, return, set -o, shift and cd with odd operands,
        the message of a command that does not start, return outside a
        function, ignored signals, expansion errors in here-documents and
        when traps run, unquoted values with pattern characters, command
        forms made at run time, the deferred SIGCHLD handler); X56 found,
        open.  The documents kept in line with their records (make
        docs-check, doc/docs-sync.tsv); the corpus analysis through make
        (make corpus-help); MetaCPAN experiments (Proc::Spawner::Docs
        links, doc/metacpan-tables.md; TODO A15).  The parse figures of
        post4-0.04 hold (the measured files differ only in $VERSION); digest
        of the code they depend on:
        06885fddc5089202adf750a065bd3b393fe6906b1727a9d09d815a9a333ec8ab.
      - The run results again (TODO B, done): every corpus run rebuilt from
        nothing (make corpus-all; the parse figures' digest unchanged, the
        scan and the figures the same), with the code of post-0.11 (X39 to
        X55 fixed) and the runner waiting for background jobs (A18), which
        changes dash's and bash's results too.  Section 7 of
        large-subset-state.md and README's run table: ours differs 6 (was
        3; with the programs inside Perl, 4): printf -v 2, the zenity
        subshell, a race, ls -l of /, and exec command -with -flags (X56);
        same 15,786, messages only 23,390, the shells differing 1,213;
        post-0.09's table kept as a part of its own.  The atomic calls
        again from this run: 650 calls of the six commands (757 from
        post4-0.04's run), the builtins as before within one or two.
        large-subset-environment.md: the variants (section 9; posix,
        sigpipe, output as before in kind), step 2 (section 10: 2,902
        samples, 1,550 unchanged, 1,348 changed, the same 4 neither), step 3
        (section 12: both trees as before but for one race, NB:12523), the
        races (section 12: only the zenity subshell left, persistent, not
        a race: TODO A17).  The match (section 11) now counts every snippet
        over all the environments it ran in: the base run, step 2's setups
        and step 3's (large-subset-matchreport.pl -run FAMILY/LABEL=DIR,
        -setups FAMILY=FILE; make corpus-match; corpus-all now runs
        corpus-step3 and corpus-races too): alike 38,603, ours not as both
        1,793: legalized 1,620, awaiting A12 88, chance 40, not legalized 45
        (of them 30 cd with extra words and 2 new races, TODO A13).
      - X56 (open): exec with a builtin's name runs ours' builtin; dash and
        bash look only for a program (not found, 127).  The base grammar
        refuses after exec only cd, : and exit.  Found by the run.
      - The tables of levels count the samples classified by reading as
        N+M=T (the user, 2026-11-13): N found by the programs, M by reading
        (AI-strict with strict, AI-values passed on ... with those rows; the
        other 52 stay undecided), T both; N alone where M is 0: strict
        365+27=392, with values passed on unchanged 664+7=671, on split
        1,476+7=1,483, undecided 3,508+52=3,560 (large-subset-blocks.pl and
        -matchreport.pl -ai; make corpus-provenance, corpus-match).
      - LV1 (the user, 2026-11-13): the samples almost strict only by step
        3's assumptions are rows of their own in every table of levels:
        "with values passed on unchanged, not in preceding rows" 664
        (passthrough) and "with values passed on split, not in preceding
        rows" 1,476 (passthrough and splitglob; none needs splitglob alone);
        almost strict 34,302.  large-subset-blocks.pl and -matchreport.pl
        print them; the record keeps A, its codes tell them apart.
      - X27, step 3: the 93 undecided samples the setups left out,
        classified by reading (the user's categories: AI- and a category
        used before, AI-PATH-twister new; TODO A19): record
        large-subset-ai-classes.tsv, large-subset-blocks.pl -ai writes the
        category as the class in large-subset-step3.tsv (the levels and the
        setups unchanged, checked).
      - The tools: large-subset-threeway.pl and -matchreport.pl split the
        builtins log right (a record starts with any handler's name; a
        call of Proc::ShellSpawn::Large::b_... or "not found" had been glued
        to the call before it); large-subset-ids.pl and the match report
        read the codes inside passthrough(...) too (the variants had
        missed 578 samples, the report 48 legalizations by B2 and $0);
        large-subset-match-known.tsv marks X56's sample and the zenity
        subshell open.
      - X55 fixed (found by the races run many times): ours' deferred
        SIGCHLD handler between run() calls (K3) did not keep the
        program's $? and $!; its waitpid set $? (-1 when nothing had
        ended), so a job ending while the program ran its own code changed
        $?, seen as ours' status 255 without a message in about 1 run of
        200 of samples with a background job.  Test in t/between.t.
      - large-subset-runcompare.pl -after SECONDS (default 5; A18, the
        user, 2026-11-12: the runner waits for every process of the run,
        with a reasonable timeout): background jobs still running when the
        shell ends get up to 5 s, then are killed ("after": "killed");
        with -env pidns, process 1 of the namespace reaps and waits.
        -after 0: as before.
      - X27, step 3: the 33 races of the class affects-shell run 20 times
        each in every environment where ours was not as both shells
        (make corpus-races; large-subset-repeat.pl, record
        large-subset-env-races.tsv).  First round: 14 were not races but a
        background job still running when the text ends, which ours
        finishes before it ends and the run killed in the shells (A18,
        decided: the runner waits, -after).  Second round, with the wait
        and X55 fixed: 27 the same in every run, 6 races (chance).  The
        class affects-shell now: 584 alike in every environment (was 556),
        chance 10, not legalized 33 ([^...] 3, cd with extra words 30).
      - X27, step 3: setups for names with an expansion (a file or
        directory whose name only an expansion gives at run time: cat <
        "$fname", cd `dirname $f`, ~/.bashrc; the user: choose the value
        and make the file of that name together).  large-subset-setups.pl
        -step3 chooses every value in such a name (x1, or the run's own:
        home/, the run's directory, $$ 2 in a PID namespace; a program in
        it a stand-in answering x1) and makes the file of that name in
        the states of its kind; the setups of every other sample are as
        before.  119 of the 149 left out got setups (control 34,
        affects-shell 85); left out 30 (18 from a variable the text sets:
        TODO B).  Ran in step 3's two environments: 38 legalized by
        rulings, A12 21, chance 1, not legalized 31 (cd with extra words
        30, head ... 10> filename 1: TODO A13).  Records
        large-subset-env-control.tsv, large-subset-env-affects-shell.tsv
        and its calls.  Partially decided now 3,508 of the 3,601
        undecided.
      - X54 fixed (found by those setups): cd with more operands made by
        an expansion at run time (x="d e"; cd $x) said "cd: bad
        argument", status 2, neither shell; now "too many arguments",
        status 1, as bash and as with the operands in the text (rule E).
        The 54 samples of both classes whose cd operand holds an
        expansion ran again.  Test in t/programs.t.
      - X27, step 2 for the class affects-shell, done (section 12): 948
        samples (-env samearg0, pidns); of the 410 where ours is not as
        both shells: legalized 301, awaiting A12 67, chance 39 (33 races;
        6 persistent results without a ruling: TODO A17), not
        legalized 3 ([^...], A13).  Partially decided 3,389 of the 3,601
        undecided.  Records large-subset-env-affects-shell.tsv and its
        calls.  The match report knows A4 by ours' status 248 and its
        message; NB:685 (two readers of one pipe) as chance.
      - X53 fixed (found by step 2 on the class affects-shell): a form of
        command the parser refuses, made only by expansions at run time
        (x=-n; command $x), is outside the subset at run time (A4, status
        248), no longer a program named -n.  large-subset-runcompare.pl
        -env clock marked not to be used (libfaketime 0.9.10 opens its
        shared memory as descriptors 3 and 4 in every process).
      - large-subset-runcompare.pl: -env samearg0 (the three shells get
        the same $0), pidns (every run in a PID namespace of its own: $$,
        $PPID, $! the same in every run), clock (libfaketime: the shell's
        clock stands at FIXTURE_MTIME; timeout keeps the real one); tried
        on $$ $PPID, $0, date and $SECONDS, $!, env (the same in two runs).
        large-subset-setups.pl -class affects-shell (with -step3): every
        code under the run's control, the shells' own variables varied
        too (not the run's).  For step 2 on the class affects-shell (the
        user, 2026-11-12).
      - X27, step 2 for step 3's control class, done (section 12;
        make corpus-step3): 2,441 samples in 8,862 setups; where the
        environment changed the class, only into a difference a ruling
        legalizes (A5 1,066, builtin-vs-coreutils 49, rule E 3, Q2 2);
        not legalized 1, the same in every environment (cd with extra
        words, TODO A13).  Records large-subset-env-control.tsv,
        large-subset-env-control-calls.jsonl.  Partially decided now 2,838
        of the 3,601 undecided.  Tools: the call logs' records may span
        lines (an argument with a newline): large-subset-threeway.pl and
        large-subset-matchreport.pl join them, and the comparison of the
        programs' calls keeps empty lines; matchreport -step3 (the
        record's header); G2 by a setup's description in
        large-subset-match-known.tsv; large-subset-setups.pl leaves out
        setups that take what the run needs (the loader, /tmp itself);
        the runner records a run whose environment cannot be made
        instead of stopping, and makes a setup's absolute names after
        its mounts.
      - X27, step 2 for step 3's control class, prepared:
        large-subset-blocks.pl -d names a redirection's or a test
        operator's absolute target too (the levels and the step-3 record
        unchanged); large-subset-setups.pl -step3: the control samples,
        absolute names (made in the run's own root), the programs'
        answer as one more dependence, the programs the record names as
        stand-ins, a setup 0 for the base runs (2,444 samples, 8,862
        setups; the step-2 mode makes step 2's setups byte for byte from
        its stored dependences; from today's -d it adds NB:5285, whose
        path-root was the '/' slip); make corpus-step3.
      - large-subset-runcompare.pl: -standin LIST (programs it runs for
        real become stand-ins too) and a setup's own "standin" list;
        -env cowroot: every run gets a root of its own, a copy-on-write
        overlay of /, so that a setup can make or remove files anywhere
        (absolute names) and nothing a run writes stays.  Tried on
        TL:linux/nologin.md:12 (> /etc/nologin.txt fails as A5 allows,
        succeeds with the file there; the real /etc untouched) and
        TL:common/ls.md:24 (ls as a stand-in answering x1).  For step 2
        on the control class (the user, 2026-11-12).
      - X27, step 3 (the user's four steps; large-subset-environment.md,
        section 12): large-subset-blocks.pl follows every value a sample
        takes from the environment to where it lands, and classes each
        sample its codes leave undecided (-p; record
        tools/large-subset-step3.tsv): pass 664, split 1,476, control
        2,525, affects-shell 1,076.  Pass and split are almost strict now (the
        user's (a)): code A:...,passthrough(r,s)[,splitglob], two new
        assumptions in table 1, probed by large-subset-valueprobe.pl
        (make probe-values).  Levels: almost strict 36,442 (was 34,291),
        undecided 3,601 (was 5,752; partially decided 1,682 of them).
        Checked against step 2's runs: pass 270 of 270 and split 948 of
        949 kept their class in every setup, control 1,278 of 1,285
        changed only within the ruling named (the rest explained).  The
        tool's slips fixed: '/' as a delimiter (cut -d /, awk -F/) was
        taken for the whole tree (11 samples); the codes of a command
        after a $( ) in its words went to the command inside it (block
        numbers of -d; the levels unaffected).  README and
        large-subset-state.md quote the new figures.
      - X52 fixed (found by a probe of splitting and globbing, X27): a word
        from an unquoted expansion whose only pattern characters are
        escaped by a backslash is not matched against files (x\* stays
        x\*, as both shells).
      - X50 fixed (found by large-subset-difffuzz.pl -j 4, seed 3, two
        hours): an expansion error inside a here-document (set -u,
        ${x:?}, arithmetic) fails that redirection, as dash (the command
        does not run, status 2, the shell goes on; a special builtin's
        still ends it), and the body is expanded with the redirections
        made before it (its message, a $( ) in it).  X51 fixed: a trapped
        signal's action runs after the foreground command, also when that
        runs in this process (a pipeline of builtins), and a trap set in a
        subshell no longer replaces the main shell's handler (a later
        signal was lost).  Tests in t/programs.t (8 of the 12 new ones
        fail before the fixes).  The fuzzer's run: 315,187 strings,
        285,676 accepted; same 251,243, OURS 826, =dash 27,358, =bash
        5,792, neither 456, timeout 1 (TODO B; return || shift, ours as
        neither shell, joins the not legalized of section 11 and TODO A13).
      - MetaCPAN experiments for 0.12 (TODO A15): Proc::Spawner::Docs links
        each document twice: its name to the rendered file of the newest
        release (/dist/Proc-Spawner/view/...), and, in HTML only, "The
        same file in this release" relatively (../../../doc/...: works
        from /release/.../view/ and /dist/.../view/, not from /pod/).
        doc/metacpan-tables.md: one table in five forms (pipe table, raw
        HTML, preformatted indented and fenced, a list), to choose the
        form the documents will use.
      - large-subset-difffuzz.pl: -j WORKERS (each makes the whole
        sequence and runs every WORKERS-th string; the same output as
        -j 1 apart from the shells' races, checked with seed 2, 300
        strings, -j 1, 3, 4), -time SECONDS, -n 0 for no limit, a
        progress line on stderr; each shell runs in run/work and the
        files of run/ are compared ('cd /tmp; cd -' is now 'cd ..; cd -',
        so no run leaves files in /tmp for the next); the worker's
        directory in the output reads RUN.
      - X27, step 2 for files, variables and standard input (the user's
        four steps, 2026-11-10; TODO A1; large-subset-environment.md,
        section 10): large-subset-blocks.pl -d records each undecided
        sample's dependences exactly (the glob, the file read, written,
        tested, sourced or run, the variable, the input; the levels and
        large-subset-provenance.tsv unchanged); large-subset-setups.pl
        gives three example setups each (a file with text, empty, a
        directory, read-only; a glob with no match, one, two; a variable
        empty, x1, "two  words *"; input of two lines, ...);
        large-subset-runcompare.pl -setup applies one.  Run on the 2,478
        samples that depend only on these by exact names, against two base
        runs: 1,349 kept their class, 1,106 changed it with ours still on
        a shell's side (nearly all the A5 status of a failed redirection
        coming and going), 15 moved to where ours agrees with neither:
        five bugs of ours and explained noise.  Record:
        tools/large-subset-env-setups.tsv.
      - X39 (ours, found by it; fixed): '.' of an empty file or a directory
        printed "sh: the: not found", then Perl died (the file was read in
        list context as a function's argument, so an empty text was no
        argument at all).  Test: t/batteries/largebattery.txt.
      - X40 (ours, found by it; fixed): a character class ([[:digit:]])
        never matched in pathname expansion (Perl's bsd_glob has none): each
        is now spelled out as its characters, the C locale's.  Test:
        t/batteries/largebattery.txt.
      - X41 (ours, found by it; fixed): in "${v#$x}" (and %, ##, %%) the
        large parser marked $x quoted by the outer "...", so its value was
        taken literally; POSIX and both shells take it as a pattern.  The
        figures' digest is now
        89075620df87ad8646ce8bc256843530dab5a2ff6a6e9dbcf6ff7dee86929a5f;
        measured again (the stored scan reparsed): figures, samples.tsv,
        the scan and large-subset-provenance.tsv identical.  Test:
        t/batteries/largebattery.txt.
      - NV1 (the user, 2026-11-10: "as the shells"; X42, X43, found by it):
        an operand of exit, return, break, continue known only at run time
        that is no number as dash takes it (digits, maybe after +, at most
        2^31-1), and set -o or set -LETTER with no such option, are an
        error with status 2 that ends the shell, as dash (before: exit 0
        after Perl's warning, $? set to x1, nothing); arithmetic on such a
        value no longer prints Perl's warning; set -o with no name at run
        time: A4.  Tests: t/batteries/largebattery.txt.
      - X27, step 2 extended (the user, 2026-11-10: "Continue"): also
        positional parameters, cd's operand and HOME (~, cd alone), and
        name, test-ops, eval, unquoted where a variable or argument of the
        same command brings them: large-subset-blocks.pl -d records them
        exactly; setups of arguments (one empty, x1, three), cd's target (a
        directory, a file, mode 000) and HOME (missing, empty, unset);
        large-subset-runcompare.pl -setup gives the shells arguments after
        the text, with $0 as each has it, and unsets a variable given as
        null; t/try.pl takes $0 and arguments after the text, as sh -c.
        In all 2,901 samples, 1,549 unchanged, 1,307 changed with ours on a
        shell's side, 25 moved to where ours agrees with neither, none a
        bug (coreutils' messages, decided; known message prefixes; a
        timeout).  The 267 samples with two dependences got every
        combination of their states, 9 setups (large-subset-setups.pl
        -full 2; the user: "3^m tries" as far as they add at most 25 %:
        18 %); the six more found nothing of a new kind.
      - X44 (ours, found by it; fixed): cd into a directory that may not be
        searched (mode 000) succeeded; now dash's error, status 2.  Test:
        t/programs.t (as a user other than root).
      - X45 (ours, found by it; fixed): cd with HOME empty failed ("HOME not
        set"); now nothing, status 0, as both shells (HOME unset: as
        before, rule E).  Test: t/batteries/largebattery.txt.
      - X46 (ours, found by the match report; fixed): shift with an operand
        known only at run time that is no number (shift -1, shift '[n]')
        did nothing or printed Perl's warning; now as NV1, dash's error.
      - X47 (ours, found the same way; fixed): the message of a command
        that does not start (not found, Permission denied) went to the
        shell's standard error, not to the command's (nosuch 2>log left the
        log empty); now as both shells.  Tests: t/batteries/largebattery.txt.
      - large-subset-setups.pl -one: samples with more dependences than
        -full covers get, after setups 1-3, each dependence alone in each
        state (3 + 3m setups; the user: "3+3*m where 3^m is too large").
      - large-subset-matchreport.pl (new) and large-subset-match-known.tsv:
        the match of the three shells across the environments a sample ran
        in (shells agree, some, differ; ours as them, with dash, bash,
        neither; justified by a rule, presumed, or not), beside the
        snippet's level, with the new level "partially decided" (the user,
        2026-11-10): an undecided snippet whose every dependence was named
        exactly and tried in example environments.
      - With HOME empty, dash drops the word ~ (and ~/x loses its
        prefix): ❰BUG❱ dash (POSIX: the result of ~ is as if quoted; bash
        and ours give an empty field).
      - X27, step 2 complete for the codes inside the run, and the match
        across environments (large-subset-environment.md, sections 10 and
        11, rewritten): every partially decided sample ran again with all
        fixes, in 3, 9 or 3 + 3m setups (10,719 in all): 1,547 unchanged,
        1,350 changed with ours on a shell's side, 4 to neither
        (explained).  The whole library's ours ran again with the fixes
        (ours changed in 82 samples: 33 by X43, X46, X47, the rest by
        chance), joined with the full run's dash and bash.  The match:
        every sample where ours is not as both shells has a rule (1,199
        A5, 170 G16, 67 B2, ...), is unchecked (51: "the program beats"
        not checked call by call), proposed (62: the ~ rule), chance (79)
        or open (2: cd - with OLDPWD unset); none without.  Section 1's
        level table has the partially decided (2,901).  The record
        tools/large-subset-env-setups.tsv is now written by
        large-subset-matchreport.pl -record (one column of setup classes,
        what each setup did); the match per sample:
        tools/large-subset-match.tsv.  TODO A12, A13: the ~ rule, the
        unchecked cases.  The tables list every case, 0 included; N/A where
        a level ran in one environment only (the user, 2026-11-11).
      - X27, section 11: builtin-vs-coreutils (ours does echo, printf,
        test, [, kill, pwd as coreutils, dash and bash by their builtins) is
        now a checked reason, no longer guessed from the text (TODO A13;
        the user, 2026-11-11: "do both").  large-subset-runcompare.pl -q
        reran ours in every environment of the 1,635 samples where ours is
        not as both shells (base and setups, about 5 minutes);
        large-subset-threeway.pl takes several logging runs, the calls
        made inside Perl (IP1: logged as builtin calls of
        Proc::ShellSpawn::inproc; it read only the program calls before),
        and -ours DIR, a fourth answer: ours alone.  All 156 distinct calls:
        ours = coreutils in status and output.  large-subset-matchreport.pl
        -calls DB -q LABEL=DIR: the reason holds in an environment when a
        call there has coreutils' answer differing from the builtin of
        each shell ours differs from, ours alone answering as coreutils; it
        goes before chance by the snippet's codes (a guess from the text;
        20 samples moved from chance to it); a column says per sample
        whether it holds.  -o again lists only the samples where ours is
        not as both shells everywhere (it listed all).  Draft figures:
        the reason holds in 80 samples (64 by it alone); no reason in 7.
      - X27, section 11: messages count nowhere now, wherever a
        redirection sends them (the user, 2026-11-11: to be compensated
        before the analysis).  large-subset-runcompare.pl records each
        file also by its hash without message lines (files_nomsg: a line
        starting with dash's, bash's or our prefix, the same pattern for
        all three); large-subset-matchreport.pl drops such lines from
        standard output and compares files by files_nomsg; the rules for
        messages in the rule table apply only with -raw (the former
        comparison).  The 34 samples whose files differ between the shells
        reran in all their environments (3 minutes): 11 more where all
        three agree, 5 from chance to A5; between the two runs 3 changed
        by chance (a fifo, ls -a | tee output.file: a race; NB:1168).
      - X27, section 11 rewritten in the user's layout (2026-11-11): two
        trees of every snippet (the match; the same with each box split
        by legalization: legalized by rulings, awaiting A12, chance, not
        legalized), columns by builtin-vs-coreutils (R-program, checked
        command by command); the commands' four answers; the rulings; the
        9 not legalized (TODO A13, replacing the unchecked cases: 7 new,
        to be decided with the rules).  large-subset-matchreport.pl prints
        the trees itself (-raw: large-subset-match-raw.md, the former
        comparison of messages, not part of the analysis).  Records:
        tools/large-subset-match.tsv (a column for builtin-vs-coreutils),
        tools/large-subset-match-calls.jsonl (155 commands of the table, 1
        more of a snippet that now agrees).  README links section 11 and
        gives its figures: 38,773 alike everywhere; of 1,622, 1,498
        legalized, 62 awaiting A12, 53 chance, 9 not legalized.
      - Corpus analysis through make (the user, 2026-11-11): Makefile.PL
        copies doc/corpus.mk into the Makefile: make corpus-help lists a
        target for every step (the sources, pools A and B, the scan and
        figures, the digest, the provenance, the shells' verdicts, the
        behaviour runs and their reports, the environment variants, step 2,
        section 11's match, the Markdeep copy), each rewriting the record
        it makes; doc/test-pools.md, section 0; README points there.  New:
        large-subset-corpora.pl (fetch the pinned sources),
        large-subset-ids.pl (the samples of some codes);
        large-subset-runreport.pl -bl -tag -noise writes the lines of
        large-subset-env-variants.tsv, which no script wrote before.
        Checked: corpora, samples, figures (digest 89075620...),
        provenance, shells' verdicts reproduce the shipped files; the
        variants, step 2 and the match, over the existing runs, give the
        shipped records byte for byte (but for explained differences).
        large-subset-state.md.html regenerated (it lagged the .md).
        MetaCPAN (checked by the author): .md files render under /view/,
        relative links between them work, tables do not (TODO A15); 53
        links into the private repository fail for others (TODO A14).
      - The documents in line with what they show (the user's idea,
        2026-11-11): doc/docs-sync.tsv maps each part of a document that
        shows results (large-subset-environment.md sections 1/3, 9, 10,
        11; large-subset-state.md sections 3 and 7; README's figures;
        large-subset-match-raw.md; the Markdeep copy) to the records,
        reports and scripts it draws on, with their SHA-256 (line ends as
        LF) when last brought in line; make docs-check (doc/docs-sync.pl)
        names every part whose inputs changed, and what to redo; make
        docs-synced DOC=... records the new hashes; make docs-map.
        Hashes, not times: Git keeps none.  t/docs-sync.t fails on a part
        not in line, only with RELEASE_TESTING=1.  Checked: all 10 parts
        are in line now (the digest, the records unchanged since 0.11,
        every line of section 11's trees as the report prints it); a
        changed input flags exactly the parts drawing on it.  TODO: A15
        (the MetaCPAN experiments, in 0.12), A16 (large-subset-state.md to
        rewrite, postponed), B (section 7 still counts messages outside
        standard error).
      - Section 11 of large-subset-environment.md reordered as the trees
        were shown to the user (2026-11-11): a short opening, tree 1 and
        its notes, tree 2, the totals, the commands, the rulings, the not
        legalized; how it was measured at the end.  Nothing else changed
        but the opening and a pointer to make corpus-match.
      - Relative links where possible, to look for other defects on
        MetaCPAN early (the user, 2026-11-11): 30 links to the old
        Project's copies (branch project-2026-11-03, private) now go to the
        shipped successor of the same document (every item they cite,
        E1, X14, D1, G3, ..., is there); 40 file names in backticks
        (outside code blocks and headings) that name a shipped file are
        links now; 12 documents.  Every relative link resolves to a file
        in MANIFEST.  The 22 links to files that are not shipped (old
        reports, probe scripts) stay: TODO A14.
      - The 14 links to old documents that 0.04 stopped shipping (the
        upstream reports between the two sessions of the old Project, the
        ceiling, the run shortcomings, the base parser's corpus statistics,
        a decision page) go to MetaCPAN's view of 0.03, which shipped them
        (each checked there through MetaCPAN's API), no longer to the
        private repository; one more goes to the shipped known issues.  9
        links are left to files never shipped (TODO A14).
      - The last 9 links into the private repository (the user, 2026-11-11):
        the 2 to the reports of bugs found in Run 0.04 go to Run's Changes
        on MetaCPAN (its 0.05 and 0.06 entries); the 3 old tools behind
        the others are shipped again, ported: large-subset-difffuzz.pl
        (random strings of large-subset pieces; now also against bash --
        posix, compared as section 11 compares: status, output, files,
        messages nowhere; make probe-fuzz), t/stress.t (sigprobe/exec-
        stress.pl: children ending while our Perl code runs; 589 runs,
        88,350 children in 20 s, all status 0) and t/memory.t
        (sigprobe/mem-large.pl: resident size 26,288 -> 26,380 kB over
        1,500 runs), both only with RELEASE_TESTING=1 (make probe-stress,
        probe-memory).  No link into the private repository is left.
      - X48 (ours; found by the fuzzer, fixed): return outside a function
        always ended the shell; as bash --posix (rule E), where its status
        is tested (&&, ||, !, if, while) it is status 2 and the shell goes
        on.  X49 (ours; found the same way, fixed): a signal ignored on
        entry could still be trapped, and kill -INT $$ ended the string;
        as POSIX and both shells, neither now.  X2: two more effects noted
        (wait $! after a job of builtins; their output comes first).
        Tests: t/programs.t.  The fuzzer, seed 2, 2,000 strings: where
        dash and bash agree and ours not, or all differ, 14 before, 6
        after (TODO B).  The map of make docs-check now has the executor's
        modules as inputs of the parts that show run results: those 7
        parts are not in line until ours is run again (TODO B).

0.11  2026-11-10
      - Since 0.10: Proc::ShellSpawn on Windows (WS1: paths with drives,
        UNC names and \, PATH and PATHEXT, NUL, USERPROFILE and ~user, $PPID;
        t/portable.t: 223 battery lines as on Linux, and Windows' own
        cases); the event loop in its own module with a Windows backend on
        an I/O completion port, Ctrl-C included (W5, Proc::Spawner::Loop);
        sh as Perl with this module where PATH has no sh (SH1); cat, sort,
        grep from PROC_SHELLSPAWN_BIN or Git for Windows (BIN1, a temporary
        workaround); pwd inside Perl too (IP1 extended); whose echo the
        in-process echo is (EC1); PATH's separator from $Config{path_sep};
        X37 and X38 fixed; t/spawner.t runs on Windows; Perl's Windows bugs
        drafted, with every workaround of ours listed
        (doc/perl-win32-system-bugs.md).  Checked on Linux and under Wine
        (Strawberry Perl 5.38.2), not on real Windows.  The figures of
        post4-0.04 hold (the measured files differ only in $VERSION); digest
        of the code they depend on:
        144cd2b89d7d5655b5bd2bd84a861e13eb1df9d295e6045bfc620f5be1d755ef.
      - Perl's argv bug on Windows described exactly where SH1 and the
        tests mention it: every " of an argument is dropped, and one with
        blanks is split too (checked under Wine: 'echo"$0"' arrives as
        'echo$0'); before: "blanks and quotes".
      - TODO A1 (X27) rewritten in the user's four steps (2026-11-10): what
        is done (step 1; step 2 for the codes an environment switch
        covers), what is not (step 2 for files, vars and the other codes
        inside the run; steps 3 and 4), and the natural next piece.
      - BIN1, a temporary workaround (the user's follow-up (d) to WS1:
        "Replace them from %ENV (inventing suitable fallbacks)"; marked
        temporary by the user, 2026-11-10): the directories in
        PROC_SHELLSPAWN_BIN ('none': none) are searched before PATH; unset,
        on Windows: Git for Windows' usr\bin (beside git.exe on PATH, or
        under %ProgramFiles%, %ProgramW6432%, %LOCALAPPDATA%\Programs),
        else MSYS2's, else Cygwin's; Windows' own sort, find, timeout and
        expand are never taken for the POSIX ones.  t/portable.t: 5
        Windows-only lines; under Wine all pass, both primitives
        (%ProgramFiles% untested: Wine sets it afresh in every process).
      - SP1 (the user, 2026-11-10; TODO A2 closed): "strict" needs no code
        for the 142 strict samples that run echo, printf, test, [, true or
        false: since IP1 these run inside Perl on every platform
        (large-subset-environment.md, section 3).
      - t/programs.t: the shell-layer checks of PROC_SHELLSPAWN_ECHO pass
        the string to t/try.pl in a file (TRY_FILE, as t/portable.t), so
        they run on Windows too; under Wine all 18 pass (2 skipped: no
        symbolic links, no coreutils), and t/portable.t passes.
      - sh as Perl with this module (SH1; the user's follow-up (c) to WS1:
        "Should not sh -c just call Perl with our module, then?
        (Configurable)"): a command sh runs as $^X -MPARSER
        -MProc::ShellSpawn=sh -e0 -- ARGS (sh_main: -c TEXT [NAME [ARGS]],
        a file, -s or stdin; other option letters as set takes them).
        PROC_SHELLSPAWN_SH: auto (the default: when PATH has no sh),
        perl, program.  run() takes args, arg0 and opt.  Tests:
        t/programs.t (10 strings the same with dash and as Perl, apart from
        dash's line numbers), t/portable.t (4 Windows-only lines; under Wine
        all pass with win_spawn.h, through system1 the one with blanks and
        quotes in an argument is TODO: Perl's argv bug, not worked around).
      - doc/perl-win32-system-bugs.md: 4, a draft report of X38's cause in
        Perl (on Windows $^F is not honoured: Perl's close-on-exec is fcntl,
        a no-op there, and its spawn passes every inheritable handle; read
        in perl 5.38.2's doio.c and win32/win32.c, reproduced under Wine
        with a 6-line script, and its control); 5, every workaround of ours
        for these bugs (module and tests) and what to do when Perl is fixed.
      - ~user on Windows (the user's follow-up (b) to WS1: "%USERPROFILE%/
        ../user"): USERPROFILE for one's own name (USERNAME), else the
        sibling of USERPROFILE of that name if it is a directory, else as
        written (it was always as written).  t/portable.t: 3 Windows-only
        lines (one's own, ~Public, none and ~..); under Wine: pass, both
        primitives.
      - EC1 (the user, 2026-11-09; TODO A2a): whose echo the in-process echo
        is: PROC_SHELLSPAWN_ECHO=coreutils (the default, as before; it
        heeds POSIXLY_CORRECT), dash or bash (their builtins: dash 0.5.12,
        escapes always but not \x, options only a first -n; bash 5.2.21 as
        bash --posix, escapes with -e, also \E, \u, \U); neither builtin
        changes with POSIXLY_CORRECT, so with dash or bash ours keeps
        matching that shell where X27's run found the program differing
        (large-subset-environment.md, section 9).  $Proc::ShellSpawn::ECHO
        holds it; an unknown value dies at load.  The program echo (outside
        %INPROC) is not affected.  Checked against the shells on 24,000
        random calls (C and C.UTF-8, with and without POSIXLY_CORRECT).
        Tests: t/programs.t, 732 runs against t/echo-shells.txt (recorded
        from dash and bash: perl t/programs.t --record-shells) and the
        shell layer; t/dash.t runs ours with PROC_SHELLSPAWN_ECHO=dash,
        with a new battery t/batteries/echo.txt (12 strings; 11 of them
        differ from dash with the default echo).
      - UNC names with backslashes tested (the user's follow-up (e) to WS1):
        t/portable.t has 7 Windows-only lines (cd to \\.\C:\windows and
        //./C:/windows, .. stopping at the share, a relative cd and test -d,
        \x, a child started there); Wine has no server shares, its device
        names are UNC names in form.  Under Wine: pass, both primitives.
        Found by them, ❰BUG❱ (ours, post-0.10 only): \x from a UNC directory
        was taken from the process's own drive, not the share (path_in);
        pwd at the root of \\.\C: printed nothing (the server '.' taken for
        a . component, Programs.pm).  t/portable.t: PATH's separator from
        %Config.
      - t/spawner.t runs on Windows too (the user, 2026-11-09: "Sure do!"):
        the programs it starts are Perl itself (true, cat, wc, pwd and the
        simple sh -c lines replaced; the Perl code from files, since Perl's
        own spawn on Windows splits an argument with blanks and a quote);
        what only POSIX has is skipped there (27 of 52).  Under Wine all
        pass through both primitives.  Found by it: X38 (ours: through
        system1 a child inherits every inheritable handle of ours; Perl's
        pipes on Windows are such, so a child can hold the write end of its
        own input; fixed: our fds from 3 up made not inheritable around the
        call and put back, as POSIX has it, not thread-safe; the user: "do
        as on POSIX"); and on Windows another waitpid does not
        take a status from the loop (the kept process handle):
        pspawn-spawner.md, G2.
      - The W5 check's text said the shell layer is not ported to Windows;
        it was ported later the same day (doc/pspawn-shell-windows.md): the
        text now says what held when it was checked (pspawn-spawner.md,
        section 8; pspawn-loop.md; large-subset-state.md, section 7), and
        TODO has the library under Wine as work.
      - PATH's separator from $Config{path_sep} (the user's follow-up (a) to
        WS1, 2026-11-09), no longer hard-coded, in one place:
        Proc::Spawner::path_split (the user, after a detour without it: "a
        helper makes sense"), used by the search emulation, win_find and the
        shell's lookups (of commands, and of . files).  On Windows it reads
        PATH as win_spawn.h and Perl's win32.c do (WS1: a "quoted;entry"
        whole; an unclosed quote runs to the end, where the shell cut the
        entry before).  Tests: t/spawner.t (both forms, a search through two
        entries), t/windows.t (a quoted entry and an empty one).
      - The user's decisions on the shell on Windows (2026-11-09): WS1, the
        rules of doc/pspawn-shell-windows.md, section 1, confirmed; pwd
        inside Perl on every platform (IP1 extended) confirmed; follow-ups
        in TODO (PATH's separator from %Config, ~user, sh -c through Perl,
        programs from %ENV, UNC names with backslashes untested).
      - Proc::ShellSpawn on Windows (the user, 2026-11-09: "Proceed with the
        shell layer on Windows"; doc/pspawn-shell-windows.md): paths with
        drives and \, PATH with ; and PATHEXT, NUL for /dev/null, $PPID from
        NtQueryInformationProcess (__win_ppid), exit statuses decoded by
        hand (no WIFSIGNALED there), no umask, no SIGCHLD handler between
        runs, ~ from USERPROFILE, traps on signals Perl lacks there,
        Permission denied (126) for what Windows cannot run; Graph: plain
        pipes, close-on-exec by __set_cloexec, no F_GETFL.  pwd runs inside
        Perl too (IP1 extended, in TODO; as coreutils 9.4, 352 runs
        checked).  A program inside Perl writing into a closed pipe gets
        SIGPIPE's 141 on Windows too.  t/portable.t: 223 battery lines with
        their output recorded on Linux (t/batteries/portable.txt); under
        Wine all the same, through both primitives, also in a Windows-way
        gmake build.  t/try.pl: TRY_FILE (the string from a file: Perl's
        own spawn mangles arguments on Windows), and status 255 for a
        string outside the subset on every platform.
      - Findability (the user, 2026-11-09): section 7 of large-subset-state.md
        opens with a list of its parts, each with the code and day it was
        measured on; IP1 and W5 are subsections of their own.  The index
        doc/large-subset/README.md names section 7's parts, section 9 of
        large-subset-environment.md, the two .tsv records and the runner's
        options.  pspawn-spawner.md, section 8: Proc::ShellSpawn on Windows,
        not ported, with what blocks it and what passes under Wine;
        pspawn-loop.md: the library under Wine.  test-pools.md: how to rerun
        ours alone after a change, the environment varied, and through Run.
        large-subset-runreport.pl -ours-from DIR (join ours of a later run
        with dash and bash of an earlier one) and -ours-vs DIR (ours of two
        runs, sample by sample), which the IP1 and W5 checks had done by
        hand; checked to give the same results.
      - doc/pspawn-loop.md: the event loop (W5) on its own page: how it works
        on each platform (the completion port, Ctrl-C, alarm), what was
        checked (a table), the known limits; linked from doc/README.md, the
        POD of Proc::Spawner::Loop and pspawn-spawner.md, section 8.
      - W5 checked over the library (2026-11-09): on Linux, ours alone at
        main against the rebuilt table, sample by sample: no change from W5
        (55 changed, all timing, process IDs, inode numbers, or X37's fix).
        Under Wine the library cannot run: Proc::ShellSpawn is not ported
        (run() dies at getppid, then at fcntl F_GETFL in Graph; in TODO);
        the Windows build's tests pass there, and all 45,501 parse trees
        are the same as on Linux.  large-subset-state.md, section 7.
      - large-subset-runcompare.pl -env mtime fixes the worker's directory
        too (the run's ".."): joining runs of different days made ls -la
        differ.
      - Section 7 of large-subset-state.md with the in-process programs
        (IP1, the code of 0.10), rebuilt from the run of post-0.09 rather
        than run again: dash and bash unchanged; ours run again on the 1,055
        samples where it ran echo, printf, test, [, true or false itself
        (and on 300 others as a check: the same, all 300), in the same
        workers.  2 samples change class: printf "\u20AC" (X37, fixed) and a
        race.  README and TODO say so.
      - X37 (ours; 0.10): the in-process printf printed \u and \U escapes as
        UTF-8 even in the C locale, where coreutils 9.4 prints the escape
        (\u20AC, \U0001F600; also above U+10FFFF), took a surrogate (\ud800)
        where coreutils stops with "invalid universal character name" and
        status 1, and %b dropped the text before a bad escape.  Now as
        coreutils; 14 runs added to t/programs.txt.  Found by the behaviour
        run with the in-process programs.
      - large-subset-runcompare.pl: -shells (run only some of dash, bash,
        ours), -workers-from DIR (each sample in the worker, so the
        directory and user, of an earlier run), and a restart that works:
        the samples already done were never found (the records' keys are
        sorted, "id" is not first).
      - W5 (the user, 2026-11-08: "Use the Windows' way"; 2026-11-09: "in a
        separate module, with an \"elegant\" API which may be shared between
        your POSIX approach and Win32"): Proc::Spawner::Loop, the one event
        loop: read, write, child, after (a timer), defer, wait_until, poll,
        pipe; each watch can be cancelled.  Backends: POSIX (select and
        SIGCHLD, the code moved from Proc::Spawner) and Win32 (win_loop.h:
        one I/O completion port; overlapped I/O where the handle allows it,
        a helper thread where not; RegisterWaitForSingleObject for children).
        Proc::Spawner's on_read, on_write, defer, wait_until, poll remain as
        its old names; a read's callback now also gets the errno of an
        error.  On Windows, Proc::Spawner::Graph writes into a pipe directly
        (no /proc).  t/loop.t: 12 tests on Linux, 17 under Wine with
        Strawberry Perl (through both Windows primitives), also in a
        Windows-way build (perl Makefile.PL, gmake, gmake test).
      - W5 on Windows (the user, 2026-11-09: "Can a handler for Ctrl-C
        report to the loop instead?"): the wait has no time limit of its
        own; while wait_until runs, a console control handler of ours posts
        Ctrl-C (and Ctrl-Break, close, shutdown) to the completion port, and
        the wait gives Perl's own handler up to 200 ms to mark the signal;
        while an alarm is set, 50 ms slices with Perl's message check.
        Checked under Wine with a console: the %SIG{INT} handler ran 0.04 s
        after Ctrl-C (without ours: at the next event, 3 s later); alarm on
        time; default and IGNORE as before.  t/loop.t: a test of it (skipped
        without a console).
      - large-subset-runcompare.pl -only with an empty list (or an empty
        @FILE) is an error: it meant every sample.
      - large-subset-state.md, section 7, and README: the behaviour table and
        "Through Run" were run before IP1, with the coreutils programs (what
        PROC_SHELLSPAWN_INPROC=none gives now), which they now say; the re-run
        with the in-process default is in TODO (not re-measured).
      - large-subset-state.md, section 7: Run's commit is a2be65e (was
        42988a4: repository ilyaz-ai/run's history rewritten with GitHub's
        no-reply address, 2026-11-08; the same Run.pm).

0.10  2026-11-08
      - Since 0.09: Spawner.so loads with perl before 5.34 (X36, RT #182809;
        thanks to Slaven Rezic for the report); echo, printf, test, [, true
        and false inside Perl as coreutils 9.4 (X21, partly; IP1, IP2);
        Windows: win_priority and win_job (W4), SetChildShowWindow, more than
        64 children, X34 fixed; on_write gets the errno; X27: the run's
        environment varied, stand-ins and fixed file times (EV1); the run-time
        comparison redone and its table replaced (A1(b)); the comparison and
        the snippet library through Run; X33, X35 fixed.  The figures of
        post4-0.04 hold (the measured files differ only in $VERSION); digest
        of the code they depend on:
        4185f0fc85d210c5d14e02c79c998ea586198496e58b4e1caf7a9154c969f11e.
      - X36 (ours; RT #182809, Slaven Rezic: 'Undefined symbol "av_count"',
        2026-11-08; thanks to Slaven): since 0.01 Spawner.xs used av_count(),
        which perl has only since 5.33.1, so with perl 5.30 (and any perl
        before 5.34, though MIN_PERL_VERSION is 5.020) Spawner.so did not load
        and every test failed.  Spawner.xs defines it where perl does not, as
        perl does (AvFILL + 1).  Checked with perl 5.30.3 built here (Linux):
        0.09 fails to load as reported; now the suite passes; the XS also
        compiles against perl 5.20.3's headers with no undeclared function
        (that perl itself did not build here, so the suite was not run on
        it).  FreeBSD itself not tested here.
      - X21, partly (the user, 2026-11-08: "echo, printf, test", "inside the
        Perl process"): Proc::ShellSpawn::Programs does echo, printf, test,
        [, true and false as GNU coreutils 9.4 does them, in Perl (Perl's
        sprintf and file tests, Math::BigInt for the 64-bit range checks,
        test's grammar as a recursive descent), with output and messages in
        the program's order (glibc's error() flushes stdout first) and its
        write errors (SIGPIPE, "write error").  Decision IP1 (the user: "when
        a well-debugged builtin is already present, use it.  (By default;
        Configurable.)"): Proc::ShellSpawn runs them inside Perl on every
        platform; PROC_SHELLSPAWN_INPROC=none runs the programs (or names
        only some), and %Proc::ShellSpawn::INPROC can be changed.  command -v
        names them where there is no program.  Checked against coreutils 9.4
        on 12,348 runs (handwritten and generated cases, C and UTF-8
        locales) and on 3,000 random words for %q; t/programs.t replays 2,284
        recorded runs (also under Wine with Strawberry Perl: pass) and
        compares the shell layer with the programs and in-process; the suite
        passes both ways (t/dash.t: 550 commands of the batteries).
        Differences: --help and --version texts, printf's floating
        conversions in double (coreutils: long double), and %q, where
        coreutils 9.4 has a ❰BUG❱ (gnulib's quotearg keeps a stale state when
        it starts a word over: an extra '', or a missing $' that makes the
        shell read the word back wrong); ours follows gnulib's fixed rule
        (decision IP2, the user: "Use the builtin").
      - Proc::Spawner::on_write: the callback gets the errno of a failed
        write (it got nothing); Proc::Spawner::Graph's perl_write passes it
        in $!.
      - W4 (the user, 2026-11-08: "(c) and (b); (c) beats (b) if present"):
        on Windows, win_priority (a priority class) and win_job (job limits
        by Windows' names: ProcessMemoryLimit, JobMemoryLimit,
        PerProcessUserTimeLimit, PerJobUserTimeLimit, ActiveProcessLimit),
        exact, per child; nice and rlimit mapped to them, approximately (nice
        to the class with the nearest nice value; AS, DATA, CPU, NPROC to job
        limits; other resources unsupported); win_* win where both are
        given, and are unsupported elsewhere.  The class is set on the
        suspended child (Wine ignores it as a creation flag).  Checked under
        Wine with a child printing its class; Wine stores no job limits, so
        theirs is not checked; the mapping is tested in t/spawner.t.
      - Windows (the TODO's work items; checked under Wine with Strawberry
        Perl 5.38.2): Win32::SetChildShowWindow is honoured as by Perl's own
        spawn (a child printing its startup info: the same through both
        primitives); more than 64 children at once work (100 checked: the
        loop polls each child; Perl's own spawn still stops at 64, its
        table); Makefile.PL skips the POSIX probes there, and perl
        Makefile.PL, gmake (no warning) and gmake test of t/windows.t and
        t/parsers.t pass with Strawberry Perl's own gcc and gmake.  nice and
        rlimit there are decisions (TODO A.12).
      - X34 ❰BUG❱ (ours; 0.07 to 0.09, Windows code): the kept process
        handles were in a fixed table of 1,024, and a child started while it
        was full lost its status if it ended before it was waited for (76 of
        1,100 under Wine); the table grows now (the same check: none lost).
      - TODO: two pending decisions added (a missing Run in M2; keeping
        Perl's reopen of the graph's own pipes).
      - X27, the run's environment varied (2026-11-08): the six kinds of
        workaround (nothing, a second run with the other case, fixtures,
        chosen values, stand-ins for programs, leaving samples out) listed
        per code in doc/large-subset/large-subset-environment.md, section 9.
        large-subset-runcompare.pl -env: posix (POSIXLY_CORRECT=1), utf8,
        sigpipe (ignored), noproc (/proc hidden), tmp (a fresh tmpfs on /tmp,
        /var/tmp, /dev/shm per run), random (a fixed file over /dev/urandom),
        private (empty /home, /root, /mnt, /media, /srv, /var), dev (a
        private /dev); the mounts in a mount namespace of each run's own.
        -base (the mounting variants need one outside what they hide),
        -only @FILE; ours runs from a copy of lib/, blib/, t/try.pl.
        large-subset-runreport.pl -b BASE (each sample's class against a base
        run's, and what changed) and -c.  Each variant on its code's samples
        against two base runs (the noise): POSIXLY_CORRECT turns the
        program echo from bash's side to dash's (29) and makes ours differ
        from both for \x (2); C.UTF-8 changes one printf "\u20AC"; SIGPIPE
        ignored changes only messages and 6 timeouts; /proc hidden makes the
        3 samples using /dev/stdin, /dev/stderr fail alike; a fresh /tmp
        makes 6 agree whose shells met each other's files there (or another
        sample's: /tmp is shared across the whole run); a fixed
        /dev/urandom makes all 25 agree; private and dev change nothing.
        Every changed sample: doc/large-subset/tools/
        large-subset-env-variants.tsv.  The levels are not changed by it
        (TODO, A1 and A2a).  bash 5.2.21 does not emulate /dev/stdin and the like
        without /proc (the document said it did; corrected).
      - X33 (ours, in large-subset-runcompare.pl since post4-0.04): each
        run's output and error files belonged to root, so /dev/stdout and
        /dev/stderr, which Linux reopens by path, were not writable for the
        run's user ("Permission denied" in all three shells; one sample
        affected in practice, tee /dev/stderr).  Fixed: the files are given
        to the run's user first.
      - X27, EV1 (the user, 2026-11-08): the runs with the environment
        varied are footnotes to the level totals
        (large-subset-environment.md, section 1; README), not levels.
      - X27, stand-ins and fixed file times (the user, 2026-11-08: their
        answers controlled by the environment): large-subset-runcompare.pl
        -env fail (every program the run only logs exits with
        STANDIN_STATUS, default 1), output (prints STANDIN_OUTPUT, default
        "one  two\n*\n\nlast"), mtime (the run's copy of the fixture gets the
        times FIXTURE_MTIME, default 2020-01-01); the values from the
        runner's own environment, recorded in OUTDIR/env.txt.  Run on the
        samples of prog-status (113), prog-output (1,014) and time (223)
        against two base runs: status 1 changes nothing beyond the messages;
        the chosen output parts dash and bash in 47 samples, none with ours
        against both (cd and kill given several words, test ==, all decided
        or known; and X35); fixed times cut the changes between two runs of
        those samples from 50 to 32 (the rest: the shells' own clocks,
        times outside the run's copy).  large-subset-environment.md,
        section 9 and footnote 2; large-subset-env-variants.tsv.
      - X35 (ours; fixed as dash, the user, 2026-11-08): unset of a word that
        is not a name (from an expansion; the parser rejects a literal one)
        did nothing with status 0.  Now, as dash, it stops the shell with
        "unset: WORD: bad variable name" and status 2, the names before it
        already unset; unset -f takes any word.  bash takes such a word for a
        function's name.  6 strings in t/batteries/largebattery.txt.
      - large-subset-runcompare.pl -run RUNLIB: ours goes through Run (as
        where there is neither posix_spawn nor fork: M2, the primitive
        system1), PROC_SPAWNER_MISSING="posix_spawn fork" for ours alone;
        t/try.pl takes the names in TRY_HIDE out of the environment once the
        modules are loaded, so that the samples do not see it.
      - The whole snippet library through Run (the user, 2026-11-08): run
        twice in the same environment (-env tmp,random,mtime), ours through
        posix_spawn and ours through Run 0.07 (-run): the tables agree, and
        sample by sample ours changed in 53, all by timing, process IDs or
        clocks (49 of them with the shells changing too); no difference comes
        from Run, so no Run bug to fix.  Run's tests and Proc-Spawner's suite
        through Run (also RUN_AS_WIN32=1) pass.  large-subset-state.md,
        section 7, "Through Run".
      - A1(b) (the user, 2026-11-08): the run-time comparison redone with a
        fresh /tmp, a fixed /dev/urandom and fixed file times (-env
        tmp,random,mtime) replaces post4-0.04's table: same 15,861 (15,751),
        messages only 23,311 (23,396), ours differs 3 (7), the shells differ
        1,220 (1,239), timeouts 15 (17).  The 3: printf -v (2, Q2) and echo
        into a broken pipe in a subshell (1); of post4-0.04's other 4,
        printf '%0.s-' now agrees (Z0), /dev/urandom agrees, the other 2 were
        chance.  large-subset-state.md, section 7 (also the external calls),
        README, large-subset-run-differences.tsv, test-pools.md; the X27
        cross-check (large-subset-environment.md, section 5) comes out the
        same.  The atomic-call tables stay those of post4-0.04.

0.09  2026-11-08
      - Since 0.08: what posix_spawn has no option for (rlimit, sigignore,
        nice: M1); no posix_spawn at all (M2): fork and exec, and where there
        is no fork either, Run 0.07 (repository ilyaz-ai/run; to be released
        first, recommended in META) applies the redirections around Perl's
        own spawn; Windows: the kept process handle released, sigignore of
        PIPE and INT, t/windows.t; ulimit through getrlimit; X29 fixed, X31
        fixed, X32 recorded; known issue X27 classified; TODO; the documents
        cross-linked.  The figures of post4-0.04 hold (the measured files
        differ only in $VERSION); digest of the code they depend on:
        80e40b1ce413ea4c39203a0c5bbd134b800477da8641dca26dc55aa01e693d41.
      - Known issue X32 (ours; possible, not observed): without /proc, Perl's
        own writes into a pipe the caller gave are blocking writes of
        PIPE_BUF bytes after select, and block if a child fills the pipe in
        between.  In the known issues, Proc::Spawner's LIMITATIONS, README
        and TODO.
      - The Markdown documents link to each other and to the files they name
        (398 links, relative; none broken): a path or file name in a code
        span became a link, a script followed by its arguments links its
        name.  Mentions of the old Project's files link to the branch
        project-2026-11-03 where they are there (19, checked through the
        GitHub API; the repository is private, so they work for those who
        can see it).  doc/perl-win32-system-bugs.md now ships (a shipped
        document links to it), so every relative link of a shipped document
        points into the distribution.
      - M1 (the user, 2026-11-06: what posix_spawn has no option for, done
        around the call where it can be undone, failed for the rest): new
        settings sigignore (signals the child ignores), rlimit (its resource
        limits) and nice (an increment), in start and spawn.  The XSUB sets
        them in this process right around its call, in C, with every signal
        blocked, and puts them back right after.  Unsupported (dropped or
        failed with ENOTSUP, as UNSUPPORTED says): a hard limit lower than
        ours without CAP_SYS_RESOURCE, a higher nice value without the right
        to lower it back, a CPU limit this process has used up, ignoring
        SIGCHLD; all three on Windows.  Checked as root (here without
        CAP_SYS_RESOURCE) and as an unprivileged user; t/spawner.t.  A memory
        limit below what this process uses makes glibc's posix_spawn fail
        with ENOMEM (documented).
      - M2 (the user, 2026-11-06: no posix_spawn at all, as Run 0.06's
        forkless mode): Makefile.PL probes posix_spawn itself; without it,
        or with PROC_SPAWNER_MISSING=posix_spawn, the primitive is the new
        XSUB __spawn_nowait, fork and exec in C with nothing in between, and
        every other setting is done around the call.  The whole suite passes
        so, and a build without posix_spawn passes t/spawner.t and t/dash.t.
        t/missing-matrix.pl: profile "no posix_spawn"; the Windows profile
        has the M1 settings.
      - Stale since 0.07: "the shell layer needs posix_spawn's chdir"
        (README, X10, t/dash.t).  chdir is done around the call where the
        libc lacks it; checked by building without chdir and fchdir: t/dash.t
        passes, 13 of 13.
      - M2, no fork either (the user, 2026-11-07): the redirections are
        delegated to Run 0.07 (repository ilyaz-ai/run; to be released
        first), whose start() applies them around Perl's own spawn, system {
        FILE } 1, ARGV (P_NOWAIT), as on OS/2, Win32 and VMS, and puts our
        fds back ($Proc::Spawner::PRIMITIVE 'system1'; chdir and fchdir are
        done here).  PROC_SPAWNER_MISSING="posix_spawn fork" selects it on
        Linux too, where Run emulates the spawn by fork and exec: the whole
        suite passes so.  The arguments go to Perl as they are: Perl's command
        line on Windows does not quote them as the C runtime parses them back
        (❰BUG❱ in Perl; doc/perl-win32-system-bugs.md, to be reported), so the
        tests of exact argv and of batch files are TODO there (the user: no
        bug-for-bug workaround).  The program is looked up here (posix_spawn's
        meaning of FILE and search).  The child sees fds 0-2 only (dup2 or
        open onto fd 3 and up unsupported); pidfd, a non-empty sigmask,
        fchdir, a group to join unsupported; at most 64 children (EAGAIN).
        New t/windows.t (Windows only): both primitives under Wine with
        Strawberry Perl 5.38.2, 13 passing, and the 2 TODO with system1.
        OS/2 and VMS not tested.  A Run that is missing or too old is now an
        error of spawn (before: any exception inside became EINVAL).
      - Windows, both primitives: sigignore of PIPE (no SIGPIPE there) and of
        INT in a new process group (Ctrl-C is off there) need nothing; other
        signals stay unsupported (Perl's kill ends a Windows child, or sends
        Ctrl-Break, which it cannot ignore).
      - ulimit (the shell layer) reads the limits with getrlimit, through
        Proc::Spawner's XSUB, instead of /proc/self/limits or, without /proc,
        asking a shell (the user, 2026-11-07: the module has XS anyway).  Same
        output as dash for -n -Hn -f -s -c -t -v -l -m -d; t/dash.t passes
        with /proc hidden too.
      - X29 ❰BUG❱ (ours; 0.07, 0.08): where the fd actions are done around the
        call, the copies of our fds went above an open action's flags plus
        1000, beyond the common limit of 1024 fds, and the call failed with
        EINVAL; now just above the highest fd the actions name (the 1000 was
        never needed).  Found on Windows; checked on Linux with ulimit -n 1024.
      - Windows: __dup_high (a copy of an fd above a number) works there.
      - Windows: the process handle kept for a child's exit code is released
        once the loop has the status (Proc::Spawner::reaped, also for callers
        of the raw spawn who wait themselves).  Checked under Wine with
        Strawberry Perl 5.38.2: the handle is gone after the status (0.08:
        still open); 1,100 children in a row give their status.
      - X31 (ours; only in the Windows code, which no release builds yet):
        Proc::Spawner's open action on Windows (win_spawn.h) did not map
        /dev/null to NUL, as Perl's own open does.  Fixed; checked under Wine
        from a directory on C: (on Wine's Z:, the Linux root, the bug hides):
        a test program of win_spawn.h failed before and passes after, and
        t/windows.t (new test) passes with both primitives.  Perl's other
        Windows special cases checked against ours: nothing else missing.
      - X11 and X12 closed (the user, 2026-11-07): the Windows primitive
        exists (win_spawn.h; M2's system1), and the corpora were run through
        the executor by the run-time comparison of post4-0.04.
      - TODO: every open thing in one place, by who has to act (the
        decisions waiting for the author first); linked from README.
      - Known issue X27 classified (the user's method, 2026-11-07: parse
        all the samples in one batch, split them into atomic blocks marked
        with their samples, and pass only what is shown unaffected, so that
        a pass is definite).  New doc/large-subset/tools/
        large-subset-blocks.pl sorts the 40,410 accepted samples from the
        parse trees alone, programs taken as real and opaque, into strict
        (365: nothing in the environment can change the comparison, given
        only how the shell is started), held (2: the same, under assumptions
        known to hold in every tested environment, such as /dev/null),
        almost strict (34,291: the same under named assumptions, mostly that
        a program does the same in each run and the shells look it up
        alike) and undecided (5,752,
        each with the codes of what it may depend on: files, variables, a
        program's output or status taken by the shell, /tmp, ...).  $? takes
        the status of what ran before it, set -e makes every later status
        decide (the user, 2026-11-07); absolute paths are told apart by the
        area they are in (path-tmp, path-system, ...), and whether the run
        could control it; /dev/null is an assumption known to hold in every
        environment tested (the level "held": dash and bash have it, ours on
        Windows gets NUL, X31).  The record,
        one line per sample by snippet ID with a short code, is
        doc/large-subset/large-subset-provenance.tsv; the codes, how to
        remake it and the findings: doc/large-subset/
        large-subset-environment.md.  Checked by 84 probes, by reading all
        strict samples and random others, and against the run's
        differences.  It replaces the first version of the same day, which
        tagged what it recognised and passed the rest (81.4 %): its passes
        were not definite.
      - The batch check (-batch): the samples joined into one text parse as
        they do alone, but for the 198 that change how the text after them
        parses (functions, aliases, P1, a trailing backslash, exec) and 2
        whose arithmetic text keeps its trailing blank (large grammar) or not
        (base grammar), same value.  New known issue X30 (ours): with $LINENO
        in a text, the parse time grows with the square of its length (the
        joined text: 89 s, 8.4 s without the one $LINENO).

0.08  2026-11-06
      - Since 0.07: Windows (EXPERIMENTAL): win_spawn.h compared with Perl's
        own spawn and fixed; the module built with MinGW-w64 against
        Strawberry Perl 5.38.2 and run under Wine; batch files; the options of
        %Proc::Spawner::WINDOWS.  Fixed on every system: spawn's result in
        scalar context when a setting is missing.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on: 3956c46c16d3290baf27f9475da8b72d431f17a194b2ba9fba91e3e0fe863bb8.
      - Windows, run under Wine with Strawberry Perl 5.38.2 (Spawner.dll built
        with MinGW-w64): spawn, waitpid, options work.  New: batch files run
        through the system's cmd.exe, arguments quoted as Rust's standard
        library does since BatBadBut (CR, LF refused); %Proc::Spawner::WINDOWS:
        new_group (Ctrl-C reaches the parent, not the children; default on),
        kill_with_parent (a job object that ends the children with the parent;
        default on), new_console (as Perl's spawn; default on); console
        pseudo-handles of Windows 7 kept out of the handle list.  Fixed: spawn
        returned its count of results in scalar context when a setting was
        missing (now the pid); Proc::Spawner no longer uses fcntl and SIGCHLD
        on Windows (the loop polls there).  doc/pspawn-spawner.md, section 8:
        how the child sees its arguments, Perl's system against ours.
      - win_spawn.h compared with Perl's own spawn on Windows (win32/win32.c of
        perl 5.38.2) and fixed: PATHEXT's extensions before the bare name, and
        quoted PATH entries, as Perl's qualified_path(); cmd.exe /c quoted as
        Perl's create_command_line() does; the environment block sorted, with
        the drives' directories (=C:) kept; E2BIG over 32766 characters;
        strings not held as UTF-8 read in the ANSI code page (as Perl's
        CreateProcessA); the default environment and directory are Perl's
        (PerlEnv_get_childenv, PerlEnv_get_childdir); its own allocator on the
        process heap (found by compiling against Strawberry Perl 5.38.2, where
        malloc and free are Perl's macros).  Spawner.dll compiles and links
        with MinGW-w64 against Strawberry Perl's headers and libperl538.a; not
        yet loaded.  Open questions and the deliberate differences:
        doc/pspawn-spawner.md, section 8.

0.07  2026-11-05
      - Since 0.06: what the spawn primitive lacks is done around the call
        ($Proc::Spawner::MISSING; t/missing-matrix.pl); win_spawn.h, posix_spawn
        for Windows (not yet built into the module); Z0 (printf's 0 flag with
        %s); the atomic calls measured.  The figures of post4-0.04 hold (the
        measured files differ only in $VERSION); digest of the code they
        depend on: 3f1c152adeabd6730535ae4380824c41e45c896190d612e75e08f956c15af270.
      - win_spawn.h: posix_spawn for Windows on CreateProcessW (the user,
        2026-11-05; EXPERIMENTAL, not yet built into the module: that needs a
        Windows Perl).  Spawner.xs includes it under _WIN32 and #defines the
        posix_spawn names to it (#define posix_spawn win_posix_spawn and so
        on), so __posix_spawn is one source; no SIGCHLD helpers there.  The
        child's fds 0-2 through STARTUPINFO, fds 3 and up through the C
        runtime's lpReserved2 table, exactly the handles it should have
        through PROC_THREAD_ATTRIBUTE_HANDLE_LIST (inheritable copies made for
        the call); the file actions in order (open by the parent, relative to
        the child's directory; chdir, fchdir, closefrom); argv quoted as the C
        runtime parses it back; posix_spawnp through PATH and PATHEXT;
        pgroup 0 as CREATE_NEW_PROCESS_GROUP; ENOTSUP for a non-empty sigmask,
        a group to join, setsid, sched.  Checked with MinGW-w64 (gcc 13.2)
        under Wine 9.0: argv round trips, fd 3, chdir, dup2, closefrom, a new
        group, the refusals, and the #define layer of Spawner.xs; compiled
        (not linked) for x86, x64 and ARM64 with clang.  Not done yet: the
        Perl side on Windows (waiting for a child through the kept process
        handle, __win_spawn_handle; no SIGCHLD).
      - Proc::Spawner::spawn does what the spawn primitive lacks around the
        call (the user, 2026-11-05: as a Windows port will have to):
        $Proc::Spawner::MISSING, a bitmap of the settings the primitive lacks
        (this libc's gaps, plus PROC_SPAWNER_MISSING for tests).  chdir,
        fchdir, the fd actions (dup2, close, open, closefrom), sigmask,
        sigdefault, pgroup => PGID, resetids, search and pidfd are done in
        this process around the call and undone; setsid, tcsetpgrp,
        pgroup => 0, sched and cgroup are unsupported (drop or fail).  New
        XSUBs __native, __set_cloexec, __dup_high, __fchdir, __pidfd_open,
        __fd_limit.  t/missing-matrix.pl runs the suite once per entry of the
        bitmap, and with the Windows gaps; doc/pspawn-spawner.md, section 8.
        t/spawner.t: a test of pgroup => PGID; pgroup => 0 and setsid are
        skipped where unsupported.
      - Z0 (the user, 2026-11-05): printf's 0 flag with %s, %c or %b (printf
        '%0.s-' 1 2 3, a common way to repeat a character; %05s) is dropped
        before the program printf runs, so it pads with blanks, as glibc's
        printf(3) and both shells; coreutils refuses it ("invalid conversion
        specification").  The flag is undefined there in C and POSIX
        (large-subset-state.md, section 1).
      - large-subset-builtins.pl: a command name starting with - or + is
        quoted, and plain names after export, readonly, unset, local stay bare
        (else the call is no longer the one that ran); -reuse OLD.jsonl.
        Results in large-subset-state.md section 7: of 25,490 atomic calls
        where no program runs, none where dash and bash agree and ours
        differs; the external calls: one (the broken pipe of echo).
        doc/large-subset/tools/large-subset-builtins-differences.tsv.

0.06  2026-11-05
      - Since 0.05, the same day: the fixes for systems without /proc, and the
        measurement of the atomic calls extended.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on:
        c19ac884428cba12a90ff7e8d1bfc4dba7b540e621dc72062563a5ce4ec950b1.
      - RT #182479 (Slaven Rezic: the tests fail on FreeBSD, which has no
        /proc and no /bin/true; thanks to Slaven for the report and for his
        smokers).  The tests find programs through PATH, read
        signal dispositions, masks and process groups from a Perl child
        instead of /proc, and skip what needs Linux's /proc (t/graph.t's
        reporter, the batteries fdbattery and sigenv, pidfd without
        /proc/self/fdinfo).  Running the suite with /proc hidden found three
        faults of the library itself without /proc, now fixed: (1) Perl's
        writes into a pipe it shares with children set O_NONBLOCK on the
        shared open file, so the children's writes failed with EAGAIN and
        output was lost (known issue L10's fallback); a pipe of ours is now a
        named FIFO opened once more for Perl alone, and a pipe we were given
        takes PIPE_BUF bytes per blocking write; (2) the check whether an fd
        is close-on-exec read /proc/self/fdinfo, and took every fd as
        inherited without it (echo x >&5 then failed like the program, not
        like dash): now fcntl, through the new XSUB
        Proc::Spawner::__fd_cloexec; (3) ulimit died ("Undefined subroutine
        Proc::ShellSpawn::Large::unsupported") without /proc/self/limits; it
        now asks a shell, which has Perl's limits: dash, bash --posix, ksh or
        sh, in /bin first, then on PATH (none: "not supported", status 248).
        Known issue X28.
      - The atomic calls where no program runs (large-subset-builtins.pl) now
        include commands not found and settings alone ($ONBUILTIN reports
        them too), with messages as dash's, bash's or neither; the external
        calls of every sample in the classes of the main table
        (large-subset-runreport.pl -x).

0.05  2026-11-05
      - The release of post4-0.04, with the version number 0.05 and
        NOTES-post3-0.04.txt no longer shipped.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on:
        743121ae0b5fb7af55b5308fb51c3db9cf61ca74ec2d3a40e98a3dc6cca634cd.
      - In short, since 0.04: functions before regular builtins (3A); rule-E
        choices; background jobs go on between run() calls (K3); aliases
        known only when they run (P1); rule F ([ a \< b ]); read with names
        from expansions; bracket expressions with quotes, expansions or '/';
        K0 (kill with no process ID); bugs X25, X26 and others fixed; the
        behaviour of every accepted corpus sample compared with dash and bash
        at run time (doc/large-subset/large-subset-state.md, section 7).
      The documents label the unreleased states between 0.04 and 0.05 post-0.04
      to post4-0.04; their entries follow, newest first.

    post4-0.04:
      - Bracket expressions with quoted members, expansions or '/' (the work
        list, large-subset-state.md section 4; both shells agree, probes
        brquote, brslash): grep ^[\>\<], ls [a"]"], ^["$v"], [$v], and
        something[^/]*.  Quoted characters are members, never operators
        (also ! ^ - ]); an unquoted expansion's characters are active.  In
        pathname expansion a bracket expression with a '/' is none: its '['
        is literal, as both shells.  Also in the pattern of ${x%[...]}.  The
        large grammar only; Proc::ShellSubset itself still rejects them.
      - In ${x:-word} (not a pattern), * ? [ are accepted: literal where the
        ${...} is quoted ("${1:-sure? [y/N]}"), active where it is not
        (probe dqpword).
      - Case patterns and ${x%p} are put together whole before they become a
        regex, so a bracket expression may span quoted and unquoted parts.
      - Fixed: [^...] (rule E: negation) was turned into [!...] by the
        parser, so a word that matched no file came out as [!...] instead of
        as written.  The parser now keeps '^'; the executor reads it.
      - Fixed: unset NAME[...] was accepted and did nothing, with status 0;
        dash stops with "bad variable name", bash unsets an array element
        (probe unsetarr).  It is rejected now, as bash's arrays are; known
        issue X26.  New detector feature unsetarr (B); unset foo][ is B by
        hand (probe unsetbad: dash fails, bash returns 0).
      - Proc::ShellSpawn: $ONBUILTIN, a code ref called before each builtin
        or function runs (for measurement; t/try.pl logs to the file named by
        RECBUILTIN); doc/large-subset/tools/large-subset-builtins.pl runs every
        builtin call of the corpus alone in dash, bash and ours;
        large-subset-runreport.pl -x compares the external calls one by one.
      - The behaviour figures in separate tables, with and without a
        coreutils program involved (large-subset-runreport.pl -q), and the
        atomic calls: every call the corpus makes to echo, printf, test, [,
        kill or pwd, run as dash's builtin, bash's builtin and the program
        (large-subset-threeway.pl; the database large-subset-threeway-db.jsonl,
        757 calls; calls made by xargs are left out).  With K0, dash and bash
        agree and ours differs in 7 samples of 40,410 (first run: 57).  The
        messages are compared as multisets of characters (the shells' messages
        of pipeline elements mix).  README: a summary, near the top.
      - The Git history now has the CPAN releases 0.01 to 0.04 (tags v0.01 to
        v0.04); CPAN's 0.04 is the tree once called "the pre-release snapshot".
        doc/pspawn-spawner.md, section 8, and the post-0.04 entry below: the
        Windows section was never in a CPAN release.
      - K0 (the user, 2026-11-05): kill with a signal (-N, -NAME, -s NAME) or
        --, and no process ID (kill $(pgrep x) when nothing matches), is
        answered by us as by both shells: dash's usage text, status 2 (the
        program kill: a long usage, status 1).  large-subset-state.md, section
        1: the rules are a triage of the past; two principles of least
        surprise in conflict (a tool called in a script as when called alone;
        a tool's own treatment of rare edge cases), and the per-tool
        decisions (K0, rule F, Q2).  Known issue X27 (TODO): which samples of
        the run-time comparison depend on the environment.
      - New: the behaviour of the accepted samples, run in dash, bash --posix
        and ours (tools/large-subset-runcompare.pl, large-subset-runreport.pl;
        large-subset-state.md section 7): of 40,410, all three agree in
        15,760, only the messages differ in 23,368, and dash and bash agree
        while ours differs in 33 (29: kill with no process ID, the program's
        status 1 against the shells' 2; question for the user).
      - Fixed, found so: a string with no command, or only comments, was
        rejected by run() (the parser option 'empty' was missing from
        run()'s options), though the measurement counted it as accepted;
        eval of empty text stopped with status 248; kill -s WINCH $$ (also
        CHLD, URG, CONT, kill -0 $$) ended the shell with status 156.
      - Figures: 44 samples rejected within the lower yardstick (post3-0.04:
        49; 3 accepted now, 2 reclassified as blatant by unsetarr); digest
        aa2734f2147be58073d44713de7f003cdee525f593d5e90f70f9f64e900f08bc.
        Tests LS:319-323; battery lines in t/batteries/large-subset-post04.txt.

    post3-0.04:
      - Aliases known only when they run (P1, the user, 2026-11-03: "go with
        better aliases"): where an alias definition has an expansion, or stands
        in a compound command, a function, an and-or list, a pipeline or the
        background, the parser stops after that line; the executor parses the
        rest of the text when it gets there, with the aliases defined by then,
        as both shells read a text one complete command at a time.  Tree node
        { rest => TEXT, lineoff => N }.
      - Rule F (the user, 2026-11-03: where a utility is implemented wrongly,
        we may do better): [ s1 \< s2 ] and [ s1 \> s2 ] (also test, also after
        !), which both shells run and the coreutils program refuses, are
        compared by the executor, by bytes; the option test_lt_gt is needed
        only for longer expressions with < or >.
      - doc/large-subset/large-subset-state.md, section 4: what POSIX says of
        the ${...} forms both shells refuse (an expansion error, or
        unspecified).
      - Figures: 49 samples rejected within the lower yardstick (post2-0.04:
        72); digest
        b189e43efd804af3e15e4224d2e7f46a890d7723301dd792fd9094ec8aaf6c59.

    post2-0.04:
      - read takes names from expansions (read $v, read "$v"), checked when it
        runs, with dash's messages and status 2; a read-only name no longer ends
        the shell (as dash).  ${##}, ${#?}, ${#!}, ${#$} accepted (both shells).
      - Fixed: a name from an expansion in export "$v" was never checked against
        the names the subset may not set (the test was always true).
      - Measurement: 14 samples left out by hand as noise
        (doc/large-subset/tools/large-subset-noise.txt, read by
        large-subset-drops.pl; the user, 2026-11-03); the detector counts the
        decided rows N1-N3 (${x:?}, a special builtin's error, command -v) as
        class X, as large-subset-logical.md says of every decided row.
      - Figures: 72 samples rejected within the lower yardstick (post-0.04: 91);
        digest (now also over large-subset-noise.txt)
        a23490770df33a87e8cbc1008d7d7301c7bcf567cb56bd2d5b5e13a94e0c4636.

    post-0.04:
      - 3A (the user, 2026-11-03): a function comes before a regular builtin of
        its name and before the commands run as programs (POSIX order).  Functions
        may be named cd, read, jobs, echo, kill, pwd, ...; such calls are checked
        at run time (Proc::ShellSpawn::Large::b_late); command NAME reaches the
        regular builtins we run.  Not alias, unalias.
      - Rule E choices done: ${@#p}, ${*%p} on each parameter (G4, as bash);
        $LINENO (G8, as bash; not in eval or trap text); UID an ordinary
        variable where the string sets it (G15, as dash); ~ without HOME from
        the password file (G16, as bash), and HOME may be set and unset; break
        and continue in a subshell inside a loop end it (as dash).  jobs takes
        %N, %%, %+, %-.
      - K3 (the user's choice B, 2026-11-03): background jobs go on between
        run() calls too: while they remain, Proc::ShellSpawn keeps a deferred
        SIGCHLD handler installed, which calls the new Proc::Spawner::poll().
        Cost (documented in doc/pspawn-design.md, section 6, and the POD): the
        program's own sleep and select return early when a child ends.  Test
        t/between.t.
      - Q2 decided (the user's choice B): printf -v stays with the program.
      - doc/checklists/corpus-gaps.html and its sources (doc/large-subset/pages/)
        are no longer shipped: all its questions are decided (they are in 0.04).
      - X25 fixed (a bug since 0.01): break or continue in a pipeline element
        inside a loop died with a Perl error.
      - Hand-kept verdict "no difference" for the new parser test LS:299
        (kill() { :; }; kill -SIGHUP 1): the detector flags kill -SIGHUP, but
        both shells run the function (probe fnkill).
      - doc/pspawn-spawner.md: section 8 (Windows) is in the distribution.
        (Corrected post4-0.04: it was not "in 0.01 but lost by 0.04"; no CPAN
        release had it, only the Project's later copy of 0.01.)
      - Detector: atpat, lineno, nohome, uidassign, SC3058 are class X (rule E);
        uidread is the old bashvar for UID.  Battery
        t/batteries/large-subset-post04.txt.
      - Figures: 91 samples rejected within the lower yardstick (0.04: 95);
        digest 1b9eac74d672e79d89764f71b1a6125e4a52f4cdc42943a6db39ec3d0b6d95aa.

0.04  2026-11-03
      - The documents of only historical interest are no longer shipped; they
        are in Proc-Spawner 0.03 on CPAN: doc/large-subset/large-subset-
        {ceiling,investigation,marks,upstream-reports}.md, doc/pspawn-run-
        shortcomings.md, doc/pspawn-upstream-reports.md, doc/parsers/shell-
        subset-corpus-stats.md.  References to them say so.
      - The decision pages that are no longer relevant are no longer shipped
        either (they are in 0.03): in doc/checklists/ child-exit, run-graph-api,
        spawn-graph, large-subset and dialect-choices.html, and in
        doc/large-subset/pages/ their sources and build.pl.  Only
        corpus-gaps.html (question Q2 is open) and its sources stay.
        Known issue X1 now names the alternatives of K3 itself.
      - The figures of post4-0.03 were checked again on 0.04: the same per-sample
        results; they are labelled 0.04.  Digest of the code they depend on:
        ecde8b9b9610680560a69e56313f01687f29e51688bcd029c6cef837fa03eeac.
      The documents label the unreleased states between 0.03 and 0.04 post-0.03
      to post4-0.03; their entries follow, newest first.

    post4-0.03:
      - Rule E (the user, 2026-11-03), replacing A3: where dash and bash
        differ, the subset takes the plainly logical behaviour, and rejects the
        form only where there is none.  doc/large-subset/large-subset-logical.md
        lists every difference with its category (info, error, posix, same,
        harm, platform, meaning, effect, none) and checks the earlier decisions
        against the rule.  Done so far: jobs, jobs -p, jobs -l list the jobs also
        in a pipeline and in $( ) (as bash; this replaces the rejection of
        post3-0.03); return outside a function is an error (as bash); cd with
        extra operands or without HOME is an error (as bash); descriptors above 9
        (option fdmulti now on by default); [^...] negates; patterns never match
        . or ..; settings with a redirection are redirected before their values
        are expanded (as dash).  getopts leaves OPTARG unset when it finds no option, or
        an option without an argument (POSIX; as bash; dash keeps or empties it).  Battery t/batteries/large-subset-e.txt (the
        differences from dash are listed in t/dash-known.txt as rule E).
      - Forms both shells run alike, now accepted: reading $PPID (assigning it
        stays rejected), export "$name", symbolic umask modes, redirections on
        cd, and the top-level jobs -p.
      - X19 fixed: a trap's action now runs before the next command of the list
        (it could run after it, about 1 run in 30).  New known issue X24.
      - The detector: the choices of rule E are class X with basis E:CATEGORY;
        ShellCheck's SC3023 and SC3026 likewise; local and return outside a
        function are separate features; local=... is no longer taken for local.
      - Figures: 95 samples rejected within the lower yardstick (post3-0.03:
        106); digest 84245e1d9a2ad88da10b320f8d2e94ddeb0b12bd888925555b920d985026e10a.

    post3-0.03:
      - Proc::ShellSubset::Large rejects jobs in a pipeline, a command
        substitution or a compound command: there dash lists nothing and bash
        the jobs (probes jobspipe, jobsppipe, jobspsubst).  Tests LS:288-292.
      - The detector has two more blatant features: jobs in a pipeline or $( )
        (jobsctx), and cd with extra operands (cdargs: dash ignores them, bash
        fails).  Its word pattern now keeps $( ), backquotes, ${ } and
        backslash-escapes in one word.  11 new probes (jobs, cd, the P6 forms) in
        doc/large-subset/dialect-cases/large-subset-dialect-probes2.txt.
      - Figures: 106 samples rejected within the lower yardstick (post-0.03:
        117); digest 64810b85d9f4cc777ef7d41f3cc3f9f4262c1c56cf877ba00489dbce07b117e8.

    post2-0.03:
      - pwd runs as the program /bin/pwd -L, with PWD set to the shell's own
        directory, and its operands dropped (known issue X23; the user's choice C,
        2026-11-03): after cd through a symbolic link it prints the directory as
        reached, as both shells (before: the physical path).  Two more lines in
        t/batteries/large-subset-p6.txt (21 of 21 agree with dash).  The parsers
        did not change, so the figures of post-0.03 hold.

    post-0.03:
      - Proc::ShellSubset::Large accepts more forms both shells run alike (item P6
        of "Shell Corpus Gaps"; probes in doc/large-subset/dialect-cases):
        read -p PROMPT (also -rp; the executor writes the prompt to fd 2 only
        when fd 0 is a terminal, as both shells); cd -P and cd -L (the last one
        counts; the tree gets cd_p => 1, and the executor resolves symbolic
        links); a pattern in a redirection target is literal (echo a >*.log
        writes the file '*.log', as dash and bash in POSIX mode); set -o
        ignoreeof, notify, nolog, set -b and set +m (nothing to do); trap with
        signal names in any case and signal numbers, also FPE, and KILL and STOP
        (cannot be caught: nothing to do, status 0).  Tests LS:273-287 (LS:114
        now accepted), and t/batteries/large-subset-p6.txt (19 of 19 agree with
        dash).
      - pwd with an option both shells refuse (pwd -x, pwd --help) is rejected
        (rule Q1; test LS:111 changed).  Known issue X23: a bare pwd after cd
        through a symbolic link differs from both shells (the program /bin/pwd,
        decision B2 of the base parser's checklist) - a question to the user.
      - The detector (tools/large-subset-features.pl): a read -p prompt with
        quotes no longer hides the options after it (read -p "`echo -e x`" m
        was taken for read -e), read -p PROMPT without a name is the bash-only
        REPLY form, and set -o pipefail and the like are seen after other
        options (set -euo pipefail).
      - Figures (doc/large-subset/large-subset-state.md): the samples rejected
        within the lower yardstick went from 163 (0.03) to 117.  They depend on
        the parsers, the measurement scripts and the verdicts, whose sha256
        (the command: doc/large-subset/large-subset-state.md, section 3) is
        4921dfbd1b838bc48dabe234a5111ad69d8fbb7d43c8278c72dcf74a5ad9b7bf.

0.03  2026-11-03
      - Proc::ShellSubset::Large accepts forms dash and bash run alike (items
        P1-P4 of "Shell Corpus Gaps"): '{' and '}' inside words where no brace
        expansion can start; $( ), `...`, ${#NAME} and $((...)) inside $((...));
        setting PS1, PS4, HOSTNAME, PIPESTATUS and BASH; export $v[=value];
        exit N above 255; umask -S; command -p PROGRAM.  '{' or '}' as a
        command name, and '{NAME}' right before a redirection (bash: a named
        descriptor), stay rejected (tests LS:212-245).
      - Proc::ShellSpawn, Proc::ShellSpawn::Large: run those forms ($( ) inside
        $(( )) expanded first; exit N modulo 256; umask -S; command -p with
        /usr/bin:/bin; export with a name from an expansion, checked at run time).
      - The borrowed test pool: the scripts that made
        t/parser/shell-subset-corpus-tests.pl are now in doc/parsers/tools/
        (extract, run, group, gentests, prov, verify and the reports), with
        the grouping of 2026-10-29 (groups.tsv); doc/test-pools.md says how to
        rebuild every pool and measurement from the pinned sources (checked:
        all 612 snippet IDs restore their texts exactly).
      - Measuring against dash and bash by the shells alone:
        doc/large-subset/large-subset-state.md (the rules, which beats which,
        the figures, the work list); large-subset-dialect-features.md (every
        probed feature, blatant or subtle, with who decided it);
        tools/large-subset-features.pl (the detector, and the per-test record
        t/parser/large-subset-shells.tsv), tools/large-subset-verdicts.txt
        (verdicts kept by hand, postponed ones as free text), and a new
        tools/large-subset-yardsticks.pl (the old one sorted our parser's
        own messages).
      - doc/pspawn-known-issues.md: X21 (the programs echo, [, printf, kill on
        Windows), X22 (bash 5.2.21 goes on after some special-builtin errors,
        against its manual).
      - Items the user agreed to on "Shell Corpus Gaps" (2026-11-03):
        B1 alias and unalias (the parser expands aliases in the lines after a
        top-level definition, as both shells do; the executor keeps the table
        for eval and . text); B2 history, fc, bind, help, compopt, logout and
        set -o history do nothing by default (parser and run() option
        interactive => reject|nop|dash), and the executor looks command names
        up in a list of tables, @Proc::ShellSpawn::CMDS, which other modules can
        extend; B3 jobs, bg, fg, hash, type, times and reading ulimit, with
        dash's texts and statuses, in the new module Proc::ShellSpawn::Builtins;
        P5 and R1 [ and test are the program, and '<' or '>' between two
        operands need the parser option test_lt_gt; Y2 a backslash at the very
        end is a literal \; Y3 tldr's <Enter> in one-line here-documents, and
        lines with keystrokes left out of the measurement; Q1 kill with a SIG
        signal name is rejected (both shells refuse it).
      - Every measured figure in doc/ names the version it was measured on
        (pre-0.01, 0.01, 0.02, early-post-0.02, 0.03; the table is in
        doc/README.md).  The figures of 0.03 were measured again on the release:
        the corpus yardsticks, the work list and the totals over the tests came
        out the same.  Corrected: spec section 8's ShellCheck figures (now as in
        the ceiling document), test-pools.md's size of the per-test record
        (1,066 lines), and the count of samples accepted only thanks to class X
        (258, not 259); test-pools.md's regrouping count is of 0.03 (577 groups,
        checked).
      - doc/checklists/corpus-gaps.html is the page "Shell Corpus Gaps" as
        published (its third version, with the figures of 0.03), and
        doc/large-subset/pages/gaps-items.txt.lede its lede (the lede had been
        edited only in the published page).  t/dash-known.txt no longer lists the
        line of largebattery.txt with an alias as a known difference: since B1 it
        agrees with dash, so a change there now fails t/dash.t.
      - lib/Proc/Spawner/Docs.pod: a map of the Markdown documents in doc/;
        doc/large-subset/large-subset-state.md.html: the same text as the .md,
        with a Markdeep footer (an experiment in how MetaCPAN shows them).
      - Proc::ShellSubset::Large no longer claims the package name
        Proc::ShellSubset for the PAUSE indexer (MetaCPAN showed it as the
        home of Proc::ShellSubset).

0.02  2026-11-02
      - Documentation: POD for Proc::ShellSubset, Proc::ShellSubset::Small and
        Proc::ShellSubset::Large; doc/large-subset/ (the large parser's
        specification, how its corpus coverage is measured, with the scripts in
        doc/large-subset/tools/, and its history); doc/parsers/ (the base
        parser's grammar and the provenance of its corpus tests); three more
        decision pages in doc/checklists/.
      - t/parsers.t also checks that Proc::ShellSubset::Small parses every test
        string as Proc::ShellSubset does, and, with dash and bash in PATH, that
        both shells accept what only the large grammar accepts; the checks that
        need dash and bash are skipped without them (they used to fail).
      - The message for a construct outside the subset found at run time
        (status 248) now names Proc::ShellSubset::Large, not ShellSubsetLarge.
      - README: the aim is matching dash/bash (what both do alike), not dash
        alone; also portable shell scripts; every started program is a child of
        the Perl process.  Proc::ShellSpawn's header says the same.
      - Every module has $VERSION.
      - t/dash.t: each run has its own tmp/ for the battery strings that wrote fixed
        files in /tmp, and ignores the length of process IDs turned into N.
        Test runs at the same time no longer fail each other (probably the
        unexplained failure X20).  The kill -INT $$ strings are marked FLAKY:
        X19 also shows with a subshell.

0.01  unreleased (2026-11-02)
      - First version: Proc::Spawner (posix_spawn with every glibc 2.39 setting, the
        one event loop), Proc::Spawner::Graph (the executable spawn graph),
        Proc::ShellSpawn (runs shell one-liners without fork), and the parsers
        Proc::ShellSubset, Proc::ShellSubset::Small, Proc::ShellSubset::Large.
      - Builds with older glibc (seen failing on 2.36): Makefile.PL probes for each
        posix_spawn extension, and an option the libc lacks follows UNSUPPORTED.
      - No "Hexadecimal number > 0xffffffff" warning from t/spawner.t or $((0x...)).
      - Tests: t/graph.t, t/parsers.t, t/dash.t besides t/spawner.t.
      - Former names: PosixSpawn (now inside Proc::Spawner), ShellSpawn,
        ShellSubset, ShellSubsetSmall, ShellSubsetLarge.
