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/doctor.mjs
CHANGED
|
@@ -22,18 +22,20 @@
|
|
|
22
22
|
// `--yes` previews: it names what it would apply and lists the rest.
|
|
23
23
|
|
|
24
24
|
import { join } from 'node:path';
|
|
25
|
+
import { repoContext } from '../context.mjs';
|
|
25
26
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
26
27
|
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
27
28
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
28
29
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
29
30
|
import { sharedWarning } from './shared.mjs';
|
|
30
31
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
31
|
-
import { manifestPath,
|
|
32
|
+
import { manifestPath, writeManifest } from '../projection/manifest.mjs';
|
|
32
33
|
import { planManifestRepair } from './sync.mjs';
|
|
33
34
|
import { provenanceDigest } from '../projection/project.mjs';
|
|
34
35
|
import { applyHooksInstall, plannedDiffs, planHooksInstall } from './hooks.mjs';
|
|
35
36
|
import { fileFor } from '../hooks/shim.mjs';
|
|
36
37
|
import { commitJournal, ignoreJournal, journalHygiene, journalIgnoreNext } from '../journal.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* One config-health finding: what is wrong, how to fix it, and whether
|
|
@@ -47,7 +49,7 @@ import { commitJournal, ignoreJournal, journalHygiene, journalIgnoreNext } from
|
|
|
47
49
|
// finding creation, before any fix is applied, so the before side is true in
|
|
48
50
|
// preview and in apply alike. Only safe findings carry pairs: an unsafe finding
|
|
49
51
|
// is never applied, so there is no write to preview.
|
|
50
|
-
async function collectFindings(root, { result, ws, shared, withDiffs = false }) {
|
|
52
|
+
async function collectFindings(root, { result, ws, shared, withDiffs = false, ctx }) {
|
|
51
53
|
const findings = [];
|
|
52
54
|
if (result.error) {
|
|
53
55
|
// Nothing else can be asked: classification failed, so every check built on
|
|
@@ -63,7 +65,7 @@ async function collectFindings(root, { result, ws, shared, withDiffs = false })
|
|
|
63
65
|
return findings;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
const { manifest, exists, error: manifestError } = await
|
|
68
|
+
const { manifest, exists, error: manifestError } = await ctx.manifest();
|
|
67
69
|
if (manifestError !== null) {
|
|
68
70
|
// An unreadable manifest is a hand-edit, not a repair: re-rendering it
|
|
69
71
|
// would destroy whatever the other tool was saying.
|
|
@@ -232,7 +234,10 @@ function findingLines(findings, { fixMode, applying }) {
|
|
|
232
234
|
//
|
|
233
235
|
// See the module header: classification first, because everything else is a guess without it.
|
|
234
236
|
export async function diagnose(root, args = {}) {
|
|
235
|
-
|
|
237
|
+
// One context for the whole diagnosis, so a caller that already holds one (a UI, a
|
|
238
|
+
// harness, an MCP request) does not pay for a second fingerprint (review 0003 Q67).
|
|
239
|
+
const ctx = args.ctx ?? repoContext(root);
|
|
240
|
+
const result = await ctx.fingerprint();
|
|
236
241
|
// Which source answered the workspace question, and where it looked. A tool
|
|
237
242
|
// that silently fell back to the wrong directory would make every downstream
|
|
238
243
|
// answer confidently wrong, so the answer is reported rather than inferred
|
|
@@ -246,7 +251,7 @@ export async function diagnose(root, args = {}) {
|
|
|
246
251
|
const shared = await sharedWarning(root, result.manifest ?? null, args);
|
|
247
252
|
|
|
248
253
|
const withDiffs = args.fix === true && args.diff === true;
|
|
249
|
-
const findings = await collectFindings(root, { result, ws, shared, withDiffs });
|
|
254
|
+
const findings = await collectFindings(root, { result, ws, shared, withDiffs, ctx });
|
|
250
255
|
// Under `--json` the document is the whole report, so it has to carry both
|
|
251
256
|
// failures the text path reports AND the workspace answer the text prints.
|
|
252
257
|
const failure = result.error
|
|
@@ -276,7 +281,7 @@ export function serialisedFindings(findings) {
|
|
|
276
281
|
}
|
|
277
282
|
|
|
278
283
|
export async function runDoctor(positional, flags) {
|
|
279
|
-
const root = flags
|
|
284
|
+
const root = commandRoot(flags, positional);
|
|
280
285
|
const { result, ws, shared, findings, failure, code: diagnosisCode } = await diagnose(root, flags);
|
|
281
286
|
|
|
282
287
|
const fixMode = flags.fix === true;
|
|
@@ -344,7 +349,6 @@ export async function runDoctor(positional, flags) {
|
|
|
344
349
|
...(shared.declared ? [`shared ${shared.state} ${shared.detail}`] : []),
|
|
345
350
|
...findingLines(findings, { fixMode, applying: mode.apply }),
|
|
346
351
|
].join('\n'),
|
|
347
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
348
352
|
);
|
|
349
353
|
// After the report: the report says what, the diff shows it (R50). Only the
|
|
350
354
|
// fix plan is diffed, so without --fix there is nothing to show.
|
|
@@ -6,13 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
|
|
9
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
10
10
|
import { readManifest } from '../projection/manifest.mjs';
|
|
11
11
|
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
claimsVerification,
|
|
14
|
+
evidenceMode,
|
|
15
|
+
hasTestEvidence,
|
|
16
|
+
indexTreeId,
|
|
17
|
+
readLedger,
|
|
18
|
+
treeId,
|
|
19
|
+
} from '../ledger/index.mjs';
|
|
20
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
13
21
|
|
|
14
22
|
export async function evidenceCommand(args, flags) {
|
|
15
|
-
const root = flags
|
|
23
|
+
const root = commandRoot(flags);
|
|
16
24
|
if (args[0] === 'check') return checkClaim(root, args[1], flags);
|
|
17
25
|
if (args[0] !== undefined) {
|
|
18
26
|
return fail(`unknown evidence subcommand ${args[0]}`, EXIT.USAGE, `run: ${CLI} evidence`);
|
|
@@ -35,7 +43,7 @@ async function showEvidence(root, flags) {
|
|
|
35
43
|
const mine = entries.filter((e) => e.tree === tree);
|
|
36
44
|
const payload = { ok: errors.length === 0, command: 'evidence', root, tree, entries: mine, errors };
|
|
37
45
|
if (isJson(flags)) {
|
|
38
|
-
emitJson(payload
|
|
46
|
+
emitJson(payload);
|
|
39
47
|
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
40
48
|
}
|
|
41
49
|
const lines = [`tree ${tree}`];
|
|
@@ -44,7 +52,7 @@ async function showEvidence(root, flags) {
|
|
|
44
52
|
lines.push(`${e.exit === 0 ? 'pass' : 'fail'} ${e.cmd} exit ${e.exit} ${e.summary}`);
|
|
45
53
|
}
|
|
46
54
|
for (const error of errors) lines.push(`error ${error}`);
|
|
47
|
-
emit(lines.join('\n')
|
|
55
|
+
emit(lines.join('\n'));
|
|
48
56
|
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -72,14 +80,17 @@ async function checkClaim(root, file, flags) {
|
|
|
72
80
|
if (isJson(flags)) emitJson(payload, { pretty: flags.pretty === true });
|
|
73
81
|
return EXIT.OK;
|
|
74
82
|
}
|
|
83
|
+
// A commit records the index, not the worktree: `git add -p` can commit bytes
|
|
84
|
+
// the worktree tree never had (review 0003 Q18). Match on the staged tree, with
|
|
85
|
+
// the worktree tree as the fallback when git cannot name the index.
|
|
75
86
|
let tree;
|
|
76
87
|
try {
|
|
77
|
-
tree = await treeId(root);
|
|
88
|
+
tree = (await indexTreeId(root)) ?? (await treeId(root));
|
|
78
89
|
} catch (error) {
|
|
79
90
|
return fail(`could not name this tree (${error.message})`, EXIT.UNSUPPORTED, 'run this inside a git repository');
|
|
80
91
|
}
|
|
81
92
|
const { entries } = await readLedger(root);
|
|
82
|
-
const supported = entries
|
|
93
|
+
const supported = hasTestEvidence(entries, tree);
|
|
83
94
|
const payload = { ok: supported || mode === 'warn', command: 'evidence check', claim: true, supported, mode, tree };
|
|
84
95
|
if (supported) {
|
|
85
96
|
if (isJson(flags)) emitJson(payload, { pretty: flags.pretty === true });
|
|
@@ -87,10 +98,6 @@ async function checkClaim(root, file, flags) {
|
|
|
87
98
|
}
|
|
88
99
|
const detail = 'the message claims a run this tree does not have';
|
|
89
100
|
const remedy = `run the command, then commit, or take the claim out of the message`;
|
|
90
|
-
|
|
91
|
-
emitJson({ ...payload, detail, remedy }, { pretty: flags.pretty === true });
|
|
92
|
-
return mode === 'block' ? EXIT.FAIL : EXIT.OK;
|
|
93
|
-
}
|
|
94
|
-
emit(`${mode === 'block' ? 'BLOCKED' : 'WARNING'} ${detail}\n remedy: ${remedy}`, { maxBytes: flags.maxBytes ?? 0 });
|
|
101
|
+
present({ ...payload, detail, remedy }, () => `${mode === 'block' ? 'BLOCKED' : 'WARNING'} ${detail}\n remedy: ${remedy}`, flags);
|
|
95
102
|
return mode === 'block' ? EXIT.FAIL : EXIT.OK;
|
|
96
103
|
}
|
package/src/commands/explain.mjs
CHANGED
|
@@ -46,7 +46,6 @@ export async function explainCommand(positional, flags) {
|
|
|
46
46
|
}
|
|
47
47
|
emit(
|
|
48
48
|
[`code ${code}`, '', row.what, '', `Why: ${row.why}`, '', `Fix: ${row.fix}`, '', `Example: ${row.example}`].join('\n'),
|
|
49
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
50
49
|
);
|
|
51
50
|
return EXIT.OK;
|
|
52
51
|
}
|
package/src/commands/gate.mjs
CHANGED
|
@@ -6,21 +6,27 @@
|
|
|
6
6
|
// this tool knows how to read is compacted; the raw log is kept.
|
|
7
7
|
|
|
8
8
|
import { execFile } from 'node:child_process';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { mkdir, readdir, unlink, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
11
12
|
import { promisify } from 'node:util';
|
|
12
13
|
|
|
13
|
-
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
14
|
+
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
14
15
|
import { CLI } from '../name.mjs';
|
|
15
16
|
import { detectTestRunner } from '../bench/mine.mjs';
|
|
16
17
|
import { compactOutput } from '../index/compact.mjs';
|
|
17
18
|
import { readImportEdges, selectTests } from '../index/gate.mjs';
|
|
18
|
-
import { packageOf } from '../index/store.mjs';
|
|
19
|
+
import { langOf, packageOf } from '../index/store.mjs';
|
|
20
|
+
import { localRel } from '../state.mjs';
|
|
21
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
19
22
|
|
|
20
23
|
const exec = promisify(execFile);
|
|
21
24
|
|
|
25
|
+
/** How many raw gate logs the state directory keeps. */
|
|
26
|
+
const RAW_LOG_KEEP = 5;
|
|
27
|
+
|
|
22
28
|
export async function gateCommand(args, flags) {
|
|
23
|
-
const root = flags
|
|
29
|
+
const root = commandRoot(flags);
|
|
24
30
|
const listed = await tracked(root);
|
|
25
31
|
if (listed.error) return listed.error;
|
|
26
32
|
let changed = args.filter(Boolean);
|
|
@@ -31,31 +37,80 @@ export async function gateCommand(args, flags) {
|
|
|
31
37
|
}
|
|
32
38
|
if (changed.length === 0) {
|
|
33
39
|
if (flags.changed === true) {
|
|
40
|
+
// No change: nothing is unmatched and no language is implicated, so the
|
|
41
|
+
// graph is vacuously resolved and the verdict is still said out loud.
|
|
34
42
|
if (isJson(flags)) {
|
|
35
|
-
emitJson(
|
|
43
|
+
emitJson(
|
|
44
|
+
{ ok: true, command: 'gate', root, selected: [], unmatched: [], graph: 'resolved', verdict: 'nothing-selected', code: 0 },
|
|
45
|
+
);
|
|
36
46
|
} else {
|
|
37
|
-
emit('selected 0'
|
|
47
|
+
emit('selected 0\nverdict nothing-selected');
|
|
38
48
|
}
|
|
39
49
|
return EXIT.OK;
|
|
40
50
|
}
|
|
41
51
|
return fail(`nothing changed`, EXIT.USAGE, `pass a path, or ${CLI} gate --changed`);
|
|
42
52
|
}
|
|
53
|
+
// Q40: an explicit path that is neither tracked nor on disk names nothing the
|
|
54
|
+
// gate could select from. That is a usage error, not an empty selection: the
|
|
55
|
+
// old answer (`ok: true`, no tests, exit 0) was indistinguishable from "this
|
|
56
|
+
// change is safe" and trained the reader to trust it.
|
|
57
|
+
const unknown = changed.filter((p) => !listed.files.includes(p) && !existsSync(join(root, p)));
|
|
58
|
+
if (unknown.length > 0) {
|
|
59
|
+
return fail(
|
|
60
|
+
`no such file: ${unknown.join(', ')}`,
|
|
61
|
+
EXIT.USAGE,
|
|
62
|
+
`pass a path git tracks, or ${CLI} gate --changed`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
43
65
|
const files = new Set(listed.files);
|
|
44
66
|
for (const file of changed) files.add(file);
|
|
45
|
-
const
|
|
67
|
+
const edges = await readImportEdges(root, files);
|
|
68
|
+
const selected = selectTests(changed, edges);
|
|
69
|
+
// Q40: the selection explains itself. `unmatched` names explicit paths the
|
|
70
|
+
// tracked tree does not carry (untracked but present, so the run proceeds);
|
|
71
|
+
// `graph` is `unresolved` when a language has edges but none of them reaches
|
|
72
|
+
// a tracked file (the Kotlin estate: thousands of specs, zero resolutions);
|
|
73
|
+
// `verdict` says the empty set out loud instead of leaving `selected: []` to
|
|
74
|
+
// be read as either safe or blind. `ok` stays as is until W27's opt-in.
|
|
75
|
+
const unmatched = changed.filter((p) => !listed.files.includes(p));
|
|
76
|
+
const graph = graphState(edges, files);
|
|
46
77
|
if (selected.length === 0) {
|
|
47
|
-
const payload = { ok: true, command: 'gate', root, selected, code: 0 };
|
|
78
|
+
const payload = { ok: true, command: 'gate', root, selected, unmatched, graph, verdict: 'nothing-selected', code: 0 };
|
|
48
79
|
if (isJson(flags)) {
|
|
49
|
-
emitJson(payload
|
|
80
|
+
emitJson(payload);
|
|
50
81
|
return EXIT.OK;
|
|
51
82
|
}
|
|
52
|
-
|
|
83
|
+
const lines = ['selected 0', 'verdict nothing-selected'];
|
|
84
|
+
if (unmatched.length > 0) lines.push(`unmatched ${unmatched.join(', ')}`);
|
|
85
|
+
if (graph === 'unresolved') lines.push('graph unresolved');
|
|
86
|
+
emit(lines.join('\n'));
|
|
53
87
|
return EXIT.OK;
|
|
54
88
|
}
|
|
55
|
-
return runSelected(root, flags, selected);
|
|
89
|
+
return runSelected(root, flags, selected, { unmatched, graph });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `resolved` unless some language present in the edges has total edges but
|
|
94
|
+
* zero reaching a tracked file. A language with no edges at all is not
|
|
95
|
+
* evidence of anything - an unimported file needs no resolution.
|
|
96
|
+
*/
|
|
97
|
+
function graphState(edges, files) {
|
|
98
|
+
const byLang = new Map();
|
|
99
|
+
for (const edge of edges) {
|
|
100
|
+
const lang = langOf(edge.src);
|
|
101
|
+
if (!lang) continue;
|
|
102
|
+
const stat = byLang.get(lang) ?? { total: 0, resolved: 0 };
|
|
103
|
+
stat.total += 1;
|
|
104
|
+
if (files.has(edge.dst)) stat.resolved += 1;
|
|
105
|
+
byLang.set(lang, stat);
|
|
106
|
+
}
|
|
107
|
+
for (const stat of byLang.values()) {
|
|
108
|
+
if (stat.total > 0 && stat.resolved === 0) return 'unresolved';
|
|
109
|
+
}
|
|
110
|
+
return 'resolved';
|
|
56
111
|
}
|
|
57
112
|
|
|
58
|
-
async function runSelected(root, flags, selected) {
|
|
113
|
+
async function runSelected(root, flags, selected, { unmatched = [], graph = 'resolved' } = {}) {
|
|
59
114
|
const cache = new Map();
|
|
60
115
|
const groups = new Map();
|
|
61
116
|
for (const file of selected) {
|
|
@@ -106,22 +161,35 @@ async function runSelected(root, flags, selected) {
|
|
|
106
161
|
}
|
|
107
162
|
const raw = await writeRaw(root, rawParts.join('\n'));
|
|
108
163
|
if (raw) text.push('', `raw ${raw}`);
|
|
109
|
-
const payload = {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
164
|
+
const payload = {
|
|
165
|
+
ok: code === 0,
|
|
166
|
+
command: 'gate',
|
|
167
|
+
root,
|
|
168
|
+
selected,
|
|
169
|
+
unmatched,
|
|
170
|
+
graph,
|
|
171
|
+
verdict: 'selected',
|
|
172
|
+
code,
|
|
173
|
+
raw,
|
|
174
|
+
runs,
|
|
175
|
+
};
|
|
176
|
+
present(payload, () => text.filter((line) => line !== undefined).join('\n'), flags);
|
|
115
177
|
return code === 0 ? EXIT.OK : EXIT.FAIL;
|
|
116
178
|
}
|
|
117
179
|
|
|
118
180
|
async function writeRaw(root, body) {
|
|
181
|
+
// Raw gate output is machine-local state (review 0003 Q19).
|
|
119
182
|
try {
|
|
120
|
-
const
|
|
183
|
+
const rel = localRel('gate', `gate-${Date.now()}.log`);
|
|
184
|
+
const dir = join(root, dirname(rel));
|
|
121
185
|
await mkdir(dir, { recursive: true });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
186
|
+
await writeFile(join(root, rel), body);
|
|
187
|
+
// Keep the newest few: raw gate output is machine-local state (Q19), and a file per
|
|
188
|
+
// run with no bound is a directory that grows on every command (Q83).
|
|
189
|
+
const kept = await readdir(dir).catch(() => []);
|
|
190
|
+
const logs = kept.filter((name) => /^gate-\d+\.log$/.test(name)).sort().reverse();
|
|
191
|
+
for (const name of logs.slice(RAW_LOG_KEEP)) await unlink(join(dir, name)).catch(() => {});
|
|
192
|
+
return rel;
|
|
125
193
|
} catch {
|
|
126
194
|
return '';
|
|
127
195
|
}
|
|
@@ -129,8 +197,10 @@ async function writeRaw(root, body) {
|
|
|
129
197
|
|
|
130
198
|
async function tracked(root) {
|
|
131
199
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
200
|
+
// `-z`: without it git C-quotes a non-ASCII path (`src/\303\274n...`) and
|
|
201
|
+
// the reader's real filename never matches it (review 0003 Q39).
|
|
202
|
+
const { stdout } = await exec('git', ['ls-files', '-z'], { cwd: root, maxBuffer: 16 << 20 });
|
|
203
|
+
return { files: stdout.split('\0').filter(Boolean) };
|
|
134
204
|
} catch (error) {
|
|
135
205
|
return {
|
|
136
206
|
error: fail(
|
|
@@ -144,9 +214,9 @@ async function tracked(root) {
|
|
|
144
214
|
|
|
145
215
|
async function changedFiles(root) {
|
|
146
216
|
try {
|
|
147
|
-
const { stdout } = await exec('git', ['diff', '--name-only', 'HEAD'], { cwd: root, maxBuffer: 8 << 20 });
|
|
148
|
-
const untracked = await exec('git', ['ls-files', '--others', '--exclude-standard'], { cwd: root, maxBuffer: 8 << 20 });
|
|
149
|
-
return { files: [...new Set([...stdout.split('\
|
|
217
|
+
const { stdout } = await exec('git', ['diff', '--name-only', '-z', 'HEAD'], { cwd: root, maxBuffer: 8 << 20 });
|
|
218
|
+
const untracked = await exec('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: root, maxBuffer: 8 << 20 });
|
|
219
|
+
return { files: [...new Set([...stdout.split('\0'), ...untracked.stdout.split('\0')].filter(Boolean))].sort() };
|
|
150
220
|
} catch (error) {
|
|
151
221
|
return {
|
|
152
222
|
error: fail(
|
package/src/commands/graph.mjs
CHANGED
|
@@ -31,16 +31,29 @@ export async function lintCommand(positional, flags) {
|
|
|
31
31
|
// client accept these files", and the second question needs no registry at all. A
|
|
32
32
|
// reader with no registry can still lint a skill directory, so the two are additive
|
|
33
33
|
// rather than one gating the other.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
//
|
|
35
|
+
// The pass runs only when asked for (`--spec`) and only against a verified
|
|
36
|
+
// registry: without it the default lint reported 37 warnings for a pass the
|
|
37
|
+
// payload claimed never ran (`spec: null`), and `ok` stayed true over errors
|
|
38
|
+
// the spec pass had just found (review 0003 Q35).
|
|
39
|
+
let specReport = { errors: [], warnings: [], checked: 0 };
|
|
40
|
+
if (spec !== null) {
|
|
41
|
+
if (registry && verified && workspace?.root) {
|
|
42
|
+
specReport = await lintSpec(registry, workspace.root);
|
|
43
|
+
} else {
|
|
44
|
+
warnings.push(
|
|
45
|
+
'spec pass skipped: it needs --spec against a registry verified on disk (see `verified` above)',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
errors.push(...specReport.errors);
|
|
38
50
|
warnings.push(...specReport.warnings);
|
|
51
|
+
const finalOk = ok && errors.length === 0;
|
|
39
52
|
|
|
40
53
|
if (isJson(flags)) {
|
|
41
54
|
emitJson(
|
|
42
55
|
{
|
|
43
|
-
ok,
|
|
56
|
+
ok: finalOk,
|
|
44
57
|
registry: path,
|
|
45
58
|
capabilities: registry?.capabilities?.length ?? 0,
|
|
46
59
|
skills: registry?.skills?.length ?? 0,
|
|
@@ -58,9 +71,8 @@ export async function lintCommand(positional, flags) {
|
|
|
58
71
|
errors,
|
|
59
72
|
warnings,
|
|
60
73
|
},
|
|
61
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
62
74
|
);
|
|
63
|
-
return
|
|
75
|
+
return finalOk ? EXIT.OK : EXIT.REGISTRY;
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
if (!registry) {
|
|
@@ -96,11 +108,11 @@ export async function lintCommand(positional, flags) {
|
|
|
96
108
|
if (errors.length > 0) {
|
|
97
109
|
lines.push('', `FAIL ${errors.length}`);
|
|
98
110
|
for (const error of errors) lines.push(` ${error}`);
|
|
99
|
-
emit(lines.join('\n')
|
|
111
|
+
emit(lines.join('\n'));
|
|
100
112
|
return EXIT.REGISTRY;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
lines.push('', verified ? 'OK no findings' : 'OK structural checks only');
|
|
104
|
-
emit(lines.join('\n')
|
|
116
|
+
emit(lines.join('\n'));
|
|
105
117
|
return EXIT.OK;
|
|
106
118
|
}
|
package/src/commands/harness.mjs
CHANGED
|
@@ -27,6 +27,7 @@ import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
|
|
|
27
27
|
import { SURFACES, CANDIDATE_DIRS } from '../projection/surfaces.mjs';
|
|
28
28
|
import { LOADERS, LOADER_IDS, caps, loader } from '../projection/loaders.mjs';
|
|
29
29
|
import { viewAll } from '../measure/loaderView.mjs';
|
|
30
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* `simt harness detect`. The subcommand is validated by dispatch, which rejects a
|
|
@@ -39,7 +40,7 @@ import { viewAll } from '../measure/loaderView.mjs';
|
|
|
39
40
|
* @returns {Promise<number>} process exit code
|
|
40
41
|
*/
|
|
41
42
|
export async function harnessCommand(positional, flags) {
|
|
42
|
-
const root = flags
|
|
43
|
+
const root = commandRoot(flags);
|
|
43
44
|
|
|
44
45
|
// MCP evidence is project-scope by construction: the loader table carries repo-root
|
|
45
46
|
// paths (.mcp.json, .codex/config.toml, …), and the same path means different scopes
|
|
@@ -256,7 +257,7 @@ function block(scan, pad) {
|
|
|
256
257
|
* do" in a repository where the answer is not about bytes.
|
|
257
258
|
*/
|
|
258
259
|
export async function harnessLoadersCommand(positional, flags) {
|
|
259
|
-
const root = flags
|
|
260
|
+
const root = commandRoot(flags);
|
|
260
261
|
const wanted = typeof flags.reader === 'string' ? flags.reader : null;
|
|
261
262
|
const rows = wanted === null ? LOADERS : [loader(wanted)].filter(Boolean);
|
|
262
263
|
|