@cassiomc1/forgeloop 1.1.1 → 1.2.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 +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/DOCS_INDEX.md +3 -0
- package/ENG/design-code-eng.md +124 -0
- package/ENG/premium-sites-studio-eng.md +28 -0
- package/ENG/taste-frontend-eng.md +3 -2
- package/ENG/test-code-eng.md +45 -0
- package/LOOP_ENGINEERING.md +74 -0
- package/LOOP_SYSTEM_DESIGN.md +9 -5
- package/ORCHESTRATOR_INTEGRATION.md +41 -6
- package/PROTOCOL_INTEGRATION.md +13 -0
- package/README.md +40 -6
- package/TERMINOLOGY.md +10 -0
- package/THIRD_PARTY_NOTICES.md +58 -1
- package/THREAT_MODEL.md +12 -1
- package/docs/ARTIFACT_REFERENCE.md +152 -2
- package/docs/CLI_REFERENCE.md +346 -30
- package/docs/CROSS_HARNESS_CONTINUITY.md +1 -0
- package/docs/DOCUMENTATION_GUIDE.md +41 -4
- package/docs/GETTING_STARTED.md +39 -8
- package/docs/RECIPES.md +66 -7
- package/docs/TROUBLESHOOTING.md +279 -6
- package/package.json +1 -1
- package/schemas/policy-baseline.schema.json +26 -0
- package/schemas/policy-discovery.schema.json +45 -0
- package/schemas/policy-lock.schema.json +16 -0
- package/schemas/policy-rules.schema.json +48 -0
- package/schemas/policy-snapshot.schema.json +16 -0
- package/src/cli.js +102 -1
- package/src/commands/baseline.js +120 -0
- package/src/commands/init.js +304 -6
- package/src/commands/next.js +15 -1
- package/src/commands/policy-diff.js +51 -0
- package/src/commands/policy-discover.js +42 -0
- package/src/commands/policy-status.js +33 -0
- package/src/commands/profile-interview.js +50 -0
- package/src/commands/progress.js +51 -0
- package/src/commands/reconcile-closure.js +49 -0
- package/src/commands/record-decision-criterion.js +34 -0
- package/src/commands/record-diagnosis.js +49 -0
- package/src/commands/rule-verify.js +36 -0
- package/src/commands/validate-receipt.js +38 -3
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +24 -0
- package/src/core/cli-command-definitions.js +163 -7
- package/src/core/cli-metadata.js +1 -1
- package/src/core/completion-artifacts.js +29 -3
- package/src/core/completion.js +101 -10
- package/src/core/diagnosis-model.js +214 -0
- package/src/core/diagnosis.js +171 -0
- package/src/core/error-codes.js +292 -0
- package/src/core/events.js +47 -1
- package/src/core/execution-prerequisites.js +38 -20
- package/src/core/execution.js +20 -3
- package/src/core/native-adapters.js +14 -4
- package/src/core/next-action-model.js +40 -5
- package/src/core/next-action.js +234 -91
- package/src/core/phase.js +29 -0
- package/src/core/policy-adapters.js +276 -0
- package/src/core/policy-baseline.js +144 -0
- package/src/core/policy-diff.js +133 -0
- package/src/core/policy-discovery.js +225 -0
- package/src/core/policy-engine.js +533 -0
- package/src/core/policy-mutation.js +139 -0
- package/src/core/preflight-consistency.js +23 -15
- package/src/core/preflight-model.js +10 -2
- package/src/core/preflight.js +65 -1
- package/src/core/progress.js +143 -0
- package/src/core/protocol.js +8 -0
- package/src/core/reconcile-closure.js +173 -0
- package/src/core/schema-validation.js +6 -0
- package/src/core/settlement-model.js +85 -0
- package/src/core/settlement.js +78 -0
- package/src/core/task-context.js +11 -0
- package/src/core/task-discovery.js +67 -1
- package/src/core/task-paths.js +9 -0
- package/src/core/templates.js +5 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
2
|
+
import { readWorkState, writeWorkState } from "./work-state.js";
|
|
3
|
+
import {
|
|
4
|
+
DIAGNOSIS_INFORMATION_GAIN,
|
|
5
|
+
assertDiagnosisDetails,
|
|
6
|
+
classifyDiagnosisInformationGain,
|
|
7
|
+
createDiagnosisDetails,
|
|
8
|
+
currentCycleDiagnosis,
|
|
9
|
+
diagnosisEventsForTask,
|
|
10
|
+
diagnosisFingerprint,
|
|
11
|
+
normalizeDiagnosisText,
|
|
12
|
+
} from "./diagnosis-model.js";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
DIAGNOSIS_INFORMATION_GAIN,
|
|
16
|
+
assertDiagnosisDetails,
|
|
17
|
+
classifyDiagnosisInformationGain,
|
|
18
|
+
createDiagnosisDetails,
|
|
19
|
+
currentCycleDiagnosis,
|
|
20
|
+
diagnosisEventsForTask,
|
|
21
|
+
diagnosisFingerprint,
|
|
22
|
+
normalizeDiagnosisText,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export async function recordDiagnosis({
|
|
26
|
+
target,
|
|
27
|
+
packageRoot,
|
|
28
|
+
hypothesis,
|
|
29
|
+
failureClass,
|
|
30
|
+
evidenceRefs,
|
|
31
|
+
settledBy,
|
|
32
|
+
nextSafeAction,
|
|
33
|
+
taskId = null,
|
|
34
|
+
statePath = null,
|
|
35
|
+
eventsPath = null,
|
|
36
|
+
}) {
|
|
37
|
+
const state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
38
|
+
if (!state) {
|
|
39
|
+
const error = new Error("Work state not found");
|
|
40
|
+
error.code = "E_STATE_MISSING";
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (state.phase !== "DIAGNOSING") {
|
|
44
|
+
const error = new Error(`Recording a diagnosis requires phase DIAGNOSING, currently ${state.phase}`);
|
|
45
|
+
error.code = "E_PHASE_PREREQUISITE_MISSING";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
if (typeof state.verificationCycle !== "number" || state.verificationCycle < 1) {
|
|
49
|
+
const error = new Error("Active verification cycle must be an integer >= 1");
|
|
50
|
+
error.code = "E_DIAGNOSIS_INVALID";
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: taskId ?? null, eventsPath });
|
|
55
|
+
if (!ledger.valid) {
|
|
56
|
+
const first = ledger.errors[0];
|
|
57
|
+
const error = new Error(first.message);
|
|
58
|
+
error.code = first.code;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const verificationStarted = ledger.events.some(
|
|
63
|
+
(e) => e.taskId === state.taskId && e.event === "VERIFICATION_STARTED",
|
|
64
|
+
);
|
|
65
|
+
if (!verificationStarted) {
|
|
66
|
+
const error = new Error("No VERIFICATION_STARTED event found for active cycle");
|
|
67
|
+
error.code = "E_PHASE_CHRONOLOGY_INVALID";
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const currentCycleChecks = (state.checks ?? []).filter(
|
|
72
|
+
(c) => c.details?.verificationCycle === state.verificationCycle,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const cleanRefs = (evidenceRefs ?? []).map((r) => String(r).trim()).filter(Boolean);
|
|
76
|
+
if (cleanRefs.length === 0) {
|
|
77
|
+
const error = new Error("record-diagnosis requires at least one evidenceRef");
|
|
78
|
+
error.code = "E_DIAGNOSIS_EVIDENCE_INVALID";
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const matchedChecks = [];
|
|
83
|
+
for (const ref of cleanRefs) {
|
|
84
|
+
const check = currentCycleChecks.find((c) => (c.id === ref || c.checkId === ref));
|
|
85
|
+
if (!check) {
|
|
86
|
+
const error = new Error(`Evidence reference "${ref}" does not match any check from verification cycle ${state.verificationCycle}`);
|
|
87
|
+
error.code = "E_DIAGNOSIS_EVIDENCE_INVALID";
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
matchedChecks.push(check);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasFailedOrBlocked = matchedChecks.some((c) => c.status === "failed" || c.status === "blocked");
|
|
94
|
+
if (!hasFailedOrBlocked) {
|
|
95
|
+
const error = new Error("Diagnosis evidenceRefs must include at least one failed or blocked check from the current cycle");
|
|
96
|
+
error.code = "E_DIAGNOSIS_EVIDENCE_INVALID";
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const existingEvent = currentCycleDiagnosis(
|
|
101
|
+
ledger.events,
|
|
102
|
+
state.taskId,
|
|
103
|
+
state.verificationCycle,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const requestedFingerprint = diagnosisFingerprint({
|
|
107
|
+
failureClass,
|
|
108
|
+
hypothesis,
|
|
109
|
+
evidenceRefs: cleanRefs,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (existingEvent?.details?.diagnosisFingerprint === requestedFingerprint) {
|
|
113
|
+
const updatedState = {
|
|
114
|
+
...state,
|
|
115
|
+
diagnosedHypothesis: existingEvent.details.hypothesis,
|
|
116
|
+
lastUpdated: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
await writeWorkState(target, updatedState, {
|
|
119
|
+
packageRoot,
|
|
120
|
+
taskId: taskId ?? null,
|
|
121
|
+
statePath,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
event: existingEvent,
|
|
125
|
+
state: updatedState,
|
|
126
|
+
diagnosis: existingEvent.details,
|
|
127
|
+
idempotent: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const taskDiagnosisEvents = diagnosisEventsForTask(ledger.events, state.taskId);
|
|
132
|
+
const previousEvent = taskDiagnosisEvents.at(-1) ?? null;
|
|
133
|
+
const previousDetails = previousEvent?.details ?? null;
|
|
134
|
+
|
|
135
|
+
const details = createDiagnosisDetails(
|
|
136
|
+
{
|
|
137
|
+
verificationCycle: state.verificationCycle,
|
|
138
|
+
failureClass,
|
|
139
|
+
hypothesis,
|
|
140
|
+
evidenceRefs: cleanRefs,
|
|
141
|
+
settledBy,
|
|
142
|
+
nextSafeAction,
|
|
143
|
+
},
|
|
144
|
+
previousDetails,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const event = await appendProtocolEvent(
|
|
148
|
+
target,
|
|
149
|
+
{
|
|
150
|
+
taskId: state.taskId,
|
|
151
|
+
event: "DIAGNOSIS_RECORDED",
|
|
152
|
+
details,
|
|
153
|
+
},
|
|
154
|
+
packageRoot,
|
|
155
|
+
{ taskId: taskId ?? null, eventsPath },
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const updatedState = {
|
|
159
|
+
...state,
|
|
160
|
+
diagnosedHypothesis: hypothesis.trim(),
|
|
161
|
+
lastUpdated: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
await writeWorkState(target, updatedState, { packageRoot, taskId: taskId ?? null, statePath });
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
event,
|
|
167
|
+
state: updatedState,
|
|
168
|
+
diagnosis: details,
|
|
169
|
+
idempotent: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
package/src/core/error-codes.js
CHANGED
|
@@ -30,6 +30,24 @@ export const E_TASK_LAYOUT_LEGACY = "E_TASK_LAYOUT_LEGACY";
|
|
|
30
30
|
export const E_TASK_MIGRATION_INVALID = "E_TASK_MIGRATION_INVALID";
|
|
31
31
|
export const E_TASK_MIGRATION_IDENTITY_MISMATCH = "E_TASK_MIGRATION_IDENTITY_MISMATCH";
|
|
32
32
|
|
|
33
|
+
export const E_DIAGNOSIS_REQUIRED = "E_DIAGNOSIS_REQUIRED";
|
|
34
|
+
export const E_DIAGNOSIS_INVALID = "E_DIAGNOSIS_INVALID";
|
|
35
|
+
export const E_DIAGNOSIS_EVIDENCE_INVALID = "E_DIAGNOSIS_EVIDENCE_INVALID";
|
|
36
|
+
export const E_DIAGNOSIS_CYCLE_MISMATCH = "E_DIAGNOSIS_CYCLE_MISMATCH";
|
|
37
|
+
export const E_DIAGNOSIS_NO_NEW_INFORMATION = "E_DIAGNOSIS_NO_NEW_INFORMATION";
|
|
38
|
+
export const E_PROGRESS_STALLED = "E_PROGRESS_STALLED";
|
|
39
|
+
export const E_DECISION_CRITERION_INVALID = "E_DECISION_CRITERION_INVALID";
|
|
40
|
+
export const E_DECISION_NOT_UNRESOLVED = "E_DECISION_NOT_UNRESOLVED";
|
|
41
|
+
|
|
42
|
+
export const E_RECONCILE_NOT_STALE = "E_RECONCILE_NOT_STALE";
|
|
43
|
+
export const E_RECONCILE_PHASE_INVALID = "E_RECONCILE_PHASE_INVALID";
|
|
44
|
+
export const E_RECONCILE_UNSUPPORTED_DRIFT = "E_RECONCILE_UNSUPPORTED_DRIFT";
|
|
45
|
+
export const E_RECONCILE_LEDGER_INVALID = "E_RECONCILE_LEDGER_INVALID";
|
|
46
|
+
export const E_RECONCILE_REQUIREMENT_UNKNOWN = "E_RECONCILE_REQUIREMENT_UNKNOWN";
|
|
47
|
+
export const E_RECONCILE_EVIDENCE_FAILED = "E_RECONCILE_EVIDENCE_FAILED";
|
|
48
|
+
export const E_REPOSITORY_CHANGED = "E_REPOSITORY_CHANGED";
|
|
49
|
+
export const E_STATE_REVALIDATION_REQUIRED = "E_STATE_REVALIDATION_REQUIRED";
|
|
50
|
+
|
|
33
51
|
/**
|
|
34
52
|
* Public, stable ForgeLoop error and reason codes documented for users and harnesses.
|
|
35
53
|
*/
|
|
@@ -160,8 +178,257 @@ export const PUBLIC_ERROR_CODES = Object.freeze({
|
|
|
160
178
|
meaning: "Modified paths in repository exceed the declared task write claims.",
|
|
161
179
|
safeResolution: "Update write claims with forgeloop task-scope or revert out-of-scope modifications.",
|
|
162
180
|
}),
|
|
181
|
+
E_RECONCILE_NOT_STALE: Object.freeze({
|
|
182
|
+
code: "E_RECONCILE_NOT_STALE",
|
|
183
|
+
category: "lifecycle",
|
|
184
|
+
classification: "PUBLIC_STABLE",
|
|
185
|
+
meaning: "reconcile-closure was invoked for a work-state checkpoint that is already fresh.",
|
|
186
|
+
safeResolution: "No reconciliation is required; continue the normal lifecycle.",
|
|
187
|
+
}),
|
|
188
|
+
E_RECONCILE_PHASE_INVALID: Object.freeze({
|
|
189
|
+
code: "E_RECONCILE_PHASE_INVALID",
|
|
190
|
+
category: "lifecycle",
|
|
191
|
+
classification: "PUBLIC_STABLE",
|
|
192
|
+
meaning: "reconcile-closure was invoked for a task that is not EXECUTING.",
|
|
193
|
+
safeResolution: "reconcile-closure supports EXECUTING tasks whose objective is already satisfied.",
|
|
194
|
+
}),
|
|
195
|
+
E_RECONCILE_UNSUPPORTED_DRIFT: Object.freeze({
|
|
196
|
+
code: "E_RECONCILE_UNSUPPORTED_DRIFT",
|
|
197
|
+
category: "freshness",
|
|
198
|
+
classification: "PUBLIC_STABLE",
|
|
199
|
+
meaning: "Work-state drift includes kinds other than REPOSITORY_CHANGED (contract or required-artifact drift).",
|
|
200
|
+
safeResolution: "Resolve contract or artifact drift through their dedicated recovery surfaces; reconcile-closure only refreshes repository fingerprint drift.",
|
|
201
|
+
}),
|
|
202
|
+
E_RECONCILE_LEDGER_INVALID: Object.freeze({
|
|
203
|
+
code: "E_RECONCILE_LEDGER_INVALID",
|
|
204
|
+
category: "integrity",
|
|
205
|
+
classification: "PUBLIC_STABLE",
|
|
206
|
+
meaning: "The append-only event ledger is not valid, so reconciliation cannot be recorded.",
|
|
207
|
+
safeResolution: "Inspect the ledger errors and repair before reconciling.",
|
|
208
|
+
}),
|
|
209
|
+
E_RECONCILE_REQUIREMENT_UNKNOWN: Object.freeze({
|
|
210
|
+
code: "E_RECONCILE_REQUIREMENT_UNKNOWN",
|
|
211
|
+
category: "verification",
|
|
212
|
+
classification: "PUBLIC_STABLE",
|
|
213
|
+
meaning: "The supplied check id and requirement text do not exactly match a contract verification item of type VERIFICATION.",
|
|
214
|
+
safeResolution: "Supply the exact id and requirement text of an existing contract verification item.",
|
|
215
|
+
}),
|
|
216
|
+
E_RECONCILE_EVIDENCE_FAILED: Object.freeze({
|
|
217
|
+
code: "E_RECONCILE_EVIDENCE_FAILED",
|
|
218
|
+
category: "verification",
|
|
219
|
+
classification: "PUBLIC_STABLE",
|
|
220
|
+
meaning: "The executed objective-satisfaction evidence command did not pass.",
|
|
221
|
+
safeResolution: "Inspect the execution artifact; reconciliation is refused until evidence passes in the current repository.",
|
|
222
|
+
}),
|
|
223
|
+
E_REPOSITORY_CHANGED: Object.freeze({
|
|
224
|
+
code: "E_REPOSITORY_CHANGED",
|
|
225
|
+
category: "freshness",
|
|
226
|
+
classification: "PUBLIC_STABLE",
|
|
227
|
+
meaning: "The repository fingerprint (branch or HEAD) moved after the work-state checkpoint was recorded.",
|
|
228
|
+
safeResolution: "If the task objective is already satisfied in the current repository, run forgeloop reconcile-closure; otherwise resume from a checkpoint that matches the current repository.",
|
|
229
|
+
}),
|
|
230
|
+
E_STATE_REVALIDATION_REQUIRED: Object.freeze({
|
|
231
|
+
code: "E_STATE_REVALIDATION_REQUIRED",
|
|
232
|
+
category: "freshness",
|
|
233
|
+
classification: "PUBLIC_STABLE",
|
|
234
|
+
meaning: "The work-state checkpoint must be revalidated before the lifecycle can continue.",
|
|
235
|
+
safeResolution: "Run forgeloop reconcile-closure for externally satisfied EXECUTING tasks, or inspect the freshness reasons for other drift.",
|
|
236
|
+
}),
|
|
237
|
+
E_DIAGNOSIS_REQUIRED: Object.freeze({
|
|
238
|
+
code: "E_DIAGNOSIS_REQUIRED",
|
|
239
|
+
category: "diagnosis",
|
|
240
|
+
classification: "PUBLIC_STABLE",
|
|
241
|
+
meaning: "Current correction cycle has no append-only diagnosis record.",
|
|
242
|
+
safeResolution: "Run forgeloop record-diagnosis with current failed evidence before correcting.",
|
|
243
|
+
}),
|
|
244
|
+
E_DIAGNOSIS_INVALID: Object.freeze({
|
|
245
|
+
code: "E_DIAGNOSIS_INVALID",
|
|
246
|
+
category: "diagnosis",
|
|
247
|
+
classification: "PUBLIC_STABLE",
|
|
248
|
+
meaning: "Diagnosis record details or parameters are malformed.",
|
|
249
|
+
safeResolution: "Provide valid failureClass, hypothesis, evidenceRefs, settledBy, and nextSafeAction.",
|
|
250
|
+
}),
|
|
251
|
+
E_DIAGNOSIS_EVIDENCE_INVALID: Object.freeze({
|
|
252
|
+
code: "E_DIAGNOSIS_EVIDENCE_INVALID",
|
|
253
|
+
category: "diagnosis",
|
|
254
|
+
classification: "PUBLIC_STABLE",
|
|
255
|
+
meaning: "Referenced diagnosis evidence is missing or has no failed checks in the current cycle.",
|
|
256
|
+
safeResolution: "Reference at least one failed or blocked check ID from the active verification cycle.",
|
|
257
|
+
}),
|
|
258
|
+
E_DIAGNOSIS_CYCLE_MISMATCH: Object.freeze({
|
|
259
|
+
code: "E_DIAGNOSIS_CYCLE_MISMATCH",
|
|
260
|
+
category: "diagnosis",
|
|
261
|
+
classification: "PUBLIC_STABLE",
|
|
262
|
+
meaning: "Diagnosis verification cycle does not match the active work state verification cycle.",
|
|
263
|
+
safeResolution: "Record diagnosis for the current active verification cycle.",
|
|
264
|
+
}),
|
|
265
|
+
E_DIAGNOSIS_NO_NEW_INFORMATION: Object.freeze({
|
|
266
|
+
code: "E_DIAGNOSIS_NO_NEW_INFORMATION",
|
|
267
|
+
category: "diagnosis",
|
|
268
|
+
classification: "PUBLIC_STABLE",
|
|
269
|
+
meaning: "The proposed retry repeats the previous hypothesis with the same evidence.",
|
|
270
|
+
safeResolution: "Change the hypothesis, collect independent evidence, or change strategy.",
|
|
271
|
+
}),
|
|
272
|
+
E_PROGRESS_STALLED: Object.freeze({
|
|
273
|
+
code: "E_PROGRESS_STALLED",
|
|
274
|
+
category: "progress",
|
|
275
|
+
classification: "PUBLIC_STABLE",
|
|
276
|
+
meaning: "Persisted correction history shows no new diagnostic information.",
|
|
277
|
+
safeResolution: "Use an independent check, revisit assumptions, or record a materially different diagnosis.",
|
|
278
|
+
}),
|
|
279
|
+
E_DECISION_CRITERION_INVALID: Object.freeze({
|
|
280
|
+
code: "E_DECISION_CRITERION_INVALID",
|
|
281
|
+
category: "contract",
|
|
282
|
+
classification: "PUBLIC_STABLE",
|
|
283
|
+
meaning: "Decision settlement criterion details or parameters are malformed.",
|
|
284
|
+
safeResolution: "Provide non-empty decision text and settledBy criterion.",
|
|
285
|
+
}),
|
|
286
|
+
E_DECISION_NOT_UNRESOLVED: Object.freeze({
|
|
287
|
+
code: "E_DECISION_NOT_UNRESOLVED",
|
|
288
|
+
category: "contract",
|
|
289
|
+
classification: "PUBLIC_STABLE",
|
|
290
|
+
meaning: "A settlement criterion referenced a decision not present in current unresolvedDecisions.",
|
|
291
|
+
safeResolution: "Use the exact current unresolved decision text or update the contract first.",
|
|
292
|
+
}),
|
|
293
|
+
E_CHECK_INERT: Object.freeze({
|
|
294
|
+
code: "E_CHECK_INERT",
|
|
295
|
+
category: "policy",
|
|
296
|
+
classification: "PUBLIC_STABLE",
|
|
297
|
+
meaning: "An enabled check has no effective scope or target files.",
|
|
298
|
+
safeResolution: "Provide an applicable target scope, configure matching files, or mark the rule unsupported.",
|
|
299
|
+
}),
|
|
300
|
+
E_CHECK_MUTATION_NOT_DETECTED: Object.freeze({
|
|
301
|
+
code: "E_CHECK_MUTATION_NOT_DETECTED",
|
|
302
|
+
category: "policy",
|
|
303
|
+
classification: "PUBLIC_STABLE",
|
|
304
|
+
meaning: "A blocking rule checker failed to detect an intentional mutation fixture.",
|
|
305
|
+
safeResolution: "Fix checker logic to properly identify target violations.",
|
|
306
|
+
}),
|
|
307
|
+
E_POLICY_DRIFT: Object.freeze({
|
|
308
|
+
code: "E_POLICY_DRIFT",
|
|
309
|
+
category: "policy",
|
|
310
|
+
classification: "PUBLIC_STABLE",
|
|
311
|
+
meaning: "Active policy lock does not match the policy snapshot captured at task activation.",
|
|
312
|
+
safeResolution: "Re-verify affected checks or restore original policy.",
|
|
313
|
+
}),
|
|
314
|
+
E_POLICY_WEAKENING: Object.freeze({
|
|
315
|
+
code: "E_POLICY_WEAKENING",
|
|
316
|
+
category: "policy",
|
|
317
|
+
classification: "PUBLIC_STABLE",
|
|
318
|
+
meaning: "Policy rules were weakened during task execution without explicit authority.",
|
|
319
|
+
safeResolution: "Restore the original policy configuration.",
|
|
320
|
+
}),
|
|
321
|
+
E_POLICY_LOCK_INVALID: Object.freeze({
|
|
322
|
+
code: "E_POLICY_LOCK_INVALID",
|
|
323
|
+
category: "policy",
|
|
324
|
+
classification: "PUBLIC_STABLE",
|
|
325
|
+
meaning: "Policy lockfile is missing, malformed, or corrupt.",
|
|
326
|
+
safeResolution: "Run forgeloop policy-status or regenerate policy.lock.",
|
|
327
|
+
}),
|
|
328
|
+
E_NEW_POLICY_VIOLATION: Object.freeze({
|
|
329
|
+
code: "E_NEW_POLICY_VIOLATION",
|
|
330
|
+
category: "policy",
|
|
331
|
+
classification: "PUBLIC_STABLE",
|
|
332
|
+
meaning: "New executable policy violation detected that is not present in brownfield baseline.",
|
|
333
|
+
safeResolution: "Fix the violation before completing the task.",
|
|
334
|
+
}),
|
|
335
|
+
E_BASELINE_EXPANSION: Object.freeze({
|
|
336
|
+
code: "E_BASELINE_EXPANSION",
|
|
337
|
+
category: "policy",
|
|
338
|
+
classification: "PUBLIC_STABLE",
|
|
339
|
+
meaning: "Attempted unauthorized addition of new violations to brownfield baseline.",
|
|
340
|
+
safeResolution: "Resolve new violations rather than expanding the baseline.",
|
|
341
|
+
}),
|
|
342
|
+
E_POLICY_PROOF_STALE: Object.freeze({
|
|
343
|
+
code: "E_POLICY_PROOF_STALE",
|
|
344
|
+
category: "policy",
|
|
345
|
+
classification: "PUBLIC_STABLE",
|
|
346
|
+
meaning: "Mutation verification proof is stale due to checker or fixture modifications.",
|
|
347
|
+
safeResolution: "Re-run forgeloop rule-verify to refresh mutation proof.",
|
|
348
|
+
}),
|
|
349
|
+
E_CHECK_MUTATION_EXECUTION_ERROR: Object.freeze({
|
|
350
|
+
code: "E_CHECK_MUTATION_EXECUTION_ERROR",
|
|
351
|
+
category: "policy",
|
|
352
|
+
classification: "PUBLIC_STABLE",
|
|
353
|
+
meaning: "A policy checker threw an unhandled exception while evaluating its mutation fixture.",
|
|
354
|
+
safeResolution: "Repair the checker execution path and rerun rule verification.",
|
|
355
|
+
}),
|
|
356
|
+
E_POLICY_EVALUATION_FAILED: Object.freeze({
|
|
357
|
+
code: "E_POLICY_EVALUATION_FAILED",
|
|
358
|
+
category: "policy",
|
|
359
|
+
classification: "PUBLIC_STABLE",
|
|
360
|
+
meaning: "Policy evaluation threw an unexpected error during execution.",
|
|
361
|
+
safeResolution: "Inspect policy configuration and checker adapters for unhandled errors.",
|
|
362
|
+
}),
|
|
363
|
+
E_POLICY_INVALID: Object.freeze({
|
|
364
|
+
code: "E_POLICY_INVALID",
|
|
365
|
+
category: "policy",
|
|
366
|
+
classification: "PUBLIC_STABLE",
|
|
367
|
+
meaning: "Policy artifact is malformed, corrupt, or schema-invalid.",
|
|
368
|
+
safeResolution: "Validate and repair rules.json, baseline.json, or discovery.json against schema.",
|
|
369
|
+
}),
|
|
370
|
+
E_POLICY_SNAPSHOT_WRITE_FAILED: Object.freeze({
|
|
371
|
+
code: "E_POLICY_SNAPSHOT_WRITE_FAILED",
|
|
372
|
+
category: "policy",
|
|
373
|
+
classification: "PUBLIC_STABLE",
|
|
374
|
+
meaning: "Failed to persist task policy snapshot during preflight.",
|
|
375
|
+
safeResolution: "Ensure the target task directory is writable and repair filesystem permissions.",
|
|
376
|
+
}),
|
|
377
|
+
E_POLICY_LOCK_MISMATCH: Object.freeze({
|
|
378
|
+
code: "E_POLICY_LOCK_MISMATCH",
|
|
379
|
+
category: "policy",
|
|
380
|
+
classification: "PUBLIC_STABLE",
|
|
381
|
+
meaning: "Persisted policy lock digest does not match current effective policy state.",
|
|
382
|
+
safeResolution: "Re-evaluate effective rules and update policy.lock or restore modified rules.",
|
|
383
|
+
}),
|
|
384
|
+
E_POLICY_DRIFT_UNKNOWN: Object.freeze({
|
|
385
|
+
code: "E_POLICY_DRIFT_UNKNOWN",
|
|
386
|
+
category: "policy",
|
|
387
|
+
classification: "PUBLIC_STABLE",
|
|
388
|
+
meaning: "Task policy drift was detected but baseline snapshot details are unavailable.",
|
|
389
|
+
safeResolution: "Re-verify the task under the current policy state.",
|
|
390
|
+
}),
|
|
391
|
+
E_BASELINE_RECORD_DURING_ACTIVE_TASK: Object.freeze({
|
|
392
|
+
code: "E_BASELINE_RECORD_DURING_ACTIVE_TASK",
|
|
393
|
+
category: "policy",
|
|
394
|
+
classification: "PUBLIC_STABLE",
|
|
395
|
+
meaning: "Cannot re-record baseline during an active task with policy snapshot.",
|
|
396
|
+
safeResolution: "Resolve new violations or use monotonic baseline --update.",
|
|
397
|
+
}),
|
|
398
|
+
E_POLICY_INITIALIZATION_FAILED: Object.freeze({
|
|
399
|
+
code: "E_POLICY_INITIALIZATION_FAILED",
|
|
400
|
+
category: "policy",
|
|
401
|
+
classification: "PUBLIC_STABLE",
|
|
402
|
+
meaning: "Executable policy bootstrap could not complete during initialization.",
|
|
403
|
+
safeResolution: "Repair the reported filesystem/schema error and rerun `forgeloop init`. Initialization is restartable while no committed manifest exists.",
|
|
404
|
+
}),
|
|
405
|
+
E_INIT_KIT_CONFLICT: Object.freeze({
|
|
406
|
+
code: "E_INIT_KIT_CONFLICT",
|
|
407
|
+
category: "project-maintenance",
|
|
408
|
+
classification: "PUBLIC_STABLE",
|
|
409
|
+
meaning: "A canonical ForgeLoop kit destination already exists with content that does not match the shipped canonical template.",
|
|
410
|
+
safeResolution: "Inspect the conflicting `.forgeloop/kit/...` file. If it is stale or partial ForgeLoop output, remove or restore it and rerun `forgeloop init`. Do not overwrite unknown content automatically.",
|
|
411
|
+
}),
|
|
163
412
|
});
|
|
164
413
|
|
|
414
|
+
export const E_CHECK_INERT = "E_CHECK_INERT";
|
|
415
|
+
export const E_CHECK_MUTATION_NOT_DETECTED = "E_CHECK_MUTATION_NOT_DETECTED";
|
|
416
|
+
export const E_POLICY_DRIFT = "E_POLICY_DRIFT";
|
|
417
|
+
export const E_POLICY_WEAKENING = "E_POLICY_WEAKENING";
|
|
418
|
+
export const E_POLICY_LOCK_INVALID = "E_POLICY_LOCK_INVALID";
|
|
419
|
+
export const E_NEW_POLICY_VIOLATION = "E_NEW_POLICY_VIOLATION";
|
|
420
|
+
export const E_BASELINE_EXPANSION = "E_BASELINE_EXPANSION";
|
|
421
|
+
export const E_POLICY_PROOF_STALE = "E_POLICY_PROOF_STALE";
|
|
422
|
+
export const E_CHECK_MUTATION_EXECUTION_ERROR = "E_CHECK_MUTATION_EXECUTION_ERROR";
|
|
423
|
+
export const E_POLICY_EVALUATION_FAILED = "E_POLICY_EVALUATION_FAILED";
|
|
424
|
+
export const E_POLICY_INVALID = "E_POLICY_INVALID";
|
|
425
|
+
export const E_POLICY_SNAPSHOT_WRITE_FAILED = "E_POLICY_SNAPSHOT_WRITE_FAILED";
|
|
426
|
+
export const E_POLICY_LOCK_MISMATCH = "E_POLICY_LOCK_MISMATCH";
|
|
427
|
+
export const E_POLICY_DRIFT_UNKNOWN = "E_POLICY_DRIFT_UNKNOWN";
|
|
428
|
+
export const E_BASELINE_RECORD_DURING_ACTIVE_TASK = "E_BASELINE_RECORD_DURING_ACTIVE_TASK";
|
|
429
|
+
export const E_POLICY_INITIALIZATION_FAILED = "E_POLICY_INITIALIZATION_FAILED";
|
|
430
|
+
export const E_INIT_KIT_CONFLICT = "E_INIT_KIT_CONFLICT";
|
|
431
|
+
|
|
165
432
|
export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
|
|
166
433
|
...FAILURE_CODES,
|
|
167
434
|
E_VERIFICATION_TOOL_UNAVAILABLE,
|
|
@@ -190,8 +457,33 @@ export const ALL_KNOWN_ERROR_CODES = Object.freeze(new Set([
|
|
|
190
457
|
E_TASK_SCOPE_DIRTY,
|
|
191
458
|
E_TASK_SCOPE_FROZEN,
|
|
192
459
|
E_TASK_CHANGE_OUTSIDE_SCOPE,
|
|
460
|
+
E_RECONCILE_NOT_STALE,
|
|
461
|
+
E_RECONCILE_PHASE_INVALID,
|
|
462
|
+
E_RECONCILE_UNSUPPORTED_DRIFT,
|
|
463
|
+
E_RECONCILE_LEDGER_INVALID,
|
|
464
|
+
E_RECONCILE_REQUIREMENT_UNKNOWN,
|
|
465
|
+
E_RECONCILE_EVIDENCE_FAILED,
|
|
466
|
+
E_REPOSITORY_CHANGED,
|
|
467
|
+
E_STATE_REVALIDATION_REQUIRED,
|
|
193
468
|
E_TASK_CHANGE_ATTRIBUTION_UNAVAILABLE,
|
|
194
469
|
E_TASK_LAYOUT_LEGACY,
|
|
195
470
|
E_TASK_MIGRATION_INVALID,
|
|
196
471
|
E_TASK_MIGRATION_IDENTITY_MISMATCH,
|
|
472
|
+
E_CHECK_INERT,
|
|
473
|
+
E_CHECK_MUTATION_NOT_DETECTED,
|
|
474
|
+
E_POLICY_DRIFT,
|
|
475
|
+
E_POLICY_WEAKENING,
|
|
476
|
+
E_POLICY_LOCK_INVALID,
|
|
477
|
+
E_NEW_POLICY_VIOLATION,
|
|
478
|
+
E_BASELINE_EXPANSION,
|
|
479
|
+
E_POLICY_PROOF_STALE,
|
|
480
|
+
E_CHECK_MUTATION_EXECUTION_ERROR,
|
|
481
|
+
E_POLICY_EVALUATION_FAILED,
|
|
482
|
+
E_POLICY_INVALID,
|
|
483
|
+
E_POLICY_SNAPSHOT_WRITE_FAILED,
|
|
484
|
+
E_POLICY_LOCK_MISMATCH,
|
|
485
|
+
E_POLICY_DRIFT_UNKNOWN,
|
|
486
|
+
E_BASELINE_RECORD_DURING_ACTIVE_TASK,
|
|
487
|
+
E_POLICY_INITIALIZATION_FAILED,
|
|
488
|
+
E_INIT_KIT_CONFLICT,
|
|
197
489
|
]));
|
package/src/core/events.js
CHANGED
|
@@ -12,6 +12,9 @@ import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
|
12
12
|
|
|
13
13
|
import { taskArtifactPath } from "./task-paths.js";
|
|
14
14
|
|
|
15
|
+
import { assertDiagnosisDetails } from "./diagnosis-model.js";
|
|
16
|
+
import { assertDecisionCriterionDetails } from "./settlement-model.js";
|
|
17
|
+
|
|
15
18
|
const EVENT_SCHEMA_VERSION = 1;
|
|
16
19
|
export const LIFECYCLE_MILESTONES = Object.freeze([
|
|
17
20
|
"CONTRACT_VALIDATED",
|
|
@@ -37,6 +40,42 @@ const REPEATABLE_MILESTONES = new Set([
|
|
|
37
40
|
"TERMINAL_RESULT_RECORDED",
|
|
38
41
|
]);
|
|
39
42
|
|
|
43
|
+
export function validateKnownEventDetails(event) {
|
|
44
|
+
if (!event || typeof event !== "object") return;
|
|
45
|
+
switch (event.event) {
|
|
46
|
+
case "DIAGNOSIS_RECORDED":
|
|
47
|
+
assertDiagnosisDetails(event.details);
|
|
48
|
+
return;
|
|
49
|
+
case "DECISION_CRITERION_RECORDED":
|
|
50
|
+
assertDecisionCriterionDetails(event.details);
|
|
51
|
+
return;
|
|
52
|
+
case "CHECKPOINT_RECONCILED":
|
|
53
|
+
assertReconcileClosureDetails(event.details);
|
|
54
|
+
return;
|
|
55
|
+
default:
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertReconcileClosureDetails(details) {
|
|
61
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
62
|
+
throw protocolError("E_EVENT_INVALID", "CHECKPOINT_RECONCILED requires structured details");
|
|
63
|
+
}
|
|
64
|
+
for (const key of ["checkId", "command", "executionId"]) {
|
|
65
|
+
if (typeof details[key] !== "string") {
|
|
66
|
+
throw protocolError("E_EVENT_INVALID", `CHECKPOINT_RECONCILED details.${key} must be a string`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const key of ["previousBranch", "currentBranch", "previousHead", "currentHead"]) {
|
|
70
|
+
if (typeof details[key] !== "string" && details[key] !== null) {
|
|
71
|
+
throw protocolError("E_EVENT_INVALID", `CHECKPOINT_RECONCILED details.${key} must be a string or null`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (typeof details.exitCode !== "number" || !Number.isInteger(details.exitCode) || details.exitCode < 0) {
|
|
75
|
+
throw protocolError("E_EVENT_INVALID", "CHECKPOINT_RECONCILED details.exitCode must be a non-negative integer");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
function eventHash(event) {
|
|
41
80
|
const { hash, ...body } = event;
|
|
42
81
|
return canonicalFingerprint(body);
|
|
@@ -64,8 +103,9 @@ export async function readEvents(target, packageRoot, options = {}) {
|
|
|
64
103
|
event = JSON.parse(line);
|
|
65
104
|
assertJsonLimits(event, `${relPath}[${index}]`);
|
|
66
105
|
assertSchema(event, schema, `${relPath}[${index}]`);
|
|
106
|
+
validateKnownEventDetails(event);
|
|
67
107
|
} catch (error) {
|
|
68
|
-
throw protocolError("E_EVENT_INVALID", `${relPath} line ${index + 1}: ${error.message}`, [relPath]);
|
|
108
|
+
throw protocolError(error.code ?? "E_EVENT_INVALID", `${relPath} line ${index + 1}: ${error.message}`, [relPath]);
|
|
69
109
|
}
|
|
70
110
|
return event;
|
|
71
111
|
});
|
|
@@ -88,6 +128,7 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
88
128
|
previousHash: previous?.hash ?? null,
|
|
89
129
|
...(input.details ? { details: structuredClone(input.details) } : {}),
|
|
90
130
|
};
|
|
131
|
+
validateKnownEventDetails(event);
|
|
91
132
|
assertSecretFree(event);
|
|
92
133
|
const schema = await readSchema("event", packageRoot);
|
|
93
134
|
assertSchema(event, schema, relPath);
|
|
@@ -125,6 +166,11 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
125
166
|
if (event.hash !== eventHash(event)) {
|
|
126
167
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
|
|
127
168
|
}
|
|
169
|
+
try {
|
|
170
|
+
validateKnownEventDetails(event);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
errors.push({ code: err.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${err.message}` });
|
|
173
|
+
}
|
|
128
174
|
const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
|
|
129
175
|
if (milestoneIndex >= 0) {
|
|
130
176
|
const count = (milestoneCounts.get(event.event) ?? 0) + 1;
|