@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.
Files changed (163) hide show
  1. package/.github/copilot-instructions.md +1 -0
  2. package/AGENTS.md +1 -0
  3. package/CLAUDE.md +1 -0
  4. package/DOCS_INDEX.md +20 -8
  5. package/EXECUTION_STATE.md +60 -0
  6. package/LOOP_ENGINEERING.md +135 -5
  7. package/LOOP_SYSTEM_DESIGN.md +54 -1
  8. package/PROTOCOL_INTEGRATION.md +87 -0
  9. package/QUALITY_SCORECARD.md +2 -0
  10. package/README.md +69 -9
  11. package/TERMINOLOGY.md +15 -0
  12. package/THIRD_PARTY_NOTICES.md +30 -0
  13. package/THREAT_MODEL.md +59 -1
  14. package/docs/ARTIFACT_REFERENCE.md +183 -0
  15. package/docs/CLI_REFERENCE.md +391 -6
  16. package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
  17. package/docs/DIAGNOSTIC_MODEL.md +181 -0
  18. package/docs/DOCUMENTATION_GUIDE.md +36 -13
  19. package/docs/EXECUTION_TRACE.md +76 -0
  20. package/docs/GETTING_STARTED.md +1 -0
  21. package/docs/MCP.md +159 -0
  22. package/docs/RECIPES.md +149 -0
  23. package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
  24. package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
  25. package/docs/TROUBLESHOOTING.md +217 -3
  26. package/docs/UNIVERSAL_INTEGRATION.md +48 -0
  27. package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
  28. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
  29. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
  30. package/docs/diagrams/README.md +55 -0
  31. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
  32. package/docs/diagrams/manifest.json +42 -0
  33. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
  34. package/package.json +21 -8
  35. package/schemas/action.schema.json +100 -0
  36. package/schemas/approval.schema.json +51 -0
  37. package/schemas/capability-policy.schema.json +41 -0
  38. package/schemas/diagnostic-case.schema.json +85 -0
  39. package/schemas/execution-receipt.schema.json +16 -0
  40. package/schemas/hypothesis-disposition.schema.json +16 -0
  41. package/schemas/intervention.schema.json +27 -0
  42. package/schemas/policy-lock.schema.json +1 -0
  43. package/schemas/policy-snapshot.schema.json +2 -0
  44. package/schemas/task-recovery.schema.json +61 -0
  45. package/schemas/trajectory-evaluation.schema.json +64 -0
  46. package/schemas/trajectory-scenario.schema.json +42 -0
  47. package/src/cli.js +267 -347
  48. package/src/commands/action-authorize.js +41 -0
  49. package/src/commands/action-propose.js +10 -0
  50. package/src/commands/action-reconcile.js +10 -0
  51. package/src/commands/action-record.js +47 -0
  52. package/src/commands/action-show.js +10 -0
  53. package/src/commands/action-verify.js +10 -0
  54. package/src/commands/advance.js +7 -2
  55. package/src/commands/approval-request.js +64 -0
  56. package/src/commands/approval-resolve.js +10 -0
  57. package/src/commands/audit.js +5 -0
  58. package/src/commands/baseline.js +3 -3
  59. package/src/commands/eval.js +6 -0
  60. package/src/commands/history.js +18 -0
  61. package/src/commands/init.js +2 -2
  62. package/src/commands/inspect.js +55 -0
  63. package/src/commands/metrics.js +7 -0
  64. package/src/commands/next.js +8 -2
  65. package/src/commands/policy-discover.js +2 -2
  66. package/src/commands/progress.js +6 -2
  67. package/src/commands/record-diagnosis.js +37 -1
  68. package/src/commands/record-hypothesis-disposition.js +45 -0
  69. package/src/commands/record-intervention.js +35 -0
  70. package/src/commands/reflect.js +38 -0
  71. package/src/commands/report.js +9 -1
  72. package/src/commands/run-action.js +18 -0
  73. package/src/commands/status.js +17 -0
  74. package/src/commands/task-create.js +39 -1
  75. package/src/commands/task-list.js +14 -1
  76. package/src/commands/task-lock-status.js +2 -2
  77. package/src/commands/task-recover.js +202 -0
  78. package/src/commands/task-repair-legacy-recovery.js +417 -0
  79. package/src/commands/task-resume.js +172 -0
  80. package/src/commands/task-scope.js +23 -4
  81. package/src/commands/task-show.js +18 -4
  82. package/src/commands/trace.js +34 -0
  83. package/src/commands/validate-protocol.js +40 -15
  84. package/src/core/action-authorization.js +106 -0
  85. package/src/core/action-constants.js +86 -0
  86. package/src/core/action-execution.js +105 -0
  87. package/src/core/action-ledger-projection.js +302 -0
  88. package/src/core/action-model.js +581 -0
  89. package/src/core/action-readiness.js +141 -0
  90. package/src/core/action-reconciliation-policy.js +49 -0
  91. package/src/core/action-reconciliation.js +66 -0
  92. package/src/core/action-verification.js +111 -0
  93. package/src/core/actions.js +462 -0
  94. package/src/core/approvals.js +405 -0
  95. package/src/core/artifact-registry.js +60 -0
  96. package/src/core/audit.js +45 -4
  97. package/src/core/bundles.js +30 -0
  98. package/src/core/capability-policy.js +226 -0
  99. package/src/core/cli-command-definitions.js +260 -5
  100. package/src/core/command-executors.js +543 -0
  101. package/src/core/command-input.js +107 -0
  102. package/src/core/command-runtime.js +117 -0
  103. package/src/core/completion-artifacts.js +39 -15
  104. package/src/core/completion-ownership.js +88 -0
  105. package/src/core/completion-recovery-rebind.js +194 -0
  106. package/src/core/completion.js +70 -0
  107. package/src/core/continuity-reconciliation.js +24 -5
  108. package/src/core/diagnostic-model.js +396 -0
  109. package/src/core/diagnostic-projection.js +51 -0
  110. package/src/core/diagnostic-record.js +360 -0
  111. package/src/core/error-codes.js +461 -1
  112. package/src/core/events.js +171 -2
  113. package/src/core/execution-prerequisites.js +4 -1
  114. package/src/core/execution.js +26 -188
  115. package/src/core/failure-signature.js +70 -0
  116. package/src/core/failure-surface.js +57 -0
  117. package/src/core/filesystem.js +55 -6
  118. package/src/core/history.js +110 -0
  119. package/src/core/hypothesis-projection.js +85 -0
  120. package/src/core/information-gain-projection.js +283 -0
  121. package/src/core/information-gain.js +138 -0
  122. package/src/core/inspect.js +132 -7
  123. package/src/core/integration-invocation-policy.js +217 -0
  124. package/src/core/integration-limits.js +20 -0
  125. package/src/core/integration-resources.js +178 -0
  126. package/src/core/next-action-model.js +94 -0
  127. package/src/core/next-action.js +490 -3
  128. package/src/core/phase.js +42 -22
  129. package/src/core/policy-engine.js +113 -6
  130. package/src/core/preflight-consistency.js +31 -5
  131. package/src/core/preflight.js +19 -2
  132. package/src/core/prepared-execution.js +227 -0
  133. package/src/core/progress.js +41 -4
  134. package/src/core/project-root.js +21 -0
  135. package/src/core/protocol-info.js +61 -0
  136. package/src/core/protocol.js +14 -0
  137. package/src/core/receipt.js +1 -0
  138. package/src/core/reconcile-closure.js +35 -10
  139. package/src/core/recovery-history.js +116 -0
  140. package/src/core/reflection.js +305 -0
  141. package/src/core/resumability.js +57 -3
  142. package/src/core/schema-validation.js +9 -0
  143. package/src/core/strategy-analysis.js +97 -0
  144. package/src/core/task-claim-state.js +272 -0
  145. package/src/core/task-command.js +5 -1
  146. package/src/core/task-conflict-inspection.js +321 -0
  147. package/src/core/task-context.js +32 -29
  148. package/src/core/task-discovery.js +14 -1
  149. package/src/core/task-lock.js +216 -22
  150. package/src/core/task-paths.js +31 -2
  151. package/src/core/task-recovery-migration.js +192 -0
  152. package/src/core/task-recovery.js +205 -0
  153. package/src/core/task-scope.js +33 -1
  154. package/src/core/task-snapshot.js +53 -0
  155. package/src/core/templates.js +9 -0
  156. package/src/core/trace.js +548 -0
  157. package/src/core/trajectory-evaluation.js +71 -0
  158. package/src/core/trajectory-metrics.js +80 -0
  159. package/src/core/transaction.js +36 -2
  160. package/src/core/work-state.js +10 -5
  161. package/src/integration.js +47 -0
  162. package/docs/assets/forgeloop-flow.svg +0 -1
  163. package/docs/forgeloop-flow.mmd +0 -51
@@ -0,0 +1,396 @@
1
+ import { canonicalFingerprint } from "./artifacts.js";
2
+ import { assertFailureClass } from "./protocol.js";
3
+
4
+ export const DIAGNOSTIC_SCHEMA_VERSION = 1;
5
+
6
+ const LIMITS = Object.freeze({
7
+ observationsPerCase: 64,
8
+ contributorsPerCase: 64,
9
+ hypothesesPerCase: 32,
10
+ evidenceRefsPerHypothesis: 32,
11
+ contributorRefsPerHypothesis: 32,
12
+ statementLength: 4096,
13
+ reasonLength: 4096,
14
+ idLength: 128,
15
+ });
16
+
17
+ export const DIAGNOSTIC_LIMITS = LIMITS;
18
+
19
+ export const OBSERVATION_KINDS = Object.freeze([
20
+ "CHECK_RESULT",
21
+ "EXECUTION_RESULT",
22
+ "REPOSITORY_STATE",
23
+ "ENVIRONMENT_STATE",
24
+ "DEPENDENCY_STATE",
25
+ "MANUAL_OBSERVATION",
26
+ "PROTOCOL_STATE",
27
+ ]);
28
+
29
+ export const OBSERVATION_PROVENANCE = Object.freeze([
30
+ "FORGELOOP_EXECUTED",
31
+ "ACTOR_REPORTED",
32
+ "MANUAL_OBSERVATION",
33
+ ]);
34
+
35
+ export const CONTRIBUTOR_TYPES = Object.freeze([
36
+ "CODE",
37
+ "CONFIGURATION",
38
+ "ARCHITECTURE",
39
+ "DEPENDENCY",
40
+ "ENVIRONMENT",
41
+ "DATA",
42
+ "TIMING",
43
+ "CONCURRENCY",
44
+ "STATE",
45
+ "AUTHORITY",
46
+ "CAPABILITY",
47
+ "ASSUMPTION",
48
+ "INTERFACE",
49
+ "PROCESS",
50
+ "EXTERNAL_SERVICE",
51
+ "UNKNOWN",
52
+ ]);
53
+
54
+ export const CONTRIBUTOR_STATUSES = Object.freeze([
55
+ "OBSERVED",
56
+ "INFERRED",
57
+ "SUSPECTED",
58
+ "ELIMINATED",
59
+ "CONFIRMED_RELEVANT",
60
+ ]);
61
+
62
+ export const HYPOTHESIS_STATUSES = Object.freeze([
63
+ "OPEN",
64
+ "SUPPORTED",
65
+ "WEAKENED",
66
+ "FALSIFIED",
67
+ "SUPERSEDED",
68
+ "UNRESOLVED",
69
+ ]);
70
+
71
+ export const SETTLEMENT_TYPES = Object.freeze([
72
+ "PREDICATE",
73
+ "CHECK_STATUS",
74
+ "FAILURE_SIGNATURE_CHANGE",
75
+ "REQUIREMENT_STATUS",
76
+ "MANUAL_OBSERVATION",
77
+ ]);
78
+
79
+ export const INTERVENTION_KINDS = Object.freeze([
80
+ "CODE_CHANGE",
81
+ "CONFIG_CHANGE",
82
+ "TEST_CHANGE",
83
+ "FIXTURE_CHANGE",
84
+ "ENVIRONMENT_CHANGE",
85
+ "DEPENDENCY_CHANGE",
86
+ "ROLLBACK",
87
+ "ISOLATION",
88
+ "INSTRUMENTATION",
89
+ "NO_MUTATION_EXPERIMENT",
90
+ "OTHER",
91
+ ]);
92
+
93
+ export const DISPOSITION_TRANSITIONS = Object.freeze({
94
+ OPEN: ["SUPPORTED", "WEAKENED", "FALSIFIED", "SUPERSEDED", "UNRESOLVED"],
95
+ SUPPORTED: ["WEAKENED", "FALSIFIED", "SUPERSEDED"],
96
+ WEAKENED: ["SUPPORTED", "FALSIFIED", "SUPERSEDED"],
97
+ });
98
+
99
+ function invalid(code, message) {
100
+ const error = new Error(message);
101
+ error.code = code;
102
+ throw error;
103
+ }
104
+
105
+ export function assertDiagnosticId(value, label) {
106
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
107
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `${label} must match ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`);
108
+ }
109
+ return value;
110
+ }
111
+
112
+ function assertBoundedText(value, label, maxLength, code = "E_DIAGNOSTIC_CASE_INVALID") {
113
+ if (typeof value !== "string" || !value.trim() || value.length > maxLength) {
114
+ invalid(code, `${label} must be a non-empty string of at most ${maxLength} characters`);
115
+ }
116
+ return value;
117
+ }
118
+
119
+ function assertStringRefArray(value, label, maxItems, missingCode) {
120
+ if (!Array.isArray(value)) invalid(missingCode ?? "E_DIAGNOSTIC_CASE_INVALID", `${label} must be an array`);
121
+ if (value.length > maxItems) invalid("E_DIAGNOSTIC_CASE_INVALID", `${label} exceeds limit of ${maxItems}`);
122
+ for (const ref of value) {
123
+ if (typeof ref !== "string" || !ref.trim()) {
124
+ invalid(missingCode ?? "E_DIAGNOSTIC_CASE_INVALID", `each ${label} entry must be a non-empty string`);
125
+ }
126
+ }
127
+ return [...new Set(value.map((ref) => ref.trim()))];
128
+ }
129
+
130
+ export function normalizeDiagnosticText(value) {
131
+ return String(value).trim().replace(/\s+/gu, " ").toLowerCase();
132
+ }
133
+
134
+ export function canonicalizeDiagnosticValue(value) {
135
+ if (Array.isArray(value)) return [...new Set(value.map(canonicalizeDiagnosticValue))].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
136
+ if (value && typeof value === "object") {
137
+ return Object.fromEntries(
138
+ Object.entries(value)
139
+ .filter(([key]) => key !== "id" && key !== "createdAt")
140
+ .sort(([a], [b]) => a.localeCompare(b))
141
+ .map(([key, child]) => [key, canonicalizeDiagnosticValue(child)]),
142
+ );
143
+ }
144
+ if (typeof value === "string") return normalizeDiagnosticText(value);
145
+ return value;
146
+ }
147
+
148
+ export function diagnosticSemanticFingerprint({ verificationCycle, failureClass, case_ }) {
149
+ return canonicalFingerprint({
150
+ verificationCycle,
151
+ failureClass,
152
+ observations: [...case_.observations]
153
+ .map(({ kind, evidenceRef, statement, provenance }) => canonicalizeDiagnosticValue({ kind, evidenceRef, statement, provenance }))
154
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
155
+ contributors: [...case_.contributors]
156
+ .map(({ type, statement, basis, status }) => canonicalizeDiagnosticValue({ type, statement, basis, status }))
157
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
158
+ hypotheses: [...case_.hypotheses]
159
+ .map(({ statement, contributorRefs, evidenceRefs, settledBy }) => canonicalizeDiagnosticValue({ statement, contributorRefs, evidenceRefs, settledBy }))
160
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
161
+ nextSafeAction: canonicalizeDiagnosticValue(case_.nextSafeAction?.statement ?? null),
162
+ });
163
+ }
164
+
165
+ export function assertObservation(observation, label = "observation") {
166
+ if (!observation || typeof observation !== "object" || Array.isArray(observation)) {
167
+ invalid("E_OBSERVATION_INVALID", `${label} must be an object`);
168
+ }
169
+ assertDiagnosticId(observation.id, `${label}.id`);
170
+ if (!OBSERVATION_KINDS.includes(observation.kind)) {
171
+ invalid("E_OBSERVATION_INVALID", `${label}.kind must be one of ${OBSERVATION_KINDS.join(", ")}`);
172
+ }
173
+ assertBoundedText(observation.statement, `${label}.statement`, LIMITS.statementLength);
174
+ if (observation.evidenceRef !== undefined && observation.evidenceRef !== null) {
175
+ assertDiagnosticId(observation.evidenceRef, `${label}.evidenceRef`);
176
+ }
177
+ if (observation.provenance !== undefined && observation.provenance !== null
178
+ && !OBSERVATION_PROVENANCE.includes(observation.provenance)) {
179
+ invalid("E_OBSERVATION_INVALID", `${label}.provenance must be one of ${OBSERVATION_PROVENANCE.join(", ")}`);
180
+ }
181
+ return observation;
182
+ }
183
+
184
+ export function assertContributor(contributor, label = "contributor") {
185
+ if (!contributor || typeof contributor !== "object" || Array.isArray(contributor)) {
186
+ invalid("E_CONTRIBUTOR_INVALID", `${label} must be an object`);
187
+ }
188
+ assertDiagnosticId(contributor.id, `${label}.id`);
189
+ if (!CONTRIBUTOR_TYPES.includes(contributor.type)) {
190
+ invalid("E_CONTRIBUTOR_INVALID", `${label}.type must be one of ${CONTRIBUTOR_TYPES.join(", ")}`);
191
+ }
192
+ assertBoundedText(contributor.statement, `${label}.statement`, LIMITS.statementLength, "E_CONTRIBUTOR_INVALID");
193
+ assertStringRefArray(contributor.basis ?? [], `${label}.basis`, LIMITS.evidenceRefsPerHypothesis);
194
+ if (contributor.status !== undefined && !CONTRIBUTOR_STATUSES.includes(contributor.status)) {
195
+ invalid("E_CONTRIBUTOR_INVALID", `${label}.status must be one of ${CONTRIBUTOR_STATUSES.join(", ")}`);
196
+ }
197
+ return contributor;
198
+ }
199
+
200
+ export function assertSettlement(settledBy, label = "settledBy") {
201
+ if (!settledBy || typeof settledBy !== "object" || Array.isArray(settledBy)) {
202
+ invalid("E_HYPOTHESIS_SETTLEMENT_MISSING", `${label} must be an object`);
203
+ }
204
+ if (!SETTLEMENT_TYPES.includes(settledBy.type)) {
205
+ invalid("E_HYPOTHESIS_SETTLEMENT_MISSING", `${label}.type must be one of ${SETTLEMENT_TYPES.join(", ")}`);
206
+ }
207
+ if (settledBy.type === "CHECK_STATUS") {
208
+ assertDiagnosticId(settledBy.checkId, `${label}.checkId`);
209
+ if (typeof settledBy.expectedStatus !== "string" || !settledBy.expectedStatus) {
210
+ invalid("E_HYPOTHESIS_SETTLEMENT_MISSING", `${label}.expectedStatus is required for CHECK_STATUS settlement`);
211
+ }
212
+ return settledBy;
213
+ }
214
+ assertBoundedText(settledBy.statement, `${label}.statement`, LIMITS.statementLength, "E_HYPOTHESIS_SETTLEMENT_MISSING");
215
+ return settledBy;
216
+ }
217
+
218
+ export function assertHypothesis(hypothesis, label = "hypothesis") {
219
+ if (!hypothesis || typeof hypothesis !== "object" || Array.isArray(hypothesis)) {
220
+ invalid("E_HYPOTHESIS_INVALID", `${label} must be an object`);
221
+ }
222
+ assertDiagnosticId(hypothesis.id, `${label}.id`);
223
+ assertBoundedText(hypothesis.statement, `${label}.statement`, LIMITS.statementLength, "E_HYPOTHESIS_INVALID");
224
+ assertStringRefArray(
225
+ hypothesis.contributorRefs ?? [],
226
+ `${label}.contributorRefs`,
227
+ LIMITS.contributorRefsPerHypothesis,
228
+ "E_CONTRIBUTOR_REFERENCE_INVALID",
229
+ );
230
+ assertStringRefArray(
231
+ hypothesis.evidenceRefs ?? [],
232
+ `${label}.evidenceRefs`,
233
+ LIMITS.evidenceRefsPerHypothesis,
234
+ "E_DIAGNOSTIC_CASE_EVIDENCE_INVALID",
235
+ );
236
+ assertSettlement(hypothesis.settledBy, `${label}.settledBy`);
237
+ if (hypothesis.status !== undefined && hypothesis.status !== "OPEN") {
238
+ invalid("E_HYPOTHESIS_INVALID", `${label}.status must be OPEN when recorded in a case`);
239
+ }
240
+ return hypothesis;
241
+ }
242
+
243
+ export function assertDiagnosticCaseDetails(details) {
244
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
245
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "Diagnostic case details must be an object");
246
+ }
247
+ if (details.schemaVersion !== DIAGNOSTIC_SCHEMA_VERSION) {
248
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `diagnostic case schemaVersion must be ${DIAGNOSTIC_SCHEMA_VERSION}`);
249
+ }
250
+ if (!Number.isInteger(details.verificationCycle) || details.verificationCycle < 1) {
251
+ invalid("E_DIAGNOSTIC_CASE_CYCLE_MISMATCH", "verificationCycle must be an integer >= 1");
252
+ }
253
+ if (!Number.isInteger(details.diagnosticRevision) || details.diagnosticRevision < 1) {
254
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "diagnosticRevision must be an integer >= 1");
255
+ }
256
+ try {
257
+ assertFailureClass(details.failureClass);
258
+ } catch {
259
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `invalid failureClass: ${details.failureClass}`);
260
+ }
261
+
262
+ if (!Array.isArray(details.observations) || details.observations.length > LIMITS.observationsPerCase) {
263
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `observations must be an array of at most ${LIMITS.observationsPerCase}`);
264
+ }
265
+ details.observations.forEach((observation, index) => assertObservation(observation, `observations[${index}]`));
266
+
267
+ if (!Array.isArray(details.contributors) || details.contributors.length > LIMITS.contributorsPerCase) {
268
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `contributors must be an array of at most ${LIMITS.contributorsPerCase}`);
269
+ }
270
+ details.contributors.forEach((contributor, index) => assertContributor(contributor, `contributors[${index}]`));
271
+
272
+ if (!Array.isArray(details.hypotheses)
273
+ || details.hypotheses.length < 1
274
+ || details.hypotheses.length > LIMITS.hypothesesPerCase) {
275
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `hypotheses must contain between 1 and ${LIMITS.hypothesesPerCase} hypotheses`);
276
+ }
277
+ details.hypotheses.forEach((hypothesis, index) => assertHypothesis(hypothesis, `hypotheses[${index}]`));
278
+
279
+ const ids = [
280
+ ...details.observations.map((o) => o.id),
281
+ ...details.contributors.map((c) => c.id),
282
+ ...details.hypotheses.map((h) => h.id),
283
+ ];
284
+ if (new Set(ids).size !== ids.length) {
285
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "duplicate IDs within the same diagnostic case are not allowed");
286
+ }
287
+ const observationIds = new Set(details.observations.map((o) => o.id));
288
+ const contributorIds = new Set(details.contributors.map((c) => c.id));
289
+ for (const contributor of details.contributors) {
290
+ for (const basis of contributor.basis ?? []) {
291
+ if (!observationIds.has(basis)) invalid("E_CONTRIBUTOR_REFERENCE_INVALID", `contributor ${contributor.id} references unknown observation ${basis}`);
292
+ }
293
+ }
294
+ for (const hypothesis of details.hypotheses) {
295
+ for (const ref of hypothesis.contributorRefs ?? []) {
296
+ if (!contributorIds.has(ref)) invalid("E_CONTRIBUTOR_REFERENCE_INVALID", `hypothesis ${hypothesis.id} references unknown contributor ${ref}`);
297
+ }
298
+ }
299
+
300
+ if (!details.nextSafeAction || typeof details.nextSafeAction !== "object"
301
+ || typeof details.nextSafeAction.statement !== "string" || !details.nextSafeAction.statement.trim()) {
302
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "nextSafeAction.statement must be a non-empty string");
303
+ }
304
+ if (details.nextSafeAction.statement.length > LIMITS.statementLength) {
305
+ invalid("E_DIAGNOSTIC_CASE_INVALID", `nextSafeAction.statement exceeds ${LIMITS.statementLength} characters`);
306
+ }
307
+
308
+ if (typeof details.diagnosticFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(details.diagnosticFingerprint)) {
309
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "diagnosticFingerprint must be a SHA-256 fingerprint");
310
+ }
311
+ const computed = diagnosticSemanticFingerprint({
312
+ verificationCycle: details.verificationCycle,
313
+ failureClass: details.failureClass,
314
+ case_: details,
315
+ });
316
+ if (computed !== details.diagnosticFingerprint) {
317
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "diagnosticFingerprint does not match computed semantic fingerprint");
318
+ }
319
+ if (details.previousDiagnosticFingerprint !== undefined && details.previousDiagnosticFingerprint !== null
320
+ && (typeof details.previousDiagnosticFingerprint !== "string" || !/^[a-f0-9]{64}$/.test(details.previousDiagnosticFingerprint))) {
321
+ invalid("E_DIAGNOSTIC_CASE_INVALID", "previousDiagnosticFingerprint must be a SHA-256 fingerprint or null");
322
+ }
323
+ return details;
324
+ }
325
+
326
+ export function assertInterventionDetails(details) {
327
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
328
+ invalid("E_INTERVENTION_INVALID", "Intervention event details must be an object");
329
+ }
330
+ if (details.schemaVersion !== DIAGNOSTIC_SCHEMA_VERSION) {
331
+ invalid("E_INTERVENTION_INVALID", `intervention schemaVersion must be ${DIAGNOSTIC_SCHEMA_VERSION}`);
332
+ }
333
+ if (!Number.isInteger(details.verificationCycle) || details.verificationCycle < 1) {
334
+ invalid("E_INTERVENTION_INVALID", "intervention verificationCycle must be an integer >= 1");
335
+ }
336
+ const intervention = details.intervention;
337
+ if (!intervention || typeof intervention !== "object" || Array.isArray(intervention)) {
338
+ invalid("E_INTERVENTION_INVALID", "intervention.intervention must be an object");
339
+ }
340
+ assertDiagnosticId(intervention.id, "intervention.id");
341
+ if (!INTERVENTION_KINDS.includes(intervention.kind)) {
342
+ invalid("E_INTERVENTION_INVALID", `intervention.kind must be one of ${INTERVENTION_KINDS.join(", ")}`);
343
+ }
344
+ assertBoundedText(intervention.statement, "intervention.statement", LIMITS.statementLength, "E_INTERVENTION_INVALID");
345
+ assertStringRefArray(intervention.targets ?? [], "intervention.targets", 32);
346
+ const hypothesisRefs = assertStringRefArray(
347
+ intervention.hypothesisRefs ?? [],
348
+ "intervention.hypothesisRefs",
349
+ LIMITS.hypothesesPerCase,
350
+ "E_INTERVENTION_REFERENCE_INVALID",
351
+ );
352
+ if (hypothesisRefs.length === 0) {
353
+ invalid("E_INTERVENTION_HYPOTHESIS_MISSING", "intervention must bind to at least one hypothesis");
354
+ }
355
+ if (intervention.reversible !== undefined && typeof intervention.reversible !== "boolean") {
356
+ invalid("E_INTERVENTION_INVALID", "intervention.reversible must be boolean");
357
+ }
358
+ return details;
359
+ }
360
+
361
+ export function interventionSemanticFingerprint(details) {
362
+ const intervention = details.intervention;
363
+ return canonicalFingerprint({
364
+ kind: intervention.kind,
365
+ statement: normalizeDiagnosticText(intervention.statement),
366
+ targets: [...new Set((intervention.targets ?? []).map((target) => target.trim()))].sort(),
367
+ hypothesisRefs: [...new Set((intervention.hypothesisRefs ?? []).map((ref) => ref.trim()))].sort(),
368
+ });
369
+ }
370
+
371
+ export function assertHypothesisDispositionDetails(details) {
372
+ if (!details || typeof details !== "object" || Array.isArray(details)) {
373
+ invalid("E_HYPOTHESIS_DISPOSITION_INVALID", "Disposition event details must be an object");
374
+ }
375
+ if (details.schemaVersion !== DIAGNOSTIC_SCHEMA_VERSION) {
376
+ invalid("E_HYPOTHESIS_DISPOSITION_INVALID", `disposition schemaVersion must be ${DIAGNOSTIC_SCHEMA_VERSION}`);
377
+ }
378
+ if (!Number.isInteger(details.verificationCycle) || details.verificationCycle < 1) {
379
+ invalid("E_HYPOTHESIS_DISPOSITION_INVALID", "disposition verificationCycle must be an integer >= 1");
380
+ }
381
+ assertDiagnosticId(details.hypothesisRef, "hypothesisRef");
382
+ if (!HYPOTHESIS_STATUSES.includes(details.status)) {
383
+ invalid("E_HYPOTHESIS_DISPOSITION_INVALID", `disposition status must be one of ${HYPOTHESIS_STATUSES.join(", ")}`);
384
+ }
385
+ assertStringRefArray(
386
+ details.evidenceRefs ?? [],
387
+ "evidenceRefs",
388
+ LIMITS.evidenceRefsPerHypothesis,
389
+ "E_HYPOTHESIS_DISPOSITION_EVIDENCE_INVALID",
390
+ );
391
+ if ((details.evidenceRefs ?? []).length === 0) {
392
+ invalid("E_HYPOTHESIS_DISPOSITION_EVIDENCE_INVALID", "disposition requires at least one evidenceRef");
393
+ }
394
+ assertBoundedText(details.reason, "reason", LIMITS.reasonLength, "E_HYPOTHESIS_DISPOSITION_INVALID");
395
+ return details;
396
+ }
@@ -0,0 +1,51 @@
1
+ export function diagnosticEventsForTask(events, taskId) {
2
+ return (events ?? []).filter((event) =>
3
+ (event.event === "DIAGNOSTIC_CASE_RECORDED" || event.event === "DIAGNOSIS_RECORDED")
4
+ && (!taskId || event.taskId === taskId));
5
+ }
6
+
7
+ function structuredProjection(event) {
8
+ const details = event.details ?? {};
9
+ return {
10
+ sourceModel: "STRUCTURED_DIAGNOSTIC_CASE_V1",
11
+ event,
12
+ details,
13
+ diagnosticCase: details,
14
+ informationGain: details.informationGain ?? null,
15
+ };
16
+ }
17
+
18
+ function legacyProjection(event) {
19
+ const details = event.details ?? {};
20
+ return {
21
+ sourceModel: "LEGACY_DIAGNOSIS_V1",
22
+ event,
23
+ details,
24
+ diagnosticCase: null,
25
+ informationGain: details.informationGain ?? null,
26
+ };
27
+ }
28
+
29
+ export function resolveCurrentCycleDiagnostic(events, taskId, verificationCycle) {
30
+ const candidates = diagnosticEventsForTask(events, taskId)
31
+ .filter((event) => {
32
+ const cycle = event.details?.verificationCycle;
33
+ return verificationCycle == null || cycle == null || cycle === verificationCycle;
34
+ })
35
+ .sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
36
+
37
+ const structured = [...candidates].reverse().find((event) => event.event === "DIAGNOSTIC_CASE_RECORDED");
38
+ if (structured) return structuredProjection(structured);
39
+
40
+ const legacy = [...candidates].reverse().find((event) => event.event === "DIAGNOSIS_RECORDED");
41
+ if (legacy) return legacyProjection(legacy);
42
+
43
+ return null;
44
+ }
45
+
46
+ export function projectDiagnosticCases(events, taskId) {
47
+ return diagnosticEventsForTask(events, taskId)
48
+ .filter((event) => event.event === "DIAGNOSTIC_CASE_RECORDED")
49
+ .sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0))
50
+ .map((event) => ({ sequence: event.seq, at: event.at, ...event.details }));
51
+ }