@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.
- package/.cursor/rules/project-loop.mdc +16 -18
- package/.github/copilot-instructions.md +16 -19
- package/AGENTS.md +18 -24
- package/AGENT_COMPATIBILITY.md +4 -211
- package/CLAUDE.md +16 -18
- package/LOOP_ENGINEERING.md +73 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +4 -0
- package/PROTOCOL_INTEGRATION.md +171 -0
- package/QUALITY_SCORECARD.md +1 -1
- package/README.md +123 -103
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +4 -0
- package/package.json +5 -4
- package/schemas/current-contract.schema.json +48 -2
- package/schemas/evidence.schema.json +1 -0
- package/schemas/execution-receipt.schema.json +2 -0
- package/schemas/work-state.schema.json +44 -1
- package/src/cli.js +48 -4
- package/src/commands/record-terminal-result.js +18 -0
- package/src/commands/validate-protocol.js +14 -2
- package/src/core/checks.js +20 -0
- package/src/core/completion-artifacts.js +266 -11
- package/src/core/completion-recovery.js +15 -0
- package/src/core/completion-relationships.js +23 -9
- package/src/core/completion.js +130 -2
- package/src/core/contract.js +82 -4
- package/src/core/coverage.js +17 -12
- package/src/core/discovery-surfaces.js +22 -0
- package/src/core/events.js +183 -2
- package/src/core/evidence-readiness.js +333 -0
- package/src/core/evidence.js +22 -4
- package/src/core/execution-prerequisites.js +6 -1
- package/src/core/inspect.js +23 -12
- package/src/core/native-adapters.js +30 -2
- package/src/core/next-action.js +103 -27
- package/src/core/phase.js +68 -5
- package/src/core/protocol.js +28 -1
- package/src/core/receipt.js +5 -0
- package/src/core/schema-validation.js +28 -6
- package/src/core/templates.js +1 -0
- package/src/core/work-state.js +27 -0
- package/conformance/README.md +0 -148
- package/conformance/backend-auth/EXPECTED_ROUTE.json +0 -7
- package/conformance/backend-auth/REQUEST.md +0 -4
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/backend-auth/REQUIRED_GATES.json +0 -3
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/blind-premium-website/REQUEST.md +0 -6
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +0 -14
- package/conformance/blind-premium-website/REQUIRED_GATES.json +0 -3
- package/conformance/complete-website/EXPECTED_ROUTE.json +0 -7
- package/conformance/complete-website/REQUEST.md +0 -6
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/complete-website/REQUIRED_GATES.json +0 -3
- package/conformance/docs-only/EXPECTED_ROUTE.json +0 -7
- package/conformance/docs-only/REQUEST.md +0 -4
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/docs-only/REQUIRED_GATES.json +0 -3
- package/conformance/runs/2026-08-11-codex-first-live.md +0 -98
- package/conformance/runs/2026-08-11-codex-second-live.md +0 -87
- package/conformance/runs/2026-08-13-codex-fifth-live.md +0 -386
- package/conformance/runs/2026-08-13-codex-fourth-live.md +0 -309
- package/conformance/runs/2026-08-13-codex-sixth-live.md +0 -412
- package/conformance/simple-bug/EXPECTED_ROUTE.json +0 -7
- package/conformance/simple-bug/REQUEST.md +0 -4
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +0 -3
- package/conformance/simple-bug/REQUIRED_GATES.json +0 -3
- 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
|
-
|
|
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
|
}
|
package/src/core/checks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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 (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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]));
|