@sleepy_panda_srl/portulan 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +7 -6
  2. package/cli/README.md +145 -358
  3. package/cli/advisory.mjs +354 -0
  4. package/cli/comments.mjs +932 -0
  5. package/cli/compile.mjs +1719 -1699
  6. package/cli/context.mjs +883 -0
  7. package/cli/control-chars.mjs +12 -301
  8. package/cli/discover.mjs +17 -574
  9. package/cli/doctor.mjs +269 -1080
  10. package/cli/drills.mjs +92 -504
  11. package/cli/feedback.mjs +16 -237
  12. package/cli/finish.mjs +437 -0
  13. package/cli/form.mjs +644 -0
  14. package/cli/fuzz-shell.mjs +20 -305
  15. package/cli/gate.mjs +7 -199
  16. package/cli/goldens.mjs +26 -184
  17. package/cli/index.mjs +305 -655
  18. package/cli/init.mjs +243 -566
  19. package/cli/inside.mjs +3 -28
  20. package/cli/instructions.mjs +829 -0
  21. package/cli/ledger.mjs +882 -0
  22. package/cli/librarian.mjs +135 -599
  23. package/cli/manifest.mjs +2 -39
  24. package/cli/mutants.mjs +9 -170
  25. package/cli/new.mjs +12 -243
  26. package/cli/pack-identity.mjs +5 -74
  27. package/cli/pack-version.mjs +13 -255
  28. package/cli/plugin-lint.mjs +42 -658
  29. package/cli/portulan.mjs +6 -125
  30. package/cli/recipe-set.mjs +15 -245
  31. package/cli/release-eval.mjs +28 -348
  32. package/cli/review-meter.mjs +29 -265
  33. package/cli/rule-carriers.mjs +12 -175
  34. package/cli/sessions.mjs +82 -0
  35. package/cli/skill-goldens.mjs +10 -176
  36. package/cli/skills-set.mjs +12 -414
  37. package/cli/stop-gate.mjs +83 -514
  38. package/cli/symbols.mjs +1132 -0
  39. package/cli/telemetry.mjs +82 -505
  40. package/cli/upgrade.mjs +171 -314
  41. package/cli/vendor.mjs +146 -476
  42. package/cli/version-carriers.mjs +8 -86
  43. package/core/README.md +1 -1
  44. package/core/engine.md +3 -9
  45. package/core/operating/autonomy.md +1 -1
  46. package/core/operating/context.md +258 -0
  47. package/core/operating/evolution.md +3 -3
  48. package/core/operating/loop.md +13 -20
  49. package/core/operating/memory.md +12 -3
  50. package/core/operating/sessions.md +100 -0
  51. package/core/personas/README.md +33 -0
  52. package/core/personas/librarian.md +2 -2
  53. package/core/skills/clarify/SKILL.md +1 -1
  54. package/core/skills/consolidate/SKILL.md +5 -3
  55. package/core/templates/handoff.md +28 -15
  56. package/core/templates/memory-entry.md +1 -0
  57. package/evals/README.md +271 -845
  58. package/evals/releases/0.2.0.json +146 -0
  59. package/evals/releases/0.2.0.md +67 -0
  60. package/evals/releases/0.3.0.json +150 -0
  61. package/evals/releases/0.3.0.md +68 -0
  62. package/evals/releases/README.md +3 -3
  63. package/package.json +5 -2
  64. package/packs/.claude-plugin/plugin.json +2 -1
  65. package/packs/README.md +7 -4
  66. package/packs/rituals/checkpoints/README.md +1 -1
  67. package/packs/tools/README.md +8 -8
  68. package/packs/tools/github/verify/actions-pinned.sh +10 -55
  69. package/packs/tools/local-llm/README.md +136 -0
  70. package/packs/tools/local-llm/pack.json +32 -0
  71. package/packs/tools/local-llm/personas/local-worker.md +44 -0
  72. package/packs/tools/local-llm/runner/local-llm.mjs +1665 -0
  73. package/packs/tools/local-llm/skills/delegate-local/SKILL.md +64 -0
  74. package/packs/tools/local-llm/verify/README.md +18 -0
  75. package/spec/README.md +113 -6
  76. package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
  77. package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
  78. package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
  79. package/spec/migrations/0004-changelog-fragments.mjs +53 -0
  80. package/spec/migrations/0005-session-log-retired.mjs +69 -0
  81. package/spec/migrations/0006-boot-card.mjs +77 -0
  82. package/spec/migrations/0007-guidance-compiled.mjs +31 -0
  83. package/spec/migrations/0008-card-reading.mjs +55 -0
  84. package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
  85. package/spec/migrations/0010-comment-rail.mjs +57 -0
  86. package/spec/migrations/README.md +45 -10
  87. package/spec/pack.schema.json +1 -1
  88. package/spec/slots.md +343 -27
  89. package/spec/workspace.schema.json +145 -4
@@ -1,63 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Grammar-aware fuzzing over this repository's two shell segmenters.
3
3
  //
4
- // Milestone 8, clause (b), second half. The first half — mutation testing over both matchers — is
5
- // ./mutants.mjs, and the two are separate because a surviving mutant and a grammar bypass are
6
- // different verdicts with different repairs.
7
- //
8
- // ## Why a grammar, and not a string mutator
9
- //
10
- // A mutator that flips bytes in `git push --force origin main` has no idea whether the string it
11
- // produced is still a command bash would run. It can therefore report neither a bypass nor a false
12
- // red — only a difference, which somebody then has to adjudicate by hand. That is the review round
13
- // this clause exists to replace.
14
- //
15
- // This generator instead COMPOSES a command from a grammar, and so knows by construction where the
16
- // payload it embedded ended up:
17
- //
18
- // - a **command position** — bash executes the payload. The matcher must answer `true`.
19
- // - a **data position** — bash never executes it. The matcher must answer `false`.
20
- //
21
- // That is an exact oracle rather than a statistical one, and it is what makes the word
22
- // *grammar-aware* mean something here.
23
- //
24
- // **The declared ground truth is measured, not asserted.** ./fuzz-shell.ground.test.mjs runs every
25
- // production under real bash with a NEUTRAL payload — `printf ok`, never a gated command — and fails
26
- // if a production's declared position disagrees with what bash actually did. Every path spelling is
27
- // verified the same way, by writing to a throwaway file in a temp directory. A hole list is a claim
28
- // like any other; so is a grammar.
29
- //
30
- // ## Two axes, and only one of them is fuzzed
31
- //
32
- // **Positions are ENUMERATED and RECORDED.** There are few of them, each has a name a reader can
33
- // argue with, and each carries the matcher's answer in `EXPECT`. Where that answer is not the one
34
- // ground truth demands, the entry names the record that documents the divergence — an entry in
35
- // `../.portulan/gate-map.md`'s honest-holes list, or, for one case, a decision recorded in
36
- // `./compile.mjs` itself. Both directions are red, exactly as `documented-hole` is in ./goldens.mjs:
37
- // a recorded escape that starts being caught is good news the record must absorb.
38
- //
39
- // **Spellings are FUZZED.** Within one position the generator writes the same semantic command many
40
- // different ways — quoting form per word, backslash escapes, line continuations, `$'…'`, and for the
41
- // write payload the path spellings a shell resolves to one file (`./`, `//`, `/./`, `/../`, quoted,
42
- // escaped). The invariant is exact and it is the one the segmenters exist to provide: **every
43
- // spelling of one command in one position must get the same answer.** A spelling that answers
44
- // differently from its cell is the finding, and it is the shape three of #336's ten review rounds
45
- // had: a reader narrower than a shell word.
46
- //
47
- // ## Deterministic, bounded, and reproducible
48
- //
49
- // A verify recipe whose verdict moves between runs is not a rail. The generator draws from a seeded
50
- // PRNG written into this file — no dependency, no `Math.random` — and the recipe pins the seed. The
51
- // seed is printed on EVERY run, green included, so a green is as reproducible as a red. `--seed` and
52
- // `--cases` exist for a session hunting deeper than the pinned budget.
53
- //
54
- // ## What it never does
55
- //
56
- // **It executes nothing.** The payloads are `git push --force origin main` and a constitution write,
57
- // by design; there is no code path from this module to a subprocess and ./fuzz-shell.test.mjs asserts
58
- // it imports no process-spawning API. The bash measurement lives in the ground-truth test, uses a
59
- // neutral payload, and is a separate file for exactly that reason.
60
- //
61
4
  // Exit 0 green · 1 red · 2 could not run.
62
5
 
63
6
  import fs from "node:fs";
@@ -67,38 +10,19 @@ import { pathToFileURL } from "node:url";
67
10
  import { CompileError, matchesRule } from "./compile.mjs";
68
11
  import { CouldNotRun, matcherPath, yieldedRules } from "./goldens.mjs";
69
12
 
70
- /** The pinned budget: spellings generated per (position × payload) cell. */
13
+ /** Spellings generated per (position × payload) cell. */
71
14
  export const DEFAULT_CASES = 48;
72
15
 
73
- /** The pinned seed. Changing it changes what is generated, so it is a reviewed edit like any other. */
74
16
  export const DEFAULT_SEED = 20260825;
75
17
 
76
- /**
77
- * The payloads: one per matcher, and TWO for the write matcher because it has two recognitions.
78
- *
79
- * `./compile.mjs`'s `shellWrites` docblock puts it plainly — *"Two recognitions, both by table: a
80
- * `>`/`>>` redirection into the path, and a file-writing command that names it"* — and measurement
81
- * showed the two do not answer alike everywhere: in the `same-line-comment` position the redirection
82
- * is caught and the named argument is not, because there the segment's head is `ls`. Folded into one
83
- * payload that cell would answer two ways and the fuzzer's invariant would have to be weakened to
84
- * admit it. Split, every cell is exact.
85
- *
86
- * Every rule named here must exist in the yielded policy or the run refuses — a fuzzer that skipped a
87
- * payload whose rule it could not find would report a clean sweep of the part it managed to run.
88
- */
18
+ /** A payload per recognition: a redirected write and a named one can answer differently in one position. */
89
19
  export const PAYLOADS = {
90
20
  shell: { rule: "force-push-without-a-lease", tool: "Bash", what: "a gated force-push" },
91
- "write-redirect": { rule: "edit-the-constitution", tool: "Bash", what: "a redirection into the constitution" },
92
- "write-named": { rule: "edit-the-constitution", tool: "Bash", what: "a writer NAMING the constitution" },
21
+ "write-redirect": { rule: "change-the-constitution", tool: "Bash", what: "a redirection into the constitution" },
22
+ "write-named": { rule: "change-the-constitution", tool: "Bash", what: "a writer NAMING the constitution" },
93
23
  };
94
24
 
95
- /**
96
- * mulberry32 — a small, fast, well-distributed 32-bit PRNG.
97
- *
98
- * Written out rather than imported because this repository installs nothing, and reached for rather
99
- * than `Math.random` because a fuzzer whose corpus changes every run cannot be a required check: a
100
- * red nobody can reproduce is a red nobody fixes.
101
- */
25
+ /** mulberry32, a seeded 32-bit PRNG. */
102
26
  export function prng(seed) {
103
27
  let a = seed >>> 0;
104
28
  return () => {
@@ -112,18 +36,7 @@ export function prng(seed) {
112
36
 
113
37
  const pick = (rand, xs) => xs[Math.floor(rand() * xs.length) % xs.length];
114
38
 
115
- /**
116
- * FNV-1a over a string, for deriving one cell's seed from its full identity.
117
- *
118
- * **The first cut mixed in `position.id.length` and `kind.length` instead**, which made every cell
119
- * whose id happened to be the same length share one stream — most of the grammar did, measured on
120
- * 2026-08-25 — while the comment beside it claimed each cell had its own. A claim broader than the code
121
- * it describes is this repository's signature defect and this session had already met it three times
122
- * in other people's code before writing one of its own. The cells stayed correct (each still drew its
123
- * full budget) and the spelling space collapsed onto far fewer distinct streams than the budget
124
- * implies, which is a quiet loss of exactly the diversity this axis exists for. Reported by Copilot,
125
- * round 1 on #338.
126
- */
39
+ /** 32-bit FNV-1a. */
127
40
  export function hash(text) {
128
41
  let h = 0x811c9dc5;
129
42
  for (let i = 0; i < text.length; i += 1) {
@@ -137,13 +50,7 @@ export function hash(text) {
137
50
  // The spelling axis — many ways to write one command
138
51
  // =============================================================================================
139
52
 
140
- /**
141
- * Quote or escape one word without changing what a shell makes of it.
142
- *
143
- * `$'…'` reaches `shellWords` with its `$` dropped and is then read as a single-quoted run, so no
144
- * ANSI-C escape sequence is generated inside one: `$'\x41'` is `A` to bash and `\x41` to the matcher,
145
- * which would be a generator that lies about its own ground truth rather than a finding.
146
- */
53
+ /** No escapes inside `$'…'`: the matcher reads it as plain `'…'`, where bash would decode them. */
147
54
  export function respell(word, rand) {
148
55
  switch (pick(rand, ["bare", "double", "single", "ansi-c", "escape-one", "split-quote"])) {
149
56
  case "double":
@@ -153,14 +60,10 @@ export function respell(word, rand) {
153
60
  case "ansi-c":
154
61
  return `$'${word}'`;
155
62
  case "escape-one": {
156
- // A backslash before an ordinary character is that character. Chosen at a position rather
157
- // than always the first, so the generator explores the word rather than one index of it.
158
63
  const i = Math.floor(rand() * word.length);
159
64
  return `${word.slice(0, i)}\\${word.slice(i)}`;
160
65
  }
161
66
  case "split-quote": {
162
- // `do"cs"/vision.md` — one word, two quoting regimes. Bash concatenates adjacent quoted
163
- // and unquoted runs into a single word, which is the property being tested.
164
67
  const i = 1 + Math.floor(rand() * Math.max(1, word.length - 1));
165
68
  return `${word.slice(0, i)}"${word.slice(i)}"`;
166
69
  }
@@ -169,14 +72,7 @@ export function respell(word, rand) {
169
72
  }
170
73
  }
171
74
 
172
- /**
173
- * Spellings of `docs/vision.md` that a shell resolves to the same file.
174
- *
175
- * Every one of these must reach the constitution's write gate. They are the "two path spellings" the
176
- * 2026-07-28 amendment names among the eight bypasses #60 shipped with, generalised: `normalisePath`
177
- * exists for exactly this list and `./fuzz-shell.ground.test.mjs` writes each one to a throwaway file
178
- * under a temp directory to prove bash agrees.
179
- */
75
+ /** Spellings of `target` that a shell resolves to the same file. */
180
76
  export function pathSpellings(target) {
181
77
  const parts = target.split("/");
182
78
  const head = parts.slice(0, -1).join("/");
@@ -186,13 +82,7 @@ export function pathSpellings(target) {
186
82
  `./${target}`,
187
83
  `${head}/./${tail}`,
188
84
  `${head}//${tail}`,
189
- // **A `..` hop names the file only where the directory it hops through EXISTS**, because bash
190
- // resolves `..` against the real filesystem while `normalisePath` resolves it lexically. That
191
- // difference is fail-CLOSED and deliberate — the matcher answers `true` for a hop through a
192
- // directory that is not there, which is a false red on a command bash would refuse anyway —
193
- // and ./fuzz-shell.ground.test.mjs creates `sibling` before it measures this spelling, so
194
- // the claim being measured is the one the list actually makes. Found by that measurement:
195
- // the first spelling here hopped through a directory nothing created and bash refused it.
85
+ // bash resolves `..` against the filesystem, so the ground test creates `sibling` first.
196
86
  `${head}/sibling/../${tail}`,
197
87
  `./${head}/./${tail}`,
198
88
  // A backslash-newline is a line continuation: both characters vanish and the word continues.
@@ -200,18 +90,6 @@ export function pathSpellings(target) {
200
90
  ];
201
91
  }
202
92
 
203
- /**
204
- * The writer shapes, split by which of `shellWrites`' TWO recognitions they reach.
205
- *
206
- * **The split is structural, not cosmetic, and measurement is what forced it.** That function's own
207
- * docblock says *"Two recognitions, both by table: a `>`/`>>` redirection into the path, and a
208
- * file-writing command that names it"* — and the two do not answer alike in every position, because a
209
- * redirection is read off the whole segment while a named argument needs the segment's HEAD to be a
210
- * writer. In the `same-line-comment` position they diverge outright: `ls # echo ok > docs/vision.md`
211
- * is caught, `ls # cp /tmp/x docs/vision.md` is not, because there the head is `ls`. Keyed as one
212
- * payload the cell would answer two ways and the invariant would have to be weakened to admit it;
213
- * keyed as two, each cell is exact and the divergence becomes a recorded fact.
214
- */
215
93
  export const WRITERS = {
216
94
  "write-redirect": [(p) => `echo ok > ${p}`, (p) => `echo ok >> ${p}`, (p) => `cat /tmp/x > ${p}`, (p) => `printf ok 1> ${p}`],
217
95
  "write-named": [
@@ -225,25 +103,14 @@ export const WRITERS = {
225
103
  ],
226
104
  };
227
105
 
228
- /** One spelling of the write payload: a writer shape, a path spelling, and quoting over both. */
229
106
  export function writePayload(rand, kind, target = "docs/vision.md") {
230
107
  const spelling = pick(rand, pathSpellings(target));
231
- // A line continuation must not be quoted — inside `"…"` a backslash-newline is still a
232
- // continuation, but inside `'…'` it is two literal characters and the word stops being the path.
233
- // Generating that would be the generator lying about its own ground truth.
108
+ // Never requote a continuation: inside `'…'` a backslash-newline is two literal characters.
234
109
  const quoted = spelling.includes("\\\n") ? spelling : respell(spelling, rand);
235
110
  return pick(rand, WRITERS[kind])(quoted);
236
111
  }
237
112
 
238
- /**
239
- * One spelling of the shell payload.
240
- *
241
- * **The gated prefix stays literal, and that is a property of the ORACLE rather than a convenience.**
242
- * `git push --force` is a command *prefix* and both the compiled permission rule and this matcher
243
- * compare it literally, so `"git" push --force …` is a spelling no layer claims to catch — hole 1's
244
- * "spellings neither layer sees". Generating it would produce a finding the gate map already answers.
245
- * Only the arguments after the prefix are respelt.
246
- */
113
+ /** Only the arguments are respelt: the prefix is matched literally, and the gate map records that hole. */
247
114
  export function shellPayload(rand, target = "git push --force") {
248
115
  const args = pick(rand, [[], ["origin", "main"], ["origin", "HEAD"], ["--repo", "o/r"], ["origin", "main", "--quiet"]]);
249
116
  return [target, ...args.map((a) => respell(a, rand))].join(" ");
@@ -253,14 +120,7 @@ export function shellPayload(rand, target = "git push --force") {
253
120
  // The position axis — enumerated, recorded, and measured under bash
254
121
  // =============================================================================================
255
122
 
256
- /**
257
- * Where the payload sits, and whether bash runs it there.
258
- *
259
- * `bashSafe: false` marks a production ./fuzz-shell.ground.test.mjs must not execute even with a
260
- * neutral payload — `sudo` would prompt for a password on a developer's machine and hang a test
261
- * suite. Its ground truth is argued in `why` instead of measured, and saying which of the two a
262
- * production got is the point of the flag.
263
- */
123
+ /** Where the payload sits: bash executes it in a `command` ground and never in a `data` one. */
264
124
  export const POSITIONS = [
265
125
  { id: "bare", ground: "command", build: (p) => p },
266
126
  { id: "after-semicolon", ground: "command", build: (p) => `ls; ${p}` },
@@ -284,69 +144,16 @@ export const POSITIONS = [
284
144
  build: (p) => `sudo ${p}`,
285
145
  },
286
146
  { id: "leading-redirection", ground: "command", build: (p) => `2> /dev/null ${p}` },
287
- // ------------------------------------------------------------------------------------------
288
- // **The redirection-TARGET family — session-open adjustment 4, and the class #336 met three
289
- // times.** A shell word may hold spaces when it is quoted or escaped, and `LEADING_REDIRECTION`
290
- // must consume the whole word or the command behind it escapes ungated. That defect shipped
291
- // twice on #336 and was fixed twice at the spelling that was quoted, until the suite stopped
292
- // asserting spellings and asserted the rule against `shellWords`. These productions put the same
293
- // rule in front of the generator, so a fourth sibling reds here rather than in a review.
294
- //
295
- // Every one is a COMMAND position: bash redirects stderr to a file and then runs the payload.
296
147
  { id: "leading-redirection-quoted-target", ground: "command", build: (p) => `2> "log file.txt" ${p}` },
297
148
  { id: "leading-redirection-single-quoted-target", ground: "command", build: (p) => `2> 'log file.txt' ${p}` },
298
149
  { id: "leading-redirection-escaped-space-target", ground: "command", build: (p) => `2> log\\ file.txt ${p}` },
299
150
  { id: "leading-redirection-escaped-quote-target", ground: "command", build: (p) => `2> "log \\"q\\" file.txt" ${p}` },
300
- // ------------------------------------------------------------------------------------------
301
- // **A CRLF continuation, which is NOT a continuation.** `shellWords` consumes `\r\n` after a
302
- // backslash as a pair and joins the word; **five shells do not, and split the command instead** —
303
- // `bash 3.2.57(1)` (arm64-apple-darwin25), `bash 5.2.15(1)` (aarch64-unknown-linux-gnu) and
304
- // `bash 5.2.37(1)` (x86_64-pc-linux-gnu), plus `zsh 5.9` and **the measured host's `/bin/sh`**, which
305
- // on that macOS host is bash 3.2.57 in POSIX mode — `dash` and `busybox`, the common `/bin/sh` on
306
- // Linux, were NOT measured.
307
- // The three bash builds are named in full rather than as "bash 5", which would be a claim from two 5.2
308
- // patchlevels; no bash 4.x was measured, and neither was any Windows-side bash — git-bash, MSYS2,
309
- // Cygwin or WSL — which is the platform the retired reachability claim named. Widened 2026-08-25
310
- // from "three shells on this machine", which was the true width until two containers were run.
311
- //
312
- // **What follows from that is NOT one answer, and an earlier draft of this very comment said it
313
- // was.** It read "the matcher answers `true` anyway: a FALSE RED", which is right for one payload
314
- // and wrong for another:
315
- //
316
- // - `shell` — nothing gated runs. The matcher answers `false`. Correct.
317
- // - `write-named` — the intended write never runs and the target is untouched. The matcher
318
- // answers `true`. (`cp` ITSELF does run, on the fragment before the split, and fails on a
319
- // malformed `\r` argument — the sibling of the cell below, corrected in the same pass after
320
- // shipping the fix to one carrier and not the other. Copilot, round 3 on #342.)
321
- // A false red, fail-closed, and the only one of the three.
322
- // - `write-redirect` — a shell applies a redirection BEFORE it looks the command up, so the
323
- // clobbering redirection on the surviving fragment still fires and TRUNCATES the target to
324
- // zero bytes. The matcher answering `true` is a **true positive**. Measured.
325
- //
326
- // So the ground truth here is a property of the payload rather than of the position, which is why
327
- // `groundByKind` exists and why this is the only production that uses it. `exitsNonZero` is
328
- // declared because the fragment left over by the split is run as a command and is not found.
329
- //
330
- // _The stale sentence survived the correction of the three cells below it — one carrier fixed and
331
- // its sibling left, in the block whose subject is that class. Reported by Copilot, round 2 on #341._
151
+ // `shellWords` joins a backslash-CRLF; bash 3.2.57, 5.2.15 and 5.2.37 and zsh 5.9 split there.
332
152
  {
333
153
  id: "crlf-continuation-in-the-payload",
334
154
  ground: "data",
335
- // **The one production whose ground truth is not a property of the position.** bash splits at
336
- // the CRLF, so the intended write never runs and a gated command never runs — data. But a
337
- // CLOBBERING redirection is applied before the command is looked up, so it fires anyway and
338
- // truncates its target to zero bytes. That is the gated effect occurring, and calling it data
339
- // would record a destroyed file as an untouched one — which an earlier draft of this entry did,
340
- // in all three of its carriers, until the pre-commit checkpoint measured it.
341
155
  groundByKind: { "write-redirect": "command" },
342
- // `>>` appends and therefore does NOT truncate, so it is the one redirect shape whose ground
343
- // truth differs from its siblings'. Refused rather than folded in: a cell whose ground truth
344
- // varies with the spelling is a cell whose invariant cannot hold. Measured — `echo \<CRLF>ok >> t`
345
- // leaves `t` byte-for-byte unchanged while `> t` and `1> t` leave it empty. Re-measured
346
- // 2026-08-25 on bash 5.2.15 and 5.2.37 as well as 3.2.57: identical on all three, so the
347
- // refusal is right on **both bash 5.2 builds measured** and not only on the shell it was
348
- // written against. Named as builds rather than as "bash 5", which would be the series
349
- // inference this production's own comment refuses eight lines up.
156
+ // `>>` appends rather than truncates, so its ground differs from its siblings' and it is refused.
350
157
  carries: (p, kind) => kind !== "write-redirect" || !p.includes(">>"),
351
158
  exitsNonZero: true,
352
159
  why: "bash splits at the CRLF, so the fragment after it is run as a command and is not found — a non-zero exit is the measurement rather than a failure of it. A clobbering redirection on that fragment still fires, which is why `write-redirect` overrides the ground.",
@@ -360,14 +167,7 @@ export const POSITIONS = [
360
167
  {
361
168
  id: "wrapper-single-quoted",
362
169
  ground: "command",
363
- // **A single-quoted wrapper cannot carry a payload holding a `'`**, and this predicate is
364
- // here because bash said so. POSIX gives `'…'` no escapes, so `sh -c 'cp /tmp/x $'docs/vision.md''`
365
- // closes the wrapper at the payload's own quote and the inner shell then reads
366
- // `cp /tmp/x $docs/vision.md` — which writes `/vision.md`, NOT the constitution. The cell
367
- // records `true` and bash would not have touched the file: a generator composing a command it
368
- // no longer has ground truth for, which is the one failure this whole design is arranged
369
- // against. Measured by ./fuzz-shell.ground.test.mjs's wrapper-respelling case, added at the
370
- // pre-commit checkpoint after it named this composition as argued-but-unmeasured.
170
+ // POSIX `'…'` has no escapes, so a payload holding `'` would close the wrapper early.
371
171
  carries: (p) => !p.includes("'"),
372
172
  build: (p) => `sh -c '${p}'`,
373
173
  },
@@ -377,7 +177,6 @@ export const POSITIONS = [
377
177
  {
378
178
  id: "nested-wrapper",
379
179
  ground: "command",
380
- // The same limit, for the same reason: the inner wrapper here is single-quoted too.
381
180
  carries: (p) => !p.includes("'"),
382
181
  build: (p) => `bash -c "sh -c '${p}'"`,
383
182
  },
@@ -386,29 +185,10 @@ export const POSITIONS = [
386
185
  { id: "heredoc-body", ground: "data", build: (p) => `cat <<'EOF'\n${p}\nEOF` },
387
186
  { id: "same-line-comment", ground: "data", build: (p) => `ls # ${p}` },
388
187
  { id: "comment-then-separator", ground: "data", build: (p) => `echo ok #; ${p}` },
389
- // Added at the pre-commit checkpoint. Its differential of the staged matcher against HEAD's found
390
- // that the 2026-08-25 widening inherits the `#` decision one level INTO a wrapper — a new false
391
- // red, and the only new false-red class the repair produced. `comment-then-separator` pins the
392
- // top-level spelling and could not see the composed one, which is the "two claims that each held
393
- // and did not compose" shape this whole session is about, met once more in the fix for it.
394
188
  { id: "wrapper-holding-comment", ground: "data", build: (p) => `bash -c "echo ok #; ${p}"` },
395
189
  ];
396
190
 
397
- /**
398
- * What the matcher answers for each cell, and the record that licenses a disagreement with ground
399
- * truth.
400
- *
401
- * **Every entry here was MEASURED against the shipped matcher before it was written, and one round of
402
- * writing it from the armchair was thrown away first.** Milestone 8 session 0's founding result is
403
- * ten hand-written expectations refuted on the corpus's first run; an expectation table is a claim
404
- * like any other and this one is not exempt.
405
- *
406
- * `answer` is what `matchesRule` returns. `record` is required whenever `answer` disagrees with what
407
- * the production's `ground` demands, and it may cite either the gate map's honest-holes list or a
408
- * decision recorded in `./compile.mjs` — the false red on `#` is the second kind, argued at
409
- * `commandSegments` and declined on a Copilot review of #60, and it appears in no hole list because
410
- * it is a choice rather than a gap.
411
- */
191
+ /** The matcher's measured answer per cell; one that differs from ground truth names its `record`. */
412
192
  export const EXPECT = {
413
193
  "leading-redirection-quoted-target|shell": { answer: true },
414
194
  "leading-redirection-quoted-target|write-redirect": { answer: true },
@@ -616,51 +396,15 @@ export const EXPECT = {
616
396
  },
617
397
  };
618
398
 
619
- /** The answer ground truth demands, before any record is consulted. */
620
399
  export const correctFor = (ground) => ground === "command";
621
400
 
622
- /**
623
- * What "the payload took effect" MEANS, per payload kind.
624
- *
625
- * **A redirection is not a command, and conflating the two hid a true positive as a false red.** A
626
- * shell applies a redirection BEFORE it looks the command up, so a `>` whose command never runs still
627
- * truncates its target to zero bytes. For a `write-redirect` payload the gated effect is therefore
628
- * *the target was written*, not *the command ran* — and measuring it as the latter reports a
629
- * destroyed file as an untouched one. Measured on bash 3.2.57: a split `printf … 1> target` leaves
630
- * the target at **0 bytes** where it held content before.
631
- */
401
+ /** How a payload shows its effect: a redirection truncates its target even when its command never runs. */
632
402
  export const EFFECT = { shell: "ran", "write-redirect": "touched", "write-named": "ran" };
633
403
 
634
- /**
635
- * The ground truth for one cell, which is not always a property of the position alone.
636
- *
637
- * Most productions put the payload somewhere bash either runs or does not, whatever the payload is.
638
- * One does not: a CRLF splits the command, and what survives depends on the payload's SHAPE — a
639
- * clobbering redirection still fires and destroys the target, while a writer command simply never
640
- * runs. A single `ground` field cannot say that, so a production may override it per kind. The
641
- * override is rare by design and every use of it is argued in the production's own `why`.
642
- */
643
404
  export const groundFor = (position, kind) => position.groundByKind?.[kind] ?? position.ground;
644
405
 
645
- /**
646
- * The ACTION kind a payload kind reaches.
647
- *
648
- * Three payload kinds, two action kinds: the write matcher's two recognitions are separate payloads
649
- * here (see `WRITERS`) and one rule kind there. Anything that hands a payload kind to a consumer
650
- * expecting an action kind is a bug, and one shipped — see `asCase`.
651
- */
652
406
  export const ruleKind = (kind) => (kind === "shell" ? "shell" : "write");
653
407
 
654
- /**
655
- * Generate one case: a spelling of one payload in one position.
656
- *
657
- * **A position may REFUSE a payload it cannot carry, and the refusal is a redraw rather than a
658
- * skip.** A single-quoted wrapper holding a `'`-quoted word is not the command it looks like, so
659
- * emitting one would give the fuzzer an exact answer about a string whose ground truth it no longer
660
- * knows. Bounded, and a budget that runs out throws rather than returning something unchecked: a
661
- * generator that quietly gave up would thin the spelling space in exactly the cell whose quoting is
662
- * hardest, which is the cell that most needs the coverage.
663
- */
664
408
  export function generate(position, kind, rand) {
665
409
  const carries = position.carries ?? (() => true);
666
410
  for (let attempt = 0; attempt < 64; attempt += 1) {
@@ -674,28 +418,12 @@ export function generate(position, kind, rand) {
674
418
  );
675
419
  }
676
420
 
677
- /**
678
- * A finding, rendered as a corpus case ready to paste into evals/goldens/gates/<rule-id>.json.
679
- *
680
- * `../evals/README.md` records that clause (a)'s fixture format is clause (b)'s output shape, so a
681
- * bypass found here becomes a permanent regression pin by paste rather than by retyping. The `expect`
682
- * written is what the MATCHER ACTUALLY DID — a fixture records behaviour, and the repair belongs in
683
- * the matcher or in the record, never in a fixture that asserts what somebody wished had happened.
684
- */
421
+ /** A finding as a case to paste into `evals/goldens/gates/<rule-id>.json`, expecting what the matcher did. */
685
422
  export function asCase(position, kind, ruleId, command, actual, index) {
686
423
  return {
687
424
  id: `fuzz-${position.id}-${kind}-${index}`,
688
425
  class: correctFor(groundFor(position, kind)) === actual ? "holds" : "documented-hole",
689
426
  tool: PAYLOADS[kind].tool,
690
- // **The RULE kind, not the payload kind.** `matcherPath` reads an action kind — `shell` /
691
- // `write` / `read` — and this passed it `write-redirect` and `write-named`, which it knows
692
- // nothing about, so it fell through to `no-branch` and two of the three payloads emitted a
693
- // case `goldens` would refuse as a mislabel. The paste-ready promise this module and
694
- // `../evals/README.md` both make was therefore FALSE for the write matcher, which is
695
- // `../.portulan/dod.md` condition 4 rather than a preference. Reported by Copilot on #338;
696
- // it survived a drill that printed a finding, because the drill exercised the shell payload —
697
- // a check written alongside a change inheriting the change's blind spot, for the fifth time
698
- // in two sessions.
699
427
  path: matcherPath(ruleKind(kind), PAYLOADS[kind].tool),
700
428
  expect: actual,
701
429
  why: `Generated by cli/fuzz-shell.mjs: ${PAYLOADS[kind].what} in the \`${position.id}\` position, which is a ${groundFor(position, kind)} position for this payload. REVIEW THIS BEFORE COMMITTING IT — a generated case records what the matcher did, not what it should do.`,
@@ -740,8 +468,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
740
468
  const flag = argv[i];
741
469
  const raw = argv[i + 1];
742
470
  i += 1;
743
- // Refused rather than coerced. `Number("12abc")` is NaN and `Number("")` is 0, and a
744
- // fuzzer silently running zero cases is the false green this whole file is against.
745
471
  if (raw === undefined || !/^[0-9]+$/.test(raw)) throw new CouldNotRun(`${flag} needs a non-negative integer, not ${JSON.stringify(raw)}`);
746
472
  const value = Number(raw);
747
473
  if (flag === "--cases" && value === 0) throw new CouldNotRun("--cases 0 would generate nothing and report green having tested nothing");
@@ -766,9 +492,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
766
492
  for (const u of unresolved) say(`pack ${u.name} UNRESOLVED — ${u.why}; its gate fragments are not in this run`);
767
493
  const byId = new Map(rules.map((r) => [r.id, r]));
768
494
 
769
- // **A payload whose rule is absent is could-not-run, never a skip.** A fuzzer that quietly
770
- // ran one of its two matchers would print a green about half the subject the clause names —
771
- // ../.portulan/memory/a-checker-must-refuse-what-it-cannot-check.md.
772
495
  for (const [kind, p] of Object.entries(PAYLOADS)) {
773
496
  if (!byId.has(p.rule)) {
774
497
  throw new CouldNotRun(
@@ -777,9 +500,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
777
500
  );
778
501
  }
779
502
  }
780
- // The table must be total over the grammar, in both directions, and this is checked before a
781
- // case is generated: a position with no recorded answer would otherwise be silently skipped,
782
- // and a recorded answer for a position nobody generates is a claim about nothing.
783
503
  const keys = new Set(Object.keys(EXPECT));
784
504
  const missing = [];
785
505
  for (const position of POSITIONS) {
@@ -812,11 +532,7 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
812
532
  for (const [kind, p] of Object.entries(PAYLOADS)) {
813
533
  const expected = EXPECT[`${position.id}|${kind}`];
814
534
  const rule = byId.get(p.rule);
815
- // Per cell, and derived from the cell's FULL identity. Two properties, both wanted:
816
- // one cell's budget cannot shift another cell's stream — a case-count change would
817
- // otherwise re-roll every later cell and turn one edit into a new corpus — and no two
818
- // cells share a stream, so the spelling space is explored once per cell rather than
819
- // once per id length. See `hash` for what the first cut got wrong.
535
+ // Seeded per cell from its full identity, so one cell's budget never re-rolls another's.
820
536
  const rand = prng(seed ^ hash(`${position.id}|${kind}`));
821
537
  for (let i = 0; i < cases; i += 1) {
822
538
  const { command } = generate(position, kind, rand);
@@ -864,8 +580,7 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
864
580
  }
865
581
  }
866
582
 
867
- // The entry guard, in the ONE form `./rule-carriers.mjs` designates — see ./goldens.mjs for why it is
868
- // copied rather than re-derived.
583
+ // Compared as URLs so a space in the path still matches, and through realpath for a symlinked launch.
869
584
  function isMain() {
870
585
  const invoked = process.argv[1];
871
586
  if (!invoked) return false;