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