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,141 @@
|
|
|
1
|
+
# ADR-032 — provenance is a digest of the content that leaves this package, not a commit id
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-10-08 · ruling 1 narrowed by ADR-054
|
|
4
|
+
**Affects:** `src/projection/rules.mjs`, `src/projection/markers.mjs`, `test/upgrade.test.mjs`,
|
|
5
|
+
`test/install.test.mjs`, `test/project.test.mjs`. **Implements:** proposal P1 in
|
|
6
|
+
`docs/reviews/0002-full-review-adoption-and-public-release.md` §10. **Relates to:**
|
|
7
|
+
ADR-031 (a test that inherits the author's directory is not evidence) — the same defect found
|
|
8
|
+
one layer down, in the tool rather than the suite — and ADR-015 (a source is content, not a
|
|
9
|
+
location), which already argued that a declared skill's identity is its bytes.
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Two tests in `test/upgrade.test.mjs` failed in any copy of this repository that had no `.git`:
|
|
14
|
+
a `git archive` export, a tarball, and — the case that matters — the published npm package.
|
|
15
|
+
|
|
16
|
+
The proximate cause was `headSha()`, which ran `git rev-parse --short=7 HEAD` in the package
|
|
17
|
+
root and returned the string `unknown` when git could not answer. That fallback was only the
|
|
18
|
+
second of two ways the stamp was wrong, and the quieter one.
|
|
19
|
+
|
|
20
|
+
**The first way: the stamp recorded the reader's own repository.** `git rev-parse` run inside
|
|
21
|
+
`node_modules/` walks *up* until it finds a `.git`, so for anyone who installed this package
|
|
22
|
+
into a repository — the entire audience for it — the tool stamped the consumer's HEAD. Not a
|
|
23
|
+
near miss: the correct-looking answer to the wrong question. A consumer's HEAD changes on
|
|
24
|
+
every commit they make, including commits that touch nothing this tool projects, so:
|
|
25
|
+
|
|
26
|
+
- the stamp changed under a region that had not changed, and `check` reported `DRIFT` on all
|
|
27
|
+
three generated files plus `STALE-GENERATED-FIELD` on the manifest — **after an unrelated
|
|
28
|
+
commit**, and **exit 1**. A gate that is red on every commit trains people to ignore it,
|
|
29
|
+
which is the failure mode this tool exists to prevent;
|
|
30
|
+
- `upgrade` reported `state behind`, `pin f9036cd -> f3014d5`, and the cause it named —
|
|
31
|
+
*"pin moved and region restamped (the rules changed)"* — was false. A pin that moves because
|
|
32
|
+
the consumer typed a commit message has not detected anything about the rules;
|
|
33
|
+
- no fallback could ever have caught this, because git succeeded and returned a plausible sha.
|
|
34
|
+
|
|
35
|
+
Review 0002 recorded this as probe #2 and slice S01 and it was correctly read there as
|
|
36
|
+
"provenance stamp comes from the consumer's git, not simt-dev". It reproduced exactly.
|
|
37
|
+
|
|
38
|
+
**The second way: where git could not answer, `unknown` was treated as a value.** The old
|
|
39
|
+
comment defended the fallback — "a region stamped with an honest `unknown` is visibly
|
|
40
|
+
unverified while one stamped with a wrong answer is not" — and the defence is sound in
|
|
41
|
+
isolation. It is defeated by the fact that `unknown` was then compared and written like any
|
|
42
|
+
other string:
|
|
43
|
+
|
|
44
|
+
- `files` in `package.json` does not list `.git`, and npm never packs one. **`npm pack`
|
|
45
|
+
produces 98 entries and no `.git`**, so a tarball or a copy extracted outside a repository
|
|
46
|
+
got `unknown`.
|
|
47
|
+
- The marker grammar reads `simt-dev:sha=([0-9a-f]{7,40})`. `unknown` does not match, so
|
|
48
|
+
`parse()` returned `sha: undefined` for a region the tool had just written. **The tool could
|
|
49
|
+
not read back its own stamp**, and `check` reported `recordedSha: null` for a file that
|
|
50
|
+
plainly carried one.
|
|
51
|
+
- `planUpgrade` computed `pinBehind = base.pinnedSha !== sha`. With `sha === 'unknown'` this
|
|
52
|
+
is true for every real pin, so `upgrade` called a repo pinned at `2614462` `behind`, again
|
|
53
|
+
naming the false cause above. `upgrade --yes` then wrote `unknown` into both the region's
|
|
54
|
+
stamp and the manifest pin, and the original pin was unrecoverable.
|
|
55
|
+
|
|
56
|
+
So the identity was never the identity of this tool. It was either the reader's repository or
|
|
57
|
+
a sentinel the tool could not parse — and the review's probe found the first while the test
|
|
58
|
+
suite, written by someone reading a checkout, could not see it.
|
|
59
|
+
|
|
60
|
+
## Ruling
|
|
61
|
+
|
|
62
|
+
**Provenance is a SHA-256 digest of the content that leaves this package, truncated to 40 hex
|
|
63
|
+
characters. It is not a commit id, and there is no fallback.** (Which content, precisely, was
|
|
64
|
+
narrowed after this record was written — see ADR-054.)
|
|
65
|
+
|
|
66
|
+
Three consequences, each of which is the point:
|
|
67
|
+
|
|
68
|
+
1. **The identity exists wherever the content does, and depends on nothing outside the
|
|
69
|
+
package.** `rules/` and `graph/` are in the published tarball, so the digest is computable
|
|
70
|
+
from a checkout, a tarball and an `npm install` alike — one value, no environment to be in.
|
|
71
|
+
This also ends the walk-up: with no `git` invoked at all, there is no ancestor repository
|
|
72
|
+
for the stamp to be borrowed from, so a consumer's own commits can no longer move a pin.
|
|
73
|
+
Verified by the review's own probe, against a package installed inside a repo that then
|
|
74
|
+
commits an unrelated file: before, `check` reported `DRIFT` on three files plus
|
|
75
|
+
`STALE-GENERATED-FIELD` and exited 1; after, three `CURRENT` lines and exit 0.
|
|
76
|
+
2. **A stamp the tool cannot read is a defect, not a degraded mode.** `compose()` now throws
|
|
77
|
+
unless `sha` matches `[0-9a-f]{7,40}`. The write and the read share one grammar by
|
|
78
|
+
construction, so the failure this record describes cannot be reintroduced by adding a
|
|
79
|
+
fallback.
|
|
80
|
+
3. **`unknown` is gone, not guarded.** There is no sentinel to compare against, so the
|
|
81
|
+
comparison that destroyed the pin has nothing to misread. A repo stamped by a version of
|
|
82
|
+
simt-dev older than this record still carries `unknown`, or a consumer commit's sha, and
|
|
83
|
+
`upgrade` now repairs it to the digest — the same path as any other stale pin.
|
|
84
|
+
|
|
85
|
+
### What the digest covers, and what it deliberately excludes
|
|
86
|
+
|
|
87
|
+
> **Superseded by ADR-054.** The exclusion below is drawn around a *directory*, and the
|
|
88
|
+
> shared `.editorconfig` and `.gitattributes` bodies are not in one — they are constants in
|
|
89
|
+
> `src/`, and they are content every adopting repository receives. `src/` as a path is still
|
|
90
|
+
> excluded; the exclusion is now a rule about content rather than about a location.
|
|
91
|
+
|
|
92
|
+
`rules/` and `graph/` are the bytes whose change moves a consumer's projection: the shared
|
|
93
|
+
rules, the per-ecosystem profiles, the Cursor rule files, and the skill registry. `src/` is
|
|
94
|
+
excluded deliberately. A refactor that changes no rule and no registry entry produces
|
|
95
|
+
byte-identical regions; stamping those consumers would report drift where there is none, and
|
|
96
|
+
the estate's own rule is that a report must be a fact.
|
|
97
|
+
|
|
98
|
+
This is the same reasoning as ADR-015: a skill is identified by its bytes, so a projection is
|
|
99
|
+
identified by the bytes it projects.
|
|
100
|
+
|
|
101
|
+
Entries are hashed in sorted path order, each framed as `path:length:` before its bytes. The
|
|
102
|
+
ordering makes the value independent of `readdir` order — determinism is a feature of this
|
|
103
|
+
tool, not a limitation. The framing means two different renames cannot collide into one
|
|
104
|
+
digest, which is why this is not a hash of concatenated text.
|
|
105
|
+
|
|
106
|
+
## Consequences
|
|
107
|
+
|
|
108
|
+
- **Every adopted repo's stamp moves once.** A consumer stamped at a commit sha will report
|
|
109
|
+
`behind` the first time it runs this version, and `simt-dev upgrade` fixes it. That is the
|
|
110
|
+
intended reading of a pin, not a regression.
|
|
111
|
+
- **The two `.git`-less test failures are fixed at the source for one of them.** The stale
|
|
112
|
+
`.mdc` test failed only because it could not find a sha to rewind; it now passes. The other
|
|
113
|
+
(`upgrade --root` at the package root) is a *test* defect and is fixed as one — see below.
|
|
114
|
+
- **`test/upgrade.test.mjs` no longer asserts a property of the author's checkout.** It had
|
|
115
|
+
asserted `summary.repos === 1`, which is only reachable when this checkout happens to be a
|
|
116
|
+
git repo. A `.git`-less package root is a directory holding no repositories, which
|
|
117
|
+
`discover` correctly refuses with exit 3 and a remedy. Both answers are right, so the
|
|
118
|
+
invariant is now stated directly: simt-dev is not a consumer of itself, so no
|
|
119
|
+
`simt-dev.json` appears. Both halves are non-vacuous — planting a manifest fails the first,
|
|
120
|
+
and forcing the no-manifest case to write one fails the second.
|
|
121
|
+
- **S01 is discharged.** Its proposal is P1; this record implements it, so the slice can move
|
|
122
|
+
to Complete against this SHA rather than waiting on a second implementation of the same idea.
|
|
123
|
+
|
|
124
|
+
## Rejected
|
|
125
|
+
|
|
126
|
+
- **Guard the sentinel instead of removing it.** Treat `unknown` as non-comparable: refuse to
|
|
127
|
+
move a pin to or from it, and say so. Smaller diff, and it leaves the tool writing a stamp
|
|
128
|
+
its own parser rejects for every npm consumer. It fixes the data loss and not the lie, and
|
|
129
|
+
it does nothing at all about the walk-up, which was never a sentinel — git succeeded.
|
|
130
|
+
- **Resolve the repository explicitly** — pass `--git-dir`, or run `git rev-parse` with
|
|
131
|
+
`GIT_CEILING_DIRECTORIES`, so the stamp names this package and never an ancestor. This is
|
|
132
|
+
the smallest change that fixes the walk-up, and it is still wrong: in a tarball there is no
|
|
133
|
+
repository to name, so it restores the `unknown` fallback and with it the pin destruction.
|
|
134
|
+
It also keeps the identity meaning "which commit of simt-dev", which is a fact about a
|
|
135
|
+
history a consumer does not have and cannot verify. The ruling is that the stamp answers
|
|
136
|
+
*what content produced these bytes*, not *which commit was checked out at the time*.
|
|
137
|
+
- **Stamp the package version.** `version` is present in the tarball. But it is `0.0.0` here
|
|
138
|
+
and rises only at release, so it cannot distinguish two working trees that project
|
|
139
|
+
differently — which is the case `check` and `upgrade` exist for.
|
|
140
|
+
- **Hash the whole package.** Includes `src/`, with the consequence above, and hashes files
|
|
141
|
+
the consumer never receives.
|
simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR-033 — three plan rows closed by decision, not by authoring
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-10-08
|
|
4
|
+
**Affects:** `docs/plans/0001-centralise-agent-rules.md` §3.1 row 3, §6 phase 6, §6 phase 7.
|
|
5
|
+
Ships no rule text. **Refuses:** the seven agent policies row 3 asks for, the twin collapse
|
|
6
|
+
phase 6 asks for, and untracking the two committed `local.properties` files phase 7 does not
|
|
7
|
+
mention but whose existence made the sweep look unfinished.
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Closing plan 0001 meant auditing each row against the tree rather than against the plan's own
|
|
12
|
+
prose, because the plan was written on 2026-10-05 and three things changed underneath it. This
|
|
13
|
+
record is the audit's residue: three items that read as unfinished work and are not.
|
|
14
|
+
|
|
15
|
+
Row 3 asked for eight agent policies as separate files. Seven were never authored. A search for
|
|
16
|
+
their source material across every tracked `*.md` and `*.mdc` in the estate returns two files,
|
|
17
|
+
neither of which is a policy: they are a progress ledger and a completed UX plan that happen to
|
|
18
|
+
contain the phrases. The three most specific of the eight — the 70/30 quality-versus-performance
|
|
19
|
+
split, the 12-state coverage list, the matrix-trigger heuristic — have no source anywhere. Writing
|
|
20
|
+
them would mean inventing doctrine this estate never agreed to, into `rules/core.md`, from where
|
|
21
|
+
every adopted repo would inherit it.
|
|
22
|
+
|
|
23
|
+
Phase 6 asked to "collapse the 7 drifted Social skill twins to canonical + stub". The registry's
|
|
24
|
+
`sources.role` mechanism landed the same evening, two hours after the plan was authored, and
|
|
25
|
+
answers the row directly. All three resolvers select `role === 'canonical'` and nothing else —
|
|
26
|
+
`src/commands/skills.mjs` twice, `src/commands/skillsinstall.mjs:291`, `src/mcp/tools.mjs:192` — so
|
|
27
|
+
a copy declared `duplicate` or `variant` is never installed. The one pair that genuinely had to be
|
|
28
|
+
collapsed, `add-country-galaxy`, is already a stub in `repo-14` pointing at the API repo.
|
|
29
|
+
|
|
30
|
+
Phase 7 asked for a sweep of hardcoded absolute paths. The sweep found 17 tracked files. Six were
|
|
31
|
+
fixed. The two Android `local.properties` files were not, and were nearly fixed by reflex.
|
|
32
|
+
|
|
33
|
+
## Rulings
|
|
34
|
+
|
|
35
|
+
1. **A row is closed by ruling on it when the thing it asks for was never a decision anyone made.**
|
|
36
|
+
Row 3's eight policies are closed: the four claims `rules/core.md` already carries — the
|
|
37
|
+
minimum viable team, never weakening a security control, anti-overclaim, and a stated agent
|
|
38
|
+
budget that never converts a failure into an approval — are the doctrine, and they ship. The
|
|
39
|
+
other four, including the 70/30 split, the 12-state list and the matrix-trigger heuristic, are
|
|
40
|
+
recorded here as unmade. If one is wanted it needs someone who held the original
|
|
41
|
+
conversation, not someone reconstructing it from a table cell.
|
|
42
|
+
|
|
43
|
+
2. **A mechanism that supersedes a row retires the row, and the deletion is the wrong move.** The
|
|
44
|
+
`sources.role` vocabulary already distinguishes `canonical`, `duplicate`, `variant`, `stub` and
|
|
45
|
+
`deprecated`, and `graph lint` passes with all of them in use. Deleting a `duplicate` copy would
|
|
46
|
+
remove a version-pinned tool from a repo that can still run it, and nothing would detect the loss
|
|
47
|
+
because resolution never read those bytes. Phase 6's twin collapse is therefore closed as
|
|
48
|
+
already answered, at `a9983c8`.
|
|
49
|
+
|
|
50
|
+
3. **`local.properties` stays tracked.** Both `repo-05` and `repo-03` commit it, and both
|
|
51
|
+
`.gitignore` files have the conventional `# local.properties` line commented out at lines 22 and
|
|
52
|
+
24 — a deliberate override of Android's own convention, not an oversight. Untracking them would
|
|
53
|
+
be a house-style change made while closing a plan, and a build machine that checks out without
|
|
54
|
+
an SDK would stop building. It stays, and plan 0001 says so, so the next sweep does not
|
|
55
|
+
rediscover it as a defect.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
Row 3 reads "done, four of eight" rather than "partial". Phase 6's twin item reads "superseded"
|
|
60
|
+
rather than "outstanding". The estate keeps two `local.properties` files whose `sdk.dir` names a
|
|
61
|
+
developer's home directory, by decision.
|
|
62
|
+
|
|
63
|
+
The registry's byte counts moved with the two skill bodies corrected in the same pass, because a
|
|
64
|
+
declared source's identity is its size (ADR-015): `cloudflare` 384277 → 384869 and
|
|
65
|
+
`production-quality-review` 5869 → 5904. That changed the provenance digest (ADR-032), so all
|
|
66
|
+
fourteen adopted repos were re-stamped afterwards rather than left pinning a value this tool can no
|
|
67
|
+
longer produce.
|
|
68
|
+
|
|
69
|
+
## Rejected
|
|
70
|
+
|
|
71
|
+
**Authoring the seven agent policies.** The plan's one-line summaries are not enough to write seven
|
|
72
|
+
files of rule text, and these rules reach every adopted repo. This is ADR-030's exact shape: an
|
|
73
|
+
unmade decision is not a gap to fill by invention.
|
|
74
|
+
|
|
75
|
+
**Collapsing `graphify` and `parallel-feature-team`.** Both are declared `duplicate` with matching
|
|
76
|
+
byte counts, so they look like the obvious candidates. `graphify` is `vendored: true` — an
|
|
77
|
+
84 534-byte, ten-file, version-pinned tool present in both one repository on purpose. Deleting
|
|
78
|
+
repo-14's copy would leave nothing to detect the loss, because resolution only ever read the
|
|
79
|
+
canonical copy.
|
|
80
|
+
|
|
81
|
+
**Untracking `local.properties`.** An Android SDK path is machine state, and two repos chose to
|
|
82
|
+
share it. That is the repo's call to reverse, not a plan's defect sweep.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ADR-034 — A staged tool refuses a partially staged file rather than staging around it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every repo with a `pre-commit` hook
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
`src/hooks/gates.mjs` defines a `staged` gate as one that "run[s] a formatter over the STAGED
|
|
8
|
+
FILES ONLY, then re-stage[s] them", and gives the reason: whole-repo formatting from a
|
|
9
|
+
pre-commit hook "would rewrite unstaged work the author did not ask to touch". (The rule
|
|
10
|
+
that hooks own format and lint for staged files is ADR-004; this record settles what the
|
|
11
|
+
dispatcher does when "staged files" is ambiguous, which ADR-004 does not say.)
|
|
12
|
+
|
|
13
|
+
The dispatcher did not implement that promise. After a passing `format` gate it ran
|
|
14
|
+
`git add` on **every** path from `git diff --cached --name-only`, and reported success
|
|
15
|
+
whenever anything was staged.
|
|
16
|
+
|
|
17
|
+
Two defects, both measured against the shipped dispatcher rather than reasoned about:
|
|
18
|
+
|
|
19
|
+
- **A partial stage was destroyed outright.** With `a.md` staged as `one\nSTAGED\n` and the
|
|
20
|
+
working tree at `one\nSTAGED\nUNSTAGED-WORK\n`, a formatter that rewrites its inputs left
|
|
21
|
+
**both** the index and the working tree as `FORMATTED`. The author's unstaged hunk was not
|
|
22
|
+
merely committed — it was replaced. This is the promise above, broken on the one path that
|
|
23
|
+
runs on every commit in every adopted repo, and it is silent.
|
|
24
|
+
- **A staged deletion silently skipped the restage.** `--name-only` with no `--diff-filter`
|
|
25
|
+
lists deleted paths, `git add -- <deleted>` fails, and the `catch` returned `false`. A
|
|
26
|
+
commit containing a deletion plus a formattable file recorded the **unformatted** bytes and
|
|
27
|
+
printed no restage line at all.
|
|
28
|
+
|
|
29
|
+
A third, smaller one: restage reported "re-staged files the formatter rewrote" whenever
|
|
30
|
+
anything was staged, whether or not a byte moved.
|
|
31
|
+
|
|
32
|
+
## Ruling
|
|
33
|
+
|
|
34
|
+
**1. A staged gate refuses to run when a file it would format also has unstaged changes.**
|
|
35
|
+
It fails the commit and names the file. It does not stash, and it does not format anyway.
|
|
36
|
+
|
|
37
|
+
The author staged part of a file deliberately; `git add -p` is how you say "this commit is
|
|
38
|
+
part of that file". A hook cannot know which part, so any answer it picks is a guess about
|
|
39
|
+
someone's intent, and a guess that rewrites their working tree is the one they cannot see
|
|
40
|
+
coming. Failing closed with a remedy the author can act on — `stage the whole file`, or run
|
|
41
|
+
the formatter yourself and stage the result — costs one command. Guessing costs work.
|
|
42
|
+
|
|
43
|
+
**2. Restage covers the files the formatter was actually given, and only those.** The
|
|
44
|
+
resolved staged tool's `argv` already holds the paths; nothing needed re-deriving them from
|
|
45
|
+
the index. For a gate declared through `gates.commands`, the command line is arbitrary and may
|
|
46
|
+
take a directory or a glob rather than paths, so ruling 1 applies to **every** staged file with
|
|
47
|
+
unstaged changes, because there is no way to know which files it will open. That is the
|
|
48
|
+
conservative reading, and being wrong in that direction fails a commit the author can retry.
|
|
49
|
+
|
|
50
|
+
**3. Restage reports a file only when that file's staged bytes actually moved**, compared as
|
|
51
|
+
the blob hash before and after. A restage line that fires on every commit is a line nobody
|
|
52
|
+
reads; the ones that fire are the ones that will be read.
|
|
53
|
+
|
|
54
|
+
A path staged as **deleted** is not a partial stage. The index and the working tree agree it
|
|
55
|
+
is gone, so there is no unstaged work to protect and nothing to refuse.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
- `restage` returns the files it staged rather than a boolean, and a failed `git add` is now
|
|
60
|
+
reported as a `restage` **failure** with a remedy. It was previously swallowed by a `catch`
|
|
61
|
+
that returned `false`, which is what made defect 2 invisible.
|
|
62
|
+
- `resolveGate` now returns `declared: true` for a `gates.commands` resolution. The dispatcher
|
|
63
|
+
needed to distinguish argv it chose from words it was handed, and sniffing the `detail`
|
|
64
|
+
string would have let the guard in ruling 2 quietly stop working the day that wording
|
|
65
|
+
changed.
|
|
66
|
+
- Repos whose authors habitually commit partially staged files will now see the gate refuse
|
|
67
|
+
once, with a remedy naming the file, and not again. That is the intended cost: it is
|
|
68
|
+
visible, and it is a command they choose to run.
|
|
69
|
+
- `rules/core.md` inherits nothing new — this changes when a hook runs, not what an agent is
|
|
70
|
+
told to do — so the provenance digest is unaffected and no repo needs re-stamping.
|
|
71
|
+
|
|
72
|
+
## Rejected
|
|
73
|
+
|
|
74
|
+
- **Stash the unstaged changes, format, restore.** The review offered this as an alternative.
|
|
75
|
+
It is a hook making the same guess by a longer route: it assumes the stash will apply
|
|
76
|
+
cleanly after a formatter has rewritten the file underneath it, and a conflict there
|
|
77
|
+
leaves the author with a stash and no working copy.
|
|
78
|
+
- **Format the staged content rather than the working tree** (e.g. piping `git show :file`
|
|
79
|
+
through the formatter and updating the index). Correct in principle, and it does not destroy
|
|
80
|
+
unstaged work — but it leaves the index holding a file the working tree no longer matches,
|
|
81
|
+
so the next commit sees a phantom edit. Worth revisiting only with a real consumer of the
|
|
82
|
+
format, which no repo in this estate has.
|
|
83
|
+
- **Skip the restage for deleted paths.** Treating a deletion as a restage failure would make
|
|
84
|
+
the common case of a renamed-or-deleted file block every commit that also formats.
|
|
85
|
+
- **Keep the boolean return and just filter the file list.** That leaves defect 2's swallowed
|
|
86
|
+
`catch` in place: the command would silently record unformatted bytes again the next time
|
|
87
|
+
any `git add` fails.
|
|
88
|
+
|
|
89
|
+
Review 0001 §`S08` attributes the "would rewrite unstaged work" promise to ADR-004. It is in
|
|
90
|
+
`src/hooks/gates.mjs:7`, the gate definition; ADR-004 settles who runs what, not what the
|
|
91
|
+
dispatcher does with an ambiguous index.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ADR-035 — One place decides whether a run may write, and one document says whether it did
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every command that writes or reports
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0001's S10 tabulated eight inconsistencies in the `--yes` / `--dry-run` / `--json`
|
|
8
|
+
contract across six commands. Each was measured against the shipped binary before anything
|
|
9
|
+
was changed, and the table did not survive intact: **six were real, one was partly wrong, one
|
|
10
|
+
was inverted, and one defect the table missed turned up during the fix.**
|
|
11
|
+
|
|
12
|
+
The six, all reproduced:
|
|
13
|
+
|
|
14
|
+
- `upgrade --yes --dry-run` **wrote**. `upgrade.mjs` tested `flags.yes === true` and never
|
|
15
|
+
consulted `flags.dryRun`, so the one flag that says "do not write" was not read on the one
|
|
16
|
+
path that moves pins across every adopted repo.
|
|
17
|
+
- `skills install --yes --json` **never wrote**. An early `if (isJson(flags)) { emit(plan);
|
|
18
|
+
return }` returned before the write — the exact combination a script uses.
|
|
19
|
+
- `skills install --yes` printed `note dry-run nothing written` and *then* wrote, because the
|
|
20
|
+
note was emitted unconditionally.
|
|
21
|
+
- `skills install` read **neither** write flag. `grep -rn "dryRun"` over the file returned
|
|
22
|
+
nothing.
|
|
23
|
+
- `doctor --json` exited **0** where the same command in a terminal exited **3** on an
|
|
24
|
+
unparseable workspace config, and omitted the workspace answer entirely: its `isJson`
|
|
25
|
+
branch returned *before* the `ws.error` check with `ok` hardcoded `true`.
|
|
26
|
+
- `hooks status --json` wrote `ok: true` and exited **0** while text mode exited **1** with
|
|
27
|
+
four problems listed.
|
|
28
|
+
|
|
29
|
+
The one the table got backwards: it claimed `sync` and `init` emit JSON errors while `hooks`,
|
|
30
|
+
`skills` and `rules` emit only stderr text. Forcing genuine error paths showed `sync --json`
|
|
31
|
+
emits a JSON error correctly. The actual error-path lie was the `doctor` row above.
|
|
32
|
+
|
|
33
|
+
`init --json` looked like the same defect and is not one. `grep -rn "resolveWorkspace"
|
|
34
|
+
src/commands/` shows `init` never consults the workspace at all, and both modes agree — exit
|
|
35
|
+
`0`, `ok: true`, and no workspace line in text either. Two modes answering one question two
|
|
36
|
+
different ways is the defect; when neither asks the question there is nothing to disagree
|
|
37
|
+
about. Making `init` fail on a corrupt workspace config would be a new decision, not this one.
|
|
38
|
+
|
|
39
|
+
## Ruling
|
|
40
|
+
|
|
41
|
+
**1. Whether a run may write is decided in exactly one function.**
|
|
42
|
+
`writeMode(flags)` in `src/output/index.mjs` returns `{ dryRun, apply }` with
|
|
43
|
+
`apply = flags.yes === true && !dryRun`, and every mutating command reads it. A command that
|
|
44
|
+
consults `flags.yes` directly is a defect whether or not its arithmetic happens to agree
|
|
45
|
+
today, because the third copy is the one that will not.
|
|
46
|
+
|
|
47
|
+
**2. `dryRun` is reported separately from `apply`.** A run that was not authorised is a dry
|
|
48
|
+
run in effect even when nobody asked for one, and the JSON payload's `dryRun` says so.
|
|
49
|
+
|
|
50
|
+
**3. Under `--json`, stdout is exactly one JSON document, and its `ok` agrees with the exit
|
|
51
|
+
code.** A caller reads `ok`; a shell reads the exit code. If they disagree, one is a lie and
|
|
52
|
+
the caller cannot tell which, so the invariant is checked across the whole command surface
|
|
53
|
+
rather than per command.
|
|
54
|
+
|
|
55
|
+
**4. A non-zero exit under `--json` carries `error` and `remedy` in that same document.**
|
|
56
|
+
`ok: false` alone says it failed without saying what to do, and ADR-010 requires the remedy.
|
|
57
|
+
`doctor --json` also carries `workspace`, because the text path prints the workspace answer
|
|
58
|
+
and a JSON document that omits it answers a different question than the terminal does
|
|
59
|
+
(ADR-014).
|
|
60
|
+
|
|
61
|
+
**5. A caveat is a field, not prose on stdout.** `skills brief` with no task and no staged
|
|
62
|
+
files warns rather than silently answering; under `--json` that warning is a `warning` field
|
|
63
|
+
in the one document, and `--all` carries it too. `ok` stays `true` — the command did what was
|
|
64
|
+
asked, and it is the *answer* that is weak.
|
|
65
|
+
|
|
66
|
+
**6. Every command that reports honours `--json`, including the ones the review did not
|
|
67
|
+
name.** `skills why` built its output as lines and had no `isJson` branch at all, so
|
|
68
|
+
`skills why <skill> --json` printed prose. Its `gate` is a field rather than a sentence
|
|
69
|
+
because a skill that can never be selected in this repo is a decision, not a line of output.
|
|
70
|
+
|
|
71
|
+
## Consequences
|
|
72
|
+
|
|
73
|
+
`test/contract.test.mjs` is a new file because neither contract belongs to one command: a
|
|
74
|
+
test that runs `doctor` and a test that runs `skills install` would each assert their own
|
|
75
|
+
command and miss the other. It drives the shipped binary rather than calling functions,
|
|
76
|
+
because a flag the dispatcher never routes is invisible to a direct unit test — which is how
|
|
77
|
+
`--dry-run` reached `skills install` unreached while every unit test passed. Nineteen
|
|
78
|
+
commands are asserted to emit one parseable document whose `ok` matches the exit code; two
|
|
79
|
+
mutating commands are asserted to leave the filesystem byte-identical without `--yes` and
|
|
80
|
+
under `--dry-run`; and one test asserts an authorised, non-rehearsal run **does** write, so
|
|
81
|
+
"refuse everything" cannot pass.
|
|
82
|
+
|
|
83
|
+
Two of those tests were themselves broken when first written, and both were found only
|
|
84
|
+
because a mutation failed to fail them. One watched a path that nothing writes to, and in
|
|
85
|
+
doing so **self-adopted simt-dev**: `skills install` reads `flags.root ?? process.cwd()`, the
|
|
86
|
+
row passed the fixture root positionally to a subcommand that takes no positional, and the
|
|
87
|
+
install landed in this checkout, leaving an untracked `.agents/` and `simt-dev.json`. The
|
|
88
|
+
other asserted a fixture was a clean repo when it was not — `init` writes `simt-dev.json`
|
|
89
|
+
without staging it, so `dirtyPaths` was never empty and the no-input branch was never
|
|
90
|
+
reached. Both are the shape of harm ADR-031 exists to prevent, arrived at from the other
|
|
91
|
+
direction. The fixture now commits what `init` wrote and asserts the tree is clean, passes
|
|
92
|
+
`--root` to every command, and asserts the root is under the temp directory.
|
|
93
|
+
|
|
94
|
+
Six mutations were applied and reverted, each caught by its own test: `upgrade` and
|
|
95
|
+
`skills install` back to reading `flags.yes` alone, `hooks` and `doctor` back to a hardcoded
|
|
96
|
+
`ok: true`, and the `isJson` guard removed from the `skills brief` no-input branch and from
|
|
97
|
+
`skills why`. The `sync --yes` requirement is **not** part of this record; it is review 0002's
|
|
98
|
+
S24 and stays queued, because `sync` already honours `--dry-run` and what it lacks is the
|
|
99
|
+
authorisation gate, which is a separate decision.
|
|
100
|
+
|
|
101
|
+
## Rejected
|
|
102
|
+
|
|
103
|
+
- **Make `sync` require `--yes` as part of this record.** It is a real inconsistency and it is
|
|
104
|
+
someone else's slice. S24 names S10 as its dependency, which is what makes S10 the
|
|
105
|
+
prerequisite rather than half of it.
|
|
106
|
+
- **Keep a per-command `ok` field and rely on reviewers to notice disagreements.** The
|
|
107
|
+
disagreement is the defect. It has to be checked where it is introduced, which is a table
|
|
108
|
+
over the surface, not a discipline applied to each command in turn.
|
|
109
|
+
- **Add an `--json` mode to `skills why` by routing it through the text renderer and parsing
|
|
110
|
+
the result back.** Printing prose and parsing it is how the no-input path went wrong: the
|
|
111
|
+
warning was in the output shape and the document was nowhere.
|
|
112
|
+
- **Treat a missing `workspace` in `doctor --json` as cosmetic.** The text path prints the
|
|
113
|
+
workspace answer; a JSON document that omits it is a second, quieter answer to the same
|
|
114
|
+
question, and the caller has no way to know one of them is missing.
|
|
115
|
+
- **Record the review's table as the defect list.** Two of its eight rows did not survive
|
|
116
|
+
measurement — one was backwards and one was not reproducible — and one real defect
|
|
117
|
+
(`skills why --json`) was absent from it. A table of claimed defects is not a list of the
|
|
118
|
+
actual ones, and writing one down as if it were is how the `skills why` gap survived.
|
|
119
|
+
</content>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ADR-036 — A command that writes says so, and every instruction that names it moves with it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects `sync`, every remedy naming it, and the
|
|
4
|
+
generated-region note shipped into every consumer repository
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
ADR-035 gave the command surface one write decision — `writeMode(flags)` — and
|
|
9
|
+
deliberately left `sync` alone, recording the omission twice: in its consequences ("it is
|
|
10
|
+
review 0002's S24 and stays queued") and in its rejected list ("it is a real inconsistency
|
|
11
|
+
and it is someone else's slice"). The reasoning was sound then. S24 depends on S10, so
|
|
12
|
+
making the gate part of S10 would have made the prerequisite half of the change it
|
|
13
|
+
prerequisite'd.
|
|
14
|
+
|
|
15
|
+
Measured against the shipped binary, `sync` was the last writer writing by default. A
|
|
16
|
+
fixture repository was adopted with `--yes`, a stray line was then inserted inside its
|
|
17
|
+
generated region, and a bare `simt-dev sync` **rewrote the file**: the stray line was gone
|
|
18
|
+
and the file was 14 bytes shorter. It reported success, because it had done what it was
|
|
19
|
+
built to do. Among the six writers in the surface, the other five preview unless told
|
|
20
|
+
otherwise, so this was also the only command where an agent that had learned the
|
|
21
|
+
interface would pattern-match onto the wrong behaviour — the preview was opt-in by command
|
|
22
|
+
line and opt-out by habit.
|
|
23
|
+
|
|
24
|
+
The second finding was less comfortable. Twenty-six places in `src/` name `simt-dev sync`
|
|
25
|
+
in a remedy, and two of them are not printed to a terminal at all: the note inside every
|
|
26
|
+
generated region, which ships into every consumer's `AGENTS.md`, and the `$comment` in
|
|
27
|
+
every consumer's manifest. Both instruct the reader to run the command that regenerates the
|
|
28
|
+
text they are looking at. Changing the write gate without changing them would have shipped
|
|
29
|
+
an instruction that is no longer true — and it would have shipped it *to the repository
|
|
30
|
+
being written*, which is precisely the reader with no other context. A gate that only
|
|
31
|
+
protects the tool from itself is not a gate on what the tool says.
|
|
32
|
+
|
|
33
|
+
## Ruling
|
|
34
|
+
|
|
35
|
+
**1. `sync` decides whether to write through `writeMode(flags)`, like every other writer.**
|
|
36
|
+
Not its own condition, and not a check of `--check` and `--dry-run` read separately. ADR-035
|
|
37
|
+
already ruled that a second copy of the decision is a defect whether or not its arithmetic
|
|
38
|
+
agrees today; this is the copy.
|
|
39
|
+
|
|
40
|
+
**2. A bare `sync` is a check that prints the plan, and a check that finds drift exits
|
|
41
|
+
non-zero.** It exits 0 only when nothing would change, which is what lets a caller run
|
|
42
|
+
`sync` in place of `sync --check` and get the same verdict. The preview names the command
|
|
43
|
+
that applies it. Exiting 0 after reporting a change would be the one-question-two-answers
|
|
44
|
+
shape ADR-035 exists to prevent, reached from the opposite direction.
|
|
45
|
+
|
|
46
|
+
**3. `--json` agrees with the exit code, as it must.** `ok` is `false` on a previewing run
|
|
47
|
+
that found work, `written` is `0`, and `dryRun` is `true`. `written` used to be a boolean
|
|
48
|
+
and is now a count, because a preview that says "nothing written" in a field typed as a
|
|
49
|
+
flag cannot distinguish "no changes" from "changes withheld".
|
|
50
|
+
|
|
51
|
+
**4. Every string that names the command moves with the behaviour, including the ones that
|
|
52
|
+
ship.** The region note, the manifest comment, all 26 remedies, and the help synopsis.
|
|
53
|
+
This is the part that is easy to skip and expensive to miss: the two shipped strings are
|
|
54
|
+
compared by `upgrade` and written into files the reader owns, so a change to the gate with
|
|
55
|
+
no change to them leaves a permanently-false instruction in every repository in the estate.
|
|
56
|
+
|
|
57
|
+
**5. The test asserts the flag, not the command.** The existing guard read
|
|
58
|
+
`/simt-dev sync/` and passed identically before and after this change — it could not have
|
|
59
|
+
prevented the defect it appeared to cover. It now reads `/simt-dev sync --yes/`, and a new
|
|
60
|
+
test walks `src/` for any occurrence of the bare spelling so the next command to grow an
|
|
61
|
+
authorisation flag cannot inherit twenty-six no-op remedies unnoticed.
|
|
62
|
+
|
|
63
|
+
## Consequences
|
|
64
|
+
|
|
65
|
+
The contract table in `test/contract.test.mjs` gains a `sync` row, which is the row that
|
|
66
|
+
would have caught this defect: every writer is asserted to leave the filesystem
|
|
67
|
+
byte-identical without `--yes`. `sync` needs stale region bytes rather than a stale pin,
|
|
68
|
+
because what it writes is the region and the pin was already current — a fixture that
|
|
69
|
+
staled the wrong file would have passed a `sync` that wrote nothing for the wrong reason,
|
|
70
|
+
which is the shape ADR-031 rules out. For `sync` alone the unauthorised case also asserts a
|
|
71
|
+
non-zero exit, because the byte comparison alone passes on a preview that returned success.
|
|
72
|
+
|
|
73
|
+
Three mutations were applied and reverted, each caught by its own test: the write gate back
|
|
74
|
+
to reading `--check` and `--dry-run` directly, the preview back to exiting 0 on drift, and
|
|
75
|
+
one remedy back to the bare spelling. The third matters most — it is what proves the
|
|
76
|
+
remedy test sees individual occurrences rather than the template literal around them.
|
|
77
|
+
|
|
78
|
+
One item in this slice's spec could not be done. It asks for a CHANGELOG entry, and there
|
|
79
|
+
is no CHANGELOG in this repository; the row that would add one is queued as S18 and is
|
|
80
|
+
named as its dependency here rather than created, because inventing the file would settle
|
|
81
|
+
a question S18 has not been closed on.
|
|
82
|
+
|
|
83
|
+
## Rejected
|
|
84
|
+
|
|
85
|
+
- **Keep `sync --check` as the only no-write mode and teach the difference in help text.**
|
|
86
|
+
Two spellings for one behaviour means every caller has to know which one it wants, and
|
|
87
|
+
the one that writes by default is the one a caller reaches for. Help text is not a gate;
|
|
88
|
+
it is only evidence that a gate was considered.
|
|
89
|
+
- **Make `sync` preview only when it detects a non-interactive caller.** The tool cannot
|
|
90
|
+
tell an agent from a terminal, and the argument for the flag is that guessing wrong
|
|
91
|
+
destroys work — which is not a cost worth paying for a convenience nobody asked for.
|
|
92
|
+
- **Exit 0 on a preview so that existing scripts keep passing.** This preserves callers
|
|
93
|
+
that were checking whether `sync` *ran*, not whether the repository was *current*. The
|
|
94
|
+
second is the question every one of them was actually asking, and the answer was `ok:
|
|
95
|
+
true` while a file was rewritten.
|
|
96
|
+
- **Update only the terminal output and leave the shipped region note and manifest comment
|
|
97
|
+
alone, since they are documentation.** They are the only copy of the instruction a
|
|
98
|
+
consumer repository's reader ever sees. Changing them is the whole second half of this
|
|
99
|
+
slice.
|
|
100
|
+
- **Fold this into ADR-035 by editing its ruling in place.** ADR-035 is accepted and its
|
|
101
|
+
rejected list names this exact change as someone else's slice. Rewriting the ruling would
|
|
102
|
+
erase the evidence that the omission was deliberate, and a reader diffing the two
|
|
103
|
+
records would have no way to tell which writer had been considered.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ADR-037 — `unchanged` means writing would not change a byte, not that the body matches
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every generated region, in every file style
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0002's S29 reported that a region appended after a single newline fails
|
|
8
|
+
`prettier --check`, and pointed at `markers.mjs:232`, the `absent` branch. Measured against
|
|
9
|
+
the shipped binary, that branch was already correct: it inserts the blank line, and its
|
|
10
|
+
output passes `prettier --check` unchanged.
|
|
11
|
+
|
|
12
|
+
The defect was two branches over.
|
|
13
|
+
|
|
14
|
+
**The seam in `splice` had no blank line.** Every fixture built by putting a region into a
|
|
15
|
+
document that already had one — a paragraph, a heading, a list, a fenced block, a
|
|
16
|
+
blockquote — came back rewritten by Prettier 3.8.1, and `--check` exited 1 on every one.
|
|
17
|
+
The list case is worse than a missing blank line: with no gap, Prettier reads the region as
|
|
18
|
+
a continuation of the last list item and indents all of it, so the repair is not one byte
|
|
19
|
+
but a reindent.
|
|
20
|
+
|
|
21
|
+
**A body that matched behind a wrong seam was reported `unchanged`.** The comparison was on
|
|
22
|
+
the region body alone, so a file whose body was correct and whose seam was not produced no
|
|
23
|
+
drift. `check` found nothing, `sync` had nothing to rewrite, and the repository stayed red
|
|
24
|
+
on its own formatter with no remedy this tool offers. This is the shape ADR-034 and
|
|
25
|
+
ADR-036 both exist to prevent, and it is why the second half of the fix mattered more than
|
|
26
|
+
the first: without it, the seam fix would have applied only to files whose body happened to
|
|
27
|
+
change, and every already-adopted repository would have kept a seam nothing would repair.
|
|
28
|
+
|
|
29
|
+
Measured end to end: a repo whose region body is current and whose seam is wrong exits 1 on
|
|
30
|
+
`check`, `sync` rewrites 11,217 → 11,218 bytes, Prettier is then satisfied, and `check`
|
|
31
|
+
exits 0.
|
|
32
|
+
|
|
33
|
+
## Ruling
|
|
34
|
+
|
|
35
|
+
**1. `seam(before)` is the one place the separator is decided.** It normalises whatever
|
|
36
|
+
follows hand-written text to a blank line, and returns `''` for an empty document so a new
|
|
37
|
+
file does not open with a blank line. The `absent` branch had a hand-rolled equivalent that
|
|
38
|
+
agreed with it exactly; keeping both would have left the next reader looking at the branch
|
|
39
|
+
that looks wrong and is not.
|
|
40
|
+
|
|
41
|
+
**2. `unchanged` means writing would not change a byte.** Not that the body matches. The
|
|
42
|
+
question `sync` asks is whether it has anything to write, and a file whose seam disagrees
|
|
43
|
+
has something to write even when its body does not.
|
|
44
|
+
|
|
45
|
+
**3. Top placement is exempt from the byte comparison, and the reason is recorded.** There,
|
|
46
|
+
`splice` is a repair: it moves a region that sits below the file's own settings back above
|
|
47
|
+
them, which moves bytes outside the markers by design. A misplaced region is reported by
|
|
48
|
+
`check` as `misplaced` with its own remedy, and re-laying-out the file there was correct
|
|
49
|
+
before this change and stays correct. Making the exemption explicit is the point — the
|
|
50
|
+
alternative is to discover the reason by breaking `baselines.test.mjs`.
|
|
51
|
+
|
|
52
|
+
**4. A repair must settle.** After `sync` rewrites the seam, the next run reports
|
|
53
|
+
`unchanged` byte for byte. A rule that reports drift on a file nobody has touched is a
|
|
54
|
+
defect with the sign flipped, and the test asserts the second run rather than the first.
|
|
55
|
+
|
|
56
|
+
**5. The assertions are about bytes, not about Prettier.** Prettier cannot be a dependency
|
|
57
|
+
of this package (ADR-012), so the tests state the shape the formatter requires and cite the
|
|
58
|
+
measurement in a comment. Every fixture in the seam table was run through
|
|
59
|
+
`prettier --check` before the assertion was written, and every one failed.
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
`test/markers.test.mjs` gains two tests. The first walks six shapes of preceding text and
|
|
64
|
+
asserts exactly one blank line — not a run of them, because a formatter collapses those
|
|
65
|
+
and `check` must not manufacture drift out of a file nobody touched. The second covers the
|
|
66
|
+
`unchanged` verdict, which no formatter-shaped assertion could reach.
|
|
67
|
+
|
|
68
|
+
The second test was itself wrong on its first version and a mutation found it. It built its
|
|
69
|
+
fixture from a hand-written region that lacked the do-not-edit note, so the body genuinely
|
|
70
|
+
differed — meaning it passed with the byte comparison removed, while claiming to police it.
|
|
71
|
+
It now builds the fixture from a real composed region and deletes the blank line, which is
|
|
72
|
+
the file the defect actually produced.
|
|
73
|
+
|
|
74
|
+
Two mutations were applied and reverted, each caught by its own test: `seam` reduced to a
|
|
75
|
+
single newline, and the byte comparison dropped from the verdict.
|
|
76
|
+
|
|
77
|
+
The second half of the spec's acceptance — the pack-install test running `format:check` in
|
|
78
|
+
a fixture that has Prettier — belongs to S20 and is not done here. S20 is the row that
|
|
79
|
+
stages a packed install, and a test that needs Prettier cannot live in this package.
|
|
80
|
+
|
|
81
|
+
## Rejected
|
|
82
|
+
|
|
83
|
+
- **Fix only the branch the review named.** It is already correct. The finding was measured
|
|
84
|
+
against the shipped binary before anything changed precisely because a plausible line
|
|
85
|
+
number is not an observation, and stopping there would have shipped a no-op and closed the
|
|
86
|
+
row.
|
|
87
|
+
- **Leave `unchanged` comparing the body, and let `check` ignore the seam.** Then the seam
|
|
88
|
+
fix only helps a repository the first time its rules change, which is the one moment
|
|
89
|
+
nobody is reading. The other half is the defect.
|
|
90
|
+
- **Have `check` report the seam as its own finding rather than as drift.** A formatter is
|
|
91
|
+
not a consumer of this tool's verdict; the bytes are simply wrong and `sync` fixes them.
|
|
92
|
+
A second status for one wrong byte gives the reader two questions to ask about it.
|
|
93
|
+
- **Compare bytes for the `top` style too, and re-lay-out its splice to match.** That is
|
|
94
|
+
the change that would have broken a misplaced region finding. Re-laying-out a file to
|
|
95
|
+
match a formatting rule is a move onto somebody else's territory, and the region moving
|
|
96
|
+
home is the only reason those bytes are the tool's business.
|
|
97
|
+
- **Ship Prettier as a devDependency so the seam table can assert against the real
|
|
98
|
+
formatter.** ADR-012 rules it, and the rule this record settles is expressible in bytes:
|
|
99
|
+
which is the same reason the projection emits the formatter's shape rather than calling
|
|
100
|
+
it.
|