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