@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.
@@ -8,10 +8,10 @@ import {
8
8
  import { readContract } from "./contract.js";
9
9
  import { appendProtocolEvent, validateEventLedger } from "./events.js";
10
10
  import { completionEvidenceForGuides } from "./guide-metadata.js";
11
- import { createCheck } from "./checks.js";
11
+ import { CHECK_PROVENANCE, createCheck } from "./checks.js";
12
12
  import { createEvidence } from "./evidence.js";
13
13
  import { coverageForRequirements } from "./coverage.js";
14
- import { assertCompletionRelationships } from "./completion-relationships.js";
14
+ import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
15
15
  import { evaluatePreflight } from "./preflight.js";
16
16
  import { currentChangedPaths } from "./repository.js";
17
17
  import { readPersistedRoute } from "./route-artifact.js";
@@ -19,6 +19,8 @@ import { createReceipt, validateReceipt } from "./receipt.js";
19
19
  import { readWorkState, writeWorkState } from "./work-state.js";
20
20
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
21
21
  import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
22
+ import { classifyCommandResolution, validateVerificationAuthority } from "./verification-capability.js";
23
+ import { readExecutionArtifact, validateExecutionBinding } from "./execution.js";
22
24
 
23
25
  function artifactError(code, message, artifacts = []) {
24
26
  const error = new Error(message);
@@ -58,6 +60,102 @@ async function readOptionalConfig(target, packageRoot) {
58
60
  }
59
61
  }
60
62
 
63
+ function formatArgv(argv) {
64
+ return argv.map((argument) => /[\s"']/u.test(argument)
65
+ ? JSON.stringify(argument)
66
+ : argument).join(" ");
67
+ }
68
+
69
+ function commandProvenanceError(message = "Observed command evidence requires a ForgeLoop execution artifact") {
70
+ return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
71
+ }
72
+
73
+ /**
74
+ * Revalidate the ForgeLoop-owned execution artifact behind an observed command
75
+ * check. This is intentionally asynchronous so completion and audit can verify
76
+ * the artifact instead of trusting duplicated check metadata.
77
+ */
78
+ export async function validateCheckExecutionProvenance(check, {
79
+ target,
80
+ packageRoot,
81
+ taskId,
82
+ executionArtifacts,
83
+ allowForeignCwd = false,
84
+ } = {}) {
85
+ if (check?.kind !== "command" || check.evidenceKind !== "OBSERVED") return null;
86
+ if (check.provenance !== "FORGELOOP_EXECUTED" || !check.executionRef) {
87
+ throw commandProvenanceError();
88
+ }
89
+ const artifact = executionArtifacts
90
+ ? { value: executionArtifacts[check.executionRef] }
91
+ : await readExecutionArtifact({
92
+ target,
93
+ executionRef: check.executionRef,
94
+ packageRoot,
95
+ });
96
+ if (!artifact.value) {
97
+ throw artifactError(
98
+ "E_EXECUTION_REF_INVALID",
99
+ "Execution reference does not resolve to an execution artifact in this bundle",
100
+ [ARTIFACT_PATHS.executionDirectory],
101
+ );
102
+ }
103
+ const execution = validateExecutionBinding({
104
+ execution: artifact.value,
105
+ taskId,
106
+ checkId: check.id,
107
+ requirement: check.requirement,
108
+ verificationCycle: check.details?.verificationCycle ?? 1,
109
+ });
110
+ if (!allowForeignCwd && path.resolve(execution.cwd) !== path.resolve(target)) {
111
+ throw artifactError(
112
+ "E_EXECUTION_REF_INVALID",
113
+ "Execution artifact cwd does not match the current target",
114
+ [ARTIFACT_PATHS.executionDirectory],
115
+ );
116
+ }
117
+ if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
118
+ throw artifactError(
119
+ "E_EXECUTION_REF_INVALID",
120
+ "A passed command check must reference a successful execution artifact",
121
+ [ARTIFACT_PATHS.executionDirectory],
122
+ );
123
+ }
124
+ if (check.status === "failed" && execution.status !== "failed") {
125
+ throw artifactError(
126
+ "E_EXECUTION_REF_INVALID",
127
+ "A failed command check must reference a failed execution artifact",
128
+ [ARTIFACT_PATHS.executionDirectory],
129
+ );
130
+ }
131
+ if (check.exitCode !== undefined && check.exitCode !== execution.exitCode) {
132
+ throw artifactError(
133
+ "E_EXECUTION_REF_INVALID",
134
+ "Check exitCode does not match its execution artifact",
135
+ [ARTIFACT_PATHS.executionDirectory],
136
+ );
137
+ }
138
+ return execution;
139
+ }
140
+
141
+ export async function validateChecksExecutionProvenance(checks, options = {}) {
142
+ const errors = [];
143
+ for (const check of Array.isArray(checks) ? checks : []) {
144
+ try {
145
+ await validateCheckExecutionProvenance(check, options);
146
+ } catch (error) {
147
+ errors.push({
148
+ code: error.code ?? "E_EXECUTION_REF_INVALID",
149
+ message: error.message,
150
+ artifacts: error.artifacts ?? [options.artifactPath ?? ARTIFACT_PATHS.receipt],
151
+ checkId: check?.id,
152
+ requirementId: check?.requirement,
153
+ });
154
+ }
155
+ }
156
+ return errors;
157
+ }
158
+
61
159
  export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
62
160
  const config = await readOptionalConfig(target, packageRoot);
63
161
  const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
@@ -69,7 +167,7 @@ export async function requiredEvidenceForTarget({ target, contract, route, packa
69
167
  ])].sort();
70
168
  }
71
169
 
72
- export async function prepareCompletion({ target, packageRoot }) {
170
+ export async function prepareCompletion({ target, packageRoot, authorityContext, runtimeContext }) {
73
171
  const contract = await readContract(target, packageRoot);
74
172
  const route = await readPersistedRoute(target, packageRoot);
75
173
  const state = await readWorkState(target, packageRoot);
@@ -83,7 +181,12 @@ export async function prepareCompletion({ target, packageRoot }) {
83
181
  let existing = null;
84
182
  try {
85
183
  existing = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
86
- await validateReceipt(existing.value, packageRoot);
184
+ await validateReceipt(existing.value, packageRoot, {
185
+ target,
186
+ taskId: contract?.value?.taskId,
187
+ authorityContext,
188
+ runtimeContext,
189
+ });
87
190
  } catch (error) {
88
191
  if (error.code !== "ARTIFACT_MISSING") throw error;
89
192
  }
@@ -97,17 +200,19 @@ export async function prepareCompletion({ target, packageRoot }) {
97
200
  additionalEvidence: preflight.policy?.requiredEvidence ?? [],
98
201
  });
99
202
  const existingValue = existing?.value ?? {};
100
- assertCompletionRelationships({
101
- contract,
102
- route,
103
- state,
104
- receipt: existingValue.taskId ? existingValue : null,
105
- requiredEvidence,
106
- requireRequiredChecks: false,
107
- });
108
- const changedPaths = existing
109
- ? [...(existingValue.changedPaths ?? [])]
110
- : (await currentChangedPaths(target) ?? []);
203
+ if (existingValue.taskId && existingValue.taskId !== contract.value.taskId) {
204
+ throw artifactError("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.receipt]);
205
+ }
206
+ if (existing && existingValue.stateFingerprint === undefined) {
207
+ throw artifactError("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.receipt]);
208
+ }
209
+ assertStateIdentity({ contract, route, state });
210
+ const observedPaths = await currentChangedPaths(target);
211
+ const changedPaths = observedPaths !== null
212
+ ? [...observedPaths]
213
+ : existing
214
+ ? [...(existingValue.changedPaths ?? [])]
215
+ : [];
111
216
  const checks = existing ? [...existingValue.checks] : [...state.checks];
112
217
  const evidence = existing ? [...(existingValue.evidence ?? [])] : [...state.verificationEvidence];
113
218
  const receipt = await createReceipt({
@@ -126,7 +231,11 @@ export async function prepareCompletion({ target, packageRoot }) {
126
231
  changedPaths,
127
232
  checks,
128
233
  evidence,
129
- evidenceCoverage: coverageForRequirements(requiredEvidence, checks),
234
+ evidenceCoverage: coverageForRequirements(requiredEvidence, checks, {
235
+ target,
236
+ taskId: contract.value.taskId,
237
+ options: { authorityContext, runtimeContext },
238
+ }),
130
239
  review: existingValue.review ?? { status: "not-run", independent: false },
131
240
  limitations: [...(existingValue.limitations ?? [])],
132
241
  publication: existingValue.publication ?? {
@@ -135,7 +244,24 @@ export async function prepareCompletion({ target, packageRoot }) {
135
244
  pullRequest: null,
136
245
  deployed: false,
137
246
  },
138
- }, packageRoot);
247
+ }, packageRoot, {
248
+ target,
249
+ taskId: contract.value.taskId,
250
+ authorityContext,
251
+ runtimeContext,
252
+ });
253
+ assertCompletionRelationships({
254
+ contract,
255
+ route,
256
+ state,
257
+ receipt,
258
+ requiredEvidence,
259
+ requireRequiredChecks: false,
260
+ target,
261
+ taskId: contract.value.taskId,
262
+ authorityContext,
263
+ runtimeContext,
264
+ });
139
265
  const written = await writeJsonArtifact(
140
266
  target,
141
267
  ARTIFACT_PATHS.receipt,
@@ -167,38 +293,20 @@ function appendUniqueEvidence(evidence, nextEvidence) {
167
293
  return exists ? [...evidence] : [...evidence, nextEvidence];
168
294
  }
169
295
 
170
- export async function recordCheck({
296
+ /**
297
+ * Read-only lifecycle checks shared by run-check and record-check. Keeping
298
+ * these checks before process launch prevents a command from running when the
299
+ * target is not ready to receive verification evidence.
300
+ */
301
+ export async function assertRecordCheckPrerequisites({
171
302
  target,
172
303
  packageRoot,
173
- id,
174
- kind = "command",
175
304
  requirement,
176
305
  status,
177
306
  evidenceKind,
178
- command,
179
- result,
180
- exitCode,
181
- details,
182
- }) {
183
- requiredString(id, "check id");
184
- requiredString(kind, "check kind");
185
- requiredString(requirement, "check requirement");
186
- requiredString(status, "check status");
187
- requiredString(evidenceKind, "evidence kind");
188
- if (command !== undefined && typeof command !== "string") {
189
- throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
190
- }
191
- if (result !== undefined && typeof result !== "string") {
192
- throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
193
- }
194
- if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
195
- throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
196
- }
197
- if ((typeof command !== "string" || command.trim() === "")
198
- && (typeof result !== "string" || result.trim() === "")) {
199
- throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
200
- }
201
-
307
+ authorityContext,
308
+ runtimeContext,
309
+ } = {}) {
202
310
  const state = await readWorkState(target, packageRoot);
203
311
  if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
204
312
  if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
@@ -235,9 +343,125 @@ export async function recordCheck({
235
343
  }
236
344
 
237
345
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
238
- await validateReceipt(existingReceipt.value, packageRoot);
239
- const source = command?.trim() || `check:${id}`;
240
- const recordedResult = result?.trim() || `recorded command: ${command.trim()}`;
346
+ await validateReceipt(existingReceipt.value, packageRoot, {
347
+ target,
348
+ taskId: contract.value.taskId,
349
+ authorityContext,
350
+ runtimeContext,
351
+ });
352
+ const ledger = await validateEventLedger(target, packageRoot);
353
+ if (!ledger.valid) {
354
+ const first = ledger.errors[0];
355
+ throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
356
+ }
357
+ if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
358
+ throw artifactError(
359
+ "E_PHASE_CHRONOLOGY_INVALID",
360
+ "record-check requires VERIFICATION_STARTED in the current task ledger",
361
+ [ARTIFACT_PATHS.events],
362
+ );
363
+ }
364
+ return {
365
+ state,
366
+ contract,
367
+ route,
368
+ preflight,
369
+ requiredEvidence,
370
+ existingReceipt,
371
+ ledger,
372
+ };
373
+ }
374
+
375
+ export async function recordCheck({
376
+ target,
377
+ packageRoot,
378
+ id,
379
+ kind = "command",
380
+ requirement,
381
+ status,
382
+ evidenceKind,
383
+ command,
384
+ result,
385
+ exitCode,
386
+ details,
387
+ executionRef,
388
+ provenance,
389
+ authorityContext,
390
+ runtimeContext,
391
+ }) {
392
+ requiredString(id, "check id");
393
+ requiredString(kind, "check kind");
394
+ requiredString(requirement, "check requirement");
395
+ requiredString(status, "check status");
396
+ requiredString(evidenceKind, "evidence kind");
397
+ if (command !== undefined && typeof command !== "string") {
398
+ throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
399
+ }
400
+ if (result !== undefined && typeof result !== "string") {
401
+ throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
402
+ }
403
+ if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
404
+ throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
405
+ }
406
+ if (executionRef !== undefined && (typeof executionRef !== "string" || executionRef.trim() === "")) {
407
+ throw artifactError("E_EXECUTION_REF_INVALID", "executionRef must be a non-empty string when supplied");
408
+ }
409
+ if (provenance !== undefined && !CHECK_PROVENANCE.includes(provenance)) {
410
+ throw artifactError("E_CHECK_INVALID", `provenance must be one of ${CHECK_PROVENANCE.join(", ")}`);
411
+ }
412
+ if ((typeof command !== "string" || command.trim() === "")
413
+ && (typeof result !== "string" || result.trim() === "")) {
414
+ throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
415
+ }
416
+
417
+ const context = await assertRecordCheckPrerequisites({
418
+ target,
419
+ packageRoot,
420
+ requirement,
421
+ status,
422
+ evidenceKind,
423
+ authorityContext,
424
+ runtimeContext,
425
+ });
426
+ const {
427
+ state,
428
+ contract,
429
+ route,
430
+ requiredEvidence,
431
+ existingReceipt,
432
+ } = context;
433
+
434
+ const commandSpec = typeof command === "string" && command.trim() !== "" ? command.trim() : undefined;
435
+ const observedCommand = kind === "command" && evidenceKind === "OBSERVED";
436
+ if (observedCommand && (!executionRef || provenance !== "FORGELOOP_EXECUTED")) {
437
+ throw commandProvenanceError();
438
+ }
439
+ const execution = executionRef
440
+ ? await validateCheckExecutionProvenance({
441
+ kind,
442
+ evidenceKind,
443
+ provenance,
444
+ executionRef,
445
+ status,
446
+ exitCode,
447
+ id,
448
+ requirement,
449
+ details: { ...(details ?? {}), verificationCycle: state.verificationCycle ?? 1 },
450
+ }, {
451
+ target,
452
+ packageRoot,
453
+ taskId: contract.value.taskId,
454
+ })
455
+ : null;
456
+ const effectiveCommand = execution ? formatArgv(execution.argv) : commandSpec;
457
+ const source = effectiveCommand || `check:${id}`;
458
+ const recordedResult = result?.trim() || `recorded command: ${effectiveCommand || source}`;
459
+ const classification = execution?.resolution ?? (effectiveCommand !== undefined ? classifyCommandResolution(effectiveCommand) : null);
460
+ const installationAuthorized = Boolean(
461
+ details?.installationAuthorized
462
+ || details?.authority?.softwareInstallation === "AUTHORIZED"
463
+ || details?.execution?.installationAuthorized
464
+ );
241
465
  const check = createCheck({
242
466
  id,
243
467
  kind,
@@ -245,14 +469,41 @@ export async function recordCheck({
245
469
  status,
246
470
  evidenceKind,
247
471
  source,
472
+ ...(executionRef === undefined ? {} : { executionRef }),
473
+ ...(provenance === undefined ? {} : { provenance }),
248
474
  timestamp: new Date().toISOString(),
249
- ...(exitCode === undefined ? {} : { exitCode }),
475
+ ...(execution?.exitCode !== null && execution?.exitCode !== undefined
476
+ ? { exitCode: execution.exitCode }
477
+ : exitCode === undefined ? {} : { exitCode }),
250
478
  details: {
251
- ...(command === undefined ? {} : { command }),
479
+ ...(effectiveCommand === undefined ? {} : { command: effectiveCommand }),
252
480
  ...(result === undefined ? {} : { result }),
253
481
  ...(details === undefined ? {} : details),
254
482
  verificationCycle: state.verificationCycle ?? 1,
483
+ ...(classification ? {
484
+ execution: {
485
+ ...(details?.execution ?? {}),
486
+ ...(execution ? {
487
+ executionRef: execution.executionId,
488
+ argv: [...execution.argv],
489
+ cwd: execution.cwd,
490
+ resolution: execution.resolution,
491
+ status: execution.status,
492
+ exitCode: execution.exitCode,
493
+ } : {}),
494
+ resolutionMode: classification.resolutionMode,
495
+ mayInstall: classification.mayInstall,
496
+ installationAuthorized,
497
+ },
498
+ } : {}),
255
499
  },
500
+ }, {
501
+ target,
502
+ taskId: contract.value.taskId,
503
+ packageRoot,
504
+ authorityContext,
505
+ runtimeContext,
506
+ requireCommandProvenance: observedCommand,
256
507
  });
257
508
  const evidence = createEvidence({
258
509
  kind: evidenceKind,
@@ -265,6 +516,19 @@ export async function recordCheck({
265
516
  },
266
517
  });
267
518
 
519
+ if (status === "passed") {
520
+ const auth = validateVerificationAuthority(check, {
521
+ target,
522
+ taskId: contract.value.taskId,
523
+ packageRoot,
524
+ authorityContext,
525
+ runtimeContext,
526
+ });
527
+ if (!auth.valid) {
528
+ throw artifactError(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]);
529
+ }
530
+ }
531
+
268
532
  const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
269
533
  const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
270
534
  assertCompletionRelationships({
@@ -274,20 +538,16 @@ export async function recordCheck({
274
538
  receipt: existingReceipt.value,
275
539
  requiredEvidence,
276
540
  requireRequiredChecks: false,
541
+ target,
542
+ taskId: contract.value.taskId,
543
+ authorityContext,
544
+ runtimeContext,
545
+ });
546
+ const coverage = coverageForRequirements(requiredEvidence, checks, {
547
+ target,
548
+ taskId: contract.value.taskId,
549
+ options: { authorityContext, runtimeContext },
277
550
  });
278
- const ledger = await validateEventLedger(target, packageRoot);
279
- if (!ledger.valid) {
280
- const first = ledger.errors[0];
281
- throw artifactError(first.code, first.message, [ARTIFACT_PATHS.events]);
282
- }
283
- if (!ledger.events.some((event) => event.taskId === state.taskId && event.event === "VERIFICATION_STARTED")) {
284
- throw artifactError(
285
- "E_PHASE_CHRONOLOGY_INVALID",
286
- "record-check requires VERIFICATION_STARTED in the current task ledger",
287
- [ARTIFACT_PATHS.events],
288
- );
289
- }
290
- const coverage = coverageForRequirements(requiredEvidence, checks);
291
551
  const nextState = {
292
552
  ...state,
293
553
  checks,
@@ -302,7 +562,12 @@ export async function recordCheck({
302
562
  evidenceCoverage: coverage,
303
563
  stateFingerprint: canonicalFingerprint(nextState),
304
564
  verificationCycle: state.verificationCycle ?? 1,
305
- }, packageRoot);
565
+ }, packageRoot, {
566
+ target,
567
+ taskId: contract.value.taskId,
568
+ authorityContext,
569
+ runtimeContext,
570
+ });
306
571
 
307
572
  assertCompletionRelationships({
308
573
  contract,
@@ -311,6 +576,10 @@ export async function recordCheck({
311
576
  receipt: nextReceipt,
312
577
  requiredEvidence,
313
578
  requireRequiredChecks: false,
579
+ target,
580
+ taskId: contract.value.taskId,
581
+ authorityContext,
582
+ runtimeContext,
314
583
  });
315
584
 
316
585
  await writeWorkState(target, nextState, { packageRoot });
@@ -351,6 +620,8 @@ export async function recordTerminalResult({
351
620
  source,
352
621
  result,
353
622
  details = {},
623
+ authorityContext,
624
+ runtimeContext,
354
625
  } = {}) {
355
626
  if (!target || !requirement || !type || !status || !source || !result) {
356
627
  throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
@@ -413,7 +684,12 @@ export async function recordTerminalResult({
413
684
  }
414
685
 
415
686
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
416
- await validateReceipt(existingReceipt.value, packageRoot);
687
+ await validateReceipt(existingReceipt.value, packageRoot, {
688
+ target,
689
+ taskId: contract?.value?.taskId,
690
+ authorityContext,
691
+ runtimeContext,
692
+ });
417
693
 
418
694
  if (type === "PUBLICATION") {
419
695
  const rank = {
@@ -540,7 +816,12 @@ export async function recordTerminalResult({
540
816
  const nextReceipt = await createReceipt({
541
817
  ...receiptUpdates,
542
818
  stateFingerprint: canonicalFingerprint(nextState),
543
- }, packageRoot);
819
+ }, packageRoot, {
820
+ target,
821
+ taskId: contract.value.taskId,
822
+ authorityContext,
823
+ runtimeContext,
824
+ });
544
825
 
545
826
  await writeWorkState(target, nextState, { packageRoot });
546
827
  await writeJsonArtifact(
@@ -8,6 +8,10 @@ export const RECOVERABLE_COMPLETION_EVIDENCE_CODES = Object.freeze([
8
8
  "E_VERIFICATION_CHECK_REQUIRED",
9
9
  "E_CHECK_REQUIRED",
10
10
  "E_CHECK_INVALID",
11
+ "E_INSTALLATION_AUTHORITY_REQUIRED",
12
+ "E_AUTHORITY_INVALID",
13
+ "E_AUTHORITY_SCOPE_MISMATCH",
14
+ "E_AUTHORITY_UNTRUSTED_SOURCE",
11
15
  ]);
12
16
 
13
17
  export function isRecoverableCompletionEvidenceCode(code) {
@@ -3,6 +3,7 @@ import { assertCheckList } from "./checks.js";
3
3
  import { assertCoverageList, coverageForRequirements } from "./coverage.js";
4
4
  import { assertEvidenceList } from "./evidence.js";
5
5
  import { evaluateRequiredEvidence } from "./evidence-readiness.js";
6
+ import { validateVerificationAuthority } from "./verification-capability.js";
6
7
 
7
8
  function issue(code, message, artifacts = [], details = {}) {
8
9
  return { code, message, artifacts, ...details };
@@ -65,10 +66,23 @@ export function completionRelationshipErrors({
65
66
  requiredEvidence = [],
66
67
  requireReceiptStateFingerprint = true,
67
68
  requireRequiredChecks = true,
69
+ target,
70
+ taskId,
71
+ authorities,
72
+ authorityContext,
73
+ runtimeContext,
68
74
  } = {}) {
69
75
  const errors = stateIdentityErrors({ contract, route, state });
70
76
  const contractValue = contract?.value ?? contract;
71
77
  const contractFingerprint = contract?.fingerprint;
78
+ const effectiveTaskId = taskId ?? contractValue?.taskId ?? state?.taskId ?? receipt?.taskId;
79
+ const authOptions = {
80
+ ...(target ? { target } : {}),
81
+ ...(effectiveTaskId ? { taskId: effectiveTaskId } : {}),
82
+ ...(authorities ? { authorities } : {}),
83
+ ...(authorityContext ? { authorityContext } : {}),
84
+ ...(runtimeContext ? { runtimeContext } : {}),
85
+ };
72
86
  if (contractValue && receipt && contractValue.taskId !== receipt.taskId) {
73
87
  errors.push(issue("E_RECEIPT_TASK_MISMATCH", "Execution receipt does not belong to the current contract task", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
74
88
  }
@@ -80,14 +94,30 @@ export function completionRelationshipErrors({
80
94
  }
81
95
 
82
96
  if (state) {
83
- addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
97
+ addAssertion(errors, () => assertCheckList(state.checks, "work-state.checks", authOptions), "E_CHECK_INVALID", [ARTIFACT_PATHS.state]);
84
98
  addAssertion(errors, () => assertEvidenceList(state.verificationEvidence, "work-state.verificationEvidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.state]);
99
+ for (const check of state.checks ?? []) {
100
+ if (check.status === "passed") {
101
+ const auth = validateVerificationAuthority(check, authOptions);
102
+ if (!auth.valid) {
103
+ errors.push(issue(auth.error.code, auth.error.message, [ARTIFACT_PATHS.state]));
104
+ }
105
+ }
106
+ }
85
107
  }
86
108
  if (receipt) {
87
- addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
109
+ addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks", authOptions), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
88
110
  addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
111
+ for (const check of receipt.checks ?? []) {
112
+ if (check.status === "passed") {
113
+ const auth = validateVerificationAuthority(check, authOptions);
114
+ if (!auth.valid) {
115
+ errors.push(issue(auth.error.code, auth.error.message, [ARTIFACT_PATHS.receipt]));
116
+ }
117
+ }
118
+ }
89
119
  if (requireRequiredChecks) {
90
- const readiness = evaluateRequiredEvidence({ requirements: requiredEvidence, checks: receipt.checks });
120
+ const readiness = evaluateRequiredEvidence({ requirements: requiredEvidence, checks: receipt.checks, options: authOptions });
91
121
  for (const requirement of readiness.missing) {
92
122
  errors.push(issue("E_EVIDENCE_REQUIRED", `Required check is missing: ${requirement.text}`, [ARTIFACT_PATHS.receipt], { requirementId: requirement.id }));
93
123
  }
@@ -100,7 +130,12 @@ export function completionRelationshipErrors({
100
130
  }
101
131
  }
102
132
 
103
- const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks) : [];
133
+ const expectedCoverage = state ? coverageForRequirements(requiredEvidence, state.checks, {
134
+ target,
135
+ taskId: effectiveTaskId,
136
+ authorities,
137
+ options: authOptions,
138
+ }) : [];
104
139
  if (state?.evidenceCoverage !== undefined) {
105
140
  addAssertion(errors, () => assertCoverageList(state.evidenceCoverage, "work-state.evidenceCoverage"), "E_EVIDENCE_COVERAGE_INVALID", [ARTIFACT_PATHS.state]);
106
141
  if (!sameValue(state.evidenceCoverage, expectedCoverage)) {