@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.
Files changed (126) hide show
  1. package/DOCS_INDEX.md +13 -8
  2. package/EXECUTION_STATE.md +20 -0
  3. package/LOOP_ENGINEERING.md +81 -0
  4. package/LOOP_SYSTEM_DESIGN.md +32 -0
  5. package/PROTOCOL_INTEGRATION.md +46 -0
  6. package/QUALITY_SCORECARD.md +2 -0
  7. package/README.md +31 -9
  8. package/THIRD_PARTY_NOTICES.md +15 -0
  9. package/THREAT_MODEL.md +39 -0
  10. package/docs/ARTIFACT_REFERENCE.md +140 -0
  11. package/docs/CLI_REFERENCE.md +294 -3
  12. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  13. package/docs/DOCUMENTATION_GUIDE.md +22 -13
  14. package/docs/EXECUTION_TRACE.md +76 -0
  15. package/docs/MCP.md +33 -0
  16. package/docs/RECIPES.md +67 -0
  17. package/docs/TROUBLESHOOTING.md +106 -2
  18. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  19. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  20. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  21. package/docs/diagrams/README.md +55 -0
  22. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  23. package/docs/diagrams/manifest.json +42 -0
  24. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  25. package/package.json +8 -6
  26. package/schemas/action.schema.json +100 -0
  27. package/schemas/approval.schema.json +51 -0
  28. package/schemas/capability-policy.schema.json +41 -0
  29. package/schemas/diagnostic-case.schema.json +85 -0
  30. package/schemas/execution-receipt.schema.json +16 -0
  31. package/schemas/hypothesis-disposition.schema.json +16 -0
  32. package/schemas/intervention.schema.json +27 -0
  33. package/schemas/policy-lock.schema.json +1 -0
  34. package/schemas/policy-snapshot.schema.json +2 -0
  35. package/schemas/trajectory-evaluation.schema.json +64 -0
  36. package/schemas/trajectory-scenario.schema.json +42 -0
  37. package/src/cli.js +94 -0
  38. package/src/commands/action-authorize.js +41 -0
  39. package/src/commands/action-propose.js +10 -0
  40. package/src/commands/action-reconcile.js +10 -0
  41. package/src/commands/action-record.js +47 -0
  42. package/src/commands/action-show.js +10 -0
  43. package/src/commands/action-verify.js +10 -0
  44. package/src/commands/advance.js +7 -2
  45. package/src/commands/approval-request.js +64 -0
  46. package/src/commands/approval-resolve.js +10 -0
  47. package/src/commands/baseline.js +3 -3
  48. package/src/commands/eval.js +6 -0
  49. package/src/commands/history.js +18 -0
  50. package/src/commands/init.js +2 -2
  51. package/src/commands/inspect.js +49 -0
  52. package/src/commands/metrics.js +7 -0
  53. package/src/commands/next.js +8 -2
  54. package/src/commands/policy-discover.js +2 -2
  55. package/src/commands/record-diagnosis.js +37 -1
  56. package/src/commands/record-hypothesis-disposition.js +45 -0
  57. package/src/commands/record-intervention.js +35 -0
  58. package/src/commands/reflect.js +38 -0
  59. package/src/commands/report.js +9 -1
  60. package/src/commands/run-action.js +18 -0
  61. package/src/commands/trace.js +34 -0
  62. package/src/commands/validate-protocol.js +21 -13
  63. package/src/core/action-authorization.js +106 -0
  64. package/src/core/action-constants.js +86 -0
  65. package/src/core/action-execution.js +105 -0
  66. package/src/core/action-ledger-projection.js +302 -0
  67. package/src/core/action-model.js +581 -0
  68. package/src/core/action-readiness.js +141 -0
  69. package/src/core/action-reconciliation-policy.js +49 -0
  70. package/src/core/action-reconciliation.js +66 -0
  71. package/src/core/action-verification.js +111 -0
  72. package/src/core/actions.js +462 -0
  73. package/src/core/approvals.js +405 -0
  74. package/src/core/artifact-registry.js +48 -0
  75. package/src/core/audit.js +25 -0
  76. package/src/core/bundles.js +15 -0
  77. package/src/core/capability-policy.js +226 -0
  78. package/src/core/cli-command-definitions.js +210 -1
  79. package/src/core/command-executors.js +171 -15
  80. package/src/core/command-runtime.js +12 -1
  81. package/src/core/completion-artifacts.js +37 -12
  82. package/src/core/completion-recovery-rebind.js +194 -0
  83. package/src/core/completion.js +70 -0
  84. package/src/core/continuity-reconciliation.js +24 -5
  85. package/src/core/diagnostic-model.js +396 -0
  86. package/src/core/diagnostic-projection.js +51 -0
  87. package/src/core/diagnostic-record.js +360 -0
  88. package/src/core/error-codes.js +343 -0
  89. package/src/core/events.js +41 -1
  90. package/src/core/execution-prerequisites.js +4 -1
  91. package/src/core/execution.js +26 -188
  92. package/src/core/failure-signature.js +70 -0
  93. package/src/core/failure-surface.js +57 -0
  94. package/src/core/history.js +110 -0
  95. package/src/core/hypothesis-projection.js +85 -0
  96. package/src/core/information-gain-projection.js +283 -0
  97. package/src/core/information-gain.js +138 -0
  98. package/src/core/inspect.js +105 -7
  99. package/src/core/integration-invocation-policy.js +47 -0
  100. package/src/core/integration-resources.js +51 -0
  101. package/src/core/next-action-model.js +35 -1
  102. package/src/core/next-action.js +459 -3
  103. package/src/core/phase.js +40 -21
  104. package/src/core/policy-engine.js +113 -6
  105. package/src/core/preflight-consistency.js +31 -5
  106. package/src/core/preflight.js +19 -2
  107. package/src/core/prepared-execution.js +227 -0
  108. package/src/core/progress.js +41 -4
  109. package/src/core/protocol-info.js +48 -0
  110. package/src/core/protocol.js +14 -0
  111. package/src/core/receipt.js +1 -0
  112. package/src/core/reconcile-closure.js +15 -12
  113. package/src/core/reflection.js +305 -0
  114. package/src/core/resumability.js +57 -3
  115. package/src/core/schema-validation.js +8 -0
  116. package/src/core/strategy-analysis.js +97 -0
  117. package/src/core/task-paths.js +28 -0
  118. package/src/core/task-snapshot.js +53 -0
  119. package/src/core/templates.js +8 -0
  120. package/src/core/trace.js +548 -0
  121. package/src/core/trajectory-evaluation.js +71 -0
  122. package/src/core/trajectory-metrics.js +80 -0
  123. package/src/core/transaction.js +8 -0
  124. package/src/core/work-state.js +10 -5
  125. package/docs/assets/forgeloop-flow.svg +0 -1
  126. 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
+ }
@@ -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 state = await readAndClassifyWorkState({ target, packageRoot, contractFile, taskId, stateFile: effectiveStateRel });
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 (state.status === "INVALID") {
192
+ if (classifiedState.status === "INVALID") {
99
193
  findings.push({
100
194
  code: "state-invalid",
101
195
  severity: "error",
102
- path: WORK_STATE_PATH,
103
- message: state.error ?? "Work state is invalid.",
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
- ...(state.evidence ?? []),
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: { ...state, path: WORK_STATE_PATH, present: statePresent },
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(state.status)
270
+ && !["INVALID", "REVALIDATION_REQUIRED"].includes(classifiedState.status)
173
271
  && continuityIsHealthy(continuity),
174
272
  };
175
273
  }