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,130 @@
1
+ # ADR-027 — the one gitattributes line that is neither dangerous nor empty
2
+
3
+ Status: accepted · 2026-10-06
4
+ **Affects:** `src/projection/baselines.mjs` (`GITATTRIBUTES_BODY`, second `BASELINES` row),
5
+ `test/baselines.test.mjs`, `test/surfaces.test.mjs`.
6
+ **Supersedes:** ADR-025 ruling 9, which ruled `.gitattributes` out for now and required this
7
+ record. Settles the `.gitattributes` item in §3.1 row 5 of
8
+ `docs/plans/0001-centralise-agent-rules.md`.
9
+
10
+ ## Context
11
+
12
+ ADR-025 shipped `.editorconfig` and, in ruling 9, ruled `.gitattributes` out for now. The
13
+ reason was not that the file cannot hold a region — `#` is a comment in gitattributes, so it
14
+ qualifies mechanically. The reason was that ruling 7 had just deferred `end_of_line` on the
15
+ grounds that line endings "are a claim about what belongs in the repository, and that claim
16
+ is made in `.gitattributes`", and every line in a shared `.gitattributes` region changes what
17
+ git does to the working tree. Ruling 9 concluded that such a region "is either dangerous or
18
+ empty", and deferred it to a record of its own.
19
+
20
+ The estate confirms the first half and not the conclusion. Thirteen of sixteen repositories
21
+ have no `.gitattributes` at all. The three that do disagree about almost everything:
22
+ `repo-05` declares only LFS filters for `.tif` and `.zip`; `repo-07` declares line
23
+ endings plus ConnectIQ binary markers; `repo-11` declares exactly one whitespace
24
+ suppression for hash-locked vendored content. There is no divergence here of the kind a
25
+ baseline fixes by being consistent. There is a file missing.
26
+
27
+ So the question ADR-025 deferred was the right one and it had a narrower answer than the one
28
+ it expected. It asked whether a *shared* set of attributes exists that is safe across every
29
+ repository. The answer is that almost none does — and that one line survives, and it happens
30
+ to be the line ruling 7 sent this file to make.
31
+
32
+ ## Rulings
33
+
34
+ 1. **`.gitattributes` is a region, not a copy, because git takes the last matching pattern.**
35
+ The region goes at the top of the file and each repository's own patterns stay beneath it,
36
+ where they win. This is the same mechanism ADR-025 chose for `.editorconfig` and for the
37
+ same reason: a repository's LFS filters and binary declarations are per-repository claims,
38
+ and the shared region must not be able to override them. The position is enforced by a
39
+ test that compares line offsets, because a region that is merely present but misplaced
40
+ would silently invert every rule it carries.
41
+
42
+ 2. **The shared body is exactly one line: `* text=auto eol=lf`.** Two halves, and neither
43
+ is decorative.
44
+
45
+ 3. **`text=auto`, not `text`.** This is the half that makes the line safe, and it is why
46
+ ruling 9's fear did not materialise. `text` is an instruction: git normalises every path
47
+ it matches, and it will happily do it to a `.tif`, a `.zip` or a Garmin `.iq` image,
48
+ because nothing in the pattern says those are binary. `text=auto` is a detection: git
49
+ sniffs each path and applies normalisation only to files it concludes are text. This is
50
+ the most common way a repository corrupts its own binaries, and it is invisible until
51
+ someone diffs an image. `repo-07` had independently arrived at `text=auto eol=lf` on
52
+ its first line, which is the strongest available evidence that the estate would pick the
53
+ cautious form if asked — this ruling does not ask, it supplies.
54
+
55
+ 4. **`eol=lf` is the claim ADR-025 ruling 7 sent here, and here it is safe to make.** Ruling
56
+ 7 refused `end_of_line` in `.editorconfig` because it "would change every checkout of
57
+ every repository on the day it landed, and a repository that wanted CRLF could only opt
58
+ out by deleting the region — losing the indentation baseline with it". Neither half
59
+ applies. In `.editorconfig` the key is a property of an editor's save behaviour; in
60
+ `.gitattributes` it is a property of the working tree, so a repository that wants CRLF
61
+ adds one line beneath the region and wins, keeping its own indentation settings. And
62
+ because the region is injected rather than applied, no file is rewritten at the moment it
63
+ lands: the next checkout normalises, and nothing before that does.
64
+
65
+ 5. **No other attribute is shared.** Not `filter=lfs`, not `binary`, not `-text`, not
66
+ `linguist-generated`, not `export-ignore`, not `diff=` or `merge=`. Each is a claim about
67
+ what a specific file *is*, and a wrong attribute is obeyed silently — a `filter=lfs` line
68
+ for a path that is not in LFS produces checkout failures in thirteen of sixteen
69
+ repositories, and a `binary` line for a text file produces a diff nobody can read. Ruling
70
+ 9's "dangerous or empty" is accurate about all of them. It was wrong that the file itself
71
+ had no non-dangerous content.
72
+
73
+ 6. **A test asserts the absence of the dangerous form by name.** `text=auto eol=lf` and
74
+ `* text eol=lf` differ by one word, that word is the entire safety argument, and a later
75
+ reader who tidies the line would have no way to know. The assertion is
76
+ `assert.doesNotMatch(/^\* text eol=lf$/m)`, so the simplification fails as a test rather
77
+ than as a corrupted binary in someone's repository.
78
+
79
+ 7. **The property is verified against git itself, not against the string.** A test creates a
80
+ repository, writes the shipped region, commits a text file and a binary one, and asserts
81
+ through `git check-attr` that `text` resolves to `auto`, `eol` to `lf`, and that no
82
+ `diff` attribute is set on the binary, plus that the working tree contains no `\r\n`.
83
+ Reading the rendered bytes back would pass for a line git ignores, and the whole claim is
84
+ about what git does with it.
85
+
86
+ ## Consequences
87
+
88
+ `.gitattributes` joins `.editorconfig` as the second baseline, and the row-table guard in
89
+ `test/baselines.test.mjs` — every row carries a real style, a reason and a record number —
90
+ now covers two files instead of one.
91
+
92
+ Ruling 9's deferral is answered rather than repeated, and the generalisation it reached for
93
+ does not survive contact with the estate: this was not "a shared `.gitattributes` region is
94
+ impossible", it was "no shared region is safe except the one line that does not claim to know
95
+ what any file is". A future baseline for a per-stack attribute would have to arrive as a
96
+ different row with its own record, which is what §3.2's keep-it-local rule asks for anyway.
97
+
98
+ The repositories with an existing `.gitattributes` are the interesting case and they are not
99
+ in conflict: `repo-05`'s LFS filters, `repo-07`'s binary markers and
100
+ `repo-11`'s whitespace suppression all stay beneath the region and all keep winning,
101
+ because git takes the last match. No repository loses a line it wrote.
102
+
103
+ This repository adopts neither baseline for itself, and the reason is the same one that keeps
104
+ its `AGENTS.md` hand-written. The region's marker records the simt-dev SHA it was rendered
105
+ from, so a copy committed here would name a commit that stops being `HEAD` the moment it
106
+ lands — `check` would report drift on the next commit and the fix would be a sync that
107
+ chases its own tail. `test/baselines.test.mjs` asserts this rather than leaving it to
108
+ restraint: the failure was made once while writing this very record, which is exactly the
109
+ condition under which a guard earns its place.
110
+
111
+ ## Rejected
112
+
113
+ - **`* text eol=lf`, the forcing form.** Rejected by ruling 3. It is one word shorter and
114
+ it is the single most common way to corrupt binaries in a repository that has them, and two
115
+ repositories here do.
116
+ - **A per-repository table of attributes,** driven by detection the way ADR-026's ecosystem
117
+ table is. Rejected because the mapping would be a guess: nothing in the tree says whether
118
+ a `.tif` is LFS-tracked, and `.gitattributes` has no `has-*` fact to hang a decision on.
119
+ Writing an attribute the repository did not ask for is worse than writing none, because
120
+ git obeys it without complaint.
121
+ - **Putting `end_of_line = lf` back into `.editorconfig` as well,** on the grounds that two
122
+ files agreeing is more robust than one. Rejected by ruling 4: the `.editorconfig` copy
123
+ cannot be overridden without losing the indentation rules, so it would be the claim that
124
+ cannot be escaped. One place for the claim is better than one place that cannot be
125
+ overruled.
126
+ - **Shipping `.gitattributes` as a whole-file copy,** on ADR-026's mechanism, since it has
127
+ no region in the strict sense. Rejected because the mechanism fits a file that cannot
128
+ merge, and this one can: there is a real region, it composes with existing content by
129
+ ordering, and taking the file over whole would clobber three repositories' LFS filters.
130
+ ADR-026's ruling 2 asks whether the grammar holds a region, and here it does.
@@ -0,0 +1,131 @@
1
+ # ADR-028 — a base file is a floor, so the disagreement it cannot repair becomes a check
2
+
3
+ Status: accepted · 2026-10-07
4
+ **Affects:** `src/tsconfig.mjs`, `src/commands/init.mjs`, `src/projection/project.mjs`,
5
+ `test/tsconfig.test.mjs`. **Narrows:** ADR-026 ruling 2 (ownership does not depend on
6
+ content being mergeable) by applying it to a file whose mechanism is `extends` rather than
7
+ a copied byte range. Settles the `tsconfig.base.json` item of §3.1 row 5 and row 7 of
8
+ `docs/plans/0001-centralise-agent-rules.md`.
9
+
10
+ ## Context
11
+
12
+ Row 5 promised eight baselines and named two problems among them that are not about a file
13
+ being missing: repo-08's seven absent strictness flags, and `moduleResolution` spelled
14
+ inconsistently across the workspace. ADR-025 ruled `tsconfig.base.json` out of the region
15
+ mechanism because JSON has no comments, and its consequences promised that "the next fix is
16
+ three more JSON files and therefore a different mechanism". ADR-026 supplied that mechanism
17
+ by saying ownership does not depend on mergeability.
18
+
19
+ What ADR-026 could not foresee is that this particular file inherits rather than copies,
20
+ and inheritance has a property that changes the design from the inside: **a child overrides
21
+ every key its parent sets.** `tsconfig.base.json` is not a shared copy of a config, it is a
22
+ floor that a child may decline to stand on.
23
+
24
+ That one fact decides the whole record, so it is worth being concrete about what it means.
25
+ A base file can repair a repository that is *missing* a flag. It is structurally incapable
26
+ of repairing one that spells a flag *differently*, because the child wins and the base is
27
+ inert for that key. Both of row 5's problems are of that kind or the other: the seven
28
+ missing flags are repairable by a base file, and the `moduleResolution` disagreement is not
29
+ repairable by any file at all.
30
+
31
+ The disagreement is also not really about disagreement. Three repositories write
32
+ `"Bundler"` and one writes `"bundler"`; TypeScript's parser is case-insensitive, so all four
33
+ compile and all four work. node.md:38 asks for it "spelled consistently across every repo in
34
+ the workspace", and the only spelling that can be checked in a repository on its own is the
35
+ one an external document uses.
36
+
37
+ ## Rulings
38
+
39
+ 1. **`tsconfig.base.json` is a whole file `init` writes once, and `check` never compares.**
40
+ ADR-026's ownership rule unchanged. There is no refresh path, deliberately: ADR-026 gave
41
+ simt-dev's own `.github` files a refresh because this repository authors the template,
42
+ and this repository has no more use for a current copy of a base file than it has for a
43
+ current copy of `.editorconfig`. A refresh path here would be a mechanism for making this
44
+ repository's own gate green by rewriting the file the gate is judging.
45
+
46
+ 2. **The floor is exactly the intersection of what the estate already wrote** — twelve
47
+ `compilerOptions` keys, asserted by name in a test rather than by count. It is not a set
48
+ of values anybody here has argued for; it is what four repositories independently wrote,
49
+ and the outlier repository is the one with the gap.
50
+
51
+ 3. **A key nobody can override safely is not in the floor.** `noEmit` is the sharp case:
52
+ node.md:36 wants it for any project that bundles elsewhere, inheritance makes it
53
+ inheritable, and a child can set `false`. It is still absent, because a base that hands
54
+ `noEmit` to a repository nobody looked at is a build that stops emitting, and the two
55
+ repositories here that bundle elsewhere did not ask for it. `target`, `lib`, `module`,
56
+ `include`, `exclude`, `paths`, `jsx` and `types` are absent for the same reason in the
57
+ other direction: `lib` is DOM+DOM.Iterable in two repositories and DOM+WebWorker in the
58
+ other two, and a floor that chose would silence one of them.
59
+ 4. **The test asserts those absences by name.** A future "harmless" addition that breaks a
60
+ repository's build must fail in this repository's suite rather than in somebody's CI.
61
+
62
+ 5. **`moduleResolution` is a check, not a key.** Shipping it in the floor would change
63
+ nothing — every repository sets it, so the child overrides the base — which is exactly
64
+ the definition of a line that looks configured and configures nothing.
65
+
66
+ 6. **A base file that nothing extends is reported as inert**, and the finding names the line
67
+ to add. The symptom is a strictness floor sitting in a repository looking adopted because
68
+ the file is right there, with a `tsc` run contradicting it. Nothing else surfaces this.
69
+
70
+ 7. **The file's own bytes cannot explain themselves, so `init` says the thing for it.** JSON
71
+ has no comments, so a shipped file with no header owes its reader nothing about who wrote
72
+ it or what to do next. This is the one place AGENTS.md's "every byte that leaves must be
73
+ readable and actionable" cannot be met inside the file, and the obligation is met by
74
+ `init` printing the `extends` line it requires and by ruling 6's finding.
75
+
76
+ 8. **`tsconfig.json` is read as JSONC, and a parse failure produces no finding.** tsc strips
77
+ comments before reading its own config, so a commented `tsconfig.json` is normal rather
78
+ than a mistake, and `JSON.parse` would throw on it. A gate that throws on a file it did
79
+ not write shows a reader a stack trace where the answer was one line, and `tsc` already
80
+ reports the malformed file better than this tool could.
81
+
82
+ 9. **The reader is a scanner, not a regex, for one reason:** `//` occurs inside every URL,
83
+ and the commonest string near a tsconfig is a URL. A regex strips the rest of the line
84
+ and then reports a parse error on a file that is perfectly valid. A test pins that case.
85
+
86
+ 10. **A repository with no TypeScript is offered nothing.** A base file in a JavaScript
87
+ repository is a file that claims to be an authority and is read by nothing. Detection is
88
+ `fingerprint`'s existing `typescript` tag, so no new fact is declared (ADR-013).
89
+
90
+ 11. **`simt-dev check` reports absence and the two inherited-from-nowhere problems, and never
91
+ the contents.** The bytes belong to the repository from the first commit, and a finding
92
+ that compared them would describe somebody's work as an error.
93
+
94
+ ## Consequences
95
+
96
+ A TypeScript repository now gets twelve flags it did not have, and one of them — the floor
97
+ arriving through `extends` — is a behaviour change that can surface real type errors in
98
+ `repo-08`, which is the point of the record and also the reason adoption is one commit
99
+ with a `tsc` run rather than a silent upgrade. A base file is a floor, and a repository
100
+ that had not been standing on one is the case where raising it finds something.
101
+
102
+ `check` gains two findings that need no workspace to evaluate, which is what makes them
103
+ usable: a repository can be checked alone, and the estate-wide agreement that
104
+ node.md:38 asks for is expressed as agreement with an external document rather than with
105
+ each other.
106
+
107
+ What this does **not** settle: whether the two repositories that bundle elsewhere should
108
+ declare `noEmit`. It is a fact about a build, it is not derivable from anything this tool
109
+ reads, and neither repository asked. It stays a per-repo decision with no gate, and the
110
+ reason is written here so the omission is not later read as an oversight.
111
+
112
+ ## Rejected
113
+
114
+ - **`moduleResolution` in the floor, with a check as well.** Rejected by ruling 5: the key
115
+ would be inert in every repository in the estate, and a line in a config that no
116
+ repository's resolution depends on is decoration.
117
+ - **A check that the estate agrees, by comparing across a workspace.** Rejected because
118
+ `check` runs in one repository, and a cross-repository finding needs a workspace, a
119
+ resolution order, and a rule for what happens when a repo is checked before the others
120
+ exist. Agreement with the documented spelling gets the same guarantee from something that
121
+ is already there.
122
+ - **A `noEmit` finding for Cloudflare Workers projects,** which `fingerprint` can identify.
123
+ Rejected on the evidence rather than on taste: the finding would read a repository's root
124
+ `tsconfig.json` and conclude that the whole project never emits, which is false of any
125
+ repository that keeps a library in a sub-package with its own config. Two of two candidate
126
+ repositories in the estate would satisfy it, which is exactly the situation where a check
127
+ that has only ever been right looks correct.
128
+ - **Comment-stripping by regex, including trailing commas.** Rejected by rulings 8 and 9.
129
+ - **A region inside the JSON**, as a string key naming the real config. Rejected by
130
+ ADR-025's grammar argument, and doubly here: the value would be a string where TypeScript
131
+ expects an object, so the file would have to be rewritten by whatever read it.
@@ -0,0 +1,120 @@
1
+ # ADR-029 — the pins a repository declares are its own, so a check reports them and never writes them
2
+
3
+ Status: accepted · 2026-10-07
4
+ **Affects:** `src/toolchain.mjs`, `src/projection/project.mjs`, `test/toolchain.test.mjs`.
5
+ **Settles:** the `packageManager`/`engines`/`.nvmrc` items of §3.1 row 5 of
6
+ `docs/plans/0001-centralise-agent-rules.md`. **Extends:** ADR-026 ruling 9 (absence and
7
+ disagreement are reportable; content is not) to a file this tool authors no part of.
8
+
9
+ ## Context
10
+
11
+ Row 5 promised to resolve "no `packageManager`" and "3 disagreeing nvmrc". Reading the
12
+ estate before writing anything turned the second of those into a different problem, and
13
+ the difference is the whole record.
14
+
15
+ Nine node repositories commit `package-lock.json`. None declares `packageManager`. That is
16
+ one real defect, stated by `rules/stacks/node.md` in four consecutive claims and enforced by
17
+ nothing: one `packageManager` matching the lockfile, a floor in `engines.node`, a `.nvmrc`
18
+ that agrees with both, and `engines` stated at all.
19
+
20
+ The `.nvmrc` claim does not survive contact. Three repositories have one, and each agrees
21
+ with its own `engines` exactly — `24.19.0` against `>=24.19`, `22.21.1` against `>=22`,
22
+ `22.22.0` against `>=22`. What the plan called three disagreeing files are three
23
+ *repositories* choosing different node majors, which is a per-repository decision each of
24
+ them has a reason for. There is no defect. A check that reported it would produce a finding
25
+ with no remedy, and a gate whose findings cannot be acted on is the failure this package
26
+ exists to prevent — so the check does not exist, and `test/toolchain.test.mjs` asserts its
27
+ absence out loud so a later reader does not add it back as a completeness improvement.
28
+
29
+ That left one defect, and it has a property none of the shipped baselines has: **there is no
30
+ correct value to write.** A strictness flag has one right answer. A node floor has as many as
31
+ the team has reasons for, and `package-lock.json` carries a lockfile format number, not the
32
+ npm version that produced it. A tool that picked one would be embedding a locally-observed
33
+ version into every repository in the estate, which is the thing ADR-001's determinism rule
34
+ exists to prevent.
35
+
36
+ ## Rulings
37
+
38
+ 1. **A pin is reported, never written.** `simt-dev init` writes no `packageManager`, no
39
+ `engines` and no `.nvmrc`. ADR-026's ruling 9 already said a check reports absence rather
40
+ than contents; this extends it to a file where simt-dev has no content to report *on*,
41
+ because the correct content is not derivable from anything in the tree.
42
+
43
+ 2. **The remedy is a command run where the answer exists.** `npm pkg set
44
+ packageManager=npm@$(npm --version)` fills in the version from the machine that has it,
45
+ and the tool ships no version of its own. This is the first remedy in the package that is
46
+ a command rather than an edit, and it is the shape the answer forces: the tool can name the
47
+ field and the manager, and only the reader can name the version.
48
+
49
+ 3. **A lockfile is the evidence that a manager was chosen.** A node repository with no
50
+ lockfile is not asked to declare a manager, because there is nothing to declare one
51
+ *about*. Every check is gated on `has-lockfile`'s evidence rather than on `package.json`
52
+ existing.
53
+
54
+ 4. **Two lockfiles are the absence of a package manager,** reported as such and never
55
+ resolved by picking the alphabetically-first one. That resolution would be a guess about
56
+ which of two committed artefacts is the mistake, and it would be wrong half the time.
57
+
58
+ 5. **`engines.node` absent is reported as a floor nobody enforces.** This is `node.md`'s own
59
+ phrasing — "a version floor nobody enforces is a comment" — promoted from a rule to a
60
+ gate. Four repositories in the estate declare neither `engines` nor a `.nvmrc`.
61
+
62
+ 6. **A `.nvmrc` the declared floor forbids is the one contradiction worth reporting**, because
63
+ the floor is a promise about what is supported and the pin is the instance of it, so the
64
+ pin is what has to move. The remedy names both values rather than choosing between them:
65
+ which one is wrong is a decision about intent, not about syntax.
66
+
67
+ 7. **A range this tool cannot evaluate produces no finding.** `lts/*`, `22.x` and anything
68
+ else outside a named subset reads as unknown, and an unknown is silence. The alternative is
69
+ an approximate reading reported with the same confidence as an exact one, and a reader who
70
+ cannot tell which findings were guesses stops trusting the ones that were right.
71
+
72
+ 8. **The subset is written down and tested as a subset:** `>=`, `>`, `<`, `<=`, `^`, `~`,
73
+ bare `22` and `22.1`, space-separated conjunctions and `||` disjunctions. A bare `22` means
74
+ every 22, as npm reads it, so a pin of `22.9.9` against a floor of `22` is satisfied —
75
+ reading either as one exact version would report a matching pin as a contradiction.
76
+
77
+ 9. **This check reads and never writes, and a test says so.** The gate that judges a
78
+ repository must not be able to green itself by editing what it judges. ADR-024 drew that
79
+ line for the `.github` files; this is the same line drawn for a check with no file of its
80
+ own.
81
+
82
+ ## Consequences
83
+
84
+ Three findings go live where nine repositories will see them: `undeclared` fires in every
85
+ node repository in the estate, `unfloored` in four, and `contradiction` in none. That is the
86
+ intended effect — row 5 asked for these to be resolved and nothing in this package will
87
+ resolve them, because it cannot. A repository that adopts simt-dev and runs `check` gets a
88
+ red gate with three commands that each end in a value only the reader can supply, which is the
89
+ honest version of "resolves this defect".
90
+
91
+ The remedy strings are also the first place this package asks a reader to run a shell
92
+ substitution, and that is worth stating plainly: a version detected on the machine running
93
+ `check` is not the version the repository should record, so the remedy does not embed one.
94
+
95
+ What this settles and what it does not: `packageManager`, `engines.node` and `.nvmrc` are now
96
+ gated for **agreement**, not normalised. A shared value for any of them was never derivable
97
+ and is not attempted. What remains unwritten in row 5 is commitlint — which no rule file
98
+ mentions and no package in the estate installs — and the ESLint block, whose four
99
+ configurations are not variants of one configuration. This paragraph named those two and
100
+ stopped. ADR-030 answers what they are: not mechanisms awaiting code, but policy questions
101
+ nobody has been asked, and it is the record to read next for either.
102
+
103
+ ## Rejected
104
+
105
+ - **A shipped `.nvmrc` or an `engines` value in a shared baseline.** Rejected as
106
+ unrulable: the value is per-repository and the tool can only guess it. Shipping `22` would
107
+ break `repo-14`, which pins 24.
108
+ - **Reporting cross-repository node divergence as a finding.** Rejected by the Context. The
109
+ plan's premise was wrong and the correction is recorded here so it is not re-derived from
110
+ the plan text.
111
+ - **Writing `engines.node` from the `.nvmrc` already present, or the reverse.** This is the
112
+ one baseline case where simt-dev could derive a value from another value in the same repo,
113
+ and it is rejected: the two disagree in four of the estate's repositories' worth of
114
+ intent, and picking the winner would make one file's absence silently decide the other's
115
+ value. The contradiction is reported with both values and the reader resolves it.
116
+ - **A full semver implementation,** for the reason in ruling 7. `22.x`, ranges with prerelease
117
+ tags, and `>=22 <23 || >=24` shaped exclusions read as unknown.
118
+ - **Deriving the npm version from `package-lock.json`.** It is not there; the file records
119
+ `lockfileVersion`, a format number. Reading a version from anywhere else would make the
120
+ remedy depend on a file that is not evidence for it.
@@ -0,0 +1,114 @@
1
+ # ADR-030 — what a plan does not ship is a decision with a record, not a silence
2
+
3
+ Status: accepted · 2026-10-07
4
+ **Affects:** `docs/plans/0001-centralise-agent-rules.md` §3.1 row 5 and row 7. Ships no
5
+ code. **Refuses:** the commitlint baseline and the shared ESLint block named in row 5 and
6
+ row 7, and records why in the plan itself rather than leaving the rows to imply that two
7
+ mechanisms are merely unwritten.
8
+
9
+ ## Context
10
+
11
+ Row 5 asks for eight baselines and row 7 asks `init` to scaffold them. Five have shipped:
12
+ `.editorconfig` (ADR-025), `.gitattributes` (ADR-027), `tsconfig.base.json` (ADR-028), and
13
+ the `packageManager`/`engines`/`.nvmrc` checks (ADR-029), alongside Dependabot (ADR-026).
14
+ Six of the eight are now ruled on one way or another.
15
+
16
+ Two are not, and the reason they are not is not the reason the first six took. Each of the
17
+ first six hit a **mechanism** question — a region or not, a floor or a check, a value to
18
+ write or none to derive — and each question had an answer once somebody wrote down what the
19
+ format actually permits. The remaining two have no mechanism question to answer yet, because
20
+ nobody has decided whether the thing should exist.
21
+
22
+ That is the gap this record closes. AGENTS.md's first rule is that every byte leaving this
23
+ repository must be actionable for a reader holding only the receiving repository, and the
24
+ plan is a byte that leaves. A row that reads "still missing" tells the reader a mechanism is
25
+ outstanding. For these two, what is outstanding is a policy decision, and a reader who
26
+ trusted the row would go looking for the file to write.
27
+
28
+ The two were also miscounted, which is a separate failure and the reason the plan needed
29
+ correcting at all.
30
+
31
+ ## Rulings
32
+
33
+ 1. **A baseline that is not shipped is recorded in the row that asked for it,** with the
34
+ reason, so the plan never implies an unfinished mechanism where the real state is an
35
+ unmade decision. An honest row and a silent row differ in what the next person does, and
36
+ the silent one is the more expensive.
37
+
38
+ 2. **A convention asked for in prose is not a tool requirement.** `slice-commit.mdc:27`
39
+ says "Conventional Commits, imperative subject, no trailing period", and gives an example.
40
+ `commitlint` appears nowhere in `rules/` — zero matches across the directory, verified
41
+ rather than assumed. What the estate mandates is a message shape; what the plan named is a
42
+ linter that enforces it. Shipping the linter is a policy ruling about how strictly the
43
+ shape is held, and nothing in the rules says how strictly.
44
+
45
+ 3. **No rule file mentions commitlint, and no `package.json` in the workspace installs
46
+ it.** Both are `grep` claims and both are written as claims: `grep -ri commitlint rules/`
47
+ returns nothing, and `grep -l commitlint */package.json` returns nothing. That is the
48
+ whole of the evidence, deliberately — a count of how many repositories were checked would
49
+ be decoration, and one already in this estate disagrees with itself about what it counts.
50
+ So there is no existing disagreement to repair, no config to inherit, and no repo
51
+ currently failing a gate over it. Row 5's other items each had at least one of those.
52
+
53
+ 4. **`eslint-config-prettier` is installed in exactly one of the four configurations,** and
54
+ the fight is in exactly the two that use `stylisticTypeChecked` without it. The plan said
55
+ "3 of 5 configs"; the estate has four configs and two conflicts, in one shape that is
56
+ detectable by reading the file. Corrected in the row.
57
+
58
+ 5. **Two of the four configurations are plain JavaScript with no TypeScript in them,**
59
+ `repo-04` and `repo-06`. A "shared 6-rule ESLint block" cannot describe all four,
60
+ because two of them have no rules to share — they differ in `ignores` and per-file
61
+ `globals`, which are per-repository facts about paths.
62
+
63
+ 6. **Flat config has no cross-repository include.** Bringing a shared block into a repo
64
+ requires either publishing it as an npm package or editing the repo's own
65
+ `eslint.config.mjs` to import and spread a local file. This package is a CLI with zero
66
+ runtime dependencies (ADR-012); becoming a registry is not a mechanism decision this row
67
+ may make on its own.
68
+
69
+ 7. **The fix for the two conflicts is one dependency each,** and it is not a shared block.
70
+ Installing `eslint-config-prettier` in `repo-14` and `repo-11` and spreading
71
+ it last in the config array resolves both. That is a command per repository, run by the
72
+ repositories, and simt-dev has no business writing it.
73
+
74
+ 8. **The `.nvmrc` claim in row 5 was wrong and is corrected there.** Three repositories
75
+ disagree, and each one's `.nvmrc` agrees exactly with its own `engines.node`. What differs
76
+ is three legitimate per-repository choices. ADR-029 rules the cross-repository check out
77
+ and asserts the silence in a test.
78
+
79
+ ## Consequences
80
+
81
+ Row 5 ships five of its eight items, and now says so in a way that separates the two kinds
82
+ of remainder. Row 7 stops reading as though commitlint and the ESLint block are waiting on
83
+ code.
84
+
85
+ Neither refusal forecloses the decision. Both are one record away from being built, and this
86
+ record is what a later one would read first: it says what the mechanism would have to be
87
+ (ruling 6), what the state of the estate is (rulings 3 and 4), and what the cheapest correct
88
+ fix is today (ruling 7). Nothing here needs to be undone to start them.
89
+
90
+ The estate consequence of this record is one line changed in a plan and one rule about what
91
+ a plan row owes its reader. That is a smaller change than the five baselines that preceded
92
+ it, and it is the change that makes the other five legible: a reader of row 5 can now tell
93
+ the difference between what was built, what was ruled out, and what nobody has decided.
94
+
95
+ ## Rejected
96
+
97
+ - **Shipping a `.commitlintrc.json` anyway,** on the grounds that the mechanism is settled
98
+ and only the file is missing. Rejected by ruling 2: the file is a decision about
99
+ enforcement strength, and writing it would answer that question by accident. A rule that
100
+ says "no trailing period" and a commitlint config that rejects one are not the same
101
+ commitment, and only one of them has been asked for.
102
+ - **A `commitlint` check** reporting a malformed subject, as a lighter alternative. Rejected
103
+ for the same reason as the rest of ADR-029: a check for a convention nobody has adopted
104
+ turns silence into a failure, and this is a plan item rather than a rule requirement.
105
+ - **A shared ESLint block as a local file each repo imports.** Rejected by ruling 6 for the
106
+ published case, and for the local case because a file only two of four configs could use
107
+ is not the shared block row 5 describes.
108
+ - **Deleting the `stylisticTypeChecked` entry from the two configs.** This would resolve the
109
+ conflict, and it is rejected as simt-dev's business: it is an edit to a rule set a team
110
+ chose, in a repository that has not asked, and its loss is that the stylistic rules were
111
+ there for a reason nobody has written down.
112
+ - **Rewriting row 5 to drop the two items,** on the grounds that the plan should only list
113
+ what will be done. Rejected by ruling 1. A plan that quietly forgets is not honest; it is
114
+ just a document nobody can audit.
@@ -0,0 +1,129 @@
1
+ # ADR-031 — the suite stages the environment it needs, because a test that inherits the author's directory is not evidence
2
+
3
+ Status: accepted · 2026-10-08
4
+ **Affects:** `test/fixture.mjs`, `test/install.test.mjs`, `test/hooks.test.mjs`,
5
+ `test/upgrade.test.mjs`, `test/workspace.test.mjs`. **Extends:** ADR-003 (test breadth)
6
+ and the AGENTS.md rule that the tool must be run the way a consumer runs it. **Relates to:**
7
+ ADR-015 (a source is content, not a location) — this is the same claim applied to the tests
8
+ that consume a source.
9
+
10
+ ## Context
11
+
12
+ `simt-dev own gate` had been red on every push to `main` since the first baseline commit
13
+ landed. Not intermittently: 22 of 543 tests failed on runs `37476671363` through
14
+ `37642437221`, and the same 22 failed on all four. Locally the suite was 542/543 green.
15
+
16
+ Three separate causes, none of them recent:
17
+
18
+ 1. **`test/install.test.mjs:17` read the estate off the author's disk.**
19
+ `const WORKSPACE = resolve(HERE, '..', '..')` is `/home/developer/code/simt`, and the
20
+ install tests copied skill bodies out of it — `repo-11/.agents/skills/d1-safe-change`,
21
+ and `singleRepoClone()` out of `repo-14`. On a runner the checkout is
22
+ `/home/runner/work/simt-dev/simt-dev`, so `../..` is `/home/runner/work/simt-dev`,
23
+ which contains this repository and nothing else. The bodies were not there,
24
+ `skills install` exited 3, and nineteen tests asserted on a refusal they never asked for.
25
+ This is the third time this repository has made this mistake. AGENTS.md already names the
26
+ first two: "workspace resolution that only worked in the checkout, and content lookup that
27
+ only worked with a workspace." The rule was written down and the suite broke it anyway,
28
+ which is worth recording plainly — a written rule does not prevent the failure, only a
29
+ fixture that cannot express it.
30
+
31
+ 2. **Three `git commit` calls carried no identity.** `hooks.test.mjs:502` and `:587` and
32
+ `upgrade.test.mjs:310` ran bare `git commit`, which dies with `Author identity unknown`
33
+ on any machine without a configured identity. Two other files in the same suite
34
+ (`compare.test.mjs:57`, `init.test.mjs:177`) passed `-c user.email=…`. The suite was
35
+ half right in a way that only shows up on a runner.
36
+
37
+ 3. **Two tests asserted the checkout's own directory name.**
38
+ `PACKAGE_ROOT is the package directory, not its parent` asserted
39
+ `PACKAGE_ROOT.endsWith('/simt-dev')`, and `CLI: doctor reports which source answered`
40
+ asserted the label `(heuristic: sibling of this package)`. Both pass when the directory is
41
+ called `simt-dev` and fail when it is called anything else, which is a fact about the
42
+ folder rather than about the code. Neither failed in CI, because CI's directory *is*
43
+ called `simt-dev`. They were found only by running the suite from a copy named `repo`.
44
+
45
+ Causes 1 and 2 explain all 22 failures. Cause 3 explains none of them and is the most
46
+ instructive, because it is a test that passes for the wrong reason.
47
+
48
+ ## Rulings
49
+
50
+ 1. **The suite builds its own environment; it never reads the author's.** A test that
51
+ resolves a body, a workspace, or a git identity from anything outside the checkout is
52
+ defective even when it passes. `test/fixture.mjs` is the single place that constructs
53
+ them, so the assumption is stated once instead of being re-derived in each file.
54
+
55
+ 2. **Bodies are generated from the registry, not copied from a sibling.** A declared source
56
+ is a *size* and a *location*; `locateSource` compares `measureSkill(path).bytes` against
57
+ the declared number and nothing else. So `skillWorkspace()` walks
58
+ `graph/skills.graph.json`, and writes a body of exactly the declared length for every
59
+ one of the 45 declared sources. Two consequences follow and both are deliberate: a source
60
+ added to the registry is staged without a second edit, and a declared size that changes
61
+ cannot leave the fixture staging the old one. Copying real bodies instead would have
62
+ reintroduced the dependency this record exists to remove, and would have made the suite
63
+ fail whenever an unrelated repository edited a skill.
64
+
65
+ 3. **Byte-exactness is a contract, not a detail.** A body one byte short is refused with
66
+ `exists but is not the declared body`, so a fixture that pads approximately turns the
67
+ tests green into tests that assert a refusal. Verified: removing one byte from the padding
68
+ fails 19 of the 31 install tests.
69
+
70
+ 4. **Only the stack signals that select something are staged.** The two repos whose names
71
+ appear in a variant-bearing skill's `sources` carry the dependencies and the
72
+ `vite.config.ts` that make `fingerprint` emit `react` + `node-web` against `hono` —
73
+ `node-web` comes from a config file, not from `package.json`, so a fixture that only
74
+ writes dependencies silently fails to select the `web` variant. Repos the registry names
75
+ for single-body sources are staged neutral, because nothing is ever chosen by signal from
76
+ them. Inventing stacks for them would be decoration, and would risk
77
+ `every variant-bearing skill resolves to exactly one variant per real repo` passing for a
78
+ reason nobody can reconstruct.
79
+
80
+ 5. **`git` is invoked through one helper that always carries an identity**, on the command
81
+ line rather than in a config file, so the suite can neither read nor write the developer's
82
+ git configuration. `commit.gpgsign=false` for the same reason: a developer with signing
83
+ configured must not be the only person whose tests pass.
84
+
85
+ 6. **A test that asserts a location-derived fact asserts the location.** Ruling 1 makes the
86
+ checkout's path and name unavailable as evidence. `PACKAGE_ROOT` is now checked
87
+ structurally — the package's own `package.json` and `bin/simt-dev.mjs` are inside it, and
88
+ they are not one level up. `doctor reports which source answered` no longer asserts
89
+ *which* source; it empties the config and disables the workspace so the answer is the
90
+ same on every machine. The sibling heuristic keeps its own coverage where it belongs, in
91
+ the `resolveWorkspace` unit tests.
92
+
93
+ ## Consequences
94
+
95
+ The suite now passes from any checkout: verified at an arbitrary path under a directory named
96
+ `fork-of-simt-dev`, with no sibling repositories present, 542/542. It passed there before this
97
+ record only if the directory happened to be named `simt-dev`.
98
+
99
+ The fixture stages bytes that satisfy the registry, not a copy of anybody's documentation.
100
+ Ten of the estate's skills ship a `references/` directory that `skillWorkspace()` does not
101
+ reproduce, because `measureSkill` sums the directory and a single padded `SKILL.md` satisfies
102
+ every check except the one test that asserts a reference file arrives — and that split is
103
+ declared for the one skill whose test needs it. If a future test asserts a layout, the split
104
+ goes in `FILE_SPLITS` rather than the fixture growing a copy of the estate.
105
+
106
+ The cost is that `test/install.test.mjs` no longer proves anything about the *real*
107
+ `d1-safe-change`. It proved it never: what it verified was that a body of the declared size is
108
+ copied verbatim, and the declared size came from the registry either way. What it did prove,
109
+ accidentally, was that this checkout's siblings are intact — which is not a property of the
110
+ tool.
111
+
112
+ ## Rejected
113
+
114
+ - **Cloning the estate into CI so the existing tests pass.** Fastest, and it leaves the
115
+ defect exactly where it is. The suite would still be unable to detect that the tool only
116
+ works with a workspace, which is the failure AGENTS.md says has already happened twice.
117
+ - **Pointing the install tests at this repository's own skill bytes.** Would make the tests
118
+ pass, but `skills install` exists to move a body *between* repositories, so a test whose
119
+ source and destination are the same repository is not exercising the command.
120
+ - **Committing the real bodies as fixture files under `test/`.** Deterministic, and it would
121
+ have avoided the padding code entirely. Rejected: it copies other repositories' work into
122
+ this one and creates a second thing to update whenever a body changes, which is a
123
+ synchronisation problem rather than a test.
124
+ - **Setting a global `SIMT_DEV_WORKSPACE` for the whole suite.** Ruling 1 makes each test
125
+ name the environment it needs; a default would let a test inherit a workspace without
126
+ having asked for one, which is the same class of defect one level up.
127
+ - **Fixing only the 22 CI failures.** Causes 1 and 2 are all the runner can see. Cause 3 was
128
+ found by running the suite from a directory with a different name, and would have stayed
129
+ invisible to anyone who only ever ran it in place.