simt-dev 0.3.0 → 0.4.0

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 (91) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +138 -74
  3. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
  4. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +1 -1
  5. package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
  6. package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
  7. package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
  8. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
  9. package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
  10. package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
  11. package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
  12. package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
  13. package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
  14. package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
  15. package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
  16. package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
  17. package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
  18. package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
  19. package/package.json +2 -2
  20. package/rules/core.md +2 -2
  21. package/src/agenthooks/policy.mjs +10 -2
  22. package/src/agenthooks/prompt.mjs +81 -0
  23. package/src/agenthooks/render.mjs +138 -17
  24. package/src/argv.mjs +34 -1
  25. package/src/bench/grade.mjs +3 -0
  26. package/src/bench/run.mjs +85 -39
  27. package/src/bench/variants.mjs +456 -0
  28. package/src/brief/assemble.mjs +346 -0
  29. package/src/brief/kind.mjs +134 -0
  30. package/src/brief/packet.mjs +197 -0
  31. package/src/ci/workflow.mjs +11 -1
  32. package/src/commands/agenthook.mjs +84 -5
  33. package/src/commands/bench.mjs +53 -16
  34. package/src/commands/brief.mjs +78 -0
  35. package/src/commands/compare.mjs +10 -2
  36. package/src/commands/decide.mjs +116 -0
  37. package/src/commands/evidence.mjs +96 -0
  38. package/src/commands/gate.mjs +165 -0
  39. package/src/commands/help.mjs +65 -3
  40. package/src/commands/hooks.mjs +45 -5
  41. package/src/commands/impact.mjs +205 -0
  42. package/src/commands/index.mjs +52 -1
  43. package/src/commands/lessons.mjs +127 -0
  44. package/src/commands/local.mjs +97 -0
  45. package/src/commands/map.mjs +126 -0
  46. package/src/commands/repoindex.mjs +55 -0
  47. package/src/commands/rules.mjs +17 -2
  48. package/src/commands/shared.mjs +398 -0
  49. package/src/commands/skills.mjs +3 -17
  50. package/src/commands/skillscompile.mjs +255 -0
  51. package/src/commands/sync.mjs +87 -3
  52. package/src/commands/tests.mjs +145 -0
  53. package/src/commands/tokens.mjs +30 -1
  54. package/src/commands/why.mjs +172 -0
  55. package/src/compare/learn.mjs +680 -0
  56. package/src/decisions/index.mjs +116 -65
  57. package/src/federation/client.mjs +188 -0
  58. package/src/federation/config.mjs +134 -0
  59. package/src/federation/query.mjs +48 -0
  60. package/src/graph/compile.mjs +475 -0
  61. package/src/graph/spec.mjs +1 -1
  62. package/src/hooks/dispatcher.mjs +15 -0
  63. package/src/hooks/shim.mjs +31 -0
  64. package/src/index/compact.mjs +228 -0
  65. package/src/index/extract.mjs +331 -0
  66. package/src/index/gate.mjs +89 -0
  67. package/src/index/govern.mjs +234 -0
  68. package/src/index/impact.mjs +186 -0
  69. package/src/index/map.mjs +627 -0
  70. package/src/index/persist.mjs +239 -0
  71. package/src/index/store.mjs +515 -0
  72. package/src/index/tests.mjs +294 -0
  73. package/src/kernel/budget.mjs +100 -0
  74. package/src/kernel/checks.mjs +378 -0
  75. package/src/kernel/variants.mjs +353 -0
  76. package/src/ledger/index.mjs +147 -0
  77. package/src/local/client.mjs +482 -0
  78. package/src/local/config.mjs +234 -0
  79. package/src/mcp/tools.mjs +138 -1
  80. package/src/measure/context.mjs +7 -5
  81. package/src/memory/decide.mjs +462 -0
  82. package/src/memory/lessons.mjs +858 -0
  83. package/src/projection/disclosure.mjs +92 -0
  84. package/src/projection/lock.mjs +125 -0
  85. package/src/projection/manifest.mjs +52 -0
  86. package/src/projection/markers.mjs +56 -17
  87. package/src/projection/project.mjs +24 -17
  88. package/src/projection/rules.mjs +407 -49
  89. package/src/projection/shared.mjs +428 -0
  90. package/src/projection/targets.mjs +2 -1
  91. package/src/toolwords.mjs +8 -0
@@ -0,0 +1,100 @@
1
+ # ADR-084 — A workspace carries its repeated rules once, and the region does not read the reader's home
2
+
3
+ Status: **accepted** · 2026-10-17 · filed as R11 (plan 0003)
4
+
5
+ **Affects:** `src/projection/shared.mjs`, `src/commands/shared.mjs`, `src/projection/rules.mjs`,
6
+ `src/projection/manifest.mjs`, `src/projection/markers.mjs`, `src/commands/tokens.mjs`,
7
+ `src/commands/index.mjs`
8
+
9
+ ## Context
10
+
11
+ The same rules are projected into every repository of a workspace, and each repository's
12
+ region carries the full text. The plan asked for the intersection to be installed once, at
13
+ user level, and for a repository to switch to the remainder when its manifest said
14
+ `projection.shared`. It also named home paths for four harnesses, and said CI should keep
15
+ rendering the full region so a teammate without the user file loses nothing in review.
16
+
17
+ Two of those would not survive contact with the rest of the tool. `projection` is generated:
18
+ `upgrade` replaces it, which is why a repository's own budget already lives beside it rather
19
+ than inside it. And a region that is full in CI and short on a reader's machine is a region
20
+ `check` cannot agree with, because `check` renders the same function the reader gets.
21
+
22
+ ## Rulings
23
+
24
+ 1. **The unit that is shared is a rule, and only a rule every repository renders with the
25
+ same bytes.** The meter can report a repeated paragraph; it cannot store one, and
26
+ `simt rules show` prints one rule. A rule one repository does not ship, or renders
27
+ differently, stays in every region. The comparison is the rendered text, because one
28
+ repository can drop a line the others keep.
29
+
30
+ 2. **The declaration is `shared` on the manifest, beside `budget` and `mode`.** `level` is
31
+ `user` and `rules` is the list of ids. It is not a key of `projection`, because that
32
+ object is replaced on upgrade. A missing key, a level other than `user`, or an empty
33
+ list means the region carries everything it carried before. A value that is not that
34
+ shape is an error from the one reader the region, `doctor` and `simt shared` share.
35
+
36
+ 3. **The file that is written is the one the loader table says the harness reads.** The
37
+ plan named paths. A path the table does not record would be written and never loaded, so
38
+ a surface with no user-level row, and a pointer surface with no loader row, gets a block
39
+ to paste and no path. Where the table names a flat user rules directory, the file is
40
+ `simt.md`. Where a harness reads one of two files and the reader already has a non-empty
41
+ one, that file is the one written. A cap the table states in tokens is reported, not
42
+ compared with a byte count, and a copy over its cap is reported rather than refused.
43
+
44
+ 4. **`simt shared plan` writes nothing. `simt shared install` writes only with `--yes`.**
45
+ The write is the user-level file, keeping whatever prose sits outside the generated
46
+ region, and the same declaration in every repository that already has a manifest. A
47
+ repository without one is skipped, with the command that adopts it. A workspace with
48
+ nothing to share writes neither an empty home file nor a declaration. The regions
49
+ change on the next `simt sync`, not during the install.
50
+
51
+ 5. **The region is a function of the declaration, not of the reader's home directory.** CI
52
+ and the reader therefore render the same delta. A teammate without the user file loses
53
+ the shared rules from the region; `simt doctor` says so — current, stale, absent,
54
+ unreadable, or no target — and does not fail the gate. The index line names the rule,
55
+ why it is not in the file, and `simt rules show <id>`. It does not name a home path,
56
+ because that path is not true on both machines.
57
+
58
+ 6. **One section holds every reason a rule is missing, and its heading names the fact.**
59
+ Class-deferred, cut to a budget, and carried once are three decisions and one fact for
60
+ the reader: the rule is not in this file. The heading is `## Rules not in this region`.
61
+ This narrows ADR-073 ruling 6 and ADR-083 ruling 7, which named that section
62
+ `## Deferred rules` when a class deferral was the only reason to be in it. The rest of
63
+ both rulings stands: one section, each line says which reason, and the remedy is the
64
+ same command.
65
+
66
+ 7. **A repository's own profile or pack is not shareable.** It is policy that repository
67
+ owns. Putting it in a home file would change one repository by editing a file every
68
+ sibling reads. Only rules this package ships are candidates, and the shareable set is
69
+ computed before the declaration drops them, so a second install does not shrink the
70
+ intersection against an already-reduced list.
71
+
72
+ 8. **`simt tokens` counts the user-level copy once.** `tokens --repos` adds the whole file
73
+ to the estate total, prose outside the region included, because that prose is also
74
+ always-on for the reader. A repository on its own counts the same file, so a single
75
+ root does not look cheaper than the workspace it belongs to.
76
+
77
+ ## Consequences
78
+
79
+ `simt shared plan` is the default of `simt shared`, for the same reason `simt skills` lists
80
+ before it installs: the read answers what would happen, and the write needs `--yes`.
81
+
82
+ The plan's accept bar — always-on bytes for the seven-root estate at most 30 KB, and no R
83
+ loss in bench — is **unmeasured**. The tests prove the mechanism on fixtures: identical
84
+ rules leave the region, a divergent rule stays, the reader's own prose survives, and the
85
+ meter counts the copy once. They do not measure the estate, and they do not run the bench.
86
+
87
+ ## Rejected
88
+
89
+ - **`projection.shared`.** The next upgrade would delete it, and the region would silently
90
+ grow back.
91
+ - **CI renders the full region while a reader renders the delta.** `check` would report
92
+ drift on every machine that had installed the copy, or would have to read a home
93
+ directory the projection is forbidden to depend on.
94
+ - **The union of rules across repositories.** A rule one repository does not ship is that
95
+ repository's absence, and installing it at user level would give the rule to a reader of
96
+ a repository that declined it.
97
+ - **Writing the paths the plan named when the loader table records a different file, or
98
+ none.** A file the harness does not read is a second copy the reader never sees.
99
+ - **An empty shared file for a workspace with nothing to share.** It would look like a
100
+ decision and contain none.
@@ -0,0 +1,72 @@
1
+ # ADR-085 — The region ends with the names of what it kept out
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R12 (plan 0003)
4
+
5
+ **Affects:** `src/projection/disclosure.mjs`, `src/projection/rules.mjs`, `src/graph/spec.mjs`,
6
+ `test/disclosure.test.mjs`, `test/scoped.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A profile that declares a scope already leaves the always-on region when every selected
11
+ surface can load it narrowly, and the region keeps a pointer (ADR-071). A package in a
12
+ monorepo already gets its own region (ADR-066). What a reader still cannot see is an
13
+ installed skill: the body lives in its own file, and a surface with no skill channel never
14
+ mentions the name, so the reader has no reason to open it.
15
+
16
+ The plan asked for one index at the end of the kernel, one line per scoped rule and per
17
+ skill, carrying the name and the trigger. The accept is that the scoped body is absent
18
+ from what loads at session start and present in the file a matching path loads, checked
19
+ per surface.
20
+
21
+ ## Rulings
22
+
23
+ 1. **The index is the last section of the region, and it is one section.** Its heading is
24
+ `## Disclosure`. A scoped profile keeps the pointer ADR-071 ruling 7 requires: the
25
+ patterns, and the files the profile moved to. A skill keeps its name and its
26
+ description, which is the trigger. A second heading for the second kind would spend
27
+ region bytes on a distinction the reader acts on the same way — open that thing when
28
+ the line says to.
29
+
30
+ 2. **A skill contributes nothing but that line.** The body stays in the skill file. The
31
+ description is read with the same frontmatter reader the skill lint uses, so a folded
32
+ description is the sentence the author wrote and not the marker in front of it. A
33
+ skill with no description says so and names `simt skills show`, because a line that
34
+ invents a trigger would send the reader to the wrong file.
35
+
36
+ 3. **The directories are the four a skill is installed into.** They are the directory
37
+ `skills install` chooses and the directories a loader row records, which together are
38
+ four and not a walk of the repository. A symlink to a skill already counted is not a
39
+ second skill. Two different files that share a name are both listed, with the
40
+ directory, because keeping one would hide a trigger.
41
+
42
+ 4. **The index is region text, so every selected surface sees the same lines.** A surface
43
+ that already shows a skill's name and description at session start sees the line
44
+ twice. The alternative is a region that depends on which surface is reading it, and
45
+ `check` would then disagree with a reader whose surface has no skill channel. The
46
+ body is the copy that stays out. ADR-071 ruling 1 still decides the profile itself:
47
+ it leaves the region only when every selected surface can narrow it.
48
+
49
+ 5. **The index is not a candidate for a budget cut.** Cutting it would remove the only
50
+ mention of the rules a cut had already removed. It is weighed, because it does cost
51
+ bytes, and it is kept.
52
+
53
+ ## Consequences
54
+
55
+ On a surface that can narrow, a fixture's scoped profile is absent from the region and
56
+ present in the file that surface loads when a matching path is in play. On a surface that
57
+ cannot, the profile stays in the region. That is the accept, checked by rendering each
58
+ surface rather than by driving the harness: this tool writes the file, and the harness
59
+ is what opens it.
60
+
61
+ A repository with no installed skill and no scoped profile gains no section. An empty
62
+ heading would be a line about nothing.
63
+
64
+ ## Rejected
65
+
66
+ - **Putting the skill body in the region.** That is the cost the index exists to avoid,
67
+ and it is what a surface with a skill channel already refuses to do.
68
+ - **Omitting the line on a surface that shows skill metadata itself.** The region is one
69
+ document. A line present for one reader and absent for another is drift.
70
+ - **Truncating a description to keep the line short.** The description is the trigger. A
71
+ shorter line that no longer says when to open the skill is a line that cannot do its
72
+ job.
@@ -0,0 +1,69 @@
1
+ # ADR-086 — The stable prefix comes first, and the lock records both digests
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R13 (plan 0003)
4
+
5
+ **Affects:** `src/projection/rules.mjs`, `src/projection/lock.mjs`, `src/commands/sync.mjs`,
6
+ `src/argv.mjs`, `test/lock.test.mjs`, `test/card.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A reader caches the instruction file. A change in the first bytes throws the rest away,
11
+ including the rules that did not change. The plan asks for the stable content first —
12
+ core, then stack, then what this repository added — and for a lock that fails when the
13
+ output moves and the input did not.
14
+
15
+ The command card was the first bytes of the region (ADR-077 ruling 1), because the
16
+ commands are what a reader cannot infer. They are also the bytes a repository edit is
17
+ most likely to move: a script change would then invalidate the core that follows them.
18
+
19
+ The content digest stays in the generated header. It is a digest of what this package
20
+ ships (ADR-032), so a repository edit does not move it, and `check` uses it to tell a
21
+ hand-edit from a version mismatch. Taking it out of the file would make those two
22
+ failures look the same. No date and no absolute path is written into the region.
23
+
24
+ ## Rulings
25
+
26
+ 1. **Shipped rules, then this repository's own bytes.** Core first, then the other
27
+ profiles this package ships, by id. Then the command card, then profiles the
28
+ repository added, then the index of what the region does not carry. This narrows
29
+ ADR-077 ruling 1: the card still leads the repository's own bytes, and it no longer
30
+ leads the region. The rest of that record stands, including that the card is derived
31
+ from committed evidence only.
32
+
33
+ 2. **The order inside the shipped prefix is by id, not by directory order.** A directory
34
+ listing is not a fact about the rules. Two machines rendering one repository produce
35
+ the same bytes; the test renders one repository twice, which is the half a single
36
+ process can see. The other half is the same argument ADR-073 ruling 7 makes: the
37
+ inputs are the files, and nothing in the render reads the clock, the home directory
38
+ or the network.
39
+
40
+ 3. **`simt-dev.lock.json` records an input digest and an output digest.** The input is
41
+ the package digest, the detected signals, the manifest keys that change the region,
42
+ the repository's own profiles and its `package.json` scripts. The output is the
43
+ region body. `sync --yes` rewrites the lock when it differs. `check` without
44
+ `--frozen` does not require one, because every repository adopted before this record
45
+ would otherwise fail a gate it had not been asked to keep.
46
+
47
+ 4. **`check --frozen` fails in two ways, and names which.** No lock, or a lock whose
48
+ input digest differs, is a stale lock: rewrite it with `sync --yes`. A lock whose
49
+ input matches and whose output does not is unexplained: the projection changed and
50
+ nothing it read did. An output change that an input change explains is the stale
51
+ case, not the unexplained one.
52
+
53
+ ## Consequences
54
+
55
+ Editing a repository profile leaves the bytes before that profile identical. A frozen
56
+ check on a matching lock passes, a tampered output fails as unexplained, and a changed
57
+ profile fails as a stale lock.
58
+
59
+ ## Rejected
60
+
61
+ - **Removing the content digest from the header.** The digest does not move when the
62
+ repository edits a line, and without it a hand-edit and a version mismatch are one
63
+ finding.
64
+ - **Hashing the whole manifest, or the whole `package.json`.** A comment, or a
65
+ dependency bump that the projection does not read, would fail the lock for a region
66
+ that had not changed.
67
+ - **Failing an ordinary `check` when the lock is absent.** The lock is new. The
68
+ repositories this package already adopted do not have one, and a gate that turned red
69
+ for that absence would be red everywhere until someone ran `sync`.
@@ -0,0 +1,67 @@
1
+ # ADR-087 — A run is evidence for one tree
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R17 (plan 0003)
4
+
5
+ **Affects:** `src/ledger/index.mjs`, `src/commands/evidence.mjs`, `src/hooks/dispatcher.mjs`,
6
+ `test/ledger.test.mjs`
7
+
8
+ ## Context
9
+
10
+ The rule that a test was run is otherwise a sentence in a commit message. Nothing checks
11
+ it, so a message can say the tests passed for a tree they were never run against, and
12
+ for a tree that has been edited since. The plan asks for a ledger, a proof block, and a
13
+ commit-message check that warns unless the repository asks it to block.
14
+
15
+ The tree has to be named without staging. `git add -A` on the real index would take the
16
+ author's unstaged work into the commit that is about to happen, which is the opposite of
17
+ what a hook is allowed to do.
18
+
19
+ ## Rulings
20
+
21
+ 1. **The tree is everything on disk, taken with a private index.** `git add -A` and
22
+ `git write-tree` run against a temporary `GIT_INDEX_FILE`, which is deleted
23
+ afterwards. The repository's own index is not read for this and not written. A file
24
+ the author left unstaged stays unstaged. The ledger file is left out of that add,
25
+ because the line is a record of the tree and writing it must not replace the tree
26
+ it names.
27
+
28
+ 2. **A ledger line records the run, not its output.** The command, the exit code, the
29
+ duration, a one-line summary, and a digest of the output. The output itself is not
30
+ stored. The line is appended when the dispatcher actually runs a gate. A gate that
31
+ does not run leaves no line. A tree that cannot be named leaves no line, and the
32
+ claim check then finds nothing.
33
+
34
+ 3. **A verification claim is a passing line for this tree.** The phrases are `tests
35
+ pass`, `tests passed`, `test pass`, `test passed`, and `verified`. `unverified` and
36
+ `not verified` are not claims. A line with a non-zero exit does not support the
37
+ claim. Editing any file changes the tree, so the line stops supporting it.
38
+
39
+ 4. **The check warns, and blocks only when the repository says so.** `gates.evidence`
40
+ set to `block` fails the commit. Unset, or `warn`, prints the warning and exits 0.
41
+ Any other value is an error, because a misspelt `block` must not become a warning.
42
+ `simt evidence` prints the lines for the current tree and writes nothing.
43
+ `simt hooks install --yes` writes a commit-msg shim only when `gates.evidence` is
44
+ `warn` or `block`. The shim forwards the message file. Unset installs nothing,
45
+ and removing the setting removes the shim, so a hook the repository did not ask
46
+ for does not stay behind and replace git's own.
47
+
48
+ ## Consequences
49
+
50
+ A green run followed by an edit of any file leaves the new tree with no evidence. The
51
+ old line is still in the ledger, and it names the tree it was true for. A commit
52
+ message that claims the tests passed is blocked only in a repository that set
53
+ `gates.evidence` to `block`.
54
+
55
+ ## Rejected
56
+
57
+ - **Staging with the real index in order to name the tree.** The next commit would
58
+ contain files the author had not staged.
59
+ - **Storing the command's output in the ledger.** A test log is not evidence the next
60
+ reader needs, and it is where a secret printed by a failing command would be kept.
61
+ - **Treating any ledger line as proof the tests passed.** A failing run is a line. It
62
+ is not a pass.
63
+ - **Blocking every repository by default.** The check is new. A repository that has
64
+ not asked for it gets a warning, which it can read, and its commit still happens.
65
+ - **Adding commit-msg to the dispatcher events.** Those shims do not forward git's
66
+ arguments, because a pre-push remote would be read as one. The message file is
67
+ the argument, so the claim check is its own shim.
@@ -0,0 +1,59 @@
1
+ # ADR-088 — A marked line is enforced by a checker
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R19 (plan 0003) · ruling 3 narrowed by ADR-090
4
+
5
+ **Affects:** `src/kernel/checks.mjs`, `rules/core.md`, `src/commands/sync.mjs`,
6
+ `test/checks.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A line an agent is trusted to follow is a line the next agent can skip. The file-length
11
+ rule is already two numbers with two meanings, and the check that was supposed to apply
12
+ them was never run. The plan asks for a line to name a checker, for the projection to
13
+ keep the name, and for the checker to be what actually decides.
14
+
15
+ One of the named checkers, `import-direction`, needs a module graph this tool does not
16
+ have yet. Removing that instruction now would leave a name and no check.
17
+
18
+ ## Rulings
19
+
20
+ 1. **A list item may name one checker.** The marker is `simt-check`, on the item,
21
+ including the lines wrapped under it. The checkers are `max-file-lines`,
22
+ `forbid-regex`, `forbidden-path-edit`, `required-file`, and `import-direction`.
23
+ Anything else is a broken rule. A marker inside a fenced sample is not a checker.
24
+
25
+ 2. **A checker that can run replaces the item with its name.** The projection says
26
+ `Enforced:` and the checker. The prose does not. `simt check` runs the checker
27
+ against the rule as written, so the line that left the prompt is the line that
28
+ still decides. `max-file-lines` reads `limits.fileLines`: over the trigger warns,
29
+ over the ceiling fails. A path is exempt when `limits.fileLines.generated` names
30
+ it, or when the file carries the generated-region marker this tool writes, which
31
+ is this tool naming itself as the generator. The numbers stay 400 and 1000 where
32
+ a repository has not named its own.
33
+
34
+ 3. **`import-direction` does not replace its item.** The module graph it reads is not
35
+ built. The instruction stays, the marker does not reach the reader, and `simt check`
36
+ says the checker is not built. It does not fail the repository for that.
37
+
38
+ 4. **A diff checker reads the change.** `forbid-regex` and `forbidden-path-edit` look
39
+ at added lines and changed paths, including untracked files. A match already in the
40
+ last commit is not a new match. A directory that is not a git repository is not a
41
+ failure of these checkers: there is no tree to name.
42
+
43
+ ## Consequences
44
+
45
+ The file-length sentence no longer occupies the region. The bytes it occupied are
46
+ smaller by the length of that sentence. Whether agents still comply with the rule is
47
+ unmeasured: the probes were not run.
48
+
49
+ `simt check` now fails a tracked file past the ceiling in every repository that
50
+ selects the core rules, which is every repository.
51
+
52
+ ## Rejected
53
+
54
+ - **Dropping `import-direction` before it can run.** The name would say the line is
55
+ enforced. Nothing would be enforcing it.
56
+ - **Failing `simt check` outside a git repository.** There is no tracked tree to count
57
+ and no diff to read. A missing repository is not a long file.
58
+ - **Exempting a file because it looks generated.** The repository names the generator.
59
+ A guess would hide the ceiling from the file that most needs it.
@@ -0,0 +1,52 @@
1
+ # ADR-089 — The local index is a cache of git blobs
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R20 (plan 0003)
4
+
5
+ **Affects:** `src/index/store.mjs`, `src/commands/repoindex.mjs`, `test/index.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Later slices ask which file defines a name, which test covers a change, and which
10
+ decision governs a path. Those questions need a store, and the store has to be
11
+ rebuildable: a cache that cannot be reconstructed from the repository is a second
12
+ source of truth. The plan names `node:sqlite`, with a fallback for a Node that does
13
+ not have it, and it names the blob hash from `git ls-files` as the unit of change.
14
+
15
+ The floor this package declares is older than the Node that first shipped that module
16
+ as something a program can rely on. A store that only works on the machine that
17
+ happened to build it is not the store the plan describes.
18
+
19
+ ## Rulings
20
+
21
+ 1. **The store is sqlite when this process can open it and create an FTS5 table.**
22
+ The file is `.simt-dev/index.sqlite`. The tables are the ones the plan names:
23
+ files, symbols, edges, an FTS5 table, and meta. When the module is absent or FTS5
24
+ cannot be created, the same file rows are written to `.simt-dev/index.jsonl`.
25
+ Nothing is added to the dependency set.
26
+
27
+ 2. **A row is keyed by the git blob.** `git ls-files -s` supplies the path and the
28
+ blob. A path whose blob has not changed is not read again, and its row is not
29
+ rewritten. A path that left the index leaves the store, and so does anything
30
+ recorded against that path. Two indexes of the same blobs produce the same file
31
+ rows. The language comes from the extension, and the package comes from the
32
+ nearest manifest walking up from the file.
33
+
34
+ 3. **`simt index` rebuilds the cache.** It does not preview, because the file it
35
+ writes is not repository content. It says when the path is not gitignored. A
36
+ directory that is not a git repository cannot be indexed.
37
+
38
+ ## Consequences
39
+
40
+ Reindexing one changed file reads that blob and leaves the others. The symbols and
41
+ edges tables exist and are cleared for a path whose blob changed. Nothing fills them
42
+ until a later slice extracts names from the bytes.
43
+
44
+ ## Rejected
45
+
46
+ - **Requiring sqlite.** The floor this package already ships does not promise the
47
+ module. A repository on that floor still gets an index.
48
+ - **Hashing the worktree instead of the blob.** An unstaged edit is not what
49
+ `git ls-files` reports. The index follows the index, so two machines with the same
50
+ committed blobs agree.
51
+ - **Treating the cache as a writer that previews.** A preview that does not write
52
+ leaves the reader with no index. The command's only effect is the cache.
@@ -0,0 +1,55 @@
1
+ # ADR-090 — An optional adapter is off until a repository names it
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R21 (plan 0003)
4
+
5
+ **Affects:** `src/index/extract.mjs`, `src/index/store.mjs`, `src/index/persist.mjs`,
6
+ `test/extract.test.mjs`
7
+
8
+ ## Context
9
+
10
+ The index can say which file defines a name only if something reads the blob. A
11
+ structural parser does that more accurately than a scan of lines, and it is also a
12
+ dependency: grammars, a WASM runtime, a package this tool would then require. The
13
+ floor this package ships is zero dependencies. An adapter that is always loaded is
14
+ not optional, and an adapter this package names for one organisation is a name the
15
+ next repository does not have.
16
+
17
+ The module graph of imports now exists. ADR-088 ruling 3 kept `import-direction` in
18
+ the prose because that graph did not. The graph still does not say which way an
19
+ import is allowed to point.
20
+
21
+ ## Rulings
22
+
23
+ 1. **The built-in extractors are the index.** They read one line at a time and
24
+ record definitions, exports and imports for the languages a map needs. The same
25
+ blob produces the same symbol rows and the same import edges. A blob that has
26
+ not changed is not read again. Recall against a hand-labelled set of exported
27
+ names, for TypeScript, Kotlin, Python and Go, is at least 90%. A comparison
28
+ against a structural parser was not run.
29
+
30
+ 2. **An optional adapter is a separate package, and it is off until named.**
31
+ `index.parsers` is the package name. Absent, nothing is imported. This package
32
+ does not depend on a parser and does not ship a default name for one. When the
33
+ named package does not load, the built-in rows are the cache and the index says
34
+ so. When it loads, its symbols and edges replace the built-in rows for that
35
+ blob. It writes those rows into the cache. It does not edit a source file.
36
+
37
+ 3. **`import-direction` still does not replace its line.** ADR-088 ruling 3 stands,
38
+ except for the sentence that the module graph is not built. The edges are
39
+ recorded. No rule names the direction they must follow, so the instruction stays
40
+ and `simt check` does not fail a repository for the missing direction.
41
+
42
+ ## Consequences
43
+
44
+ `simt index` now reports how many symbols and edges the cache holds. Whether the
45
+ line extractor misses a name a parser would have found, on a real repository, is
46
+ unmeasured: the bar above is the labelled fixtures, not a live parser.
47
+
48
+ ## Rejected
49
+
50
+ - **Depending on a parser, or probing one this package names.** The dependency set
51
+ would no longer be empty, and a name that only one organisation publishes is not
52
+ a default for every repository.
53
+ - **Treating the import edges as a direction check.** An edge says that A imports B.
54
+ It does not say that A was allowed to. Dropping the instruction would report the
55
+ line as enforced.
@@ -0,0 +1,60 @@
1
+ # ADR-091 — The map ranks the files a task mentions, and the files next to them
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R22 (plan 0003)
4
+
5
+ **Affects:** `src/index/map.mjs`, `src/commands/map.mjs`, `src/commands/bench.mjs`,
6
+ `test/map.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A task names a behaviour. The files that behaviour lives in are a small part of
11
+ the tree, and an agent that reads the tree in path order spends its budget on
12
+ files the task never touches. The index now records imports. What it did not
13
+ do is say which of those files to read first.
14
+
15
+ The mined tasks store a gold patch outside the worktree. The published task
16
+ record counts the files and the lines. It does not list the paths. Recall has
17
+ to be read from the patch, and a machine without that patch cannot invent the
18
+ denominator.
19
+
20
+ ## Rulings
21
+
22
+ 1. **The map is PageRank over import and call edges, personalised to the task.**
23
+ Twenty iterations, damping 0.85. An edge counts in both directions, once.
24
+ A file the task mentions sorts ahead of a file it does not: on this graph
25
+ the file every module imports otherwise occupies the top 10. A term that
26
+ labels too many paths is ignored. A token that contains a digit and names
27
+ one path counts more the more often the task says it, so a citation does
28
+ not rank with the subject. A file that already matched, in a directory of
29
+ at most eight files, is lifted toward its neighbour. Files the task does
30
+ not mention, and that are not one edge away from a file it does, are left
31
+ out. With no task text the whole graph is ranked. Equal ranks break by
32
+ path, ascending. The same graph and the same text return the same order.
33
+
34
+ 2. **The rendering fits a budget, at one of three levels.** `outline` is paths.
35
+ `names` adds the exported symbols. `signatures` adds the line they were
36
+ declared on. The default budget is 2048 bytes. Zero means no ceiling. The
37
+ first file is kept when it alone is over the budget, so a map of a real
38
+ ranking is not an empty page.
39
+
40
+ 3. **recall@10 is the mean, over mined tasks, of the fraction of gold files
41
+ that land in the top 10.** The gold paths are the destination side of the
42
+ patch. A task whose patch is not in the vault is unmeasured, not a miss.
43
+ `simt bench report` prints the figure. It does not fail the cost report
44
+ when the figure cannot be computed.
45
+
46
+ ## Consequences
47
+
48
+ `simt map` reads the index and rebuilds it when the cache is empty. The
49
+ comparison against the mined patches is a property of this checkout's index
50
+ and of the vault on this machine. A checkout with no index, or no vault,
51
+ reports the figure as unmeasured. On the 20 mined tasks available here, mean
52
+ recall@10 is 55.6% of gold files. The bar is 60%, and this ranking does not
53
+ meet it.
54
+
55
+ ## Rejected
56
+
57
+ - **One-directional edges.** Rank would flow only toward the imported file, and
58
+ the test that imports the file the task named would never surface.
59
+ - **A zero when the gold patch is absent.** That number would punish a machine
60
+ for a file the benchmark hides, and it would read as a measurement.
@@ -0,0 +1,60 @@
1
+ # ADR-092 — A test is an import, a conventional neighbour, or a coverage record
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R24 (plan 0003)
4
+
5
+ **Affects:** `src/index/tests.mjs`, `src/commands/tests.mjs`, `test/mapping.test.mjs`
6
+
7
+ ## Context
8
+
9
+ The index records imports. A test file that imports a source file is evidence
10
+ that the test exercises it, and so is a coverage report that names both. A
11
+ shared commit is weaker: most commits touch a test and several sources that
12
+ the test does not exercise.
13
+
14
+ The line extractor reads one line at a time, so an import whose specifier sits
15
+ on the next line is absent from the stored edges. The mapping has to see that
16
+ specifier or it misses the tests this repository actually writes.
17
+
18
+ ## Rulings
19
+
20
+ 1. **The default answer uses only the precise edges.** Kind `test`, weight 1.
21
+ A test file imports a tracked source file, including a dynamic `import()`.
22
+ Or the conventional source beside the test exists: `foo_test.go` beside
23
+ `foo.go`, `test_foo.py` beside `foo.py`, `FooTest` beside `Foo` in the same
24
+ directory, `foo.test.mjs` beside `foo.mjs`, and `src/test/java` mirrored to
25
+ `src/main/java`. Or a coverage record names both sides. lcov pairs a source
26
+ only when the test name is non-empty. JaCoCo pairs a source file with the
27
+ one class whose stem is that file plus `Test` or `Tests`. A Go profile
28
+ counts a hit, and the test is `stem_test.go` in that directory, or the
29
+ single `*_test.go` there. A file that is not one of those is not an edge.
30
+
31
+ 2. **Co-change is a different kind, and it is off unless asked.** Kind
32
+ `cochange`. A pair counts when it appears in at least two commits. A commit
33
+ with more than five tests or more than fifteen sources is skipped. Each
34
+ test keeps its eight strongest pairs. History is the last 200 non-merge
35
+ commits. `simt tests --cochange` appends those files after the precise
36
+ ones. A git failure leaves the history empty; the import map still answers.
37
+
38
+ 3. **The edges are computed when asked, and not stored.** The index write
39
+ replaces every edge of a changed path. Storing the test map in that write
40
+ would drop the import edges, and reading the commit log inside a reindex
41
+ would miss the one-file time bar. Coverage is read from a fixed list of
42
+ paths. A path that is absent is not a failure.
43
+
44
+ 4. **Precision is the share of predicted tests that the label also names.**
45
+ An empty prediction does not pass. On the 30 hand-labelled files in this
46
+ checkout the figure is 1.0. The bar is 0.8.
47
+
48
+ ## Consequences
49
+
50
+ `simt tests` with no path prints how many test edges and how many co-change
51
+ edges the tree has. `simt tests <path>` lists the tests for that file. A path
52
+ the index does not contain is a usage error. A directory that is not a git
53
+ repository is unsupported.
54
+
55
+ ## Rejected
56
+
57
+ - **A stem that is unique somewhere else in the tree.** The test named for a
58
+ directory is not the test of the one file that happens to share that stem.
59
+ - **Writing the test edges into the index.** The next reindex of a touched
60
+ path would have to know about every edge kind or it would delete them.
@@ -0,0 +1,58 @@
1
+ # ADR-093 — why names what governs a file
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R25 (plan 0003)
4
+
5
+ **Affects:** `src/index/govern.mjs`, `src/commands/why.mjs`, `test/why.test.mjs`,
6
+ `test/why-fixture.test.mjs`, `test/why-snapshot.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A decision says which files it governs in an Affects line. The line is prose.
11
+ It wraps. It names a command, a path from the repository root, a path from a
12
+ source directory, or a phrase that is not a location at all. A reader who
13
+ greps the word Affects still has to decide which of those a backtick was.
14
+
15
+ Rules, skills, gates and lessons name paths in their own way. The question
16
+ `why` answers is the same for each: does this one govern this file?
17
+
18
+ ## Rulings
19
+
20
+ 1. **The Affects line contributes the tokens in backticks, and nothing else.**
21
+ Continuation lines are part of the paragraph. A blank line, a heading, or
22
+ the next field ends it. The token is resolved in order: a command prefixed
23
+ with the tool name, when that command exists; a path or a glob at the
24
+ repository root; the same path under each directory named src. The tool
25
+ name alone is not a command. A token that is none of these is reported as
26
+ `UNRESOLVED-AFFECTS`, with the record it came from. It is not dropped and
27
+ it is not guessed.
28
+
29
+ 2. **The other governors are the path lists they already publish.** A rule
30
+ governs a file its scope matches. An applies entry governs a file only when
31
+ the entry is itself a path. A skill governs a file its paths match. A
32
+ staged gate governs a file of an extension it formats, and a manifest
33
+ command governs a file it names. A lesson governs a file its trigger names.
34
+ A missing lessons directory, a missing skill registry, or a missing
35
+ manifest is an empty list.
36
+
37
+ 3. **The answer is computed when asked, and not stored.** The file list is
38
+ the paths git tracks. Writing the index to answer the question would change
39
+ the tree the question is about.
40
+
41
+ ## Consequences
42
+
43
+ `simt why <path>` prints one line for each decision, rule, skill, gate and
44
+ lesson that governs that path. `simt why` with no path prints the unresolved
45
+ Affects tokens. A path git does not track is a usage error. A directory that
46
+ is not a git repository is unsupported.
47
+
48
+ On the tree at 4c27514, sixteen Affects lines were the whole set, and the
49
+ only record naming the dispatcher was ADR-017. Records written after that
50
+ commit name it as well, and `why` lists those too.
51
+
52
+ ## Rejected
53
+
54
+ - **Attaching every always-on rule to every file.** The rule governs the
55
+ repository. Listing it on each file would hide the ones that actually name
56
+ the file.
57
+ - **Treating an unresolved token as a path that might exist later.** The
58
+ report is the point. A guess would make the report empty.