simt-dev 0.2.0__py3-none-any.whl

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 (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,95 @@
1
+ # ADR-038 — The gate filter belongs to every writer of a manifest, not to `init`
2
+
3
+ Status: **accepted** · 2026-10-08 · amends [ADR-017](ADR-017-a-declared-gate-must-be-runnable.md) ruling 1 · affects every code path that calls `buildManifest`
4
+
5
+ ## Context
6
+
7
+ ADR-017 ruled that a gate is declared only if the repo can run it, and attributed the
8
+ mechanism to one command: *"`init` filters the stack table's proposals through what the
9
+ repo demonstrably has."* The belief is general. The attribution is what was read, and it
10
+ stayed true only as long as `init` was the sole writer of a manifest.
11
+
12
+ Review 0002's S27 is what the attribution bought. `recordLocal` in `skills install` writes a
13
+ manifest for a repo that has none, and called `buildManifest` with no capability. Measured
14
+ end to end: a bare Node repo — a `package.json` with no scripts and no `node_modules` — ran
15
+ `skills install api-contract-sync --yes`, and the manifest it was handed declared `format`
16
+ and `lint` on pre-commit and `test` and `typecheck` on pre-push. `check` then reported four
17
+ `UNRUNNABLE` findings and exited 1. After `hooks install`, a real commit ran the pre-commit
18
+ gate, which failed both staged gates with their remedies and blocked. The hook's own exit
19
+ code is not the commit's, so the reader's first evidence is a commit that silently did not
20
+ happen.
21
+
22
+ Four declared gates the repo cannot run, written by a command whose entire job is to pin
23
+ one skill. ADR-017's own wording names this as the failure: *"declaring one would block
24
+ every commit"*.
25
+
26
+ The test that should have caught it asserted the opposite of the truth. It checked that
27
+ `gates` was present and that `preCommit` and `prePush` were **non-empty**, and read
28
+ non-empty as correct — so the test about manifests being right passed on a manifest
29
+ declaring four gates nobody could run.
30
+
31
+ ## Ruling
32
+
33
+ **1. `buildManifest` requires a capability from every caller.** The filtering happens in one
34
+ place, and that place is reached only with the evidence it filters on. A caller that passes
35
+ no `capability` is a caller asking for the defaults, and today it gets them — which is
36
+ correct for a pure function and wrong for a writer. Rather than make the parameter
37
+ mandatory and break the three call sites that legitimately want defaults, the ruling is on
38
+ who writes: **a command that writes a manifest for a repo that has none must probe first**,
39
+ because it is making a promise on that repo's behalf.
40
+
41
+ **2. A command that writes a manifest declares nothing it cannot prove.** This is ADR-017
42
+ ruling 1 with its attribution removed, and it covers a shape ADR-017 did not anticipate:
43
+ a writer that is not an adoption command. `skills install` is not adopting the repo. It
44
+ pins one file into `.agents/skills/`. Writing a full manifest was already the wrong shape
45
+ for it and was ruled on once already; the record for that ruling said *"a `{localSkills}`
46
+ stub would read as authoritative while omitting `gates`"*, and the fix was to write the full
47
+ canonical shape. The reasoning was right and the conclusion drawn from it was wrong, because
48
+ canonical shape and default gates are different things and only one of them is a fact about
49
+ the file's schema.
50
+
51
+ **3. A manifest that declares no gates is a valid answer.** A repo with no scripts and no
52
+ formatter gets `preCommit: []` and `prePush: []`. ADR-017's consequences section already
53
+ ruled this — `init` has been writing it — so the only new fact is that a reader of the
54
+ record can now see the invariant is not `init`'s to keep.
55
+
56
+ **4. The test asserts the manifest is true, not that the field is present.** The new test
57
+ runs `unrunnableGates(manifest, await readCapability(root))` — the function `check` runs,
58
+ with the probe `check` uses — and requires an empty result. It also runs against a fixture
59
+ that *can* run gates and requires them to be there, because a fix that hardcoded two empty
60
+ lists would pass the first half and arm the project with no gates at all.
61
+
62
+ ## Consequences
63
+
64
+ `test/install.test.mjs`'s existing assertion changed from
65
+ `assert.ok(manifest.gates.preCommit.length > 0 && …)` to
66
+ `assert.deepEqual(manifest.gates, { preCommit: [], prePush: [] })`. That is a test whose
67
+ pass condition was the defect, and it is the fourth in this repository to read presence as
68
+ correctness — the shape ADR-030 exists to catch.
69
+
70
+ Three mutations were applied and reverted, each caught by its own test: the `capability`
71
+ argument removed; a hand-built empty capability substituted for the probe (caught by the
72
+ capable half, not the bare one); and the capable half's non-empty assertion deleted — which
73
+ the bare half alone did not notice, confirming both halves earn their place.
74
+
75
+ ## Rejected
76
+
77
+ - **Make `capability` a required argument of `buildManifest`.** Three callers legitimately
78
+ want the unfiltered defaults, including the tests that exercise `planGates` directly. A
79
+ signature that cannot be called wrongly is worth having, and the wrong call here is one of
80
+ two lines in a function whose purpose is obvious from its name; ruling on the caller keeps
81
+ `buildManifest` a pure function that a test can call with any evidence it likes.
82
+ - **Have `skills install` write a `{localSkills}`-only manifest after all.** That was ruled
83
+ on and the ruling holds: a partial manifest reads as authoritative while omitting `gates`,
84
+ and `init` would then have to be run to complete it. Probing is the smaller change.
85
+ - **Let `check` repair it, since `check` already reports the finding.** `check` reports
86
+ findings; it is not the command a reader runs before `skills install` has already blocked
87
+ their commits. The landmine has to be defused at the moment it is laid.
88
+ - **Leave ADR-017 as written and put the generalisation only in the code.** The code is
89
+ where the second writer is, and a record that names `init` is a record a reader trusts to
90
+ have already covered the case. ADR-017's own context section says nothing else owns the
91
+ manifest — that was true in 2026-10-06 and this is what made it false.
92
+ - **Amend ADR-017's ruling 1 in place.** ADR-017 is accepted and its context is the evidence
93
+ for why the gate filter exists at all. Rewriting the sentence would leave no trace that the
94
+ rule was ever narrower than it reads, which is the same trick the gate list itself was
95
+ performing on its reader.
@@ -0,0 +1,101 @@
1
+ # ADR-039 — A gate a CI step runs must have a form that can fail, and a refusal is a failure
2
+
3
+ Status: **accepted** · 2026-10-08 · affects `ci`, its own workflow copy, and any command a workflow runs
4
+
5
+ ## Context
6
+
7
+ Review 0002's S23 found three things about `simt-dev ci` and the step this repository's own
8
+ workflow runs. All three reproduced against the shipped binary, and the first is the reason
9
+ this record exists.
10
+
11
+ **The own gate's own copy is checked by a step that cannot fail.** `.github/workflows/simt-dev-check.yml`
12
+ carries a step named "the gate copy in this repo is current", with the comment *"the drift
13
+ this exception exists to prevent, checked where it would be caught"*, running
14
+ `node bin/simt-dev.mjs ci`. With a hand edit appended to the copy, `ci` printed:
15
+
16
+ ```
17
+ file .github/workflows/simt-dev-check.yml
18
+ action refresh
19
+ why this repository is simt-dev's own, and the gate template has changed since this copy was written
20
+ ```
21
+
22
+ and exited **0**. The step read the drift it was written to catch, printed it, and passed.
23
+ That is the shape this repository's own rules keep warning about — a control that appears to
24
+ hold and does not — except that here the control was the one enforcing the control.
25
+
26
+ **`ci --json --dry-run` printed nothing.** The JSON branch was guarded by
27
+ `flags.yes !== true && flags.dryRun !== true`, so the one combination a script reaches for
28
+ was the one combination with no document on stdout at all.
29
+
30
+ **A refusal reported success.** Run in a repository that adopted simt-dev, `ci` explains at
31
+ length that the `.github` file is the repository's own, names `simt-dev init --yes` as the
32
+ remedy, and then emits `{"ok": true, …, "own": false}` and exits **0**. A caller reading `ok`
33
+ is told the request succeeded, one line above a paragraph saying it was refused.
34
+
35
+ ## Ruling
36
+
37
+ **1. A command whose output a CI step consumes has a form that can fail, and that form is
38
+ what the step runs.** Previewing by default is right for a human at a terminal and wrong for
39
+ a gate: a step has no reader to hand the plan to. `ci --check` exists for that and never
40
+ writes, whatever else it is given — a check that repairs is a second writer, which ADR-036
41
+ is about.
42
+
43
+ **2. A check that names drift shows the drift.** `--check` prints a unified diff of both
44
+ files. The reader is the author of the template change that made the copy stale, and the
45
+ question they have is "what did I change". The plan now carries both sides of the
46
+ comparison — `source: { text, desired }` — so the verdict and the diff cannot disagree, and
47
+ those two bodies are stripped from the JSON payload, because a document is not the place
48
+ for two copies of a workflow file.
49
+
50
+ **3. A refusal is `EXIT.UNSUPPORTED` with `ok: false`.** Not because nothing is wrong, but
51
+ because the request cannot be satisfied *in this repository*, which is what the code is
52
+ documented to mean (`output/index.mjs:16`). The prior behaviour also contradicted ADR-035
53
+ directly: it emitted `ok: true` and exited 0 while the text beside it said the opposite, and
54
+ ADR-035 requires the two to agree. This is the first place in the tool where they
55
+ disagreed by design rather than by oversight.
56
+
57
+ **4. `--json` emits one document in every mode, and `ok` agrees with the exit code.** The
58
+ mode guard was the bug; the invariant is ADR-035's. The test walks the modes rather than
59
+ trusting the guard.
60
+
61
+ ## Consequences
62
+
63
+ `test/ci.test.mjs` gains a `run()` helper that reports the exit code instead of rejecting.
64
+ `promisify(execFile)` rejects on non-zero, which is right for a helper asserting success and
65
+ useless for every test about a command that must fail — and the code a refusal test needs to
66
+ check is the code inside the rejection.
67
+
68
+ The existing refusal test asserted exit 0 and carried the reasoning *"the refusal must not
69
+ be an error, because nothing is wrong"*. That reasoning is this record's third ruling
70
+ overturned, so the comment now says why the opposite holds.
71
+
72
+ The `--check` and `--json` tests run **in this repository on a stale copy**, with a `finally`
73
+ that restores the file, because `isOwnRepo` compares against the package root and no temp
74
+ repo can reach this code. The first version of the JSON test used a fixture and passed with
75
+ the guard restored: the fixture was not the own repository, so every mode took the refusal
76
+ path and printed a perfectly good document — for the wrong reason. A mutation found it.
77
+
78
+ Four mutations were applied and reverted, each caught by its own test: `--check` returning
79
+ `EXIT.OK`, the refusal returning `EXIT.OK`, the mode guard restored, and the workflow
80
+ template reverted to the bare `ci`.
81
+
82
+ ## Rejected
83
+
84
+ - **Leave the own gate's step running `ci` and rely on the reader.** The reader is a person
85
+ who did not make the template change and will not know that the step passed. A gate whose
86
+ failure is visible only to someone reading logs is a note.
87
+ - **Make `ci` itself exit non-zero on drift, without a `--check`.** That is what the review
88
+ asked for and it would break ADR-036: previewing by default is the rule every writer
89
+ follows, and one writer that reports drift is the exception nobody remembers.
90
+ - **Keep the refusal at exit 0 and change only `ok` to false.** Two answers to one question
91
+ is the defect ADR-035 exists to prevent, and the more surprising half of the two: the
92
+ shell reads the code, and it was reading success.
93
+ - **Exit `EXIT.FAIL` (1) for the refusal.** It is not a gate that failed; nothing ran. `1`
94
+ means "a gate failed: drift, policy violation, missing evidence", and a caller deciding
95
+ whether to re-run on `1` would retry a request that can never succeed in this repository.
96
+ - **Ship the diff as a dependency rather than writing it.** ADR-012 rules the dependency, and
97
+ the diff here has one reader and one shape: two whole files this tool wrote, differing by
98
+ lines. A line-level LCS table is a screen and a risk, not a dependency.
99
+ - **Print the whole file rather than eliding context.** The output is the only reason the
100
+ step is worth having over a bare exit code, and fifty lines of context around one changed
101
+ step buries it.
@@ -0,0 +1,158 @@
1
+ # ADR-040 — The last writer that wrote by default was not the one named
2
+
3
+ Status: **accepted** · 2026-10-08 · affects `simt-dev hooks install`, every remedy naming it, and the shim it installs
4
+
5
+ ## Context
6
+
7
+ ADR-035 ruled that `writeMode(flags)` decides whether a run may write, "and every mutating
8
+ command reads it. A command that consults `flags.yes` directly is a defect whether or not its
9
+ arithmetic happens to agree today". ADR-036 then took `sync` out of the set of writers that write
10
+ by default. Review 0002's S24 opened by calling `sync` "the only writer that writes by default"
11
+ and named its five siblings as the ones that "all preview first": `init`, `upgrade`, `ci`,
12
+ `hooks install` and `skills install`.
13
+
14
+ Four of those five were true. One was not.
15
+
16
+ Measured against the shipped binary, in a fresh repository that declares `lint` and `test`:
17
+
18
+ ```
19
+ $ simt-dev hooks install --root .
20
+ repo .
21
+ core.hooksPath created (unset) -> .simt-hooks
22
+ CREATED .simt-hooks/pre-commit
23
+ CREATED .simt-hooks/pre-push
24
+ WROTE shims and set core.hooksPath
25
+ $ echo $?
26
+ 0
27
+ ```
28
+
29
+ `hooks.mjs` read `flags.yes` at line 146 and `flags.dryRun` at line 169 and imported neither
30
+ `writeMode` nor any other write decision. In the ordinary case — no `core.hooksPath`, no foreign
31
+ hook — neither flag was consulted at all and the shims were written.
32
+
33
+ Six independent sources agree this is a defect rather than an exemption:
34
+
35
+ 1. ADR-035's ruling names the exact pattern as a defect.
36
+ 2. The code is the pattern.
37
+ 3. S24's premise, which is the finding the rest of Wave 0 was ordered against, was wrong on one
38
+ of six.
39
+ 4. Review 0002's own migration script instructs: "**Route hooks.** `simt-dev hooks install`
40
+ (plan), then `--yes`." An operator following it would read a plan and get a write, and the
41
+ `--yes` on the next line would be spent on nothing.
42
+ 5. Three **shipped** strings assumed the preview: `init`'s remedy, two `compare` remedies, and
43
+ the `Reinstall with:` comment inside the shim itself, which is written into every adopting
44
+ repository.
45
+ 6. `contract.test.mjs`'s `WRITERS` table covered `upgrade`, `sync` and `skills install`. It never
46
+ covered `hooks install`, which is why 586 tests did not notice.
47
+
48
+ Six is not an accident and it is not a subtle shape. It is what happens when a rule about a set of
49
+ commands is checked by reading the commands one at a time.
50
+
51
+ The comment `sync.mjs` gained in ADR-036 is part of the evidence. It reads "the odd one out among
52
+ six: `init`, `upgrade`, `ci`, `hooks install` and `skills install` all preview unless told
53
+ otherwise" — a claim about code a different file owns, one commit out of date, which this record
54
+ corrects.
55
+
56
+ ## Ruling
57
+
58
+ **1. `hooks install` reads `writeMode(flags)`, like every other writer.** `apply` gates the write
59
+ and the `core.hooksPath` assignment together, so there is no half-installed state reachable by a
60
+ flag combination.
61
+
62
+ **2. The preview exits `EXIT.OK` and names the command that applies it.** A rehearsal is not a
63
+ failure: `upgrade`, `ci` and `skills install` previews all exit 0, and a preview that exits
64
+ non-zero would make `--dry-run` unusable in a script for no gain. This is deliberately **not**
65
+ `sync`'s rule, where a preview that found work exits 1 because plain `sync` is also a check
66
+ (ADR-036). `hooks install` is not a check; `hooks status` is, and it says so through its own exit
67
+ code.
68
+
69
+ **3. `--yes` is one flag doing two jobs here, and that is not a reason to keep two.** It
70
+ authorises the write *and* it is the consent ADR-018 requires to take over routing git is already
71
+ using. A bare command is refused in the takeover case and merely previews in the ordinary one, so
72
+ the consent is required exactly where it was before and the write is now gated by the same word.
73
+ The BLOCKED text needed no change: it already ended "then re-run with `--yes` to accept".
74
+
75
+ **4. Every string that names the command moves with it.** `init`'s remedy, both `compare`
76
+ remedies, the shim's own `Reinstall with:` comment, the help synopsis, and the ADR-036 comment
77
+ that misdescribed it. This is ADR-036's own rule — "a writer that writes says so, and the
78
+ instructions naming it move with it" — applied to the second writer rather than the first.
79
+
80
+ **5. A remedy is a guard, and the guard covers both writers.** `shipped.test.mjs`'s "every remedy
81
+ names a command that does what the remedy claims" now scans `simt-dev hooks install` as well as
82
+ `simt-dev sync`. The assertion had to be rewritten to be worth having: the original required the
83
+ authorising flag immediately after the command, which would have reported `simt-dev hooks install
84
+ --root <path> --yes` — the shape `init` prints — as an offender. It searches the rest of the line
85
+ instead, stopping at the next `simt-dev` so a correct remedy cannot vouch for a bare one later on
86
+ the same line, and it skips comment lines because prose about the tool is not an instruction to
87
+ run it.
88
+
89
+ **6. An unauthorised run that finds work is either a rehearsal or a refusal, and the exit code is
90
+ the only thing that says which.** This is a change to the `WRITERS` table, not to `hooks install`.
91
+ Asserting only the bytes passes on a command that **refuses** rather than previews, because a
92
+ refusal changes nothing. `hooks install` on a repo with no runnable gate is refused exactly that
93
+ way, and the refusal is correct: a shim with an empty gate list enforces nothing yet merely
94
+ existing replaces whatever git was running. The row therefore carries `stack: 'node'` and the
95
+ table asserts the exit code of every unauthorised run — 0 for a rehearsal, non-zero only for the
96
+ row (`sync`) that is also a check. The old `if (writer.name === 'sync')` special case became a
97
+ `previewFindsWork` column, because a table that encodes one row's name in the loop is a table that
98
+ cannot get a new row right.
99
+
100
+ The assertion is scoped to the unauthorised form. A rehearsal is a different question:
101
+ `sync --yes --dry-run` reports the work it found exactly as the unauthorised form does (ADR-036),
102
+ and requiring 0 there would fail on behaviour that record deliberately settled. **That was caught
103
+ by a mutation, not by reading it** — the first version asserted the exit code in both modes and
104
+ failed the whole table.
105
+
106
+ ## Consequences
107
+
108
+ `hooks install` becomes the fourth row in `contract.test.mjs`'s `WRITERS` table, and its
109
+ `verify` fingerprints **both** things it writes: the shim under `.simt-hooks/` and
110
+ `core.hooksPath` inside `.git/config`. The second is not decoration. A preview that set the config
111
+ without writing the shims would leave a repository whose commits are gated by a hook git does not
112
+ run, which is the same silent downgrade ADR-018 exists to prevent — and a shim-only fingerprint
113
+ does not notice it. Removing the config probe makes that mutation pass; that was measured rather
114
+ than assumed.
115
+
116
+ Six mutations were applied and reverted, each caught by its own test:
117
+
118
+ | Mutation | Caught by |
119
+ | --- | --- |
120
+ | the `!mode.apply` gate removed — always write | the `WRITERS` row |
121
+ | the preview drops the "apply with:" clause | `hooks install: without --yes it prints the plan and the command that applies it` |
122
+ | `init`'s remedy loses `--yes` | `every remedy names a command that does what the remedy claims` |
123
+ | the shim's `Reinstall with:` loses `--yes` | the same guard, and `the installed shim tells the reader how to reinstall it` |
124
+ | the `WRITERS` row pointed at the python fixture | the new exit assertion — *"hooks install exited 1 instead of previewing"* |
125
+ | the preview half-writes `core.hooksPath` | the `WRITERS` row, and **not** without the config probe |
126
+
127
+ A seventh mutation is worth recording because it was **not** caught by the new row: `hooks
128
+ install --yes` that never sets `core.hooksPath` passes `contract.test.mjs` entirely, because the
129
+ row only exercises unauthorised and rehearsed runs. Six tests in `hooks.test.mjs` catch it. The
130
+ row does not carry that claim and this record does not claim it does.
131
+
132
+ Two of the new assertions were wrong before they were right. The exit-code assertion was, as
133
+ above. The `sync` row's own preview also exits 1 in `--dry-run`, which is the same finding seen
134
+ from the other row. Two of two wrong first is not a coincidence worth explaining: a table that
135
+ grows a new column needs each cell written down, and neither cell was.
136
+
137
+ ## Rejected
138
+
139
+ - **Treat `hooks install` as exempt because ADR-018 already asks for consent.** ADR-018's consent
140
+ is about *taking over* routing git already uses. In the ordinary case there is nothing to take
141
+ over, so the consent requirement never fired — and the write happened anyway. An exemption
142
+ invented to fit a defect is indistinguishable from a decision, which is why this one would have
143
+ needed a reason and there is not one.
144
+ - **Leave `--yes` as takeover-only and add a second flag for the write.** Two flags for one
145
+ decision is ADR-035's defect restated: a command reading `flags.a` for one thing and
146
+ `flags.b` for another is how `upgrade` read `yes` and ignored `dryRun`. The word "yes" is also
147
+ what the reader expects, and every remedy already says `--yes`.
148
+ - **Make the preview exit 1 when it finds work, matching `sync`.** Defensible, and it would break
149
+ `hooks install` in the one script shape that uses it: read the plan, then decide. `hooks status`
150
+ is the command for "is this repo wired up", and it already reports drift through its exit code.
151
+ - **Only add the `WRITERS` row and leave the shipped strings.** The strings are the defect's
152
+ widest blast radius. A remedy that prints a command which no longer reinstalls is ADR-036's
153
+ exact failure, one writer later, and `Reinstall with:` lives inside the file that gets installed
154
+ into every adopting repository.
155
+ - **Rewrite `sync.mjs`'s ADR-036 comment by deleting the sentence.** The sentence is the record of
156
+ why that change was made. It is amended instead, with the correction stated, because a comment
157
+ that enumerates which commands follow a rule is a claim about code this file does not own and
158
+ the next reader has no way to check it cheaply.
@@ -0,0 +1,92 @@
1
+ # ADR-041 — The suite installs the package in order to test it
2
+
3
+ Status: **accepted** · 2026-10-08
4
+ **Affects:** `test/pack.test.mjs` and what the suite may assume about `npm`.
5
+
6
+ ## Context
7
+
8
+ Every other file in `test/` imports from `../src`. That is what they are for: they are tests of
9
+ units, and a unit test cannot see a file that `package.json` forgot to ship. S01 shipped a
10
+ package whose `files` list and whose runtime reads disagreed, and the suite was green — because
11
+ every test in it was reading the checkout rather than the artefact.
12
+
13
+ Review 0001 §S20 named five missing test families. Four have since landed: the JSON and
14
+ write-mode contract table (S10), the git-state hook tests (S08), the resolver tests (Wave 1) and
15
+ the MCP parity work in progress. The first family on the list, pack-and-install end to end, was
16
+ still open, and review 0002 §5 says plainly that it "is the one that would have caught S01, S27
17
+ and S35 before they shipped; land it first, with S01". S01 had already been closed. So the row
18
+ that was supposed to be carried by it was never carried at all.
19
+
20
+ Two facts about this repository make that gap harder to notice than it looks. `test/` may not
21
+ reach outside the checkout (ADR-031), and this package has no dependencies (ADR-012). Together
22
+ those mean the suite has no installed copy of itself to test, and the natural workaround —
23
+ borrowing a formatter from a neighbouring checkout — is the one ADR-031 rules out. Prettier 3.8.1
24
+ is sitting in `../repo-06/node_modules/.bin` and is not available here.
25
+
26
+ ## Ruling
27
+
28
+ **One.** `test/pack.test.mjs` drives `node_modules/.bin/simt-dev`, installed from a tarball that
29
+ `npm pack` produced from the working tree. Anything a reader of this repository only learns from
30
+ the published package — that the registry resolves out of `node_modules`, that the decisions are
31
+ present to read, that a full adoption survives a commit and a hook — is asserted here and nowhere
32
+ else.
33
+
34
+ **Two.** The install is `npm install --offline`. That is a constraint on this repository, not a
35
+ setting: the package must install with no network and no warm cache, so `dependencies` and
36
+ `devDependencies` stay empty and adding a build-time or test-time tool to either of them is a
37
+ change to a guarantee rather than a convenience. A suite that fails on a cold runner gets
38
+ skipped on one, and a skipped test and a passing test are the same line of output.
39
+
40
+ **Three.** The pack test runs `npm pack` against the working tree rather than against a staged
41
+ or committed tree. A mutation test has to be able to change a byte in `src/` and watch this file
42
+ notice, and packing a commit would hide the change it is supposed to catch.
43
+
44
+ **Four.** This file does not assert that the projected output is formatted, because there is no
45
+ formatter to assert it with. S29's second acceptance half — a packed install whose output passes
46
+ `format:check` — therefore stays open, and the seam it would have caught is asserted
47
+ structurally instead, in `test/markers.test.mjs`. The record is here so that the open half is a
48
+ decision rather than an oversight, and so that whoever makes Prettier reachable does not have to
49
+ rediscover why this file stops where it stops.
50
+
51
+ ## Consequences
52
+
53
+ Four mutations were made against the four rulings, each reverted immediately, and each was caught
54
+ by the test that claims the behaviour:
55
+
56
+ | Mutation | Caught by |
57
+ | --- | --- |
58
+ | `"graph"` removed from `package.json` `files` | `the registry the installed tool reads comes out of node_modules`, plus three others |
59
+ | `"docs/decisions"` removed from `files` | `the decisions the tool explains itself with are in the tarball` |
60
+ | `planGates` stops filtering by capability | `a repo that cannot run a gate is not told it has one` |
61
+ | `upgrade` and `hooks install` write without `--yes` | `a packed install survives the whole adoption, and no writer writes unasked` |
62
+
63
+ The first two are the S01 class and they shipped green for a long time. The third is S27. The
64
+ fourth is ADR-035 and ADR-036, and it is in the list because `hooks install` was found by hand the
65
+ day before, not by any of these tests.
66
+
67
+ One thing this file deliberately does not assert, having been measured while writing it: the
68
+ `files` list in `package.json` names four directories that do not exist in this repository —
69
+ `skills`, `agents`, `tooling` and `hooks`. npm ignores them without complaint and the tarball is
70
+ complete. An assertion that each entry exists would pass the moment the directories are created
71
+ and say nothing in the meantime, so the fact is recorded here rather than encoded as a check.
72
+
73
+ ## Rejected
74
+
75
+ **Running the installed binary out of a staged copy of `src/`.** It reproduces the checkout's
76
+ bytes under a different path, which is precisely the state S01 shipped in. A test that cannot
77
+ distinguish the two is worse than no test.
78
+
79
+ **Adding Prettier as a devDependency so the formatting assertion can be written.** It would
80
+ make the assertion possible and the suite unreliable: `npm install` would need the network on a
81
+ cold runner, and ruling two exists because that is not a cost this repository pays. The
82
+ correct fix is a formatter the suite can already reach, which is a decision somebody should make
83
+ deliberately rather than a side effect of this one.
84
+
85
+ **Skipping the pack test when `npm` is unavailable.** Every other test in the suite already needs
86
+ node, git and a POSIX shell; npm is in the same class. A conditional skip converts a green run
87
+ into an unverified one, which is the failure this file was written to stop.
88
+
89
+ **Copying Prettier out of a sibling checkout.** It exists, it works, and ADR-031 rules it out for
90
+ the same reason it rules out reading skill bodies from `../repo-14`: a suite that passes on
91
+ the author's machine and fails on a runner with only this checkout is evidence about the author's
92
+ directory.
@@ -0,0 +1,94 @@
1
+ # ADR-042 — A declared glob must match a path built from it
2
+
3
+ Status: **accepted** · 2026-10-08 · ruling 1 superseded by ADR-053
4
+ **Affects:** `src/resolve/resolve.mjs` and `graph lint`.
5
+
6
+ ## Context
7
+
8
+ `matchesGlob` rewrote one shape of double star and left the rest to the JavaScript regex engine:
9
+
10
+ ```js
11
+ .replace(/\/\*\*/g, '(?:/.*)?')
12
+ ```
13
+
14
+ A glob that **started** with a double star compiled to an anchored pattern beginning `**`, which is
15
+ not a valid regex. A `try`/`catch` returned `false`, so the failure was indistinguishable from the
16
+ glob not matching — which is what a glob is for. 13 of the registry's 65 globs start that way
17
+ (`**/*.test.ts`, `**/*.kt`, `**/build/**`, and ten more), and path is the heaviest signal the
18
+ resolver has at weight 100. Every one of those skills was unreachable by a diff, and the only
19
+ evidence was a staged `.spec.ts` file producing a selection that did not include the skill for
20
+ writing tests.
21
+
22
+ Two facts made this durable rather than a slip. The registry is hand-authored, so nothing compiles
23
+ a glob until a real diff happens to contain a matching path. And `graph lint` — the check that
24
+ exists to keep the registry describing reality — asked only whether a path array was **present**.
25
+ `checkStack` had already established the principle one field over: a tag `fingerprint()` cannot emit
26
+ matches nothing, forever, with no error at resolution time, so lint rejects it. A glob that matches
27
+ nothing is the same failure wearing a different field name, and no check asked.
28
+
29
+ Review 0001 §S02 diagnosed both halves and proposed the segment-by-segment translation. Review 0002
30
+ §4.1 noted that with a raised Node floor this becomes `path.matchesGlob`, which is one line instead
31
+ of twenty. That floor is proposal P8 and S47, still queued; `engines.node` reads `>=20` today and
32
+ using a built-in that needs `>=22.20` would have quietly broken the declared range. So the translation
33
+ is hand-written here and becomes a one-liner when S47 lands.
34
+
35
+ ## Ruling
36
+
37
+ **One.** A glob is translated segment by segment. A leading double star is any number of leading
38
+ directories, a middle one is any number of middle ones, a trailing one is anything beneath, and a
39
+ bare one is everything. `*` remains confined to a single segment, because that is what makes the
40
+ double star worth translating rather than treating every star alike.
41
+
42
+ **Two.** A glob that matches nothing is a lint error, not a zero. `graph lint` now builds paths from
43
+ each declared glob and requires the glob to match every one of them. This is the same question the
44
+ resolver asks, asked at authoring time by the only tool in this repository that sees the whole
45
+ registry at once.
46
+
47
+ **Three.** A check that reads the same generator it is validating cannot catch that generator
48
+ misbehaving. The synthetic expansion is asserted in its own right — a glob claiming any depth must
49
+ be shown a path at depth and a path not at depth — because the registry-wide test otherwise just
50
+ checks fewer paths when the expansion is narrowed, and checks fewer without saying so.
51
+
52
+ **Four.** `?` is escaped, not read as a single-character wildcard. Nothing in the registry uses it,
53
+ and reading it as a quantifier made `src/a?.ts` match `.ts`. A glob the matcher cannot honour now
54
+ fails ruling two loudly instead of matching the wrong paths.
55
+
56
+ ## Consequences
57
+
58
+ | Mutation | Caught by |
59
+ | --- | --- |
60
+ | a leading double star translated as a trailing one | `** in each position is anchored the way a path segment is` |
61
+ | the synthetic expansion narrowed to the empty case only | `a double star is expanded both ways, or the check above is a mirror` |
62
+ | `checkGlobs` deleted | `lint: detects a path glob that matches nothing (S02)` |
63
+ | the path signal dropped from `scoreSkill` | `a path signal now reaches the skills that declare a leading **` |
64
+
65
+ The second mutation is the one worth reading twice. The registry-wide test passed with it — it reads
66
+ the generator it is checking, so narrowing the generator narrows the test in step. Only the lint
67
+ rule noticed, and only because a bare double star then expanded to nothing at all. Ruling three is
68
+ the fix, and it exists because the obvious assertion was not enough.
69
+
70
+ Measured end to end through the shipped binary: in a Node repo with one staged `src/other.spec.ts`,
71
+ `complete-testing` now resolves with the reason `path:**/*.spec.ts`. Before this record it did not
72
+ resolve at all. `graph lint` over the real registry reports 49 unique globs and 0 unreachable.
73
+
74
+ ## Rejected
75
+
76
+ **Waiting for S47 and using `path.matchesGlob`.** It is the better implementation and it is one line.
77
+ It is also unavailable under the `engines.node` this package currently declares, and a resolver fix
78
+ that breaks a stated runtime range is a worse defect than the one it removes. When the floor moves,
79
+ this file's ruling one collapses into that call and the rest stands unchanged.
80
+
81
+ **Catching the bad glob at match time instead of lint time.** A `throw` in `matchesGlob` converts one
82
+ silent zero into a loud crash for whoever happened to stage the right file first — a user, in a
83
+ repository, with no pointer to the hand-authored registry field that caused it. The same fact is
84
+ available to `graph lint`, where the remedy is a file and a line number.
85
+
86
+ **Extending the glob language to cover brace and character-class patterns.** `matchesGlob` is called
87
+ with `skill.paths` and nothing else; the registry declares no braces and no character classes, and
88
+ every one added is a rule that must hold for every future glob with no second caller to check it
89
+ against. Ruling four turns the gap into a lint error instead, which is the honest description of a
90
+ field the matcher does not implement.
91
+
92
+ **Testing only the translation, as review 0001's acceptance described.** Unit cases for leading,
93
+ middle, trailing and bare double stars all pass while a declared glob nobody staged still matches
94
+ nothing. The unit tests prove the function; the lint rule is what proves the data.
@@ -0,0 +1,90 @@
1
+ # ADR-043 — A keyword is a phrase, and two letters is a word
2
+
3
+ Status: **accepted** · 2026-10-08
4
+ **Affects:** `src/resolve/resolve.mjs` and what `graph lint` rejects.
5
+
6
+ ## Context
7
+
8
+ Review 0001 §S03, marked P1, reproduced exactly. Two independent filters in `scoreSkill` were
9
+ between a task description and every skill it named.
10
+
11
+ `tokenize` dropped tokens of length 2 or less. In this domain those are not noise: `d1`, `r2`, `kv`,
12
+ `ci` and `ui` are the vocabulary, and the capability aliases `ui`, `r2`, `kv` and `d1` were dead for
13
+ that reason alone. `tokenize("fix d1 migration")` returned `fix, migration` — the one word that
14
+ identifies the change was removed from the sentence before anything read it.
15
+
16
+ Keywords were then compared one task token at a time. A multi-word keyword could therefore never
17
+ match, because no single token equals `"durable object"`. Thirteen of the registry's 232 unique
18
+ keywords were unreachable, nine of them multi-word, and `durable object` — declared on
19
+ `durable-objects`, `cloudflare` and `api-security-review` — was dead on all three. Review 0001 §4
20
+ records the symptom: `skills find "durable object"` returned four matches and not one `keyword:`
21
+ reason.
22
+
23
+ `graph lint` reported the registry clean. It checked that `keywords` was non-empty, which a list of
24
+ words the tokenizer discards satisfies.
25
+
26
+ ## Ruling
27
+
28
+ **One.** The tokenizer keeps words of length 2. The floor was 3 and it was chosen as a noise filter
29
+ in a domain where the two-letter words are the signal. Lowering it is only half a fix; the noise it
30
+ admits is the subject of S04 and is measured below rather than absorbed here.
31
+
32
+ **Two.** A declared keyword is matched as a contiguous phrase, not token by token. Adjacency is the
33
+ whole of the rule, so there is no similarity and no model: `"durable storage object"` does not match
34
+ `"durable object"` and `"a durable object"` does.
35
+
36
+ **Three.** The `keyword:` reason quotes the declaration as written. `keyword:durable object` is a
37
+ string a reader can look up in the registry; `keyword:durable` is a keyword nobody declared.
38
+
39
+ **Four.** A keyword whose words the tokenizer all discards is a lint error. This is the same
40
+ question `checkGlobs` asks of a path (ADR-042), added for the same reason: a hand-authored field
41
+ costs nothing to declare, so only a check that speaks the resolver's language catches one nobody
42
+ executed.
43
+
44
+ ## Consequences
45
+
46
+ Both acceptance cases named in review 0001 §S03 are now asserted against the shipped binary rather
47
+ than against a function's return value, because both are claims about what a reader sees:
48
+ `skills find "durable object"` prints `keyword:durable object`, and `skills brief --task "d1
49
+ migration"` prints `keyword:d1`.
50
+
51
+ Five mutations, each reverted, each caught by the test that claims it:
52
+
53
+ | Mutation | Caught by |
54
+ | --- | --- |
55
+ | the tokenizer floor put back to 3 | five tests, including both acceptance cases |
56
+ | phrase matching reverted to token equality | `a multi-word keyword matches as a phrase`, plus `skills find` |
57
+ | `hasPhrase` becomes a bag of words | `a phrase needs its words adjacent` |
58
+ | `checkKeywords` deleted | `lint: detects a keyword no task can produce` |
59
+ | the reason names a token rather than the declaration | both phrase tests, plus `skills find` |
60
+
61
+ ### The noise this admits, measured
62
+
63
+ Keeping two-letter words lets six of them reach the summary signal: `an`, `in`, `on`, `of`, `so`, and
64
+ `or`, which selects eleven skills on its own. That is real and it is S04's finding, which was filed
65
+ against the old floor and is still open.
66
+
67
+ The measurement that keeps this record honest is the other side of it: `the` selected eleven skills
68
+ and `and` selected twenty-nine **before** this change. A `summary:` hit is weight 8, and any score
69
+ above 0 selects, so the threshold was already low before a single stopword was admitted. S03 adds
70
+ six offenders to a pre-existing problem rather than creating it, and the remedy is the one review
71
+ 0001 named — a stopword list and a minimum score — not a longer tokenizer floor. Restoring the floor
72
+ to 3 would hide the problem by making `d1` disappear again.
73
+
74
+ ## Rejected
75
+
76
+ **Restoring the length-3 floor and declaring the two-letter aliases dead instead.** It would leave the
77
+ suite quieter and `d1` unselectable, which is the defect this record exists to end. The word is in
78
+ the domain; the filter is what was wrong.
79
+
80
+ **Matching multi-word keywords as an unordered bag of words.** Cheaper, and it matches
81
+ `"durable storage object"`. It also makes `google-services.json` a keyword reachable by any task
82
+ merely mentioning Google, which is not what the declaration means.
83
+
84
+ **Tokenizing the task and then searching the joined string for the keyword.** Same adjacency, but it
85
+ loses the `start` offset a phrase needs and re-tokenizes a task the caller already tokenized. The
86
+ keyword is a phrase over the same token stream, so the check belongs on the stream.
87
+
88
+ **Widening `tokenize` to keep single letters as well.** `c`, `x` and `r` appear in skill names as
89
+ initialisms, and every one of them is a stopword or a typo in a task description. Two is the point
90
+ below which a word stops being a word.