@cassiomc1/forgeloop 0.1.1 → 0.1.2

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 (99) hide show
  1. package/.cursor/rules/project-loop.mdc +7 -1
  2. package/.github/copilot-instructions.md +6 -0
  3. package/AGENTS.md +9 -1
  4. package/AGENT_COMPATIBILITY.md +14 -1
  5. package/CLAUDE.md +7 -1
  6. package/ENG/accessibility-eng.md +5 -0
  7. package/ENG/clean-code-eng.md +1 -0
  8. package/ENG/design-code-eng.md +5 -0
  9. package/ENG/games-code-design-web-eng.md +6 -0
  10. package/ENG/perf-code-eng.md +3 -0
  11. package/ENG/premium-sites-studio-eng.md +9 -0
  12. package/ENG/sec-code-eng.md +5 -0
  13. package/ENG/test-code-eng.md +3 -0
  14. package/GUIDE_ROUTER.md +16 -0
  15. package/LOOP_ENGINEERING.md +139 -4
  16. package/ORCHESTRATOR_INTEGRATION.md +15 -0
  17. package/QUALITY_SCORECARD.md +4 -0
  18. package/README.md +43 -1
  19. package/THREAT_MODEL.md +6 -0
  20. package/conformance/README.md +42 -0
  21. package/conformance/backend-auth/EXPECTED_ROUTE.json +7 -0
  22. package/conformance/backend-auth/REQUEST.md +4 -0
  23. package/conformance/backend-auth/REQUIRED_EVIDENCE.json +3 -0
  24. package/conformance/backend-auth/REQUIRED_GATES.json +3 -0
  25. package/conformance/blind-premium-website/EXPECTED_ROUTE.json +7 -0
  26. package/conformance/blind-premium-website/REQUEST.md +6 -0
  27. package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +14 -0
  28. package/conformance/blind-premium-website/REQUIRED_GATES.json +3 -0
  29. package/conformance/complete-website/EXPECTED_ROUTE.json +7 -0
  30. package/conformance/complete-website/REQUEST.md +6 -0
  31. package/conformance/complete-website/REQUIRED_EVIDENCE.json +3 -0
  32. package/conformance/complete-website/REQUIRED_GATES.json +3 -0
  33. package/conformance/docs-only/EXPECTED_ROUTE.json +7 -0
  34. package/conformance/docs-only/REQUEST.md +4 -0
  35. package/conformance/docs-only/REQUIRED_EVIDENCE.json +3 -0
  36. package/conformance/docs-only/REQUIRED_GATES.json +3 -0
  37. package/conformance/runs/2026-08-11-codex-first-live.md +98 -0
  38. package/conformance/runs/2026-08-11-codex-second-live.md +87 -0
  39. package/conformance/simple-bug/EXPECTED_ROUTE.json +7 -0
  40. package/conformance/simple-bug/REQUEST.md +4 -0
  41. package/conformance/simple-bug/REQUIRED_EVIDENCE.json +3 -0
  42. package/conformance/simple-bug/REQUIRED_GATES.json +3 -0
  43. package/package.json +4 -3
  44. package/schemas/activation.schema.json +15 -0
  45. package/schemas/check.schema.json +23 -0
  46. package/schemas/config.schema.json +16 -0
  47. package/schemas/current-contract.schema.json +50 -0
  48. package/schemas/event.schema.json +20 -0
  49. package/schemas/evidence-coverage.schema.json +17 -0
  50. package/schemas/execution-receipt.schema.json +10 -0
  51. package/schemas/gate.schema.json +32 -0
  52. package/schemas/policy.schema.json +19 -0
  53. package/schemas/preflight.schema.json +23 -0
  54. package/schemas/routing-result.schema.json +1 -0
  55. package/schemas/source-registry.schema.json +29 -0
  56. package/schemas/task-bundle.schema.json +14 -0
  57. package/schemas/work-state.schema.json +7 -1
  58. package/src/cli.js +230 -9
  59. package/src/commands/activate.js +11 -0
  60. package/src/commands/advance.js +12 -0
  61. package/src/commands/audit.js +12 -0
  62. package/src/commands/bundle.js +10 -0
  63. package/src/commands/complete.js +18 -0
  64. package/src/commands/policy.js +19 -0
  65. package/src/commands/preflight.js +12 -0
  66. package/src/commands/prepare-completion.js +18 -0
  67. package/src/commands/record-check.js +20 -0
  68. package/src/commands/report.js +10 -0
  69. package/src/commands/route.js +14 -2
  70. package/src/commands/validate-receipt.js +7 -6
  71. package/src/core/activation.js +16 -0
  72. package/src/core/artifacts.js +132 -0
  73. package/src/core/audit.js +88 -0
  74. package/src/core/bundles.js +120 -0
  75. package/src/core/checks.js +115 -0
  76. package/src/core/completion-artifacts.js +281 -0
  77. package/src/core/completion.js +295 -0
  78. package/src/core/config.js +35 -0
  79. package/src/core/conformance.js +7 -0
  80. package/src/core/contract.js +141 -0
  81. package/src/core/coverage.js +89 -0
  82. package/src/core/events.js +124 -0
  83. package/src/core/gate-artifact.js +54 -0
  84. package/src/core/gates.js +55 -0
  85. package/src/core/guide-metadata.js +62 -0
  86. package/src/core/phase.js +88 -0
  87. package/src/core/policies.js +69 -0
  88. package/src/core/preflight.js +246 -0
  89. package/src/core/profile.js +48 -0
  90. package/src/core/protocol.js +34 -0
  91. package/src/core/receipt.js +40 -1
  92. package/src/core/report.js +49 -0
  93. package/src/core/repository.js +18 -0
  94. package/src/core/route-artifact.js +33 -0
  95. package/src/core/router.js +16 -8
  96. package/src/core/schema-validation.js +11 -0
  97. package/src/core/sources.js +86 -0
  98. package/src/core/templates.js +11 -0
  99. package/src/core/work-state.js +41 -13
@@ -0,0 +1,281 @@
1
+ import {
2
+ ARTIFACT_PATHS,
3
+ canonicalFingerprint,
4
+ readJsonArtifact,
5
+ writeJsonArtifact,
6
+ } from "./artifacts.js";
7
+ import { readContract } from "./contract.js";
8
+ import { appendProtocolEvent } from "./events.js";
9
+ import { completionEvidenceForGuides } from "./guide-metadata.js";
10
+ import { createCheck } from "./checks.js";
11
+ import { createEvidence } from "./evidence.js";
12
+ import { coverageForRequirements } from "./coverage.js";
13
+ import { evaluatePreflight } from "./preflight.js";
14
+ import { currentChangedPaths } from "./repository.js";
15
+ import { readPersistedRoute } from "./route-artifact.js";
16
+ import { createReceipt, validateReceipt } from "./receipt.js";
17
+ import { readWorkState, writeWorkState } from "./work-state.js";
18
+
19
+ function artifactError(code, message, artifacts = []) {
20
+ const error = new Error(message);
21
+ error.code = code;
22
+ error.artifacts = artifacts;
23
+ return error;
24
+ }
25
+
26
+ function requiredString(value, label) {
27
+ if (typeof value !== "string" || value.trim() === "") {
28
+ throw artifactError("E_CHECK_INVALID", `${label} must be a non-empty string`);
29
+ }
30
+ return value;
31
+ }
32
+
33
+ async function readCurrentReceipt(target, packageRoot) {
34
+ try {
35
+ return await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
36
+ } catch (error) {
37
+ if (error.code === "ARTIFACT_MISSING") {
38
+ throw artifactError(
39
+ "E_RECEIPT_MISSING",
40
+ "Execution receipt is missing; run forgeloop prepare-completion first",
41
+ [ARTIFACT_PATHS.receipt],
42
+ );
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ async function readOptionalConfig(target, packageRoot) {
49
+ try {
50
+ return (await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot)).value;
51
+ } catch (error) {
52
+ if (error.code === "ARTIFACT_MISSING") return {};
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ export async function requiredEvidenceForTarget({ target, contract, route, packageRoot, additionalEvidence = [] }) {
58
+ const config = await readOptionalConfig(target, packageRoot);
59
+ const guideEvidence = await completionEvidenceForGuides(route.value.guides, packageRoot);
60
+ return [...new Set([
61
+ ...(contract.value.successCriteria ?? []),
62
+ ...guideEvidence,
63
+ ...(config.requiredEvidence ?? []),
64
+ ...additionalEvidence,
65
+ ])].sort();
66
+ }
67
+
68
+ export async function prepareCompletion({ target, packageRoot }) {
69
+ const contract = await readContract(target, packageRoot);
70
+ const route = await readPersistedRoute(target, packageRoot);
71
+ const state = await readWorkState(target, packageRoot);
72
+ if (!state) {
73
+ throw artifactError("E_STATE_MISSING", "Work state is required before preparing completion", [ARTIFACT_PATHS.state]);
74
+ }
75
+
76
+ let existing = null;
77
+ try {
78
+ existing = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
79
+ await validateReceipt(existing.value, packageRoot);
80
+ } catch (error) {
81
+ if (error.code !== "ARTIFACT_MISSING") throw error;
82
+ }
83
+
84
+ const preflight = await evaluatePreflight({ target, packageRoot });
85
+ const requiredEvidence = await requiredEvidenceForTarget({
86
+ target,
87
+ contract,
88
+ route,
89
+ packageRoot,
90
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
91
+ });
92
+ const existingValue = existing?.value ?? {};
93
+ const changedPaths = existing
94
+ ? [...(existingValue.changedPaths ?? [])]
95
+ : (await currentChangedPaths(target) ?? []);
96
+ const checks = [...(existingValue.checks ?? [])];
97
+ const receipt = await createReceipt({
98
+ ...existingValue,
99
+ taskId: contract.value.taskId,
100
+ contractFingerprint: contract.fingerprint,
101
+ routeFingerprint: route.fingerprint,
102
+ ...(Object.hasOwn(existingValue, "stateFingerprint")
103
+ ? { stateFingerprint: canonicalFingerprint(state) }
104
+ : {}),
105
+ status: existingValue.status ?? "in-progress",
106
+ taskStatus: existingValue.taskStatus ?? "in-progress",
107
+ verificationStatus: existingValue.verificationStatus ?? "not-verified",
108
+ publicationStatus: existingValue.publicationStatus ?? "local-only",
109
+ productionReadiness: existingValue.productionReadiness ?? "not-verified",
110
+ selectedGuides: [...route.value.guides],
111
+ changedPaths,
112
+ checks,
113
+ evidence: [...(existingValue.evidence ?? [])],
114
+ evidenceCoverage: coverageForRequirements(requiredEvidence, checks),
115
+ review: existingValue.review ?? { status: "not-run", independent: false },
116
+ limitations: [...(existingValue.limitations ?? [])],
117
+ publication: existingValue.publication ?? {
118
+ committed: false,
119
+ pushed: false,
120
+ pullRequest: null,
121
+ deployed: false,
122
+ },
123
+ }, packageRoot);
124
+ const written = await writeJsonArtifact(
125
+ target,
126
+ ARTIFACT_PATHS.receipt,
127
+ receipt,
128
+ "execution-receipt",
129
+ packageRoot,
130
+ );
131
+ return {
132
+ path: written.path,
133
+ receipt: written.value,
134
+ requiredEvidence,
135
+ changedPaths,
136
+ };
137
+ }
138
+
139
+ function mergeByCheckId(checks, nextCheck) {
140
+ const next = [...checks];
141
+ const index = next.findIndex((item) => item?.schemaVersion === 1 && item.id === nextCheck.id);
142
+ if (index >= 0) next[index] = nextCheck;
143
+ else next.push(nextCheck);
144
+ return next;
145
+ }
146
+
147
+ function appendUniqueEvidence(evidence, nextEvidence) {
148
+ const exists = evidence.some((item) => item.kind === nextEvidence.kind
149
+ && item.source === nextEvidence.source
150
+ && item.result === nextEvidence.result
151
+ && JSON.stringify(item.details ?? null) === JSON.stringify(nextEvidence.details ?? null));
152
+ return exists ? [...evidence] : [...evidence, nextEvidence];
153
+ }
154
+
155
+ export async function recordCheck({
156
+ target,
157
+ packageRoot,
158
+ id,
159
+ kind = "command",
160
+ requirement,
161
+ status,
162
+ evidenceKind,
163
+ command,
164
+ result,
165
+ exitCode,
166
+ details,
167
+ }) {
168
+ requiredString(id, "check id");
169
+ requiredString(kind, "check kind");
170
+ requiredString(requirement, "check requirement");
171
+ requiredString(status, "check status");
172
+ requiredString(evidenceKind, "evidence kind");
173
+ if (command !== undefined && typeof command !== "string") {
174
+ throw artifactError("E_CHECK_INVALID", "command must be a string when supplied");
175
+ }
176
+ if (result !== undefined && typeof result !== "string") {
177
+ throw artifactError("E_CHECK_INVALID", "result must be a string when supplied");
178
+ }
179
+ if (details !== undefined && (!details || typeof details !== "object" || Array.isArray(details))) {
180
+ throw artifactError("E_CHECK_INVALID", "check details must be a JSON object");
181
+ }
182
+ if ((typeof command !== "string" || command.trim() === "")
183
+ && (typeof result !== "string" || result.trim() === "")) {
184
+ throw artifactError("E_CHECK_INVALID", "record-check requires --command or --result");
185
+ }
186
+
187
+ const state = await readWorkState(target, packageRoot);
188
+ if (!state) throw artifactError("E_STATE_MISSING", "Work state is required before recording a check", [ARTIFACT_PATHS.state]);
189
+ if (["COMPLETE", "BLOCKED"].includes(state.phase)) {
190
+ throw artifactError("E_PHASE_TRANSITION_INVALID", `Cannot record a check in ${state.phase}`, [ARTIFACT_PATHS.state]);
191
+ }
192
+ if (state.phase !== "VERIFYING") {
193
+ throw artifactError(
194
+ "E_PHASE_PREREQUISITE_MISSING",
195
+ `record-check requires VERIFYING before review; found ${state.phase}`,
196
+ [ARTIFACT_PATHS.state],
197
+ );
198
+ }
199
+
200
+ const existingReceipt = await readCurrentReceipt(target, packageRoot);
201
+ await validateReceipt(existingReceipt.value, packageRoot);
202
+ const source = command?.trim() || `check:${id}`;
203
+ const recordedResult = result?.trim() || `recorded command: ${command.trim()}`;
204
+ const check = createCheck({
205
+ id,
206
+ kind,
207
+ requirement,
208
+ status,
209
+ evidenceKind,
210
+ source,
211
+ ...(exitCode === undefined ? {} : { exitCode }),
212
+ details: {
213
+ ...(command === undefined ? {} : { command }),
214
+ ...(result === undefined ? {} : { result }),
215
+ ...(details === undefined ? {} : details),
216
+ },
217
+ });
218
+ const evidence = createEvidence({
219
+ kind: evidenceKind,
220
+ source,
221
+ result: recordedResult,
222
+ ...(details === undefined ? {} : { details: structuredClone(details) }),
223
+ });
224
+
225
+ const checks = mergeByCheckId(existingReceipt.value.checks ?? [], check);
226
+ const evidenceList = appendUniqueEvidence(existingReceipt.value.evidence ?? [], evidence);
227
+ const contract = await readContract(target, packageRoot);
228
+ const route = await readPersistedRoute(target, packageRoot);
229
+ const preflight = await evaluatePreflight({ target, packageRoot });
230
+ const requiredEvidence = await requiredEvidenceForTarget({
231
+ target,
232
+ contract,
233
+ route,
234
+ packageRoot,
235
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
236
+ });
237
+ const coverage = coverageForRequirements(requiredEvidence, checks);
238
+ const nextState = {
239
+ ...state,
240
+ checks,
241
+ verificationEvidence: appendUniqueEvidence(state.verificationEvidence ?? [], evidence),
242
+ evidenceCoverage: coverage,
243
+ lastUpdated: new Date().toISOString(),
244
+ };
245
+ const nextReceipt = await createReceipt({
246
+ ...existingReceipt.value,
247
+ checks,
248
+ evidence: evidenceList,
249
+ evidenceCoverage: coverage,
250
+ ...(Object.hasOwn(existingReceipt.value, "stateFingerprint")
251
+ ? { stateFingerprint: canonicalFingerprint(nextState) }
252
+ : {}),
253
+ }, packageRoot);
254
+
255
+ await writeWorkState(target, nextState, { packageRoot });
256
+ const written = await writeJsonArtifact(
257
+ target,
258
+ ARTIFACT_PATHS.receipt,
259
+ nextReceipt,
260
+ "execution-receipt",
261
+ packageRoot,
262
+ );
263
+ const event = await appendProtocolEvent(target, {
264
+ taskId: state.taskId,
265
+ event: "VERIFICATION_RECORDED",
266
+ details: {
267
+ checkId: check.id,
268
+ requirement: check.requirement,
269
+ status: check.status,
270
+ evidenceKind: check.evidenceKind,
271
+ },
272
+ }, packageRoot);
273
+ return {
274
+ path: written.path,
275
+ receipt: written.value,
276
+ check,
277
+ evidence,
278
+ coverage,
279
+ event,
280
+ };
281
+ }
@@ -0,0 +1,295 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact } from "./artifacts.js";
2
+ import { requiredEvidenceForTarget } from "./completion-artifacts.js";
3
+ import { appendProtocolEvent, validateEventLedger } from "./events.js";
4
+ import { evaluatePreflight } from "./preflight.js";
5
+ import { readContract } from "./contract.js";
6
+ import { readPersistedRoute } from "./route-artifact.js";
7
+ import { readWorkState, writeWorkState, classifyLoadedWorkState } from "./work-state.js";
8
+ import { validateReceipt } from "./receipt.js";
9
+ import { assertCheckList, requiredChecksSatisfied } from "./checks.js";
10
+ import { assertCoverageList, coverageForRequirements } from "./coverage.js";
11
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
12
+
13
+ function issue(code, message, artifacts = [], details = {}) {
14
+ return { code, message, artifacts, ...details };
15
+ }
16
+
17
+ function repairNext(error) {
18
+ switch (error.code) {
19
+ case "E_RECEIPT_MISSING":
20
+ return "Run forgeloop prepare-completion after recording verification evidence.";
21
+ case "E_RECEIPT_INVALID":
22
+ case "E_RECEIPT_CONTRACT_MISMATCH":
23
+ case "E_RECEIPT_ROUTE_MISMATCH":
24
+ case "E_RECEIPT_STATE_MISMATCH":
25
+ return "Run forgeloop prepare-completion, inspect the receipt, and validate it before retrying completion.";
26
+ case "E_PHASE_PREREQUISITE_MISSING":
27
+ return "Advance from EXECUTING through VERIFYING and REVIEWING, resolving the named prerequisite before completion.";
28
+ case "E_PHASE_CHRONOLOGY_INVALID":
29
+ return "Record the missing lifecycle event through the normal phase transition or forgeloop record-check path.";
30
+ case "E_EVIDENCE_REQUIRED":
31
+ case "E_EVIDENCE_COVERAGE_INVALID":
32
+ case "E_EVIDENCE_COVERAGE_PARTIAL":
33
+ case "E_EVIDENCE_KIND_INVALID":
34
+ case "E_CHECK_INVALID":
35
+ case "E_CHECK_STATUS_CONTRADICTION":
36
+ return "Run forgeloop record-check with compatible observed evidence for the named requirement.";
37
+ case "E_GATE_UNVERIFIED":
38
+ case "E_GATE_STALE":
39
+ return "Satisfy or refresh the named gate, then rerun forgeloop preflight.";
40
+ case "E_PROFILE_UNVERIFIED":
41
+ return "Use Standard mode for a fresh target, or verify PROJECT_PROFILE.md before Strict completion.";
42
+ default:
43
+ return "Resolve this validator finding in the named artifact before retrying completion.";
44
+ }
45
+ }
46
+
47
+ function withRepairGuidance(error) {
48
+ const normalized = {
49
+ ...error,
50
+ code: error.code ?? "E_COMPLETION_REJECTED",
51
+ message: error.message ?? String(error),
52
+ artifacts: error.artifacts ?? [],
53
+ };
54
+ if (!normalized.next) normalized.next = repairNext(normalized);
55
+ return normalized;
56
+ }
57
+
58
+ function sortIssues(errors) {
59
+ const unique = [...new Map(errors.map((rawError) => {
60
+ const error = withRepairGuidance(rawError);
61
+ return [
62
+ `${error.code}\0${(error.artifacts ?? []).join("\0")}\0${error.message}`,
63
+ error,
64
+ ];
65
+ })).values()];
66
+ return unique.sort((left, right) => left.code.localeCompare(right.code)
67
+ || left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
68
+ || left.message.localeCompare(right.message));
69
+ }
70
+
71
+ async function loadRequired(loader, code, message, artifacts, errors) {
72
+ try {
73
+ return await loader();
74
+ } catch (error) {
75
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? code : `${code.replace(/_MISSING$/, "")}_INVALID`, `${message}: ${error.message}`, artifacts));
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function publicationStatus(receipt) {
81
+ if (receipt.publicationStatus) return receipt.publicationStatus;
82
+ if (receipt.publication?.deployed) return "deployed";
83
+ if (receipt.publication?.pushed) return "pushed";
84
+ if (receipt.publication?.committed) return "committed";
85
+ return receipt.changedPaths?.length > 0 ? "local-only" : "not-published";
86
+ }
87
+
88
+ async function validateLedger(target, taskId, state, errors, packageRoot) {
89
+ await assertSafePath(target, ARTIFACT_PATHS.events);
90
+ const eventsPath = ensureWithin(target, ARTIFACT_PATHS.events);
91
+ if (!(await fileExists(eventsPath))) {
92
+ errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "Protocol event ledger is required before completion", [ARTIFACT_PATHS.events]));
93
+ return { valid: false, events: [], errors: [] };
94
+ }
95
+ const ledger = await validateEventLedger(target, packageRoot);
96
+ for (const error of ledger.errors) errors.push({ ...error, artifacts: [ARTIFACT_PATHS.events] });
97
+ const requiredEvents = ["CONTRACT_VALIDATED", "ROUTE_VALIDATED", "PREFLIGHT_READY", "EXECUTION_STARTED", "VERIFICATION_RECORDED"];
98
+ const observed = new Set(ledger.events.filter((event) => event.taskId === taskId).map((event) => event.event));
99
+ for (const event of requiredEvents) {
100
+ if (!observed.has(event)) {
101
+ errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", `Required protocol event is missing: ${event}`, [ARTIFACT_PATHS.events], { event }));
102
+ }
103
+ }
104
+ if (ledger.events.some((event) => event.taskId !== taskId)) {
105
+ errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "Protocol events must belong to the current task", [ARTIFACT_PATHS.events]));
106
+ }
107
+ if (state?.phase === "COMPLETE" && !observed.has("COMPLETION_VALIDATED")) {
108
+ errors.push(issue("E_PHASE_CHRONOLOGY_INVALID", "COMPLETE state requires COMPLETION_VALIDATED", [ARTIFACT_PATHS.events]));
109
+ }
110
+ return ledger;
111
+ }
112
+
113
+ export async function evaluateCompletion({ target, packageRoot, strict = false } = {}) {
114
+ const errors = [];
115
+ const preflight = await evaluatePreflight({ target, packageRoot, strict });
116
+ errors.push(...preflight.errors);
117
+ const contract = await loadRequired(
118
+ () => readContract(target, packageRoot),
119
+ "E_CONTRACT_MISSING",
120
+ "Current contract is not available",
121
+ [ARTIFACT_PATHS.contract],
122
+ errors,
123
+ );
124
+ const route = await loadRequired(
125
+ () => readPersistedRoute(target, packageRoot),
126
+ "E_ROUTE_MISSING",
127
+ "Persisted routing result is not available",
128
+ [ARTIFACT_PATHS.route],
129
+ errors,
130
+ );
131
+ const state = await loadRequired(
132
+ () => readWorkState(target, packageRoot).then((value) => {
133
+ if (!value) {
134
+ const error = new Error("Work state is missing");
135
+ error.code = "ARTIFACT_MISSING";
136
+ throw error;
137
+ }
138
+ return value;
139
+ }),
140
+ "E_STATE_MISSING",
141
+ "Work state is not available",
142
+ [ARTIFACT_PATHS.state],
143
+ errors,
144
+ );
145
+ const receipt = await loadRequired(
146
+ () => readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot),
147
+ "E_RECEIPT_MISSING",
148
+ "Execution receipt is not available",
149
+ [ARTIFACT_PATHS.receipt],
150
+ errors,
151
+ );
152
+
153
+ if (receipt) {
154
+ try {
155
+ await validateReceipt(receipt.value, packageRoot);
156
+ } catch (error) {
157
+ errors.push(issue(error.code ?? "E_RECEIPT_INVALID", `Execution receipt is invalid: ${error.message}`, [ARTIFACT_PATHS.receipt]));
158
+ }
159
+ }
160
+
161
+ if (contract && route && route.value.contractFingerprint !== undefined
162
+ && route.value.contractFingerprint !== contract.fingerprint) {
163
+ errors.push(issue("E_ROUTE_STALE", "Routing result does not match the current contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract]));
164
+ }
165
+ if (contract && state && state.contractFingerprint !== contract.fingerprint) {
166
+ errors.push(issue("E_CONTRACT_STALE", "Work state does not match the current contract", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.contract]));
167
+ }
168
+ if (route && state && JSON.stringify(route.value.guides) !== JSON.stringify(state.selectedGuides)) {
169
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Work state guides do not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
170
+ }
171
+ if (route && receipt && JSON.stringify(route.value.guides) !== JSON.stringify(receipt.value.selectedGuides)) {
172
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "Receipt guides do not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
173
+ }
174
+ if (contract && receipt && receipt.value.contractFingerprint !== contract.fingerprint) {
175
+ errors.push(issue("E_RECEIPT_CONTRACT_MISMATCH", "Receipt does not match the current contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.receipt]));
176
+ }
177
+ if (route && receipt && receipt.value.routeFingerprint !== undefined && receipt.value.routeFingerprint !== route.fingerprint) {
178
+ errors.push(issue("E_RECEIPT_ROUTE_MISMATCH", "Receipt does not match the persisted route", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.receipt]));
179
+ }
180
+ if (state && receipt && receipt.value.stateFingerprint !== undefined && receipt.value.stateFingerprint !== canonicalFingerprint(state)) {
181
+ errors.push(issue("E_RECEIPT_STATE_MISMATCH", "Receipt does not match the recorded work state", [ARTIFACT_PATHS.state, ARTIFACT_PATHS.receipt]));
182
+ }
183
+ if (state && !["REVIEWING", "COMPLETE"].includes(state.phase)) {
184
+ errors.push(issue("E_PHASE_PREREQUISITE_MISSING", `Completion requires REVIEWING or COMPLETE state, found ${state.phase}`, [ARTIFACT_PATHS.state]));
185
+ }
186
+
187
+ let coverage = [];
188
+ if (receipt && contract && route) {
189
+ const requiredEvidence = await requiredEvidenceForTarget({
190
+ target,
191
+ contract,
192
+ route,
193
+ packageRoot,
194
+ additionalEvidence: preflight?.policy?.requiredEvidence ?? [],
195
+ });
196
+ coverage = receipt.value.evidenceCoverage
197
+ ? receipt.value.evidenceCoverage
198
+ : coverageForRequirements(requiredEvidence, receipt.value.checks);
199
+ try {
200
+ assertCoverageList(coverage);
201
+ } catch (error) {
202
+ errors.push(issue(error.code ?? "E_EVIDENCE_COVERAGE_PARTIAL", error.message, [ARTIFACT_PATHS.receipt]));
203
+ }
204
+ const byRequirement = new Map(coverage.map((item) => [item.requirement, item]));
205
+ for (const requirement of requiredEvidence) {
206
+ const item = byRequirement.get(requirement);
207
+ if (!item) {
208
+ errors.push(issue("E_EVIDENCE_REQUIRED", `Evidence coverage is missing: ${requirement}`, [ARTIFACT_PATHS.receipt], { requirement }));
209
+ } else if (item.status !== "COVERED") {
210
+ errors.push(issue(
211
+ item.status === "BLOCKED" ? "E_EVIDENCE_COVERAGE_PARTIAL" : "E_EVIDENCE_COVERAGE_PARTIAL",
212
+ `Evidence coverage is ${item.status}: ${requirement}`,
213
+ [ARTIFACT_PATHS.receipt],
214
+ { requirement, status: item.status },
215
+ ));
216
+ }
217
+ }
218
+ const structuredChecks = receipt.value.checks.filter((check) => check?.schemaVersion === 1 || check?.id !== undefined || check?.evidenceKind !== undefined);
219
+ if (structuredChecks.length > 0) {
220
+ try {
221
+ assertCheckList(structuredChecks, "receipt.checks");
222
+ errors.push(...requiredChecksSatisfied(structuredChecks, requiredEvidence));
223
+ } catch (error) {
224
+ errors.push(issue(error.code ?? "E_CHECK_INVALID", error.message, [ARTIFACT_PATHS.receipt]));
225
+ }
226
+ }
227
+ }
228
+
229
+ const ledger = contract && state
230
+ ? await validateLedger(target, contract.value.taskId, state, errors, packageRoot)
231
+ : { valid: false, events: [], errors: [] };
232
+
233
+ if (state) {
234
+ try {
235
+ const classification = await classifyLoadedWorkState({
236
+ target,
237
+ state,
238
+ contractFile: ARTIFACT_PATHS.contract,
239
+ });
240
+ if (classification.status === "REVALIDATION_REQUIRED") {
241
+ for (const reason of classification.reasons) {
242
+ errors.push(issue(reason === "CONTRACT_CHANGED" ? "E_CONTRACT_STALE" : "E_PHASE_ARTIFACT_STALE", `State freshness check failed: ${reason}`, [ARTIFACT_PATHS.state]));
243
+ }
244
+ }
245
+ } catch (error) {
246
+ errors.push(issue("E_PHASE_ARTIFACT_STALE", error.message, [ARTIFACT_PATHS.state]));
247
+ }
248
+ }
249
+
250
+ const sortedErrors = sortIssues(errors);
251
+ const receiptValue = receipt?.value;
252
+ const publication = receiptValue ? publicationStatus(receiptValue) : "not-published";
253
+ const valid = sortedErrors.length === 0;
254
+ return {
255
+ status: valid ? "VALID" : "REJECTED",
256
+ taskStatus: valid ? "COMPLETE" : receiptValue?.status === "blocked" ? "BLOCKED" : "INCOMPLETE",
257
+ verificationStatus: valid ? "VALID" : "invalid",
258
+ publicationStatus: publication,
259
+ productionReadiness: receiptValue?.productionReadiness ?? "not-verified",
260
+ errors: sortedErrors,
261
+ warnings: [],
262
+ preflight,
263
+ coverage,
264
+ ledger: {
265
+ status: ledger.valid ? "valid" : "invalid",
266
+ events: ledger.events.length,
267
+ },
268
+ };
269
+ }
270
+
271
+ export async function runComplete({ target, packageRoot, strict = false, persist = true } = {}) {
272
+ const result = await evaluateCompletion({ target, packageRoot, strict });
273
+ if (persist && result.status === "VALID") {
274
+ const state = await readWorkState(target, packageRoot);
275
+ if (state && state.phase !== "COMPLETE") {
276
+ const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
277
+ const next = {
278
+ ...state,
279
+ previousPhase: state.phase,
280
+ phase: "COMPLETE",
281
+ evidenceCoverage: result.coverage,
282
+ verificationEvidence: receipt.value.evidence,
283
+ publicationStatus: result.publicationStatus,
284
+ lastUpdated: new Date().toISOString(),
285
+ };
286
+ await writeWorkState(target, next, { packageRoot });
287
+ }
288
+ const contract = await readContract(target, packageRoot);
289
+ const ledger = await validateEventLedger(target, packageRoot);
290
+ if (!ledger.events.some((event) => event.event === "COMPLETION_VALIDATED")) {
291
+ await appendProtocolEvent(target, { taskId: contract.value.taskId, event: "COMPLETION_VALIDATED" }, packageRoot);
292
+ }
293
+ }
294
+ return result;
295
+ }
@@ -0,0 +1,35 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
+
4
+ export const CONFIG_SCHEMA_VERSION = 1;
5
+ export const COMPLIANCE_MODES = Object.freeze(["advisory", "standard", "strict"]);
6
+
7
+ function stringArray(value, label) {
8
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
9
+ throw new Error(`${label} must be an array of non-empty strings`);
10
+ }
11
+ return [...new Set(value)];
12
+ }
13
+
14
+ export function createConfig(input = {}) {
15
+ const complianceMode = input.complianceMode ?? "standard";
16
+ if (!COMPLIANCE_MODES.includes(complianceMode)) {
17
+ throw new Error(`Unknown compliance mode: ${complianceMode}`);
18
+ }
19
+ return {
20
+ schemaVersion: CONFIG_SCHEMA_VERSION,
21
+ protocolVersion: PROTOCOL_VERSION,
22
+ complianceMode,
23
+ ...(input.policy !== undefined ? { policy: input.policy } : {}),
24
+ ...(input.requiredGates !== undefined ? { requiredGates: stringArray(input.requiredGates, "requiredGates") } : {}),
25
+ ...(input.requiredEvidence !== undefined ? { requiredEvidence: stringArray(input.requiredEvidence, "requiredEvidence") } : {}),
26
+ };
27
+ }
28
+
29
+ export async function readConfig(target, packageRoot) {
30
+ return (await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot)).value;
31
+ }
32
+
33
+ export async function writeConfig(target, config, packageRoot, options = {}) {
34
+ return writeJsonArtifact(target, ARTIFACT_PATHS.config, config, "config", packageRoot, options);
35
+ }
@@ -1,5 +1,6 @@
1
1
  import { PROTOCOL_VERSION } from "./protocol.js";
2
2
  import { createEvidence } from "./evidence.js";
3
+ import { canonicalFingerprint } from "./artifacts.js";
3
4
 
4
5
  function error(code, message, artifacts = []) {
5
6
  return { code, message, artifacts };
@@ -51,6 +52,12 @@ export function validateTaskArtifactSet({
51
52
  if (route && state && JSON.stringify(route.guides) !== JSON.stringify(state.selectedGuides)) {
52
53
  errors.push(error("ROUTE_STATE_GUIDES_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", ["route", "state"]));
53
54
  }
55
+ if (route && state && state.routeFingerprint !== undefined && state.routeFingerprint !== canonicalFingerprint(route)) {
56
+ errors.push(error("ROUTE_STATE_FINGERPRINT_MISMATCH", "work-state.routeFingerprint must match routing-result", ["route", "state"]));
57
+ }
58
+ if (route && receipt && receipt.routeFingerprint !== undefined && receipt.routeFingerprint !== canonicalFingerprint(route)) {
59
+ errors.push(error("STATE_RECEIPT_ROUTE_MISMATCH", "execution-receipt.routeFingerprint must match routing-result", ["route", "receipt"]));
60
+ }
54
61
  if (state && receipt && JSON.stringify(state.selectedGuides) !== JSON.stringify(receipt.selectedGuides)) {
55
62
  errors.push(error("STATE_RECEIPT_GUIDES_MISMATCH", "execution-receipt.selectedGuides must equal work-state.selectedGuides", ["state", "receipt"]));
56
63
  }