@cassiomc1/forgeloop 0.1.10 → 0.1.12

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.
Files changed (69) hide show
  1. package/.cursor/rules/project-loop.mdc +16 -18
  2. package/.github/copilot-instructions.md +16 -19
  3. package/AGENTS.md +18 -24
  4. package/AGENT_COMPATIBILITY.md +4 -211
  5. package/CLAUDE.md +16 -18
  6. package/LOOP_ENGINEERING.md +73 -0
  7. package/LOOP_SYSTEM_DESIGN.md +9 -5
  8. package/ORCHESTRATOR_INTEGRATION.md +4 -0
  9. package/PROTOCOL_INTEGRATION.md +171 -0
  10. package/QUALITY_SCORECARD.md +1 -1
  11. package/README.md +123 -103
  12. package/TERMINOLOGY.md +2 -0
  13. package/THREAT_MODEL.md +4 -0
  14. package/package.json +5 -4
  15. package/schemas/current-contract.schema.json +48 -2
  16. package/schemas/evidence.schema.json +1 -0
  17. package/schemas/execution-receipt.schema.json +2 -0
  18. package/schemas/work-state.schema.json +44 -1
  19. package/src/cli.js +48 -4
  20. package/src/commands/record-terminal-result.js +18 -0
  21. package/src/commands/validate-protocol.js +14 -2
  22. package/src/core/checks.js +20 -0
  23. package/src/core/completion-artifacts.js +266 -11
  24. package/src/core/completion-recovery.js +15 -0
  25. package/src/core/completion-relationships.js +23 -9
  26. package/src/core/completion.js +130 -2
  27. package/src/core/contract.js +82 -4
  28. package/src/core/coverage.js +17 -12
  29. package/src/core/discovery-surfaces.js +22 -0
  30. package/src/core/events.js +183 -2
  31. package/src/core/evidence-readiness.js +333 -0
  32. package/src/core/evidence.js +22 -4
  33. package/src/core/execution-prerequisites.js +6 -1
  34. package/src/core/inspect.js +23 -12
  35. package/src/core/native-adapters.js +30 -2
  36. package/src/core/next-action.js +103 -27
  37. package/src/core/phase.js +68 -5
  38. package/src/core/protocol.js +28 -1
  39. package/src/core/receipt.js +5 -0
  40. package/src/core/schema-validation.js +28 -6
  41. package/src/core/templates.js +1 -0
  42. package/src/core/work-state.js +27 -0
  43. package/conformance/README.md +0 -148
  44. package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
  45. package/conformance/backend-auth/REQUEST.md +0 -4
  46. package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
  47. package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
  48. package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
  49. package/conformance/blind-premium-website/REQUEST.md +0 -6
  50. package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
  51. package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
  52. package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
  53. package/conformance/complete-website/REQUEST.md +0 -6
  54. package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
  55. package/conformance/complete-website/REQUIRED_GATES.json +0 -3
  56. package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
  57. package/conformance/docs-only/REQUEST.md +0 -4
  58. package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
  59. package/conformance/docs-only/REQUIRED_GATES.json +0 -3
  60. package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
  61. package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
  62. package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
  63. package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
  64. package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
  65. package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
  66. package/conformance/simple-bug/REQUEST.md +0 -4
  67. package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
  68. package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
  69. package/src/core/agent-support.js +0 -89
@@ -46,7 +46,50 @@
46
46
  "diagnosedHypothesis": { "type": "string", "minLength": 1 },
47
47
  "verificationEvidence": { "type": "array", "items": { "type": "object" } },
48
48
  "evidenceCoverage": { "type": "array", "items": { "type": "object" } },
49
- "publicationStatus": { "enum": ["not-published", "local-only", "committed", "pushed", "published", "deployed"] }
49
+ "publicationStatus": { "enum": ["not-published", "local-only", "committed", "pushed", "published", "deployed"] },
50
+ "verificationCycle": { "type": "integer", "minimum": 1 },
51
+ "lastCompletionAttempt": {
52
+ "type": "object",
53
+ "required": [
54
+ "status",
55
+ "reasonCodes",
56
+ "missingRequirementIds",
57
+ "verificationCycle",
58
+ "timestamp"
59
+ ],
60
+ "properties": {
61
+ "status": { "const": "REJECTED" },
62
+ "reasonCodes": {
63
+ "type": "array",
64
+ "items": { "type": "string", "minLength": 1 },
65
+ "uniqueItems": true
66
+ },
67
+ "missingRequirementIds": {
68
+ "type": "array",
69
+ "items": { "type": "string", "minLength": 1 },
70
+ "uniqueItems": true
71
+ },
72
+ "verificationCycle": {
73
+ "type": "integer",
74
+ "minimum": 1
75
+ },
76
+ "stateFingerprint": {
77
+ "type": "string",
78
+ "pattern": "^[a-f0-9]{64}$"
79
+ },
80
+ "receiptFingerprint": {
81
+ "oneOf": [
82
+ { "type": "string", "pattern": "^[a-f0-9]{64}$" },
83
+ { "type": "null" }
84
+ ]
85
+ },
86
+ "timestamp": {
87
+ "type": "string",
88
+ "minLength": 1
89
+ }
90
+ },
91
+ "additionalProperties": false
92
+ }
50
93
  },
51
94
  "additionalProperties": false
52
95
  }
package/src/cli.js CHANGED
@@ -24,13 +24,14 @@ 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 { formatRecordTerminalResult, runRecordTerminalResult } from "./commands/record-terminal-result.js";
27
28
  import { formatNextActionResult, runNext } from "./commands/next.js";
28
29
  import { resolveTarget } from "./core/filesystem.js";
29
30
  import { getPackageRoot } from "./core/templates.js";
30
31
  import { ARTIFACT_PATHS } from "./core/artifacts.js";
31
32
 
32
33
  function usage(command = null) {
33
- const commands = "init|doctor|update|activate|route|preflight|advance|next|prepare-completion|record-check|complete|audit|report|policy|bundle|inspect|status|validate-state|clear-state|validate-receipt|validate-protocol";
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";
34
35
  const options = [" --path <directory> target project directory (default: current directory)"];
35
36
  if (!command || command === "init" || command === "update") {
36
37
  options.push(" --dry-run show planned writes without changing files");
@@ -52,7 +53,7 @@ function usage(command = null) {
52
53
  if (!command || command === "advance") {
53
54
  options.push(" --to <phase> destination workflow phase");
54
55
  }
55
- if (!command || ["activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(command)) {
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)) {
56
57
  options.push(" --json emit structured output as JSON");
57
58
  }
58
59
  if (!command || ["preflight", "complete", "audit", "report"].includes(command)) {
@@ -88,6 +89,14 @@ function usage(command = null) {
88
89
  options.push(" --exit-code <number> observed process exit code");
89
90
  options.push(" --details <json> additional structured check details");
90
91
  }
92
+ if (!command || command === "record-terminal-result") {
93
+ options.push(" --requirement <id> terminal requirement covered by the result");
94
+ options.push(" --type <type> PUBLICATION or PRODUCTION_READINESS");
95
+ options.push(" --status <status> observed terminal status");
96
+ options.push(" --source <text> external action source (e.g. npm publish, git push)");
97
+ options.push(" --result <text> observed external result description");
98
+ options.push(" --details <json> additional structured result details");
99
+ }
91
100
  options.push(" --version show the installed package version");
92
101
  options.push(" --help show this help");
93
102
 
@@ -124,6 +133,8 @@ export function parseArgs(argv) {
124
133
  checkResult: null,
125
134
  checkExitCode: null,
126
135
  checkDetails: null,
136
+ checkType: null,
137
+ checkSource: null,
127
138
  policy: null,
128
139
  task: null,
129
140
  help: false,
@@ -133,7 +144,7 @@ export function parseArgs(argv) {
133
144
 
134
145
  for (let index = 0; index < argv.length; index += 1) {
135
146
  const argument = argv[index];
136
- if (["init", "doctor", "update", "activate", "route", "preflight", "advance", "next", "prepare-completion", "record-check", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"].includes(argument)) {
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)) {
137
148
  if (command) throw new Error(`Multiple commands are not supported: ${argument}`);
138
149
  command = argument;
139
150
  } else if (argument === "--help" || argument === "-h") {
@@ -248,6 +259,24 @@ export function parseArgs(argv) {
248
259
  if (!commandText || commandText.startsWith("-")) throw new Error("--command requires recorded text");
249
260
  options.checkCommand = commandText;
250
261
  index += 1;
262
+ } else if (argument === "--source") {
263
+ const sourceText = argv[index + 1];
264
+ if (!sourceText || sourceText.startsWith("-")) throw new Error("--source requires recorded text");
265
+ options.checkSource = sourceText;
266
+ index += 1;
267
+ } else if (argument.startsWith("--source=")) {
268
+ const sourceText = argument.slice("--source=".length);
269
+ if (!sourceText) throw new Error("--source requires recorded text");
270
+ options.checkSource = sourceText;
271
+ } else if (argument === "--type") {
272
+ const typeValue = argv[index + 1];
273
+ if (!typeValue || typeValue.startsWith("-")) throw new Error("--type requires a terminal type");
274
+ options.checkType = typeValue;
275
+ index += 1;
276
+ } else if (argument.startsWith("--type=")) {
277
+ const typeValue = argument.slice("--type=".length);
278
+ if (!typeValue) throw new Error("--type requires a terminal type");
279
+ options.checkType = typeValue;
251
280
  } else if (argument === "--result") {
252
281
  const resultText = argv[index + 1];
253
282
  if (!resultText || resultText.startsWith("-")) throw new Error("--result requires recorded text");
@@ -295,7 +324,7 @@ export function parseArgs(argv) {
295
324
 
296
325
  if (!command) return { command: null, options };
297
326
 
298
- const jsonCommands = ["doctor", "route", "activate", "advance", "next", "prepare-completion", "record-check", "preflight", "complete", "audit", "report", "policy", "bundle", "inspect", "status", "validate-state", "clear-state", "validate-receipt", "validate-protocol"];
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"];
299
328
  if (!jsonCommands.includes(command) && options.json) {
300
329
  throw new Error(`Option --json is not valid for ${command}`);
301
330
  }
@@ -479,6 +508,21 @@ export async function main(argv = process.argv.slice(2)) {
479
508
  return 0;
480
509
  }
481
510
 
511
+ if (command === "record-terminal-result") {
512
+ const result = await runRecordTerminalResult({
513
+ target,
514
+ packageRoot,
515
+ requirement: options.checkRequirement,
516
+ type: options.checkType,
517
+ status: options.checkStatus,
518
+ source: options.checkSource ?? options.checkCommand,
519
+ result: options.checkResult,
520
+ details: options.checkDetails ?? undefined,
521
+ });
522
+ console.log(options.json ? JSON.stringify(result, null, 2) : formatRecordTerminalResult(result));
523
+ return 0;
524
+ }
525
+
482
526
  if (command === "complete") {
483
527
  const result = await runComplete({ target, packageRoot, strict: options.strict });
484
528
  console.log(options.json ? JSON.stringify(result, null, 2) : formatCompleteResult(result));
@@ -0,0 +1,18 @@
1
+ import { recordTerminalResult as recordTerminalResultArtifact } from "../core/completion-artifacts.js";
2
+
3
+ export { recordTerminalResultArtifact as recordTerminalResult };
4
+
5
+ export async function runRecordTerminalResult(input) {
6
+ return recordTerminalResultArtifact(input);
7
+ }
8
+
9
+ export function formatRecordTerminalResult(result) {
10
+ return [
11
+ "FORGELOOP TERMINAL RESULT RECORDED",
12
+ `requirement: ${result.requirementId}`,
13
+ `type: ${result.type}`,
14
+ `status: ${result.status}`,
15
+ `receipt: ${result.path}`,
16
+ "",
17
+ ].join("\n");
18
+ }
@@ -8,6 +8,7 @@ import { validateReceipt } from "../core/receipt.js";
8
8
  import { validateTaskBrief, validateDelegatedResult } from "../core/delegation.js";
9
9
  import { ARTIFACT_PATHS, readJsonArtifact } from "../core/artifacts.js";
10
10
  import { evaluatePreflight, validateReadyProtocolConsistency } from "../core/preflight.js";
11
+ import { validateEventLedger, validateStateLedgerCoherence } from "../core/events.js";
11
12
 
12
13
  async function readArtifact(target, relativePath, label) {
13
14
  if (!relativePath) return null;
@@ -118,11 +119,22 @@ export async function runValidateProtocol({
118
119
  } catch {
119
120
  // A missing or invalid preflight is already outside the optional protocol set.
120
121
  }
121
- if (readErrors.length > 0 || schemaErrors.length > 0 || readyConsistencyErrors.length > 0) {
122
+ let ledgerErrors = [];
123
+ if (state && !stateValidationError) {
124
+ const ledger = await validateEventLedger(target, packageRoot);
125
+ ledgerErrors = [
126
+ ...ledger.errors.map((error) => ({ ...error, artifacts: [ARTIFACT_PATHS.events] })),
127
+ ...validateStateLedgerCoherence(state, ledger.events).map((error) => ({
128
+ ...error,
129
+ artifacts: [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events],
130
+ })),
131
+ ];
132
+ }
133
+ if (readErrors.length > 0 || schemaErrors.length > 0 || readyConsistencyErrors.length > 0 || ledgerErrors.length > 0) {
122
134
  return {
123
135
  ...result,
124
136
  status: "INVALID",
125
- errors: [...result.errors, ...readErrors, ...schemaErrors, ...readyConsistencyErrors]
137
+ errors: [...result.errors, ...readErrors, ...schemaErrors, ...readyConsistencyErrors, ...ledgerErrors]
126
138
  .sort((left, right) => left.code.localeCompare(right.code) || left.message.localeCompare(right.message)),
127
139
  };
128
140
  }
@@ -26,6 +26,25 @@ function contradiction(message) {
26
26
  return checkError("E_CHECK_STATUS_CONTRADICTION", message);
27
27
  }
28
28
 
29
+ function assertCompoundStatus(value, label) {
30
+ const components = value.details?.components;
31
+ if (!Array.isArray(components)) return;
32
+ for (const [index, component] of components.entries()) {
33
+ if (!component || typeof component !== "object" || Array.isArray(component)) {
34
+ throw checkError("E_CHECK_INVALID", `${label}.details.components[${index}] must be an object`);
35
+ }
36
+ string(component.requirementId ?? component.requirement, `${label}.details.components[${index}].requirementId`);
37
+ if (!CHECK_STATUSES.includes(component.status) || !CHECK_EVIDENCE_KINDS.includes(component.evidenceKind)) {
38
+ throw checkError("E_CHECK_INVALID", `${label}.details.components[${index}] has invalid status or evidenceKind`);
39
+ }
40
+ }
41
+ if (value.status === "passed" && components.some((component) => (
42
+ component.status !== "passed" || component.evidenceKind !== "OBSERVED"
43
+ ))) {
44
+ throw contradiction(`${label} passed cannot contain an unverified, partial, blocked, or failed component`);
45
+ }
46
+ }
47
+
29
48
  export function createCheck(input = {}) {
30
49
  const check = {
31
50
  schemaVersion: CHECK_SCHEMA_VERSION,
@@ -78,6 +97,7 @@ export function assertCheck(value, label = "check") {
78
97
  if (value.status === "not-run" && value.evidenceKind !== "NOT_VERIFIED") {
79
98
  throw contradiction(`${label} not-run must use NOT_VERIFIED evidence`);
80
99
  }
100
+ assertCompoundStatus(value, label);
81
101
  return value;
82
102
  }
83
103
 
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import {
2
3
  ARTIFACT_PATHS,
3
4
  canonicalFingerprint,
@@ -17,6 +18,7 @@ import { readPersistedRoute } from "./route-artifact.js";
17
18
  import { createReceipt, validateReceipt } from "./receipt.js";
18
19
  import { readWorkState, writeWorkState } from "./work-state.js";
19
20
  import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
21
+ import { normalizeRequirements, classifyRequirement } from "./evidence-readiness.js";
20
22
 
21
23
  function artifactError(code, message, artifacts = []) {
22
24
  const error = new Error(message);
@@ -114,6 +116,7 @@ export async function prepareCompletion({ target, packageRoot }) {
114
116
  contractFingerprint: contract.fingerprint,
115
117
  routeFingerprint: route.fingerprint,
116
118
  stateFingerprint: canonicalFingerprint(state),
119
+ verificationCycle: state.verificationCycle ?? 1,
117
120
  status: existingValue.status ?? "in-progress",
118
121
  taskStatus: existingValue.taskStatus ?? "in-progress",
119
122
  verificationStatus: existingValue.verificationStatus ?? "not-verified",
@@ -210,6 +213,27 @@ export async function recordCheck({
210
213
  }
211
214
  await assertExecutionPrerequisites({ target, state, packageRoot });
212
215
 
216
+ const contract = await readContract(target, packageRoot);
217
+ const route = await readPersistedRoute(target, packageRoot);
218
+ const preflight = await evaluatePreflight({ target, packageRoot });
219
+ const requiredEvidence = await requiredEvidenceForTarget({
220
+ target,
221
+ contract,
222
+ route,
223
+ packageRoot,
224
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
225
+ });
226
+ const requested = normalizeRequirements(requiredEvidence).find((item) => (
227
+ item.id === requirement || item.text === requirement
228
+ ));
229
+ if (requested?.terminalOwned && status === "passed" && evidenceKind === "OBSERVED") {
230
+ throw artifactError(
231
+ "E_FUTURE_LIFECYCLE_EVIDENCE",
232
+ `Terminal-owned requirement cannot be recorded before its authoritative result: ${requested.text}`,
233
+ [ARTIFACT_PATHS.state, ARTIFACT_PATHS.events],
234
+ );
235
+ }
236
+
213
237
  const existingReceipt = await readCurrentReceipt(target, packageRoot);
214
238
  await validateReceipt(existingReceipt.value, packageRoot);
215
239
  const source = command?.trim() || `check:${id}`;
@@ -221,32 +245,28 @@ export async function recordCheck({
221
245
  status,
222
246
  evidenceKind,
223
247
  source,
248
+ timestamp: new Date().toISOString(),
224
249
  ...(exitCode === undefined ? {} : { exitCode }),
225
250
  details: {
226
251
  ...(command === undefined ? {} : { command }),
227
252
  ...(result === undefined ? {} : { result }),
228
253
  ...(details === undefined ? {} : details),
254
+ verificationCycle: state.verificationCycle ?? 1,
229
255
  },
230
256
  });
231
257
  const evidence = createEvidence({
232
258
  kind: evidenceKind,
233
259
  source,
234
260
  result: recordedResult,
235
- ...(details === undefined ? {} : { details: structuredClone(details) }),
261
+ verificationCycle: state.verificationCycle ?? 1,
262
+ details: {
263
+ ...(details === undefined ? {} : structuredClone(details)),
264
+ verificationCycle: state.verificationCycle ?? 1,
265
+ },
236
266
  });
237
267
 
238
268
  const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
239
269
  const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
240
- const contract = await readContract(target, packageRoot);
241
- const route = await readPersistedRoute(target, packageRoot);
242
- const preflight = await evaluatePreflight({ target, packageRoot });
243
- const requiredEvidence = await requiredEvidenceForTarget({
244
- target,
245
- contract,
246
- route,
247
- packageRoot,
248
- additionalEvidence: preflight.policy?.requiredEvidence ?? [],
249
- });
250
270
  assertCompletionRelationships({
251
271
  contract,
252
272
  route,
@@ -281,6 +301,7 @@ export async function recordCheck({
281
301
  evidence: evidenceList,
282
302
  evidenceCoverage: coverage,
283
303
  stateFingerprint: canonicalFingerprint(nextState),
304
+ verificationCycle: state.verificationCycle ?? 1,
284
305
  }, packageRoot);
285
306
 
286
307
  assertCompletionRelationships({
@@ -308,6 +329,7 @@ export async function recordCheck({
308
329
  requirement: check.requirement,
309
330
  status: check.status,
310
331
  evidenceKind: check.evidenceKind,
332
+ verificationCycle: state.verificationCycle ?? 1,
311
333
  },
312
334
  }, packageRoot);
313
335
  return {
@@ -319,3 +341,236 @@ export async function recordCheck({
319
341
  event,
320
342
  };
321
343
  }
344
+
345
+ export async function recordTerminalResult({
346
+ target,
347
+ packageRoot,
348
+ requirement,
349
+ type,
350
+ status,
351
+ source,
352
+ result,
353
+ details = {},
354
+ } = {}) {
355
+ if (!target || !requirement || !type || !status || !source || !result) {
356
+ throw artifactError("E_CHECK_INVALID", "record-terminal-result requires target, requirement, type, status, source, and result", [ARTIFACT_PATHS.state]);
357
+ }
358
+ if (!["PUBLICATION", "PRODUCTION_READINESS"].includes(type)) {
359
+ throw artifactError("E_FUTURE_TERMINAL_EVIDENCE", `record-terminal-result does not support type ${type}`, [ARTIFACT_PATHS.state]);
360
+ }
361
+ if (type === "PUBLICATION" && !["committed", "pushed", "published", "deployed"].includes(status)) {
362
+ throw artifactError("E_CHECK_INVALID", `Invalid publication status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
363
+ }
364
+ if (type === "PRODUCTION_READINESS" && !["ready", "blocked"].includes(status)) {
365
+ throw artifactError("E_CHECK_INVALID", `Invalid production readiness status for record-terminal-result: ${status}`, [ARTIFACT_PATHS.state]);
366
+ }
367
+
368
+ const state = await readWorkState(target, packageRoot);
369
+ if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a terminal result", [ARTIFACT_PATHS.state]);
370
+ if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
371
+ throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a terminal result in ${state.phase}`, [ARTIFACT_PATHS.state]);
372
+ }
373
+ await assertExecutionPrerequisites({ target, state, packageRoot });
374
+
375
+ const contract = await readContract(target, packageRoot);
376
+ const route = await readPersistedRoute(target, packageRoot);
377
+ const preflight = await evaluatePreflight({ target, packageRoot });
378
+ const requiredEvidence = await requiredEvidenceForTarget({
379
+ target,
380
+ contract,
381
+ route,
382
+ packageRoot,
383
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
384
+ });
385
+
386
+ const normalized = normalizeRequirements(requiredEvidence);
387
+ const requested = normalized.find((item) => (
388
+ item.id === requirement || item.text === requirement
389
+ ));
390
+
391
+ if (!requested) {
392
+ throw artifactError(
393
+ "E_TERMINAL_REQUIREMENT_UNKNOWN",
394
+ `Terminal results must reference an existing canonical terminal requirement: ${requirement}`,
395
+ [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt],
396
+ );
397
+ }
398
+
399
+ if (!requested.terminalOwned) {
400
+ throw artifactError(
401
+ "E_TERMINAL_REQUIREMENT_NOT_TERMINAL",
402
+ `record-terminal-result may only target terminal-owned requirements; found ${requested.type}: ${requested.text}`,
403
+ [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt],
404
+ );
405
+ }
406
+
407
+ if (requested.type !== type) {
408
+ throw artifactError(
409
+ "E_TERMINAL_REQUIREMENT_TYPE_MISMATCH",
410
+ `Requirement ${requested.id} is ${requested.type}, not ${type}.`,
411
+ [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt],
412
+ );
413
+ }
414
+
415
+ const existingReceipt = await readCurrentReceipt(target, packageRoot);
416
+ await validateReceipt(existingReceipt.value, packageRoot);
417
+
418
+ if (type === "PUBLICATION") {
419
+ const rank = {
420
+ "not-published": 0,
421
+ "local-only": 1,
422
+ "committed": 2,
423
+ "pushed": 3,
424
+ "published": 4,
425
+ };
426
+ const prev = existingReceipt.value.publicationStatus ?? "not-published";
427
+ if (prev in rank && status in rank && rank[status] < rank[prev]) {
428
+ throw artifactError(
429
+ "E_TERMINAL_STATUS_REGRESSION",
430
+ `Publication status cannot regress from ${prev} to ${status}.`,
431
+ [ARTIFACT_PATHS.receipt],
432
+ );
433
+ }
434
+ }
435
+
436
+ const cycle = state.verificationCycle ?? 1;
437
+
438
+ // Idempotency check:
439
+ const isIdentical = (existingReceipt.value.evidence ?? []).some((item) => (
440
+ item.kind === "OBSERVED"
441
+ && item.source === source.trim()
442
+ && item.result === result.trim()
443
+ && (item.verificationCycle ?? 1) === cycle
444
+ && item.details?.requirementId === requested.id
445
+ && item.details?.terminalType === type
446
+ && item.details?.terminalStatus === status
447
+ ));
448
+
449
+ if (isIdentical) {
450
+ const ledger = await validateEventLedger(target, packageRoot);
451
+ const matchingEvent = ledger.events.find((event) => (
452
+ event.taskId === state.taskId
453
+ && event.event === "TERMINAL_RESULT_RECORDED"
454
+ && event.details?.requirementId === requested.id
455
+ && event.details?.type === type
456
+ && event.details?.status === status
457
+ && event.details?.verificationCycle === cycle
458
+ && event.details?.source === source.trim()
459
+ && event.details?.result === result.trim()
460
+ ));
461
+
462
+ if (matchingEvent) {
463
+ return {
464
+ path: path.join(target, ARTIFACT_PATHS.receipt),
465
+ receipt: existingReceipt.value,
466
+ requirementId: requested.id,
467
+ type,
468
+ status,
469
+ idempotent: true,
470
+ repaired: false,
471
+ event: matchingEvent,
472
+ };
473
+ }
474
+
475
+ const repairedEvent = await appendProtocolEvent(target, {
476
+ taskId: state.taskId,
477
+ event: "TERMINAL_RESULT_RECORDED",
478
+ details: {
479
+ requirementId: requested.id,
480
+ type,
481
+ status,
482
+ verificationCycle: cycle,
483
+ source: source.trim(),
484
+ result: result.trim(),
485
+ },
486
+ }, packageRoot);
487
+
488
+ return {
489
+ path: path.join(target, ARTIFACT_PATHS.receipt),
490
+ receipt: existingReceipt.value,
491
+ requirementId: requested.id,
492
+ type,
493
+ status,
494
+ idempotent: true,
495
+ repaired: true,
496
+ event: repairedEvent,
497
+ };
498
+ }
499
+
500
+ const terminalEvidence = createEvidence({
501
+ kind: "OBSERVED",
502
+ source: source.trim(),
503
+ result: result.trim(),
504
+ verificationCycle: cycle,
505
+ details: {
506
+ requirementId: requested.id,
507
+ requirementText: requested.text,
508
+ terminalType: type,
509
+ terminalStatus: status,
510
+ ...(details === undefined ? {} : structuredClone(details)),
511
+ verificationCycle: cycle,
512
+ },
513
+ });
514
+
515
+ const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], terminalEvidence);
516
+ const nextState = {
517
+ ...state,
518
+ verificationEvidence: evidenceList,
519
+ lastUpdated: new Date().toISOString(),
520
+ };
521
+
522
+ const receiptUpdates = {
523
+ ...existingReceipt.value,
524
+ evidence: evidenceList,
525
+ verificationCycle: cycle,
526
+ };
527
+
528
+ if (type === "PUBLICATION") {
529
+ receiptUpdates.publicationStatus = status;
530
+ receiptUpdates.publication = {
531
+ ...(existingReceipt.value.publication ?? {}),
532
+ committed: status === "committed" || (existingReceipt.value.publication?.committed ?? false),
533
+ pushed: status === "pushed" || (existingReceipt.value.publication?.pushed ?? false),
534
+ deployed: status === "deployed" || (existingReceipt.value.publication?.deployed ?? false),
535
+ };
536
+ } else if (type === "PRODUCTION_READINESS") {
537
+ receiptUpdates.productionReadiness = status;
538
+ }
539
+
540
+ const nextReceipt = await createReceipt({
541
+ ...receiptUpdates,
542
+ stateFingerprint: canonicalFingerprint(nextState),
543
+ }, packageRoot);
544
+
545
+ await writeWorkState(target, nextState, { packageRoot });
546
+ await writeJsonArtifact(
547
+ target,
548
+ ARTIFACT_PATHS.receipt,
549
+ nextReceipt,
550
+ "execution-receipt",
551
+ packageRoot,
552
+ );
553
+
554
+ const event = await appendProtocolEvent(target, {
555
+ taskId: state.taskId,
556
+ event: "TERMINAL_RESULT_RECORDED",
557
+ details: {
558
+ requirementId: requested.id,
559
+ type,
560
+ status,
561
+ verificationCycle: cycle,
562
+ source: source.trim(),
563
+ result: result.trim(),
564
+ },
565
+ }, packageRoot);
566
+
567
+ return {
568
+ path: path.join(target, ARTIFACT_PATHS.receipt),
569
+ receipt: nextReceipt,
570
+ requirementId: requested.id,
571
+ type,
572
+ status,
573
+ evidence: terminalEvidence,
574
+ event,
575
+ };
576
+ }
@@ -0,0 +1,15 @@
1
+ export const RECOVERABLE_COMPLETION_EVIDENCE_CODES = Object.freeze([
2
+ "E_EVIDENCE_REQUIRED",
3
+ "E_EVIDENCE_PARTIAL",
4
+ "E_EVIDENCE_INVALID",
5
+ "E_EVIDENCE_KIND_INVALID",
6
+ "E_EVIDENCE_COVERAGE_PARTIAL",
7
+ "E_EVIDENCE_COVERAGE_INVALID",
8
+ "E_VERIFICATION_CHECK_REQUIRED",
9
+ "E_CHECK_REQUIRED",
10
+ "E_CHECK_INVALID",
11
+ ]);
12
+
13
+ export function isRecoverableCompletionEvidenceCode(code) {
14
+ return typeof code === "string" && RECOVERABLE_COMPLETION_EVIDENCE_CODES.includes(code);
15
+ }
@@ -1,10 +1,11 @@
1
1
  import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
2
- import { assertCheckList, requiredChecksSatisfiedForRequirements } from "./checks.js";
2
+ import { assertCheckList } from "./checks.js";
3
3
  import { assertCoverageList, coverageForRequirements } from "./coverage.js";
4
4
  import { assertEvidenceList } from "./evidence.js";
5
+ import { evaluateRequiredEvidence } from "./evidence-readiness.js";
5
6
 
6
- function issue(code, message, artifacts = []) {
7
- return { code, message, artifacts };
7
+ function issue(code, message, artifacts = [], details = {}) {
8
+ return { code, message, artifacts, ...details };
8
9
  }
9
10
 
10
11
  function sameValue(left, right) {
@@ -86,8 +87,15 @@ export function completionRelationshipErrors({
86
87
  addAssertion(errors, () => assertCheckList(receipt.checks, "receipt.checks"), "E_CHECK_INVALID", [ARTIFACT_PATHS.receipt]);
87
88
  addAssertion(errors, () => assertEvidenceList(receipt.evidence ?? [], "receipt.evidence"), "E_EVIDENCE_INVALID", [ARTIFACT_PATHS.receipt]);
88
89
  if (requireRequiredChecks) {
89
- for (const error of requiredChecksSatisfiedForRequirements(receipt.checks, requiredEvidence)) {
90
- errors.push(issue(error.code, error.message, [ARTIFACT_PATHS.receipt]));
90
+ const readiness = evaluateRequiredEvidence({ requirements: requiredEvidence, checks: receipt.checks });
91
+ for (const requirement of readiness.missing) {
92
+ errors.push(issue("E_EVIDENCE_REQUIRED", `Required check is missing: ${requirement.text}`, [ARTIFACT_PATHS.receipt], { requirementId: requirement.id }));
93
+ }
94
+ for (const requirement of readiness.partial) {
95
+ errors.push(issue("E_EVIDENCE_PARTIAL", `Required evidence is partial: ${requirement.text}`, [ARTIFACT_PATHS.receipt], { requirementId: requirement.id }));
96
+ }
97
+ for (const requirement of readiness.invalid) {
98
+ errors.push(issue(requirement.reasonCode ?? "E_EVIDENCE_INVALID", `Required evidence is invalid: ${requirement.text}`, [ARTIFACT_PATHS.receipt], { requirementId: requirement.id }));
91
99
  }
92
100
  }
93
101
  }
@@ -111,10 +119,16 @@ export function completionRelationshipErrors({
111
119
  }
112
120
 
113
121
  if (state && receipt) {
114
- if (requireReceiptStateFingerprint && receipt.stateFingerprint === undefined) {
115
- errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
116
- } else if (receipt.stateFingerprint !== undefined && receipt.stateFingerprint !== canonicalFingerprint(state)) {
117
- errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
122
+ if (state.verificationCycle !== undefined && receipt.verificationCycle !== undefined
123
+ && state.verificationCycle !== receipt.verificationCycle) {
124
+ errors.push(issue("E_RECEIPT_CYCLE_MISMATCH", "Work state and execution receipt verification cycles diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
125
+ }
126
+ if (requireReceiptStateFingerprint) {
127
+ if (receipt.stateFingerprint === undefined) {
128
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt requires the current work-state fingerprint", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
129
+ } else if (receipt.stateFingerprint !== canonicalFingerprint(state)) {
130
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Execution receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
131
+ }
118
132
  }
119
133
  if (!sameValue(state.checks, receipt.checks)) {
120
134
  errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Work state and execution receipt checks diverge", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));