@cassiomc1/forgeloop 0.1.13 → 0.1.15

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/cli.js CHANGED
@@ -24,6 +24,7 @@ import { formatPolicyResult, runPolicy } from "./commands/policy.js";
24
24
  import { formatBundleResult, runBundle } from "./commands/bundle.js";
25
25
  import { formatPrepareCompletionResult, runPrepareCompletion } from "./commands/prepare-completion.js";
26
26
  import { formatRecordCheckResult, runRecordCheck } from "./commands/record-check.js";
27
+ import { formatRunCheckResult, runCheck } from "./commands/run-check.js";
27
28
  import { formatRecordTerminalResult, runRecordTerminalResult } from "./commands/record-terminal-result.js";
28
29
  import { formatNextActionResult, runNext } from "./commands/next.js";
29
30
  import { resolveTarget } from "./core/filesystem.js";
@@ -31,7 +32,7 @@ import { getPackageRoot } from "./core/templates.js";
31
32
  import { ARTIFACT_PATHS } from "./core/artifacts.js";
32
33
 
33
34
  function usage(command = null) {
34
- const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
35
+ const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|run-check|record-check|record-terminal-result|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
35
36
  const options = [" --path <directory> target project directory (default: current directory)"];
36
37
  if (!command || command === "init" || command === "update") {
37
38
  options.push(" --dry-run show planned writes without changing files");
@@ -53,7 +54,7 @@ function usage(command = null) {
53
54
  if (!command || command === "advance") {
54
55
  options.push(" --to <phase> destination workflow phase");
55
56
  }
56
- if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
57
+ if (!command || ["activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
57
58
  options.push(" --json emit structured output as JSON");
58
59
  }
59
60
  if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
@@ -78,16 +79,23 @@ function usage(command = null) {
78
79
  if (!command || command === "validate-receipt") {
79
80
  options.push(" --file <path> receipt file relative to target");
80
81
  }
81
- if (!command || command === "record-check") {
82
+ if (!command || command === "record-check" || command === "run-check") {
82
83
  options.push(" --id <id> stable check identifier");
83
- options.push(" --kind <kind> check kind (default: command)");
84
84
  options.push(" --requirement <id> completion requirement covered by the check");
85
+ options.push(" --details <json> additional structured check details");
86
+ }
87
+ if (!command || command === "record-check") {
88
+ options.push(" --kind <kind> check kind (default: command; use manual-review for manual evidence)");
85
89
  options.push(" --status <status> passed, failed, blocked, or not-run");
86
90
  options.push(" --evidence-kind <kind> OBSERVED, INFERRED, NOT_VERIFIED, or BLOCKED");
87
- options.push(" --command <text> command already run by the agent (recorded only)");
88
- options.push(" --result <text> observed result supplied by the agent");
91
+ options.push(" --command <text> recorded only as metadata; it is never executed");
92
+ options.push(" --result <text> observed result supplied by the actor");
89
93
  options.push(" --exit-code <number> observed process exit code");
90
- options.push(" --details <json> additional structured check details");
94
+ options.push(" --execution-ref <id> ForgeLoop execution artifact reference");
95
+ options.push(" --provenance <value> FORGELOOP_EXECUTED, ACTOR_REPORTED, or MANUAL_OBSERVATION");
96
+ }
97
+ if (!command || command === "run-check") {
98
+ options.push(" -- <argv> exact command argv to classify, execute, and attest");
91
99
  }
92
100
  if (!command || command === "record-terminal-result") {
93
101
  options.push(" --requirement <id> terminal requirement covered by the result");
@@ -133,6 +141,9 @@ export function parseArgs(argv) {
133
141
  checkResult: null,
134
142
  checkExitCode: null,
135
143
  checkDetails: null,
144
+ checkExecutionRef: null,
145
+ checkProvenance: null,
146
+ commandArgv: [],
136
147
  checkType: null,
137
148
  checkSource: null,
138
149
  policy: null,
@@ -144,7 +155,7 @@ export function parseArgs(argv) {
144
155
 
145
156
  for (let index = 0; index < argv.length; index += 1) {
146
157
  const argument = argv[index];
147
- if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
158
+ if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
148
159
  if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
149
160
  command = argument;
150
161
  } else if (argument === "--help" || argument === "-h") {
@@ -304,6 +315,19 @@ export function parseArgs(argv) {
304
315
  throw new Error("--details must be a JSON object");
305
316
  }
306
317
  index += 1;
318
+ } else if (argument === "--execution-ref") {
319
+ const executionRef = argv[index + 1];
320
+ if (!executionRef || executionRef.startsWith("-")) throw new Error("--execution-ref requires an execution ID");
321
+ options.checkExecutionRef = executionRef;
322
+ index += 1;
323
+ } else if (argument === "--provenance") {
324
+ const provenance = argv[index + 1];
325
+ if (!provenance || provenance.startsWith("-")) throw new Error("--provenance requires a provenance value");
326
+ options.checkProvenance = provenance;
327
+ index += 1;
328
+ } else if (argument === "--" && command === "run-check") {
329
+ options.commandArgv = argv.slice(index + 1);
330
+ break;
307
331
  } else if (argument === "--path") {
308
332
  options.path = argv[index + 1];
309
333
  if (!options.path || options.path.startsWith("-")) throw new Error("--path requires a directory");
@@ -324,7 +348,7 @@ export function parseArgs(argv) {
324
348
 
325
349
  if (!command) return { command: null, options };
326
350
 
327
- const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
351
+ const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "run-check", "record-check", "record-terminal-result", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
328
352
  if (!jsonCommands.includes(command) && options.json) {
329
353
  throw new Error(`Option --json is not valid for ${command}`);
330
354
  }
@@ -377,8 +401,10 @@ export function parseArgs(argv) {
377
401
  options.checkResult,
378
402
  options.checkExitCode,
379
403
  options.checkDetails,
404
+ options.checkExecutionRef,
405
+ options.checkProvenance,
380
406
  ];
381
- if (command !== "record-check" && checkOptions.some((value) => value !== null)) {
407
+ if (!["record-check", "run-check"].includes(command) && checkOptions.some((value) => value !== null)) {
382
408
  throw new Error(`Check recording options are not valid for ${command}`);
383
409
  }
384
410
  if (command === "record-check" && !options.help) {
@@ -388,6 +414,17 @@ export function parseArgs(argv) {
388
414
  if (!options.checkEvidenceKind) throw new Error("record-check requires --evidence-kind");
389
415
  if (!options.checkCommand && !options.checkResult) throw new Error("record-check requires --command or --result");
390
416
  }
417
+ if (command === "run-check" && !options.help) {
418
+ if (!options.checkId) throw new Error("run-check requires --id");
419
+ if (!options.checkRequirement) throw new Error("run-check requires --requirement");
420
+ if (options.checkKind || options.checkStatus || options.checkEvidenceKind || options.checkCommand
421
+ || options.checkResult || options.checkExitCode !== null || options.checkExecutionRef || options.checkProvenance) {
422
+ throw new Error("run-check accepts only --id, --requirement, --details, and -- <argv>");
423
+ }
424
+ if (!Array.isArray(options.commandArgv) || options.commandArgv.length === 0) {
425
+ throw new Error("run-check requires -- followed by an exact command argv");
426
+ }
427
+ }
391
428
  return { command, options };
392
429
  }
393
430
 
@@ -490,6 +527,19 @@ export async function main(argv = process.argv.slice(2)) {
490
527
  return 0;
491
528
  }
492
529
 
530
+ if (command === "run-check") {
531
+ const result = await runCheck({
532
+ target,
533
+ packageRoot,
534
+ id: options.checkId,
535
+ requirement: options.checkRequirement,
536
+ argv: options.commandArgv,
537
+ details: options.checkDetails ?? undefined,
538
+ });
539
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRunCheckResult(result));
540
+ return result.check.status === "passed" ? 0 : 1;
541
+ }
542
+
493
543
  if (command === "record-check") {
494
544
  const result = await runRecordCheck({
495
545
  target,
@@ -501,8 +551,10 @@ export async function main(argv = process.argv.slice(2)) {
501
551
  evidenceKind: options.checkEvidenceKind,
502
552
  command: options.checkCommand ?? undefined,
503
553
  result: options.checkResult ?? undefined,
504
- exitCode: options.checkExitCode,
554
+ ...(options.checkExitCode === null ? {} : { exitCode: options.checkExitCode }),
505
555
  details: options.checkDetails ?? undefined,
556
+ executionRef: options.checkExecutionRef ?? undefined,
557
+ provenance: options.checkProvenance ?? undefined,
506
558
  });
507
559
  console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordCheckResult(result));
508
560
  return 0;
@@ -606,7 +658,7 @@ export async function main(argv = process.argv.slice(2)) {
606
658
  }
607
659
  return result.conflicts.length === 0 ? 0 : 1;
608
660
  } catch (error) {
609
- console.error(`error: ${error.message}`);
661
+ console.error(`error: ${error.code ? `${error.code}: ` : ""}${error.message}`);
610
662
  return 1;
611
663
  }
612
664
  }
@@ -8,6 +8,8 @@ export function formatInspectResult(report) {
8
8
  `Manifest: ${report.manifest.status}`,
9
9
  `Profile: ${report.profile.mode ?? "unknown"}/${report.profile.status ?? "unknown"}`,
10
10
  `Protocol: v${report.protocol.version}`,
11
+ `Authority source: ${report.authority.sourceType ?? "none configured"} / ${report.authority.trusted ? "TRUSTED" : report.authority.trustMode === "NONE" ? "UNATTESTED" : "UNTRUSTED"}`,
12
+ `Authority trust: ${report.authority.trustMode}`,
11
13
  `State: ${report.state.status}`,
12
14
  `Adapters: ${report.adapters.detected.length} detected`,
13
15
  `Findings: ${report.findings.length}`,
@@ -2,8 +2,8 @@ import { prepareCompletion as prepareCompletionArtifacts } from "../core/complet
2
2
 
3
3
  export { prepareCompletionArtifacts as prepareCompletion };
4
4
 
5
- export async function runPrepareCompletion({ target, packageRoot }) {
6
- return prepareCompletionArtifacts({ target, packageRoot });
5
+ export async function runPrepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
6
+ return prepareCompletionArtifacts({ target, packageRoot, authorityContext, runtimeContext });
7
7
  }
8
8
 
9
9
  export function formatPrepareCompletionResult(result) {
@@ -0,0 +1,83 @@
1
+ import {
2
+ assertRecordCheckPrerequisites,
3
+ recordCheck as recordCheckArtifact,
4
+ } from "../core/completion-artifacts.js";
5
+ import { runCommandExecution } from "../core/execution.js";
6
+
7
+ export async function runCheck({
8
+ target,
9
+ packageRoot,
10
+ id,
11
+ requirement,
12
+ argv,
13
+ details,
14
+ authorityContext,
15
+ runtimeContext,
16
+ }) {
17
+ if (typeof id !== "string" || id.trim() === "" || typeof requirement !== "string" || requirement.trim() === "") {
18
+ const error = new Error("run-check requires non-empty id and requirement");
19
+ error.code = "E_CHECK_INVALID";
20
+ throw error;
21
+ }
22
+ const ready = await assertRecordCheckPrerequisites({
23
+ target,
24
+ packageRoot,
25
+ requirement,
26
+ status: "passed",
27
+ evidenceKind: "OBSERVED",
28
+ authorityContext,
29
+ runtimeContext,
30
+ });
31
+ const verificationCycle = ready.state.verificationCycle ?? 1;
32
+ const execution = await runCommandExecution({
33
+ target,
34
+ packageRoot,
35
+ taskId: ready.contract.value.taskId,
36
+ checkId: id,
37
+ requirement,
38
+ verificationCycle,
39
+ argv,
40
+ details,
41
+ authorityContext,
42
+ runtimeContext,
43
+ });
44
+ const status = execution.execution.status === "passed" ? "passed" : "failed";
45
+ const recorded = await recordCheckArtifact({
46
+ target,
47
+ packageRoot,
48
+ id,
49
+ kind: "command",
50
+ requirement,
51
+ status,
52
+ evidenceKind: "OBSERVED",
53
+ command: execution.execution.argv.join(" "),
54
+ result: execution.result,
55
+ ...(execution.execution.exitCode === null ? {} : { exitCode: execution.execution.exitCode }),
56
+ details,
57
+ executionRef: execution.execution.executionId,
58
+ provenance: "FORGELOOP_EXECUTED",
59
+ authorityContext,
60
+ runtimeContext,
61
+ });
62
+ return {
63
+ ...recorded,
64
+ execution: execution.execution,
65
+ executionPath: execution.path,
66
+ };
67
+ }
68
+
69
+ export function formatRunCheckResult(result) {
70
+ return [
71
+ "FORGELOOP CHECK EXECUTED",
72
+ `id: ${result.check.id}`,
73
+ `requirement: ${result.check.requirement}`,
74
+ `status: ${result.check.status}`,
75
+ `execution: ${result.execution.executionId}`,
76
+ `argv: ${result.execution.argv.join(" ")}`,
77
+ `exit code: ${result.execution.exitCode ?? "not-started"}`,
78
+ `artifact: ${result.executionPath}`,
79
+ `coverage: ${result.coverage.find((item) => item.requirement === result.check.requirement)?.status ?? "NOT_VERIFIED"}`,
80
+ `receipt: ${result.path}`,
81
+ "",
82
+ ].join("\n");
83
+ }
@@ -9,6 +9,7 @@ import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.j
9
9
  import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
10
10
  import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
11
11
  import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
12
+ import { validateChecksExecutionProvenance } from "../core/completion-artifacts.js";
12
13
 
13
14
  async function readArtifact(target, relativePath, label) {
14
15
  if (!relativePath) return null;
@@ -87,6 +88,23 @@ export async function runValidateProtocol({
87
88
  await validateLoaded(loaded.find((item) => item.label === "route"), "routing-result", async (value) => assertRouteInvariants(value));
88
89
  await validateLoaded(loaded.find((item) => item.label === "state"), "work-state", async (value) => assertWorkStateSemantics(value));
89
90
  await validateLoaded(loaded.find((item) => item.label === "receipt"), "execution-receipt", async (value) => validateReceipt(value, packageRoot));
91
+ for (const [value, artifactPath] of [
92
+ [state, stateFile],
93
+ [receipt, receiptFile],
94
+ ]) {
95
+ if (!value) continue;
96
+ const provenanceErrors = await validateChecksExecutionProvenance(value.checks, {
97
+ target,
98
+ packageRoot,
99
+ taskId: value.taskId,
100
+ artifactPath,
101
+ });
102
+ schemaErrors.push(...provenanceErrors.map((error) => ({
103
+ ...error,
104
+ message: `Command provenance validation failed: ${error.message}`,
105
+ artifacts: [artifactPath, ...(error.artifacts ?? [])],
106
+ })));
107
+ }
90
108
  for (const item of loaded.filter((candidate) => candidate.label.startsWith("task brief:"))) {
91
109
  await validateLoaded(item, "task-brief", async (value) => validateTaskBrief(value, packageRoot));
92
110
  }
@@ -17,8 +17,16 @@ export const ARTIFACT_PATHS = Object.freeze({
17
17
  gates: ".forgeloop/gates",
18
18
  state: ".forgeloop/work-state.json",
19
19
  receipt: ".forgeloop/execution-receipt.json",
20
+ executionDirectory: ".forgeloop/executions",
20
21
  });
21
22
 
23
+ export function executionArtifactPath(executionId) {
24
+ if (typeof executionId !== "string" || !/^exec-[A-Za-z0-9_-]+$/.test(executionId)) {
25
+ throw new ArtifactError("E_EXECUTION_REF_INVALID", "Execution reference must be a simple execution ID");
26
+ }
27
+ return `${ARTIFACT_PATHS.executionDirectory}/${executionId}.json`;
28
+ }
29
+
22
30
  export class ArtifactError extends Error {
23
31
  constructor(code, message, artifacts = []) {
24
32
  super(message);
package/src/core/audit.js CHANGED
@@ -34,8 +34,8 @@ async function compareChangedPaths(target, packageRoot) {
34
34
  };
35
35
  }
36
36
 
37
- export async function evaluateAudit({ target, packageRoot, strict = false } = {}) {
38
- const completion = await evaluateCompletion({ target, packageRoot, strict });
37
+ export async function evaluateAudit({ target, packageRoot, strict = false, authorityContext, runtimeContext } = {}) {
38
+ const completion = await evaluateCompletion({ target, packageRoot, strict, authorityContext, runtimeContext });
39
39
  let manifest = null;
40
40
  let manifestError = null;
41
41
  try {
@@ -4,6 +4,8 @@ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts
4
4
  import { readContract, validateContract } from "./contract.js";
5
5
  import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
6
6
  import { PROTOCOL_VERSION } from "./protocol.js";
7
+ import { validateChecksExecutionProvenance } from "./completion-artifacts.js";
8
+ import { readExecutionArtifact } from "./execution.js";
7
9
 
8
10
  export const BUNDLE_SCHEMA_VERSION = 1;
9
11
  const BUNDLE_ROOT = ".forgeloop/tasks";
@@ -37,6 +39,34 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
37
39
  safeTaskId(taskId);
38
40
  const directory = bundleDirectory(taskId);
39
41
  const artifacts = [];
42
+ const stateSource = await readJsonArtifact(target, ARTIFACT_PATHS.state, "work-state", packageRoot);
43
+ let receiptSource = null;
44
+ try {
45
+ receiptSource = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
46
+ } catch (error) {
47
+ if (error.code !== "ARTIFACT_MISSING") throw error;
48
+ }
49
+ const provenanceErrors = [
50
+ ...(await validateChecksExecutionProvenance(stateSource.value.checks, {
51
+ target,
52
+ packageRoot,
53
+ taskId,
54
+ artifactPath: ARTIFACT_PATHS.state,
55
+ })),
56
+ ...(await validateChecksExecutionProvenance(receiptSource?.value?.checks, {
57
+ target,
58
+ packageRoot,
59
+ taskId,
60
+ artifactPath: ARTIFACT_PATHS.receipt,
61
+ })),
62
+ ];
63
+ if (provenanceErrors.length > 0) {
64
+ const first = provenanceErrors[0];
65
+ const error = new Error(first.message);
66
+ error.code = first.code;
67
+ error.artifacts = first.artifacts;
68
+ throw error;
69
+ }
40
70
  const required = [
41
71
  [ARTIFACT_PATHS.contract, "contract.json", "current-contract"],
42
72
  [ARTIFACT_PATHS.route, "route.json", "routing-result"],
@@ -64,6 +94,16 @@ export async function exportTaskBundle(target, taskId, packageRoot) {
64
94
  const copied = await copyJson(target, sourcePath, `${directory}/${destinationName}`, schemaName, packageRoot, artifacts, destinationName);
65
95
  if (copied && !artifacts.includes(destinationName)) artifacts.push(destinationName);
66
96
  }
97
+ const executionRefs = [...new Set([
98
+ ...(stateSource.value.checks ?? []),
99
+ ...(receiptSource?.value?.checks ?? []),
100
+ ].map((check) => check?.executionRef).filter(Boolean))].sort();
101
+ for (const executionRef of executionRefs) {
102
+ const execution = await readExecutionArtifact({ target, executionRef, packageRoot });
103
+ const destination = `${directory}/executions/${execution.value.executionId}.json`;
104
+ await writeJsonArtifact(target, destination, execution.value, "execution", packageRoot);
105
+ artifacts.push(`executions/${execution.value.executionId}.json`);
106
+ }
67
107
  const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
68
108
  if (await fileExists(eventsPath)) {
69
109
  await assertSafePath(target, `${directory}/events.ndjson`);
@@ -107,7 +147,13 @@ export async function readTaskBundle(target, taskId, packageRoot) {
107
147
  "sources.json": ["sources", "source-registry"],
108
148
  "config.json": ["config", "config"],
109
149
  };
150
+ const executions = {};
110
151
  for (const artifact of manifest.value.artifacts) {
152
+ if (artifact.startsWith("executions/") && artifact.endsWith(".json")) {
153
+ const execution = await readJsonArtifact(target, `${directory}/${artifact}`, "execution", packageRoot);
154
+ executions[execution.value.executionId] = execution.value;
155
+ continue;
156
+ }
111
157
  const mapping = mappings[artifact];
112
158
  if (!mapping) continue;
113
159
  const loadedArtifact = await readJsonArtifact(target, `${directory}/${artifact}`, mapping[1], packageRoot);
@@ -116,5 +162,31 @@ export async function readTaskBundle(target, taskId, packageRoot) {
116
162
  }
117
163
  loaded[mapping[0]] = loadedArtifact.value;
118
164
  }
165
+ if (Object.keys(executions).length > 0) loaded.executions = executions;
166
+ const provenanceErrors = [
167
+ ...(await validateChecksExecutionProvenance(loaded.state?.checks, {
168
+ target,
169
+ packageRoot,
170
+ taskId,
171
+ executionArtifacts: executions,
172
+ allowForeignCwd: true,
173
+ artifactPath: "state.json",
174
+ })),
175
+ ...(await validateChecksExecutionProvenance(loaded.receipt?.checks, {
176
+ target,
177
+ packageRoot,
178
+ taskId,
179
+ executionArtifacts: executions,
180
+ allowForeignCwd: true,
181
+ artifactPath: "receipt.json",
182
+ })),
183
+ ];
184
+ if (provenanceErrors.length > 0) {
185
+ const first = provenanceErrors[0];
186
+ const error = new Error(first.message);
187
+ error.code = first.code;
188
+ error.artifacts = first.artifacts;
189
+ throw error;
190
+ }
119
191
  return { manifest: manifest.value, artifacts: loaded };
120
192
  }
@@ -1,8 +1,10 @@
1
1
  import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { validateVerificationAuthority } from "./verification-capability.js";
2
3
 
3
4
  export const CHECK_SCHEMA_VERSION = 1;
4
5
  export const CHECK_STATUSES = Object.freeze(["passed", "failed", "blocked", "not-run"]);
5
6
  export const CHECK_EVIDENCE_KINDS = Object.freeze(["OBSERVED", "INFERRED", "NOT_VERIFIED", "BLOCKED"]);
7
+ export const CHECK_PROVENANCE = Object.freeze(["FORGELOOP_EXECUTED", "ACTOR_REPORTED", "MANUAL_OBSERVATION"]);
6
8
 
7
9
  function checkError(code, message, artifacts = []) {
8
10
  const error = new Error(message);
@@ -45,7 +47,7 @@ function assertCompoundStatus(value, label) {
45
47
  }
46
48
  }
47
49
 
48
- export function createCheck(input = {}) {
50
+ export function createCheck(input = {}, options = {}) {
49
51
  const check = {
50
52
  schemaVersion: CHECK_SCHEMA_VERSION,
51
53
  protocolVersion: PROTOCOL_VERSION,
@@ -59,12 +61,14 @@ export function createCheck(input = {}) {
59
61
  ...(input.timestamp !== undefined ? { timestamp: input.timestamp } : {}),
60
62
  ...(input.repositoryFingerprint !== undefined ? { repositoryFingerprint: input.repositoryFingerprint } : {}),
61
63
  ...(input.viewport !== undefined ? { viewport: structuredClone(input.viewport) } : {}),
64
+ ...(input.executionRef !== undefined ? { executionRef: input.executionRef } : {}),
65
+ ...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
62
66
  ...(input.details !== undefined ? { details: structuredClone(input.details) } : {}),
63
67
  };
64
- return assertCheck(check);
68
+ return assertCheck(check, "check", options);
65
69
  }
66
70
 
67
- export function assertCheck(value, label = "check") {
71
+ export function assertCheck(value, label = "check", options = {}) {
68
72
  if (!value || typeof value !== "object" || Array.isArray(value)) {
69
73
  throw checkError("E_CHECK_INVALID", `${label} must be an object`);
70
74
  }
@@ -75,6 +79,19 @@ export function assertCheck(value, label = "check") {
75
79
  string(value.kind, `${label}.kind`);
76
80
  string(value.requirement, `${label}.requirement`);
77
81
  string(value.source, `${label}.source`);
82
+ optionalString(value.executionRef, `${label}.executionRef`);
83
+ if (value.provenance !== undefined && !CHECK_PROVENANCE.includes(value.provenance)) {
84
+ throw checkError("E_CHECK_INVALID", `${label}.provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
85
+ }
86
+ if (options.requireCommandProvenance === true
87
+ && value.kind === "command"
88
+ && value.evidenceKind === "OBSERVED"
89
+ && (value.executionRef === undefined || value.provenance !== "FORGELOOP_EXECUTED")) {
90
+ throw checkError(
91
+ "E_COMMAND_PROVENANCE_UNATTESTED",
92
+ `${label} observed command evidence requires ForgeLoop execution provenance`,
93
+ );
94
+ }
78
95
  if (!CHECK_STATUSES.includes(value.status)) {
79
96
  throw checkError("E_CHECK_INVALID", `${label}.status must be one of ${CHECK_STATUSES.join(", ")}`);
80
97
  }
@@ -97,15 +114,21 @@ export function assertCheck(value, label = "check") {
97
114
  if (value.status === "not-run" && value.evidenceKind !== "NOT_VERIFIED") {
98
115
  throw contradiction(`${label} not-run must use NOT_VERIFIED evidence`);
99
116
  }
117
+ if (value.status === "passed") {
118
+ const auth = validateVerificationAuthority(value, options);
119
+ if (!auth.valid) {
120
+ throw checkError(auth.error.code, auth.error.message);
121
+ }
122
+ }
100
123
  assertCompoundStatus(value, label);
101
124
  return value;
102
125
  }
103
126
 
104
- export function assertCheckList(value, label = "checks") {
127
+ export function assertCheckList(value, label = "checks", options = {}) {
105
128
  if (!Array.isArray(value)) throw checkError("E_CHECK_INVALID", `${label} must be an array`);
106
129
  const ids = new Set();
107
130
  value.forEach((item, index) => {
108
- assertCheck(item, `${label}[${index}]`);
131
+ assertCheck(item, `${label}[${index}]`, options);
109
132
  if (ids.has(item.id)) throw checkError("E_CHECK_INVALID", `${label} contains duplicate id ${item.id}`);
110
133
  ids.add(item.id);
111
134
  });