simt-dev 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +375 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/README.md +17 -0
- package/package.json +1 -2
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +40 -0
- package/rules/stacks/deno.md +38 -0
- package/rules/stacks/dotnet.md +37 -0
- package/rules/stacks/elixir.md +41 -0
- package/rules/stacks/go.md +36 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +38 -0
- package/rules/stacks/rust.md +36 -0
- package/rules/stacks/swift.md +39 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +54 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +58 -25
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +37 -5
- package/src/commands/hooks.mjs +173 -57
- package/src/commands/index.mjs +95 -69
- package/src/commands/init.mjs +100 -20
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +96 -60
- package/src/commands/skills.mjs +189 -86
- package/src/commands/skillsinstall.mjs +20 -5
- package/src/commands/sync.mjs +213 -15
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +107 -7
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +84 -7
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +290 -3
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +86 -9
- package/src/package-version.mjs +74 -0
- package/src/packs.mjs +182 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +335 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +139 -8
- package/src/projection/rules.mjs +308 -27
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +90 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# ADR-060 — A repository with nothing to run is adopted and says so, and a gate is verified where it runs
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-12 · filed as S36
|
|
4
|
+
**Affects:** `hooks install`, `compare`, the adoption criterion in review 0002
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Review 0002 defined adoption as eight things a repository must have, and gave the estate
|
|
9
|
+
roll-out an acceptance criterion: every repository current, every repository routed, no
|
|
10
|
+
unrunnable gate, and a green CI run in every repository that has CI. Fourteen repositories
|
|
11
|
+
were in scope.
|
|
12
|
+
|
|
13
|
+
Three of them cannot satisfy the second of those, and the row contradicted itself about it.
|
|
14
|
+
The runbook's own step 4, for one of the three, reads: *"none (no runner). Record that in
|
|
15
|
+
the manifest rather than invent one."* That is an instruction not to route it. The
|
|
16
|
+
acceptance criterion then requires that it be routed. One of the two is wrong, and the row
|
|
17
|
+
could not be closed without settling which.
|
|
18
|
+
|
|
19
|
+
This stopped being hypothetical when the roll-out ran. The three are:
|
|
20
|
+
|
|
21
|
+
- **A Gradle module consumed as a submodule.** It has no wrapper of its own — the two
|
|
22
|
+
repositories that embed it own that — so `./gradlew` cannot be run from it. Its
|
|
23
|
+
`package.json` declares no scripts at all; it exists to carry one devDependency.
|
|
24
|
+
- **A repository with no `package.json`.** No runner, no scripts, nothing to probe.
|
|
25
|
+
- **A static site with no gate and its own hook.** No `format:check` and no `test` script to
|
|
26
|
+
declare a gate from, and a Lighthouse pre-push in `.githooks/` that routing would displace.
|
|
27
|
+
|
|
28
|
+
Each is ungated for a reason that was checked rather than assumed, and each records that
|
|
29
|
+
reason in its own commit message.
|
|
30
|
+
|
|
31
|
+
ADR-017 already settled the principle in one direction: a gate is declared only if the
|
|
32
|
+
repository can run it. This record settles the other, which fourteen repositories of real
|
|
33
|
+
use never exercised — what a repository that can run nothing looks like once it has adopted
|
|
34
|
+
the tool.
|
|
35
|
+
|
|
36
|
+
A second thing surfaced at the same moment, and it is not a repository's fault. The roll-out
|
|
37
|
+
pushed to fourteen repositories and GitHub Actions executed no steps: every run completed
|
|
38
|
+
with `failure`, zero steps, and no logs — including a pre-existing lint-and-test workflow
|
|
39
|
+
whose body this work never touched. Actions reports `enabled: true` on all eight
|
|
40
|
+
repositories; the organisation-level setting needs `admin:org` and could not be read. The
|
|
41
|
+
workflow files parse, and the same workflows have run before. The jobs are not starting.
|
|
42
|
+
|
|
43
|
+
## Ruling
|
|
44
|
+
|
|
45
|
+
**One. A repository that cannot run a gate is adopted when it says so, and routing is not
|
|
46
|
+
required of it.** `hooksComplete` is not the measure of adoption, and "every repository
|
|
47
|
+
routed" was never achievable. The three carry the evidence in the manifest and in their own
|
|
48
|
+
commit messages: what is missing, and why. That is a stronger artefact than a shim that
|
|
49
|
+
runs nothing — which is what forcing them would have produced, and which the runbook already
|
|
50
|
+
forbade inventing.
|
|
51
|
+
|
|
52
|
+
**Two. A gate must be runnable, and a shim must not be installed over one that is working.**
|
|
53
|
+
This is ADR-018 as amended, restated because the roll-out made it concrete. The static site's
|
|
54
|
+
pre-push keeps running in `.githooks/` precisely because its manifest declares no `prePush`
|
|
55
|
+
gate and therefore nothing displaces it. Routing a repository that enforces nothing is not
|
|
56
|
+
a smaller change than routing one that does: `core.hooksPath` moves, git reads every hook
|
|
57
|
+
from the one directory, and the gain is zero.
|
|
58
|
+
|
|
59
|
+
**Three. "Green CI" is verified where CI runs, and is not a property of a repository when it
|
|
60
|
+
does not.** The clause is replaced by two that can be answered: the workflow file is present
|
|
61
|
+
and valid in every repository that has one, and the gate runs locally. Whether GitHub
|
|
62
|
+
executes those workflows is a fact about the account, and a gate that reports a property it
|
|
63
|
+
cannot observe is ADR-039's defect — the step that cannot fail is the one nobody reads.
|
|
64
|
+
|
|
65
|
+
**Four. The estate's CI runs on pull requests, and delivery does not.** A check that fires on
|
|
66
|
+
a timer is not evidence about a change: by the time it reports, the commit is days old and
|
|
67
|
+
nobody is waiting on it. That is why the weekly runs were removed rather than de-scheduled.
|
|
68
|
+
It is also why the two API-contract workflows were split out of the `ci.yml` files they
|
|
69
|
+
shared: republishing to the edge needs a credential a fork does not have, so it fires on
|
|
70
|
+
merge to `main`, and one workflow cannot carry two triggers once its checks and its delivery
|
|
71
|
+
need different ones. The template moved in one commit and the estate followed the template,
|
|
72
|
+
which is the only way to move fourteen repositories that do not own the file.
|
|
73
|
+
|
|
74
|
+
**Five. The measure is `check`, and `compare` is what makes it visible at estate scale.**
|
|
75
|
+
ADR-045 made `compare` count a defect it had been hiding. This cycle found the same row
|
|
76
|
+
reporting every repository current while `check` exited non-zero in all of them. Both are now
|
|
77
|
+
driven at one fixture and compared — a parity test, because two copies of one question, each
|
|
78
|
+
asserted separately, is how they drift. That is ADR-052's shape, reached from a different
|
|
79
|
+
direction.
|
|
80
|
+
|
|
81
|
+
## Consequences
|
|
82
|
+
|
|
83
|
+
- **The roll-out closes with eleven of fourteen routed, and the three exceptions named in
|
|
84
|
+
three commit messages** rather than in a dashboard. An audit trail beats a count.
|
|
85
|
+
- **If one of the three gains something to run, it routes in one command** and nothing else
|
|
86
|
+
about it changes. This is not a permanent exemption; it is the current answer to "can this
|
|
87
|
+
repository enforce anything", and `compare` re-reads it every run.
|
|
88
|
+
- **The result is stronger than the row asked for.** Every repository's gate exits zero,
|
|
89
|
+
every manifest records a released version, no rule is loaded twice, no declared gate is
|
|
90
|
+
unrunnable, and no region sits past a harness's read limit. Three of those were false when
|
|
91
|
+
the row was written.
|
|
92
|
+
- **The CI clause is recorded as unverified, not met.** Nothing in a repository can make
|
|
93
|
+
GitHub execute a workflow, and a claim about a green build nobody has seen is the thing
|
|
94
|
+
this repository's own rules exist to prevent. Re-run the query and the clause comes back.
|
|
95
|
+
|
|
96
|
+
## Rejected
|
|
97
|
+
|
|
98
|
+
- **Route the three anyway, with an empty gate list.** `hooks install` already refuses —
|
|
99
|
+
it prints *"declares no gate it can run, so there is nothing to enforce."* Overriding it
|
|
100
|
+
means a shim on every commit that dispatches, reads a manifest, and exits having enforced
|
|
101
|
+
nothing. The cost is a repository that believes it is gated.
|
|
102
|
+
- **Declare a gate to satisfy the count.** That needs a name for a check that does not
|
|
103
|
+
exist. The Connect IQ repository has the shape of this: seven scripts in `scripts/` that
|
|
104
|
+
all run, and a gate vocabulary with no name for five of them. Two were declared, where the
|
|
105
|
+
name matched, and the other five stay manual rather than borrow `test` or `lint` — a gate
|
|
106
|
+
whose printed name misdescribes the command it runs teaches a reader not to trust the
|
|
107
|
+
other seven.
|
|
108
|
+
- **Keep the weekly runs because they are "just checks".** They were the most expensive
|
|
109
|
+
thing in the estate — the Android one booted a headless emulator every Saturday — and the
|
|
110
|
+
least timely. Where the checks were worth keeping they moved to the pull request that
|
|
111
|
+
changes the code, and one repository's own test suite refused the push that deleted them,
|
|
112
|
+
which is the gate working.
|
|
113
|
+
- **Close the row before measuring it.** Every figure in this record comes from a committed
|
|
114
|
+
build run over the estate, not from the working tree that produced it. That distinction
|
|
115
|
+
matters here: earlier in this cycle an apparent fourteen-repository defect turned out to be
|
|
116
|
+
caused by an uncommitted change, and the number would have been reported as a fact about
|
|
117
|
+
the estate.
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
This record names no repository. The sweep that pseudonymised the estate used stable
|
|
122
|
+
`repo-NN` labels because several older records use repository names as *evidence* — a table
|
|
123
|
+
of how three repositories answer the same question, where "a repository and a repository"
|
|
124
|
+
would destroy the argument. That argument is not present here: every repository in this
|
|
125
|
+
record is identified by what it *is*, which is both publishable and more useful to a reader
|
|
126
|
+
who has none of the files.
|
|
127
|
+
|
|
128
|
+
`test/shipped.test.mjs` now fails on any estate name in anything this package publishes —
|
|
129
|
+
the records, the changelog, the readme. It is scoped to the published set and asserts that
|
|
130
|
+
scope against `package.json`, in both directions, so a directory added to `files` without
|
|
131
|
+
being guarded fails rather than leaking.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ADR-061 — The skill registry is workspace data, and the package ships none of it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-12 · filed as S61
|
|
4
|
+
**Affects:** `graph lint`, `skills list`, `skills show`, `skills resolve`, `skills brief`,
|
|
5
|
+
`src/graph/load.mjs`, `package.json` `files`, the estate-name guard in `test/shipped.test.mjs`
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`graph/skills.graph.json` is published. It is in `files`, so it is in the npm tarball and in
|
|
10
|
+
the wheel, and `REGISTRY_PATH` finds it by resolving from `src/` — which in an installed
|
|
11
|
+
package is inside the package itself:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
export const REGISTRY_PATH = resolve(HERE, '..', '..', 'graph', 'skills.graph.json');
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
So the registry every one of those commands reads is not the reader's. It is *this* source
|
|
18
|
+
tree's catalogue of the skills that live in sibling repositories of this estate. An adopting
|
|
19
|
+
repository has none of them.
|
|
20
|
+
|
|
21
|
+
The estate-name guard found the symptom, and the symptom is the smaller half. Widened from
|
|
22
|
+
`.md` to every text extension the package ships, it reported **51 strings in that one file**
|
|
23
|
+
naming eight private repositories. The paths are the larger half and are not names at all:
|
|
24
|
+
every `sources[].path` is `.agents/skills/<id>` or `.cursor/skills/<id>`, and not one of
|
|
25
|
+
them resolves in a repository that is not this workspace. A reader who runs
|
|
26
|
+
`simt skills list` gets skills they do not have, and `simt skills resolve` finds the
|
|
27
|
+
declaration and then looks for a body that was never there. That is the corpus check this
|
|
28
|
+
repository exists to enforce, failing in a file this repository publishes, on a scale the
|
|
29
|
+
name grep was too narrow to see.
|
|
30
|
+
|
|
31
|
+
Two more facts made the shape of the answer unambiguous rather than a judgement call:
|
|
32
|
+
|
|
33
|
+
- **The sources are load-bearing, not decoration.** `graph lint` refuses a skill with none
|
|
34
|
+
("no sources; every skill must point at the file it came from"), and the same array does
|
|
35
|
+
byte verification, variant resolution and duplicate detection. Nothing can simply be
|
|
36
|
+
deleted from it.
|
|
37
|
+
- **There is already a workspace, and it is already resolved here.** `loadRegistry` calls
|
|
38
|
+
`workspace(flags)` before it validates anything, precisely so the sources can be checked
|
|
39
|
+
against a root. The registry is read from *inside the installed module* and verified
|
|
40
|
+
against *the reader's workspace* — a workspace that has nothing to do with it.
|
|
41
|
+
|
|
42
|
+
So the file is not being shipped with a defect in it. It is a file that describes this
|
|
43
|
+
workspace and is being shipped as though the workspace were universal, which is the one
|
|
44
|
+
condition under which it makes no sense to any reader.
|
|
45
|
+
|
|
46
|
+
## Rulings
|
|
47
|
+
|
|
48
|
+
1. **The skill registry is workspace data.** It describes which skill bodies a particular
|
|
49
|
+
workspace contains, where they are, how big they are, and which stack tags separate the
|
|
50
|
+
variants. Every one of those is a fact about a workspace and none of them is a fact
|
|
51
|
+
about the tool.
|
|
52
|
+
2. **The package ships no registry.** `graph` leaves `files`. The file stays in this
|
|
53
|
+
checkout, because this repository still has to lint and resolve its own skills, and the
|
|
54
|
+
rule that a tool must not carry its author's portfolio is about what it *publishes*.
|
|
55
|
+
3. **A command reads the workspace's registry, and says so when there is none.** The
|
|
56
|
+
registry lives in a *member* of the workspace, not in the root: the root is the
|
|
57
|
+
directory that contains the member repositories, and it holds no `graph/` of its own.
|
|
58
|
+
When the reader names no path, resolution proceeds in tiers. First, the package root —
|
|
59
|
+
which is how this checkout finds its own registry; in an installed package `graph` is not
|
|
60
|
+
in the published set (ruling 2), so it never does. Next, a member of the resolved
|
|
61
|
+
workspace, when exactly one holds it: a tool installed from node_modules or a formula is
|
|
62
|
+
configured to find its workspace, and the member that holds the registry is usually the
|
|
63
|
+
tool's own checkout. More than one holding it means the reader must pick; none means the
|
|
64
|
+
command reports the workspace has no registry, naming that workspace. And where there is
|
|
65
|
+
no workspace at all: an honest absence — the command says there is no registry to read,
|
|
66
|
+
with a remedy that names the ways to point at one — which is the same shape
|
|
67
|
+
`loadRegistry` already uses for a missing file, and it does not become a new failure
|
|
68
|
+
mode. An explicitly named path always wins over all of this, and every tier is file
|
|
69
|
+
existence plus the configured workspace: there is no scoring, so the answer can be
|
|
70
|
+
explained to whoever asks.
|
|
71
|
+
|
|
72
|
+
What must not happen is the current silence: `skills list` in an adopting repository
|
|
73
|
+
answering with this estate's skills is a confident wrong answer, and a wrong answer from
|
|
74
|
+
a command whose only job is to list what is here. Nor may the remedy be a dead end: a
|
|
75
|
+
reader who follows the ways to point at a workspace must get the registry that workspace
|
|
76
|
+
holds, which is what the member tier exists for.
|
|
77
|
+
4. **The guard keeps widening, and the suite stays red until the file is gone.** The
|
|
78
|
+
coverage test asserts in both directions that what is guarded is what is published, so
|
|
79
|
+
removing `graph` from `files` removes it from `SHIPPED_ROOTS` too — the two are one
|
|
80
|
+
number. Pseudonymising the names was rejected because it would have left the paths, the
|
|
81
|
+
byte counts and the variant stacks describing this workspace while hiding only the part
|
|
82
|
+
a grep can see.
|
|
83
|
+
|
|
84
|
+
## Consequences
|
|
85
|
+
|
|
86
|
+
- `simt skills list`, `show`, `resolve`, `brief` and `graph lint` in an adopting repository
|
|
87
|
+
report no registry rather than a foreign one. This is a behaviour change for anyone who
|
|
88
|
+
ran those commands and got this estate's skills; it was wrong, and the commands now say
|
|
89
|
+
so instead.
|
|
90
|
+
- A copy of the tool installed from node_modules or a formula, pointed at a workspace that
|
|
91
|
+
holds a registry in one of its members, reads that member's registry and verifies it
|
|
92
|
+
against the workspace. This is how this estate keeps using its skills from an installed
|
|
93
|
+
copy; without it the remedy the absence message prints would be a dead end.
|
|
94
|
+
- Byte verification still works here, because the file is still in this checkout. Nothing
|
|
95
|
+
about this repository's own use of the tool changes.
|
|
96
|
+
- `test/shipped.test.mjs`'s `SHIPPED_ROOTS` loses `graph`, and the guard still covers
|
|
97
|
+
`bin`, `src`, `rules` and `docs/decisions`. The estate-name count in that file drops to
|
|
98
|
+
zero, which is the point.
|
|
99
|
+
- The 51 strings are no longer shipped and the suite stops being red on purpose.
|
|
100
|
+
|
|
101
|
+
## Rejected
|
|
102
|
+
|
|
103
|
+
- **Pseudonymise the eight names to `repo-NN`.** It hides the part a grep can see and keeps
|
|
104
|
+
the part that matters: hundreds of paths that resolve nowhere for the reader, byte counts
|
|
105
|
+
of bodies they do not have, and variant stacks drawn from stacks they did not write. It
|
|
106
|
+
would also have required a second registry mapping the pseudonyms back, and that registry
|
|
107
|
+
is estate data — so it cannot be published either.
|
|
108
|
+
- **Ship the registry with an empty `sources`.** `graph lint` refuses a skill with no
|
|
109
|
+
sources, so this would have meant shipping a registry that fails its own lint, or
|
|
110
|
+
shipping a second, weaker lint for adopters.
|
|
111
|
+
- **Keep it, because a reader can ignore it.** The corpus check is not about whether a
|
|
112
|
+
reader is misled; it is about whether the shipped artefact is actionable by someone with
|
|
113
|
+
only the receiving repository. This one is not, for every field it has.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ADR-062 — A fresh adoption seeds the canonical gates it can run, and never more
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed as S40
|
|
4
|
+
**Affects:** `simt init`, `src/projection/manifest.mjs` (the `ECOSYSTEMS` table, `defaultGates`),
|
|
5
|
+
`src/hooks/capability.mjs` (the tool probe, the `tools` field), `src/commands/init.mjs`,
|
|
6
|
+
eight new files in `rules/stacks/`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
S40's finding: the eight common ecosystems outside the JavaScript world — Rust, Go, .NET,
|
|
11
|
+
Swift, Deno, Ruby, Dart and Elixir — emit no stack tag. The fingerprint recognises them as
|
|
12
|
+
`has-*` facts, and the only reader of those facts was the Dependabot configuration. So a
|
|
13
|
+
fresh Rust repository received the core essay and a manifest that runs nothing: no profile
|
|
14
|
+
was selected because there was none to select, and no gate was proposed because no row keyed
|
|
15
|
+
on a fact. The asymmetry is the defect — the ecosystems the tool recognises were the ones it
|
|
16
|
+
served least, while the ecosystems that do emit stack tags got the full treatment.
|
|
17
|
+
|
|
18
|
+
Two existing mechanisms make the fix data-only, per ADR-013:
|
|
19
|
+
|
|
20
|
+
- **Profiles select on tags or facts.** `simt-applies` already accepts a fact, and the
|
|
21
|
+
genericity tests already assert that a profile may select on a fact no stack tag names.
|
|
22
|
+
- **Gates are planned by signal.** `planGates` takes the union of stack tags and facts, so a
|
|
23
|
+
row can key on either; the two never cross-match, because facts are `has-*` and tags are
|
|
24
|
+
not.
|
|
25
|
+
|
|
26
|
+
What was missing is one artefact per ecosystem: the profile file — and for gates, a row that
|
|
27
|
+
says what a fresh adoption of one should declare. The second is the harder half, because
|
|
28
|
+
declaring a gate is not free:
|
|
29
|
+
|
|
30
|
+
A declared gate that cannot run is worse than an absent one. Absent, an author sees nothing
|
|
31
|
+
and knows nothing is being checked; declared-and-unrunnable, the dispatcher fails closed on
|
|
32
|
+
every commit, and the only exit an author can find is `--no-verify`, which teaches the whole
|
|
33
|
+
team that the gate is theatre. So the question a seeding design has to answer is not "which
|
|
34
|
+
ecosystem is here" — the marker file answers that — but "can this machine run what the gate
|
|
35
|
+
says it runs". That is ADR-017's probe-before-declare, and it shapes every ruling below.
|
|
36
|
+
|
|
37
|
+
## Rulings
|
|
38
|
+
|
|
39
|
+
1. **Every ecosystem the fingerprint names gets a profile.** Eight new stack profiles, each
|
|
40
|
+
`simt-applies:` on its own fact (the files are named in the Affects line), so one marker
|
|
41
|
+
file reaches both the region the repository reads and the gates its commits run. Maven's profile (`jvm`) predates
|
|
42
|
+
this ruling and applies on the same fact, so the table has nine rows and eight new files.
|
|
43
|
+
An ecosystem arriving later is a profile file and a row: no engine change (ADR-013).
|
|
44
|
+
2. **A fresh adoption seeds the canonical commands, and only where the probe found the
|
|
45
|
+
tool.** On `init`, when the fingerprint emits an ecosystem fact, the row's commands land in
|
|
46
|
+
`gates.commands` for each gate it proposes — but only when the probe found `tool`. The
|
|
47
|
+
probe is the same discipline ADR-017 applies to node tools: a binary on PATH, or a file in
|
|
48
|
+
the repository when the row says `file` (the Maven wrapper, probed the way `gradlew`
|
|
49
|
+
already is). A marker says the ecosystem is here; the probe says its program can run. Both
|
|
50
|
+
must be true for a seed to land. `readCapability` reports the results in a `tools` field
|
|
51
|
+
next to `binaries`, so the plan can say which seeds fired and which did not.
|
|
52
|
+
3. **A seed is a default, and a default loses to every form of declaration.** A command the
|
|
53
|
+
repository already declared in `gates.commands` is never re-seeded. An npm runner keeps its
|
|
54
|
+
own scripts untouched: `package.json` is the repository's own answer to "how do I test and
|
|
55
|
+
format", and a seed that outranked a script would replace a choice with a default —
|
|
56
|
+
precisely in the mixed repository (a `package.json` and a `Cargo.toml`) where the script is
|
|
57
|
+
the deliberate answer. The escape hatch for a repository whose runner differs from the
|
|
58
|
+
canonical one (a Rails app, a Flutter module) is to declare its own command, and
|
|
59
|
+
declaration beats seed everywhere.
|
|
60
|
+
4. **Where the probe fails, the gate is dropped with a why and a remedy, never declared.**
|
|
61
|
+
This is the invariant `planGates` already holds for every proposed gate; seeding inherits
|
|
62
|
+
it rather than creating an exception to it. A fresh Rust checkout on a machine without
|
|
63
|
+
cargo gets `dropped gate test: …` with a remedy that names the install, not a gate that
|
|
64
|
+
blocks its first commit.
|
|
65
|
+
|
|
66
|
+
## Consequences
|
|
67
|
+
|
|
68
|
+
- `simt init` on a fresh Rust, Go, .NET, Swift, Deno, Ruby, Dart or Elixir repository declares
|
|
69
|
+
runnable gates when the toolchain is present — a `seeded` line in the plan output names each
|
|
70
|
+
command that landed — and drops them with a remedy when it is not.
|
|
71
|
+
- `readCapability` gains the probe and the `tools` field: a PATH lookup, plus an executable
|
|
72
|
+
check in the repository root for a `file` row.
|
|
73
|
+
- `defaultGates`'s expansion of an ecosystem row tolerates a missing phase key: the Maven row
|
|
74
|
+
proposes only pre-push, and absence is "none", not a defect.
|
|
75
|
+
- The seed picks one canonical command per gate (`bundle exec rspec`, `dart test`). A
|
|
76
|
+
repository whose runner differs declares its own; ruling 3 makes that declaration win.
|
|
77
|
+
- Acceptance: one test per ecosystem asserting marker → fact → profile selection; a seeding
|
|
78
|
+
matrix (every row seeds when the tool can run, every row drops when it cannot, an npm
|
|
79
|
+
repository takes no seed, a declaration beats a seed); and two end-to-end Rust inits, with
|
|
80
|
+
and without `cargo` on PATH.
|
|
81
|
+
|
|
82
|
+
## Rejected
|
|
83
|
+
|
|
84
|
+
- **Seed on the marker's word alone.** The marker is a file in the repository; the tool is a
|
|
85
|
+
program on the machine. A repository can have one without the other — a freshly cloned Rust
|
|
86
|
+
checkout on a machine that has never installed the toolchain is the common case — and
|
|
87
|
+
seeding there would declare a gate that fails closed on every commit. ADR-017 exists for
|
|
88
|
+
exactly this.
|
|
89
|
+
- **Seed into an npm repository's scripts.** The scripts are the repository's own answer, and
|
|
90
|
+
a seed that outranks them replaces a choice with a default. The mixed repository is where
|
|
91
|
+
this matters most, because there the script and the marker coexist on purpose.
|
|
92
|
+
- **A second gate table or a new engine path for ecosystems.** The fact was already flowing
|
|
93
|
+
into `planGates`, and the union of tags and facts never cross-matches. What was missing was
|
|
94
|
+
a row and a file, which is data; the engine that reads it already existed.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ADR-063 — The adoption escape hatch is opt-in, and the default writes in place
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed after a consumer asked why every adoption branched
|
|
4
|
+
**Affects:** `simt init`, `src/commands/init.mjs` (`establishEscapeHatch` and its call site),
|
|
5
|
+
`src/argv.mjs`, `src/commands/help.mjs`; amends plan 0002 §8.2 item 3
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Plan 0002 §8.2 item 3 ruled: "Create a branch and a `backup-before-simt-dev` tag before the
|
|
10
|
+
first write, always." The tool complied — `simt init --yes` in a git repository tagged the
|
|
11
|
+
pre-adoption commit, checked out a branch named after the tool, and wrote every adoption file
|
|
12
|
+
onto it. The reviewable-diff argument is sound: adoption writes a dozen files into a
|
|
13
|
+
repository the tool does not own, and doing it on a dedicated branch leaves the mainline
|
|
14
|
+
untouched with two named undo paths.
|
|
15
|
+
|
|
16
|
+
The "always" was the part that failed. Consumers kept reporting the branch switch as a
|
|
17
|
+
surprise: they ran the documented command to adopt their repository, and found themselves on
|
|
18
|
+
a branch they had not chosen, with a tag they had not asked for. Review 0001's S15 fixed the
|
|
19
|
+
silence — the output now names both operations and the command that undoes each — but a
|
|
20
|
+
well-announced surprise is still a surprise. The branch and the tag are an undo path for
|
|
21
|
+
readers who want one; making them unconditional turned a safety feature into a default that
|
|
22
|
+
every consumer has to explain away.
|
|
23
|
+
|
|
24
|
+
## Rulings
|
|
25
|
+
|
|
26
|
+
1. **The default adoption writes in place and creates no git object.** `simt init --yes`
|
|
27
|
+
without the new flag leaves the reader on the branch they were on, tags nothing, and
|
|
28
|
+
writes the adoption files as uncommitted changes. The reviewable diff survives: `git diff`
|
|
29
|
+
shows the whole adoption, and discarding it is `git checkout -- .` plus a clean. The run
|
|
30
|
+
says so in an `in place` line that names the flag which restores the undo path.
|
|
31
|
+
2. **The escape hatch moves behind a flag named for what it gives.** `--backup` is the
|
|
32
|
+
opt-in: tag the pre-adoption commit as the backup tag and branch onto the adopt branch
|
|
33
|
+
before the first write, exactly as the tool did before. The name is the promise — a way
|
|
34
|
+
back to what the repository was — and it already appears in the output the tool prints, so
|
|
35
|
+
the flag and its effect share one word.
|
|
36
|
+
3. **Whatever a run does to git state is reported, including doing nothing.** Plan 0002's
|
|
37
|
+
heading over the adoption rules is "Nothing is silent". With the hatch opt-in, the
|
|
38
|
+
in-place run is a git-state event too — the deliberate absence of one — and it gets a line
|
|
39
|
+
that says what was not done and how to get it done. The `--backup` run keeps the two lines
|
|
40
|
+
S15 established: what was tagged, and where the branch came from.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
- `simt init --yes` on a git repository no longer moves the reader or writes a tag; consumers
|
|
45
|
+
who scripted against the old default add `--backup`. This is the one behaviour the change
|
|
46
|
+
invalidates, and it is named here rather than discovered in a broken pipeline.
|
|
47
|
+
- The `in place` line appears on every real adoption that does not pass the flag, and the
|
|
48
|
+
help text for `init` and for the flag say what it means.
|
|
49
|
+
- The dry-run path is untouched: it already created no git object, and it still says `apply
|
|
50
|
+
with: simt init --yes`.
|
|
51
|
+
- Acceptance: the S15 test now runs with the flag and asserts both operations are reported;
|
|
52
|
+
a new test runs without it and asserts no tag, no branch, the reader stays put, and the
|
|
53
|
+
in-place line names the flag; the not-a-git-work-tree test runs with the flag, because that
|
|
54
|
+
is where the best-effort note lives now.
|
|
55
|
+
|
|
56
|
+
## Rejected
|
|
57
|
+
|
|
58
|
+
- **Keep "always" and add a flag to opt out.** An opt-out flag fixes the surprise for those
|
|
59
|
+
who read the help, and it still surprises everyone else — which is every consumer of a tool
|
|
60
|
+
whose default action is to touch someone's repository. The cost of the wrong default is
|
|
61
|
+
paid on every adoption; the cost of the right default is one flag on the few that want it.
|
|
62
|
+
- **Two flags, one per operation.** The tag and the branch arrive together in the code and in
|
|
63
|
+
the undo story: the tag is the reset target, the branch is where the adoption sits while it
|
|
64
|
+
is reviewed. Splitting them lets a reader build a state the tool has never tested — a branch
|
|
65
|
+
without a tag to reset to, or a tag with nothing isolated under it.
|
|
66
|
+
- **Move the decision into the dry-run preview.** The dry run shows what would change in the
|
|
67
|
+
files; git state is not a file, and a preview line for an operation that now does not happen
|
|
68
|
+
by default would describe the opt-in as if it were the plan. The `in place` line after
|
|
69
|
+
`--yes` is where the reader learns what the real run did not do.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# ADR-064 — The context meter counts what the tool wrote, and says what it did not
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed after review 0002 S38
|
|
4
|
+
|
|
5
|
+
**Affects:** the `tokens` command, the new context meter in `src/measure/context.mjs`, and
|
|
6
|
+
the structure field `readLimitFindings` now returns.
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Review 0002 S38 measured the estate by hand and filed a number: 90,042 bytes of always-on
|
|
11
|
+
context per turn across seven workspace roots, of which 64,738 bytes — 72% — were a
|
|
12
|
+
paragraph an earlier root had already loaded. It also found a repository whose region
|
|
13
|
+
ended past the 32,768 bytes Codex reads before it stops.
|
|
14
|
+
|
|
15
|
+
Two things are wrong with a finding that arrives as a hand count. The count ages: the
|
|
16
|
+
moment a rule is edited, the number is a description of a version nobody has any more.
|
|
17
|
+
And it cannot be re-derived by the reader who has to act on it — it was produced by
|
|
18
|
+
reading seven repositories, which is the work the tool exists to do.
|
|
19
|
+
|
|
20
|
+
Plan 0003 R06 asks for the same measurement as a command, with an acceptance criterion
|
|
21
|
+
that names the figures it must reproduce: the 90,042 bytes, the 64,738 redundant, and a
|
|
22
|
+
repository beyond Codex's limit. Reproducing a hand count is only possible if the meter
|
|
23
|
+
counts what the hand counted — which means settling what "always-on" means before
|
|
24
|
+
writing the code, because every choice here changes the number the criterion will be
|
|
25
|
+
checked against.
|
|
26
|
+
|
|
27
|
+
The answer is the hardest question in the command, and it is not "bytes".
|
|
28
|
+
|
|
29
|
+
## Rulings
|
|
30
|
+
|
|
31
|
+
1. **The meter counts what this tool wrote, and reports hand-written bytes beside it
|
|
32
|
+
rather than folding them in.** The budget being defended is the projection's: a
|
|
33
|
+
repository's own 40 KB of prose in AGENTS.md is an authoring decision the tool did not
|
|
34
|
+
make and cannot shrink. Billing it to a projection budget would make the budget
|
|
35
|
+
unachievable for reasons outside the projection, which is how a budget stops being
|
|
36
|
+
read. So every channel reports `generatedBytes` and `readerBytes` separately, and the
|
|
37
|
+
headline figure is the generated one.
|
|
38
|
+
|
|
39
|
+
2. **A token count is an approximation, and it is labelled as one every time it is
|
|
40
|
+
printed.** Four bytes per token, stated in the payload and in the text, with a `~`
|
|
41
|
+
in front of the number. Nobody has measured this corpus, and a figure that looks exact
|
|
42
|
+
invites the reader to spend it as though it were.
|
|
43
|
+
|
|
44
|
+
3. **A region is the span between its two markers, and nothing outside it counts as
|
|
45
|
+
generated — not even the blank line above the marker.** This is the defect the first
|
|
46
|
+
test run caught: subtracting the hand-written prefix from the file billed the
|
|
47
|
+
projection for a newline a person typed. In a command whose whole claim is that it
|
|
48
|
+
counts exactly, that byte is the whole argument.
|
|
49
|
+
|
|
50
|
+
4. **Redundancy is byte-exact, blank-line-paragraph by blank-line-paragraph, measured
|
|
51
|
+
across roots in sorted path order.** That is the method review 0002 used by hand, and
|
|
52
|
+
it is reproducible: the same corpus measured twice gives the same figure, which is
|
|
53
|
+
what lets the number appear in a commit message. Order is therefore load-bearing —
|
|
54
|
+
`discover` sorts, and the first root to carry a paragraph owns it. Reordering the walk
|
|
55
|
+
would move bytes between roots, so nothing downstream re-sorts them.
|
|
56
|
+
|
|
57
|
+
5. **The budget is reported, not enforced, and the command exits 0 for it.** A
|
|
58
|
+
repository over budget is a fact about the repository. A meter that went red for it
|
|
59
|
+
would train people to ignore the number it exists to print. Enforcement is R08's
|
|
60
|
+
business, and it is a manifest field rather than a constant so a repository can argue
|
|
61
|
+
with its own budget.
|
|
62
|
+
|
|
63
|
+
6. **A pointer is counted once, as the file it points at.** One reader, one copy of the
|
|
64
|
+
text; enumerating the target as well would double every rule the projection replaced
|
|
65
|
+
with a pointer, which is precisely the saving the pointer was made for.
|
|
66
|
+
|
|
67
|
+
7. **MCP `tools/list` bytes are reported once, outside every repository total.** The
|
|
68
|
+
same tool list is loaded whatever the repository, so summing it into each root would
|
|
69
|
+
multiply one number by the number of roots and call the result a workspace cost.
|
|
70
|
+
|
|
71
|
+
8. **The flag is `--repos`, not `--workspace`.** Plan 0003 R06 wrote `--workspace`, but
|
|
72
|
+
`--workspace <path>` has been a value flag since plan 0002, where it names a
|
|
73
|
+
directory of skill sources. One flag cannot mean two things depending on which
|
|
74
|
+
command read it — that is the silent-ignore defect the argument parser's own header
|
|
75
|
+
refuses — so the new one is `--repos`, and it says what it measures: every repository
|
|
76
|
+
under the root, in sorted order.
|
|
77
|
+
|
|
78
|
+
9. **No MCP tool exposes this yet.** The byte counts are already in the payload, but a
|
|
79
|
+
tool that returns a per-repository context cost is a second public surface to keep
|
|
80
|
+
honest. It arrives when there is a second caller for the logic to be wrong in.
|
|
81
|
+
|
|
82
|
+
## Consequences
|
|
83
|
+
|
|
84
|
+
- `simt tokens` answers "what does a turn pay for this repository", and
|
|
85
|
+
`simt tokens --repos` answers "what does a turn pay for the workspace". Both print
|
|
86
|
+
bytes and a labelled approximation; `--json` carries the same figures, because two
|
|
87
|
+
renderings of one question must not be able to disagree (ADR-050).
|
|
88
|
+
- The command refuses two shapes of path — a directory holding repositories when
|
|
89
|
+
`--repos` was not passed, and a directory holding none — with remedies that are
|
|
90
|
+
commands rather than locations.
|
|
91
|
+
- `readLimitFindings` grew structured fields (`reader`, `limitBytes`, `endByte`,
|
|
92
|
+
`overBy`) beside its prose, so the meter and the check that already reported the
|
|
93
|
+
Codex limit are reading the same numbers rather than each re-deriving them.
|
|
94
|
+
- S38 stays open. What landed is the instrument. The 30 KB figure it wants needs R07's
|
|
95
|
+
kernel, R08's budget field, R11's dedupe, and R01–R05's evaluation of whether moving
|
|
96
|
+
the process rules behind skills costs compliance — and this meter's job is to report
|
|
97
|
+
whether that work succeeded.
|
|
98
|
+
|
|
99
|
+
## Rejected
|
|
100
|
+
|
|
101
|
+
- **Failing the run when a repository is over budget.** The measurement would become
|
|
102
|
+
something people route around, and the number would go with it.
|
|
103
|
+
- **Treating a pointer as free rather than counted once.** Its target is still loaded;
|
|
104
|
+
calling that zero is the sort of accounting that makes a report unfalsifiable.
|
|
105
|
+
- **`--workspace` for the new flag, on the grounds that the plan says so.** The plan
|
|
106
|
+
wrote that before `--workspace` was a value flag for something else, and a plan is a
|
|
107
|
+
record of an intention, not a reason to ship two meanings for one name.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# ADR-065 — A pack is one shared rule set, vendored into the repository and pinned by digest
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed as S39
|
|
4
|
+
**Affects:** `src/packs.mjs`, `src/projection/rules.mjs` (`loadRules`, `select`), `src/projection/manifest.mjs` (`packs` field), `src/commands/sync.mjs` (`check`), `src/compare/row.mjs` (`packs`, `problems`), `test/packs.test.mjs`
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
S39 found estate product policy shipping as if it were universal: a profile that named
|
|
9
|
+
one company's domain layer, a rule file that named its Firebase environment variables, and
|
|
10
|
+
a `gender` sentinel in a map profile. Fourteen repositories shared one set of those rules
|
|
11
|
+
and each carried its own copy, so a policy change was fourteen edits that could be thirteen.
|
|
12
|
+
|
|
13
|
+
Two answers were available. The first is a remote: a pack published somewhere, a `source`
|
|
14
|
+
in the manifest, and the renderer fetching it. The second is a directory in the repository
|
|
15
|
+
holding the bytes, with a digest recorded so nobody has to trust that the two agree.
|
|
16
|
+
|
|
17
|
+
The remote is the one that reads better in a design document and is worse for the reader
|
|
18
|
+
this tool is for. A region rendered from a fetched pack is a region whose bytes depend on
|
|
19
|
+
something the receiving repository does not contain: a reader who clones the repo, deletes
|
|
20
|
+
their network, and runs the gate gets a different answer, and a reviewer approving a diff
|
|
21
|
+
is approving text they cannot produce from what is in front of them. That is the single
|
|
22
|
+
failure this repository exists to prevent, and a shared-rules feature is exactly where it
|
|
23
|
+
would arrive.
|
|
24
|
+
|
|
25
|
+
The vendored answer has one obvious weakness: updating a pack is a commit in every
|
|
26
|
+
repository that declares it, not one push somewhere. That is the cost of the property
|
|
27
|
+
above, and it is the right trade — a rule a reader cannot account for is worth less than a
|
|
28
|
+
rule that is one merge behind.
|
|
29
|
+
|
|
30
|
+
ADR-061 already settled half of P4. A pack may have carried the skill registry, and the
|
|
31
|
+
review's fix line says it should; ADR-061 ruled the registry to be workspace data that the
|
|
32
|
+
package no longer ships. A second source for it would reopen that ruling by accident, from
|
|
33
|
+
a feature that is not about skills at all.
|
|
34
|
+
|
|
35
|
+
## Rulings
|
|
36
|
+
|
|
37
|
+
1. **A pack's bytes live in the adopting repository, and the manifest records a digest of
|
|
38
|
+
them.** The manifest gains a `packs` list of `{name, source, version, digest}`; the
|
|
39
|
+
directory the tool reads is a fixed one, and `source` and `version` record where the
|
|
40
|
+
bytes came from for a human and for a future fetch. Nothing in a projection reads a
|
|
41
|
+
path outside the repository it is rendering into.
|
|
42
|
+
|
|
43
|
+
2. **Precedence is ranked, not special-cased: the repository's own profile, then a pack it
|
|
44
|
+
declares, then what the tool ships.** The ranking is one table read by the selector, so
|
|
45
|
+
the precedence and the order the files are collected in cannot disagree. A replacement
|
|
46
|
+
is reported in the existing `shadowed` list, because a profile that silently replaced
|
|
47
|
+
another leaves a reader unable to tell which of two texts they are being held to.
|
|
48
|
+
|
|
49
|
+
3. **A pack that is present but disagrees with its pin still projects.** What is
|
|
50
|
+
questioned is the pin, and the gate blocks and prints both digests. Dropping the pack
|
|
51
|
+
instead would let the remedy the finding names — `sync` — delete every rule the pack
|
|
52
|
+
carries, which is the opposite of what a reader who ran it meant. A pack that is absent
|
|
53
|
+
has nothing to project and blocks with a remedy that restores it; a pack with no digest
|
|
54
|
+
is reported and does not block, because its rules render correctly and only the record of
|
|
55
|
+
which bytes they were is missing.
|
|
56
|
+
|
|
57
|
+
4. **A pack carries rule files, not a skill registry.** A profile it ships is selected by
|
|
58
|
+
the same matcher as any other and appears in the region's source list under its pack's
|
|
59
|
+
directory. A registry inside a pack is read by nothing, and that is the ruling rather
|
|
60
|
+
than an oversight: ADR-061 makes the registry workspace data, and a second source for
|
|
61
|
+
it needs its own decision about which of two registries answers.
|
|
62
|
+
|
|
63
|
+
5. **`compare` reports every pack and the digest it is pinned to, per repository.** The
|
|
64
|
+
pinned digest, not the computed one — it answers "which bytes was this declared at", and
|
|
65
|
+
a row reporting the new digest would hide the mismatch from anything reading the JSON.
|
|
66
|
+
A pack that cannot be used is a problem on the row; one that is merely unpinned is a
|
|
67
|
+
note, which is the same division the gate draws.
|
|
68
|
+
|
|
69
|
+
6. **`packs` is repository-owned content, not a generated field.** Which packs a repository
|
|
70
|
+
uses is its decision, so a re-init must not empty it, and the drift check does not
|
|
71
|
+
compare it. What is compared is the directory's digest against the pin.
|
|
72
|
+
|
|
73
|
+
## Consequences
|
|
74
|
+
|
|
75
|
+
- A group of repositories sharing one rule set declares the same pack and vendors the same
|
|
76
|
+
bytes; a policy change is a commit per repository, and the digest is what makes an
|
|
77
|
+
unmerged one visible before a reader follows stale instructions.
|
|
78
|
+
- Nothing fetches anything. A pack cannot be pulled by name, cannot be published by this
|
|
79
|
+
tool, and cannot be refreshed by `upgrade`. That is the deliberate half of the answer,
|
|
80
|
+
and it leaves the fetch as a later ruling rather than a missing feature.
|
|
81
|
+
- The rule provenance digest does not cover pack bytes, so `projection.simtDevSha` still
|
|
82
|
+
answers "what did the tool render" and the pack digest answers "which bytes were these".
|
|
83
|
+
Two answers to two questions, both in the manifest.
|
|
84
|
+
|
|
85
|
+
## Rejected
|
|
86
|
+
|
|
87
|
+
- **Fetching a pack from a remote during projection.** It makes the rendered bytes depend
|
|
88
|
+
on something the receiving repository does not carry, and it puts the network inside a
|
|
89
|
+
command whose defining property is that it is deterministic and offline.
|
|
90
|
+
- **Refusing to project a pack whose digest does not match.** The remedy would delete the
|
|
91
|
+
rules it reports, so the tool would cause the damage it exists to report.
|
|
92
|
+
- **One flag per pack state**, so a reader could choose what blocks the gate. Three flags
|
|
93
|
+
for one directory's worth of state is a configuration surface nobody will read twice, and
|
|
94
|
+
the division that matters — does the region account for its rules — has one defensible
|
|
95
|
+
answer.
|
|
96
|
+
- **A pack carrying the skill registry**, which proposal P4 asked for before ADR-061
|
|
97
|
+
settled where the registry comes from.
|