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.
- python/simt_dev/__init__.py +32 -0
- python/simt_dev/cli.py +225 -0
- simt_dev/__init__.py +32 -0
- simt_dev/cli.py +225 -0
- simt_dev/vendored/CHANGELOG.md +166 -0
- simt_dev/vendored/bin/simt-dev.mjs +28 -0
- simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
- simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
- simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
- simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
- simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
- simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- 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
- simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- 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
- simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- simt_dev/vendored/docs/decisions/README.md +530 -0
- simt_dev/vendored/graph/skills.graph.json +1622 -0
- simt_dev/vendored/package.json +43 -0
- simt_dev/vendored/rules/core.md +76 -0
- simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
- simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
- simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
- simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
- simt_dev/vendored/rules/stacks/android.md +63 -0
- simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
- simt_dev/vendored/rules/stacks/connectiq.md +62 -0
- simt_dev/vendored/rules/stacks/container.md +31 -0
- simt_dev/vendored/rules/stacks/jvm.md +54 -0
- simt_dev/vendored/rules/stacks/node.md +45 -0
- simt_dev/vendored/rules/stacks/python.md +48 -0
- simt_dev/vendored/rules/stacks/react-native.md +58 -0
- simt_dev/vendored/rules/stacks/react-web.md +61 -0
- simt_dev/vendored/rules/stacks/typescript.md +21 -0
- simt_dev/vendored/rules/stacks/uv.md +32 -0
- simt_dev/vendored/src/argv.mjs +116 -0
- simt_dev/vendored/src/ci/dependabot.mjs +268 -0
- simt_dev/vendored/src/ci/workflow.mjs +360 -0
- simt_dev/vendored/src/commands/ci.mjs +285 -0
- simt_dev/vendored/src/commands/compare.mjs +195 -0
- simt_dev/vendored/src/commands/graph.mjs +73 -0
- simt_dev/vendored/src/commands/help.mjs +108 -0
- simt_dev/vendored/src/commands/hooks.mjs +471 -0
- simt_dev/vendored/src/commands/index.mjs +241 -0
- simt_dev/vendored/src/commands/init.mjs +632 -0
- simt_dev/vendored/src/commands/mcp.mjs +63 -0
- simt_dev/vendored/src/commands/rules.mjs +374 -0
- simt_dev/vendored/src/commands/skills.mjs +647 -0
- simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
- simt_dev/vendored/src/commands/sync.mjs +331 -0
- simt_dev/vendored/src/commands/upgrade.mjs +202 -0
- simt_dev/vendored/src/compare/discover.mjs +212 -0
- simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
- simt_dev/vendored/src/compare/row.mjs +299 -0
- simt_dev/vendored/src/decisions/index.mjs +536 -0
- simt_dev/vendored/src/graph/load.mjs +541 -0
- simt_dev/vendored/src/hooks/capability.mjs +129 -0
- simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
- simt_dev/vendored/src/hooks/gates.mjs +144 -0
- simt_dev/vendored/src/hooks/resolve.mjs +194 -0
- simt_dev/vendored/src/hooks/shim.mjs +140 -0
- simt_dev/vendored/src/hooks/where.mjs +118 -0
- simt_dev/vendored/src/identity.mjs +218 -0
- simt_dev/vendored/src/locate.mjs +75 -0
- simt_dev/vendored/src/mcp/server.mjs +167 -0
- simt_dev/vendored/src/mcp/tools.mjs +410 -0
- simt_dev/vendored/src/name.mjs +95 -0
- simt_dev/vendored/src/output/index.mjs +96 -0
- simt_dev/vendored/src/projection/baselines.mjs +209 -0
- simt_dev/vendored/src/projection/conflicts.mjs +205 -0
- simt_dev/vendored/src/projection/manifest.mjs +478 -0
- simt_dev/vendored/src/projection/markers.mjs +349 -0
- simt_dev/vendored/src/projection/mdc.mjs +327 -0
- simt_dev/vendored/src/projection/project.mjs +504 -0
- simt_dev/vendored/src/projection/rules.mjs +640 -0
- simt_dev/vendored/src/projection/surfaces.mjs +398 -0
- simt_dev/vendored/src/projection/targets.mjs +221 -0
- simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
- simt_dev/vendored/src/resolve/near.mjs +61 -0
- simt_dev/vendored/src/resolve/resolve.mjs +457 -0
- simt_dev/vendored/src/resolve/source.mjs +105 -0
- simt_dev/vendored/src/toolchain.mjs +202 -0
- simt_dev/vendored/src/tsconfig.mjs +314 -0
- simt_dev/vendored/src/upgrade/plan.mjs +203 -0
- simt_dev/vendored/src/workspace.mjs +289 -0
- simt_dev-0.2.0.dist-info/METADATA +150 -0
- simt_dev-0.2.0.dist-info/RECORD +138 -0
- simt_dev-0.2.0.dist-info/WHEEL +4 -0
- simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
- 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.
|
simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md
ADDED
|
@@ -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.
|