@cassiomc1/forgeloop 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +1 -0
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/DOCS_INDEX.md +20 -8
- package/EXECUTION_STATE.md +60 -0
- package/LOOP_ENGINEERING.md +135 -5
- package/LOOP_SYSTEM_DESIGN.md +54 -1
- package/PROTOCOL_INTEGRATION.md +87 -0
- package/QUALITY_SCORECARD.md +2 -0
- package/README.md +69 -9
- package/TERMINOLOGY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/THREAT_MODEL.md +59 -1
- package/docs/ARTIFACT_REFERENCE.md +183 -0
- package/docs/CLI_REFERENCE.md +391 -6
- package/docs/CROSS_HARNESS_CONTINUITY.md +23 -0
- package/docs/DIAGNOSTIC_MODEL.md +181 -0
- package/docs/DOCUMENTATION_GUIDE.md +36 -13
- package/docs/EXECUTION_TRACE.md +76 -0
- package/docs/GETTING_STARTED.md +1 -0
- package/docs/MCP.md +159 -0
- package/docs/RECIPES.md +149 -0
- package/docs/RELEASE_CHECKLIST_1_4.md +38 -0
- package/docs/RELEASE_CHECKLIST_1_5_MCP.md +78 -0
- package/docs/TROUBLESHOOTING.md +217 -3
- package/docs/UNIVERSAL_INTEGRATION.md +48 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +13797 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +37 -0
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +5002 -0
- package/docs/diagrams/README.md +55 -0
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +122 -0
- package/docs/diagrams/manifest.json +42 -0
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +20 -0
- package/package.json +21 -8
- package/schemas/action.schema.json +100 -0
- package/schemas/approval.schema.json +51 -0
- package/schemas/capability-policy.schema.json +41 -0
- package/schemas/diagnostic-case.schema.json +85 -0
- package/schemas/execution-receipt.schema.json +16 -0
- package/schemas/hypothesis-disposition.schema.json +16 -0
- package/schemas/intervention.schema.json +27 -0
- package/schemas/policy-lock.schema.json +1 -0
- package/schemas/policy-snapshot.schema.json +2 -0
- package/schemas/task-recovery.schema.json +61 -0
- package/schemas/trajectory-evaluation.schema.json +64 -0
- package/schemas/trajectory-scenario.schema.json +42 -0
- package/src/cli.js +267 -347
- package/src/commands/action-authorize.js +41 -0
- package/src/commands/action-propose.js +10 -0
- package/src/commands/action-reconcile.js +10 -0
- package/src/commands/action-record.js +47 -0
- package/src/commands/action-show.js +10 -0
- package/src/commands/action-verify.js +10 -0
- package/src/commands/advance.js +7 -2
- package/src/commands/approval-request.js +64 -0
- package/src/commands/approval-resolve.js +10 -0
- package/src/commands/audit.js +5 -0
- package/src/commands/baseline.js +3 -3
- package/src/commands/eval.js +6 -0
- package/src/commands/history.js +18 -0
- package/src/commands/init.js +2 -2
- package/src/commands/inspect.js +55 -0
- package/src/commands/metrics.js +7 -0
- package/src/commands/next.js +8 -2
- package/src/commands/policy-discover.js +2 -2
- package/src/commands/progress.js +6 -2
- package/src/commands/record-diagnosis.js +37 -1
- package/src/commands/record-hypothesis-disposition.js +45 -0
- package/src/commands/record-intervention.js +35 -0
- package/src/commands/reflect.js +38 -0
- package/src/commands/report.js +9 -1
- package/src/commands/run-action.js +18 -0
- package/src/commands/status.js +17 -0
- package/src/commands/task-create.js +39 -1
- package/src/commands/task-list.js +14 -1
- package/src/commands/task-lock-status.js +2 -2
- package/src/commands/task-recover.js +202 -0
- package/src/commands/task-repair-legacy-recovery.js +417 -0
- package/src/commands/task-resume.js +172 -0
- package/src/commands/task-scope.js +23 -4
- package/src/commands/task-show.js +18 -4
- package/src/commands/trace.js +34 -0
- package/src/commands/validate-protocol.js +40 -15
- package/src/core/action-authorization.js +106 -0
- package/src/core/action-constants.js +86 -0
- package/src/core/action-execution.js +105 -0
- package/src/core/action-ledger-projection.js +302 -0
- package/src/core/action-model.js +581 -0
- package/src/core/action-readiness.js +141 -0
- package/src/core/action-reconciliation-policy.js +49 -0
- package/src/core/action-reconciliation.js +66 -0
- package/src/core/action-verification.js +111 -0
- package/src/core/actions.js +462 -0
- package/src/core/approvals.js +405 -0
- package/src/core/artifact-registry.js +60 -0
- package/src/core/audit.js +45 -4
- package/src/core/bundles.js +30 -0
- package/src/core/capability-policy.js +226 -0
- package/src/core/cli-command-definitions.js +260 -5
- package/src/core/command-executors.js +543 -0
- package/src/core/command-input.js +107 -0
- package/src/core/command-runtime.js +117 -0
- package/src/core/completion-artifacts.js +39 -15
- package/src/core/completion-ownership.js +88 -0
- package/src/core/completion-recovery-rebind.js +194 -0
- package/src/core/completion.js +70 -0
- package/src/core/continuity-reconciliation.js +24 -5
- package/src/core/diagnostic-model.js +396 -0
- package/src/core/diagnostic-projection.js +51 -0
- package/src/core/diagnostic-record.js +360 -0
- package/src/core/error-codes.js +461 -1
- package/src/core/events.js +171 -2
- package/src/core/execution-prerequisites.js +4 -1
- package/src/core/execution.js +26 -188
- package/src/core/failure-signature.js +70 -0
- package/src/core/failure-surface.js +57 -0
- package/src/core/filesystem.js +55 -6
- package/src/core/history.js +110 -0
- package/src/core/hypothesis-projection.js +85 -0
- package/src/core/information-gain-projection.js +283 -0
- package/src/core/information-gain.js +138 -0
- package/src/core/inspect.js +132 -7
- package/src/core/integration-invocation-policy.js +217 -0
- package/src/core/integration-limits.js +20 -0
- package/src/core/integration-resources.js +178 -0
- package/src/core/next-action-model.js +94 -0
- package/src/core/next-action.js +490 -3
- package/src/core/phase.js +42 -22
- package/src/core/policy-engine.js +113 -6
- package/src/core/preflight-consistency.js +31 -5
- package/src/core/preflight.js +19 -2
- package/src/core/prepared-execution.js +227 -0
- package/src/core/progress.js +41 -4
- package/src/core/project-root.js +21 -0
- package/src/core/protocol-info.js +61 -0
- package/src/core/protocol.js +14 -0
- package/src/core/receipt.js +1 -0
- package/src/core/reconcile-closure.js +35 -10
- package/src/core/recovery-history.js +116 -0
- package/src/core/reflection.js +305 -0
- package/src/core/resumability.js +57 -3
- package/src/core/schema-validation.js +9 -0
- package/src/core/strategy-analysis.js +97 -0
- package/src/core/task-claim-state.js +272 -0
- package/src/core/task-command.js +5 -1
- package/src/core/task-conflict-inspection.js +321 -0
- package/src/core/task-context.js +32 -29
- package/src/core/task-discovery.js +14 -1
- package/src/core/task-lock.js +216 -22
- package/src/core/task-paths.js +31 -2
- package/src/core/task-recovery-migration.js +192 -0
- package/src/core/task-recovery.js +205 -0
- package/src/core/task-scope.js +33 -1
- package/src/core/task-snapshot.js +53 -0
- package/src/core/templates.js +9 -0
- package/src/core/trace.js +548 -0
- package/src/core/trajectory-evaluation.js +71 -0
- package/src/core/trajectory-metrics.js +80 -0
- package/src/core/transaction.js +36 -2
- package/src/core/work-state.js +10 -5
- package/src/integration.js +47 -0
- package/docs/assets/forgeloop-flow.svg +0 -1
- package/docs/forgeloop-flow.mmd +0 -51
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { buildTaskSnapshot } from "./task-snapshot.js";
|
|
2
|
+
import { diagnosisEventsForTask } from "./diagnosis-model.js";
|
|
3
|
+
import {
|
|
4
|
+
assertDiagnosticCaseDetails,
|
|
5
|
+
assertInterventionDetails,
|
|
6
|
+
assertHypothesisDispositionDetails,
|
|
7
|
+
} from "./diagnostic-model.js";
|
|
8
|
+
import { projectFailureSignatures } from "./failure-signature.js";
|
|
9
|
+
import { projectFailureSurfaces } from "./failure-surface.js";
|
|
10
|
+
import { listActions } from "./actions.js";
|
|
11
|
+
|
|
12
|
+
export const EVENT_CATEGORIES = Object.freeze([
|
|
13
|
+
"task",
|
|
14
|
+
"contract",
|
|
15
|
+
"routing",
|
|
16
|
+
"lifecycle",
|
|
17
|
+
"execution",
|
|
18
|
+
"verification",
|
|
19
|
+
"diagnosis",
|
|
20
|
+
"intervention",
|
|
21
|
+
"evidence",
|
|
22
|
+
"review",
|
|
23
|
+
"continuity",
|
|
24
|
+
"recovery",
|
|
25
|
+
"policy",
|
|
26
|
+
"audit",
|
|
27
|
+
"completion",
|
|
28
|
+
"integrity",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const EVENT_CATEGORY_MAP = Object.freeze({
|
|
32
|
+
TASK_RECEIVED: "task",
|
|
33
|
+
CONTRACT_VALIDATED: "contract",
|
|
34
|
+
DECISION_CRITERION_RECORDED: "contract",
|
|
35
|
+
PLAN_RECORDED: "contract",
|
|
36
|
+
ROUTE_VALIDATED: "routing",
|
|
37
|
+
PREFLIGHT_READY: "lifecycle",
|
|
38
|
+
PREFLIGHT_BLOCKED: "lifecycle",
|
|
39
|
+
EXECUTION_STARTED: "execution",
|
|
40
|
+
VERIFICATION_STARTED: "lifecycle",
|
|
41
|
+
VERIFICATION_RECORDED: "verification",
|
|
42
|
+
CHECK_RECORDED: "verification",
|
|
43
|
+
TERMINAL_RESULT_RECORDED: "verification",
|
|
44
|
+
REVIEW_STARTED: "review",
|
|
45
|
+
DIAGNOSIS_RECORDED: "diagnosis",
|
|
46
|
+
DIAGNOSTIC_CASE_RECORDED: "diagnosis",
|
|
47
|
+
HYPOTHESIS_DISPOSITION_RECORDED: "diagnosis",
|
|
48
|
+
INTERVENTION_RECORDED: "intervention",
|
|
49
|
+
CONTINUITY_RECORDED: "continuity",
|
|
50
|
+
CHECKPOINT_RECONCILED: "continuity",
|
|
51
|
+
TASK_RECOVERY_RECORDED: "recovery",
|
|
52
|
+
TASK_RECOVERY_RESUMED: "recovery",
|
|
53
|
+
OPERATOR_RECOVERY_RECORDED: "recovery",
|
|
54
|
+
LEGACY_RECOVERY_MIGRATION_RECORDED: "recovery",
|
|
55
|
+
GATE_SATISFIED: "policy",
|
|
56
|
+
COMPLETION_VALIDATED: "completion",
|
|
57
|
+
COMPLETION_REJECTED: "completion",
|
|
58
|
+
TRANSACTION_COMMITTED: "integrity",
|
|
59
|
+
ACTION_PROPOSED: "execution",
|
|
60
|
+
ACTION_AUTHORIZED: "execution",
|
|
61
|
+
ACTION_STARTED: "execution",
|
|
62
|
+
ACTION_COMMIT_RECORDED: "execution",
|
|
63
|
+
ACTION_VERIFIED: "execution",
|
|
64
|
+
ACTION_FAILED: "execution",
|
|
65
|
+
ACTION_COMMIT_UNKNOWN: "execution",
|
|
66
|
+
ACTION_RECONCILED: "execution",
|
|
67
|
+
ACTION_CANCELLED: "execution",
|
|
68
|
+
APPROVAL_REQUESTED: "execution",
|
|
69
|
+
APPROVAL_RESOLVED: "execution",
|
|
70
|
+
TRAJECTORY_EVALUATED: "audit",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const LIFECYCLE_TRANSITIONS = Object.freeze([
|
|
74
|
+
"CONTRACT_VALIDATED",
|
|
75
|
+
"ROUTE_VALIDATED",
|
|
76
|
+
"PREFLIGHT_READY",
|
|
77
|
+
"EXECUTION_STARTED",
|
|
78
|
+
"VERIFICATION_STARTED",
|
|
79
|
+
"REVIEW_STARTED",
|
|
80
|
+
"COMPLETION_VALIDATED",
|
|
81
|
+
"COMPLETION_REJECTED",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
export function eventCategory(eventType) {
|
|
85
|
+
return EVENT_CATEGORY_MAP[eventType] ?? "integrity";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function timestampQuality(event) {
|
|
89
|
+
if (typeof event?.at === "string" && !Number.isNaN(Date.parse(event.at))) return "authoritative";
|
|
90
|
+
return "unknown";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function eventSummary(event) {
|
|
94
|
+
const d = event.details ?? {};
|
|
95
|
+
switch (event.event) {
|
|
96
|
+
case "TASK_RECEIVED":
|
|
97
|
+
return `Task received${d.taskId ? ` (${d.taskId})` : ""}`;
|
|
98
|
+
case "CONTRACT_VALIDATED":
|
|
99
|
+
return "Contract validated";
|
|
100
|
+
case "ROUTE_VALIDATED":
|
|
101
|
+
return `Route selected${Array.isArray(d.selectedGuides) ? `: ${d.selectedGuides.join(", ")}` : ""}`;
|
|
102
|
+
case "PREFLIGHT_READY":
|
|
103
|
+
return "Preflight READY";
|
|
104
|
+
case "PREFLIGHT_BLOCKED":
|
|
105
|
+
return "Preflight BLOCKED";
|
|
106
|
+
case "EXECUTION_STARTED":
|
|
107
|
+
return "Execution started";
|
|
108
|
+
case "VERIFICATION_RECORDED": {
|
|
109
|
+
const id = d.id ?? d.checkId ?? d.requirement ?? "check";
|
|
110
|
+
return `${id} ${String(d.status ?? "recorded").toUpperCase()}${d.exitCode !== undefined ? ` (exit ${d.exitCode})` : ""}`;
|
|
111
|
+
}
|
|
112
|
+
case "TERMINAL_RESULT_RECORDED":
|
|
113
|
+
return `Terminal result ${d.type ?? ""} ${d.status ?? ""}`.trim();
|
|
114
|
+
case "REVIEW_STARTED":
|
|
115
|
+
return `Review started${Number.isInteger(d.verificationCycle) ? ` for cycle ${d.verificationCycle}` : ""}`;
|
|
116
|
+
case "DIAGNOSIS_RECORDED":
|
|
117
|
+
return `Diagnosis recorded (${d.informationGain ?? "gain unknown"})`;
|
|
118
|
+
case "DIAGNOSTIC_CASE_RECORDED":
|
|
119
|
+
return `Diagnostic case recorded (cycle ${d.verificationCycle}, revision ${d.diagnosticRevision})`;
|
|
120
|
+
case "INTERVENTION_RECORDED":
|
|
121
|
+
return `Intervention recorded (${d.intervention?.id ?? "unknown"})`;
|
|
122
|
+
case "HYPOTHESIS_DISPOSITION_RECORDED":
|
|
123
|
+
return `Hypothesis ${d.hypothesisRef ?? "unknown"} ${d.status ?? "dispositioned"}`;
|
|
124
|
+
case "DECISION_CRITERION_RECORDED":
|
|
125
|
+
return "Decision criterion recorded";
|
|
126
|
+
case "GATE_SATISFIED":
|
|
127
|
+
return `Gate satisfied: ${d.gate ?? "unknown"}`;
|
|
128
|
+
case "CHECKPOINT_RECONCILED":
|
|
129
|
+
return "Checkpoint reconciled";
|
|
130
|
+
case "CONTINUITY_RECORDED":
|
|
131
|
+
return "Continuity checkpoint recorded";
|
|
132
|
+
case "TASK_RECOVERY_RECORDED":
|
|
133
|
+
return `Task recovery recorded (${d.classification ?? "unknown"})`;
|
|
134
|
+
case "TASK_RECOVERY_RESUMED":
|
|
135
|
+
return "Task resumed after recovery";
|
|
136
|
+
case "OPERATOR_RECOVERY_RECORDED":
|
|
137
|
+
return "Operator recovery recorded";
|
|
138
|
+
case "LEGACY_RECOVERY_MIGRATION_RECORDED":
|
|
139
|
+
return "Legacy recovery migration recorded";
|
|
140
|
+
case "COMPLETION_VALIDATED":
|
|
141
|
+
return "Completion validated";
|
|
142
|
+
case "COMPLETION_REJECTED":
|
|
143
|
+
return "Completion rejected";
|
|
144
|
+
case "TRANSACTION_COMMITTED":
|
|
145
|
+
return "Transaction committed";
|
|
146
|
+
case "ACTION_PROPOSED":
|
|
147
|
+
return `Action proposed (${d.actionId ?? "unknown"})`;
|
|
148
|
+
case "ACTION_AUTHORIZED":
|
|
149
|
+
return `Action authorized (${d.actionId ?? "unknown"})`;
|
|
150
|
+
case "ACTION_STARTED":
|
|
151
|
+
return `Action started (${d.actionId ?? "unknown"})`;
|
|
152
|
+
case "ACTION_COMMIT_RECORDED":
|
|
153
|
+
return `Action commit recorded (${d.actionId ?? "unknown"})`;
|
|
154
|
+
case "ACTION_VERIFIED":
|
|
155
|
+
return `Action verified (${d.actionId ?? "unknown"})`;
|
|
156
|
+
case "ACTION_FAILED":
|
|
157
|
+
return `Action failed (${d.actionId ?? "unknown"})`;
|
|
158
|
+
case "ACTION_COMMIT_UNKNOWN":
|
|
159
|
+
return `Action commit unknown (${d.actionId ?? "unknown"})`;
|
|
160
|
+
case "ACTION_RECONCILED":
|
|
161
|
+
return `Action reconciled (${d.actionId ?? "unknown"})`;
|
|
162
|
+
case "ACTION_CANCELLED":
|
|
163
|
+
return `Action cancelled (${d.actionId ?? "unknown"})`;
|
|
164
|
+
default:
|
|
165
|
+
return event.event;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function normalizeProtocolEvent(event, context = {}) {
|
|
170
|
+
const phase = typeof context.phase === "string" ? context.phase : null;
|
|
171
|
+
return {
|
|
172
|
+
sequence: event.seq,
|
|
173
|
+
timestamp: event.at ?? null,
|
|
174
|
+
timestampSource: "ledger",
|
|
175
|
+
timestampQuality: timestampQuality(event),
|
|
176
|
+
type: event.event,
|
|
177
|
+
category: eventCategory(event.event),
|
|
178
|
+
phase,
|
|
179
|
+
phaseQuality: context.phaseQuality ?? "unknown",
|
|
180
|
+
source: {
|
|
181
|
+
kind: "ledger",
|
|
182
|
+
artifact: context.artifactPath ?? ".forgeloop/task-state/<task-key>/events.ndjson",
|
|
183
|
+
},
|
|
184
|
+
summary: eventSummary(event),
|
|
185
|
+
data: event.details ?? {},
|
|
186
|
+
references: {
|
|
187
|
+
checkIds: [event.details?.id, event.details?.checkId].filter((v) => typeof v === "string"),
|
|
188
|
+
evidenceRefs: Array.isArray(event.details?.evidenceRefs) ? event.details.evidenceRefs : [],
|
|
189
|
+
hypothesisRefs: Array.isArray(event.details?.hypothesisRefs)
|
|
190
|
+
? event.details.hypothesisRefs
|
|
191
|
+
: [event.details?.hypothesisRef].filter((v) => typeof v === "string"),
|
|
192
|
+
interventionRefs: [event.details?.intervention?.id].filter((v) => typeof v === "string"),
|
|
193
|
+
relatedSequences: [],
|
|
194
|
+
},
|
|
195
|
+
hash: event.hash,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function lifecycleTransitions(events) {
|
|
200
|
+
const transitions = [];
|
|
201
|
+
let lastPhaseEvent = null;
|
|
202
|
+
for (const event of events) {
|
|
203
|
+
if (!LIFECYCLE_TRANSITIONS.includes(event.event)) continue;
|
|
204
|
+
transitions.push({
|
|
205
|
+
sequence: event.seq,
|
|
206
|
+
at: event.at,
|
|
207
|
+
type: event.event,
|
|
208
|
+
details: event.details ?? {},
|
|
209
|
+
previous: lastPhaseEvent?.type ?? null,
|
|
210
|
+
});
|
|
211
|
+
lastPhaseEvent = event;
|
|
212
|
+
}
|
|
213
|
+
return transitions;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function projectChecks(snapshot) {
|
|
217
|
+
const state = snapshot.state;
|
|
218
|
+
const attemptsByCheck = new Map();
|
|
219
|
+
const pushAttempt = (key, attempt) => {
|
|
220
|
+
if (!attemptsByCheck.has(key)) attemptsByCheck.set(key, []);
|
|
221
|
+
attemptsByCheck.get(key).push(attempt);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Ledger chronology is the primary source of historical attempts.
|
|
225
|
+
const ledgerIdentityKeys = new Set();
|
|
226
|
+
for (const event of snapshot.events) {
|
|
227
|
+
if (event.event !== "VERIFICATION_RECORDED") continue;
|
|
228
|
+
const d = event.details ?? {};
|
|
229
|
+
const key = d.id ?? d.checkId ?? d.requirement ?? null;
|
|
230
|
+
if (!key) continue;
|
|
231
|
+
ledgerIdentityKeys.add(`${key}@@${d.verificationCycle ?? "*"}`);
|
|
232
|
+
pushAttempt(key, {
|
|
233
|
+
sequence: event.seq,
|
|
234
|
+
at: event.at,
|
|
235
|
+
status: d.status ?? null,
|
|
236
|
+
exitCode: d.exitCode ?? null,
|
|
237
|
+
requirement: d.requirement ?? null,
|
|
238
|
+
verificationCycle: d.verificationCycle ?? null,
|
|
239
|
+
provenance: d.provenance ?? null,
|
|
240
|
+
executionMode: d.provenance === "FORGELOOP_EXECUTED"
|
|
241
|
+
? "executed"
|
|
242
|
+
: (d.provenance === "ACTOR_REPORTED" || d.provenance === "MANUAL_OBSERVATION" ? "observed" : (d.executionId ? "executed" : "unknown")),
|
|
243
|
+
failureToken: d.failureToken ?? d.details?.failureToken ?? null,
|
|
244
|
+
source: "ledger",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// State checks only enrich or fill legacy gaps; never duplicate ledger attempts.
|
|
249
|
+
for (const check of state?.checks ?? []) {
|
|
250
|
+
const key = check.id ?? check.checkId ?? null;
|
|
251
|
+
if (!key) continue;
|
|
252
|
+
const identityKey = `${key}@@${check.details?.verificationCycle ?? "*"}`;
|
|
253
|
+
if (ledgerIdentityKeys.has(identityKey) || ledgerIdentityKeys.has(`${key}@@*`)) continue;
|
|
254
|
+
pushAttempt(key, {
|
|
255
|
+
sequence: null,
|
|
256
|
+
at: check.at ?? check.lastUpdatedAt ?? null,
|
|
257
|
+
status: check.status,
|
|
258
|
+
exitCode: check.exitCode ?? null,
|
|
259
|
+
requirement: check.requirement ?? null,
|
|
260
|
+
verificationCycle: check.details?.verificationCycle ?? null,
|
|
261
|
+
provenance: check.provenance ?? null,
|
|
262
|
+
executionMode: check.provenance === "ACTOR_REPORTED" || check.provenance === "MANUAL_OBSERVATION" ? "observed" : "executed",
|
|
263
|
+
failureToken: check.details?.failureToken ?? null,
|
|
264
|
+
source: "state-fallback",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const checks = [];
|
|
269
|
+
const MAX = Number.MAX_SAFE_INTEGER;
|
|
270
|
+
for (const [id, attempts] of [...attemptsByCheck.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
271
|
+
attempts.sort((a, b) =>
|
|
272
|
+
((a.verificationCycle ?? MAX) - (b.verificationCycle ?? MAX))
|
|
273
|
+
|| ((a.sequence ?? MAX) - (b.sequence ?? MAX)));
|
|
274
|
+
checks.push({
|
|
275
|
+
id,
|
|
276
|
+
requirement: attempts.findLast((a) => a.requirement)?.requirement ?? null,
|
|
277
|
+
attemptCount: attempts.length,
|
|
278
|
+
failedAttempts: attempts.filter((a) => a.status === "failed" || a.status === "blocked").length,
|
|
279
|
+
currentResult: attempts.at(-1)?.status ?? null,
|
|
280
|
+
attempts,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return checks;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function legacyDiagnoses(events, taskId) {
|
|
287
|
+
return diagnosisEventsForTask(events, taskId).map((event) => ({
|
|
288
|
+
sequence: event.seq,
|
|
289
|
+
at: event.at,
|
|
290
|
+
sourceModel: "LEGACY_DIAGNOSIS_V1",
|
|
291
|
+
...event.details,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function structuredDiagnostics(events) {
|
|
296
|
+
const cases = [];
|
|
297
|
+
const interventions = [];
|
|
298
|
+
const dispositions = [];
|
|
299
|
+
const invalidRevisions = [];
|
|
300
|
+
const lastCaseByCycle = new Map();
|
|
301
|
+
for (const event of events) {
|
|
302
|
+
try {
|
|
303
|
+
if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
|
|
304
|
+
assertDiagnosticCaseDetails(event.details);
|
|
305
|
+
const cycle = event.details?.verificationCycle;
|
|
306
|
+
const previous = lastCaseByCycle.get(cycle);
|
|
307
|
+
if (Number.isInteger(event.details?.diagnosticRevision) && event.details.diagnosticRevision > 1 && previous) {
|
|
308
|
+
if (event.details.previousDiagnosticFingerprint !== previous.diagnosticFingerprint) {
|
|
309
|
+
invalidRevisions.push({
|
|
310
|
+
sequence: event.seq,
|
|
311
|
+
verificationCycle: cycle,
|
|
312
|
+
code: "E_DIAGNOSTIC_CASE_INVALID",
|
|
313
|
+
message: "Revision chain broken: previousDiagnosticFingerprint does not match the prior case fingerprint.",
|
|
314
|
+
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lastCaseByCycle.set(cycle, event.details);
|
|
319
|
+
cases.push({ sequence: event.seq, at: event.at, ...event.details });
|
|
320
|
+
} else if (event.event === "INTERVENTION_RECORDED") {
|
|
321
|
+
assertInterventionDetails(event.details);
|
|
322
|
+
interventions.push({ sequence: event.seq, at: event.at, ...event.details });
|
|
323
|
+
} else if (event.event === "HYPOTHESIS_DISPOSITION_RECORDED") {
|
|
324
|
+
assertHypothesisDispositionDetails(event.details);
|
|
325
|
+
dispositions.push({ sequence: event.seq, at: event.at, ...event.details });
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Invalid diagnostic details surface through ledger integrity errors;
|
|
329
|
+
// projections must not crash on them.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { cases, interventions, dispositions, invalidRevisions };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const PHASE_MILESTONES = Object.freeze({
|
|
336
|
+
TASK_RECEIVED: { phase: "RECEIVED", quality: "authoritative" },
|
|
337
|
+
CONTRACT_VALIDATED: { phase: "CONTRACT_READY", quality: "authoritative" },
|
|
338
|
+
ROUTE_VALIDATED: { phase: "ROUTED", quality: "authoritative" },
|
|
339
|
+
DESIGN_GATE_STARTED: { phase: "DESIGNING", quality: "authoritative" },
|
|
340
|
+
PLAN_RECORDED: { phase: "PLANNED", quality: "authoritative" },
|
|
341
|
+
EXECUTION_STARTED: { phase: "EXECUTING", quality: "authoritative" },
|
|
342
|
+
VERIFICATION_STARTED: { phase: "VERIFYING", quality: "authoritative" },
|
|
343
|
+
REVIEW_STARTED: { phase: "REVIEWING", quality: "authoritative" },
|
|
344
|
+
COMPLETION_VALIDATED: { phase: "COMPLETE", quality: "authoritative" },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const PHASE_DERIVATIONS = Object.freeze({
|
|
348
|
+
VERIFICATION_RECORDED: (details) => (["failed", "blocked"].includes(details?.status) ? "DIAGNOSING" : null),
|
|
349
|
+
DIAGNOSIS_RECORDED: () => "DIAGNOSING",
|
|
350
|
+
DIAGNOSTIC_CASE_RECORDED: () => "DIAGNOSING",
|
|
351
|
+
HYPOTHESIS_DISPOSITION_RECORDED: () => "CORRECTING",
|
|
352
|
+
INTERVENTION_RECORDED: () => "CORRECTING",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
function reconstructPhaseChronology(events) {
|
|
356
|
+
let currentPhase = null;
|
|
357
|
+
const phaseBySequence = new Map();
|
|
358
|
+
const qualityBySequence = new Map();
|
|
359
|
+
for (const event of events) {
|
|
360
|
+
const milestone = PHASE_MILESTONES[event.event];
|
|
361
|
+
if (milestone) {
|
|
362
|
+
currentPhase = milestone.phase;
|
|
363
|
+
qualityBySequence.set(event.seq, milestone.quality);
|
|
364
|
+
} else {
|
|
365
|
+
const derive = PHASE_DERIVATIONS[event.event];
|
|
366
|
+
const derived = derive ? derive(event.details ?? {}) : null;
|
|
367
|
+
if (derived) {
|
|
368
|
+
currentPhase = derived;
|
|
369
|
+
qualityBySequence.set(event.seq, "derived");
|
|
370
|
+
} else {
|
|
371
|
+
qualityBySequence.set(event.seq, currentPhase ? "derived" : "unknown");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
phaseBySequence.set(event.seq, currentPhase);
|
|
375
|
+
}
|
|
376
|
+
return { phaseBySequence, qualityBySequence };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function historyQualityFor({ snapshot, normalizedEvents }) {
|
|
380
|
+
const reasons = [];
|
|
381
|
+
const hasLedger = normalizedEvents.length > 0;
|
|
382
|
+
if (!hasLedger) reasons.push("LEDGER_ABSENT");
|
|
383
|
+
if (normalizedEvents.some((event) => event.timestampQuality !== "authoritative")) {
|
|
384
|
+
reasons.push("LEGACY_PHASE_TIMESTAMPS_UNAVAILABLE");
|
|
385
|
+
}
|
|
386
|
+
if (snapshot.state && Number.isInteger(snapshot.state.verificationCycle) && snapshot.state.verificationCycle > 1
|
|
387
|
+
&& !snapshot.events.some((event) => event.event === "DIAGNOSIS_RECORDED" || event.event === "DIAGNOSTIC_CASE_RECORDED")) {
|
|
388
|
+
reasons.push("STRUCTURED_DIAGNOSTICS_UNAVAILABLE");
|
|
389
|
+
}
|
|
390
|
+
const level = reasons.length === 0 ? "COMPLETE" : (hasLedger ? "PARTIAL" : "MINIMAL");
|
|
391
|
+
return { level, reasons };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function buildTaskTrace({ target, packageRoot, taskId = null, eventsPath = null } = {}) {
|
|
395
|
+
const snapshot = await buildTaskSnapshot({ target, packageRoot, taskId, eventsPath });
|
|
396
|
+
const artifactPath = eventsPath ?? ".forgeloop/task-state/<task-key>/events.ndjson";
|
|
397
|
+
|
|
398
|
+
const taskEvents = taskId ? snapshot.events.filter((event) => !event.taskId || event.taskId === taskId) : snapshot.events;
|
|
399
|
+
const { phaseBySequence, qualityBySequence } = reconstructPhaseChronology(taskEvents);
|
|
400
|
+
const normalizedEvents = [];
|
|
401
|
+
for (const event of taskEvents) {
|
|
402
|
+
normalizedEvents.push(normalizeProtocolEvent(event, {
|
|
403
|
+
phase: phaseBySequence.get(event.seq) ?? null,
|
|
404
|
+
artifactPath,
|
|
405
|
+
phaseQuality: qualityBySequence.get(event.seq) ?? "unknown",
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const integrity = {
|
|
410
|
+
valid: snapshot.integrity.valid,
|
|
411
|
+
errors: snapshot.integrity.errors,
|
|
412
|
+
};
|
|
413
|
+
const historyQuality = historyQualityFor({ snapshot, normalizedEvents });
|
|
414
|
+
const diagnostics = structuredDiagnostics(taskEvents);
|
|
415
|
+
|
|
416
|
+
const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
|
|
417
|
+
const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
|
|
418
|
+
|
|
419
|
+
const executions = [];
|
|
420
|
+
const seenExecutions = new Set();
|
|
421
|
+
for (const event of taskEvents) {
|
|
422
|
+
const d = event.details ?? {};
|
|
423
|
+
const executionId = d.executionId ?? d.executionRef ?? null;
|
|
424
|
+
if (!executionId || seenExecutions.has(executionId)) continue;
|
|
425
|
+
seenExecutions.add(executionId);
|
|
426
|
+
executions.push({
|
|
427
|
+
executionId,
|
|
428
|
+
sequence: event.seq,
|
|
429
|
+
at: event.at,
|
|
430
|
+
status: d.status ?? null,
|
|
431
|
+
exitCode: d.exitCode ?? null,
|
|
432
|
+
resolution: d.resolution ?? null,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const evidenceSources = new Map();
|
|
437
|
+
for (const event of taskEvents) {
|
|
438
|
+
const refs = [
|
|
439
|
+
...(Array.isArray(event.details?.evidenceRefs) ? event.details.evidenceRefs : []),
|
|
440
|
+
...(Array.isArray(event.details?.hypotheses)
|
|
441
|
+
? event.details.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? [])
|
|
442
|
+
: []),
|
|
443
|
+
];
|
|
444
|
+
for (const ref of refs) {
|
|
445
|
+
if (!ref) continue;
|
|
446
|
+
if (!evidenceSources.has(ref)) evidenceSources.set(ref, { ref, sources: [] });
|
|
447
|
+
if (!evidenceSources.get(ref).sources.includes(event.event)) evidenceSources.get(ref).sources.push(event.event);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const evidence = [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
451
|
+
|
|
452
|
+
const recovery = taskEvents
|
|
453
|
+
.filter((event) => ["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event))
|
|
454
|
+
.map((event) => ({ sequence: event.seq, at: event.at, type: event.event, details: event.details ?? {} }));
|
|
455
|
+
|
|
456
|
+
const completionEvents = taskEvents.filter((event) => ["COMPLETION_VALIDATED", "COMPLETION_REJECTED"].includes(event.event));
|
|
457
|
+
const completion = {
|
|
458
|
+
validatedAt: completionEvents.findLast((event) => event.event === "COMPLETION_VALIDATED")?.at ?? null,
|
|
459
|
+
rejectedAt: completionEvents.findLast((event) => event.event === "COMPLETION_REJECTED")?.at ?? null,
|
|
460
|
+
attempts: completionEvents.map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const actions = snapshot.taskId
|
|
464
|
+
? await listActions(target, { packageRoot, taskId: snapshot.taskId })
|
|
465
|
+
: [];
|
|
466
|
+
// Artifacts are the current-state materialization; chronology comes from
|
|
467
|
+
// the canonical ledger replay projection, never from artifacts alone.
|
|
468
|
+
const { projectActionLedger } = await import("./action-ledger-projection.js");
|
|
469
|
+
const projections = [];
|
|
470
|
+
for (const action of actions) {
|
|
471
|
+
projections.push(await projectActionLedger({
|
|
472
|
+
target,
|
|
473
|
+
packageRoot,
|
|
474
|
+
taskId: snapshot.taskId,
|
|
475
|
+
actionId: action.actionId,
|
|
476
|
+
artifact: action,
|
|
477
|
+
}));
|
|
478
|
+
}
|
|
479
|
+
const actionEvents = taskEvents.filter((event) => event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_"));
|
|
480
|
+
const byState = Object.fromEntries([...new Set(actions.map((action) => action.state))].sort()
|
|
481
|
+
.map((state) => [state, actions.filter((action) => action.state === state).length]));
|
|
482
|
+
const byCapability = Object.fromEntries([...new Set(actions.map((action) => action.capability))].sort()
|
|
483
|
+
.map((capability) => [capability, actions.filter((action) => action.capability === capability).length]));
|
|
484
|
+
const idempotencyAttempts = actions.map((action) => action.idempotencyKey).filter(Boolean);
|
|
485
|
+
const repeatedIdempotencyAttempts = idempotencyAttempts.length - new Set(idempotencyAttempts).size;
|
|
486
|
+
const actionProjection = {
|
|
487
|
+
total: actions.length,
|
|
488
|
+
byState,
|
|
489
|
+
byCapability,
|
|
490
|
+
required: actions.filter((action) => action.requiredForCompletion).length,
|
|
491
|
+
ambiguous: actions.filter((action) => action.state === "COMMIT_UNKNOWN").length,
|
|
492
|
+
failed: actions.filter((action) => action.state === "FAILED").length,
|
|
493
|
+
verified: actions.filter((action) => action.state === "VERIFIED").length,
|
|
494
|
+
trustedSatisfied: projections.filter(
|
|
495
|
+
(projection) => projection.valid && projection.state === "VERIFIED"
|
|
496
|
+
&& projection.authorization.valid && projection.verification.valid,
|
|
497
|
+
).length,
|
|
498
|
+
untrustedRequired: projections.filter((projection) =>
|
|
499
|
+
!projection.valid
|
|
500
|
+
|| (projection.state === "VERIFIED" && !(projection.authorization.valid && projection.verification.valid))
|
|
501
|
+
).length,
|
|
502
|
+
repeatedIdempotencyAttempts,
|
|
503
|
+
reconciliationCount: taskEvents.filter((event) => event.event === "ACTION_RECONCILED").length,
|
|
504
|
+
eventCount: actionEvents.length,
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
schemaVersion: 1,
|
|
509
|
+
protocolVersion: 1,
|
|
510
|
+
command: "trace",
|
|
511
|
+
task: {
|
|
512
|
+
id: snapshot.taskId,
|
|
513
|
+
phase: snapshot.state?.phase ?? null,
|
|
514
|
+
status: snapshot.state?.status ?? null,
|
|
515
|
+
revision: snapshot.state?.revision ?? null,
|
|
516
|
+
verificationCycle: snapshot.state?.verificationCycle ?? null,
|
|
517
|
+
present: Boolean(snapshot.state),
|
|
518
|
+
},
|
|
519
|
+
snapshot: {
|
|
520
|
+
consistent: snapshot.consistent,
|
|
521
|
+
stateRevision: snapshot.anchors.stateRevision,
|
|
522
|
+
ledgerTailSequence: snapshot.anchors.sequence,
|
|
523
|
+
capturedAt: snapshot.capturedAt,
|
|
524
|
+
},
|
|
525
|
+
historyQuality,
|
|
526
|
+
integrity,
|
|
527
|
+
artifacts: {},
|
|
528
|
+
events: normalizedEvents,
|
|
529
|
+
transitions: lifecycleTransitions(taskEvents),
|
|
530
|
+
executions,
|
|
531
|
+
checks: projectChecks(snapshot),
|
|
532
|
+
evidence,
|
|
533
|
+
diagnostics: {
|
|
534
|
+
legacyDiagnoses: legacyDiagnoses(taskEvents, snapshot.taskId),
|
|
535
|
+
...diagnostics,
|
|
536
|
+
},
|
|
537
|
+
failureSignatures,
|
|
538
|
+
failureSurfaces,
|
|
539
|
+
continuity: taskEvents
|
|
540
|
+
.filter((event) => ["CONTINUITY_RECORDED", "CHECKPOINT_RECONCILED"].includes(event.event))
|
|
541
|
+
.map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
|
|
542
|
+
recovery,
|
|
543
|
+
policy: {},
|
|
544
|
+
audit: {},
|
|
545
|
+
completion,
|
|
546
|
+
actions: actionProjection,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { canonicalFingerprint, writeJsonArtifact } from "./artifacts.js";
|
|
5
|
+
import { buildTaskTrace } from "./trace.js";
|
|
6
|
+
import { buildTrajectoryMetrics } from "./trajectory-metrics.js";
|
|
7
|
+
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
8
|
+
import { taskEvaluationPath } from "./task-paths.js";
|
|
9
|
+
import { appendProtocolEvent } from "./events.js";
|
|
10
|
+
import { assertSafePath, ensureWithin } from "./filesystem.js";
|
|
11
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
12
|
+
|
|
13
|
+
function evaluationError(code, message) { const error = new Error(message); error.code = code; return error; }
|
|
14
|
+
|
|
15
|
+
export async function evaluateTrajectory({ target, packageRoot, taskId, scenarioPath, evaluationId = `eval-${randomUUID()}` }) {
|
|
16
|
+
if (typeof scenarioPath !== "string" || !scenarioPath || path.isAbsolute(scenarioPath) || scenarioPath.includes("..")) {
|
|
17
|
+
throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "scenario path must be a relative project-local file");
|
|
18
|
+
}
|
|
19
|
+
let scenario;
|
|
20
|
+
try {
|
|
21
|
+
await assertSafePath(target, scenarioPath);
|
|
22
|
+
scenario = JSON.parse(await readFile(ensureWithin(target, scenarioPath), "utf8"));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", `unable to read scenario safely: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
assertSchema(scenario, await readSchema("trajectory-scenario", packageRoot), "trajectory scenario");
|
|
27
|
+
if (!/^eval-[A-Za-z0-9_-]+$/.test(evaluationId)) throw evaluationError("E_TRAJECTORY_SCENARIO_INVALID", "evaluationId is invalid");
|
|
28
|
+
const trace = await buildTaskTrace({ target, packageRoot, taskId });
|
|
29
|
+
const metrics = await buildTrajectoryMetrics({ target, packageRoot, taskId });
|
|
30
|
+
const milestoneSet = new Set(trace.events.map((event) => event.type));
|
|
31
|
+
const missingMilestones = (scenario.requiredMilestones ?? []).filter((milestone) => !milestoneSet.has(milestone));
|
|
32
|
+
const completionEvent = trace.events.find((event) => event.type === "COMPLETION_VALIDATED");
|
|
33
|
+
const verificationEvent = trace.events.find((event) => event.type === "VERIFICATION_STARTED" || event.type === "VERIFICATION_RECORDED");
|
|
34
|
+
const completionBeforeVerification = Boolean(completionEvent && (!verificationEvent || completionEvent.sequence < verificationEvent.sequence));
|
|
35
|
+
// Canonical truth: ambiguous actions or unresolved trusted-readiness items
|
|
36
|
+
// block safety. A raw VERIFIED label alone never resolves a required action.
|
|
37
|
+
const unresolvedRequiredAction = (trace.actions?.ambiguous ?? 0) > 0
|
|
38
|
+
|| ((metrics.actions.unresolvedRequired ?? null) !== null
|
|
39
|
+
? metrics.actions.unresolvedRequired > 0
|
|
40
|
+
: (trace.actions?.required ?? 0) > (trace.actions?.verified ?? 0));
|
|
41
|
+
const limit = (name, actual, max) => ({ actual, ...(max === undefined ? {} : { max, pass: actual <= max }) });
|
|
42
|
+
const limits = {
|
|
43
|
+
verificationCycles: limit("verificationCycles", metrics.trajectory.verificationCycles, scenario.limits?.maxVerificationCycles),
|
|
44
|
+
nonInformativeInterventions: limit("nonInformativeInterventions", metrics.trajectory.interventions.nonInformative, scenario.limits?.maxNonInformativeInterventions),
|
|
45
|
+
ambiguousActions: limit("ambiguousActions", metrics.actions.ambiguous, scenario.limits?.maxAmbiguousActions),
|
|
46
|
+
};
|
|
47
|
+
const safetyValid = !(scenario.forbidden?.completionBeforeVerification && completionBeforeVerification)
|
|
48
|
+
&& !(scenario.forbidden?.unresolvedRequiredAction && unresolvedRequiredAction);
|
|
49
|
+
const completionValid = metrics.completion.validated && missingMilestones.length === 0;
|
|
50
|
+
const limitsValid = Object.values(limits).every((item) => item.pass !== false);
|
|
51
|
+
// Comparable steps have exactly one owner: buildTrajectoryMetrics.
|
|
52
|
+
const efficiency = Number.isInteger(scenario.reference?.comparableSteps) && scenario.reference.comparableSteps > 0
|
|
53
|
+
? { referenceComparableSteps: scenario.reference.comparableSteps, actualComparableSteps: metrics.comparableSteps,
|
|
54
|
+
ratio: scenario.reference.comparableSteps / Math.max(1, metrics.comparableSteps) }
|
|
55
|
+
: null;
|
|
56
|
+
const base = {
|
|
57
|
+
schemaVersion: 1, evaluationId, scenarioId: scenario.scenarioId,
|
|
58
|
+
scenarioFingerprint: canonicalFingerprint(scenario), taskId,
|
|
59
|
+
result: completionValid && safetyValid && limitsValid ? "PASS" : "FAIL",
|
|
60
|
+
completionValid, safetyValid, missingMilestones, limits, efficiency,
|
|
61
|
+
computedAt: new Date().toISOString(), source: "PROJECT_LOCAL_REFERENCE",
|
|
62
|
+
};
|
|
63
|
+
const evaluation = { ...base, evaluationFingerprint: canonicalFingerprint(base) };
|
|
64
|
+
assertSchema(evaluation, await readSchema("trajectory-evaluation", packageRoot), "trajectory evaluation");
|
|
65
|
+
await withTaskTransaction({ target, taskId, operation: "trajectory-evaluated" }, async () => {
|
|
66
|
+
await writeJsonArtifact(target, taskEvaluationPath(taskId, evaluationId), evaluation, "trajectory-evaluation", packageRoot);
|
|
67
|
+
await appendProtocolEvent(target, { taskId, event: "TRAJECTORY_EVALUATED", fingerprint: evaluation.evaluationFingerprint,
|
|
68
|
+
details: { evaluationId, scenarioId: scenario.scenarioId, evaluationFingerprint: evaluation.evaluationFingerprint } }, packageRoot, { taskId });
|
|
69
|
+
});
|
|
70
|
+
return evaluation;
|
|
71
|
+
}
|