@zq-silk/yui 0.6.0 → 0.6.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/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { spawn } from "node:child_process";
|
|
37
37
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
38
|
-
import { join } from "node:path";
|
|
38
|
+
import { dirname, join } from "node:path";
|
|
39
39
|
import { describeReport, runMigration } from "../migration/index.js";
|
|
40
40
|
import { validateCompatibleFileTaskStore } from "../compatibleTaskStore.js";
|
|
41
41
|
import { stopFileTaskController, ensureFileTaskController, ensureFileTaskControllerIdentity } from "../../controller/clientRuntime.js";
|
|
@@ -47,8 +47,11 @@ import { withUpgradeCoordinationLock } from "../upgradeCoordination.js";
|
|
|
47
47
|
import { clearUpgradeReceipt, writeUpgradeReceipt, upgradeReceiptPath } from "./upgradeReceipt.js";
|
|
48
48
|
import { switchProgressPath } from "./switchProgress.js";
|
|
49
49
|
import { classifyHome } from "./homeClassification.js";
|
|
50
|
-
import { createHomeMigrationTarget, describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime } from "./homeMigrationTarget.js";
|
|
50
|
+
import { createHomeMigrationTarget, describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState } from "./homeMigrationTarget.js";
|
|
51
|
+
import { repairPseudoLayout7 } from "./pseudoLayoutRepair.js";
|
|
51
52
|
import { createSqliteMigrationTarget } from "./sqliteMigrationTarget.js";
|
|
53
|
+
import { createSqliteRecordMigrationTarget } from "./sqliteRecordMigrationTarget.js";
|
|
54
|
+
import { COMMITTED_DATABASE_FILENAME } from "./sqliteStateMigration.js";
|
|
52
55
|
import { inspectOfflineUpgradeInventory } from "./offlineUpgradeInventory.js";
|
|
53
56
|
/**
|
|
54
57
|
* Run the storage upgrade for one Home. Never throws for an expected blocker;
|
|
@@ -92,6 +95,64 @@ export async function runStorageUpgrade(options) {
|
|
|
92
95
|
action: blocker.action
|
|
93
96
|
}, classification);
|
|
94
97
|
}
|
|
98
|
+
// A pseudo-layout-7 Home (manifest 7, no yui.db, readable state.json) needs
|
|
99
|
+
// the deterministic staged state.json→SQLite repair, not the version
|
|
100
|
+
// migration engine (Issue 01). The update preflight reports it as
|
|
101
|
+
// migration-required so a parent update can stop the old Controller first;
|
|
102
|
+
// dry-run stages and verifies without promoting; execute runs the full
|
|
103
|
+
// fenced repair with Controller lifecycle management.
|
|
104
|
+
if (verdict === "NEEDS_STORAGE_REPAIR") {
|
|
105
|
+
if (mode === "update-preflight") {
|
|
106
|
+
return {
|
|
107
|
+
outcome: "update-preflight",
|
|
108
|
+
status: "migration-required",
|
|
109
|
+
stepCount: 1,
|
|
110
|
+
classification
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (mode === "dry-run") {
|
|
114
|
+
const repairResult = repairPseudoLayout7({
|
|
115
|
+
home,
|
|
116
|
+
latest,
|
|
117
|
+
mode: "dry-run",
|
|
118
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
119
|
+
});
|
|
120
|
+
if (repairResult.outcome === "blocked") {
|
|
121
|
+
return withClassification({
|
|
122
|
+
outcome: "blocked",
|
|
123
|
+
stage: repairResult.stage,
|
|
124
|
+
message: repairResult.message,
|
|
125
|
+
action: repairResult.action
|
|
126
|
+
}, classification);
|
|
127
|
+
}
|
|
128
|
+
const inspected = inspectSourceVersionState(home, latest);
|
|
129
|
+
const source = "corruption" in inspected ? latest : inspected.source;
|
|
130
|
+
return {
|
|
131
|
+
outcome: "dry-run",
|
|
132
|
+
classification,
|
|
133
|
+
report: {
|
|
134
|
+
outcome: "dry-run",
|
|
135
|
+
mode: "dry-run",
|
|
136
|
+
source,
|
|
137
|
+
target: latest,
|
|
138
|
+
steps: [],
|
|
139
|
+
effects: [],
|
|
140
|
+
derived: { rebuiltEffects: [] },
|
|
141
|
+
validation: {
|
|
142
|
+
checks: [
|
|
143
|
+
{
|
|
144
|
+
name: "pseudo-layout-7 repair verification",
|
|
145
|
+
outcome: "passed",
|
|
146
|
+
detail: `verified ${repairResult.verifiedFamilies} record families against an `
|
|
147
|
+
+ "independent state.json re-read; staged database discarded"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return executePseudoLayout7Repair(options, classification, now);
|
|
155
|
+
}
|
|
95
156
|
// An all-compatible chain is an online-load contract, not a migration plan.
|
|
96
157
|
// Validate the declared old source shape and its in-memory normalization before
|
|
97
158
|
// ANY mode reports compatible, including the internal update preflight. This is
|
|
@@ -162,20 +223,36 @@ export async function runStorageUpgrade(options) {
|
|
|
162
223
|
return withClassification(offlineInventoryBlocker(inventory, true), classification);
|
|
163
224
|
}
|
|
164
225
|
}
|
|
165
|
-
// Select the migration target.
|
|
166
|
-
// control-plane layout (7) via the
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
226
|
+
// Select the migration target.
|
|
227
|
+
// - A layout-6 Home migrates to the SQLite control-plane layout (7) via the
|
|
228
|
+
// staged state.json→SQLite target.
|
|
229
|
+
// - A layout-7 Home whose authoritative store is `yui.db` migrates
|
|
230
|
+
// record/aggregate versions via the SQLite record target (state.json may
|
|
231
|
+
// have been archived by the pseudo-layout-7 repair).
|
|
232
|
+
// - A layout-6 Home with record-only migrations (no layout step) uses the
|
|
233
|
+
// file-document target.
|
|
234
|
+
const usesSqliteLayoutTarget = classification.layoutVersion === 6 && latest.layout === 7;
|
|
235
|
+
const usesSqliteRecordTarget = classification.layoutVersion === latest.layout
|
|
236
|
+
&& latest.layout >= 7;
|
|
237
|
+
const target = usesSqliteLayoutTarget
|
|
170
238
|
? createSqliteMigrationTarget({ home, latest, registry, now, callerPid })
|
|
171
|
-
:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
239
|
+
: usesSqliteRecordTarget
|
|
240
|
+
? createSqliteRecordMigrationTarget({
|
|
241
|
+
home,
|
|
242
|
+
latest,
|
|
243
|
+
registry,
|
|
244
|
+
now,
|
|
245
|
+
callerPid,
|
|
246
|
+
...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl })
|
|
247
|
+
})
|
|
248
|
+
: createHomeMigrationTarget({
|
|
249
|
+
home,
|
|
250
|
+
latest,
|
|
251
|
+
now,
|
|
252
|
+
callerPid,
|
|
253
|
+
...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl }),
|
|
254
|
+
...(options.switchFaultHook === undefined ? {} : { switchFaultHook: options.switchFaultHook })
|
|
255
|
+
});
|
|
179
256
|
// 2) A USABLE (already-current) Home has nothing to migrate; the engine
|
|
180
257
|
// confirms with a no-op and we never fence, drain, or switch.
|
|
181
258
|
if (verdict === "USABLE") {
|
|
@@ -499,6 +576,375 @@ async function execute(options, classification, target, callerPid, now) {
|
|
|
499
576
|
}
|
|
500
577
|
return result;
|
|
501
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Execute the pseudo-layout-7 repair with Controller lifecycle management
|
|
581
|
+
* (Issue 01). This mirrors the lifecycle of {@link execute} — fence, quiesce,
|
|
582
|
+
* repair, post-verify, restart — but runs the dedicated staged state.json→SQLite
|
|
583
|
+
* repair instead of the version-migration engine.
|
|
584
|
+
*/
|
|
585
|
+
async function executePseudoLayout7Repair(options, classification, now) {
|
|
586
|
+
const { home, latest } = options;
|
|
587
|
+
const callerPid = options.callerPid ?? process.pid;
|
|
588
|
+
let releaseFence;
|
|
589
|
+
try {
|
|
590
|
+
releaseFence = placeUpgradeFence(home, {
|
|
591
|
+
reason: "storage repair in progress",
|
|
592
|
+
createdAt: now().toISOString(),
|
|
593
|
+
ownerPid: callerPid
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
if (!(error instanceof UpgradeFenceError))
|
|
598
|
+
throw error;
|
|
599
|
+
return withClassification({
|
|
600
|
+
outcome: "blocked",
|
|
601
|
+
stage: "coordination",
|
|
602
|
+
message: `Repair coordination could not be acquired: ${error.message}`,
|
|
603
|
+
action: "Another upgrade is already coordinating this Home. Wait for it to finish, then retry."
|
|
604
|
+
}, classification);
|
|
605
|
+
}
|
|
606
|
+
const externallyQuiesced = options.controllerLifecycle === "externally-quiesced";
|
|
607
|
+
let controllerWasRunning = false;
|
|
608
|
+
let controllerStopConfirmed = false;
|
|
609
|
+
let controllerIdentity;
|
|
610
|
+
let result;
|
|
611
|
+
let unexpected;
|
|
612
|
+
const switchState = { committed: false };
|
|
613
|
+
try {
|
|
614
|
+
if (!externallyQuiesced) {
|
|
615
|
+
const controllerStatus = options.controllerStatus
|
|
616
|
+
?? ((h) => defaultControllerStatus(h, options.controllerOptions));
|
|
617
|
+
let status;
|
|
618
|
+
try {
|
|
619
|
+
status = await controllerStatus(home);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
result = withClassification(controllerLifecycleBlocker("Controller status could not be verified", error), classification);
|
|
623
|
+
}
|
|
624
|
+
if (result === undefined && !isControllerLifecycleStatus(status)) {
|
|
625
|
+
result = withClassification(controllerLifecycleBlocker("Controller status was malformed", new Error("expected a boolean running field")), classification);
|
|
626
|
+
}
|
|
627
|
+
if (result === undefined && status.running && !isControllerLaunchIdentity(status.identity)) {
|
|
628
|
+
result = withClassification(controllerLifecycleBlocker("Controller launch identity could not be authenticated", new Error("executable/argv/version identity is unavailable")), classification);
|
|
629
|
+
}
|
|
630
|
+
if (result === undefined && status.running && !isPositivePid(status.pid)) {
|
|
631
|
+
result = withClassification(controllerLifecycleBlocker("Controller PID could not be authenticated", new Error("a positive status PID is unavailable for fenced stop")), classification);
|
|
632
|
+
}
|
|
633
|
+
if (result === undefined) {
|
|
634
|
+
controllerWasRunning = status.running;
|
|
635
|
+
controllerIdentity = status.running ? status.identity : undefined;
|
|
636
|
+
controllerStopConfirmed = !controllerWasRunning;
|
|
637
|
+
}
|
|
638
|
+
const stopController = options.stopController
|
|
639
|
+
?? ((h, expectedPid) => defaultStopController(h, expectedPid, options.controllerOptions));
|
|
640
|
+
if (result === undefined && controllerWasRunning) {
|
|
641
|
+
try {
|
|
642
|
+
const expectedPid = status.pid;
|
|
643
|
+
const stopped = await stopController(home, expectedPid);
|
|
644
|
+
if (!confirmedControllerStopped(stopped, expectedPid)) {
|
|
645
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop did not confirm a drained process", new Error(`stop did not confirm captured PID ${expectedPid} with stopped:true`)), classification);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
controllerStopConfirmed = true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
result = withClassification(controllerLifecycleBlocker("Controller stop/drain failed", error), classification);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
controllerStopConfirmed = true;
|
|
658
|
+
}
|
|
659
|
+
if (result === undefined) {
|
|
660
|
+
try {
|
|
661
|
+
repinRevision(home);
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
result = withClassification({
|
|
665
|
+
outcome: "blocked",
|
|
666
|
+
stage: "active-runtime",
|
|
667
|
+
message: `The pre-fence writer window is undeterminable and could not be closed safely: ` +
|
|
668
|
+
`${messageOf(error)}.`,
|
|
669
|
+
action: "The Home was not repaired. Inspect the exact storage-lock owner and retry only " +
|
|
670
|
+
"after it is settled."
|
|
671
|
+
}, classification);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (result === undefined) {
|
|
675
|
+
const inventory = await readOfflineInventory(options, home);
|
|
676
|
+
if (inventory.total > 0) {
|
|
677
|
+
result = withClassification(offlineInventoryBlocker(inventory, false), classification);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (result === undefined) {
|
|
681
|
+
try {
|
|
682
|
+
result = executePseudoLayout7RepairFenced(options, classification, now, switchState);
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (switchState.committed) {
|
|
686
|
+
result = withClassification(postSwitchAmbiguity(home, switchState.backupPath, error), classification);
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
unexpected = error;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
try {
|
|
696
|
+
releaseFence();
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
if (switchState.committed && result === undefined) {
|
|
700
|
+
result = withClassification(postSwitchAmbiguity(home, switchState.backupPath, error), classification);
|
|
701
|
+
}
|
|
702
|
+
else if (unexpected === undefined) {
|
|
703
|
+
unexpected = error;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (unexpected !== undefined) {
|
|
708
|
+
if (switchState.committed) {
|
|
709
|
+
return withClassification(postSwitchAmbiguity(home, switchState.backupPath, unexpected), classification);
|
|
710
|
+
}
|
|
711
|
+
if (controllerWasRunning && controllerStopConfirmed) {
|
|
712
|
+
await restoreController(home, options, unexpected, controllerIdentity);
|
|
713
|
+
}
|
|
714
|
+
throw unexpected;
|
|
715
|
+
}
|
|
716
|
+
if (result === undefined) {
|
|
717
|
+
throw new Error("Storage repair did not produce a result.");
|
|
718
|
+
}
|
|
719
|
+
if (result.outcome === "upgraded") {
|
|
720
|
+
const startController = options.startController
|
|
721
|
+
?? ((h) => defaultStartController(h, options.controllerOptions));
|
|
722
|
+
if (!externallyQuiesced && controllerWasRunning) {
|
|
723
|
+
try {
|
|
724
|
+
await startController(home);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
return {
|
|
728
|
+
...withClassification(postSwitchAmbiguity(home, result.backupPath, new Error(`The replacement Controller could not start after the committed repair: ${messageOf(error)}`)), classification),
|
|
729
|
+
report: result.report
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
options.postSwitchFaultHook?.("receipt-clear");
|
|
735
|
+
clearUpgradeReceipt(home);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
return {
|
|
739
|
+
...withClassification(postSwitchAmbiguity(home, result.backupPath, error), classification),
|
|
740
|
+
report: result.report
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
if (!externallyQuiesced
|
|
746
|
+
&& controllerWasRunning
|
|
747
|
+
&& controllerStopConfirmed
|
|
748
|
+
&& result.outcome === "blocked"
|
|
749
|
+
&& result.stage !== "post-verify"
|
|
750
|
+
&& result.stage !== "switch-ambiguous") {
|
|
751
|
+
try {
|
|
752
|
+
await restoreController(home, options, new Error(result.message), controllerIdentity);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
return {
|
|
756
|
+
...withClassification({
|
|
757
|
+
outcome: "blocked",
|
|
758
|
+
stage: "active-runtime",
|
|
759
|
+
message: `${result.message} The previously running Controller could not be restored: ` +
|
|
760
|
+
`${messageOf(error)}.`,
|
|
761
|
+
action: "Keep the old Home quiesced and resolve the Controller startup failure; " +
|
|
762
|
+
"do not resume writes until the Controller and Home are verified."
|
|
763
|
+
}, classification),
|
|
764
|
+
...(result.report === undefined ? {} : { report: result.report })
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
/** Run the staged state.json→SQLite repair inside the upgrade fence. */
|
|
771
|
+
function executePseudoLayout7RepairFenced(options, classification, now, switchState) {
|
|
772
|
+
const { home, latest } = options;
|
|
773
|
+
const repairResult = repairPseudoLayout7({
|
|
774
|
+
home,
|
|
775
|
+
latest,
|
|
776
|
+
mode: "execute",
|
|
777
|
+
...(options.now === undefined ? {} : { now: options.now })
|
|
778
|
+
});
|
|
779
|
+
if (repairResult.outcome === "blocked") {
|
|
780
|
+
return withClassification({
|
|
781
|
+
outcome: "blocked",
|
|
782
|
+
stage: repairResult.stage,
|
|
783
|
+
message: repairResult.message,
|
|
784
|
+
action: repairResult.action
|
|
785
|
+
}, classification);
|
|
786
|
+
}
|
|
787
|
+
if (repairResult.outcome !== "repaired") {
|
|
788
|
+
// Execute mode never returns the dry-run variant; fail closed if it does.
|
|
789
|
+
return withClassification({
|
|
790
|
+
outcome: "blocked",
|
|
791
|
+
stage: "validate",
|
|
792
|
+
message: `Unexpected repair outcome: ${repairResult.outcome}`,
|
|
793
|
+
action: "Retry the repair; the Home was not changed."
|
|
794
|
+
}, classification);
|
|
795
|
+
}
|
|
796
|
+
// The database was promoted (atomic rename committed). From this point the
|
|
797
|
+
// old Home is no longer authoritative; a fault is a post-switch ambiguity.
|
|
798
|
+
switchState.committed = true;
|
|
799
|
+
switchState.backupPath = repairResult.stateBackupPath;
|
|
800
|
+
// Post-switch health check: open the promoted database through a fresh
|
|
801
|
+
// SqliteTaskStore and verify the core families (the repair already did a
|
|
802
|
+
// read-back, but this is the orchestrator's independent verification).
|
|
803
|
+
const postVerify = postSwitchHealthCheck(home);
|
|
804
|
+
if (postVerify !== null) {
|
|
805
|
+
return withClassification(postSwitchAmbiguity(home, repairResult.stateBackupPath, new Error(postVerify.message)), classification);
|
|
806
|
+
}
|
|
807
|
+
// Multi-phase (Issue 01 cross-issue handoff): the repair promoted yui.db but
|
|
808
|
+
// the database may still carry older record/aggregate versions. Re-classify
|
|
809
|
+
// the Home and, when MIGRATABLE, run the record-family migration in the same
|
|
810
|
+
// fenced window so one upgrade attempt reaches the current version. The
|
|
811
|
+
// Controller is already stopped and the fence is held, so no writer can
|
|
812
|
+
// observe the intermediate state.
|
|
813
|
+
const postRepairClassification = classifyHome({
|
|
814
|
+
home,
|
|
815
|
+
registry: options.registry,
|
|
816
|
+
latest
|
|
817
|
+
});
|
|
818
|
+
if (postRepairClassification.classification.verdict === "MIGRATABLE") {
|
|
819
|
+
const callerPid = options.callerPid ?? process.pid;
|
|
820
|
+
const target = createSqliteRecordMigrationTarget({
|
|
821
|
+
home,
|
|
822
|
+
latest,
|
|
823
|
+
registry: options.registry,
|
|
824
|
+
now,
|
|
825
|
+
callerPid,
|
|
826
|
+
...(options.renameImpl === undefined ? {} : { renameImpl: options.renameImpl })
|
|
827
|
+
});
|
|
828
|
+
const report = runMigration({
|
|
829
|
+
registry: options.registry,
|
|
830
|
+
target,
|
|
831
|
+
latest,
|
|
832
|
+
mode: "execute"
|
|
833
|
+
});
|
|
834
|
+
if (report.outcome === "failed") {
|
|
835
|
+
target.discardFreshOutput();
|
|
836
|
+
return {
|
|
837
|
+
...blockedFromFailedReport(report, postRepairClassification),
|
|
838
|
+
report
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
if (report.outcome === "switch-ambiguous") {
|
|
842
|
+
return {
|
|
843
|
+
...blockedFromSwitchAmbiguous(report, postRepairClassification),
|
|
844
|
+
report
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (report.outcome !== "migrated") {
|
|
848
|
+
target.discardFreshOutput();
|
|
849
|
+
return {
|
|
850
|
+
...blockedFromEngineReport(report, postRepairClassification),
|
|
851
|
+
report
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
// The record migration committed its own atomic switch. Verify the fully
|
|
855
|
+
// migrated database before writing the completion receipt.
|
|
856
|
+
switchState.backupPath = report.switch.backupPath ?? repairResult.stateBackupPath;
|
|
857
|
+
const postRecordVerify = postSwitchHealthCheck(home);
|
|
858
|
+
if (postRecordVerify !== null) {
|
|
859
|
+
return withClassification(postSwitchAmbiguity(home, report.switch.backupPath, new Error(postRecordVerify.message)), postRepairClassification);
|
|
860
|
+
}
|
|
861
|
+
options.postSwitchFaultHook?.("receipt-write");
|
|
862
|
+
writeUpgradeReceipt(home, {
|
|
863
|
+
switched: true,
|
|
864
|
+
homePath: home,
|
|
865
|
+
completedAt: now().toISOString(),
|
|
866
|
+
targetLayoutVersion: latest.layout,
|
|
867
|
+
targetAggregateVersion: latest.aggregate,
|
|
868
|
+
...(report.switch.backupPath === undefined
|
|
869
|
+
? {}
|
|
870
|
+
: { backupPath: report.switch.backupPath })
|
|
871
|
+
});
|
|
872
|
+
return {
|
|
873
|
+
outcome: "upgraded",
|
|
874
|
+
classification: postRepairClassification,
|
|
875
|
+
...(report.switch.backupPath === undefined
|
|
876
|
+
? {}
|
|
877
|
+
: { backupPath: report.switch.backupPath }),
|
|
878
|
+
report
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
// Fail closed: the repair promoted a healthy yui.db, but the pure version
|
|
882
|
+
// classifier still fences this Home (a future/pre-baseline record axis, or
|
|
883
|
+
// structural damage). Reporting "upgraded" would hide a blocker the old
|
|
884
|
+
// pure-classifier route surfaced before the physical-backend check moved
|
|
885
|
+
// ahead of it; the Controller's strict schema gate would then fail at
|
|
886
|
+
// startup with a less precise diagnosis. The repaired database is healthy,
|
|
887
|
+
// so a newer build (or a restored backup) can still proceed from here.
|
|
888
|
+
const postRepairVerdict = postRepairClassification.classification.verdict;
|
|
889
|
+
if (postRepairVerdict === "NEEDS_NEW_VERSION"
|
|
890
|
+
|| postRepairVerdict === "CORRUPTED") {
|
|
891
|
+
const detail = postRepairVerdict === "CORRUPTED"
|
|
892
|
+
? postRepairClassification.classification.detail
|
|
893
|
+
: postRepairClassification.classification.blocker.message;
|
|
894
|
+
return withClassification({
|
|
895
|
+
outcome: "blocked",
|
|
896
|
+
stage: postRepairVerdict === "CORRUPTED" ? "post-verify" : "missing-step",
|
|
897
|
+
message: postRepairVerdict === "CORRUPTED"
|
|
898
|
+
? `The repaired database is structurally damaged: ${detail}`
|
|
899
|
+
: `The repaired database still cannot be migrated by this build: ${detail}`,
|
|
900
|
+
action: postRepairVerdict === "CORRUPTED"
|
|
901
|
+
? "Restore the Home from a backup; do not start a Controller against the repaired database."
|
|
902
|
+
: "Install a newer Yui release and re-run `yui upgrade`; the repaired database is healthy and ready for the record migration."
|
|
903
|
+
}, postRepairClassification);
|
|
904
|
+
}
|
|
905
|
+
// Write the temporary upgrade receipt (for the update flow's ambiguity
|
|
906
|
+
// window); it is cleared by the caller after the Controller restarts.
|
|
907
|
+
options.postSwitchFaultHook?.("receipt-write");
|
|
908
|
+
writeUpgradeReceipt(home, {
|
|
909
|
+
switched: true,
|
|
910
|
+
homePath: home,
|
|
911
|
+
completedAt: now().toISOString(),
|
|
912
|
+
targetLayoutVersion: latest.layout,
|
|
913
|
+
targetAggregateVersion: latest.aggregate,
|
|
914
|
+
backupPath: repairResult.stateBackupPath
|
|
915
|
+
});
|
|
916
|
+
const inspected = inspectSourceVersionState(home, latest);
|
|
917
|
+
const source = "corruption" in inspected ? latest : inspected.source;
|
|
918
|
+
return {
|
|
919
|
+
outcome: "upgraded",
|
|
920
|
+
classification,
|
|
921
|
+
backupPath: repairResult.stateBackupPath,
|
|
922
|
+
report: {
|
|
923
|
+
outcome: "migrated",
|
|
924
|
+
mode: "execute",
|
|
925
|
+
source,
|
|
926
|
+
target: latest,
|
|
927
|
+
steps: [],
|
|
928
|
+
effects: [],
|
|
929
|
+
derived: { rebuiltEffects: [] },
|
|
930
|
+
validation: {
|
|
931
|
+
checks: [
|
|
932
|
+
{
|
|
933
|
+
name: "SQLite staged-database checksum verification",
|
|
934
|
+
outcome: "passed",
|
|
935
|
+
detail: `verified ${repairResult.verifiedFamilies} record families against an independent state.json re-read`
|
|
936
|
+
}
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
switch: {
|
|
940
|
+
status: "switched",
|
|
941
|
+
backupPath: repairResult.stateBackupPath,
|
|
942
|
+
detail: `SQLite database promoted and state.json backed up to ${repairResult.stateBackupPath}.`
|
|
943
|
+
},
|
|
944
|
+
completedAt: now().toISOString()
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
502
948
|
/** Execute the fenced, coordinated migration after Controller quiesce. */
|
|
503
949
|
function executeFenced(options, classification, target, callerPid, now, switchState) {
|
|
504
950
|
const { home, registry, latest } = options;
|
|
@@ -792,11 +1238,31 @@ function blockedFromSwitchAmbiguous(report, classification) {
|
|
|
792
1238
|
stage: "switch-ambiguous",
|
|
793
1239
|
message: `Storage switch is AMBIGUOUS and partially applied: ${report.error}`,
|
|
794
1240
|
action: `Do NOT assume the Home is unchanged. The original Home is at ${report.backupPath}; `
|
|
795
|
-
+ `restore it to recover:
|
|
1241
|
+
+ `restore it to recover: ${switchAmbiguousRestoreCommand(report)}. The interrupted `
|
|
796
1242
|
+ `switch marker "${switchProgressPath(report.homePath)}" records this ambiguity; verify `
|
|
797
1243
|
+ `with "yui doctor" after restoring.`
|
|
798
1244
|
}, classification);
|
|
799
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* The exact restore command for an ambiguous switch. A file-layout backup is a
|
|
1248
|
+
* sibling directory moved back over the Home; a SQLite backup is the committed
|
|
1249
|
+
* `yui.db` file moved aside *inside* the Home and must be renamed back onto the
|
|
1250
|
+
* database path, not onto the Home directory (which would nest the file).
|
|
1251
|
+
*/
|
|
1252
|
+
function switchAmbiguousRestoreCommand(report) {
|
|
1253
|
+
const committedDbPath = join(report.homePath, COMMITTED_DATABASE_FILENAME);
|
|
1254
|
+
// The degenerate SQLite case: no prior database existed, so there is no
|
|
1255
|
+
// backup to move back; the promoted database is already in place and the
|
|
1256
|
+
// recovery is to advance schema.json (or restore externally).
|
|
1257
|
+
if (report.backupPath === committedDbPath) {
|
|
1258
|
+
return `the database is already at ${committedDbPath}; advance schema.json `
|
|
1259
|
+
+ "record-family versions or restore from an external backup";
|
|
1260
|
+
}
|
|
1261
|
+
const restoreTarget = dirname(report.backupPath) === report.homePath
|
|
1262
|
+
? committedDbPath
|
|
1263
|
+
: report.homePath;
|
|
1264
|
+
return `mv "${report.backupPath}" "${restoreTarget}"`;
|
|
1265
|
+
}
|
|
800
1266
|
/**
|
|
801
1267
|
* A structured blocker for failures after the atomic Home switch. The receipt
|
|
802
1268
|
* and progress marker are named explicitly so an operator can reconcile the
|