@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.2
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/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +2 -1
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +8 -4
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/README.md +28 -39
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/completions/_forgeloop +11 -3
- package/completions/forgeloop.bash +19 -3
- package/completions/forgeloop.fish +61 -1
- package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
- package/docs/ARTIFACT_REFERENCE.md +50 -37
- package/docs/CLI_REFERENCE.md +147 -3
- package/docs/DOCUMENTATION_GUIDE.md +6 -4
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/PACKAGE_CONTENTS.md +14 -13
- package/docs/RELEASE_CHECKLIST.md +81 -10
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/documentation-manifest.json +25 -12
- package/package.json +15 -10
- package/schemas/task-bundle.schema.json +7 -2
- package/scripts/generate-agent-protocol-summary.mjs +2 -2
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/src/adapters/agent-browser/provider.js +14 -11
- package/src/adapters/emulated-services/provider.js +11 -4
- package/src/adapters/temporary-paths.js +18 -0
- package/src/cli.js +146 -210
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +24 -8
- package/src/commands/complete.js +19 -3
- package/src/commands/contract-create.js +3 -1
- package/src/commands/doctor.js +36 -13
- package/src/commands/gate-record.js +20 -31
- package/src/commands/gate-revalidate.js +11 -13
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +9 -1
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/semantic-plan.js +26 -2
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +1 -1
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/validate-protocol.js +29 -14
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +59 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit.js +6 -1
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +12 -10
- package/src/core/cli-command-definitions.js +109 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +182 -126
- package/src/core/command-input.js +21 -1
- package/src/core/completion-artifacts.js +71 -29
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +60 -9
- package/src/core/conformance.js +2 -1
- package/src/core/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +42 -41
- package/src/core/contract-revision.js +8 -9
- package/src/core/decision/artifact.js +2 -1
- package/src/core/decision/events.js +9 -6
- package/src/core/decision/resolver.js +19 -11
- package/src/core/decision/service.js +63 -38
- package/src/core/decision/task-bindings.js +6 -1
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/events.js +316 -97
- package/src/core/execution-prerequisites.js +22 -13
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +6 -5
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +10 -0
- package/src/core/integration-resources.js +41 -34
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-phases.js +1 -1
- package/src/core/next-action-review-phase.js +2 -2
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +21 -2
- package/src/core/phase.js +138 -93
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +72 -59
- package/src/core/preflight-loaders.js +19 -12
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +3 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/reconcile-closure.js +162 -24
- package/src/core/recovery-history.js +66 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/route-artifact.js +13 -3
- package/src/core/runtime-context.js +5 -0
- package/src/core/schema-validation.js +8 -3
- package/src/core/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +52 -18
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +112 -107
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +20 -8
- package/src/core/test-intelligence/prune.js +5 -1
- package/src/core/trace.js +154 -99
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -288
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- package/src/repository-index/binary-manager.js +7 -5
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +663 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
package/src/core/events.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { ledgerRelationMap, ledgerRelationSet } from "./ledger-relations.js";
|
|
2
|
+
import { ledgerEventAt, ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
3
|
+
import { needsExistingProjectScope, withExistingProjectScope, isOperationalArtifactPath } from "../storage/existing-project-scope.js";
|
|
1
4
|
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
5
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { closeSync, lstatSync, openSync, readSync } from "node:fs";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
4
8
|
|
|
5
9
|
import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
|
|
6
10
|
import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
|
|
@@ -21,6 +25,7 @@ import {
|
|
|
21
25
|
|
|
22
26
|
import { taskArtifactPath } from "./task-paths.js";
|
|
23
27
|
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
28
|
+
import { getOperationalStore } from "../storage/operational-context.js";
|
|
24
29
|
|
|
25
30
|
import { assertDiagnosisDetails } from "./diagnosis-model.js";
|
|
26
31
|
import {
|
|
@@ -131,28 +136,12 @@ function validateExecutionGateChronology(events, index, seen, errors) {
|
|
|
131
136
|
const latestContractRevisionSeq = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT)?.seq ?? 0;
|
|
132
137
|
const beforeExecution = events.slice(0, index);
|
|
133
138
|
for (const gate of requiredGates) {
|
|
134
|
-
if (!hasCurrentGateEvidence(beforeExecution, events
|
|
139
|
+
if (!hasCurrentGateEvidence(beforeExecution, ledgerEventAt(events, index)?.taskId, gate, latestContractRevisionSeq)) {
|
|
135
140
|
errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
function eventIndexPath(eventsPath) {
|
|
141
|
-
return `${eventsPath}.index.json`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseEventIndex(text, relativePath) {
|
|
145
|
-
if (typeof text !== "string") return null;
|
|
146
|
-
try {
|
|
147
|
-
const value = JSON.parse(text);
|
|
148
|
-
if (!value || value.schemaVersion !== 1 || !Number.isInteger(value.seq) || value.seq < 0
|
|
149
|
-
|| (value.lastHash !== null && !/^[a-f0-9]{64}$/.test(value.lastHash))) return null;
|
|
150
|
-
return value;
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
145
|
function checkpointFromEvents(events) {
|
|
157
146
|
const last = events.at(-1) ?? null;
|
|
158
147
|
return {
|
|
@@ -166,17 +155,7 @@ async function readEventCheckpoint(target, packageRoot, relPath, options, transa
|
|
|
166
155
|
if (!transaction.eventCheckpoints) transaction.eventCheckpoints = new Map();
|
|
167
156
|
const cached = transaction.eventCheckpoints.get(relPath);
|
|
168
157
|
if (cached) return cached;
|
|
169
|
-
|
|
170
|
-
const indexPath = eventIndexPath(relPath);
|
|
171
|
-
const indexed = parseEventIndex(await transaction.readText(indexPath), indexPath);
|
|
172
|
-
const tail = await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 });
|
|
173
|
-
const last = tail.at(-1) ?? null;
|
|
174
|
-
let checkpoint;
|
|
175
|
-
if (indexed && indexed.seq === (last?.seq ?? 0) && indexed.lastHash === (last?.hash ?? null)) {
|
|
176
|
-
checkpoint = indexed;
|
|
177
|
-
} else {
|
|
178
|
-
checkpoint = checkpointFromEvents(await readEvents(target, packageRoot, { ...options, eventsPath: relPath }));
|
|
179
|
-
}
|
|
158
|
+
const checkpoint = checkpointFromEvents(await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 }));
|
|
180
159
|
transaction.eventCheckpoints.set(relPath, checkpoint);
|
|
181
160
|
return checkpoint;
|
|
182
161
|
}
|
|
@@ -524,35 +503,79 @@ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
|
|
|
524
503
|
}
|
|
525
504
|
|
|
526
505
|
export async function readEvents(target, packageRoot, options = {}) {
|
|
506
|
+
if (await needsExistingProjectScope(target)) {
|
|
507
|
+
return withExistingProjectScope(target, () => readEvents(target, packageRoot, options), { readOnly: true });
|
|
508
|
+
}
|
|
527
509
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
510
|
+
const store = getOperationalStore(target);
|
|
511
|
+
if (store?.recognizes(relPath)) {
|
|
512
|
+
const schema = await readSchema("event", packageRoot);
|
|
513
|
+
const events = [];
|
|
514
|
+
// Array callers consume the synchronous cursor directly. Streaming callers
|
|
515
|
+
// retain iterateEvents; both paths validate and fully bind observed rows.
|
|
516
|
+
for (const event of store.iterateEvents(relPath)) {
|
|
517
|
+
validateStoredEvent(event, schema, `${relPath}[${events.length}]`);
|
|
518
|
+
events.push(event);
|
|
519
|
+
}
|
|
520
|
+
return events;
|
|
534
521
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
522
|
+
if (await assertOperationalLedgerAbsent(target, relPath)) return [];
|
|
523
|
+
return readPortableEvents(target, packageRoot, { ...options, eventsPath: relPath });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function assertOperationalLedgerAbsent(target, relativePath) {
|
|
527
|
+
await assertSafePath(target, relativePath);
|
|
528
|
+
if (isOperationalArtifactPath(relativePath) && await fileExists(ensureWithin(target, relativePath))) {
|
|
529
|
+
throw protocolError("E_STORAGE_MIGRATION_REQUIRED", "Operational ledger reads require canonical SQLite; inspect legacy exports explicitly", [relativePath]);
|
|
530
|
+
}
|
|
531
|
+
return isOperationalArtifactPath(relativePath);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Explicit read-only inspection of an import/export ledger; never selects native authority. */
|
|
535
|
+
export async function readPortableEvents(target, packageRoot, options = {}) {
|
|
536
|
+
const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
537
|
+
const filename = await assertSafePath(target, relPath);
|
|
538
|
+
if (!(await fileExists(filename))) return [];
|
|
539
|
+
return parseEventsText(await readFile(filename, "utf8"), relPath, packageRoot);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Stream schema-validated canonical rows within the captured operational scope. */
|
|
543
|
+
export function iterateEvents(target, packageRoot, options = {}) {
|
|
544
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
545
|
+
const store = getOperationalStore(target);
|
|
546
|
+
return (async function* validatedEvents() {
|
|
547
|
+
if (!store?.recognizes(relPath)) { yield* await readEvents(target, packageRoot, options); return; }
|
|
548
|
+
const schema = await readSchema("event", packageRoot);
|
|
549
|
+
let index = 0;
|
|
550
|
+
for (const event of store.iterateEvents(relPath)) {
|
|
551
|
+
validateStoredEvent(event, schema, `${relPath}[${index}]`);
|
|
552
|
+
index += 1;
|
|
553
|
+
yield event;
|
|
554
|
+
}
|
|
555
|
+
})();
|
|
538
556
|
}
|
|
539
557
|
|
|
540
558
|
export async function readEventTail(target, packageRoot, options = {}) {
|
|
559
|
+
if (await needsExistingProjectScope(target)) {
|
|
560
|
+
return withExistingProjectScope(target, () => readEventTail(target, packageRoot, options), { readOnly: true });
|
|
561
|
+
}
|
|
541
562
|
const limit = options.limit ?? 50;
|
|
542
563
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
543
564
|
throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
|
|
544
565
|
}
|
|
545
566
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
567
|
+
const store = getOperationalStore(target);
|
|
568
|
+
if (store?.recognizes(relPath)) return validateStoredEvents(store.readEvents(relPath, limit), relPath, packageRoot);
|
|
569
|
+
if (await assertOperationalLedgerAbsent(target, relPath)) return [];
|
|
570
|
+
return readPortableEventTail(target, packageRoot, { ...options, eventsPath: relPath });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Bounded tail inspection for explicit legacy/import/export tooling. */
|
|
574
|
+
export async function readPortableEventTail(target, packageRoot, options = {}) {
|
|
575
|
+
const limit = options.limit ?? 50;
|
|
576
|
+
if (!Number.isInteger(limit) || limit < 1) throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
|
|
577
|
+
const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
578
|
+
const eventsPath = await assertSafePath(target, relPath);
|
|
556
579
|
if (!(await fileExists(eventsPath))) return [];
|
|
557
580
|
const size = (await stat(eventsPath)).size;
|
|
558
581
|
let window = Math.min(size, 64 * 1024);
|
|
@@ -600,18 +623,41 @@ async function parseEventsText(text, relPath, packageRoot) {
|
|
|
600
623
|
});
|
|
601
624
|
}
|
|
602
625
|
|
|
626
|
+
async function validateStoredEvents(events, relativePath, packageRoot) {
|
|
627
|
+
const schema = await readSchema("event", packageRoot);
|
|
628
|
+
for (const [index, event] of events.entries()) {
|
|
629
|
+
validateStoredEvent(event, schema, `${relativePath}[${index}]`);
|
|
630
|
+
}
|
|
631
|
+
return events;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function validateStoredEvent(event, schema, label) {
|
|
635
|
+
assertJsonBytes(JSON.stringify(event), label);
|
|
636
|
+
assertJsonLimits(event, label);
|
|
637
|
+
assertSchema(event, schema, label);
|
|
638
|
+
validateKnownEventDetails(event);
|
|
639
|
+
}
|
|
640
|
+
|
|
603
641
|
export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
|
|
604
642
|
const activeTransaction = (await getTaskTransaction(target));
|
|
605
643
|
if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
|
|
606
644
|
if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
|
|
607
645
|
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
608
646
|
if (!activeTransaction) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
647
|
+
// Only a standalone append is replayable: nested domain mutations and
|
|
648
|
+
// external observations never enter this retry boundary.
|
|
649
|
+
const capturedInput = structuredClone(input);
|
|
650
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
651
|
+
try {
|
|
652
|
+
return await withTaskTransaction({
|
|
653
|
+
target,
|
|
654
|
+
taskId: options.taskId ?? capturedInput.taskId,
|
|
655
|
+
operation: "append-event",
|
|
656
|
+
}, () => appendProtocolEvent(target, capturedInput, packageRoot, options));
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error.code !== "E_STATE_REVISION_CONFLICT" || attempt >= 2) throw error;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
615
661
|
}
|
|
616
662
|
const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
|
|
617
663
|
const event = buildProtocolEvent(input, { checkpoint });
|
|
@@ -619,16 +665,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
619
665
|
assertSchema(event, schema, relPath);
|
|
620
666
|
event.hash = eventHash(event);
|
|
621
667
|
if (!options.dryRun) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
const nextCheckpoint = { schemaVersion: 1, seq: event.seq, lastHash: event.hash };
|
|
625
|
-
await activeTransaction.stageText(eventIndexPath(relPath), `${JSON.stringify(nextCheckpoint)}\n`);
|
|
626
|
-
activeTransaction.eventCheckpoints.set(relPath, nextCheckpoint);
|
|
627
|
-
} else {
|
|
628
|
-
const eventsPath = ensureWithin(target, relPath);
|
|
629
|
-
await mkdir(path.dirname(eventsPath), { recursive: true });
|
|
630
|
-
await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
|
|
631
|
-
}
|
|
668
|
+
activeTransaction.appendEvent(event);
|
|
669
|
+
activeTransaction.eventCheckpoints.set(relPath, { schemaVersion: 1, seq: event.seq, lastHash: event.hash });
|
|
632
670
|
}
|
|
633
671
|
return event;
|
|
634
672
|
}
|
|
@@ -640,8 +678,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
|
|
|
640
678
|
* before appending the migration events.
|
|
641
679
|
*/
|
|
642
680
|
function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegacyRecoveryEvents = false } = {}) {
|
|
643
|
-
const migrationBySeq =
|
|
644
|
-
for (const event of events) {
|
|
681
|
+
const migrationBySeq = ledgerRelationMap();
|
|
682
|
+
for (const event of ledgerEventsOfTypes(events, [LEGACY_RECOVERY_MIGRATION_EVENT])) {
|
|
645
683
|
if (event.event !== LEGACY_RECOVERY_MIGRATION_EVENT) continue;
|
|
646
684
|
try {
|
|
647
685
|
assertLegacyMigrationDetails(event.details);
|
|
@@ -656,11 +694,12 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
656
694
|
});
|
|
657
695
|
continue;
|
|
658
696
|
}
|
|
659
|
-
migrationBySeq.set(event.details.legacyEventSeq, event);
|
|
697
|
+
migrationBySeq.set(event.details.legacyEventSeq, events.indexOf(event));
|
|
660
698
|
}
|
|
661
|
-
for (const event of events) {
|
|
699
|
+
for (const event of ledgerEventsOfTypes(events, ["OPERATOR_RECOVERY_RECORDED"])) {
|
|
662
700
|
if (!isLegacyRecoveryEventShape(event)) continue;
|
|
663
|
-
const
|
|
701
|
+
const migrationPosition = migrationBySeq.get(event.seq);
|
|
702
|
+
const migration = migrationPosition === undefined ? undefined : ledgerEventAt(events, migrationPosition);
|
|
664
703
|
if (!migration) {
|
|
665
704
|
if (!allowUnmigratedLegacyRecoveryEvents) {
|
|
666
705
|
errors.push({
|
|
@@ -692,7 +731,8 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
692
731
|
});
|
|
693
732
|
}
|
|
694
733
|
}
|
|
695
|
-
for (const [legacySeq,
|
|
734
|
+
for (const [legacySeq, position] of migrationBySeq) {
|
|
735
|
+
const migration = ledgerEventAt(events, position);
|
|
696
736
|
errors.push({
|
|
697
737
|
code: "E_EVENT_INVALID",
|
|
698
738
|
message: `migration event ${migration.seq} references unknown legacy recovery event seq ${legacySeq}`,
|
|
@@ -700,30 +740,37 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
|
|
|
700
740
|
}
|
|
701
741
|
}
|
|
702
742
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Pure ledger validation over an already-loaded event array.
|
|
745
|
+
*
|
|
746
|
+
* This is the smallest reusable seam between the filesystem path and a
|
|
747
|
+
* database-backed reader: every rule below is computed from the events alone,
|
|
748
|
+
* with no filesystem access. `validateEventLedger` keeps its existing signature
|
|
749
|
+
* and delegates here after reading events, so behavior is unchanged for every
|
|
750
|
+
* existing caller.
|
|
751
|
+
*
|
|
752
|
+
* Callers that already hold events (for example a database reader) can obtain
|
|
753
|
+
* exactly the same guarantees without materializing files first.
|
|
754
|
+
*/
|
|
755
|
+
export function validateLedgerEvents(events, options = {}) {
|
|
711
756
|
const errors = [];
|
|
712
757
|
let taskId = null;
|
|
758
|
+
let previousHash = null;
|
|
713
759
|
const seen = new Set();
|
|
714
760
|
let lastMilestone = -1;
|
|
715
761
|
const milestoneCounts = new Map();
|
|
716
|
-
const createdHandoffs =
|
|
717
|
-
const acceptedHandoffs =
|
|
762
|
+
const createdHandoffs = ledgerRelationMap();
|
|
763
|
+
const acceptedHandoffs = ledgerRelationSet();
|
|
718
764
|
for (const [index, event] of events.entries()) {
|
|
719
765
|
if (event.seq !== index + 1) {
|
|
720
766
|
errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
|
|
721
767
|
}
|
|
722
768
|
if (taskId === null) taskId = event.taskId;
|
|
723
769
|
if (event.taskId !== taskId) errors.push({ code: "E_EVENT_INVALID", message: "event task IDs must remain stable" });
|
|
724
|
-
if (event.previousHash !==
|
|
770
|
+
if (event.previousHash !== previousHash) {
|
|
725
771
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} previousHash does not match` });
|
|
726
772
|
}
|
|
773
|
+
previousHash = event.hash;
|
|
727
774
|
if (event.hash !== eventHash(event)) {
|
|
728
775
|
errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
|
|
729
776
|
}
|
|
@@ -774,11 +821,172 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
|
774
821
|
errors.push(...validateContractRevisionEventBindings(events));
|
|
775
822
|
errors.push(...validateGateSatisfactionBindings(events));
|
|
776
823
|
errors.push(...validateSemanticDecisionEventBindings(events));
|
|
777
|
-
errors.push(...await validateSemanticDecisionArtifactBindings(target, packageRoot, events));
|
|
778
824
|
const repairedErrors = validateContractBootstrapRepairLedger(events, errors, options);
|
|
779
825
|
return { valid: repairedErrors.length === 0, events, errors: repairedErrors };
|
|
780
826
|
}
|
|
781
827
|
|
|
828
|
+
/** Validate canonical native or non-operational interchange ledger with all domain bindings. */
|
|
829
|
+
export async function validateEventLedger(target, packageRoot, options = {}) {
|
|
830
|
+
if (await needsExistingProjectScope(target)) {
|
|
831
|
+
return withExistingProjectScope(target, () => validateEventLedger(target, packageRoot, options), { readOnly: true });
|
|
832
|
+
}
|
|
833
|
+
return validateReadLedger(target, packageRoot, options, readEvents);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Explicit legacy source validation; never makes the source writable authority. */
|
|
837
|
+
export async function validatePortableEventLedger(target, packageRoot, options = {}) {
|
|
838
|
+
return validateReadLedger(target, packageRoot, options, readPortableEvents);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function validateReadLedger(target, packageRoot, options, reader) {
|
|
842
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
843
|
+
let events;
|
|
844
|
+
try {
|
|
845
|
+
events = await reader(target, packageRoot, { ...options, eventsPath: relPath });
|
|
846
|
+
} catch (error) {
|
|
847
|
+
// A concurrent commit can invalidate a preparation read between statements;
|
|
848
|
+
// that is the standard retryable revision conflict, not ledger evidence.
|
|
849
|
+
// Converting it into an invalid-ledger result would let classification
|
|
850
|
+
// surface a transient contention loser as permanent ownership
|
|
851
|
+
// inconsistency, so a conflict keeps its own domain error here.
|
|
852
|
+
if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
853
|
+
return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
|
|
854
|
+
}
|
|
855
|
+
const result = validateLedgerEvents(events, options);
|
|
856
|
+
const bindingErrors = await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
|
|
857
|
+
if (bindingErrors.length === 0) return result;
|
|
858
|
+
return { valid: false, events, errors: [...result.errors, ...bindingErrors] };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Proof reuse is minted only for one owned read-only backup callback. Public
|
|
862
|
+
// collections and caller-supplied auditSource metadata cannot opt into it.
|
|
863
|
+
const ownedLedgerProofs = new WeakMap();
|
|
864
|
+
|
|
865
|
+
function ownedSnapshotFileIdentity(filename) {
|
|
866
|
+
// The WAL is durable database content that can change while a read cursor
|
|
867
|
+
// pins the main file. The SHM sidecar only carries transient lock/index
|
|
868
|
+
// state, so it is intentionally excluded from the byte identity.
|
|
869
|
+
const paths = [filename, `${filename}-wal`];
|
|
870
|
+
const files = paths.map(current => {
|
|
871
|
+
try {
|
|
872
|
+
const file = lstatSync(current, { bigint: true });
|
|
873
|
+
if (!file.isFile()) throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot file changed during its audit");
|
|
874
|
+
return file;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (error?.code === "ENOENT") return null;
|
|
877
|
+
throw error;
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
const identity = files.map(file => file
|
|
881
|
+
? [file.dev, file.ino, file.size, file.mtimeNs, file.ctimeNs].join(":")
|
|
882
|
+
: "missing").join("|");
|
|
883
|
+
if (process.platform !== "win32") return identity;
|
|
884
|
+
// Windows may defer file timestamps until SQLite's open handle closes.
|
|
885
|
+
// Hash readable bytes with bounded memory so raw writes cannot reuse a proof.
|
|
886
|
+
const hash = createHash("sha256");
|
|
887
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
888
|
+
for (const [index, current] of paths.entries()) {
|
|
889
|
+
hash.update(`${index}:`);
|
|
890
|
+
if (!files[index]) continue;
|
|
891
|
+
const fd = openSync(current, "r");
|
|
892
|
+
try {
|
|
893
|
+
let count;
|
|
894
|
+
while ((count = readSync(fd, buffer, 0, buffer.length, null)) !== 0) hash.update(buffer.subarray(0, count));
|
|
895
|
+
} finally { closeSync(fd); }
|
|
896
|
+
}
|
|
897
|
+
return `${identity}:${hash.digest("hex")}`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function assertOwnedLedgerUnchanged(events) {
|
|
901
|
+
const owned = ownedLedgerProofs.get(events);
|
|
902
|
+
if (owned && (owned.versionStatement.get().data_version !== owned.version
|
|
903
|
+
|| ownedSnapshotFileIdentity(owned.filename) !== owned.fileIdentity)) {
|
|
904
|
+
throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot changed during its audit");
|
|
905
|
+
}
|
|
906
|
+
return owned;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function validateOwnedLedgerEvents(events, options) {
|
|
910
|
+
const owned = assertOwnedLedgerUnchanged(events);
|
|
911
|
+
if (!owned) return validateLedgerEvents(events, options);
|
|
912
|
+
const flags = {
|
|
913
|
+
allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
|
|
914
|
+
allowUnmigratedLegacyContractBootstrapRepairMarkers: options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true,
|
|
915
|
+
};
|
|
916
|
+
const key = JSON.stringify(flags);
|
|
917
|
+
if (owned.successes.has(key)) return { valid: true, events, errors: [] };
|
|
918
|
+
const result = validateLedgerEvents(events, flags);
|
|
919
|
+
assertOwnedLedgerUnchanged(events);
|
|
920
|
+
if (result.valid) owned.successes.add(key);
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath) {
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
result = validateOwnedLedgerEvents(events, options);
|
|
928
|
+
const owned = assertOwnedLedgerUnchanged(events);
|
|
929
|
+
const sameScope = owned && owned.store === getOperationalStore(target)
|
|
930
|
+
&& owned.db === owned.store.db && owned.packageRoot === packageRoot
|
|
931
|
+
&& owned.relPath === relPath && owned.taskId === options?.taskId;
|
|
932
|
+
const bindingErrors = sameScope && owned.semanticValidated
|
|
933
|
+
? [] : await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
|
|
934
|
+
assertOwnedLedgerUnchanged(events);
|
|
935
|
+
if (sameScope && result.valid && bindingErrors.length === 0) owned.semanticValidated = true;
|
|
936
|
+
if (bindingErrors.length) result = { valid: false, events, errors: [...result.errors, ...bindingErrors] };
|
|
937
|
+
} catch (error) {
|
|
938
|
+
result = { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
|
|
939
|
+
}
|
|
940
|
+
const initiallyValid = result.valid;
|
|
941
|
+
const callbackResult = await callback(result);
|
|
942
|
+
// The caller may await other work or mutate its result object. Keep the
|
|
943
|
+
// private snapshot proof valid for the entire successful observation.
|
|
944
|
+
if (initiallyValid) assertOwnedLedgerUnchanged(events);
|
|
945
|
+
return callbackResult;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Internal callback audit: events remain owned by one immutable native snapshot. */
|
|
949
|
+
export async function withEventLedgerAudit(target, packageRoot, options, callback) {
|
|
950
|
+
if (await needsExistingProjectScope(target)) {
|
|
951
|
+
return withExistingProjectScope(target, () => withEventLedgerAudit(target, packageRoot, options, callback), { readOnly: true });
|
|
952
|
+
}
|
|
953
|
+
const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
|
|
954
|
+
const store = getOperationalStore(target);
|
|
955
|
+
// Prepared mutations retain their existing read-set/CAS and staged-overlay semantics.
|
|
956
|
+
if (!store?.recognizes(relPath) || !relPath.endsWith("/events.ndjson") || store.hasPreparedMutations()) {
|
|
957
|
+
return callback(await validateEventLedger(target, packageRoot, options));
|
|
958
|
+
}
|
|
959
|
+
const taskId = store.taskId({ taskKey: relPath.split("/")[2] });
|
|
960
|
+
if (!taskId) return callback(await validateEventLedger(target, packageRoot, options));
|
|
961
|
+
if (store.auditSource?.taskId === taskId && store.auditSource.packageRoot === packageRoot && store.auditSource.relPath === relPath) {
|
|
962
|
+
return evaluateCallbackLedgerAudit(target, packageRoot, options, callback, store.auditSource.events, relPath);
|
|
963
|
+
}
|
|
964
|
+
const schema = await readSchema("event", packageRoot);
|
|
965
|
+
const { withDetachedLedgerSnapshot } = await import("../storage/ledger-event-snapshot.js");
|
|
966
|
+
const { withOperationalReadSnapshot } = await import("../storage/unit-of-work.js");
|
|
967
|
+
let completeObservation;
|
|
968
|
+
return withDetachedLedgerSnapshot(store.db, taskId, (events, db) => withOperationalReadSnapshot({ db, target }, async snapshotStore => {
|
|
969
|
+
completeObservation = snapshotStore.beginEventSnapshotObservation(relPath);
|
|
970
|
+
snapshotStore.auditSource = { taskId, packageRoot, relPath, events };
|
|
971
|
+
const filename = db.prepare("PRAGMA database_list").all().find(row => row.name === "main").file;
|
|
972
|
+
const versionStatement = db.prepare("PRAGMA data_version");
|
|
973
|
+
ownedLedgerProofs.set(events, { db, filename, store: snapshotStore, taskId, packageRoot, relPath, semanticValidated: false, fileIdentity: ownedSnapshotFileIdentity(filename),
|
|
974
|
+
versionStatement, version: versionStatement.get().data_version, successes: new Set() });
|
|
975
|
+
try { return await evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath); }
|
|
976
|
+
finally { ownedLedgerProofs.delete(events); }
|
|
977
|
+
}), {
|
|
978
|
+
validate(event, index) { validateStoredEvent(event, schema, `${relPath}[${index}]`); return event; },
|
|
979
|
+
onFullScan: store.captureObservations ? digest => completeObservation(digest) : null,
|
|
980
|
+
// Keep the private schema/index proof valid for every collection read,
|
|
981
|
+
// including a cursor resumed after the caller yielded an event. The guard
|
|
982
|
+
// deliberately uses the full owned snapshot identity check on all
|
|
983
|
+
// platforms; Windows may defer filesystem timestamps, so a cheap-only
|
|
984
|
+
// resume check would expose a raw-tampered payload before the outer audit
|
|
985
|
+
// boundary noticed it.
|
|
986
|
+
guard(_events, _phase) { assertOwnedLedgerUnchanged(_events); },
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
782
990
|
function validateContractBootstrapRepairLedger(events, errors, options) {
|
|
783
991
|
const allowUnmigrated = options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true;
|
|
784
992
|
validateContractBootstrapRepairMigrations(events, errors, {
|
|
@@ -789,16 +997,36 @@ function validateContractBootstrapRepairLedger(events, errors, options) {
|
|
|
789
997
|
});
|
|
790
998
|
}
|
|
791
999
|
|
|
1000
|
+
function summarizeStateEvents(state, events) {
|
|
1001
|
+
const milestones = ["EXECUTION_STARTED", "VERIFICATION_STARTED", "REVIEW_STARTED", "COMPLETION_VALIDATED"];
|
|
1002
|
+
const observed = new Set();
|
|
1003
|
+
let supportsReviewEvents = false;
|
|
1004
|
+
let latestVerification;
|
|
1005
|
+
let latestReview;
|
|
1006
|
+
let index = 0;
|
|
1007
|
+
for (const event of ledgerEventsOfTypes(events, milestones)) {
|
|
1008
|
+
if (event.taskId !== state.taskId) continue;
|
|
1009
|
+
observed.add(event.event);
|
|
1010
|
+
if (event.event === "VERIFICATION_STARTED") {
|
|
1011
|
+
latestVerification = { event, index };
|
|
1012
|
+
if (Number.isInteger(event.details?.verificationCycle)) supportsReviewEvents = true;
|
|
1013
|
+
}
|
|
1014
|
+
if (event.event === "REVIEW_STARTED") {
|
|
1015
|
+
supportsReviewEvents = true;
|
|
1016
|
+
latestReview = { event, index };
|
|
1017
|
+
}
|
|
1018
|
+
index += 1;
|
|
1019
|
+
}
|
|
1020
|
+
return { observed, supportsReviewEvents, latestVerification, latestReview };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
792
1023
|
export function validateStateLedgerCoherence(state, events) {
|
|
793
1024
|
const errors = [
|
|
794
1025
|
...validateCheckpointRevalidationCurrentBinding(state, events),
|
|
795
1026
|
...validateContractRevisionCurrentBinding(state, events),
|
|
796
1027
|
];
|
|
797
1028
|
if (!Number.isInteger(state.verificationCycle)) return errors;
|
|
798
|
-
const
|
|
799
|
-
const observed = new Set(taskEvents.map((event) => event.event));
|
|
800
|
-
const supportsReviewEvents = taskEvents.some((event) => event.event === "REVIEW_STARTED"
|
|
801
|
-
|| (event.event === "VERIFICATION_STARTED" && Number.isInteger(event.details?.verificationCycle)));
|
|
1029
|
+
const { observed, supportsReviewEvents, latestVerification, latestReview } = summarizeStateEvents(state, events);
|
|
802
1030
|
const phaseRequirements = {
|
|
803
1031
|
EXECUTING: ["EXECUTION_STARTED"],
|
|
804
1032
|
VERIFYING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
|
|
@@ -816,21 +1044,12 @@ export function validateStateLedgerCoherence(state, events) {
|
|
|
816
1044
|
}
|
|
817
1045
|
}
|
|
818
1046
|
if (Number.isInteger(state.verificationCycle)) {
|
|
819
|
-
|
|
820
|
-
.map((event, index) => ({ event, index }))
|
|
821
|
-
.filter(({ event }) => event.event === "VERIFICATION_STARTED");
|
|
822
|
-
const cycles = verificationEvents.map(({ event }) => event.details?.verificationCycle ?? 1);
|
|
823
|
-
if (cycles.at(-1) !== state.verificationCycle) {
|
|
1047
|
+
if ((latestVerification ? latestVerification.event.details?.verificationCycle ?? 1 : undefined) !== state.verificationCycle) {
|
|
824
1048
|
errors.push({
|
|
825
1049
|
code: "E_STATE_LEDGER_DIVERGENCE",
|
|
826
1050
|
message: "Work-state verification cycle does not match the lifecycle ledger",
|
|
827
1051
|
});
|
|
828
1052
|
}
|
|
829
|
-
const latestVerification = verificationEvents.at(-1);
|
|
830
|
-
const latestReview = taskEvents
|
|
831
|
-
.map((event, index) => ({ event, index }))
|
|
832
|
-
.filter(({ event }) => event.event === "REVIEW_STARTED")
|
|
833
|
-
.at(-1);
|
|
834
1053
|
const latestReviewCycle = latestReview?.event.details?.verificationCycle;
|
|
835
1054
|
if (state.phase === "VERIFYING"
|
|
836
1055
|
&& latestReviewCycle === state.verificationCycle
|
|
@@ -60,6 +60,7 @@ async function load(loadArtifact, code, message, artifacts, errors) {
|
|
|
60
60
|
try {
|
|
61
61
|
return await loadArtifact();
|
|
62
62
|
} catch (error) {
|
|
63
|
+
if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
63
64
|
errors.push(issue(code, `${message}: ${error.message}`, artifacts));
|
|
64
65
|
return null;
|
|
65
66
|
}
|
|
@@ -146,6 +147,10 @@ function prerequisiteLedgerErrors(ledger, taskId, preflight, route) {
|
|
|
146
147
|
return errors;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function prerequisiteArtifactPath(explicitPath, taskId, kind) {
|
|
151
|
+
return explicitPath ?? (taskId ? taskArtifactPath(taskId, kind) : ARTIFACT_PATHS[kind]);
|
|
152
|
+
}
|
|
153
|
+
|
|
149
154
|
export async function evaluateStartExecutionPrerequisites({
|
|
150
155
|
target,
|
|
151
156
|
state,
|
|
@@ -156,13 +161,14 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
156
161
|
statePath = null,
|
|
157
162
|
preflightPath = null,
|
|
158
163
|
eventsPath = null,
|
|
164
|
+
readers = null,
|
|
159
165
|
} = {}) {
|
|
160
166
|
const effectiveTaskId = taskId ?? null;
|
|
161
|
-
const contractRel = contractPath
|
|
162
|
-
const routeRel = routePath
|
|
163
|
-
const stateRel = statePath
|
|
164
|
-
const preflightRel = preflightPath
|
|
165
|
-
const eventsRel = eventsPath
|
|
167
|
+
const contractRel = prerequisiteArtifactPath(contractPath, effectiveTaskId, "contract");
|
|
168
|
+
const routeRel = prerequisiteArtifactPath(routePath, effectiveTaskId, "route");
|
|
169
|
+
const stateRel = prerequisiteArtifactPath(statePath, effectiveTaskId, "state");
|
|
170
|
+
const preflightRel = prerequisiteArtifactPath(preflightPath, effectiveTaskId, "preflight");
|
|
171
|
+
const eventsRel = prerequisiteArtifactPath(eventsPath, effectiveTaskId, "events");
|
|
166
172
|
|
|
167
173
|
const errors = [];
|
|
168
174
|
const requiredArtifacts = [
|
|
@@ -180,14 +186,14 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
const contract = await load(
|
|
183
|
-
() => readContract(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
|
|
189
|
+
() => (readers?.readContract ?? readContract)(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
|
|
184
190
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
185
191
|
`EXECUTING requires ${contractRel}`,
|
|
186
192
|
[contractRel],
|
|
187
193
|
errors,
|
|
188
194
|
);
|
|
189
195
|
const route = await load(
|
|
190
|
-
() => readPersistedRoute(target, packageRoot, { taskId: effectiveTaskId, routePath }),
|
|
196
|
+
() => (readers?.readRoute ?? readPersistedRoute)(target, packageRoot, { taskId: effectiveTaskId, routePath }),
|
|
191
197
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
192
198
|
`EXECUTING requires ${routeRel}`,
|
|
193
199
|
[routeRel],
|
|
@@ -204,21 +210,24 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
204
210
|
}
|
|
205
211
|
errors.push(...stateIdentityErrors({ contract, route, state }));
|
|
206
212
|
|
|
207
|
-
const freshness = await classifyLoadedWorkState({
|
|
213
|
+
const freshness = await (readers?.classifyState ?? classifyLoadedWorkState)({
|
|
208
214
|
target,
|
|
209
215
|
state,
|
|
210
216
|
contractFile: contractRel,
|
|
211
217
|
});
|
|
212
218
|
errors.push(...freshnessErrors(state, freshness));
|
|
213
219
|
|
|
214
|
-
const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath });
|
|
220
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath, readers });
|
|
215
221
|
let persistedPreflight = null;
|
|
222
|
+
let persistedPreflightReadError = null;
|
|
216
223
|
try {
|
|
217
|
-
persistedPreflight = await readJsonArtifact(target, preflightRel, "preflight", packageRoot);
|
|
218
|
-
} catch {
|
|
224
|
+
persistedPreflight = await (readers?.readPreflight ?? readJsonArtifact)(target, preflightRel, "preflight", packageRoot);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
227
|
+
persistedPreflightReadError = error;
|
|
219
228
|
// validatePersistedPreflight reports the stable, actionable preflight reason.
|
|
220
229
|
}
|
|
221
|
-
const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight);
|
|
230
|
+
const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight, persistedPreflightReadError);
|
|
222
231
|
if (!sameStringSet(state.requiredGates, preflight.requiredGates)
|
|
223
232
|
|| !sameStringSet(state.satisfiedGates, preflight.satisfiedGates)) {
|
|
224
233
|
errors.push(issue(
|
|
@@ -228,7 +237,7 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
228
237
|
));
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
const ledger = await validateEventLedger(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
|
|
240
|
+
const ledger = await (readers?.validateLedger ?? validateEventLedger)(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
|
|
232
241
|
errors.push(...prerequisiteLedgerErrors(ledger, contract.value.taskId, preflight, route));
|
|
233
242
|
errors.push(...validateStateLedgerCoherence(state, ledger.events).map((error) => issue(
|
|
234
243
|
error.code,
|