@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,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The central migration registry.
|
|
3
|
-
*
|
|
4
|
-
* Declarations and offline transforms are discoverable strictly by
|
|
5
|
-
* `axis + (recordKind) + fromVersion`. There is no version-magnitude or semver
|
|
6
|
-
* guessing: only an explicitly declared chain of adjacent transitions is
|
|
7
|
-
* usable. A fresh registry is EMPTY, so every older version fails closed until
|
|
8
|
-
* its compatible normalization or offline migration is authored explicitly.
|
|
9
|
-
*/
|
|
10
|
-
export class MigrationRegistryError extends Error {
|
|
11
|
-
constructor(message) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "MigrationRegistryError";
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Explicit key-field delimiter: the ASCII Unit Separator (U+001F), built with
|
|
18
|
-
* `String.fromCharCode` so this source file stays pure printable ASCII (no raw
|
|
19
|
-
* control byte embedded in the text). The Unit Separator never appears in an
|
|
20
|
-
* `axis` literal, a `recordKind`, or a decimal version, so the three key fields
|
|
21
|
-
* cannot run together and the composite key is unambiguous.
|
|
22
|
-
*/
|
|
23
|
-
const KEY_SEPARATOR = String.fromCharCode(0x1f);
|
|
24
|
-
/**
|
|
25
|
-
* Composite lookup key. The record axis includes its `recordKind`; the layout
|
|
26
|
-
* and aggregate axes use a fixed sentinel so a single map serves all axes.
|
|
27
|
-
*/
|
|
28
|
-
function stepKey(axis, recordKind, fromVersion) {
|
|
29
|
-
const family = axis === "record" ? requireRecordKind(recordKind) : "-";
|
|
30
|
-
return `${axis}${KEY_SEPARATOR}${family}${KEY_SEPARATOR}${fromVersion}`;
|
|
31
|
-
}
|
|
32
|
-
function requireRecordKind(recordKind) {
|
|
33
|
-
if (typeof recordKind !== "string" || recordKind.trim().length === 0) {
|
|
34
|
-
throw new MigrationRegistryError("A record-axis migration step must declare a non-empty recordKind.");
|
|
35
|
-
}
|
|
36
|
-
return recordKind;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* A discoverable set of adjacent migration steps. Register at most one step per
|
|
40
|
-
* `(axis, recordKind, fromVersion)`; lookup returns only that exact adjacent
|
|
41
|
-
* step (or `undefined`) — it never infers a step from version ordering.
|
|
42
|
-
*/
|
|
43
|
-
export class MigrationRegistry {
|
|
44
|
-
#steps = new Map();
|
|
45
|
-
#declarations = new Map();
|
|
46
|
-
/** Backward-compatible spelling: explicitly registers one offline transition and its transform. */
|
|
47
|
-
register(step) {
|
|
48
|
-
this.#validateMigrationStep(step);
|
|
49
|
-
const key = stepKey(step.axis, step.recordKind, step.fromVersion);
|
|
50
|
-
this.#assertDeclarationAvailable(step, key);
|
|
51
|
-
this.#assertStepAvailable(step, key);
|
|
52
|
-
// Publish the declaration and transform together only after every check has
|
|
53
|
-
// passed. A rejected combined registration must not leave a newly-runnable
|
|
54
|
-
// half update behind in the registry.
|
|
55
|
-
this.#declarations.set(key, { ...step, kind: "offline-migration" });
|
|
56
|
-
this.#steps.set(key, step);
|
|
57
|
-
return this;
|
|
58
|
-
}
|
|
59
|
-
/** Explicitly register the declaration and transform for an offline change. */
|
|
60
|
-
registerOfflineMigration(step) {
|
|
61
|
-
return this.register(step);
|
|
62
|
-
}
|
|
63
|
-
/** Declare an offline change. A missing transform remains fail-closed. */
|
|
64
|
-
declareOfflineMigration(declaration) {
|
|
65
|
-
this.#validateCoordinates(declaration);
|
|
66
|
-
this.#registerDeclaration({ ...declaration, kind: "offline-migration" });
|
|
67
|
-
return this;
|
|
68
|
-
}
|
|
69
|
-
/** Declare an online-compatible normalization with deterministic defaults. */
|
|
70
|
-
registerCompatible(step) {
|
|
71
|
-
const runtimeAxis = step.axis;
|
|
72
|
-
if (runtimeAxis !== "record") {
|
|
73
|
-
throw new MigrationRegistryError("A compatible transition must use the record axis; layout and aggregate changes require offline migration.");
|
|
74
|
-
}
|
|
75
|
-
this.#validateCoordinates(step);
|
|
76
|
-
if (!Array.isArray(step.defaults)
|
|
77
|
-
|| step.defaults.length === 0
|
|
78
|
-
|| step.defaults.some((value) => typeof value !== "string" || value.trim().length === 0)) {
|
|
79
|
-
throw new MigrationRegistryError("A compatible transition must declare at least one deterministic default or rebuild rule.");
|
|
80
|
-
}
|
|
81
|
-
if (typeof step.normalize !== "function" || typeof step.validateSource !== "function") {
|
|
82
|
-
throw new MigrationRegistryError("A compatible transition must provide a strict source validator and normalizer.");
|
|
83
|
-
}
|
|
84
|
-
this.#registerDeclaration({ ...step, kind: "compatible" });
|
|
85
|
-
return this;
|
|
86
|
-
}
|
|
87
|
-
/** Register a transform without inferring its operational declaration. */
|
|
88
|
-
registerMigrationStep(step) {
|
|
89
|
-
this.#validateMigrationStep(step);
|
|
90
|
-
const key = stepKey(step.axis, step.recordKind, step.fromVersion);
|
|
91
|
-
this.#assertStepAvailable(step, key);
|
|
92
|
-
this.#steps.set(key, step);
|
|
93
|
-
return this;
|
|
94
|
-
}
|
|
95
|
-
#validateMigrationStep(step) {
|
|
96
|
-
this.#validateCoordinates(step);
|
|
97
|
-
if (!Array.isArray(step.declaredEffects)
|
|
98
|
-
|| typeof step.transform !== "function"
|
|
99
|
-
|| typeof step.preconditions !== "function") {
|
|
100
|
-
throw new MigrationRegistryError("A migration step must provide its transform contract.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
#assertStepAvailable(step, key) {
|
|
104
|
-
if (!this.#steps.has(key))
|
|
105
|
-
return;
|
|
106
|
-
throw new MigrationRegistryError(`A migration step is already registered for ${step.axis}` +
|
|
107
|
-
`${step.recordKind ? `/${step.recordKind}` : ""} ${step.fromVersion}->${step.toVersion}.`);
|
|
108
|
-
}
|
|
109
|
-
#assertDeclarationAvailable(declaration, key) {
|
|
110
|
-
if (!this.#declarations.has(key))
|
|
111
|
-
return;
|
|
112
|
-
throw new MigrationRegistryError(`A transition is already registered for ${declaration.axis}` +
|
|
113
|
-
`${declaration.recordKind ? `/${declaration.recordKind}` : ""} ` +
|
|
114
|
-
`${declaration.fromVersion}->${declaration.toVersion}.`);
|
|
115
|
-
}
|
|
116
|
-
#validateCoordinates(step) {
|
|
117
|
-
if (step.axis === "record") {
|
|
118
|
-
requireRecordKind(step.recordKind);
|
|
119
|
-
}
|
|
120
|
-
else if (step.recordKind !== undefined) {
|
|
121
|
-
throw new MigrationRegistryError(`A ${step.axis}-axis transition must not declare a recordKind.`);
|
|
122
|
-
}
|
|
123
|
-
if (!Number.isSafeInteger(step.fromVersion) || step.fromVersion < 0) {
|
|
124
|
-
throw new MigrationRegistryError(`Transition fromVersion must be a non-negative integer: ${String(step.fromVersion)}.`);
|
|
125
|
-
}
|
|
126
|
-
if (step.fromVersion === 0) {
|
|
127
|
-
if (step.axis !== "record" || step.introduction !== true || step.toVersion !== 1) {
|
|
128
|
-
throw new MigrationRegistryError("Only an explicit record-family introduction may use the 0->1 transition.");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else if (step.introduction === true) {
|
|
132
|
-
throw new MigrationRegistryError("A record-family introduction must be the explicit 0->1 transition.");
|
|
133
|
-
}
|
|
134
|
-
if (step.toVersion !== step.fromVersion + 1) {
|
|
135
|
-
throw new MigrationRegistryError(`Transitions must be adjacent and one-directional: ${step.axis}` +
|
|
136
|
-
`${step.recordKind ? `/${step.recordKind}` : ""} ${step.fromVersion}->${step.toVersion} ` +
|
|
137
|
-
`(expected ${step.fromVersion}->${step.fromVersion + 1}).`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
#registerDeclaration(declaration) {
|
|
141
|
-
const key = stepKey(declaration.axis, declaration.recordKind, declaration.fromVersion);
|
|
142
|
-
this.#assertDeclarationAvailable(declaration, key);
|
|
143
|
-
this.#declarations.set(key, declaration);
|
|
144
|
-
}
|
|
145
|
-
/** Look up the exact adjacent step advancing `fromVersion -> fromVersion + 1`. */
|
|
146
|
-
lookup(axis, recordKind, fromVersion) {
|
|
147
|
-
return this.#steps.get(stepKey(axis, recordKind, fromVersion));
|
|
148
|
-
}
|
|
149
|
-
/** Look up the exact compatible/offline declaration for one adjacent change. */
|
|
150
|
-
lookupDeclaration(axis, recordKind, fromVersion) {
|
|
151
|
-
return this.#declarations.get(stepKey(axis, recordKind, fromVersion));
|
|
152
|
-
}
|
|
153
|
-
/** Total number of declared adjacent transitions (0 for an EMPTY registry). */
|
|
154
|
-
get size() {
|
|
155
|
-
return this.#declarations.size;
|
|
156
|
-
}
|
|
157
|
-
/** True when neither declarations nor offline transforms are registered. */
|
|
158
|
-
isEmpty() {
|
|
159
|
-
return this.#declarations.size === 0 && this.#steps.size === 0;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Create a fresh, EMPTY registry for isolated planning or callers that
|
|
164
|
-
* authorize no transitions. Production wiring builds its explicit graph in a
|
|
165
|
-
* separate factory.
|
|
166
|
-
*/
|
|
167
|
-
export function createEmptyRegistry() {
|
|
168
|
-
return new MigrationRegistry();
|
|
169
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for building and describing {@link MigrationReport}s.
|
|
3
|
-
*
|
|
4
|
-
* These never perform I/O and never interpret domain data — they only shape the
|
|
5
|
-
* structured evidence the engine emits.
|
|
6
|
-
*/
|
|
7
|
-
/** Reduce a planned step to the concise summary carried in reports. */
|
|
8
|
-
export function toStepSummary(planned) {
|
|
9
|
-
return {
|
|
10
|
-
axis: planned.axis,
|
|
11
|
-
...(planned.recordKind ? { recordKind: planned.recordKind } : {}),
|
|
12
|
-
fromVersion: planned.fromVersion,
|
|
13
|
-
toVersion: planned.toVersion,
|
|
14
|
-
transition: planned.transition,
|
|
15
|
-
declaredEffects: planned.step.declaredEffects
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* The de-duplicated union of every planned step's declared effects, in
|
|
20
|
-
* first-seen deterministic order. This is exactly what the engine forwards to
|
|
21
|
-
* the injected `rebuildDerivedState` — the generic core keeps no domain list.
|
|
22
|
-
*/
|
|
23
|
-
export function collectEffects(steps) {
|
|
24
|
-
const seen = new Set();
|
|
25
|
-
const effects = [];
|
|
26
|
-
for (const planned of steps) {
|
|
27
|
-
for (const effect of planned.step.declaredEffects) {
|
|
28
|
-
if (!seen.has(effect)) {
|
|
29
|
-
seen.add(effect);
|
|
30
|
-
effects.push(effect);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return effects;
|
|
35
|
-
}
|
|
36
|
-
/** A short, human-readable one-line description of a report outcome. */
|
|
37
|
-
export function describeReport(report) {
|
|
38
|
-
switch (report.outcome) {
|
|
39
|
-
case "already-current":
|
|
40
|
-
return "Storage is already at the current version; nothing to migrate.";
|
|
41
|
-
case "blocked":
|
|
42
|
-
return `Migration blocked (${report.blocker.reason}): ${report.blocker.message} ${report.blocker.action}`;
|
|
43
|
-
case "active-runtime":
|
|
44
|
-
return `Migration refused: a live runtime is active${report.detail ? ` (${report.detail})` : ""}.`;
|
|
45
|
-
case "dry-run":
|
|
46
|
-
return `Dry run validated ${report.steps.length} step(s); fresh output discarded, source unchanged.`;
|
|
47
|
-
case "migrated":
|
|
48
|
-
return `Migrated through ${report.steps.length} step(s); source backed up at ${report.switch.backupPath ?? "(unspecified)"}.`;
|
|
49
|
-
case "failed":
|
|
50
|
-
return `Migration failed at ${report.stage}: ${report.error} Source is unchanged; delete any partial output and retry.`;
|
|
51
|
-
case "switch-ambiguous":
|
|
52
|
-
return `Migration switch is AMBIGUOUS: ${report.error} Recover from the backup at ${report.backupPath}.`;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic, pure migration-core vocabulary.
|
|
3
|
-
*
|
|
4
|
-
* This module is deliberately domain-agnostic: it knows about three independent
|
|
5
|
-
* monotonic version axes and how to chain adjacent steps, but it never encodes a
|
|
6
|
-
* list of concrete Yui records, derived-state fields, or reference invariants.
|
|
7
|
-
* Those live behind the injected {@link MigrationTarget} boundary so a new record
|
|
8
|
-
* family can be added without editing the generic engine.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Thrown by a target's `atomicSwitchWithBackup` when the switch is left in a
|
|
12
|
-
* partially-applied, ambiguous state (the original was moved aside but the
|
|
13
|
-
* promotion and its rollback both failed). It carries the exact recovery path so
|
|
14
|
-
* the orchestrator can report a truthful, actionable manual recovery rather than
|
|
15
|
-
* a false "the Home is unchanged".
|
|
16
|
-
*/
|
|
17
|
-
export class AmbiguousSwitchError extends Error {
|
|
18
|
-
/** The logical Home path that is now partially switched. */
|
|
19
|
-
homePath;
|
|
20
|
-
/** Where the original Home currently lives (the recovery source). */
|
|
21
|
-
backupPath;
|
|
22
|
-
/** The staged output that was not promoted. */
|
|
23
|
-
stagingPath;
|
|
24
|
-
constructor(options) {
|
|
25
|
-
super(options.detail);
|
|
26
|
-
this.name = "AmbiguousSwitchError";
|
|
27
|
-
this.homePath = options.homePath;
|
|
28
|
-
this.backupPath = options.backupPath;
|
|
29
|
-
this.stagingPath = options.stagingPath;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Four-state classification of a real Yui Home.
|
|
3
|
-
*
|
|
4
|
-
* This layers real, I/O-based corruption detection onto the pure WI-3
|
|
5
|
-
* classifier. It reports exactly one shared product state — current,
|
|
6
|
-
* compatible-old, migration-required, or unsupported — together with the
|
|
7
|
-
* on-disk layout and aggregate versions and, when the store cannot be used, the
|
|
8
|
-
* incompatible component (layout vs aggregate).
|
|
9
|
-
*
|
|
10
|
-
* Corruption is only ever reported from a *real* structural failure: a
|
|
11
|
-
* `state.json` that is not parseable / not shaped like the record locators
|
|
12
|
-
* describe / carries a record with an invalid `schemaVersion`, a broken reference
|
|
13
|
-
* graph (detected by the strict loader once every axis is already current), or a
|
|
14
|
-
* `schema.json` manifest that is not valid. It is never inferred from version
|
|
15
|
-
* magnitude. An older or newer version — on ANY axis, including a single older
|
|
16
|
-
* record family — is a version verdict (`COMPATIBLE` for an all-compatible path,
|
|
17
|
-
* `MIGRATABLE` for an offline path, otherwise `NEEDS_NEW_VERSION`), never
|
|
18
|
-
* `CORRUPTED`. An older Home without a complete declared path fails closed with
|
|
19
|
-
* a precise missing declaration/step reason.
|
|
20
|
-
*
|
|
21
|
-
* The three axes are independent: record versions come from the durable
|
|
22
|
-
* manifest and raw `state.json` is traversed structurally only to verify that
|
|
23
|
-
* non-empty families agree. This keeps a record-only-older Home on its version
|
|
24
|
-
* axis without letting an empty target family masquerade as current.
|
|
25
|
-
*/
|
|
26
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
-
import { join } from "node:path";
|
|
28
|
-
import Database from "better-sqlite3";
|
|
29
|
-
import { classifyStorage } from "../migration/index.js";
|
|
30
|
-
import { inspectStorageSchema } from "../storageSchema.js";
|
|
31
|
-
import { FileTaskStore, STORAGE_STATE_FILE, StorageRecordError } from "../taskStore.js";
|
|
32
|
-
import { SqliteTaskStore } from "../sqliteStore.js";
|
|
33
|
-
import { inspectSqliteSchemaMigrations } from "../sqliteSchema.js";
|
|
34
|
-
import { inspectSourceVersionState } from "./homeMigrationTarget.js";
|
|
35
|
-
import { readMigrationReceipt } from "./migrationReceipt.js";
|
|
36
|
-
/**
|
|
37
|
-
* Classify a real Home. Reads `schema.json` (and, for a current Home,
|
|
38
|
-
* `state.json` through the strict loader) read-only; never mutates the Home.
|
|
39
|
-
*/
|
|
40
|
-
export function classifyHome(options) {
|
|
41
|
-
const { home, registry, latest } = options;
|
|
42
|
-
const schema = inspectStorageSchema(home);
|
|
43
|
-
const base = {
|
|
44
|
-
latestLayoutVersion: latest.layout,
|
|
45
|
-
latestAggregateVersion: latest.aggregate
|
|
46
|
-
};
|
|
47
|
-
if (schema.status === "uninitialized") {
|
|
48
|
-
return {
|
|
49
|
-
...base,
|
|
50
|
-
classification: { verdict: "USABLE", status: "current" },
|
|
51
|
-
uninitialized: true
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
if (schema.status === "invalid") {
|
|
55
|
-
// A malformed manifest is real structural damage, not a version verdict.
|
|
56
|
-
return {
|
|
57
|
-
...base,
|
|
58
|
-
classification: {
|
|
59
|
-
verdict: "CORRUPTED",
|
|
60
|
-
status: "unsupported",
|
|
61
|
-
detail: `Storage schema manifest is invalid: ${schema.detail}`
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// Read all three durable axes read-only, then structurally cross-check raw
|
|
66
|
-
// state.json without invoking the strict current loader. Version differences
|
|
67
|
-
// remain version facts; manifest/state contradictions surface as corruption.
|
|
68
|
-
const inspected = inspectSourceVersionState(home, latest);
|
|
69
|
-
if ("corruption" in inspected) {
|
|
70
|
-
return {
|
|
71
|
-
...base,
|
|
72
|
-
classification: {
|
|
73
|
-
verdict: "CORRUPTED",
|
|
74
|
-
status: "unsupported",
|
|
75
|
-
detail: inspected.corruption.detail
|
|
76
|
-
},
|
|
77
|
-
layoutVersion: schema.currentLayoutVersion,
|
|
78
|
-
aggregateVersion: schema.currentAggregateSchemaVersion,
|
|
79
|
-
...(incompatibleComponentOf(schema) === undefined
|
|
80
|
-
? {}
|
|
81
|
-
: { incompatibleComponent: incompatibleComponentOf(schema) })
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
const source = inspected.source;
|
|
85
|
-
// Layout 7 physical-backend invariant (Issue 01): the manifest claims SQLite
|
|
86
|
-
// WAL as the authoritative store, so `yui.db` must exist and be healthy. A
|
|
87
|
-
// layout-7 Home without `yui.db` is a *pseudo-layout-7* Home — repairable
|
|
88
|
-
// when `state.json` is strictly readable, corrupted otherwise. A Home with
|
|
89
|
-
// both `state.json` and `yui.db` but no persistent migration receipt is an
|
|
90
|
-
// ambiguous dual-copy conflict. These are physical-backend facts, not
|
|
91
|
-
// version verdicts, so they are decided before the pure classifier runs.
|
|
92
|
-
//
|
|
93
|
-
// The invariant binds a Home whose *layout* is current (7), regardless of
|
|
94
|
-
// whether the record/aggregate axes are also current. A layout-7 Home with
|
|
95
|
-
// older record versions and no yui.db is still a pseudo-layout-7 Home: it
|
|
96
|
-
// needs the state.json→SQLite repair first, then the record-family migration
|
|
97
|
-
// (multi-phase orchestration in the upgrade executor). Routing it to the
|
|
98
|
-
// pure version classifier would select the SQLite record target, which
|
|
99
|
-
// cannot read a yui.db that does not exist yet. A Home with an older/future
|
|
100
|
-
// *layout* is fenced by version policy below, as before.
|
|
101
|
-
if (schema.currentLayoutVersion === latest.layout && latest.layout >= 7) {
|
|
102
|
-
const physical = inspectLayout7PhysicalBackend(home);
|
|
103
|
-
if (physical !== undefined) {
|
|
104
|
-
return {
|
|
105
|
-
...base,
|
|
106
|
-
classification: physical,
|
|
107
|
-
layoutVersion: schema.currentLayoutVersion,
|
|
108
|
-
aggregateVersion: schema.currentAggregateSchemaVersion,
|
|
109
|
-
...(incompatibleComponentOf(schema) === undefined
|
|
110
|
-
? {}
|
|
111
|
-
: { incompatibleComponent: incompatibleComponentOf(schema) })
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Once the logical Home axes are current, the SQLite ledger is the only
|
|
116
|
-
// remaining upgrade coordinate. A valid applied prefix with pending entries
|
|
117
|
-
// is upgradeable in place, not corruption and not a reason to rebuild the
|
|
118
|
-
// database. Ordinary store opens still validate-and-refuse this state; only
|
|
119
|
-
// the explicit updater is allowed to apply it.
|
|
120
|
-
if (schema.status === "current"
|
|
121
|
-
&& schema.currentLayoutVersion >= 7
|
|
122
|
-
&& isFullyCurrent(source, latest)) {
|
|
123
|
-
let sqliteMigration;
|
|
124
|
-
try {
|
|
125
|
-
sqliteMigration = inspectLayout7SqliteMigrations(home);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
return {
|
|
129
|
-
...base,
|
|
130
|
-
classification: {
|
|
131
|
-
verdict: "CORRUPTED",
|
|
132
|
-
status: "unsupported",
|
|
133
|
-
detail: error instanceof Error ? error.message : String(error)
|
|
134
|
-
},
|
|
135
|
-
layoutVersion: schema.currentLayoutVersion,
|
|
136
|
-
aggregateVersion: schema.currentAggregateSchemaVersion
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
if (sqliteMigration.pendingVersions.length > 0) {
|
|
140
|
-
return {
|
|
141
|
-
...base,
|
|
142
|
-
classification: {
|
|
143
|
-
verdict: "MIGRATABLE",
|
|
144
|
-
status: "migration-required",
|
|
145
|
-
stepCount: sqliteMigration.pendingVersions.length
|
|
146
|
-
},
|
|
147
|
-
layoutVersion: schema.currentLayoutVersion,
|
|
148
|
-
aggregateVersion: schema.currentAggregateSchemaVersion,
|
|
149
|
-
sqliteMigration
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// The reference graph can only be validated by the strict loader, which only
|
|
154
|
-
// understands the current versions. So run it exactly when every axis is
|
|
155
|
-
// already current (the plan would be a no-op); a throw there is genuine
|
|
156
|
-
// structural/reference corruption, never a version mismatch. An older/newer
|
|
157
|
-
// axis skips the loader and is decided purely by the planner below.
|
|
158
|
-
//
|
|
159
|
-
// Crucially, the strict loader is ONLY invoked when the manifest itself is
|
|
160
|
-
// `current`. An `unsupported` manifest (e.g. a pre-baseline Home with no
|
|
161
|
-
// recordVersions field) must never reach FileTaskStore: requireStorageSchema
|
|
162
|
-
// would throw StorageSchemaError, which doctor/upgrade must surface as a
|
|
163
|
-
// structured NEEDS_NEW_VERSION verdict, not as an invalid/corruption error.
|
|
164
|
-
const corruption = (schema.status === "current" && isFullyCurrent(source, latest))
|
|
165
|
-
? detectCurrentHomeCorruption(home)
|
|
166
|
-
: undefined;
|
|
167
|
-
const classification = classifyStorage(registry, source, latest, corruption);
|
|
168
|
-
return {
|
|
169
|
-
...base,
|
|
170
|
-
classification,
|
|
171
|
-
layoutVersion: schema.currentLayoutVersion,
|
|
172
|
-
aggregateVersion: schema.currentAggregateSchemaVersion,
|
|
173
|
-
...(incompatibleComponentOf(schema) === undefined
|
|
174
|
-
? {}
|
|
175
|
-
: { incompatibleComponent: incompatibleComponentOf(schema) })
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
function inspectLayout7SqliteMigrations(home) {
|
|
179
|
-
const db = new Database(join(home, "yui.db"), {
|
|
180
|
-
readonly: true,
|
|
181
|
-
fileMustExist: true
|
|
182
|
-
});
|
|
183
|
-
try {
|
|
184
|
-
db.pragma("query_only = ON");
|
|
185
|
-
return inspectSqliteSchemaMigrations(db);
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
db.close();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Load a Home whose every axis is already current through the strict store
|
|
193
|
-
* gate to detect real structural/reference corruption. This is only ever
|
|
194
|
-
* called when the source equals `latest` across all three axes, so a version
|
|
195
|
-
* error cannot occur here and any throw is genuine corruption (bad record
|
|
196
|
-
* shape, a broken reference graph, or a damaged SQLite database).
|
|
197
|
-
*
|
|
198
|
-
* A layout-7 Home whose authoritative store is `yui.db` is verified through
|
|
199
|
-
* {@link SqliteTaskStore}; a layout-7 Home that still uses the aggregate
|
|
200
|
-
* `state.json` (or a layout-6 Home in tests) is verified through
|
|
201
|
-
* {@link FileTaskStore}.
|
|
202
|
-
*/
|
|
203
|
-
function detectCurrentHomeCorruption(home) {
|
|
204
|
-
try {
|
|
205
|
-
if (existsSync(`${home}/yui.db`)) {
|
|
206
|
-
const store = new SqliteTaskStore(home);
|
|
207
|
-
try {
|
|
208
|
-
store.getConfig();
|
|
209
|
-
store.listTasks();
|
|
210
|
-
store.listProjects();
|
|
211
|
-
store.listConfiguredAgents();
|
|
212
|
-
store.listWorkMailboxes();
|
|
213
|
-
}
|
|
214
|
-
finally {
|
|
215
|
-
store.close();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
const store = new FileTaskStore(home);
|
|
220
|
-
store.getConfig();
|
|
221
|
-
store.listTasks();
|
|
222
|
-
store.listProjects();
|
|
223
|
-
store.listConfiguredAgents();
|
|
224
|
-
store.listWorkMailboxes();
|
|
225
|
-
}
|
|
226
|
-
return undefined;
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
if (error instanceof StorageRecordError) {
|
|
230
|
-
return { corrupted: true, detail: error.message };
|
|
231
|
-
}
|
|
232
|
-
// A SQLite-level error (corrupt database file, I/O fault) is structural
|
|
233
|
-
// damage, not a version mismatch.
|
|
234
|
-
if (error instanceof Error && (error.name === "SqliteError" || error.message.includes("SQLite"))) {
|
|
235
|
-
return { corrupted: true, detail: error.message };
|
|
236
|
-
}
|
|
237
|
-
// A non-record error (e.g. an unexpected I/O fault) is surfaced, not
|
|
238
|
-
// silently swallowed as "usable".
|
|
239
|
-
throw error;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
/** True when the source is already at `latest` on every axis (a no-op plan). */
|
|
243
|
-
function isFullyCurrent(source, latest) {
|
|
244
|
-
if (source.layout !== latest.layout || source.aggregate !== latest.aggregate) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
// Every family shared by source and target must already match; a family
|
|
248
|
-
// present on only one side is a difference the planner reasons about, not a
|
|
249
|
-
// "current" match, so it is not fully current.
|
|
250
|
-
const kinds = new Set([
|
|
251
|
-
...Object.keys(source.record),
|
|
252
|
-
...Object.keys(latest.record)
|
|
253
|
-
]);
|
|
254
|
-
for (const kind of kinds) {
|
|
255
|
-
const from = source.record[kind]?.version;
|
|
256
|
-
const to = latest.record[kind]?.version;
|
|
257
|
-
if (from !== to)
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
function incompatibleComponentOf(schema) {
|
|
263
|
-
return schema.status === "unsupported" ? schema.incompatibleComponent : undefined;
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Inspect the physical-backend invariant of a layout-7 Home (Issue 01):
|
|
267
|
-
* `yui.db` must exist and be healthy. Returns a classification verdict when
|
|
268
|
-
* the invariant is violated, or `undefined` when the Home is physically sound
|
|
269
|
-
* (the pure classifier then decides the version verdict).
|
|
270
|
-
*
|
|
271
|
-
* - manifest=7, no `yui.db`, `state.json` strictly readable →
|
|
272
|
-
* `NEEDS_STORAGE_REPAIR` (pseudo-layout-7);
|
|
273
|
-
* - manifest=7, no `yui.db`, no readable `state.json` → `CORRUPTED`
|
|
274
|
-
* (no authoritative backend);
|
|
275
|
-
* - `yui.db` unopenable or failing `PRAGMA quick_check` → `CORRUPTED`
|
|
276
|
-
* (damaged database);
|
|
277
|
-
* - both `state.json` and `yui.db` without a persistent migration receipt →
|
|
278
|
-
* `CORRUPTED` (dual-copy conflict; never guess which copy is newer).
|
|
279
|
-
*/
|
|
280
|
-
function inspectLayout7PhysicalBackend(home) {
|
|
281
|
-
const dbPath = join(home, "yui.db");
|
|
282
|
-
const statePath = join(home, STORAGE_STATE_FILE);
|
|
283
|
-
if (!existsSync(dbPath)) {
|
|
284
|
-
if (isReadableStateObject(statePath)) {
|
|
285
|
-
return {
|
|
286
|
-
verdict: "NEEDS_STORAGE_REPAIR",
|
|
287
|
-
status: "needs-storage-repair",
|
|
288
|
-
detail: "Storage declares layout 7 but has no yui.db; state.json is the only "
|
|
289
|
-
+ "authoritative copy (pseudo-layout-7). Run `yui upgrade` to rebuild "
|
|
290
|
-
+ "the SQLite database."
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
verdict: "CORRUPTED",
|
|
295
|
-
status: "unsupported",
|
|
296
|
-
detail: "Storage declares layout 7 but has neither yui.db nor a readable "
|
|
297
|
-
+ "state.json; there is no authoritative backend."
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
// `yui.db` exists: it must open and pass an integrity check.
|
|
301
|
-
try {
|
|
302
|
-
const db = new Database(dbPath, { readonly: true });
|
|
303
|
-
try {
|
|
304
|
-
const integrity = db.pragma("quick_check", { simple: true });
|
|
305
|
-
if (integrity !== "ok") {
|
|
306
|
-
return {
|
|
307
|
-
verdict: "CORRUPTED",
|
|
308
|
-
status: "unsupported",
|
|
309
|
-
detail: `SQLite integrity check failed: ${String(integrity)}.`
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
finally {
|
|
314
|
-
db.close();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
return {
|
|
319
|
-
verdict: "CORRUPTED",
|
|
320
|
-
status: "unsupported",
|
|
321
|
-
detail: `SQLite database cannot be opened: ${error instanceof Error ? error.message : String(error)}.`
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
// A healthy `yui.db` plus `state.json` is only legitimate right after a
|
|
325
|
-
// certified switch; without the persistent migration receipt it is a
|
|
326
|
-
// dual-copy conflict.
|
|
327
|
-
if (existsSync(statePath) && readMigrationReceipt(home) === null) {
|
|
328
|
-
return {
|
|
329
|
-
verdict: "CORRUPTED",
|
|
330
|
-
status: "unsupported",
|
|
331
|
-
detail: "Both state.json and yui.db exist without a migration receipt; the "
|
|
332
|
-
+ "authoritative copy is ambiguous. Restore one copy from a backup; do "
|
|
333
|
-
+ "not guess which is newer."
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
return undefined;
|
|
337
|
-
}
|
|
338
|
-
/** True when `state.json` exists and parses as a strict JSON object. */
|
|
339
|
-
function isReadableStateObject(statePath) {
|
|
340
|
-
if (!existsSync(statePath))
|
|
341
|
-
return false;
|
|
342
|
-
try {
|
|
343
|
-
const value = JSON.parse(readFileSync(statePath, "utf8"));
|
|
344
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
/** A structural corruption check over an already-read snapshot (for tests/reports). */
|
|
351
|
-
export function snapshotHasState(snapshot) {
|
|
352
|
-
return snapshot.state !== null;
|
|
353
|
-
}
|