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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { journaledWriteFile } from '../journal.mjs';
|
|
5
|
+
import { getGate } from '../hooks/gates.mjs';
|
|
6
|
+
import { runScript } from '../ecosystem/node-runner.mjs';
|
|
7
|
+
import { ecosystemGateRows } from '../ecosystem/records.mjs';
|
|
8
|
+
import { MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
9
|
+
import { GENERATED_KEYS_OF, KEYS, loadManifest, manifestPath } from '../manifest/schema.mjs';
|
|
10
|
+
import { CLI } from '../name.mjs';
|
|
11
|
+
import { packageVersion } from '../package-version.mjs';
|
|
12
|
+
|
|
13
|
+
// The gates a repository declares: what each one runs, and whether it can run at all.
|
|
14
|
+
//
|
|
15
|
+
// Moved out of projection/manifest.mjs. A gate is a claim about what a commit will run,
|
|
16
|
+
// and the manifest is where the claim is written down; keeping the two in one module meant
|
|
17
|
+
// a change to the gate table was a change to the manifest writer. `manifest.mjs` still
|
|
18
|
+
// re-exports every name here, so a caller imports the surface it always did.
|
|
19
|
+
|
|
20
|
+
const GATES_BY_STACK = [
|
|
21
|
+
{
|
|
22
|
+
when: ['gradle', 'kotlin', 'connectiq'],
|
|
23
|
+
preCommit: ['format', 'lint', 'i18n-completeness'],
|
|
24
|
+
prePush: ['test'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
when: ['python'],
|
|
28
|
+
preCommit: ['format', 'lint', 'i18n-completeness'],
|
|
29
|
+
prePush: ['test', 'coverage'],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
when: ['node', 'node-web', 'cloudflare-workers', 'typescript'],
|
|
33
|
+
preCommit: ['format', 'lint'],
|
|
34
|
+
prePush: ['test', 'typecheck'],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Extra gate implied by a specific tag, applied after the table above. */
|
|
39
|
+
const TAG_GATES = [
|
|
40
|
+
{ tag: 'connectiq', preCommit: ['memory', 'codegen-drift'] },
|
|
41
|
+
{ tag: 'hono', preCommit: ['i18n-completeness'] },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The ecosystems that produce a fact rather than a stack tag, and what a fresh
|
|
46
|
+
* adoption of one should declare.
|
|
47
|
+
*
|
|
48
|
+
* Read from the ecosystem records (ADR-013: content is data, the engine is code), not
|
|
49
|
+
* written here. A row proposes gates the way `GATES_BY_STACK` does, and the fact it keys
|
|
50
|
+
* on is the same signal a profile's `simt-applies` names, so one marker file reaches
|
|
51
|
+
* both: the region this repo reads and the gates its commits run.
|
|
52
|
+
*
|
|
53
|
+
* `commands` is what a FRESH manifest declares for each proposed gate, and it is written
|
|
54
|
+
* only when the probe in `readCapability` found `tool` where the record says it lives.
|
|
55
|
+
* A declared gate that cannot run blocks every commit, so a command is never declared on
|
|
56
|
+
* the faith of a marker file: the marker says the ecosystem is here, the probe says its
|
|
57
|
+
* program can run (ADR-017). A command the repo already declared wins over the seed.
|
|
58
|
+
*
|
|
59
|
+
* `file` marks a tool that is a file in the repository rather than a binary on PATH: the
|
|
60
|
+
* Maven wrapper, probed the way this tool already probes `gradlew`.
|
|
61
|
+
*
|
|
62
|
+
* The shipped set, read without a root, is what the const-iterating tests pin; a
|
|
63
|
+
* repository's own `.simt/ecosystems/` overrides arrive through the views' `root` argument.
|
|
64
|
+
*/
|
|
65
|
+
export const ECOSYSTEMS = ecosystemGateRows();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Can this repo actually run the gate?
|
|
69
|
+
*
|
|
70
|
+
* A declared gate that cannot run is worse than an absent one. Absent, an author sees
|
|
71
|
+
* nothing and knows nothing is being checked; declared-and-unrunnable, the dispatcher
|
|
72
|
+
* fails closed on every commit and the only exit an author can find is `--no-verify`,
|
|
73
|
+
* which teaches the whole team that the gate is theatre. The dispatcher is right to
|
|
74
|
+
* fail closed — the defect is declaring a gate this repo has no way to run.
|
|
75
|
+
*
|
|
76
|
+
* The check mirrors what the dispatcher actually attempts:
|
|
77
|
+
* - `script` gates run `<manager> run <script>`, so the script must exist
|
|
78
|
+
* - `staged` gates look for each named tool in `node_modules/.bin`, so it must be
|
|
79
|
+
* installed, not merely declared as a dependency
|
|
80
|
+
* - `command` gates run whatever the manifest's `gates.commands` declares, so a
|
|
81
|
+
* command gate with no declared command is unrunnable by definition
|
|
82
|
+
*
|
|
83
|
+
* @param {object} gate an entry from GATES in src/hooks/gates.mjs
|
|
84
|
+
* @param {{scripts?: Record<string,string>, binaries?: string[], commands?: Record<string,string>}} capability
|
|
85
|
+
*/
|
|
86
|
+
export function gateRunnable(gate, capability = {}) {
|
|
87
|
+
if (!gate) return { runnable: false, why: 'unknown gate' };
|
|
88
|
+
// Only a KNOWN non-npm runner changes the wording. A capability that does not report
|
|
89
|
+
// its runner (a hand-built one, a caller from an older version) is treated as npm,
|
|
90
|
+
// because claiming a repo has no package.json is a much worse guess than assuming it.
|
|
91
|
+
const foreign = isForeignRunner(capability.runner);
|
|
92
|
+
|
|
93
|
+
// A declared command satisfies the gate whatever its kind, because that is the
|
|
94
|
+
// dispatcher's own precedence and the two must never disagree about one manifest.
|
|
95
|
+
const declared = capability.commands?.[gate.name];
|
|
96
|
+
if (declared) {
|
|
97
|
+
return declared.trim() === ''
|
|
98
|
+
? { runnable: false, why: `gates.commands["${gate.name}"] is empty` }
|
|
99
|
+
: { runnable: true, why: `runs: ${declared}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (gate.kind === 'builtin') {
|
|
103
|
+
// The one kind with no external requirement: the implementation ships inside
|
|
104
|
+
// the binary that is reading this manifest (ADR-100). Saying otherwise would
|
|
105
|
+
// make `check` report a gate as unrunnable that runs on every commit.
|
|
106
|
+
return { runnable: true, why: 'runs inside the tool itself; nothing to install' };
|
|
107
|
+
}
|
|
108
|
+
if (gate.kind === 'command') {
|
|
109
|
+
return { runnable: false, why: `no command is declared for it in gates.commands` };
|
|
110
|
+
}
|
|
111
|
+
if (gate.kind === 'script') {
|
|
112
|
+
const script = gate.script ?? gate.name;
|
|
113
|
+
// Q31: the runnable sentence names the repo's own runner, never a bare npm
|
|
114
|
+
// assumption — the card, the dispatcher and this check read one resolver.
|
|
115
|
+
const spell = runScript(capability.runner ?? 'npm', script);
|
|
116
|
+
if (capability.scripts?.[script]) return { runnable: true, why: `runs: ${spell}` };
|
|
117
|
+
// Saying "no script in package.json" about a repo with no package.json states a
|
|
118
|
+
// fact about a file that does not exist and implies a fix that cannot be taken.
|
|
119
|
+
return {
|
|
120
|
+
runnable: false,
|
|
121
|
+
why: foreign
|
|
122
|
+
? `this repo runs ${runnerName(capability.runner)}, not npm, so "${spell}" cannot run here`
|
|
123
|
+
: `no "${script}" script in package.json`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// staged: a formatter must actually be installed
|
|
127
|
+
for (const tool of gate.tools ?? []) {
|
|
128
|
+
if (!(capability.binaries ?? []).includes(tool)) {
|
|
129
|
+
return {
|
|
130
|
+
runnable: false,
|
|
131
|
+
why: foreign
|
|
132
|
+
? `${tool} is not installed, and this repo has no node_modules/.bin to install it into`
|
|
133
|
+
: `${tool} is not installed in node_modules/.bin`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { runnable: true, why: `formats staged files with ${(gate.tools ?? []).join(', ')}` };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What the reader has to do to make a dropped gate runnable.
|
|
142
|
+
*
|
|
143
|
+
* A drop is a decision the tool made on the repo's behalf, so it owes the reader the
|
|
144
|
+
* undo. Every remedy is one of three real choices — install the tool, add the script,
|
|
145
|
+
* or name the command — because "this repo cannot run format" with no next step reads
|
|
146
|
+
* as simt-dev deciding formatting does not matter here.
|
|
147
|
+
*
|
|
148
|
+
* The two npm-shaped remedies only apply where npm is the runner. In a Gradle, Python or
|
|
149
|
+
* Rust repo the honest undo is the `command` gate: the repo names its own program, which
|
|
150
|
+
* is the same escape hatch ConnectIQ memory budgets already use.
|
|
151
|
+
*/
|
|
152
|
+
function gateRemedy(gate, { existing = false, runner = null } = {}) {
|
|
153
|
+
const name = gate?.name ?? 'the gate';
|
|
154
|
+
const npmShaped = gate?.kind === 'staged' || gate?.kind === 'script';
|
|
155
|
+
const foreign = isForeignRunner(runner);
|
|
156
|
+
// Q31: a missing tool in a Node repo names the repo's own installer —
|
|
157
|
+
// `pnpm add -D`, not `npm i -D` — and anything elsewhere keeps the honest
|
|
158
|
+
// undo: name the program under `gates.commands`.
|
|
159
|
+
const addDev = { npm: 'npm i -D', pnpm: 'pnpm add -D', yarn: 'yarn add -D', bun: 'bun add -d' }[runner] ?? null;
|
|
160
|
+
const fix =
|
|
161
|
+
npmShaped && foreign
|
|
162
|
+
? `this repo runs ${runnerName(runner)}, not npm; name the program under "gates.commands" in ${MANIFEST_FILE} instead of the npm equivalent`
|
|
163
|
+
: gate?.kind === 'staged'
|
|
164
|
+
? addDev
|
|
165
|
+
? `install ${gate.tools.join(', ')} as a dev dependency: ${addDev} ${gate.tools.join(' ')} (the gate runs from the pinned dependency)`
|
|
166
|
+
: `install ${gate.tools.join(', ')} as a dev dependency`
|
|
167
|
+
: gate?.kind === 'script'
|
|
168
|
+
? `add a "${gate.script ?? name}" script to package.json`
|
|
169
|
+
: gate?.kind === 'command'
|
|
170
|
+
? `declare the program under "gates.commands" in ${MANIFEST_FILE}`
|
|
171
|
+
: `remove "${name}" from the gates list in ${MANIFEST_FILE}`;
|
|
172
|
+
// A re-init never re-adds a gate to a manifest that already has one, so "install the
|
|
173
|
+
// tool and re-run init" would send the reader round the loop and back to where they
|
|
174
|
+
// started. On an existing manifest the declaration is the second half of the fix.
|
|
175
|
+
const second =
|
|
176
|
+
npmShaped && foreign
|
|
177
|
+
? `, or add "${name}" to gates.preCommit or gates.prePush in ${MANIFEST_FILE}`
|
|
178
|
+
: existing
|
|
179
|
+
? `, then add "${name}" to gates.preCommit or gates.prePush in ${MANIFEST_FILE}`
|
|
180
|
+
: `, then re-run: ${CLI} init --yes`;
|
|
181
|
+
return `${fix}${second}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Is this runner one where `npm run` is not how a gate is spelled?
|
|
186
|
+
*
|
|
187
|
+
* `undefined` means "the caller did not report a runner", and every Node manager
|
|
188
|
+
* (`npm`, `pnpm`, `yarn`, `bun`) spells gates as `<manager> run <script>` with
|
|
189
|
+
* scripts living in the same `package.json`. Only a positively identified
|
|
190
|
+
* non-Node runner changes the wording. (Q31: pnpm is Node-shaped, not foreign.)
|
|
191
|
+
*/
|
|
192
|
+
function isForeignRunner(runner) {
|
|
193
|
+
if (runner == null) return false;
|
|
194
|
+
return !['npm', 'pnpm', 'yarn', 'bun'].includes(runner);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** How a reader would name the runner in a sentence. */
|
|
198
|
+
function runnerName(runner) {
|
|
199
|
+
return (
|
|
200
|
+
{
|
|
201
|
+
none: 'no recognised runner',
|
|
202
|
+
gradle: 'Gradle',
|
|
203
|
+
python: 'Python',
|
|
204
|
+
npm: 'npm',
|
|
205
|
+
pnpm: 'pnpm',
|
|
206
|
+
yarn: 'yarn',
|
|
207
|
+
bun: 'bun',
|
|
208
|
+
cargo: 'Cargo',
|
|
209
|
+
go: 'Go',
|
|
210
|
+
}[runner] ?? runner
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The stack table, filtered by what this repo can actually run.
|
|
216
|
+
*
|
|
217
|
+
* A declared gate that cannot run is worse than an absent one. Absent, an author sees
|
|
218
|
+
* nothing and knows nothing is being checked; declared-and-unrunnable, the dispatcher
|
|
219
|
+
* fails closed on every commit and the only exit an author can find is `--no-verify`,
|
|
220
|
+
* which teaches the whole team that the gate is theatre. The dispatcher is right to
|
|
221
|
+
* fail closed — the defect is declaring a gate this repo has no way to run.
|
|
222
|
+
*
|
|
223
|
+
* The filter mirrors what the dispatcher actually attempts:
|
|
224
|
+
* - `script` gates run `npm run <script>`, so the script must exist
|
|
225
|
+
* - `staged` gates look for each named tool in `node_modules/.bin`, so it must be
|
|
226
|
+
* installed, not merely declared as a dependency
|
|
227
|
+
* - `command` gates run whatever `gates.commands` declares, so a command gate with
|
|
228
|
+
* no declared command is unrunnable by definition
|
|
229
|
+
*
|
|
230
|
+
* The seeds are returned alongside the plan because a fresh adoption writes them
|
|
231
|
+
* into `gates.commands` and the reader is owed the line that says so: a command
|
|
232
|
+
* that lands in the manifest without appearing in the plan is a silent write.
|
|
233
|
+
*
|
|
234
|
+
* @returns {{gates: {preCommit: string[], prePush: string[]}, dropped: {gate: string, why: string}[], commands: Record<string,string>}}
|
|
235
|
+
*/
|
|
236
|
+
export function planGates(stack = [], capability = null, facts = [], root = null) {
|
|
237
|
+
const proposed = defaultGates(stack, facts, root);
|
|
238
|
+
if (!capability) return { gates: proposed, dropped: [], commands: {} };
|
|
239
|
+
const seeds = ecosystemCommands(facts, capability, root);
|
|
240
|
+
// A command the repo already declared wins over a seed: the seed is a default,
|
|
241
|
+
// the declaration is a choice, and a default overwriting a choice is a re-init
|
|
242
|
+
// that edits the repo's mind.
|
|
243
|
+
const effective = { ...capability, commands: { ...seeds, ...capability.commands } };
|
|
244
|
+
const dropped = [];
|
|
245
|
+
const runnable = (list) =>
|
|
246
|
+
list.filter((name) => {
|
|
247
|
+
const gate = getGate(name, effective.commands);
|
|
248
|
+
const verdict = gateRunnable(gate, effective);
|
|
249
|
+
if (!verdict.runnable) {
|
|
250
|
+
dropped.push({ gate: name, why: verdict.why, remedy: gateRemedy(gate, { runner: capability.runner }) });
|
|
251
|
+
}
|
|
252
|
+
return verdict.runnable;
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
gates: { preCommit: runnable(proposed.preCommit), prePush: runnable(proposed.prePush) },
|
|
256
|
+
dropped,
|
|
257
|
+
commands: seeds,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The canonical `gates.commands` a fresh manifest of this repository should carry.
|
|
263
|
+
*
|
|
264
|
+
* Two probes, and both must pass before a command is declared: the fact says the
|
|
265
|
+
* ecosystem is in the repository, and `capability.tools` says its program can run
|
|
266
|
+
* on this machine. A marker file without a toolchain would otherwise declare a
|
|
267
|
+
* gate that fails closed on every commit — the defect ADR-017 exists to prevent,
|
|
268
|
+
* reached from the ecosystem table rather than the stack table.
|
|
269
|
+
*
|
|
270
|
+
* Pure, so `planGates` and `buildManifest` answer with one implementation.
|
|
271
|
+
*/
|
|
272
|
+
function ecosystemCommands(facts = [], capability = null, root = null) {
|
|
273
|
+
if (!capability?.tools?.length) return {};
|
|
274
|
+
// A Node runner (npm/pnpm/yarn/bun) already answers "how do I test and format"
|
|
275
|
+
// with its own package.json scripts, and a seed only wins over those scripts when
|
|
276
|
+
// the repo has not declared a command of its own — which is exactly the mixed
|
|
277
|
+
// repos (package.json AND Cargo.toml) where the script is the deliberate answer.
|
|
278
|
+
// Seeding there would replace a choice with a default; the reader's escape hatch
|
|
279
|
+
// is declaring gates.commands by hand, which a seed would lose to anyway. Pure
|
|
280
|
+
// ecosystem repos have runner 'cargo'/'go'/... and seed normally.
|
|
281
|
+
// Q31: every Node manager skips seeding — pnpm is not "foreign", it is Node.
|
|
282
|
+
if (['npm', 'pnpm', 'yarn', 'bun'].includes(capability.runner)) return {};
|
|
283
|
+
const present = new Set(facts);
|
|
284
|
+
const commands = {};
|
|
285
|
+
for (const eco of ecosystemGateRows(root)) {
|
|
286
|
+
if (!present.has(eco.fact)) continue;
|
|
287
|
+
if (!capability.tools.includes(eco.tool)) continue;
|
|
288
|
+
for (const [gate, command] of Object.entries(eco.commands)) {
|
|
289
|
+
if (!capability.commands?.[gate]) commands[gate] = command;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return commands;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build a manifest from a fingerprint. Pure, so the whole default surface is
|
|
297
|
+
* testable without a filesystem.
|
|
298
|
+
*
|
|
299
|
+
* When `capability` is supplied, newly proposed gates the repo cannot run are dropped.
|
|
300
|
+
* Gates the repo ALREADY declared are kept regardless: that is the repo's choice to
|
|
301
|
+
* make, and quietly disarming a gate somebody declared on purpose is a worse surprise
|
|
302
|
+
* than a visible one.
|
|
303
|
+
*/
|
|
304
|
+
export function unrunnableGates(manifest, capability) {
|
|
305
|
+
const declared = [
|
|
306
|
+
...(manifest?.gates?.preCommit ?? []),
|
|
307
|
+
...(manifest?.gates?.prePush ?? []),
|
|
308
|
+
];
|
|
309
|
+
const findings = [];
|
|
310
|
+
const seen = new Set();
|
|
311
|
+
// `gates.cwd` names a directory a gate runs in, and a name that is not a declared
|
|
312
|
+
// gate is as much a landmine as one that cannot run: the entry is inert, the reader
|
|
313
|
+
// believes the gate runs in the package it names, and nothing anywhere says so. Same
|
|
314
|
+
// shape as the unknown-gate finding in the dispatcher, so both are repaired the same
|
|
315
|
+
// way.
|
|
316
|
+
const cwds = manifest?.gates?.cwd;
|
|
317
|
+
if (cwds && typeof cwds === 'object' && !Array.isArray(cwds)) {
|
|
318
|
+
for (const name of Object.keys(cwds).sort()) {
|
|
319
|
+
if (declared.includes(name)) continue;
|
|
320
|
+
findings.push({
|
|
321
|
+
file: MANIFEST_FILE,
|
|
322
|
+
status: 'unknown-gate-cwd',
|
|
323
|
+
gate: name,
|
|
324
|
+
detail: `gates.cwd names "${name}", which is not declared in gates.preCommit or gates.prePush, so it is never read`,
|
|
325
|
+
remedy: `declare "${name}" in gates.preCommit or gates.prePush, or remove gates.cwd["${name}"]`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const name of declared) {
|
|
330
|
+
if (seen.has(name)) continue;
|
|
331
|
+
seen.add(name);
|
|
332
|
+
const gate = getGate(name, capability.commands);
|
|
333
|
+
const verdict = gateRunnable(gate, capability);
|
|
334
|
+
if (verdict.runnable) continue;
|
|
335
|
+
findings.push({
|
|
336
|
+
file: MANIFEST_FILE,
|
|
337
|
+
status: 'unrunnable',
|
|
338
|
+
gate: name,
|
|
339
|
+
detail: `declares gate "${name}", which this repo cannot run: ${verdict.why}`,
|
|
340
|
+
remedy: gateRemedy(gate, { existing: true, runner: capability.runner }),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return findings;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** ADR-005: 400 is a review trigger, 1000 a hard ceiling. Generated files exempt. */
|
|
347
|
+
function defaultGates(stack, facts = [], root = null) {
|
|
348
|
+
// Tags and facts are one signal set for gate purposes: a row in `GATES_BY_STACK`
|
|
349
|
+
// keys on a tag, a row in `ECOSYSTEMS` on a fact, and the two never name each
|
|
350
|
+
// other's vocabulary (facts are `has-*`, tags are not), so one union cannot
|
|
351
|
+
// cross-match.
|
|
352
|
+
const signals = new Set([...stack, ...facts]);
|
|
353
|
+
const gates = { preCommit: [], prePush: [] };
|
|
354
|
+
|
|
355
|
+
for (const row of GATES_BY_STACK) {
|
|
356
|
+
if (row.when.some((tag) => signals.has(tag))) {
|
|
357
|
+
gates.preCommit.push(...row.preCommit);
|
|
358
|
+
gates.prePush.push(...row.prePush);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const row of ecosystemGateRows(root)) {
|
|
362
|
+
if (signals.has(row.fact)) {
|
|
363
|
+
// A row proposes what it has: the Maven row has no preCommit gates, so a
|
|
364
|
+
// missing key is "none", not a defect. Spreading undefined throws.
|
|
365
|
+
gates.preCommit.push(...(row.preCommit ?? []));
|
|
366
|
+
gates.prePush.push(...(row.prePush ?? []));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const row of TAG_GATES) {
|
|
370
|
+
if (signals.has(row.tag)) gates.preCommit.push(...row.preCommit);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// A repo with no recognised stack still gets a gate list, because a gate that
|
|
374
|
+
// runs nothing is indistinguishable from a gate that was never installed.
|
|
375
|
+
if (gates.preCommit.length === 0) gates.preCommit.push('format', 'lint');
|
|
376
|
+
if (gates.prePush.length === 0) gates.prePush.push('test');
|
|
377
|
+
|
|
378
|
+
return { preCommit: dedupe(gates.preCommit), prePush: dedupe(gates.prePush) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function dedupe(list) {
|
|
382
|
+
return [...new Set(list)];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export { defaultGates, dedupe };
|
package/src/graph/compile.mjs
CHANGED
|
@@ -15,10 +15,14 @@ import { createHash } from 'node:crypto';
|
|
|
15
15
|
import { checkSkill, nameProblems, readFrontmatter } from './spec.mjs';
|
|
16
16
|
import { matchesGlob } from '../resolve/resolve.mjs';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
import {
|
|
19
|
+
BODY_BYTE_BUDGET,
|
|
20
|
+
BODY_LINE_BUDGET,
|
|
21
|
+
BODY_TOKEN_BUDGET,
|
|
22
|
+
BYTES_PER_TOKEN,
|
|
23
|
+
} from './spec.mjs';
|
|
24
|
+
|
|
25
|
+
export { BODY_BYTE_BUDGET, BODY_LINE_BUDGET, BODY_TOKEN_BUDGET, BYTES_PER_TOKEN };
|
|
22
26
|
|
|
23
27
|
const TARGET_MAIN_BYTES = 12_000;
|
|
24
28
|
const TARGET_MAIN_LINES = 350;
|
package/src/graph/load.mjs
CHANGED
|
@@ -18,7 +18,8 @@ import { existsSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
|
|
20
20
|
import { KNOWN_TAGS } from '../resolve/fingerprint.mjs';
|
|
21
|
-
import { matchesGlob, syntheticPathFor
|
|
21
|
+
import { matchesGlob, syntheticPathFor } from '../resolve/resolve.mjs';
|
|
22
|
+
import { tokenize } from '../text/tokens.mjs';
|
|
22
23
|
import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
23
24
|
|
|
24
25
|
/**
|
package/src/graph/spec.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// error the reader cannot act on is a papercut that trains people to skip the gate, so
|
|
10
10
|
// every message names the rule it broke and the fix.
|
|
11
11
|
|
|
12
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
13
|
+
|
|
12
14
|
/** The one specification this knows, and the name `--spec` accepts. */
|
|
13
15
|
export const SPEC = 'agentskills';
|
|
14
16
|
|
|
@@ -19,9 +21,10 @@ const SPEC_FIELDS = ['name', 'description', 'license', 'compatibility', 'metadat
|
|
|
19
21
|
|
|
20
22
|
/** Tokens, not bytes, because the specification counts tokens and every number here is
|
|
21
23
|
* the same four-bytes-per-token estimate the context meter uses. */
|
|
22
|
-
const BODY_TOKEN_BUDGET = 5000;
|
|
23
|
-
const BODY_LINE_BUDGET = 500;
|
|
24
|
-
const BYTES_PER_TOKEN = 4;
|
|
24
|
+
export const BODY_TOKEN_BUDGET = 5000;
|
|
25
|
+
export const BODY_LINE_BUDGET = 500;
|
|
26
|
+
export const BYTES_PER_TOKEN = 4;
|
|
27
|
+
export const BODY_BYTE_BUDGET = BODY_TOKEN_BUDGET * BYTES_PER_TOKEN;
|
|
25
28
|
|
|
26
29
|
/** The `name` rules, as one predicate with the rule it enforces. */
|
|
27
30
|
const NAME_RULES = {
|
|
@@ -167,10 +170,9 @@ export function checkSkill({ directoryName, fileName = 'SKILL.md', text, at }) {
|
|
|
167
170
|
export function readFrontmatter(text) {
|
|
168
171
|
const meta = {};
|
|
169
172
|
const present = [];
|
|
170
|
-
const body = text.replace(/^/, '');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const lines = match[1].split(/\r?\n/);
|
|
173
|
+
const { front, body } = parseFrontmatter(text.replace(/^/, ''));
|
|
174
|
+
if (front === null) return { meta, present, body };
|
|
175
|
+
const lines = front.split(/\r?\n/);
|
|
174
176
|
|
|
175
177
|
for (let i = 0; i < lines.length; i += 1) {
|
|
176
178
|
const line = lines[i];
|
|
@@ -215,7 +217,7 @@ export function readFrontmatter(text) {
|
|
|
215
217
|
}
|
|
216
218
|
meta[key] = scalar(raw);
|
|
217
219
|
}
|
|
218
|
-
return { meta, present, body
|
|
220
|
+
return { meta, present, body };
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
/** Strip the quoting and the trailing comment a scalar may carry. */
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Harness data records.
|
|
2
|
+
//
|
|
3
|
+
// ADR-013: content is data, the engine is code. A harness is a record under
|
|
4
|
+
// `harnesses/`, not a row in a table the engine edits. A new harness arrives as a new
|
|
5
|
+
// file; the engine — this loader and the views over it — never changes shape to make a
|
|
6
|
+
// product work. The surface row, the reader row, the hook dialect and the plugin layout
|
|
7
|
+
// of one harness land together in one file and cannot drift apart, which is the failure
|
|
8
|
+
// the five-module spread they used to occupy made routine (review 0003 Q71).
|
|
9
|
+
//
|
|
10
|
+
// Records are sparse: a facet is present only when the harness has one. DeepSeek has a
|
|
11
|
+
// reader row and nothing else; Copilot has a surface row, a reader row and a scoping key;
|
|
12
|
+
// Claude has all seven. Absence is a fact — a facet that is not there is one the vendor
|
|
13
|
+
// documents none of — so the views filter, they do not fill.
|
|
14
|
+
//
|
|
15
|
+
// The shipped records live in `harnesses/` at the package root. A repository may override
|
|
16
|
+
// them under `.simt/harnesses/` the way it overrides `ecosystems/`: a same-id record is a
|
|
17
|
+
// shallow merge over the shipped one (arrays replaced whole), a new id is appended, and a
|
|
18
|
+
// record that does not parse is skipped, never a crash.
|
|
19
|
+
//
|
|
20
|
+
// The package root is computed here rather than imported from workspace.mjs for the same
|
|
21
|
+
// reason `ecosystem/records.mjs` computes it: workspace.mjs is part of the import graph
|
|
22
|
+
// that builds these tables at evaluation time, and an import cycle would leave the binding
|
|
23
|
+
// in the temporal dead zone. This file is two levels below the root (src/harness/), so
|
|
24
|
+
// two levels up is the root — a fixed path, not a heuristic.
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
27
|
+
import { dirname, join, resolve } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
31
|
+
const SHIPPED_DIR = join(PACKAGE_ROOT, 'harnesses');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The records in force for a repository: the shipped set, with a repo-local
|
|
35
|
+
* `.simt/harnesses/` merged over it. `null` is the shipped set alone, which is what the
|
|
36
|
+
* const-iterating tests and every call without a root read.
|
|
37
|
+
*/
|
|
38
|
+
export function loadHarnesses(root = null) {
|
|
39
|
+
const base = shipped();
|
|
40
|
+
if (!root) return base;
|
|
41
|
+
const localDir = join(root, '.simt', 'harnesses');
|
|
42
|
+
if (!existsSync(localDir)) return base;
|
|
43
|
+
const byId = new Map(base.map((r) => [r.id, r]));
|
|
44
|
+
for (const file of readdirSync(localDir)) {
|
|
45
|
+
if (!file.endsWith('.json')) continue;
|
|
46
|
+
const id = file.slice(0, -5);
|
|
47
|
+
const rec = parse(join(localDir, file));
|
|
48
|
+
if (!rec) continue;
|
|
49
|
+
// Same id: a shallow merge over the shipped record, so a local override that omits
|
|
50
|
+
// a field keeps the shipped one. New id: the record as-is.
|
|
51
|
+
byId.set(id, byId.has(id) ? { ...byId.get(id), ...rec } : rec);
|
|
52
|
+
}
|
|
53
|
+
return [...byId.values()];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The shipped records, read once per process and cached. */
|
|
57
|
+
function shipped() {
|
|
58
|
+
if (shippedCache) return shippedCache;
|
|
59
|
+
const map = new Map();
|
|
60
|
+
if (existsSync(SHIPPED_DIR)) readRecords(SHIPPED_DIR, map);
|
|
61
|
+
shippedCache = [...map.values()];
|
|
62
|
+
return shippedCache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let shippedCache = null;
|
|
66
|
+
|
|
67
|
+
/** Read one directory of records into `into`, keyed by the file stem. */
|
|
68
|
+
function readRecords(dir, into) {
|
|
69
|
+
for (const file of readdirSync(dir)) {
|
|
70
|
+
if (!file.endsWith('.json')) continue;
|
|
71
|
+
const id = file.slice(0, -5);
|
|
72
|
+
const rec = parse(join(dir, file));
|
|
73
|
+
// The id must match the file stem: a mismatch is a record pointing at the wrong
|
|
74
|
+
// harness, and shipping it would make two names mean one record.
|
|
75
|
+
if (rec && rec.id === id) into.set(id, rec);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** One record, or null when the file is absent, unreadable or does not parse. */
|
|
80
|
+
function parse(path) {
|
|
81
|
+
try {
|
|
82
|
+
const rec = JSON.parse(readFileSync(path, 'utf8'));
|
|
83
|
+
return rec && typeof rec === 'object' ? rec : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// The views. Each returns the rows the old module constant held, in the order
|
|
91
|
+
// the const-iterating tests pin: the records' `*Order` fields, not readdir's.
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** The surface rows, in the order `project` iterates them (ADR-020's union order). */
|
|
95
|
+
export function harnessSurfaces(root = null) {
|
|
96
|
+
return loadHarnesses(root)
|
|
97
|
+
.filter((r) => r.surface)
|
|
98
|
+
.sort((a, b) => a.surfaceOrder - b.surfaceOrder)
|
|
99
|
+
.map((r) => r.surface);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The pointer rows, in their table order. */
|
|
103
|
+
export function harnessPointers(root = null) {
|
|
104
|
+
return loadHarnesses(root)
|
|
105
|
+
.filter((r) => r.pointer)
|
|
106
|
+
.sort((a, b) => a.pointerOrder - b.pointerOrder)
|
|
107
|
+
.map((r) => r.pointer);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The reader rows, in the order the reader table lists its harnesses. */
|
|
111
|
+
export function harnessLoaders(root = null) {
|
|
112
|
+
return loadHarnesses(root)
|
|
113
|
+
.filter((r) => r.loader)
|
|
114
|
+
.sort((a, b) => a.loaderOrder - b.loaderOrder)
|
|
115
|
+
.map((r) => r.loader);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Surface ids whose scoping key was read in the vendor's own documentation. */
|
|
119
|
+
export function harnessScopeKeys(root = null) {
|
|
120
|
+
return new Set(loadHarnesses(root).filter((r) => r.scopeKey === true).map((r) => r.id));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The hook dialect per harness, keyed by id: the data a config document and a decision are shaped by. */
|
|
124
|
+
export function harnessHooks(root = null) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const r of loadHarnesses(root)) if (r.hooks) out[r.id] = r.hooks;
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The payload extraction contract per harness, keyed by id: how a hook payload names its tool, its input and its writes. */
|
|
131
|
+
export function harnessPayloads(root = null) {
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const r of loadHarnesses(root)) if (r.payload) out[r.id] = r.payload;
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The plugin layouts, in the order the `simt plugin` help line names its kinds. */
|
|
138
|
+
export function harnessPlugins(root = null) {
|
|
139
|
+
return loadHarnesses(root)
|
|
140
|
+
.filter((r) => r.plugin)
|
|
141
|
+
.sort((a, b) => a.pluginOrder - b.pluginOrder)
|
|
142
|
+
.map((r) => ({ id: r.id, ...r.plugin }));
|
|
143
|
+
}
|
package/src/hooks/capability.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { constants as fsConstants } from 'node:fs';
|
|
22
22
|
import { access, readdir, readFile, stat } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import { ecosystemGateRows } from '../ecosystem/records.mjs';
|
|
25
25
|
import { loadManifest } from '../manifest/schema.mjs';
|
|
26
26
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
27
27
|
|
|
@@ -82,7 +82,7 @@ async function executableFile(path) {
|
|
|
82
82
|
/**
|
|
83
83
|
* Can this machine run the ecosystem's program?
|
|
84
84
|
*
|
|
85
|
-
* Two kinds of evidence, matching the two kinds of tool in
|
|
85
|
+
* Two kinds of evidence, matching the two kinds of tool in the ecosystem records: a binary on
|
|
86
86
|
* PATH, probed directory by directory the way the shell would find it, and a file the
|
|
87
87
|
* repository commits (the Maven wrapper), probed in place the way `gradlew` is probed.
|
|
88
88
|
* Either answer that is not a positive YES is NO: a seed that guesses a toolchain is
|
|
@@ -158,7 +158,9 @@ export async function readCapability(root) {
|
|
|
158
158
|
// file in the repo cannot: a Cargo.toml without cargo installed is an ecosystem that
|
|
159
159
|
// is here but whose gates would all fail closed. The probe is per-row, because a row
|
|
160
160
|
// that marks its tool `file` lives in the repository, not on PATH.
|
|
161
|
-
|
|
161
|
+
// Q70: the rows come from the records, and `root` lets a repository's own
|
|
162
|
+
// `.simt/ecosystems/` add a tool the shipped set does not probe.
|
|
163
|
+
for (const eco of ecosystemGateRows(root)) {
|
|
162
164
|
if (await probeTool(root, eco)) capability.tools.push(eco.tool);
|
|
163
165
|
}
|
|
164
166
|
|