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,150 @@
1
+ # ADR-048 — a baseline carries only keys a reader honours
2
+
3
+ Status: accepted · 2026-10-09 · amends ADR-025 (withdraws ruling 8, corrects its context)
4
+ **Affects:** `src/projection/baselines.mjs`, `test/baselines.test.mjs`,
5
+ `docs/plans/0001-centralise-agent-rules.md`. Closes S30 of
6
+ `docs/reviews/0002-full-review-adoption-and-public-release.md`.
7
+
8
+ ## Context
9
+
10
+ ADR-025 ruling 8 put `[prettier] singleQuote = true` in the shared `.editorconfig` and
11
+ called it *"the only comment-capable home prettier reads"*. Both halves of that sentence
12
+ are false, for one normative reason and one vendor reason, and neither is visible from
13
+ inside this repository.
14
+
15
+ **A section header is a glob.** The EditorConfig specification (v0.17.2, *File Format*)
16
+ is explicit: a "Section Header … define[s] globs", and *Glob Expressions* lists `*`,
17
+ `**`, `?`, `[seq]`, `[!seq]` and `{s1,s2,s3}` as the characters that do the matching.
18
+ `[prettier]` therefore matches a file named `prettier`, in a directory of the
19
+ repository or below it. No repository has one. The block matched no file, ever.
20
+
21
+ **`singleQuote` is not a key anything honours.** The same specification's *Supported
22
+ Pairs* section defines nine keys — `indent_style`, `indent_size`, `tab_width`,
23
+ `end_of_line`, `charset`, `spelling_language`, `trim_trailing_whitespace`,
24
+ `insert_final_newline`, `root` — and `singleQuote` is not among them. That section also
25
+ settles what an undefined key does, and it settles it in the worst possible direction:
26
+ cores "shall accept and report" syntactically valid pairs even when the key is not
27
+ defined, while plugins "shall ignore unrecognized keys". An unknown key is not an error
28
+ to be reported; it is a line that parses and does nothing.
29
+
30
+ Prettier's own mapping (`src/config/editorconfig/editorconfig-to-prettier.js` on `main`)
31
+ reads exactly six properties: `indent_style`, `indent_size`, `tab_width`,
32
+ `max_line_length`, `quote_type` and `end_of_line`. `singleQuote` is not one of them, and
33
+ neither `quote_type` nor `max_line_length` is a key the specification defines — the
34
+ source labels `quote_type` an "Undocumented feature". Its published documentation
35
+ mentions only `max_line_length`, `indent_size`, `tab_width`, `indent_style` and
36
+ `end_of_line` as the editorconfig properties it reads.
37
+
38
+ So the region asserted, in a comment, that it was the file that settled the estate's
39
+ `singleQuote` disagreement; the value was in a section that matched nothing, under a key
40
+ that no reader honours. That is the "decoration that misdescribes the artefact" failure
41
+ this repository's own `AGENTS.md` names, and no gate here could see it: `check` compares
42
+ bytes, and the bytes were current.
43
+
44
+ **This record cites the specification and the vendor's source rather than a local run.**
45
+ That is a change from the review's evidence, which measured Prettier 3.8.3, and it is
46
+ deliberate. ADR-041 rules this package's suite against reaching for a formatter, and the
47
+ npm registry this was written against refuses the fetch (`403`), so a reproduced run was
48
+ not available. The two citations are stronger for the purpose anyway: one is normative and
49
+ one is the mapping Prettier actually runs, whereas a version-pinned run would date the
50
+ moment the question was asked.
51
+
52
+ ## The survey S30 asked for
53
+
54
+ S30's fix was split in two on purpose: delete the block **now**, and add `quote_type =
55
+ single` **only with an estate survey**. The survey is this section, and it is the reason
56
+ the second half is not taken.
57
+
58
+ Five of the sixteen repositories run Prettier. Prettier resolves `.editorconfig` first and
59
+ merges the repository's own config over it (`resolve-config.js`: `{ ...editorConfigured,
60
+ ...mergeOverrides(...) }`), so a repository that states `singleQuote` is unaffected by
61
+ anything this baseline says, and a repository that states nothing is decided entirely by
62
+ it.
63
+
64
+ | Repository | Prettier config | States `singleQuote` | Adopting `quote_type = single` |
65
+ | --- | --- | --- | --- |
66
+ | `repo-04` | `.prettierrc` | `true` | no change |
67
+ | `repo-06` | `.prettierrc.json` | `true` | no change |
68
+ | `repo-15` | `.prettierrc.json` | `true` | no change |
69
+ | `repo-11` | `.prettierrc.json` | absent | **every source file reformatted** |
70
+ | `repo-14` | `.prettierrc.json` | absent | **every source file reformatted** |
71
+
72
+ Two repositories would have every source file rewritten on the day they adopted the
73
+ tool, in a commit whose message says it adopted a rules projection. The measurement is
74
+ the answer to the question S30 deferred: the second half is not taken, and the reason is
75
+ recorded here so the next person does not run the survey again to reach it.
76
+
77
+ ## Rulings
78
+
79
+ 1. **`[prettier] singleQuote = true` is withdrawn.** ADR-025 ruling 8 does not stand; its
80
+ `printWidth` half is unaffected and was never implemented. This changes no behaviour,
81
+ because the block was inert — which is why two reviews and the whole suite read past
82
+ it: the gate compares bytes, and the bytes were what the source said they were.
83
+
84
+ 2. **A baseline section header is a glob over paths, never a name.** A header with no
85
+ special character matches one filename, and the only filenames a file every
86
+ repository receives may name are the ones every repository has. A literal travels
87
+ inside a brace group with the globs it belongs with — `[{*.go,*.proto}]`, which this
88
+ baseline already does for exactly this reason. `test/baselines.test.mjs` asserts it,
89
+ because the defect it prevents is a header that parses.
90
+
91
+ 3. **Every key in a baseline is one a reader honours.** The nine keys the specification
92
+ defines are the whole vocabulary, and the test names them with the section they come
93
+ from. This is what rules `singleQuote` out by construction rather than by deletion:
94
+ `quote_type` is excluded by the same test, and the reason it is excluded is written
95
+ next to it.
96
+
97
+ 4. **`quote_type = single` is not adopted, and the estate survey above is its record.**
98
+ Not rejected in principle: rejected for now, on a measurement — two of five
99
+ repositories would be wholly reformatted, and this tool does not reformat a
100
+ repository on the day it adopts one. The disagreement it would settle is real and is
101
+ still open (ADR-025's consequences already recorded `singleQuote` as unsettled
102
+ everywhere else), and it belongs in each repository's own Prettier config, which is
103
+ the only place that can change a repository's bytes.
104
+
105
+ ## Consequences
106
+
107
+ - `.editorconfig` loses eleven lines, one of which was the only claim in the file about a
108
+ tool. Every adopting repository sees a real `.editorconfig` change on its next
109
+ `sync --yes`, and `check` reports it: the baseline target list is the one `check` and
110
+ `upgrade` both read, so the repository is correctly classified as behind. What `upgrade`
111
+ will *not* do is name the cause correctly. Measured on a scratch repository carrying the
112
+ body this record withdraws and every other byte current:
113
+
114
+ ```
115
+ DRIFT .editorconfig region is updated; run: simt-dev sync --yes
116
+ pin 0cfea39f…6671 (unchanged)
117
+ region restamped; the pin was already current (the rules changed)
118
+ ```
119
+
120
+ No rule changed. The provenance digest covers `rules/` and `graph/` and deliberately
121
+ excludes `src/` — the rationale is that "a change that moves a consumer's projection"
122
+ must move the stamp, and the baseline bodies are content that leaves the package while
123
+ living in `src/`. `check` is unaffected because it compares bytes, and `upgrade` is
124
+ affected because it compares the stamp. ADR-032 found this exact false cause once
125
+ already, for a different reason, and the fix there was to identify the projection by its
126
+ content rather than by a commit; the same gap survives for the bytes that are not in
127
+ `rules/`. It is filed as **S53** with this measurement attached rather than fixed here,
128
+ because widening the digest changes what the stamp means and that is ADR-032's subject
129
+ (ADR-002).
130
+
131
+ - Plan 0001 claims the contradiction is settled "in `.editorconfig`'s `[prettier]`
132
+ section" in two places. Both were true of the file and false of its meaning, so both
133
+ are amended to point here. Leaving them would leave the plan asserting a mechanism this
134
+ record has just withdrawn, which is ADR-036's failure one document up.
135
+
136
+ - `test/baselines.test.mjs` grew a guard rather than an assertion about today's string,
137
+ because a string assertion passes the day somebody pastes the block back.
138
+
139
+ ## Rejected
140
+
141
+ - **Keeping the block and correcting only its comment.** Rejected: it would leave a
142
+ section matching nothing, and the next reader would have to rediscover that a comment
143
+ admitting it is inert is still inert.
144
+ - **`quote_type = single` under the JS/TS glob, as S30's second half proposed.**
145
+ Rejected on the survey above. It is recorded as a measurement, not as a plan, because
146
+ the cost lands on two repositories that did not ask for it.
147
+ - **Widening the digest to cover baseline bodies in this slice.** Rejected under
148
+ ADR-002: it is a change to what the stamp means, which is ADR-032's subject and
149
+ deserves its own record. It is filed as S53 with this measurement attached, so the
150
+ next reader has the evidence rather than the conclusion.
@@ -0,0 +1,153 @@
1
+ # ADR-049 — a ruling that a later record moved says so where it is printed
2
+
3
+ Status: accepted · 2026-10-09
4
+ **Affects:** `src/decisions/index.mjs`, `src/commands/rules.mjs`, `src/mcp/tools.mjs`,
5
+ `test/rules.test.mjs`. Closes S31 of
6
+ `docs/reviews/0002-full-review-adoption-and-public-release.md`.
7
+
8
+ ## Context
9
+
10
+ Review 0002 §5 filed S31 with a measurement: `rules why ADR-023` printed the status
11
+ `accepted · 2026-10-06 · ruling 2 superseded by` and then printed ruling 2 — *"It is written
12
+ once and never rewritten"* — as a ruling in force. ADR-024 had superseded exactly that
13
+ ruling. The module header names this as its own failure mode: *"the failure mode of a
14
+ provenance feature that only proves provenance, not currency."*
15
+
16
+ Two more defects were in the same reader and neither was in the finding.
17
+
18
+ **The status is a paragraph, and it was read as a line.** `loadDecisions` matched
19
+ `/^Status:\s*(.*)$/m`, which stops at the newline. Four of forty-eight records wrap their
20
+ status over more than one line, and the one that matters is ADR-023: its second and third
21
+ lines are the whole claim — which record superseded ruling 2, and that rulings 3 and 4 were
22
+ narrowed by the same record. The parser saw `ruling 2 superseded by` and stopped, which is
23
+ a sentence with no object.
24
+
25
+ **A partial supersession could not become a whole one, and the obvious fix makes it do
26
+ exactly that.** `supersededBy` was `/superseded by \[ADR-(\d{3})\]/i` over the status.
27
+ Once the status is the whole paragraph, that pattern matches ADR-023's *"ruling 2
28
+ superseded by [ADR-024](…)"* — and `supersededBy` is the field that makes `rules why`
29
+ answer a record by printing its replacement's rulings and suppressing its own. Five
30
+ rulings in ADR-023 that the status line itself says stand would have been dropped, by the
31
+ field that exists to stop precisely that. Anchoring the match to the start of the status is
32
+ not a detail of the fix; it is the fix.
33
+
34
+ **Rulings are numbered five different ways and one of them was read.** This is the finding
35
+ that made S31's stated fix impossible as written, and it was not visible until the ruling
36
+ text was measured rather than the status line:
37
+
38
+ | Family | Records | Example |
39
+ | --- | --- | --- |
40
+ | `1. **bold**` | 17 | ADR-021 |
41
+ | `**1. prose**` | 8 | ADR-034–040 |
42
+ | `**One.**` | 7 | ADR-041–047 |
43
+ | `### 1. Title` | 4 | ADR-013–016 |
44
+ | a bare bullet list | 1 | ADR-009 |
45
+ | unnumbered paragraphs | 11 | ADR-001–012 |
46
+
47
+ Only the first was recognised. The consequences are not stylistic. ADR-045 declares six
48
+ `**One.**` rulings and printed **one** merged blob of them. ADR-013 printed the literal
49
+ string `### 2.` to an agent as though the record had ruled with it. ADR-015 lists four
50
+ resolution strategies inside `### 2.`, and the first version of the fix read three of them
51
+ as rulings numbered 1, 2 and 3. And a supersession says *"ruling 3"*: a number that cannot
52
+ be located cannot be reported as no longer in force, so annotating a ruling depends on
53
+ knowing what one is.
54
+
55
+ **`accepted` was one record's spelling, not the document's.** The check was
56
+ `status.includes('**accepted**')`. ADR-001 to ADR-020 write `**accepted**`; ADR-021 onwards
57
+ write `accepted`. Fourteen accepted records read as not accepted. This is S19's finding and
58
+ S31 names the fix; it is the sixth instance in this repository of a check that reads the
59
+ author's habits rather than the artefact.
60
+
61
+ **A section ended at a heading inside a code block.** `section()` ended a section at the
62
+ next `^## `, and ADR-007's ruling is a document skeleton in a fence whose headings are
63
+ `##`. The record kept only the lead-in — which was still longer than the minimum length,
64
+ so the test asking whether every record yields a ruling passed on a fragment.
65
+
66
+ ## Rulings
67
+
68
+ 1. **`Status:` is a paragraph.** It is read to the first blank line or the first sibling
69
+ field, whichever comes first, and flattened. A sibling field is a capitalised label and
70
+ a colon, optionally in emphasis: `Affects:` and `**Affects:**` both end it, and no
71
+ continuation line in the corpus does. Getting the boundary wrong is not cosmetic in
72
+ either direction — stopping early truncates the sentence that says what was overturned,
73
+ and reading on swallows `Affects:`, whose value is a list of paths in a file read inside
74
+ repositories that own none of them.
75
+
76
+ 2. **Every field is read off the flattened, markdown-free status.** The link form and the
77
+ bare form are then one spelling, so a record that writes `ADR-046` and one that writes
78
+ `[ADR-046](…)` are read by the same line rather than by two patterns kept in step.
79
+
80
+ 3. **A record is superseded only when its own status says so first.** `supersededBy` is
81
+ anchored to the start of the status. See the Context: the unanchored form turns a
82
+ partial supersession into a total one.
83
+
84
+ 4. **`accepted` is read from the status.** `/^accepted\b/i` on the flattened paragraph, so
85
+ the record's own wording is what counts and no record's habits are.
86
+
87
+ 5. **A partial supersession is data, not a sentence to be parsed out of prose at print
88
+ time.** `partial: [{ruling, kind, by}]`, one entry per ruling a later record moved, in
89
+ three kinds the corpus uses — `superseded`, `narrowed`, `withdrawn`. A ruling a later
90
+ record names is printed with the fact attached, in place: *(superseded by ADR-024)*.
91
+ `ruling: null` is a record that says "one ruling" without saying which — a fact about the
92
+ record, carried rather than guessed at — and `by: null` is "the same record" in a status
93
+ that has not named one. Neither is resolvable, and neither is silently dropped.
94
+
95
+ 6. **"The same record" resolves to the last record this status named.** ADR-023 uses it for
96
+ rulings 3 and 4 immediately after naming ADR-024 for ruling 2. Resolving it to nothing
97
+ would drop two of the three supersessions on the one record this slice is about.
98
+
99
+ 7. **A ruling's identity is its number, and a section has one family.** The family is read
100
+ off the first line that looks like any of the six, and that family alone opens a ruling
101
+ for the whole section. One family per section is what keeps a list *inside* a ruling
102
+ inside it, which is ADR-015's four resolution strategies and ADR-013's four bullets.
103
+ A blank line does not close a ruling, because a record's parenthetical about its own
104
+ rulings sits between them. The heading marker and the bullet are structure and do not
105
+ survive into the text.
106
+
107
+ 8. **A section ends at the next heading, but not at one inside a fenced block.** The fence
108
+ marker is structure; its contents are not, and ADR-007's ruling is entirely inside one.
109
+
110
+ 9. **`summary.rulingNumbers` is exported.** It is the join between a supersession and the
111
+ ruling it moves, and nothing else observes it. The `**One.**` ordinal was dropped in
112
+ testing — one line of code — and **every test passed**, because no record in the estate
113
+ has both a word-numbered ruling and a partial supersession. That is ADR-042's failure
114
+ (a declared value nobody executes) inside a fix whose subject is a declared value
115
+ nobody executes, and the field is exported so the next such mutation has something to
116
+ fail against.
117
+
118
+ ## Consequences
119
+
120
+ - `rules why` prints a `partial` line naming every ruling the record lost, because two
121
+ rulings are printed and a record may have six: ADR-021's overturned ruling is the third,
122
+ so the annotation cannot reach it and the status line is all that is left. The same fact
123
+ appears twice by design — once as a line, once on the ruling — because the two print
124
+ different sets, and one copy could not cover both.
125
+ - The JSON payload gains `partial` and `summary.rulingNumbers`, and `rules_why` over MCP
126
+ carries `partial`. Both are additive; `ruled` stays a list of strings, because three
127
+ surfaces read it and the annotation is attached to the text rather than beside it.
128
+ - `supersedes` no longer reports ADR-046 as superseding ADR-021. It supersedes *ruling 3*
129
+ of ADR-021, which is `partial` on the other record. A whole-record supersession is the
130
+ rarer case and the one that has always been reported.
131
+ - `accepted` is now true for fourteen records that were false. Nothing in this repository
132
+ reads the field outside `rules why --json`, where a caller asking "is this in force?" was
133
+ being told no about records that are.
134
+
135
+ ## Rejected
136
+
137
+ - **Model the backward direction too — `supersedes ADR-021 ruling 3`,
138
+ `amends ADR-017 ruling 1`, `refines ADR-013 ruling 1`.** Rejected under ADR-002, with
139
+ the evidence attached: the same edge appears in both records, the passive side is the one
140
+ that must not print a ruling as in force, and parsing only the passive side is what makes
141
+ the anchoring in ruling 3 cheap. The active side is a reader convenience, not a safety
142
+ property, and it would need the `Affects:` paragraph as well as the status because
143
+ ADR-024, ADR-026, ADR-027 and ADR-028 all state it there.
144
+ - **Rewrite the forty-eight records to one spelling.** Rejected: the parser reading five
145
+ shapes is one function; reformatting every record is forty-eight edits to evidence, and
146
+ the `**accepted**` half of the problem was a one-line fix in the reader.
147
+ - **Number the rulings in ADR-016 so `ruling: null` disappears.** Rejected: ADR-045 says
148
+ what it narrows, and ADR-016's narrowing targets a bullet in its `Consequences` section
149
+ rather than one of its six numbered rulings. There is no number to write.
150
+ - **Print every ruling rather than two.** Rejected under ADR-002: it changes the output size
151
+ of every `rules why` in every repository, and no measured defect needs it. A ruling past
152
+ the excerpt cannot mislead a reader who is not shown it; the `partial` line is what makes
153
+ it knowable.
@@ -0,0 +1,102 @@
1
+ # ADR-050 — a byte count is not a region, and `updated` is not a change of rules
2
+
3
+ Status: accepted · 2026-10-09
4
+ **Affects:** `src/upgrade/plan.mjs`, `src/projection/markers.mjs`,
5
+ `src/projection/project.mjs`, `src/projection/targets.mjs`, `src/projection/baselines.mjs`,
6
+ `test/upgrade.test.mjs`, `test/markers.test.mjs`. Closes S26 of
7
+ `docs/reviews/0002-full-review-adoption-and-public-release.md`.
8
+
9
+ ## Context
10
+
11
+ S26's finding was one line of code: the no-region guard in `upgrade` was
12
+ `list.every((t) => t.action === 'added' && !t.previousBytes)`, and a repository whose
13
+ `AGENTS.md` is hand-written prose has `previousBytes > 0`. Reproduced on a repository built
14
+ the way the estate builds one — `init`, then `AGENTS.md` replaced by hand — and `upgrade`
15
+ wrote a 9 KB region into a repository that had never had one, reporting
16
+ *"pin moved and region restamped (the rules changed)"*. No rule had changed; no region had
17
+ existed.
18
+
19
+ Two further defects were in the same command, and neither was in the finding.
20
+
21
+ **The guard as written was not the fix it looked like.** Replacing `previousBytes` with
22
+ "is every region absent" reproduces the same defect, because the estate's version of this
23
+ state leaves the **baselines'** regions in place: `.editorconfig` and `.gitattributes` were
24
+ written by the same `init` and are untouched by the hand-written `AGENTS.md`. A guard
25
+ asking about every target sees two regions, concludes the repository is adopted, and writes
26
+ one. The guard has to ask about the **primary** target, which is the rule `compare` already
27
+ applies — its `region` field is the primary document's, and its problem line for this state
28
+ is *"adopted but AGENTS.md has no generated region"*. Two commands answering "is this
29
+ repository adopted?" differently is the defect S26 and the reason it was easy to write.
30
+
31
+ **`updated` means two things, and this command reads it to explain itself.** ADR-037 made a
32
+ correct region body behind a wrong seam report as `updated`, so `sync` would repair it. That
33
+ was right, and it gave `action` a second meaning: the body moved, *or* the bytes around it
34
+ did. `upgrade`'s whole purpose is to say why a repository is behind, so with
35
+ `action !== 'unchanged'` as the evidence it reports a formatting repair as a change of
36
+ policy — and sends the reader to diff rules that did not move. Measured, with the pin
37
+ already current in all three:
38
+
39
+ | State | Reported |
40
+ | --- | --- |
41
+ | body edited by hand | `the rules changed` — correct |
42
+ | seam wrong, body correct | `the rules changed` — **false** |
43
+ | stamp moved, body identical | `the rules were already correct` — right, but only by accident of ordering |
44
+
45
+ The third row is the second half of the trap. Comparing a whole target — bytes, header and
46
+ all — makes **every** pin bump claim the rules changed, because the region header carries
47
+ the stamp by design.
48
+
49
+ ## Rulings
50
+
51
+ 1. **A region is present or absent; a file is not "small".** `inject` and `diff` return
52
+ `region` (`absent` | `ok` | `malformed`) and `previousBody` alongside `action`, and every
53
+ target carries both. `parse` has always known this; `action` cannot express it and
54
+ `previousBytes` cannot answer it.
55
+
56
+ 2. **Adoption is a question about the primary document.** `upgrade` skips when
57
+ `AGENTS.md` has no region, whatever the baselines hold. This is `compare`'s rule and
58
+ `ADR-016`'s rule about display versus gate, applied to a command that was about to write.
59
+
60
+ 3. **The remedy is `simt-dev sync --yes`,** because `sync` does the thing the skip refused
61
+ to do and nothing else. S26 proposed `simt-dev init`; rejected — `init` re-runs
62
+ adoption, tags and branches, and the remedy would be doing more than the command it is
63
+ the remedy for. `compare` already names `sync --yes` here, and two commands must not
64
+ answer the same question with different instructions.
65
+
66
+ 4. **"The rules changed" is answered by comparing region bodies,** not by reading `action`
67
+ and not by comparing targets. A target is a change to the rules when its previous body
68
+ and the body this simt-dev would write differ, ignoring the leading and trailing
69
+ newlines the region may pick up from the document — the same normalisation `inject`
70
+ uses to decide `unchanged`.
71
+
72
+ 5. **A repair is named as a repair.** `region repaired; the pin and the rules were already
73
+ current` distinguishes a seam fix from a change of policy, because the reader's next
74
+ action differs: one is a formatting commit, the other is a review of what the rules now
75
+ say.
76
+
77
+ ## Consequences
78
+
79
+ - `upgrade` exits non-zero on a region-less repository, and `check` already did. They now
80
+ agree, and the test asserts the exit code rather than tolerating it: a command that names
81
+ the problem and exits 0 is the ADR-023 failure, and this command was about to start doing
82
+ it in a new place.
83
+ - Six mutations were applied and reverted, each caught by its own test. Two of them are
84
+ the finding and its tempting half-measure: falling back to `previousBytes`, and guarding
85
+ on every target instead of the primary one. Both pass the whole suite if the fixture is a
86
+ repository whose `AGENTS.md` is *absent* rather than hand-written — which is why the
87
+ fixture builds the estate's shape instead.
88
+ - `inject`'s and `diff`'s return shape grows two fields. Both are derived from a `parse`
89
+ that already ran, and no caller reconstructs them.
90
+
91
+ ## Rejected
92
+
93
+ - **Repairing the region in `upgrade` rather than skipping.** Rejected: `upgrade` moves a
94
+ pin. Adoption, and the repair of a repository that lost its region, are `init` and `sync`.
95
+ A command that silently performs them during a version bump is a command nobody can
96
+ predict — this module's own header says so.
97
+ - **Comparing `previousBody` to `body` including the header,** which would make the stamp a
98
+ rule change on every pin bump. Rejected: ruling 4's normalisation, and the reason the
99
+ comparison is body-to-body rather than target-to-target.
100
+ - **Making `region` a field on the plan rather than the target.** Rejected under ADR-002:
101
+ the target is what every caller already receives, and a second place computing the same
102
+ fact is how `compare`'s `profiles` ended up declared and never assigned (ADR-045).
@@ -0,0 +1,134 @@
1
+ # ADR-051 — a pointer is the reader's only route, so it is not a convenience
2
+
3
+ Status: accepted · 2026-10-09
4
+ **Affects:** `src/projection/project.mjs`, `src/projection/targets.mjs`,
5
+ `src/commands/sync.mjs`, `test/surfaces.test.mjs`. Closes S09 of
6
+ `docs/reviews/0002-full-review-adoption-and-public-release.md`.
7
+
8
+ ## Context
9
+
10
+ `pointer-drift` was in `NON_BLOCKING`, with a comment saying a misaimed `CLAUDE.md` is *"a
11
+ smaller problem than a red gate on every commit: the editors that matter already read
12
+ AGENTS.md, and the pointer is a convenience."* The first half is true and the second half
13
+ is the defect. Reproduced on a repository that had adopted and then had its pointer moved
14
+ to `README.md`:
15
+
16
+ ```
17
+ check exit 0, three files CURRENT
18
+ compare problems 0 repos
19
+ CLAUDE.md # readme
20
+ AGENTS.md 13 rule citations, none of them read
21
+ ```
22
+
23
+ Claude Code's own documentation, quoted in this repository's `POINTERS[0].verified`, says
24
+ **"Claude reads CLAUDE.md by default and AGENTS.md only as a fallback."** On a repository
25
+ that selected the `claude` surface, a pointer aimed elsewhere means the reader for whom the
26
+ rules were written is reading something else — and the gate said nothing. This is the same
27
+ shape as ADR-018's *"an unset hooks path is not an absent hook"*: the file exists, so the
28
+ thing that reads it is satisfied, and nothing reports that it is reading the wrong bytes.
29
+
30
+ The comment's reasoning came from a real observation about a **different** surface.
31
+ DeepSeek's loader renders a drifted *copy* in full beside its twin, which is duplication.
32
+ A *symlink* aimed elsewhere is not duplication — it is a reader looking at the wrong file.
33
+ The old test named DeepSeek and asserted the asymmetry, so the belief was written down and
34
+ the finding it produced was, by construction, unable to see.
35
+
36
+ A second defect, not in the finding: `pointerState` classified a path as a symlink, a
37
+ regular file, or **absent** — three answers, and a directory matched the third. A directory
38
+ at `CLAUDE.md` is not hypothetical: it is where Claude Code keeps per-project instructions.
39
+ Measured:
40
+
41
+ ```
42
+ check says nothing at all
43
+ sync ERROR 1 EEXIST: file already exists, symlink 'AGENTS.md' -> 'CLAUDE.md'
44
+ REMEDY run: simt-dev doctor
45
+ ```
46
+
47
+ `EEXIST` fires *after* every other target is written, so the run left a half-migrated
48
+ repository, and the remedy it offered reports nothing about a directory. The reason it was
49
+ invisible for as long as it was: the note that would have said so printed
50
+ `POINTER CLAUDE.md: a directory, not a pointer (undefined bytes)`, and `undefined` in a
51
+ report reads as nothing at a glance.
52
+
53
+ ## Rulings
54
+
55
+ 1. **A misaimed pointer blocks `check` when its reader is a surface this repository
56
+ serves.** The gate is where the exit code lives and where CI runs. A pointer whose
57
+ surface was not selected is not reported at all: the reader it serves is not reading
58
+ this repository, and a gate that reddens for it is a gate people learn to ignore.
59
+
60
+ 2. **The finding names the reader.** The remedy is `simt-dev sync --yes (Claude Code reads
61
+ CLAUDE.md, not AGENTS.md)` — because "run the command" does not tell a reader why this
62
+ matters, and a warning that has been learned to skip is worse than no warning.
63
+
64
+ 3. **`pointer-unread`, not `pointer-drift`.** The name is the reader's word for it: the
65
+ pointer is present and unreadable *as the thing it is for*. `pointer-drift` named the
66
+ defect as staleness, which is a file's problem, and that is how a reader-blindness
67
+ arrived at a file-scoped non-blocking rule.
68
+
69
+ 4. **`sync` does not block on it and `check` does, and they are two sets.** The remedy is
70
+ `sync --yes`; a `sync` that refused on this finding would be a gate whose remedy is the
71
+ command being run, which ADR-025 ruling 6 calls worse than a noisy gate. `check` is the
72
+ gate CI runs and the one whose exit code a repository acts on, and there the finding is
73
+ the defect.
74
+
75
+ This was the finding of the slice, and it was found by rendering the output rather than
76
+ by reading the code — the same acceptance `AGENTS.md` sets for rule text. **The first
77
+ version of this fix kept one `NON_BLOCKING` set** shared by both commands, on the
78
+ reasoning that the writer's rule was the correct one. So `check` exited 0 on the exact
79
+ repository this record exists to fix, and a test asserting `check` exited 1 **passed** —
80
+ on a bare scratch directory that was red for a missing `AGENTS.md`. One set covering both
81
+ commands is how the defect survived the first time, which is the sentence worth keeping:
82
+ the rule was written to keep `sync` quiet and quietly quieted `check`'s exit code too.
83
+
84
+ Two sets, both asserted, and the relationship between them asserted as well: everything
85
+ `check` tolerates, `sync` tolerates. The gate is never the laxer one.
86
+
87
+ 5. **A directory is `foreign`, and its measure is entries.** `stat` distinguishes a
88
+ directory from a file before anything is read, because reading a directory as utf8
89
+ throws `EISDIR` and the old catch reported that as *absent* — the one answer that hides
90
+ it. The size reported is what would be lost: bytes for a file, entries for a directory.
91
+
92
+ 6. **`sync` repairs a misaimed pointer, because it is the remedy `check` prints.** The
93
+ pointer is this tool's own output, it is a symlink so it carries no content, and
94
+ `symlink()` at an existing path is `EEXIST` — which is what happened, after every other
95
+ target was written, with `simt-dev doctor` named as the remedy and `doctor` reporting
96
+ nothing about it. A command whose own remedy crashes on the state it names has no
97
+ remedy at all. Only a symlink is unlinked; a regular file or a directory never entered
98
+ the plan, so the repair is keyed on the pointer's **state** and not on the path
99
+ existing.
100
+
101
+ ## Consequences
102
+
103
+ - `SYNC_NON_BLOCKING` and `CHECK_NON_BLOCKING` are two exported sets, and both are asserted
104
+ as sets *and* as a relationship. Three mutations passed the whole suite before that: `check`
105
+ keeping the finding non-blocking (the defect), `sync` dropping it (the ADR-025 hazard), and
106
+ the gate reading the writer's set. The text output is identical in all three, which is
107
+ ADR-035's sentence about promises about a *set* of commands, applied to a set of statuses.
108
+ - Two tests asserted a gate that was red for the wrong reason, and both are fixed the same
109
+ way: "a pointer this repository does not serve stays a note" and "check blocks a mispointed
110
+ pointer" checked `check` on a bare scratch directory, which is red for a missing
111
+ `AGENTS.md`. Both now sync first, so the only thing that can redden the gate is the
112
+ pointer. ADR-031's shape, one level down — and the second instance is the one that matters,
113
+ because it was the test written *for this fix* that passed against the defect unfixed.
114
+ - `sync` keeps its note-and-continue for a foreign pointer, and now says what kind of thing
115
+ is in the way. It repairs a *misaimed* one, and the test asserts that the file the bad
116
+ link pointed at survives — because the repair is a deletion, and a test that only checked
117
+ the link now points where it should would pass if `unlink` had removed the wrong thing.
118
+ - The corpus test that encoded the old belief was **replaced, not amended**, and its
119
+ comment now records what it got wrong and why the observation it was built on does not
120
+ transfer.
121
+
122
+ ## Rejected
123
+
124
+ - **Blocking on a misaimed pointer for every repository.** Rejected: `pointerTargets`
125
+ filters on the selected surfaces, and a repository on `opencode` that has a stray
126
+ `CLAUDE.md` gains nothing. The scoping is not a compromise; it is the same rule the
127
+ pointer table already encodes (ADR-021 ruling 1: a pointer belongs to the surface that
128
+ needs it).
129
+ - **Writing the pointer anyway when it is misaimed.** Rejected: `symlink()` over an
130
+ existing path is `EEXIST`, and unlinking a path someone may have pointed deliberately is
131
+ a deletion this tool does not do (ADR-021 ruling 6).
132
+ - **Treating a directory as `pointer-blocked` with a byte count of zero.** Rejected: the
133
+ count would be a number the reader cannot use, and the report says what is in the way so
134
+ a person can decide.
@@ -0,0 +1,101 @@
1
+ # ADR-052 — a dependency is counted against the budget, and there is one budget
2
+
3
+ Status: accepted · 2026-10-09
4
+ **Affects:** `src/resolve/resolve.mjs`, `src/commands/skills.mjs`, `src/mcp/tools.mjs`,
5
+ `test/resolve.test.mjs`, `test/mcp.test.mjs`. Closes S06 of
6
+ `docs/reviews/0002-full-review-adoption-and-public-release.md`.
7
+
8
+ ## Context
9
+
10
+ S06 filed two defects. Both reproduced; one of them is a different defect now.
11
+
12
+ **A dependency overran the budget by 6.7×.** Both budget loops tested
13
+ `entry.via.size > 0` and, if the entry was a dependency, inlined it regardless of size:
14
+
15
+ ```js
16
+ const isDep = entry.via.size > 0;
17
+ if (flags.all || used + skill.bytes <= budget || isDep) {
18
+ ```
19
+
20
+ Measured against the shipped registry:
21
+
22
+ ```
23
+ $ simt-dev skills brief --skill parallel-feature-team --budget 2000
24
+ resolved: parallel-feature-team (1131), slice-commit (5048), complete-testing (7324)
25
+ = 13,503 bytes against a 2,000-byte budget
26
+ ```
27
+
28
+ Nothing in the output says the budget was exceeded, because the condition that enforced it
29
+ had already been short-circuited. The code's own comment gives the reason — *"a skill that
30
+ is useless without its dependency is worse than a large context, because it silently
31
+ misleads"* — and the premise is correct while the conclusion does not follow from it. A
32
+ missing dependency is fixed by a line in the output; an unbounded budget is not a fix for
33
+ anything.
34
+
35
+ **The two copies had already drifted.** This is the part that had become worse than filed,
36
+ and it is the shape ADR-035 named: a promise about a *set* of readers holds only while
37
+ something checks the set, and here the set was two loops. `skills brief` tested `deferred`
38
+ first and never inlined one; the MCP tool tested the budget first and then had a nested
39
+ `if (isDep || !entry.skill.deferred)`, which **did** inline a deferred dependency. One
40
+ command inlined a 384 KB body, the other did not, and neither could see the other.
41
+
42
+ The review's probe #14 evidence is stale and the record should say so: it measured
43
+ `summary:the` selecting four estate skills, which stopped being true when ADR-043 and
44
+ ADR-044 landed. What is *not* stale is the defect — the `isDep` bypass is in the shipped
45
+ code and 6.7× over budget is measured against today's registry, on a path the review never
46
+ used.
47
+
48
+ ## Rulings
49
+
50
+ 1. **Dependencies are counted against the budget.** No entry gets in because of what it
51
+ is. This is the whole behavioural change and it is one condition.
52
+
53
+ 2. **A group that does not fit is deferred together.** An entry reached through `requires`
54
+ that misses the budget takes its parents with it, to a fixpoint — because a parent shown
55
+ without the thing it requires is precisely the misleading state ruling 1's comment was
56
+ written to avoid. It is now avoided by dropping the parent rather than by ignoring the
57
+ budget. The direction matters and is the whole of it: a small parent does not earn the
58
+ right to appear without its dependency.
59
+
60
+ 3. **`deferred` is a pointer, not a budget failure, so it does not propagate.**
61
+ `deferred` is derived from `deferredThreshold` and means "fetch this with
62
+ `skills show`". `--all` means take everything, and the pointer to a deferred dependency
63
+ is what a pointer is for. Only *budget* pressure moves a parent. Conflating the two is
64
+ how a 384 KB body would end up suppressing an unrelated 300-byte skill.
65
+
66
+ 4. **`selectWithinBudget()` is the whole arithmetic, and there is one of it.** It returns
67
+ `{ picked, deferred, used }`, and `used` is recomputed from what was actually picked
68
+ rather than accumulated during the walk — a parent pulled out by ruling 2 must stop
69
+ counting, because a total that includes a dropped skill reports bytes nobody is paying
70
+ and the header line prints it.
71
+
72
+ 5. **The two surfaces are compared to each other in a test.** Not "each is correct" —
73
+ each asserted against the other's output at two budgets. A parity test is the only thing
74
+ that catches a copy drifting from a copy, and the review's probe for this row caught the
75
+ drift by reading both.
76
+
77
+ ## Consequences
78
+
79
+ - `--skill parallel-feature-team --budget 2000` now resolves nothing and names all four
80
+ deferred skills with `over budget`. At `--budget 20000` it resolves the group whole. The
81
+ reader can raise the budget or ask for one skill; before, it got 13 KB and no explanation.
82
+ - `skills_brief` over MCP and `skills brief` are now the same function, and the test drives
83
+ both at 2,000 and 20,000 bytes and compares selected, deferred and bytes.
84
+ - Six mutations were applied and reverted, each caught: dependencies bypassing the budget,
85
+ the group rule dropped, `deferred` no longer excluded, `deferred` counted against the
86
+ budget, the total no longer being the bytes paid, and the MCP tool not calling the shared
87
+ function.
88
+
89
+ ## Rejected
90
+
91
+ - **Keep the bypass and report the overrun.** Rejected: the output already has a `deferred`
92
+ list with reasons, and a budget that means "unless" is not a budget. If the estate ever
93
+ decides a dependency is worth any number of bytes, that is a flag with a name saying so.
94
+ - **Defer only the dependency and leave its parent.** Rejected: that is the state the old
95
+ comment called *silently misleading*, and it is what ruling 2 exists to stop.
96
+ - **Propagate `deferred` to parents too, for symmetry.** Rejected under ADR-013: the two are
97
+ different signals from different fields — `bytes` against a budget the caller set, and
98
+ `deferred` against a threshold the registry declares — and collapsing them makes a size
99
+ threshold suppress a selection.
100
+ - **Change the default budget or the deferred threshold.** Rejected: nothing here measured a
101
+ case where the budget, rather than its enforcement, was the problem.