simt-dev 0.7.1 → 0.8.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.
- package/CHANGELOG.md +36 -1
- package/README.md +169 -80
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +3 -1
- package/rules/mdc/intake-questions.mdc +8 -0
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +7 -5
- package/src/argv.mjs +19 -0
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +155 -113
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +12 -26
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +17 -535
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +15 -1037
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +116 -971
- package/src/bench/run.mjs +164 -881
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +11 -13
- package/src/ci/dependabot.mjs +25 -55
- package/src/commands/agent.mjs +1 -9
- package/src/commands/agenthook.mjs +66 -35
- package/src/commands/bench.mjs +2 -2
- package/src/commands/ci.mjs +11 -13
- package/src/commands/docs.mjs +88 -0
- package/src/commands/gate.mjs +10 -13
- package/src/commands/harness.mjs +3 -3
- package/src/commands/help.mjs +44 -3
- package/src/commands/hooks.mjs +20 -20
- package/src/commands/ignore.mjs +11 -7
- package/src/commands/impact.mjs +4 -8
- package/src/commands/index.mjs +49 -9
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +122 -104
- package/src/commands/lease.mjs +67 -18
- package/src/commands/map.mjs +10 -5
- package/src/commands/mcp.mjs +2 -17
- package/src/commands/repoindex.mjs +8 -8
- package/src/commands/rules.mjs +8 -4
- package/src/commands/shared.mjs +12 -16
- package/src/commands/skills.mjs +5 -6
- package/src/commands/skillscompile.mjs +3 -3
- package/src/commands/skillsinstall.mjs +5 -4
- package/src/commands/slice.mjs +199 -64
- package/src/commands/spec.mjs +78 -62
- package/src/commands/status.mjs +24 -12
- package/src/commands/sync.mjs +14 -14
- package/src/commands/tests.mjs +12 -12
- package/src/commands/tokens.mjs +19 -24
- package/src/commands/upgrade.mjs +8 -4
- package/src/commands/why.mjs +6 -13
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/decisions/index.mjs +3 -2
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +2 -1
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +5 -3
- package/src/hooks/dispatcher.mjs +26 -32
- package/src/hooks/gates.mjs +12 -2
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +2 -11
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +16 -11
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +124 -17
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +49 -9
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -1005
- package/src/{bench → measure}/corpus.mjs +1 -1
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/output/index.mjs +122 -31
- package/src/packs/manage.mjs +15 -9
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/manifest.mjs +10 -435
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +16 -547
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +13 -1
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +0 -7
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +8 -12
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +1 -1
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +3 -1
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/src/bench/tasks.mjs +0 -36
|
@@ -27,6 +27,10 @@ import { getGate } from '../hooks/gates.mjs';
|
|
|
27
27
|
// Q31: the one Node runner table. This module and the card, capability,
|
|
28
28
|
// dispatcher and CI workflow all spell `<manager> run <script>` from it.
|
|
29
29
|
import { runScript } from '../ecosystem/node-runner.mjs';
|
|
30
|
+
// Q70: the ecosystem records. A new ecosystem is a new file under `ecosystems/`, not a
|
|
31
|
+
// row added to this table. records.mjs imports nothing from this package, so this edge
|
|
32
|
+
// cannot cycle.
|
|
33
|
+
import { ecosystemGateRows } from '../ecosystem/records.mjs';
|
|
30
34
|
|
|
31
35
|
// Imported as well as re-exported: a bare `export { X as Y } from` binds nothing here,
|
|
32
36
|
// and the manifest name is used a dozen times below in remedy strings that name this
|
|
@@ -35,6 +39,8 @@ import { MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
|
35
39
|
import { GENERATED_KEYS_OF, KEYS, loadManifest, manifestPath } from '../manifest/schema.mjs';
|
|
36
40
|
import { CLI } from '../name.mjs';
|
|
37
41
|
import { packageVersion } from '../package-version.mjs';
|
|
42
|
+
import { defaultGates, planGates } from '../gates/index.mjs';
|
|
43
|
+
export { ECOSYSTEMS, defaultGates, gateRunnable, planGates, unrunnableGates } from '../gates/index.mjs';
|
|
38
44
|
export const MANIFEST = MANIFEST_FILE;
|
|
39
45
|
|
|
40
46
|
// Still exported from here because this is where the manifest lived: every caller that
|
|
@@ -43,8 +49,6 @@ export { manifestPath };
|
|
|
43
49
|
|
|
44
50
|
export { MANIFEST_LEGACY };
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
52
|
/**
|
|
49
53
|
* The one definition of the file's own comment, exported because `upgrade` rewrites
|
|
50
54
|
* the manifest and must not carry a stale copy forward. Two literals for one string
|
|
@@ -61,7 +65,7 @@ export const MANIFEST_COMMENT = {
|
|
|
61
65
|
`Written and maintained by simt-dev. \`${CLI} sync --yes\` regenerates the projected rules.`,
|
|
62
66
|
`\`${CLI} init\` refreshes the SHA and keeps every key it does not write, so a value you add here survives a re-init.`,
|
|
63
67
|
`\`plans\` is such a key: it selects the tracker template and the directories \`${CLI} plan\` reads.`,
|
|
64
|
-
'Omit it and the tracker template applies, which reads docs/plans and docs/reviews.',
|
|
68
|
+
'Omit it and the tracker template applies, which reads docs/plans, docs/research and docs/reviews.',
|
|
65
69
|
].join(' '),
|
|
66
70
|
};
|
|
67
71
|
|
|
@@ -69,362 +73,14 @@ export const MANIFEST_COMMENT = {
|
|
|
69
73
|
* Gate names the shared dispatcher knows how to run (step 8). Choosing these from
|
|
70
74
|
* the detected stack rather than asking a human keeps `init` non-interactive.
|
|
71
75
|
*/
|
|
72
|
-
const GATES_BY_STACK = [
|
|
73
|
-
{
|
|
74
|
-
when: ['gradle', 'kotlin', 'connectiq'],
|
|
75
|
-
preCommit: ['format', 'lint', 'i18n-completeness'],
|
|
76
|
-
prePush: ['test'],
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
when: ['python'],
|
|
80
|
-
preCommit: ['format', 'lint', 'i18n-completeness'],
|
|
81
|
-
prePush: ['test', 'coverage'],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
when: ['node', 'node-web', 'cloudflare-workers', 'typescript'],
|
|
85
|
-
preCommit: ['format', 'lint'],
|
|
86
|
-
prePush: ['test', 'typecheck'],
|
|
87
|
-
},
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/** Extra gate implied by a specific tag, applied after the table above. */
|
|
91
|
-
const TAG_GATES = [
|
|
92
|
-
{ tag: 'connectiq', preCommit: ['memory', 'codegen-drift'] },
|
|
93
|
-
{ tag: 'hono', preCommit: ['i18n-completeness'] },
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The ecosystems that produce a fact rather than a stack tag, and what a fresh
|
|
98
|
-
* adoption of one should declare.
|
|
99
|
-
*
|
|
100
|
-
* A row proposes gates the way `GATES_BY_STACK` does, and the fact it keys on is
|
|
101
|
-
* the same signal a profile's `simt-applies` names, so one marker file reaches
|
|
102
|
-
* both: the region this repo reads and the gates its commits run.
|
|
103
|
-
*
|
|
104
|
-
* `commands` is what a FRESH manifest declares for each proposed gate, and it is
|
|
105
|
-
* written only when the probe in `readCapability` found `tool` where this row says
|
|
106
|
-
* it lives. A declared gate that cannot run blocks every commit, so a command is
|
|
107
|
-
* never declared on the faith of a marker file: the marker says the ecosystem is
|
|
108
|
-
* here, the probe says its program can run (ADR-017). A command the repo already
|
|
109
|
-
* declared wins over the seed, because a seed is a default and a declaration is a
|
|
110
|
-
* choice.
|
|
111
|
-
*
|
|
112
|
-
* `file` marks a tool that is a file in the repository rather than a binary on
|
|
113
|
-
* PATH: the Maven wrapper, probed the way this tool already probes `gradlew`.
|
|
114
|
-
*/
|
|
115
|
-
export const ECOSYSTEMS = [
|
|
116
|
-
{
|
|
117
|
-
fact: 'has-cargo-toml',
|
|
118
|
-
tool: 'cargo',
|
|
119
|
-
preCommit: ['format', 'lint'],
|
|
120
|
-
prePush: ['test'],
|
|
121
|
-
commands: { format: 'cargo fmt --check', lint: 'cargo clippy -- -D warnings', test: 'cargo test' },
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
fact: 'has-go-mod',
|
|
125
|
-
tool: 'go',
|
|
126
|
-
preCommit: ['format', 'lint'],
|
|
127
|
-
prePush: ['test'],
|
|
128
|
-
commands: { format: 'test -z "$(gofmt -l .)"', lint: 'go vet ./...', test: 'go test ./...' },
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
fact: 'has-nuget',
|
|
132
|
-
tool: 'dotnet',
|
|
133
|
-
preCommit: ['format'],
|
|
134
|
-
prePush: ['test', 'typecheck'],
|
|
135
|
-
commands: { format: 'dotnet format --verify-no-changes', test: 'dotnet test', typecheck: 'dotnet build -warnaserror' },
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
fact: 'has-swift-package',
|
|
139
|
-
tool: 'swift',
|
|
140
|
-
preCommit: ['format'],
|
|
141
|
-
prePush: ['test', 'typecheck'],
|
|
142
|
-
commands: { format: 'swift format lint -r .', test: 'swift test', typecheck: 'swift build' },
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
fact: 'has-deno-config',
|
|
146
|
-
tool: 'deno',
|
|
147
|
-
preCommit: ['format', 'lint'],
|
|
148
|
-
prePush: ['test', 'typecheck'],
|
|
149
|
-
commands: { format: 'deno fmt --check', lint: 'deno lint', test: 'deno test', typecheck: 'deno check .' },
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
fact: 'has-gemfile',
|
|
153
|
-
tool: 'bundle',
|
|
154
|
-
preCommit: ['format'],
|
|
155
|
-
prePush: ['test'],
|
|
156
|
-
commands: { format: 'bundle exec rubocop', test: 'bundle exec rspec' },
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
fact: 'has-pubspec',
|
|
160
|
-
tool: 'dart',
|
|
161
|
-
preCommit: ['format', 'lint'],
|
|
162
|
-
prePush: ['test'],
|
|
163
|
-
commands: { format: 'dart format --output=none --set-exit-if-changed .', lint: 'dart analyze', test: 'dart test' },
|
|
164
|
-
},
|
|
165
|
-
{
|
|
166
|
-
fact: 'has-mix',
|
|
167
|
-
tool: 'mix',
|
|
168
|
-
preCommit: ['format'],
|
|
169
|
-
prePush: ['test'],
|
|
170
|
-
commands: { format: 'mix format --check-formatted', test: 'mix test' },
|
|
171
|
-
},
|
|
172
|
-
// The wrapper is a file the repository commits, not a program on PATH. A repo with
|
|
173
|
-
// a pom.xml and no wrapper cannot run `./mvnw -q verify`, so the probe is the file.
|
|
174
|
-
{
|
|
175
|
-
fact: 'has-maven-pom',
|
|
176
|
-
tool: 'mvnw',
|
|
177
|
-
file: true,
|
|
178
|
-
prePush: ['test'],
|
|
179
|
-
commands: { test: './mvnw -q verify' },
|
|
180
|
-
},
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Can this repo actually run the gate?
|
|
185
|
-
*
|
|
186
|
-
* A declared gate that cannot run is worse than an absent one. Absent, an author sees
|
|
187
|
-
* nothing and knows nothing is being checked; declared-and-unrunnable, the dispatcher
|
|
188
|
-
* fails closed on every commit and the only exit an author can find is `--no-verify`,
|
|
189
|
-
* which teaches the whole team that the gate is theatre. The dispatcher is right to
|
|
190
|
-
* fail closed — the defect is declaring a gate this repo has no way to run.
|
|
191
|
-
*
|
|
192
|
-
* The check mirrors what the dispatcher actually attempts:
|
|
193
|
-
* - `script` gates run `<manager> run <script>`, so the script must exist
|
|
194
|
-
* - `staged` gates look for each named tool in `node_modules/.bin`, so it must be
|
|
195
|
-
* installed, not merely declared as a dependency
|
|
196
|
-
* - `command` gates run whatever the manifest's `gates.commands` declares, so a
|
|
197
|
-
* command gate with no declared command is unrunnable by definition
|
|
198
|
-
*
|
|
199
|
-
* @param {object} gate an entry from GATES in src/hooks/gates.mjs
|
|
200
|
-
* @param {{scripts?: Record<string,string>, binaries?: string[], commands?: Record<string,string>}} capability
|
|
201
|
-
*/
|
|
202
|
-
export function gateRunnable(gate, capability = {}) {
|
|
203
|
-
if (!gate) return { runnable: false, why: 'unknown gate' };
|
|
204
|
-
// Only a KNOWN non-npm runner changes the wording. A capability that does not report
|
|
205
|
-
// its runner (a hand-built one, a caller from an older version) is treated as npm,
|
|
206
|
-
// because claiming a repo has no package.json is a much worse guess than assuming it.
|
|
207
|
-
const foreign = isForeignRunner(capability.runner);
|
|
208
|
-
|
|
209
|
-
// A declared command satisfies the gate whatever its kind, because that is the
|
|
210
|
-
// dispatcher's own precedence and the two must never disagree about one manifest.
|
|
211
|
-
const declared = capability.commands?.[gate.name];
|
|
212
|
-
if (declared) {
|
|
213
|
-
return declared.trim() === ''
|
|
214
|
-
? { runnable: false, why: `gates.commands["${gate.name}"] is empty` }
|
|
215
|
-
: { runnable: true, why: `runs: ${declared}` };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (gate.kind === 'builtin') {
|
|
219
|
-
// The one kind with no external requirement: the implementation ships inside
|
|
220
|
-
// the binary that is reading this manifest (ADR-100). Saying otherwise would
|
|
221
|
-
// make `check` report a gate as unrunnable that runs on every commit.
|
|
222
|
-
return { runnable: true, why: 'runs inside the tool itself; nothing to install' };
|
|
223
|
-
}
|
|
224
|
-
if (gate.kind === 'command') {
|
|
225
|
-
return { runnable: false, why: `no command is declared for it in gates.commands` };
|
|
226
|
-
}
|
|
227
|
-
if (gate.kind === 'script') {
|
|
228
|
-
const script = gate.script ?? gate.name;
|
|
229
|
-
// Q31: the runnable sentence names the repo's own runner, never a bare npm
|
|
230
|
-
// assumption — the card, the dispatcher and this check read one resolver.
|
|
231
|
-
const spell = runScript(capability.runner ?? 'npm', script);
|
|
232
|
-
if (capability.scripts?.[script]) return { runnable: true, why: `runs: ${spell}` };
|
|
233
|
-
// Saying "no script in package.json" about a repo with no package.json states a
|
|
234
|
-
// fact about a file that does not exist and implies a fix that cannot be taken.
|
|
235
|
-
return {
|
|
236
|
-
runnable: false,
|
|
237
|
-
why: foreign
|
|
238
|
-
? `this repo runs ${runnerName(capability.runner)}, not npm, so "${spell}" cannot run here`
|
|
239
|
-
: `no "${script}" script in package.json`,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
// staged: a formatter must actually be installed
|
|
243
|
-
for (const tool of gate.tools ?? []) {
|
|
244
|
-
if (!(capability.binaries ?? []).includes(tool)) {
|
|
245
|
-
return {
|
|
246
|
-
runnable: false,
|
|
247
|
-
why: foreign
|
|
248
|
-
? `${tool} is not installed, and this repo has no node_modules/.bin to install it into`
|
|
249
|
-
: `${tool} is not installed in node_modules/.bin`,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return { runnable: true, why: `formats staged files with ${(gate.tools ?? []).join(', ')}` };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* What the reader has to do to make a dropped gate runnable.
|
|
258
|
-
*
|
|
259
|
-
* A drop is a decision the tool made on the repo's behalf, so it owes the reader the
|
|
260
|
-
* undo. Every remedy is one of three real choices — install the tool, add the script,
|
|
261
|
-
* or name the command — because "this repo cannot run format" with no next step reads
|
|
262
|
-
* as simt-dev deciding formatting does not matter here.
|
|
263
|
-
*
|
|
264
|
-
* The two npm-shaped remedies only apply where npm is the runner. In a Gradle, Python or
|
|
265
|
-
* Rust repo the honest undo is the `command` gate: the repo names its own program, which
|
|
266
|
-
* is the same escape hatch ConnectIQ memory budgets already use.
|
|
267
|
-
*/
|
|
268
|
-
function gateRemedy(gate, { existing = false, runner = null } = {}) {
|
|
269
|
-
const name = gate?.name ?? 'the gate';
|
|
270
|
-
const npmShaped = gate?.kind === 'staged' || gate?.kind === 'script';
|
|
271
|
-
const foreign = isForeignRunner(runner);
|
|
272
|
-
// Q31: a missing tool in a Node repo names the repo's own installer —
|
|
273
|
-
// `pnpm add -D`, not `npm i -D` — and anything elsewhere keeps the honest
|
|
274
|
-
// undo: name the program under `gates.commands`.
|
|
275
|
-
const addDev = { npm: 'npm i -D', pnpm: 'pnpm add -D', yarn: 'yarn add -D', bun: 'bun add -d' }[runner] ?? null;
|
|
276
|
-
const fix =
|
|
277
|
-
npmShaped && foreign
|
|
278
|
-
? `this repo runs ${runnerName(runner)}, not npm; name the program under "gates.commands" in ${MANIFEST_FILE} instead of the npm equivalent`
|
|
279
|
-
: gate?.kind === 'staged'
|
|
280
|
-
? addDev
|
|
281
|
-
? `install ${gate.tools.join(', ')} as a dev dependency: ${addDev} ${gate.tools.join(' ')} (the gate runs from the pinned dependency)`
|
|
282
|
-
: `install ${gate.tools.join(', ')} as a dev dependency`
|
|
283
|
-
: gate?.kind === 'script'
|
|
284
|
-
? `add a "${gate.script ?? name}" script to package.json`
|
|
285
|
-
: gate?.kind === 'command'
|
|
286
|
-
? `declare the program under "gates.commands" in ${MANIFEST_FILE}`
|
|
287
|
-
: `remove "${name}" from the gates list in ${MANIFEST_FILE}`;
|
|
288
|
-
// A re-init never re-adds a gate to a manifest that already has one, so "install the
|
|
289
|
-
// tool and re-run init" would send the reader round the loop and back to where they
|
|
290
|
-
// started. On an existing manifest the declaration is the second half of the fix.
|
|
291
|
-
const second =
|
|
292
|
-
npmShaped && foreign
|
|
293
|
-
? `, or add "${name}" to gates.preCommit or gates.prePush in ${MANIFEST_FILE}`
|
|
294
|
-
: existing
|
|
295
|
-
? `, then add "${name}" to gates.preCommit or gates.prePush in ${MANIFEST_FILE}`
|
|
296
|
-
: `, then re-run: ${CLI} init --yes`;
|
|
297
|
-
return `${fix}${second}`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Is this runner one where `npm run` is not how a gate is spelled?
|
|
302
|
-
*
|
|
303
|
-
* `undefined` means "the caller did not report a runner", and every Node manager
|
|
304
|
-
* (`npm`, `pnpm`, `yarn`, `bun`) spells gates as `<manager> run <script>` with
|
|
305
|
-
* scripts living in the same `package.json`. Only a positively identified
|
|
306
|
-
* non-Node runner changes the wording. (Q31: pnpm is Node-shaped, not foreign.)
|
|
307
|
-
*/
|
|
308
|
-
function isForeignRunner(runner) {
|
|
309
|
-
if (runner == null) return false;
|
|
310
|
-
return !['npm', 'pnpm', 'yarn', 'bun'].includes(runner);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** How a reader would name the runner in a sentence. */
|
|
314
|
-
function runnerName(runner) {
|
|
315
|
-
return (
|
|
316
|
-
{
|
|
317
|
-
none: 'no recognised runner',
|
|
318
|
-
gradle: 'Gradle',
|
|
319
|
-
python: 'Python',
|
|
320
|
-
npm: 'npm',
|
|
321
|
-
pnpm: 'pnpm',
|
|
322
|
-
yarn: 'yarn',
|
|
323
|
-
bun: 'bun',
|
|
324
|
-
cargo: 'Cargo',
|
|
325
|
-
go: 'Go',
|
|
326
|
-
}[runner] ?? runner
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The stack table, filtered by what this repo can actually run.
|
|
332
|
-
*
|
|
333
|
-
* A declared gate that cannot run is worse than an absent one. Absent, an author sees
|
|
334
|
-
* nothing and knows nothing is being checked; declared-and-unrunnable, the dispatcher
|
|
335
|
-
* fails closed on every commit and the only exit an author can find is `--no-verify`,
|
|
336
|
-
* which teaches the whole team that the gate is theatre. The dispatcher is right to
|
|
337
|
-
* fail closed — the defect is declaring a gate this repo has no way to run.
|
|
338
|
-
*
|
|
339
|
-
* The filter mirrors what the dispatcher actually attempts:
|
|
340
|
-
* - `script` gates run `npm run <script>`, so the script must exist
|
|
341
|
-
* - `staged` gates look for each named tool in `node_modules/.bin`, so it must be
|
|
342
|
-
* installed, not merely declared as a dependency
|
|
343
|
-
* - `command` gates run whatever `gates.commands` declares, so a command gate with
|
|
344
|
-
* no declared command is unrunnable by definition
|
|
345
|
-
*
|
|
346
|
-
* The seeds are returned alongside the plan because a fresh adoption writes them
|
|
347
|
-
* into `gates.commands` and the reader is owed the line that says so: a command
|
|
348
|
-
* that lands in the manifest without appearing in the plan is a silent write.
|
|
349
|
-
*
|
|
350
|
-
* @returns {{gates: {preCommit: string[], prePush: string[]}, dropped: {gate: string, why: string}[], commands: Record<string,string>}}
|
|
351
|
-
*/
|
|
352
|
-
export function planGates(stack = [], capability = null, facts = []) {
|
|
353
|
-
const proposed = defaultGates(stack, facts);
|
|
354
|
-
if (!capability) return { gates: proposed, dropped: [], commands: {} };
|
|
355
|
-
const seeds = ecosystemCommands(facts, capability);
|
|
356
|
-
// A command the repo already declared wins over a seed: the seed is a default,
|
|
357
|
-
// the declaration is a choice, and a default overwriting a choice is a re-init
|
|
358
|
-
// that edits the repo's mind.
|
|
359
|
-
const effective = { ...capability, commands: { ...seeds, ...capability.commands } };
|
|
360
|
-
const dropped = [];
|
|
361
|
-
const runnable = (list) =>
|
|
362
|
-
list.filter((name) => {
|
|
363
|
-
const gate = getGate(name);
|
|
364
|
-
const verdict = gateRunnable(gate, effective);
|
|
365
|
-
if (!verdict.runnable) {
|
|
366
|
-
dropped.push({ gate: name, why: verdict.why, remedy: gateRemedy(gate, { runner: capability.runner }) });
|
|
367
|
-
}
|
|
368
|
-
return verdict.runnable;
|
|
369
|
-
});
|
|
370
|
-
return {
|
|
371
|
-
gates: { preCommit: runnable(proposed.preCommit), prePush: runnable(proposed.prePush) },
|
|
372
|
-
dropped,
|
|
373
|
-
commands: seeds,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* The canonical `gates.commands` a fresh manifest of this repository should carry.
|
|
379
|
-
*
|
|
380
|
-
* Two probes, and both must pass before a command is declared: the fact says the
|
|
381
|
-
* ecosystem is in the repository, and `capability.tools` says its program can run
|
|
382
|
-
* on this machine. A marker file without a toolchain would otherwise declare a
|
|
383
|
-
* gate that fails closed on every commit — the defect ADR-017 exists to prevent,
|
|
384
|
-
* reached from the ecosystem table rather than the stack table.
|
|
385
|
-
*
|
|
386
|
-
* Pure, so `planGates` and `buildManifest` answer with one implementation.
|
|
387
|
-
*/
|
|
388
|
-
function ecosystemCommands(facts = [], capability = null) {
|
|
389
|
-
if (!capability?.tools?.length) return {};
|
|
390
|
-
// A Node runner (npm/pnpm/yarn/bun) already answers "how do I test and format"
|
|
391
|
-
// with its own package.json scripts, and a seed only wins over those scripts when
|
|
392
|
-
// the repo has not declared a command of its own — which is exactly the mixed
|
|
393
|
-
// repos (package.json AND Cargo.toml) where the script is the deliberate answer.
|
|
394
|
-
// Seeding there would replace a choice with a default; the reader's escape hatch
|
|
395
|
-
// is declaring gates.commands by hand, which a seed would lose to anyway. Pure
|
|
396
|
-
// ecosystem repos have runner 'cargo'/'go'/... and seed normally.
|
|
397
|
-
// Q31: every Node manager skips seeding — pnpm is not "foreign", it is Node.
|
|
398
|
-
if (['npm', 'pnpm', 'yarn', 'bun'].includes(capability.runner)) return {};
|
|
399
|
-
const present = new Set(facts);
|
|
400
|
-
const commands = {};
|
|
401
|
-
for (const eco of ECOSYSTEMS) {
|
|
402
|
-
if (!present.has(eco.fact)) continue;
|
|
403
|
-
if (!capability.tools.includes(eco.tool)) continue;
|
|
404
|
-
for (const [gate, command] of Object.entries(eco.commands)) {
|
|
405
|
-
if (!capability.commands?.[gate]) commands[gate] = command;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return commands;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Build a manifest from a fingerprint. Pure, so the whole default surface is
|
|
413
|
-
* testable without a filesystem.
|
|
414
|
-
*
|
|
415
|
-
* When `capability` is supplied, newly proposed gates the repo cannot run are dropped.
|
|
416
|
-
* Gates the repo ALREADY declared are kept regardless: that is the repo's choice to
|
|
417
|
-
* make, and quietly disarming a gate somebody declared on purpose is a worse surprise
|
|
418
|
-
* than a visible one.
|
|
419
|
-
*/
|
|
420
76
|
export function buildManifest({ root, stack = [], sha, version, existing = null, capability = null, surfaces = undefined, facts = [] }) {
|
|
421
|
-
const gates = defaultGates(stack, facts);
|
|
77
|
+
const gates = defaultGates(stack, facts, root);
|
|
422
78
|
const limits = defaultLimits();
|
|
423
79
|
|
|
424
80
|
// A re-init must never silently weaken a repo's own declaration: existing
|
|
425
81
|
// limits and gates win over defaults, and only the projection stamp is refreshed.
|
|
426
82
|
const limitsFinal = existing?.limits ? { ...limits, ...existing.limits } : limits;
|
|
427
|
-
const planned = planGates(stack, capability, facts);
|
|
83
|
+
const planned = planGates(stack, capability, facts, root);
|
|
428
84
|
// The seeds land only on a FRESH manifest. On an existing one the repo's
|
|
429
85
|
// declaration is the answer, and adding commands under it would be a re-init
|
|
430
86
|
// writing into a field the reader owns — the same rule as the gates above.
|
|
@@ -530,91 +186,11 @@ function withRepoKeys(existing, owned) {
|
|
|
530
186
|
*
|
|
531
187
|
* @returns {{file: string, status: string, gate: string, detail: string, remedy: string}[]}
|
|
532
188
|
*/
|
|
533
|
-
export function unrunnableGates(manifest, capability) {
|
|
534
|
-
const declared = [
|
|
535
|
-
...(manifest?.gates?.preCommit ?? []),
|
|
536
|
-
...(manifest?.gates?.prePush ?? []),
|
|
537
|
-
];
|
|
538
|
-
const findings = [];
|
|
539
|
-
const seen = new Set();
|
|
540
|
-
// `gates.cwd` names a directory a gate runs in, and a name that is not a declared
|
|
541
|
-
// gate is as much a landmine as one that cannot run: the entry is inert, the reader
|
|
542
|
-
// believes the gate runs in the package it names, and nothing anywhere says so. Same
|
|
543
|
-
// shape as the unknown-gate finding in the dispatcher, so both are repaired the same
|
|
544
|
-
// way.
|
|
545
|
-
const cwds = manifest?.gates?.cwd;
|
|
546
|
-
if (cwds && typeof cwds === 'object' && !Array.isArray(cwds)) {
|
|
547
|
-
for (const name of Object.keys(cwds).sort()) {
|
|
548
|
-
if (declared.includes(name)) continue;
|
|
549
|
-
findings.push({
|
|
550
|
-
file: MANIFEST,
|
|
551
|
-
status: 'unknown-gate-cwd',
|
|
552
|
-
gate: name,
|
|
553
|
-
detail: `gates.cwd names "${name}", which is not declared in gates.preCommit or gates.prePush, so it is never read`,
|
|
554
|
-
remedy: `declare "${name}" in gates.preCommit or gates.prePush, or remove gates.cwd["${name}"]`,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
for (const name of declared) {
|
|
559
|
-
if (seen.has(name)) continue;
|
|
560
|
-
seen.add(name);
|
|
561
|
-
const gate = getGate(name);
|
|
562
|
-
const verdict = gateRunnable(gate, capability);
|
|
563
|
-
if (verdict.runnable) continue;
|
|
564
|
-
findings.push({
|
|
565
|
-
file: MANIFEST,
|
|
566
|
-
status: 'unrunnable',
|
|
567
|
-
gate: name,
|
|
568
|
-
detail: `declares gate "${name}", which this repo cannot run: ${verdict.why}`,
|
|
569
|
-
remedy: gateRemedy(gate, { existing: true, runner: capability.runner }),
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
return findings;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/** ADR-005: 400 is a review trigger, 1000 a hard ceiling. Generated files exempt. */
|
|
576
189
|
function defaultLimits() {
|
|
577
190
|
return { coverage: null, fileLines: { warn: 400, error: 1000 } };
|
|
578
191
|
}
|
|
579
192
|
|
|
580
193
|
/** ADR-006: coverage stays null. Set it only if the repo has a real numeric gate. */
|
|
581
|
-
function defaultGates(stack, facts = []) {
|
|
582
|
-
// Tags and facts are one signal set for gate purposes: a row in `GATES_BY_STACK`
|
|
583
|
-
// keys on a tag, a row in `ECOSYSTEMS` on a fact, and the two never name each
|
|
584
|
-
// other's vocabulary (facts are `has-*`, tags are not), so one union cannot
|
|
585
|
-
// cross-match.
|
|
586
|
-
const signals = new Set([...stack, ...facts]);
|
|
587
|
-
const gates = { preCommit: [], prePush: [] };
|
|
588
|
-
|
|
589
|
-
for (const row of GATES_BY_STACK) {
|
|
590
|
-
if (row.when.some((tag) => signals.has(tag))) {
|
|
591
|
-
gates.preCommit.push(...row.preCommit);
|
|
592
|
-
gates.prePush.push(...row.prePush);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
for (const row of ECOSYSTEMS) {
|
|
596
|
-
if (signals.has(row.fact)) {
|
|
597
|
-
// A row proposes what it has: the Maven row has no preCommit gates, so a
|
|
598
|
-
// missing key is "none", not a defect. Spreading undefined throws.
|
|
599
|
-
gates.preCommit.push(...(row.preCommit ?? []));
|
|
600
|
-
gates.prePush.push(...(row.prePush ?? []));
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
for (const row of TAG_GATES) {
|
|
604
|
-
if (signals.has(row.tag)) gates.preCommit.push(...row.preCommit);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// A repo with no recognised stack still gets a gate list, because a gate that
|
|
608
|
-
// runs nothing is indistinguishable from a gate that was never installed.
|
|
609
|
-
if (gates.preCommit.length === 0) gates.preCommit.push('format', 'lint');
|
|
610
|
-
if (gates.prePush.length === 0) gates.prePush.push('test');
|
|
611
|
-
|
|
612
|
-
return { preCommit: dedupe(gates.preCommit), prePush: dedupe(gates.prePush) };
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function dedupe(list) {
|
|
616
|
-
return [...new Set(list)];
|
|
617
|
-
}
|
|
618
194
|
|
|
619
195
|
/**
|
|
620
196
|
* A repo id is the directory name. Using it rather than a guessed field keeps
|
|
@@ -735,7 +311,6 @@ export function declaredShared(manifest) {
|
|
|
735
311
|
// marked generated, and a second list here is the drift this module exists to end.
|
|
736
312
|
export const GENERATED_KEYS = GENERATED_KEYS_OF;
|
|
737
313
|
|
|
738
|
-
|
|
739
314
|
// The sub-keys of `projection` this tool writes. `projection` is a generated key, but
|
|
740
315
|
// not wholesale: the repository owns sub-keys this writer does not emit (`place` is the
|
|
741
316
|
// one that shipped), and a repair that replaces the object deletes them (review 0003 Q26).
|
|
@@ -907,4 +482,4 @@ export async function writeManifest(root, manifest, journal = null) {
|
|
|
907
482
|
if (journal) await journaledWriteFile(root, journal, fresh, manifestText(manifest));
|
|
908
483
|
else await writeFile(fresh, manifestText(manifest));
|
|
909
484
|
return fresh;
|
|
910
|
-
}
|
|
485
|
+
}
|
package/src/projection/mdc.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
29
29
|
|
|
30
30
|
import { parse } from './markers.mjs';
|
|
31
31
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
32
|
+
import { parseFrontmatter as splitBlock, stripFrontmatter as stripBlock } from '../text/frontmatter.mjs';
|
|
32
33
|
|
|
33
34
|
// ROOT is computed here rather than imported: project.mjs is the caller of every
|
|
34
35
|
// function below, so importing it back would create a cycle for the sake of one
|
|
@@ -134,9 +135,8 @@ export async function loadMdcRules() {
|
|
|
134
135
|
* rather than skipped, because a silently-dropped substitution projects a rule
|
|
135
136
|
* containing a literal `{{placeholder}}`. */
|
|
136
137
|
function parseSubstitutions(text) {
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
const block = m[1];
|
|
138
|
+
const { front: block } = splitBlock(text);
|
|
139
|
+
if (block === null) return [];
|
|
140
140
|
if (!/^simt-substitutions:/m.test(block)) return [];
|
|
141
141
|
|
|
142
142
|
const out = [];
|
|
@@ -168,10 +168,10 @@ function parseSubstitutions(text) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function parseFrontmatter(text) {
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
171
|
+
const { front } = splitBlock(text);
|
|
172
|
+
if (front === null) return {};
|
|
173
173
|
const out = {};
|
|
174
|
-
for (const line of
|
|
174
|
+
for (const line of front.split('\n')) {
|
|
175
175
|
// `simt-substitutions:` opens a nested block this parser cannot represent, so
|
|
176
176
|
// it and its list items are skipped here and read by parseSubstitutions.
|
|
177
177
|
if (/^\s*-\s/.test(line)) continue;
|
|
@@ -201,8 +201,7 @@ function unquote(s) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function stripFrontmatter(text) {
|
|
204
|
-
return text
|
|
205
|
-
.replace(/^---\n[\s\S]*?\n---\n*/, '')
|
|
204
|
+
return stripBlock(text)
|
|
206
205
|
.replace(/^#\s+.*\n+/, '')
|
|
207
206
|
.trim();
|
|
208
207
|
}
|