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