@cassiomc1/forgeloop 1.5.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/DOCS_INDEX.md +13 -8
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +81 -0
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +46 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +31 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +39 -0
- package/docs/ARTIFACT_REFERENCE.md +140 -0
- package/docs/CLI_REFERENCE.md +294 -3
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +22 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/MCP.md +33 -0
- package/docs/RECIPES.md +67 -0
- package/docs/TROUBLESHOOTING.md +106 -2
- 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 +8 -6
- 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/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +94 -0
- 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/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 +49 -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/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/trace.js +34 -0
- package/src/commands/validate-protocol.js +21 -13
- 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 +48 -0
- package/src/core/audit.js +25 -0
- package/src/core/bundles.js +15 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +210 -1
- package/src/core/command-executors.js +171 -15
- package/src/core/command-runtime.js +12 -1
- package/src/core/completion-artifacts.js +37 -12
- 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 +343 -0
- package/src/core/events.js +41 -1
- 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/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 +105 -7
- package/src/core/integration-invocation-policy.js +47 -0
- package/src/core/integration-resources.js +51 -0
- package/src/core/next-action-model.js +35 -1
- package/src/core/next-action.js +459 -3
- package/src/core/phase.js +40 -21
- 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/protocol-info.js +48 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +15 -12
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +8 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-paths.js +28 -0
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +8 -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 +8 -0
- package/src/core/work-state.js +10 -5
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { diagnosticEventsForTask } from "./diagnostic-projection.js";
|
|
2
|
+
import { normalizeDiagnosticSnapshot, computeInformationGain } from "./information-gain.js";
|
|
3
|
+
import { computeFailureSignature } from "./failure-signature.js";
|
|
4
|
+
|
|
5
|
+
function snapshotFor(event) {
|
|
6
|
+
const details = event.details ?? {};
|
|
7
|
+
if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
|
|
8
|
+
return normalizeDiagnosticSnapshot({ ...details, legacy: false });
|
|
9
|
+
}
|
|
10
|
+
return normalizeDiagnosticSnapshot({ ...details, legacy: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sameSortedSet = (a, b) =>
|
|
14
|
+
JSON.stringify([...(a ?? [])].sort()) === JSON.stringify([...(b ?? [])].sort());
|
|
15
|
+
|
|
16
|
+
// Cycle interval rule (documented contract):
|
|
17
|
+
// For diagnostic event D[n], the analysis interval is
|
|
18
|
+
// (D[n-1].sequence, D[n].sequence] — previous diagnostic sequence exclusive,
|
|
19
|
+
// current diagnostic sequence inclusive.
|
|
20
|
+
// Events inside an interval belong to the *current* cycle's knowledge state;
|
|
21
|
+
// they are never attributed retroactively to the earlier diagnosis.
|
|
22
|
+
function intervalEvents(taskEvents, fromExclusive, toInclusive) {
|
|
23
|
+
return taskEvents.filter((event) =>
|
|
24
|
+
event.seq > fromExclusive && event.seq <= toInclusive);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function failureStateByCycle(taskEvents) {
|
|
28
|
+
const surfaces = new Map();
|
|
29
|
+
const signatures = new Map();
|
|
30
|
+
const record = (cycle, details) => {
|
|
31
|
+
if (!Number.isInteger(cycle)) cycle = Number(cycle) || 1;
|
|
32
|
+
if (!surfaces.has(cycle)) surfaces.set(cycle, new Set());
|
|
33
|
+
if (!signatures.has(cycle)) signatures.set(cycle, new Set());
|
|
34
|
+
const requirement = details.requirement ?? details.id ?? details.checkId;
|
|
35
|
+
if (!requirement) return;
|
|
36
|
+
if (details.status === "failed" || details.status === "blocked") {
|
|
37
|
+
surfaces.get(cycle).add(requirement);
|
|
38
|
+
signatures.get(cycle).add(computeFailureSignature({
|
|
39
|
+
requirement,
|
|
40
|
+
status: details.status,
|
|
41
|
+
exitCode: Number.isInteger(details.exitCode) ? details.exitCode : null,
|
|
42
|
+
failureToken: typeof details.failureToken === "string" ? details.failureToken : (typeof details.details?.failureToken === "string" ? details.details.failureToken : null),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const event of taskEvents) {
|
|
47
|
+
if (event.event === "VERIFICATION_STARTED") {
|
|
48
|
+
const cycle = event.details?.verificationCycle;
|
|
49
|
+
if (Number.isInteger(cycle)) {
|
|
50
|
+
if (!surfaces.has(cycle)) surfaces.set(cycle, new Set());
|
|
51
|
+
if (!signatures.has(cycle)) signatures.set(cycle, new Set());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (event.event === "VERIFICATION_RECORDED") {
|
|
55
|
+
record(event.details?.verificationCycle ?? 1, event.details ?? {});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { surfaces, signatures };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function strategyFingerprintFor(diagnosticEvent, interventionsUpTo) {
|
|
63
|
+
const details = diagnosticEvent?.details ?? {};
|
|
64
|
+
const components = {
|
|
65
|
+
hypotheses: (details.hypotheses ?? []).map((hypothesis) => `${hypothesis.statement}`.trim().toLowerCase()),
|
|
66
|
+
contributors: (details.contributors ?? []).map((contributor) => `${contributor.statement}`.trim().toLowerCase()),
|
|
67
|
+
legacyHypothesis: diagnosticEvent?.event === "DIAGNOSIS_RECORDED"
|
|
68
|
+
? [`${details.hypothesis ?? ""}`.trim().toLowerCase()]
|
|
69
|
+
: [],
|
|
70
|
+
interventions: interventionsUpTo.map((entry) => entry.fingerprint),
|
|
71
|
+
};
|
|
72
|
+
return JSON.stringify([
|
|
73
|
+
[...components.hypotheses, ...components.legacyHypothesis].sort(),
|
|
74
|
+
components.contributors.sort(),
|
|
75
|
+
components.interventions.sort(),
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function snapshotHasContentDelta(previousDetails, currentDetails) {
|
|
80
|
+
const statementsOf = (details) => ({
|
|
81
|
+
observations: new Set((details.observations ?? []).map((o) => `${o.statement}`.trim().toLowerCase())),
|
|
82
|
+
contributors: new Set((details.contributors ?? []).map((c) => `${c.statement}`.trim().toLowerCase())),
|
|
83
|
+
hypotheses: new Set((details.hypotheses ?? []).map((h) => `${h.statement}`.trim().toLowerCase())),
|
|
84
|
+
legacyHypothesis: details.hypothesis ? new Set([`${details.hypothesis}`.trim().toLowerCase()]) : null,
|
|
85
|
+
evidence: new Set([
|
|
86
|
+
...((details.hypotheses ?? []).flatMap((h) => h.evidenceRefs ?? [])),
|
|
87
|
+
...((details.observations ?? []).map((o) => o.evidenceRef).filter(Boolean)),
|
|
88
|
+
...(details.evidenceRefs ?? []),
|
|
89
|
+
]),
|
|
90
|
+
});
|
|
91
|
+
const prev = statementsOf(previousDetails);
|
|
92
|
+
const cur = statementsOf(currentDetails);
|
|
93
|
+
const differs = (a, b) => {
|
|
94
|
+
if (!a || !b) return false;
|
|
95
|
+
for (const value of b) if (!a.has(value)) return true;
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
return differs(prev.observations, cur.observations)
|
|
99
|
+
|| differs(prev.contributors, cur.contributors)
|
|
100
|
+
|| differs(prev.hypotheses, cur.hypotheses)
|
|
101
|
+
|| differs(prev.legacyHypothesis, cur.legacyHypothesis)
|
|
102
|
+
|| differs(prev.evidence, cur.evidence);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildInformationGainProjection(events, taskId) {
|
|
106
|
+
const taskEvents = (events ?? [])
|
|
107
|
+
.filter((event) => !taskId || !event.taskId || event.taskId === taskId)
|
|
108
|
+
.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
109
|
+
|
|
110
|
+
const diagnosticEvents = diagnosticEventsForTask(taskEvents, taskId)
|
|
111
|
+
.sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
112
|
+
if (diagnosticEvents.length === 0) return [];
|
|
113
|
+
|
|
114
|
+
const failure = failureStateByCycle(taskEvents);
|
|
115
|
+
|
|
116
|
+
// Build final per-cycle entries first; effectiveGain is computed once at the
|
|
117
|
+
// end from fully final dimensions (no post-mutation anywhere).
|
|
118
|
+
const built = [];
|
|
119
|
+
let previousDiagnostic = null;
|
|
120
|
+
for (const diagnostic of diagnosticEvents) {
|
|
121
|
+
const intervalStart = previousDiagnostic ? previousDiagnostic.seq : -Infinity;
|
|
122
|
+
const interval = intervalEvents(taskEvents, intervalStart, diagnostic.seq);
|
|
123
|
+
|
|
124
|
+
const hypothesisDispositionChanged =
|
|
125
|
+
interval.some((event) => event.event === "HYPOTHESIS_DISPOSITION_RECORDED");
|
|
126
|
+
|
|
127
|
+
// Intervention deltas recognize only genuinely NEW semantic interventions:
|
|
128
|
+
// recorded after the previous diagnosis, attributed to the current
|
|
129
|
+
// correction cycle, and never attempted before. Repeating an already-known
|
|
130
|
+
// intervention — or merely having executed the previous cycle's corrective
|
|
131
|
+
// action — is not new information.
|
|
132
|
+
const knownUpToPrev = new Set(
|
|
133
|
+
taskEvents
|
|
134
|
+
.filter((event) => event.event === "INTERVENTION_RECORDED"
|
|
135
|
+
&& (previousDiagnostic ? (event.seq ?? 0) <= previousDiagnostic.seq : false))
|
|
136
|
+
.map((event) => event.details?.interventionSemanticFingerprint
|
|
137
|
+
?? `${event.details?.intervention?.statement ?? ""}`.trim().toLowerCase())
|
|
138
|
+
.filter(Boolean),
|
|
139
|
+
);
|
|
140
|
+
const novelInInterval = interval
|
|
141
|
+
.filter((event) => event.event === "INTERVENTION_RECORDED")
|
|
142
|
+
.map((event) => event.details?.interventionSemanticFingerprint
|
|
143
|
+
?? `${event.details?.intervention?.statement ?? ""}`.trim().toLowerCase())
|
|
144
|
+
.filter((fingerprint) => fingerprint && !knownUpToPrev.has(fingerprint));
|
|
145
|
+
// A genuinely new intervention counts as information only when the
|
|
146
|
+
// diagnosis itself moves: an identical re-proposal of the previous
|
|
147
|
+
// diagnosis after executing its already-known corrective action carries
|
|
148
|
+
// no new semantic state.
|
|
149
|
+
const sameSemanticsAsPrevious = Boolean(previousDiagnostic)
|
|
150
|
+
&& !snapshotHasContentDelta(previousDiagnostic.details ?? {}, diagnostic.details ?? {});
|
|
151
|
+
const interventionChanged = Boolean(previousDiagnostic)
|
|
152
|
+
&& novelInInterval.length > 0
|
|
153
|
+
&& !sameSemanticsAsPrevious;
|
|
154
|
+
|
|
155
|
+
const cycle = diagnostic.details?.verificationCycle ?? 1;
|
|
156
|
+
const previousCycle = previousDiagnostic?.details?.verificationCycle ?? null;
|
|
157
|
+
const surface = [...(failure.surfaces.get(cycle) ?? new Set())].sort();
|
|
158
|
+
const signatures = [...(failure.signatures.get(cycle) ?? new Set())].sort();
|
|
159
|
+
const previousSurface = previousCycle != null
|
|
160
|
+
? [...(failure.surfaces.get(previousCycle) ?? new Set())].sort()
|
|
161
|
+
: null;
|
|
162
|
+
const previousSignatures = previousCycle != null
|
|
163
|
+
? [...(failure.signatures.get(previousCycle) ?? new Set())].sort()
|
|
164
|
+
: null;
|
|
165
|
+
const hasPreviousFailureState = previousSurface !== null;
|
|
166
|
+
const failureSurfaceChanged = hasPreviousFailureState
|
|
167
|
+
? !sameSortedSet(surface, previousSurface)
|
|
168
|
+
: false;
|
|
169
|
+
const failureSignatureChanged = hasPreviousFailureState
|
|
170
|
+
? !sameSortedSet(signatures, previousSignatures)
|
|
171
|
+
: false;
|
|
172
|
+
|
|
173
|
+
// Strategy compares the PROPOSED diagnostic approach: the case's own
|
|
174
|
+
// semantic content on both sides (identical bases, so the delta is real).
|
|
175
|
+
const strategyFingerprint = strategyFingerprintFor(diagnostic, []);
|
|
176
|
+
const previousStrategyFingerprint = previousDiagnostic
|
|
177
|
+
? strategyFingerprintFor(previousDiagnostic, [])
|
|
178
|
+
: null;
|
|
179
|
+
const strategyChanged = Boolean(previousStrategyFingerprint
|
|
180
|
+
&& strategyFingerprint !== previousStrategyFingerprint);
|
|
181
|
+
|
|
182
|
+
const snapshot = snapshotFor(diagnostic);
|
|
183
|
+
|
|
184
|
+
built.push({
|
|
185
|
+
verificationCycle: cycle,
|
|
186
|
+
sequence: diagnostic.seq ?? null,
|
|
187
|
+
diagnosticSequence: diagnostic.seq ?? null,
|
|
188
|
+
sourceModel: diagnostic.event === "DIAGNOSTIC_CASE_RECORDED"
|
|
189
|
+
? "STRUCTURED_DIAGNOSTIC_CASE_V1"
|
|
190
|
+
: "LEGACY_DIAGNOSIS_V1",
|
|
191
|
+
snapshot,
|
|
192
|
+
dimensionsInput: {
|
|
193
|
+
hypothesisDispositionChanged,
|
|
194
|
+
failureSignatureChanged,
|
|
195
|
+
failureSurfaceChanged,
|
|
196
|
+
interventionChanged,
|
|
197
|
+
strategyChanged,
|
|
198
|
+
hypothesisEliminated: false,
|
|
199
|
+
},
|
|
200
|
+
evidence: {
|
|
201
|
+
semanticRefs: [...(snapshot.evidenceRefs ?? [])].sort(),
|
|
202
|
+
surface, signatures, strategyFingerprint,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
previousDiagnostic = diagnostic;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Hypothesis elimination: an id disappearing only counts as elimination when
|
|
210
|
+
// no surviving hypothesis carries the same normalized statement — ID-only
|
|
211
|
+
// churn is artificial novelty and must never create gain.
|
|
212
|
+
for (let i = 1; i < built.length; i++) {
|
|
213
|
+
if (built[i].sourceModel !== "STRUCTURED_DIAGNOSTIC_CASE_V1"
|
|
214
|
+
|| built[i - 1].sourceModel !== "STRUCTURED_DIAGNOSTIC_CASE_V1") continue;
|
|
215
|
+
const previousHypotheses = diagnosticEvents[i - 1]?.details?.hypotheses ?? [];
|
|
216
|
+
const currentHypotheses = diagnosticEvents[i]?.details?.hypotheses ?? [];
|
|
217
|
+
const currentStatements = new Set(currentHypotheses.map(
|
|
218
|
+
(hypothesis) => `${hypothesis.statement}`.trim().toLowerCase()));
|
|
219
|
+
built[i].dimensionsInput.hypothesisEliminated = previousHypotheses.some((hypothesis) => {
|
|
220
|
+
const survivedById = currentHypotheses.some((candidate) => candidate.id === hypothesis.id);
|
|
221
|
+
return !survivedById
|
|
222
|
+
&& !currentStatements.has(`${hypothesis.statement}`.trim().toLowerCase());
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Final classification + single-point effectiveGain computation.
|
|
227
|
+
const entries = computeInformationGain(built.map((entry) => ({
|
|
228
|
+
verificationCycle: entry.verificationCycle,
|
|
229
|
+
sequence: entry.sequence,
|
|
230
|
+
snapshot: entry.snapshot,
|
|
231
|
+
context: entry.dimensionsInput,
|
|
232
|
+
})));
|
|
233
|
+
|
|
234
|
+
return built.map((entry, index) => {
|
|
235
|
+
const { dimensions, classification, effectiveGain } = entries[index];
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
verificationCycle: entry.verificationCycle,
|
|
238
|
+
sequence: entry.sequence,
|
|
239
|
+
diagnosticSequence: entry.diagnosticSequence,
|
|
240
|
+
sourceModel: entry.sourceModel,
|
|
241
|
+
evidence: Object.freeze({
|
|
242
|
+
semanticRefs: Object.freeze(entry.evidence.semanticRefs),
|
|
243
|
+
failureSurface: Object.freeze(entry.evidence.surface),
|
|
244
|
+
failureSignatures: Object.freeze(entry.evidence.signatures),
|
|
245
|
+
strategyFingerprint: entry.evidence.strategyFingerprint,
|
|
246
|
+
}),
|
|
247
|
+
dimensions: Object.freeze({ ...dimensions }),
|
|
248
|
+
classification,
|
|
249
|
+
effectiveGain,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// One canonical structured-stall policy (fail-fast):
|
|
255
|
+
// The latest comparable diagnostic state that produces no effective
|
|
256
|
+
// information gain is stalled and may not trigger another blind correction
|
|
257
|
+
// retry. The first diagnosis is never stalled. Legacy diagnosis keeps its
|
|
258
|
+
// own compatibility rule (informationGain === NONE).
|
|
259
|
+
export function evaluateStructuredDiagnosticStall(gainProjection, { verificationCycle = null } = {}) {
|
|
260
|
+
const candidates = verificationCycle == null
|
|
261
|
+
? (gainProjection ?? [])
|
|
262
|
+
: (gainProjection ?? []).filter((entry) => entry.verificationCycle === verificationCycle);
|
|
263
|
+
|
|
264
|
+
const latest = candidates.at(-1) ?? null;
|
|
265
|
+
if (!latest) {
|
|
266
|
+
return { stalled: false, latestGain: null, reason: null };
|
|
267
|
+
}
|
|
268
|
+
if (latest.classification === "FIRST_DIAGNOSIS") {
|
|
269
|
+
return { stalled: false, latestGain: latest, reason: null };
|
|
270
|
+
}
|
|
271
|
+
const stalled = latest.effectiveGain === false;
|
|
272
|
+
return {
|
|
273
|
+
stalled,
|
|
274
|
+
latestGain: latest,
|
|
275
|
+
reason: stalled ? "NO_DIAGNOSTIC_INFORMATION_GAIN" : null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function computeCycleInformationGain(events, taskId, verificationCycle) {
|
|
280
|
+
const projection = buildInformationGainProjection(events, taskId);
|
|
281
|
+
const matching = projection.filter((entry) => entry.verificationCycle === verificationCycle);
|
|
282
|
+
return matching.at(-1) ?? null;
|
|
283
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export const GAIN_DIMENSIONS = Object.freeze([
|
|
2
|
+
"newObservation",
|
|
3
|
+
"newContributor",
|
|
4
|
+
"newHypothesis",
|
|
5
|
+
"hypothesisDispositionChanged",
|
|
6
|
+
"failureSignatureChanged",
|
|
7
|
+
"failureSurfaceChanged",
|
|
8
|
+
"interventionChanged",
|
|
9
|
+
"strategyChanged",
|
|
10
|
+
"newEvidence",
|
|
11
|
+
"hypothesisEliminated",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function statementSet(values) {
|
|
15
|
+
return new Set((values ?? []).map((value) => `${value}`.trim().toLowerCase()).filter(Boolean));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeDiagnosticSnapshot(input) {
|
|
19
|
+
// Idempotent: already-normalized snapshots pass through unchanged so
|
|
20
|
+
// repeated normalization never fabricates empty semantic sets.
|
|
21
|
+
if (input
|
|
22
|
+
&& input.observationStatements instanceof Set
|
|
23
|
+
&& input.contributorStatements instanceof Set
|
|
24
|
+
&& input.hypothesisStatements instanceof Set
|
|
25
|
+
&& input.evidenceRefs instanceof Set) {
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
if (!input) {
|
|
29
|
+
return {
|
|
30
|
+
observationStatements: new Set(),
|
|
31
|
+
contributorStatements: new Set(),
|
|
32
|
+
hypothesisStatements: new Set(),
|
|
33
|
+
evidenceRefs: new Set(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const legacyHypothesis = input.legacy
|
|
37
|
+
? [input.hypothesis ?? ""]
|
|
38
|
+
: (input.hypotheses ?? []).map((hypothesis) => hypothesis.statement ?? "");
|
|
39
|
+
const evidenceRefs = input.legacy
|
|
40
|
+
? (input.evidenceRefs ?? [])
|
|
41
|
+
: [
|
|
42
|
+
...(input.hypotheses ?? []).flatMap((hypothesis) => hypothesis.evidenceRefs ?? []),
|
|
43
|
+
...(input.observations ?? []).map((observation) => observation.evidenceRef).filter(Boolean),
|
|
44
|
+
];
|
|
45
|
+
return {
|
|
46
|
+
observationStatements: statementSet(input.legacy ? [] : (input.observations ?? []).map((observation) => observation.statement)),
|
|
47
|
+
contributorStatements: statementSet(input.legacy ? [] : (input.contributors ?? []).map((contributor) => contributor.statement)),
|
|
48
|
+
hypothesisStatements: statementSet(legacyHypothesis),
|
|
49
|
+
evidenceRefs: statementSet(evidenceRefs),
|
|
50
|
+
hypothesisIds: new Set(input.legacy ? [] : (input.hypotheses ?? []).map((hypothesis) => hypothesis.id)),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasNewValue(previousSet, currentSet) {
|
|
55
|
+
for (const value of currentSet) {
|
|
56
|
+
if (!previousSet.has(value)) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function compareDiagnosticCycles(previousInput, currentInput, context = {}) {
|
|
62
|
+
const previous = previousInput instanceof Set || Array.isArray(previousInput)
|
|
63
|
+
? normalizeDiagnosticSnapshot(null)
|
|
64
|
+
: normalizeDiagnosticSnapshot(previousInput);
|
|
65
|
+
const current = normalizeDiagnosticSnapshot(currentInput);
|
|
66
|
+
|
|
67
|
+
const dimensions = {
|
|
68
|
+
newObservation: hasNewValue(previous.observationStatements, current.observationStatements),
|
|
69
|
+
newContributor: hasNewValue(previous.contributorStatements, current.contributorStatements),
|
|
70
|
+
newHypothesis: hasNewValue(previous.hypothesisStatements, current.hypothesisStatements),
|
|
71
|
+
hypothesisDispositionChanged: Boolean(context.hypothesisDispositionChanged),
|
|
72
|
+
failureSignatureChanged: Boolean(context.failureSignatureChanged),
|
|
73
|
+
failureSurfaceChanged: Boolean(context.failureSurfaceChanged),
|
|
74
|
+
interventionChanged: Boolean(context.interventionChanged),
|
|
75
|
+
strategyChanged: Boolean(context.strategyChanged),
|
|
76
|
+
newEvidence: hasNewValue(previous.evidenceRefs, current.evidenceRefs),
|
|
77
|
+
// Elimination is decided semantically by the cycle analysis projection:
|
|
78
|
+
// an id disappearing while its statement survives is ID-only churn, not gain.
|
|
79
|
+
hypothesisEliminated: Boolean(context.hypothesisEliminated),
|
|
80
|
+
};
|
|
81
|
+
return dimensions;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const CLASSIFICATION = Object.freeze({
|
|
85
|
+
FIRST_DIAGNOSIS: "FIRST_DIAGNOSIS",
|
|
86
|
+
NEW_HYPOTHESIS: "NEW_HYPOTHESIS",
|
|
87
|
+
NEW_EVIDENCE: "NEW_EVIDENCE",
|
|
88
|
+
NEW_HYPOTHESIS_AND_EVIDENCE: "NEW_HYPOTHESIS_AND_EVIDENCE",
|
|
89
|
+
NONE: "NONE",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export function classifyGain(dimensions, { first = false } = {}) {
|
|
93
|
+
if (first) return CLASSIFICATION.FIRST_DIAGNOSIS;
|
|
94
|
+
const hypothesis = dimensions.newHypothesis;
|
|
95
|
+
const evidence = dimensions.newEvidence;
|
|
96
|
+
if (hypothesis && evidence) return CLASSIFICATION.NEW_HYPOTHESIS_AND_EVIDENCE;
|
|
97
|
+
if (hypothesis) return CLASSIFICATION.NEW_HYPOTHESIS;
|
|
98
|
+
if (evidence) return CLASSIFICATION.NEW_EVIDENCE;
|
|
99
|
+
return CLASSIFICATION.NONE;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isEffectiveGain(dimensions, classification) {
|
|
103
|
+
if (classification !== CLASSIFICATION.NONE) return true;
|
|
104
|
+
|
|
105
|
+
// Every meaningful semantic dimension counts as effective gain.
|
|
106
|
+
// newHypothesis and newEvidence remain covered by the compatibility
|
|
107
|
+
// classification above; the remaining dimensions are checked explicitly.
|
|
108
|
+
return Boolean(
|
|
109
|
+
dimensions.newObservation
|
|
110
|
+
|| dimensions.newContributor
|
|
111
|
+
|| dimensions.hypothesisDispositionChanged
|
|
112
|
+
|| dimensions.failureSignatureChanged
|
|
113
|
+
|| dimensions.failureSurfaceChanged
|
|
114
|
+
|| dimensions.interventionChanged
|
|
115
|
+
|| dimensions.strategyChanged
|
|
116
|
+
|| dimensions.hypothesisEliminated
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function computeInformationGain(entries) {
|
|
121
|
+
const results = [];
|
|
122
|
+
let previous = null;
|
|
123
|
+
let seenFirst = false;
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const dimensions = compareDiagnosticCycles(previous?.snapshot ?? null, entry.snapshot, entry.context ?? {});
|
|
126
|
+
const classification = classifyGain(dimensions, { first: !seenFirst });
|
|
127
|
+
seenFirst = true;
|
|
128
|
+
results.push({
|
|
129
|
+
verificationCycle: entry.verificationCycle,
|
|
130
|
+
sequence: entry.sequence ?? null,
|
|
131
|
+
effectiveGain: isEffectiveGain(dimensions, classification),
|
|
132
|
+
classification,
|
|
133
|
+
dimensions,
|
|
134
|
+
});
|
|
135
|
+
previous = entry;
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
package/src/core/inspect.js
CHANGED
|
@@ -12,6 +12,99 @@ import { trustedAuthorityConfiguration } from "./trusted-authority.js";
|
|
|
12
12
|
import { reconcileContinuity } from "./continuity-reconciliation.js";
|
|
13
13
|
import { continuityFinding, continuityIsHealthy } from "./continuity-observability.js";
|
|
14
14
|
import { findTaskById } from "./task-discovery.js";
|
|
15
|
+
import { buildTaskTrace } from "./trace.js";
|
|
16
|
+
import { evaluateProgress } from "./progress.js";
|
|
17
|
+
import { readEvents } from "./events.js";
|
|
18
|
+
|
|
19
|
+
function reasonCodesFor({ state, trace, progress }) {
|
|
20
|
+
const codes = [];
|
|
21
|
+
codes.push(trace.integrity.valid ? "LEDGER_VALID" : "LEDGER_INCONSISTENT");
|
|
22
|
+
if (state?.status === "VALID" || state?.status === "FRESH") codes.push("REPOSITORY_FRESH");
|
|
23
|
+
if (["STALE", "REVALIDATION_REQUIRED"].includes(state?.status)) codes.push("REPOSITORY_STALE");
|
|
24
|
+
if (trace.task.phase === "COMPLETE") codes.push("COMPLETION_VALID");
|
|
25
|
+
else codes.push("COMPLETION_INCOMPLETE");
|
|
26
|
+
if (progress.status === "ADVANCING") codes.push("DIAGNOSTIC_PROGRESS_ADVANCING");
|
|
27
|
+
if (progress.status === "STALLED") codes.push("DIAGNOSTIC_PROGRESS_STALLED");
|
|
28
|
+
return codes;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildTaskInspection({ target, packageRoot, taskId, state, classifiedStatus = null }) {
|
|
32
|
+
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
33
|
+
const events = await readEvents(target, packageRoot, { taskId });
|
|
34
|
+
const progress = evaluateProgress({ state, events });
|
|
35
|
+
const failedRequirements = [...new Set(
|
|
36
|
+
trace.checks
|
|
37
|
+
.filter((check) => check.currentResult === "failed" || check.currentResult === "blocked")
|
|
38
|
+
.map((check) => check.requirement ?? check.id),
|
|
39
|
+
)].sort();
|
|
40
|
+
|
|
41
|
+
const issues = [];
|
|
42
|
+
if (!trace.snapshot.consistent) {
|
|
43
|
+
issues.push({ code: "E_TRACE_SNAPSHOT_INCONSISTENT", message: "Task artifacts changed while being read; rerun inspect for a consistent view." });
|
|
44
|
+
}
|
|
45
|
+
for (const error of trace.integrity.errors) {
|
|
46
|
+
issues.push({ code: error.code ?? "E_EVENT_INVALID", message: error.message });
|
|
47
|
+
}
|
|
48
|
+
if (failedRequirements.length > 0 && trace.diagnostics.cases.length === 0 && trace.diagnostics.legacyDiagnoses.length === 0) {
|
|
49
|
+
issues.push({ code: "E_DIAGNOSIS_REQUIRED", message: "Failed requirements have no recorded diagnosis." });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const explanation = {
|
|
53
|
+
result: failedRequirements.length > 0 ? "INCOMPLETE_VERIFICATION" : (trace.task.phase === "COMPLETE" ? "COMPLETE" : "INCOMPLETE"),
|
|
54
|
+
reasons: reasonCodesFor({ state: { status: classifiedStatus }, trace, progress }),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
task: taskId,
|
|
59
|
+
snapshot: {
|
|
60
|
+
consistent: trace.snapshot.consistent,
|
|
61
|
+
stateRevision: trace.snapshot.stateRevision,
|
|
62
|
+
ledgerTailSequence: trace.snapshot.ledgerTailSequence,
|
|
63
|
+
},
|
|
64
|
+
lifecycle: {
|
|
65
|
+
phase: trace.task.phase,
|
|
66
|
+
verificationCycle: trace.task.verificationCycle,
|
|
67
|
+
transitions: trace.transitions,
|
|
68
|
+
},
|
|
69
|
+
history: {
|
|
70
|
+
eventCount: trace.events.length,
|
|
71
|
+
quality: trace.historyQuality,
|
|
72
|
+
},
|
|
73
|
+
verification: {
|
|
74
|
+
checks: trace.checks.map((check) => ({
|
|
75
|
+
id: check.id,
|
|
76
|
+
requirement: check.requirement,
|
|
77
|
+
currentResult: check.currentResult,
|
|
78
|
+
attemptCount: check.attemptCount,
|
|
79
|
+
failedAttempts: check.failedAttempts,
|
|
80
|
+
})),
|
|
81
|
+
failedRequirements,
|
|
82
|
+
},
|
|
83
|
+
diagnostics: {
|
|
84
|
+
legacyDiagnosisCount: trace.diagnostics.legacyDiagnoses.length,
|
|
85
|
+
diagnosticCaseCount: trace.diagnostics.cases.length,
|
|
86
|
+
interventionCount: trace.diagnostics.interventions.length,
|
|
87
|
+
dispositionCount: trace.diagnostics.dispositions.length,
|
|
88
|
+
latestCase: trace.diagnostics.cases.at(-1) ?? null,
|
|
89
|
+
},
|
|
90
|
+
progress,
|
|
91
|
+
failureSurfaces: trace.failureSurfaces,
|
|
92
|
+
failureSignatures: trace.failureSignatures.map((entry) => ({
|
|
93
|
+
signature: entry.signature,
|
|
94
|
+
requirements: entry.requirements,
|
|
95
|
+
cycles: entry.cycles,
|
|
96
|
+
})),
|
|
97
|
+
integrity: {
|
|
98
|
+
valid: trace.integrity.valid,
|
|
99
|
+
errors: trace.integrity.errors,
|
|
100
|
+
},
|
|
101
|
+
audit: {},
|
|
102
|
+
completion: {},
|
|
103
|
+
issues,
|
|
104
|
+
explanation,
|
|
105
|
+
next: { command: `forgeloop next --task ${taskId} --json` },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
15
108
|
|
|
16
109
|
function profileMetadata(bytes) {
|
|
17
110
|
const text = bytes.toString("utf8");
|
|
@@ -40,7 +133,8 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
40
133
|
const effectiveStateRel = stateFile ?? (taskId ? taskArtifactPath(taskId, "state") : WORK_STATE_PATH);
|
|
41
134
|
const statePath = ensureWithin(target, effectiveStateRel);
|
|
42
135
|
const statePresent = await fileExists(statePath);
|
|
43
|
-
const
|
|
136
|
+
const classifiedState = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
|
|
137
|
+
const rawState = classifiedState?.state ?? null;
|
|
44
138
|
const taskInfo = taskId ? await findTaskById(target, taskId, packageRoot) : null;
|
|
45
139
|
const continuity = await reconcileContinuity({ target, packageRoot, taskId });
|
|
46
140
|
const schemaRoot = manifest?.layoutVersion >= 2
|
|
@@ -95,12 +189,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
95
189
|
});
|
|
96
190
|
}
|
|
97
191
|
|
|
98
|
-
if (
|
|
192
|
+
if (classifiedState.status === "INVALID") {
|
|
99
193
|
findings.push({
|
|
100
194
|
code: "state-invalid",
|
|
101
195
|
severity: "error",
|
|
102
|
-
path:
|
|
103
|
-
message:
|
|
196
|
+
path: effectiveStateRel,
|
|
197
|
+
message: classifiedState.error ?? "Work state is invalid.",
|
|
104
198
|
remediation: "Repair or clear the checkpoint after reviewing the parse error.",
|
|
105
199
|
evidence: createEvidence({ kind: "BLOCKED", source: WORK_STATE_PATH, result: "invalid" }),
|
|
106
200
|
});
|
|
@@ -113,9 +207,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
113
207
|
})];
|
|
114
208
|
const evidence = [
|
|
115
209
|
...(doctor.evidence ?? []),
|
|
116
|
-
...(
|
|
210
|
+
...(classifiedState.evidence ?? []),
|
|
117
211
|
...protocolEvidence,
|
|
118
212
|
];
|
|
213
|
+
const taskInspection = taskId
|
|
214
|
+
? await buildTaskInspection({ target, packageRoot, taskId, state: rawState, classifiedStatus: classifiedState.status })
|
|
215
|
+
: null;
|
|
119
216
|
return {
|
|
120
217
|
target: { path: target },
|
|
121
218
|
authority: trustedAuthorityConfiguration({ target, authorityContext, runtimeContext }),
|
|
@@ -148,7 +245,7 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
148
245
|
schemas: schemaHealth.schemas,
|
|
149
246
|
evidence: protocolEvidence,
|
|
150
247
|
},
|
|
151
|
-
state: { ...
|
|
248
|
+
state: { ...classifiedState, path: effectiveStateRel, present: statePresent },
|
|
152
249
|
recovery: taskInfo?.recovery ?? null,
|
|
153
250
|
claims: taskInfo ? {
|
|
154
251
|
state: taskInfo.claimState,
|
|
@@ -165,11 +262,12 @@ export async function inspectTarget({ target, packageRoot, contractFile = null,
|
|
|
165
262
|
},
|
|
166
263
|
findings,
|
|
167
264
|
evidence,
|
|
265
|
+
...(taskInspection ? { taskInspection } : {}),
|
|
168
266
|
ok: doctor.ok
|
|
169
267
|
&& !manifestError
|
|
170
268
|
&& schemaHealth.status === "valid"
|
|
171
269
|
&& taskInfo?.ownershipValid !== false
|
|
172
|
-
&& !["INVALID", "REVALIDATION_REQUIRED"].includes(
|
|
270
|
+
&& !["INVALID", "REVALIDATION_REQUIRED"].includes(classifiedState.status)
|
|
173
271
|
&& continuityIsHealthy(continuity),
|
|
174
272
|
};
|
|
175
273
|
}
|