@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/preflight.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ARTIFACT_PATHS, writeJsonArtifact } from "./artifacts.js";
|
|
2
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
2
3
|
import { readContract } from "./contract.js";
|
|
3
4
|
import { assertRouteInvariants } from "./router.js";
|
|
4
5
|
import { validateProfileSources } from "./profile.js";
|
|
@@ -35,13 +36,17 @@ const PREVIEW_DECISION_MAX_LENGTH = 240;
|
|
|
35
36
|
|
|
36
37
|
export { validatePersistedPreflight } from "./preflight-model.js";
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
function preflightArtifactPath(explicitPath, taskId, kind) {
|
|
40
|
+
return explicitPath ?? (taskId ? taskArtifactPath(taskId, kind) : ARTIFACT_PATHS[kind]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function evaluatePreflight({ target, packageRoot, strict = false, taskId = null, contractPath = null, routePath = null, statePath = null, readers = null } = {}) {
|
|
39
44
|
const errors = [];
|
|
40
45
|
const profile = await readProfile(target);
|
|
41
46
|
const profileProvenance = await validateProfileSources(target, packageRoot);
|
|
42
47
|
errors.push(...(profileProvenance.errors ?? []));
|
|
43
|
-
const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
|
|
44
|
-
const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
|
|
48
|
+
const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath, readers });
|
|
49
|
+
const route = await loadRoute(target, packageRoot, errors, { taskId, routePath, readers });
|
|
45
50
|
const config = await optionalConfig(target, packageRoot, errors);
|
|
46
51
|
if (config.structuralQuality !== undefined) {
|
|
47
52
|
try {
|
|
@@ -59,9 +64,9 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
59
64
|
errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
const contractRelPath = contractPath
|
|
63
|
-
const routeRelPath = routePath
|
|
64
|
-
const stateRelPath = statePath
|
|
67
|
+
const contractRelPath = preflightArtifactPath(contractPath, taskId, "contract");
|
|
68
|
+
const routeRelPath = preflightArtifactPath(routePath, taskId, "route");
|
|
69
|
+
const stateRelPath = preflightArtifactPath(statePath, taskId, "state");
|
|
65
70
|
|
|
66
71
|
const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
|
|
67
72
|
if (unresolvedDecisions.length > 0) {
|
|
@@ -88,11 +93,11 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const sources = await loadSources(target, contract, packageRoot, errors);
|
|
91
|
-
const gates = await inspectGates(target, contract, route, packageRoot, errors, config, { taskId });
|
|
96
|
+
const gates = await inspectGates(target, contract, route, packageRoot, errors, config, { taskId, readers });
|
|
92
97
|
|
|
93
98
|
let state = null;
|
|
94
99
|
try {
|
|
95
|
-
state = await readWorkState(target, { packageRoot, taskId, statePath });
|
|
100
|
+
state = await (readers?.readState ?? readWorkState)(target, { packageRoot, taskId, statePath });
|
|
96
101
|
} catch (error) {
|
|
97
102
|
errors.push(issue("E_STATE_INVALID", error.message, [stateRelPath]));
|
|
98
103
|
}
|
|
@@ -135,7 +140,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
|
|
|
135
140
|
reasonCodes: [],
|
|
136
141
|
next: config.structuralQuality?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
|
|
137
142
|
};
|
|
138
|
-
if (effectiveTaskId !== "unknown") {
|
|
143
|
+
if (effectiveTaskId !== "unknown" && (!readers || structuralQuality.mode !== "off")) {
|
|
139
144
|
try {
|
|
140
145
|
const { projectStructuralQualityStatus } = await import("./structural-quality/service.js");
|
|
141
146
|
structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId: effectiveTaskId });
|
|
@@ -183,7 +188,19 @@ export async function validateReadyProtocolConsistency(options = {}) {
|
|
|
183
188
|
});
|
|
184
189
|
}
|
|
185
190
|
|
|
186
|
-
export async function runPreflight({
|
|
191
|
+
export async function runPreflight(options = {}) {
|
|
192
|
+
if (options.persist === false) return runPreflightInTransaction(options);
|
|
193
|
+
let taskId = options.taskId;
|
|
194
|
+
if (!taskId) {
|
|
195
|
+
const { target, packageRoot, strict, contractPath, routePath, statePath } = options;
|
|
196
|
+
const result = await evaluatePreflight({ target, packageRoot, strict, contractPath, routePath, statePath });
|
|
197
|
+
if (result.taskId !== "unknown") taskId = result.taskId;
|
|
198
|
+
}
|
|
199
|
+
if (!taskId) return runPreflightInTransaction(options);
|
|
200
|
+
return withTaskTransaction({ target: options.target, taskId, packageRoot: options.packageRoot, operation: "preflight" }, () => runPreflightInTransaction({ ...options, taskId }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function runPreflightInTransaction({
|
|
187
204
|
target,
|
|
188
205
|
packageRoot,
|
|
189
206
|
strict = false,
|
|
@@ -198,7 +215,7 @@ export async function runPreflight({
|
|
|
198
215
|
let result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
|
|
199
216
|
if (!persist) return result;
|
|
200
217
|
|
|
201
|
-
let ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
|
|
218
|
+
let ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, contractPath, routePath, statePath, eventsPath });
|
|
202
219
|
assertPreflightResultPersistenceSafety(result);
|
|
203
220
|
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
204
221
|
|
|
@@ -224,7 +241,7 @@ export async function runPreflight({
|
|
|
224
241
|
result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
|
|
225
242
|
assertPreflightResultPersistenceSafety(result);
|
|
226
243
|
assertExistingReadyLifecycleCompatibility(ledger, result);
|
|
227
|
-
ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
|
|
244
|
+
ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, contractPath, routePath, statePath, eventsPath });
|
|
228
245
|
}
|
|
229
246
|
}
|
|
230
247
|
|
package/src/core/progress.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isLedgerEventCollection, ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
2
|
+
import { latestDiagnosisForTask } from "./diagnosis-model.js";
|
|
2
3
|
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
evaluateStructuredDiagnosticStall,
|
|
5
|
+
summarizeInformationGain,
|
|
6
6
|
} from "./information-gain-projection.js";
|
|
7
7
|
|
|
8
8
|
export const PROGRESS_STATUS = Object.freeze({
|
|
@@ -27,6 +27,17 @@ export function diagnosisRequirements(diagnosis, checksById) {
|
|
|
27
27
|
)].sort();
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function progressTaskEvents(events, taskId) {
|
|
31
|
+
if (Array.isArray(events)) return events.filter(event => !taskId || event.taskId === taskId);
|
|
32
|
+
return isLedgerEventCollection(events) ? events : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function* progressVerificationEvents(events, taskId) {
|
|
36
|
+
for (const event of ledgerEventsOfTypes(events, ["VERIFICATION_RECORDED"])) {
|
|
37
|
+
if (!taskId || event.taskId === taskId) yield event;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
export function evaluateProgress({ state, events = [] } = {}) {
|
|
31
42
|
const signals = [];
|
|
32
43
|
let status = PROGRESS_STATUS.ADVANCING;
|
|
@@ -35,10 +46,10 @@ export function evaluateProgress({ state, events = [] } = {}) {
|
|
|
35
46
|
return { status, signals };
|
|
36
47
|
}
|
|
37
48
|
|
|
38
|
-
const taskEvents =
|
|
39
|
-
const
|
|
49
|
+
const taskEvents = progressTaskEvents(events, state.taskId);
|
|
50
|
+
const latestLegacy = latestDiagnosisForTask(taskEvents, state.taskId);
|
|
40
51
|
const resolvedDiagnostic = resolveCurrentCycleDiagnostic(taskEvents, state.taskId, state.verificationCycle ?? null);
|
|
41
|
-
const latestDiag = resolvedDiagnostic?.details ??
|
|
52
|
+
const latestDiag = resolvedDiagnostic?.details ?? latestLegacy?.details ?? null;
|
|
42
53
|
|
|
43
54
|
let latestGainClassification = latestDiag?.informationGain ?? null;
|
|
44
55
|
let structuredStall = null;
|
|
@@ -49,8 +60,8 @@ export function evaluateProgress({ state, events = [] } = {}) {
|
|
|
49
60
|
)];
|
|
50
61
|
}
|
|
51
62
|
// Canonical structured-stall truth; compatibility classification is presentation only.
|
|
52
|
-
structuredStall =
|
|
53
|
-
|
|
63
|
+
structuredStall = summarizeInformationGain(
|
|
64
|
+
taskEvents, state.taskId,
|
|
54
65
|
{ verificationCycle: state.verificationCycle ?? null },
|
|
55
66
|
);
|
|
56
67
|
latestDiag.effectiveInformationGain = !(structuredStall.stalled);
|
|
@@ -63,7 +74,7 @@ export function evaluateProgress({ state, events = [] } = {}) {
|
|
|
63
74
|
if (check.id) checksById.set(check.id, check);
|
|
64
75
|
if (check.checkId) checksById.set(check.checkId, check);
|
|
65
76
|
}
|
|
66
|
-
for (const event of taskEvents) {
|
|
77
|
+
for (const event of progressVerificationEvents(taskEvents, state.taskId)) {
|
|
67
78
|
if (event.event === "VERIFICATION_RECORDED" && event.details) {
|
|
68
79
|
if (event.details.id) checksById.set(event.details.id, event.details);
|
|
69
80
|
if (event.details.checkId) checksById.set(event.details.checkId, event.details);
|
|
@@ -108,7 +119,7 @@ export function evaluateProgress({ state, events = [] } = {}) {
|
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
// Also extract from VERIFICATION_RECORDED events
|
|
111
|
-
for (const event of taskEvents) {
|
|
122
|
+
for (const event of progressVerificationEvents(taskEvents, state.taskId)) {
|
|
112
123
|
if (event.event === "VERIFICATION_RECORDED") {
|
|
113
124
|
const details = event.details ?? {};
|
|
114
125
|
if (details.status === "failed" || details.status === "blocked") {
|
|
@@ -7,6 +7,8 @@ import { VERIFICATION_ISOLATION_MODES } from "./verification-execution.js";
|
|
|
7
7
|
import { PROVIDER_KINDS } from "../providers/capabilities.js";
|
|
8
8
|
import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "./decision/constants.js";
|
|
9
9
|
|
|
10
|
+
import { getStorageCapabilities } from "../storage/capabilities.js";
|
|
11
|
+
|
|
10
12
|
export const SCHEMA_COMPATIBILITY_POLICY = Object.freeze({
|
|
11
13
|
protocolVersion: PROTOCOL_VERSION,
|
|
12
14
|
schemaVersion: 1,
|
|
@@ -38,6 +40,7 @@ export function protocolInfo({ packageVersion = null } = {}) {
|
|
|
38
40
|
writesSchemaVersions: schemaVersions,
|
|
39
41
|
compatibility: SCHEMA_COMPATIBILITY_POLICY,
|
|
40
42
|
features: {
|
|
43
|
+
operationalStorage: getStorageCapabilities(),
|
|
41
44
|
semanticDecisionPlane: {
|
|
42
45
|
version: 1,
|
|
43
46
|
supported: true,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED } from "./error-codes.js";
|
|
2
2
|
import { PROTOCOL_VERSION } from "./protocol.js";
|
|
3
|
-
import { detectLegacySingletonLayout
|
|
3
|
+
import { detectLegacySingletonLayout } from "./task-migration.js";
|
|
4
|
+
import { migrateLegacyTaskStorage } from "./task-storage-migration.js";
|
|
4
5
|
import { getPackageRoot } from "./templates.js";
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
+
export function assertSupportedProtocolMigrationTarget(to) {
|
|
7
8
|
if (to === undefined || to === null || to === "") {
|
|
8
9
|
const error = new Error("migrate-protocol requires --to <protocolVersion>");
|
|
9
10
|
error.code = E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED;
|
|
@@ -26,9 +27,9 @@ function assertSupportedTarget(to) {
|
|
|
26
27
|
*/
|
|
27
28
|
export async function migrateProtocol(
|
|
28
29
|
target,
|
|
29
|
-
{ to, dryRun = false, packageRoot = getPackageRoot() } = {},
|
|
30
|
+
{ to, dryRun = false, packageRoot = getPackageRoot(), destination, writersQuiesced = false } = {},
|
|
30
31
|
) {
|
|
31
|
-
|
|
32
|
+
assertSupportedProtocolMigrationTarget(to);
|
|
32
33
|
const legacy = await detectLegacySingletonLayout(target);
|
|
33
34
|
|
|
34
35
|
if (!legacy.hasLegacy) {
|
|
@@ -43,7 +44,7 @@ export async function migrateProtocol(
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const result = await
|
|
47
|
+
const result = await migrateLegacyTaskStorage(target, { dryRun, packageRoot, destination, writersQuiesced });
|
|
47
48
|
return {
|
|
48
49
|
...result,
|
|
49
50
|
fromProtocol: PROTOCOL_VERSION,
|
|
@@ -52,7 +53,7 @@ export async function migrateProtocol(
|
|
|
52
53
|
actions: [{
|
|
53
54
|
kind: "LEGACY_LAYOUT_MIGRATION",
|
|
54
55
|
command: "task-migrate",
|
|
55
|
-
receipt: "
|
|
56
|
+
receipt: result.migrationReceipt ?? "<destination>/publication/publication-journal.json",
|
|
56
57
|
artifacts: legacy.legacyFiles.map((item) => item.path),
|
|
57
58
|
}],
|
|
58
59
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withTaskTransaction } from "./transaction.js";
|
|
1
2
|
import { readContract } from "./contract.js";
|
|
2
3
|
import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
4
|
import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
|
|
@@ -7,12 +8,74 @@ import { createReceipt } from "./receipt.js";
|
|
|
7
8
|
import { currentRepositoryFingerprint } from "./repository.js";
|
|
8
9
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
10
|
import { resolveTaskClaimState } from "./task-claim-state.js";
|
|
11
|
+
import { DIAGNOSTIC_SCHEMA_VERSION, diagnosticSemanticFingerprint } from "./diagnostic-model.js";
|
|
10
12
|
import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
|
|
11
13
|
import { classifyRequirement } from "./evidence-readiness.js";
|
|
14
|
+
import { assertDiagnosingToCorrectingTransition } from "./phase.js";
|
|
15
|
+
import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
|
|
12
16
|
|
|
13
17
|
export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Build the schema-valid rebound diagnostic case for a drifted DIAGNOSING task
|
|
21
|
+
* that entered the phase before its cycle recorded any diagnosis. The case
|
|
22
|
+
* binds every observation to the just-passed contract-bound evidence execution
|
|
23
|
+
* so the diagnosis carries explicit provenance instead of being manufactured
|
|
24
|
+
* by recovery: the hypothesis is the recovery statement, settled by the
|
|
25
|
+
* successful evidence, with the next safe action being the canonical advance.
|
|
26
|
+
*/
|
|
27
|
+
function reconcileReboundDiagnosticCase({
|
|
28
|
+
taskId,
|
|
29
|
+
verificationCycle,
|
|
30
|
+
checkId,
|
|
31
|
+
requirement,
|
|
32
|
+
command,
|
|
33
|
+
executionId,
|
|
34
|
+
repositoryHead,
|
|
35
|
+
}) {
|
|
36
|
+
const statementLimit = 2000;
|
|
37
|
+
const observations = [{
|
|
38
|
+
id: "obs-reconcile-evidence",
|
|
39
|
+
kind: "CHECK_RESULT",
|
|
40
|
+
evidenceRef: checkId,
|
|
41
|
+
provenance: "FORGELOOP_EXECUTED",
|
|
42
|
+
statement: `Contract-bound evidence ${checkId} passed in the current repository (${repositoryHead}); execution ${executionId}: ${command}`.slice(0, statementLimit),
|
|
43
|
+
}];
|
|
44
|
+
const hypotheses = [{
|
|
45
|
+
id: "h-reconcile-rebind",
|
|
46
|
+
statement: `Repository moved after the task entered DIAGNOSING but before the cycle recorded any diagnosis; cycle ${verificationCycle} diagnosis is rebound from the passed contract-bound evidence requirement "${requirement}".`.slice(0, statementLimit),
|
|
47
|
+
contributorRefs: [],
|
|
48
|
+
evidenceRefs: [checkId],
|
|
49
|
+
settledBy: { type: "CHECK_STATUS", checkId, expectedStatus: "passed" },
|
|
50
|
+
status: "OPEN",
|
|
51
|
+
}];
|
|
52
|
+
const details = {
|
|
53
|
+
schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
|
|
54
|
+
verificationCycle,
|
|
55
|
+
diagnosticRevision: 1,
|
|
56
|
+
failureClass: "VERIFICATION_FAILURE",
|
|
57
|
+
observations,
|
|
58
|
+
contributors: [],
|
|
59
|
+
hypotheses,
|
|
60
|
+
nextSafeAction: {
|
|
61
|
+
statement: "Advance DIAGNOSING -> CORRECTING -> VERIFYING and continue the canonical verification pipeline in the reconciled repository.".slice(0, statementLimit),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
details.diagnosticFingerprint = diagnosticSemanticFingerprint({
|
|
65
|
+
verificationCycle: details.verificationCycle,
|
|
66
|
+
failureClass: details.failureClass,
|
|
67
|
+
case_: details,
|
|
68
|
+
});
|
|
69
|
+
details.previousDiagnosticFingerprint = null;
|
|
70
|
+
return details;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// DIAGNOSING is reconcilable under the same diagnosis-boundary condition as
|
|
74
|
+
// CORRECTING: the active cycle must already carry an append-only diagnosis.
|
|
75
|
+
// Without it, drift entering DIAGNOSING before the cycle's first diagnosis is
|
|
76
|
+
// unrecoverable (reconcile-closure, task-recover, handoff, and every mutation
|
|
77
|
+
// all refuse), which stranded real drifted tasks on this exact edge.
|
|
78
|
+
const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING"]);
|
|
16
79
|
|
|
17
80
|
function reconcileError(code, message, artifacts = []) {
|
|
18
81
|
const error = new Error(message);
|
|
@@ -100,6 +163,23 @@ async function validateReconciliationCheckpoint({
|
|
|
100
163
|
);
|
|
101
164
|
}
|
|
102
165
|
|
|
166
|
+
await assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel });
|
|
167
|
+
|
|
168
|
+
const reboundOrState = await rebindReconciliationState(target, {
|
|
169
|
+
packageRoot,
|
|
170
|
+
taskId,
|
|
171
|
+
state,
|
|
172
|
+
ledger,
|
|
173
|
+
authorityContext,
|
|
174
|
+
runtimeContext,
|
|
175
|
+
stateRel,
|
|
176
|
+
receiptRel,
|
|
177
|
+
});
|
|
178
|
+
return reboundOrState;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Fail closed unless the task claim is valid, mutable, and ACTIVE in this checkout. */
|
|
182
|
+
async function assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel }) {
|
|
103
183
|
const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
|
|
104
184
|
if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
|
|
105
185
|
const first = ownership.ownershipErrors?.[0] ?? ownership.errors?.[0] ?? {};
|
|
@@ -109,18 +189,46 @@ async function validateReconciliationCheckpoint({
|
|
|
109
189
|
[stateRel, eventsRel],
|
|
110
190
|
);
|
|
111
191
|
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Handle the phase-bound diagnosis rebind gates for CORRECTING/DIAGNOSING and the REVIEWING recovery authorization.
|
|
195
|
+
* Returns the possibly-rebound state (or the original state when no rebind applies). */
|
|
196
|
+
async function rebindReconciliationState(target, {
|
|
197
|
+
packageRoot,
|
|
198
|
+
taskId,
|
|
199
|
+
state,
|
|
200
|
+
ledger,
|
|
201
|
+
authorityContext,
|
|
202
|
+
runtimeContext,
|
|
203
|
+
stateRel,
|
|
204
|
+
receiptRel,
|
|
205
|
+
}) {
|
|
206
|
+
if (state.phase === "CORRECTING" || state.phase === "DIAGNOSING") {
|
|
207
|
+
// The diagnosis boundary for CORRECTING stays absolute: recovery can never
|
|
208
|
+
// manufacture a diagnosis or authorize a retry the ordinary correction
|
|
209
|
+
// transition would refuse. A drifted DIAGNOSING task without a cycle
|
|
210
|
+
// diagnosis is the one legal exception: the drift struck between entering
|
|
211
|
+
// DIAGNOSING and recording the cycle's first diagnosis, and no other
|
|
212
|
+
// command can create either the diagnosis or the fresh checkpoint. Such a
|
|
213
|
+
// task reconciles only after contract-bound evidence passes, and the
|
|
214
|
+
// reconcile itself records the cycle diagnosis below (provenance bound to
|
|
215
|
+
// the passed evidence execution).
|
|
216
|
+
const cycleDiagnosis = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, state.verificationCycle ?? 1);
|
|
217
|
+
if (state.phase === "DIAGNOSING" && !cycleDiagnosis) {
|
|
218
|
+
return { ...state, undiagnosedDiagnosingRebind: true };
|
|
219
|
+
}
|
|
220
|
+
assertDiagnosingToCorrectingTransition({
|
|
221
|
+
state,
|
|
222
|
+
events: ledger.events,
|
|
223
|
+
resolveDiagnosis: resolveCurrentCycleDiagnostic,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
112
226
|
|
|
113
227
|
if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) {
|
|
114
228
|
return state;
|
|
115
229
|
}
|
|
116
230
|
|
|
117
|
-
const recovery = await authorizeCompletionRecoveryOrRebind({
|
|
118
|
-
target,
|
|
119
|
-
packageRoot,
|
|
120
|
-
taskId,
|
|
121
|
-
authorityContext,
|
|
122
|
-
runtimeContext,
|
|
123
|
-
});
|
|
231
|
+
const recovery = await authorizeCompletionRecoveryOrRebind({ target, packageRoot, taskId, authorityContext, runtimeContext });
|
|
124
232
|
if (!recovery.recoveryAuth.authorized) {
|
|
125
233
|
const first = recovery.recoveryAuth.errors?.[0] ?? {};
|
|
126
234
|
throw reconcileError(
|
|
@@ -133,12 +241,12 @@ async function validateReconciliationCheckpoint({
|
|
|
133
241
|
}
|
|
134
242
|
|
|
135
243
|
/**
|
|
136
|
-
* Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
|
|
244
|
+
* Canonical recovery for an EXECUTING, VERIFYING, CORRECTING, or REVIEWING task whose
|
|
137
245
|
* objective is already satisfied in the current repository but whose
|
|
138
246
|
* work-state checkpoint is stale because the repository fingerprint moved.
|
|
139
247
|
*
|
|
140
248
|
* The command refreshes the checkpoint repository fingerprint only after:
|
|
141
|
-
* - the task is EXECUTING, VERIFYING, or REVIEWING (with either a
|
|
249
|
+
* - the task is EXECUTING, VERIFYING, CORRECTING, or REVIEWING (with either a
|
|
142
250
|
* persisted completion rejection or the narrow repository-only bootstrap
|
|
143
251
|
* path),
|
|
144
252
|
* - classification requires revalidation and the only drift is
|
|
@@ -152,7 +260,14 @@ async function validateReconciliationCheckpoint({
|
|
|
152
260
|
* VERIFYING, prepare-completion, record-check, complete); claims are
|
|
153
261
|
* released only by canonical COMPLETE.
|
|
154
262
|
*/
|
|
155
|
-
export async function runReconcileClosure({
|
|
263
|
+
export async function runReconcileClosure(options = {}) {
|
|
264
|
+
if (typeof options.taskId !== "string" || !options.taskId.trim()) return reconcileSelectedClosure(options);
|
|
265
|
+
return withTaskTransaction({ target: options.target, packageRoot: options.packageRoot,
|
|
266
|
+
taskId: options.taskId, operation: "reconcile-closure", recordCommitEvent: true },
|
|
267
|
+
() => reconcileSelectedClosure(options));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function reconcileSelectedClosure({
|
|
156
271
|
target,
|
|
157
272
|
packageRoot,
|
|
158
273
|
taskId,
|
|
@@ -163,18 +278,7 @@ export async function runReconcileClosure({
|
|
|
163
278
|
authorityContext,
|
|
164
279
|
runtimeContext,
|
|
165
280
|
} = {}) {
|
|
166
|
-
|
|
167
|
-
throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
|
|
168
|
-
}
|
|
169
|
-
if (typeof checkId !== "string" || !checkId.trim()) {
|
|
170
|
-
throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
|
|
171
|
-
}
|
|
172
|
-
if (typeof requirement !== "string" || !requirement.trim()) {
|
|
173
|
-
throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
|
|
174
|
-
}
|
|
175
|
-
if (!Array.isArray(argv) || argv.length === 0) {
|
|
176
|
-
throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
|
|
177
|
-
}
|
|
281
|
+
requireReconcileInputs({ taskId, checkId, requirement, argv });
|
|
178
282
|
|
|
179
283
|
const stateRel = taskArtifactPath(taskId, "state");
|
|
180
284
|
const contractRel = taskArtifactPath(taskId, "contract");
|
|
@@ -188,7 +292,7 @@ export async function runReconcileClosure({
|
|
|
188
292
|
if (!RECONCILABLE_PHASES.has(state.phase)) {
|
|
189
293
|
throw reconcileError(
|
|
190
294
|
"E_RECONCILE_PHASE_INVALID",
|
|
191
|
-
`reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
|
|
295
|
+
`reconcile-closure supports EXECUTING, VERIFYING, DIAGNOSING, CORRECTING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
|
|
192
296
|
[stateRel],
|
|
193
297
|
);
|
|
194
298
|
}
|
|
@@ -245,6 +349,24 @@ export async function runReconcileClosure({
|
|
|
245
349
|
const repository = await currentRepositoryFingerprint(target);
|
|
246
350
|
const previous = state.repositoryFingerprint;
|
|
247
351
|
|
|
352
|
+
const rebindDiagnosis = Boolean(state.undiagnosedDiagnosingRebind);
|
|
353
|
+
if (rebindDiagnosis) {
|
|
354
|
+
delete state.undiagnosedDiagnosingRebind;
|
|
355
|
+
await appendProtocolEvent(target, {
|
|
356
|
+
taskId,
|
|
357
|
+
event: "DIAGNOSTIC_CASE_RECORDED",
|
|
358
|
+
details: reconcileReboundDiagnosticCase({
|
|
359
|
+
taskId,
|
|
360
|
+
verificationCycle: state.verificationCycle ?? 1,
|
|
361
|
+
checkId,
|
|
362
|
+
requirement,
|
|
363
|
+
command: argv.join(" "),
|
|
364
|
+
executionId: execution.execution.executionId,
|
|
365
|
+
repositoryHead: repository.head,
|
|
366
|
+
}),
|
|
367
|
+
}, packageRoot, { taskId });
|
|
368
|
+
}
|
|
369
|
+
|
|
248
370
|
await appendProtocolEvent(target, {
|
|
249
371
|
taskId,
|
|
250
372
|
event: RECONCILE_EVENT,
|
|
@@ -295,3 +417,19 @@ export async function runReconcileClosure({
|
|
|
295
417
|
event: RECONCILE_EVENT,
|
|
296
418
|
};
|
|
297
419
|
}
|
|
420
|
+
|
|
421
|
+
/** Validate the reconcile arguments exactly as before; shared by the phase-independent input gate. */
|
|
422
|
+
function requireReconcileInputs({ taskId, checkId, requirement, argv } = {}) {
|
|
423
|
+
if (typeof taskId !== "string" || !taskId.trim()) {
|
|
424
|
+
throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
|
|
425
|
+
}
|
|
426
|
+
if (typeof checkId !== "string" || !checkId.trim()) {
|
|
427
|
+
throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
|
|
428
|
+
}
|
|
429
|
+
if (typeof requirement !== "string" || !requirement.trim()) {
|
|
430
|
+
throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
|
|
431
|
+
}
|
|
432
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
433
|
+
throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
2
|
+
import { ledgerRelationSet } from "./ledger-relations.js";
|
|
1
3
|
import {
|
|
2
4
|
LEGACY_RECOVERY_MIGRATION_EVENT,
|
|
3
5
|
isLegacyRecoveryEventShape,
|
|
@@ -23,56 +25,65 @@ function recoveryIdOf(event) {
|
|
|
23
25
|
: null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
function recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors) {
|
|
29
|
+
// The known legacy defect signature is never an owning recovery cycle by
|
|
30
|
+
// itself. It remains non-owning historical evidence; ownership comes
|
|
31
|
+
// exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
|
|
32
|
+
// which is validated (binding, uniqueness) by the ledger validator.
|
|
33
|
+
if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) return null;
|
|
34
|
+
// A migration event only counts as a canonical recovery cycle when it
|
|
35
|
+
// binds an actual legacy event present earlier in this ledger.
|
|
36
|
+
if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
|
|
37
|
+
&& !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
|
|
38
|
+
&& isLegacyRecoveryEventShape(candidate))) {
|
|
39
|
+
errors.push(recoveryError(
|
|
40
|
+
`Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
|
|
41
|
+
));
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const recoveryId = recoveryIdOf(event);
|
|
45
|
+
if (!recoveryId) {
|
|
46
|
+
errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (seenRecoveryIds.has(recoveryId)) {
|
|
50
|
+
errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (activeCycle) {
|
|
54
|
+
errors.push(recoveryError(
|
|
55
|
+
`Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
|
|
56
|
+
));
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
recoveryId,
|
|
62
|
+
recoveryEventSeq: event.seq,
|
|
63
|
+
resumedEventSeq: null,
|
|
64
|
+
active: true,
|
|
65
|
+
event,
|
|
66
|
+
resumedEvent: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function classifyHistory(events, { retainCycles = true, onRecovery = null } = {}) {
|
|
27
71
|
const recoveries = [];
|
|
28
72
|
const completedRecoveries = [];
|
|
29
73
|
const errors = [];
|
|
30
|
-
const seenRecoveryIds =
|
|
74
|
+
const seenRecoveryIds = ledgerRelationSet();
|
|
31
75
|
let activeCycle = null;
|
|
76
|
+
let completedRecoveryCount = 0;
|
|
32
77
|
|
|
33
|
-
|
|
78
|
+
const relevant = Array.isArray(events) ? events : ledgerEventsOfTypes(events, [...RECOVERY_EVENT_TYPES, "TASK_RECOVERY_RESUMED"]);
|
|
79
|
+
for (const event of relevant) {
|
|
34
80
|
if (RECOVERY_EVENT_TYPES.has(event?.event)) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
// binds an actual legacy event present earlier in this ledger.
|
|
42
|
-
if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
|
|
43
|
-
&& !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
|
|
44
|
-
&& isLegacyRecoveryEventShape(candidate))) {
|
|
45
|
-
errors.push(recoveryError(
|
|
46
|
-
`Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
|
|
47
|
-
));
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const recoveryId = recoveryIdOf(event);
|
|
51
|
-
if (!recoveryId) {
|
|
52
|
-
errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (seenRecoveryIds.has(recoveryId)) {
|
|
56
|
-
errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (activeCycle) {
|
|
60
|
-
errors.push(recoveryError(
|
|
61
|
-
`Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
|
|
62
|
-
));
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
activeCycle = {
|
|
67
|
-
recoveryId,
|
|
68
|
-
recoveryEventSeq: event.seq,
|
|
69
|
-
resumedEventSeq: null,
|
|
70
|
-
active: true,
|
|
71
|
-
event,
|
|
72
|
-
resumedEvent: null,
|
|
73
|
-
};
|
|
74
|
-
seenRecoveryIds.add(recoveryId);
|
|
75
|
-
recoveries.push(activeCycle);
|
|
81
|
+
const candidate = recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors);
|
|
82
|
+
if (!candidate) continue;
|
|
83
|
+
activeCycle = candidate;
|
|
84
|
+
seenRecoveryIds.add(activeCycle.recoveryId);
|
|
85
|
+
if (retainCycles) recoveries.push(activeCycle);
|
|
86
|
+
if (onRecovery) onRecovery(activeCycle);
|
|
76
87
|
continue;
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -95,23 +106,31 @@ export function classifyRecoveryHistory(events = []) {
|
|
|
95
106
|
activeCycle.resumedEventSeq = event.seq;
|
|
96
107
|
activeCycle.resumedEvent = event;
|
|
97
108
|
activeCycle.active = false;
|
|
98
|
-
|
|
109
|
+
completedRecoveryCount += 1;
|
|
110
|
+
if (retainCycles) completedRecoveries.push(activeCycle);
|
|
99
111
|
activeCycle = null;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
const valid = errors.length === 0;
|
|
103
115
|
return {
|
|
104
|
-
recoveries,
|
|
116
|
+
...(retainCycles ? { recoveries } : {}),
|
|
105
117
|
activeRecoveryId: valid ? activeCycle?.recoveryId ?? null : null,
|
|
106
118
|
activeRecovery: valid && activeCycle
|
|
107
119
|
? { recoveryId: activeCycle.recoveryId, event: activeCycle.event }
|
|
108
120
|
: null,
|
|
109
|
-
completedRecoveries,
|
|
121
|
+
...(retainCycles ? { completedRecoveries } : { completedRecoveryCount }),
|
|
110
122
|
valid,
|
|
111
123
|
errors,
|
|
112
124
|
};
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
/** Public evidence keeps its historical arrays. Internal classification can stream claims. */
|
|
128
|
+
export function classifyRecoveryHistory(events = []) { return classifyHistory(events); }
|
|
129
|
+
|
|
130
|
+
export function summarizeRecoveryHistory(events, onRecovery) {
|
|
131
|
+
return classifyHistory(events, { retainCycles: false, onRecovery });
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
export function resolveRecoveryHistory(events = []) {
|
|
116
135
|
return classifyRecoveryHistory(events);
|
|
117
136
|
}
|