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/lessons.mjs
CHANGED
|
@@ -17,12 +17,13 @@ import {
|
|
|
17
17
|
promoteLesson,
|
|
18
18
|
readLessonCandidates,
|
|
19
19
|
} from '../memory/lessons.mjs';
|
|
20
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
20
21
|
|
|
21
22
|
export async function lessonsListCommand(args, flags) {
|
|
22
23
|
if (args.length > 0) {
|
|
23
24
|
return fail(`unknown lessons argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons`);
|
|
24
25
|
}
|
|
25
|
-
const root = resolve(flags
|
|
26
|
+
const root = resolve(commandRoot(flags));
|
|
26
27
|
// Pure: listing must not capture. Capture used to run here, so `list` wrote
|
|
27
28
|
// to the store it was reading and a read-only question had write side
|
|
28
29
|
// effects. Capturing is `lessons capture` now (review 0003 Q36).
|
|
@@ -48,7 +49,7 @@ export async function lessonsListCommand(args, flags) {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
if (isJson(flags)) {
|
|
51
|
-
emitJson(payload
|
|
52
|
+
emitJson(payload);
|
|
52
53
|
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -81,7 +82,7 @@ export async function lessonsListCommand(args, flags) {
|
|
|
81
82
|
|
|
82
83
|
for (const err of errors) lines.push(`error ${err}`);
|
|
83
84
|
|
|
84
|
-
emit(lines.join('\n')
|
|
85
|
+
emit(lines.join('\n'));
|
|
85
86
|
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -96,7 +97,7 @@ export async function lessonsCaptureCommand(args, flags) {
|
|
|
96
97
|
if (args.length > 0) {
|
|
97
98
|
return fail(`unknown lessons capture argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons capture`);
|
|
98
99
|
}
|
|
99
|
-
const root = resolve(flags
|
|
100
|
+
const root = resolve(commandRoot(flags));
|
|
100
101
|
const fromLedger = await captureFromLedger(root);
|
|
101
102
|
const reverts = await captureRevertedCommits(root);
|
|
102
103
|
|
|
@@ -109,7 +110,7 @@ export async function lessonsCaptureCommand(args, flags) {
|
|
|
109
110
|
captured: [...fromLedger, ...reverts],
|
|
110
111
|
};
|
|
111
112
|
if (isJson(flags)) {
|
|
112
|
-
emitJson(payload
|
|
113
|
+
emitJson(payload);
|
|
113
114
|
return EXIT.OK;
|
|
114
115
|
}
|
|
115
116
|
const lines = [
|
|
@@ -120,7 +121,7 @@ export async function lessonsCaptureCommand(args, flags) {
|
|
|
120
121
|
for (const c of payload.captured) {
|
|
121
122
|
lines.push(` ${c.id} ${c.kind} ${c.summary}`);
|
|
122
123
|
}
|
|
123
|
-
emit(lines.join('\n')
|
|
124
|
+
emit(lines.join('\n'));
|
|
124
125
|
return EXIT.OK;
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -128,7 +129,7 @@ export async function lessonsReviewCommand(args, flags) {
|
|
|
128
129
|
if (args.length > 1) {
|
|
129
130
|
return fail(`unknown lessons review argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} lessons review [<id>] [--yes]`);
|
|
130
131
|
}
|
|
131
|
-
const root = resolve(flags
|
|
132
|
+
const root = resolve(commandRoot(flags));
|
|
132
133
|
const mode = writeMode(flags);
|
|
133
134
|
const parsedPaths = parsePathList(flags.paths);
|
|
134
135
|
const paths = parsedPaths.length > 0 ? parsedPaths : undefined;
|
|
@@ -173,7 +174,7 @@ export async function lessonsReviewCommand(args, flags) {
|
|
|
173
174
|
};
|
|
174
175
|
|
|
175
176
|
if (isJson(flags)) {
|
|
176
|
-
emitJson(payload
|
|
177
|
+
emitJson(payload);
|
|
177
178
|
return EXIT.OK;
|
|
178
179
|
}
|
|
179
180
|
|
|
@@ -185,7 +186,7 @@ export async function lessonsReviewCommand(args, flags) {
|
|
|
185
186
|
result.markdown.trimEnd(),
|
|
186
187
|
];
|
|
187
188
|
|
|
188
|
-
emit(lines.join('\n')
|
|
189
|
+
emit(lines.join('\n'));
|
|
189
190
|
// After the report: the report says what, the diff shows it (R50).
|
|
190
191
|
emitDiffs(diffs, flags);
|
|
191
192
|
if (!mode.apply) {
|
package/src/commands/local.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { resolve } from 'node:path';
|
|
|
8
8
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
9
|
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
10
10
|
import { probeLocal } from '../local/client.mjs';
|
|
11
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
11
12
|
|
|
12
13
|
function formatRoleLine(role, report) {
|
|
13
14
|
if (!report) return `${role.padEnd(8)} (unset)`;
|
|
@@ -55,7 +56,7 @@ function remedyFor(result) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export async function localDoctorCommand(args, flags) {
|
|
58
|
-
const root = resolve(flags
|
|
59
|
+
const root = resolve(commandRoot(flags, args));
|
|
59
60
|
const allowRemote = flags.allowRemote === true;
|
|
60
61
|
const result = await probeLocal(root, { allowRemote });
|
|
61
62
|
|
|
@@ -86,7 +87,6 @@ export async function localDoctorCommand(args, flags) {
|
|
|
86
87
|
findings: result.findings,
|
|
87
88
|
...(error ? { error, remedy, exitCode: code } : {}),
|
|
88
89
|
},
|
|
89
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
90
90
|
);
|
|
91
91
|
return code;
|
|
92
92
|
}
|
|
@@ -99,7 +99,7 @@ export async function localDoctorCommand(args, flags) {
|
|
|
99
99
|
`sockets ${result.socketsOpened}`,
|
|
100
100
|
];
|
|
101
101
|
|
|
102
|
-
emit(lines.join('\n')
|
|
102
|
+
emit(lines.join('\n'));
|
|
103
103
|
|
|
104
104
|
if (!result.ok) {
|
|
105
105
|
return fail(error, code, remedy);
|
package/src/commands/map.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// `build` on are ones a person typed at a terminal, where a few hundred
|
|
6
6
|
// milliseconds is not a bar to meet.
|
|
7
7
|
|
|
8
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
8
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
9
9
|
import { CLI } from '../name.mjs';
|
|
10
10
|
import { isIndexStale, readEdges, readEmbeddings, readFiles, readFts, readSymbols, reindex, searchFtsBm25 } from '../index/store.mjs';
|
|
11
11
|
import {
|
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
rankFiles,
|
|
17
17
|
renderMap,
|
|
18
18
|
} from '../index/map.mjs';
|
|
19
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
19
20
|
|
|
20
21
|
const LEVELS = new Set(['outline', 'names', 'signatures']);
|
|
21
22
|
|
|
22
23
|
export async function mapCommand(args, flags) {
|
|
23
24
|
if (args.length > 0) return fail(`unknown map argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} map`);
|
|
24
|
-
const root = flags
|
|
25
|
+
const root = commandRoot(flags);
|
|
25
26
|
if (flags.recall === true) return recall(root, flags);
|
|
26
27
|
const level = flags.level ?? 'outline';
|
|
27
28
|
if (!LEVELS.has(level)) {
|
|
@@ -44,12 +45,19 @@ export async function mapCommand(args, flags) {
|
|
|
44
45
|
: null;
|
|
45
46
|
const ranked = rankFiles({ ...loaded, bm25, queryVector, task });
|
|
46
47
|
const rendered = renderMap(ranked, loaded.symbols, { level, budget });
|
|
48
|
+
// What the ranking is an answer to. `graph-only` says no file or symbol mentioned the
|
|
49
|
+
// task and the list is the graph hub; reading that as a hit is how `map --task
|
|
50
|
+
// zebracorn` looked useful (review 0003 Q52).
|
|
51
|
+
const matched = ranked.filter((row) => (row.score ?? 0) > 0).length;
|
|
52
|
+
const basis = task.trim().length === 0 ? 'no-task' : matched > 0 ? 'matched' : 'graph-only';
|
|
47
53
|
const payload = {
|
|
48
54
|
ok: true,
|
|
49
55
|
command: 'map',
|
|
50
56
|
root,
|
|
51
57
|
level,
|
|
52
58
|
budget,
|
|
59
|
+
matched,
|
|
60
|
+
basis,
|
|
53
61
|
shown: rendered.shown,
|
|
54
62
|
total: rendered.total,
|
|
55
63
|
truncated: rendered.truncated,
|
|
@@ -57,17 +65,18 @@ export async function mapCommand(args, flags) {
|
|
|
57
65
|
text: rendered.text,
|
|
58
66
|
};
|
|
59
67
|
if (isJson(flags)) {
|
|
60
|
-
emitJson(payload
|
|
68
|
+
emitJson(payload);
|
|
61
69
|
return EXIT.OK;
|
|
62
70
|
}
|
|
63
71
|
const lines = [
|
|
64
72
|
`level ${level}`,
|
|
73
|
+
`basis ${basis}${basis === 'graph-only' ? ' (no file or symbol mentions this task; the list is the graph hub)' : ''}`,
|
|
65
74
|
`shown ${rendered.shown}`,
|
|
66
75
|
`total ${rendered.total}`,
|
|
67
76
|
`budget ${budget}`,
|
|
68
77
|
];
|
|
69
78
|
if (rendered.text) lines.push(rendered.text);
|
|
70
|
-
emit(lines.join('\n')
|
|
79
|
+
emit(lines.join('\n'));
|
|
71
80
|
return EXIT.OK;
|
|
72
81
|
}
|
|
73
82
|
|
|
@@ -82,11 +91,7 @@ async function recall(root, flags) {
|
|
|
82
91
|
return fail(`could not measure recall (${error.message})`, EXIT.UNSUPPORTED, `run ${CLI} index in a git repository`);
|
|
83
92
|
}
|
|
84
93
|
const payload = { ok: true, command: 'map', root, recall: result };
|
|
85
|
-
|
|
86
|
-
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
87
|
-
return EXIT.OK;
|
|
88
|
-
}
|
|
89
|
-
emit(formatRecall(result), { maxBytes: flags.maxBytes ?? 0 });
|
|
94
|
+
present(payload, () => formatRecall(result), flags);
|
|
90
95
|
return EXIT.OK;
|
|
91
96
|
}
|
|
92
97
|
|
package/src/commands/mcp.mjs
CHANGED
package/src/commands/pack.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../packs/manage.mjs';
|
|
29
29
|
import { readManifest } from '../projection/manifest.mjs';
|
|
30
30
|
import { diffField, emitDiffs } from '../diff.mjs';
|
|
31
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
31
32
|
|
|
32
33
|
export async function packCommand(sub, args, flags) {
|
|
33
34
|
if (sub === 'new') return packNew(args, flags);
|
|
@@ -40,9 +41,13 @@ export async function packCommand(sub, args, flags) {
|
|
|
40
41
|
* Where a lint or publish resolves its pack: the positional directory, the
|
|
41
42
|
* `--root`, or the working directory. `hooks` reads the same three in the same
|
|
42
43
|
* order, so the two commands cannot disagree about which directory was meant.
|
|
44
|
+
*
|
|
45
|
+
* The positional wins, as this always said it did. The code read `--root` first,
|
|
46
|
+
* so `pack lint .simt/packs/a --root ../b` judged `b` while the prose and the
|
|
47
|
+
* command's own usage promised `a` (review 0003 Q58).
|
|
43
48
|
*/
|
|
44
49
|
function packDir(args, flags) {
|
|
45
|
-
return resolve(
|
|
50
|
+
return resolve(args[0] ?? commandRoot(flags));
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
/**
|
|
@@ -98,7 +103,7 @@ async function packNew(args, flags) {
|
|
|
98
103
|
return fail(sourceProblem, EXIT.USAGE, 'a pack source names where the bytes come from: `npm:<package>` or `git+https://…`');
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
const dir = join(resolve(flags
|
|
106
|
+
const dir = join(resolve(commandRoot(flags)), name);
|
|
102
107
|
const files = scaffoldPack({ name, source });
|
|
103
108
|
const { apply, dryRun } = writeMode(flags);
|
|
104
109
|
// A scaffold always creates into a directory that must not exist, so every
|
|
@@ -129,7 +134,6 @@ async function packNew(args, flags) {
|
|
|
129
134
|
'',
|
|
130
135
|
dryRun ? 'DRY-RUN no files written' : `PLAN nothing written; apply with: ${CLI} pack new ${name} --source ${source} --yes`,
|
|
131
136
|
].join('\n'),
|
|
132
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
133
137
|
);
|
|
134
138
|
// After the report: the report says what, the diff shows it (R50).
|
|
135
139
|
emitDiffs(diffs, flags);
|
|
@@ -149,7 +153,6 @@ async function packNew(args, flags) {
|
|
|
149
153
|
'',
|
|
150
154
|
`WROTE ${files.length} scaffold file(s); write the rule, then: ${CLI} pack lint ${dir}`,
|
|
151
155
|
].join('\n'),
|
|
152
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
153
156
|
);
|
|
154
157
|
// After the report: the report says what, the diff shows it (R50).
|
|
155
158
|
emitDiffs(diffs, flags);
|
|
@@ -187,7 +190,7 @@ async function packLint(args, flags) {
|
|
|
187
190
|
lines.push(` remedy: ${f.remedy}`);
|
|
188
191
|
}
|
|
189
192
|
lines.push('', ok ? 'PASSED the pack is well-formed' : `FAILED ${findings.length} finding(s)`);
|
|
190
|
-
emit(lines.join('\n')
|
|
193
|
+
emit(lines.join('\n'));
|
|
191
194
|
return ok ? EXIT.OK : EXIT.FAIL;
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -247,7 +250,7 @@ async function packPublish(args, flags) {
|
|
|
247
250
|
} else {
|
|
248
251
|
lines.push('', `FAILED ${result.findings.length} finding(s)`);
|
|
249
252
|
}
|
|
250
|
-
emit(lines.join('\n')
|
|
253
|
+
emit(lines.join('\n'));
|
|
251
254
|
if (result.ok && apply && result.published) {
|
|
252
255
|
note(`note never committed or pushed by this tool (ADR-001); the registry now has the bytes, the repos still need vendoring`);
|
|
253
256
|
}
|
package/src/commands/plan.mjs
CHANGED
|
@@ -3,12 +3,13 @@ import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.m
|
|
|
3
3
|
import { formatCounts, planDone, planNext, planStatus } from '../plan/index.mjs';
|
|
4
4
|
import { diffField, emitDiffs } from '../diff.mjs';
|
|
5
5
|
import { CLI } from '../name.mjs';
|
|
6
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
6
7
|
|
|
7
8
|
// Preview unless --yes, and --dry-run wins even then. A tracker row marked done
|
|
8
9
|
// for a commit that was never written is a claim the next reader will believe.
|
|
9
10
|
|
|
10
11
|
export async function planCommand(sub, args, flags) {
|
|
11
|
-
const root = flags
|
|
12
|
+
const root = commandRoot(flags);
|
|
12
13
|
if (sub === 'status') return runStatus(root, flags);
|
|
13
14
|
if (sub === 'next') return runNext(root, args[0] ?? null, flags);
|
|
14
15
|
if (sub === 'done') {
|
|
@@ -35,7 +36,7 @@ async function runStatus(root, flags) {
|
|
|
35
36
|
}
|
|
36
37
|
lines.push('', `total ${formatCounts(result.totals, result.stateOrder) || '-'}`);
|
|
37
38
|
lines.push(`ready ${result.ready}`);
|
|
38
|
-
emit(lines.join('\n')
|
|
39
|
+
emit(lines.join('\n'));
|
|
39
40
|
return EXIT.OK;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -55,7 +56,7 @@ async function runNext(root, id, flags) {
|
|
|
55
56
|
lines.push(` ${row.id} ${row.doc} depends on: ${row.dependsOn || '(none)'}${unknown}`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
|
-
emit(lines.join('\n')
|
|
59
|
+
emit(lines.join('\n'));
|
|
59
60
|
return EXIT.OK;
|
|
60
61
|
}
|
|
61
62
|
const next = result.next;
|
|
@@ -68,7 +69,7 @@ async function runNext(root, id, flags) {
|
|
|
68
69
|
'',
|
|
69
70
|
next.section || '(no section for this id)',
|
|
70
71
|
];
|
|
71
|
-
emit(head.join('\n')
|
|
72
|
+
emit(head.join('\n'));
|
|
72
73
|
return EXIT.OK;
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -104,7 +105,7 @@ async function runDone(root, id, sha, flags) {
|
|
|
104
105
|
}
|
|
105
106
|
if (result.evidenceRecorded) lines.push(`evidence ${result.sha}${result.test ? ` test: ${result.test}` : ''}`);
|
|
106
107
|
else if (result.evidenceNote) lines.push(`note ${result.evidenceNote}`);
|
|
107
|
-
emit(lines.join('\n')
|
|
108
|
+
emit(lines.join('\n'));
|
|
108
109
|
// After the report: the report says what, the diff shows it (R50).
|
|
109
110
|
emitDiffs(diffs, flags);
|
|
110
111
|
return EXIT.OK;
|
package/src/commands/plugin.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.m
|
|
|
10
10
|
import { CLI } from '../name.mjs';
|
|
11
11
|
import { buildPackage, PLUGIN_KINDS } from '../plugins.mjs';
|
|
12
12
|
import { diffField, emitDiffs } from '../diff.mjs';
|
|
13
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
13
14
|
|
|
14
15
|
export async function pluginCommand(args, flags) {
|
|
15
16
|
const kind = args[0];
|
|
@@ -27,7 +28,7 @@ export async function pluginCommand(args, flags) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const { apply } = writeMode(flags);
|
|
30
|
-
const root = flags
|
|
31
|
+
const root = commandRoot(flags);
|
|
31
32
|
const plan = await buildPackage(kind, { root, out, apply });
|
|
32
33
|
|
|
33
34
|
// The `--diff` pairs: the generated files, created or refreshed (R50). The
|
|
@@ -68,7 +69,6 @@ export async function pluginCommand(args, flags) {
|
|
|
68
69
|
? `WROTE ${plan.files.length} generated file(s) plus the runtime`
|
|
69
70
|
: `PLAN nothing written; apply with: ${CLI} plugin ${kind} --out ${out} --yes`,
|
|
70
71
|
].join('\n'),
|
|
71
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
72
72
|
);
|
|
73
73
|
// After the report: the report says what, the diff shows it (R50).
|
|
74
74
|
emitDiffs(diffs, flags);
|
package/src/commands/prbody.mjs
CHANGED
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
13
13
|
import { CLI } from '../name.mjs';
|
|
14
14
|
import { buildPrBody, renderPrBody } from '../prbody.mjs';
|
|
15
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
15
16
|
|
|
16
17
|
export async function prBodyCommand(args, flags) {
|
|
17
|
-
const root = flags
|
|
18
|
+
const root = commandRoot(flags);
|
|
18
19
|
if (args.length > 0) {
|
|
19
20
|
return fail(`pr-body takes no positional arguments`, EXIT.USAGE, `run: ${CLI} pr-body [--base <ref>]`);
|
|
20
21
|
}
|
|
@@ -36,10 +37,9 @@ export async function prBodyCommand(args, flags) {
|
|
|
36
37
|
verdict: report.verdict,
|
|
37
38
|
unreviewed: report.unreviewed,
|
|
38
39
|
},
|
|
39
|
-
{ pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 },
|
|
40
40
|
);
|
|
41
41
|
return EXIT.OK;
|
|
42
42
|
}
|
|
43
|
-
emit(renderPrBody(report)
|
|
43
|
+
emit(renderPrBody(report));
|
|
44
44
|
return EXIT.OK;
|
|
45
45
|
}
|
|
@@ -7,10 +7,11 @@ import { execFile } from 'node:child_process';
|
|
|
7
7
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
8
8
|
import { CLI } from '../name.mjs';
|
|
9
9
|
import { JSONL_FILE, SQLITE_FILE, reindex } from '../index/store.mjs';
|
|
10
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
10
11
|
|
|
11
12
|
export async function indexCommand(args, flags) {
|
|
12
13
|
if (args.length > 0) return fail(`unknown index argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} index`);
|
|
13
|
-
const root = flags
|
|
14
|
+
const root = commandRoot(flags);
|
|
14
15
|
let result;
|
|
15
16
|
try {
|
|
16
17
|
result = await reindex(root, {
|
|
@@ -31,7 +32,7 @@ export async function indexCommand(args, flags) {
|
|
|
31
32
|
const ignored = await isIgnored(root, file);
|
|
32
33
|
const payload = { ok: true, command: 'index', root, file, ignored, ...result };
|
|
33
34
|
if (isJson(flags)) {
|
|
34
|
-
emitJson(payload
|
|
35
|
+
emitJson(payload);
|
|
35
36
|
return EXIT.OK;
|
|
36
37
|
}
|
|
37
38
|
const lines = [
|
|
@@ -46,7 +47,7 @@ export async function indexCommand(args, flags) {
|
|
|
46
47
|
];
|
|
47
48
|
if (result.parsers === 'missing') lines.push('note index.parsers did not load, so the built-in extractors ran');
|
|
48
49
|
if (!ignored) lines.push(`note ${file} is not gitignored, and the cache is not meant to be committed`);
|
|
49
|
-
emit(lines.join('\n')
|
|
50
|
+
emit(lines.join('\n'));
|
|
50
51
|
return EXIT.OK;
|
|
51
52
|
}
|
|
52
53
|
|
package/src/commands/review.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export async function reviewCommand(args, flags, commands = []) {
|
|
|
23
23
|
if (typeof result === 'number') return result;
|
|
24
24
|
|
|
25
25
|
if (isJson(flags)) {
|
|
26
|
-
emitJson(result
|
|
26
|
+
emitJson(result);
|
|
27
27
|
return EXIT.OK;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -60,6 +60,6 @@ export async function reviewCommand(args, flags, commands = []) {
|
|
|
60
60
|
}
|
|
61
61
|
if (result.metric) lines.push(`metric ${result.metric.note}`);
|
|
62
62
|
|
|
63
|
-
emit(lines.join('\n')
|
|
63
|
+
emit(lines.join('\n'));
|
|
64
64
|
return EXIT.OK;
|
|
65
65
|
}
|
package/src/commands/rules.mjs
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
// reviewer needs and exactly the fact an empty output would hide.
|
|
16
16
|
|
|
17
17
|
import { readdir } from 'node:fs/promises';
|
|
18
|
+
import { repoContext } from '../context.mjs';
|
|
18
19
|
import { join } from 'node:path';
|
|
20
|
+
import { renderDecisionsIndex } from '../decisions/index-doc.mjs';
|
|
21
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
19
22
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
20
23
|
import { loadRules, select } from '../projection/project.mjs';
|
|
21
24
|
import { loadMdcRules } from '../projection/mdc.mjs';
|
|
@@ -49,8 +52,8 @@ function sorted(rules) {
|
|
|
49
52
|
return [...rules].sort((a, b) => a.id.localeCompare(b.id));
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
async function load(root) {
|
|
53
|
-
const rules = await
|
|
55
|
+
async function load(root, ctx = repoContext(root)) {
|
|
56
|
+
const rules = await ctx.rules();
|
|
54
57
|
const broken = rules.filter((r) => r.error);
|
|
55
58
|
return { rules: [...rules, ...(await loadPerSurfaceRules())], broken };
|
|
56
59
|
}
|
|
@@ -153,12 +156,12 @@ export async function listCommand(args, flags) {
|
|
|
153
156
|
if (locals.length > 0) lines.push('', `local profiles come from ${locals.map((r) => r.source).join(', ')}`);
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
emit(lines.join('\n')
|
|
159
|
+
emit(lines.join('\n'));
|
|
157
160
|
return broken.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
|
-
async function selectionFor(rules, root) {
|
|
161
|
-
const { stack, facts } = await fingerprint(
|
|
163
|
+
async function selectionFor(rules, root, ctx = repoContext(root)) {
|
|
164
|
+
const { stack, facts } = await ctx.fingerprint();
|
|
162
165
|
const { selected, notes } = select(rules, stack, facts);
|
|
163
166
|
return { selected: new Set(selected.map((r) => r.id)), notes };
|
|
164
167
|
}
|
|
@@ -210,9 +213,11 @@ async function loadDecisionsFor(root, all) {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
/** The catalogue both surfaces answer from: every rule, its decisions, and the selection. */
|
|
213
|
-
export async function rulesCatalogue(root, { selectFor = null } = {}) {
|
|
214
|
-
const { rules, broken } = await load(root);
|
|
215
|
-
|
|
216
|
+
export async function rulesCatalogue(root, { selectFor = null, ctx = null } = {}) {
|
|
217
|
+
const { rules, broken } = await load(root, ctx ?? repoContext(root));
|
|
218
|
+
// `selectFor` is a different repository: the caller asked for the catalogue here and
|
|
219
|
+
// the selection there, so it gets its own context rather than this repository answer.
|
|
220
|
+
const selection = selectFor ? await selectionFor(rules, selectFor, repoContext(selectFor)) : null;
|
|
216
221
|
return {
|
|
217
222
|
rules: sorted(rules).map((r) => ({
|
|
218
223
|
id: r.id,
|
|
@@ -236,8 +241,8 @@ export async function rulesCatalogue(root, { selectFor = null } = {}) {
|
|
|
236
241
|
* The provenance facts for one rule or one decision record, in the shape the
|
|
237
242
|
* JSON documents carry. Text renders from the same facts; nothing re-derives.
|
|
238
243
|
*/
|
|
239
|
-
export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
240
|
-
const { rules } = await load(root);
|
|
244
|
+
export async function rulesWhyFacts(root, what, { all = false, ctx = null } = {}) {
|
|
245
|
+
const { rules } = await load(root, ctx ?? repoContext(root));
|
|
241
246
|
if (/^ADR-\d{3,}$/i.test(what)) {
|
|
242
247
|
const id = what.toUpperCase();
|
|
243
248
|
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
|
|
@@ -312,7 +317,7 @@ export async function showCommand(args, flags) {
|
|
|
312
317
|
|
|
313
318
|
const adrs = citations(rule.body);
|
|
314
319
|
if (isJson(flags)) {
|
|
315
|
-
emitJson({ ok: !rule.error, ...rule, adrs }
|
|
320
|
+
emitJson({ ok: !rule.error, ...rule, adrs });
|
|
316
321
|
return rule.error ? EXIT.FAIL : EXIT.OK;
|
|
317
322
|
}
|
|
318
323
|
|
|
@@ -329,7 +334,6 @@ export async function showCommand(args, flags) {
|
|
|
329
334
|
'',
|
|
330
335
|
rule.body,
|
|
331
336
|
].join('\n'),
|
|
332
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
333
337
|
);
|
|
334
338
|
return rule.error ? EXIT.FAIL : EXIT.OK;
|
|
335
339
|
}
|
|
@@ -370,7 +374,7 @@ function ruleDirection(facts, flags) {
|
|
|
370
374
|
' this rule cites no decision, so nothing settles it and nothing can overturn it.',
|
|
371
375
|
' Either cite the ADR that produced it, or accept that it is this repo\'s own policy.',
|
|
372
376
|
);
|
|
373
|
-
emit(lines.join('\n')
|
|
377
|
+
emit(lines.join('\n'));
|
|
374
378
|
return EXIT.OK;
|
|
375
379
|
}
|
|
376
380
|
|
|
@@ -395,7 +399,7 @@ function ruleDirection(facts, flags) {
|
|
|
395
399
|
lines.push('', `shares a decision with ${facts.others.join(', ')}`);
|
|
396
400
|
}
|
|
397
401
|
|
|
398
|
-
emit(lines.join('\n')
|
|
402
|
+
emit(lines.join('\n'));
|
|
399
403
|
return EXIT.OK;
|
|
400
404
|
}
|
|
401
405
|
|
|
@@ -486,7 +490,7 @@ function adrDirection(facts, flags) {
|
|
|
486
490
|
);
|
|
487
491
|
}
|
|
488
492
|
|
|
489
|
-
emit(lines.join('\n')
|
|
493
|
+
emit(lines.join('\n'));
|
|
490
494
|
// A superseded record is not a failure; it is the supersession rule working.
|
|
491
495
|
// The caller asked a question and got a true answer.
|
|
492
496
|
return EXIT.OK;
|
|
@@ -519,6 +523,19 @@ export async function decisionsCommand(args, flags) {
|
|
|
519
523
|
file: d.file,
|
|
520
524
|
}));
|
|
521
525
|
|
|
526
|
+
// The index is output, not authorship (review 0003 Q91): the table is rendered from
|
|
527
|
+
// the same records this command lists, so a new ADR cannot leave it behind. The
|
|
528
|
+
// write is journaled like every other repo-file write, so `undo` restores the old
|
|
529
|
+
// table - the index is not internal state even though it is generated.
|
|
530
|
+
if (flags.yes === true) {
|
|
531
|
+
const rootDir = root ?? process.cwd();
|
|
532
|
+
const journal = [];
|
|
533
|
+
await journaledWriteFile(rootDir, journal, join(rootDir, 'docs', 'decisions-index.md'), renderDecisionsIndex(rows));
|
|
534
|
+
await commitJournal(rootDir, 'rules decisions', journal);
|
|
535
|
+
emit(`wrote docs/decisions-index.md (${rows.length} decisions)`);
|
|
536
|
+
return EXIT.OK;
|
|
537
|
+
}
|
|
538
|
+
|
|
522
539
|
if (isJson(flags)) {
|
|
523
540
|
emitJson({ ok: true, command: 'rules decisions', root, decisions: rows });
|
|
524
541
|
return EXIT.OK;
|
|
@@ -534,6 +551,6 @@ export async function decisionsCommand(args, flags) {
|
|
|
534
551
|
const head = r.status.length > 80 ? `${r.status.slice(0, 80)}...` : r.status;
|
|
535
552
|
lines.push(`${r.id.padEnd(8)} ${r.origin.padEnd(7)} ${r.title} — ${head}`);
|
|
536
553
|
}
|
|
537
|
-
emit(lines.join('\n')
|
|
554
|
+
emit(lines.join('\n'));
|
|
538
555
|
return EXIT.OK;
|
|
539
556
|
}
|
package/src/commands/shared.mjs
CHANGED
|
@@ -189,7 +189,7 @@ export async function sharedPlanCommand(_args, flags) {
|
|
|
189
189
|
if (over !== null) lines.push(`warn ${over}`);
|
|
190
190
|
}
|
|
191
191
|
lines.push(`nothing written; re-run with: ${CLI} shared install --yes`);
|
|
192
|
-
emit(lines.join('\n')
|
|
192
|
+
emit(lines.join('\n'));
|
|
193
193
|
return EXIT.OK;
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -306,7 +306,6 @@ export async function sharedInstallCommand(_args, flags) {
|
|
|
306
306
|
`manifest ${manifests.map((m) => `${m.action}:${basename(m.root)}`).join(' ')}`,
|
|
307
307
|
dryRun ? 'dry-run nothing written' : `nothing written; re-run with: ${CLI} shared install --yes`,
|
|
308
308
|
].join('\n'),
|
|
309
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
310
309
|
);
|
|
311
310
|
// After the report: the report says what, the diff shows it (R50).
|
|
312
311
|
emitDiffs(diffs, flags);
|
|
@@ -354,7 +353,7 @@ export async function sharedInstallCommand(_args, flags) {
|
|
|
354
353
|
if (over !== null) lines.push(`warn ${over}`);
|
|
355
354
|
}
|
|
356
355
|
lines.push(`verify ${CLI} doctor reports the copy as current, and ${CLI} tokens --repos --root . reports what is left`);
|
|
357
|
-
emit(lines.join('\n')
|
|
356
|
+
emit(lines.join('\n'));
|
|
358
357
|
// After the report: the report says what, the diff shows it (R50).
|
|
359
358
|
emitDiffs(diffs, flags);
|
|
360
359
|
return EXIT.OK;
|