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,211 @@
|
|
|
1
|
+
# ADR-014 — The workspace is configuration, and a single repo is a first-class case
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-05
|
|
4
|
+
Affects: `simt-dev` CLI (workspace resolution, `compare`, `skills`, registry validation, packaging)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
ADR-013 made the *vocabulary* generic. It did not make the *filesystem layout* generic, and the
|
|
9
|
+
gap was invisible from inside the checkout. Three failures were measured rather than reasoned
|
|
10
|
+
about, by running the tool from a throwaway repo outside `/code/simt` and from a simulated
|
|
11
|
+
`node_modules` install:
|
|
12
|
+
|
|
13
|
+
1. **Every `skills` command and `graph lint` fails outside the SIMT workspace.** `WORKSPACE_ROOT`
|
|
14
|
+
was `resolve(HERE, '..', '..', '..')` — three levels up from the source file. In the checkout
|
|
15
|
+
that is `/code/simt`, which is correct. In an npm install it is
|
|
16
|
+
`node_modules/@quazarmedia`:
|
|
17
|
+
|
|
18
|
+
| install location | `WORKSPACE_ROOT` resolves to |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `/code/simt/simt-dev/src/graph` | `/code/simt` — correct |
|
|
21
|
+
| `…/node_modules/@quazarmedia/simt-dev/src/graph` | `…/node_modules/@quazarmedia` — wrong |
|
|
22
|
+
|
|
23
|
+
The symptom is `ENOENT` for all 38 sources. The *remedy* printed is
|
|
24
|
+
`run: simt-dev graph lint`, which is itself wrong, because the reader has no workspace to lint.
|
|
25
|
+
|
|
26
|
+
2. **`rules why` cannot work in the published package.** `package.json` `files` lists
|
|
27
|
+
`bin src graph rules skills agents tooling hooks` and not `docs`. `npm pack` produces 39 files
|
|
28
|
+
and zero from `docs/`, so every ADR lookup fails in a real install. The whole provenance
|
|
29
|
+
feature — the reason `rules why` exists — is unusable outside the checkout.
|
|
30
|
+
|
|
31
|
+
3. **`compare` treats a repo as a workspace and rejects it.** `--root <a-repo>` fails with
|
|
32
|
+
`contains no git repositories` and exit 3, because `discover` scans the *children* of the
|
|
33
|
+
given directory. The most common invocation outside a multi-repo workspace — point the tool at
|
|
34
|
+
one repo — is an error. The remedy, `no git repositories found under <path>`, is also
|
|
35
|
+
misleading: there is one, you are standing in it.
|
|
36
|
+
|
|
37
|
+
None of this was visible in the tests, because every test ran from the checkout. That is the
|
|
38
|
+
general lesson and it is the reason this record exists: **a tool's own layout is part of its
|
|
39
|
+
interface, and a test suite that only runs the tool from its source tree cannot detect that the
|
|
40
|
+
tool only works from its source tree.**
|
|
41
|
+
|
|
42
|
+
The related question — how does `simt-dev upgrade` know which repos to bump? — has the same root
|
|
43
|
+
cause. There is no consumer registry on disk. The only thing aware of sibling repos is
|
|
44
|
+
`compare`'s one-level-deep scan, rooted at `..` of the *current checkout*. That is why
|
|
45
|
+
`compare` already prints `run: simt-dev upgrade` for every drifted repo: the detection half exists and
|
|
46
|
+
the remedy is a dangling reference.
|
|
47
|
+
|
|
48
|
+
## Ruling
|
|
49
|
+
|
|
50
|
+
### 1. The workspace is configuration, resolved in a fixed, reported order
|
|
51
|
+
|
|
52
|
+
Precedence, highest first:
|
|
53
|
+
|
|
54
|
+
1. `--workspace <path>` — an explicit flag always wins.
|
|
55
|
+
2. `SIMT_DEV_WORKSPACE` — the environment variable, for CI and one-off invocations.
|
|
56
|
+
3. A user config file at the platform's conventional location (§2).
|
|
57
|
+
4. The sibling heuristic — the parent of this package, when that parent actually contains git
|
|
58
|
+
repositories. This is what makes a fresh clone of the checkout work with no configuration at
|
|
59
|
+
all, and it is a **heuristic, not a rule**: it is consulted last and it is only accepted when
|
|
60
|
+
it verifies.
|
|
61
|
+
|
|
62
|
+
**Every command that consults the workspace reports where it came from.** `simt-dev doctor` prints
|
|
63
|
+
the resolved root and its source. An invisible configuration is a configuration nobody can debug,
|
|
64
|
+
and a silent fallback to a wrong directory is the worst outcome available: every answer is then
|
|
65
|
+
confidently wrong.
|
|
66
|
+
|
|
67
|
+
### 2. The user config file lives where the platform expects it, with a documented fallback
|
|
68
|
+
|
|
69
|
+
Candidate paths, searched in order; the first that exists wins, and the first is the one named
|
|
70
|
+
when reporting a path to create:
|
|
71
|
+
|
|
72
|
+
| Platform | Candidates, in order |
|
|
73
|
+
|---|---|
|
|
74
|
+
| any | `$XDG_CONFIG_HOME/simt-dev/config.json` |
|
|
75
|
+
| POSIX | `$HOME/.config/simt-dev/config.json` |
|
|
76
|
+
| macOS | `$HOME/Library/Application Support/simt-dev/config.json` |
|
|
77
|
+
| Windows | `%APPDATA%/simt-dev/config.json` |
|
|
78
|
+
|
|
79
|
+
`$XDG_CONFIG_HOME` first because it is the only way to *override* the location on a POSIX system,
|
|
80
|
+
and honouring an explicit override before guessing is the whole point. macOS is listed after the
|
|
81
|
+
XDG candidates deliberately: `~/Library/Application Support` is Apple's documented location for
|
|
82
|
+
application data, but a tool that honours XDG where set is friendlier to a developer who already
|
|
83
|
+
uses it, and the fallback means neither convention is unsupported.
|
|
84
|
+
|
|
85
|
+
The file is JSON. Its shape is deliberately **not** a workspace-shaped object:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"workspace": "/path/containing/repos"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`workspace` is an optional key. An empty, absent or unreadable file is not an error — it means
|
|
94
|
+
"not configured", and resolution continues down the precedence list. Only a file that exists and
|
|
95
|
+
does not parse is reported, because that is a real mistake the reader can fix.
|
|
96
|
+
|
|
97
|
+
Windows appears in the table although [ADR-012](ADR-012-drop-windows-support.md) drops Windows
|
|
98
|
+
support. The lookup function stays total so it is testable on any host and correct if that
|
|
99
|
+
reversal ever happens; `package.json` still refuses to install on Windows.
|
|
100
|
+
|
|
101
|
+
### 3. A workspace is optional; a single repo is a normal, supported case
|
|
102
|
+
|
|
103
|
+
Every command that needs a workspace must work with **one repo and no workspace at all**. Concretely:
|
|
104
|
+
|
|
105
|
+
- `compare --root <path>` where `<path>` is a repository compares that repository. Where it is a
|
|
106
|
+
directory, its git-repository children are compared. The distinction is detected, not configured,
|
|
107
|
+
because the two cases are already distinguishable from the filesystem.
|
|
108
|
+
- Discovery no longer reports "no git repositories" when it is standing in one.
|
|
109
|
+
- Nothing anywhere requires a second repo to exist.
|
|
110
|
+
|
|
111
|
+
### 4. A missing workspace is a fact, reported once — never a fatal error
|
|
112
|
+
|
|
113
|
+
When no workspace is configured and the heuristic does not verify, the skill catalogue cannot be
|
|
114
|
+
resolved from disk. That is a **warning with one remedy**, not an error:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
WARNING skill sources are not available: no workspace configured
|
|
118
|
+
REMEDY set one with --workspace <path>, SIMT_DEV_WORKSPACE, or the config file
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Registry disk verification is **skipped** in that state, with the skip itself reported by
|
|
122
|
+
`graph lint`. The alternative — 38 `ENOENT` lines — teaches the reader that the lint is noisy,
|
|
123
|
+
and a lint that is always noisy is a lint that is always ignored.
|
|
124
|
+
|
|
125
|
+
The exit code follows the same principle as `compare`'s non-adoption rule
|
|
126
|
+
([ADR-013](ADR-013-genericity-by-facts-and-local-profiles.md), and step 9 of plan 0002): exit
|
|
127
|
+
non-zero when something is **wrong**, zero when something is merely **absent**.
|
|
128
|
+
|
|
129
|
+
### 5. The decision records ship with the package
|
|
130
|
+
|
|
131
|
+
`docs/decisions` is added to `package.json` `files`. A feature whose input is not packaged is not
|
|
132
|
+
a feature; `rules why` is the provenance mechanism the whole one-canonical-home doctrine rests
|
|
133
|
+
on, and it cannot depend on a git checkout being present.
|
|
134
|
+
|
|
135
|
+
`docs/plans` ships with it, because every non-zero exit prints a remedy pointing at
|
|
136
|
+
`docs/plans/0002-simt-dev-cli.md section 11`.
|
|
137
|
+
|
|
138
|
+
### 6. "Absent", "wrong" and "moved" are three different facts
|
|
139
|
+
|
|
140
|
+
Section 4 settled the case where **no** workspace is configured. Writing the tests for it exposed
|
|
141
|
+
two more, and they are not the same case:
|
|
142
|
+
|
|
143
|
+
| State | Reported as |
|
|
144
|
+
|---|---|
|
|
145
|
+
| Nothing configured | `no workspace configured` + the remedy |
|
|
146
|
+
| Configured, directory **missing** | `the configured workspace <path> (from <source>) does not exist` |
|
|
147
|
+
| Configured, directory exists but **holds none of the named repositories** | `<path> contains none of the repositories the registry names (…)` |
|
|
148
|
+
|
|
149
|
+
All three are warnings and all three skip disk verification. What changed is the **specificity**
|
|
150
|
+
of the message, and that specificity is the whole value: "you configured nothing" and "you
|
|
151
|
+
configured the wrong directory" are different problems with different fixes, and the reader can
|
|
152
|
+
only fix the one they are told about.
|
|
153
|
+
|
|
154
|
+
The third state is the one that mattered. Existence is not a sufficient test for a workspace, and
|
|
155
|
+
before this section `--workspace <an empty directory>` produced 38 `ENOENT` lines reading as a
|
|
156
|
+
broken registry — precisely backwards, because the registry is fine and the pointer is wrong. So
|
|
157
|
+
verification requires that the workspace hold at least one of the repositories the registry
|
|
158
|
+
names, checked before any file is measured.
|
|
159
|
+
|
|
160
|
+
### 7. A workspace can be declined, and a broken config is reported whatever outranks it
|
|
161
|
+
|
|
162
|
+
Two things the first implementation got wrong, both found by running the CLI rather than by
|
|
163
|
+
testing a function:
|
|
164
|
+
|
|
165
|
+
- **There was no way to say "no workspace".** The sibling heuristic always fired when nothing
|
|
166
|
+
was configured, so a reader who did not want a directory guessed for them had no way to refuse
|
|
167
|
+
— and no test running from the checkout could reach the unconfigured path at all, because the
|
|
168
|
+
checkout always has a verifying sibling. `--workspace off`, `SIMT_DEV_WORKSPACE=off` and
|
|
169
|
+
`"workspace": "off"` now all mean *declined*, and return the same shape as any other answer so
|
|
170
|
+
callers need no special case.
|
|
171
|
+
- **A config file that does not parse was only read if nothing outranked it.** Precedence decided
|
|
172
|
+
the *answer*, but it was also suppressing a *defect*: an unparseable config is a real mistake in
|
|
173
|
+
the reader's setup, and hiding it because `SIMT_DEV_WORKSPACE` happened to be set sends them
|
|
174
|
+
looking for a configuration problem in the wrong place. The config file is therefore read
|
|
175
|
+
first, and a parse failure is returned immediately.
|
|
176
|
+
|
|
177
|
+
That second bug is the standing lesson of this record, discovered by its own meta-lesson:
|
|
178
|
+
`configPaths` defaulted `env` to `{}` so that tests could inject a fake environment, which meant
|
|
179
|
+
`XDG_CONFIG_HOME` — the one documented way to *override* the config location — was ignored on
|
|
180
|
+
every real invocation. Every unit test passed `{env}` explicitly. **A test that supplies a
|
|
181
|
+
dependency explicitly cannot detect that the default for that dependency is wrong.**
|
|
182
|
+
|
|
183
|
+
## Consequences
|
|
184
|
+
|
|
185
|
+
- The checkout keeps working with zero configuration, because the sibling heuristic is consulted
|
|
186
|
+
last and verified.
|
|
187
|
+
- A single developer with one repository never has to learn what a workspace is. `compare`,
|
|
188
|
+
`rules`, `sync`, `check`, `hooks` and `init` all work against one repo.
|
|
189
|
+
- The skill catalogue — which is SIMT content — degrades to a single warning outside the SIMT
|
|
190
|
+
workspace, while the *rules* engine stays fully functional, because `rules/` ships in the
|
|
191
|
+
package and `rules/*.md` has no external sources.
|
|
192
|
+
- `simt-dev upgrade` gets a real answer for consumer discovery: the resolved workspace, plus an
|
|
193
|
+
explicit `--root` for a single repo. No consumer list needs to be maintained in simt-dev,
|
|
194
|
+
because a repo that has not adopted cannot be listed by the thing it has not adopted.
|
|
195
|
+
|
|
196
|
+
## Rejected
|
|
197
|
+
|
|
198
|
+
- **A consumer list inside simt-dev.** A repo that has not adopted has no manifest, so it cannot
|
|
199
|
+
appear in a manifest kept by the tool it has not adopted. Discovery has to come from the
|
|
200
|
+
filesystem.
|
|
201
|
+
- **Inferring the workspace from git remotes.** Fragile, and wrong in the common case: several
|
|
202
|
+
sibling repos legitimately share one remote organisation, and a personal fork of one repo has a
|
|
203
|
+
remote that points nowhere useful.
|
|
204
|
+
- **Requiring a workspace.** It would make the common case — one repo, one user, no org — pay a
|
|
205
|
+
configuration tax to use a tool whose rules engine is already ecosystem-neutral.
|
|
206
|
+
- **A machine-wide registry of skill sources.** A directory of symlinks into the workspace would
|
|
207
|
+
reintroduce exactly the cross-repo symlink fragility that made plan 0001 choose a generated
|
|
208
|
+
projection instead.
|
|
209
|
+
- **Treating an unresolvable workspace as a registry error.** Same failure as printing 38
|
|
210
|
+
`ENOENT` lines: it converts an absent thing into a loud failure and trains the reader to ignore
|
|
211
|
+
the output.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ADR-015 — A source is identified by its content; the repository is a location, not a precondition
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-05
|
|
4
|
+
Affects: `resolve/source.mjs`, `skills install`, `skills show`, `skills brief`
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
[ADR-014](../decisions/ADR-014-workspace-is-configuration.md) made the workspace optional and
|
|
9
|
+
single-repo use first class. It fixed resolution, not lookup, and the next defect was found
|
|
10
|
+
immediately by the user who asked for the previous one.
|
|
11
|
+
|
|
12
|
+
Someone who cloned **only `simt-social`** has the `simt-social` body of `slice-commit` on disk at
|
|
13
|
+
`.agents/skills/slice-commit`. They have no workspace. The registry entry
|
|
14
|
+
`simt-social/.agents/skills/slice-commit` is satisfied by that file. Both commands refused:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
$ simt-dev skills install slice-commit
|
|
18
|
+
ERROR 2 slice-commit has 2 variants and none matches this repo (node)
|
|
19
|
+
|
|
20
|
+
$ simt-dev skills show slice-commit
|
|
21
|
+
(body unavailable: this installation has no workspace, so slice-commit has no body to fetch; …)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The body was in the current directory for both.
|
|
25
|
+
|
|
26
|
+
The root cause is a definition that was never written down. `sources: [{repo, path, bytes, role}]`
|
|
27
|
+
resolves at exactly one place — `join(workspaceRoot, repo, path)`. There is a second place, and it is
|
|
28
|
+
not even exotic: **`<root>/<path>`**, because the repository being operated on is itself a location
|
|
29
|
+
for a declared source when the registry entry's `repo` is that repository.
|
|
30
|
+
|
|
31
|
+
The refusal was also wrong for a reason that had nothing to do with the workspace. `slice-commit`
|
|
32
|
+
has two variants, `web` and `api`, discriminated by stack tag. That clone's `package.json` emitted
|
|
33
|
+
`node`, which matches neither, so variant selection failed — while the clone's own 5048 bytes were
|
|
34
|
+
an exact answer sitting in the working tree. **The tool asked the manifest a question the manifest
|
|
35
|
+
could not answer, and ignored the evidence in front of it.**
|
|
36
|
+
|
|
37
|
+
## Ruling
|
|
38
|
+
|
|
39
|
+
### 1. A declared source is found by content, at every legal location
|
|
40
|
+
|
|
41
|
+
`resolve/source.mjs` exports `candidateLocations(source, {workspaceRoot, root})` — the workspace
|
|
42
|
+
location first, then the repo-relative location — and `locateSource(source, options)`, which accepts
|
|
43
|
+
a candidate **only when the measured size equals the declared size**.
|
|
44
|
+
|
|
45
|
+
Bytes are what make this safe rather than hopeful. Every source declares its size, and `graph lint`
|
|
46
|
+
already verifies every declared size against the file on disk. A candidate that exists at the right
|
|
47
|
+
path with the wrong size is not a degraded match; it is a different file, and it is rejected.
|
|
48
|
+
|
|
49
|
+
Two rules keep this from becoming a way to install the wrong body:
|
|
50
|
+
|
|
51
|
+
- **A workspace location wins over a repo location.** The canonical body beats the incidental copy.
|
|
52
|
+
- **Content decides identity, never location.**
|
|
53
|
+
|
|
54
|
+
### 2. What the repo already holds is a resolution signal, not a fallback
|
|
55
|
+
|
|
56
|
+
`sourceHeldLocally(skill, root)` is a first-class input to source selection, placed after explicit
|
|
57
|
+
flags and after the repo's stack signals, and ahead of refusing. `chooseSource`'s precedence is now:
|
|
58
|
+
|
|
59
|
+
1. `--from` — explicit. Obeyed even when it contradicts the repo's contents; the disagreement is a
|
|
60
|
+
note, because the author is answering a question the tool cannot.
|
|
61
|
+
2. `--variant` — explicit, same treatment.
|
|
62
|
+
3. **The repo's signals name exactly one variant.** The normal path.
|
|
63
|
+
4. **The repo already holds exactly one declared source.** Its bytes are an exact answer, so this
|
|
64
|
+
settles both an unmatched repo (step 3 found nothing) and an ambiguous one (step 3 found several).
|
|
65
|
+
5. Refuse — with the reason named.
|
|
66
|
+
|
|
67
|
+
Content beats taxonomy because a byte count cannot be a coincidence. A stack tag can.
|
|
68
|
+
|
|
69
|
+
### 3. A contradiction is a refusal; an ambiguity is a decision
|
|
70
|
+
|
|
71
|
+
Two rules that look similar and are not:
|
|
72
|
+
|
|
73
|
+
- **Signals name one variant and the repo holds another** → **refuse.** One of two inputs is wrong
|
|
74
|
+
and the reader needs to know which; installing either silently picks a side.
|
|
75
|
+
- **Signals name several variants and the repo holds one of them** → **install that one**, because
|
|
76
|
+
the repo's bytes are not a guess. Reported as `the repo's own copy decides`.
|
|
77
|
+
|
|
78
|
+
### 4. A file of the right shape and the wrong body gets its own message
|
|
79
|
+
|
|
80
|
+
`simt-dev skills install` reports `<path> exists but is not the declared body (5048 bytes declared)`.
|
|
81
|
+
The alternative message — "none of the variants matches this repo" — is true and useless: it sends
|
|
82
|
+
the reader to inspect their stack tags when the actual problem is the file in front of them.
|
|
83
|
+
|
|
84
|
+
### 5. `skills show` and `skills brief` read a repo-local body, and say which
|
|
85
|
+
|
|
86
|
+
`sourcePath` tries the canonical source first and falls back to what the repo holds.
|
|
87
|
+
`skills show` gained a `body <workspace|repo> <path>` line, because a reader who has two candidate
|
|
88
|
+
locations needs to know which one answered. `skills install` gained a matching `read` line.
|
|
89
|
+
|
|
90
|
+
An install that cannot read a body **fails**. `skills show` that cannot read one still answers,
|
|
91
|
+
because a catalogue plus a pointer is useful with no workspace at all. The asymmetry is deliberate
|
|
92
|
+
and is the same asymmetry as ADR-014's: degrade the read, never the write.
|
|
93
|
+
|
|
94
|
+
## Consequences
|
|
95
|
+
|
|
96
|
+
- A clone of any single repository is fully self-sufficient for `skills show`, `skills brief` and
|
|
97
|
+
`skills install`, with no configuration at all.
|
|
98
|
+
- A repo holding two declared sources at once (`{ambiguous: true}`) is refused: the registry and the
|
|
99
|
+
repo disagree, and neither is authoritative enough to break the tie.
|
|
100
|
+
- `graph lint` remains the thing that proves the registry still matches the world. It is unchanged,
|
|
101
|
+
and it is the reason a byte comparison is trustworthy rather than optimistic.
|
|
102
|
+
- The `--from` and `--variant` escapes are load-bearing: they are now the only ways to override the
|
|
103
|
+
repo's own contents, which is correct, because overriding the evidence should be deliberate.
|
|
104
|
+
|
|
105
|
+
## Rejected
|
|
106
|
+
|
|
107
|
+
- **Treat any existing path as a match.** A near-miss body would install silently, which is worse
|
|
108
|
+
than refusing: the tool would have written a file it had not verified.
|
|
109
|
+
- **Ask for a workspace when the repo holds the body.** The workspace would then be required for the
|
|
110
|
+
one case where it is provably unnecessary.
|
|
111
|
+
- **Select the variant from the repo's directory name.** `<root>/.agents/skills/slice-commit` says
|
|
112
|
+
which skill it is, not which variant it is; only its bytes do.
|
|
113
|
+
- **Prefer the repo's copy over the workspace's.** A workspace checkout is the canonical body, and a
|
|
114
|
+
symlink fan-out can make them the same file; preferring either silently would hide a divergence.
|
|
115
|
+
- **Make variant selection advisory everywhere.** "Signals say api, the repo holds web" would
|
|
116
|
+
install one of them and print nothing, which is exactly the silent divergence variants exist to
|
|
117
|
+
prevent.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# ADR-016 — A divergent rule is retired, not merged
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-06 · one ruling narrowed by [ADR-045](ADR-045-a-dashboard-that-counts-a-defect.md)
|
|
4
|
+
Affects: `rules/mdc/*.mdc`, `projection/mdc.mjs`, `projection/project.mjs`, `projection/conflicts.mjs`, `sync`, `check`, `init`
|
|
5
|
+
|
|
6
|
+
> **Narrowed by ADR-045.** The bullet below that says `compare` reports "using `AGENTS.md`
|
|
7
|
+
> only" still governs the row's **display**, which is where it was aimed. It was read more
|
|
8
|
+
> broadly than it was written, and `compare`'s *problems list* stayed silent about stale rule
|
|
9
|
+
> files as a result: seventeen of them across five adopted repositories, none named. The
|
|
10
|
+
> problems list is not a display — it is the exit code. Every other ruling here stands, which
|
|
11
|
+
> is why this is a narrowing and not a supersession.
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The estate carried three always-on `.mdc` rule families across three repos, in eight
|
|
16
|
+
files, with no two byte-identical.
|
|
17
|
+
|
|
18
|
+
The worst pair differed by **one byte** (`simt-social/.cursor/rules/intake-questions.mdc`
|
|
19
|
+
at 1280 against `simt-social-api`'s at 1281) while hiding three semantically loaded line
|
|
20
|
+
changes: a plan-tracker path rewritten to point at the other repo, `migrations` added to the
|
|
21
|
+
list of decisions worth asking about, and a requirement silently dropped. No size-, hash-,
|
|
22
|
+
hash-adjacency- or line-count-based duplicate detector could ever have flagged that pair. The
|
|
23
|
+
reason is not that the detectors were weak: it is that **divergence here is not a formatting
|
|
24
|
+
difference, it is a semantic loss**, and the smaller copy is the one missing a rule.
|
|
25
|
+
|
|
26
|
+
Reading all eight produced three findings that a variant mechanism cannot represent:
|
|
27
|
+
|
|
28
|
+
1. `test-gate`'s fourth `BLOCK` bullet is **complementary across the pair, not alternative**.
|
|
29
|
+
`simt-social` blocks a consumer that requires an API field with no backward-compatibility
|
|
30
|
+
or deploy-order evidence; `simt-social-api` blocks a producer that ships without a test
|
|
31
|
+
asserting the new invariant. Each repo's reviewer was told to check only its own half.
|
|
32
|
+
2. `preview-postflight`'s second Requirements bullet is likewise complementary, and looks
|
|
33
|
+
like a conflict because the two files put different rules in the same slot: `simt-social`
|
|
34
|
+
says set `SOCIAL_API_BASE_URL` in the Cloudflare Workers Builds preview environment;
|
|
35
|
+
`simt-social-api` says credentials live in the operator keychain only, never in source,
|
|
36
|
+
chat, D1 or logs.
|
|
37
|
+
3. Two deletions left no trace. `simt-social-api` dropped `simt-social`'s worked example
|
|
38
|
+
(`Partial pass (e.g. 6/8 pass) is a hard blocker`) — the example is what does the work,
|
|
39
|
+
and its removal is invisible in a diff review because the rule's normative force looks
|
|
40
|
+
unchanged. `simt-social-media` dropped the entire `## Allowed Without Account` section, so
|
|
41
|
+
it has no documented set of probes that are legal without credentials.
|
|
42
|
+
|
|
43
|
+
A variant mechanism models "these are the same rule, rendered per stack". None of these three
|
|
44
|
+
is that. Two are **unions**, and one is a **deletion that must be undone**.
|
|
45
|
+
|
|
46
|
+
## Rulings
|
|
47
|
+
|
|
48
|
+
### 1. Canonical `.mdc` rules are unions, never variants
|
|
49
|
+
|
|
50
|
+
`rules/mdc/*.mdc` holds one body per rule, carrying every clause any repo needs. Where two
|
|
51
|
+
copies disagreed, both clauses are present and both are unconditional. Concretely:
|
|
52
|
+
|
|
53
|
+
- `test-gate` keeps both `BLOCK` bullets and says explicitly that they are complementary, so
|
|
54
|
+
a reader cannot tell that checking one is sufficient.
|
|
55
|
+
- `preview-postflight` keeps both Requirements bullets.
|
|
56
|
+
- `test-gate` keeps the `6/8 pass` example, and `preview-postflight` restores
|
|
57
|
+
`## Allowed Without Account` with social-api's `D1 migration verification` addition.
|
|
58
|
+
|
|
59
|
+
**Why:** a variant preserves a choice. These files do not record a choice; they record three
|
|
60
|
+
independent accidents, and two of the eight copies are strictly weaker than the best one. A
|
|
61
|
+
mechanism that keeps choosing between them will keep choosing the weaker one.
|
|
62
|
+
|
|
63
|
+
### 2. Only substrate is a substitution; only what varies per repo is declared
|
|
64
|
+
|
|
65
|
+
What genuinely differs between the copies is paths and names: a plan-tracker glob, a test
|
|
66
|
+
file path, a path vocabulary, a production domain, a preview Worker name. Those become
|
|
67
|
+
`{{placeholders}}` with **declared defaults**, in `rules/mdc/*.mdc`:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
substitutions:
|
|
71
|
+
- key: plansGlob
|
|
72
|
+
default: docs/plans/*
|
|
73
|
+
describe: glob for this repo's active plan tracker tables
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Values live in the repo's `simt-dev.json` under `substitutions`, keyed by rule id. They are
|
|
77
|
+
facts about the repo, while a rule is shared by every repo that adopts it — putting them in
|
|
78
|
+
the rule would either hard-code one repo's paths for everyone or push the decision back into
|
|
79
|
+
prose, which is the drift this mechanism exists to remove.
|
|
80
|
+
|
|
81
|
+
An **empty** value, or an empty default, drops the line it sits on. That is how one canonical
|
|
82
|
+
file reaches `simt-social-media` (no plan-tracker note) and `simt-space` (not a Workers repo
|
|
83
|
+
at all) without printing a blank bullet. A placeholder with neither a default nor a
|
|
84
|
+
declaration is a **gate failure**, because a rule shipping a literal `{{key}}` reads as
|
|
85
|
+
finished and is not.
|
|
86
|
+
|
|
87
|
+
### 3. A rule that names one product's infrastructure is opt-in, not stack-detected
|
|
88
|
+
|
|
89
|
+
`preview-postflight` declares `applies: []` and `optIn: true`. It names specific preview
|
|
90
|
+
Workers, `AUTH_PROVIDER_POLICY`, `POSTFLIGHT_PASSWORD_UID` and specific production domains.
|
|
91
|
+
Selecting it on `cloudflare-workers` would hand it to `simt-space` and `simt-goto` — Workers
|
|
92
|
+
repos with no preview Workers and no Social identity — which is the same over-match bug that
|
|
93
|
+
`rules/stacks/react-web.md` had when it declared `typescript` and `vitest`.
|
|
94
|
+
|
|
95
|
+
Detection cannot distinguish a Workers repo that has this problem from one that does not, so
|
|
96
|
+
the repo declares it: `"substitutions": { "__optIn": ["preview-postflight"] }`. `intake-questions`
|
|
97
|
+
and `test-gate` are `applies: always`, because they are genuinely stack-agnostic.
|
|
98
|
+
|
|
99
|
+
This is the first place ADR-013's fact-based detection is deliberately **not** used. A fact
|
|
100
|
+
answers "what is this repo made of"; opt-in answers "does this repo have this problem", which
|
|
101
|
+
is a different question and detection cannot reach it.
|
|
102
|
+
|
|
103
|
+
### 4. A managed `.mdc` rule is a generated file, not a region inside a hand-owned one
|
|
104
|
+
|
|
105
|
+
`AGENTS.md` keeps its hand-written prose and receives a region. A `.mdc` rule does not: the
|
|
106
|
+
body after the frontmatter **is** the region, full stop. There is no hand-owned prose to
|
|
107
|
+
preserve, because any prose that was there is exactly the divergent copy being retired.
|
|
108
|
+
|
|
109
|
+
This was found by running it. The first implementation appended the region and produced a
|
|
110
|
+
file with two `## Requirements` sections and every bullet twice — the duplication the
|
|
111
|
+
estate's own review agent (`principal-slice-reviewer` checklist item 5) is explicitly
|
|
112
|
+
instructed to block, produced by the tool meant to remove it. Appending is the obvious
|
|
113
|
+
implementation and it is wrong here.
|
|
114
|
+
|
|
115
|
+
So `sync` keeps the existing frontmatter (the editor reads `description` and `alwaysApply`
|
|
116
|
+
before any rule text) and replaces everything below it. `check` compares against the same
|
|
117
|
+
reconstruction, or it would demand the divergent text still be present.
|
|
118
|
+
|
|
119
|
+
Because that is a destructive edit to a real file, `init` and `sync` report it:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
note .cursor/rules/test-gate.mdc: retiring 928 bytes of divergent hand-written rule text
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Silent deletion is the one thing a reviewer cannot audit.
|
|
126
|
+
|
|
127
|
+
### 5. The conflict scanner must reach every rule surface, including ones added later
|
|
128
|
+
|
|
129
|
+
`RULE_FILES` named only `AGENTS.md`, `CLAUDE.md` and `CONTRIBUTING.md` for the whole life of
|
|
130
|
+
the tool. `simt-dev init` therefore reported **`CONFLICTS none detected`** on
|
|
131
|
+
`simt-social-api` — the repo holding the worst-drifted content in the estate — because it
|
|
132
|
+
never looked at the `.mdc` files at all.
|
|
133
|
+
|
|
134
|
+
A detector that only knows the filenames it was written for will quietly miss a whole rule
|
|
135
|
+
surface added later, and it will miss it *confidently*, reporting a clean result. Rule
|
|
136
|
+
discovery is now by name **and** by glob (`.cursor/rules/*.mdc`,
|
|
137
|
+
`.github/instructions/*.md`), and generated regions are stripped before scanning so that
|
|
138
|
+
quoting a ruling is never mistaken for contradicting it.
|
|
139
|
+
|
|
140
|
+
Writing that test found a second-order bug worth recording: the glob was split at its first
|
|
141
|
+
`*` and the tail matched as a name *prefix*, so `.cursor/rules/*.mdc` matched nothing. A
|
|
142
|
+
detector that silently matches zero files is indistinguishable from a repo with no rules —
|
|
143
|
+
the same failure, one level down.
|
|
144
|
+
|
|
145
|
+
### 6. An autonomous-push probe must match every phrasing the estate actually used
|
|
146
|
+
|
|
147
|
+
The probe matched `committed and pushed`, which is `simt-garmin`'s exact DoD wording. A
|
|
148
|
+
different author wrote `Always commit and pushed`; another would write `commit and push`. The
|
|
149
|
+
first version matched only the first form and reported a real autonomous-push rule as clean.
|
|
150
|
+
|
|
151
|
+
The general lesson is the one this repository keeps re-learning: **probes are tested against
|
|
152
|
+
the estate's real text, not against text written to match the probe.**
|
|
153
|
+
|
|
154
|
+
## Consequences
|
|
155
|
+
|
|
156
|
+
- `rules/mdc/{intake-questions,test-gate,preview-postflight}.mdc` are the canonical bodies.
|
|
157
|
+
Eight files in three repos become generated output.
|
|
158
|
+
- `simt-dev.json` gains a `substitutions` object, and the manifest generator seeds it empty
|
|
159
|
+
rather than leaving the key absent, so the shape is visible before anything needs it.
|
|
160
|
+
- `simt-dev sync` and `simt-dev check` handle four targets, not one. A malformed region in any target
|
|
161
|
+
refuses the whole run **before the first write** — writing the good three and skipping the
|
|
162
|
+
broken one would leave a repo half-adopted with no record of which half.
|
|
163
|
+
- `compare` continues to report one row per repo using `AGENTS.md` only, because a dashboard
|
|
164
|
+
row cannot represent four files. The gate uses the full finding list, so the dashboard's
|
|
165
|
+
coarser view is not inherited by the thing that must not be wrong.
|
|
166
|
+
|
|
167
|
+
## Rejected
|
|
168
|
+
|
|
169
|
+
- **A variant mechanism per stack** (one `test-gate.mdc` per repo). This is what produced the
|
|
170
|
+
drift; modelling it as intended behaviour would make the drift permanent and legitimate.
|
|
171
|
+
- **Picking the "best" copy per family and adopting it as canonical.** `simt-social`'s
|
|
172
|
+
`test-gate` is the better copy only by accident; it is missing the producer-side `BLOCK`
|
|
173
|
+
bullet that social-api has. Choosing one per family would have discarded a real rule.
|
|
174
|
+
- **Deleting the 8 files outright** rather than generating over them. A repo with no
|
|
175
|
+
`intake-questions.mdc` and one with a generated one must behave identically to an agent, and
|
|
176
|
+
only the second is verifiable by `simt-dev check`.
|
|
177
|
+
- **Making `preview-postflight` apply to `cloudflare-workers`.** Hands an un-actionable rule
|
|
178
|
+
to two repos, and is the same bug `react-web.md` had.
|
|
179
|
+
- **Leaving `RULE_FILES` as a fixed list** on the grounds that the three families are the only
|
|
180
|
+
`.mdc` rules that exist. They are the ones that exist *today*; that is not a reason.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ADR-017 — A declared gate must be one the repo can actually run
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-06
|
|
4
|
+
Affects: `hooks/capability.mjs`, `projection/manifest.mjs`, `commands/init.mjs`, `commands/sync.mjs` (`check`), `hooks/dispatcher.mjs`
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The first real adoption wrote a manifest declaring five gates into `simt-social-media`.
|
|
9
|
+
Three of them could not run:
|
|
10
|
+
|
|
11
|
+
| Gate | Kind | Why it could not run |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `format` | staged | prettier is not a dependency and is not in `node_modules/.bin` |
|
|
14
|
+
| `lint` | script | there is no `lint` script in `package.json` |
|
|
15
|
+
| `i18n-completeness` | command | nothing is declared under `gates.commands` |
|
|
16
|
+
|
|
17
|
+
It reported as healthy. `simt-dev check` was green, the projected files were current, and the
|
|
18
|
+
repo's own 37 tests passed. The defect was invisible to all of that because nothing had asked
|
|
19
|
+
whether the gates could run — the stack table proposed them from the repo's tags, and the tags
|
|
20
|
+
said `node` and `cloudflare-workers`, which is true.
|
|
21
|
+
|
|
22
|
+
The dispatcher would have caught it, and correctly: it fails closed. Installing it on that
|
|
23
|
+
manifest blocks **every commit** with `FAILED format … [ADR-004]`. So the first thing a new
|
|
24
|
+
user does after adopting simt-dev is discover that the tool makes committing impossible, and
|
|
25
|
+
the only escape any of them will find is `--no-verify`.
|
|
26
|
+
|
|
27
|
+
That is the exact spiral the dispatcher was built to prevent. Step 8's design note says a gate
|
|
28
|
+
that cannot run must be a failure rather than a skip, precisely because `simt-space` removed
|
|
29
|
+
tests from `pre-commit` for being slow and the team's answer was `--no-verify`. Failing closed
|
|
30
|
+
is right; declaring an unrunnable gate is wrong.
|
|
31
|
+
|
|
32
|
+
Two further facts made it survivable for as long as it was:
|
|
33
|
+
|
|
34
|
+
- **Nothing else owns the manifest.** `sync` writes the projected rules; `init` writes the
|
|
35
|
+
manifest. A manifest written by an older version is therefore never revisited, which is the
|
|
36
|
+
same class of bug as the stale `$comment` fixed in `c607d0a`.
|
|
37
|
+
- **`hooks status` already said so** — *"gate `i18n-completeness` in preCommit is a command
|
|
38
|
+
gate with no declared command; the hook will fail"*. The information existed. It was printed
|
|
39
|
+
by the one command nobody runs before installing hooks.
|
|
40
|
+
|
|
41
|
+
## Rulings
|
|
42
|
+
|
|
43
|
+
1. **A gate is declared only if the repo can run it.** `init` filters the stack table's
|
|
44
|
+
proposals through what the repo demonstrably has: a script that exists in `package.json`,
|
|
45
|
+
a binary installed in `node_modules/.bin`, a command the manifest itself names. This is
|
|
46
|
+
`has-*` fact discipline applied to gates (ADR-013): detection reports evidence, it does not
|
|
47
|
+
assume capability.
|
|
48
|
+
|
|
49
|
+
2. **Every drop is named, with the remedy that undoes it.** A silently shorter gate list reads
|
|
50
|
+
as "this repo needs no formatting check", when the truth is "declaring one would block every
|
|
51
|
+
commit". The three ways to make a gate runnable are a real choice the repo owns, so the
|
|
52
|
+
reader is given all three — install the tool, add the script, or name the command.
|
|
53
|
+
|
|
54
|
+
3. **A gate the repo already declared is never dropped.** Quietly disarming a gate somebody
|
|
55
|
+
declared on purpose is a worse surprise than a visible one, and a re-init that silently
|
|
56
|
+
removed gates would make the gate list look like simt-dev's opinion rather than the repo's.
|
|
57
|
+
|
|
58
|
+
4. **`check` reports an unrunnable declared gate as a finding.** The repo must be red *before*
|
|
59
|
+
anyone installs hooks, not after. `check` is the gate that runs in CI, so this is where a
|
|
60
|
+
landmine belongs; `hooks status` saying so is not enough when it is the command nobody runs.
|
|
61
|
+
|
|
62
|
+
5. **The remedy differs by whether a manifest already exists.** A re-init never re-adds a gate
|
|
63
|
+
to a manifest that already declares one, so on an existing manifest "install prettier, then
|
|
64
|
+
re-run init" would send the reader round the loop and back to where they started. The remedy
|
|
65
|
+
there names the declaration as the second half of the fix.
|
|
66
|
+
|
|
67
|
+
## Consequences
|
|
68
|
+
|
|
69
|
+
- `simt-dev init` needs to read the repo before it writes the manifest, so `init` now depends on
|
|
70
|
+
the hooks subsystem. The dependency runs one way: `gates.mjs` imports nothing.
|
|
71
|
+
- `check` is no longer only about the generated rules. It is the gate over what the manifest
|
|
72
|
+
promises, which is what a gate is for.
|
|
73
|
+
- A repo whose only answer is "no tool for any of these" gets `preCommit: []`. That is a valid
|
|
74
|
+
manifest, and the dispatcher says so in one line rather than passing in silence. A string
|
|
75
|
+
where the caller expected an array once printed that line one character at a time.
|
|
76
|
+
|
|
77
|
+
## Rejected
|
|
78
|
+
|
|
79
|
+
- **Fail open in the dispatcher when a gate cannot run.** The spiral is the point; failing open
|
|
80
|
+
makes the tool's green meaningless, which is worse than a blocked commit because it is silent.
|
|
81
|
+
- **Infer capability from the dependency graph.** A repo with `prettier` in `devDependencies`
|
|
82
|
+
and no `node_modules` cannot run a formatter. Reading the manifest and the installed binaries
|
|
83
|
+
are two facts; guessing a third from a lockfile is how the two get to disagree.
|
|
84
|
+
- **Have `init` refuse to adopt a repo that cannot run its gates.** Refusing is worse than
|
|
85
|
+
writing fewer gates: the repo gets no rules at all, and the obstacle is a formatting tool.
|
|
86
|
+
- **Leave it to `hooks status`.** It is the command run after installing hooks, which is one
|
|
87
|
+
step after the commit that fails.
|
|
88
|
+
- **Drop unrunnable gates on re-init too.** Silently; the reader sees a shorter list and no
|
|
89
|
+
explanation, and cannot tell a removed gate from one that was never there.
|