@zq-silk/yui 0.2.0 → 0.4.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/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transactional migration engine.
|
|
3
|
+
*
|
|
4
|
+
* The engine is parameterized over the abstract {@link MigrationTarget} and the
|
|
5
|
+
* `latest` supported {@link StorageVersionState}; it never touches a real Home
|
|
6
|
+
* and holds no domain list of derived-state fields. All domain knowledge lives
|
|
7
|
+
* behind the injected `rebuildDerivedState` / `validateCurrentState` boundary,
|
|
8
|
+
* and every step only *declares* its effects.
|
|
9
|
+
*
|
|
10
|
+
* Flow (execute mode):
|
|
11
|
+
* inspect -> plan (reject on missing step) -> detectLiveRuntime (fail-closed
|
|
12
|
+
* if active) -> read immutable source -> apply step transforms into a fresh
|
|
13
|
+
* output -> rebuildDerivedState(effects) -> validateCurrentState() ->
|
|
14
|
+
* atomicSwitchWithBackup -> structured MigrationReport.
|
|
15
|
+
*
|
|
16
|
+
* Dry-run mode runs the same pipeline through the validation gate, then discards
|
|
17
|
+
* the fresh output and never switches.
|
|
18
|
+
*
|
|
19
|
+
* Invariants: on any failure the source is never switched and stays byte-for-
|
|
20
|
+
* byte unchanged, and — because input is immutable and output is fresh — an
|
|
21
|
+
* interrupted run is safely retriable (delete partial output and re-run). Once
|
|
22
|
+
* the source already equals `latest`, the run is a no-op (`already-current`).
|
|
23
|
+
*/
|
|
24
|
+
import { planMigration } from "./planner.js";
|
|
25
|
+
import { collectEffects, toStepSummary } from "./report.js";
|
|
26
|
+
import { AmbiguousSwitchError } from "./types.js";
|
|
27
|
+
/** Run the transactional migration and return a structured report. Never throws. */
|
|
28
|
+
export function runMigration(options) {
|
|
29
|
+
const { registry, target, latest, mode } = options;
|
|
30
|
+
// 1) inspect — read-only.
|
|
31
|
+
const source = target.inspectVersions();
|
|
32
|
+
// 2) plan — a missing adjacent step or a future version fails closed here.
|
|
33
|
+
const plan = planMigration(registry, source, latest);
|
|
34
|
+
if (plan.kind === "blocked") {
|
|
35
|
+
return { outcome: "blocked", mode, source, target: latest, blocker: plan.blocker };
|
|
36
|
+
}
|
|
37
|
+
if (plan.kind === "no-op") {
|
|
38
|
+
return { outcome: "already-current", mode, source, target: latest };
|
|
39
|
+
}
|
|
40
|
+
const stepSummaries = plan.steps.map(toStepSummary);
|
|
41
|
+
const effects = collectEffects(plan.steps);
|
|
42
|
+
// 3) detectLiveRuntime — refuse to migrate a target a live runtime is holding.
|
|
43
|
+
const live = target.detectLiveRuntime();
|
|
44
|
+
if (live.active) {
|
|
45
|
+
return {
|
|
46
|
+
outcome: "active-runtime",
|
|
47
|
+
mode,
|
|
48
|
+
source,
|
|
49
|
+
target: latest,
|
|
50
|
+
steps: stepSummaries,
|
|
51
|
+
...(live.detail ? { detail: live.detail } : {})
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// 4) apply steps to a fresh output; the source is only ever read.
|
|
55
|
+
let stage = "precondition";
|
|
56
|
+
const applied = [];
|
|
57
|
+
try {
|
|
58
|
+
let snapshot = target.readSource();
|
|
59
|
+
for (const planned of plan.steps) {
|
|
60
|
+
stage = "precondition";
|
|
61
|
+
planned.step.preconditions(snapshot);
|
|
62
|
+
stage = "transform";
|
|
63
|
+
const next = planned.step.transform(snapshot);
|
|
64
|
+
assertFreshOutput(next, snapshot, planned);
|
|
65
|
+
snapshot = next;
|
|
66
|
+
applied.push(toStepSummary(planned));
|
|
67
|
+
}
|
|
68
|
+
stage = "write-fresh-output";
|
|
69
|
+
target.writeFreshOutput(snapshot);
|
|
70
|
+
// 5) injected canonical derived-state rebuild over the fresh output.
|
|
71
|
+
stage = "rebuild";
|
|
72
|
+
const derived = target.rebuildDerivedState(effects);
|
|
73
|
+
// 6) injected canonical validation gate (the real post-migration loader).
|
|
74
|
+
stage = "validate";
|
|
75
|
+
const validation = target.validateCurrentState();
|
|
76
|
+
if (mode === "dry-run") {
|
|
77
|
+
// Validate only; never switch. Discard the staged output.
|
|
78
|
+
target.discardFreshOutput();
|
|
79
|
+
return {
|
|
80
|
+
outcome: "dry-run",
|
|
81
|
+
mode: "dry-run",
|
|
82
|
+
source,
|
|
83
|
+
target: latest,
|
|
84
|
+
steps: stepSummaries,
|
|
85
|
+
effects,
|
|
86
|
+
derived,
|
|
87
|
+
validation
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 7) atomic switch with timestamped backup — the only write to the source.
|
|
91
|
+
stage = "switch";
|
|
92
|
+
const switched = target.atomicSwitchWithBackup();
|
|
93
|
+
return {
|
|
94
|
+
outcome: "migrated",
|
|
95
|
+
mode: "execute",
|
|
96
|
+
source,
|
|
97
|
+
target: latest,
|
|
98
|
+
steps: stepSummaries,
|
|
99
|
+
effects,
|
|
100
|
+
derived,
|
|
101
|
+
validation,
|
|
102
|
+
switch: switched
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// A partially-applied, ambiguous switch is NOT a clean "source unchanged"
|
|
107
|
+
// failure: the original was moved aside and could not be restored. Surface it
|
|
108
|
+
// as its own outcome so the orchestrator reports a truthful manual recovery
|
|
109
|
+
// instead of a false "unchanged" (P1-4).
|
|
110
|
+
if (error instanceof AmbiguousSwitchError) {
|
|
111
|
+
return {
|
|
112
|
+
outcome: "switch-ambiguous",
|
|
113
|
+
mode,
|
|
114
|
+
source,
|
|
115
|
+
target: latest,
|
|
116
|
+
steps: stepSummaries,
|
|
117
|
+
homePath: error.homePath,
|
|
118
|
+
backupPath: error.backupPath,
|
|
119
|
+
stagingPath: error.stagingPath,
|
|
120
|
+
error: error.message
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
outcome: "failed",
|
|
125
|
+
mode,
|
|
126
|
+
source,
|
|
127
|
+
target: latest,
|
|
128
|
+
stage,
|
|
129
|
+
stepsApplied: applied,
|
|
130
|
+
error: error instanceof Error ? error.message : String(error)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Defend the pure `immutable-input -> fresh-output` contract: a transform must
|
|
136
|
+
* return a value distinct from its input (never mutate-and-return the source).
|
|
137
|
+
*/
|
|
138
|
+
function assertFreshOutput(next, previous, planned) {
|
|
139
|
+
if (next === previous &&
|
|
140
|
+
typeof next === "object" &&
|
|
141
|
+
next !== null) {
|
|
142
|
+
throw new Error(`Migration step ${planned.axis}` +
|
|
143
|
+
`${planned.recordKind ? `/${planned.recordKind}` : ""} ` +
|
|
144
|
+
`${planned.fromVersion}->${planned.toVersion} returned its input object; ` +
|
|
145
|
+
"transforms must produce a fresh output.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, pure storage-migration core.
|
|
3
|
+
*
|
|
4
|
+
* A future-facing, fully unit-testable migration framework over three
|
|
5
|
+
* independent monotonic version axes (layout / aggregate / record, where the
|
|
6
|
+
* record axis is a `recordKind -> {version, path}` map). The transactional engine
|
|
7
|
+
* is parameterized over an abstract {@link MigrationTarget}, so it neither
|
|
8
|
+
* touches a real Home nor hardcodes any domain derived-state list. Real wiring
|
|
9
|
+
* (the production step graph, canonical rebuild/validate boundary, and target)
|
|
10
|
+
* is injected elsewhere.
|
|
11
|
+
*/
|
|
12
|
+
import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storageVersions.js";
|
|
13
|
+
export * from "./types.js";
|
|
14
|
+
export { MigrationRegistry, MigrationRegistryError, createEmptyRegistry } from "./registry.js";
|
|
15
|
+
export { planMigration } from "./planner.js";
|
|
16
|
+
export { classifyStorage } from "./classifier.js";
|
|
17
|
+
export { runMigration } from "./engine.js";
|
|
18
|
+
export { collectEffects, describeReport, toStepSummary } from "./report.js";
|
|
19
|
+
export { BASELINE_STORAGE_LAYOUT_VERSION, BASELINE_AGGREGATE_SCHEMA_VERSION, BASELINE_RECORD_VERSIONS, BASELINE_STORAGE_VERSION_STATE, baselineStorageVersionState, assertBaselineConsistency } from "./baseline.js";
|
|
20
|
+
export { createProductionRegistry, createProductionStorageRegistry, assertRegistryCoversBaselineToCurrent } from "./productionRegistry.js";
|
|
21
|
+
export { loadCompatibleSnapshot, writeCurrentSnapshot, StorageCompatibilityError } from "./compatibleCodec.js";
|
|
22
|
+
/**
|
|
23
|
+
* The current baseline scalar versions, sourced from the single authoritative
|
|
24
|
+
* definitions in `storageSchema.ts` (never re-hardcoded here). Callers compose a
|
|
25
|
+
* full {@link StorageVersionState} by supplying the record-family map for the
|
|
26
|
+
* families they own — the generic core intentionally holds no record list.
|
|
27
|
+
*/
|
|
28
|
+
export function latestScalarVersions() {
|
|
29
|
+
return {
|
|
30
|
+
layout: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
31
|
+
aggregate: CURRENT_AGGREGATE_SCHEMA_VERSION
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cross-axis migration path planner.
|
|
3
|
+
*
|
|
4
|
+
* Given the source and target (latest-supported) {@link StorageVersionState}s and
|
|
5
|
+
* a {@link MigrationRegistry}, it produces a deterministically-ordered plan or a
|
|
6
|
+
* precise fail-closed blocker. It performs no I/O and interprets no domain data.
|
|
7
|
+
*
|
|
8
|
+
* Deterministic order encodes the axis dependency: `layout -> aggregate ->
|
|
9
|
+
* record`. Record families are nested inside the aggregate, so their steps run
|
|
10
|
+
* after the aggregate reaches its target version; record kinds are visited in
|
|
11
|
+
* sorted order and each family's steps ascend by version. Any single missing
|
|
12
|
+
* adjacent step, or any axis whose source is newer than supported, fails closed.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Plan the migration from `source` to `target`.
|
|
16
|
+
*
|
|
17
|
+
* Returns `no-op` when every axis is already current, `runnable` with a fully
|
|
18
|
+
* ordered adjacent step chain, or `blocked` with the first fail-closed reason in
|
|
19
|
+
* deterministic axis order (`layout`, then `aggregate`, then record kinds sorted).
|
|
20
|
+
*/
|
|
21
|
+
export function planMigration(registry, source, target) {
|
|
22
|
+
const ordered = [];
|
|
23
|
+
// 1) layout, then 2) aggregate — scalars migrate before any nested record.
|
|
24
|
+
for (const axis of ["layout", "aggregate"]) {
|
|
25
|
+
const chain = planScalarAxis(registry, axis, source[axis], target[axis]);
|
|
26
|
+
if (chain.kind === "blocked")
|
|
27
|
+
return { kind: "blocked", blocker: chain.blocker };
|
|
28
|
+
ordered.push(...chain.steps);
|
|
29
|
+
}
|
|
30
|
+
// 3) record families — nested inside the aggregate; visit target kinds in
|
|
31
|
+
// sorted order so the plan is deterministic. A target-only family starts at
|
|
32
|
+
// the explicit pre-introduction version 0 and therefore requires a registered
|
|
33
|
+
// `introduction` step. It is not an aggregate transform or an implicit
|
|
34
|
+
// no-op: the record family must pass the same fail-closed delivery gate as
|
|
35
|
+
// every later version transition.
|
|
36
|
+
const targetKinds = Object.keys(target.record).sort();
|
|
37
|
+
for (const recordKind of targetKinds) {
|
|
38
|
+
const sourceEntry = source.record[recordKind];
|
|
39
|
+
const chain = planRecordFamily(registry, recordKind, sourceEntry?.version ?? 0, target.record[recordKind].version);
|
|
40
|
+
if (chain.kind === "blocked")
|
|
41
|
+
return { kind: "blocked", blocker: chain.blocker };
|
|
42
|
+
ordered.push(...chain.steps);
|
|
43
|
+
}
|
|
44
|
+
return ordered.length === 0
|
|
45
|
+
? { kind: "no-op" }
|
|
46
|
+
: {
|
|
47
|
+
kind: "runnable",
|
|
48
|
+
path: ordered.some(({ transition }) => transition === "offline-migration")
|
|
49
|
+
? "offline-migration"
|
|
50
|
+
: "compatible",
|
|
51
|
+
steps: ordered
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function planScalarAxis(registry, axis, from, to) {
|
|
55
|
+
return planChain(registry, axis, undefined, from, to);
|
|
56
|
+
}
|
|
57
|
+
function planRecordFamily(registry, recordKind, from, to) {
|
|
58
|
+
return planChain(registry, "record", recordKind, from, to);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the adjacent step chain that advances one axis from `from` to `to`.
|
|
62
|
+
* A source newer than supported is `future-version`; a gap with no registered
|
|
63
|
+
* adjacent step is `missing-step`. Both carry a precise reason and action.
|
|
64
|
+
*/
|
|
65
|
+
function planChain(registry, axis, recordKind, from, to) {
|
|
66
|
+
const label = axisLabel(axis, recordKind);
|
|
67
|
+
if (from > to) {
|
|
68
|
+
return {
|
|
69
|
+
kind: "blocked",
|
|
70
|
+
blocker: {
|
|
71
|
+
reason: "future-version",
|
|
72
|
+
axis,
|
|
73
|
+
...(recordKind ? { recordKind } : {}),
|
|
74
|
+
found: from,
|
|
75
|
+
supported: to,
|
|
76
|
+
message: `${label} version ${from} is newer than this release supports (${to}).`,
|
|
77
|
+
action: "Upgrade to a newer Yui release that understands this version."
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const steps = [];
|
|
82
|
+
for (let version = from; version < to; version += 1) {
|
|
83
|
+
const declaration = registry.lookupDeclaration(axis, recordKind, version);
|
|
84
|
+
const registeredStep = registry.lookup(axis, recordKind, version);
|
|
85
|
+
if (declaration === undefined && registeredStep !== undefined) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "blocked",
|
|
88
|
+
blocker: {
|
|
89
|
+
reason: "missing-declaration",
|
|
90
|
+
axis,
|
|
91
|
+
...(recordKind ? { recordKind } : {}),
|
|
92
|
+
from: version,
|
|
93
|
+
to: version + 1,
|
|
94
|
+
message: `No compatible/offline-migration declaration is registered for ` +
|
|
95
|
+
`${label} ${version}->${version + 1}.`,
|
|
96
|
+
action: "Declare the adjacent change explicitly; do not infer compatibility from its version."
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (declaration === undefined) {
|
|
101
|
+
return {
|
|
102
|
+
kind: "blocked",
|
|
103
|
+
blocker: {
|
|
104
|
+
reason: "missing-step",
|
|
105
|
+
axis,
|
|
106
|
+
...(recordKind ? { recordKind } : {}),
|
|
107
|
+
from: version,
|
|
108
|
+
to: version + 1,
|
|
109
|
+
message: `No migration step is registered for ${label} ${version}->${version + 1}.`,
|
|
110
|
+
action: "This version cannot be migrated by this release; a newer release must provide the step."
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const step = declaration.kind === "compatible"
|
|
115
|
+
? {
|
|
116
|
+
axis,
|
|
117
|
+
...(recordKind ? { recordKind } : {}),
|
|
118
|
+
fromVersion: version,
|
|
119
|
+
toVersion: version + 1,
|
|
120
|
+
preconditions: declaration.validateSource,
|
|
121
|
+
transform: declaration.normalize,
|
|
122
|
+
declaredEffects: []
|
|
123
|
+
}
|
|
124
|
+
: registeredStep;
|
|
125
|
+
if (step === undefined) {
|
|
126
|
+
return {
|
|
127
|
+
kind: "blocked",
|
|
128
|
+
blocker: {
|
|
129
|
+
reason: "missing-step",
|
|
130
|
+
axis,
|
|
131
|
+
...(recordKind ? { recordKind } : {}),
|
|
132
|
+
from: version,
|
|
133
|
+
to: version + 1,
|
|
134
|
+
message: `No migration step is registered for ${label} ${version}->${version + 1}.`,
|
|
135
|
+
action: "This offline transition cannot run until this release provides its migration step."
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
steps.push({
|
|
140
|
+
axis,
|
|
141
|
+
...(recordKind ? { recordKind } : {}),
|
|
142
|
+
fromVersion: version,
|
|
143
|
+
toVersion: version + 1,
|
|
144
|
+
transition: declaration.kind,
|
|
145
|
+
step
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return { kind: "ok", steps };
|
|
149
|
+
}
|
|
150
|
+
function axisLabel(axis, recordKind) {
|
|
151
|
+
if (axis === "record")
|
|
152
|
+
return `record family '${recordKind ?? "?"}'`;
|
|
153
|
+
return axis === "layout" ? "storage layout" : "aggregate schema";
|
|
154
|
+
}
|