simt-dev 0.6.3 → 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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/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 +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- 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 +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- 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 +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- 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 +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- 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/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// simt plugin <claude|gemini|cursor> --out <dir> [--yes]
|
|
1
|
+
// simt plugin <claude|gemini|cursor|opencode> --out <dir> [--yes]
|
|
2
2
|
//
|
|
3
3
|
// Build one harness package from the same tables `agent-hooks install` writes into a
|
|
4
4
|
// repository, so a package and a repository install cannot drift apart. A dry run prints
|
|
@@ -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 = [
|
|
@@ -39,13 +40,14 @@ export async function indexCommand(args, flags) {
|
|
|
39
40
|
`files ${result.files}`,
|
|
40
41
|
`updated ${result.updated}`,
|
|
41
42
|
`removed ${result.removed}`,
|
|
43
|
+
`skipped ${result.skipped ?? 0}`,
|
|
42
44
|
`symbols ${result.symbols}`,
|
|
43
45
|
`edges ${result.edges}`,
|
|
44
46
|
`ms ${result.ms}`,
|
|
45
47
|
];
|
|
46
48
|
if (result.parsers === 'missing') lines.push('note index.parsers did not load, so the built-in extractors ran');
|
|
47
49
|
if (!ignored) lines.push(`note ${file} is not gitignored, and the cache is not meant to be committed`);
|
|
48
|
-
emit(lines.join('\n')
|
|
50
|
+
emit(lines.join('\n'));
|
|
49
51
|
return EXIT.OK;
|
|
50
52
|
}
|
|
51
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,12 +15,17 @@
|
|
|
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';
|
|
22
25
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
23
|
-
import { byId, citations,
|
|
26
|
+
import { byId, citations, loadDecisionsCached, redactEstate } from '../decisions/index.mjs';
|
|
27
|
+
import { DECISIONS_DIR } from '../decisions/index.mjs';
|
|
28
|
+
import { decisionsDir } from '../decisions/dir.mjs';
|
|
24
29
|
import { nearMatches } from '../resolve/near.mjs';
|
|
25
30
|
import { CLI } from '../name.mjs';
|
|
26
31
|
|
|
@@ -47,8 +52,8 @@ function sorted(rules) {
|
|
|
47
52
|
return [...rules].sort((a, b) => a.id.localeCompare(b.id));
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
async function load(root) {
|
|
51
|
-
const rules = await
|
|
55
|
+
async function load(root, ctx = repoContext(root)) {
|
|
56
|
+
const rules = await ctx.rules();
|
|
52
57
|
const broken = rules.filter((r) => r.error);
|
|
53
58
|
return { rules: [...rules, ...(await loadPerSurfaceRules())], broken };
|
|
54
59
|
}
|
|
@@ -151,12 +156,12 @@ export async function listCommand(args, flags) {
|
|
|
151
156
|
if (locals.length > 0) lines.push('', `local profiles come from ${locals.map((r) => r.source).join(', ')}`);
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
emit(lines.join('\n')
|
|
159
|
+
emit(lines.join('\n'));
|
|
155
160
|
return broken.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
async function selectionFor(rules, root) {
|
|
159
|
-
const { stack, facts } = await fingerprint(
|
|
163
|
+
async function selectionFor(rules, root, ctx = repoContext(root)) {
|
|
164
|
+
const { stack, facts } = await ctx.fingerprint();
|
|
160
165
|
const { selected, notes } = select(rules, stack, facts);
|
|
161
166
|
return { selected: new Set(selected.map((r) => r.id)), notes };
|
|
162
167
|
}
|
|
@@ -175,21 +180,44 @@ function rulesFailure(message, remedy, exitCode) {
|
|
|
175
180
|
throw Object.assign(new Error(message), { remedy, exitCode });
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The merged decision index: the package's shipped records underlay the
|
|
185
|
+
* repository's own, which win on an id collision (review 0003 Q34).
|
|
186
|
+
*
|
|
187
|
+
* Before this, a consumer with its own ADR-001/002 read `rules why core` as
|
|
188
|
+
* settled by *their* ADR-001 while the package's ADR-003…009 — the ones the
|
|
189
|
+
* shipped rules actually cite — reported MISSING. The overlay keeps both
|
|
190
|
+
* visible: every decision carries which layer it came from, so a same-number
|
|
191
|
+
* collision reads as two records, never as one answer wearing the wrong title.
|
|
192
|
+
*
|
|
193
|
+
* @returns {{ok: boolean, decisions: object[], errors: string[], origins: Record<string,'repo'|'package'>}}
|
|
194
|
+
*/
|
|
178
195
|
async function loadDecisionsFor(root, all) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
});
|
|
196
|
+
// Q55: one resolution (manifest key, then detection) and the shared cache,
|
|
197
|
+
// so `rules why` answers from the repository's own records without re-parsing
|
|
198
|
+
// every record on every call. No root means the package's own records.
|
|
199
|
+
const pkg = await loadDecisionsCached({ root, dir: DECISIONS_DIR, all });
|
|
200
|
+
if (!root) return { ...pkg, origins: Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package'])) };
|
|
201
|
+
const relDir = await decisionsDir(root);
|
|
202
|
+
const absDir = join(root, relDir);
|
|
203
|
+
const hasLocal = (await readdir(absDir).catch(() => [])).some((n) => /^ADR-\d{3,}-.*\.md$/i.test(n));
|
|
204
|
+
if (!hasLocal) return { ...pkg, origins: Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package'])) };
|
|
205
|
+
const local = await loadDecisionsCached({ root, dir: absDir, all });
|
|
206
|
+
const merged = new Map(pkg.decisions.map((d) => [d.id, d]));
|
|
207
|
+
const origins = Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package']));
|
|
208
|
+
for (const d of local.decisions) {
|
|
209
|
+
merged.set(d.id, d);
|
|
210
|
+
origins[d.id] = 'repo';
|
|
211
|
+
}
|
|
212
|
+
return { ok: pkg.ok && local.ok, decisions: [...merged.values()], errors: [...pkg.errors, ...local.errors], origins };
|
|
187
213
|
}
|
|
188
214
|
|
|
189
215
|
/** The catalogue both surfaces answer from: every rule, its decisions, and the selection. */
|
|
190
|
-
export async function rulesCatalogue(root, { selectFor = null } = {}) {
|
|
191
|
-
const { rules, broken } = await load(root);
|
|
192
|
-
|
|
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;
|
|
193
221
|
return {
|
|
194
222
|
rules: sorted(rules).map((r) => ({
|
|
195
223
|
id: r.id,
|
|
@@ -213,24 +241,26 @@ export async function rulesCatalogue(root, { selectFor = null } = {}) {
|
|
|
213
241
|
* The provenance facts for one rule or one decision record, in the shape the
|
|
214
242
|
* JSON documents carry. Text renders from the same facts; nothing re-derives.
|
|
215
243
|
*/
|
|
216
|
-
export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
217
|
-
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));
|
|
218
246
|
if (/^ADR-\d{3,}$/i.test(what)) {
|
|
219
247
|
const id = what.toUpperCase();
|
|
220
|
-
const { ok, decisions, errors } = await loadDecisionsFor(root, all);
|
|
248
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
|
|
221
249
|
if (!ok) {
|
|
222
250
|
rulesFailure(errors[0], 'the decision records must be readable for provenance to work', EXIT.CONFIG);
|
|
223
251
|
}
|
|
224
|
-
const record = decisions.find((x) => x.id === id);
|
|
252
|
+
const record = decisions.find((x) => x.id === id) ?? null;
|
|
225
253
|
// Was 'list them with: ls docs/decisions/' - a path relative to simt-dev's own
|
|
226
254
|
// tree, which resolves to nothing in the repo the reader is standing in. The
|
|
227
|
-
// remedy must be a command they can run
|
|
228
|
-
|
|
255
|
+
// remedy must be a command they can run (Q34: the decisions list, not the
|
|
256
|
+
// rules list - the reader asked about a decision, not a rule).
|
|
257
|
+
if (!record) rulesFailure(`no decision record ${id}`, `list the known decisions with: ${CLI} rules decisions`, EXIT.USAGE);
|
|
229
258
|
const replacement = record.supersededBy ? decisions.find((x) => x.id === record.supersededBy) : null;
|
|
230
259
|
return {
|
|
231
260
|
direction: 'decision',
|
|
232
261
|
id,
|
|
233
|
-
adr: record,
|
|
262
|
+
adr: { ...record, origin: origins[id] ?? 'package' },
|
|
263
|
+
origins,
|
|
234
264
|
supersededBy: replacement ? { id: replacement.id, title: replacement.title } : null,
|
|
235
265
|
inForce: replacement
|
|
236
266
|
? { id: replacement.id, status: replacement.status, summary: replacement.summary }
|
|
@@ -247,7 +277,7 @@ export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
|
247
277
|
EXIT.USAGE,
|
|
248
278
|
);
|
|
249
279
|
}
|
|
250
|
-
const { ok, decisions, errors } = await loadDecisionsFor(root, all);
|
|
280
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
|
|
251
281
|
if (!ok) {
|
|
252
282
|
rulesFailure(errors[0], 'the decision records must be readable for provenance to work', EXIT.CONFIG);
|
|
253
283
|
}
|
|
@@ -261,9 +291,10 @@ export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
|
261
291
|
title: rule.title ?? null,
|
|
262
292
|
source: rule.source,
|
|
263
293
|
origin: rule.origin,
|
|
264
|
-
adrs: cited.map((c) => index.get(c) ?? { id: c, missing: true }),
|
|
294
|
+
adrs: cited.map((c) => (index.has(c) ? { ...index.get(c), origin: origins[c] ?? 'package' } : { id: c, missing: true })),
|
|
265
295
|
uncited: cited.length === 0,
|
|
266
296
|
others: others.map((r) => r.id),
|
|
297
|
+
origins,
|
|
267
298
|
};
|
|
268
299
|
}
|
|
269
300
|
|
|
@@ -286,7 +317,7 @@ export async function showCommand(args, flags) {
|
|
|
286
317
|
|
|
287
318
|
const adrs = citations(rule.body);
|
|
288
319
|
if (isJson(flags)) {
|
|
289
|
-
emitJson({ ok: !rule.error, ...rule, adrs }
|
|
320
|
+
emitJson({ ok: !rule.error, ...rule, adrs });
|
|
290
321
|
return rule.error ? EXIT.FAIL : EXIT.OK;
|
|
291
322
|
}
|
|
292
323
|
|
|
@@ -303,7 +334,6 @@ export async function showCommand(args, flags) {
|
|
|
303
334
|
'',
|
|
304
335
|
rule.body,
|
|
305
336
|
].join('\n'),
|
|
306
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
307
337
|
);
|
|
308
338
|
return rule.error ? EXIT.FAIL : EXIT.OK;
|
|
309
339
|
}
|
|
@@ -344,17 +374,18 @@ function ruleDirection(facts, flags) {
|
|
|
344
374
|
' this rule cites no decision, so nothing settles it and nothing can overturn it.',
|
|
345
375
|
' Either cite the ADR that produced it, or accept that it is this repo\'s own policy.',
|
|
346
376
|
);
|
|
347
|
-
emit(lines.join('\n')
|
|
377
|
+
emit(lines.join('\n'));
|
|
348
378
|
return EXIT.OK;
|
|
349
379
|
}
|
|
350
380
|
|
|
351
381
|
lines.push('', `settled by ${facts.adrs.length} decision${facts.adrs.length === 1 ? '' : 's'}`);
|
|
352
382
|
for (const d of facts.adrs) {
|
|
353
383
|
if (d.missing) {
|
|
354
|
-
lines.push(` ${d.id} MISSING - the rule cites it, but this installation has
|
|
384
|
+
lines.push(` ${d.id} MISSING - the rule cites it, but neither this repo nor this installation has a record of it`);
|
|
385
|
+
lines.push(` remedy: list the known decisions with: ${CLI} rules decisions`);
|
|
355
386
|
continue;
|
|
356
387
|
}
|
|
357
|
-
lines.push(` ${d.id} ${d.title}`);
|
|
388
|
+
lines.push(` ${d.id} [${d.origin ?? 'package'}] ${d.title}`);
|
|
358
389
|
lines.push(` ${d.status}`);
|
|
359
390
|
if (d.supersededBy) {
|
|
360
391
|
// Never print a reversed ruling as if it were current.
|
|
@@ -368,7 +399,7 @@ function ruleDirection(facts, flags) {
|
|
|
368
399
|
lines.push('', `shares a decision with ${facts.others.join(', ')}`);
|
|
369
400
|
}
|
|
370
401
|
|
|
371
|
-
emit(lines.join('\n')
|
|
402
|
+
emit(lines.join('\n'));
|
|
372
403
|
return EXIT.OK;
|
|
373
404
|
}
|
|
374
405
|
|
|
@@ -379,6 +410,7 @@ function adrDirection(facts, flags) {
|
|
|
379
410
|
const lines = [
|
|
380
411
|
`${d.id} - ${d.title}`,
|
|
381
412
|
'',
|
|
413
|
+
`origin ${d.origin ?? 'package'}${d.origin === 'repo' ? ' (this repository overlays the shipped record of the same number)' : ''}`,
|
|
382
414
|
`status ${d.status}`,
|
|
383
415
|
`restates ${restates.length > 0 ? restates.join(', ') : '(no rule cites it)'}`,
|
|
384
416
|
];
|
|
@@ -458,8 +490,67 @@ function adrDirection(facts, flags) {
|
|
|
458
490
|
);
|
|
459
491
|
}
|
|
460
492
|
|
|
461
|
-
emit(lines.join('\n')
|
|
493
|
+
emit(lines.join('\n'));
|
|
462
494
|
// A superseded record is not a failure; it is the supersession rule working.
|
|
463
495
|
// The caller asked a question and got a true answer.
|
|
464
496
|
return EXIT.OK;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- decisions ------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* `simt rules decisions`: every known decision record, both layers.
|
|
503
|
+
*
|
|
504
|
+
* The shipped records underlay the repository's own, which win on a number
|
|
505
|
+
* collision (review 0003 Q34). The column that matters is `origin`: a reader in
|
|
506
|
+
* a consumer repository must be able to tell the package's ADR-001 from their
|
|
507
|
+
* own, because they are two records that happen to share a number.
|
|
508
|
+
*/
|
|
509
|
+
export async function decisionsCommand(args, flags) {
|
|
510
|
+
const root = flags.root ?? null;
|
|
511
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, flags.all === true);
|
|
512
|
+
if (!ok) {
|
|
513
|
+
return fail(errors[0], EXIT.CONFIG, 'the decision records must be readable to list them');
|
|
514
|
+
}
|
|
515
|
+
const rows = [...decisions]
|
|
516
|
+
.sort((a, b) => Number(a.id.replace(/^ADR-/i, '')) - Number(b.id.replace(/^ADR-/i, '')))
|
|
517
|
+
.map((d) => ({
|
|
518
|
+
id: d.id,
|
|
519
|
+
title: d.title,
|
|
520
|
+
status: d.status,
|
|
521
|
+
origin: origins[d.id] ?? 'package',
|
|
522
|
+
supersededBy: d.supersededBy ?? null,
|
|
523
|
+
file: d.file,
|
|
524
|
+
}));
|
|
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
|
+
|
|
539
|
+
if (isJson(flags)) {
|
|
540
|
+
emitJson({ ok: true, command: 'rules decisions', root, decisions: rows });
|
|
541
|
+
return EXIT.OK;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const repo = rows.filter((r) => r.origin === 'repo').length;
|
|
545
|
+
const lines = [
|
|
546
|
+
`${rows.length} decisions${root && repo > 0 ? ` (${repo} from this repo overlay the shipped set)` : ''}`,
|
|
547
|
+
'',
|
|
548
|
+
'id origin title',
|
|
549
|
+
];
|
|
550
|
+
for (const r of rows) {
|
|
551
|
+
const head = r.status.length > 80 ? `${r.status.slice(0, 80)}...` : r.status;
|
|
552
|
+
lines.push(`${r.id.padEnd(8)} ${r.origin.padEnd(7)} ${r.title} — ${head}`);
|
|
553
|
+
}
|
|
554
|
+
emit(lines.join('\n'));
|
|
555
|
+
return EXIT.OK;
|
|
465
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;
|