simt-dev 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -2
  4. package/bin/simt-dev.mjs +28 -0
  5. package/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  6. package/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  7. package/docs/decisions/ADR-003-test-breadth.md +46 -0
  8. package/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  9. package/docs/decisions/ADR-005-file-length.md +42 -0
  10. package/docs/decisions/ADR-006-coverage-bar.md +59 -0
  11. package/docs/decisions/ADR-007-section-skeleton.md +54 -0
  12. package/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  13. package/docs/decisions/ADR-009-skill-layout.md +48 -0
  14. package/docs/decisions/ADR-010-runner-invocation.md +56 -0
  15. package/docs/decisions/ADR-011-windows-support.md +52 -0
  16. package/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  17. package/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  18. package/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  19. package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  20. package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  21. package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  22. package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  23. package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  24. package/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  25. package/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  26. package/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  27. package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  28. package/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  29. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  30. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  31. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  32. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
  33. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  34. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  35. package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  36. package/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  37. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  38. package/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  39. package/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  40. package/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  41. package/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  42. package/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  43. package/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  44. package/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  45. package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  46. package/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  47. package/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  48. package/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  49. package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  50. package/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  51. package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  52. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  53. package/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  54. package/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
  55. package/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  56. package/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  57. package/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  58. package/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  59. package/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  60. package/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  61. package/docs/decisions/README.md +528 -0
  62. package/graph/skills.graph.json +1622 -0
  63. package/package.json +41 -4
  64. package/rules/core.md +76 -0
  65. package/rules/mdc/intake-questions.mdc +50 -0
  66. package/rules/mdc/preview-postflight.mdc +61 -0
  67. package/rules/mdc/slice-commit.mdc +52 -0
  68. package/rules/mdc/test-gate.mdc +54 -0
  69. package/rules/stacks/android.md +63 -0
  70. package/rules/stacks/cloudflare-workers.md +65 -0
  71. package/rules/stacks/connectiq.md +62 -0
  72. package/rules/stacks/container.md +31 -0
  73. package/rules/stacks/jvm.md +54 -0
  74. package/rules/stacks/node.md +45 -0
  75. package/rules/stacks/python.md +48 -0
  76. package/rules/stacks/react-native.md +58 -0
  77. package/rules/stacks/react-web.md +61 -0
  78. package/rules/stacks/typescript.md +21 -0
  79. package/rules/stacks/uv.md +32 -0
  80. package/src/argv.mjs +116 -0
  81. package/src/ci/dependabot.mjs +268 -0
  82. package/src/ci/workflow.mjs +360 -0
  83. package/src/commands/ci.mjs +285 -0
  84. package/src/commands/compare.mjs +195 -0
  85. package/src/commands/graph.mjs +73 -0
  86. package/src/commands/help.mjs +108 -0
  87. package/src/commands/hooks.mjs +471 -0
  88. package/src/commands/index.mjs +241 -0
  89. package/src/commands/init.mjs +566 -0
  90. package/src/commands/mcp.mjs +63 -0
  91. package/src/commands/rules.mjs +374 -0
  92. package/src/commands/skills.mjs +647 -0
  93. package/src/commands/skillsinstall.mjs +515 -0
  94. package/src/commands/sync.mjs +331 -0
  95. package/src/commands/upgrade.mjs +202 -0
  96. package/src/compare/discover.mjs +212 -0
  97. package/src/compare/hooksstate.mjs +159 -0
  98. package/src/compare/row.mjs +299 -0
  99. package/src/decisions/index.mjs +512 -0
  100. package/src/graph/load.mjs +541 -0
  101. package/src/hooks/capability.mjs +129 -0
  102. package/src/hooks/dispatcher.mjs +268 -0
  103. package/src/hooks/gates.mjs +144 -0
  104. package/src/hooks/resolve.mjs +194 -0
  105. package/src/hooks/shim.mjs +140 -0
  106. package/src/hooks/where.mjs +118 -0
  107. package/src/locate.mjs +75 -0
  108. package/src/mcp/server.mjs +167 -0
  109. package/src/mcp/tools.mjs +410 -0
  110. package/src/name.mjs +95 -0
  111. package/src/output/index.mjs +96 -0
  112. package/src/projection/baselines.mjs +209 -0
  113. package/src/projection/conflicts.mjs +205 -0
  114. package/src/projection/manifest.mjs +478 -0
  115. package/src/projection/markers.mjs +349 -0
  116. package/src/projection/mdc.mjs +327 -0
  117. package/src/projection/project.mjs +504 -0
  118. package/src/projection/rules.mjs +640 -0
  119. package/src/projection/surfaces.mjs +398 -0
  120. package/src/projection/targets.mjs +221 -0
  121. package/src/resolve/fingerprint.mjs +553 -0
  122. package/src/resolve/near.mjs +61 -0
  123. package/src/resolve/resolve.mjs +457 -0
  124. package/src/resolve/source.mjs +105 -0
  125. package/src/toolchain.mjs +202 -0
  126. package/src/tsconfig.mjs +314 -0
  127. package/src/upgrade/plan.mjs +203 -0
  128. package/src/workspace.mjs +289 -0
@@ -0,0 +1,42 @@
1
+ # ADR-005 — File length is two tiers: 400 review trigger, 1000 hard ceiling
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ - `simt-social` / `simt-social-api`: "**Split any file once it exceeds 1000 lines**; extract
8
+ along the nearest cohesive boundary instead of growing the file further."
9
+ - `simt-garmin`: "**File length: 400 lines is a smell threshold, not a hard gate.**" Generated
10
+ `.mc` files are exempt, with a named exception for a 408-line template.
11
+ - Several repos: no file-length rule stated at all.
12
+
13
+ The two numbers are not in conflict — they measure different things. 400 is where a human
14
+ should start asking questions; 1000 is where the file must already have been split.
15
+
16
+ ## Ruling
17
+
18
+ Two tiers, both advisory in wording but checkable in `simt-dev check`:
19
+
20
+ - **400 lines — review trigger.** Crossing it requires a stated reason to keep the file whole.
21
+ - **1000 lines — hard ceiling.** `simt-dev check` fails. Extract along the nearest cohesive
22
+ boundary.
23
+ - **Generated files are exempt**, declared per repo in `simt-dev.json`. The hook must name the
24
+ generator.
25
+
26
+ Per-repo overrides are permitted via `limits.fileLines` in `simt-dev.json`.
27
+
28
+ ## Consequences
29
+
30
+ - The Social repos' hard 1000 gate and garmin's soft 400 both survive unchanged in force.
31
+ - The generated-file exemption that garmin had to state in prose becomes a declared property,
32
+ so the hook can honour it without hardcoding `.mc`.
33
+ - `simt-social`'s pre-commit file-size check (currently inlined as 1000 lines of shell, and
34
+ duplicating `scripts/check-file-sizes.ts`) becomes one implementation in the shared
35
+ dispatcher.
36
+
37
+ ## Rejected
38
+
39
+ - **One number org-wide.** Either it is too tight for legitimately long declarative files or
40
+ too loose to prompt a human at 400.
41
+ - **No rule.** The Social repos' pre-commit check exists precisely because a rule was missing,
42
+ and it is now implemented twice.
@@ -0,0 +1,59 @@
1
+ # ADR-006 — Evidence discipline is central; the numeric bar is per-repo
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ The coverage bar is the one concept that is mutually incompatible *in kind* across the estate:
8
+
9
+ | Repo | Bar |
10
+ |---|---|
11
+ | Repos with a declared numeric bar (outside this estate) | Quantitative. `fail_under = 100`, branch coverage, "a behavioural coverage gate, not a line-count scoreboard" |
12
+ | `simt-garmin` | Quantitative, but a different quantity. Memory: 64 KB app / 24 KB Glance against a 32 KB ceiling, plus 12 named lint scripts |
13
+ | `simt-social`, `simt-social-api`, `simt-social-media` | Qualitative. No number anywhere. Evidence discipline: named commands, 100 % pass, no unjustified skips, behavioural test delta |
14
+ | `simt-api` | Neither. Three verification commands. |
15
+
16
+ You cannot average these. 100 % line coverage is unreachable for Kotlin/Compose UI code and for
17
+ Monkey C memory budgets; a memory ceiling is not a coverage proxy; "no number" is not a weaker
18
+ version of 100 %, it is a different instrument.
19
+
20
+ ## Ruling
21
+
22
+ **The central rule mandates evidence discipline. The numeric threshold is a declared,
23
+ per-repo constant.**
24
+
25
+ Central, mandatory, portable — for every repo regardless of stack:
26
+
27
+ 1. Name the exact commands and the exact test files.
28
+ 2. Those commands must exit 0 with **all** selected tests passing. Partial pass is a hard
29
+ blocker; "6/8 pass" is the canonical worked example of a blocked state.
30
+ 3. No test is skipped without a pre-existing written justification.
31
+ 4. Every executable change carries a test delta: new or extended test for new behaviour, a
32
+ regression test reproducing the failure mode for a bugfix, or a stated reason existing tests
33
+ fully prove the invariant.
34
+ 5. Never claim a test you did not run. Never cite a partial suite as evidence.
35
+ 6. Approval by budget never converts a known failure into an approval.
36
+
37
+ Per-repo, declared in `simt-dev.json` under `limits`: the numeric gate, if any — coverage
38
+ percentage, memory budget, size ceiling. The central rule specifies *how to declare it*, never
39
+ the value.
40
+
41
+ ## Consequences
42
+
43
+ - A repo that declares `coverage: 100` keeps it and its "untested paths are typed holes"
44
+ framing intact; the ruling constrains only what a new repo declares.
45
+ - garmin declares its memory budgets in the same slot, and its "layer-conditional 5a–5m matrix"
46
+ becomes a declared gate list rather than 12 prose sentences.
47
+ - The three social repos declare no number and keep their evidence discipline verbatim — which
48
+ is now shared rather than independently re-invented in each.
49
+ - A new repo inherits a real policy from `simt-dev init` with no configuration, and can opt into a
50
+ number later without changing the rule.
51
+
52
+ ## Rejected
53
+
54
+ - **Hard 100 % coverage org-wide.** Unreachable in Kotlin/Compose and meaningless for Monkey C;
55
+ would push repos to delete the rule rather than meet it.
56
+ - **No number anywhere.** Leaves a genuinely strong gate unenforced org-wide and invites
57
+ every new repo to skip the question.
58
+ - **One number for coverage and a separate slot for memory.** Two slots invite a repo to declare
59
+ only the one it likes.
@@ -0,0 +1,54 @@
1
+ # ADR-007 — One canonical section skeleton
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ Six `AGENTS.md` files use the same conceptual sections under three different headings:
8
+
9
+ | File | Authority | Invariants |
10
+ |---|---|---|
11
+ | `simt-api`, `simt-social-media` | "Authority and Scope" | "Non-Negotiable Invariants" |
12
+ | `simt-garmin`, `simt-social` | "Authority and scope" | "Non-negotiable invariants" |
13
+ | `simt-social-api` | "Authority and **ownership**" | "Non-negotiable invariants" |
14
+
15
+ At least one repo outside this estate uses a numbered 12-part layout with no equivalent
16
+ sections at all.
17
+
18
+ The deeper difference is in what "non-negotiable" means. Garmin's are **platform physics**
19
+ ("breaking any of these produces a product that is wrong, crashes, or cannot be shipped"); the
20
+ Social pair's are **product and privacy policy**; api/media's are **data and schema
21
+ invariants**. Those are three different kinds of claim wearing one heading, which is why the
22
+ naming drifted — nobody could tell whether the sections were meant to align.
23
+
24
+ ## Ruling
25
+
26
+ One canonical skeleton, sentence case:
27
+
28
+ ```
29
+ ## Authority and ownership
30
+ ## Non-negotiable invariants
31
+ ## Engineering conventions
32
+ ## Workflow and validation
33
+ ```
34
+
35
+ Repos may add sections after these; they may not rename them. A repo that arrives with its
36
+ own numbered layout keeps it during adoption and maps each part onto a canonical section in
37
+ `simt-dev.json`, so both structures remain navigable while it migrates.
38
+
39
+ Each invariant states **which kind of claim it is** — platform, policy, or data — so a reader
40
+ knows immediately what breaking it costs and who may change it.
41
+
42
+ ## Consequences
43
+
44
+ - Cross-repo reading becomes possible: section N means the same class of thing everywhere.
45
+ - A future grep for "what are the invariants" returns a comparable list across all 16 repos.
46
+ - "Ownership" is kept in the heading because `simt-social-api` earned it by being the only
47
+ file that enumerates ownership explicitly.
48
+
49
+ ## Rejected
50
+
51
+ - **Standardise on Title Case** (`simt-api`'s variant). Capitalisation in headings is noise;
52
+ three repos already use sentence case.
53
+ - **Normalise a differently-shaped layout immediately.** A mechanical conversion of 12
54
+ numbered parts would produce a worse document than a mapping.
@@ -0,0 +1,59 @@
1
+ # ADR-008 — One review verdict vocabulary: three values
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ The estate has two incompatible verdict vocabularies.
8
+
9
+ `.cursor/rules/test-gate.mdc` — used by all three Social repos — allows one outcome:
10
+
11
+ > Return `BLOCK` when: any test fails, is skipped without pre-existing justification, or is
12
+ > unrun; the executable diff has no test delta without explicit justification; the coordinator
13
+ > cites partial suite results or defers failing tests.
14
+
15
+ `principal-milestone-reviewer.agent.md` — the final pre-commit gate — uses three:
16
+
17
+ > `APPROVE` / `APPROVE WITH RESIDUAL RISK` / `BLOCK`
18
+
19
+ With only two values, a reviewer who finds no blocker but did not finish has no honest answer
20
+ and is pushed toward `APPROVE` by omission or toward `BLOCK` by exaggeration. With three, the
21
+ budget mechanism works: "Use one focused pass, normally no more than 5 minutes or 8 targeted
22
+ tool calls… If the budget is reached without a concrete blocker, stop and return `APPROVE WITH
23
+ RESIDUAL RISK`. List what was not reviewed."
24
+
25
+ The agent also re-embeds the `.mdc` text verbatim as its checklist item 1, so the vocabulary
26
+ mismatch propagates from the rule into the gate.
27
+
28
+ ## Ruling
29
+
30
+ Adopt the three-value vocabulary everywhere:
31
+
32
+ | Verdict | Meaning |
33
+ |---|---|
34
+ | `BLOCK` | A concrete defect, missing mandatory gate, partial test pass, or missing test delta. Always paired with a file:line or command reference. |
35
+ | `APPROVE WITH RESIDUAL RISK` | No blocker found within the review budget. **Must** list what was not reviewed. |
36
+ | `APPROVE` | Fully reviewed, no blocker. |
37
+
38
+ `APPROVE WITH RESIDUAL RISK` never converts a known failure into an approval — that constraint
39
+ travels with the verdict, verbatim from the review agent.
40
+
41
+ The `.mdc` files are updated to the three-value vocabulary. `BLOCK` remains the only value that
42
+ stops work; the other two do not weaken the block conditions, which are stated identically in
43
+ all three rules.
44
+
45
+ ## Consequences
46
+
47
+ - A reviewer who runs out of budget has a correct answer, so the budget stops being a reason to
48
+ over-search.
49
+ - Rule text and agent text stop diverging, because they now share a vocabulary and the agent no
50
+ longer has to paraphrase the rule.
51
+ - Output format is uniform across the 15 agent files: severity-ordered findings, clickable
52
+ file:line references, a verdict, and — when not fully reviewed — the unreviewed list.
53
+
54
+ ## Rejected
55
+
56
+ - **`BLOCK` only.** Loses the honest "I did not finish" answer and makes the strict review
57
+ budget unusable as written.
58
+ - **Four values** (adding `NEEDS WORK`). Three already distinguishes blocked from
59
+ budget-limited; a fourth would be chosen on judgement and collapse back into two.
@@ -0,0 +1,48 @@
1
+ # ADR-009 — Skills are `<name>/SKILL.md` with mandatory frontmatter
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ All 37 skill entries in the estate use `<name>/SKILL.md`. A wider audit that included repos
8
+ now out of scope found exactly one entry that did not: a flat 7 266-byte file that also omitted
9
+ the `name` frontmatter key. The layout ruling exists because that defect class is real, not
10
+ because this estate still has it.
11
+
12
+ Frontmatter field usage across the wider audit's 46 entries: `name` 45, `description` 46,
13
+ `argument-hint` 14, `disable-model-invocation` 2, `references` 1, `compatibility` 1, and
14
+ **`globs` zero**. No skill in any audited repo scopes itself by file pattern.
15
+
16
+ That last point matters for discovery. A skill that could declare `globs: "migrations/**"`
17
+ would let the resolver select it from the diff alone. Since none do, path-based resolution has
18
+ to come from the graph registry instead — which is why `paths` lives in
19
+ `graph/skills.graph.json` (plan 0002 §3.2) rather than in frontmatter.
20
+
21
+ ## Ruling
22
+
23
+ - A skill is a directory `<name>/` containing `SKILL.md`.
24
+ - `name` and `description` are **mandatory** in frontmatter.
25
+ - `argument-hint` is recommended for any skill invoked by hand, and is optional otherwise —
26
+ simt-app's 7 skills omit it legitimately, since they are task-shaped rather than
27
+ user-invoked.
28
+ - Body size and reference-file count are declared in `graph/skills.graph.json`, not in
29
+ frontmatter, so the registry stays the single source of truth for resolution metadata.
30
+
31
+ The one flat file found in the wider audit was wrapped into `coverage-hunt/SKILL.md` with
32
+ its `name` added. This estate has no such entry left to migrate.
33
+
34
+ ## Consequences
35
+
36
+ - Every skill becomes addressable by one stable name across all 16 repos, which is a
37
+ precondition for `simt-dev skills find/show/install` and for `variant-of` edges.
38
+ - The registry in `graph/skills.graph.json` becomes the one place resolution metadata lives,
39
+ instead of metadata split across frontmatter, prose and directory size.
40
+ - `simt-dev skills eval` can key on skill names, because every skill has one.
41
+
42
+ ## Rejected
43
+
44
+ - **Require `globs` in frontmatter.** Attractive, but it would put resolution metadata in two
45
+ places and drift from the graph — the exact failure mode that produced the 8 divergent
46
+ `.mdc` files. Recorded as considered and rejected.
47
+ - **Keep flat `.md` as an accepted form.** Two layouts means two resolver paths and two sets of
48
+ conventions to police.
@@ -0,0 +1,56 @@
1
+ # ADR-010 — Invoke tools through the repo's own runner, never a package runner
2
+
3
+ Status: **accepted** · 2026-10-05 · affects all repos
4
+
5
+ ## Context
6
+
7
+ Four repos independently arrived at the same rule, in two ecosystems, with the same rationale.
8
+
9
+ - `simt-social` / `simt-social-api`: "Run tests and quality checks through repository `npm run
10
+ ...` scripts… **Do not invoke local package binaries through `npx` or `npm exec`** unless no
11
+ repository script supports the required operation."
12
+ - A Python repo outside this estate, in the same form: "**Do not use `uv run pytest`**; the
13
+ console entrypoint can miss the `uv` import path and fail to import dependencies." Its
14
+ contributor guide states the same rule with a stronger failure description: it "can
15
+ **silently** run against the wrong environment."
16
+
17
+ The failure mode is identical: a package runner resolves a binary outside the project's
18
+ configured environment, so the tool runs, the import path is wrong, and the result is either an
19
+ obvious import error or — worse — a pass against the wrong interpreter.
20
+
21
+ Note the severity disagreement on that last point: "fail to import" versus "silently run
22
+ against the wrong environment." The second is far worse and is treated as canonical below.
23
+
24
+ ## Ruling
25
+
26
+ Invoke tools through the repository's own declared runner, in every ecosystem:
27
+
28
+ - Node: `npm run <script>`. Never `npx` or `npm exec` for a binary that is a declared
29
+ dependency.
30
+ - Python: the module form, `uv run -m pytest`, or the repo's own wrapper. Never the bare
31
+ console entrypoint.
32
+ - Gradle: `./gradlew`, never a system `gradle`.
33
+ - Any other ecosystem: the repo's declared entrypoint, never a bare global binary.
34
+
35
+ **Read the actual runner manifest before running anything.** A command that does not exist in
36
+ `package.json` is not a command. This is stated because it is the other half of the failure:
37
+ guessing a script name is how an agent concludes a check "passed" when it never ran.
38
+
39
+ Exception: a tool that is genuinely not a project dependency (a one-off inspector, a global
40
+ formatter) may be invoked directly, provided its version is pinned.
41
+
42
+ ## Consequences
43
+
44
+ - The estates' two ecosystems converge on one rule, so an agent moving between a Python repo
45
+ and a TypeScript repo does not have to learn the rule twice.
46
+ - The stronger failure description is canonical: silently running against the wrong environment
47
+ is worse than failing, and the rule exists to prevent it.
48
+ - Repos with no declared scripts get them from the tooling baseline (plan 0001 §3.1), so this
49
+ ruling is always actionable.
50
+
51
+ ## Rejected
52
+
53
+ - **Adopt only the npm half.** Leaves the identical failure unguarded in Python, Gradle and
54
+ every future ecosystem.
55
+ - **Permit `npx` for convenience.** `npx` will fetch a package that is not the project's
56
+ version, which reintroduces exactly the environment drift the rule prevents.
@@ -0,0 +1,52 @@
1
+ # ADR-011 — Windows is supported from v1
2
+
3
+ Status: **superseded by [ADR-012](ADR-012-drop-windows-support.md)** · 2026-10-05
4
+ Affects: the `simt-dev` CLI
5
+
6
+ > Superseded the same day. The cost table below is retained because it is what ADR-012's
7
+ > reasoning turns on: three of the five obligations are permanent tax on the hook dispatcher
8
+ > and the projection injector, not one-off work. See ADR-012 for the weighing.
9
+
10
+ ## Context
11
+
12
+ No Windows developer currently appears in the estate, and the cheap alternative was
13
+ `"os": ["darwin", "linux"]` in `package.json`, which makes npm refuse to install on Windows
14
+ with a clear message instead of failing obscurely inside a git hook.
15
+
16
+ That option was put to you and declined in favour of full v1 support.
17
+
18
+ The cost is real and is recorded here so it is paid deliberately rather than discovered:
19
+
20
+ | Obligation | Why |
21
+ |---|---|
22
+ | Hook scripts must not assume POSIX | `core.hooksPath` hooks run under Git for Windows' bundled sh, which does exist — but the dispatcher cannot assume a POSIX `#!/bin/sh` shebang alone is sufficient. Hook entrypoints are `.cmd`-aware or invoked through `node` explicitly. |
23
+ | The projection injector must normalise line endings | The audit found **10 of 16 repos have no `.editorconfig`**, so a generated region written with the host's EOL would produce a whole-file diff on Windows checkouts. The injector writes the canonical EOL from `.gitattributes` (`* text=auto eol=lf`), defaulting to LF. |
24
+ | Path handling must not assume `/` | Resolver globs, `paths` matching and every emitted file reference must normalise separators. |
25
+ | npm bin shim | The published package must rely on npm's generated shim rather than a hand-written launcher. |
26
+ | CI must actually run it | A macOS-only CI run cannot verify a Windows claim. GitHub Actions gains a `windows-latest` job for the CLI itself — not for the repos it serves, which stay macOS/Linux as their own tooling dictates. |
27
+
28
+ ## Ruling
29
+
30
+ Windows is a supported v1 platform for the `simt-dev` CLI itself. `package.json` declares no
31
+ `os` restriction, and CI verifies on `windows-latest`.
32
+
33
+ Scope is deliberately limited: this covers the CLI running *on* Windows. It does not imply that
34
+ any repo's own toolchain is Windows-ready — Python packaging, Monkey C, and Android Gradle
35
+ builds are governed by their own repos.
36
+
37
+ ## Consequences
38
+
39
+ - The shared hook dispatcher ships with a Windows-safe entrypoint from day one. Retrofitting
40
+ this after 16 repos have installed the dispatcher is far more expensive than building it now.
41
+ - The EOL normalisation in the projection injector becomes a correctness requirement rather
42
+ than a nicety — which is worth doing regardless, given 10 repos lack `.editorconfig`.
43
+ - `windows-latest` CI costs minutes per run and buys an install story that does not break on the
44
+ first non-Mac contributor.
45
+
46
+ ## Rejected
47
+
48
+ - **`os: ["darwin", "linux"]`.** Cheapest, and a defensible engineering call on estate
49
+ evidence — declined because the hook dispatcher's Windows-safety is substantially harder to
50
+ retrofit across 16 installed repos than to build.
51
+ - **Windows via WSL only.** Avoids the native path work but leaves a Windows contributor
52
+ needing a Linux environment, which is the failure this ruling exists to prevent.
@@ -0,0 +1,59 @@
1
+ # ADR-012 — Windows support is dropped; POSIX only
2
+
3
+ Status: **accepted** · 2026-10-05 · supersedes [ADR-011](ADR-011-windows-support.md)
4
+ Affects: the `simt-dev` CLI
5
+
6
+ ## Context
7
+
8
+ ADR-011 committed to supporting Windows in the CLI from v1, against the recommendation to
9
+ declare `"os": ["darwin", "linux"]` and let npm refuse the install elsewhere. The reasoning
10
+ for that decision was sound but the weighing was wrong.
11
+
12
+ ADR-011's own cost table listed five obligations, three of which are permanent tax on
13
+ every future change to the hook dispatcher and the projection injector rather than one-off
14
+ work:
15
+
16
+ 1. Hook entrypoints must be `.cmd`-aware instead of relying on a POSIX shebang.
17
+ 2. The projection injector must normalise line endings on every write.
18
+ 3. Glob matching and every emitted path reference must normalise separators.
19
+ 4. npm's generated bin shim must be relied on rather than a hand-written launcher.
20
+ 5. CI must carry a permanent `windows-latest` job.
21
+
22
+ Against that: **no Windows developer appears anywhere in the repos**, and no repo's own
23
+ toolchain is Windows-ready either — `simt-garmin` drives the Monkey C toolchain, and
24
+ `simt-app`/`wakt-app`/`app-ui` drive Gradle and Android tooling. So the CLI would have been the
25
+ *only* Windows-ready artefact in the estate, serving nobody.
26
+
27
+ The deciding argument is not the cost, it is the shape: obligations 1–3 are correctness
28
+ constraints that every contributor must keep satisfying forever. Adopting them to serve a
29
+ population of zero is how a codebase accumulates permanent complexity nobody can evaluate.
30
+
31
+ ## Ruling
32
+
33
+ **POSIX only: macOS and Linux.** `package.json` declares `"os": ["darwin", "linux"]`, so npm
34
+ refuses to install on Windows with a clear message instead of failing obscurely inside a git
35
+ hook. CI verifies on `macos-latest` and `ubuntu-latest` only.
36
+
37
+ The code obligations of ADR-011 are withdrawn, with one exception that stands on its own
38
+ merits rather than on Windows: **the projection injector still normalises line endings to the
39
+ canonical EOL.** 10 of 16 repos have no `.editorconfig`, so a generated region written with
40
+ whatever the host produced would cause whole-file diffs on checkouts whose
41
+ `core.autocrlf` differs. That is a cross-platform problem among POSIX hosts and has nothing
42
+ to do with Windows.
43
+
44
+ ## Consequences
45
+
46
+ - `bin/simt-dev.mjs` keeps its shebang as a plain POSIX entrypoint; no `.cmd` shim handling.
47
+ - Glob matching and emitted paths assume `/`. The existing forward-slash normalisation in
48
+ `fingerprint.mjs` is retained as cheap insurance, not as a Windows requirement.
49
+ - The permanent `windows-latest` CI job is not added.
50
+ - Revisit only when a Windows developer joins the estate — at which point the deciding
51
+ argument is gone and the cost table in ADR-011 becomes the deciding one.
52
+
53
+ ## Rejected
54
+
55
+ - **Keeping Windows v1 (ADR-011).** Permanent correctness constraints in the dispatcher and
56
+ injector, serving zero users, in the only part of the estate that would have been
57
+ Windows-ready.
58
+ - **Deferring rather than dropping.** A deferred platform is an unbounded promise; the
59
+ obligations would still shape the code.
@@ -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.