@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.
- package/.cursor/rules/project-loop.mdc +7 -1
- package/.github/copilot-instructions.md +6 -0
- package/AGENTS.md +9 -1
- package/AGENT_COMPATIBILITY.md +14 -1
- package/CLAUDE.md +7 -1
- package/ENG/accessibility-eng.md +5 -0
- package/ENG/clean-code-eng.md +1 -0
- package/ENG/design-code-eng.md +5 -0
- package/ENG/games-code-design-web-eng.md +6 -0
- package/ENG/perf-code-eng.md +3 -0
- package/ENG/premium-sites-studio-eng.md +9 -0
- package/ENG/sec-code-eng.md +5 -0
- package/ENG/test-code-eng.md +3 -0
- package/GUIDE_ROUTER.md +16 -0
- package/LOOP_ENGINEERING.md +139 -4
- package/ORCHESTRATOR_INTEGRATION.md +15 -0
- package/QUALITY_SCORECARD.md +4 -0
- package/README.md +43 -1
- package/THREAT_MODEL.md +6 -0
- package/conformance/README.md +42 -0
- package/conformance/backend-auth/EXPECTED_ROUTE.json +7 -0
- package/conformance/backend-auth/REQUEST.md +4 -0
- package/conformance/backend-auth/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/backend-auth/REQUIRED_GATES.json +3 -0
- package/conformance/blind-premium-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/blind-premium-website/REQUEST.md +6 -0
- package/conformance/blind-premium-website/REQUIRED_EVIDENCE.json +14 -0
- package/conformance/blind-premium-website/REQUIRED_GATES.json +3 -0
- package/conformance/complete-website/EXPECTED_ROUTE.json +7 -0
- package/conformance/complete-website/REQUEST.md +6 -0
- package/conformance/complete-website/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/complete-website/REQUIRED_GATES.json +3 -0
- package/conformance/docs-only/EXPECTED_ROUTE.json +7 -0
- package/conformance/docs-only/REQUEST.md +4 -0
- package/conformance/docs-only/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/docs-only/REQUIRED_GATES.json +3 -0
- package/conformance/runs/2026-08-11-codex-first-live.md +98 -0
- package/conformance/runs/2026-08-11-codex-second-live.md +87 -0
- package/conformance/simple-bug/EXPECTED_ROUTE.json +7 -0
- package/conformance/simple-bug/REQUEST.md +4 -0
- package/conformance/simple-bug/REQUIRED_EVIDENCE.json +3 -0
- package/conformance/simple-bug/REQUIRED_GATES.json +3 -0
- package/package.json +5 -4
- package/schemas/activation.schema.json +15 -0
- package/schemas/check.schema.json +23 -0
- package/schemas/config.schema.json +16 -0
- package/schemas/current-contract.schema.json +50 -0
- package/schemas/event.schema.json +20 -0
- package/schemas/evidence-coverage.schema.json +17 -0
- package/schemas/execution-receipt.schema.json +10 -0
- package/schemas/gate.schema.json +32 -0
- package/schemas/policy.schema.json +19 -0
- package/schemas/preflight.schema.json +23 -0
- package/schemas/routing-result.schema.json +1 -0
- package/schemas/source-registry.schema.json +29 -0
- package/schemas/task-bundle.schema.json +14 -0
- package/schemas/work-state.schema.json +7 -1
- package/src/cli.js +230 -9
- package/src/commands/activate.js +11 -0
- package/src/commands/advance.js +12 -0
- package/src/commands/audit.js +12 -0
- package/src/commands/bundle.js +10 -0
- package/src/commands/complete.js +18 -0
- package/src/commands/policy.js +19 -0
- package/src/commands/preflight.js +12 -0
- package/src/commands/prepare-completion.js +18 -0
- package/src/commands/record-check.js +20 -0
- package/src/commands/report.js +10 -0
- package/src/commands/route.js +14 -2
- package/src/commands/validate-receipt.js +7 -6
- package/src/core/activation.js +16 -0
- package/src/core/artifacts.js +132 -0
- package/src/core/audit.js +88 -0
- package/src/core/bundles.js +120 -0
- package/src/core/checks.js +115 -0
- package/src/core/completion-artifacts.js +281 -0
- package/src/core/completion.js +295 -0
- package/src/core/config.js +35 -0
- package/src/core/conformance.js +7 -0
- package/src/core/contract.js +141 -0
- package/src/core/coverage.js +89 -0
- package/src/core/events.js +124 -0
- package/src/core/gate-artifact.js +54 -0
- package/src/core/gates.js +55 -0
- package/src/core/guide-metadata.js +62 -0
- package/src/core/phase.js +88 -0
- package/src/core/policies.js +69 -0
- package/src/core/preflight.js +246 -0
- package/src/core/profile.js +48 -0
- package/src/core/protocol.js +34 -0
- package/src/core/receipt.js +40 -1
- package/src/core/report.js +49 -0
- package/src/core/repository.js +18 -0
- package/src/core/route-artifact.js +33 -0
- package/src/core/router.js +16 -8
- package/src/core/schema-validation.js +11 -0
- package/src/core/sources.js +86 -0
- package/src/core/templates.js +22 -4
- package/src/core/work-state.js +41 -13
- /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
|
+
}
|
package/src/core/protocol.js
CHANGED
|
@@ -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",
|
package/src/core/receipt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/core/repository.js
CHANGED
|
@@ -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
|
+
}
|
package/src/core/router.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
284
|
+
throw invariantError("E_ROUTE_INVALID", `Route result contains unknown excluded guide: ${guide}`);
|
|
277
285
|
}
|
|
278
286
|
if (result.guides.includes(guide)) {
|
|
279
|
-
throw
|
|
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
|
|
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
|
|
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 {
|