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,116 @@
|
|
|
1
|
+
# ADR-044 — Compatibility is not evidence, and a resolver nobody measured is merely felt
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects what `skills brief` and `skills find` return
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0001 §S04, marked P1, reproduced exactly through the shipped binary. Two facts, both
|
|
8
|
+
measured in a Workers fixture before anything was changed:
|
|
9
|
+
|
|
10
|
+
- `skills brief --task "fix d1 migration"` resolved 29 914 bytes against a 20 480-byte budget and
|
|
11
|
+
reported `(budget capped)`. The tail was `complete-testing` and `slice-commit`, selected on
|
|
12
|
+
`stack:node+cloudflare-workers` alone.
|
|
13
|
+
- `skills brief --task "fix a typo in the README"` resolved four skills, every one of them on
|
|
14
|
+
`summary:the` or `summary:in`, and filled the budget with bodies about launch galaxies.
|
|
15
|
+
|
|
16
|
+
The mechanism is one line. Stack overlap is 15 a tag, any score above zero selects, and the stack
|
|
17
|
+
gate already ran. Compatibility is a **precondition**, so a score made of nothing but compatibility is
|
|
18
|
+
not a weak match — it is no match, reported as one. Meanwhile `summary` is weight 8 and clears a
|
|
19
|
+
threshold of zero, so a word of shared English was enough to select anything.
|
|
20
|
+
|
|
21
|
+
The review asked for three things: make stack a tiebreaker, add a stopword list with a minimum score
|
|
22
|
+
of 25, and add a gold corpus. It also noted that plan 0002 §11 step 5 claims an `eval` corpus
|
|
23
|
+
"passes" and is marked **done**. There is no `skills eval` command and there was no corpus. Every
|
|
24
|
+
ranking change in this repository had been argued from examples.
|
|
25
|
+
|
|
26
|
+
## Ruling
|
|
27
|
+
|
|
28
|
+
**One.** A score may select only with evidence. `scoreSkill` returns `evidence` — the score without
|
|
29
|
+
its stack contribution — and the threshold is applied to that, never to the total. Both halves matter:
|
|
30
|
+
stack is 15 a tag, so a repo matching three of a skill's tags is 45 on its own, and a threshold that
|
|
31
|
+
counted stack would wave it through.
|
|
32
|
+
|
|
33
|
+
**Two.** `MIN_SCORE` is `W.capability`, 25. Below it the only available signals are `summary` at 8 and
|
|
34
|
+
`stack` at 15, and neither is a statement about the task. "Only a capability-strength signal or better
|
|
35
|
+
selects" is the rule the review states and the number it gives.
|
|
36
|
+
|
|
37
|
+
**Three.** Stopwords are suppressed in the summary signal only. A keyword is an explicit declaration
|
|
38
|
+
and a name is an identity, so hiding those would suppress something a person wrote on purpose; a
|
|
39
|
+
summary is prose, and prose carries words that are there for grammar. `do not push` is a declared
|
|
40
|
+
keyword and stays reachable.
|
|
41
|
+
|
|
42
|
+
**Four.** The rejection says why. `NOT SELECTED` is the block an agent uses to override, so a
|
|
43
|
+
rejection that cannot be acted on is the same defect as a bad selection.
|
|
44
|
+
|
|
45
|
+
**Five.** One place decides, and three commands call it: `rankSkills`, `skills find`, and the MCP
|
|
46
|
+
`skills_find` tool. They each ranked on `score > 0` independently, and an assertion that one of them
|
|
47
|
+
kept that rule would have passed the entire suite — see the fourth mutation below.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
The two acceptance cases in review 0001 §S04 now hold, and both are asserted against the shipped
|
|
52
|
+
binary: `fix d1 migration` selects `d1-safe-change` and not `api-contract-sync`, and a task of
|
|
53
|
+
stopwords resolves nothing and says why. The budget no longer caps on either.
|
|
54
|
+
|
|
55
|
+
The gold corpus is `test/fixtures/eval-corpus.json`, ten cases across four ecosystems, run by
|
|
56
|
+
`test/eval.test.mjs`. It states `select` (must return) and `reject` (must not), and asserts that
|
|
57
|
+
every skill it names still exists in the registry — a corpus citing a renamed skill otherwise passes
|
|
58
|
+
by comparing two lists that no longer overlap.
|
|
59
|
+
|
|
60
|
+
Five mutations, each reverted, each caught by the test that claims it:
|
|
61
|
+
|
|
62
|
+
| Mutation | Caught by |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| the minimum score dropped | two corpus cases, the weak-signal test, and the shipped-binary test |
|
|
65
|
+
| stack counted toward the threshold again | two corpus cases and the weak-signal test |
|
|
66
|
+
| stopwords not applied to the summary signal | both stopword tests |
|
|
67
|
+
| `skills find` reverted to `score > 0` | **nothing** — see below |
|
|
68
|
+
| the stopword list emptied | both stopword tests |
|
|
69
|
+
|
|
70
|
+
The fourth is the one worth keeping. Reverting `skills find` to its own weaker rule passed all 621
|
|
71
|
+
tests at the time, because it was the third command to be moved and no test covered the third. It is
|
|
72
|
+
ADR-035's lesson exactly: a promise about a set of commands is only true while something checks the
|
|
73
|
+
set. The test that now catches it queries `secrets`, chosen because that word lands strictly between
|
|
74
|
+
0 and `MIN_SCORE` for exactly one skill (`wrangler`, at 8 via its summary), so the two rules give
|
|
75
|
+
visibly different answers — 3 matches against 4.
|
|
76
|
+
|
|
77
|
+
The first version of the precision metric was also wrong and was caught by the corpus itself. It
|
|
78
|
+
scored precision against `select + reject`, so a case where the resolver selected a skill neither
|
|
79
|
+
list named scored a perfect 1.0. That is not precision, it is agreement with whatever was written
|
|
80
|
+
down. Precision is now measured two ways and both must hold: every named negative stays unselected,
|
|
81
|
+
and every case whose answer is "nothing" returns nothing at all.
|
|
82
|
+
|
|
83
|
+
### What the corpus found that the review did not predict
|
|
84
|
+
|
|
85
|
+
The `regenerate the openapi contract` case failed on the first run, against my own expectation: I had
|
|
86
|
+
written that `web-feature-slice` and `api-feature-slice` should not be selected. The resolver was
|
|
87
|
+
right and the corpus was wrong. `web-feature-slice` and `add-country-galaxy` both declare capability
|
|
88
|
+
`contract`, and `api-feature-slice` declares keyword `openapi` — so each carries a capability-strength
|
|
89
|
+
signal, which is exactly what ruling two admits. A hand-authored gold set is an opinion written down,
|
|
90
|
+
and this one had been written without checking the registry.
|
|
91
|
+
|
|
92
|
+
That leaves a real question this record does not answer: whether `contract` is the right capability
|
|
93
|
+
for a skill that registers a country. Changing `graph/skills.graph.json` changes
|
|
94
|
+
`provenanceDigest()`, which stamps every consumer repository, so the answer is not a question this
|
|
95
|
+
slice may decide by editing one line of data.
|
|
96
|
+
|
|
97
|
+
## Rejected
|
|
98
|
+
|
|
99
|
+
**Making the minimum score a `--min-score` flag.** It would let a user recover the old behaviour and
|
|
100
|
+
it would put the default in a place nobody reads. The old behaviour is not a preference some
|
|
101
|
+
repositories want; it is the reason `NOT SELECTED` was empty and the budget was full of bodies.
|
|
102
|
+
|
|
103
|
+
**Thresholding on the total score instead of on evidence.** Simpler, and wrong: it fails for exactly
|
|
104
|
+
the repos this tool serves. A repo matching three tags is the common case, not the edge.
|
|
105
|
+
|
|
106
|
+
**Removing the stack signal from the score entirely.** It orders candidates, which is a tiebreaker's
|
|
107
|
+
job, and removing it would make two equally-evidenced skills order by name alone.
|
|
108
|
+
|
|
109
|
+
**Putting the corpus under `graph/`, beside the registry.** `provenanceDigest()` walks `rules/` and
|
|
110
|
+
`graph/`, so a corpus file there changes the content digest and forces every consumer repository to
|
|
111
|
+
be restamped. A test corpus is not worth a migration, and the digest would move invisibly.
|
|
112
|
+
|
|
113
|
+
**Building the `skills eval` command this record could have built.** Plan 0002 §9 lists it. It is a
|
|
114
|
+
new product surface with its own flags, JSON contract and help text, and its absence is a defect in
|
|
115
|
+
the plan rather than in the tool. The corpus and the runner are what S04 asked for; the command is
|
|
116
|
+
recorded below as still owed.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ADR-045 — A dashboard that counts a defect and will not name it is worse than one that does not count it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · amends ADR-016 · affects what `compare` reports and exits on
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0002 §S25, marked P1, reported two defects in one row. Both measured against the estate
|
|
8
|
+
before anything was changed, and both confirmed exactly as written.
|
|
9
|
+
|
|
10
|
+
**One.** `src/compare/row.mjs` gated the stale-rule-file problem on `row.adoption === 'adopted'`.
|
|
11
|
+
`adoption` was initialised at line 78 and only assigned at lines 127–131, so the gate ran before
|
|
12
|
+
anything settled it and compared against the initialiser's `'unmanaged'`. It could never fire.
|
|
13
|
+
|
|
14
|
+
Measured over the 16 repositories at `~/code/simt`:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
staleRuleFiles: 17
|
|
18
|
+
repos naming stale rule files in problems: 0
|
|
19
|
+
repo-05 problems:
|
|
20
|
+
"the generated region was hand-edited; restore it with: simt-dev sync --yes"
|
|
21
|
+
"core.hooksPath is \"hooks\", not the shared dispatcher; run: simt-dev hooks install --yes"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Five adopted repositories have stale rule files — `repo-05`, `repo-06` and `repo-14` at 0
|
|
25
|
+
of 3 current, `repo-11` and `repo-13` at 0 of 4 — and not one of them said so in
|
|
26
|
+
its problems list. The summary already said `staleRuleFiles: 17`, because `summarise` has always
|
|
27
|
+
counted them. So the dashboard was reporting a count of a defect it refused to call a defect.
|
|
28
|
+
|
|
29
|
+
**Two.** `profiles` was documented at `row.mjs:33` as "profile ids that would be selected",
|
|
30
|
+
initialised to `[]` at line 83, and never assigned anywhere in the file's life. Every row of every
|
|
31
|
+
run reported no profiles. `plan()` — the thing `sync` renders — returns them.
|
|
32
|
+
|
|
33
|
+
## Ruling
|
|
34
|
+
|
|
35
|
+
**One.** A row's problems list names every stale rule file, and the exit code follows. The gate
|
|
36
|
+
reads `adoption` after it is settled, which means the ordering is part of the contract: **a gate
|
|
37
|
+
that reads a field the line above has not set yet is not a gate.** That sentence is the whole fix.
|
|
38
|
+
|
|
39
|
+
**Two.** The gate stays. A repo that has never been adopted has its own files, and reporting them
|
|
40
|
+
as "not current" turns the normal mid-migration state into a red estate — ADR-018's reasoning for
|
|
41
|
+
hooks, applied here to rule files. There is a test for the negative half specifically, because
|
|
42
|
+
fixing the gate is exactly the kind of change that turns this on.
|
|
43
|
+
|
|
44
|
+
**Three.** `profiles` is filled from the same `select()` call, on the same inputs, that
|
|
45
|
+
`desiredRegion` makes. Not from a copy of the rule table and not from the manifest's declared
|
|
46
|
+
stack: a dashboard that disagrees with the tool about what it would write is worse than one that
|
|
47
|
+
says nothing, because it is believed.
|
|
48
|
+
|
|
49
|
+
**Four.** A field documented as an answer must be either answered or absent. `profiles` was neither:
|
|
50
|
+
it was present, empty, and described as populated in the type it was documented with.
|
|
51
|
+
|
|
52
|
+
## Consequences
|
|
53
|
+
|
|
54
|
+
The estate now names all 17: five repositories carry the problem line, and `compare` exits non-zero
|
|
55
|
+
on them. Nothing else about the detection changed — the count was 17 before the fix and is 17 after
|
|
56
|
+
it. This slice changed reporting, not measurement, and the test asserts that by requiring the
|
|
57
|
+
fixture to start current before the edit.
|
|
58
|
+
|
|
59
|
+
### The ruling this amends
|
|
60
|
+
|
|
61
|
+
ADR-016 ruled that "`compare` continues to report one row per repo using `AGENTS.md` only, because a
|
|
62
|
+
dashboard row cannot represent four files. The gate uses the full finding list, so the dashboard's
|
|
63
|
+
coarser view is not inherited by the thing that must not be wrong."
|
|
64
|
+
|
|
65
|
+
That ruling is about the row's **shape** — which files it displays — and it still holds. The row
|
|
66
|
+
already carried `ruleFiles.total`, `ruleFiles.current` and `ruleFiles.stale` before this change, so
|
|
67
|
+
ADR-016's coarser display was never the problem.
|
|
68
|
+
|
|
69
|
+
What was missing is that ADR-016 reasoned about the *display* and the *problems list* together, and
|
|
70
|
+
they came apart. The dashboard was allowed to be coarse about which files it shows, and it was also
|
|
71
|
+
allowed to be silent about whether an agent reading them would get correct rules. Those are
|
|
72
|
+
different permissions, and only the first was granted on purpose. The problems list is not a
|
|
73
|
+
display; it is the exit code.
|
|
74
|
+
|
|
75
|
+
So: ADR-016 is amended, not overruled. The row stays coarse; the problems list does not.
|
|
76
|
+
|
|
77
|
+
### Three mutations, each caught by exactly its own test
|
|
78
|
+
|
|
79
|
+
| Mutation | Caught by |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| the stale-rule-file gate cannot fire | `a stale rule file on an adopted repo is a problem` |
|
|
82
|
+
| `profiles` back to its initialiser | `compare reports the profiles sync would project` |
|
|
83
|
+
| `profiles` computed from the declared stack instead of the detected one | `compare reports the profiles sync would project` |
|
|
84
|
+
|
|
85
|
+
The third is the one that matters. It is the shape this defect actually takes when it recurs: not an
|
|
86
|
+
empty array but a plausible array computed from the wrong input. A test asserting a hardcoded
|
|
87
|
+
expected list would have caught the second mutation and passed the third; asserting
|
|
88
|
+
`row.profiles` equals `plan(root).profiles` catches both, because it is the relationship rather
|
|
89
|
+
than the value that is the contract.
|
|
90
|
+
|
|
91
|
+
### A fixture that asserted nothing
|
|
92
|
+
|
|
93
|
+
The first version of the stale-file test appended a line after the `END` marker. That is hand-owned
|
|
94
|
+
content by design — the containment guarantee another test in the same file already asserts — so the
|
|
95
|
+
file was correctly not stale and the test failed. The fixture was making an edit the test claimed
|
|
96
|
+
wasn't being made. It now edits inside the region and asserts that the edit landed by checking the
|
|
97
|
+
recorded SHA marker is present first.
|
|
98
|
+
|
|
99
|
+
A first attempt at the mutation was also too coarse: re-deriving `adoption` in the wrong place broke
|
|
100
|
+
ten tests at once, which told me the ordering is load-bearing but not which gate depended on it. The
|
|
101
|
+
isolated mutation is `if (false && …)` on the one gate.
|
|
102
|
+
|
|
103
|
+
## Rejected
|
|
104
|
+
|
|
105
|
+
**Reporting per-file drift in the row's display, so the dashboard shows all four files.** ADR-016
|
|
106
|
+
already rejected this and its reason has not changed. A row is read across sixteen repositories at
|
|
107
|
+
once; four filenames per row is noise at that scale.
|
|
108
|
+
|
|
109
|
+
**Making `compare` defer entirely to `check` and drop the problems list.** The dashboard is the thing
|
|
110
|
+
a reader runs when they want to know where the estate stands; a command that says "run the other one"
|
|
111
|
+
has moved the problem rather than answered it.
|
|
112
|
+
|
|
113
|
+
**Exiting non-zero on an unmanaged repo's stray rule files.** The gate exists for the reason in
|
|
114
|
+
ruling two, and there is a test that asserts the negative so the fix cannot quietly widen it.
|
|
115
|
+
|
|
116
|
+
**Filling `profiles` from the manifest's declared stack.** It is cheaper and it is wrong: the
|
|
117
|
+
manifest is a declaration, `fingerprint` is a measurement, and `sync` selects on the measurement.
|
|
118
|
+
A dashboard reporting the declaration would be consistent with itself and inconsistent with the
|
|
119
|
+
tool. Mutation three is what that looks like when it happens.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ADR-046 — One rule, one copy: a reader that reads AGENTS.md must not also get the file
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · supersedes ADR-021 ruling 3 · settles P7's context-budget question
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Only ADR-021 **ruling 3** is superseded. Its other rulings — one surface per repo, a pointer is not
|
|
8
|
+
a copy, a written file is not removed, skills follow the same rule — stand, and ruling 6 is load
|
|
9
|
+
bearing *here*. So the status line deliberately does not name it in the form the index parses
|
|
10
|
+
(`supersedes [ADR-NNN]`): marking the whole record superseded would be false, exactly as it would
|
|
11
|
+
be for a narrowing.
|
|
12
|
+
|
|
13
|
+
Review 0001 §S12, marked P1 and "decision". Measured across the estate before anything changed —
|
|
14
|
+
the five repositories selecting `opencode,cursor`:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
repo-05 folded=3 .mdc=3 0/3 current
|
|
18
|
+
repo-06 folded=3 .mdc=3 0/3 current
|
|
19
|
+
repo-14 folded=3 .mdc=3 0/3 current
|
|
20
|
+
repo-11 folded=4 .mdc=4 0/4 current
|
|
21
|
+
repo-13 folded=4 .mdc=4 0/4 current
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Seventeen generated rule files. Every one of them byte-identical to the section already folded into
|
|
25
|
+
the same repository's `AGENTS.md`, verified by extracting both bodies and diffing: the only
|
|
26
|
+
difference was a trailing marker line in my first extraction, and the second extraction made 14 of
|
|
27
|
+
the 17 identical and the remaining three identical apart from that marker. **28,101 bytes of context
|
|
28
|
+
loaded twice per session**, in five repositories, forever.
|
|
29
|
+
|
|
30
|
+
ADR-021 ruling 3 decided this deliberately: "Folding is `some`, not `all`: a repo that also opts into
|
|
31
|
+
Cursor still folds, because an opencode reader's only channel is `AGENTS.md` **and a Cursor reader
|
|
32
|
+
getting `.mdc` files does not serve them**."
|
|
33
|
+
|
|
34
|
+
The bolded clause is the whole dispute, and it is a factual claim about a vendor. It is false.
|
|
35
|
+
Cursor's own documentation says `AGENTS.md` is "a simple markdown file for defining agent
|
|
36
|
+
instructions... an alternative to `.cursor/rules`", and that "Cursor supports AGENTS.md in the
|
|
37
|
+
project root and subdirectories." Cursor reads `AGENTS.md`.
|
|
38
|
+
|
|
39
|
+
Note where the original claim came from: the `readers` string on **simt-dev's own opencode row**
|
|
40
|
+
already said "AGENTS.md is also read natively by Codex, Cursor, Windsurf, Zed, Amp, Jules, Copilot
|
|
41
|
+
CLI and the DeepSeek harness". The table knew. The decision that depended on the fact was written
|
|
42
|
+
without checking the table, which is the shape of error this repository keeps having.
|
|
43
|
+
|
|
44
|
+
## Ruling
|
|
45
|
+
|
|
46
|
+
**One.** A surface declares `readsPrimary: true` when its own vendor documents that its reader loads
|
|
47
|
+
`AGENTS.md`. When any selected surface folds, `ruleTargets` skips per-rule files for those surfaces
|
|
48
|
+
and says so in the plan: the note names the file and the reason.
|
|
49
|
+
|
|
50
|
+
**Two.** It is per-surface, not global, because Copilot is the counter-example and the row says so
|
|
51
|
+
already: its CLI reads `AGENTS.md`; its editor and code review do not. A per-rule file there is the
|
|
52
|
+
only channel two of its three readers have, and dropping it would remove the process rules from a
|
|
53
|
+
chat that had nowhere else to get them.
|
|
54
|
+
|
|
55
|
+
**Three.** The skip depends on folding. With no folding surface selected there is no region to
|
|
56
|
+
duplicate, so a repo on `cursor` alone still gets its files — asserted, because a flag that reads
|
|
57
|
+
as "cursor never gets files" would be a different and much larger change.
|
|
58
|
+
|
|
59
|
+
**Four.** `sync` never deletes a file it wrote (ADR-021 ruling 6), so a file that becomes orphaned
|
|
60
|
+
cannot be repaired by any command. It is therefore a **note with a remedy, never a problem**, and it
|
|
61
|
+
never fails the gate. A gate nothing can satisfy is worse than a gate that is merely noisy.
|
|
62
|
+
|
|
63
|
+
**Five.** The tool must not stop looking at it. `orphanedRuleFiles` scans the rule directories of
|
|
64
|
+
every `readsPrimary` surface — **whether or not the repo still selects it** — for files carrying a
|
|
65
|
+
simt-dev region, and reports them. A file without the marker is hand-owned and is never mentioned.
|
|
66
|
+
|
|
67
|
+
## Consequences
|
|
68
|
+
|
|
69
|
+
`sync --surfaces opencode,cursor` now writes `AGENTS.md` and no `.cursor/rules/*.mdc`. The three
|
|
70
|
+
always-on rules still arrive, folded, and the plan says why each file was not written.
|
|
71
|
+
|
|
72
|
+
The estate reports all 17 as notes with a delete command. Nothing was deleted: removal is a human
|
|
73
|
+
decision with a visible diff, and this record does not authorise a tool to make it.
|
|
74
|
+
|
|
75
|
+
### The invisibility that ruling five exists to prevent
|
|
76
|
+
|
|
77
|
+
The skip landed first and the estate run showed `staleRuleFiles` go from **17 to 0**. Every one of
|
|
78
|
+
those files was still on disk. A tool that stops looking at a file it generated has not cleaned up —
|
|
79
|
+
it has lost track of it — and the symptom is a dashboard reporting a clean estate while seventeen
|
|
80
|
+
files are being read twice.
|
|
81
|
+
|
|
82
|
+
The first version of the scan then made the same mistake in a subtler form: it iterated the
|
|
83
|
+
*selected* surfaces. A file becomes orphaned precisely when a repo **drops** the surface that wrote
|
|
84
|
+
it, so scanning the selected set finds nothing at all — the one repo that needs the note is the one
|
|
85
|
+
that dropped the surface. Both defects were found by running the command against the estate, not by
|
|
86
|
+
reading the code. The estate run is worth more than either unit test.
|
|
87
|
+
|
|
88
|
+
### Six mutations, three of which caught nothing at first
|
|
89
|
+
|
|
90
|
+
| Mutation | Caught by |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| the skip never fires | `opting into a rule surface ... writes one file, not two` |
|
|
93
|
+
| `readsPrimary` applied without the folding condition | three tests, including `cursor alone still gets its own files` |
|
|
94
|
+
| the orphan scan deleted | **nothing** — found, then fixed |
|
|
95
|
+
| `orphaned` made blocking | **nothing** — found, then fixed |
|
|
96
|
+
| `orphaned` counted as a stale problem | **nothing** — found, then fixed |
|
|
97
|
+
| orphan scan restricted to selected surfaces | `compare reports an orphaned rule file as a note` |
|
|
98
|
+
|
|
99
|
+
The middle three passed all 627 tests, which is worth recording plainly: the half of this slice with
|
|
100
|
+
the most dangerous failure mode — a gate nothing can satisfy — shipped with no test at all, and only
|
|
101
|
+
a deliberate mutation found it. Four tests were written afterwards, including a negative control
|
|
102
|
+
("a repo's own rule files are never reported as orphaned"), because a scan that always returned an
|
|
103
|
+
empty array satisfies every positive test in the file.
|
|
104
|
+
|
|
105
|
+
### Two tests that encoded the old ruling
|
|
106
|
+
|
|
107
|
+
`test/surfaces.test.mjs` asserted the three `.mdc` files beside the folded region, with the comment
|
|
108
|
+
"One canonical source, two destinations" — a fair description of the behaviour, and the behaviour
|
|
109
|
+
was wrong. `test/upgrade.test.mjs` used `--surfaces opencode,cursor` to produce the generated file
|
|
110
|
+
whose wholesale repair it tests. Both now use **copilot**, whose reader does not read the primary, so
|
|
111
|
+
the properties under test are unchanged while their fixtures remain honest. A test fixture built on
|
|
112
|
+
a behaviour a new ADR reverses has to be moved deliberately, and the reason recorded, or the next
|
|
113
|
+
reader moves it back.
|
|
114
|
+
|
|
115
|
+
`test/compare.test.mjs`'s S25 fixture also moved to copilot for the same reason — and its helper
|
|
116
|
+
selected rule files by the `.mdc` suffix, which silently matched nothing and reported "the fixture
|
|
117
|
+
must project at least one rule file" rather than "the fixture is broken".
|
|
118
|
+
|
|
119
|
+
## Rejected
|
|
120
|
+
|
|
121
|
+
**Deleting the orphaned files automatically.** ADR-021 ruling 6, and the reason is sound: `sync` is a
|
|
122
|
+
projection, and a projection that deletes is a second, silent writer. The file is reported with the
|
|
123
|
+
command that removes it.
|
|
124
|
+
|
|
125
|
+
**Making `readsPrimary` global.** It would drop Copilot's only channel for its editor and code
|
|
126
|
+
review. The flag is per-surface for that reason and the Copilot row carries the note that says why.
|
|
127
|
+
|
|
128
|
+
**Keeping the `.mdc` files and not folding into the region when both surfaces are selected** — the
|
|
129
|
+
alternative review 0001 named. It gives opencode users nothing in mixed repos, which is ADR-021's
|
|
130
|
+
original objection to one surface per repo and still true.
|
|
131
|
+
|
|
132
|
+
**Removing Cursor support entirely, on the grounds that it reads `AGENTS.md`.** That would be true
|
|
133
|
+
and would lose the `.mdc` channel for anyone whose Cursor does not surface `AGENTS.md`, and it would
|
|
134
|
+
make a narrowing into a removal — which is not this tool's habit. The rules still reach a Cursor
|
|
135
|
+
reader; they just reach them once.
|
|
136
|
+
|
|
137
|
+
**The worktree-agent caveat becoming a gate.** A Cursor bug report records `AGENTS.md` not being read
|
|
138
|
+
in worktree agents by default, fixed in 3.9. That is a fact about a product and it is recorded on
|
|
139
|
+
the row, but gating on a version number would make a rule's presence depend on a vendor's release
|
|
140
|
+
cadence — and the alternative for those readers is a duplicated 5 KB, which is the cheaper error.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# ADR-047 — A profile selects on what a repository runs on, not on what it is written in
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-09 · refines ADR-013 ruling 1
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0002 §S28, P1, marked "decision" (proposal P3). Four profiles selected on a language or a
|
|
8
|
+
build tool while their bodies advised on a platform. Measured across the estate before anything
|
|
9
|
+
changed — the review's four rows, plus two smaller defects in the same table:
|
|
10
|
+
|
|
11
|
+
| Repo shape | Profile it received | Why that is wrong |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| any TypeScript library | `cloudflare-workers` (D1, Durable Objects, wrangler) | applied on `typescript` |
|
|
14
|
+
| Spring or Ktor backend on Gradle | `jvm-android` (Compose, Wear OS, overlay policy) | applied on `gradle`, `kotlin` |
|
|
15
|
+
| React Native / Expo app | `react-web` (CSS logical properties, WCAG reflow, a browser matrix) | applied on `react`, and `DEP_TAGS` mapped `react` and `react-dom` to the same tag |
|
|
16
|
+
| Poetry project | a `python` body that says `uv run -m`, `uv.lock` | the body was uv-specific, the selection was any Python marker |
|
|
17
|
+
|
|
18
|
+
Two smaller ones in the same table: `hono` also keyed `cloudflare-workers`, and Hono runs on
|
|
19
|
+
Node, Bun and Deno as happily as on Workers — a library tag read as platform evidence. And
|
|
20
|
+
`wrangler.json` was missing from `MARKERS`, so a Worker configured with the plain JSON file and
|
|
21
|
+
no `wrangler` dependency had no evidence of being a Worker at all.
|
|
22
|
+
|
|
23
|
+
ADR-013 ruling 1 made platform tags and facts interchangeable in `applies`, and its consequences
|
|
24
|
+
promised that "every tag a profile may name is one the engine emits" would hold for facts too,
|
|
25
|
+
with "the registry validator and the profile loader share one derived vocabulary so they cannot
|
|
26
|
+
desynchronise." Half of that was implemented: `graph lint` checked a skill's `stack` array
|
|
27
|
+
against the vocabulary, and nothing checked a profile's `applies`. The four rows above are what
|
|
28
|
+
that half produced — not a typo, but selections the vocabulary permitted and no check objected to.
|
|
29
|
+
|
|
30
|
+
## Ruling
|
|
31
|
+
|
|
32
|
+
**One.** A profile's `applies` names the evidence of what its body advises on. Where platform
|
|
33
|
+
evidence exists, a bare language or build-tool tag is not that evidence:
|
|
34
|
+
|
|
35
|
+
| Profile | Before | After | What selects it |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| `cloudflare-workers` | `cloudflare-workers, hono, typescript` | `cloudflare-workers` | a wrangler config or the `wrangler` dependency |
|
|
38
|
+
| `jvm-android`, split into `jvm` | `gradle, kotlin, wear-os` | `jvm`: `gradle, has-maven-pom, kotlin, java` | a JVM build |
|
|
39
|
+
| `jvm-android`, split into `android` | (the same file) | `android`: `has-android-manifest` | a module with an `AndroidManifest.xml` |
|
|
40
|
+
| `react-web` | `react, react-router, maplibre, firebase` | `react-dom, react-router, maplibre, firebase` | the browser renderer |
|
|
41
|
+
| `react-native`, new | — | `react-native` | a `react-native` or `expo` dependency |
|
|
42
|
+
| `python`, split into `python` and `uv` | `python` (with a uv-specific body) | `python`: `python`; `uv`: `has-uv-lock` | the language / the lockfile |
|
|
43
|
+
|
|
44
|
+
The exception is where a language *is* the finest fact available: `python`'s body advises
|
|
45
|
+
invocation hygiene that holds in every Python repository, so the `python` marker is the right
|
|
46
|
+
key there. The test that separates those two cases is the pair of rows whose shared stack is
|
|
47
|
+
exactly `node, react` and which differ only by the renderer tag: one receives `react-web`, the
|
|
48
|
+
other `react-native`, and neither receives both.
|
|
49
|
+
|
|
50
|
+
**Two.** The Android and uv facts name a path at an unknown depth, which the two existing kinds
|
|
51
|
+
(`file`, `dir`) cannot express. A third kind, `glob`, walks at most `GLOB_DEPTH` directory levels
|
|
52
|
+
below the root. The bound is a convention, not a preference: the Android Gradle Plugin puts a
|
|
53
|
+
module's manifest at `<module>/src/main/AndroidManifest.xml`, and repo-05's sits at
|
|
54
|
+
`app/src/main/AndroidManifest.xml`. A shallower bound makes `has-android-manifest` the shape S07
|
|
55
|
+
found — a fact no repository could have. A deeper one is a scan rather than a probe, and
|
|
56
|
+
ADR-013's promise is that adding an ecosystem is data, so a fact must not cost a search.
|
|
57
|
+
|
|
58
|
+
The walk skips dot-directories and `node_modules` for the reason the extension sample skips them;
|
|
59
|
+
the matcher is the registry's `matchesGlob` (ADR-042), so a fact's shape and a skill's `path:`
|
|
60
|
+
are one language rather than two; and the evidence for a `glob` fact is the matched path, never
|
|
61
|
+
the pattern — `doctor` answers "why did you think that?", and a shape is not an answer.
|
|
62
|
+
|
|
63
|
+
**Three.** `wrangler.json` and `wrangler.jsonc` join the Workers markers beside `wrangler.toml`.
|
|
64
|
+
|
|
65
|
+
**Four.** The vocabulary check now covers profiles as well as skills. `describeDefect` rejects a
|
|
66
|
+
`simt-applies` entry that `fingerprint()` cannot emit — `simt-applies names "pyton", which
|
|
67
|
+
fingerprint() never emits, so this profile could never be selected` — for central and local
|
|
68
|
+
profiles alike. A tag the engine cannot emit is a profile that can never be selected, and the
|
|
69
|
+
defect lives in the file that names it, not in whatever repository happens to load it. This is
|
|
70
|
+
the half of ADR-013's shared-vocabulary consequence that was unimplemented, and it is what makes
|
|
71
|
+
ruling one enforceable rather than aspirational: a future profile that re-keys on a language tag
|
|
72
|
+
spelling no detector emits fails at load time, in the file's own defect list.
|
|
73
|
+
|
|
74
|
+
## Consequences
|
|
75
|
+
|
|
76
|
+
`compare` over the estate before and after — 16 repositories, **4 move, 12 unchanged**, and no
|
|
77
|
+
repository loses coverage:
|
|
78
|
+
|
|
79
|
+
| Repo | Before | After |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `repo-01`, `repo-05`, `repo-03` | `core, jvm-android` | `core, android, jvm` |
|
|
82
|
+
| `repo-02` | `container, core, python` | `container, core, python, uv` |
|
|
83
|
+
|
|
84
|
+
The three Android repositories gain the profile whose body they actually needed and keep the JVM
|
|
85
|
+
build rules; `repo-05` gaining `android` was the row's acceptance criterion. `repo-02`
|
|
86
|
+
carries a committed `uv.lock`, so its gain is the defect repaired, not a new selection.
|
|
87
|
+
|
|
88
|
+
The rename is breaking in one narrow way: the id `jvm-android` no longer exists. A consumer that
|
|
89
|
+
pinned it — a manifest's `profiles` array, a local profile shadowing by id, a document that names
|
|
90
|
+
it — must rename to `jvm` and `android`. Nothing in the estate pinned it, which is why the table
|
|
91
|
+
above is complete; a consumer outside it learns from this record.
|
|
92
|
+
|
|
93
|
+
The provenance digest moves `47160e9e…` to `0cfea39f…` because files under `rules/stacks/` were
|
|
94
|
+
added and removed. Every adopted repository re-stamps on its next `sync` or `upgrade`.
|
|
95
|
+
|
|
96
|
+
The fixture table in `test/genericity.test.mjs` asserts each row's `stack` **and** its exact
|
|
97
|
+
profile set, in order — `core` first because it applies always, the rest in profile-file read
|
|
98
|
+
order. Asserting the set alone would still pass if a fixture stopped being the thing its row
|
|
99
|
+
claims (a "TypeScript library" that is no longer TypeScript), which is the weaker claim a
|
|
100
|
+
mutation produces. The hand-maintained `PROFILES` list in `test/shipped.test.mjs` is deleted:
|
|
101
|
+
S28's rename broke it, and it guarded nothing — the leak check already iterates `loadRules()`.
|
|
102
|
+
|
|
103
|
+
Seven mutations, all caught:
|
|
104
|
+
|
|
105
|
+
| Mutation | Caught by |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `react-web` re-keyed on `react-dom, react` | the exact-set row test |
|
|
108
|
+
| the `glob` probe reduced to a root-presence check | four tests: row, evidence, bound, root shape |
|
|
109
|
+
| `GLOB_DEPTH` 3 → 2 | three tests: row, evidence, bound |
|
|
110
|
+
| `wrangler.json` dropped from the markers | the row test |
|
|
111
|
+
| `hono` restored to the Workers applies | the row test |
|
|
112
|
+
| the vocabulary check removed from `describeDefect` | the local-profile probe: an error where the message belongs |
|
|
113
|
+
| the android fact made a shallow `dir` probe (S07's shape) | three exact-set rows |
|
|
114
|
+
|
|
115
|
+
## Rejected
|
|
116
|
+
|
|
117
|
+
**Selecting on the language and excluding with an extra condition** — `cloudflare-workers`
|
|
118
|
+
applies on `typescript`, but not when …. Selection by subtraction needs its own detector for
|
|
119
|
+
every new shape of repository, and each exclusion is a hole until it is written. The positive
|
|
120
|
+
form asks one question — *is this a Worker?* — and the answer is a file or a dependency, which is
|
|
121
|
+
what a fact is for.
|
|
122
|
+
|
|
123
|
+
**The build-plugin half of `has-android-manifest`.** The review proposed the manifest *or* the
|
|
124
|
+
`com.android.application` / `com.android.library` plugin in a build file. The plugin has two
|
|
125
|
+
spellings (Groovy `apply plugin:` and the Kotlin DSL `id(...)`), moves with the plugin's
|
|
126
|
+
versioning, and a repository can carry the plugin without the manifest while it is mid-migration.
|
|
127
|
+
The manifest is the one path shape that makes a module buildable as Android, and it is what
|
|
128
|
+
`doctor` can point at. The plugin half is recorded here rather than implemented, so the next
|
|
129
|
+
reader does not discover it as a gap.
|
|
130
|
+
|
|
131
|
+
**An unbounded glob walk.** It would find the manifest, and it would also make detection a scan
|
|
132
|
+
of every file below the root, which is what ADR-013's promise rules out: adding an ecosystem is a
|
|
133
|
+
profile file, not a search. The bound is the convention the fact exists to see.
|
|
134
|
+
|
|
135
|
+
**Keeping `jvm-android` and documenting the mis-selection.** That ships a profile whose body the
|
|
136
|
+
review measured as wrong for a whole class of repositories, with a comment where a fix belongs.
|
|
137
|
+
|
|
138
|
+
**A per-renderer tag for every framework.** `react-dom` is the renderer, and it is what
|
|
139
|
+
distinguishes a browser from every other surface. The other keys on `react-web` — `react-router`,
|
|
140
|
+
`maplibre`, `firebase` — were already there as discriminators, and they stay: each names a
|
|
141
|
+
library whose presence implies a browser deployment the same way `wrangler` implies Workers.
|