@zq-silk/yui 0.13.9 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { selectedActiveSchedulerTasks, isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
2
|
-
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
3
|
-
import { wakeReason } from "./wakeReason.js";
|
|
4
|
-
import { projectTaskExecution } from "./taskExecutionProjection.js";
|
|
5
|
-
import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake, hasDispatchableQueuedResourceLane } from "./actionability.js";
|
|
6
|
-
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
7
|
-
/**
|
|
8
|
-
* Repairs an active Task that has no durable owner capable of advancing it.
|
|
9
|
-
* This is a low-frequency safety net; normal transitions enqueue their own
|
|
10
|
-
* Role mailbox signal in the same transaction.
|
|
11
|
-
*
|
|
12
|
-
* Issue 05: a `task-orphaned` wake is suppressed when the last Leader Run
|
|
13
|
-
* ended waiting/blocked and its observed actionability digest equals the
|
|
14
|
-
* current digest. Suppression is silent — no Message, Event, or progress
|
|
15
|
-
* record is written — because a periodic scan that changes nothing is Yui
|
|
16
|
-
* engineering behavior with no Task-visible effect. Computation errors fail
|
|
17
|
-
* open: the Task is woken once and the error is recorded so it can never be
|
|
18
|
-
* silently starved.
|
|
19
|
-
*/
|
|
20
|
-
export function repairOrphanedActiveTasks(store, now, selection) {
|
|
21
|
-
const repaired = [];
|
|
22
|
-
for (const task of selectedActiveSchedulerTasks(store, selection)) {
|
|
23
|
-
const roles = store.listRoles(task.id);
|
|
24
|
-
const activeRuns = roles.flatMap((role) => {
|
|
25
|
-
const run = store.getActiveAgentRun(task.id, role.name);
|
|
26
|
-
return run === null ? [] : [run];
|
|
27
|
-
});
|
|
28
|
-
const leaderTarget = { kind: "role", taskId: task.id, roleName: "leader" };
|
|
29
|
-
const leaderMailbox = store.getWorkMailbox(leaderTarget);
|
|
30
|
-
const projection = projectTaskExecution({
|
|
31
|
-
task,
|
|
32
|
-
roles,
|
|
33
|
-
runs: activeRuns,
|
|
34
|
-
pendingWakeup: store.getPendingWakeup(task.id),
|
|
35
|
-
leaderMailbox,
|
|
36
|
-
leaderFailure: store.getLeaderFailure(task.id)
|
|
37
|
-
});
|
|
38
|
-
const queuedAlongsideActiveSibling = hasDispatchableQueuedResourceLane(store, task.id)
|
|
39
|
-
&& !activeRuns.some(({ roleName }) => roleName === "leader")
|
|
40
|
-
&& activeRuns.some(({ roleName }) => roleName !== "leader")
|
|
41
|
-
&& projection.status === "waiting-on-agents";
|
|
42
|
-
if (store.hasOpenInputRequest(task.id)
|
|
43
|
-
|| store.getLeaderFailure(task.id) !== null
|
|
44
|
-
|| (projection.status !== "needs-leader-action" && !queuedAlongsideActiveSibling)
|
|
45
|
-
|| hasUnclaimedLeaderWork(store, task.id, leaderMailbox)) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
// Digest admission covers both an ownerless Task and a resource-queued
|
|
49
|
-
// Task with active siblings. The latter wakes only when capacity/deadline
|
|
50
|
-
// actionability changes, so repeated full scans remain silent.
|
|
51
|
-
if (projection.reason === "no-executor" || queuedAlongsideActiveSibling) {
|
|
52
|
-
if (admitOrphanWake(store, task.id, now) === "suppress")
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
56
|
-
if (!isSchedulerTaskWorkspaceReady(task, taskWorkspace)) {
|
|
57
|
-
store.queueTaskProgress(task.id, "task-orphaned", now);
|
|
58
|
-
}
|
|
59
|
-
if (leaderMailbox?.processing !== null && leaderMailbox?.processing !== undefined) {
|
|
60
|
-
const recovered = store.releaseLeaderWakeupAndEnqueue === undefined
|
|
61
|
-
? store.releaseWorkMailbox(leaderTarget, leaderMailbox.processing.batchId)
|
|
62
|
-
&& (queueLeaderWakeup(store, task.id, wakeReason("task-orphaned"), now), true)
|
|
63
|
-
: store.releaseLeaderWakeupAndEnqueue(task.id, leaderMailbox.processing.batchId, wakeReason("task-orphaned"), now);
|
|
64
|
-
if (recovered) {
|
|
65
|
-
repaired.push(task.id);
|
|
66
|
-
}
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
queueLeaderWakeup(store, task.id, wakeReason("task-orphaned"), now);
|
|
70
|
-
repaired.push(task.id);
|
|
71
|
-
}
|
|
72
|
-
return repaired;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Decide whether one `task-orphaned` scan should wake the Leader. Returns
|
|
76
|
-
* `"suppress"` without writing anything when the digest is unchanged since
|
|
77
|
-
* the last waiting/blocked Leader Run. Computation errors fail open.
|
|
78
|
-
*/
|
|
79
|
-
function admitOrphanWake(store, taskId, now) {
|
|
80
|
-
let digest;
|
|
81
|
-
try {
|
|
82
|
-
const input = collectTaskActionability(store, taskId, now);
|
|
83
|
-
digest = computeActionabilityDigest(input);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
// Fail open: wake once and record the computation error so the Task can
|
|
87
|
-
// never be silently starved by a broken projection.
|
|
88
|
-
store.queueTaskProgress(taskId, "actionability-unknown", new Date());
|
|
89
|
-
return "wake";
|
|
90
|
-
}
|
|
91
|
-
const lastLeaderRun = findLastLeaderRun(store, taskId);
|
|
92
|
-
const decision = decideOrphanWake({ currentDigest: digest, lastLeaderRun });
|
|
93
|
-
return decision.kind === "suppress" ? "suppress" : "wake";
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* The most recent Leader Run (terminal or active). Active Runs are returned
|
|
97
|
-
* so the admission check never suppresses while a Leader is still running.
|
|
98
|
-
*/
|
|
99
|
-
function findLastLeaderRun(store, taskId) {
|
|
100
|
-
const runs = operationalTaskRecords(store.listAgentRuns?.(taskId) ?? [], store.listEvents?.(taskId) ?? [], "agent-run");
|
|
101
|
-
let latest = null;
|
|
102
|
-
for (const run of runs) {
|
|
103
|
-
if (run.roleName !== "leader")
|
|
104
|
-
continue;
|
|
105
|
-
if (latest === null || run.createdAt > latest.createdAt)
|
|
106
|
-
latest = run;
|
|
107
|
-
}
|
|
108
|
-
if (latest === null)
|
|
109
|
-
return null;
|
|
110
|
-
return {
|
|
111
|
-
status: latest.status,
|
|
112
|
-
...(latest.disposition === undefined ? {} : { disposition: latest.disposition }),
|
|
113
|
-
...(latest.observedActionabilityDigest === undefined
|
|
114
|
-
? {}
|
|
115
|
-
: { observedActionabilityDigest: latest.observedActionabilityDigest })
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function hasUnclaimedLeaderWork(store, taskId, leaderMailbox) {
|
|
119
|
-
if (store.getPendingWakeup(taskId) !== null)
|
|
120
|
-
return true;
|
|
121
|
-
if (leaderMailbox === null)
|
|
122
|
-
return false;
|
|
123
|
-
return leaderMailbox.processing === null
|
|
124
|
-
&& (leaderMailbox.pending.normal !== null
|
|
125
|
-
|| leaderMailbox.pending.userCorrection !== null);
|
|
126
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { StorageCompatibilityError, loadCompatibleSnapshot } from "./migration/index.js";
|
|
4
|
-
import { createProductionStorageRegistry } from "./migration/productionRegistry.js";
|
|
5
|
-
import { FileTaskStore, STORAGE_STATE_FILE, stateFileFingerprint, StorageRecordError, validateCurrentStorageStateSnapshot } from "./taskStore.js";
|
|
6
|
-
import { readSqliteHomeIdentity, SqliteTaskStore } from "./sqliteStore.js";
|
|
7
|
-
import { COMMITTED_DATABASE_FILENAME } from "./upgrade/sqliteStateMigration.js";
|
|
8
|
-
import { validateHomeIdentity } from "../repository/homeIdentity.js";
|
|
9
|
-
import { CURRENT_STORAGE_LAYOUT_VERSION, ensureStorageSchema, inspectStorageSchema, readStorageSchemaManifest, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
|
|
10
|
-
import { classifyHome } from "./upgrade/homeClassification.js";
|
|
11
|
-
import { inspectSnapshotVersionState } from "./upgrade/homeMigrationTarget.js";
|
|
12
|
-
import { latestStorageVersionState } from "./upgrade/recordVersions.js";
|
|
13
|
-
export { createProductionStorageRegistry } from "./migration/productionRegistry.js";
|
|
14
|
-
/**
|
|
15
|
-
* Initialize a brand-new Home with the current authoritative backend, or open
|
|
16
|
-
* an existing Home through the same compatibility classification as every
|
|
17
|
-
* ordinary command. Setup is the one ordinary flow that is also responsible
|
|
18
|
-
* for creating the initial manifest.
|
|
19
|
-
*/
|
|
20
|
-
export function initializeCompatibleTaskStore(home, options = {}) {
|
|
21
|
-
if (inspectStorageSchema(home).status === "uninitialized") {
|
|
22
|
-
ensureStorageSchema(home);
|
|
23
|
-
if (CURRENT_STORAGE_LAYOUT_VERSION >= 7) {
|
|
24
|
-
return new SqliteTaskStore(home);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return openCompatibleFileTaskStore(home, options);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Open current or explicitly compatible-old storage. Compatible records are
|
|
31
|
-
* normalized in memory through strict old-shape validators; FileTaskStore then
|
|
32
|
-
* runs its one current parser and its existing writer emits only current bytes.
|
|
33
|
-
*
|
|
34
|
-
* A current Home is opened from a single fingerprint-fenced `state.json`
|
|
35
|
-
* snapshot: the same bytes drive the record-version inspection, the strict
|
|
36
|
-
* shape/reference validation, and the returned store's initial cache seed, so
|
|
37
|
-
* the open never re-reads the (large) state merely to construct or first use
|
|
38
|
-
* the store.
|
|
39
|
-
*/
|
|
40
|
-
export function openCompatibleFileTaskStore(home, options = {}) {
|
|
41
|
-
const registry = options.registry ?? createProductionStorageRegistry();
|
|
42
|
-
const latest = options.latest ?? latestStorageVersionState();
|
|
43
|
-
const schema = inspectStorageSchema(home);
|
|
44
|
-
// Issue 01: a layout-7 Home's authoritative backend is SQLite WAL. When
|
|
45
|
-
// `yui.db` exists, open it directly — never fall back to a FileTaskStore
|
|
46
|
-
// over a `state.json` that the repair may already have archived.
|
|
47
|
-
if (!options.forceStateSource
|
|
48
|
-
&&
|
|
49
|
-
(schema.status === "current" || schema.status === "unsupported")
|
|
50
|
-
&& schema.currentLayoutVersion >= 7
|
|
51
|
-
&& existsSync(join(home, COMMITTED_DATABASE_FILENAME))) {
|
|
52
|
-
return new SqliteTaskStore(home);
|
|
53
|
-
}
|
|
54
|
-
if (schema.status === "current") {
|
|
55
|
-
return openCurrentFileTaskStore(home, latest);
|
|
56
|
-
}
|
|
57
|
-
const classification = classifyHome({ home, registry, latest });
|
|
58
|
-
switch (classification.classification.status) {
|
|
59
|
-
case "current":
|
|
60
|
-
case "needs-storage-repair":
|
|
61
|
-
// A pseudo-layout-7 Home whose aggregate axis is also older cannot be
|
|
62
|
-
// normalized in memory: the compatible loader (requireCompatibleStorageSchema)
|
|
63
|
-
// only bypasses record-axis mismatches, not aggregate/layout ones. Such a
|
|
64
|
-
// Home needs the offline migration path (repair + aggregate migration in
|
|
65
|
-
// one upgrade), so surface the same migration-required error the pure
|
|
66
|
-
// version classifier would have produced, rather than a lazy strict-schema
|
|
67
|
-
// error from the first store read.
|
|
68
|
-
if (classification.classification.status === "needs-storage-repair"
|
|
69
|
-
&& classification.aggregateVersion !== undefined
|
|
70
|
-
&& classification.aggregateVersion < latest.aggregate) {
|
|
71
|
-
throw new StorageCompatibilityError("Storage requires an offline migration. Re-run `yui update` when active Sessions are clear.");
|
|
72
|
-
}
|
|
73
|
-
// A pseudo-layout-7 Home (manifest 7, no yui.db) may also carry
|
|
74
|
-
// compatible-old record versions, so open with the same normalization
|
|
75
|
-
// path as `compatible-old` rather than the strict current gate.
|
|
76
|
-
return new FileTaskStore(home, {
|
|
77
|
-
normalizeState: (raw) => normalizeState(home, raw, registry, latest)
|
|
78
|
-
});
|
|
79
|
-
case "compatible-old":
|
|
80
|
-
return new FileTaskStore(home, {
|
|
81
|
-
normalizeState: (raw) => normalizeState(home, raw, registry, latest)
|
|
82
|
-
});
|
|
83
|
-
case "migration-required":
|
|
84
|
-
throw new StorageCompatibilityError("Storage requires an offline migration. Re-run `yui update` when active Sessions are clear.");
|
|
85
|
-
case "unsupported":
|
|
86
|
-
throw new StorageCompatibilityError(describeUnsupported(classification.classification));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Read the one authoritative durable Home identity without creating runtime
|
|
91
|
-
* state. SQLite is opened read-only; file-backed Homes read only the immutable
|
|
92
|
-
* identity field, so an aggregate that requires offline migration can still
|
|
93
|
-
* authenticate its already-running Controller before the migration stops it.
|
|
94
|
-
*/
|
|
95
|
-
export function readCompatibleHomeIdentity(home, options = {}) {
|
|
96
|
-
const schema = inspectStorageSchema(home);
|
|
97
|
-
if (!options.forceStateSource
|
|
98
|
-
&& (schema.status === "current" || schema.status === "unsupported")
|
|
99
|
-
&& schema.currentLayoutVersion >= 7
|
|
100
|
-
&& existsSync(join(home, COMMITTED_DATABASE_FILENAME))) {
|
|
101
|
-
return readSqliteHomeIdentity(home, COMMITTED_DATABASE_FILENAME);
|
|
102
|
-
}
|
|
103
|
-
if (!existsSync(join(home, STORAGE_STATE_FILE))) {
|
|
104
|
-
throw new StorageRecordError("Durable Home identity is missing.");
|
|
105
|
-
}
|
|
106
|
-
let parsed;
|
|
107
|
-
try {
|
|
108
|
-
parsed = JSON.parse(readFileSync(join(home, STORAGE_STATE_FILE), "utf8"));
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
throw new StorageRecordError("Durable Home identity state is invalid JSON.");
|
|
112
|
-
}
|
|
113
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
114
|
-
throw new StorageRecordError("Durable Home identity state is invalid.");
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
return validateHomeIdentity(parsed.homeIdentity);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
throw new StorageRecordError("Durable Home identity is invalid.");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* The one-snapshot open for a Home whose manifest is already current. A single
|
|
125
|
-
* fingerprint-fenced read of `state.json` supplies:
|
|
126
|
-
* - the record-version inspection (structural scan cross-checked against the
|
|
127
|
-
* durable manifest, never the strict loader),
|
|
128
|
-
* - the strict shape/reference validation (the store's own parser, run while
|
|
129
|
-
* seeding its cache), and
|
|
130
|
-
* - the returned store's initial cache seed under the same fingerprint.
|
|
131
|
-
*
|
|
132
|
-
* A fingerprint that drifts across the read retries once and then fails
|
|
133
|
-
* closed; a later external writer still invalidates the seeded cache on the
|
|
134
|
-
* store's next read. Corruption surfaces as the same `StorageCompatibilityError`
|
|
135
|
-
* diagnosis the classifier produced before this fast path existed.
|
|
136
|
-
*/
|
|
137
|
-
function openCurrentFileTaskStore(home, latest) {
|
|
138
|
-
const statePath = join(home, STORAGE_STATE_FILE);
|
|
139
|
-
if (!existsSync(statePath)) {
|
|
140
|
-
// No state.json yet: the store's own missing-file path seeds an empty
|
|
141
|
-
// state without reading anything.
|
|
142
|
-
return new FileTaskStore(home);
|
|
143
|
-
}
|
|
144
|
-
const manifest = readStorageSchemaManifest(home);
|
|
145
|
-
const snapshot = readFingerprintFencedState(statePath);
|
|
146
|
-
let parsed;
|
|
147
|
-
try {
|
|
148
|
-
parsed = JSON.parse(snapshot.raw);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
throw new StorageCompatibilityError(`Invalid state.json: state.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
|
-
}
|
|
153
|
-
if (!isRecord(parsed)) {
|
|
154
|
-
throw new StorageCompatibilityError("Invalid state.json: state.json is not a JSON object.");
|
|
155
|
-
}
|
|
156
|
-
const inspection = inspectSnapshotVersionState(Object.freeze({ schemaManifest: manifest, state: parsed }), latest);
|
|
157
|
-
if ("corruption" in inspection) {
|
|
158
|
-
throw new StorageCompatibilityError(`Invalid state.json: ${inspection.corruption.detail}`);
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
return new FileTaskStore(home, {
|
|
162
|
-
initialStateSnapshot: { fingerprint: snapshot.fingerprint, raw: snapshot.raw }
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (error instanceof StorageRecordError) {
|
|
167
|
-
throw new StorageCompatibilityError(`Invalid state.json: ${error.message}`);
|
|
168
|
-
}
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Read `state.json` once, fenced by the same fingerprint the store's read cache
|
|
174
|
-
* is keyed on. A fingerprint that changes between the before and after stat
|
|
175
|
-
* means a concurrent writer is mid-update: retry once (bounded), then fail
|
|
176
|
-
* closed rather than seed a store from bytes that may be torn.
|
|
177
|
-
*/
|
|
178
|
-
export function readFingerprintFencedState(statePath, io = {}) {
|
|
179
|
-
const fingerprint = io.fingerprint ?? stateFileFingerprint;
|
|
180
|
-
const read = io.read ?? ((path) => readFileSync(path, "utf8"));
|
|
181
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
182
|
-
const before = fingerprint(statePath);
|
|
183
|
-
const raw = read(statePath);
|
|
184
|
-
const after = fingerprint(statePath);
|
|
185
|
-
if (before === after)
|
|
186
|
-
return { raw, fingerprint: after };
|
|
187
|
-
}
|
|
188
|
-
throw new StorageCompatibilityError("state.json changed while opening; a concurrent writer is updating the Home. Retry the command.");
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Eagerly prove that a compatible-old Home reaches the strict current parser.
|
|
192
|
-
* Staged update/upgrade preflight uses this before any Controller or binary
|
|
193
|
-
* action; ordinary commands may keep the store's normal lazy-read behavior.
|
|
194
|
-
*/
|
|
195
|
-
export function validateCompatibleFileTaskStore(home, options = {}) {
|
|
196
|
-
openCompatibleFileTaskStore(home, { ...options, forceStateSource: true }).getConfig();
|
|
197
|
-
}
|
|
198
|
-
function normalizeState(home, raw, registry, latest) {
|
|
199
|
-
let state;
|
|
200
|
-
try {
|
|
201
|
-
state = JSON.parse(raw);
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
throw new StorageCompatibilityError(`Compatible state is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
-
}
|
|
206
|
-
if (!isRecord(state)) {
|
|
207
|
-
throw new StorageCompatibilityError("Compatible state must be a JSON object.");
|
|
208
|
-
}
|
|
209
|
-
const schemaManifest = JSON.parse(readFileSync(join(home, STORAGE_SCHEMA_FILE), "utf8"));
|
|
210
|
-
if (!isRecord(schemaManifest)) {
|
|
211
|
-
throw new StorageCompatibilityError("Compatible schema manifest must be a JSON object.");
|
|
212
|
-
}
|
|
213
|
-
const snapshot = { schemaManifest, state };
|
|
214
|
-
const source = versionsOf(snapshot, latest);
|
|
215
|
-
const normalized = loadCompatibleSnapshot({
|
|
216
|
-
registry,
|
|
217
|
-
source,
|
|
218
|
-
latest,
|
|
219
|
-
snapshot,
|
|
220
|
-
inspectVersions: (candidate) => versionsOf(candidate, latest),
|
|
221
|
-
validateCurrent: (candidate) => {
|
|
222
|
-
if (candidate.state === null) {
|
|
223
|
-
throw new StorageCompatibilityError("Compatible state unexpectedly disappeared.");
|
|
224
|
-
}
|
|
225
|
-
validateCurrentStorageStateSnapshot(candidate.state);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
return `${JSON.stringify(normalized.state)}\n`;
|
|
229
|
-
}
|
|
230
|
-
function versionsOf(snapshot, latest) {
|
|
231
|
-
const inspection = inspectSnapshotVersionState(snapshot, latest);
|
|
232
|
-
if ("corruption" in inspection) {
|
|
233
|
-
throw new StorageCompatibilityError(inspection.corruption.detail);
|
|
234
|
-
}
|
|
235
|
-
return inspection.source;
|
|
236
|
-
}
|
|
237
|
-
function describeUnsupported(classification) {
|
|
238
|
-
if (classification.verdict === "CORRUPTED") {
|
|
239
|
-
return `Invalid state.json: ${classification.detail}`;
|
|
240
|
-
}
|
|
241
|
-
if (classification.verdict === "NEEDS_NEW_VERSION") {
|
|
242
|
-
return classification.blocker.message;
|
|
243
|
-
}
|
|
244
|
-
return "Storage is unsupported by this Yui release.";
|
|
245
|
-
}
|
|
246
|
-
function isRecord(value) {
|
|
247
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
248
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
|
|
2
|
-
import { currentRecordVersions } from "../upgrade/recordVersions.js";
|
|
3
|
-
export const BASELINE_STORAGE_LAYOUT_VERSION = 6;
|
|
4
|
-
export const BASELINE_AGGREGATE_SCHEMA_VERSION = 16;
|
|
5
|
-
const BASELINE_RECORD_DESCRIPTORS = Object.freeze({
|
|
6
|
-
config: descriptor(1, "state.json#/config"),
|
|
7
|
-
configuredAgent: descriptor(2, "state.json#/configuredAgents"),
|
|
8
|
-
project: descriptor(2, "state.json#/projects"),
|
|
9
|
-
agentProfile: descriptor(2, "state.json#/agentProfiles"),
|
|
10
|
-
globalRole: descriptor(3, "state.json#/globalRoles"),
|
|
11
|
-
globalRoleSessionSet: descriptor(3, "state.json#/globalRoleSessionSets"),
|
|
12
|
-
storedTask: descriptor(14, "state.json#/tasks/*"),
|
|
13
|
-
task: descriptor(3, "state.json#/tasks/*/task"),
|
|
14
|
-
taskBrief: descriptor(2, "state.json#/tasks/*/brief"),
|
|
15
|
-
taskRole: descriptor(3, "state.json#/tasks/*/roles"),
|
|
16
|
-
managedWorkspace: descriptor(1, "state.json#/tasks/*/managedWorkspaces"),
|
|
17
|
-
taskRoleSessionSet: descriptor(4, "state.json#/tasks/*/roleSessionSets"),
|
|
18
|
-
workItem: descriptor(6, "state.json#/tasks/*/workItems"),
|
|
19
|
-
agentRun: descriptor(5, "state.json#/tasks/*/agentRuns"),
|
|
20
|
-
reviewRound: descriptor(2, "state.json#/tasks/*/reviewRounds"),
|
|
21
|
-
changeSet: descriptor(2, "state.json#/tasks/*/changeSets"),
|
|
22
|
-
integrationAttempt: descriptor(2, "state.json#/tasks/*/integrationAttempts"),
|
|
23
|
-
activeRunPointer: descriptor(1, "state.json#/tasks/*/activeRuns"),
|
|
24
|
-
message: descriptor(2, "state.json#/tasks/*/messages"),
|
|
25
|
-
inputRequest: descriptor(2, "state.json#/tasks/*/inputRequests"),
|
|
26
|
-
decision: descriptor(1, "state.json#/tasks/*/decisions"),
|
|
27
|
-
milestone: descriptor(1, "state.json#/tasks/*/milestones"),
|
|
28
|
-
event: descriptor(2, "state.json#/tasks/*/events"),
|
|
29
|
-
leaderFailure: descriptor(1, "state.json#/tasks/*/leaderFailure"),
|
|
30
|
-
operatorNotification: descriptor(1, "state.json#/tasks/*/operatorNotification"),
|
|
31
|
-
workMailbox: descriptor(1, "state.json#/mailboxes")
|
|
32
|
-
});
|
|
33
|
-
/** Frozen baseline record-family version map retained as a public delivery contract. */
|
|
34
|
-
export const BASELINE_RECORD_VERSIONS = Object.freeze(Object.fromEntries(Object.entries(BASELINE_RECORD_DESCRIPTORS).map(([kind, entry]) => [kind, entry.version])));
|
|
35
|
-
export function baselineStorageVersionState() {
|
|
36
|
-
return Object.freeze({
|
|
37
|
-
layout: BASELINE_STORAGE_LAYOUT_VERSION,
|
|
38
|
-
aggregate: BASELINE_AGGREGATE_SCHEMA_VERSION,
|
|
39
|
-
record: Object.freeze({ ...BASELINE_RECORD_DESCRIPTORS })
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** Constant spelling used by delivery-gate callers and historical tests. */
|
|
43
|
-
export const BASELINE_STORAGE_VERSION_STATE = baselineStorageVersionState();
|
|
44
|
-
/**
|
|
45
|
-
* Fail the delivery gate if the immutable baseline is ahead of the live schema,
|
|
46
|
-
* loses a record family, or changes a locator without an explicit scalar
|
|
47
|
-
* storage boundary. The production registry gate separately proves the full
|
|
48
|
-
* executable baseline-to-current transition path.
|
|
49
|
-
*/
|
|
50
|
-
export function assertBaselineConsistency(current = {
|
|
51
|
-
layout: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
52
|
-
aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION,
|
|
53
|
-
record: currentRecordVersions()
|
|
54
|
-
}) {
|
|
55
|
-
if (BASELINE_STORAGE_LAYOUT_VERSION > current.layout) {
|
|
56
|
-
throw new Error(`Baseline layout ${BASELINE_STORAGE_LAYOUT_VERSION} exceeds current layout ${current.layout}.`);
|
|
57
|
-
}
|
|
58
|
-
if (BASELINE_AGGREGATE_SCHEMA_VERSION > current.aggregate) {
|
|
59
|
-
throw new Error(`Baseline aggregate ${BASELINE_AGGREGATE_SCHEMA_VERSION} exceeds current aggregate ${current.aggregate}.`);
|
|
60
|
-
}
|
|
61
|
-
const scalarVersionAdvanced = current.layout > BASELINE_STORAGE_LAYOUT_VERSION
|
|
62
|
-
|| current.aggregate > BASELINE_AGGREGATE_SCHEMA_VERSION;
|
|
63
|
-
for (const [kind, baselineEntry] of Object.entries(BASELINE_RECORD_DESCRIPTORS)) {
|
|
64
|
-
const currentEntry = current.record[kind];
|
|
65
|
-
if (!Number.isSafeInteger(baselineEntry.version) || baselineEntry.version < 1) {
|
|
66
|
-
throw new Error(`Baseline record family '${kind}' has invalid version ${String(baselineEntry.version)}.`);
|
|
67
|
-
}
|
|
68
|
-
if (currentEntry === undefined || baselineEntry.version > currentEntry.version) {
|
|
69
|
-
throw new Error(`Baseline record family '${kind}' is ahead of or missing from current.`);
|
|
70
|
-
}
|
|
71
|
-
if (baselineEntry.path !== currentEntry.path && !scalarVersionAdvanced) {
|
|
72
|
-
throw new Error(`Baseline record family '${kind}' path drift requires a layout or aggregate version change.`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function descriptor(version, path) {
|
|
77
|
-
return Object.freeze({ version, path });
|
|
78
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pure four-state compatibility classifier.
|
|
3
|
-
*
|
|
4
|
-
* Given the source and target (latest-supported) version states, an EMPTY-or-
|
|
5
|
-
* populated registry, and an optional caller-supplied corruption signal, it
|
|
6
|
-
* returns exactly one verdict:
|
|
7
|
-
*
|
|
8
|
-
* - `USABLE` — every axis is already at the current version.
|
|
9
|
-
* - `COMPATIBLE` — strictly older record axes whose complete adjacent
|
|
10
|
-
* path consists only of compatible normalizations.
|
|
11
|
-
* - `MIGRATABLE` — at least one adjacent hop requires offline migration,
|
|
12
|
-
* and every axis has a complete deterministic path.
|
|
13
|
-
* - `NEEDS_NEW_VERSION` — any axis is newer than supported (`future-version`),
|
|
14
|
-
* or older with a broken/absent step path (`missing-step`). Carries a precise
|
|
15
|
-
* reason + action. Under an EMPTY registry, any strictly-older version lands
|
|
16
|
-
* here via a missing transition contract.
|
|
17
|
-
* - `CORRUPTED` — only when the caller reports real JSON/structural/
|
|
18
|
-
* reference-invariant damage. The classifier never infers corruption from
|
|
19
|
-
* version numbers.
|
|
20
|
-
*
|
|
21
|
-
* There are no baseline/pre-release special cases: compatibility is decided
|
|
22
|
-
* solely by explicit registry step-paths, never by version magnitude or semver.
|
|
23
|
-
*/
|
|
24
|
-
import { planMigration } from "./planner.js";
|
|
25
|
-
/**
|
|
26
|
-
* Classify a storage target. Pure: no I/O, no mutation, deterministic.
|
|
27
|
-
*
|
|
28
|
-
* `corruption` is supplied by the caller when parsing/structural/reference
|
|
29
|
-
* validation has already failed; when present it takes precedence, because a
|
|
30
|
-
* corrupted target's version numbers cannot be trusted.
|
|
31
|
-
*/
|
|
32
|
-
export function classifyStorage(registry, source, target, corruption) {
|
|
33
|
-
if (corruption?.corrupted === true) {
|
|
34
|
-
return { verdict: "CORRUPTED", status: "unsupported", detail: corruption.detail };
|
|
35
|
-
}
|
|
36
|
-
const plan = planMigration(registry, source, target);
|
|
37
|
-
switch (plan.kind) {
|
|
38
|
-
case "no-op":
|
|
39
|
-
return { verdict: "USABLE", status: "current" };
|
|
40
|
-
case "runnable":
|
|
41
|
-
return plan.path === "compatible"
|
|
42
|
-
? { verdict: "COMPATIBLE", status: "compatible-old", stepCount: plan.steps.length }
|
|
43
|
-
: { verdict: "MIGRATABLE", status: "migration-required", stepCount: plan.steps.length };
|
|
44
|
-
case "blocked":
|
|
45
|
-
return {
|
|
46
|
-
verdict: "NEEDS_NEW_VERSION",
|
|
47
|
-
status: "unsupported",
|
|
48
|
-
blocker: plan.blocker
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { planMigration } from "./planner.js";
|
|
3
|
-
export class StorageCompatibilityError extends Error {
|
|
4
|
-
constructor(message) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.name = "StorageCompatibilityError";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Apply an all-compatible declaration chain in memory. This never writes the
|
|
11
|
-
* source. It rejects offline paths, missing declarations/steps, non-fresh
|
|
12
|
-
* normalizers, and output which does not pass the strict current-model gate.
|
|
13
|
-
*/
|
|
14
|
-
export function loadCompatibleSnapshot(options) {
|
|
15
|
-
const plan = planMigration(options.registry, options.source, options.latest);
|
|
16
|
-
if (plan.kind === "blocked") {
|
|
17
|
-
throw new StorageCompatibilityError(plan.blocker.message);
|
|
18
|
-
}
|
|
19
|
-
if (plan.kind === "runnable" && plan.path !== "compatible") {
|
|
20
|
-
throw new StorageCompatibilityError("Storage requires an offline migration and cannot be normalized by the compatible loader.");
|
|
21
|
-
}
|
|
22
|
-
let current = options.snapshot;
|
|
23
|
-
if (plan.kind === "runnable") {
|
|
24
|
-
for (const planned of plan.steps) {
|
|
25
|
-
planned.step.preconditions(current);
|
|
26
|
-
const next = planned.step.transform(current);
|
|
27
|
-
if (next === current && typeof next === "object" && next !== null) {
|
|
28
|
-
throw new StorageCompatibilityError(`Compatible normalizer ${planned.axis}` +
|
|
29
|
-
`${planned.recordKind ? `/${planned.recordKind}` : ""} ` +
|
|
30
|
-
`${planned.fromVersion}->${planned.toVersion} returned its input object.`);
|
|
31
|
-
}
|
|
32
|
-
current = next;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
assertCurrentVersions(options.inspectVersions(current), options.latest, "normalized model");
|
|
36
|
-
options.validateCurrent(current);
|
|
37
|
-
return current;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Encode only the current record contract. There is intentionally no old
|
|
41
|
-
* writer or dual-write branch: a non-current encoding fails before persistence.
|
|
42
|
-
*/
|
|
43
|
-
export function writeCurrentSnapshot(options) {
|
|
44
|
-
const snapshot = options.encode(options.model);
|
|
45
|
-
assertCurrentVersions(options.inspectVersions(snapshot), options.latest, "writer output");
|
|
46
|
-
options.validateCurrent(snapshot);
|
|
47
|
-
return snapshot;
|
|
48
|
-
}
|
|
49
|
-
function assertCurrentVersions(actual, latest, label) {
|
|
50
|
-
if (!isDeepStrictEqual(actual, latest)) {
|
|
51
|
-
throw new StorageCompatibilityError(`Compatible ${label} is not at the current storage versions; old-version writing is forbidden.`);
|
|
52
|
-
}
|
|
53
|
-
}
|