@cassiomc1/forgeloop 0.1.0 → 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 (100) 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 +5 -4
  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 +22 -4
  99. package/src/core/work-state.js +41 -13
  100. /package/.forgeloop/{.gitignore → forgeloop.gitignore} +0 -0
@@ -0,0 +1,246 @@
1
+ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
+ import { readContract } from "./contract.js";
3
+ import { readGateIfPresent, validateGateArtifacts } from "./gate-artifact.js";
4
+ import { requiredGatesForGuides } from "./guide-metadata.js";
5
+ import { assertRouteInvariants } from "./router.js";
6
+ import { assertSourceProvenance } from "./sources.js";
7
+ import { readPersistedRoute } from "./route-artifact.js";
8
+ import { appendProtocolEvent } from "./events.js";
9
+ import { readWorkState } from "./work-state.js";
10
+ import { readConfig } from "./config.js";
11
+ import { assertSafePath, ensureWithin, fileExists, readBytes } from "./filesystem.js";
12
+ import { sha256 } from "./manifest.js";
13
+ import { validateProfileSources } from "./profile.js";
14
+
15
+ const PREVIEW_DECISION_LIMIT = 10;
16
+ const PREVIEW_DECISION_MAX_LENGTH = 240;
17
+
18
+ function issue(code, message, artifacts = [], details = {}) {
19
+ return { code, message, artifacts, ...details };
20
+ }
21
+
22
+ function sortIssues(errors) {
23
+ const unique = [...new Map(errors.map((error) => [
24
+ `${error.code}\0${error.artifacts.join("\0")}\0${error.message}`,
25
+ error,
26
+ ])).values()];
27
+ return unique.sort((left, right) => left.code.localeCompare(right.code)
28
+ || left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
29
+ || left.message.localeCompare(right.message));
30
+ }
31
+
32
+ async function readProfile(target) {
33
+ const relativePath = "PROJECT_PROFILE.md";
34
+ await assertSafePath(target, relativePath);
35
+ const filePath = ensureWithin(target, relativePath);
36
+ if (!(await fileExists(filePath))) return { status: "missing", fingerprint: null };
37
+ const bytes = await readBytes(filePath);
38
+ const text = bytes.toString("utf8");
39
+ const mode = text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
40
+ const status = text.match(/^profile-status:\s*([^\s]+)\s*$/m)?.[1] ?? null;
41
+ return {
42
+ status: status === "verified" && mode !== "template" ? "verified" : "unverified",
43
+ mode,
44
+ profileStatus: status,
45
+ fingerprint: sha256(bytes),
46
+ };
47
+ }
48
+
49
+ async function optionalConfig(target, packageRoot, errors) {
50
+ try {
51
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot);
52
+ return artifact.value;
53
+ } catch (error) {
54
+ if (error.code === "ARTIFACT_MISSING") return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
55
+ errors.push(issue("E_CONFIG_INVALID", error.message, [ARTIFACT_PATHS.config]));
56
+ return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
57
+ }
58
+ }
59
+
60
+ async function loadContract(target, packageRoot, errors) {
61
+ try {
62
+ return await readContract(target, packageRoot);
63
+ } catch (error) {
64
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_CONTRACT_MISSING" : "E_CONTRACT_INVALID", error.message, [ARTIFACT_PATHS.contract]));
65
+ return null;
66
+ }
67
+ }
68
+
69
+ async function loadRoute(target, packageRoot, errors) {
70
+ try {
71
+ return await readPersistedRoute(target, packageRoot);
72
+ } catch (error) {
73
+ const code = error.code === "ARTIFACT_MISSING"
74
+ ? "E_ROUTE_MISSING"
75
+ : ["E_ROUTE_REASON_MISSING", "E_ROUTE_INVALID"].includes(error.code) ? error.code : "E_ROUTE_INVALID";
76
+ errors.push(issue(code, error.message, [ARTIFACT_PATHS.route]));
77
+ return null;
78
+ }
79
+ }
80
+
81
+ async function loadSources(target, contract, packageRoot, errors) {
82
+ if (!contract?.value?.sourceRefs?.length) return null;
83
+ let registry;
84
+ try {
85
+ registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
86
+ } catch (error) {
87
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_PROFILE_SOURCE_MISSING" : "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
88
+ return null;
89
+ }
90
+ try {
91
+ assertSourceProvenance(registry, contract.value.sourceRefs);
92
+ } catch (error) {
93
+ errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
94
+ }
95
+ return registry;
96
+ }
97
+
98
+ async function inspectGates(target, contract, route, packageRoot, errors, config = {}) {
99
+ if (!route) return { required: [], satisfied: [], records: {} };
100
+ const guideGates = await requiredGatesForGuides(route.value.guides, packageRoot);
101
+ const required = [...new Set([...guideGates, ...(config.requiredGates ?? [])])].sort();
102
+ const satisfied = [];
103
+ const records = {};
104
+ for (const gate of required) {
105
+ let artifact;
106
+ try {
107
+ artifact = await readGateIfPresent(target, gate, packageRoot);
108
+ } catch (error) {
109
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_GATE_UNVERIFIED" : "E_GATE_INVALID", error.message, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
110
+ continue;
111
+ }
112
+ if (!artifact) {
113
+ errors.push(issue("E_GATE_UNVERIFIED", `Required gate is missing or unverified: ${gate}`, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
114
+ continue;
115
+ }
116
+ records[gate] = artifact;
117
+ if (artifact.value.taskId !== contract?.value?.taskId) {
118
+ errors.push(issue("E_GATE_TASK_MISMATCH", `Gate ${gate} belongs to a different task`, [artifact.path], { gate }));
119
+ continue;
120
+ }
121
+ if (artifact.value.status !== "satisfied") {
122
+ errors.push(issue("E_GATE_UNVERIFIED", `Required gate is ${artifact.value.status}: ${gate}`, [artifact.path], { gate }));
123
+ continue;
124
+ }
125
+ const stale = await validateGateArtifacts(target, artifact.value, packageRoot);
126
+ if (stale.length > 0) {
127
+ errors.push(issue("E_GATE_STALE", `Gate ${gate} references stale artifacts`, [artifact.path], { gate, stale }));
128
+ continue;
129
+ }
130
+ satisfied.push(gate);
131
+ }
132
+ return { required, satisfied: satisfied.sort(), records };
133
+ }
134
+
135
+ export async function evaluatePreflight({ target, packageRoot, strict = false } = {}) {
136
+ const errors = [];
137
+ const profile = await readProfile(target);
138
+ const profileProvenance = await validateProfileSources(target, packageRoot);
139
+ const provenanceErrors = profileProvenance.errors ?? [];
140
+ errors.push(...provenanceErrors);
141
+ const contract = await loadContract(target, packageRoot, errors);
142
+ const route = await loadRoute(target, packageRoot, errors);
143
+ const config = await optionalConfig(target, packageRoot, errors);
144
+ const effectiveStrict = strict || config.complianceMode === "strict";
145
+ if (effectiveStrict && profile.status !== "verified") {
146
+ errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", ["PROJECT_PROFILE.md"]));
147
+ }
148
+ const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
149
+ if (unresolvedDecisions.length > 0) {
150
+ errors.push(issue(
151
+ "E_CONTRACT_UNRESOLVED_DECISION",
152
+ "The current contract contains unresolved blocking decisions.",
153
+ [ARTIFACT_PATHS.contract],
154
+ {
155
+ decisions: unresolvedDecisions
156
+ .slice(0, PREVIEW_DECISION_LIMIT)
157
+ .map((decision) => decision.slice(0, PREVIEW_DECISION_MAX_LENGTH)),
158
+ decisionCount: unresolvedDecisions.length,
159
+ ...(unresolvedDecisions.length > PREVIEW_DECISION_LIMIT ? { decisionsTruncated: true } : {}),
160
+ next: "Resolve the blocking decision with the user or applicable authority, update current-contract.json, then rerun preflight.",
161
+ },
162
+ ));
163
+ }
164
+
165
+ if (route && contract) {
166
+ assertRouteInvariants(route.value);
167
+ if (route.value.contractFingerprint !== undefined && route.value.contractFingerprint !== contract.fingerprint) {
168
+ errors.push(issue("E_ROUTE_STALE", "Routing result was created for a different contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract]));
169
+ }
170
+ }
171
+
172
+ const sources = await loadSources(target, contract, packageRoot, errors);
173
+ const gates = await inspectGates(target, contract, route, packageRoot, errors, config);
174
+
175
+ let state = null;
176
+ try {
177
+ state = await readWorkState(target, packageRoot);
178
+ } catch (error) {
179
+ errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
180
+ }
181
+ if (state && route && JSON.stringify(state.selectedGuides) !== JSON.stringify(route.value.guides)) {
182
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
183
+ }
184
+ if (state && contract && state.contractFingerprint !== contract.fingerprint) {
185
+ errors.push(issue("E_CONTRACT_STALE", "work-state references a different contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
186
+ }
187
+
188
+ const sortedErrors = sortIssues(errors);
189
+ const taskId = contract?.value?.taskId ?? state?.taskId ?? "unknown";
190
+ return {
191
+ schemaVersion: 1,
192
+ protocolVersion: 1,
193
+ taskId,
194
+ status: sortedErrors.length === 0 ? "READY" : "BLOCKED",
195
+ profile: { ...profile, provenance: profileProvenance.status },
196
+ contract: contract
197
+ ? { status: "valid", fingerprint: contract.fingerprint }
198
+ : { status: "missing", fingerprint: null },
199
+ routing: route
200
+ ? { status: "valid", fingerprint: route.fingerprint, guides: [...route.value.guides] }
201
+ : { status: "missing", fingerprint: null, guides: [] },
202
+ requiredGates: gates.required,
203
+ satisfiedGates: gates.satisfied,
204
+ errors: sortedErrors,
205
+ fingerprints: {
206
+ contract: contract?.fingerprint ?? null,
207
+ routing: route?.fingerprint ?? null,
208
+ profile: profile.fingerprint,
209
+ },
210
+ ...(config.policy ? {
211
+ policy: {
212
+ name: config.policy,
213
+ complianceMode: config.complianceMode,
214
+ requiredGates: [...(config.requiredGates ?? [])],
215
+ requiredEvidence: [...(config.requiredEvidence ?? [])],
216
+ },
217
+ } : {}),
218
+ ...(sources ? { sources: { status: "valid", fingerprint: null } } : {}),
219
+ };
220
+ }
221
+
222
+ export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
223
+ const result = await evaluatePreflight({ target, packageRoot, strict });
224
+ if (persist) {
225
+ await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
226
+ if (result.taskId !== "unknown") {
227
+ for (const gate of result.satisfiedGates) {
228
+ await appendProtocolEvent(target, {
229
+ taskId: result.taskId,
230
+ event: "GATE_SATISFIED",
231
+ details: { gate },
232
+ }, packageRoot);
233
+ }
234
+ await appendProtocolEvent(target, {
235
+ taskId: result.taskId,
236
+ event: result.status === "READY" ? "PREFLIGHT_READY" : "PREFLIGHT_BLOCKED",
237
+ fingerprint: result.fingerprints.contract ?? undefined,
238
+ details: {
239
+ requiredGates: result.requiredGates,
240
+ satisfiedGates: result.satisfiedGates,
241
+ },
242
+ }, packageRoot);
243
+ }
244
+ }
245
+ return result;
246
+ }
@@ -0,0 +1,48 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { ARTIFACT_PATHS, readJsonArtifact } from "./artifacts.js";
4
+ import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
5
+ import { assertSourceProvenance } from "./sources.js";
6
+
7
+ const SOURCE_ID_PATTERN = /\b(?:USER|FILE|CMD|DECISION|OBS|INFER|UNKNOWN)-[A-Z0-9_-]+\b/g;
8
+ const DIRECTIVE_PATTERN = /forgeloop-source:\s*([A-Z0-9_-]+)\s+kind=([a-z-]+)/gi;
9
+
10
+ function issue(code, message, artifacts = []) {
11
+ return { code, message, artifacts };
12
+ }
13
+
14
+ export async function validateProfileSources(target, packageRoot) {
15
+ const profilePath = "PROJECT_PROFILE.md";
16
+ await assertSafePath(target, profilePath);
17
+ const absolutePath = ensureWithin(target, profilePath);
18
+ if (!(await fileExists(absolutePath))) {
19
+ return { status: "missing", refs: [], errors: [] };
20
+ }
21
+ const text = await readFile(absolutePath, "utf8");
22
+ const refs = [...new Set(text.match(SOURCE_ID_PATTERN) ?? [])].sort();
23
+ const directives = [...text.matchAll(DIRECTIVE_PATTERN)].map((match) => ({ id: match[1], kind: match[2] }));
24
+ if (refs.length === 0 && directives.length === 0) {
25
+ return { status: "not-referenced", refs: [], errors: [] };
26
+ }
27
+ const errors = [];
28
+ let registry;
29
+ try {
30
+ registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
31
+ } catch (error) {
32
+ errors.push(issue("E_PROFILE_SOURCE_MISSING", error.message, [ARTIFACT_PATHS.sources]));
33
+ return { status: "invalid", refs, errors };
34
+ }
35
+ try {
36
+ assertSourceProvenance(registry, refs);
37
+ } catch (error) {
38
+ errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
39
+ }
40
+ for (const directive of directives) {
41
+ try {
42
+ assertSourceProvenance(registry, [directive.id], { expectedKind: directive.kind });
43
+ } catch (error) {
44
+ errors.push(issue(error.code ?? "E_PROFILE_SOURCE_MISCLASSIFIED", error.message, [ARTIFACT_PATHS.sources]));
45
+ }
46
+ }
47
+ return { status: errors.length === 0 ? "valid" : "invalid", refs, errors };
48
+ }
@@ -15,6 +15,40 @@ export const FAILURE_CLASSES = Object.freeze([
15
15
  "STALE_STATE_FAILURE",
16
16
  ]);
17
17
 
18
+ export const FAILURE_CODES = Object.freeze([
19
+ "E_PREFLIGHT_NOT_READY",
20
+ "E_CONTRACT_MISSING",
21
+ "E_CONTRACT_INVALID",
22
+ "E_CONTRACT_UNRESOLVED_DECISION",
23
+ "E_CONTRACT_STALE",
24
+ "E_ROUTE_MISSING",
25
+ "E_ROUTE_INVALID",
26
+ "E_ROUTE_STALE",
27
+ "E_ROUTE_GUIDE_MISMATCH",
28
+ "E_ROUTE_REASON_MISSING",
29
+ "E_GATE_REQUIRED",
30
+ "E_GATE_UNVERIFIED",
31
+ "E_GATE_STALE",
32
+ "E_PHASE_TRANSITION_INVALID",
33
+ "E_PHASE_PREREQUISITE_MISSING",
34
+ "E_PHASE_CHRONOLOGY_INVALID",
35
+ "E_EVIDENCE_REQUIRED",
36
+ "E_EVIDENCE_KIND_INVALID",
37
+ "E_EVIDENCE_STALE",
38
+ "E_EVIDENCE_COVERAGE_PARTIAL",
39
+ "E_CHECK_STATUS_CONTRADICTION",
40
+ "E_PROFILE_SOURCE_MISSING",
41
+ "E_PROFILE_SOURCE_UNKNOWN",
42
+ "E_PROFILE_SOURCE_MISCLASSIFIED",
43
+ "E_RECEIPT_ROUTE_MISMATCH",
44
+ "E_RECEIPT_STATE_MISMATCH",
45
+ "E_RECEIPT_CONTRACT_MISMATCH",
46
+ "E_RECEIPT_PATH_MISMATCH",
47
+ "E_COMPLETION_REJECTED",
48
+ "E_PUBLICATION_CLAIM_UNVERIFIED",
49
+ "E_PRODUCTION_READINESS_UNVERIFIED",
50
+ ]);
51
+
18
52
  export const WORK_PHASES = Object.freeze([
19
53
  "RECEIVED",
20
54
  "DISCOVERING",
@@ -2,6 +2,8 @@ import { GUIDE_IDS, PROTOCOL_VERSION } from "./protocol.js";
2
2
  import { assertSchema, readSchema } from "./schema-validation.js";
3
3
  import { assertEvidenceList, evidenceMatches } from "./evidence.js";
4
4
  import { assertJsonLimits } from "./json-safety.js";
5
+ import { assertCheck, assertCheckList } from "./checks.js";
6
+ import { assertCoverageList } from "./coverage.js";
5
7
 
6
8
  const RECEIPT_SCHEMA_VERSION = 1;
7
9
  const SECRET_WORDS = new Set(["token", "password", "secret", "credential"]);
@@ -59,6 +61,10 @@ export function assertReceiptSemantics(receipt) {
59
61
  assertEvidenceList(evidence, "receipt.evidence");
60
62
 
61
63
  for (const [index, check] of receipt.checks.entries()) {
64
+ if (check?.schemaVersion !== undefined || check?.id !== undefined || check?.evidenceKind !== undefined) {
65
+ assertCheck(check, `receipt.checks[${index}]`);
66
+ continue;
67
+ }
62
68
  if (check?.status === "passed") {
63
69
  const hasCommandOrResult = [check.command, check.result, check.name]
64
70
  .some((value) => typeof value === "string" && value.trim().length > 0);
@@ -68,6 +74,10 @@ export function assertReceiptSemantics(receipt) {
68
74
  }
69
75
  }
70
76
 
77
+ if (receipt.evidenceCoverage !== undefined) {
78
+ assertCoverageList(receipt.evidenceCoverage, "receipt.evidenceCoverage");
79
+ }
80
+
71
81
  if (receipt.status === "complete") {
72
82
  const verificationEvidence = evidence.filter((item) => ["OBSERVED", "INFERRED"].includes(item.kind));
73
83
  if (verificationEvidence.length === 0) {
@@ -82,10 +92,32 @@ export function assertReceiptSemantics(receipt) {
82
92
  ];
83
93
  for (const [claimed, terms, field, label] of publicationEvidence) {
84
94
  if (claimed && !evidenceMatches(evidence, terms)) {
85
- throw new Error(`publication.${field} requires ${label} evidence`);
95
+ const error = new Error(`publication.${field} requires ${label} evidence`);
96
+ error.code = "E_PUBLICATION_CLAIM_UNVERIFIED";
97
+ throw error;
86
98
  }
87
99
  }
88
100
 
101
+ if (["committed", "pushed", "published", "deployed"].includes(receipt.publicationStatus)) {
102
+ const requiredTerms = receipt.publicationStatus === "committed"
103
+ ? ["commit", "committed"]
104
+ : receipt.publicationStatus === "pushed"
105
+ ? ["git push", "pushed"]
106
+ : receipt.publicationStatus === "deployed"
107
+ ? ["deploy", "deployed", "deployment"]
108
+ : ["publish", "published", "release"];
109
+ if (!evidenceMatches(evidence, requiredTerms)) {
110
+ const error = new Error(`publicationStatus ${receipt.publicationStatus} requires publication evidence`);
111
+ error.code = "E_PUBLICATION_CLAIM_UNVERIFIED";
112
+ throw error;
113
+ }
114
+ }
115
+ if (receipt.productionReadiness === "ready" && !evidenceMatches(evidence, ["production readiness", "production validation", "deployment"])) {
116
+ const error = new Error("productionReadiness ready requires production evidence");
117
+ error.code = "E_PRODUCTION_READINESS_UNVERIFIED";
118
+ throw error;
119
+ }
120
+
89
121
  if (receipt.review.independent === true) {
90
122
  const implementer = receipt.review.implementerId;
91
123
  const reviewer = receipt.review.reviewerId;
@@ -111,11 +143,18 @@ export async function createReceipt(input, packageRoot) {
111
143
  protocolVersion: PROTOCOL_VERSION,
112
144
  taskId: input.taskId,
113
145
  contractFingerprint: input.contractFingerprint,
146
+ ...(input.routeFingerprint !== undefined ? { routeFingerprint: input.routeFingerprint } : {}),
147
+ ...(input.stateFingerprint !== undefined ? { stateFingerprint: input.stateFingerprint } : {}),
114
148
  status: input.status ?? "in-progress",
149
+ ...(input.taskStatus !== undefined ? { taskStatus: input.taskStatus } : {}),
150
+ ...(input.verificationStatus !== undefined ? { verificationStatus: input.verificationStatus } : {}),
151
+ ...(input.publicationStatus !== undefined ? { publicationStatus: input.publicationStatus } : {}),
152
+ ...(input.productionReadiness !== undefined ? { productionReadiness: input.productionReadiness } : {}),
115
153
  selectedGuides: [...(input.selectedGuides ?? [])],
116
154
  changedPaths: [...(input.changedPaths ?? [])],
117
155
  checks: [...(input.checks ?? [])],
118
156
  evidence: [...(input.evidence ?? [])],
157
+ ...(input.evidenceCoverage !== undefined ? { evidenceCoverage: [...input.evidenceCoverage] } : {}),
119
158
  review: input.review ?? { status: "not-run", independent: false },
120
159
  limitations: [...(input.limitations ?? [])],
121
160
  publication: input.publication ?? {
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import { evaluateAudit } from "./audit.js";
4
+ import { fileExists, ensureWithin } from "./filesystem.js";
5
+
6
+ async function profileStatus(target) {
7
+ const profilePath = ensureWithin(target, "PROJECT_PROFILE.md");
8
+ if (!(await fileExists(profilePath))) return "NOT_VERIFIED";
9
+ const text = await readFile(profilePath, "utf8");
10
+ return /^profile-status:\s*verified\s*$/m.test(text) ? "PASS" : "NOT_VERIFIED";
11
+ }
12
+
13
+ function section(id, label, status, details = null) {
14
+ return { id, label, status, ...(details ? { details } : {}) };
15
+ }
16
+
17
+ export async function evaluateReport({ target, packageRoot, strict = false } = {}) {
18
+ const audit = await evaluateAudit({ target, packageRoot, strict });
19
+ const completion = audit.completion;
20
+ const preflight = completion.preflight;
21
+ const errorCodes = new Set(audit.errors.map((error) => error.code));
22
+ const coverage = completion.coverage ?? [];
23
+ const coverageStatus = coverage.length === 0
24
+ ? "NOT_VERIFIED"
25
+ : coverage.every((item) => item.status === "COVERED") ? "PASS" : "PARTIAL";
26
+ const sections = [
27
+ section("installation", "Installation", audit.installation.status === "ready" ? "PASS" : "NOT_VERIFIED"),
28
+ section("profile", "Profile", await profileStatus(target)),
29
+ section("contract", "Contract", preflight.contract.status === "valid" ? "PASS" : "NOT_VERIFIED"),
30
+ section("routing", "Routing", preflight.routing.status === "valid" ? "PASS" : "NOT_VERIFIED"),
31
+ section("required-gates", "Required gates", preflight.requiredGates.length === preflight.satisfiedGates.length ? "PASS" : "BLOCKED"),
32
+ section("phase-chronology", "Phase chronology", completion.ledger.status === "valid" ? "PASS" : "INVALID"),
33
+ section("evidence-coverage", "Evidence coverage", coverageStatus),
34
+ section("receipt", "Receipt", errorCodes.has("E_RECEIPT_MISSING") || errorCodes.has("E_RECEIPT_INVALID") ? "NOT_VERIFIED" : "PASS"),
35
+ section("publication", "Publication state", audit.publicationStatus.toUpperCase().replaceAll("-", "_")),
36
+ section("production-readiness", "Production readiness", audit.productionReadiness.toUpperCase().replaceAll("-", "_")),
37
+ ];
38
+ return {
39
+ schemaVersion: 1,
40
+ protocolVersion: 1,
41
+ status: audit.status,
42
+ verdict: audit.status === "VALID" ? "VALID" : "INCOMPLETE",
43
+ sections,
44
+ publicationStatus: audit.publicationStatus,
45
+ productionReadiness: audit.productionReadiness,
46
+ errors: audit.errors,
47
+ warnings: audit.warnings,
48
+ };
49
+ }
@@ -17,3 +17,21 @@ export async function currentRepositoryFingerprint(target) {
17
17
  return { branch: null, head: null };
18
18
  }
19
19
  }
20
+
21
+ export async function currentChangedPaths(target) {
22
+ try {
23
+ const { stdout } = await execFileAsync(
24
+ "git",
25
+ ["-C", target, "status", "--porcelain=v1", "--untracked-files=all"],
26
+ { windowsHide: true },
27
+ );
28
+ return stdout
29
+ .split(/\r?\n/)
30
+ .filter(Boolean)
31
+ .map((line) => line.slice(3).trim())
32
+ .filter((relativePath) => relativePath && !relativePath.startsWith(".forgeloop/"))
33
+ .sort();
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ import { assertRouteInvariants } from "./router.js";
2
+ import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
+ import { readContract } from "./contract.js";
4
+
5
+ export async function persistRoute(target, route, packageRoot, options = {}) {
6
+ assertRouteInvariants(route);
7
+ let { contractFingerprint, ...writeOptions } = options;
8
+ if (contractFingerprint === undefined) {
9
+ try {
10
+ contractFingerprint = (await readContract(target, packageRoot)).fingerprint;
11
+ } catch (error) {
12
+ if (error.code !== "ARTIFACT_MISSING") throw error;
13
+ }
14
+ }
15
+ const value = contractFingerprint === undefined
16
+ ? route
17
+ : { ...route, contractFingerprint };
18
+ assertRouteInvariants(value);
19
+ return writeJsonArtifact(
20
+ target,
21
+ ARTIFACT_PATHS.route,
22
+ value,
23
+ "routing-result",
24
+ packageRoot,
25
+ writeOptions,
26
+ );
27
+ }
28
+
29
+ export async function readPersistedRoute(target, packageRoot) {
30
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.route, "routing-result", packageRoot);
31
+ assertRouteInvariants(artifact.value);
32
+ return artifact;
33
+ }
@@ -259,31 +259,39 @@ export function evaluateRoute(input = {}) {
259
259
  }
260
260
 
261
261
  export function assertRouteInvariants(result) {
262
+ const invariantError = (code, message) => {
263
+ const error = new RouteInputError(message);
264
+ error.code = code;
265
+ return error;
266
+ };
262
267
  if (!result || typeof result !== "object" || Array.isArray(result)) {
263
- throw new RouteInputError("Route result must be an object");
268
+ throw invariantError("E_ROUTE_INVALID", "Route result must be an object");
264
269
  }
265
270
  if (!Array.isArray(result.guides) || new Set(result.guides).size !== result.guides.length) {
266
- throw new RouteInputError("Route result contains duplicate guides");
271
+ throw invariantError("E_ROUTE_INVALID", "Route result contains duplicate guides");
272
+ }
273
+ if (result.contractFingerprint !== undefined && !/^[a-f0-9]{64}$/.test(result.contractFingerprint)) {
274
+ throw invariantError("E_ROUTE_INVALID", "Route contractFingerprint must be a lowercase SHA-256 fingerprint");
267
275
  }
268
276
  for (const guide of result.guides) {
269
- if (!GUIDE_IDS.includes(guide)) throw new RouteInputError(`Route result contains unknown guide: ${guide}`);
277
+ if (!GUIDE_IDS.includes(guide)) throw invariantError("E_ROUTE_INVALID", `Route result contains unknown guide: ${guide}`);
270
278
  if (!Array.isArray(result.reasons?.[guide]) || result.reasons[guide].length === 0) {
271
- throw new RouteInputError(`Selected guide has no reason: ${guide}`);
279
+ throw invariantError("E_ROUTE_REASON_MISSING", `Selected guide has no reason: ${guide}`);
272
280
  }
273
281
  }
274
282
  for (const [guide, reasons] of Object.entries(result.excluded ?? {})) {
275
283
  if (guide !== "documentation" && !GUIDE_IDS.includes(guide)) {
276
- throw new RouteInputError(`Route result contains unknown excluded guide: ${guide}`);
284
+ throw invariantError("E_ROUTE_INVALID", `Route result contains unknown excluded guide: ${guide}`);
277
285
  }
278
286
  if (result.guides.includes(guide)) {
279
- throw new RouteInputError(`Guide cannot be selected and excluded: ${guide}`);
287
+ throw invariantError("E_ROUTE_INVALID", `Guide cannot be selected and excluded: ${guide}`);
280
288
  }
281
289
  if (!Array.isArray(reasons) || reasons.length === 0) {
282
- throw new RouteInputError(`Excluded guide has no exclusion reason: ${guide}`);
290
+ throw invariantError("E_ROUTE_REASON_MISSING", `Excluded guide has no exclusion reason: ${guide}`);
283
291
  }
284
292
  }
285
293
  if (result.primary !== null && !result.guides.includes(result.primary)) {
286
- throw new RouteInputError("Primary guide must be null or selected");
294
+ throw invariantError("E_ROUTE_INVALID", "Primary guide must be null or selected");
287
295
  }
288
296
  return result;
289
297
  }
@@ -13,6 +13,17 @@ export const SHIPPED_SCHEMA_NAMES = Object.freeze([
13
13
  "task-brief",
14
14
  "delegated-result",
15
15
  "evidence",
16
+ "current-contract",
17
+ "gate",
18
+ "source-registry",
19
+ "config",
20
+ "preflight",
21
+ "check",
22
+ "evidence-coverage",
23
+ "event",
24
+ "activation",
25
+ "policy",
26
+ "task-bundle",
16
27
  ]);
17
28
 
18
29
  export class SchemaValidationError extends Error {