simt-dev 0.0.0-stage → 0.1.1

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