@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,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
export {
|
|
7
|
-
export const
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { inspectSqliteSchemaMigrations, storageMigrationPlan } from "./sqliteSchema.js";
|
|
5
|
+
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
|
|
6
|
+
export { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "./storageVersions.js";
|
|
7
|
+
export const CURRENT_DATABASE_FILENAME = "yui.db";
|
|
8
8
|
export class StorageSchemaError extends Error {
|
|
9
9
|
code;
|
|
10
10
|
constructor(code, message, options) {
|
|
@@ -13,250 +13,119 @@ export class StorageSchemaError extends Error {
|
|
|
13
13
|
this.code = code;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Inspect the one authoritative SQLite migration head without changing it.
|
|
18
|
+
*
|
|
19
|
+
* `schema.json` and `state.json` are recognized only to prevent setup from
|
|
20
|
+
* overwriting an old Home whose SQLite authority is missing. They are not
|
|
21
|
+
* version authorities for current Homes.
|
|
22
|
+
*/
|
|
16
23
|
export function inspectStorageSchema(rootDir) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
24
|
+
const databasePath = join(rootDir, CURRENT_DATABASE_FILENAME);
|
|
25
|
+
const hasPreBaselineEvidence = existsSync(join(rootDir, "schema.json")) || existsSync(join(rootDir, "state.json"));
|
|
26
|
+
if (hasPreBaselineEvidence) {
|
|
20
27
|
return {
|
|
21
|
-
status: "
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
status: "unsupported",
|
|
29
|
+
direction: "older",
|
|
30
|
+
currentVersion: 0,
|
|
31
|
+
latestVersion: CURRENT_STORAGE_VERSION,
|
|
32
|
+
minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
33
|
+
databasePath
|
|
26
34
|
};
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
if (!existsSync(databasePath)) {
|
|
37
|
+
if (existsSync(rootDir)) {
|
|
38
|
+
try {
|
|
39
|
+
if (readdirSync(rootDir).length > 0) {
|
|
40
|
+
return invalid(databasePath, new Error("The authoritative yui.db is missing from a non-empty Home."));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return invalid(databasePath, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
33
47
|
return {
|
|
34
|
-
status: "
|
|
35
|
-
latestVersion:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
manifestPath,
|
|
39
|
-
detail: error instanceof Error ? error.message : String(error)
|
|
48
|
+
status: "uninitialized",
|
|
49
|
+
latestVersion: CURRENT_STORAGE_VERSION,
|
|
50
|
+
minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
51
|
+
databasePath
|
|
40
52
|
};
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
};
|
|
54
|
+
let database;
|
|
55
|
+
try {
|
|
56
|
+
database = new Database(databasePath, { readonly: true, fileMustExist: true });
|
|
55
57
|
}
|
|
56
|
-
|
|
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
|
-
};
|
|
58
|
+
catch (error) {
|
|
59
|
+
return invalid(databasePath, error);
|
|
71
60
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
61
|
+
try {
|
|
62
|
+
const migration = inspectSqliteSchemaMigrations(database);
|
|
63
|
+
const fields = {
|
|
64
|
+
currentVersion: migration.currentVersion,
|
|
65
|
+
latestVersion: CURRENT_STORAGE_VERSION,
|
|
66
|
+
minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
67
|
+
databasePath
|
|
85
68
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
return {
|
|
91
|
-
status: "unsupported",
|
|
92
|
-
incompatibleComponent: "record",
|
|
93
|
-
direction: "older",
|
|
94
|
-
currentVersion: 0,
|
|
95
|
-
latestVersion: currentEntry.version,
|
|
96
|
-
currentLayoutVersion: manifest.storageVersion,
|
|
97
|
-
latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
98
|
-
currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
|
|
99
|
-
latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
|
|
100
|
-
recordFamily: kind,
|
|
101
|
-
manifestPath
|
|
102
|
-
};
|
|
69
|
+
if (migration.currentVersion === CURRENT_STORAGE_VERSION) {
|
|
70
|
+
return { status: "current", ...fields };
|
|
71
|
+
}
|
|
72
|
+
if (migration.currentVersion > CURRENT_STORAGE_VERSION) {
|
|
73
|
+
return { status: "unsupported", direction: "newer", ...fields };
|
|
103
74
|
}
|
|
104
|
-
if (
|
|
75
|
+
if (migration.currentVersion < MIN_SUPPORTED_STORAGE_VERSION) {
|
|
76
|
+
return { status: "unsupported", direction: "older", ...fields };
|
|
77
|
+
}
|
|
78
|
+
const plan = storageMigrationPlan(migration.currentVersion);
|
|
79
|
+
if (plan === null) {
|
|
105
80
|
return {
|
|
106
|
-
status: "
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
latestLayoutVersion: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
113
|
-
currentAggregateSchemaVersion: manifest.aggregateSchemaVersion,
|
|
114
|
-
latestAggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
|
|
115
|
-
recordFamily: kind,
|
|
116
|
-
manifestPath
|
|
81
|
+
status: "invalid",
|
|
82
|
+
latestVersion: CURRENT_STORAGE_VERSION,
|
|
83
|
+
minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
84
|
+
databasePath,
|
|
85
|
+
detail: `Storage migration registry has no complete path from `
|
|
86
|
+
+ `${migration.currentVersion} to ${CURRENT_STORAGE_VERSION}.`
|
|
117
87
|
};
|
|
118
88
|
}
|
|
89
|
+
return {
|
|
90
|
+
status: "upgradeable",
|
|
91
|
+
pendingVersions: plan.map(({ toVersion }) => toVersion),
|
|
92
|
+
...fields
|
|
93
|
+
};
|
|
119
94
|
}
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
};
|
|
130
|
-
}
|
|
131
|
-
export function ensureStorageSchema(rootDir, now = new Date()) {
|
|
132
|
-
const state = inspectStorageSchema(rootDir);
|
|
133
|
-
if (state.status === "uninitialized") {
|
|
134
|
-
writeCurrentStorageManifest(rootDir, now);
|
|
135
|
-
return;
|
|
95
|
+
catch (error) {
|
|
96
|
+
return invalid(databasePath, error);
|
|
136
97
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
/** Persist the current three-axis manifest through the existing atomic-file seam. */
|
|
140
|
-
export function writeCurrentStorageManifest(rootDir, now = new Date()) {
|
|
141
|
-
const recordVersions = {};
|
|
142
|
-
for (const [kind, entry] of Object.entries(currentRecordVersions())) {
|
|
143
|
-
recordVersions[kind] = entry.version;
|
|
98
|
+
finally {
|
|
99
|
+
database.close();
|
|
144
100
|
}
|
|
145
|
-
const manifest = {
|
|
146
|
-
schemaVersion: 1,
|
|
147
|
-
storageVersion: CURRENT_STORAGE_LAYOUT_VERSION,
|
|
148
|
-
aggregateSchemaVersion: CURRENT_AGGREGATE_SCHEMA_VERSION,
|
|
149
|
-
recordVersions,
|
|
150
|
-
updatedAt: now.toISOString()
|
|
151
|
-
};
|
|
152
|
-
writeTextFileAtomically(join(rootDir, STORAGE_SCHEMA_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
153
101
|
}
|
|
154
|
-
|
|
102
|
+
/** Require the current storage contract without normalizing historical data. */
|
|
103
|
+
export function requireCurrentStorageSchema(rootDir) {
|
|
104
|
+
const state = inspectStorageSchema(rootDir);
|
|
155
105
|
switch (state.status) {
|
|
156
106
|
case "current":
|
|
157
107
|
return;
|
|
158
108
|
case "uninitialized":
|
|
159
109
|
throw new StorageSchemaError("STORAGE_UNINITIALIZED", "Yui storage is not initialized. Run `yui setup`.");
|
|
160
110
|
case "invalid":
|
|
161
|
-
throw new StorageSchemaError("STORAGE_SCHEMA_INVALID", `Invalid storage
|
|
111
|
+
throw new StorageSchemaError("STORAGE_SCHEMA_INVALID", `Invalid storage at ${state.databasePath}: ${state.detail}`);
|
|
112
|
+
case "upgradeable":
|
|
113
|
+
throw new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", `Storage version ${state.currentVersion} requires an explicit upgrade to `
|
|
114
|
+
+ `${state.latestVersion}. Run \`yui upgrade\` or \`yui update\`.`);
|
|
162
115
|
case "unsupported":
|
|
163
|
-
throw
|
|
116
|
+
throw new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", state.direction === "newer"
|
|
117
|
+
? `Storage version ${state.currentVersion} is newer than supported `
|
|
118
|
+
+ `${state.latestVersion}; use a newer Yui release.`
|
|
119
|
+
: `Storage version ${state.currentVersion} is older than the minimum supported `
|
|
120
|
+
+ `${state.minimumSupportedVersion}.`);
|
|
164
121
|
}
|
|
165
122
|
}
|
|
166
|
-
function
|
|
167
|
-
if (component === "record" && recordFamily === undefined) {
|
|
168
|
-
return new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", "This Home predates the current record-version manifest. Preserve it for inspection and initialize a new Home.");
|
|
169
|
-
}
|
|
170
|
-
const label = component === "layout"
|
|
171
|
-
? "Storage layout"
|
|
172
|
-
: component === "aggregate"
|
|
173
|
-
? "Aggregate schema"
|
|
174
|
-
: `Record family '${recordFamily}'`;
|
|
175
|
-
if (current < required) {
|
|
176
|
-
return new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", `${label} ${current} is older than required ${component} version ${required}; use the matching Yui version or initialize a new Home.`);
|
|
177
|
-
}
|
|
178
|
-
return new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", `${label} ${current} is newer than supported ${component} version ${required}; use a newer Yui release.`);
|
|
179
|
-
}
|
|
180
|
-
function parseStorageManifest(raw) {
|
|
181
|
-
const value = parseJsonObject(raw, "Storage schema manifest");
|
|
182
|
-
return parseStorageSchemaManifest(value);
|
|
183
|
-
}
|
|
184
|
-
/** Strictly parse one already-decoded manifest object through the shared contract. */
|
|
185
|
-
export function parseStorageSchemaManifest(value) {
|
|
186
|
-
if (!isRecord(value))
|
|
187
|
-
throw new Error("Storage schema manifest must be an object");
|
|
188
|
-
assertKeys(value, ["schemaVersion", "storageVersion", "aggregateSchemaVersion", "updatedAt"], ["recordVersions"], "Storage schema manifest");
|
|
189
|
-
if (value.schemaVersion !== 1)
|
|
190
|
-
throw new Error("schemaVersion must be 1");
|
|
191
|
-
if (!Number.isInteger(value.storageVersion) || value.storageVersion < 1) {
|
|
192
|
-
throw new Error("storageVersion must be a positive integer");
|
|
193
|
-
}
|
|
194
|
-
const aggregateSchemaVersion = value.aggregateSchemaVersion;
|
|
195
|
-
if (!Number.isInteger(aggregateSchemaVersion) || aggregateSchemaVersion < 1) {
|
|
196
|
-
throw new Error("aggregateSchemaVersion must be a positive integer");
|
|
197
|
-
}
|
|
198
|
-
if (typeof value.updatedAt !== "string" || !Number.isFinite(Date.parse(value.updatedAt))) {
|
|
199
|
-
throw new Error("updatedAt must be an ISO timestamp");
|
|
200
|
-
}
|
|
201
|
-
let recordVersions;
|
|
202
|
-
if (Object.hasOwn(value, "recordVersions")) {
|
|
203
|
-
if (!isRecord(value.recordVersions))
|
|
204
|
-
throw new Error("recordVersions must be an object");
|
|
205
|
-
const known = currentRecordVersions();
|
|
206
|
-
const kinds = Object.keys(value.recordVersions);
|
|
207
|
-
const unknown = kinds.filter((kind) => !Object.hasOwn(known, kind));
|
|
208
|
-
if (unknown.length > 0) {
|
|
209
|
-
throw new Error(`recordVersions has unknown family: ${unknown[0]}`);
|
|
210
|
-
}
|
|
211
|
-
const parsed = {};
|
|
212
|
-
for (const kind of kinds) {
|
|
213
|
-
const version = value.recordVersions[kind];
|
|
214
|
-
if (!Number.isInteger(version) || version < 1) {
|
|
215
|
-
throw new Error(`recordVersions['${kind}'] must be a positive integer`);
|
|
216
|
-
}
|
|
217
|
-
parsed[kind] = version;
|
|
218
|
-
}
|
|
219
|
-
recordVersions = Object.freeze(parsed);
|
|
220
|
-
}
|
|
123
|
+
function invalid(databasePath, error) {
|
|
221
124
|
return {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
125
|
+
status: "invalid",
|
|
126
|
+
latestVersion: CURRENT_STORAGE_VERSION,
|
|
127
|
+
minimumSupportedVersion: MIN_SUPPORTED_STORAGE_VERSION,
|
|
128
|
+
databasePath,
|
|
129
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
227
130
|
};
|
|
228
131
|
}
|
|
229
|
-
function parseJsonObject(raw, label) {
|
|
230
|
-
let value;
|
|
231
|
-
try {
|
|
232
|
-
value = JSON.parse(raw);
|
|
233
|
-
}
|
|
234
|
-
catch (error) {
|
|
235
|
-
throw new Error(`${label} is not valid JSON`, { cause: error });
|
|
236
|
-
}
|
|
237
|
-
if (!isRecord(value))
|
|
238
|
-
throw new Error(`${label} must be an object`);
|
|
239
|
-
return value;
|
|
240
|
-
}
|
|
241
|
-
function assertKeys(value, requiredKeys, optionalKeys, label) {
|
|
242
|
-
const expected = new Set([...requiredKeys, ...optionalKeys]);
|
|
243
|
-
const unknown = Object.keys(value).filter((key) => !expected.has(key));
|
|
244
|
-
const missing = requiredKeys.filter((key) => !Object.hasOwn(value, key));
|
|
245
|
-
if (unknown.length > 0)
|
|
246
|
-
throw new Error(`${label} has unknown field: ${unknown[0]}`);
|
|
247
|
-
if (missing.length > 0)
|
|
248
|
-
throw new Error(`${label} is missing field: ${missing[0]}`);
|
|
249
|
-
}
|
|
250
|
-
function readOptionalText(path) {
|
|
251
|
-
try {
|
|
252
|
-
return readFileSync(path, "utf8");
|
|
253
|
-
}
|
|
254
|
-
catch (error) {
|
|
255
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
256
|
-
return null;
|
|
257
|
-
throw error;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
function isRecord(value) {
|
|
261
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
262
|
-
}
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The one durable compatibility axis for a Yui Home.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Version of the on-disk layout (`schema.json` plus the SQLite database).
|
|
4
|
+
* Version 1 is the clean baseline introduced by Yui 0.15.0: SQLite's migration
|
|
5
|
+
* ledger is authoritative and the historical layout/aggregate/record-version
|
|
6
|
+
* manifest is not part of the active Home. Pre-0.15.0 Homes are outside this
|
|
7
|
+
* compatibility line and remain untouched.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Aggregate 31 makes Agent results opaque, removes parsed Review/finding
|
|
18
|
-
* authority, and intentionally has no upgrade path from earlier contracts.
|
|
9
|
+
* Runtime code reads only {@link CURRENT_STORAGE_VERSION}. The explicit
|
|
10
|
+
* upgrade boundary carries every forward migration from
|
|
11
|
+
* {@link MIN_SUPPORTED_STORAGE_VERSION} to the current version so one target
|
|
12
|
+
* CLI can upgrade any valid Home in that interval without installing
|
|
13
|
+
* intermediate Yui releases.
|
|
19
14
|
*/
|
|
20
|
-
export const
|
|
15
|
+
export const MIN_SUPPORTED_STORAGE_VERSION = 1;
|
|
16
|
+
export const CURRENT_STORAGE_VERSION = 1;
|