simt-dev 0.3.0 → 0.4.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 +172 -0
- package/README.md +138 -74
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +1 -1
- package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +2 -2
- package/rules/core.md +2 -2
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +34 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +85 -39
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +346 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +197 -0
- package/src/ci/workflow.mjs +11 -1
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +53 -16
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/help.mjs +65 -3
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +52 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +126 -0
- package/src/commands/repoindex.mjs +55 -0
- package/src/commands/rules.mjs +17 -2
- package/src/commands/shared.mjs +398 -0
- package/src/commands/skills.mjs +3 -17
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +87 -3
- package/src/commands/tests.mjs +145 -0
- package/src/commands/tokens.mjs +30 -1
- package/src/commands/why.mjs +172 -0
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +89 -0
- package/src/index/govern.mjs +234 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +627 -0
- package/src/index/persist.mjs +239 -0
- package/src/index/store.mjs +515 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +147 -0
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +234 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/measure/context.mjs +7 -5
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/disclosure.mjs +92 -0
- package/src/projection/lock.mjs +125 -0
- package/src/projection/manifest.mjs +52 -0
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +407 -49
- package/src/projection/shared.mjs +428 -0
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +8 -0
package/src/commands/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
whyCommand as skillsWhy,
|
|
26
26
|
evalCommand as skillsEval,
|
|
27
27
|
} from './skills.mjs';
|
|
28
|
+
import { briefCommand } from './brief.mjs';
|
|
28
29
|
import { syncCommand, checkCommand } from './sync.mjs';
|
|
29
30
|
import { initCommand } from './init.mjs';
|
|
30
31
|
import { ciCommand } from './ci.mjs';
|
|
@@ -34,6 +35,7 @@ import { compareCommand } from './compare.mjs';
|
|
|
34
35
|
import { upgradeCommand } from './upgrade.mjs';
|
|
35
36
|
import { mcpCommand } from './mcp.mjs';
|
|
36
37
|
import { installCommand } from './skillsinstall.mjs';
|
|
38
|
+
import { compileCommand } from './skillscompile.mjs';
|
|
37
39
|
import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
|
|
38
40
|
import { tokensCommand } from './tokens.mjs';
|
|
39
41
|
import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
|
|
@@ -43,9 +45,22 @@ import {
|
|
|
43
45
|
benchReportCommand,
|
|
44
46
|
benchAblateCommand,
|
|
45
47
|
benchProbesCommand,
|
|
48
|
+
benchVariantsCommand,
|
|
46
49
|
} from './bench.mjs';
|
|
47
50
|
import { planCommand } from './plan.mjs';
|
|
51
|
+
import { sharedPlanCommand, sharedInstallCommand, sharedWarning } from './shared.mjs';
|
|
52
|
+
import { evidenceCommand } from './evidence.mjs';
|
|
53
|
+
import { indexCommand } from './repoindex.mjs';
|
|
54
|
+
import { mapCommand } from './map.mjs';
|
|
55
|
+
import { testsCommand } from './tests.mjs';
|
|
56
|
+
import { whyCommand } from './why.mjs';
|
|
57
|
+
import { gateCommand } from './gate.mjs';
|
|
58
|
+
import { impactCommand } from './impact.mjs';
|
|
59
|
+
import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
|
|
60
|
+
import { decideCommand } from './decide.mjs';
|
|
61
|
+
import { localDoctorCommand } from './local.mjs';
|
|
48
62
|
import { parse } from '../argv.mjs';
|
|
63
|
+
import { CLI } from '../name.mjs';
|
|
49
64
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
50
65
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
51
66
|
|
|
@@ -66,6 +81,13 @@ const GROUPS = new Map([
|
|
|
66
81
|
['agent-hooks', 'status'],
|
|
67
82
|
['harness', 'detect'],
|
|
68
83
|
['bench', 'mine'],
|
|
84
|
+
// ADR-084. `shared` is the one group whose default writes nothing, and it is `plan` for
|
|
85
|
+
// the same reason `skills`'s is `index`: the read answers "what would this do, and
|
|
86
|
+
// where", and `install` — the subcommand that touches a reader's home directory — is the
|
|
87
|
+
// one that needs `--yes` before it does anything.
|
|
88
|
+
['shared', 'plan'],
|
|
89
|
+
['lessons', 'list'],
|
|
90
|
+
['local', 'doctor'],
|
|
69
91
|
]);
|
|
70
92
|
|
|
71
93
|
/**
|
|
@@ -82,6 +104,7 @@ const GROUPS = new Map([
|
|
|
82
104
|
* just correct: nothing in the map depends on the call.
|
|
83
105
|
*/
|
|
84
106
|
export const IMPLEMENTED = {
|
|
107
|
+
brief: briefCommand,
|
|
85
108
|
doctor: runDoctor,
|
|
86
109
|
'graph lint': lintCommand,
|
|
87
110
|
'skills index': skillsIndex,
|
|
@@ -91,6 +114,7 @@ export const IMPLEMENTED = {
|
|
|
91
114
|
'skills why': skillsWhy,
|
|
92
115
|
'skills eval': skillsEval,
|
|
93
116
|
'skills install': installCommand,
|
|
117
|
+
'skills compile': compileCommand,
|
|
94
118
|
'rules list': rulesList,
|
|
95
119
|
'rules show': rulesShow,
|
|
96
120
|
'rules why': rulesWhy,
|
|
@@ -99,6 +123,20 @@ export const IMPLEMENTED = {
|
|
|
99
123
|
'plan status': (args, flags) => planCommand('status', args, flags),
|
|
100
124
|
'plan next': (args, flags) => planCommand('next', args, flags),
|
|
101
125
|
'plan done': (args, flags) => planCommand('done', args, flags),
|
|
126
|
+
'shared plan': sharedPlanCommand,
|
|
127
|
+
'shared install': sharedInstallCommand,
|
|
128
|
+
evidence: evidenceCommand,
|
|
129
|
+
index: indexCommand,
|
|
130
|
+
map: mapCommand,
|
|
131
|
+
tests: testsCommand,
|
|
132
|
+
why: (args, flags) => whyCommand(args, flags, COMMANDS),
|
|
133
|
+
gate: gateCommand,
|
|
134
|
+
impact: (args, flags) => impactCommand(args, flags, COMMANDS),
|
|
135
|
+
'lessons list': lessonsListCommand,
|
|
136
|
+
'lessons review': lessonsReviewCommand,
|
|
137
|
+
decide: decideCommand,
|
|
138
|
+
'local doctor': localDoctorCommand,
|
|
139
|
+
'evidence check': (args, flags) => evidenceCommand(['check', ...args], flags),
|
|
102
140
|
init: initCommand,
|
|
103
141
|
ci: ciCommand,
|
|
104
142
|
'hooks install': (args, flags) => hooksCommand('install', args, flags),
|
|
@@ -116,6 +154,7 @@ export const IMPLEMENTED = {
|
|
|
116
154
|
'bench report': benchReportCommand,
|
|
117
155
|
'bench ablate': benchAblateCommand,
|
|
118
156
|
'bench probes': benchProbesCommand,
|
|
157
|
+
'bench variants': benchVariantsCommand,
|
|
119
158
|
upgrade: upgradeCommand,
|
|
120
159
|
mcp: mcpCommand,
|
|
121
160
|
};
|
|
@@ -163,7 +202,7 @@ export async function dispatch(argv) {
|
|
|
163
202
|
// simt-dev: they run this tool, not its source tree. The step number is the
|
|
164
203
|
// stable handle; the file that documents it is an implementation detail.
|
|
165
204
|
return fail(
|
|
166
|
-
|
|
205
|
+
`\`${CLI} ${path}\` is declared but not implemented`,
|
|
167
206
|
EXIT.UNSUPPORTED,
|
|
168
207
|
`implementation step ${step} is not built yet`,
|
|
169
208
|
);
|
|
@@ -220,6 +259,13 @@ async function runDoctor(positional, flags) {
|
|
|
220
259
|
// answer confidently wrong, so the answer is reported rather than inferred
|
|
221
260
|
// (ADR-014).
|
|
222
261
|
const ws = await resolveWorkspace(flags);
|
|
262
|
+
// The shared file this repository hands rules to (ADR-084). Asked here and nowhere else
|
|
263
|
+
// because it is the one thing about a repository that is true of the reader's machine
|
|
264
|
+
// rather than of the tree: `check` must not read `$HOME`, so `doctor` is the only command
|
|
265
|
+
// that can notice a region that dropped rules into a file nobody has. Reported, never
|
|
266
|
+
// blocking — a stale copy costs a reader some of their rules, which is worth knowing and
|
|
267
|
+
// is not this repository's gate failing.
|
|
268
|
+
const shared = await sharedWarning(root, result.manifest ?? null, flags);
|
|
223
269
|
|
|
224
270
|
// Under `--json` the document is the whole report, so it has to carry both
|
|
225
271
|
// failures the text path reports AND the workspace answer the text path prints.
|
|
@@ -254,6 +300,7 @@ async function runDoctor(positional, flags) {
|
|
|
254
300
|
detail: ws.detail,
|
|
255
301
|
error: ws.error ?? null,
|
|
256
302
|
},
|
|
303
|
+
shared,
|
|
257
304
|
...(failure ?? {}),
|
|
258
305
|
});
|
|
259
306
|
return code;
|
|
@@ -276,6 +323,10 @@ async function runDoctor(positional, flags) {
|
|
|
276
323
|
}`,
|
|
277
324
|
`manifest ${result.manifest ? (result.manifest.error ?? result.manifest.id ?? 'present') : '(absent)'}`,
|
|
278
325
|
`workspace ${ws.root ?? '(none)'} (${ws.source}: ${ws.detail})`,
|
|
326
|
+
// Printed only when this repository handed rules to a workspace copy: a line in
|
|
327
|
+
// every `doctor` is a line in every reader's terminal, and the reader who has
|
|
328
|
+
// nothing to install is not served by one (ADR-084).
|
|
329
|
+
...(shared.declared ? [`shared ${shared.state} ${shared.detail}`] : []),
|
|
279
330
|
].join('\n'),
|
|
280
331
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
281
332
|
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `simt lessons [list|review]` — deterministic lesson candidates and promotion to
|
|
2
|
+
// `docs/lessons/NNNN-<slug>.md` (plan 0003 R32).
|
|
3
|
+
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
7
|
+
import { CLI } from '../name.mjs';
|
|
8
|
+
import { parsePathList } from '../argv.mjs';
|
|
9
|
+
import {
|
|
10
|
+
LESSONS_DIR,
|
|
11
|
+
LESSONS_FILE,
|
|
12
|
+
captureFromLedger,
|
|
13
|
+
captureRevertedCommits,
|
|
14
|
+
listPromotedLessons,
|
|
15
|
+
promoteLesson,
|
|
16
|
+
readLessonCandidates,
|
|
17
|
+
} from '../memory/lessons.mjs';
|
|
18
|
+
|
|
19
|
+
export async function lessonsListCommand(args, flags) {
|
|
20
|
+
if (args.length > 0) {
|
|
21
|
+
return fail(`unknown lessons argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons`);
|
|
22
|
+
}
|
|
23
|
+
const root = resolve(flags.root ?? process.cwd());
|
|
24
|
+
await captureFromLedger(root);
|
|
25
|
+
await captureRevertedCommits(root);
|
|
26
|
+
|
|
27
|
+
const [{ candidates, errors }, promoted] = await Promise.all([
|
|
28
|
+
readLessonCandidates(root),
|
|
29
|
+
listPromotedLessons(root),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const payload = {
|
|
33
|
+
ok: errors.length === 0,
|
|
34
|
+
command: 'lessons list',
|
|
35
|
+
root,
|
|
36
|
+
ledgerFile: LESSONS_FILE,
|
|
37
|
+
lessonsDir: LESSONS_DIR,
|
|
38
|
+
candidateCount: candidates.length,
|
|
39
|
+
promotedCount: promoted.length,
|
|
40
|
+
candidates,
|
|
41
|
+
promoted,
|
|
42
|
+
errors,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (isJson(flags)) {
|
|
46
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
47
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines = [
|
|
51
|
+
`candidates ${candidates.length} (${LESSONS_FILE})`,
|
|
52
|
+
`promoted ${promoted.length} (${LESSONS_DIR})`,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
if (candidates.length > 0) {
|
|
56
|
+
lines.push('', 'CANDIDATES');
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
const sigShort = c.signature.slice(0, 19);
|
|
59
|
+
const state = c.status === 'promoted' ? `promoted -> ${c.promotedTo}` : 'candidate';
|
|
60
|
+
lines.push(` ${c.id} ${c.kind.padEnd(14)} ${sigShort} ${state} ${c.summary}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (promoted.length > 0) {
|
|
65
|
+
lines.push('', 'LESSONS');
|
|
66
|
+
for (const l of promoted) {
|
|
67
|
+
lines.push(` ${l.id.padEnd(28)} ${(l.signature ?? 'no-signature').slice(0, 19)} ${l.title}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const err of errors) lines.push(`error ${err}`);
|
|
72
|
+
|
|
73
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
74
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function lessonsReviewCommand(args, flags) {
|
|
78
|
+
if (args.length > 1) {
|
|
79
|
+
return fail(`unknown lessons review argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} lessons review [<id>] [--yes]`);
|
|
80
|
+
}
|
|
81
|
+
const root = resolve(flags.root ?? process.cwd());
|
|
82
|
+
const mode = writeMode(flags);
|
|
83
|
+
const parsedPaths = parsePathList(flags.paths);
|
|
84
|
+
const paths = parsedPaths.length > 0 ? parsedPaths : undefined;
|
|
85
|
+
const title = flags.task ? String(flags.task) : undefined;
|
|
86
|
+
|
|
87
|
+
const result = await promoteLesson(root, {
|
|
88
|
+
id: args[0],
|
|
89
|
+
apply: mode.apply,
|
|
90
|
+
paths,
|
|
91
|
+
title,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
return fail(result.error, EXIT.FAIL, `record a session failure-then-fix or run: ${CLI} lessons`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const payload = {
|
|
99
|
+
ok: true,
|
|
100
|
+
command: 'lessons review',
|
|
101
|
+
root,
|
|
102
|
+
dryRun: mode.dryRun,
|
|
103
|
+
written: result.written,
|
|
104
|
+
path: result.path,
|
|
105
|
+
candidate: result.candidate,
|
|
106
|
+
markdown: result.markdown,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (isJson(flags)) {
|
|
110
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
111
|
+
return EXIT.OK;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = [
|
|
115
|
+
`promote ${result.candidate.id} (${result.candidate.kind}) -> ${result.path}`,
|
|
116
|
+
`signature ${result.candidate.signature}`,
|
|
117
|
+
`mode ${mode.apply ? 'applied' : mode.dryRun ? 'dry-run (writes nothing)' : `preview (pass --yes to write ${result.path})`}`,
|
|
118
|
+
'',
|
|
119
|
+
result.markdown.trimEnd(),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
123
|
+
if (!mode.apply) {
|
|
124
|
+
note(`note dry-run nothing written; apply with: ${CLI} lessons review --yes`);
|
|
125
|
+
}
|
|
126
|
+
return EXIT.OK;
|
|
127
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// `simt local doctor` — probe configured local model endpoints (plan 0003 R37).
|
|
2
|
+
//
|
|
3
|
+
// Reads `local: { chat: { url, model }, embed: { url, model } }` from `simt.json`.
|
|
4
|
+
// When `local` is unset, opens no socket and exits 0. Refuses non-loopback URLs
|
|
5
|
+
// without `--allow-remote` (ADR-075).
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
10
|
+
import { probeLocal } from '../local/client.mjs';
|
|
11
|
+
|
|
12
|
+
function formatRoleLine(role, report) {
|
|
13
|
+
if (!report) return `${role.padEnd(8)} (unset)`;
|
|
14
|
+
if (report.status === 'refused-remote') {
|
|
15
|
+
return `${role.padEnd(8)} refused-remote ${report.model} @ ${report.url}`;
|
|
16
|
+
}
|
|
17
|
+
if (!report.ok) {
|
|
18
|
+
return `${role.padEnd(8)} ${report.status} ${report.model} @ ${report.url} (${report.error})`;
|
|
19
|
+
}
|
|
20
|
+
const extra =
|
|
21
|
+
role === 'embed' && Number.isInteger(report.dimensions)
|
|
22
|
+
? ` dimensions=${report.dimensions}`
|
|
23
|
+
: '';
|
|
24
|
+
const listed =
|
|
25
|
+
report.modelListed === true
|
|
26
|
+
? ' listed=yes'
|
|
27
|
+
: report.modelListed === false
|
|
28
|
+
? ' listed=no'
|
|
29
|
+
: '';
|
|
30
|
+
return `${role.padEnd(8)} ok ${report.model} @ ${report.url}${extra}${listed}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function remedyFor(result) {
|
|
34
|
+
if (result.status === 'refused-remote') {
|
|
35
|
+
return `set a loopback URL (http://127.0.0.1:<port>/v1) in ${MANIFEST_FILE}, or run: ${CLI} local doctor --allow-remote`;
|
|
36
|
+
}
|
|
37
|
+
if (result.status === 'invalid-config' || result.status === 'invalid-manifest') {
|
|
38
|
+
return `fix the \`local\` configuration in ${MANIFEST_FILE}`;
|
|
39
|
+
}
|
|
40
|
+
if (result.status === 'unreachable') {
|
|
41
|
+
return `start the local model server at the URL in ${MANIFEST_FILE}, then rerun: ${CLI} local doctor`;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function localDoctorCommand(args, flags) {
|
|
47
|
+
const root = resolve(flags.root ?? args[0] ?? process.cwd());
|
|
48
|
+
const allowRemote = flags.allowRemote === true;
|
|
49
|
+
const result = await probeLocal(root, { allowRemote });
|
|
50
|
+
|
|
51
|
+
const code = result.ok
|
|
52
|
+
? EXIT.OK
|
|
53
|
+
: result.status === 'refused-remote' ||
|
|
54
|
+
result.status === 'invalid-config' ||
|
|
55
|
+
result.status === 'invalid-manifest'
|
|
56
|
+
? EXIT.CONFIG
|
|
57
|
+
: EXIT.FAIL;
|
|
58
|
+
|
|
59
|
+
const error = result.ok ? null : result.findings[0] ?? `local model probe failed (${result.status})`;
|
|
60
|
+
const remedy = result.ok ? null : remedyFor(result);
|
|
61
|
+
|
|
62
|
+
if (isJson(flags)) {
|
|
63
|
+
emitJson(
|
|
64
|
+
{
|
|
65
|
+
ok: code === EXIT.OK,
|
|
66
|
+
command: 'local doctor',
|
|
67
|
+
root,
|
|
68
|
+
configured: result.configured,
|
|
69
|
+
status: result.status,
|
|
70
|
+
allowRemote: result.allowRemote,
|
|
71
|
+
remoteBlocked: result.remoteBlocked,
|
|
72
|
+
socketsOpened: result.socketsOpened,
|
|
73
|
+
chat: result.chat,
|
|
74
|
+
embed: result.embed,
|
|
75
|
+
findings: result.findings,
|
|
76
|
+
...(error ? { error, remedy, exitCode: code } : {}),
|
|
77
|
+
},
|
|
78
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
79
|
+
);
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lines = [
|
|
84
|
+
`root ${root}`,
|
|
85
|
+
`local ${result.configured ? result.status : '(unset) optional adapter is off; no socket opened'}`,
|
|
86
|
+
formatRoleLine('chat', result.chat),
|
|
87
|
+
formatRoleLine('embed', result.embed),
|
|
88
|
+
`sockets ${result.socketsOpened}`,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
92
|
+
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return fail(error, code, remedy);
|
|
95
|
+
}
|
|
96
|
+
return EXIT.OK;
|
|
97
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// `simt map` ranks the files a task is most likely to touch (R22, ADR-091).
|
|
2
|
+
// The cache is read, and rebuilt once when it is empty, because a map of no
|
|
3
|
+
// files is not an answer. The rebuild writes the gitignored index, which is
|
|
4
|
+
// the same cache `simt index` writes.
|
|
5
|
+
|
|
6
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
7
|
+
import { CLI } from '../name.mjs';
|
|
8
|
+
import { readEdges, readEmbeddings, readFiles, readFts, readSymbols, reindex, searchFtsBm25 } from '../index/store.mjs';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_BUDGET,
|
|
11
|
+
embedTaskQuery,
|
|
12
|
+
formatRecall,
|
|
13
|
+
measureRecall,
|
|
14
|
+
rankFiles,
|
|
15
|
+
renderMap,
|
|
16
|
+
} from '../index/map.mjs';
|
|
17
|
+
|
|
18
|
+
const LEVELS = new Set(['outline', 'names', 'signatures']);
|
|
19
|
+
|
|
20
|
+
export async function mapCommand(args, flags) {
|
|
21
|
+
if (args.length > 0) return fail(`unknown map argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} map`);
|
|
22
|
+
const root = flags.root ?? process.cwd();
|
|
23
|
+
if (flags.recall === true) return recall(root, flags);
|
|
24
|
+
const level = flags.level ?? 'outline';
|
|
25
|
+
if (!LEVELS.has(level)) {
|
|
26
|
+
return fail(`unknown level ${level}`, EXIT.USAGE, 'use outline, names or signatures');
|
|
27
|
+
}
|
|
28
|
+
const budget = flags.budget === undefined ? DEFAULT_BUDGET : Number(flags.budget);
|
|
29
|
+
if (!Number.isInteger(budget) || budget < 0) {
|
|
30
|
+
return fail(`budget must be a non-negative integer`, EXIT.USAGE, `run: ${CLI} map --budget 2048`);
|
|
31
|
+
}
|
|
32
|
+
const loaded = await loadGraph(root);
|
|
33
|
+
if (loaded.error) return loaded.error;
|
|
34
|
+
const task = typeof flags.task === 'string' ? flags.task : '';
|
|
35
|
+
const bm25 = task.trim().length > 0 ? await searchFtsBm25(root, task, { fts: loaded.fts }) : [];
|
|
36
|
+
const queryVector =
|
|
37
|
+
flags.embed === true && loaded.embeddings.length > 0
|
|
38
|
+
? await embedTaskQuery(root, task, {
|
|
39
|
+
embed: true,
|
|
40
|
+
allowRemote: flags.allowRemote === true,
|
|
41
|
+
})
|
|
42
|
+
: null;
|
|
43
|
+
const ranked = rankFiles({ ...loaded, bm25, queryVector, task });
|
|
44
|
+
const rendered = renderMap(ranked, loaded.symbols, { level, budget });
|
|
45
|
+
const payload = {
|
|
46
|
+
ok: true,
|
|
47
|
+
command: 'map',
|
|
48
|
+
root,
|
|
49
|
+
level,
|
|
50
|
+
budget,
|
|
51
|
+
shown: rendered.shown,
|
|
52
|
+
total: rendered.total,
|
|
53
|
+
truncated: rendered.truncated,
|
|
54
|
+
files: rendered.files,
|
|
55
|
+
text: rendered.text,
|
|
56
|
+
};
|
|
57
|
+
if (isJson(flags)) {
|
|
58
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
59
|
+
return EXIT.OK;
|
|
60
|
+
}
|
|
61
|
+
const lines = [
|
|
62
|
+
`level ${level}`,
|
|
63
|
+
`shown ${rendered.shown}`,
|
|
64
|
+
`total ${rendered.total}`,
|
|
65
|
+
`budget ${budget}`,
|
|
66
|
+
];
|
|
67
|
+
if (rendered.text) lines.push(rendered.text);
|
|
68
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
69
|
+
return EXIT.OK;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function recall(root, flags) {
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = await measureRecall(root, {
|
|
76
|
+
embed: flags.embed === true,
|
|
77
|
+
allowRemote: flags.allowRemote === true,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return fail(`could not measure recall (${error.message})`, EXIT.UNSUPPORTED, `run ${CLI} index in a git repository`);
|
|
81
|
+
}
|
|
82
|
+
const payload = { ok: true, command: 'map', root, recall: result };
|
|
83
|
+
if (isJson(flags)) {
|
|
84
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
85
|
+
return EXIT.OK;
|
|
86
|
+
}
|
|
87
|
+
emit(formatRecall(result), { maxBytes: flags.maxBytes ?? 0 });
|
|
88
|
+
return EXIT.OK;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The files, symbols, edges, FTS rows and stored vector embeddings the index holds,
|
|
93
|
+
* building the index once when it is empty.
|
|
94
|
+
*
|
|
95
|
+
* `build: false` refuses that rebuild and answers with `cold: true` instead. The one
|
|
96
|
+
* caller that passes it is the prompt-submit hook (R29), and the reason is the bar that
|
|
97
|
+
* hook has to meet: a delivered packet that built an index would make a person's first
|
|
98
|
+
* prompt in a fresh clone wait on a walk of the whole repository, and a prompt that takes
|
|
99
|
+
* a minute to appear is how a hook gets uninstalled. The next prompt gets the packet, and
|
|
100
|
+
* a command a person typed at a terminal always builds what it needs.
|
|
101
|
+
*/
|
|
102
|
+
export async function loadGraph(root, { build = true } = {}) {
|
|
103
|
+
let files = await readFiles(root);
|
|
104
|
+
if (files.length === 0) {
|
|
105
|
+
if (!build) return { files: [], symbols: [], edges: [], fts: [], embeddings: [], cold: true };
|
|
106
|
+
try {
|
|
107
|
+
await reindex(root);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
error: fail(
|
|
111
|
+
`could not map this repository (${error.message})`,
|
|
112
|
+
EXIT.UNSUPPORTED,
|
|
113
|
+
'run this inside a git repository',
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
files = await readFiles(root);
|
|
118
|
+
}
|
|
119
|
+
const [symbols, edges, fts, embeddings] = await Promise.all([
|
|
120
|
+
readSymbols(root),
|
|
121
|
+
readEdges(root),
|
|
122
|
+
readFts(root),
|
|
123
|
+
readEmbeddings(root),
|
|
124
|
+
]);
|
|
125
|
+
return { files, symbols, edges, fts, embeddings };
|
|
126
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// `simt index` rebuilds the local cache from git. The cache is not repository
|
|
2
|
+
// content: it is gitignored, and a second run with the same blobs writes the same
|
|
3
|
+
// rows (ADR-089).
|
|
4
|
+
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
8
|
+
import { CLI } from '../name.mjs';
|
|
9
|
+
import { JSONL_FILE, SQLITE_FILE, reindex } from '../index/store.mjs';
|
|
10
|
+
|
|
11
|
+
export async function indexCommand(args, flags) {
|
|
12
|
+
if (args.length > 0) return fail(`unknown index argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} index`);
|
|
13
|
+
const root = flags.root ?? process.cwd();
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await reindex(root, {
|
|
17
|
+
embed: flags.embed === true,
|
|
18
|
+
allowRemote: flags.allowRemote === true,
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return fail(
|
|
22
|
+
`could not index this repository (${error.message})`,
|
|
23
|
+
EXIT.UNSUPPORTED,
|
|
24
|
+
'run this inside a git repository',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const file = result.backend === 'sqlite' ? SQLITE_FILE : JSONL_FILE;
|
|
28
|
+
const ignored = await isIgnored(root, file);
|
|
29
|
+
const payload = { ok: true, command: 'index', root, file, ignored, ...result };
|
|
30
|
+
if (isJson(flags)) {
|
|
31
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
32
|
+
return EXIT.OK;
|
|
33
|
+
}
|
|
34
|
+
const lines = [
|
|
35
|
+
`index ${result.backend}`,
|
|
36
|
+
`files ${result.files}`,
|
|
37
|
+
`updated ${result.updated}`,
|
|
38
|
+
`removed ${result.removed}`,
|
|
39
|
+
`symbols ${result.symbols}`,
|
|
40
|
+
`edges ${result.edges}`,
|
|
41
|
+
`ms ${result.ms}`,
|
|
42
|
+
];
|
|
43
|
+
if (result.parsers === 'missing') lines.push('note index.parsers did not load, so the built-in extractors ran');
|
|
44
|
+
if (!ignored) lines.push(`note ${file} is not gitignored, and the cache is not meant to be committed`);
|
|
45
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
46
|
+
return EXIT.OK;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isIgnored(root, file) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
execFile('git', ['check-ignore', '-q', '--', file], { cwd: root }, (error) => {
|
|
52
|
+
resolve(!error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
package/src/commands/rules.mjs
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// one, because "this rule has no decision behind it" is exactly the fact a
|
|
15
15
|
// reviewer needs and exactly the fact an empty output would hide.
|
|
16
16
|
|
|
17
|
+
import { readdir } from 'node:fs/promises';
|
|
18
|
+
import { join } from 'node:path';
|
|
17
19
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
18
20
|
import { loadRules, select } from '../projection/project.mjs';
|
|
19
21
|
import { loadMdcRules } from '../projection/mdc.mjs';
|
|
@@ -217,10 +219,17 @@ export async function whyCommand(args, flags) {
|
|
|
217
219
|
if (!what) return fail('rules why needs a rule id or an ADR id', EXIT.USAGE, `usage: ${CLI} rules why <rule-id|ADR-NNN>`);
|
|
218
220
|
|
|
219
221
|
const { rules, broken } = await load(flags.root ?? null);
|
|
220
|
-
const
|
|
222
|
+
const localDir = flags.root ? join(flags.root, 'docs', 'decisions') : null;
|
|
223
|
+
const hasLocal = localDir
|
|
224
|
+
? (await readdir(localDir).catch(() => [])).some((n) => /^ADR-\d{3,}-.*\.md$/i.test(n))
|
|
225
|
+
: false;
|
|
226
|
+
const { ok, decisions, errors } = await loadDecisions({
|
|
227
|
+
all: flags.all === true,
|
|
228
|
+
...(hasLocal ? { dir: localDir } : {}),
|
|
229
|
+
});
|
|
221
230
|
if (!ok) return fail(errors[0], EXIT.CONFIG, 'the decision records must be readable for provenance to work');
|
|
222
231
|
|
|
223
|
-
return what.match(/^ADR-\d{3}$/i)
|
|
232
|
+
return what.match(/^ADR-\d{3,}$/i)
|
|
224
233
|
? adrDirection(what.toUpperCase(), rules, decisions, flags)
|
|
225
234
|
: ruleDirection(what, rules, decisions, flags, broken);
|
|
226
235
|
}
|
|
@@ -326,6 +335,12 @@ async function adrDirection(adr, rules, decisions, flags) {
|
|
|
326
335
|
];
|
|
327
336
|
if (d.supersededBy) lines.push(`SUPERSEDED by ${d.supersededBy} - this record is retained as evidence, not as policy.`);
|
|
328
337
|
if (d.supersedes.length > 0) lines.push(`supersedes ${d.supersedes.join(', ')}`);
|
|
338
|
+
const partialForward = (d.supersedesRulings ?? []).filter((s) => s.ruling !== null);
|
|
339
|
+
if (partialForward.length > 0) {
|
|
340
|
+
lines.push(
|
|
341
|
+
`supersedes ${partialForward.map((s) => `${s.adr} ruling ${s.ruling} (${s.kind})`).join('; ')}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
329
344
|
// A partial supersession is named here as well as on the ruling it moved, because the
|
|
330
345
|
// two print different sets: this prints every ruling the record lost, and the
|
|
331
346
|
// annotation prints the ones inside the excerpt below. A ruling past the excerpt
|