@zq-silk/yui 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +27 -12
- package/README.md +52 -52
- package/dist/cli/commandCatalog.js +1 -1
- package/dist/cli/updateCommand.js +17 -9
- package/dist/cli/updateOrchestrator.js +81 -15
- package/dist/cli/updatePorts.js +72 -10
- package/dist/cli/upgradeCommand.js +104 -19
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/core/controllerServer.js +5 -5
- package/dist/doctor/doctor.js +37 -14
- package/dist/observability/runtimeIdentity.js +48 -50
- package/dist/release/runtimeRelease.js +9 -1
- package/dist/runtime/exactControlPlane.js +20 -29
- package/dist/storage/currentTaskStore.js +6 -4
- package/dist/storage/sqliteSchema.js +134 -59
- package/dist/storage/sqliteStore.js +7 -5
- package/dist/storage/storageSchema.js +92 -223
- package/dist/storage/storageVersions.js +12 -16
- package/dist/storage/upgrade/upgradeOrchestrator.js +224 -62
- package/dist/version.js +3 -3
- package/docs/task-local-identity.md +9 -9
- package/i18n/README.zh-CN.md +26 -17
- package/package.json +1 -1
- package/dist/storage/upgrade/recordVersions.js +0 -82
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
/** `yui upgrade`
|
|
2
|
-
import { latestStorageVersionState } from "../storage/upgrade/recordVersions.js";
|
|
1
|
+
/** `yui upgrade` plans or applies the supported linear storage migration chain. */
|
|
3
2
|
import { runStorageUpgrade } from "../storage/upgrade/upgradeOrchestrator.js";
|
|
4
|
-
import {
|
|
3
|
+
import { ensureFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
|
|
4
|
+
import { runtimeError, usageError } from "../errors/cliError.js";
|
|
5
|
+
import { acquireHandoverLock, isForeignHandoverLockHeld, isHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
5
6
|
/**
|
|
6
7
|
* Run the upgrade command. Parses the public `[--dry-run]` form plus the staged
|
|
7
8
|
* updater's internal `--update-preflight` form, then returns rendered text,
|
|
8
9
|
* structured data, and an exit code (0 for a safe result, 5 for a blocker).
|
|
9
10
|
*/
|
|
10
|
-
export async function runUpgradeCommand(args, home) {
|
|
11
|
+
export async function runUpgradeCommand(args, home, environment = process.env) {
|
|
11
12
|
const mode = parseUpgradeArgs(args);
|
|
12
|
-
const result =
|
|
13
|
-
home,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const result = mode === "execute"
|
|
14
|
+
? await runInteractiveUpgrade(home, environment)
|
|
15
|
+
: mode === "update-apply"
|
|
16
|
+
? await runUpdateOwnedUpgrade(home, environment)
|
|
17
|
+
: await runStorageUpgrade({ home, mode });
|
|
17
18
|
return {
|
|
18
19
|
output: renderUpgradeResult(result, mode),
|
|
19
20
|
data: result,
|
|
20
|
-
exitCode: result.outcome === "blocked" ? 5 : 0
|
|
21
|
+
exitCode: result.outcome === "blocked" || result.outcome === "failed" ? 5 : 0
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
function parseUpgradeArgs(args) {
|
|
@@ -26,37 +27,121 @@ function parseUpgradeArgs(args) {
|
|
|
26
27
|
if (args.length === 1 && args[0] === "--dry-run")
|
|
27
28
|
return "dry-run";
|
|
28
29
|
// Intentionally omitted from public command help: this is the machine contract
|
|
29
|
-
// used by a staged `yui update`, not a replacement for user-facing dry-
|
|
30
|
+
// used by a staged `yui update`, not a replacement for user-facing dry-run.
|
|
30
31
|
if (args.length === 1 && args[0] === "--update-preflight")
|
|
31
32
|
return "update-preflight";
|
|
33
|
+
if (args.length === 1 && args[0] === "--update-apply")
|
|
34
|
+
return "update-apply";
|
|
32
35
|
throw usageError("Upgrade usage: yui upgrade [--dry-run]");
|
|
33
36
|
}
|
|
37
|
+
async function runInteractiveUpgrade(home, environment) {
|
|
38
|
+
const preflight = await runStorageUpgrade({ home, mode: "update-preflight" });
|
|
39
|
+
if (preflight.outcome !== "update-preflight"
|
|
40
|
+
|| preflight.status === "already-current") {
|
|
41
|
+
return runStorageUpgrade({ home, mode: "execute" });
|
|
42
|
+
}
|
|
43
|
+
const handover = acquireHandoverLock(home);
|
|
44
|
+
let controllerWasRunning = false;
|
|
45
|
+
try {
|
|
46
|
+
const stopped = await stopFileTaskController(home, {
|
|
47
|
+
environment,
|
|
48
|
+
handoverOwnerPid: process.pid
|
|
49
|
+
});
|
|
50
|
+
controllerWasRunning = stopped.stopped;
|
|
51
|
+
const result = await runStorageUpgrade({ home, mode: "execute" });
|
|
52
|
+
if (controllerWasRunning
|
|
53
|
+
&& (result.outcome === "upgraded" || result.outcome === "already-current")) {
|
|
54
|
+
try {
|
|
55
|
+
await ensureFileTaskController(home, {
|
|
56
|
+
environment,
|
|
57
|
+
handoverOwnerPid: process.pid
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw runtimeError(`Storage reached version ${result.report.targetVersion}, but the current `
|
|
62
|
+
+ `Controller could not restart: ${messageOf(error)} Backup: `
|
|
63
|
+
+ `${result.report.backupPath ?? "none"}. Keep the Home quiesced, inspect `
|
|
64
|
+
+ "Controller ownership, and start only the current Yui Controller.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
handover.release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runUpdateOwnedUpgrade(home, environment) {
|
|
74
|
+
const ownerText = environment.YUI_UPDATE_HANDOVER_OWNER_PID;
|
|
75
|
+
const ownerPid = ownerText === undefined ? Number.NaN : Number(ownerText);
|
|
76
|
+
if (!Number.isSafeInteger(ownerPid)
|
|
77
|
+
|| ownerPid < 1
|
|
78
|
+
|| process.ppid !== ownerPid
|
|
79
|
+
|| !isHandoverLockHeld(home)
|
|
80
|
+
|| isForeignHandoverLockHeld(home, ownerPid)) {
|
|
81
|
+
throw runtimeError("The internal update migration requires its direct parent to own the live "
|
|
82
|
+
+ "Controller handover lock.");
|
|
83
|
+
}
|
|
84
|
+
return runStorageUpgrade({ home, mode: "execute" });
|
|
85
|
+
}
|
|
34
86
|
/** Render an {@link UpgradeResult} as concise, CLI-style text. */
|
|
35
87
|
export function renderUpgradeResult(result, mode) {
|
|
36
88
|
const header = versionHeader(result);
|
|
37
89
|
switch (result.outcome) {
|
|
38
90
|
case "already-current":
|
|
39
91
|
return `${header}\nStorage is already at the current version; nothing to upgrade.`;
|
|
92
|
+
case "upgrade-plan":
|
|
93
|
+
return [
|
|
94
|
+
header,
|
|
95
|
+
`Upgrade plan: ${renderSteps(result.report.steps)}. Storage was not modified.`
|
|
96
|
+
].join("\n");
|
|
97
|
+
case "upgraded":
|
|
98
|
+
return [
|
|
99
|
+
header,
|
|
100
|
+
`Storage upgraded through ${renderSteps(result.report.steps)}.`,
|
|
101
|
+
`Backup: ${result.report.backupPath ?? "none"}`
|
|
102
|
+
].join("\n");
|
|
40
103
|
case "update-preflight":
|
|
41
104
|
return `${header}\nUpdate preflight: ${result.status} (${result.stepCount} steps). Storage was not modified.`;
|
|
42
105
|
case "blocked": {
|
|
43
106
|
return [
|
|
44
107
|
header,
|
|
45
|
-
`${mode === "dry-run"
|
|
108
|
+
`${mode === "dry-run"
|
|
109
|
+
? "Dry run"
|
|
110
|
+
: mode === "update-preflight"
|
|
111
|
+
? "Update preflight"
|
|
112
|
+
: "Upgrade"} blocked at ${result.stage}: ${result.message}`,
|
|
46
113
|
`Action: ${result.action}`,
|
|
47
114
|
"The authoritative Home is unchanged."
|
|
48
115
|
].join("\n");
|
|
49
116
|
}
|
|
117
|
+
case "failed":
|
|
118
|
+
return [
|
|
119
|
+
header,
|
|
120
|
+
`Upgrade failed at ${result.stage}: ${result.message}`,
|
|
121
|
+
`Action: ${result.action}`,
|
|
122
|
+
`Backup: ${result.backupPath ?? "none"}`,
|
|
123
|
+
result.sceneUnchanged
|
|
124
|
+
? "The authoritative Home was restored."
|
|
125
|
+
: "The authoritative Home may have changed; keep it quiesced."
|
|
126
|
+
].join("\n");
|
|
50
127
|
}
|
|
51
128
|
}
|
|
52
129
|
function versionHeader(result) {
|
|
53
130
|
const classification = result.classification;
|
|
54
|
-
const
|
|
55
|
-
|
|
131
|
+
const storage = classification.storageVersion === undefined
|
|
132
|
+
? "unknown"
|
|
133
|
+
: String(classification.storageVersion);
|
|
56
134
|
const verdict = classification.classification.verdict;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
135
|
+
return `Storage: ${verdict} version=${storage}/${classification.currentStorageVersion} `
|
|
136
|
+
+ `minimum=${classification.minimumSupportedStorageVersion}`;
|
|
137
|
+
}
|
|
138
|
+
function renderSteps(steps) {
|
|
139
|
+
if (steps.length === 0)
|
|
140
|
+
return "no migrations";
|
|
141
|
+
return steps
|
|
142
|
+
.map(({ fromVersion, toVersion, name }) => `${fromVersion}->${toVersion} ${name}`)
|
|
143
|
+
.join(", ");
|
|
144
|
+
}
|
|
145
|
+
function messageOf(error) {
|
|
146
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
147
|
}
|
|
@@ -201,7 +201,7 @@ export function renderRuntimeIdentitySection(input) {
|
|
|
201
201
|
` Entry digest ${build.entryDigest}`,
|
|
202
202
|
` Source commit ${build.sourceCommit}`,
|
|
203
203
|
` Node ${build.nodeVersion} (${build.platform})`,
|
|
204
|
-
` Storage
|
|
204
|
+
` Storage version ${storage.storageVersion} (status ${storage.storageStatus}, minimum ${storage.minimumStorageVersion}) · backend ${storage.configuredBackend} · worker ${storage.workerEnabled ? "on" : "off"}`,
|
|
205
205
|
` Store files state.json ${storage.physicalStateJson.present ? "present" : "absent"} · yui.db ${storage.physicalDatabase.present ? "present" : "absent"}${storage.physicalDatabase.wal ? " +WAL" : ""}${storage.physicalDatabase.present && storage.physicalDatabase.health !== UNSUPPORTED ? ` (${storage.physicalDatabase.health})` : ""}`
|
|
206
206
|
];
|
|
207
207
|
for (const finding of storage.findings) {
|
|
@@ -400,8 +400,8 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
|
|
|
400
400
|
homeFilesystemId,
|
|
401
401
|
controllerInstanceId,
|
|
402
402
|
version: YUI_VERSION,
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
storageVersion: yuiVersionIdentity().storageVersion,
|
|
404
|
+
minimumStorageVersion: yuiVersionIdentity().minimumStorageVersion,
|
|
405
405
|
...(status === undefined ? {} : { runtime: status(telemetry) })
|
|
406
406
|
}
|
|
407
407
|
});
|
|
@@ -675,7 +675,7 @@ export function buildRuntimeIdentityReceipt(input) {
|
|
|
675
675
|
}
|
|
676
676
|
})();
|
|
677
677
|
return Object.freeze({
|
|
678
|
-
schemaVersion:
|
|
678
|
+
schemaVersion: 2,
|
|
679
679
|
version: YUI_VERSION,
|
|
680
680
|
executablePath: process.execPath,
|
|
681
681
|
args: process.argv.slice(1),
|
|
@@ -685,8 +685,8 @@ export function buildRuntimeIdentityReceipt(input) {
|
|
|
685
685
|
cliRealpath: controllerCliRealpath(),
|
|
686
686
|
controllerRealpath: realpathSync(fileURLToPath(import.meta.url)),
|
|
687
687
|
controllerProtocolVersion: FILE_TASK_CONTROLLER_PROTOCOL_VERSION,
|
|
688
|
-
|
|
689
|
-
|
|
688
|
+
storageVersion: yuiVersionIdentity().storageVersion,
|
|
689
|
+
minimumStorageVersion: yuiVersionIdentity().minimumStorageVersion,
|
|
690
690
|
storageBackend: input.storageBackend,
|
|
691
691
|
workerEnabled: input.workerEnabled,
|
|
692
692
|
pid: process.pid,
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -34,7 +34,7 @@ export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
|
34
34
|
* healthy only when schema, compatibility, and state are all `ok`. Any
|
|
35
35
|
* `unsupported` (version mismatch / needs-new-version), `invalid` (corrupted /
|
|
36
36
|
* unreadable), or `missing` (uninitialized) storage check is blocking — even
|
|
37
|
-
*
|
|
37
|
+
* and the machine-readable command exits non-zero.
|
|
38
38
|
*/
|
|
39
39
|
export function summarizeStorageHealth(checks) {
|
|
40
40
|
const names = new Set(STORAGE_DOCTOR_CHECK_NAMES);
|
|
@@ -56,14 +56,16 @@ export function buildDoctorReport(env, executor) {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* Read the physical-backend facts for a Home (Issue 01 observability). Every
|
|
59
|
-
* field is best-effort read-only evidence: an unreadable database
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* field is best-effort read-only evidence: an unreadable database yields
|
|
60
|
+
* `null` for that field, and the `storage state` check reports the structural
|
|
61
|
+
* problem separately.
|
|
62
62
|
*/
|
|
63
63
|
function inspectStorageDetails(home, env) {
|
|
64
64
|
const schema = inspectStorageSchema(home);
|
|
65
|
-
const
|
|
66
|
-
|
|
65
|
+
const storageVersion = schema.status === "current"
|
|
66
|
+
|| schema.status === "upgradeable"
|
|
67
|
+
|| schema.status === "unsupported"
|
|
68
|
+
? schema.currentVersion
|
|
67
69
|
: null;
|
|
68
70
|
const authoritativeBackend = "sqlite";
|
|
69
71
|
const dbPath = join(home, CURRENT_DATABASE_FILENAME);
|
|
@@ -88,7 +90,8 @@ function inspectStorageDetails(home, env) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
return {
|
|
91
|
-
|
|
93
|
+
storageVersion,
|
|
94
|
+
minimumSupportedVersion: schema.minimumSupportedVersion,
|
|
92
95
|
authoritativeBackend,
|
|
93
96
|
databasePath,
|
|
94
97
|
journalMode,
|
|
@@ -216,13 +219,22 @@ function checkSchema(state) {
|
|
|
216
219
|
return {
|
|
217
220
|
name: "storage schema",
|
|
218
221
|
status: "ok",
|
|
219
|
-
detail: `current=${state.currentVersion} latest=${state.latestVersion}`
|
|
222
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
223
|
+
+ `minimum=${state.minimumSupportedVersion}`
|
|
224
|
+
};
|
|
225
|
+
case "upgradeable":
|
|
226
|
+
return {
|
|
227
|
+
name: "storage schema",
|
|
228
|
+
status: "unsupported",
|
|
229
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
230
|
+
+ `minimum=${state.minimumSupportedVersion} migration=available`
|
|
220
231
|
};
|
|
221
232
|
case "unsupported":
|
|
222
233
|
return {
|
|
223
234
|
name: "storage schema",
|
|
224
235
|
status: "unsupported",
|
|
225
|
-
detail: `current=${state.currentVersion} latest=${state.latestVersion}
|
|
236
|
+
detail: `current=${state.currentVersion} latest=${state.latestVersion} `
|
|
237
|
+
+ `minimum=${state.minimumSupportedVersion} direction=${state.direction}`
|
|
226
238
|
};
|
|
227
239
|
case "invalid":
|
|
228
240
|
return { name: "storage schema", status: "invalid", detail: state.detail };
|
|
@@ -251,9 +263,20 @@ function inspectCompatibility(home, homeCheck, schema) {
|
|
|
251
263
|
check: {
|
|
252
264
|
name,
|
|
253
265
|
status: "unsupported",
|
|
254
|
-
detail: `
|
|
255
|
-
+ `(current=${schema.currentVersion},
|
|
256
|
-
+
|
|
266
|
+
detail: `storage version is ${schema.direction} `
|
|
267
|
+
+ `(current=${schema.currentVersion}, supported=`
|
|
268
|
+
+ `${schema.minimumSupportedVersion}..${schema.latestVersion}).`
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (schema.status === "upgradeable") {
|
|
273
|
+
return {
|
|
274
|
+
storageStatus: "upgradeable",
|
|
275
|
+
check: {
|
|
276
|
+
name,
|
|
277
|
+
status: "unsupported",
|
|
278
|
+
detail: `migration available from ${schema.currentVersion} to ${schema.latestVersion}; `
|
|
279
|
+
+ "run `yui upgrade` or `yui update`."
|
|
257
280
|
}
|
|
258
281
|
};
|
|
259
282
|
}
|
|
@@ -262,8 +285,8 @@ function inspectCompatibility(home, homeCheck, schema) {
|
|
|
262
285
|
check: {
|
|
263
286
|
name,
|
|
264
287
|
status: "ok",
|
|
265
|
-
detail: `current
|
|
266
|
-
+ `
|
|
288
|
+
detail: `current=${schema.currentVersion}/${schema.latestVersion} `
|
|
289
|
+
+ `minimum=${schema.minimumSupportedVersion}`
|
|
267
290
|
}
|
|
268
291
|
};
|
|
269
292
|
}
|
|
@@ -13,7 +13,8 @@ import { spawnSync } from "node:child_process";
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import Database from "better-sqlite3";
|
|
16
|
-
import {
|
|
16
|
+
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
|
|
17
|
+
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
17
18
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
18
19
|
export const UNSUPPORTED = "unsupported";
|
|
19
20
|
/**
|
|
@@ -140,14 +141,7 @@ export function collectRuntimeBuildIdentity(ports) {
|
|
|
140
141
|
export function createProductionStorageIdentityPorts(env = process.env) {
|
|
141
142
|
return {
|
|
142
143
|
env,
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
return readFileSync(path, "utf8");
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
},
|
|
144
|
+
inspectStorage: (home) => inspectStorageSchema(home),
|
|
151
145
|
fileSize: (path) => {
|
|
152
146
|
try {
|
|
153
147
|
return statSync(path).size;
|
|
@@ -181,36 +175,12 @@ export function createProductionStorageIdentityPorts(env = process.env) {
|
|
|
181
175
|
* are never treated as an alternate authority or a repair source.
|
|
182
176
|
*/
|
|
183
177
|
export function collectStorageIdentity(home, ports = createProductionStorageIdentityPorts()) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
manifestStatus = "current";
|
|
191
|
-
try {
|
|
192
|
-
const manifest = JSON.parse(manifestText);
|
|
193
|
-
if (typeof manifest.storageVersion === "number"
|
|
194
|
-
&& Number.isFinite(manifest.storageVersion)) {
|
|
195
|
-
logicalLayout = manifest.storageVersion;
|
|
196
|
-
}
|
|
197
|
-
if (typeof manifest.aggregateSchemaVersion === "number"
|
|
198
|
-
&& Number.isFinite(manifest.aggregateSchemaVersion)) {
|
|
199
|
-
aggregateSchemaVersion = manifest.aggregateSchemaVersion;
|
|
200
|
-
}
|
|
201
|
-
if (typeof logicalLayout !== "number"
|
|
202
|
-
|| typeof aggregateSchemaVersion !== "number"
|
|
203
|
-
|| logicalLayout !== CURRENT_STORAGE_LAYOUT_VERSION
|
|
204
|
-
|| aggregateSchemaVersion !== CURRENT_AGGREGATE_SCHEMA_VERSION) {
|
|
205
|
-
manifestStatus = "unsupported";
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
manifestStatus = "invalid";
|
|
210
|
-
logicalLayout = UNSUPPORTED;
|
|
211
|
-
aggregateSchemaVersion = UNSUPPORTED;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
178
|
+
const storage = ports.inspectStorage(home);
|
|
179
|
+
const storageVersion = storage.status === "current"
|
|
180
|
+
|| storage.status === "upgradeable"
|
|
181
|
+
|| storage.status === "unsupported"
|
|
182
|
+
? storage.currentVersion
|
|
183
|
+
: UNSUPPORTED;
|
|
214
184
|
const statePath = join(home, "state.json");
|
|
215
185
|
const statePresent = ports.exists(statePath);
|
|
216
186
|
const stateBytes = ports.fileSize(statePath);
|
|
@@ -223,7 +193,7 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
223
193
|
const workerEnabled = resolveStoreWorkerEnabledForHome(home, ports.env);
|
|
224
194
|
const dbHealth = dbPresent ? ports.probeDatabaseHealth(dbPath) : null;
|
|
225
195
|
const findings = [];
|
|
226
|
-
if (
|
|
196
|
+
if (storage.status === "current" && !dbPresent) {
|
|
227
197
|
findings.push({
|
|
228
198
|
code: "current-database-missing",
|
|
229
199
|
severity: "contradiction",
|
|
@@ -231,20 +201,48 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
231
201
|
remediation: "Preserve this Home for diagnosis and initialize a new Home."
|
|
232
202
|
});
|
|
233
203
|
}
|
|
234
|
-
if (
|
|
204
|
+
if (storage.status === "uninitialized") {
|
|
205
|
+
findings.push({
|
|
206
|
+
code: "storage-uninitialized",
|
|
207
|
+
severity: "needs-repair",
|
|
208
|
+
message: "Yui storage has not been initialized for this Home.",
|
|
209
|
+
remediation: "Run `yui setup` for a new, empty Home."
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (storage.status === "upgradeable") {
|
|
213
|
+
findings.push({
|
|
214
|
+
code: "storage-upgrade-required",
|
|
215
|
+
severity: "needs-repair",
|
|
216
|
+
message: `Storage version ${storage.currentVersion} requires migration to `
|
|
217
|
+
+ `${CURRENT_STORAGE_VERSION}.`,
|
|
218
|
+
remediation: "Run `yui upgrade` or `yui update` before resuming writes."
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (storage.status === "unsupported") {
|
|
235
222
|
findings.push({
|
|
236
223
|
code: "unsupported-storage-contract",
|
|
237
224
|
severity: "contradiction",
|
|
238
|
-
message:
|
|
239
|
-
|
|
225
|
+
message: `Storage version ${storage.currentVersion} is outside the supported migration range `
|
|
226
|
+
+ `${MIN_SUPPORTED_STORAGE_VERSION}..${CURRENT_STORAGE_VERSION}.`,
|
|
227
|
+
remediation: storage.direction === "newer"
|
|
228
|
+
? "Use a newer Yui release."
|
|
229
|
+
: "Use a Yui release whose migration floor includes this Home."
|
|
240
230
|
});
|
|
241
231
|
}
|
|
242
|
-
if (
|
|
232
|
+
if (storage.status === "invalid") {
|
|
243
233
|
findings.push({
|
|
244
|
-
code: "invalid-storage
|
|
234
|
+
code: "invalid-storage",
|
|
245
235
|
severity: "contradiction",
|
|
246
|
-
message:
|
|
247
|
-
remediation: "Preserve this Home for diagnosis and
|
|
236
|
+
message: storage.detail,
|
|
237
|
+
remediation: "Preserve this Home for diagnosis and restore a known-good backup."
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (ports.exists(join(home, "schema.json"))) {
|
|
241
|
+
findings.push({
|
|
242
|
+
code: "ignored-legacy-storage-manifest",
|
|
243
|
+
severity: "warning",
|
|
244
|
+
message: "schema.json is legacy metadata and is not a storage authority.",
|
|
245
|
+
remediation: "A successful storage upgrade removes it automatically."
|
|
248
246
|
});
|
|
249
247
|
}
|
|
250
248
|
if (dbPresent && dbHealth !== null && dbHealth !== "ok") {
|
|
@@ -274,9 +272,9 @@ export function collectStorageIdentity(home, ports = createProductionStorageIden
|
|
|
274
272
|
}
|
|
275
273
|
return {
|
|
276
274
|
home,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
storageStatus: storage.status,
|
|
276
|
+
storageVersion,
|
|
277
|
+
minimumStorageVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
280
278
|
configuredBackend,
|
|
281
279
|
workerEnabled,
|
|
282
280
|
physicalStateJson: {
|
|
@@ -484,7 +484,7 @@ function validatePointer(value) {
|
|
|
484
484
|
function validateRuntimeIdentity(value) {
|
|
485
485
|
if (value === null
|
|
486
486
|
|| typeof value !== "object"
|
|
487
|
-
|| value.schemaVersion !==
|
|
487
|
+
|| value.schemaVersion !== 2
|
|
488
488
|
|| typeof value.version !== "string"
|
|
489
489
|
|| typeof value.executablePath !== "string"
|
|
490
490
|
|| value.executablePath === ""
|
|
@@ -492,6 +492,11 @@ function validateRuntimeIdentity(value) {
|
|
|
492
492
|
|| typeof value.buildId !== "string"
|
|
493
493
|
|| typeof value.cliRealpath !== "string"
|
|
494
494
|
|| typeof value.controllerRealpath !== "string"
|
|
495
|
+
|| !isPositiveInteger(value.controllerProtocolVersion)
|
|
496
|
+
|| !isPositiveInteger(value.storageVersion)
|
|
497
|
+
|| !isPositiveInteger(value.minimumStorageVersion)
|
|
498
|
+
|| value.minimumStorageVersion
|
|
499
|
+
> value.storageVersion
|
|
495
500
|
|| typeof value.pid !== "number"
|
|
496
501
|
|| typeof value.processStartIdentity !== "string"
|
|
497
502
|
|| (value.mode !== "primary"
|
|
@@ -520,6 +525,9 @@ function isHandoverPhase(value) {
|
|
|
520
525
|
function isStringArray(value) {
|
|
521
526
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
522
527
|
}
|
|
528
|
+
function isPositiveInteger(value) {
|
|
529
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
530
|
+
}
|
|
523
531
|
function isEexist(error) {
|
|
524
532
|
return isNodeError(error) && error.code === "EEXIST";
|
|
525
533
|
}
|
|
@@ -73,7 +73,7 @@ export function extractExactControlArgument(args) {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* One read-only gate shared by every managed Task control command. It verifies
|
|
76
|
-
* the frozen executable/CLI/Home/digest, protocol and
|
|
76
|
+
* the frozen executable/CLI/Home/digest, protocol and storage identity,
|
|
77
77
|
* on-disk schema, and any live Controller before command routing may construct
|
|
78
78
|
* a writable store. Package version alone may advance at the same managed path
|
|
79
79
|
* so an existing Session can cross an explicitly compatible in-place update.
|
|
@@ -92,15 +92,10 @@ export async function assertExactControlPlanePreflight(input, options = {}) {
|
|
|
92
92
|
if (storage.status !== "current") {
|
|
93
93
|
throw new Error(`Exact control-plane storage is not current: ${storage.status}.`);
|
|
94
94
|
}
|
|
95
|
-
if (storage.
|
|
96
|
-
throw new Error("Exact control-plane storage
|
|
97
|
-
+ `(expected ${descriptor.identity.
|
|
98
|
-
+ `${storage.
|
|
99
|
-
}
|
|
100
|
-
if (storage.currentAggregateSchemaVersion !== descriptor.identity.aggregateSchemaVersion) {
|
|
101
|
-
throw new Error("Exact control-plane aggregate schema does not match its frozen descriptor "
|
|
102
|
-
+ `(expected ${descriptor.identity.aggregateSchemaVersion}, found `
|
|
103
|
-
+ `${storage.currentAggregateSchemaVersion ?? "unknown"}).`);
|
|
95
|
+
if (storage.currentVersion !== descriptor.identity.storageVersion) {
|
|
96
|
+
throw new Error("Exact control-plane storage version does not match its frozen descriptor "
|
|
97
|
+
+ `(expected ${descriptor.identity.storageVersion}, found `
|
|
98
|
+
+ `${storage.currentVersion ?? "unknown"}).`);
|
|
104
99
|
}
|
|
105
100
|
// A frozen descriptor authenticates the command that created it; it no
|
|
106
101
|
// longer pins the Home's deployment pointer for the lifetime of a Session.
|
|
@@ -133,15 +128,10 @@ export async function assertCompatibleControlPlanePreflight(input, options = {})
|
|
|
133
128
|
if (storage.status !== "current") {
|
|
134
129
|
throw new Error(`Managed control-plane storage is not current: ${storage.status}.`);
|
|
135
130
|
}
|
|
136
|
-
if (storage.
|
|
137
|
-
throw new Error("Managed control-plane storage
|
|
138
|
-
+ `(expected ${identity.
|
|
139
|
-
+ `${storage.
|
|
140
|
-
}
|
|
141
|
-
if (storage.currentAggregateSchemaVersion !== identity.aggregateSchemaVersion) {
|
|
142
|
-
throw new Error("Managed control-plane aggregate schema is incompatible "
|
|
143
|
-
+ `(expected ${identity.aggregateSchemaVersion}, found `
|
|
144
|
-
+ `${storage.currentAggregateSchemaVersion ?? "unknown"}).`);
|
|
131
|
+
if (storage.currentVersion !== identity.storageVersion) {
|
|
132
|
+
throw new Error("Managed control-plane storage version is incompatible "
|
|
133
|
+
+ `(expected ${identity.storageVersion}, found `
|
|
134
|
+
+ `${storage.currentVersion ?? "unknown"}).`);
|
|
145
135
|
}
|
|
146
136
|
if (options.checkController !== false) {
|
|
147
137
|
const call = options.callController ?? defaultCallController;
|
|
@@ -162,29 +152,31 @@ export function assertControllerStatusIdentity(status, expected = yuiVersionIden
|
|
|
162
152
|
}
|
|
163
153
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
164
154
|
assertControllerField(status.version, expected.version, "version");
|
|
165
|
-
assertControllerField(status.
|
|
166
|
-
assertControllerField(status.
|
|
155
|
+
assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
|
|
156
|
+
assertControllerField(status.minimumStorageVersion, expected.minimumStorageVersion, "minimum storage migration version");
|
|
167
157
|
}
|
|
168
158
|
function validateVersionIdentity(value) {
|
|
169
159
|
if (!isRecord(value))
|
|
170
160
|
throw new Error("Yui version identity is invalid.");
|
|
171
161
|
const version = requireText(value.version, "Yui version");
|
|
172
162
|
const controllerProtocolVersion = requireVersion(value.controllerProtocolVersion, "Controller protocol version");
|
|
173
|
-
const
|
|
174
|
-
const
|
|
163
|
+
const storageVersion = requireVersion(value.storageVersion, "Storage version");
|
|
164
|
+
const minimumStorageVersion = requireVersion(value.minimumStorageVersion, "Minimum storage migration version");
|
|
165
|
+
if (minimumStorageVersion > storageVersion) {
|
|
166
|
+
throw new Error("Minimum storage migration version cannot exceed the current storage version.");
|
|
167
|
+
}
|
|
175
168
|
return {
|
|
176
169
|
version,
|
|
177
170
|
controllerProtocolVersion,
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
storageVersion,
|
|
172
|
+
minimumStorageVersion
|
|
180
173
|
};
|
|
181
174
|
}
|
|
182
175
|
/** Managed continuity is a protocol/storage contract, not a package pin. */
|
|
183
176
|
function assertContinuityIdentity(label, expected, actual) {
|
|
184
177
|
for (const field of [
|
|
185
178
|
"controllerProtocolVersion",
|
|
186
|
-
"
|
|
187
|
-
"aggregateSchemaVersion"
|
|
179
|
+
"storageVersion"
|
|
188
180
|
]) {
|
|
189
181
|
if (expected[field] !== actual[field]) {
|
|
190
182
|
throw new Error(`${label} ${field} does not match the frozen control plane `
|
|
@@ -200,8 +192,7 @@ function assertControllerContinuityIdentity(status, expected) {
|
|
|
200
192
|
throw new Error("Controller version is invalid at the managed continuity gate.");
|
|
201
193
|
}
|
|
202
194
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
203
|
-
assertControllerField(status.
|
|
204
|
-
assertControllerField(status.aggregateSchemaVersion, expected.aggregateSchemaVersion, "aggregate schema");
|
|
195
|
+
assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
|
|
205
196
|
}
|
|
206
197
|
function assertControllerField(actual, expected, label) {
|
|
207
198
|
if (actual !== expected) {
|
|
@@ -2,12 +2,11 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { StorageRecordError } from "./taskStore.js";
|
|
4
4
|
import { readSqliteHomeIdentity, SqliteTaskStore } from "./sqliteStore.js";
|
|
5
|
-
import {
|
|
6
|
-
export
|
|
5
|
+
import { CURRENT_DATABASE_FILENAME, inspectStorageSchema } from "./storageSchema.js";
|
|
6
|
+
export { CURRENT_DATABASE_FILENAME } from "./storageSchema.js";
|
|
7
7
|
/** Initialize a new Home, or open an existing Home at the exact current contract. */
|
|
8
8
|
export function initializeCurrentTaskStore(home) {
|
|
9
9
|
if (inspectStorageSchema(home).status === "uninitialized") {
|
|
10
|
-
ensureStorageSchema(home);
|
|
11
10
|
return new SqliteTaskStore(home);
|
|
12
11
|
}
|
|
13
12
|
return openCurrentTaskStore(home);
|
|
@@ -16,7 +15,10 @@ export function initializeCurrentTaskStore(home) {
|
|
|
16
15
|
export function openCurrentTaskStore(home) {
|
|
17
16
|
const schema = inspectStorageSchema(home);
|
|
18
17
|
if (schema.status !== "current") {
|
|
19
|
-
throw new StorageRecordError(
|
|
18
|
+
throw new StorageRecordError(schema.status === "upgradeable"
|
|
19
|
+
? `This Home uses storage version ${schema.currentVersion}; run \`yui upgrade\` `
|
|
20
|
+
+ `to reach ${schema.latestVersion}.`
|
|
21
|
+
: "This Home does not use a supported storage contract.");
|
|
20
22
|
}
|
|
21
23
|
if (!existsSync(join(home, CURRENT_DATABASE_FILENAME))) {
|
|
22
24
|
throw new StorageRecordError("The current Home is incomplete: yui.db is missing. Preserve it for diagnosis and initialize a new Home.");
|