@cassiomc1/forgeloop 0.1.1 → 0.1.3

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 (103) hide show
  1. package/.cursor/rules/project-loop.mdc +12 -1
  2. package/.github/copilot-instructions.md +11 -0
  3. package/AGENTS.md +13 -1
  4. package/AGENT_COMPATIBILITY.md +14 -1
  5. package/CLAUDE.md +11 -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 +163 -4
  16. package/ORCHESTRATOR_INTEGRATION.md +15 -0
  17. package/QUALITY_SCORECARD.md +16 -0
  18. package/README.md +64 -1
  19. package/THREAT_MODEL.md +6 -0
  20. package/conformance/README.md +47 -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 +249 -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/next.js +29 -0
  65. package/src/commands/policy.js +19 -0
  66. package/src/commands/preflight.js +12 -0
  67. package/src/commands/prepare-completion.js +18 -0
  68. package/src/commands/record-check.js +20 -0
  69. package/src/commands/report.js +10 -0
  70. package/src/commands/route.js +14 -2
  71. package/src/commands/validate-receipt.js +7 -6
  72. package/src/core/activation.js +16 -0
  73. package/src/core/artifacts.js +132 -0
  74. package/src/core/audit.js +88 -0
  75. package/src/core/bundles.js +120 -0
  76. package/src/core/checks.js +115 -0
  77. package/src/core/completion-artifacts.js +321 -0
  78. package/src/core/completion-relationships.js +141 -0
  79. package/src/core/completion.js +266 -0
  80. package/src/core/config.js +35 -0
  81. package/src/core/conformance.js +7 -0
  82. package/src/core/contract.js +141 -0
  83. package/src/core/coverage.js +89 -0
  84. package/src/core/events.js +158 -0
  85. package/src/core/execution-prerequisites.js +200 -0
  86. package/src/core/gate-artifact.js +54 -0
  87. package/src/core/gates.js +55 -0
  88. package/src/core/guide-metadata.js +62 -0
  89. package/src/core/next-action.js +709 -0
  90. package/src/core/phase.js +212 -0
  91. package/src/core/policies.js +69 -0
  92. package/src/core/preflight.js +409 -0
  93. package/src/core/profile.js +48 -0
  94. package/src/core/protocol.js +34 -0
  95. package/src/core/receipt.js +40 -1
  96. package/src/core/report.js +49 -0
  97. package/src/core/repository.js +18 -0
  98. package/src/core/route-artifact.js +33 -0
  99. package/src/core/router.js +16 -8
  100. package/src/core/schema-validation.js +11 -0
  101. package/src/core/sources.js +86 -0
  102. package/src/core/templates.js +11 -0
  103. package/src/core/work-state.js +41 -13
@@ -0,0 +1,212 @@
1
+ import { ARTIFACT_PATHS, canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
2
+ import { readContract } from "./contract.js";
3
+ import { appendProtocolEvent, LIFECYCLE_MILESTONES, validateEventLedger } from "./events.js";
4
+ import { readPersistedRoute } from "./route-artifact.js";
5
+ import { assertWorkPhase, isValidTransition } from "./protocol.js";
6
+ import { readWorkState, writeWorkState } from "./work-state.js";
7
+ import { evaluateCompletion } from "./completion.js";
8
+ import { evaluatePreflight, validatePersistedPreflight } from "./preflight.js";
9
+ import { requiredEvidenceForTarget } from "./completion-artifacts.js";
10
+ import { assertCompletionRelationships, assertStateIdentity } from "./completion-relationships.js";
11
+ import { createReceipt, validateReceipt } from "./receipt.js";
12
+ import { assertExecutionPrerequisites, hasExecutionStarted } from "./execution-prerequisites.js";
13
+
14
+ function phaseError(code, message, artifacts = []) {
15
+ const error = new Error(message);
16
+ error.code = code;
17
+ error.artifacts = artifacts;
18
+ return error;
19
+ }
20
+
21
+ const PHASE_EVENTS = Object.freeze({
22
+ CONTRACT_READY: "CONTRACT_VALIDATED",
23
+ ROUTED: "ROUTE_VALIDATED",
24
+ DESIGNING: "DESIGN_GATE_STARTED",
25
+ PLANNED: "PLAN_RECORDED",
26
+ EXECUTING: "EXECUTION_STARTED",
27
+ VERIFYING: "VERIFICATION_STARTED",
28
+ COMPLETE: "COMPLETION_VALIDATED",
29
+ });
30
+
31
+ const LATE_PHASES = new Set([
32
+ "EXECUTING",
33
+ "VERIFYING",
34
+ "DIAGNOSING",
35
+ "CORRECTING",
36
+ "REVIEWING",
37
+ "COMPLETE",
38
+ ]);
39
+
40
+ async function assertPersistedStateIdentity(target, state, toPhase, packageRoot) {
41
+ const requireContract = LATE_PHASES.has(state.phase)
42
+ || ["CONTRACT_READY", "ROUTED", "EXECUTING"].includes(toPhase);
43
+ const requireRoute = LATE_PHASES.has(state.phase)
44
+ || ["ROUTED", "EXECUTING"].includes(toPhase);
45
+ let contract = null;
46
+ let route = null;
47
+ try {
48
+ contract = await readContract(target, packageRoot);
49
+ } catch (error) {
50
+ if (error.code === "ARTIFACT_MISSING" && !requireContract) {
51
+ contract = null;
52
+ } else {
53
+ throw phaseError(
54
+ requireContract ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_CONTRACT_INVALID",
55
+ `${requireContract ? `Phase ${toPhase} requires current contract` : "Unable to validate current contract"}: ${error.message}`,
56
+ [ARTIFACT_PATHS.contract],
57
+ );
58
+ }
59
+ }
60
+ try {
61
+ route = await readPersistedRoute(target, packageRoot);
62
+ } catch (error) {
63
+ if (error.code === "ARTIFACT_MISSING" && !requireRoute) {
64
+ route = null;
65
+ } else {
66
+ throw phaseError(
67
+ requireRoute ? "E_PHASE_PREREQUISITE_MISSING" : error.code ?? "E_ROUTE_INVALID",
68
+ `${requireRoute ? `Phase ${toPhase} requires persisted route` : "Unable to validate persisted route"}: ${error.message}`,
69
+ [ARTIFACT_PATHS.route],
70
+ );
71
+ }
72
+ }
73
+ if (!contract && !route) return;
74
+ try {
75
+ assertStateIdentity({ contract, route, state });
76
+ } catch (error) {
77
+ throw phaseError(
78
+ error.code,
79
+ error.message,
80
+ error.artifacts,
81
+ );
82
+ }
83
+ }
84
+
85
+ function reconcileImplementationStep(state, toPhase) {
86
+ if (state.phase !== "EXECUTING" || toPhase !== "VERIFYING") return state;
87
+ if (!state.pendingSteps.includes("implementation")) return state;
88
+ return {
89
+ ...state,
90
+ completedSteps: state.completedSteps.includes("implementation")
91
+ ? [...state.completedSteps]
92
+ : [...state.completedSteps, "implementation"],
93
+ pendingSteps: state.pendingSteps.filter((step) => step !== "implementation"),
94
+ };
95
+ }
96
+
97
+ async function assertPhasePrerequisites(target, state, toPhase, packageRoot) {
98
+ if (toPhase === "CONTRACT_READY" || toPhase === "ROUTED" || toPhase === "EXECUTING") {
99
+ try {
100
+ await readContract(target, packageRoot);
101
+ } catch (error) {
102
+ throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${ARTIFACT_PATHS.contract}: ${error.message}`, [ARTIFACT_PATHS.contract]);
103
+ }
104
+ }
105
+ if (toPhase === "ROUTED" || toPhase === "EXECUTING") {
106
+ try {
107
+ await readPersistedRoute(target, packageRoot);
108
+ } catch (error) {
109
+ throw phaseError("E_PHASE_PREREQUISITE_MISSING", `Phase ${toPhase} requires ${ARTIFACT_PATHS.route}: ${error.message}`, [ARTIFACT_PATHS.route]);
110
+ }
111
+ }
112
+ if (hasExecutionStarted(toPhase)) {
113
+ try {
114
+ await assertExecutionPrerequisites({ target, state, packageRoot });
115
+ } catch (error) {
116
+ throw phaseError(error.code, error.message, error.artifacts);
117
+ }
118
+ }
119
+ if (toPhase === "COMPLETE" && state.verificationEvidence.length === 0) {
120
+ throw phaseError("E_PHASE_EVIDENCE_MISSING", "COMPLETE requires verification evidence");
121
+ }
122
+ if (toPhase === "COMPLETE") {
123
+ const completion = await evaluateCompletion({ target, packageRoot, persist: false });
124
+ if (completion.status !== "VALID") {
125
+ throw phaseError("E_COMPLETION_REJECTED", "COMPLETE requires a valid completion audit", completion.errors.flatMap((error) => error.artifacts ?? []));
126
+ }
127
+ }
128
+ }
129
+
130
+ export async function advanceWorkState(target, toPhase, { packageRoot, now = new Date().toISOString() } = {}) {
131
+ assertWorkPhase(toPhase);
132
+ const state = await readWorkState(target, packageRoot);
133
+ if (!state) throw phaseError("E_PHASE_PREREQUISITE_MISSING", "Cannot advance without work state", [ARTIFACT_PATHS.state]);
134
+ await assertPhasePrerequisites(target, state, toPhase, packageRoot);
135
+ await assertPersistedStateIdentity(target, state, toPhase, packageRoot);
136
+ if (!isValidTransition(state.phase, toPhase)) {
137
+ throw phaseError("E_PHASE_TRANSITION_INVALID", `Invalid work-state transition: ${state.phase} -> ${toPhase}`);
138
+ }
139
+ const ledger = await validateEventLedger(target, packageRoot);
140
+ if (!ledger.valid) {
141
+ const first = ledger.errors[0];
142
+ throw phaseError(first.code, first.message, [ARTIFACT_PATHS.events]);
143
+ }
144
+ if (ledger.events.some((event) => event.taskId !== state.taskId)) {
145
+ throw phaseError(
146
+ "E_PHASE_CHRONOLOGY_INVALID",
147
+ "Cannot advance work state with lifecycle events from a different task",
148
+ [ARTIFACT_PATHS.events, ARTIFACT_PATHS.state],
149
+ );
150
+ }
151
+ const eventType = PHASE_EVENTS[toPhase];
152
+ if (eventType && ledger.events.some((event) => event.taskId === state.taskId && event.event === eventType)) {
153
+ throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Lifecycle milestone already exists: ${eventType}`, [ARTIFACT_PATHS.events]);
154
+ }
155
+ const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(eventType);
156
+ if (milestoneIndex >= 0) {
157
+ const lastMilestone = ledger.events.reduce((last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)), -1);
158
+ if (lastMilestone !== milestoneIndex - 1) {
159
+ throw phaseError("E_PHASE_CHRONOLOGY_INVALID", `Phase ${toPhase} cannot append ${eventType} after the current lifecycle ledger`, [ARTIFACT_PATHS.events]);
160
+ }
161
+ }
162
+ const reconciled = reconcileImplementationStep(state, toPhase);
163
+ const next = {
164
+ ...reconciled,
165
+ previousPhase: state.phase,
166
+ phase: toPhase,
167
+ lastUpdated: now,
168
+ };
169
+ let nextReceipt = null;
170
+ try {
171
+ const receipt = await readJsonArtifact(target, ARTIFACT_PATHS.receipt, "execution-receipt", packageRoot);
172
+ const contract = await readContract(target, packageRoot);
173
+ const route = await readPersistedRoute(target, packageRoot);
174
+ const preflight = await evaluatePreflight({ target, packageRoot });
175
+ const requiredEvidence = await requiredEvidenceForTarget({
176
+ target,
177
+ contract,
178
+ route,
179
+ packageRoot,
180
+ additionalEvidence: preflight.policy?.requiredEvidence ?? [],
181
+ });
182
+ await validateReceipt(receipt.value, packageRoot);
183
+ assertCompletionRelationships({
184
+ contract,
185
+ route,
186
+ state,
187
+ receipt: receipt.value,
188
+ requiredEvidence,
189
+ requireRequiredChecks: false,
190
+ });
191
+ nextReceipt = await createReceipt({
192
+ ...receipt.value,
193
+ stateFingerprint: canonicalFingerprint(next),
194
+ }, packageRoot);
195
+ assertCompletionRelationships({
196
+ contract,
197
+ route,
198
+ state: next,
199
+ receipt: nextReceipt,
200
+ requiredEvidence,
201
+ requireRequiredChecks: false,
202
+ });
203
+ } catch (error) {
204
+ if (error.code !== "ARTIFACT_MISSING") throw error;
205
+ }
206
+ await writeWorkState(target, next, { packageRoot });
207
+ if (nextReceipt) {
208
+ await writeJsonArtifact(target, ARTIFACT_PATHS.receipt, nextReceipt, "execution-receipt", packageRoot);
209
+ }
210
+ if (eventType) await appendProtocolEvent(target, { taskId: state.taskId, event: eventType, at: now }, packageRoot);
211
+ return next;
212
+ }
@@ -0,0 +1,69 @@
1
+ import { PROTOCOL_VERSION } from "./protocol.js";
2
+ import { assertSchema, readSchema } from "./schema-validation.js";
3
+
4
+ export const POLICY_SCHEMA_VERSION = 1;
5
+
6
+ const POLICIES = Object.freeze({
7
+ "web-premium": {
8
+ name: "web-premium",
9
+ description: "Strict completion for premium website delivery.",
10
+ complianceMode: "strict",
11
+ requiredGates: ["design", "quality"],
12
+ requiredEvidence: ["responsive-validation", "accessibility-validation", "build", "visual-validation"],
13
+ allowedCompletionStates: ["COMPLETE"],
14
+ freshnessRequired: true,
15
+ },
16
+ bugfix: {
17
+ name: "bugfix",
18
+ description: "Standard completion with diagnosis and regression evidence.",
19
+ complianceMode: "standard",
20
+ requiredGates: ["diagnosis", "regression"],
21
+ requiredEvidence: ["reproduction", "regression"],
22
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
23
+ freshnessRequired: true,
24
+ },
25
+ "security-critical": {
26
+ name: "security-critical",
27
+ description: "Strict completion with a declared trust boundary and observed security evidence.",
28
+ complianceMode: "strict",
29
+ requiredGates: ["threat-boundary", "security-verification"],
30
+ requiredEvidence: ["security-validation", "negative-test"],
31
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
32
+ freshnessRequired: true,
33
+ },
34
+ prototype: {
35
+ name: "prototype",
36
+ description: "Advisory local work with explicit non-production semantics.",
37
+ complianceMode: "advisory",
38
+ requiredGates: [],
39
+ requiredEvidence: [],
40
+ allowedCompletionStates: ["COMPLETE", "BLOCKED"],
41
+ freshnessRequired: false,
42
+ },
43
+ });
44
+
45
+ export function getPolicy(name) {
46
+ const value = POLICIES[name];
47
+ if (!value) {
48
+ const error = new Error(`Unknown policy pack: ${name}`);
49
+ error.code = "E_POLICY_UNKNOWN";
50
+ throw error;
51
+ }
52
+ return {
53
+ schemaVersion: POLICY_SCHEMA_VERSION,
54
+ protocolVersion: PROTOCOL_VERSION,
55
+ ...value,
56
+ requiredGates: [...value.requiredGates],
57
+ requiredEvidence: [...value.requiredEvidence],
58
+ allowedCompletionStates: [...value.allowedCompletionStates],
59
+ };
60
+ }
61
+
62
+ export function listPolicies() {
63
+ return Object.keys(POLICIES).sort().map(getPolicy);
64
+ }
65
+
66
+ export async function validatePolicy(policy, packageRoot) {
67
+ const schema = await readSchema("policy", packageRoot);
68
+ return assertSchema(policy, schema, "policy");
69
+ }
@@ -0,0 +1,409 @@
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, LIFECYCLE_MILESTONES, validateEventLedger } 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
+ import { assertStateIdentity } from "./completion-relationships.js";
15
+
16
+ const PREVIEW_DECISION_LIMIT = 10;
17
+ const PREVIEW_DECISION_MAX_LENGTH = 240;
18
+
19
+ function issue(code, message, artifacts = [], details = {}) {
20
+ return { code, message, artifacts, ...details };
21
+ }
22
+
23
+ function sortIssues(errors) {
24
+ const unique = [...new Map(errors.map((error) => [
25
+ `${error.code}\0${error.artifacts.join("\0")}\0${error.message}`,
26
+ error,
27
+ ])).values()];
28
+ return unique.sort((left, right) => left.code.localeCompare(right.code)
29
+ || left.artifacts.join("\0").localeCompare(right.artifacts.join("\0"))
30
+ || left.message.localeCompare(right.message));
31
+ }
32
+
33
+ function preflightError(code, message, artifacts = []) {
34
+ const error = new Error(message);
35
+ error.code = code;
36
+ error.artifacts = artifacts;
37
+ return error;
38
+ }
39
+
40
+ function sameStringSet(left, right) {
41
+ if (!Array.isArray(left) || !Array.isArray(right)) return false;
42
+ const leftSet = new Set(left);
43
+ const rightSet = new Set(right);
44
+ return leftSet.size === left.length
45
+ && rightSet.size === right.length
46
+ && leftSet.size === rightSet.size
47
+ && [...leftSet].every((value) => rightSet.has(value));
48
+ }
49
+
50
+ export function validatePersistedPreflight(persisted, current) {
51
+ const errors = [];
52
+ if (persisted?.status !== "READY") {
53
+ errors.push(issue("E_PREFLIGHT_NOT_READY", "A persisted READY preflight is required", [ARTIFACT_PATHS.preflight]));
54
+ return errors;
55
+ }
56
+ if (current?.status !== "READY") {
57
+ errors.push(issue("E_PREFLIGHT_NOT_READY", "The current preflight evaluation is not READY", [ARTIFACT_PATHS.preflight]));
58
+ }
59
+ if (persisted.taskId !== current?.taskId) {
60
+ errors.push(issue(
61
+ "E_PREFLIGHT_TASK_MISMATCH",
62
+ "Persisted preflight does not belong to the current task",
63
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
64
+ ));
65
+ }
66
+ if (persisted.fingerprints?.contract !== current?.fingerprints?.contract
67
+ || persisted.contract?.fingerprint !== current?.contract?.fingerprint) {
68
+ errors.push(issue(
69
+ "E_PREFLIGHT_CONTRACT_STALE",
70
+ "Persisted preflight does not match the current contract fingerprint",
71
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.contract],
72
+ ));
73
+ }
74
+ if (persisted.fingerprints?.routing !== current?.fingerprints?.routing
75
+ || persisted.routing?.fingerprint !== current?.routing?.fingerprint) {
76
+ errors.push(issue(
77
+ "E_PREFLIGHT_ROUTE_STALE",
78
+ "Persisted preflight does not match the current routing fingerprint",
79
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
80
+ ));
81
+ }
82
+ if (!sameStringSet(persisted.requiredGates, current?.requiredGates)
83
+ || !sameStringSet(persisted.satisfiedGates, current?.satisfiedGates)) {
84
+ errors.push(issue(
85
+ "E_PREFLIGHT_GATES_STALE",
86
+ "Persisted preflight gate sets do not match the current evaluation",
87
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.gates],
88
+ ));
89
+ }
90
+ return sortIssues(errors);
91
+ }
92
+
93
+ async function readProfile(target) {
94
+ const relativePath = "PROJECT_PROFILE.md";
95
+ await assertSafePath(target, relativePath);
96
+ const filePath = ensureWithin(target, relativePath);
97
+ if (!(await fileExists(filePath))) return { status: "missing", fingerprint: null };
98
+ const bytes = await readBytes(filePath);
99
+ const text = bytes.toString("utf8");
100
+ const mode = text.match(/^profile-mode:\s*([^\s]+)\s*$/m)?.[1] ?? null;
101
+ const status = text.match(/^profile-status:\s*([^\s]+)\s*$/m)?.[1] ?? null;
102
+ return {
103
+ status: status === "verified" && mode !== "template" ? "verified" : "unverified",
104
+ mode,
105
+ profileStatus: status,
106
+ fingerprint: sha256(bytes),
107
+ };
108
+ }
109
+
110
+ async function optionalConfig(target, packageRoot, errors) {
111
+ try {
112
+ const artifact = await readJsonArtifact(target, ARTIFACT_PATHS.config, "config", packageRoot);
113
+ return artifact.value;
114
+ } catch (error) {
115
+ if (error.code === "ARTIFACT_MISSING") return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
116
+ errors.push(issue("E_CONFIG_INVALID", error.message, [ARTIFACT_PATHS.config]));
117
+ return { schemaVersion: 1, protocolVersion: 1, complianceMode: "standard" };
118
+ }
119
+ }
120
+
121
+ async function loadContract(target, packageRoot, errors) {
122
+ try {
123
+ return await readContract(target, packageRoot);
124
+ } catch (error) {
125
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_CONTRACT_MISSING" : "E_CONTRACT_INVALID", error.message, [ARTIFACT_PATHS.contract]));
126
+ return null;
127
+ }
128
+ }
129
+
130
+ async function loadRoute(target, packageRoot, errors) {
131
+ try {
132
+ return await readPersistedRoute(target, packageRoot);
133
+ } catch (error) {
134
+ const code = error.code === "ARTIFACT_MISSING"
135
+ ? "E_ROUTE_MISSING"
136
+ : ["E_ROUTE_REASON_MISSING", "E_ROUTE_INVALID"].includes(error.code) ? error.code : "E_ROUTE_INVALID";
137
+ errors.push(issue(code, error.message, [ARTIFACT_PATHS.route]));
138
+ return null;
139
+ }
140
+ }
141
+
142
+ async function loadSources(target, contract, packageRoot, errors) {
143
+ if (!contract?.value?.sourceRefs?.length) return null;
144
+ let registry;
145
+ try {
146
+ registry = (await readJsonArtifact(target, ARTIFACT_PATHS.sources, "source-registry", packageRoot)).value;
147
+ } catch (error) {
148
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_PROFILE_SOURCE_MISSING" : "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
149
+ return null;
150
+ }
151
+ try {
152
+ assertSourceProvenance(registry, contract.value.sourceRefs);
153
+ } catch (error) {
154
+ errors.push(issue(error.code ?? "E_PROFILE_SOURCE_UNKNOWN", error.message, [ARTIFACT_PATHS.sources]));
155
+ }
156
+ return registry;
157
+ }
158
+
159
+ async function inspectGates(target, contract, route, packageRoot, errors, config = {}) {
160
+ if (!route) return { required: [], satisfied: [], records: {} };
161
+ const guideGates = await requiredGatesForGuides(route.value.guides, packageRoot);
162
+ const required = [...new Set([...guideGates, ...(config.requiredGates ?? [])])].sort();
163
+ const satisfied = [];
164
+ const records = {};
165
+ for (const gate of required) {
166
+ let artifact;
167
+ try {
168
+ artifact = await readGateIfPresent(target, gate, packageRoot);
169
+ } catch (error) {
170
+ errors.push(issue(error.code === "ARTIFACT_MISSING" ? "E_GATE_UNVERIFIED" : "E_GATE_INVALID", error.message, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
171
+ continue;
172
+ }
173
+ if (!artifact) {
174
+ errors.push(issue("E_GATE_UNVERIFIED", `Required gate is missing or unverified: ${gate}`, [`${ARTIFACT_PATHS.gates}/${gate}.json`], { gate }));
175
+ continue;
176
+ }
177
+ records[gate] = artifact;
178
+ if (artifact.value.taskId !== contract?.value?.taskId) {
179
+ errors.push(issue("E_GATE_TASK_MISMATCH", `Gate ${gate} belongs to a different task`, [artifact.path], { gate }));
180
+ continue;
181
+ }
182
+ if (artifact.value.status !== "satisfied") {
183
+ errors.push(issue("E_GATE_UNVERIFIED", `Required gate is ${artifact.value.status}: ${gate}`, [artifact.path], { gate }));
184
+ continue;
185
+ }
186
+ const stale = await validateGateArtifacts(target, artifact.value, packageRoot);
187
+ if (stale.length > 0) {
188
+ errors.push(issue("E_GATE_STALE", `Gate ${gate} references stale artifacts`, [artifact.path], { gate, stale }));
189
+ continue;
190
+ }
191
+ satisfied.push(gate);
192
+ }
193
+ return { required, satisfied: satisfied.sort(), records };
194
+ }
195
+
196
+ export async function evaluatePreflight({ target, packageRoot, strict = false } = {}) {
197
+ const errors = [];
198
+ const profile = await readProfile(target);
199
+ const profileProvenance = await validateProfileSources(target, packageRoot);
200
+ const provenanceErrors = profileProvenance.errors ?? [];
201
+ errors.push(...provenanceErrors);
202
+ const contract = await loadContract(target, packageRoot, errors);
203
+ const route = await loadRoute(target, packageRoot, errors);
204
+ const config = await optionalConfig(target, packageRoot, errors);
205
+ const effectiveStrict = strict || config.complianceMode === "strict";
206
+ if (effectiveStrict && profile.status !== "verified") {
207
+ errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", ["PROJECT_PROFILE.md"]));
208
+ }
209
+ const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
210
+ if (unresolvedDecisions.length > 0) {
211
+ errors.push(issue(
212
+ "E_CONTRACT_UNRESOLVED_DECISION",
213
+ "The current contract contains unresolved blocking decisions.",
214
+ [ARTIFACT_PATHS.contract],
215
+ {
216
+ decisions: unresolvedDecisions
217
+ .slice(0, PREVIEW_DECISION_LIMIT)
218
+ .map((decision) => decision.slice(0, PREVIEW_DECISION_MAX_LENGTH)),
219
+ decisionCount: unresolvedDecisions.length,
220
+ ...(unresolvedDecisions.length > PREVIEW_DECISION_LIMIT ? { decisionsTruncated: true } : {}),
221
+ next: "Resolve the blocking decision with the user or applicable authority, update current-contract.json, then rerun preflight.",
222
+ },
223
+ ));
224
+ }
225
+
226
+ if (route && contract) {
227
+ assertRouteInvariants(route.value);
228
+ if (route.value.contractFingerprint !== undefined && route.value.contractFingerprint !== contract.fingerprint) {
229
+ errors.push(issue("E_ROUTE_STALE", "Routing result was created for a different contract", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.contract]));
230
+ }
231
+ }
232
+
233
+ const sources = await loadSources(target, contract, packageRoot, errors);
234
+ const gates = await inspectGates(target, contract, route, packageRoot, errors, config);
235
+
236
+ let state = null;
237
+ try {
238
+ state = await readWorkState(target, packageRoot);
239
+ } catch (error) {
240
+ errors.push(issue("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]));
241
+ }
242
+ if (state && route && JSON.stringify(state.selectedGuides) !== JSON.stringify(route.value.guides)) {
243
+ errors.push(issue("E_ROUTE_GUIDE_MISMATCH", "work-state.selectedGuides must equal routing-result.guides", [ARTIFACT_PATHS.route, ARTIFACT_PATHS.state]));
244
+ }
245
+ if (state && contract && state.contractFingerprint !== contract.fingerprint) {
246
+ errors.push(issue("E_CONTRACT_STALE", "work-state references a different contract", [ARTIFACT_PATHS.contract, ARTIFACT_PATHS.state]));
247
+ }
248
+
249
+ const sortedErrors = sortIssues(errors);
250
+ const taskId = contract?.value?.taskId ?? state?.taskId ?? "unknown";
251
+ return {
252
+ schemaVersion: 1,
253
+ protocolVersion: 1,
254
+ taskId,
255
+ status: sortedErrors.length === 0 ? "READY" : "BLOCKED",
256
+ profile: { ...profile, provenance: profileProvenance.status },
257
+ contract: contract
258
+ ? { status: "valid", fingerprint: contract.fingerprint }
259
+ : { status: "missing", fingerprint: null },
260
+ routing: route
261
+ ? { status: "valid", fingerprint: route.fingerprint, guides: [...route.value.guides] }
262
+ : { status: "missing", fingerprint: null, guides: [] },
263
+ requiredGates: gates.required,
264
+ satisfiedGates: gates.satisfied,
265
+ errors: sortedErrors,
266
+ fingerprints: {
267
+ contract: contract?.fingerprint ?? null,
268
+ routing: route?.fingerprint ?? null,
269
+ profile: profile.fingerprint,
270
+ },
271
+ ...(config.policy ? {
272
+ policy: {
273
+ name: config.policy,
274
+ complianceMode: config.complianceMode,
275
+ requiredGates: [...(config.requiredGates ?? [])],
276
+ requiredEvidence: [...(config.requiredEvidence ?? [])],
277
+ },
278
+ } : {}),
279
+ ...(sources ? { sources: { status: "valid", fingerprint: null } } : {}),
280
+ };
281
+ }
282
+
283
+ async function readOptionalIdentityArtifact(readArtifact, invalidCode, artifactPath) {
284
+ try {
285
+ return await readArtifact();
286
+ } catch (error) {
287
+ if (error.code === "ARTIFACT_MISSING") return null;
288
+ throw preflightError(invalidCode, error.message, [artifactPath]);
289
+ }
290
+ }
291
+
292
+ async function assertPreflightPersistenceSafety(target, packageRoot, taskId) {
293
+ let state;
294
+ try {
295
+ state = await readWorkState(target, packageRoot);
296
+ } catch (error) {
297
+ throw preflightError("E_STATE_INVALID", error.message, [ARTIFACT_PATHS.state]);
298
+ }
299
+ const contract = await readOptionalIdentityArtifact(
300
+ () => readContract(target, packageRoot),
301
+ "E_CONTRACT_INVALID",
302
+ ARTIFACT_PATHS.contract,
303
+ );
304
+ const route = await readOptionalIdentityArtifact(
305
+ () => readPersistedRoute(target, packageRoot),
306
+ "E_ROUTE_INVALID",
307
+ ARTIFACT_PATHS.route,
308
+ );
309
+ if (state) {
310
+ if (contract || route) assertStateIdentity({ contract, route, state });
311
+ }
312
+
313
+ if (taskId === "unknown") return null;
314
+ const ledger = await validateEventLedger(target, packageRoot);
315
+ if (!ledger.valid) {
316
+ const first = ledger.errors[0];
317
+ throw preflightError(first.code, first.message, [ARTIFACT_PATHS.events]);
318
+ }
319
+ if (ledger.events.some((event) => event.taskId !== taskId)) {
320
+ throw preflightError(
321
+ "E_PHASE_CHRONOLOGY_INVALID",
322
+ "Preflight cannot append events to a ledger owned by a different task",
323
+ [ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract],
324
+ );
325
+ }
326
+ return ledger;
327
+ }
328
+
329
+ const PREFLIGHT_IDENTITY_BARRIER_CODES = new Set([
330
+ "E_CONTRACT_STALE",
331
+ "E_GATE_TASK_MISMATCH",
332
+ "E_ROUTE_STALE",
333
+ "E_STATE_TASK_MISMATCH",
334
+ "E_ROUTE_GUIDE_MISMATCH",
335
+ ]);
336
+
337
+ function assertPreflightResultPersistenceSafety(result) {
338
+ const identityError = result.errors.find((error) => PREFLIGHT_IDENTITY_BARRIER_CODES.has(error.code));
339
+ if (identityError) {
340
+ throw preflightError(identityError.code, identityError.message, identityError.artifacts);
341
+ }
342
+ }
343
+
344
+ function sameReadyPreflightEvent(event, result) {
345
+ return event.fingerprint === result.fingerprints.contract
346
+ && event.details?.routingFingerprint === result.fingerprints.routing
347
+ && sameStringSet(event.details?.requiredGates, result.requiredGates)
348
+ && sameStringSet(event.details?.satisfiedGates, result.satisfiedGates);
349
+ }
350
+
351
+ function planReadyPreflightLifecycleWrite(ledger, result) {
352
+ if (!ledger) return { appendEvents: false };
353
+ const existing = ledger.events.find((event) => event.event === "PREFLIGHT_READY");
354
+ if (existing) {
355
+ if (sameReadyPreflightEvent(existing, result)) return { appendEvents: false };
356
+ throw preflightError(
357
+ "E_PHASE_CHRONOLOGY_INVALID",
358
+ "PREFLIGHT_READY already exists with different READY preflight details; repair the contract, route, or gate lifecycle before refreshing preflight",
359
+ [ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.events, ARTIFACT_PATHS.contract, ARTIFACT_PATHS.route, ARTIFACT_PATHS.gates],
360
+ );
361
+ }
362
+ const lastMilestone = ledger.events.reduce(
363
+ (last, event) => Math.max(last, LIFECYCLE_MILESTONES.indexOf(event.event)),
364
+ -1,
365
+ );
366
+ const routeMilestone = LIFECYCLE_MILESTONES.indexOf("ROUTE_VALIDATED");
367
+ const preflightMilestone = LIFECYCLE_MILESTONES.indexOf("PREFLIGHT_READY");
368
+ if (lastMilestone < routeMilestone) return { appendEvents: false };
369
+ if (lastMilestone !== preflightMilestone - 1) {
370
+ throw preflightError(
371
+ "E_PHASE_CHRONOLOGY_INVALID",
372
+ "PREFLIGHT_READY cannot be appended after the current lifecycle ledger",
373
+ [ARTIFACT_PATHS.events],
374
+ );
375
+ }
376
+ return { appendEvents: true };
377
+ }
378
+
379
+ export async function runPreflight({ target, packageRoot, strict = false, persist = true } = {}) {
380
+ const result = await evaluatePreflight({ target, packageRoot, strict });
381
+ if (persist) {
382
+ const ledger = await assertPreflightPersistenceSafety(target, packageRoot, result.taskId);
383
+ assertPreflightResultPersistenceSafety(result);
384
+ const lifecycleWrite = result.status === "READY"
385
+ ? planReadyPreflightLifecycleWrite(ledger, result)
386
+ : { appendEvents: true };
387
+ await writeJsonArtifact(target, ARTIFACT_PATHS.preflight, result, "preflight", packageRoot);
388
+ if (result.taskId !== "unknown" && lifecycleWrite.appendEvents) {
389
+ for (const gate of result.satisfiedGates) {
390
+ await appendProtocolEvent(target, {
391
+ taskId: result.taskId,
392
+ event: "GATE_SATISFIED",
393
+ details: { gate },
394
+ }, packageRoot);
395
+ }
396
+ await appendProtocolEvent(target, {
397
+ taskId: result.taskId,
398
+ event: result.status === "READY" ? "PREFLIGHT_READY" : "PREFLIGHT_BLOCKED",
399
+ fingerprint: result.fingerprints.contract ?? undefined,
400
+ details: {
401
+ requiredGates: result.requiredGates,
402
+ satisfiedGates: result.satisfiedGates,
403
+ routingFingerprint: result.fingerprints.routing,
404
+ },
405
+ }, packageRoot);
406
+ }
407
+ }
408
+ return result;
409
+ }