simt-dev 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,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.