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.
Files changed (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. 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
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. 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
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,49 @@
1
+ # ADR-053 — The Node floor is 22.20.0
2
+
3
+ Status: **accepted** · 2026-10-10
4
+ **Affects:** `package.json`, `src/ci/workflow.mjs`, `src/resolve/resolve.mjs`.
5
+
6
+ ## Context
7
+
8
+ Node 20 reached end of life on 2026-04-30. `engines.node` still said `>=20`, both workflow
9
+ templates pinned setup-node to `20`, and `matchesGlob` was a hand-written translation because
10
+ `path.matchesGlob` is stable from Node 22.20.0 (and 24.8). ADR-042 recorded that translation
11
+ and said ruling 1 collapses into the built-in when the floor moves. Review 0002 S47 settles
12
+ the floor at `>=22.20.0` and says the own CI runs 22, 24 and 26.
13
+
14
+ ## Ruling
15
+
16
+ **One.** `engines.node` is `>=22.20.0`. Both workflow templates pin `actions/setup-node` to the
17
+ version that range names, read from `package.json`, so a template cannot name a version the
18
+ package says it does not run on. This repository's own gate also runs Node 24 and Node 26. A
19
+ floor that is only executed at the minimum is a floor in name.
20
+
21
+ **Two.** `matchesGlob` is `path.matchesGlob`. A trailing double star also matches the directory
22
+ itself: the built-in requires a child, so `migrations` misses `migrations/**`, and that empty
23
+ path is the one `graph lint` builds (ADR-042, ruling 3). A glob containing `?`, `{` or `[`
24
+ matches nothing, so ruling 4 still holds. Ruling 1 of ADR-042 is superseded by this record.
25
+ Rulings 2, 3 and 4 of ADR-042 stand.
26
+
27
+ **Three.** The runtime line is the same sentence in `package.json`, both templates, this
28
+ record, plan 0002's live Runtime line, and `README.md`. The README states the floor and
29
+ nothing else; the packaging README is S18 and does not get to pick a different floor.
30
+
31
+ ## Rejected
32
+
33
+ **Waiting for S18's README before raising the floor.** The floor is what CI and the matcher
34
+ run on. A missing README was not a reason to keep declaring a runtime that is end of life.
35
+
36
+ **Pinning both templates at Node 24 in this record.** S48 owns "templates use 24", the SHA
37
+ pins, and the macOS matrix. This record's adopting template uses the floor so it agrees with
38
+ `engines`. The own gate's extra versions are coverage, not a second floor.
39
+
40
+ **Letting `path.matchesGlob` implement `?` and braces.** It does, and that is how `src/a?.ts`
41
+ matches `src/aq.ts`. ADR-042 ruling 4 refused that reading because the registry does not use
42
+ it and a glob that matches the wrong paths is worse than a glob lint rejects.
43
+
44
+ ## Consequences
45
+
46
+ - `compileGlob` is gone. The cases ADR-042's tests lock (`**/*.ts`, a star that does not cross
47
+ `/`, a bare `**`) are now the built-in's, and the `?` case still fails lint.
48
+ - SHA-pinning `actions/checkout` and `actions/setup-node`, and the macOS runner, stay with S48.
49
+ - The adopting workflow's install step stays with S35.
@@ -0,0 +1,100 @@
1
+ # ADR-054 — The digest covers every byte that leaves the package, and a cause is read off the body
2
+
3
+ Status: **accepted** · 2026-10-10 · amends [ADR-032](ADR-032-provenance-is-a-content-digest-not-a-commit.md)
4
+ **Affects:** `src/projection/rules.mjs`, `src/upgrade/plan.mjs`.
5
+
6
+ ## Context
7
+
8
+ ADR-032 made the stamp a SHA-256 digest of `rules/` and `graph/`, and said why `src/` is
9
+ excluded: a refactor that changes no rule and no registry entry projects byte-identical
10
+ output, and stamping those consumers would report drift where there is none.
11
+
12
+ That reasoning is right and the boundary was drawn one level too low. ADR-032 excluded a
13
+ *directory*; the shared `.editorconfig` and `.gitattributes` blocks are not in a directory,
14
+ they are two string constants in `src/projection/baselines.mjs` — and every adopting
15
+ repository receives them. By ADR-015's own rule, a source is identified by its content rather
16
+ than by the file it happens to live in, so the body defined in `src/` is a source like any
17
+ other, and excluding `src/` as a path excluded content that leaves the package with it.
18
+
19
+ The finding arrived sideways, which is the only way this class gets found. Removing the
20
+ inert `[prettier]` block (S30) deleted seven lines from a shipped baseline — the first change
21
+ in this repository that moves bytes out of `src/` without moving the provenance digest.
22
+ Measured on a scratch repository with **only** that baseline body changed:
23
+
24
+ - `check` reported `DRIFT .editorconfig`, which is correct: the gate compares bytes, and the
25
+ bytes were wrong.
26
+ - `upgrade` reported the pin as `(unchanged)` and named the cause as
27
+ *"region restamped; the pin was already current (the rules changed)"*.
28
+
29
+ Both halves of that sentence are false. The pin was unchanged because the stamp could not see
30
+ the change, and the rules did not change — seven lines of an indentation setting did. The
31
+ first false statement made the second one reachable: with the stamp silent, the only cause
32
+ left to report was the one that was least true.
33
+
34
+ The severity is the estate's own: a report that names a cause which did not happen sends the
35
+ reader to diff `rules/` when the change is in `.editorconfig`. This is ADR-050's mistake one
36
+ layer up — a field read for the thing it was not — and ADR-036's, where an instruction that
37
+ is false in every repository is worse than one that is missing.
38
+
39
+ ## Ruling
40
+
41
+ **One. The digest covers every byte string that leaves the package: `rules/` and `graph/` on
42
+ disk, plus the rendered baseline bodies.** The baselines enter keyed by the file each becomes
43
+ in the adopting repository (`baselines/.editorconfig`), not by the module that defines it,
44
+ because the stamp answers what content produced those bytes and `.editorconfig` is the name
45
+ a reader of a generated region recognises. `src/` remains excluded *as a directory*; the
46
+ ruling narrows ADR-032's exclusion from a path to a rule about content. Ruling 1 of ADR-032
47
+ and its "what the digest covers" section are superseded; ADR-032's framing, its framing
48
+ discipline and its other two consequences stand.
49
+
50
+ **Two. `provenanceInputs()` is exported, and `provenanceDigest()` hashes what it returns.**
51
+ "The digest covers the right things" is a claim about a set, and a set assembled only inside
52
+ the function that hashes it cannot be asserted from outside — the test would have to mutate
53
+ the package to get an answer. Reading the list and hashing it in one place is ADR-042's
54
+ ruling 3 applied to the stamp: the rendering is shared, so narrowing it narrows the test with
55
+ it, and the test passed with the rendering sabotaged is not evidence.
56
+
57
+ **Three. `upgrade`'s cause is read off the projected body, and the two kinds of content are
58
+ counted apart.** A baseline region is not a rule: `.editorconfig` and `.gitattributes` are
59
+ settings the repository owns. `rulesChanged` and `baselineChanged` are computed separately
60
+ and the sentence names one, both, or neither. The comparison is body to body rather than
61
+ byte to byte, because the region header carries the stamp and folding it in would make every
62
+ pin bump claim a change of policy — which is ADR-050's ruling, and it is why a target whose
63
+ seam moved reads as `updated` with no cause.
64
+
65
+ **Four. "Neither" is a real answer and is not collapsed into the nearest cause.** A seam
66
+ repair, a stamp-only move and a generated-field move are three different problems that one
67
+ command fixes, and the sentence distinguishing them is the whole reason the command exists.
68
+
69
+ ## Consequences
70
+
71
+ - **Every adopted repo's stamp moves once more.** A repo pinned at a digest computed without
72
+ the baselines reports `behind` the first time it runs this version and `simt-dev upgrade`
73
+ fixes it. That is a pin doing its job, not a regression — the same reading as ADR-032's own
74
+ first consequence.
75
+ - **`upgrade` can no longer report a cause it cannot see.** The two defects were independent
76
+ and are now separately guarded: the digest test asserts the baselines are in the hashed set
77
+ and that changing one changes the value; the planner test asserts a baseline body move
78
+ produces "a baseline changed" and not "the rules changed".
79
+ - **The planner needed a `kind` it did not have.** Baseline targets already carried
80
+ `kind: 'baseline'`; rule and primary targets carry none. That is the discriminator, and it
81
+ is read from the targets `plan` produced rather than recomputed from the file list, so the
82
+ two cannot disagree about which file is a baseline.
83
+ - **A `src/` refactor still moves nobody.** The exclusion was narrowed from a path to a rule
84
+ about content, and the test asserts no `src/` entry appears in the hashed set — so the
85
+ narrowing cannot be re-widened silently by a future import.
86
+
87
+ ## Rejected
88
+
89
+ - **Hash `src/` as a directory.** The smallest diff and the wrong one: it puts every
90
+ refactor, comment change and test edit into every consumer's stamp, which is precisely the
91
+ false report ADR-032 ruled out.
92
+ - **Leave the stamp alone and fix only the wording.** The wording was the visible half. With
93
+ the pin reporting `(unchanged)` beside a restamped region, `upgrade` would still be
94
+ describing a move that did not happen — and the reader has no way to tell which line of
95
+ the output to disbelieve.
96
+ - **Give baselines their own stamp field.** A second identity per repository, and the reader
97
+ would have to know which of two values to compare against. One stamp, covering everything
98
+ it stamps.
99
+ - **Call the baseline a "rule" in the message.** Cheaper than a discriminator, and it sends
100
+ the reader to `rules/` — which is the defect.
@@ -0,0 +1,98 @@
1
+ # ADR-055 — A status line travels, so it may not name a location; prose is the record's own argument
2
+
3
+ Status: **accepted** · 2026-10-10 · filed as S54 · affects `src/decisions/index.mjs`
4
+
5
+ ## Context
6
+
7
+ `rules why` prints a decision record's `Status:` line into whatever repository the reader
8
+ is standing in, and most of them are repositories that adopted simt-dev and therefore own
9
+ none of this package's files. The `summarise` contract said so itself, in the module that
10
+ produces the output: *"Never contains a path, because a record read inside a consumer repo
11
+ cannot name one."*
12
+
13
+ The `ruled` half honoured that. The `status` half did not, because it is the record's own
14
+ prose: five records wrote `· affects src/resolve/resolve.mjs` into a field the tool parses,
15
+ copies and prints verbatim on every `rules why`, `rules show` and MCP call. The review
16
+ measured this as "two records out of forty-eight, pre-existing". The corpus says **five**,
17
+ and **three of them were written after that measurement** — ADR-053 and ADR-054 in this
18
+ cycle, and ADR-041 days earlier. A finding that grows while it is being fixed is a finding
19
+ whose real cause is a convention rather than a typo, and the convention is what needed
20
+ ruling.
21
+
22
+ The convention was ADR-038's: *"That is what the `Affects:` line on a record is for."* Five
23
+ records had an `Affects:` field available and put the clause in `Status:` instead — because
24
+ `Status:` is the line the tool reads, so putting it there feels like the place that counts.
25
+ It is the opposite: it is the one field guaranteed to travel.
26
+
27
+ ## Ruling
28
+
29
+ **One. A `Status:` line may not name a location.** It is this tool's own metadata — accepted,
30
+ the date, what superseded what, what it affects — and every clause in it is a statement about
31
+ *this package* by construction. There is no benign reading to confuse it with, so the
32
+ predicate needs no judgement: a location resolving at this package's root is redacted to a
33
+ phrase naming the thing rather than the path. The redaction happens once, in `loadDecisions`,
34
+ so the text command, the JSON payload and the MCP surface cannot disagree about it — which is
35
+ the same "one place decides" shape ADR-035 settled for writers.
36
+
37
+ **Two. `Affects:` is where a location belongs, and it stays unprinted.** The loader already
38
+ stops the status paragraph at a sibling field, so moving the clause is a reformat rather
39
+ than a change of ruling, and the five records are corrected in place for the clause only.
40
+
41
+ **Three. The exempt list is what `init` writes, and it is executed.** Dot-entries and three
42
+ named files (`AGENTS.md`, `package.json`, `simt-dev.json`) are excluded from redaction,
43
+ because a repository that adopted simt-dev has them. That is a claim about a set held in a
44
+ comment, and a claim about a set held in a comment is a value nobody runs — so a test adopts
45
+ a repository and asserts that every top-level file the tool writes survives the predicate
46
+ unchanged. A name that drifts on stops being redacted; a name that drifts off starts
47
+ redacting a file the reader does have.
48
+
49
+ **Four. Prose is the record's argument and is left as written.** The `ruled` and `rejected`
50
+ excerpts are a human argument, and they may legitimately quote a location — `docs/plans/`
51
+ means *this package's* plan in one record and *the reader's own plans* in another, and no
52
+ amount of reading the string separates them. Redaction there would have to guess, and a
53
+ guessed redaction makes a correct sentence wrong, which is the worse defect. `redactEstate`
54
+ redacts prose and is not a counter-example: an estate repository name is never the reader's
55
+ business either, whereas a path inside an argument sometimes is.
56
+
57
+ **Five. The cost of ruling four is a number, not a shrug.** Seven records still print a
58
+ location in their prose — six older ones, and this record, which quotes the collision it
59
+ turns on — and a test asserts the count and the ids. A record that stops quoting one moves
60
+ it; a record that starts moves it too. The alternative — leaving it unmeasured — is how "two
61
+ of forty-eight, pre-existing" came to describe ten.
62
+
63
+ **Six. The `summarise` docstring's overclaim is withdrawn.** It asserted a property of prose
64
+ that the function never had and could not have had. An unverifiable promise in a comment is
65
+ the defect ADR-049 is about, one level down: a sentence that reads like evidence and carries
66
+ none. The comment now states the scope it can keep.
67
+
68
+ ## Consequences
69
+
70
+ - **Five records changed, and only their `Status:` line.** No ruling, no context and no
71
+ rejected alternative was touched. ADR-049's supersession machinery is unaffected:
72
+ `partialSupersessions` still reads the raw paragraph, because a ruling number is not a
73
+ location and redacting it would have been the wrong trade.
74
+ - **`rules why ADR-042` no longer prints `src/resolve/resolve.mjs`.** It prints
75
+ `accepted · 2026-10-08 · ruling 1 superseded by ADR-053`, which is the whole of what the
76
+ line is for.
77
+ - **A sixth record with a path in `Status:` is scrubbed rather than blocked.** The records
78
+ are corrected so they stay readable to a maintainer on this repository, where the path is
79
+ the useful part; the loader is hardened so a future one is harmless. The corpus test
80
+ asserts the *unscrubbed* status carries none, which is the stronger statement.
81
+ - **Prose leaks are visible rather than absent.** Seven records — six older and ADR-055
82
+ itself, which quotes the collision it turns on. Anyone who decides that is still too many
83
+ now has the count and the argument against redaction.
84
+
85
+ ## Rejected
86
+
87
+ - **Redact the prose too.** The `docs/plans/` collision is not hypothetical — ADR-014,
88
+ ADR-016, ADR-025 and ADR-030 all use that exact string for both meanings, and `test/leaks.mjs`
89
+ already singles it out as a substitution *default* that names where a repo keeps its own
90
+ plans. Redacting it would corrupt four records to protect six.
91
+ - **Rewrite the ten records to drop the paths.** This repository's rule is that a record is
92
+ never edited in place, and the argument these records make is better with the file named.
93
+ The clause moves to the field designed for it; the sentence does not change.
94
+ - **Stop printing the status line.** It carries the supersession, which is the one thing a
95
+ reader needs in order not to act on a reversed ruling.
96
+ - **Put the redaction in `commands/rules.mjs`.** It would then apply to the text output and
97
+ not to `--json` or MCP, and the three surfaces would disagree about what a record says —
98
+ the copy ADR-052 exists to prevent.
@@ -0,0 +1,160 @@
1
+ # ADR-056 — The command, the package and the project are three names, and only one of them is cheap to move
2
+
3
+ Status: **accepted** · 2026-10-10 · filed as S18 · amends [ADR-023](ADR-023-the-gate-ships-as-a-file-the-repo-owns.md) · affects `src/name.mjs`, `package.json`, `src/projection/manifest.mjs`, `src/projection/markers.mjs`
4
+
5
+ ## Context
6
+
7
+ This tool had four names and no record of which was which. `PACKAGE_NAME` was spelled in
8
+ `locate.mjs`, the manifest name in `manifest.mjs`, the config directory in `workspace.mjs`
9
+ and the gate filename in `workflow.mjs` — four definitions, four places to forget. The
10
+ command itself appeared **276 times across 33 files**, in remedy strings, help columns, shim
11
+ source, MCP tool descriptions and the note every generated region carries.
12
+
13
+ Publishing forced the question S18 had been asking since it was filed: *what is this
14
+ called?* And the answer turned out to be four different answers depending on what you were
15
+ asking about, which is not an answer.
16
+
17
+ The measurements that settled it:
18
+
19
+ - `simt-dev.json` is on disk in **fourteen repositories** that adopted the tool.
20
+ - `<!-- simt-dev:begin -->` is in every projected `AGENTS.md` in every one of them.
21
+ - `.simt-hooks/` and `SIMT_DEV_BIN` are in the `.git` directories of those repositories.
22
+ - `simt-dev-check.yml` is in the `.github/workflows` of each of them.
23
+ - The command name appears in **no file at all** until a reader types it.
24
+
25
+ ## Ruling
26
+
27
+ **One. There are three names, and they are recorded as three.** `src/name.mjs` owns all of
28
+ them and nothing else spells them:
29
+
30
+ - `PROJECT` — the repository, and the artefacts already on somebody else's disk.
31
+ - `CLI` — the command a reader types.
32
+ - `PACKAGE_NAME` — what you install to get the command.
33
+
34
+ They are three constants that happen to be related. Deriving one from another was
35
+ considered and rejected: a derivation that picks between a package and a scope turns a
36
+ one-line rename into a one-line diff whose meaning depends on a comparison nobody reading
37
+ it would think to check.
38
+
39
+ **Two. The command is `simt`; the project and the package keep `simt-dev`.** This is the
40
+ whole ruling, and it rests on the asymmetry in the measurement above: the command appears
41
+ only in text the tool itself writes, and that text migrates on the next write, while the
42
+ other two names sit in files the tool cannot rewrite in place. Spending the rename on the
43
+ cheap name is what made this a rename instead of a fourteen-repository roll-forward.
44
+
45
+ **Three. The manifest follows the command, and its move is a read-both/write-new.** The
46
+ manifest was one of the on-disk names and it is the one that moved: `simt-dev.json` is now
47
+ `simt.json`. Without a fallback, all fourteen adopting repositories would read as "no
48
+ manifest" on their next `check` — and "no manifest" is the answer that offers to re-adopt a
49
+ repository that is already done. So:
50
+
51
+ - **Read** takes `simt.json` if it exists, else `simt-dev.json`. Every read site goes
52
+ through one resolver (`manifestPath`) rather than knowing the fallback itself, because a
53
+ fallback that lives in five places is a fallback that eventually stops living in one of
54
+ them. The fingerprint, which reads the directory listing rather than a path, accepts both
55
+ spellings for the same reason: a fingerprint that misses the manifest silently loses the
56
+ repository's declared stack, which is the one input no amount of marker reading replaces.
57
+ - **Write** goes to `simt.json`, and a lone `simt-dev.json` is moved, not shadowed: two
58
+ manifests in one repository is a state nobody can reason about, and the second would be
59
+ the one that is never read.
60
+ - **The window closes per repository** on its first `init` or manifest write after the
61
+ upgrade. Until then, the old name keeps working everywhere.
62
+
63
+ The fallback is asserted in `test/naming.test.mjs` with both spellings — the new name wins
64
+ when both exist, the old name is still read, and a write moves the old file — because its
65
+ failure mode is silent: the tool would simply stop seeing a repository it has been
66
+ managing.
67
+
68
+ **Four. The region namespace follows the command, and its move is read-both/write-new
69
+ too.** The second on-disk name to move: the region was stamped `simt-dev:begin` /
70
+ `simt-dev:end`, and so was the provenance header inside it (`simt-dev:generated`,
71
+ `simt-dev:sha=`, `simt-dev:sources=`); all of those lines are now `simt:`. A marker cannot
72
+ be renamed the way a manifest can, because a region cannot be moved — it lives inside a
73
+ file the reader owns. So the fallback lives in the marker grammar itself, one read regex
74
+ per dialect:
75
+
76
+ - **Read** accepts either spelling in both dialects (HTML comment and `#` comment),
77
+ because a marker that does not match is simply not a region, and a caller that does not
78
+ accept the old spelling would silently report an adopted repository as unadopted.
79
+ - **Write** emits only the `simt:` spelling, so the first write to each region migrates it
80
+ in place: the body comparison in `inject` finds the header prefix drifted and rewrites
81
+ the region where it stands. The window closes per repository on its first `sync`.
82
+ - **A file holding both spellings is malformed, not merged.** Only a hand edit can produce
83
+ it — the tool writes one region per file and finds it by either spelling. Guessing which
84
+ region wins would silently delete the other, so `parse` reports it and a human deletes
85
+ the old one.
86
+
87
+ The provenance digest covers `rules/`, `graph/` and `baselines/` — not `src/`, where the
88
+ marker strings live (ADR-032, S53) — so the stamp *value* does not move with this rename;
89
+ only the key's prefix does. No repository needs re-pinning, and `upgrade` sees no version
90
+ change: the migration is marker bytes only.
91
+
92
+ The fallback is asserted in `test/markers.test.mjs` — a legacy region is read and its
93
+ stamp recovered, the first write migrates it in place, the migrated region is stable, and
94
+ a mixed file is reported malformed — because its failure mode is the manifest's: the tool
95
+ would simply stop seeing a repository it has been managing.
96
+
97
+ **Five. The remaining on-disk names are locked, and the lock is asserted.**
98
+ `test/naming.test.mjs` pins `simt-dev-check.yml`, `.simt-hooks`, `SIMT_DEV_BIN` and the
99
+ project name as literals, so renaming one is a deliberate edit to a test rather than a
100
+ constant change that looks harmless. A lock nobody can trip is a comment.
101
+
102
+ **Six. The command name lives in one constant and is executed, not agreed.**
103
+ `test/naming.test.mjs` walks `src/` and fails on any command string that *spells* the
104
+ command instead of interpolating it. The pattern matches the real verb list rather than
105
+ "space then a lowercase word", because the looser rule flags the tool naming itself in
106
+ prose — `simt-dev will not remove a file it wrote` and `the simt-dev region is not at the
107
+ top of the file` are the project talking about itself, and the project keeps its name.
108
+ The guard was proven with a negative control: one injected spelled command, one failure
109
+ naming the file and line, reverted.
110
+
111
+ **Seven. The package is unscoped.** `@simt` and `@quazarmedia` were both free on npm; an
112
+ unscoped `simt-dev` was already reserved by this project's own account. An unscoped name
113
+ is the one a reader can guess, and the reserved one was the only spelling already pointing
114
+ at something.
115
+
116
+ ## Consequences
117
+
118
+ - **No repository has to do anything by hand, and the migration announces itself.** Every
119
+ adopted repo keeps its hooks directory and gate. The manifest is read under either name
120
+ and moves to `simt.json` on its first write; the region is read under either namespace
121
+ and migrates to `simt:` on its first `sync`. Until each repository has written once,
122
+ `check` reports the region as drifted — which is the migration making itself visible
123
+ rather than hiding, and it disappears on the `sync` that performs it. The migration is
124
+ real, but it runs itself — one repository per write — instead of running as a
125
+ fourteen-repository roll-forward.
126
+ - **The generated gate file names the command correctly.** `name: simt check`, "If `simt`
127
+ cannot be found the job FAILS", "run `simt ci --yes`". These are shipped bytes into
128
+ every adopting repository, which is why they were checked by rendering them, not by
129
+ reading the source.
130
+ - **One upgrade step is real and is documented.** A shim written before this change looks
131
+ for `simt-dev` on `PATH`. A machine that installed the package and never re-ran
132
+ `hooks install` finds nothing until the dependency is refreshed and `simt hooks install
133
+ --yes` re-runs. That is in the CHANGELOG rather than left to be discovered.
134
+ - **The remaining on-disk names can still move, but only deliberately.** The gate file,
135
+ the hooks directory and the shim's environment variable would each need the shape the
136
+ manifest and the markers got — read-both, write-new, an asserted fallback — and S36's
137
+ estate roll-out to execute it.
138
+
139
+ ## Rejected
140
+
141
+ - **Rename the artefacts too — `masar.json`, `masar-hooks/`, `<!-- masar:begin -->`.**
142
+ This was the shape the slice was originally taken in and it is strictly worse: it turns
143
+ a rename into a fourteen-repository migration, every one of which would see its
144
+ `AGENTS.md` stop being managed on the first `check` after the upgrade. It was rejected
145
+ for what it would have cost, not for being unclear. The manifest and the region markers
146
+ are exceptions to this rejection, and they are exceptions only because rulings three
147
+ and four make their moves safe: a marker that does not match is simply not a region, so
148
+ the fallback lives in the marker grammar itself — one read regex per dialect that
149
+ accepts both spellings — and a file holding two regions is reported malformed rather
150
+ than merged. The hooks directory and the gate remain fully rejected: they have no
151
+ second spelling a reader's file could fall back to.
152
+ - **`@quazarmedia/simt-dev` as the package name.** Free, but already named in the shim
153
+ remedy, the review and the workflow install step. Changing it would have edited all
154
+ three for no availability gain, since the unscoped name was already reserved.
155
+ - **Rename the command by a script that rewrites every literal.** The first attempt at the
156
+ propagation used "space then a lowercase word" and rewrote `user.name=simt-dev tests`
157
+ and a test title — both of which would have passed. A rename that cannot tell a command
158
+ from a git identity in a fixture is a rename that will eventually rewrite a sentence.
159
+ - **Derive `PACKAGE_NAME` from `SCOPE` and `PROJECT`.** Rejected above; the three names
160
+ will not always agree, and the day they disagree the derivation picks for you.
@@ -0,0 +1,115 @@
1
+ # An identity is per-developer, and a repository does not own it
2
+
3
+ Status: **accepted** · 2026-10-11
4
+
5
+ Affects: `simt init`, `src/identity.mjs`, `test/identity.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Two repositories in the estate carried a `git.config` file in their tracked files.
10
+ It holds a name and an email address:
11
+
12
+ ```ini
13
+ [user]
14
+ name = <one developer's name>
15
+ email = <that developer's email address>
16
+ ```
17
+
18
+ Both had a local `include.path` pointing at it, and both worked. Both were also
19
+ wrong in ways that had nothing to do with each other.
20
+
21
+ **It was a tracked file.** A repository has more than one person in it. Committed,
22
+ one developer's name and email is published to every other developer and to
23
+ everybody who can read the repository. This repository's own release review had
24
+ already said so, in the table of findings, as: *"`git.config` tracked at the root
25
+ with a personal name and email — Identity file in a public tree, read by nothing
26
+ — Remove it from the tree."* The finding was recorded and the file stayed.
27
+
28
+ **It pointed somewhere else.** Both `include.path` values named
29
+ `../../simt-dev/git.config` — another repository's identity file, resolved through
30
+ the git directory rather than the working tree. Including the git config's own
31
+ file from a relative path three directories up means one repository's commits
32
+ depend on a sibling checkout existing at an expected path, which fails the moment
33
+ anybody clones without it. It went unnoticed because every developer here has a
34
+ global identity configured, so the include resolved to the same person the
35
+ include would have skipped to, and the file it never read was the reason nothing
36
+ broke.
37
+
38
+ The interesting part is that these are opposite mistakes with the same root.
39
+ `git.config` is not the repository's to own, and treating it as the repository's
40
+ produced both: a file that should have been ignored got committed, and an
41
+ `include` that should have named the repository's own file named a stranger's.
42
+
43
+ ## Ruling
44
+
45
+ 1. **`simt init` writes `git.config` when the repository has none.** It is planned
46
+ like everything else `init` writes, so it appears in the same preview that lists
47
+ the rest, and its source is named on the line — writing somebody's name is a
48
+ statement about a person and the plan says where the name came from.
49
+
50
+ 2. **An existing `git.config` is left exactly as it is.** Not updated, not merged,
51
+ not reformatted. It may have been written by a person, by an older version of
52
+ this tool, or by hand years ago, and none of those are this tool's to
53
+ second-guess.
54
+
55
+ 3. **The identity is derived, in this order, and the first answer wins:**
56
+ the repository's **last commit author**, then the identity **git would use on
57
+ this machine anyway**, then nothing.
58
+
59
+ Last commit first because it is a fact rather than a default: it is who has
60
+ actually been working here. It is read from `git log`, not from
61
+ `git config user.email`, because at that moment the configured value is what the
62
+ file is about to be derived from, and the step would be circular.
63
+
64
+ The second is `--get` rather than `--global`, so it resolves through git's own
65
+ precedence and returns what a commit would have used had this file never
66
+ existed. That is the fallback's whole point: it must not be a second opinion
67
+ about what identity means.
68
+
69
+ 4. **When nothing is available, no file is written.** A placeholder is an identity
70
+ git would use on the next commit, and an adopting command is the last thing that
71
+ should invent one. The refusal is printed with its reason rather than swallowed.
72
+
73
+ 5. **Only the keys that were found are written.** A partial identity is written as a
74
+ partial identity, for the same reason.
75
+
76
+ 6. **`init` adds `git.config` to `.gitignore`** when it creates one, and points
77
+ `include.path` at `../git.config` — the repository's own file.
78
+
79
+ `--replace-all`, not `--add`: a value naming *another* repository's identity file
80
+ is not an addition to keep, it is a link to break.
81
+
82
+ A repository that already *tracks* `git.config` is not untracked by `init`.
83
+ Removing a tracked file is not something an adopting command should do silently,
84
+ and doing it would be a change to that repository's history rather than to its
85
+ working tree.
86
+
87
+ ## Consequences
88
+
89
+ `src/identity.mjs` is new and is the single answer to "whose identity is this, and
90
+ may we write it". `init` plans it, writes it, gitignores it and repoints the
91
+ include; the four refusals are asserted in `test/identity.test.mjs` rather than
92
+ described, because each of them is the correct answer rather than a failure to try
93
+ harder.
94
+
95
+ One test scrubs `HOME` and `GIT_CONFIG_GLOBAL` before asserting the "nothing
96
+ anywhere" case. That is not tidiness: a developer with a global `user.email` — which
97
+ is most of them — makes the identity resolvable, so without scrubbing the test
98
+ would pass for the wrong reason on that machine and nowhere else. ADR-031 is about
99
+ a result depending on the author's environment; this test would have been the next
100
+ instance of it.
101
+
102
+ ## Rejected
103
+
104
+ - **Write `git.config` into the tracked tree, as a shared record of who works
105
+ here.** This is what the two repositories did, and it is a publication of a
106
+ person's email address with a commit attached.
107
+ - **Take the identity from the OS (the login name, the home directory).** It
108
+ produces `<login-name>@example.com` and similar: an identity nobody configured,
109
+ used for authorship. Ruling 4's refusal covers it.
110
+ - **Overwrite an existing `git.config` when it differs from the derived one.**
111
+ Difference is not error. A file that has been there for years and disagrees with
112
+ the last commit author is a decision somebody made.
113
+ - **Fix the `include` only when it is wrong.** The include is only written when it
114
+ is missing or does not name this repository's own file. Rewriting it when it is
115
+ already correct would be a no-op that reads as a change.