@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.
Files changed (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. 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
- }