@intentius/chant 0.93.0 → 0.95.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/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +14 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/operator.d.ts +90 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts +84 -0
- package/dist/op/steward-beside.d.ts.map +1 -0
- package/dist/op/steward.d.ts +87 -2
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/box-intent.d.ts +85 -0
- package/dist/workspace/box-intent.d.ts.map +1 -0
- package/dist/workspace/box-services.d.ts +31 -0
- package/dist/workspace/box-services.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +5 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts +14 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +11 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +5 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +43 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +62 -1
- package/dist/workspace/graph-cache.d.ts +168 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -5
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +5 -5
- package/dist/workspace/kind-readers.d.ts +39 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +29 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +15 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-run.d.ts +2 -0
- package/dist/workspace/member-run.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +3 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +5 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +10 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +4 -2
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +8 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +31 -9
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +20 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +26 -0
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +7 -3
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +97 -0
- package/src/cli/handlers/operator.ts +53 -11
- package/src/cli/handlers/run.test.ts +71 -0
- package/src/cli/handlers/run.ts +65 -7
- package/src/cli/main.ts +21 -9
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/registry.ts +2 -0
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- package/src/cli/static-config-read.test.ts +8 -2
- package/src/meta/source-is-text.test.ts +21 -3
- package/src/okf.test.ts +6 -1
- package/src/op/activities/decide.test.ts +8 -0
- package/src/op/builders.ts +14 -3
- package/src/op/index.ts +8 -2
- package/src/op/operator.ts +264 -16
- package/src/op/steward-beside.test.ts +267 -0
- package/src/op/steward-beside.ts +219 -0
- package/src/op/steward-points.test.ts +112 -1
- package/src/op/steward.ts +135 -3
- package/src/workspace/box-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -0
- package/src/workspace/box-services.test.ts +129 -0
- package/src/workspace/box-services.ts +51 -0
- package/src/workspace/chant-migrations.ts +5 -0
- package/src/workspace/check.schema.json +7 -3
- package/src/workspace/checks/boxes.test.ts +3 -1
- package/src/workspace/checks/boxes.ts +66 -0
- package/src/workspace/checks.ts +12 -1
- package/src/workspace/compose-graph.test.ts +1 -0
- package/src/workspace/compose-graph.ts +11 -0
- package/src/workspace/composites.test.ts +1 -1
- package/src/workspace/composites.ts +12 -5
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -2
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +62 -1
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +26 -0
- package/src/workspace/graph-cache.test.ts +343 -0
- package/src/workspace/graph-cache.ts +409 -0
- package/src/workspace/graph-cli.ts +93 -26
- package/src/workspace/graph-contract.test.ts +129 -6
- package/src/workspace/graph.schema.json +21 -1
- package/src/workspace/intent-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/kind-readers.e2e.test.ts +68 -0
- package/src/workspace/kind-readers.test.ts +134 -0
- package/src/workspace/kind-readers.ts +111 -0
- package/src/workspace/kinds.test.ts +49 -0
- package/src/workspace/kinds.ts +59 -2
- package/src/workspace/member-commands.test.ts +15 -0
- package/src/workspace/member-commands.ts +47 -7
- package/src/workspace/member-run.ts +10 -2
- package/src/workspace/points-cli.ts +3 -0
- package/src/workspace/points.schema.json +18 -0
- package/src/workspace/points.test.ts +15 -14
- package/src/workspace/points.ts +4 -16
- package/src/workspace/read-contract.test.ts +8 -0
- package/src/workspace/reason-codes.test.ts +7 -7
- package/src/workspace/reason-codes.ts +6 -1
- package/src/workspace/records-amend.schema.json +1 -0
- package/src/workspace/records-cli.ts +34 -13
- package/src/workspace/records-contract.test.ts +2 -1
- package/src/workspace/records-formats.test.ts +15 -15
- package/src/workspace/records-new.schema.json +1 -0
- package/src/workspace/records-quorum.test.ts +10 -3
- package/src/workspace/records-sessions-write.test.ts +2 -1
- package/src/workspace/records-since.test.ts +8 -7
- package/src/workspace/records-write.test.ts +101 -1
- package/src/workspace/records-write.ts +50 -6
- package/src/workspace/records.test.ts +1 -1
- package/src/workspace/records.ts +22 -5
- package/src/workspace/status-contract.test.ts +32 -1
- package/src/workspace/status-stewards.ts +88 -9
- package/src/workspace/status.schema.json +66 -4
- package/src/workspace/status.ts +32 -0
- package/src/workspace/trust/record-seal.test.ts +26 -2
- package/src/workspace/work-evidence.schema.json +1 -0
- package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +17 -0
- package/src/workspace/workspace-kinds.schema.json +26 -0
package/src/workspace/kinds.ts
CHANGED
|
@@ -67,6 +67,16 @@ export type KindOutputs =
|
|
|
67
67
|
/** The names listed here, plus any the member entry lists in `outputs` (`other`, and kinds from a package). */
|
|
68
68
|
| { from: "declared"; names: string[] };
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* How `chant workspace graph` reads a member of a package's kind (#2874): a
|
|
72
|
+
* lexicon, by name, and that lexicon's config namespace for a reader project
|
|
73
|
+
* chant writes itself. See {@link substituteGraphConfig} for the placeholders.
|
|
74
|
+
*/
|
|
75
|
+
export interface KindGraph {
|
|
76
|
+
lexicon: string;
|
|
77
|
+
config: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
export interface MemberKind {
|
|
71
81
|
name: string;
|
|
72
82
|
/** One line for listings and error messages. */
|
|
@@ -84,6 +94,10 @@ export interface MemberKind {
|
|
|
84
94
|
outputs: KindOutputs;
|
|
85
95
|
/** Where the kind comes from: `builtin`, or the package that supplies it. */
|
|
86
96
|
source: string;
|
|
97
|
+
/** The supplying package's directory, absolute; unset for a built-in kind. */
|
|
98
|
+
packageDir?: string;
|
|
99
|
+
/** How `workspace graph` reads a member of this kind (#2874); unset, the member is `kind-not-run`. */
|
|
100
|
+
graph?: KindGraph;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
export interface KindRegistry {
|
|
@@ -310,7 +324,7 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
310
324
|
const problems: string[] = [];
|
|
311
325
|
const kinds: MemberKind[] = [];
|
|
312
326
|
const seen = new Set<string>();
|
|
313
|
-
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[] }[] }).kinds) {
|
|
327
|
+
for (const k of (raw as { kinds: { name: string; description: string; precedence: number; probe: FileProbe; outputs?: string[]; graph?: KindGraph }[] }).kinds) {
|
|
314
328
|
if (BUILTIN_KIND_NAMES.includes(k.name)) {
|
|
315
329
|
problems.push(`${source}: kind ${k.name} is built in and can't be supplied by a package`);
|
|
316
330
|
continue;
|
|
@@ -331,6 +345,7 @@ export function parseKindData(text: string, source: string): KindData {
|
|
|
331
345
|
shape: "member",
|
|
332
346
|
outputs: { from: "declared", names: [...(k.outputs ?? [])] },
|
|
333
347
|
source,
|
|
348
|
+
...(k.graph ? { graph: { lexicon: k.graph.lexicon, config: structuredClone(k.graph.config) } } : {}),
|
|
334
349
|
});
|
|
335
350
|
}
|
|
336
351
|
return { kinds, problems };
|
|
@@ -389,7 +404,49 @@ export function readPackageKinds(packageDir: string, source?: string): PackageKi
|
|
|
389
404
|
} catch {
|
|
390
405
|
return { kinds: [], problems: [`${name}: exports["${KINDS_SUBPATH}"] names ${target}, which does not exist`], file };
|
|
391
406
|
}
|
|
392
|
-
|
|
407
|
+
const data = parseKindData(text, name);
|
|
408
|
+
const packageName = typeof pkg.name === "string" ? pkg.name : undefined;
|
|
409
|
+
for (const k of data.kinds) {
|
|
410
|
+
k.packageDir = packageDir;
|
|
411
|
+
if (k.graph && packageName !== lexiconPackageName(k.graph.lexicon)) {
|
|
412
|
+
data.problems.push(
|
|
413
|
+
`${name}: kind ${k.name} reads its members with lexicon ${k.graph.lexicon}, which is ${lexiconPackageName(k.graph.lexicon)}, ` +
|
|
414
|
+
`and this package is ${packageName ?? "unnamed"}; the kind is kept, and workspace graph lists its members with kind-not-run`,
|
|
415
|
+
);
|
|
416
|
+
delete k.graph;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { ...data, file };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** The package a lexicon name resolves to. */
|
|
423
|
+
export function lexiconPackageName(lexicon: string): string {
|
|
424
|
+
return `@intentius/chant-lexicon-${lexicon}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Where each placeholder in a kind's graph config points (#2874). */
|
|
428
|
+
export interface GraphPlaceholders {
|
|
429
|
+
/** The member's name. */
|
|
430
|
+
member: string;
|
|
431
|
+
/** The member's directory, absolute. */
|
|
432
|
+
dir: string;
|
|
433
|
+
/** The workspace root, absolute. */
|
|
434
|
+
workspace: string;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A kind's graph config with `{member}`, `{dir}` and `{workspace}` replaced
|
|
439
|
+
* in every string key and value. Other braces are left as they are.
|
|
440
|
+
*/
|
|
441
|
+
export function substituteGraphConfig(config: Record<string, unknown>, at: GraphPlaceholders): Record<string, unknown> {
|
|
442
|
+
const text = (v: string) => v.replace(/\{(member|dir|workspace)\}/g, (_, key: keyof GraphPlaceholders) => at[key]);
|
|
443
|
+
const walk = (v: unknown): unknown => {
|
|
444
|
+
if (typeof v === "string") return text(v);
|
|
445
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
446
|
+
if (v !== null && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [text(k), walk(x)]));
|
|
447
|
+
return v;
|
|
448
|
+
};
|
|
449
|
+
return walk(config) as Record<string, unknown>;
|
|
393
450
|
}
|
|
394
451
|
|
|
395
452
|
/**
|
|
@@ -156,6 +156,21 @@ describe("member command lines", () => {
|
|
|
156
156
|
expect(memberArgv("audit", unit, args({ failOn: "error" }))).toEqual(["audit", ".", "--format", "json", "--fail-on", "error"]);
|
|
157
157
|
expect(memberArgv("graph", unit, args({ format: "mermaid" }))).toEqual(["graph", ".", "--format", "ir"]);
|
|
158
158
|
});
|
|
159
|
+
|
|
160
|
+
test("graph hands each member the live read's flags as they are (#2875)", () => {
|
|
161
|
+
expect(memberArgv("graph", unit, args({ env: "prod", live: true, overlay: true, traffic: "100 rps, p50" }))).toEqual([
|
|
162
|
+
"graph", ".", "--format", "ir", "--env", "prod", "--live", "--overlay", "--traffic", "100 rps, p50",
|
|
163
|
+
]);
|
|
164
|
+
expect(memberArgv("graph", unit, args({ env: "prod" }))).toEqual(["graph", ".", "--format", "ir", "--env", "prod"]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a reader unit takes --env only with --live, which needs one (#2874, #2875)", () => {
|
|
168
|
+
const reader = { ...unit, reader: { lexicon: "terraform", config: {}, packageDir: "/p" } } as typeof unit;
|
|
169
|
+
expect(memberArgv("graph", reader, args({ env: "prod" }))).toEqual(["graph", ".", "--format", "ir"]);
|
|
170
|
+
expect(memberArgv("graph", reader, args({ env: "prod", live: true, overlay: true }))).toEqual([
|
|
171
|
+
"graph", ".", "--format", "ir", "--env", "prod", "--live", "--overlay",
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
159
174
|
});
|
|
160
175
|
|
|
161
176
|
describe("the member-run protocol", () => {
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* Members of kind `other` are never read, and a nested `workspace` member
|
|
10
10
|
* runs its own workspace commands, so both are listed as skipped with the
|
|
11
11
|
* reason code `kind-not-run`.
|
|
12
|
+
* - For `graph` only, members of a pinned package's kind whose kinds file
|
|
13
|
+
* says how to read them (#2874, a `graph` block): each runs `chant graph`
|
|
14
|
+
* in a reader project chant writes for it (`kind-readers.ts`). Other
|
|
15
|
+
* package kinds are `kind-not-run`, as before.
|
|
12
16
|
* - For `build` and `lint` only, every project an example group matches
|
|
13
17
|
* (ws-051): groups are built and linted, and have no ledger, audit or place
|
|
14
18
|
* in the graph. On a large repository this is most of the run, so
|
|
@@ -38,7 +42,7 @@ import { findWorkspaceRoot } from "../project-root";
|
|
|
38
42
|
import type { ComposedMember, MemberReason } from "./compose-graph";
|
|
39
43
|
import { mergeAudit, mergeSarif, type MemberOutput } from "./compose-reports";
|
|
40
44
|
import { readDeclaration, resolveGroups, rootExclusions, WorkspaceReadError, type Declaration } from "./declaration";
|
|
41
|
-
import { builtinKindRegistry, type KindRegistry } from "./kinds";
|
|
45
|
+
import { builtinKindRegistry, type KindGraph, type KindRegistry } from "./kinds";
|
|
42
46
|
import { memberReason } from "./ls";
|
|
43
47
|
import { MEMBER_RUN_PROTOCOL, PROTOCOL_PREFIX, type MemberRunLine, type MemberRunRequest } from "./member-run";
|
|
44
48
|
import { workingTree, type WorkspaceTree } from "./tree";
|
|
@@ -58,6 +62,12 @@ export interface RunUnit {
|
|
|
58
62
|
abs: string;
|
|
59
63
|
/** Directories (relative to `dir`) left out of discovery; set for member `.`. */
|
|
60
64
|
exclude: string[];
|
|
65
|
+
/**
|
|
66
|
+
* Set for a member read through its kind's `graph` block (#2874). `abs` is
|
|
67
|
+
* the member's directory until `kind-readers.ts` writes the reader project
|
|
68
|
+
* and points `abs` at it.
|
|
69
|
+
*/
|
|
70
|
+
reader?: KindGraph & { packageDir: string };
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export interface SkippedEntry {
|
|
@@ -189,6 +199,25 @@ export function planMembers(verb: WorkspaceVerb, root: string, options: PlanOpti
|
|
|
189
199
|
}
|
|
190
200
|
continue;
|
|
191
201
|
}
|
|
202
|
+
const byKind = verb === "graph" && entry.kind !== "chant" ? kinds.get(entry.kind) : undefined;
|
|
203
|
+
if (byKind?.graph && byKind.packageDir) {
|
|
204
|
+
const reason = memberReason(entry, tree, kinds);
|
|
205
|
+
if (reason) {
|
|
206
|
+
unreadable.push({ name: entry.name, dir: entry.dir, kind: entry.kind, reason });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
units.push({
|
|
210
|
+
id: entry.name,
|
|
211
|
+
member: entry.name,
|
|
212
|
+
kind: entry.kind,
|
|
213
|
+
group: false,
|
|
214
|
+
dir: entry.dir,
|
|
215
|
+
abs: entry.dir === "." ? root : join(root, entry.dir),
|
|
216
|
+
exclude: [],
|
|
217
|
+
reader: { ...byKind.graph, packageDir: byKind.packageDir },
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
192
221
|
if (entry.kind !== "chant") {
|
|
193
222
|
const why =
|
|
194
223
|
entry.kind === "workspace"
|
|
@@ -265,8 +294,15 @@ export function memberArgv(verb: WorkspaceVerb, unit: RunUnit, args: ParsedArgs)
|
|
|
265
294
|
if (args.tier) argv.push("--tier", args.tier);
|
|
266
295
|
if (args.failOn) argv.push("--fail-on", args.failOn);
|
|
267
296
|
if (args.maxFiles !== undefined) argv.push("--max-files", String(args.maxFiles));
|
|
268
|
-
} else
|
|
269
|
-
|
|
297
|
+
} else {
|
|
298
|
+
// graph. A reader project declares no environments, so a source read
|
|
299
|
+
// leaves --env to the chant members. A live read needs one, and a reader
|
|
300
|
+
// project with none declared takes any name, so --live carries it (#2875).
|
|
301
|
+
if (args.env && (!unit.reader || args.live)) argv.push("--env", args.env);
|
|
302
|
+
// The live read's flags go to every member as they are (#2875).
|
|
303
|
+
if (args.live) argv.push("--live");
|
|
304
|
+
if (args.overlay) argv.push("--overlay");
|
|
305
|
+
if (args.traffic) argv.push("--traffic", args.traffic);
|
|
270
306
|
}
|
|
271
307
|
return argv;
|
|
272
308
|
}
|
|
@@ -286,6 +322,8 @@ export interface UnitResult extends MemberOutput {
|
|
|
286
322
|
chant: string | null;
|
|
287
323
|
/** How the member ran: inside its toolchain's shared process, or in a level-0 process of its own. */
|
|
288
324
|
mode: "member-run" | "per-member";
|
|
325
|
+
/** When the member's command finished, as an ISO time (#2875). */
|
|
326
|
+
finishedAt?: string;
|
|
289
327
|
}
|
|
290
328
|
|
|
291
329
|
interface Spawned {
|
|
@@ -349,13 +387,15 @@ async function runGroup(verb: WorkspaceVerb, group: ToolchainGroup, root: string
|
|
|
349
387
|
units: units.map((u) => ({ id: u.id, dir: u.abs, argv: argvs.get(u.id)!, ...(u.exclude.length ? { exclude: u.exclude } : {}) })),
|
|
350
388
|
};
|
|
351
389
|
const answer = await run(toolchain.command, ["workspace", "member-run"], root, JSON.stringify(request));
|
|
390
|
+
const ended = new Date().toISOString();
|
|
352
391
|
const parsed = parseMemberRunOutput(answer.stdout);
|
|
353
392
|
if (parsed) {
|
|
354
393
|
if (parsed.stray) process.stderr.write(`${parsed.stray}\n`);
|
|
355
394
|
return units.map((unit) => {
|
|
356
395
|
const r = parsed.results.get(unit.id);
|
|
357
396
|
const base = { unit, toolchain, chant: parsed.chant, mode: "member-run" as const, id: unit.id, member: unit.member, dir: unit.dir, exclude: unit.exclude };
|
|
358
|
-
|
|
397
|
+
// A member-run from before #2875 names no time; the group's end is the closest one.
|
|
398
|
+
if (r) return { ...base, exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr, finishedAt: r.finishedAt ?? ended };
|
|
359
399
|
return { ...base, exitCode: answer.exitCode || 1, stdout: "", stderr: `the member-run process for ${toolchain.identity} ended before running this member\n${answer.stderr}` };
|
|
360
400
|
});
|
|
361
401
|
}
|
|
@@ -363,7 +403,7 @@ async function runGroup(verb: WorkspaceVerb, group: ToolchainGroup, root: string
|
|
|
363
403
|
const out: UnitResult[] = [];
|
|
364
404
|
for (const unit of units) {
|
|
365
405
|
const r = await run(toolchain.command, argvs.get(unit.id)!, unit.abs);
|
|
366
|
-
out.push({ unit, toolchain, chant: null, mode: "per-member", id: unit.id, member: unit.member, dir: unit.dir, exclude: unit.exclude, ...r });
|
|
406
|
+
out.push({ unit, toolchain, chant: null, mode: "per-member", id: unit.id, member: unit.member, dir: unit.dir, exclude: unit.exclude, ...r, finishedAt: new Date().toISOString() });
|
|
367
407
|
}
|
|
368
408
|
return out;
|
|
369
409
|
}
|
|
@@ -385,7 +425,7 @@ const USAGE: Record<WorkspaceVerb, string> = {
|
|
|
385
425
|
build: "chant workspace build [dir] [--member <name>] [-o <dir>] [--format json|yaml] [--env <env>] [--param k=v] [--dry-run]",
|
|
386
426
|
lint: "chant workspace lint [dir] [--member <name>] [--format stylish|json|sarif] [-o <file>] [--fix] [--dry-run]",
|
|
387
427
|
audit: "chant workspace audit [dir] [--member <name>] [--format stylish|json] [-o <file>] [--tier <tier>] [--fail-on <level>] [--dry-run]",
|
|
388
|
-
graph: "chant workspace graph [dir] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--dry-run]",
|
|
428
|
+
graph: "chant workspace graph [dir] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--live [--overlay] [--traffic <level>]] [--no-cache] [--dry-run]",
|
|
389
429
|
};
|
|
390
430
|
|
|
391
431
|
export function describePlan(plan: MemberPlan): string {
|
|
@@ -461,7 +501,7 @@ function formatAuditText(doc: ReturnType<typeof mergeAudit>, plan: MemberPlan):
|
|
|
461
501
|
}
|
|
462
502
|
|
|
463
503
|
export function memberStatus(name: string, dir: string, kind: string, status: ComposedMember["status"], reason: MemberReason | null, chant: string | null): ComposedMember {
|
|
464
|
-
return { name, dir, kind, status, reason, chant, irVersion: null };
|
|
504
|
+
return { name, dir, kind, status, reason, chant, irVersion: null, live: false };
|
|
465
505
|
}
|
|
466
506
|
|
|
467
507
|
export async function runWorkspaceMembers(ctx: CommandContext, verb: WorkspaceVerb): Promise<number> {
|
|
@@ -49,7 +49,15 @@ export interface MemberRunUnit {
|
|
|
49
49
|
|
|
50
50
|
export type MemberRunLine =
|
|
51
51
|
| { type: "header"; protocol: number; chant: string }
|
|
52
|
-
| {
|
|
52
|
+
| {
|
|
53
|
+
type: "result";
|
|
54
|
+
id: string;
|
|
55
|
+
exitCode: number;
|
|
56
|
+
stdout: string;
|
|
57
|
+
stderr: string;
|
|
58
|
+
/** When the member's command finished, as an ISO time (#2875). A member-run from before it leaves this out. */
|
|
59
|
+
finishedAt?: string;
|
|
60
|
+
};
|
|
53
61
|
|
|
54
62
|
/** A `process.exit` inside a command, turned into a value. */
|
|
55
63
|
class ExitRequest extends Error {
|
|
@@ -125,7 +133,7 @@ export async function runMemberUnits(request: MemberRunRequest, run: (argv: stri
|
|
|
125
133
|
if (startEnv === undefined) delete process.env[ENV_VAR];
|
|
126
134
|
else process.env[ENV_VAR] = startEnv;
|
|
127
135
|
process.chdir(startDir);
|
|
128
|
-
emit({ type: "result", id: unit.id, ...result });
|
|
136
|
+
emit({ type: "result", id: unit.id, ...result, finishedAt: new Date().toISOString() });
|
|
129
137
|
}
|
|
130
138
|
return 0;
|
|
131
139
|
}
|
|
@@ -71,6 +71,8 @@ export interface PointView {
|
|
|
71
71
|
questionType: string;
|
|
72
72
|
instructions: string;
|
|
73
73
|
candidates: (string | boolean)[];
|
|
74
|
+
/** What each candidate means, as the points file declares it: an object of strings for noul and choice, an array for score. */
|
|
75
|
+
criteria: Record<string, string> | string[];
|
|
74
76
|
/** Each input: its name, the read-contract output it names, and its description. */
|
|
75
77
|
inputs: { name: string; output: string; description: string }[];
|
|
76
78
|
deciders: Decider[];
|
|
@@ -163,6 +165,7 @@ export async function workspacePoints(query: PointsQuery): Promise<PointsDocumen
|
|
|
163
165
|
questionType: p.question.type,
|
|
164
166
|
instructions: p.question.instructions,
|
|
165
167
|
candidates: candidates(p.question),
|
|
168
|
+
criteria: p.question.criteria,
|
|
166
169
|
inputs: Object.entries(p.inputs).map(([n, description]) => ({ name: n, output: inputOutput(n), description })),
|
|
167
170
|
deciders: p.deciders,
|
|
168
171
|
quorum: quorumOf(p),
|
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
"questionType",
|
|
192
192
|
"instructions",
|
|
193
193
|
"candidates",
|
|
194
|
+
"criteria",
|
|
194
195
|
"inputs",
|
|
195
196
|
"deciders",
|
|
196
197
|
"quorum"
|
|
@@ -236,6 +237,23 @@
|
|
|
236
237
|
]
|
|
237
238
|
}
|
|
238
239
|
},
|
|
240
|
+
"criteria": {
|
|
241
|
+
"description": "What each candidate means, as the points file declares it: noul and choice give an object of strings keyed by candidate, score an array of ordered level descriptions.",
|
|
242
|
+
"oneOf": [
|
|
243
|
+
{
|
|
244
|
+
"type": "object",
|
|
245
|
+
"additionalProperties": {
|
|
246
|
+
"type": "string"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"type": "array",
|
|
251
|
+
"items": {
|
|
252
|
+
"type": "string"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
},
|
|
239
257
|
"inputs": {
|
|
240
258
|
"type": "array",
|
|
241
259
|
"items": {
|
|
@@ -24,10 +24,11 @@ import {
|
|
|
24
24
|
} from "./points";
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* jhgaylor/chud@43afcf1
|
|
27
|
+
* Points shaped like an externally authored template's decisions file
|
|
28
|
+
* (ported from chud, jhgaylor/chud@43afcf1's
|
|
29
|
+
* template/delivery/decisions/points.yaml), as JSON.
|
|
29
30
|
*/
|
|
30
|
-
const
|
|
31
|
+
const IMPORTED = {
|
|
31
32
|
points: {
|
|
32
33
|
"slice-tier": {
|
|
33
34
|
title: "Which builder tier builds this contract",
|
|
@@ -63,7 +64,7 @@ const CHUD = {
|
|
|
63
64
|
"ship-skip": {
|
|
64
65
|
title: "May this release skip the human gate",
|
|
65
66
|
question: {
|
|
66
|
-
type: "
|
|
67
|
+
type: "noul",
|
|
67
68
|
instructions: "May this release pass the ship gate without a person approving it? The state is what the release plan would change.",
|
|
68
69
|
criteria: {
|
|
69
70
|
true: "An agent may pass the gate for this release (only in enforce mode).",
|
|
@@ -86,9 +87,9 @@ const CHUD = {
|
|
|
86
87
|
},
|
|
87
88
|
};
|
|
88
89
|
|
|
89
|
-
/**
|
|
90
|
-
function renamed(): typeof
|
|
91
|
-
const copy = JSON.parse(JSON.stringify(
|
|
90
|
+
/** The imported points with each input named as a read-contract output: the slice tier reads a work item, the ship gate a release. */
|
|
91
|
+
function renamed(): typeof IMPORTED {
|
|
92
|
+
const copy = JSON.parse(JSON.stringify(IMPORTED)) as typeof IMPORTED;
|
|
92
93
|
const prefix: Record<string, string> = { "slice-tier": "work-item", "ship-skip": "release" };
|
|
93
94
|
for (const [name, point] of Object.entries(copy.points) as [string, { inputs: Record<string, string>; deciders: { rows?: { when: Record<string, unknown> }[] }[] }][]) {
|
|
94
95
|
const to = (k: string) => `${prefix[name]}.${k}`;
|
|
@@ -123,14 +124,14 @@ describe("the decision points schema (#2738)", () => {
|
|
|
123
124
|
}
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
test("
|
|
127
|
-
// As
|
|
128
|
-
const found = problems(
|
|
127
|
+
test("the imported slice-tier and ship-skip validate unchanged, apart from the input names", () => {
|
|
128
|
+
// As the template declares them, only the input names are refused: each names no read-contract output.
|
|
129
|
+
const found = problems(IMPORTED);
|
|
129
130
|
expect(found.length).toBeGreaterThan(0);
|
|
130
131
|
for (const p of found) expect(p.message).toMatch(/is not a read-contract output|is not one of this point's inputs/);
|
|
131
132
|
expect(found.filter((p) => p.message.includes("read-contract output")).map((p) => p.field)).toEqual([
|
|
132
|
-
...Object.keys(
|
|
133
|
-
...Object.keys(
|
|
133
|
+
...Object.keys(IMPORTED.points["slice-tier"].inputs).map((k) => `points.slice-tier.inputs.${k}`),
|
|
134
|
+
...Object.keys(IMPORTED.points["ship-skip"].inputs).map((k) => `points.ship-skip.inputs.${k}`),
|
|
134
135
|
]);
|
|
135
136
|
// With the inputs renamed, nothing else changes and both validate.
|
|
136
137
|
const points = parsePoints(text(renamed()), "points.json");
|
|
@@ -156,7 +157,7 @@ describe("the decision points schema (#2738)", () => {
|
|
|
156
157
|
|
|
157
158
|
test("a point with an unknown input is refused, and so is a row testing an undeclared one", () => {
|
|
158
159
|
const v = renamed() as unknown as { points: Record<string, { inputs: Record<string, string>; deciders: { rows?: { when: Record<string, unknown> }[] }[] }> };
|
|
159
|
-
v.points["slice-tier"].inputs["contract.size"] = "
|
|
160
|
+
v.points["slice-tier"].inputs["contract.size"] = "a contract, which the read contract has no output for";
|
|
160
161
|
expect(problems(v)).toEqual([
|
|
161
162
|
{
|
|
162
163
|
field: "points.slice-tier.inputs.contract.size",
|
|
@@ -168,7 +169,7 @@ describe("the decision points schema (#2738)", () => {
|
|
|
168
169
|
expect(problems(w)).toEqual([{ field: "points.slice-tier.deciders.0.rows.0.when.work-item.tier", message: expect.stringContaining("is not one of this point's inputs") }]);
|
|
169
170
|
});
|
|
170
171
|
|
|
171
|
-
test("the schema and the code refuse the rest of what
|
|
172
|
+
test("the schema and the code refuse the rest of what an imported points file can get wrong, and an alias model id", () => {
|
|
172
173
|
const cases: [(p: Record<string, unknown>) => void, RegExp][] = [
|
|
173
174
|
[(p) => ((p.deciders as Record<string, unknown>[])[1].model = "jev-latest"), /is an alias/],
|
|
174
175
|
[(p) => ((p.deciders as Record<string, unknown>[])[1].count = 2), /is only for a quorum decider/],
|
package/src/workspace/points.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* runtime's decider or a test's stub supplies. Without one, a model decider is
|
|
21
21
|
* not asked and the chain moves on.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* Ported from chud's `packages/runtime/src/decide.mjs` at 43afcf1: the chain,
|
|
24
24
|
* the observation rule, the point version and the inputs hash. The records
|
|
25
25
|
* are chant records: `decide.ts` writes them, and {@link applyAnswers} warns
|
|
26
26
|
* about them on read.
|
|
@@ -71,7 +71,6 @@ export function inputOutput(name: string): string {
|
|
|
71
71
|
export type QuestionType = "noul" | "choice" | "score";
|
|
72
72
|
|
|
73
73
|
export interface Question {
|
|
74
|
-
/** `boolean` in a points file is read as `noul`. */
|
|
75
74
|
type: QuestionType;
|
|
76
75
|
instructions: string;
|
|
77
76
|
criteria: Record<string, string> | string[];
|
|
@@ -220,20 +219,9 @@ export function schemaProblems(data: unknown): PointProblem[] {
|
|
|
220
219
|
return out;
|
|
221
220
|
}
|
|
222
221
|
|
|
223
|
-
/** `boolean` read as `noul`. The declaration is otherwise kept as written. */
|
|
224
|
-
function normalise(points: Record<string, Point>): Record<string, Point> {
|
|
225
|
-
const out: Record<string, Point> = {};
|
|
226
|
-
for (const [name, p] of Object.entries(points)) {
|
|
227
|
-
const type = (p.question.type as string) === "boolean" ? "noul" : p.question.type;
|
|
228
|
-
out[name] = { ...p, question: { ...p.question, type } };
|
|
229
|
-
}
|
|
230
|
-
return out;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
222
|
/**
|
|
234
|
-
* Parse and validate a points file's text. Returns its points
|
|
235
|
-
*
|
|
236
|
-
* each field.
|
|
223
|
+
* Parse and validate a points file's text. Returns its points. Throws a
|
|
224
|
+
* {@link PointsError} naming the file and each field.
|
|
237
225
|
*/
|
|
238
226
|
export function parsePoints(text: string, file: string): Record<string, Point> {
|
|
239
227
|
let data: unknown;
|
|
@@ -244,7 +232,7 @@ export function parsePoints(text: string, file: string): Record<string, Point> {
|
|
|
244
232
|
}
|
|
245
233
|
const problems = schemaProblems(data);
|
|
246
234
|
if (problems.length > 0) throw new PointsError(file, problems);
|
|
247
|
-
const points =
|
|
235
|
+
const points = (data as { points: Record<string, Point> }).points;
|
|
248
236
|
const more = pointProblems(points);
|
|
249
237
|
if (more.length > 0) throw new PointsError(file, more);
|
|
250
238
|
return points;
|
|
@@ -193,6 +193,14 @@ describe("every schema against the reference workspace (#2543)", () => {
|
|
|
193
193
|
expectValid(doc);
|
|
194
194
|
if ("error" in doc) throw new Error(doc.error.message);
|
|
195
195
|
expect(doc.points.map((p) => p.name)).toEqual(["slice-tier", "ship-skip", "finding-triage", "needs-a-decision"]);
|
|
196
|
+
const sliceTier = doc.points.find((p) => p.name === "slice-tier")!;
|
|
197
|
+
expect(sliceTier.criteria).toEqual({
|
|
198
|
+
small: "A haiku-class builder. The work item fits the small limits.",
|
|
199
|
+
medium: "A mid-size builder. The work item fits the medium limits.",
|
|
200
|
+
large: "The largest builder. The work item is bigger than the medium limits.",
|
|
201
|
+
});
|
|
202
|
+
const shipSkip = doc.points.find((p) => p.name === "ship-skip")!;
|
|
203
|
+
expect(shipSkip.criteria).toEqual({ true: "An agent may pass the gate for this release.", false: "A person approves the release at the gate." });
|
|
196
204
|
}
|
|
197
205
|
const open = await workspacePoints({ cwd: FIXTURE, open: true });
|
|
198
206
|
expectValid(open);
|
|
@@ -157,11 +157,11 @@ describe("the closed list of reason codes", () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
test("a plugin's finding codes are in its own namespace, outside the list, and the intent schema accepts them (#2656)", () => {
|
|
160
|
-
expect(isPluginCode("plugin:
|
|
161
|
-
expect(isPluginCode("plugin:
|
|
162
|
-
expect(isPluginCode("plugin:
|
|
163
|
-
for (const bad of ["plugin:
|
|
164
|
-
expect(isReasonCode("plugin:
|
|
160
|
+
expect(isPluginCode("plugin:acme:contract-criteria-changed")).toBe(true);
|
|
161
|
+
expect(isPluginCode("plugin:acme:contract-criteria-changed", "acme")).toBe(true);
|
|
162
|
+
expect(isPluginCode("plugin:acme:contract-criteria-changed", "units")).toBe(false);
|
|
163
|
+
for (const bad of ["plugin:acme", "plugin::x", "plugin:acme:Upper", "plugin:acme:a:b", "intent-commit-bare", 7]) expect(isPluginCode(bad), String(bad)).toBe(false);
|
|
164
|
+
expect(isReasonCode("plugin:acme:contract-criteria-changed")).toBe(false);
|
|
165
165
|
const { validate } = contract(intentSchema);
|
|
166
166
|
const finding = (code: string) => ({ id: `finding:${code}:1`, kind: "finding", code, message: "m", concerns: [] });
|
|
167
167
|
const doc = (code: string) => ({
|
|
@@ -178,9 +178,9 @@ describe("the closed list of reason codes", () => {
|
|
|
178
178
|
reasons: [],
|
|
179
179
|
summary: { commits: 0, decisions: 0, artifacts: 0, findings: 1 },
|
|
180
180
|
});
|
|
181
|
-
expect(validate(doc("plugin:
|
|
181
|
+
expect(validate(doc("plugin:acme:contract-criteria-changed"))).toBe(true);
|
|
182
182
|
expect(validate(doc("intent-commit-bare"))).toBe(true);
|
|
183
|
-
expect(validate(doc("plugin:
|
|
183
|
+
expect(validate(doc("plugin:acme:Nope"))).toBe(false);
|
|
184
184
|
expect(validate(doc("made-up-code"))).toBe(false);
|
|
185
185
|
});
|
|
186
186
|
|
|
@@ -38,6 +38,7 @@ export const REASONS = {
|
|
|
38
38
|
// --at and git.
|
|
39
39
|
"not-a-git-repository": "--at, or a ledger read, needs a git repository and there is none.",
|
|
40
40
|
"revision-unknown": "--at names no commit.",
|
|
41
|
+
"live-at-revision": "graph was given --live and --at: a live read is of the account now, not of a revision.",
|
|
41
42
|
// status.
|
|
42
43
|
"environment-invalid": "An environment name that can't name a ledger directory.",
|
|
43
44
|
// A member or group that can't be read (ls, graph).
|
|
@@ -47,7 +48,7 @@ export const REASONS = {
|
|
|
47
48
|
"no-matches": "An example group matches no directory holding a chant project.",
|
|
48
49
|
// A member graph leaves out (graph, and the other per-member commands).
|
|
49
50
|
"kind-not-run": "The member's kind is one the per-member commands don't run, such as other.",
|
|
50
|
-
"command-failed": "The member's own command exited with a failure.",
|
|
51
|
+
"command-failed": "The member's own command exited with a failure, or the lexicon that reads the member through its kind's graph block isn't installed where the workspace resolves its pin.",
|
|
51
52
|
"output-unreadable": "The member's command printed something that isn't the document asked for.",
|
|
52
53
|
"ir-version-unsupported": "The member's IR has a version this chant can't read.",
|
|
53
54
|
// A ledger status can't fully read.
|
|
@@ -129,6 +130,7 @@ export const REASONS = {
|
|
|
129
130
|
"review-sign-failed": "--sign was given and no seal could be made: the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
130
131
|
"record-state-not-initial": "A record written through chant serve mcp gives a state other than the kind's first: a new record opens proposed, and a person moves it on.",
|
|
131
132
|
"source-harvest-not-proposed": "A harvested record (source.via harvest) was written in a state other than the kind's first: a harvest proposes, and a person decides.",
|
|
133
|
+
"ratify-quorum-not-met": "The write puts a record in its kind's ratified state (reviews.ratified), and the record's quorum is not met: too few agreeing verdicts count.",
|
|
132
134
|
"record-sign-failed": "--sign was given and no author seal could be made: the record names no author, the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
133
135
|
// A review given in a session (records review --session, #2693).
|
|
134
136
|
"session-unknown": "--session names no session of a session kind whose subjects are the record's kind.",
|
|
@@ -192,6 +194,9 @@ export const REASONS = {
|
|
|
192
194
|
"box-capability-unbrokered": "A capability in a member's box block names no broker, so the box would hold its credential.",
|
|
193
195
|
// The credential fountain itself hands a persistent box (check, #2780). Also a WSP finding.
|
|
194
196
|
"box-fountain-callback-undeclared": "A box member builds a fountain Box, whose persistent sandbox fountain gives a callback token scoped to its owner, and the member's box block does not declare the fountain-callback capability brokered by fountain with scope owner.",
|
|
197
|
+
// A box's intent, the decision record its box block names (check, #2850). Each is also a WSP finding.
|
|
198
|
+
"box-intent-unknown": "A box block names an intent, and no record of a declared kind named decision has that id.",
|
|
199
|
+
"box-intent-unconstrained": "The decision record a box names as its intent constrains no member or path of this workspace: no member: entry for a declared member and no path: entry at, above or inside one's directory.",
|
|
195
200
|
// The work lease (chant workspace work claim|renew|release, #2732): why the command could not run.
|
|
196
201
|
"work-kind-missing": "No work kind to find the item in: --kind names a kind with no work block, or the declaration names no work kind.",
|
|
197
202
|
"work-kind-ambiguous": "More than one declared work kind has a record with the id, so --kind must name one.",
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
RecordReadError,
|
|
39
39
|
type LoadedRecordKind,
|
|
40
40
|
type Quorum,
|
|
41
|
+
type QuorumOptions,
|
|
41
42
|
type ReadErrorCode,
|
|
42
43
|
type ReadRecordsResult,
|
|
43
44
|
type RecordEntry,
|
|
@@ -210,6 +211,24 @@ function declaredQuorum(tree: WorkspaceTree): { need: number; needFrom: "declara
|
|
|
210
211
|
return { need: DEFAULT_QUORUM, needFrom: "default" };
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The options a kind's quorum is computed with, or undefined when the kind
|
|
216
|
+
* has no reviews list: the need from the declaration in the tree read, and
|
|
217
|
+
* the agents, whether verdicts need a seal, and the keys a seal verifies
|
|
218
|
+
* against, all from the policy at base (#2671, #2687). `records` and the
|
|
219
|
+
* ratified check of `records new` and `amend` (#2873) both use it.
|
|
220
|
+
*/
|
|
221
|
+
export async function quorumOptionsFor(loaded: LoadedRecordKind, tree: WorkspaceTree, policy: TrustPolicy): Promise<QuorumOptions | undefined> {
|
|
222
|
+
if (!loaded.kind.reviews) return undefined;
|
|
223
|
+
const { checkVerdictSeal } = await import("./trust/seal");
|
|
224
|
+
return {
|
|
225
|
+
...declaredQuorum(tree),
|
|
226
|
+
agents: new Set((policy.roles[AGENT_ROLE] ?? []).map(normalisePrincipal)),
|
|
227
|
+
attestation: policy.active,
|
|
228
|
+
verifySeal: (v: SealInput) => checkVerdictSeal(policy, v),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
213
232
|
/**
|
|
214
233
|
* Where a kind's pinned paths resolve: the workspace whose declaration sits
|
|
215
234
|
* nearest above the kind file, when it is inside the repository, or else the
|
|
@@ -300,19 +319,8 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
300
319
|
paths: result.records.map((r) => r.path),
|
|
301
320
|
attestors: policy.active ? await activeAttestors() : [],
|
|
302
321
|
});
|
|
303
|
-
// The quorum: the need from the declaration in the tree read, agents,
|
|
304
|
-
// whether verdicts need a seal, and the keys a seal verifies against,
|
|
305
|
-
// all from the policy at base (#2671, #2687).
|
|
306
322
|
const seals = loaded.kind.reviews ? await import("./trust/seal") : undefined;
|
|
307
|
-
const
|
|
308
|
-
const quorumOptions = checkVerdictSeal
|
|
309
|
-
? {
|
|
310
|
-
...declaredQuorum(tree),
|
|
311
|
-
agents: new Set((policy.roles[AGENT_ROLE] ?? []).map(normalisePrincipal)),
|
|
312
|
-
attestation: policy.active,
|
|
313
|
-
verifySeal: (v: SealInput) => checkVerdictSeal(policy, v),
|
|
314
|
-
}
|
|
315
|
-
: undefined;
|
|
323
|
+
const quorumOptions = await quorumOptionsFor(loaded, tree, policy);
|
|
316
324
|
const records: RecordView[] = result.records.map((r) => ({
|
|
317
325
|
...r,
|
|
318
326
|
provenance: provenance.get(r.path)!,
|
|
@@ -510,6 +518,14 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
|
|
|
510
518
|
* names, or, when it names none or there is no declaration, the error it has
|
|
511
519
|
* always been. A kind whose read fails is listed with its error, the others
|
|
512
520
|
* are still read, and the exit code is 1.
|
|
521
|
+
*
|
|
522
|
+
* Locating the declaration itself can fail before any kind is known. For
|
|
523
|
+
* `not-a-git-repository` and `revision-unknown`, the codes a single kind's
|
|
524
|
+
* read can also fail with (#2860), `--json` prints the same
|
|
525
|
+
* `{ $schema, contract, error: { code, message } }` document that read
|
|
526
|
+
* failure would, so a reader of `--json` never sees a silent exit 1. A
|
|
527
|
+
* declaration-level code the schema doesn't carry (`declaration-invalid` and
|
|
528
|
+
* the rest of `WorkspaceErrorCode`) still prints text on stderr only.
|
|
513
529
|
*/
|
|
514
530
|
async function runDeclaredRecords(args: CommandContext["args"]): Promise<number> {
|
|
515
531
|
if (args.require !== undefined && args.require !== "attested") {
|
|
@@ -521,7 +537,12 @@ async function runDeclaredRecords(args: CommandContext["args"]): Promise<number>
|
|
|
521
537
|
declared = declaredKindFiles(process.cwd(), args.at);
|
|
522
538
|
} catch (err) {
|
|
523
539
|
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
524
|
-
|
|
540
|
+
const message = `${err.describe()}; without --kind, the declaration names the record kinds`;
|
|
541
|
+
if (args.json && (err.code === "not-a-git-repository" || err.code === "revision-unknown")) {
|
|
542
|
+
console.log(JSON.stringify({ $schema: RECORDS_OUTPUT_SCHEMA_ID, contract: RECORDS_CONTRACT_VERSION, error: { code: err.code, message } }, null, 2));
|
|
543
|
+
} else {
|
|
544
|
+
console.error(formatError({ message: `${err.code}: ${message}`, hint: USAGE }));
|
|
545
|
+
}
|
|
525
546
|
return 1;
|
|
526
547
|
}
|
|
527
548
|
if (declared.length === 0) {
|
|
@@ -116,7 +116,8 @@ describe("records output schema", () => {
|
|
|
116
116
|
JSON.stringify({ name: "w", schema: 1, quorum: 1, members: [{ name: "docs", dir: "docs", kind: "other", because: "decisions only" }] }),
|
|
117
117
|
);
|
|
118
118
|
const base = readFileSync(join(dir, "ws-003-seal-scope.md"), "utf-8").replace(/^id: .*$/m, 'id: "ws-900"');
|
|
119
|
-
|
|
119
|
+
// The decision kind names a ratified state, so the state leaves the digest too (#2873).
|
|
120
|
+
const digest = recordTextDigest(base, ["reviews", "seal", "state"]);
|
|
120
121
|
const entry = (reviewer: string, verdict: string, extra = "") => ` - reviewer: "${reviewer}"\n verdict: "${verdict}"\n on: "2026-09-24"${extra}`;
|
|
121
122
|
const reviews = [
|
|
122
123
|
entry("alice", "agree", `\n digest: "${digest}"`),
|