@intentset/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -33,10 +33,12 @@ export type { Io } from "./output.ts";
33
33
  export const USAGE = `usage: intentset <command> [options]
34
34
 
35
35
  commands
36
- init [--repository <name>] [--example]
37
- create .intentset/config.yaml and an empty .intentset/registries.yaml;
38
- --example adds the scheduling example under product/scheduling/.
39
- Never overwrites a file.
36
+ init [--repository <name>] [--example] [--agents]
37
+ create .intentset/config.yaml, an empty .intentset/registries.yaml and
38
+ .intentset/agents.md, the guide that tells coding agents how to keep the
39
+ model current; --example adds the scheduling example under
40
+ product/scheduling/; --agents writes only the guide, for a repository
41
+ already initialized. Never overwrites a file.
40
42
  validate every check the level asks for; exit 1 when any diagnostic is an error
41
43
  graph [--format json] [--include-bodies] [--release <product>:<label>] [--out <file>]
42
44
  [--generated-at <time>] [--report evidence|knowledge|impact|ownership|all]...
@@ -52,9 +54,14 @@ commands
52
54
  [--tool-version <v>]
53
55
  turn a test report into run records bound to this commit and graph
54
56
  impact <ID> what depends on an artifact: direct, candidates, review context, ancestors
55
- context <ID> [--include-restricted]
56
- the bounded engineering context of an artifact, with paths and bodies
57
- review [--base <ref>] the local review report: checks, coverage, and the impact of what changed
57
+ context <ID|path> [--include-restricted]
58
+ the bounded engineering context of an artifact, with paths and bodies;
59
+ given a file a slice claims, the context of that slice
60
+ review [--base <ref>] [--fail-on-drift]
61
+ the local review report: checks, coverage, the impact of what changed,
62
+ and each slice whose code changed while its records did not;
63
+ --fail-on-drift exits 1 for any such slice no commit acknowledged with
64
+ an Intentset-Unchanged trailer
58
65
  publish --visibility <v> --audience <a> --product <ID> --release <r> --role <r> --edition <e>
59
66
  [--flag <f>]... [--authorized-internal] [--authorized-restricted] --out <dir> [--html]
60
67
  [--published-at <time>]
@@ -84,7 +91,8 @@ options
84
91
  (for mcp, --mode names the server's mode instead)
85
92
  -h, --help show this help
86
93
 
87
- exit codes: 0 no errors, 1 validation errors, 2 invocation or tool failure`;
94
+ exit codes: 0 no errors, 1 validation errors (or, with review --fail-on-drift, unacknowledged drift),
95
+ 2 invocation or tool failure`;
88
96
 
89
97
  const COMMANDS = [
90
98
  "init",
@@ -103,7 +111,7 @@ const COMMANDS = [
103
111
  const GLOBAL = ["root", "json", "carrier", "level", "help"];
104
112
  const LEVEL_OPTIONS = ["evidence", "product", "release", "mode", "baseline"];
105
113
  const COMMAND_OPTIONS: Record<string, string[]> = {
106
- init: ["repository", "example"],
114
+ init: ["repository", "example", "agents"],
107
115
  validate: LEVEL_OPTIONS,
108
116
  graph: [
109
117
  "format",
@@ -131,7 +139,7 @@ const COMMAND_OPTIONS: Record<string, string[]> = {
131
139
  ],
132
140
  impact: LEVEL_OPTIONS,
133
141
  context: ["include-restricted", ...LEVEL_OPTIONS],
134
- review: ["base", ...LEVEL_OPTIONS],
142
+ review: ["base", "fail-on-drift", ...LEVEL_OPTIONS],
135
143
  serve: ["port", "host", "out", ...LEVEL_OPTIONS],
136
144
  mcp: [
137
145
  "mode",
@@ -168,7 +176,7 @@ const POSITIONALS: Record<string, { sub?: string; count: number; what?: string }
168
176
  architecture: { sub: "check", count: 0 },
169
177
  evidence: { sub: "import", count: 1, what: "a report file" },
170
178
  impact: { count: 1, what: "an artifact ID" },
171
- context: { count: 1, what: "an artifact ID" },
179
+ context: { count: 1, what: "an artifact ID or a file path" },
172
180
  };
173
181
 
174
182
  export async function main(argv: string[], io: Io): Promise<number> {
@@ -228,7 +236,12 @@ export async function main(argv: string[], io: Io): Promise<number> {
228
236
  return usage(`--mode ${values.mode}: the modes are migration and strict`);
229
237
  }
230
238
  if (command === "init") {
231
- return initCommand({ root: values.root, repository: values.repository, example: values.example }, io);
239
+ if (values.agents && values.example)
240
+ return usage("--agents writes only the guide; it does not combine with --example");
241
+ return initCommand(
242
+ { root: values.root, repository: values.repository, example: values.example, agentsOnly: values.agents },
243
+ io,
244
+ );
232
245
  }
233
246
 
234
247
  // Evidence scope: --product with --release, or graph's --release <product>:<label>.
@@ -321,7 +334,12 @@ export async function main(argv: string[], io: Io): Promise<number> {
321
334
  }
322
335
  const session = openSession(command, sessionOptions(level), io);
323
336
  if (typeof session === "number") return session;
324
- return reviewCommand(session, { base: values.base, levelReason: reason }, values.json, io);
337
+ return reviewCommand(
338
+ session,
339
+ { base: values.base, levelReason: reason, failOnDrift: values["fail-on-drift"] },
340
+ values.json,
341
+ io,
342
+ );
325
343
  }
326
344
  case "serve": {
327
345
  const port = values.port === undefined ? 3000 : Number(values.port);
@@ -391,6 +409,8 @@ function parse(argv: string[]) {
391
409
  help: { type: "boolean", short: "h", default: false },
392
410
  repository: { type: "string" },
393
411
  example: { type: "boolean", default: false },
412
+ agents: { type: "boolean", default: false },
413
+ "fail-on-drift": { type: "boolean", default: false },
394
414
  format: { type: "string" },
395
415
  "include-bodies": { type: "boolean", default: false },
396
416
  release: { type: "string" },
package/src/slices.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Slices, the code they claim and the records that describe them, for
3
+ * `review`'s list of slices whose code changed while their records did not
4
+ * (Core §10) and for `context <path>`.
5
+ *
6
+ * A slice's code is what its `source`, `backend` and `contract` claims match;
7
+ * its `verification` and `documentation` claims are checks and prose, not
8
+ * implementation. The records that describe a slice are the slice itself, the
9
+ * behaviors it implements, the rules that govern them, the scenarios that
10
+ * illustrate them, the verifications of any of those, the contracts it
11
+ * exposes, and the decisions that inform the slice, its behaviors or its
12
+ * contracts. Knowledge is left out: it explains the model to an audience, and
13
+ * review pins already mark it for review when the model changes.
14
+ */
15
+ import { type Artifact, compareStrings, type Graph } from "@intentset/core";
16
+ import { matchGlob } from "./glob.ts";
17
+
18
+ const CODE_CLAIMS = new Set(["source", "backend", "contract"]);
19
+
20
+ /** Every slice that is not retired and has slice metadata, sorted by ID. */
21
+ export function activeSlices(graph: Graph): Artifact[] {
22
+ return [...graph.artifacts.values()]
23
+ .filter((a) => a.meta.type === "slice" && a.meta.status !== "retired" && a.meta.slice !== undefined)
24
+ .sort((a, b) => compareStrings(a.meta.id, b.meta.id));
25
+ }
26
+
27
+ /** The slices with any claim matching `path`, sorted by ID. */
28
+ export function claimingSlices(graph: Graph, path: string): Artifact[] {
29
+ return activeSlices(graph).filter((slice) => slice.meta.slice?.claims.some((claim) => matchGlob(claim.path, path)));
30
+ }
31
+
32
+ /** True when one of the slice's source, backend or contract claims matches `path`. */
33
+ export function isSliceCode(slice: Artifact, path: string): boolean {
34
+ return slice.meta.slice?.claims.some((claim) => CODE_CLAIMS.has(claim.kind) && matchGlob(claim.path, path)) ?? false;
35
+ }
36
+
37
+ /** The IDs of the records that describe a slice, the slice included, sorted. Only IDs present in the graph. */
38
+ export function describingRecords(graph: Graph, slice: Artifact): string[] {
39
+ const out = new Set<string>([slice.meta.id]);
40
+ const targets = (id: string, kind: string) =>
41
+ (graph.out.get(id) ?? []).filter((edge) => edge.kind === kind).map((edge) => edge.to);
42
+ const sources = (id: string, kind: string) =>
43
+ (graph.in.get(id) ?? []).filter((edge) => edge.kind === kind).map((edge) => edge.from);
44
+
45
+ const behaviors = targets(slice.meta.id, "implements");
46
+ const rules = behaviors.flatMap((id) => targets(id, "governedBy"));
47
+ const scenarios = behaviors.flatMap((id) => sources(id, "illustrates"));
48
+ const claims = [...behaviors, ...rules, ...scenarios];
49
+ const verifications = claims.flatMap((id) => sources(id, "verifies"));
50
+ const contracts = targets(slice.meta.id, "exposes");
51
+ const decisions = [slice.meta.id, ...behaviors, ...contracts].flatMap((id) => targets(id, "informedBy"));
52
+ for (const id of [...claims, ...verifications, ...contracts, ...decisions]) out.add(id);
53
+ return [...out].filter((id) => graph.artifacts.has(id)).sort(compareStrings);
54
+ }