@cassiomc1/forgeloop 1.5.0 → 1.6.1
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/CONTRACT_COVERAGE.md +1 -0
- package/DOCS_INDEX.md +20 -10
- package/EXECUTION_STATE.md +20 -0
- package/LOOP_ENGINEERING.md +103 -1
- package/LOOP_SYSTEM_DESIGN.md +32 -0
- package/PROTOCOL_INTEGRATION.md +90 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +47 -9
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/THREAT_MODEL.md +41 -1
- package/docs/ARTIFACT_REFERENCE.md +159 -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 +34 -1
- package/docs/RECIPES.md +67 -0
- package/docs/RELEASE_CHECKLIST_1_6_1.md +121 -0
- package/docs/TROUBLESHOOTING.md +133 -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 +13 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/execution.schema.json +15 -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/run-check.js +1 -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 +106 -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 +71 -14
- 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 +363 -0
- package/src/core/events.js +41 -1
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +29 -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 +55 -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 +256 -0
- package/src/core/progress.js +41 -4
- package/src/core/protocol-info.js +56 -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/runtime-context.js +19 -1
- 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/verification-execution.js +257 -0
- package/src/core/work-state.js +10 -5
- package/src/integration.js +8 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
package/src/core/completion.js
CHANGED
|
@@ -13,6 +13,25 @@ import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
|
|
|
13
13
|
import { evaluateTerminalRequirements } from "./evidence-readiness.js";
|
|
14
14
|
import { PROJECT_ARTIFACT_PATHS, taskArtifactPath } from "./task-paths.js";
|
|
15
15
|
import { detectPolicyCapability, evaluateTargetPolicy } from "./policy-engine.js";
|
|
16
|
+
import { listActions } from "./actions.js";
|
|
17
|
+
|
|
18
|
+
async function actionReceiptSummary(target, packageRoot, taskId) {
|
|
19
|
+
const actions = await listActions(target, { packageRoot, taskId });
|
|
20
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
21
|
+
const readiness = await evaluateRequiredActionReadiness({ target, packageRoot, taskId });
|
|
22
|
+
return {
|
|
23
|
+
count: actions.length,
|
|
24
|
+
required: readiness.total,
|
|
25
|
+
verified: actions.filter((action) => action.state === "VERIFIED").length,
|
|
26
|
+
// Raw state counts are observability only; trusted completion is below.
|
|
27
|
+
trustedSatisfied: readiness.satisfied,
|
|
28
|
+
unresolvedRequired: readiness.unresolved,
|
|
29
|
+
failed: actions.filter((action) => action.state === "FAILED").length,
|
|
30
|
+
ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
31
|
+
pending: actions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
32
|
+
actionRefs: actions.map((action) => action.actionId),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
/**
|
|
18
37
|
* Canonical completion return statuses shared by the runtime, tests, and
|
|
@@ -424,6 +443,44 @@ export async function evaluateCompletion({
|
|
|
424
443
|
}
|
|
425
444
|
}
|
|
426
445
|
|
|
446
|
+
const actionTaskId = contract?.value?.taskId ?? taskId;
|
|
447
|
+
const durableActions = actionTaskId
|
|
448
|
+
? await listActions(target, { packageRoot, taskId: actionTaskId })
|
|
449
|
+
: [];
|
|
450
|
+
const contractRequirements = new Set([
|
|
451
|
+
...(contract?.value?.verification ?? []), ...(contract?.value?.successCriteria ?? []),
|
|
452
|
+
]);
|
|
453
|
+
// Completion truth consumes the canonical action-readiness projection, not
|
|
454
|
+
// raw state labels: a forged VERIFIED label can never satisfy a required
|
|
455
|
+
// action (INV-VERIFY-02).
|
|
456
|
+
const { evaluateRequiredActionReadiness } = await import("./action-readiness.js");
|
|
457
|
+
const requiredActionReadiness = actionTaskId
|
|
458
|
+
? await evaluateRequiredActionReadiness({ target, packageRoot, taskId: actionTaskId })
|
|
459
|
+
: { total: 0, satisfied: 0, unresolved: 0, ambiguous: 0, failed: 0, untrusted: 0, actions: [] };
|
|
460
|
+
for (const readiness of requiredActionReadiness.actions) {
|
|
461
|
+
if (readiness.status === "SATISFIED") continue;
|
|
462
|
+
const action = durableActions.find((candidate) => candidate.actionId === readiness.actionId);
|
|
463
|
+
if (
|
|
464
|
+
readiness.status === "FAILED"
|
|
465
|
+
&& action?.state === "CANCELLED"
|
|
466
|
+
&& action.requirement
|
|
467
|
+
&& !contractRequirements.has(action.requirement)
|
|
468
|
+
) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
const code = readiness.status === "AMBIGUOUS"
|
|
472
|
+
? "E_ACTION_RECONCILIATION_REQUIRED"
|
|
473
|
+
: readiness.status === "UNTRUSTED"
|
|
474
|
+
? "E_ACTION_VERIFICATION_REQUIRED"
|
|
475
|
+
: "E_ACTION_STATE_MISMATCH";
|
|
476
|
+
errors.push(issue(
|
|
477
|
+
code,
|
|
478
|
+
`Required action ${readiness.actionId} is not trusted-satisfied (${readiness.status}): ${readiness.reasons[0] ?? ""}`,
|
|
479
|
+
[taskArtifactPath(action?.taskId ?? actionTaskId, "actions")],
|
|
480
|
+
{ actionId: readiness.actionId, actionState: action?.state, readiness: readiness.status },
|
|
481
|
+
));
|
|
482
|
+
}
|
|
483
|
+
|
|
427
484
|
const sortedErrors = sortIssues(errors);
|
|
428
485
|
const valid = sortedErrors.length === 0;
|
|
429
486
|
return {
|
|
@@ -440,6 +497,17 @@ export async function evaluateCompletion({
|
|
|
440
497
|
status: ledger.valid ? "valid" : "invalid",
|
|
441
498
|
events: ledger.events.length,
|
|
442
499
|
},
|
|
500
|
+
actions: {
|
|
501
|
+
count: durableActions.length,
|
|
502
|
+
required: requiredActionReadiness.total,
|
|
503
|
+
verified: durableActions.filter((action) => action.state === "VERIFIED").length,
|
|
504
|
+
trustedSatisfied: requiredActionReadiness.satisfied,
|
|
505
|
+
unresolvedRequired: requiredActionReadiness.unresolved,
|
|
506
|
+
failed: durableActions.filter((action) => action.state === "FAILED").length,
|
|
507
|
+
ambiguous: durableActions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
508
|
+
pending: durableActions.filter((action) => !["VERIFIED", "FAILED", "CANCELLED"].includes(action.state)).length,
|
|
509
|
+
actionRefs: durableActions.map((action) => action.actionId),
|
|
510
|
+
},
|
|
443
511
|
};
|
|
444
512
|
}
|
|
445
513
|
|
|
@@ -528,6 +596,7 @@ export async function runComplete({
|
|
|
528
596
|
if (receipt) {
|
|
529
597
|
nextReceipt = await createReceipt({
|
|
530
598
|
...receipt.value,
|
|
599
|
+
actions: await actionReceiptSummary(target, packageRoot, state.taskId),
|
|
531
600
|
stateFingerprint: canonicalFingerprint(next),
|
|
532
601
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
533
602
|
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
@@ -574,6 +643,7 @@ export async function runComplete({
|
|
|
574
643
|
next.revision = (state.revision ?? 0) + 1;
|
|
575
644
|
const nextReceipt = await createReceipt({
|
|
576
645
|
...receipt.value,
|
|
646
|
+
actions: await actionReceiptSummary(target, packageRoot, state.taskId),
|
|
577
647
|
stateFingerprint: canonicalFingerprint(next),
|
|
578
648
|
verificationCycle: next.verificationCycle ?? receipt.value.verificationCycle ?? 1,
|
|
579
649
|
}, packageRoot, { target, taskId: state.taskId, authorityContext, runtimeContext });
|
|
@@ -169,20 +169,25 @@ export function classifyContinuity({
|
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
172
|
+
export async function reconcileContinuity({ target, packageRoot, taskId = null } = {}) {
|
|
173
173
|
const [{ readWorkState }, { readContract }, repository] = await Promise.all([
|
|
174
174
|
import("./work-state.js"),
|
|
175
175
|
import("./contract.js"),
|
|
176
176
|
import("./repository.js"),
|
|
177
177
|
]);
|
|
178
178
|
|
|
179
|
-
const state = await readWorkState(target, packageRoot);
|
|
179
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
180
180
|
let continuityArtifact;
|
|
181
181
|
try {
|
|
182
|
-
continuityArtifact = await readContinuity(target, packageRoot);
|
|
182
|
+
continuityArtifact = await readContinuity(target, { packageRoot, taskId });
|
|
183
183
|
} catch (error) {
|
|
184
184
|
if (error.code === "ARTIFACT_MISSING") {
|
|
185
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
...classifyContinuity({ continuity: null, state }),
|
|
187
|
+
path: ".forgeloop/continuity.json",
|
|
188
|
+
present: false,
|
|
189
|
+
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
190
|
+
};
|
|
186
191
|
}
|
|
187
192
|
return {
|
|
188
193
|
...baseResult("INVALID", {
|
|
@@ -197,7 +202,7 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
|
197
202
|
|
|
198
203
|
let contractFingerprint = null;
|
|
199
204
|
try {
|
|
200
|
-
const contract = await readContract(target, packageRoot);
|
|
205
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
201
206
|
contractFingerprint = contract.fingerprint;
|
|
202
207
|
} catch {
|
|
203
208
|
contractFingerprint = null;
|
|
@@ -220,5 +225,19 @@ export async function reconcileContinuity({ target, packageRoot } = {}) {
|
|
|
220
225
|
present: true,
|
|
221
226
|
fingerprint: continuityArtifact.fingerprint,
|
|
222
227
|
continuity: continuityArtifact.value,
|
|
228
|
+
diagnosticContext: await deriveDiagnosticContextSafe({ target, packageRoot, state }),
|
|
223
229
|
};
|
|
224
230
|
}
|
|
231
|
+
|
|
232
|
+
async function deriveDiagnosticContextSafe({ target, packageRoot, state }) {
|
|
233
|
+
try {
|
|
234
|
+
const [{ readEvents }, { deriveDiagnosticContext }] = await Promise.all([
|
|
235
|
+
import("./events.js"),
|
|
236
|
+
import("./reflection.js"),
|
|
237
|
+
]);
|
|
238
|
+
const events = await readEvents(target, packageRoot, { taskId: state?.taskId ?? null });
|
|
239
|
+
return { present: true, ...deriveDiagnosticContext(events, state) };
|
|
240
|
+
} catch {
|
|
241
|
+
return { present: false };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -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
|
+
}
|