simt-dev 0.7.0 → 0.7.1
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 +158 -872
- package/README.md +19 -133
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +10 -2
- package/rules/core.md +7 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/shim.mjs +1 -1
- package/src/api.mjs +29 -0
- package/src/argv.mjs +29 -3
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +199 -35
- package/src/bench/probes.mjs +99 -62
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +17 -4
- package/src/bench/run.mjs +242 -142
- package/src/brief/assemble.mjs +38 -3
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +3 -4
- package/src/commands/agenthook.mjs +4 -5
- package/src/commands/bench.mjs +190 -17
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +16 -12
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +125 -17
- package/src/commands/hooks.mjs +100 -30
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +4 -7
- package/src/commands/index.mjs +273 -188
- package/src/commands/init.mjs +2 -1
- package/src/commands/lease.mjs +6 -7
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +14 -9
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +4 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +33 -16
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +28 -26
- package/src/commands/skillscompile.mjs +4 -5
- package/src/commands/skillsinstall.mjs +2 -2
- package/src/commands/slice.mjs +21 -10
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +13 -11
- package/src/commands/tests.mjs +6 -9
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +7 -3
- package/src/commands/why.mjs +9 -8
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/graph/load.mjs +4 -1
- package/src/hooks/capability.mjs +3 -3
- package/src/hooks/dispatcher.mjs +198 -43
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/shim.mjs +5 -2
- package/src/identity.mjs +29 -1
- package/src/journal.mjs +78 -9
- package/src/ledger/index.mjs +30 -1
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/server.mjs +70 -16
- package/src/mcp/tools.mjs +45 -7
- package/src/measure/session.mjs +3 -2
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +20 -34
- package/src/projection/rules.mjs +4 -3
- package/src/repo-root.mjs +19 -0
- package/src/resolve/fingerprint.mjs +19 -15
- package/src/review/critic.mjs +2 -1
- package/src/toolwords.mjs +3 -0
- package/src/workspace.mjs +55 -10
- package/docs/decisions/README.md +0 -600
package/src/commands/sync.mjs
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { mkdir, readFile, stat, symlink, unlink } from 'node:fs/promises';
|
|
20
20
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
21
21
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { repoContext } from '../context.mjs';
|
|
23
|
+
import { plan, checkTargets, checkTarget, select, pointerTargets, pointerState } from '../projection/project.mjs';
|
|
23
24
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
24
25
|
import { manifestDrift, readManifest, unrunnableGates, buildManifest, writeManifest, withGenerated, manifestText, manifestPath } from '../projection/manifest.mjs';
|
|
25
26
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
@@ -34,6 +35,7 @@ import { computeLock, readLock } from '../projection/lock.mjs';
|
|
|
34
35
|
import { runDeclaredChecks } from '../kernel/checks.mjs';
|
|
35
36
|
import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
|
|
36
37
|
import { lintDecisions } from '../memory/decide.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
37
39
|
|
|
38
40
|
/** Findings `sync` reports and does not fail on.
|
|
39
41
|
*
|
|
@@ -123,7 +125,7 @@ export const CHECK_NON_BLOCKING = new Set([
|
|
|
123
125
|
]);
|
|
124
126
|
|
|
125
127
|
export async function syncCommand(positional, flags) {
|
|
126
|
-
const root = flags
|
|
128
|
+
const root = commandRoot(flags, positional);
|
|
127
129
|
const checkOnly = flags.check === true;
|
|
128
130
|
const mode = writeMode(flags);
|
|
129
131
|
|
|
@@ -409,7 +411,7 @@ export async function syncCommand(positional, flags) {
|
|
|
409
411
|
: `would write ${changed.length + pointersAdded.length + (manifestChanged ? 1 : 0)} file(s); apply with: ${CLI} sync --yes`,
|
|
410
412
|
);
|
|
411
413
|
}
|
|
412
|
-
emit(lines.join('\n')
|
|
414
|
+
emit(lines.join('\n'));
|
|
413
415
|
// After the report: the report says what, the diff shows it (R50).
|
|
414
416
|
emitDiffs(diffs, flags);
|
|
415
417
|
}
|
|
@@ -544,8 +546,9 @@ async function gateCwdFindings(root, manifest) {
|
|
|
544
546
|
// reports 77. The command below renders these facts for terminals; the tool
|
|
545
547
|
// returns them as JSON. The CLI keeps its field names; the tool adopts them.
|
|
546
548
|
export async function checkRepo(root, opts = {}) {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
+
const ctx = opts.ctx ?? repoContext(root);
|
|
550
|
+
const findings = await checkTargets(root, { ...opts, ctx });
|
|
551
|
+
const rules = await ctx.rules();
|
|
549
552
|
|
|
550
553
|
// A rule file with no id would be projected without a traceable source, so it
|
|
551
554
|
// is a gate failure rather than a warning: `rules why` could not resolve it.
|
|
@@ -568,7 +571,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
568
571
|
// A declared gate this repo cannot run fails closed on every commit once hooks are
|
|
569
572
|
// installed. Reported here rather than left to `hooks status`, so the repo is red
|
|
570
573
|
// before anyone installs anything rather than after.
|
|
571
|
-
const { manifest, exists } = await
|
|
574
|
+
const { manifest, exists } = await ctx.manifest();
|
|
572
575
|
// The gates slot, counted — not just reported — so the line can print run/total.
|
|
573
576
|
// A cwd finding names a declared gate that will not run, so it counts with the
|
|
574
577
|
// unrunnable; an unknown-gate-cwd names no declared gate, so it does not.
|
|
@@ -603,14 +606,14 @@ export async function checkRepo(root, opts = {}) {
|
|
|
603
606
|
// hand either: the next `init` puts simt-dev's own bytes back and the formatter
|
|
604
607
|
// takes them away again. Reporting it here is what stops that being a surprise
|
|
605
608
|
// discovered at upgrade time.
|
|
606
|
-
const { stack } = await fingerprint(
|
|
607
|
-
const drift = await manifestDrift(root, stack, await
|
|
609
|
+
const { stack } = await ctx.fingerprint();
|
|
610
|
+
const drift = await manifestDrift(root, stack, await ctx.digest());
|
|
608
611
|
if (drift) findings.push(drift);
|
|
609
612
|
}
|
|
610
613
|
if (opts.frozen) findings.push(...(await frozenFindings(root, opts)));
|
|
611
614
|
// The checkers a selected rule declared (ADR-088). They read the rule source,
|
|
612
615
|
// not the folded region, so a line that left the prompt is still the line that runs.
|
|
613
|
-
const fp = await fingerprint(
|
|
616
|
+
const fp = await ctx.fingerprint();
|
|
614
617
|
const { selected } = select(rules.filter((r) => r.error === null), fp.stack ?? [], fp.facts ?? []);
|
|
615
618
|
findings.push(...(await runDeclaredChecks(root, selected, manifest ?? null)));
|
|
616
619
|
|
|
@@ -652,7 +655,7 @@ export async function checkRepo(root, opts = {}) {
|
|
|
652
655
|
}
|
|
653
656
|
|
|
654
657
|
export async function checkCommand(positional, flags) {
|
|
655
|
-
const root = flags
|
|
658
|
+
const root = commandRoot(flags, positional);
|
|
656
659
|
const { ok, findings } = await checkRepo(root, flags);
|
|
657
660
|
const payload = {
|
|
658
661
|
ok,
|
|
@@ -678,7 +681,6 @@ export async function checkCommand(positional, flags) {
|
|
|
678
681
|
return f.remedy ? [head, `${' '.repeat(11)}remedy: ${f.remedy}`] : [head];
|
|
679
682
|
})
|
|
680
683
|
.join('\n'),
|
|
681
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
682
684
|
);
|
|
683
685
|
return ok ? EXIT.OK : EXIT.FAIL;
|
|
684
686
|
}
|
package/src/commands/tests.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
|
|
13
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
13
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
14
14
|
import { CLI } from '../name.mjs';
|
|
15
15
|
import { isTestPath } from '../bench/mine.mjs';
|
|
16
16
|
import { blobTexts, isIndexStale, langOf, readEdges, readFiles, reindex } from '../index/store.mjs';
|
|
17
17
|
import { HISTORY, importsOf, parseCoverage, testEdges, testsFor } from '../index/tests.mjs';
|
|
18
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
18
19
|
|
|
19
20
|
const exec = promisify(execFile);
|
|
20
21
|
|
|
@@ -31,7 +32,7 @@ const COVERAGE_PATHS = [
|
|
|
31
32
|
|
|
32
33
|
export async function testsCommand(args, flags) {
|
|
33
34
|
if (args.length > 1) return fail(`unknown tests argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} tests [path]`);
|
|
34
|
-
const root = flags
|
|
35
|
+
const root = commandRoot(flags);
|
|
35
36
|
const loaded = await loadFiles(root);
|
|
36
37
|
if (loaded.error) return loaded.error;
|
|
37
38
|
const { files } = loaded;
|
|
@@ -58,11 +59,11 @@ export async function testsCommand(args, flags) {
|
|
|
58
59
|
const tests = testsFor(target, edges, { cochange: flags.cochange === true });
|
|
59
60
|
const payload = { ok: true, command: 'tests', root, path: target, tests };
|
|
60
61
|
if (isJson(flags)) {
|
|
61
|
-
emitJson(payload
|
|
62
|
+
emitJson(payload);
|
|
62
63
|
return EXIT.OK;
|
|
63
64
|
}
|
|
64
65
|
const lines = [`tests ${tests.length}`, ...tests];
|
|
65
|
-
emit(lines.join('\n')
|
|
66
|
+
emit(lines.join('\n'));
|
|
66
67
|
return EXIT.OK;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -74,11 +75,7 @@ function summarise(root, flags, edges) {
|
|
|
74
75
|
else if (edge.kind === 'cochange') cochange += 1;
|
|
75
76
|
}
|
|
76
77
|
const payload = { ok: true, command: 'tests', root, test, cochange };
|
|
77
|
-
|
|
78
|
-
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
79
|
-
return EXIT.OK;
|
|
80
|
-
}
|
|
81
|
-
emit(`test ${test}\ncochange ${cochange}`, { maxBytes: flags.maxBytes ?? 0 });
|
|
78
|
+
present(payload, () => `test ${test}\ncochange ${cochange}`, flags);
|
|
82
79
|
return EXIT.OK;
|
|
83
80
|
}
|
|
84
81
|
|
package/src/commands/tokens.mjs
CHANGED
|
@@ -73,7 +73,7 @@ export async function tokensCommand(positional, flags) {
|
|
|
73
73
|
`tool output ${plain(session.toolOutput.bytes)} B ~${plain(session.toolOutput.tokens)} tokens (${plain(session.toolOutput.share)}%)`,
|
|
74
74
|
`total ${plain(session.total.bytes)} B ~${plain(session.total.tokens)} tokens`,
|
|
75
75
|
];
|
|
76
|
-
emit(lines.join('\n')
|
|
76
|
+
emit(lines.join('\n'));
|
|
77
77
|
return EXIT.OK;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -193,6 +193,6 @@ export async function tokensCommand(positional, flags) {
|
|
|
193
193
|
`mcp tools/list ${plain(mcp.bytes)} B of ${plain(mcp.tools)} tools (the same for every repository, so it is not in any total)`,
|
|
194
194
|
);
|
|
195
195
|
|
|
196
|
-
emit(lines.join('\n')
|
|
196
|
+
emit(lines.join('\n'));
|
|
197
197
|
return EXIT.OK;
|
|
198
198
|
}
|
package/src/commands/undo.mjs
CHANGED
|
@@ -11,24 +11,21 @@
|
|
|
11
11
|
// the rest around a hand edit would silently merge two authors' intentions.
|
|
12
12
|
// Like every other writer this command previews without `--yes`.
|
|
13
13
|
|
|
14
|
-
import { EXIT, emit, emitJson, isJson, writeMode } from '../output/index.mjs';
|
|
14
|
+
import { EXIT, emit, emitJson, isJson, writeMode, present } from '../output/index.mjs';
|
|
15
15
|
import { CLI } from '../name.mjs';
|
|
16
16
|
import { beforeText, entryLabel, listJournalSets, readGitValue, readJournalSet, undoLastJournalSet } from '../journal.mjs';
|
|
17
17
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { invalidateCheckCache } from '../status.mjs';
|
|
20
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
20
21
|
|
|
21
22
|
export async function undoCommand(positional, flags) {
|
|
22
|
-
const root = flags
|
|
23
|
+
const root = commandRoot(flags, positional);
|
|
23
24
|
const mode = writeMode(flags);
|
|
24
25
|
|
|
25
26
|
const [id] = await listJournalSets(root);
|
|
26
27
|
if (!id) {
|
|
27
|
-
|
|
28
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply });
|
|
29
|
-
return EXIT.OK;
|
|
30
|
-
}
|
|
31
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
28
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply }, () => 'nothing to undo: no journaled writes', flags);
|
|
32
29
|
return EXIT.OK;
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -64,7 +61,6 @@ export async function undoCommand(positional, flags) {
|
|
|
64
61
|
'',
|
|
65
62
|
`would restore ${files} file(s)${keys > 0 ? ` and ${keys} git config key(s)` : ''}; apply with: ${CLI} undo --yes`,
|
|
66
63
|
].join('\n'),
|
|
67
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
68
64
|
);
|
|
69
65
|
// After the report: the report says what, the diff shows it (R50).
|
|
70
66
|
emitDiffs(diffs, flags);
|
|
@@ -73,11 +69,7 @@ export async function undoCommand(positional, flags) {
|
|
|
73
69
|
|
|
74
70
|
const result = await undoLastJournalSet(root);
|
|
75
71
|
if (result.empty) {
|
|
76
|
-
|
|
77
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [] });
|
|
78
|
-
return EXIT.OK;
|
|
79
|
-
}
|
|
80
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
72
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [] }, () => 'nothing to undo: no journaled writes', flags);
|
|
81
73
|
return EXIT.OK;
|
|
82
74
|
}
|
|
83
75
|
if (result.refused) {
|
|
@@ -92,7 +84,6 @@ export async function undoCommand(positional, flags) {
|
|
|
92
84
|
'',
|
|
93
85
|
'nothing was restored; re-run the writing command or merge by hand, then undo again',
|
|
94
86
|
].join('\n'),
|
|
95
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
96
87
|
);
|
|
97
88
|
return EXIT.FAIL;
|
|
98
89
|
}
|
|
@@ -106,7 +97,6 @@ export async function undoCommand(positional, flags) {
|
|
|
106
97
|
}
|
|
107
98
|
emit(
|
|
108
99
|
[`undone ${result.set}`, ...result.restored.map((p) => `restored ${p}`)].join('\n'),
|
|
109
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
110
100
|
);
|
|
111
101
|
// After the report: the report says what, the diff shows it (R50).
|
|
112
102
|
emitDiffs(diffs, flags);
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -216,8 +216,12 @@ export async function upgradeCommand(positional, flags) {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// One verdict, stated once. `ok` used to be `skipped === 0`, so a workspace with a
|
|
220
|
+
// repo that never adopted answered ok:false and exited 0: the document and the process
|
|
221
|
+
// disagreed, and a caller had no way to tell which one was the answer (review 0003 Q65).
|
|
222
|
+
const code = exitFor(plans, failed);
|
|
219
223
|
const payload = {
|
|
220
|
-
ok:
|
|
224
|
+
ok: code === EXIT.OK,
|
|
221
225
|
command: 'upgrade',
|
|
222
226
|
root,
|
|
223
227
|
shape: found.shape,
|
|
@@ -232,7 +236,7 @@ export async function upgradeCommand(positional, flags) {
|
|
|
232
236
|
|
|
233
237
|
if (isJson(flags)) {
|
|
234
238
|
emitJson(payload);
|
|
235
|
-
return
|
|
239
|
+
return code;
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
emit(render(plans, summary, { root, shape: found.shape, source: target.source, sha, applied, failed }));
|
|
@@ -256,7 +260,7 @@ export async function upgradeCommand(positional, flags) {
|
|
|
256
260
|
} else {
|
|
257
261
|
note('note every consumer already runs this simt-dev');
|
|
258
262
|
}
|
|
259
|
-
return
|
|
263
|
+
return code;
|
|
260
264
|
}
|
|
261
265
|
|
|
262
266
|
/**
|
package/src/commands/why.mjs
CHANGED
|
@@ -11,8 +11,9 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { promisify } from 'node:util';
|
|
12
12
|
|
|
13
13
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
14
|
-
import { CLI
|
|
14
|
+
import { CLI } from '../name.mjs';
|
|
15
15
|
import { FORMATTABLE, GATES } from '../hooks/gates.mjs';
|
|
16
|
+
import { loadManifest } from '../manifest/schema.mjs';
|
|
16
17
|
import { decisionsDir } from '../decisions/dir.mjs';
|
|
17
18
|
import {
|
|
18
19
|
decisionHits,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from '../index/govern.mjs';
|
|
25
26
|
import { splitFrontmatter } from '../memory/decide.mjs';
|
|
26
27
|
import { listPromotedLessons } from '../memory/lessons.mjs';
|
|
28
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
27
29
|
|
|
28
30
|
const exec = promisify(execFile);
|
|
29
31
|
|
|
@@ -31,7 +33,7 @@ const RULE_DIRS = ['rules', 'rules/stacks', 'rules/mdc', '.simt/profiles'];
|
|
|
31
33
|
|
|
32
34
|
export async function whyCommand(args, flags, commands = []) {
|
|
33
35
|
if (args.length > 1) return fail(`unknown why argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} why [path]`);
|
|
34
|
-
const root = flags
|
|
36
|
+
const root = commandRoot(flags);
|
|
35
37
|
const listed = await tracked(root);
|
|
36
38
|
if (listed.error) return listed.error;
|
|
37
39
|
const files = new Set(listed.files);
|
|
@@ -45,22 +47,22 @@ export async function whyCommand(args, flags, commands = []) {
|
|
|
45
47
|
const items = sortHits([...hits, ...scopeHits(target, corpus)]);
|
|
46
48
|
const payload = { ok: true, command: 'why', root, path: target, items };
|
|
47
49
|
if (isJson(flags)) {
|
|
48
|
-
emitJson(payload
|
|
50
|
+
emitJson(payload);
|
|
49
51
|
return EXIT.OK;
|
|
50
52
|
}
|
|
51
53
|
const lines = items.map((item) => `${item.kind.padEnd(10)}${item.id} ${item.detail}`);
|
|
52
|
-
emit(lines.length > 0 ? lines.join('\n') : 'none'
|
|
54
|
+
emit(lines.length > 0 ? lines.join('\n') : 'none');
|
|
53
55
|
return EXIT.OK;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
function reportUnresolved(root, flags, unresolved) {
|
|
57
59
|
const payload = { ok: true, command: 'why', root, unresolved };
|
|
58
60
|
if (isJson(flags)) {
|
|
59
|
-
emitJson(payload
|
|
61
|
+
emitJson(payload);
|
|
60
62
|
return EXIT.OK;
|
|
61
63
|
}
|
|
62
64
|
const lines = unresolved.map((item) => `UNRESOLVED-AFFECTS ${item.token} ${item.id}`);
|
|
63
|
-
emit([`unresolved ${unresolved.length}`, ...lines].join('\n')
|
|
65
|
+
emit([`unresolved ${unresolved.length}`, ...lines].join('\n'));
|
|
64
66
|
return EXIT.OK;
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -172,8 +174,7 @@ export async function lessonScopes(root) {
|
|
|
172
174
|
|
|
173
175
|
async function gateCommands(root) {
|
|
174
176
|
try {
|
|
175
|
-
const
|
|
176
|
-
const commands = parsed?.gates?.commands;
|
|
177
|
+
const commands = (await loadManifest(root)).manifest?.gates?.commands;
|
|
177
178
|
return commands && typeof commands === 'object' ? commands : {};
|
|
178
179
|
} catch {
|
|
179
180
|
return {};
|
package/src/compare/row.mjs
CHANGED
|
@@ -21,7 +21,6 @@ import { checkTargets, PRIMARY_TARGET } from '../projection/project.mjs';
|
|
|
21
21
|
import { manifestDrift, readManifest } from '../projection/manifest.mjs';
|
|
22
22
|
import { loadRules, select } from '../projection/rules.mjs';
|
|
23
23
|
import { resolvePacks } from '../packs.mjs';
|
|
24
|
-
import { knownGates } from '../hooks/resolve.mjs';
|
|
25
24
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
26
25
|
import { readHooksState, wantsShim } from './hooksstate.mjs';
|
|
27
26
|
import { CLI } from '../name.mjs';
|
|
@@ -421,4 +420,3 @@ export function summarise(rows, extra = {}) {
|
|
|
421
420
|
};
|
|
422
421
|
}
|
|
423
422
|
|
|
424
|
-
export { knownGates };
|
package/src/context.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The per-run memo a command reads its repository through (review 0003 Q67).
|
|
2
|
+
//
|
|
3
|
+
// `check` did the same work twice in one run: `fingerprint(root)` at two sites, a
|
|
4
|
+
// manifest read that `manifestDrift` repeated, and a rules load that every consumer
|
|
5
|
+
// paid for again. Each fingerprint spawns `git ls-files` and walks the tree once per
|
|
6
|
+
// glob fact, so the second call was never cheap - it was invisible.
|
|
7
|
+
//
|
|
8
|
+
// The cache lives for one command, not for the process. `bench` writes manifests and
|
|
9
|
+
// rewrites trees mid-run; a process-lifetime memo would hand a benchmark the answer
|
|
10
|
+
// for a repository it had just changed, which is the one way a cache can be worse
|
|
11
|
+
// than no cache. A caller that wants a fresh answer makes a fresh context.
|
|
12
|
+
//
|
|
13
|
+
// A rejected computation is not cached: a transient failure (a git lock, a permission
|
|
14
|
+
// error) should not become the answer for the rest of the run, and every one of these
|
|
15
|
+
// readers is safe to retry.
|
|
16
|
+
|
|
17
|
+
import { fingerprint } from './resolve/fingerprint.mjs';
|
|
18
|
+
import { readManifest } from './projection/manifest.mjs';
|
|
19
|
+
import { loadRules, provenanceDigest } from './projection/rules.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One repository, read once per question.
|
|
23
|
+
*
|
|
24
|
+
* Each accessor is a promise, and two calls with the same key share it rather than
|
|
25
|
+
* racing: the first caller to ask starts the work, and everyone else awaits it.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} root the repository this context answers for
|
|
28
|
+
*/
|
|
29
|
+
export function repoContext(root) {
|
|
30
|
+
const cache = new Map();
|
|
31
|
+
const counts = { hits: 0, misses: 0 };
|
|
32
|
+
|
|
33
|
+
const once = (key, compute) => {
|
|
34
|
+
if (cache.has(key)) {
|
|
35
|
+
counts.hits += 1;
|
|
36
|
+
return cache.get(key);
|
|
37
|
+
}
|
|
38
|
+
counts.misses += 1;
|
|
39
|
+
const pending = Promise.resolve()
|
|
40
|
+
.then(compute)
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
// Not cached, or one failed read would answer every later caller.
|
|
43
|
+
cache.delete(key);
|
|
44
|
+
throw error;
|
|
45
|
+
});
|
|
46
|
+
cache.set(key, pending);
|
|
47
|
+
return pending;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
/** What the cache did, for a test or a measurement - never printed by a command. */
|
|
53
|
+
get stats() {
|
|
54
|
+
return { ...counts, entries: cache.size };
|
|
55
|
+
},
|
|
56
|
+
fingerprint: () => once('fingerprint', () => fingerprint(root)),
|
|
57
|
+
// The legacy shape, because that is what the commands print: `error` is the
|
|
58
|
+
// unreadable-file case and every caller already renders it. Memoising the loader's
|
|
59
|
+
// `problems` array here instead gave doctor `undefined` where it tests for null.
|
|
60
|
+
manifest: () => once('manifest', () => readManifest(root)),
|
|
61
|
+
rules: () => once('rules', () => loadRules(root)),
|
|
62
|
+
digest: () => once('digest', () => provenanceDigest()),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The decisions index document, rendered from the records (review 0003 Q91).
|
|
2
|
+
//
|
|
3
|
+
// The file was 590 hand-written lines: a table of every ADR kept in step by whoever
|
|
4
|
+
// remembered. It agreed with the records until it did not, and nothing read it, so
|
|
5
|
+
// nothing noticed - the failure the generated rule regions exist to prevent, one
|
|
6
|
+
// directory over. The table is output now: this module renders it, `simt rules
|
|
7
|
+
// decisions --yes` writes it, and test/decisions-index.test.mjs fails on drift.
|
|
8
|
+
//
|
|
9
|
+
// The narrative that trailed the table moved to docs/architecture/history.md: prose a
|
|
10
|
+
// reader consults is not prose a command rewrites.
|
|
11
|
+
|
|
12
|
+
const HEADER = `# Decisions
|
|
13
|
+
|
|
14
|
+
Architecture decision records for the SIMT development ecosystem. One canonical home per
|
|
15
|
+
decision; nothing here is duplicated into a repo's \`AGENTS.md\`.
|
|
16
|
+
|
|
17
|
+
A decision is superseded by writing a new ADR with a higher number and editing the old
|
|
18
|
+
record's status line — never by editing the ruling in place.
|
|
19
|
+
|
|
20
|
+
This table is generated from the records: \`simt rules decisions --yes\` writes it, and
|
|
21
|
+
\`test/decisions-index.test.mjs\` fails when it drifts. The history behind the numbers is
|
|
22
|
+
in [architecture/history.md](architecture/history.md).
|
|
23
|
+
|
|
24
|
+
| ADR | Decision | Status |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The index document, from the records.
|
|
30
|
+
*
|
|
31
|
+
* Ordered by number, not by filename: the number IS the order, and a directory listing
|
|
32
|
+
* is not (`ADR-100` sorts before `ADR-99` as text).
|
|
33
|
+
*
|
|
34
|
+
* @param {{id: string, title: string, status: string, file: string}[]} rows
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function renderDecisionsIndex(rows) {
|
|
38
|
+
const ordered = [...rows].sort((a, b) => number(a.id) - number(b.id));
|
|
39
|
+
const lines = ordered.map((row) => `| [${number(row.id)}](decisions/${row.file}) | ${row.title} | ${row.status} |`);
|
|
40
|
+
return `${HEADER}${lines.join('\n')}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `ADR-007` -> `007`, the number the table and the ordering both use. */
|
|
44
|
+
function number(id) {
|
|
45
|
+
return id.replace(/^ADR-/i, '').padStart(3, '0');
|
|
46
|
+
}
|
package/src/diff.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function readBefore(path) {
|
|
|
49
49
|
export function emitDiffs(pairs, flags) {
|
|
50
50
|
if (isJson(flags)) return;
|
|
51
51
|
const text = renderDiffs(pairs);
|
|
52
|
-
if (text !== '') emit(text
|
|
52
|
+
if (text !== '') emit(text);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** The `--diff` bytes for a JSON payload: absent entirely without the flag, so
|
package/src/distill/distill.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_BUDGET = 2048;
|
|
37
37
|
import { LOCAL_DIR } from '../state.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
38
39
|
|
|
39
40
|
const DISTILL_DIR = join(LOCAL_DIR, 'distilled');
|
|
40
41
|
|
|
@@ -175,7 +176,7 @@ export async function distillTarget(args, flags) {
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
const root = resolve(flags
|
|
179
|
+
const root = resolve(commandRoot(flags));
|
|
179
180
|
const source = await resolveTarget(target, root, flags);
|
|
180
181
|
if (source.exit !== undefined) return source.exit;
|
|
181
182
|
|
|
@@ -192,6 +193,16 @@ export async function distillTarget(args, flags) {
|
|
|
192
193
|
const cached = flags.refresh === true ? null : await readFile(absProposal, 'utf8').catch(() => null);
|
|
193
194
|
if (cached !== null) {
|
|
194
195
|
const fields = parseExistingFrontmatter(cached);
|
|
196
|
+
let faults = [];
|
|
197
|
+
if (typeof fields.faults === 'string') {
|
|
198
|
+
try {
|
|
199
|
+
const parsedFaults = JSON.parse(fields.faults);
|
|
200
|
+
if (Array.isArray(parsedFaults)) faults = parsedFaults.map(String);
|
|
201
|
+
} catch {
|
|
202
|
+
// A malformed faults line is not a reason to refuse the proposal; the proposal
|
|
203
|
+
// still says it is unverified, and `--refresh` re-derives the list.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
195
206
|
return {
|
|
196
207
|
ok: true,
|
|
197
208
|
command: 'distill',
|
|
@@ -201,6 +212,7 @@ export async function distillTarget(args, flags) {
|
|
|
201
212
|
budget: Number(fields.budget ?? budget),
|
|
202
213
|
citations: countCitations(cached),
|
|
203
214
|
verified: fields.verified !== 'false',
|
|
215
|
+
faults,
|
|
204
216
|
metric: { note: METRIC_NOTE },
|
|
205
217
|
proposal: { path: relProposal, state: 'cached', bytes: Buffer.byteLength(cached, 'utf8') },
|
|
206
218
|
};
|
|
@@ -227,6 +239,7 @@ export async function distillTarget(args, flags) {
|
|
|
227
239
|
engine: provenance,
|
|
228
240
|
budget,
|
|
229
241
|
verified: faults.length === 0,
|
|
242
|
+
faults,
|
|
230
243
|
}) + `\n${outcome.body}\n`;
|
|
231
244
|
|
|
232
245
|
await mkdir(join(root, DISTILL_DIR), { recursive: true });
|
package/src/distill/engine.mjs
CHANGED
|
@@ -167,7 +167,7 @@ export function verifyCitations(body, sourceLineCount) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** The proposal file's frontmatter. No wall-clock value here: byte-stable (P4). */
|
|
170
|
-
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory }) {
|
|
170
|
+
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory, faults = [] }) {
|
|
171
171
|
return [
|
|
172
172
|
'---',
|
|
173
173
|
`source: ${source}`,
|
|
@@ -178,6 +178,10 @@ export function proposalFrontmatter({ source, digest, engine, budget, prompt, ve
|
|
|
178
178
|
verdict ? `verdict: ${verdict}` : null,
|
|
179
179
|
advisory ? 'advisory: true' : null,
|
|
180
180
|
verified === false ? 'verified: false' : 'verified: true',
|
|
181
|
+
// The faults belong to the proposal, not only to the report that wrote it: a cached
|
|
182
|
+
// proposal re-read without them reported `verified:false` and nothing to fix
|
|
183
|
+
// (review 0003 Q52).
|
|
184
|
+
Array.isArray(faults) && faults.length > 0 ? `faults: ${JSON.stringify(faults)}` : null,
|
|
181
185
|
'status: proposal',
|
|
182
186
|
'---',
|
|
183
187
|
'',
|
package/src/graph/load.mjs
CHANGED
|
@@ -117,7 +117,9 @@ async function defaultRegistry(flags) {
|
|
|
117
117
|
* failure to resolve is a WARNING, not 38 `ENOENT` lines (ADR-014).
|
|
118
118
|
*
|
|
119
119
|
* Memoised per the inputs that decide the answer: the `--workspace` flag, the
|
|
120
|
-
* `SIMT_DEV_WORKSPACE` environment
|
|
120
|
+
* `SIMT_DEV_WORKSPACE` environment, the user-config location, and the current
|
|
121
|
+
* directory, whose manifest declares the workspace the variable may point into
|
|
122
|
+
* (ADR-119). A single
|
|
121
123
|
* first-call-wins promise served one caller's workspace to every later caller with
|
|
122
124
|
* different flags (Q14) — `skills_index({workspace: ws})` followed by
|
|
123
125
|
* `skills_index({})` answered `ws` twice. Callers with the same inputs still share
|
|
@@ -129,6 +131,7 @@ export function workspace(flags = {}) {
|
|
|
129
131
|
flags?.workspace ?? null,
|
|
130
132
|
process.env.SIMT_DEV_WORKSPACE ?? null,
|
|
131
133
|
process.env.XDG_CONFIG_HOME ?? null,
|
|
134
|
+
process.cwd(),
|
|
132
135
|
]);
|
|
133
136
|
if (!workspacePromises.has(key)) workspacePromises.set(key, resolveWorkspace(flags));
|
|
134
137
|
return workspacePromises.get(key);
|
package/src/hooks/capability.mjs
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
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 { ECOSYSTEMS
|
|
24
|
+
import { ECOSYSTEMS } from '../projection/manifest.mjs';
|
|
25
|
+
import { loadManifest } from '../manifest/schema.mjs';
|
|
25
26
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
26
27
|
|
|
27
28
|
/** Binaries the shared dispatcher shells out to directly, rather than via a script. */
|
|
@@ -170,8 +171,7 @@ export async function readCapability(root) {
|
|
|
170
171
|
// so the only evidence that can prove it is the declaration itself. A repo with no
|
|
171
172
|
// manifest simply has no declared commands.
|
|
172
173
|
try {
|
|
173
|
-
|
|
174
|
-
capability.commands = declaredCommands(manifest);
|
|
174
|
+
capability.commands = declaredCommands((await loadManifest(root)).manifest);
|
|
175
175
|
} catch {
|
|
176
176
|
capability.commands = {};
|
|
177
177
|
}
|