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,114 @@
|
|
|
1
|
+
# ADR-013 — Genericity comes from emitted facts and repo-local profiles, not from a bigger hardcoded tag table
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-05
|
|
4
|
+
Affects: the `simt-dev` CLI (fingerprint, profile selection, registry validation)
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The engine is already only loosely coupled to SIMT. Measured, not asserted: `src/argv.mjs`,
|
|
9
|
+
`src/resolve/resolve.mjs` and `src/graph/load.mjs` contain **zero** non-comment,
|
|
10
|
+
non-string references to SIMT. Everywhere else, "SIMT" appears as ADR citations in comments
|
|
11
|
+
documenting *why*, plus one hardcoded npm scope in the hook shim. The project is named SIMT;
|
|
12
|
+
the mechanisms are not.
|
|
13
|
+
|
|
14
|
+
The coupling that does exist is in the **vocabulary**, not the code. `fingerprint()` emits 17
|
|
15
|
+
tags from three hardcoded tables (`MARKERS`, `EXTENSION_TAGS`, `DEP_TAGS`) plus a literal
|
|
16
|
+
`'wear-os'`, and those tables are keyed by *curated product* names: `connectiq`,
|
|
17
|
+
`maplibre`, `hono`, `react-router`. That has three consequences, and only the first is
|
|
18
|
+
obvious:
|
|
19
|
+
|
|
20
|
+
1. **A new ecosystem needs a code change.** Detecting Django is one line in `MARKERS`; the
|
|
21
|
+
engine then hands a Django repo the generic `node` fallback profile, which is wrong advice,
|
|
22
|
+
and there is nowhere to put the correct profile because `rules/stacks/` is central-only.
|
|
23
|
+
2. **The tag names encode opinions that age badly.** `hono` and `maplibre` are libraries, not
|
|
24
|
+
stacks. A profile that selects on a library breaks the day the library is swapped.
|
|
25
|
+
3. **Detection is untestable in the abstract.** There is no way to ask "what did the tool
|
|
26
|
+
actually observe?" other than reading three tables.
|
|
27
|
+
|
|
28
|
+
There is also an unresolved question from the design review: a **hosted MCP service** would
|
|
29
|
+
make the tool more capable, but it was not clear whether it would make it more *generic*, more
|
|
30
|
+
*accurate*, or merely more available.
|
|
31
|
+
|
|
32
|
+
## Ruling
|
|
33
|
+
|
|
34
|
+
### 1. Detection emits facts; profiles select on facts
|
|
35
|
+
|
|
36
|
+
`fingerprint()` gains a second, orthogonal output: **`facts`** — observable, ecosystem-neutral
|
|
37
|
+
properties of the working tree, named `has-*` (`has-pyproject`, `has-go-mod`,
|
|
38
|
+
`has-cargo-toml`, `has-migrations-dir`, `has-dockerfile`, …). Facts are derived from evidence,
|
|
39
|
+
never from a product taxonomy, so there is no table to grow when a new ecosystem arrives: the
|
|
40
|
+
fact was already being observed.
|
|
41
|
+
|
|
42
|
+
The existing product tags remain, because a profile that genuinely wants "this is a Cloudflare
|
|
43
|
+
Worker" should say so in words that mean something to a reader. But a profile may now select
|
|
44
|
+
on **either**, and new profiles are expected to prefer facts where a fact will do:
|
|
45
|
+
|
|
46
|
+
- `applies: [has-cargo-toml]` — honest, survives the framework being replaced
|
|
47
|
+
- `applies: [cloudflare-workers]` — precise, dies with the platform
|
|
48
|
+
|
|
49
|
+
Selection is the union of both sets. A profile therefore needs **no engine change** to be
|
|
50
|
+
usable by an ecosystem nobody has heard of.
|
|
51
|
+
|
|
52
|
+
### 2. Profiles are content, and content may live in the repo
|
|
53
|
+
|
|
54
|
+
`simt-dev init` prints where local profiles live and the manifest gains a `profiles` array
|
|
55
|
+
naming profile ids. The directory needs no creation — the first file in it creates it, and
|
|
56
|
+
an empty directory would not survive git anyway. Central profiles ship in `rules/stacks/`; **repo-local profiles are read from
|
|
57
|
+
`.simt-dev/profiles/*.md`**, use the same frontmatter contract (ADR-009), and are selected
|
|
58
|
+
with the same matcher. A local profile may declare `fallback: true`.
|
|
59
|
+
|
|
60
|
+
A repo that ships its own profile has made a decision about its own conventions. It does not
|
|
61
|
+
need a central contribution, a release, or a maintainer's permission — and when it is wrong,
|
|
62
|
+
the fix is a one-file PR in the repo that is wrong, which is the correct blast radius
|
|
63
|
+
(ADR-002).
|
|
64
|
+
|
|
65
|
+
Local profiles are **additive and shadowing**: a local `id` replaces a central one with the
|
|
66
|
+
same id, and otherwise both are considered. Central content is never silently overridden in
|
|
67
|
+
place — it is superseded by an explicit, reviewable file.
|
|
68
|
+
|
|
69
|
+
### 3. A hosted service may serve authoring; it may never serve resolution
|
|
70
|
+
|
|
71
|
+
These are different products and must not be conflated.
|
|
72
|
+
|
|
73
|
+
| | Authoring | Resolution |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| Hosted MCP is useful | yes — discovering profiles, learning an ecosystem, publishing findings | **no** |
|
|
76
|
+
| Non-determinism | tolerable: a human reviews the output | fatal: "did the agent follow the rules?" becomes unanswerable |
|
|
77
|
+
| Offline | irrelevant | required: a hook and a gate must not depend on a network call |
|
|
78
|
+
| Diffable in a PR | partially | required: `simt-dev check` output is the gate's whole product |
|
|
79
|
+
| Fails | loudly, at authoring time | must fail **closed** with a remedy |
|
|
80
|
+
|
|
81
|
+
A hosted resolver would make every gate in the estate depend on a network round trip and a
|
|
82
|
+
model's ranking, and would make CI results unreproducible. That is a worse tool, not a more
|
|
83
|
+
capable one.
|
|
84
|
+
|
|
85
|
+
**Community extensibility therefore ships as a registry of portable profile files** in a git
|
|
86
|
+
repository, fetched and pinned by the same single-SHA mechanism (ruling 4) that already pins
|
|
87
|
+
rules. Same trust model, no hosted runtime, and the diff a reviewer sees is the diff of a
|
|
88
|
+
profile file.
|
|
89
|
+
|
|
90
|
+
## Consequences
|
|
91
|
+
|
|
92
|
+
- `fingerprint()` returns `{stack, facts, evidence, manifest, error}`. `evidence` gains the
|
|
93
|
+
file that proved each fact, so `simt-dev doctor` can answer "why did you think that?" without a
|
|
94
|
+
human re-deriving it.
|
|
95
|
+
- Every tag a profile may name is still emitted by the engine. The existing test — *tags used
|
|
96
|
+
by content but not emitted by the engine is the empty set* — now covers facts too, and the
|
|
97
|
+
registry validator and the profile loader share one derived vocabulary so they cannot
|
|
98
|
+
desynchronise.
|
|
99
|
+
- `graph lint` validates against the same derived vocabulary, so a profile naming a fact the
|
|
100
|
+
engine cannot emit fails at lint time rather than selecting nothing at runtime.
|
|
101
|
+
- Step 11 of the implementation sequence is genericity; it lands **before** `compare`, because
|
|
102
|
+
it changes the tag vocabulary and every later step is cheaper once it is settled.
|
|
103
|
+
|
|
104
|
+
## Rejected
|
|
105
|
+
|
|
106
|
+
- **A larger hardcoded tag table.** Moves the wall, does not remove it. Every new ecosystem
|
|
107
|
+
would still need a maintainer, a code change and a release, and the names would still encode
|
|
108
|
+
today's libraries rather than the repo's actual properties.
|
|
109
|
+
- **Community profiles in a hosted registry served at runtime.** See the table above.
|
|
110
|
+
- **Semantic or embedding search as the mechanism for choosing rules.** Already rejected in
|
|
111
|
+
plan 0002 §3, and this ruling reinforces it: it is a resolution mechanism, so it inherits
|
|
112
|
+
every constraint in row two of the table above.
|
|
113
|
+
- **Fingerprinting on library names alone.** Precise today, and silently wrong the day a
|
|
114
|
+
dependency is replaced.
|
|
@@ -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 `repo-14`** has the `repo-14` body of `slice-commit` on disk at
|
|
13
|
+
`.agents/skills/slice-commit`. They have no workspace. The registry entry
|
|
14
|
+
`repo-14/.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** (`repo-14/.cursor/rules/intake-questions.mdc`
|
|
19
|
+
at 1280 against `repo-11`'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
|
+
`repo-14` blocks a consumer that requires an API field with no backward-compatibility
|
|
30
|
+
or deploy-order evidence; `repo-11` 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: `repo-14`
|
|
34
|
+
says set `SOCIAL_API_BASE_URL` in the Cloudflare Workers Builds preview environment;
|
|
35
|
+
`repo-11` says credentials live in the operator keychain only, never in source,
|
|
36
|
+
chat, D1 or logs.
|
|
37
|
+
3. Two deletions left no trace. `repo-11` dropped `repo-14`'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. `repo-13` 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 `repo-13` (no plan-tracker note) and `repo-15` (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 `repo-15` and `repo-08` — 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
|
+
`repo-11` — 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 `repo-07`'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.** `repo-14`'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.
|