@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.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -0,0 +1,54 @@
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
+ }
@@ -0,0 +1,31 @@
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,20 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { writeTextFileAtomically } from "./durableFile.js";
4
- /** Version of the on-disk layout (`schema.json`, root `state.json`, and locks). */
5
- export const CURRENT_STORAGE_LAYOUT_VERSION = 5;
6
- /** Version of the authoritative aggregate stored in `state.json`. */
7
- export const CURRENT_AGGREGATE_SCHEMA_VERSION = 1;
8
- /** @deprecated Use CURRENT_STORAGE_LAYOUT_VERSION for new code. */
9
- export const CURRENT_STORAGE_SCHEMA_VERSION = CURRENT_STORAGE_LAYOUT_VERSION;
4
+ import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "./storageVersions.js";
5
+ import { currentRecordVersions } from "./upgrade/recordVersions.js";
6
+ export { CURRENT_STORAGE_LAYOUT_VERSION, CURRENT_AGGREGATE_SCHEMA_VERSION };
10
7
  export const STORAGE_SCHEMA_FILE = "schema.json";
11
- /**
12
- * The dispatcher is intentionally present from the first v5 release. There
13
- * are no v4-to-v5 migrations: v5 is a fresh authoritative storage contract.
14
- */
15
- export const STORAGE_MIGRATIONS = Object.freeze([]);
16
- /** No historical aggregate is supported by this development release yet. */
17
- export const AGGREGATE_MIGRATIONS = Object.freeze([]);
18
8
  export class StorageSchemaError extends Error {
19
9
  code;
20
10
  constructor(code, message, options) {
@@ -35,65 +25,113 @@ export function inspectStorageSchema(rootDir) {
35
25
  manifestPath
36
26
  };
37
27
  }
28
+ let manifest;
38
29
  try {
39
- const manifest = parseStorageManifest(raw);
40
- if (manifest.storageVersion !== CURRENT_STORAGE_LAYOUT_VERSION) {
30
+ manifest = parseStorageManifest(raw);
31
+ }
32
+ catch (error) {
33
+ return {
34
+ status: "invalid",
35
+ latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
36
+ latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
37
+ latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
38
+ manifestPath,
39
+ detail: error instanceof Error ? error.message : String(error)
40
+ };
41
+ }
42
+ if (manifest.storageVersion !== CURRENT_STORAGE_LAYOUT_VERSION) {
43
+ return {
44
+ status: "unsupported",
45
+ incompatibleComponent: "layout",
46
+ direction: manifest.storageVersion < CURRENT_STORAGE_LAYOUT_VERSION ? "older" : "newer",
47
+ currentVersion: manifest.storageVersion,
48
+ latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
49
+ currentLayoutVersion: manifest.storageVersion,
50
+ latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
51
+ currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
52
+ latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
53
+ manifestPath
54
+ };
55
+ }
56
+ if (manifest.aggregateSchemaVersion !== CURRENT_AGGREGATE_SCHEMA_VERSION) {
57
+ return {
58
+ status: "unsupported",
59
+ incompatibleComponent: "aggregate",
60
+ direction: manifest.aggregateSchemaVersion < CURRENT_AGGREGATE_SCHEMA_VERSION
61
+ ? "older"
62
+ : "newer",
63
+ currentVersion: manifest.aggregateSchemaVersion,
64
+ latestVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
65
+ currentLayoutVersion: manifest.storageVersion,
66
+ latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
67
+ currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
68
+ latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
69
+ manifestPath
70
+ };
71
+ }
72
+ const currentRecords = currentRecordVersions();
73
+ if (manifest.recordVersions === undefined) {
74
+ return {
75
+ status: "unsupported",
76
+ incompatibleComponent: "record",
77
+ direction: "older",
78
+ currentVersion: 0,
79
+ latestVersion: 1,
80
+ currentLayoutVersion: manifest.storageVersion,
81
+ latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
82
+ currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
83
+ latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
84
+ manifestPath
85
+ };
86
+ }
87
+ for (const [kind, currentEntry] of Object.entries(currentRecords)) {
88
+ const persisted = manifest.recordVersions[kind];
89
+ if (persisted === undefined) {
41
90
  return {
42
91
  status: "unsupported",
43
- incompatibleComponent: "layout",
44
- direction: manifest.storageVersion < CURRENT_STORAGE_LAYOUT_VERSION ? "older" : "newer",
45
- currentVersion: manifest.storageVersion,
46
- latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
92
+ incompatibleComponent: "record",
93
+ direction: "older",
94
+ currentVersion: 0,
95
+ latestVersion: currentEntry.version,
47
96
  currentLayoutVersion: manifest.storageVersion,
48
97
  latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
49
98
  currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
50
99
  latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
100
+ recordFamily: kind,
51
101
  manifestPath
52
102
  };
53
103
  }
54
- if (manifest.aggregateSchemaVersion !== CURRENT_AGGREGATE_SCHEMA_VERSION) {
104
+ if (persisted !== currentEntry.version) {
55
105
  return {
56
106
  status: "unsupported",
57
- incompatibleComponent: "aggregate",
58
- direction: manifest.aggregateSchemaVersion < CURRENT_AGGREGATE_SCHEMA_VERSION
59
- ? "older"
60
- : "newer",
61
- currentVersion: manifest.aggregateSchemaVersion,
62
- latestVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
107
+ incompatibleComponent: "record",
108
+ direction: persisted < currentEntry.version ? "older" : "newer",
109
+ currentVersion: persisted,
110
+ latestVersion: currentEntry.version,
63
111
  currentLayoutVersion: manifest.storageVersion,
64
112
  latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
65
113
  currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
66
114
  latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
115
+ recordFamily: kind,
67
116
  manifestPath
68
117
  };
69
118
  }
70
- return {
71
- status: "current",
72
- currentVersion: manifest.storageVersion,
73
- latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
74
- currentLayoutVersion: manifest.storageVersion,
75
- latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
76
- currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
77
- latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
78
- activeGeneration: manifest.activeGeneration,
79
- manifestPath
80
- };
81
- }
82
- catch (error) {
83
- return {
84
- status: "invalid",
85
- latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
86
- latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
87
- latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
88
- manifestPath,
89
- detail: error instanceof Error ? error.message : String(error)
90
- };
91
119
  }
120
+ return {
121
+ status: "current",
122
+ currentVersion: manifest.storageVersion,
123
+ latestVersion: CURRENT_STORAGE_LAYOUT_VERSION,
124
+ currentLayoutVersion: manifest.storageVersion,
125
+ latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
126
+ currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
127
+ latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
128
+ manifestPath
129
+ };
92
130
  }
93
131
  export function ensureStorageSchema(rootDir, now = new Date()) {
94
132
  const state = inspectStorageSchema(rootDir);
95
133
  if (state.status === "uninitialized") {
96
- writeStorageManifest(rootDir, now);
134
+ writeCurrentStorageManifest(rootDir, now);
97
135
  return;
98
136
  }
99
137
  requireInspectedSchema(state);
@@ -101,70 +139,31 @@ export function ensureStorageSchema(rootDir, now = new Date()) {
101
139
  export function requireStorageSchema(rootDir) {
102
140
  requireInspectedSchema(inspectStorageSchema(rootDir));
103
141
  }
104
- export function dispatchStorageMigrations(rootDir, fromVersion, targetVersion = CURRENT_STORAGE_LAYOUT_VERSION, migrations = STORAGE_MIGRATIONS) {
105
- assertLayoutVersion(fromVersion, "Migration source");
106
- assertLayoutVersion(targetVersion, "Migration target");
107
- if (fromVersion > targetVersion) {
108
- throw unsupportedVersion(fromVersion, targetVersion, "layout");
109
- }
110
- // Build and validate the complete plan before invoking any filesystem mutation.
111
- const bySource = new Map();
112
- for (const migration of migrations) {
113
- assertLayoutVersion(migration.fromVersion, "Migration fromVersion");
114
- assertLayoutVersion(migration.toVersion, "Migration toVersion");
115
- if (migration.toVersion !== migration.fromVersion + 1) {
116
- throw invalidMigration(`Migration ${migration.fromVersion}->${migration.toVersion} must advance exactly one layout version.`);
117
- }
118
- if (bySource.has(migration.fromVersion)) {
119
- throw invalidMigration(`Duplicate migration from layout version ${migration.fromVersion}.`);
120
- }
121
- bySource.set(migration.fromVersion, migration);
122
- }
123
- const plan = [];
124
- for (let current = fromVersion; current < targetVersion; current += 1) {
125
- const migration = bySource.get(current);
126
- if (migration === undefined || migration.toVersion !== current + 1) {
127
- throw unsupportedVersion(current, targetVersion, "layout");
128
- }
129
- plan.push(migration);
130
- }
131
- for (const migration of plan)
132
- migration.migrate(rootDir);
142
+ /**
143
+ * Compatible readers may bypass only a record-axis mismatch. Layout,
144
+ * aggregate, syntax, and initialization remain strict.
145
+ */
146
+ export function requireCompatibleStorageSchema(rootDir) {
147
+ const state = inspectStorageSchema(rootDir);
148
+ if (state.status === "current")
149
+ return;
150
+ if (state.status === "unsupported"
151
+ && state.incompatibleComponent === "record"
152
+ && state.direction === "older")
153
+ return;
154
+ requireInspectedSchema(state);
133
155
  }
134
- export function dispatchAggregateMigrations(state, fromVersion, targetVersion = CURRENT_AGGREGATE_SCHEMA_VERSION, migrations = AGGREGATE_MIGRATIONS) {
135
- assertLayoutVersion(fromVersion, "Aggregate migration source");
136
- assertLayoutVersion(targetVersion, "Aggregate migration target");
137
- if (fromVersion > targetVersion) {
138
- throw unsupportedVersion(fromVersion, targetVersion, "aggregate");
156
+ /** Persist the current three-axis manifest through the existing atomic-file seam. */
157
+ export function writeCurrentStorageManifest(rootDir, now = new Date()) {
158
+ const recordVersions = {};
159
+ for (const [kind, entry] of Object.entries(currentRecordVersions())) {
160
+ recordVersions[kind] = entry.version;
139
161
  }
140
- const bySource = new Map();
141
- for (const migration of migrations) {
142
- assertLayoutVersion(migration.fromVersion, "Aggregate migration fromVersion");
143
- assertLayoutVersion(migration.toVersion, "Aggregate migration toVersion");
144
- if (migration.toVersion !== migration.fromVersion + 1) {
145
- throw invalidMigration(`Aggregate migration ${migration.fromVersion}->${migration.toVersion} must advance exactly one schema version.`);
146
- }
147
- if (bySource.has(migration.fromVersion)) {
148
- throw invalidMigration(`Duplicate migration from aggregate schema version ${migration.fromVersion}.`);
149
- }
150
- bySource.set(migration.fromVersion, migration);
151
- }
152
- const plan = [];
153
- for (let current = fromVersion; current < targetVersion; current += 1) {
154
- const migration = bySource.get(current);
155
- if (migration === undefined || migration.toVersion !== current + 1) {
156
- throw unsupportedVersion(current, targetVersion, "aggregate");
157
- }
158
- plan.push(migration);
159
- }
160
- return plan.reduce((current, migration) => migration.migrate(current), state);
161
- }
162
- function writeStorageManifest(rootDir, now) {
163
162
  const manifest = {
164
163
  schemaVersion: 1,
165
164
  storageVersion: CURRENT_STORAGE_LAYOUT_VERSION,
166
165
  aggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
167
- activeGeneration: null,
166
+ recordVersions,
168
167
  updatedAt: now.toISOString()
169
168
  };
170
169
  writeTextFileAtomically(join(rootDir, STORAGE_SCHEMA_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
@@ -178,11 +177,18 @@ function requireInspectedSchema(state) {
178
177
  case "invalid":
179
178
  throw new StorageSchemaError("STORAGE_SCHEMA_INVALID", `Invalid storage schema manifest at ${state.manifestPath}: ${state.detail}`);
180
179
  case "unsupported":
181
- throw unsupportedVersion(state.currentVersion, state.latestVersion, state.incompatibleComponent);
180
+ throw unsupportedVersion(state.currentVersion, state.latestVersion, state.incompatibleComponent, state.recordFamily);
182
181
  }
183
182
  }
184
- function unsupportedVersion(current, required, component) {
185
- const label = component === "layout" ? "Storage layout" : "Aggregate schema";
183
+ function unsupportedVersion(current, required, component, recordFamily) {
184
+ if (component === "record" && recordFamily === undefined) {
185
+ return new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", "This Home predates the durable record-version manifest and cannot be opened without explicit introduction migrations.");
186
+ }
187
+ const label = component === "layout"
188
+ ? "Storage layout"
189
+ : component === "aggregate"
190
+ ? "Aggregate schema"
191
+ : `Record family '${recordFamily}'`;
186
192
  if (current < required) {
187
193
  return new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", `${label} ${current} is older than required ${component} version ${required}; no migration is available in this Yui release.`);
188
194
  }
@@ -190,7 +196,13 @@ function unsupportedVersion(current, required, component) {
190
196
  }
191
197
  function parseStorageManifest(raw) {
192
198
  const value = parseJsonObject(raw, "Storage schema manifest");
193
- assertKeys(value, ["schemaVersion", "storageVersion", "aggregateSchemaVersion", "updatedAt"], ["activeGeneration"], "Storage schema manifest");
199
+ return parseStorageSchemaManifest(value);
200
+ }
201
+ /** Strictly parse one already-decoded manifest object through the shared contract. */
202
+ export function parseStorageSchemaManifest(value) {
203
+ if (!isRecord(value))
204
+ throw new Error("Storage schema manifest must be an object");
205
+ assertKeys(value, ["schemaVersion", "storageVersion", "aggregateSchemaVersion", "updatedAt"], ["recordVersions"], "Storage schema manifest");
194
206
  if (value.schemaVersion !== 1)
195
207
  throw new Error("schemaVersion must be 1");
196
208
  if (!Number.isInteger(value.storageVersion) || value.storageVersion < 1) {
@@ -200,21 +212,41 @@ function parseStorageManifest(raw) {
200
212
  if (!Number.isInteger(aggregateSchemaVersion) || aggregateSchemaVersion < 1) {
201
213
  throw new Error("aggregateSchemaVersion must be a positive integer");
202
214
  }
203
- const activeGeneration = value.activeGeneration ?? null;
204
- if (activeGeneration !== null) {
205
- throw new Error(`activeGeneration is not supported by storage layout ${String(value.storageVersion)}`);
206
- }
207
215
  if (typeof value.updatedAt !== "string" || !Number.isFinite(Date.parse(value.updatedAt))) {
208
216
  throw new Error("updatedAt must be an ISO timestamp");
209
217
  }
218
+ let recordVersions;
219
+ if (Object.hasOwn(value, "recordVersions")) {
220
+ if (!isRecord(value.recordVersions))
221
+ throw new Error("recordVersions must be an object");
222
+ const known = currentRecordVersions();
223
+ const kinds = Object.keys(value.recordVersions);
224
+ const unknown = kinds.filter((kind) => !Object.hasOwn(known, kind));
225
+ if (unknown.length > 0) {
226
+ throw new Error(`recordVersions has unknown family: ${unknown[0]}`);
227
+ }
228
+ const parsed = {};
229
+ for (const kind of kinds) {
230
+ const version = value.recordVersions[kind];
231
+ if (!Number.isInteger(version) || version < 1) {
232
+ throw new Error(`recordVersions['${kind}'] must be a positive integer`);
233
+ }
234
+ parsed[kind] = version;
235
+ }
236
+ recordVersions = Object.freeze(parsed);
237
+ }
210
238
  return {
211
239
  schemaVersion: 1,
212
240
  storageVersion: value.storageVersion,
213
241
  aggregateSchemaVersion: aggregateSchemaVersion,
214
- activeGeneration,
242
+ ...(recordVersions === undefined ? {} : { recordVersions }),
215
243
  updatedAt: value.updatedAt
216
244
  };
217
245
  }
246
+ /** Read and strictly parse the durable manifest through the same contract. */
247
+ export function readStorageSchemaManifest(rootDir) {
248
+ return parseStorageManifest(readFileSync(join(rootDir, STORAGE_SCHEMA_FILE), "utf8"));
249
+ }
218
250
  function parseJsonObject(raw, label) {
219
251
  let value;
220
252
  try {
@@ -236,14 +268,6 @@ function assertKeys(value, requiredKeys, optionalKeys, label) {
236
268
  if (missing.length > 0)
237
269
  throw new Error(`${label} is missing field: ${missing[0]}`);
238
270
  }
239
- function assertLayoutVersion(version, label) {
240
- if (!Number.isInteger(version) || version < 1) {
241
- throw invalidMigration(`${label} must be a positive integer.`);
242
- }
243
- }
244
- function invalidMigration(message) {
245
- return new StorageSchemaError("STORAGE_MIGRATION_INVALID", message);
246
- }
247
271
  function readOptionalText(path) {
248
272
  try {
249
273
  return readFileSync(path, "utf8");
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Scalar storage version constants.
3
+ *
4
+ * These live in their own module so that `storageSchema.ts`, `recordVersions.ts`,
5
+ * and `taskStore.ts` can all import them without creating a circular dependency.
6
+ * `storageSchema.ts` re-exports them for backward compatibility.
7
+ */
8
+ /** Version of the on-disk layout (`schema.json`, root `state.json`, and locks). */
9
+ export const CURRENT_STORAGE_LAYOUT_VERSION = 6;
10
+ /** Version of the authoritative aggregate stored in `state.json`. */
11
+ export const CURRENT_AGGREGATE_SCHEMA_VERSION = 17;