simt-dev 0.2.0__py3-none-any.whl

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