@cassiomc1/forgeloop 0.1.0
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 +18 -0
- package/.forgeloop/.gitignore +2 -0
- package/.github/copilot-instructions.md +16 -0
- package/AGENTS.md +16 -0
- package/AGENT_COMPATIBILITY.md +147 -0
- package/CLAUDE.md +14 -0
- package/CONTRACT_COVERAGE.md +27 -0
- package/DELEGATION_PROTOCOL.md +91 -0
- package/ENG/accessibility-eng.md +155 -0
- package/ENG/clean-code-eng.md +223 -0
- package/ENG/design-code-eng.md +511 -0
- package/ENG/games-code-design-web-eng.md +751 -0
- package/ENG/perf-code-eng.md +441 -0
- package/ENG/premium-sites-studio-eng.md +320 -0
- package/ENG/sec-code-eng.md +706 -0
- package/ENG/test-code-eng.md +257 -0
- package/EXECUTION_STATE.md +107 -0
- package/GUIDE_ROUTER.md +274 -0
- package/LICENSE +21 -0
- package/LICENSE-DOCS.md +13 -0
- package/LOOP_ENGINEERING.md +551 -0
- package/LOOP_SYSTEM_DESIGN.md +394 -0
- package/ORCHESTRATOR_INTEGRATION.md +106 -0
- package/PROJECT_PROFILE.md +124 -0
- package/QUALITY_SCORECARD.md +54 -0
- package/README.md +492 -0
- package/TERMINOLOGY.md +21 -0
- package/THIRD_PARTY_NOTICES.md +129 -0
- package/THREAT_MODEL.md +35 -0
- package/package.json +51 -0
- package/schemas/delegated-result.schema.json +33 -0
- package/schemas/evidence.schema.json +15 -0
- package/schemas/execution-receipt.schema.json +46 -0
- package/schemas/routing-input.schema.json +17 -0
- package/schemas/routing-result.schema.json +17 -0
- package/schemas/task-brief.schema.json +24 -0
- package/schemas/work-state.schema.json +46 -0
- package/src/cli.js +341 -0
- package/src/commands/clear-state.js +11 -0
- package/src/commands/doctor.js +165 -0
- package/src/commands/init.js +42 -0
- package/src/commands/inspect.js +17 -0
- package/src/commands/route.js +32 -0
- package/src/commands/status.js +29 -0
- package/src/commands/update.js +109 -0
- package/src/commands/validate-protocol.js +133 -0
- package/src/commands/validate-receipt.js +19 -0
- package/src/commands/validate-state.js +30 -0
- package/src/core/agent-support.js +89 -0
- package/src/core/conformance.js +133 -0
- package/src/core/delegation.js +283 -0
- package/src/core/evidence.js +56 -0
- package/src/core/filesystem.js +122 -0
- package/src/core/inspect.js +115 -0
- package/src/core/json-safety.js +54 -0
- package/src/core/manifest.js +75 -0
- package/src/core/protocol.js +81 -0
- package/src/core/receipt.js +129 -0
- package/src/core/repository.js +19 -0
- package/src/core/router.js +296 -0
- package/src/core/schema-validation.js +179 -0
- package/src/core/templates.js +56 -0
- package/src/core/work-state.js +471 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assertSafePath,
|
|
7
|
+
ensureWithin,
|
|
8
|
+
fileExists,
|
|
9
|
+
readBytes,
|
|
10
|
+
writeFileAtomic,
|
|
11
|
+
} from "./filesystem.js";
|
|
12
|
+
import { GUIDE_IDS, PROTOCOL_VERSION, assertFailureClass, assertWorkPhase, isValidTransition } from "./protocol.js";
|
|
13
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
14
|
+
import { assertSecretFree } from "./receipt.js";
|
|
15
|
+
import { getPackageRoot } from "./templates.js";
|
|
16
|
+
import { currentRepositoryFingerprint } from "./repository.js";
|
|
17
|
+
import { createEvidence } from "./evidence.js";
|
|
18
|
+
import { assertJsonBytes } from "./json-safety.js";
|
|
19
|
+
|
|
20
|
+
export const WORK_STATE_PATH = ".forgeloop/work-state.json";
|
|
21
|
+
|
|
22
|
+
export class WorkStateError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "WorkStateError";
|
|
26
|
+
this.code = "STALE_STATE_FAILURE";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalize(value) {
|
|
31
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
32
|
+
if (value && typeof value === "object") {
|
|
33
|
+
return Object.fromEntries(
|
|
34
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function contractFingerprint(contract) {
|
|
41
|
+
return createHash("sha256")
|
|
42
|
+
.update(JSON.stringify(canonicalize(contract)))
|
|
43
|
+
.digest("hex");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertFingerprint(value, label) {
|
|
47
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
|
|
48
|
+
throw new WorkStateError(`${label} must be a lowercase SHA-256 fingerprint`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeArtifactPath(value) {
|
|
53
|
+
if (typeof value !== "string" || !value) {
|
|
54
|
+
throw new WorkStateError("requiredArtifacts.path must be a non-empty relative path");
|
|
55
|
+
}
|
|
56
|
+
const portable = value.replaceAll("\\", "/");
|
|
57
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable)) {
|
|
58
|
+
throw new WorkStateError(`requiredArtifacts.path must remain relative: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
const normalized = path.posix.normalize(portable);
|
|
61
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
|
|
62
|
+
throw new WorkStateError(`requiredArtifacts.path escapes the task target: ${value}`);
|
|
63
|
+
}
|
|
64
|
+
return normalized.replace(/^\.\//, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeRequiredArtifacts(value) {
|
|
68
|
+
if (value === undefined) return [];
|
|
69
|
+
if (!Array.isArray(value)) throw new WorkStateError("requiredArtifacts must be an array");
|
|
70
|
+
const artifacts = value.map((artifact) => {
|
|
71
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
72
|
+
throw new WorkStateError("requiredArtifacts entries must be objects");
|
|
73
|
+
}
|
|
74
|
+
const normalized = {
|
|
75
|
+
path: normalizeArtifactPath(artifact.path),
|
|
76
|
+
sha256: artifact.sha256,
|
|
77
|
+
};
|
|
78
|
+
assertFingerprint(normalized.sha256, `requiredArtifacts.${normalized.path}.sha256`);
|
|
79
|
+
return normalized;
|
|
80
|
+
});
|
|
81
|
+
if (new Set(artifacts.map((artifact) => artifact.path)).size !== artifacts.length) {
|
|
82
|
+
throw new WorkStateError("requiredArtifacts must not contain duplicate paths");
|
|
83
|
+
}
|
|
84
|
+
return artifacts.sort((left, right) => left.path.localeCompare(right.path));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertStringArray(value, label) {
|
|
88
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
|
|
89
|
+
throw new WorkStateError(`${label} must be an array of non-empty strings`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function assertWorkStateSemantics(state) {
|
|
94
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
95
|
+
throw new WorkStateError("Work state must be a JSON object");
|
|
96
|
+
}
|
|
97
|
+
if (state.schemaVersion !== 1 || state.protocolVersion !== PROTOCOL_VERSION) {
|
|
98
|
+
throw new WorkStateError("Unsupported work-state protocol version");
|
|
99
|
+
}
|
|
100
|
+
if (typeof state.taskId !== "string" || !state.taskId) {
|
|
101
|
+
throw new WorkStateError("Work state taskId is required");
|
|
102
|
+
}
|
|
103
|
+
assertFingerprint(state.contractFingerprint, "contractFingerprint");
|
|
104
|
+
if (!state.repositoryFingerprint || typeof state.repositoryFingerprint !== "object" || Array.isArray(state.repositoryFingerprint)) {
|
|
105
|
+
throw new WorkStateError("repositoryFingerprint is required");
|
|
106
|
+
}
|
|
107
|
+
for (const key of ["branch", "head"]) {
|
|
108
|
+
if (state.repositoryFingerprint[key] !== null && typeof state.repositoryFingerprint[key] !== "string") {
|
|
109
|
+
throw new WorkStateError(`repositoryFingerprint.${key} must be a string or null`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
assertWorkPhase(state.phase);
|
|
113
|
+
assertStringArray(state.selectedGuides, "selectedGuides");
|
|
114
|
+
if (state.selectedGuides.some((guide) => !GUIDE_IDS.includes(guide))) {
|
|
115
|
+
throw new WorkStateError("Work state contains an unknown guide");
|
|
116
|
+
}
|
|
117
|
+
if (new Set(state.selectedGuides).size !== state.selectedGuides.length) {
|
|
118
|
+
throw new WorkStateError("selectedGuides must not contain duplicates");
|
|
119
|
+
}
|
|
120
|
+
assertStringArray(state.completedSteps, "completedSteps");
|
|
121
|
+
assertStringArray(state.pendingSteps, "pendingSteps");
|
|
122
|
+
normalizeRequiredArtifacts(state.requiredArtifacts);
|
|
123
|
+
for (const key of ["checks", "failures", "blockers", "verificationEvidence"]) {
|
|
124
|
+
if (!Array.isArray(state[key])) throw new WorkStateError(`${key} must be an array`);
|
|
125
|
+
}
|
|
126
|
+
if (state.phase === "COMPLETE" && state.verificationEvidence.length === 0) {
|
|
127
|
+
throw new WorkStateError("COMPLETE requires verification evidence");
|
|
128
|
+
}
|
|
129
|
+
if (state.phase === "BLOCKED" && state.blockers.length === 0) {
|
|
130
|
+
throw new WorkStateError("BLOCKED requires a blocker");
|
|
131
|
+
}
|
|
132
|
+
if (state.phase === "CORRECTING" && (typeof state.diagnosedHypothesis !== "string" || !state.diagnosedHypothesis)) {
|
|
133
|
+
throw new WorkStateError("CORRECTING requires a diagnosed hypothesis");
|
|
134
|
+
}
|
|
135
|
+
if (state.previousPhase !== undefined) {
|
|
136
|
+
if (!isValidTransition(state.previousPhase, state.phase)) {
|
|
137
|
+
throw new WorkStateError(`Invalid work-state transition: ${state.previousPhase} -> ${state.phase}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const failure of state.failures) {
|
|
141
|
+
if (failure?.failureClass !== undefined) assertFailureClass(failure.failureClass);
|
|
142
|
+
}
|
|
143
|
+
assertSecretFree(state);
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createWorkState(input) {
|
|
148
|
+
assertSecretFree(input);
|
|
149
|
+
const state = {
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
152
|
+
taskId: input.taskId,
|
|
153
|
+
contractFingerprint: input.contractFingerprint,
|
|
154
|
+
repositoryFingerprint: input.repositoryFingerprint ?? { branch: null, head: null },
|
|
155
|
+
phase: input.phase,
|
|
156
|
+
selectedGuides: [...(input.selectedGuides ?? [])],
|
|
157
|
+
completedSteps: [...(input.completedSteps ?? [])],
|
|
158
|
+
pendingSteps: [...(input.pendingSteps ?? [])],
|
|
159
|
+
requiredArtifacts: normalizeRequiredArtifacts(input.requiredArtifacts),
|
|
160
|
+
checks: [...(input.checks ?? [])],
|
|
161
|
+
failures: [...(input.failures ?? [])],
|
|
162
|
+
blockers: [...(input.blockers ?? [])],
|
|
163
|
+
verificationEvidence: [...(input.verificationEvidence ?? [])],
|
|
164
|
+
lastUpdated: input.lastUpdated ?? new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
if (input.previousPhase !== undefined) state.previousPhase = input.previousPhase;
|
|
167
|
+
if (input.diagnosedHypothesis !== undefined) state.diagnosedHypothesis = input.diagnosedHypothesis;
|
|
168
|
+
return assertWorkStateSemantics(state);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function validateStoredState(state, packageRoot = getPackageRoot()) {
|
|
172
|
+
const schema = await readSchema("work-state", packageRoot);
|
|
173
|
+
assertSchema(state, schema, "work state");
|
|
174
|
+
return assertWorkStateSemantics(state);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function readWorkState(target, packageRoot = getPackageRoot()) {
|
|
178
|
+
await assertSafePath(target, WORK_STATE_PATH);
|
|
179
|
+
const statePath = ensureWithin(target, WORK_STATE_PATH);
|
|
180
|
+
if (!(await fileExists(statePath))) return null;
|
|
181
|
+
let state;
|
|
182
|
+
try {
|
|
183
|
+
const bytes = await readBytes(statePath);
|
|
184
|
+
assertJsonBytes(bytes, WORK_STATE_PATH);
|
|
185
|
+
state = JSON.parse(bytes.toString("utf8"));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new WorkStateError(`Unable to parse ${WORK_STATE_PATH}: ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return await validateStoredState(state, packageRoot);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof WorkStateError) throw error;
|
|
193
|
+
throw new WorkStateError(error.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function readContractFingerprint(target, contractFile) {
|
|
198
|
+
await assertSafePath(target, contractFile);
|
|
199
|
+
const contractPath = ensureWithin(target, contractFile);
|
|
200
|
+
let contract;
|
|
201
|
+
try {
|
|
202
|
+
const bytes = await readBytes(contractPath);
|
|
203
|
+
assertJsonBytes(bytes, contractFile);
|
|
204
|
+
contract = JSON.parse(bytes.toString("utf8"));
|
|
205
|
+
} catch (error) {
|
|
206
|
+
throw new WorkStateError(`Unable to parse contract ${contractFile}: ${error.message}`);
|
|
207
|
+
}
|
|
208
|
+
return { path: contractFile, fingerprint: contractFingerprint(contract) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function writeWorkState(target, state, { dryRun = false, packageRoot = getPackageRoot() } = {}) {
|
|
212
|
+
await validateStoredState(state, packageRoot);
|
|
213
|
+
await assertSafePath(target, WORK_STATE_PATH);
|
|
214
|
+
const statePath = ensureWithin(target, WORK_STATE_PATH);
|
|
215
|
+
await writeFileAtomic(statePath, `${JSON.stringify(state, null, 2)}\n`, { dryRun });
|
|
216
|
+
return state;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function clearWorkState(target) {
|
|
220
|
+
await assertSafePath(target, WORK_STATE_PATH);
|
|
221
|
+
const statePath = ensureWithin(target, WORK_STATE_PATH);
|
|
222
|
+
if (!(await fileExists(statePath))) return { removed: false, path: WORK_STATE_PATH };
|
|
223
|
+
await unlink(statePath);
|
|
224
|
+
return { removed: true, path: WORK_STATE_PATH };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function readRequiredArtifactFingerprints(target, artifacts) {
|
|
228
|
+
const normalized = normalizeRequiredArtifacts(artifacts);
|
|
229
|
+
const current = [];
|
|
230
|
+
for (const artifact of normalized) {
|
|
231
|
+
await assertSafePath(target, artifact.path);
|
|
232
|
+
const artifactPath = ensureWithin(target, artifact.path);
|
|
233
|
+
if (!(await fileExists(artifactPath))) {
|
|
234
|
+
current.push({ path: artifact.path, sha256: null, status: "missing" });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
current.push({
|
|
238
|
+
path: artifact.path,
|
|
239
|
+
sha256: createHash("sha256").update(await readBytes(artifactPath)).digest("hex"),
|
|
240
|
+
status: "present",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeClassificationInput(currentInput = {}) {
|
|
247
|
+
if (!currentInput || typeof currentInput !== "object" || Array.isArray(currentInput)) {
|
|
248
|
+
return { repositoryFingerprint: { branch: null, head: null } };
|
|
249
|
+
}
|
|
250
|
+
const isOptions = Object.prototype.hasOwnProperty.call(currentInput, "repositoryFingerprint")
|
|
251
|
+
|| Object.prototype.hasOwnProperty.call(currentInput, "contractFingerprint")
|
|
252
|
+
|| Object.prototype.hasOwnProperty.call(currentInput, "requiredArtifacts")
|
|
253
|
+
|| Object.prototype.hasOwnProperty.call(currentInput, "maxAgeMs")
|
|
254
|
+
|| Object.prototype.hasOwnProperty.call(currentInput, "now");
|
|
255
|
+
if (isOptions) {
|
|
256
|
+
return {
|
|
257
|
+
repositoryFingerprint: currentInput.repositoryFingerprint ?? { branch: null, head: null },
|
|
258
|
+
contractFingerprint: currentInput.contractFingerprint,
|
|
259
|
+
requiredArtifacts: currentInput.requiredArtifacts,
|
|
260
|
+
maxAgeMs: currentInput.maxAgeMs,
|
|
261
|
+
now: currentInput.now,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { repositoryFingerprint: currentInput };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function classifyWorkState(state, currentInput = {}) {
|
|
268
|
+
if (!state) return { status: "ABSENT", reasons: ["NO_CHECKPOINT"] };
|
|
269
|
+
const current = normalizeClassificationInput(currentInput);
|
|
270
|
+
const reasons = [];
|
|
271
|
+
const warnings = [];
|
|
272
|
+
const saved = state.repositoryFingerprint;
|
|
273
|
+
const repositoryFingerprint = current.repositoryFingerprint ?? { branch: null, head: null };
|
|
274
|
+
const repositoryUnavailable = saved.branch === null
|
|
275
|
+
&& saved.head === null
|
|
276
|
+
&& repositoryFingerprint.branch === null
|
|
277
|
+
&& repositoryFingerprint.head === null;
|
|
278
|
+
const repositoryComparison = repositoryUnavailable
|
|
279
|
+
? "NOT_VERIFIED"
|
|
280
|
+
: saved.branch !== repositoryFingerprint.branch || saved.head !== repositoryFingerprint.head
|
|
281
|
+
? "MISMATCH"
|
|
282
|
+
: "MATCH";
|
|
283
|
+
if (repositoryComparison === "MISMATCH") {
|
|
284
|
+
reasons.push("REPOSITORY_CHANGED");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let contractComparison = "NOT_VERIFIED";
|
|
288
|
+
if (typeof current.contractFingerprint === "string") {
|
|
289
|
+
contractComparison = state.contractFingerprint === current.contractFingerprint ? "MATCH" : "MISMATCH";
|
|
290
|
+
if (contractComparison === "MISMATCH") reasons.push("CONTRACT_CHANGED");
|
|
291
|
+
} else {
|
|
292
|
+
reasons.push("CONTRACT_NOT_VERIFIED");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const savedArtifacts = normalizeRequiredArtifacts(state.requiredArtifacts);
|
|
296
|
+
let artifactComparison = "NOT_APPLICABLE";
|
|
297
|
+
if (savedArtifacts.length > 0) {
|
|
298
|
+
if (!Array.isArray(current.requiredArtifacts)) {
|
|
299
|
+
artifactComparison = "NOT_VERIFIED";
|
|
300
|
+
reasons.push("REQUIRED_ARTIFACTS_NOT_VERIFIED");
|
|
301
|
+
} else {
|
|
302
|
+
const currentArtifacts = new Map(current.requiredArtifacts.map((artifact) => [artifact.path, artifact]));
|
|
303
|
+
const missing = savedArtifacts.some((artifact) => !currentArtifacts.has(artifact.path)
|
|
304
|
+
|| currentArtifacts.get(artifact.path)?.sha256 === null
|
|
305
|
+
|| currentArtifacts.get(artifact.path)?.status === "missing");
|
|
306
|
+
const changed = savedArtifacts.some((artifact) => currentArtifacts.get(artifact.path)?.sha256 !== artifact.sha256);
|
|
307
|
+
if (missing) {
|
|
308
|
+
artifactComparison = "MISSING";
|
|
309
|
+
reasons.push("REQUIRED_ARTIFACT_MISSING");
|
|
310
|
+
} else if (changed) {
|
|
311
|
+
artifactComparison = "MISMATCH";
|
|
312
|
+
reasons.push("REQUIRED_ARTIFACT_CHANGED");
|
|
313
|
+
} else {
|
|
314
|
+
artifactComparison = "MATCH";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const now = current.now ?? Date.now();
|
|
320
|
+
const updatedAt = Date.parse(state.lastUpdated);
|
|
321
|
+
let stateAgeMs = null;
|
|
322
|
+
if (Number.isFinite(updatedAt) && Number.isFinite(now) && now >= updatedAt) {
|
|
323
|
+
stateAgeMs = now - updatedAt;
|
|
324
|
+
if (Number.isFinite(current.maxAgeMs) && current.maxAgeMs >= 0 && stateAgeMs > current.maxAgeMs) {
|
|
325
|
+
warnings.push("CHECKPOINT_OLD");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
status: reasons.length > 0 ? "REVALIDATION_REQUIRED" : "FRESH",
|
|
330
|
+
reasons,
|
|
331
|
+
warnings,
|
|
332
|
+
repositoryComparison,
|
|
333
|
+
contractComparison,
|
|
334
|
+
artifactComparison,
|
|
335
|
+
stateAgeMs,
|
|
336
|
+
state,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function classifyLoadedWorkState({ target, state, contractFile = null, maxAgeMs } = {}) {
|
|
341
|
+
const repository = await currentRepositoryFingerprint(target);
|
|
342
|
+
|
|
343
|
+
let contract = null;
|
|
344
|
+
let contractError = null;
|
|
345
|
+
if (contractFile) {
|
|
346
|
+
try {
|
|
347
|
+
contract = await readContractFingerprint(target, contractFile);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
contractError = error.message;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let currentArtifacts;
|
|
354
|
+
let artifactError = null;
|
|
355
|
+
if ((state.requiredArtifacts ?? []).length > 0) {
|
|
356
|
+
try {
|
|
357
|
+
currentArtifacts = await readRequiredArtifactFingerprints(target, state.requiredArtifacts);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
artifactError = error.message;
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
currentArtifacts = [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const classification = classifyWorkState(state, {
|
|
366
|
+
repositoryFingerprint: repository,
|
|
367
|
+
contractFingerprint: contract?.fingerprint,
|
|
368
|
+
requiredArtifacts: currentArtifacts,
|
|
369
|
+
maxAgeMs,
|
|
370
|
+
});
|
|
371
|
+
if (contractError) {
|
|
372
|
+
classification.contractComparison = "INVALID";
|
|
373
|
+
classification.reasons = classification.reasons.filter((reason) => reason !== "CONTRACT_NOT_VERIFIED");
|
|
374
|
+
classification.reasons.push("CONTRACT_INVALID");
|
|
375
|
+
}
|
|
376
|
+
if (artifactError) {
|
|
377
|
+
classification.artifactComparison = "NOT_VERIFIED";
|
|
378
|
+
classification.reasons.push("REQUIRED_ARTIFACTS_NOT_VERIFIED");
|
|
379
|
+
}
|
|
380
|
+
classification.reasons = [...new Set(classification.reasons)];
|
|
381
|
+
return {
|
|
382
|
+
...classification,
|
|
383
|
+
repository,
|
|
384
|
+
error: contractError ?? artifactError,
|
|
385
|
+
contract: {
|
|
386
|
+
path: contractFile,
|
|
387
|
+
status: contractError ? "INVALID" : contract ? "OBSERVED" : "NOT_VERIFIED",
|
|
388
|
+
},
|
|
389
|
+
evidence: [
|
|
390
|
+
createEvidence({
|
|
391
|
+
kind: classification.repositoryComparison === "NOT_VERIFIED" ? "NOT_VERIFIED" : "OBSERVED",
|
|
392
|
+
source: "repository fingerprint",
|
|
393
|
+
result: classification.repositoryComparison,
|
|
394
|
+
}),
|
|
395
|
+
createEvidence({
|
|
396
|
+
kind: classification.contractComparison === "NOT_VERIFIED" ? "NOT_VERIFIED" : "OBSERVED",
|
|
397
|
+
source: contractFile ?? "current contract",
|
|
398
|
+
result: classification.contractComparison,
|
|
399
|
+
}),
|
|
400
|
+
...(classification.artifactComparison !== "NOT_APPLICABLE"
|
|
401
|
+
? [createEvidence({
|
|
402
|
+
kind: classification.artifactComparison === "NOT_VERIFIED" ? "NOT_VERIFIED" : "OBSERVED",
|
|
403
|
+
source: "required artifacts",
|
|
404
|
+
result: classification.artifactComparison,
|
|
405
|
+
})]
|
|
406
|
+
: []),
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function readAndClassifyWorkState({ target, packageRoot = getPackageRoot(), contractFile = null, maxAgeMs } = {}) {
|
|
412
|
+
let state = null;
|
|
413
|
+
try {
|
|
414
|
+
state = await readWorkState(target, packageRoot);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return {
|
|
417
|
+
path: WORK_STATE_PATH,
|
|
418
|
+
present: true,
|
|
419
|
+
status: "INVALID",
|
|
420
|
+
reasons: ["STATE_INVALID"],
|
|
421
|
+
warnings: [],
|
|
422
|
+
error: error.message,
|
|
423
|
+
state: null,
|
|
424
|
+
phase: null,
|
|
425
|
+
completed: [],
|
|
426
|
+
pending: [],
|
|
427
|
+
repository: await currentRepositoryFingerprint(target),
|
|
428
|
+
contractComparison: "NOT_VERIFIED",
|
|
429
|
+
artifactComparison: "NOT_VERIFIED",
|
|
430
|
+
contract: { path: contractFile, status: "NOT_VERIFIED" },
|
|
431
|
+
evidence: [createEvidence({ kind: "BLOCKED", source: WORK_STATE_PATH, result: error.message })],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!state) {
|
|
436
|
+
const repository = await currentRepositoryFingerprint(target);
|
|
437
|
+
return {
|
|
438
|
+
path: WORK_STATE_PATH,
|
|
439
|
+
present: false,
|
|
440
|
+
status: "ABSENT",
|
|
441
|
+
reasons: ["NO_CHECKPOINT"],
|
|
442
|
+
warnings: [],
|
|
443
|
+
error: null,
|
|
444
|
+
state: null,
|
|
445
|
+
phase: null,
|
|
446
|
+
completed: [],
|
|
447
|
+
pending: [],
|
|
448
|
+
repository,
|
|
449
|
+
contractComparison: "NOT_VERIFIED",
|
|
450
|
+
artifactComparison: "NOT_APPLICABLE",
|
|
451
|
+
contract: { path: contractFile, status: contractFile ? "NOT_USED" : "NOT_VERIFIED" },
|
|
452
|
+
evidence: [createEvidence({
|
|
453
|
+
kind: "NOT_VERIFIED",
|
|
454
|
+
source: WORK_STATE_PATH,
|
|
455
|
+
result: "No work-state checkpoint is present",
|
|
456
|
+
})],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const classification = await classifyLoadedWorkState({ target, state, contractFile, maxAgeMs });
|
|
461
|
+
return {
|
|
462
|
+
path: WORK_STATE_PATH,
|
|
463
|
+
present: true,
|
|
464
|
+
...classification,
|
|
465
|
+
phase: state.phase,
|
|
466
|
+
completed: state.completedSteps,
|
|
467
|
+
pending: state.pendingSteps,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export { currentRepositoryFingerprint };
|