@cassiomc1/forgeloop 1.3.0 → 1.6.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/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
2
|
+
import { readWorkState, mutateWorkState } from "./work-state.js";
|
|
3
|
+
import {
|
|
4
|
+
DIAGNOSTIC_SCHEMA_VERSION,
|
|
5
|
+
DISPOSITION_TRANSITIONS,
|
|
6
|
+
assertDiagnosticCaseDetails,
|
|
7
|
+
assertHypothesisDispositionDetails,
|
|
8
|
+
assertInterventionDetails,
|
|
9
|
+
diagnosticSemanticFingerprint,
|
|
10
|
+
interventionSemanticFingerprint,
|
|
11
|
+
} from "./diagnostic-model.js";
|
|
12
|
+
import { projectHypothesisStates, getHypothesisState } from "./hypothesis-projection.js";
|
|
13
|
+
|
|
14
|
+
const TERMINAL_STATUSES = Object.freeze(["FALSIFIED", "SUPERSEDED", "UNRESOLVED"]);
|
|
15
|
+
|
|
16
|
+
function canTransitionStatus(from, to) {
|
|
17
|
+
if (from === to) return false;
|
|
18
|
+
if (TERMINAL_STATUSES.includes(from)) return false;
|
|
19
|
+
return (DISPOSITION_TRANSITIONS[from] ?? []).includes(to);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
DIAGNOSTIC_SCHEMA_VERSION,
|
|
24
|
+
assertDiagnosticCaseDetails,
|
|
25
|
+
assertInterventionDetails,
|
|
26
|
+
assertHypothesisDispositionDetails,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function stateError(code, message) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.code = code;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function requireActiveState(target, { packageRoot, taskId }) {
|
|
36
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
37
|
+
if (!state) throw stateError("E_STATE_MISSING", "Work state not found");
|
|
38
|
+
if (typeof state.verificationCycle !== "number" || state.verificationCycle < 1) {
|
|
39
|
+
throw stateError("E_DIAGNOSTIC_CASE_CYCLE_MISMATCH", "Active verification cycle must be an integer >= 1");
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function requireValidLedger(target, packageRoot, taskId) {
|
|
45
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: taskId ?? null });
|
|
46
|
+
if (!ledger.valid) {
|
|
47
|
+
throw stateError(ledger.errors[0].code ?? "E_EVENT_INVALID", ledger.errors[0].message);
|
|
48
|
+
}
|
|
49
|
+
return ledger;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function legacyDiagnosisToDiagnosticCase(diagnosisDetails) {
|
|
53
|
+
if (!diagnosisDetails || typeof diagnosisDetails !== "object") return null;
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
56
|
+
verificationCycle: diagnosisDetails.verificationCycle,
|
|
57
|
+
diagnosticRevision: 1,
|
|
58
|
+
failureClass: diagnosisDetails.failureClass,
|
|
59
|
+
observations: (diagnosisDetails.evidenceRefs ?? []).map((ref, index) => ({
|
|
60
|
+
id: `obs-legacy-${index + 1}`,
|
|
61
|
+
kind: "CHECK_RESULT",
|
|
62
|
+
evidenceRef: ref,
|
|
63
|
+
statement: `Legacy diagnosis bound evidence ${ref}.`,
|
|
64
|
+
})),
|
|
65
|
+
contributors: [],
|
|
66
|
+
hypotheses: [
|
|
67
|
+
{
|
|
68
|
+
id: "h-legacy",
|
|
69
|
+
statement: diagnosisDetails.hypothesis,
|
|
70
|
+
contributorRefs: [],
|
|
71
|
+
evidenceRefs: [...(diagnosisDetails.evidenceRefs ?? [])],
|
|
72
|
+
settledBy: { type: "PREDICATE", statement: diagnosisDetails.settledBy },
|
|
73
|
+
status: "OPEN",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
nextSafeAction: { statement: diagnosisDetails.nextSafeAction },
|
|
77
|
+
diagnosticFingerprint: diagnosisDetails.diagnosisFingerprint,
|
|
78
|
+
previousDiagnosticFingerprint: diagnosisDetails.previousDiagnosisFingerprint ?? null,
|
|
79
|
+
sourceModel: "LEGACY_DIAGNOSIS_V1",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function recordStructuredDiagnosticCase({
|
|
84
|
+
target,
|
|
85
|
+
packageRoot,
|
|
86
|
+
caseFile = null,
|
|
87
|
+
caseInput = null,
|
|
88
|
+
taskId = null,
|
|
89
|
+
}) {
|
|
90
|
+
let parsed;
|
|
91
|
+
if (caseFile) {
|
|
92
|
+
const { readFile } = await import("node:fs/promises");
|
|
93
|
+
const { assertSafePath, ensureWithin } = await import("./filesystem.js");
|
|
94
|
+
await assertSafePath(target, caseFile);
|
|
95
|
+
const absolute = ensureWithin(target, caseFile);
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(await readFile(absolute, "utf8"));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw stateError("E_DIAGNOSTIC_CASE_INVALID", `diagnostic case file is not valid JSON: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
parsed = caseInput;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const state = await requireActiveState(target, { packageRoot, taskId });
|
|
106
|
+
if (state.phase !== "DIAGNOSING") {
|
|
107
|
+
throw stateError("E_PHASE_PREREQUISITE_MISSING", `Recording a structured diagnostic case requires phase DIAGNOSING, currently ${state.phase}`);
|
|
108
|
+
}
|
|
109
|
+
const ledger = await requireValidLedger(target, packageRoot, taskId);
|
|
110
|
+
|
|
111
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
112
|
+
throw stateError("E_DIAGNOSTIC_CASE_INVALID", "diagnostic case must be a JSON object");
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(parsed.hypotheses) || parsed.hypotheses.length < 1) {
|
|
115
|
+
throw stateError("E_DIAGNOSTIC_CASE_INVALID", "diagnostic case requires at least one hypothesis (hypotheses.minItems = 1)");
|
|
116
|
+
}
|
|
117
|
+
if (Number.isInteger(parsed.verificationCycle) && parsed.verificationCycle !== state.verificationCycle) {
|
|
118
|
+
throw stateError("E_DIAGNOSTIC_CASE_CYCLE_MISMATCH", `case verificationCycle ${parsed.verificationCycle} does not match active cycle ${state.verificationCycle}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const activeCycleChecks = (state.checks ?? []).filter(
|
|
122
|
+
(candidate) => candidate.details?.verificationCycle === state.verificationCycle,
|
|
123
|
+
);
|
|
124
|
+
const findActiveCheck = (ref) => activeCycleChecks.find(
|
|
125
|
+
(candidate) => candidate.id === ref || candidate.checkId === ref,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const hypothesis of parsed.hypotheses ?? []) {
|
|
129
|
+
for (const ref of hypothesis.evidenceRefs ?? []) {
|
|
130
|
+
if (!findActiveCheck(ref)) {
|
|
131
|
+
throw stateError("E_DIAGNOSTIC_CASE_EVIDENCE_INVALID", `evidenceRef "${ref}" does not match any check from verification cycle ${state.verificationCycle}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Observation evidence must resolve: CHECK_RESULT observations to a real
|
|
137
|
+
// active-cycle check, MANUAL_OBSERVATION provenance to be stated explicitly.
|
|
138
|
+
for (const observation of parsed.observations ?? []) {
|
|
139
|
+
if (observation.kind === "CHECK_RESULT") {
|
|
140
|
+
if (!observation.evidenceRef || !findActiveCheck(observation.evidenceRef)) {
|
|
141
|
+
throw stateError("E_DIAGNOSTIC_CASE_EVIDENCE_INVALID", `observation "${observation.id}" evidenceRef "${observation.evidenceRef ?? ""}" does not resolve to a real check from verification cycle ${state.verificationCycle}`);
|
|
142
|
+
}
|
|
143
|
+
} else if (observation.provenance && observation.provenance !== "MANUAL_OBSERVATION" && observation.kind !== "CHECK_RESULT") {
|
|
144
|
+
throw stateError("E_DIAGNOSTIC_CASE_INVALID", `observation "${observation.id}" has unsupported provenance "${observation.provenance}"`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A VERIFICATION_FAILURE case must bind to failed/blocked evidence from the
|
|
149
|
+
// active correction cycle so it cannot be formally valid yet semantically empty.
|
|
150
|
+
if (parsed.failureClass === "VERIFICATION_FAILURE") {
|
|
151
|
+
const bindsToFailure = [
|
|
152
|
+
...parsed.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? []),
|
|
153
|
+
...(parsed.observations ?? [])
|
|
154
|
+
.filter((observation) => observation.kind === "CHECK_RESULT")
|
|
155
|
+
.map((observation) => observation.evidenceRef),
|
|
156
|
+
].some((ref) => {
|
|
157
|
+
const check = ref ? findActiveCheck(ref) : null;
|
|
158
|
+
return check?.status === "failed" || check?.status === "blocked";
|
|
159
|
+
});
|
|
160
|
+
if (!bindsToFailure) {
|
|
161
|
+
throw stateError("E_DIAGNOSTIC_CASE_EVIDENCE_INVALID", "VERIFICATION_FAILURE cases require at least one hypothesis or observation bound to failed/blocked evidence from the active verification cycle");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const previousCases = ledger.events.filter(
|
|
166
|
+
(event) => event.event === "DIAGNOSTIC_CASE_RECORDED"
|
|
167
|
+
&& event.taskId === state.taskId
|
|
168
|
+
&& event.details?.verificationCycle === state.verificationCycle,
|
|
169
|
+
);
|
|
170
|
+
const previousCase = previousCases.at(-1)?.details ?? null;
|
|
171
|
+
const revision = (previousCase?.diagnosticRevision ?? 0) + 1;
|
|
172
|
+
|
|
173
|
+
const details = {
|
|
174
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
175
|
+
verificationCycle: state.verificationCycle,
|
|
176
|
+
diagnosticRevision: revision,
|
|
177
|
+
failureClass: parsed.failureClass,
|
|
178
|
+
observations: parsed.observations ?? [],
|
|
179
|
+
contributors: parsed.contributors ?? [],
|
|
180
|
+
hypotheses: (parsed.hypotheses ?? []).map((hypothesis) => ({ ...hypothesis, status: "OPEN" })),
|
|
181
|
+
nextSafeAction: parsed.nextSafeAction,
|
|
182
|
+
};
|
|
183
|
+
details.diagnosticFingerprint = diagnosticSemanticFingerprint({
|
|
184
|
+
verificationCycle: details.verificationCycle,
|
|
185
|
+
failureClass: details.failureClass,
|
|
186
|
+
case_: details,
|
|
187
|
+
});
|
|
188
|
+
details.previousDiagnosticFingerprint = previousCase?.diagnosticFingerprint ?? null;
|
|
189
|
+
|
|
190
|
+
if (previousCase && previousCase.diagnosticFingerprint === details.diagnosticFingerprint) {
|
|
191
|
+
return {
|
|
192
|
+
event: previousCases.at(-1),
|
|
193
|
+
state,
|
|
194
|
+
diagnosticCase: previousCase,
|
|
195
|
+
idempotent: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
assertDiagnosticCaseDetails(details);
|
|
200
|
+
|
|
201
|
+
const event = await appendProtocolEvent(
|
|
202
|
+
target,
|
|
203
|
+
{ taskId: state.taskId, event: "DIAGNOSTIC_CASE_RECORDED", details },
|
|
204
|
+
packageRoot,
|
|
205
|
+
{ taskId: taskId ?? null },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const updatedState = await mutateWorkState(target, {
|
|
209
|
+
expectedRevision: state.revision ?? 0,
|
|
210
|
+
packageRoot,
|
|
211
|
+
taskId,
|
|
212
|
+
}, () => ({
|
|
213
|
+
...state,
|
|
214
|
+
diagnosedHypothesis: details.hypotheses[0]?.statement ?? state.diagnosedHypothesis,
|
|
215
|
+
lastUpdated: new Date().toISOString(),
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
return { event, state: updatedState, diagnosticCase: details, idempotent: false };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function recordIntervention({
|
|
222
|
+
target,
|
|
223
|
+
packageRoot,
|
|
224
|
+
interventionFile = null,
|
|
225
|
+
interventionInput = null,
|
|
226
|
+
taskId = null,
|
|
227
|
+
}) {
|
|
228
|
+
let parsed;
|
|
229
|
+
if (interventionFile) {
|
|
230
|
+
const { readFile } = await import("node:fs/promises");
|
|
231
|
+
const { assertSafePath, ensureWithin } = await import("./filesystem.js");
|
|
232
|
+
await assertSafePath(target, interventionFile);
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(await readFile(ensureWithin(target, interventionFile), "utf8"));
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw stateError("E_INTERVENTION_INVALID", `intervention file is not valid JSON: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
parsed = interventionInput;
|
|
240
|
+
}
|
|
241
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
242
|
+
throw stateError("E_INTERVENTION_INVALID", "intervention must be a JSON object");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const state = await requireActiveState(target, { packageRoot, taskId });
|
|
246
|
+
if (state.phase !== "CORRECTING") {
|
|
247
|
+
throw stateError("E_PHASE_PREREQUISITE_MISSING", `Recording an intervention requires phase CORRECTING, currently ${state.phase}`);
|
|
248
|
+
}
|
|
249
|
+
const ledger = await requireValidLedger(target, packageRoot, taskId);
|
|
250
|
+
|
|
251
|
+
const knownHypothesisIds = new Set();
|
|
252
|
+
for (const event of ledger.events) {
|
|
253
|
+
if (event.event !== "DIAGNOSTIC_CASE_RECORDED" && event.event !== "DIAGNOSIS_RECORDED") continue;
|
|
254
|
+
if (event.taskId !== state.taskId) continue;
|
|
255
|
+
if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
|
|
256
|
+
for (const hypothesis of event.details?.hypotheses ?? []) knownHypothesisIds.add(hypothesis.id);
|
|
257
|
+
} else {
|
|
258
|
+
knownHypothesisIds.add("h-legacy");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (const ref of parsed.hypothesisRefs ?? []) {
|
|
262
|
+
if (!knownHypothesisIds.has(ref)) {
|
|
263
|
+
throw stateError("E_INTERVENTION_REFERENCE_INVALID", `intervention references unknown hypothesis "${ref}"`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const details = {
|
|
268
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
269
|
+
verificationCycle: state.verificationCycle,
|
|
270
|
+
intervention: {
|
|
271
|
+
id: parsed.id,
|
|
272
|
+
kind: parsed.kind,
|
|
273
|
+
statement: parsed.statement,
|
|
274
|
+
...(Array.isArray(parsed.targets) ? { targets: parsed.targets } : {}),
|
|
275
|
+
hypothesisRefs: parsed.hypothesisRefs ?? [],
|
|
276
|
+
...(typeof parsed.expectedObservation === "string" ? { expectedObservation: parsed.expectedObservation } : {}),
|
|
277
|
+
...(typeof parsed.reversible === "boolean" ? { reversible: parsed.reversible } : {}),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
details.interventionSemanticFingerprint = interventionSemanticFingerprint(details);
|
|
281
|
+
assertInterventionDetails(details);
|
|
282
|
+
|
|
283
|
+
const priorInterventions = ledger.events.filter(
|
|
284
|
+
(event) => event.event === "INTERVENTION_RECORDED"
|
|
285
|
+
&& event.taskId === state.taskId
|
|
286
|
+
&& event.details?.interventionSemanticFingerprint === details.interventionSemanticFingerprint,
|
|
287
|
+
);
|
|
288
|
+
const repeatedSemanticIntervention = priorInterventions.length > 0;
|
|
289
|
+
|
|
290
|
+
const event = await appendProtocolEvent(
|
|
291
|
+
target,
|
|
292
|
+
{ taskId: state.taskId, event: "INTERVENTION_RECORDED", details },
|
|
293
|
+
packageRoot,
|
|
294
|
+
{ taskId: taskId ?? null },
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
event,
|
|
299
|
+
state,
|
|
300
|
+
intervention: details,
|
|
301
|
+
repeatedSemanticIntervention,
|
|
302
|
+
effectiveness: "PENDING",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function recordHypothesisDisposition({
|
|
307
|
+
target,
|
|
308
|
+
packageRoot,
|
|
309
|
+
hypothesisRef,
|
|
310
|
+
status,
|
|
311
|
+
evidenceRefs = [],
|
|
312
|
+
reason,
|
|
313
|
+
taskId = null,
|
|
314
|
+
}) {
|
|
315
|
+
const state = await requireActiveState(target, { packageRoot, taskId });
|
|
316
|
+
const ledger = await requireValidLedger(target, packageRoot, taskId);
|
|
317
|
+
|
|
318
|
+
// Authoritative current status comes from the append-only projection, never
|
|
319
|
+
// from the source diagnostic case.
|
|
320
|
+
const projection = projectHypothesisStates(ledger.events, { taskId: state.taskId });
|
|
321
|
+
const projectedState = getHypothesisState(projection, hypothesisRef);
|
|
322
|
+
const knownHypothesisIds = new Set(projection.hypotheses.map((hypothesis) => hypothesis.id));
|
|
323
|
+
if (!knownHypothesisIds.has(hypothesisRef)) {
|
|
324
|
+
throw stateError("E_HYPOTHESIS_DISPOSITION_INVALID", `unknown hypothesis "${hypothesisRef}"`);
|
|
325
|
+
}
|
|
326
|
+
const currentStatus = projectedState.currentStatus;
|
|
327
|
+
|
|
328
|
+
if (!canTransitionStatus(currentStatus, status)) {
|
|
329
|
+
throw stateError("E_HYPOTHESIS_DISPOSITION_INVALID", `disposition transition ${currentStatus} -> ${status} is not allowed`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const ref of evidenceRefs) {
|
|
333
|
+
const check = (state.checks ?? []).find(
|
|
334
|
+
(candidate) => candidate.details?.verificationCycle === state.verificationCycle
|
|
335
|
+
&& (candidate.id === ref || candidate.checkId === ref),
|
|
336
|
+
);
|
|
337
|
+
if (!check) {
|
|
338
|
+
throw stateError("E_HYPOTHESIS_DISPOSITION_EVIDENCE_INVALID", `evidenceRef "${ref}" does not match any check from verification cycle ${state.verificationCycle}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const details = {
|
|
343
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
344
|
+
verificationCycle: state.verificationCycle,
|
|
345
|
+
hypothesisRef,
|
|
346
|
+
status,
|
|
347
|
+
evidenceRefs: [...new Set(evidenceRefs)],
|
|
348
|
+
reason,
|
|
349
|
+
};
|
|
350
|
+
assertHypothesisDispositionDetails(details);
|
|
351
|
+
|
|
352
|
+
const event = await appendProtocolEvent(
|
|
353
|
+
target,
|
|
354
|
+
{ taskId: state.taskId, event: "HYPOTHESIS_DISPOSITION_RECORDED", details },
|
|
355
|
+
packageRoot,
|
|
356
|
+
{ taskId: taskId ?? null },
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return { event, state, disposition: details };
|
|
360
|
+
}
|