@intentset/cli 0.6.1 → 0.7.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/README.md +35 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +18 -10
- package/dist/agents.js.map +1 -1
- package/dist/commands/evidence-stale.d.ts +19 -0
- package/dist/commands/evidence-stale.d.ts.map +1 -0
- package/dist/commands/evidence-stale.js +118 -0
- package/dist/commands/evidence-stale.js.map +1 -0
- package/dist/commands/evidence.d.ts +7 -0
- package/dist/commands/evidence.d.ts.map +1 -1
- package/dist/commands/evidence.js +19 -2
- package/dist/commands/evidence.js.map +1 -1
- package/dist/commands/graph.js +1 -1
- package/dist/commands/guide.d.ts +5 -0
- package/dist/commands/guide.d.ts.map +1 -0
- package/dist/commands/guide.js +16 -0
- package/dist/commands/guide.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/migrate.d.ts +23 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +125 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/git.d.ts +13 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +26 -0
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +146 -44
- package/dist/main.js.map +1 -1
- package/dist/reports.d.ts.map +1 -1
- package/dist/reports.js +26 -3
- package/dist/reports.js.map +1 -1
- package/dist/session.d.ts +8 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +18 -1
- package/dist/session.js.map +1 -1
- package/dist/subject.d.ts +16 -0
- package/dist/subject.d.ts.map +1 -0
- package/dist/subject.js +44 -0
- package/dist/subject.js.map +1 -0
- package/package.json +18 -10
- package/src/agents.ts +18 -10
- package/src/commands/evidence-stale.ts +151 -0
- package/src/commands/evidence.ts +30 -3
- package/src/commands/graph.ts +1 -1
- package/src/commands/guide.ts +17 -0
- package/src/commands/init.ts +12 -3
- package/src/commands/migrate.ts +136 -0
- package/src/git.ts +25 -0
- package/src/index.ts +3 -1
- package/src/main.ts +150 -44
- package/src/reports.ts +27 -3
- package/src/session.ts +26 -1
- package/src/subject.ts +61 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `intentset migrate --to 0.2` (Core §1): moves the records the model reads from the specifications' `0.1` to `0.2`
|
|
3
|
+
* by rewriting two values in each record's frontmatter, `intentset.spec` and `intentset.profile`, which Core §4
|
|
4
|
+
* requires to agree with it. It reads the same records every other command reads (the files the configured scope
|
|
5
|
+
* matches), changes those two lines and nothing else, byte for byte, and keeps the quoting each line used. A tree
|
|
6
|
+
* with uncommitted changes to tracked files is refused unless `--allow-dirty` says otherwise, so the rewrite is a
|
|
7
|
+
* diff of its own that git can undo. Other files (`.intentset/agents.md`, which carries an example record) are not
|
|
8
|
+
* records and are left to their author.
|
|
9
|
+
*/
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { sortDiagnostics } from "@intentset/core";
|
|
13
|
+
import { trackedChanges } from "../git.ts";
|
|
14
|
+
import { formatDiagnostic, type Io } from "../output.ts";
|
|
15
|
+
import { type CarrierName, CONFIG_PATH, findRoot, loadRepository } from "../repository.ts";
|
|
16
|
+
|
|
17
|
+
/** The specification versions this command moves between. */
|
|
18
|
+
export const MIGRATE_FROM = "0.1";
|
|
19
|
+
export const MIGRATE_TO = "0.2";
|
|
20
|
+
|
|
21
|
+
export interface MigrateOptions {
|
|
22
|
+
root?: string;
|
|
23
|
+
to?: string;
|
|
24
|
+
allowDirty?: boolean;
|
|
25
|
+
carrier: CarrierName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A message when the options cannot be run, before anything is read; null when they can. */
|
|
29
|
+
export function migrateUsageProblem(options: Pick<MigrateOptions, "to">): string | null {
|
|
30
|
+
if (options.to === undefined) return `--to is required: the only target is ${MIGRATE_TO}, from ${MIGRATE_FROM}`;
|
|
31
|
+
if (options.to !== MIGRATE_TO) {
|
|
32
|
+
return `--to ${options.to}: the only target is ${MIGRATE_TO}, which moves records written for ${MIGRATE_FROM}`;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const SPEC_LINE = /^(\s+spec:\s*)(['"]?)(\d+\.\d+)\2(\s*(?:#.*)?)$/;
|
|
38
|
+
const PROFILE_LINE = /^(\s+profile:\s*)(['"]?)intentset\/([a-z0-9-]+)\/(\d+\.\d+)\2(\s*(?:#.*)?)$/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The record's text moved from `from` to `to`, or null when it declares another value. Only lines inside the
|
|
42
|
+
* `intentset:` mapping of the frontmatter, and only `spec` and `profile`, are rewritten; line endings are kept.
|
|
43
|
+
*/
|
|
44
|
+
export function migrateText(
|
|
45
|
+
source: string,
|
|
46
|
+
frontmatter: { line: number; text: string },
|
|
47
|
+
from: string,
|
|
48
|
+
to: string,
|
|
49
|
+
): string | null {
|
|
50
|
+
const lines = source.split("\n");
|
|
51
|
+
const first = frontmatter.line - 1;
|
|
52
|
+
const last = first + frontmatter.text.split("\n").length;
|
|
53
|
+
let inIntentset = false;
|
|
54
|
+
let childIndent: number | null = null;
|
|
55
|
+
let specSeen = false;
|
|
56
|
+
const edits = new Map<number, string>();
|
|
57
|
+
for (let i = first; i < last && i < lines.length; i++) {
|
|
58
|
+
const raw = lines[i];
|
|
59
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
60
|
+
const eol = raw.endsWith("\r") ? "\r" : "";
|
|
61
|
+
if (/^\S/.test(line)) {
|
|
62
|
+
inIntentset = /^intentset:\s*(#.*)?$/.test(line);
|
|
63
|
+
childIndent = null;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!inIntentset || line.trim() === "") continue;
|
|
67
|
+
const indent = line.length - line.trimStart().length;
|
|
68
|
+
childIndent ??= indent;
|
|
69
|
+
if (indent !== childIndent) continue;
|
|
70
|
+
const spec = SPEC_LINE.exec(line);
|
|
71
|
+
if (spec !== null) {
|
|
72
|
+
if (spec[3] !== from) return null;
|
|
73
|
+
specSeen = true;
|
|
74
|
+
edits.set(i, `${spec[1]}${spec[2]}${to}${spec[2]}${spec[4]}${eol}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const profile = PROFILE_LINE.exec(line);
|
|
78
|
+
if (profile !== null && profile[4] === from) {
|
|
79
|
+
edits.set(i, `${profile[1]}${profile[2]}intentset/${profile[3]}/${to}${profile[2]}${profile[5]}${eol}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!specSeen) return null;
|
|
83
|
+
for (const [i, text] of edits) lines[i] = text;
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function migrateCommand(options: MigrateOptions, io: Io): number {
|
|
88
|
+
const root = findRoot(io.cwd, options.root);
|
|
89
|
+
if (root === null) {
|
|
90
|
+
const where = options.root === undefined ? "this directory or any above it" : options.root;
|
|
91
|
+
io.stderr(`intentset migrate: no ${CONFIG_PATH} in ${where}.\n`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
if (options.allowDirty !== true) {
|
|
95
|
+
const changes = trackedChanges(root);
|
|
96
|
+
if (changes === null) {
|
|
97
|
+
io.stderr(
|
|
98
|
+
"intentset migrate: this is not a git work tree, so the rewrite could not be undone; pass --allow-dirty to migrate anyway.\n",
|
|
99
|
+
);
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
if (changes.length > 0) {
|
|
103
|
+
io.stderr(
|
|
104
|
+
`intentset migrate: ${changes.length} tracked file${changes.length === 1 ? " has" : "s have"} uncommitted changes. ` +
|
|
105
|
+
"Commit or stash them so the rewrite is a diff on its own, or pass --allow-dirty.\n",
|
|
106
|
+
);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const repo = loadRepository(root, options.carrier);
|
|
111
|
+
if (!repo.configOk) {
|
|
112
|
+
io.stderr("intentset migrate: the configuration has errors, so the scope is unknown.\n");
|
|
113
|
+
for (const d of sortDiagnostics(repo.diagnostics)) io.stderr(formatDiagnostic(d));
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
let moved = 0;
|
|
117
|
+
let current = 0;
|
|
118
|
+
let other = 0;
|
|
119
|
+
for (const input of repo.inputs) {
|
|
120
|
+
if (input.frontmatter === null) continue;
|
|
121
|
+
const text = migrateText(input.source, input.frontmatter, MIGRATE_FROM, MIGRATE_TO);
|
|
122
|
+
if (text === null) {
|
|
123
|
+
if (/^\s+spec:\s*['"]?0\.2['"]?\s*(#.*)?$/m.test(input.frontmatter.text)) current++;
|
|
124
|
+
else other++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (text === input.source) continue;
|
|
128
|
+
writeFileSync(join(root, input.path), text);
|
|
129
|
+
moved++;
|
|
130
|
+
}
|
|
131
|
+
io.stdout(
|
|
132
|
+
`intentset migrate: moved ${moved} record${moved === 1 ? "" : "s"} from ${MIGRATE_FROM} to ${MIGRATE_TO}` +
|
|
133
|
+
`${current > 0 ? `; ${current} already ${MIGRATE_TO}` : ""}${other > 0 ? `; ${other} declare neither (left as they are)` : ""}.\n`,
|
|
134
|
+
);
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
package/src/git.ts
CHANGED
|
@@ -94,3 +94,28 @@ export function acknowledgedSlices(root: string, base: Base | null): string[] {
|
|
|
94
94
|
if (out === null) return [];
|
|
95
95
|
return [...new Set(out.split(/[\s,]+/).filter((id) => id !== ""))].sort(compareStrings);
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The files that differ between `commit` and the working tree (tracked changes, staged or not, and untracked files
|
|
100
|
+
* .gitignore does not exclude), sorted, as paths relative to the CLI's root; or null when `commit` is not an ancestor of
|
|
101
|
+
* HEAD or git cannot say. One `git diff --name-only`, so the evidence report can list what changed outside every
|
|
102
|
+
* subject (Core §8) without computing anything per file.
|
|
103
|
+
*/
|
|
104
|
+
export function changedSince(root: string, commit: string): string[] | null {
|
|
105
|
+
if (git(root, ["merge-base", "--is-ancestor", commit, "HEAD"]) === null) return null;
|
|
106
|
+
const diff = git(root, ["diff", "--relative", "--name-only", "-z", commit]);
|
|
107
|
+
if (diff === null) return null;
|
|
108
|
+
const untracked = paths(git(root, ["ls-files", "-z", "--others", "--exclude-standard"]));
|
|
109
|
+
return [...new Set([...paths(diff), ...untracked])].sort(compareStrings);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The tracked files that differ from HEAD, staged or not, as `git status` lists them; an empty list for a clean
|
|
114
|
+
* tree, and null when this is not a git work tree or git cannot say. Untracked files are not counted: a command that
|
|
115
|
+
* rewrites tracked files asks only that its rewrite be a diff on its own.
|
|
116
|
+
*/
|
|
117
|
+
export function trackedChanges(root: string): string[] | null {
|
|
118
|
+
const out = git(root, ["status", "--porcelain", "--untracked-files=no", "-z"]);
|
|
119
|
+
if (out === null) return null;
|
|
120
|
+
return out.split("\0").filter((entry) => entry !== "");
|
|
121
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @intentset/cli as a library: `main` for running the command in-process,
|
|
3
|
-
*
|
|
3
|
+
* the agent guide `init` writes (intentset.org serves it from the same
|
|
4
|
+
* generator), and the repository loader and pattern matcher it is built on.
|
|
4
5
|
*/
|
|
5
6
|
export { main, USAGE } from "./main.ts";
|
|
7
|
+
export { AGENT_GUIDE_PATH, AGENT_POINTERS, agentGuide, type PackageManager } from "./agents.ts";
|
|
6
8
|
export type { Io } from "./output.ts";
|
|
7
9
|
export { matchAny, matchGlob } from "./glob.ts";
|
|
8
10
|
export { CONFIG_PATH, enumerate, findRoot, loadRepository, readCommit, type Repository } from "./repository.ts";
|
package/src/main.ts
CHANGED
|
@@ -5,20 +5,24 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Exit codes (Core §11, ADR 0004): 0 no errors, 1 validation errors, 2
|
|
7
7
|
* invocation or tool failure. validate, graph, architecture check, impact,
|
|
8
|
-
* context, review, serve and
|
|
8
|
+
* context, review, evidence stale, serve, mcp and guide never write; graph --out, architecture
|
|
9
9
|
* check --write-baseline, evidence import --out, publish --out and serve
|
|
10
|
-
* --out write only what they name; init only creates
|
|
10
|
+
* --out write only what they name; init only creates; migrate rewrites the
|
|
11
|
+
* two version values of each record's frontmatter and nothing else.
|
|
11
12
|
*/
|
|
12
13
|
import { parseArgs } from "node:util";
|
|
13
14
|
import type { Mode } from "@intentset/architecture";
|
|
14
|
-
import { LEVELS, type Level } from "@intentset/core";
|
|
15
|
+
import { EVIDENCE_BINDINGS, type EvidenceBinding, LEVELS, type Level } from "@intentset/core";
|
|
15
16
|
import type { RunScope } from "@intentset/verification";
|
|
16
17
|
import { architectureCommand } from "./commands/architecture.ts";
|
|
17
18
|
import { contextCommand } from "./commands/context.ts";
|
|
18
19
|
import { evidenceImportCommand, evidenceUsageProblem } from "./commands/evidence.ts";
|
|
20
|
+
import { evidenceStaleCommand, evidenceStaleUsageProblem } from "./commands/evidence-stale.ts";
|
|
19
21
|
import { graphCommand, graphUsageProblem, parseRelease } from "./commands/graph.ts";
|
|
22
|
+
import { guideCommand } from "./commands/guide.ts";
|
|
20
23
|
import { impactCommand } from "./commands/impact.ts";
|
|
21
24
|
import { initCommand } from "./commands/init.ts";
|
|
25
|
+
import { migrateCommand, migrateUsageProblem } from "./commands/migrate.ts";
|
|
22
26
|
import { mcpCommand, mcpUsageProblem } from "./commands/mcp.ts";
|
|
23
27
|
import { publishCommand, publishUsageProblem } from "./commands/publish.ts";
|
|
24
28
|
import { reviewCommand } from "./commands/review.ts";
|
|
@@ -34,69 +38,96 @@ export const USAGE = `usage: intentset <command> [options]
|
|
|
34
38
|
|
|
35
39
|
commands
|
|
36
40
|
init [--repository <name>] [--example] [--agents]
|
|
37
|
-
create .intentset/config.yaml, an empty .intentset/registries.yaml
|
|
38
|
-
.intentset/agents.md, the guide that tells coding agents how to
|
|
39
|
-
model current; --example adds the scheduling example under
|
|
40
|
-
product/scheduling/; --agents writes only the guide, for a
|
|
41
|
-
already initialized. Never overwrites a file.
|
|
42
|
-
|
|
41
|
+
create .intentset/config.yaml, an empty .intentset/registries.yaml
|
|
42
|
+
and .intentset/agents.md, the guide that tells coding agents how to
|
|
43
|
+
keep the model current; --example adds the scheduling example under
|
|
44
|
+
product/scheduling/; --agents writes only the guide, for a
|
|
45
|
+
repository already initialized. Never overwrites a file.
|
|
46
|
+
guide print that guide for this repository, for the scope its config
|
|
47
|
+
names, without writing anything
|
|
48
|
+
validate every check the level asks for; exit 1 when any diagnostic is an
|
|
49
|
+
error
|
|
43
50
|
graph [--format json] [--include-bodies] [--release <product>:<label>] [--out <file>]
|
|
44
51
|
[--generated-at <time>] [--report evidence|knowledge|impact|ownership|all]...
|
|
45
52
|
[--include-restricted]
|
|
46
|
-
print the intentset/export/0.
|
|
53
|
+
print the intentset/export/0.4 envelope, or write it to --out;
|
|
47
54
|
each --report adds a section (ownership needs L2, evidence L3; all
|
|
48
55
|
is every one the level reads). Restricted artifacts are withheld
|
|
49
56
|
and counted unless --include-restricted
|
|
50
57
|
architecture check [--mode migration|strict] [--baseline <file>] [--write-baseline <file>]
|
|
51
58
|
ownership, claims, imports, layers and regions (default level L2)
|
|
52
|
-
evidence import --from vitest|node-tap <report> --out <file> --product <ID>
|
|
59
|
+
evidence import --from vitest|playwright|node-tap <report> --out <file> --product <ID>
|
|
60
|
+
--release <label>
|
|
53
61
|
[--environment <text>] [--started-at <time>] [--finished-at <time>] [--uri-base <uri>]
|
|
54
62
|
[--tool-version <v>]
|
|
55
63
|
turn a test report into run records bound to this commit and graph
|
|
56
|
-
|
|
64
|
+
evidence stale [--release <product>:<label>] [--check] [--binding <subject|commit>]
|
|
65
|
+
[--format list|json|playwright|vitest|node-test]
|
|
66
|
+
what needs a run: the automated verifications whose evidence is
|
|
67
|
+
not a current pass, as a table, as JSON, or as the arguments that
|
|
68
|
+
make a test runner run only those tests; never writes; --check
|
|
69
|
+
exits 1 when any needs a run
|
|
70
|
+
impact <ID> what depends on an artifact: direct, candidates, review context,
|
|
71
|
+
ancestors
|
|
57
72
|
context <ID|path> [--include-restricted]
|
|
58
|
-
the bounded engineering context of an artifact, with paths and
|
|
59
|
-
given a file a slice claims, the context of that slice
|
|
73
|
+
the bounded engineering context of an artifact, with paths and
|
|
74
|
+
bodies; given a file a slice claims, the context of that slice
|
|
60
75
|
review [--base <ref>] [--fail-on-drift]
|
|
61
|
-
the local review report: checks, coverage, the impact of what
|
|
62
|
-
and each slice whose code changed while its records did
|
|
63
|
-
--fail-on-drift exits 1 for any such slice no commit
|
|
64
|
-
an Intentset-Unchanged trailer
|
|
76
|
+
the local review report: checks, coverage, the impact of what
|
|
77
|
+
changed, and each slice whose code changed while its records did
|
|
78
|
+
not; --fail-on-drift exits 1 for any such slice no commit
|
|
79
|
+
acknowledged with an Intentset-Unchanged trailer
|
|
65
80
|
publish --visibility <v> --audience <a> --product <ID> --release <r> --role <r> --edition <e>
|
|
66
81
|
[--flag <f>]... [--authorized-internal] [--authorized-restricted] --out <dir> [--html]
|
|
67
82
|
[--published-at <time>]
|
|
68
|
-
reviewed knowledge for one audience and release, as Markset with
|
|
83
|
+
reviewed knowledge for one audience and release, as Markset with
|
|
84
|
+
provenance
|
|
69
85
|
serve [--port <n>] [--host <address>] [--out <dir>]
|
|
70
|
-
the Atlas, the internal review pages, on http://127.0.0.1:3000/
|
|
71
|
-
when a file changes), or written to --out; runs at L2, or
|
|
86
|
+
the Atlas, the internal review pages, on http://127.0.0.1:3000/
|
|
87
|
+
(rebuilt when a file changes), or written to --out; runs at L2, or
|
|
88
|
+
L3 with run records
|
|
89
|
+
migrate --to 0.2 [--allow-dirty]
|
|
90
|
+
move the records the model reads from specifications 0.1 to 0.2:
|
|
91
|
+
rewrites intentset.spec and intentset.profile in each record's
|
|
92
|
+
frontmatter and nothing else, and says how many. Refuses a tree
|
|
93
|
+
with uncommitted changes to tracked files unless --allow-dirty
|
|
72
94
|
mcp --mode engineering [--include-restricted]
|
|
73
|
-
mcp --mode customer --visibility <public|customer> --audience <a> --product <ID>
|
|
74
|
-
--role <r> --edition <e> [--flag <f>]...
|
|
95
|
+
mcp --mode customer --visibility <public|customer> --audience <a> --product <ID>
|
|
96
|
+
--release <r> --role <r> --edition <e> [--flag <f>]...
|
|
75
97
|
the read-only MCP server on stdio: the engineering graph, or one
|
|
76
98
|
publication made in memory as publish makes it, and nothing else
|
|
77
99
|
|
|
78
100
|
options
|
|
79
|
-
--root <dir> repository root (default: the nearest directory at or above this
|
|
80
|
-
holding .intentset/config.yaml)
|
|
101
|
+
--root <dir> repository root (default: the nearest directory at or above this
|
|
102
|
+
one holding .intentset/config.yaml)
|
|
81
103
|
--json machine-readable output, keys in canonical order
|
|
82
104
|
--carrier <markset|plain> how documents are read (default markset)
|
|
83
105
|
--level <L1..L4> conformance level; each includes those below it (default L1):
|
|
84
106
|
L2 adds architecture, L3 evidence, L4 publication readiness
|
|
85
|
-
--evidence <file> L3 and above: run records to read, repeatable (default: every
|
|
86
|
-
${EVIDENCE_DIR}/, which holds local output and is not
|
|
107
|
+
--evidence <file> L3 and above: run records to read, repeatable (default: every
|
|
108
|
+
*.json in ${EVIDENCE_DIR}/, which holds local output and is not
|
|
109
|
+
committed)
|
|
87
110
|
--product <ID> --release <label>
|
|
88
|
-
L3 and above: the exact scope evidence is assessed for (default:
|
|
111
|
+
L3 and above: the exact scope evidence is assessed for (default:
|
|
112
|
+
every scope)
|
|
113
|
+
--binding <subject|commit>
|
|
114
|
+
L3 and above, and evidence import: how run evidence is judged
|
|
115
|
+
current, overriding evidence.binding in the configuration (default:
|
|
116
|
+
subject where slices claim files, else commit). Subject keeps a
|
|
117
|
+
record current while what it checked is unchanged; commit only at
|
|
118
|
+
the commit it ran at
|
|
89
119
|
--mode <migration|strict> --baseline <file>
|
|
90
|
-
L2 and above: whether a baseline of known violations applies, and
|
|
91
|
-
(for mcp, --mode names the server's mode instead)
|
|
120
|
+
L2 and above: whether a baseline of known violations applies, and
|
|
121
|
+
which (for mcp, --mode names the server's mode instead)
|
|
92
122
|
-h, --help show this help
|
|
93
123
|
--version print "intentset <version>", the checker version a report names
|
|
94
124
|
|
|
95
|
-
exit codes: 0 no errors, 1 validation errors (or, with review --fail-on-drift,
|
|
96
|
-
2 invocation or tool failure`;
|
|
125
|
+
exit codes: 0 no errors, 1 validation errors (or, with review --fail-on-drift,
|
|
126
|
+
unacknowledged drift), 2 invocation or tool failure`;
|
|
97
127
|
|
|
98
128
|
const COMMANDS = [
|
|
99
129
|
"init",
|
|
130
|
+
"guide",
|
|
100
131
|
"validate",
|
|
101
132
|
"graph",
|
|
102
133
|
"architecture",
|
|
@@ -106,13 +137,16 @@ const COMMANDS = [
|
|
|
106
137
|
"review",
|
|
107
138
|
"publish",
|
|
108
139
|
"serve",
|
|
140
|
+
"migrate",
|
|
109
141
|
"mcp",
|
|
110
142
|
];
|
|
111
143
|
|
|
112
144
|
const GLOBAL = ["root", "json", "carrier", "level", "help", "version"];
|
|
113
|
-
const LEVEL_OPTIONS = ["evidence", "product", "release", "mode", "baseline"];
|
|
145
|
+
const LEVEL_OPTIONS = ["evidence", "product", "release", "mode", "baseline", "binding"];
|
|
114
146
|
const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
115
147
|
init: ["repository", "example", "agents"],
|
|
148
|
+
guide: [],
|
|
149
|
+
migrate: ["to", "allow-dirty"],
|
|
116
150
|
validate: LEVEL_OPTIONS,
|
|
117
151
|
graph: [
|
|
118
152
|
"format",
|
|
@@ -125,9 +159,13 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
125
159
|
"evidence",
|
|
126
160
|
"mode",
|
|
127
161
|
"baseline",
|
|
162
|
+
"binding",
|
|
128
163
|
],
|
|
129
164
|
architecture: ["mode", "baseline", "write-baseline"],
|
|
130
165
|
evidence: [
|
|
166
|
+
"format",
|
|
167
|
+
"check",
|
|
168
|
+
"evidence",
|
|
131
169
|
"from",
|
|
132
170
|
"out",
|
|
133
171
|
"product",
|
|
@@ -137,6 +175,7 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
137
175
|
"finished-at",
|
|
138
176
|
"uri-base",
|
|
139
177
|
"tool-version",
|
|
178
|
+
"binding",
|
|
140
179
|
],
|
|
141
180
|
impact: LEVEL_OPTIONS,
|
|
142
181
|
context: ["include-restricted", ...LEVEL_OPTIONS],
|
|
@@ -173,9 +212,9 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
173
212
|
};
|
|
174
213
|
|
|
175
214
|
/** Positional arguments after the command: a fixed subcommand, then how many more. */
|
|
176
|
-
const POSITIONALS: Record<string, {
|
|
177
|
-
architecture: {
|
|
178
|
-
evidence: {
|
|
215
|
+
const POSITIONALS: Record<string, { subs?: Record<string, number>; count: number; what?: string }> = {
|
|
216
|
+
architecture: { subs: { check: 0 }, count: 0 },
|
|
217
|
+
evidence: { subs: { import: 1, stale: 0 }, count: 0, what: "a report file" },
|
|
179
218
|
impact: { count: 1, what: "an artifact ID" },
|
|
180
219
|
context: { count: 1, what: "an artifact ID or a file path" },
|
|
181
220
|
};
|
|
@@ -216,14 +255,48 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
216
255
|
|
|
217
256
|
const shape = POSITIONALS[command] ?? { count: 0 };
|
|
218
257
|
let args = rest;
|
|
219
|
-
|
|
220
|
-
|
|
258
|
+
let wanted = shape.count;
|
|
259
|
+
let sub: string | undefined;
|
|
260
|
+
if (shape.subs !== undefined) {
|
|
261
|
+
const names = Object.keys(shape.subs);
|
|
262
|
+
sub = args[0];
|
|
263
|
+
if (sub === undefined || !Object.hasOwn(shape.subs, sub)) {
|
|
264
|
+
return usage(
|
|
265
|
+
names.length === 1
|
|
266
|
+
? `the only subcommand is "${command} ${names[0]}"`
|
|
267
|
+
: `the subcommands are ${names.map((name) => `"${command} ${name}"`).join(" and ")}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
wanted = shape.subs[sub];
|
|
221
271
|
args = args.slice(1);
|
|
222
272
|
}
|
|
223
|
-
if (args.length <
|
|
224
|
-
if (args.length >
|
|
225
|
-
const hint =
|
|
226
|
-
return usage(`unexpected argument "${args[
|
|
273
|
+
if (args.length < wanted) return usage(`${shape.what} is required`);
|
|
274
|
+
if (args.length > wanted) {
|
|
275
|
+
const hint = wanted === 0 ? " (use --root <dir> to name the repository)" : "";
|
|
276
|
+
return usage(`unexpected argument "${args[wanted]}"${hint}`);
|
|
277
|
+
}
|
|
278
|
+
if (command === "evidence") {
|
|
279
|
+
// Each subcommand takes its own options, though one list allows them all.
|
|
280
|
+
const own =
|
|
281
|
+
sub === "stale"
|
|
282
|
+
? ["format", "check", "evidence", "release", "binding"]
|
|
283
|
+
: [
|
|
284
|
+
"from",
|
|
285
|
+
"out",
|
|
286
|
+
"product",
|
|
287
|
+
"release",
|
|
288
|
+
"environment",
|
|
289
|
+
"started-at",
|
|
290
|
+
"finished-at",
|
|
291
|
+
"uri-base",
|
|
292
|
+
"tool-version",
|
|
293
|
+
"binding",
|
|
294
|
+
];
|
|
295
|
+
for (const token of tokens) {
|
|
296
|
+
if (token.kind === "option" && COMMAND_OPTIONS.evidence.includes(token.name) && !own.includes(token.name)) {
|
|
297
|
+
return usage(`--${token.name} is not an option of evidence ${sub}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
227
300
|
}
|
|
228
301
|
|
|
229
302
|
const carrier = values.carrier ?? "markset";
|
|
@@ -240,6 +313,15 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
240
313
|
if (command !== "mcp" && values.mode !== undefined && values.mode !== "migration" && values.mode !== "strict") {
|
|
241
314
|
return usage(`--mode ${values.mode}: the modes are migration and strict`);
|
|
242
315
|
}
|
|
316
|
+
if (command === "guide") return guideCommand({ root: values.root }, io);
|
|
317
|
+
if (command === "migrate") {
|
|
318
|
+
const problem = migrateUsageProblem({ to: values.to });
|
|
319
|
+
if (problem !== null) return usage(problem);
|
|
320
|
+
return migrateCommand(
|
|
321
|
+
{ root: values.root, to: values.to, allowDirty: values["allow-dirty"], carrier: carrier as CarrierName },
|
|
322
|
+
io,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
243
325
|
if (command === "init") {
|
|
244
326
|
if (values.agents && values.example)
|
|
245
327
|
return usage("--agents writes only the guide; it does not combine with --example");
|
|
@@ -251,8 +333,8 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
251
333
|
|
|
252
334
|
// Evidence scope: --product with --release, or graph's --release <product>:<label>.
|
|
253
335
|
let scope: RunScope | null = null;
|
|
254
|
-
if (command !== "evidence" && command !== "publish" && command !== "mcp") {
|
|
255
|
-
if (command === "graph") {
|
|
336
|
+
if ((command !== "evidence" || sub === "stale") && command !== "publish" && command !== "mcp") {
|
|
337
|
+
if (command === "graph" || command === "evidence") {
|
|
256
338
|
const release = values.release === undefined ? null : parseRelease(values.release);
|
|
257
339
|
scope = release === null ? null : { product: release.product, release: release.label };
|
|
258
340
|
} else if ((values.product === undefined) !== (values.release === undefined)) {
|
|
@@ -262,6 +344,9 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
262
344
|
}
|
|
263
345
|
}
|
|
264
346
|
|
|
347
|
+
if (values.binding !== undefined && !(EVIDENCE_BINDINGS as readonly string[]).includes(values.binding)) {
|
|
348
|
+
return usage(`--binding ${values.binding}: the bindings are ${EVIDENCE_BINDINGS.join(" and ")}`);
|
|
349
|
+
}
|
|
265
350
|
const sessionOptions = (level: Level): SessionOptions => ({
|
|
266
351
|
root: values.root,
|
|
267
352
|
carrier: carrier as CarrierName,
|
|
@@ -270,6 +355,7 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
270
355
|
baseline: values.baseline,
|
|
271
356
|
evidence: values.evidence,
|
|
272
357
|
scope,
|
|
358
|
+
binding: values.binding as EvidenceBinding | undefined,
|
|
273
359
|
});
|
|
274
360
|
|
|
275
361
|
switch (command) {
|
|
@@ -299,6 +385,21 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
299
385
|
return architectureCommand(session, { writeBaseline: values["write-baseline"] }, values.json, io);
|
|
300
386
|
}
|
|
301
387
|
case "evidence": {
|
|
388
|
+
if (sub === "stale") {
|
|
389
|
+
if (levelGiven === "L1" || levelGiven === "L2") {
|
|
390
|
+
return usage("evidence is judged at L3 and above, where run records are read");
|
|
391
|
+
}
|
|
392
|
+
const staleOptions = {
|
|
393
|
+
release: values.release,
|
|
394
|
+
format: values.format,
|
|
395
|
+
check: values.check,
|
|
396
|
+
json: values.json,
|
|
397
|
+
};
|
|
398
|
+
const checked = evidenceStaleUsageProblem(staleOptions);
|
|
399
|
+
if ("problem" in checked) return usage(checked.problem);
|
|
400
|
+
const session = openSession("evidence stale", sessionOptions(levelGiven ?? "L3"), io);
|
|
401
|
+
return typeof session === "number" ? session : evidenceStaleCommand(session, checked.format, staleOptions, io);
|
|
402
|
+
}
|
|
302
403
|
const options = {
|
|
303
404
|
from: values.from,
|
|
304
405
|
report: args[0],
|
|
@@ -310,6 +411,7 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
310
411
|
finishedAt: values["finished-at"],
|
|
311
412
|
uriBase: values["uri-base"],
|
|
312
413
|
toolVersion: values["tool-version"],
|
|
414
|
+
binding: values.binding as EvidenceBinding | undefined,
|
|
313
415
|
};
|
|
314
416
|
const problem = evidenceUsageProblem(options);
|
|
315
417
|
if (problem !== null) return usage(problem);
|
|
@@ -428,7 +530,9 @@ function parse(argv: string[]) {
|
|
|
428
530
|
product: { type: "string" },
|
|
429
531
|
mode: { type: "string" },
|
|
430
532
|
baseline: { type: "string" },
|
|
533
|
+
binding: { type: "string" },
|
|
431
534
|
"write-baseline": { type: "string" },
|
|
535
|
+
check: { type: "boolean", default: false },
|
|
432
536
|
from: { type: "string" },
|
|
433
537
|
environment: { type: "string" },
|
|
434
538
|
"started-at": { type: "string" },
|
|
@@ -447,6 +551,8 @@ function parse(argv: string[]) {
|
|
|
447
551
|
"published-at": { type: "string" },
|
|
448
552
|
port: { type: "string" },
|
|
449
553
|
host: { type: "string" },
|
|
554
|
+
to: { type: "string" },
|
|
555
|
+
"allow-dirty": { type: "boolean", default: false },
|
|
450
556
|
},
|
|
451
557
|
});
|
|
452
558
|
}
|
package/src/reports.ts
CHANGED
|
@@ -35,6 +35,17 @@ function ofCounts(part: ClaimCounts, whole: ClaimCounts): string {
|
|
|
35
35
|
].join(", ");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Which binding judged the evidence, and why (Core §8). */
|
|
39
|
+
function bindingLine(run: EvidenceRun): string {
|
|
40
|
+
const { binding, reason } = run.binding;
|
|
41
|
+
if (binding === "subject") {
|
|
42
|
+
return `Binding: subject (${reason === "default" ? "the default, since slices claim files" : "as configured"}); a record is current while the subject it checked is unchanged`;
|
|
43
|
+
}
|
|
44
|
+
return reason === "no-slices"
|
|
45
|
+
? "Binding: commit, because no slice claims files and there is no subject to bind to (EVID004); a record is current only at the assessed commit and graph hash"
|
|
46
|
+
: "Binding: commit, as configured; a record is current only at the assessed commit and graph hash";
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function evidenceLines(run: EvidenceRun): string[] {
|
|
39
50
|
const { coverage, classification } = run.check;
|
|
40
51
|
const snap = run.snapshot;
|
|
@@ -46,6 +57,7 @@ export function evidenceLines(run: EvidenceRun): string[] {
|
|
|
46
57
|
const d = coverage.denominator;
|
|
47
58
|
const lines = [
|
|
48
59
|
`Evidence: ${count(run.records, "run record")} from ${from}; assessed at ${snap.commit === null ? "no commit" : `commit ${snap.commit}`}, graph ${snap.graphHash}, for ${scope}`,
|
|
60
|
+
bindingLine(run),
|
|
49
61
|
`Coverage over ${count(d.behaviors, "behavior")}, ${count(d.rules, "rule")} and ${count(d.scenarios, "scenario")} that are not retired:`,
|
|
50
62
|
` linked: ${ofCounts(coverage.linked, d)} (${coverage.measures.linked})`,
|
|
51
63
|
` verified: ${ofCounts(coverage.verified, d)} (${coverage.measures.verified})`,
|
|
@@ -57,7 +69,7 @@ export function evidenceLines(run: EvidenceRun): string[] {
|
|
|
57
69
|
if (ids.length === 0) lines.push(" none");
|
|
58
70
|
for (const id of ids) {
|
|
59
71
|
const v = classification.verifications[id];
|
|
60
|
-
lines.push(` ${id} ${v.method} ${v.status}${v.note === undefined ? "" : ` (${v.note})`}`);
|
|
72
|
+
lines.push(` ${id} ${v.method} ${v.status} [${v.binding}]${v.note === undefined ? "" : ` (${v.note})`}`);
|
|
61
73
|
}
|
|
62
74
|
lines.push(
|
|
63
75
|
`Not checked, so not a pass (${coverage.unresolved.length}):${coverage.unresolved.length === 0 ? " none" : ""}`,
|
|
@@ -94,9 +106,21 @@ export function levelJson(session: Session): Record<string, unknown> {
|
|
|
94
106
|
const { files, records, snapshot, check } = session.evidence;
|
|
95
107
|
const verifications: Record<string, unknown> = {};
|
|
96
108
|
for (const [id, v] of Object.entries(check.classification.verifications)) {
|
|
97
|
-
verifications[id] = {
|
|
109
|
+
verifications[id] = {
|
|
110
|
+
method: v.method,
|
|
111
|
+
status: v.status,
|
|
112
|
+
binding: v.binding,
|
|
113
|
+
...(v.note === undefined ? {} : { note: v.note }),
|
|
114
|
+
};
|
|
98
115
|
}
|
|
99
|
-
out.evidence = {
|
|
116
|
+
out.evidence = {
|
|
117
|
+
files,
|
|
118
|
+
records,
|
|
119
|
+
binding: session.evidence.binding,
|
|
120
|
+
snapshot: { commit: snapshot.commit, graphHash: snapshot.graphHash, scope: snapshot.scope ?? null },
|
|
121
|
+
coverage: check.coverage,
|
|
122
|
+
verifications,
|
|
123
|
+
};
|
|
100
124
|
}
|
|
101
125
|
if (session.publication !== undefined) out.publication = { checked: session.publication.checked };
|
|
102
126
|
return out;
|