@aiwg/cli 2026.8.19 → 2026.8.25

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 (41) hide show
  1. package/THIRD_PARTY_NOTICES.md +12 -0
  2. package/dist/src/api/index.d.ts +2 -0
  3. package/dist/src/api/index.js +2 -0
  4. package/dist/src/artifacts/backend-runtime.js +26 -0
  5. package/dist/src/artifacts/backends/sqlite-backend.js +204 -28
  6. package/dist/src/artifacts/dep-graph.js +27 -5
  7. package/dist/src/artifacts/graph-backend.js +2 -2
  8. package/dist/src/artifacts/graph-query.js +21 -9
  9. package/dist/src/artifacts/index-builder.js +15 -0
  10. package/dist/src/artifacts/index-status.js +4 -1
  11. package/dist/src/artifacts/stats.js +4 -1
  12. package/dist/src/artifacts/types.js +13 -1
  13. package/dist/src/cli/handlers/artifact-verify.js +3 -0
  14. package/dist/src/cli/handlers/help.js +2 -0
  15. package/dist/src/cli/handlers/index.js +6 -2
  16. package/dist/src/cli/handlers/mission.js +27 -0
  17. package/dist/src/cli/handlers/refresh.js +37 -2
  18. package/dist/src/cli/handlers/runtime-info.js +29 -0
  19. package/dist/src/cli/handlers/steward.js +12 -0
  20. package/dist/src/cli/handlers/subcommands.js +26 -20
  21. package/dist/src/cli/handlers/uhp.js +88 -0
  22. package/dist/src/cli/handlers/utilities.js +3 -0
  23. package/dist/src/cli/router.js +27 -0
  24. package/dist/src/config/aiwg-config.js +10 -0
  25. package/dist/src/extensions/commands/definitions.js +38 -0
  26. package/dist/src/installation/manager-command.mjs +10 -1
  27. package/dist/src/mission-protocol/codecs.js +265 -0
  28. package/dist/src/mission-protocol/index.js +3 -0
  29. package/dist/src/mission-protocol/types.js +2 -0
  30. package/dist/src/storage/backend-contract.js +64 -0
  31. package/dist/src/storage/index.js +2 -0
  32. package/dist/src/storage/migration-protocol.js +378 -0
  33. package/dist/src/uhp/client.js +374 -0
  34. package/dist/src/uhp/config.js +130 -0
  35. package/dist/src/uhp/errors.js +63 -0
  36. package/dist/src/uhp/index.js +7 -0
  37. package/dist/src/uhp/mission.js +111 -0
  38. package/dist/src/uhp/sse.js +76 -0
  39. package/dist/src/uhp/types.js +2 -0
  40. package/dist/src/update/service.mjs +2 -5
  41. package/package.json +1 -1
@@ -0,0 +1,378 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ export const STORAGE_MIGRATION_PROTOCOL = 'aiwg.storage-migration/v1';
3
+ export class MigrationProtocolError extends Error {
4
+ code;
5
+ retryable;
6
+ constructor(code, message, retryable = false) {
7
+ super(message);
8
+ this.code = code;
9
+ this.retryable = retryable;
10
+ this.name = 'MigrationProtocolError';
11
+ }
12
+ }
13
+ export class StorageMigrationCoordinator {
14
+ source;
15
+ destination;
16
+ store;
17
+ routing;
18
+ options;
19
+ batchSize;
20
+ concurrency;
21
+ maxRetries;
22
+ baseBackoffMs;
23
+ rollbackWindowMs;
24
+ now;
25
+ random;
26
+ persistChain = Promise.resolve();
27
+ constructor(source, destination, store, routing, options) {
28
+ this.source = source;
29
+ this.destination = destination;
30
+ this.store = store;
31
+ this.routing = routing;
32
+ this.options = options;
33
+ this.batchSize = boundedInteger(options.batchSize, 100, 1, 10_000, 'batchSize');
34
+ this.concurrency = boundedInteger(options.concurrency, 4, 1, 32, 'concurrency');
35
+ this.maxRetries = boundedInteger(options.maxRetries, 5, 0, 20, 'maxRetries');
36
+ this.baseBackoffMs = boundedInteger(options.baseBackoffMs, 25, 1, 60_000, 'baseBackoffMs');
37
+ this.rollbackWindowMs = boundedInteger(options.rollbackWindowMs, 86_400_000, 1, 31_536_000_000, 'rollbackWindowMs');
38
+ this.now = options.now ?? (() => new Date());
39
+ this.random = options.random ?? Math.random;
40
+ }
41
+ async preview() {
42
+ this.throwIfAborted();
43
+ const snapshot = await this.source.snapshot(this.options.signal);
44
+ const createdAt = this.now().toISOString();
45
+ const sourceDigest = digestRecords(snapshot.records);
46
+ const manifest = {
47
+ protocol: STORAGE_MIGRATION_PROTOCOL,
48
+ migrationId: randomUUID(),
49
+ mode: this.options.mode,
50
+ state: 'preview',
51
+ source: this.source.identity,
52
+ destination: this.destination.identity,
53
+ toolVersion: this.options.toolVersion,
54
+ createdAt,
55
+ updatedAt: createdAt,
56
+ snapshot: {
57
+ id: snapshot.id,
58
+ highWaterMark: snapshot.highWaterMark,
59
+ ...(snapshot.cursor === undefined ? {} : { cursor: snapshot.cursor }),
60
+ },
61
+ checkpoint: { highWaterMark: snapshot.highWaterMark, committedBatches: 0 },
62
+ counts: {
63
+ source: snapshot.records.length,
64
+ upserts: snapshot.records.filter(record => !record.tombstone).length,
65
+ tombstones: snapshot.records.filter(record => record.tombstone).length,
66
+ committed: 0,
67
+ },
68
+ digests: { source: sourceDigest },
69
+ records: [],
70
+ };
71
+ await this.store.save(manifest);
72
+ return manifest;
73
+ }
74
+ async apply(manifest) {
75
+ validateManifest(manifest, this.source.identity, this.destination.identity);
76
+ const snapshot = await this.source.snapshot(this.options.signal);
77
+ if (snapshot.id !== manifest.snapshot.id || digestRecords(snapshot.records) !== manifest.digests.source) {
78
+ throw new MigrationProtocolError('AIWG_MIGRATION_SNAPSHOT_CHANGED', 'source snapshot no longer matches the preview; create a new preview');
79
+ }
80
+ manifest.state = 'copying';
81
+ await this.persist(manifest);
82
+ await this.copyRecords(snapshot.records, manifest);
83
+ if (manifest.mode === 'online') {
84
+ if (!this.source.changes) {
85
+ throw new MigrationProtocolError('AIWG_MIGRATION_CURSOR_UNAVAILABLE', 'online migration requires a source change cursor');
86
+ }
87
+ manifest.state = 'replaying';
88
+ await this.persist(manifest);
89
+ let cursor = manifest.snapshot.cursor;
90
+ for (;;) {
91
+ this.throwIfAborted();
92
+ const page = await this.source.changes(cursor, this.options.signal);
93
+ await this.copyRecords(page.records, manifest);
94
+ cursor = page.nextCursor;
95
+ manifest.checkpoint = {
96
+ ...manifest.checkpoint,
97
+ ...(cursor === undefined ? {} : { cursor }),
98
+ highWaterMark: page.highWaterMark,
99
+ drained: page.records.length === 0 || cursor === undefined,
100
+ };
101
+ await this.persist(manifest);
102
+ if (page.records.length === 0 || cursor === undefined)
103
+ break;
104
+ }
105
+ }
106
+ manifest.state = 'verifying';
107
+ await this.persist(manifest);
108
+ const verification = await this.verify(manifest);
109
+ if (!verification.valid) {
110
+ manifest.state = 'failed';
111
+ manifest.failure = { stage: 'verify', message: summarizeVerificationFailure(verification) };
112
+ await this.persist(manifest);
113
+ throw new MigrationProtocolError('AIWG_MIGRATION_PARITY_FAILED', manifest.failure.message);
114
+ }
115
+ manifest.digests.destination = verification.destinationDigest;
116
+ manifest.state = 'awaiting-approval';
117
+ manifest.updatedAt = this.now().toISOString();
118
+ manifest.digests.approval = approvalDigest(manifest);
119
+ await this.save(manifest);
120
+ return manifest;
121
+ }
122
+ async verify(manifest) {
123
+ const [sourceRecords, destinationRecords] = await Promise.all([
124
+ this.source.readAll(this.options.signal),
125
+ this.destination.readAll(this.options.signal),
126
+ ]);
127
+ const sourceMap = recordMap(sourceRecords);
128
+ const destinationMap = recordMap(destinationRecords);
129
+ const missing = [...sourceMap.keys()].filter(key => !destinationMap.has(key)).sort();
130
+ const unexpected = [...destinationMap.keys()].filter(key => !sourceMap.has(key)).sort();
131
+ const corrupt = [...sourceMap.entries()]
132
+ .filter(([key, record]) => {
133
+ const destination = destinationMap.get(key);
134
+ return destination !== undefined && (destination.digest !== record.digest ||
135
+ Boolean(destination.tombstone) !== Boolean(record.tombstone));
136
+ })
137
+ .map(([key]) => key)
138
+ .sort();
139
+ const lag = manifest.mode === 'online' ? Number(!manifest.checkpoint.drained) : 0;
140
+ const sourceDigest = digestRecords(sourceRecords);
141
+ const destinationDigest = digestRecords(destinationRecords);
142
+ return {
143
+ valid: missing.length === 0 && unexpected.length === 0 && corrupt.length === 0 && lag === 0,
144
+ sourceCount: sourceRecords.length,
145
+ destinationCount: destinationRecords.length,
146
+ sourceDigest,
147
+ destinationDigest,
148
+ missing,
149
+ unexpected,
150
+ corrupt,
151
+ lag,
152
+ };
153
+ }
154
+ async cutover(manifest, approval) {
155
+ validateManifest(manifest, this.source.identity, this.destination.identity);
156
+ if (manifest.state !== 'awaiting-approval') {
157
+ throw new MigrationProtocolError('AIWG_MIGRATION_NOT_READY', 'migration is not awaiting cutover approval');
158
+ }
159
+ const expected = approvalDigest(manifest);
160
+ if (approval !== expected || manifest.digests.approval !== expected) {
161
+ throw new MigrationProtocolError('AIWG_MIGRATION_APPROVAL_MISMATCH', 'cutover approval is not bound to this verified manifest');
162
+ }
163
+ const verification = await this.verify(manifest);
164
+ if (!verification.valid) {
165
+ throw new MigrationProtocolError('AIWG_MIGRATION_PARITY_FAILED', summarizeVerificationFailure(verification));
166
+ }
167
+ manifest.state = 'cutover';
168
+ await this.persist(manifest);
169
+ try {
170
+ await this.routing.switchReads(manifest);
171
+ await this.routing.switchWrites(manifest);
172
+ }
173
+ catch (error) {
174
+ await this.routing.restoreSource(manifest);
175
+ manifest.state = 'failed';
176
+ manifest.failure = { stage: 'cutover', message: errorMessage(error) };
177
+ await this.persist(manifest);
178
+ throw new MigrationProtocolError('AIWG_MIGRATION_CUTOVER_FAILED', manifest.failure.message);
179
+ }
180
+ manifest.state = 'observing';
181
+ manifest.rollbackWindowEndsAt = new Date(this.now().getTime() + this.rollbackWindowMs).toISOString();
182
+ await this.persist(manifest);
183
+ return manifest;
184
+ }
185
+ async complete(manifest) {
186
+ if (manifest.state !== 'observing') {
187
+ throw new MigrationProtocolError('AIWG_MIGRATION_NOT_OBSERVING', 'migration is not in its rollback window');
188
+ }
189
+ manifest.state = 'completed';
190
+ await this.persist(manifest);
191
+ return manifest;
192
+ }
193
+ async rollback(manifest) {
194
+ if (!['cutover', 'observing', 'failed'].includes(manifest.state)) {
195
+ throw new MigrationProtocolError('AIWG_MIGRATION_NOT_ROLLBACKABLE', `cannot roll back state ${manifest.state}`);
196
+ }
197
+ await this.routing.restoreSource(manifest);
198
+ manifest.state = 'rolled-back';
199
+ await this.persist(manifest);
200
+ return manifest;
201
+ }
202
+ async copyRecords(records, manifest) {
203
+ const completed = new Set(manifest.records.map(receiptKey));
204
+ const pending = records.filter(record => !completed.has(recordKey(record)));
205
+ const batches = chunk(pending, this.batchSize);
206
+ let next = 0;
207
+ const workers = Array.from({ length: Math.min(this.concurrency, batches.length) }, async () => {
208
+ for (;;) {
209
+ const index = next++;
210
+ if (index >= batches.length)
211
+ return;
212
+ const batch = batches[index];
213
+ const mutations = batch.map(record => mutationFor(manifest.migrationId, record));
214
+ const receipt = await this.retry(() => this.destination.commitBatch(mutations, this.options.signal), `batch ${index}`);
215
+ if (!receipt.committed || receipt.recordReceipts.length !== batch.length) {
216
+ throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_RECEIPT', `destination returned an invalid receipt for batch ${index}`);
217
+ }
218
+ const committedAt = this.now().toISOString();
219
+ const committedByIdentity = new Map(receipt.recordReceipts.map(recordReceipt => [identityKey(recordReceipt.identity), recordReceipt]));
220
+ for (let recordIndex = 0; recordIndex < batch.length; recordIndex++) {
221
+ const record = batch[recordIndex];
222
+ const committed = committedByIdentity.get(identityKey(record.identity));
223
+ if (!committed || committed.digest !== record.digest) {
224
+ throw new MigrationProtocolError('AIWG_MIGRATION_CORRUPT_RECEIPT', `destination receipt diverged for ${identityKey(record.identity)}`);
225
+ }
226
+ manifest.records.push({
227
+ identity: record.identity,
228
+ sourceRevision: record.sourceRevision,
229
+ contentDigest: record.digest,
230
+ destinationRevision: committed.sourceRevision,
231
+ tombstone: Boolean(record.tombstone),
232
+ idempotencyKey: mutations[recordIndex].idempotencyKey,
233
+ batchId: receipt.batchId,
234
+ committedAt,
235
+ });
236
+ }
237
+ manifest.counts.committed = manifest.records.length;
238
+ manifest.checkpoint.committedBatches += 1;
239
+ manifest.checkpoint.highWaterMark = receipt.highWaterMark;
240
+ await this.persist(manifest);
241
+ }
242
+ });
243
+ await Promise.all(workers);
244
+ }
245
+ async retry(operation, label) {
246
+ let attempt = 0;
247
+ for (;;) {
248
+ this.throwIfAborted();
249
+ try {
250
+ return await operation();
251
+ }
252
+ catch (error) {
253
+ const retryable = error instanceof MigrationProtocolError && error.retryable;
254
+ if (!retryable)
255
+ throw error;
256
+ if (attempt >= this.maxRetries) {
257
+ throw new MigrationProtocolError('AIWG_MIGRATION_RETRY_EXHAUSTED', `${label} failed after ${attempt + 1} attempt(s): ${errorMessage(error)}`);
258
+ }
259
+ const exponential = this.baseBackoffMs * 2 ** attempt;
260
+ const jitter = Math.floor(exponential * 0.25 * this.random());
261
+ await abortableDelay(Math.min(exponential + jitter, 60_000), this.options.signal);
262
+ attempt += 1;
263
+ }
264
+ }
265
+ }
266
+ async persist(manifest) {
267
+ manifest.updatedAt = this.now().toISOString();
268
+ await this.save(manifest);
269
+ }
270
+ async save(manifest) {
271
+ const snapshot = structuredClone(manifest);
272
+ const save = this.persistChain.then(() => this.store.save(snapshot));
273
+ this.persistChain = save.catch(() => undefined);
274
+ await save;
275
+ }
276
+ throwIfAborted() {
277
+ if (this.options.signal?.aborted) {
278
+ throw new MigrationProtocolError('AIWG_MIGRATION_CANCELLED', 'migration cancelled by operator');
279
+ }
280
+ }
281
+ }
282
+ export function digestRecords(records) {
283
+ const normalized = [...records]
284
+ .map(record => ({
285
+ identity: record.identity,
286
+ sourceRevision: record.sourceRevision,
287
+ digest: record.digest,
288
+ tombstone: record.tombstone ?? null,
289
+ }))
290
+ .sort((left, right) => identityKey(left.identity).localeCompare(identityKey(right.identity)));
291
+ return sha256(stableStringify(normalized));
292
+ }
293
+ export function approvalDigest(manifest) {
294
+ const copy = structuredClone(manifest);
295
+ delete copy.digests.approval;
296
+ return sha256(stableStringify(copy));
297
+ }
298
+ export function validateManifest(manifest, source, destination) {
299
+ if (manifest.protocol !== STORAGE_MIGRATION_PROTOCOL) {
300
+ throw new MigrationProtocolError('AIWG_MIGRATION_PROTOCOL_UNSUPPORTED', `unsupported migration protocol ${String(manifest.protocol)}`);
301
+ }
302
+ if (!manifest.migrationId || !manifest.snapshot?.id || !manifest.digests?.source) {
303
+ throw new MigrationProtocolError('AIWG_MIGRATION_MANIFEST_CORRUPT', 'migration manifest is missing required identity or digest fields');
304
+ }
305
+ if (source && stableStringify(manifest.source) !== stableStringify(source)) {
306
+ throw new MigrationProtocolError('AIWG_MIGRATION_SOURCE_MISMATCH', 'manifest source does not match the active source');
307
+ }
308
+ if (destination && stableStringify(manifest.destination) !== stableStringify(destination)) {
309
+ throw new MigrationProtocolError('AIWG_MIGRATION_DESTINATION_MISMATCH', 'manifest destination does not match the active destination');
310
+ }
311
+ }
312
+ function mutationFor(migrationId, record) {
313
+ return {
314
+ operation: record.tombstone ? 'delete' : 'upsert',
315
+ record,
316
+ idempotencyKey: sha256(`${migrationId}\0${recordKey(record)}`),
317
+ };
318
+ }
319
+ function recordMap(records) {
320
+ return new Map(records.map(record => [identityKey(record.identity), record]));
321
+ }
322
+ function identityKey(identity) {
323
+ return `${identity.tenant}\0${identity.subsystem}\0${identity.path}`;
324
+ }
325
+ function recordKey(record) {
326
+ return `${identityKey(record.identity)}\0${record.sourceRevision}\0${record.digest}`;
327
+ }
328
+ function receiptKey(receipt) {
329
+ return `${identityKey(receipt.identity)}\0${receipt.sourceRevision}\0${receipt.contentDigest}`;
330
+ }
331
+ function sha256(value) {
332
+ return createHash('sha256').update(value).digest('hex');
333
+ }
334
+ function stableStringify(value) {
335
+ if (Array.isArray(value))
336
+ return `[${value.map(stableStringify).join(',')}]`;
337
+ if (value && typeof value === 'object') {
338
+ return `{${Object.entries(value)
339
+ .filter(([, child]) => child !== undefined)
340
+ .sort(([left], [right]) => left.localeCompare(right))
341
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`)
342
+ .join(',')}}`;
343
+ }
344
+ return JSON.stringify(value);
345
+ }
346
+ function chunk(items, size) {
347
+ const chunks = [];
348
+ for (let index = 0; index < items.length; index += size)
349
+ chunks.push(items.slice(index, index + size));
350
+ return chunks;
351
+ }
352
+ function boundedInteger(raw, fallback, min, max, label) {
353
+ const value = raw ?? fallback;
354
+ if (!Number.isInteger(value) || value < min || value > max) {
355
+ throw new MigrationProtocolError('AIWG_MIGRATION_INVALID_OPTION', `${label} must be an integer from ${min} through ${max}`);
356
+ }
357
+ return value;
358
+ }
359
+ function summarizeVerificationFailure(verification) {
360
+ return `migration parity failed: missing=${verification.missing.length} unexpected=${verification.unexpected.length} corrupt=${verification.corrupt.length} lag=${verification.lag}`;
361
+ }
362
+ function errorMessage(error) {
363
+ return error instanceof Error ? error.message : String(error);
364
+ }
365
+ async function abortableDelay(ms, signal) {
366
+ if (!signal) {
367
+ await new Promise(resolve => setTimeout(resolve, ms));
368
+ return;
369
+ }
370
+ await new Promise((resolve, reject) => {
371
+ const timer = setTimeout(resolve, ms);
372
+ signal.addEventListener('abort', () => {
373
+ clearTimeout(timer);
374
+ reject(new MigrationProtocolError('AIWG_MIGRATION_CANCELLED', 'migration cancelled by operator'));
375
+ }, { once: true });
376
+ });
377
+ }
378
+ //# sourceMappingURL=migration-protocol.js.map