simt-dev 0.6.2 → 0.7.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 +83 -1
- package/README.md +46 -17
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/README.md +53 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +10 -11
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +4 -3
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +238 -15
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shell.mjs +523 -0
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +62 -9
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/mine.mjs +48 -1
- package/src/bench/probes.mjs +5 -6
- package/src/bench/report.mjs +243 -80
- package/src/bench/run.mjs +12 -11
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +6 -0
- package/src/ci/workflow.mjs +16 -7
- package/src/commands/agent.mjs +206 -0
- package/src/commands/agenthook.mjs +133 -17
- package/src/commands/bench.mjs +2 -1
- package/src/commands/ci.mjs +28 -3
- package/src/commands/compare.mjs +8 -7
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +20 -6
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/explain.mjs +52 -0
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +96 -26
- package/src/commands/hooks.mjs +258 -67
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +69 -102
- package/src/commands/init.mjs +239 -37
- package/src/commands/lease.mjs +304 -0
- package/src/commands/lessons.mjs +77 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +17 -2
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +4 -0
- package/src/commands/rules.mjs +219 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +329 -206
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +112 -30
- package/src/commands/slice.mjs +712 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +146 -36
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +114 -0
- package/src/commands/upgrade.mjs +103 -9
- package/src/commands/why.mjs +10 -4
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -1
- package/src/completions.mjs +196 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +254 -0
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +270 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +52 -24
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +441 -9
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +23 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/ignore.mjs +133 -0
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +372 -55
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +545 -0
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +195 -0
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +86 -13
- package/src/mcp/tools.mjs +340 -264
- package/src/memory/decide.mjs +98 -38
- package/src/memory/lessons.mjs +180 -33
- package/src/name.mjs +9 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +37 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +33 -8
- package/src/prbody.mjs +253 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +21 -14
- package/src/projection/manifest.mjs +82 -21
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +54 -3
- package/src/projection/shared.mjs +4 -1
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +69 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +5 -0
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +4 -2
- package/src/secrets.mjs +374 -0
- package/src/state.mjs +76 -0
- package/src/status.mjs +157 -0
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
package/src/diff.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Unified diffs of the plan (plan 0003 R50).
|
|
2
|
+
//
|
|
3
|
+
// Every writer accepts `--diff` and prints what it would change — or, with
|
|
4
|
+
// `--yes`, what it changed — as unified diffs, one per file. The engine is
|
|
5
|
+
// here, zero dependencies like everything else (a DX tool that needs an
|
|
6
|
+
// install step fails on a fresh machine); each command only supplies
|
|
7
|
+
// {path, before, after} pairs, read before anything is written.
|
|
8
|
+
//
|
|
9
|
+
// Two deliberate boundaries:
|
|
10
|
+
//
|
|
11
|
+
// - Lines, not bytes. All files this tool writes are text; a binary would
|
|
12
|
+
// diff as one giant replaced line, which says nothing. Callers pass
|
|
13
|
+
// strings (null when the file is absent on that side).
|
|
14
|
+
// - Big files degrade, they do not hang. Past 10,000 lines combined the
|
|
15
|
+
// pair renders as one delete-all/insert-all hunk: still a true statement
|
|
16
|
+
// about what changed, just without the search. Myers is O(ND) and a
|
|
17
|
+
// pathological pair would otherwise own the prompt.
|
|
18
|
+
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
20
|
+
|
|
21
|
+
import { emit, isJson } from './output/index.mjs';
|
|
22
|
+
|
|
23
|
+
const CONTEXT = 3;
|
|
24
|
+
const LINE_CAP = 10000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The current bytes of a path, or null when it does not exist. Any other
|
|
28
|
+
* failure propagates: an unreadable file is fail-closed, not treated as
|
|
29
|
+
* absent — a diff against the wrong before would be worse than no diff.
|
|
30
|
+
*/
|
|
31
|
+
export async function readBefore(path) {
|
|
32
|
+
try {
|
|
33
|
+
return await readFile(path, 'utf8');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT') return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Print a run's pairs as unified diffs, or nothing when the plan changes no
|
|
42
|
+
* bytes (an empty creation, an already-current file). Never changes what the
|
|
43
|
+
* run writes or its exit code — callers invoke it in preview and apply alike.
|
|
44
|
+
*
|
|
45
|
+
* Silent under `--json`: the document is the whole report there, and prose on
|
|
46
|
+
* stdout would break the one-document contract. Callers spread `diffField`
|
|
47
|
+
* into their payload instead, so the JSON answer carries the same bytes.
|
|
48
|
+
*/
|
|
49
|
+
export function emitDiffs(pairs, flags) {
|
|
50
|
+
if (isJson(flags)) return;
|
|
51
|
+
const text = renderDiffs(pairs);
|
|
52
|
+
if (text !== '') emit(text, { maxBytes: flags.maxBytes ?? 0 });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The `--diff` bytes for a JSON payload: absent entirely without the flag, so
|
|
56
|
+
* payloads without it are byte-identical to before. */
|
|
57
|
+
export function diffField(pairs, flags) {
|
|
58
|
+
return flags.diff === true ? { diff: renderDiffs(pairs) } : {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Split text into lines, remembering whether the last line was terminated. */
|
|
62
|
+
function splitLines(text) {
|
|
63
|
+
if (text === '') return { lines: [], terminated: true };
|
|
64
|
+
const terminated = text.endsWith('\n');
|
|
65
|
+
const raw = text.split('\n');
|
|
66
|
+
if (terminated) raw.pop();
|
|
67
|
+
return { lines: raw.map((l) => (l.endsWith('\r') ? l.slice(0, -1) : l)), terminated };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Myers O(ND) edit script as a list of {op, line} with op in keep/del/add.
|
|
72
|
+
* Deterministic: deletions sort before additions at every branch, so the
|
|
73
|
+
* same pair always renders the same hunks.
|
|
74
|
+
*/
|
|
75
|
+
function script(a, b) {
|
|
76
|
+
const n = a.length;
|
|
77
|
+
const m = b.length;
|
|
78
|
+
if (n + m > LINE_CAP) {
|
|
79
|
+
return [...a.map((line) => ({ op: 'del', line })), ...b.map((line) => ({ op: 'add', line }))];
|
|
80
|
+
}
|
|
81
|
+
const max = n + m;
|
|
82
|
+
const trace = [];
|
|
83
|
+
let v = new Map([[1, 0]]);
|
|
84
|
+
let done = false;
|
|
85
|
+
for (let d = 0; d <= max && !done; d += 1) {
|
|
86
|
+
const cur = new Map(v);
|
|
87
|
+
for (let k = -d; k <= d; k += 2) {
|
|
88
|
+
let x;
|
|
89
|
+
if (k === -d || (k !== d && (v.get(k - 1) ?? -1) < (v.get(k + 1) ?? -1))) {
|
|
90
|
+
x = v.get(k + 1) ?? 0;
|
|
91
|
+
} else {
|
|
92
|
+
x = (v.get(k - 1) ?? -1) + 1;
|
|
93
|
+
}
|
|
94
|
+
let y = x - k;
|
|
95
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
96
|
+
x += 1;
|
|
97
|
+
y += 1;
|
|
98
|
+
}
|
|
99
|
+
cur.set(k, x);
|
|
100
|
+
if (x >= n && y >= m) {
|
|
101
|
+
done = true;
|
|
102
|
+
trace.push(cur);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!done) trace.push(cur);
|
|
107
|
+
v = cur;
|
|
108
|
+
if (done) break;
|
|
109
|
+
}
|
|
110
|
+
// Walk the trace backwards, emitting the script in reverse.
|
|
111
|
+
const ops = [];
|
|
112
|
+
let x = n;
|
|
113
|
+
let y = m;
|
|
114
|
+
for (let d = trace.length - 1; d > 0; d -= 1) {
|
|
115
|
+
const prev = trace[d - 1];
|
|
116
|
+
const k = x - y;
|
|
117
|
+
const down = k === -d || (k !== d && (prev.get(k - 1) ?? -1) < (prev.get(k + 1) ?? -1));
|
|
118
|
+
const kPrev = down ? k + 1 : k - 1;
|
|
119
|
+
const xPrev = prev.get(kPrev) ?? 0;
|
|
120
|
+
const yPrev = xPrev - kPrev;
|
|
121
|
+
while (x > xPrev && y > yPrev) {
|
|
122
|
+
ops.push({ op: 'keep', line: a[x - 1] });
|
|
123
|
+
x -= 1;
|
|
124
|
+
y -= 1;
|
|
125
|
+
}
|
|
126
|
+
if (down) {
|
|
127
|
+
ops.push({ op: 'add', line: b[y - 1] });
|
|
128
|
+
y -= 1;
|
|
129
|
+
} else {
|
|
130
|
+
ops.push({ op: 'del', line: a[x - 1] });
|
|
131
|
+
x -= 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
while (x > 0 && y > 0) {
|
|
135
|
+
ops.push({ op: 'keep', line: a[x - 1] });
|
|
136
|
+
x -= 1;
|
|
137
|
+
y -= 1;
|
|
138
|
+
}
|
|
139
|
+
return ops.reverse();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Group changed ops into hunks with context. Two changes merge when at most
|
|
144
|
+
* 2×CONTEXT unchanged lines sit between them — the trailing context of one
|
|
145
|
+
* is the leading context of the next — and split otherwise.
|
|
146
|
+
*/
|
|
147
|
+
function hunks(ops) {
|
|
148
|
+
// Line numbers before each op, per side.
|
|
149
|
+
const nums = [];
|
|
150
|
+
let a = 0;
|
|
151
|
+
let b = 0;
|
|
152
|
+
for (const o of ops) {
|
|
153
|
+
nums.push([a, b]);
|
|
154
|
+
if (o.op !== 'add') a += 1;
|
|
155
|
+
if (o.op !== 'del') b += 1;
|
|
156
|
+
}
|
|
157
|
+
const changed = ops.map((o, i) => (o.op === 'keep' ? -1 : i)).filter((i) => i >= 0);
|
|
158
|
+
if (changed.length === 0) return [];
|
|
159
|
+
const groups = [];
|
|
160
|
+
let g = [changed[0]];
|
|
161
|
+
for (let i = 1; i < changed.length; i += 1) {
|
|
162
|
+
if (changed[i] - g[g.length - 1] <= CONTEXT * 2) g.push(changed[i]);
|
|
163
|
+
else {
|
|
164
|
+
groups.push(g);
|
|
165
|
+
g = [changed[i]];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
groups.push(g);
|
|
169
|
+
return groups.map((idx) => {
|
|
170
|
+
const start = Math.max(0, idx[0] - CONTEXT);
|
|
171
|
+
const end = Math.min(ops.length - 1, idx[idx.length - 1] + CONTEXT);
|
|
172
|
+
const slice = ops.slice(start, end + 1);
|
|
173
|
+
let aLen = 0;
|
|
174
|
+
let bLen = 0;
|
|
175
|
+
for (const o of slice) {
|
|
176
|
+
if (o.op !== 'add') aLen += 1;
|
|
177
|
+
if (o.op !== 'del') bLen += 1;
|
|
178
|
+
}
|
|
179
|
+
// GNU shape: an empty side starts at the line before (`-0,0`), a single
|
|
180
|
+
// line omits its `,1`.
|
|
181
|
+
const aStart = aLen === 0 ? nums[start][0] : nums[start][0] + 1;
|
|
182
|
+
const bStart = bLen === 0 ? nums[start][1] : nums[start][1] + 1;
|
|
183
|
+
return { aStart, bStart, aLen, bLen, ops: slice };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hunkHeader(h) {
|
|
188
|
+
const a = h.aLen === 1 ? `${h.aStart}` : `${h.aStart},${h.aLen}`;
|
|
189
|
+
const b = h.bLen === 1 ? `${h.bStart}` : `${h.bStart},${h.bLen}`;
|
|
190
|
+
return `@@ -${a} +${b} @@`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* One file's unified diff, or '' when the two sides are equal. `before` null
|
|
195
|
+
* is a creation, `after` null a removal. An empty creation (or removal of an
|
|
196
|
+
* empty file) has no line changes and renders as '' — the plan already says
|
|
197
|
+
* `created`, and headers with no hunks are noise.
|
|
198
|
+
*/
|
|
199
|
+
export function unifiedDiff(path, before, after) {
|
|
200
|
+
const b = before ?? null;
|
|
201
|
+
const a = after ?? null;
|
|
202
|
+
if (b === a) return '';
|
|
203
|
+
const bs = b === null ? { lines: [], terminated: true } : splitLines(b);
|
|
204
|
+
const as = a === null ? { lines: [], terminated: true } : splitLines(a);
|
|
205
|
+
const ops = script(bs.lines, as.lines);
|
|
206
|
+
if (ops.every((o) => o.op === 'keep')) {
|
|
207
|
+
// Same lines, different terminator: the bytes still differ, so the last
|
|
208
|
+
// line rewrites rather than vanishing from the diff.
|
|
209
|
+
if (bs.terminated === as.terminated || ops.length === 0) return '';
|
|
210
|
+
const last = ops[ops.length - 1];
|
|
211
|
+
ops.splice(ops.length - 1, 1, { op: 'del', line: last.line }, { op: 'add', line: last.line });
|
|
212
|
+
}
|
|
213
|
+
const head = [
|
|
214
|
+
`diff --git a/${path} b/${path}`,
|
|
215
|
+
b === null ? '--- /dev/null' : `--- a/${path}`,
|
|
216
|
+
a === null ? '+++ /dev/null' : `+++ b/${path}`,
|
|
217
|
+
];
|
|
218
|
+
const body = [];
|
|
219
|
+
for (const h of hunks(ops)) {
|
|
220
|
+
body.push(hunkHeader(h));
|
|
221
|
+
// Positions inside the hunk, to place the missing-newline markers exactly
|
|
222
|
+
// where GNU does: after the last line of a side that lacks its terminator.
|
|
223
|
+
let ai = h.aStart + (h.aLen === 0 ? 1 : 0);
|
|
224
|
+
let bi = h.bStart + (h.bLen === 0 ? 1 : 0);
|
|
225
|
+
for (const o of h.ops) {
|
|
226
|
+
if (o.op === 'keep') {
|
|
227
|
+
body.push(` ${o.line}`);
|
|
228
|
+
ai += 1;
|
|
229
|
+
bi += 1;
|
|
230
|
+
} else if (o.op === 'del') {
|
|
231
|
+
body.push(`-${o.line}`);
|
|
232
|
+
if (ai === bs.lines.length && !bs.terminated) body.push('\');
|
|
233
|
+
ai += 1;
|
|
234
|
+
} else {
|
|
235
|
+
body.push(`+${o.line}`);
|
|
236
|
+
if (bi === as.lines.length && !as.terminated) body.push('\');
|
|
237
|
+
bi += 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return [...head, ...body].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Render a run's pairs, skipping the equal and the empty. Paths print in the
|
|
246
|
+
* order given — the caller owns the order, usually the write order — and
|
|
247
|
+
* blocks join with one blank line, the way a reader scans them.
|
|
248
|
+
*/
|
|
249
|
+
export function renderDiffs(pairs) {
|
|
250
|
+
return pairs
|
|
251
|
+
.map(({ path, before, after }) => unifiedDiff(path, before ?? null, after ?? null))
|
|
252
|
+
.filter((d) => d !== '')
|
|
253
|
+
.join('\n\n');
|
|
254
|
+
}
|
package/src/distill/distill.mjs
CHANGED
|
@@ -34,7 +34,9 @@ import {
|
|
|
34
34
|
} from './engine.mjs';
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_BUDGET = 2048;
|
|
37
|
-
|
|
37
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
38
|
+
|
|
39
|
+
const DISTILL_DIR = join(LOCAL_DIR, 'distilled');
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* The row's acceptance metric, stated where a caller reads results (ADR-076):
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The one Node runner resolver every consumer reads (review 0003 Q31).
|
|
2
|
+
//
|
|
3
|
+
// Before this, four tables answered "how does this repo run a script" and
|
|
4
|
+
// disagreed: the card printed `npm run`, the dispatcher spawned `npm run`,
|
|
5
|
+
// the manifest remedies named `npm run`, and CI installed with `npm ci` — in a
|
|
6
|
+
// repository whose lockfile and `packageManager` said pnpm. A pnpm adoption
|
|
7
|
+
// then failed on its first pull request, and a docs-only or Go repo got
|
|
8
|
+
// remedies proposing npm gates it could never run.
|
|
9
|
+
//
|
|
10
|
+
// One data table, one resolver, every consumer. The table is records, not
|
|
11
|
+
// code, so the Q70 ecosystem work can adopt it without a second answer: a
|
|
12
|
+
// future `ecosystems/<id>.json` row carries the same fields and this resolver
|
|
13
|
+
// stays the shape of the lookup.
|
|
14
|
+
//
|
|
15
|
+
// Precedence is committed evidence first. A lockfile on disk names the manager
|
|
16
|
+
// the repo actually installs with; `packageManager` names the one it declares.
|
|
17
|
+
// Where the two disagree `checkToolchain` already reports `mismatched`, and the
|
|
18
|
+
// runner follows the lockfile — the install step must restore the tree CI
|
|
19
|
+
// checks out, not the field. With no lockfile the declared field wins over the
|
|
20
|
+
// npm default, so a fresh `pnpm init` names pnpm before its first install.
|
|
21
|
+
|
|
22
|
+
export const NODE_MANAGERS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'pnpm',
|
|
25
|
+
lockfiles: ['pnpm-lock.yaml'],
|
|
26
|
+
fact: 'has-pnpm-lock',
|
|
27
|
+
run: 'pnpm',
|
|
28
|
+
install: 'pnpm install --frozen-lockfile',
|
|
29
|
+
installFallback: 'pnpm install',
|
|
30
|
+
addDev: 'pnpm add -D',
|
|
31
|
+
silent: true,
|
|
32
|
+
setupNode: true,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'yarn',
|
|
36
|
+
lockfiles: ['yarn.lock'],
|
|
37
|
+
fact: 'has-yarn-lock',
|
|
38
|
+
run: 'yarn',
|
|
39
|
+
install: 'yarn install --frozen-lockfile',
|
|
40
|
+
installFallback: 'yarn install',
|
|
41
|
+
addDev: 'yarn add -D',
|
|
42
|
+
silent: true,
|
|
43
|
+
setupNode: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'bun',
|
|
47
|
+
lockfiles: ['bun.lock', 'bun.lockb'],
|
|
48
|
+
fact: 'has-bun-lock',
|
|
49
|
+
run: 'bun',
|
|
50
|
+
install: 'bun install --frozen-lockfile',
|
|
51
|
+
installFallback: 'bun install',
|
|
52
|
+
addDev: 'bun add -d',
|
|
53
|
+
silent: false,
|
|
54
|
+
setupNode: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'npm',
|
|
58
|
+
lockfiles: ['package-lock.json'],
|
|
59
|
+
fact: 'has-package-lock',
|
|
60
|
+
run: 'npm',
|
|
61
|
+
install: 'npm ci',
|
|
62
|
+
installFallback: 'npm install',
|
|
63
|
+
addDev: 'npm i -D',
|
|
64
|
+
silent: true,
|
|
65
|
+
setupNode: true,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const BY_ID = new Map(NODE_MANAGERS.map((row) => [row.id, row]));
|
|
70
|
+
const BY_LOCKFILE = new Map();
|
|
71
|
+
for (const row of NODE_MANAGERS) {
|
|
72
|
+
for (const file of row.lockfiles) BY_LOCKFILE.set(file, row);
|
|
73
|
+
}
|
|
74
|
+
const BY_FACT = new Map(NODE_MANAGERS.map((row) => [row.fact, row]));
|
|
75
|
+
|
|
76
|
+
/** The npm row. Exported so callers never spell a default twice. */
|
|
77
|
+
export function npmManager() {
|
|
78
|
+
return BY_ID.get('npm');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Name the manager a `packageManager` field declares, or null.
|
|
83
|
+
*
|
|
84
|
+
* `packageManager` is `manager@version`; only the manager names the runner.
|
|
85
|
+
* Unknown managers and non-strings are null, never a guess.
|
|
86
|
+
*/
|
|
87
|
+
export function managerFromPackageField(value) {
|
|
88
|
+
if (typeof value !== 'string') return null;
|
|
89
|
+
const id = value.split('@')[0].trim();
|
|
90
|
+
return BY_ID.has(id) ? id : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the one Node runner for a repository.
|
|
95
|
+
*
|
|
96
|
+
* @param {{facts?: string[]|Set<string>, lockfiles?: string[], packageManager?: string|null}} input
|
|
97
|
+
* @returns {object} the NODE_MANAGERS row that owns this repo
|
|
98
|
+
*/
|
|
99
|
+
export function resolveNodeRunner({ facts = [], lockfiles = [], packageManager = null } = {}) {
|
|
100
|
+
const present = facts instanceof Set ? facts : new Set(facts);
|
|
101
|
+
for (const row of NODE_MANAGERS) {
|
|
102
|
+
if (present.has(row.fact)) return row;
|
|
103
|
+
for (const file of row.lockfiles) {
|
|
104
|
+
if (lockfiles.includes(file)) return row;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const declared = managerFromPackageField(packageManager);
|
|
108
|
+
if (declared) return BY_ID.get(declared);
|
|
109
|
+
return npmManager();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether this runner id spells gates as `<run> run <script>`. */
|
|
113
|
+
export function isNodeRunner(runner) {
|
|
114
|
+
return BY_ID.has(runner);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The command that runs one script: `pnpm run lint`, `bun run lint`. */
|
|
118
|
+
export function runScript(manager, script) {
|
|
119
|
+
const id = typeof manager === 'string' ? manager : manager?.id ?? 'npm';
|
|
120
|
+
const run = BY_ID.get(id)?.run ?? 'npm';
|
|
121
|
+
return `${run} run ${script}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The argv the dispatcher spawns for one script gate. */
|
|
125
|
+
export function runScriptArgv(manager, script) {
|
|
126
|
+
const id = typeof manager === 'string' ? manager : manager?.id ?? 'npm';
|
|
127
|
+
const row = BY_ID.get(id) ?? npmManager();
|
|
128
|
+
if (row.silent) return [row.run, 'run', '--silent', script];
|
|
129
|
+
return [row.run, 'run', script];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Install step for CI: the frozen command plus its fallback. */
|
|
133
|
+
export function installCommands(manager) {
|
|
134
|
+
const row = typeof manager === 'string' ? (BY_ID.get(manager) ?? npmManager()) : (manager ?? npmManager());
|
|
135
|
+
return { install: row.install, fallback: row.installFallback, setupNode: row.setupNode };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Look up a manager record by lockfile name, fact, or id. */
|
|
139
|
+
export function lookupNodeManager({ lockfile = null, fact = null, id = null } = {}) {
|
|
140
|
+
if (id && BY_ID.has(id)) return BY_ID.get(id);
|
|
141
|
+
if (lockfile && BY_LOCKFILE.has(lockfile)) return BY_LOCKFILE.get(lockfile);
|
|
142
|
+
if (fact && BY_FACT.has(fact)) return BY_FACT.get(fact);
|
|
143
|
+
return null;
|
|
144
|
+
}
|