@intentset/cli 0.6.0 → 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 +2 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +154 -45
- 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 +158 -46
- 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,26 +5,30 @@
|
|
|
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";
|
|
25
29
|
import { serveCommand } from "./commands/serve.ts";
|
|
26
30
|
import { validateCommand } from "./commands/validate.ts";
|
|
27
|
-
import type
|
|
31
|
+
import { type Io, TOOL } from "./output.ts";
|
|
28
32
|
import { type CarrierName, CARRIERS, findRoot } from "./repository.ts";
|
|
29
33
|
import { EVIDENCE_DIR, evidenceFiles, openSession, type SessionOptions } from "./session.ts";
|
|
30
34
|
|
|
@@ -34,68 +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
|
|
123
|
+
--version print "intentset <version>", the checker version a report names
|
|
93
124
|
|
|
94
|
-
exit codes: 0 no errors, 1 validation errors (or, with review --fail-on-drift,
|
|
95
|
-
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`;
|
|
96
127
|
|
|
97
128
|
const COMMANDS = [
|
|
98
129
|
"init",
|
|
130
|
+
"guide",
|
|
99
131
|
"validate",
|
|
100
132
|
"graph",
|
|
101
133
|
"architecture",
|
|
@@ -105,13 +137,16 @@ const COMMANDS = [
|
|
|
105
137
|
"review",
|
|
106
138
|
"publish",
|
|
107
139
|
"serve",
|
|
140
|
+
"migrate",
|
|
108
141
|
"mcp",
|
|
109
142
|
];
|
|
110
143
|
|
|
111
|
-
const GLOBAL = ["root", "json", "carrier", "level", "help"];
|
|
112
|
-
const LEVEL_OPTIONS = ["evidence", "product", "release", "mode", "baseline"];
|
|
144
|
+
const GLOBAL = ["root", "json", "carrier", "level", "help", "version"];
|
|
145
|
+
const LEVEL_OPTIONS = ["evidence", "product", "release", "mode", "baseline", "binding"];
|
|
113
146
|
const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
114
147
|
init: ["repository", "example", "agents"],
|
|
148
|
+
guide: [],
|
|
149
|
+
migrate: ["to", "allow-dirty"],
|
|
115
150
|
validate: LEVEL_OPTIONS,
|
|
116
151
|
graph: [
|
|
117
152
|
"format",
|
|
@@ -124,9 +159,13 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
124
159
|
"evidence",
|
|
125
160
|
"mode",
|
|
126
161
|
"baseline",
|
|
162
|
+
"binding",
|
|
127
163
|
],
|
|
128
164
|
architecture: ["mode", "baseline", "write-baseline"],
|
|
129
165
|
evidence: [
|
|
166
|
+
"format",
|
|
167
|
+
"check",
|
|
168
|
+
"evidence",
|
|
130
169
|
"from",
|
|
131
170
|
"out",
|
|
132
171
|
"product",
|
|
@@ -136,6 +175,7 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
136
175
|
"finished-at",
|
|
137
176
|
"uri-base",
|
|
138
177
|
"tool-version",
|
|
178
|
+
"binding",
|
|
139
179
|
],
|
|
140
180
|
impact: LEVEL_OPTIONS,
|
|
141
181
|
context: ["include-restricted", ...LEVEL_OPTIONS],
|
|
@@ -172,9 +212,9 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
|
|
|
172
212
|
};
|
|
173
213
|
|
|
174
214
|
/** Positional arguments after the command: a fixed subcommand, then how many more. */
|
|
175
|
-
const POSITIONALS: Record<string, {
|
|
176
|
-
architecture: {
|
|
177
|
-
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" },
|
|
178
218
|
impact: { count: 1, what: "an artifact ID" },
|
|
179
219
|
context: { count: 1, what: "an artifact ID or a file path" },
|
|
180
220
|
};
|
|
@@ -189,6 +229,10 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
189
229
|
}
|
|
190
230
|
const { values, positionals, tokens } = parsed;
|
|
191
231
|
const [command, ...rest] = positionals;
|
|
232
|
+
if (values.version && !values.help) {
|
|
233
|
+
io.stdout(`${TOOL.name} ${TOOL.version}\n`);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
192
236
|
if (values.help || command === undefined) {
|
|
193
237
|
io.stdout(`${USAGE}\n`);
|
|
194
238
|
return values.help ? 0 : 2;
|
|
@@ -211,14 +255,48 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
211
255
|
|
|
212
256
|
const shape = POSITIONALS[command] ?? { count: 0 };
|
|
213
257
|
let args = rest;
|
|
214
|
-
|
|
215
|
-
|
|
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];
|
|
216
271
|
args = args.slice(1);
|
|
217
272
|
}
|
|
218
|
-
if (args.length <
|
|
219
|
-
if (args.length >
|
|
220
|
-
const hint =
|
|
221
|
-
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
|
+
}
|
|
222
300
|
}
|
|
223
301
|
|
|
224
302
|
const carrier = values.carrier ?? "markset";
|
|
@@ -235,6 +313,15 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
235
313
|
if (command !== "mcp" && values.mode !== undefined && values.mode !== "migration" && values.mode !== "strict") {
|
|
236
314
|
return usage(`--mode ${values.mode}: the modes are migration and strict`);
|
|
237
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
|
+
}
|
|
238
325
|
if (command === "init") {
|
|
239
326
|
if (values.agents && values.example)
|
|
240
327
|
return usage("--agents writes only the guide; it does not combine with --example");
|
|
@@ -246,8 +333,8 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
246
333
|
|
|
247
334
|
// Evidence scope: --product with --release, or graph's --release <product>:<label>.
|
|
248
335
|
let scope: RunScope | null = null;
|
|
249
|
-
if (command !== "evidence" && command !== "publish" && command !== "mcp") {
|
|
250
|
-
if (command === "graph") {
|
|
336
|
+
if ((command !== "evidence" || sub === "stale") && command !== "publish" && command !== "mcp") {
|
|
337
|
+
if (command === "graph" || command === "evidence") {
|
|
251
338
|
const release = values.release === undefined ? null : parseRelease(values.release);
|
|
252
339
|
scope = release === null ? null : { product: release.product, release: release.label };
|
|
253
340
|
} else if ((values.product === undefined) !== (values.release === undefined)) {
|
|
@@ -257,6 +344,9 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
257
344
|
}
|
|
258
345
|
}
|
|
259
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
|
+
}
|
|
260
350
|
const sessionOptions = (level: Level): SessionOptions => ({
|
|
261
351
|
root: values.root,
|
|
262
352
|
carrier: carrier as CarrierName,
|
|
@@ -265,6 +355,7 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
265
355
|
baseline: values.baseline,
|
|
266
356
|
evidence: values.evidence,
|
|
267
357
|
scope,
|
|
358
|
+
binding: values.binding as EvidenceBinding | undefined,
|
|
268
359
|
});
|
|
269
360
|
|
|
270
361
|
switch (command) {
|
|
@@ -294,6 +385,21 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
294
385
|
return architectureCommand(session, { writeBaseline: values["write-baseline"] }, values.json, io);
|
|
295
386
|
}
|
|
296
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
|
+
}
|
|
297
403
|
const options = {
|
|
298
404
|
from: values.from,
|
|
299
405
|
report: args[0],
|
|
@@ -305,6 +411,7 @@ export async function main(argv: string[], io: Io): Promise<number> {
|
|
|
305
411
|
finishedAt: values["finished-at"],
|
|
306
412
|
uriBase: values["uri-base"],
|
|
307
413
|
toolVersion: values["tool-version"],
|
|
414
|
+
binding: values.binding as EvidenceBinding | undefined,
|
|
308
415
|
};
|
|
309
416
|
const problem = evidenceUsageProblem(options);
|
|
310
417
|
if (problem !== null) return usage(problem);
|
|
@@ -407,6 +514,7 @@ function parse(argv: string[]) {
|
|
|
407
514
|
carrier: { type: "string" },
|
|
408
515
|
level: { type: "string" },
|
|
409
516
|
help: { type: "boolean", short: "h", default: false },
|
|
517
|
+
version: { type: "boolean", default: false },
|
|
410
518
|
repository: { type: "string" },
|
|
411
519
|
example: { type: "boolean", default: false },
|
|
412
520
|
agents: { type: "boolean", default: false },
|
|
@@ -422,7 +530,9 @@ function parse(argv: string[]) {
|
|
|
422
530
|
product: { type: "string" },
|
|
423
531
|
mode: { type: "string" },
|
|
424
532
|
baseline: { type: "string" },
|
|
533
|
+
binding: { type: "string" },
|
|
425
534
|
"write-baseline": { type: "string" },
|
|
535
|
+
check: { type: "boolean", default: false },
|
|
426
536
|
from: { type: "string" },
|
|
427
537
|
environment: { type: "string" },
|
|
428
538
|
"started-at": { type: "string" },
|
|
@@ -441,6 +551,8 @@ function parse(argv: string[]) {
|
|
|
441
551
|
"published-at": { type: "string" },
|
|
442
552
|
port: { type: "string" },
|
|
443
553
|
host: { type: "string" },
|
|
554
|
+
to: { type: "string" },
|
|
555
|
+
"allow-dirty": { type: "boolean", default: false },
|
|
444
556
|
},
|
|
445
557
|
});
|
|
446
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;
|