@aiwg/cli 2026.10.11 → 2026.10.12

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 (132) hide show
  1. package/agentic/code/providers/model-catalog.v1.json +62 -12
  2. package/dist/src/artifacts/binding-target-identity.js +51 -0
  3. package/dist/src/artifacts/checksum-manifest.js +12 -0
  4. package/dist/src/artifacts/index-builder.js +11 -13
  5. package/dist/src/artifacts/query-engine.js +5 -11
  6. package/dist/src/cli/handlers/doctor-help.js +32 -0
  7. package/dist/src/cli/handlers/gates.js +19 -18
  8. package/dist/src/cli/handlers/models-help.js +7 -1
  9. package/dist/src/cli/handlers/models.js +3 -3
  10. package/dist/src/cli/handlers/script-runner.js +8 -5
  11. package/dist/src/cli/handlers/sessions-intake.js +104 -0
  12. package/dist/src/cli/handlers/sessions-tail.js +172 -0
  13. package/dist/src/cli/handlers/sessions.js +90 -17
  14. package/dist/src/cli/handlers/skill-lint.js +10 -3
  15. package/dist/src/cli/handlers/steward-format.js +100 -0
  16. package/dist/src/cli/handlers/steward-models.js +167 -0
  17. package/dist/src/cli/handlers/steward.js +14 -248
  18. package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
  19. package/dist/src/cli/handlers/use.js +6 -24
  20. package/dist/src/cli/handlers/utilities.js +1 -24
  21. package/dist/src/config/aiwg-config.js +7 -0
  22. package/dist/src/config/authorization-cli.js +89 -0
  23. package/dist/src/config/cli-help.js +1 -0
  24. package/dist/src/config/cli.js +10 -1
  25. package/dist/src/decision/context-pruning.js +3 -2
  26. package/dist/src/decision/ensemble/contract.js +15 -4
  27. package/dist/src/decision/ensemble-study/artifacts.js +5 -0
  28. package/dist/src/decision/ensemble-study/score.js +4 -3
  29. package/dist/src/decision/feature-export.js +3 -9
  30. package/dist/src/decision/heldout/collector.js +2 -2
  31. package/dist/src/decision/issue-triage/runtime.js +3 -9
  32. package/dist/src/decision/qualification/gates.js +2 -1
  33. package/dist/src/decision/qualification/release.js +2 -3
  34. package/dist/src/decision/routing/report.js +2 -0
  35. package/dist/src/decision/sdlc-screening.js +3 -5
  36. package/dist/src/decision/sensitivity/comparative.js +5 -3
  37. package/dist/src/gates/driver.js +6 -77
  38. package/dist/src/gates/evaluate.js +4 -11
  39. package/dist/src/gates/floors.js +4 -4
  40. package/dist/src/gates/index.js +3 -3
  41. package/dist/src/gates/lattice.js +26 -0
  42. package/dist/src/gates/registry.js +2 -1
  43. package/dist/src/gates/report.js +19 -7
  44. package/dist/src/issues/audit-cli.js +120 -0
  45. package/dist/src/issues/audit-contract.js +64 -0
  46. package/dist/src/issues/audit-local.js +82 -0
  47. package/dist/src/issues/audit.js +220 -0
  48. package/dist/src/lint/rule-activation.js +15 -1
  49. package/dist/src/mission-protocol/canonical-codecs.js +270 -0
  50. package/dist/src/mission-protocol/codecs.js +22 -550
  51. package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
  52. package/dist/src/mission-protocol/health-trace-types.js +43 -0
  53. package/dist/src/mission-protocol/types.js +2 -42
  54. package/dist/src/mission-protocol/worker-contract-json.js +10 -0
  55. package/dist/src/mission-protocol/worker-review-gate.js +100 -0
  56. package/dist/src/mission-protocol/worker-review-types.js +2 -0
  57. package/dist/src/models/catalog-policy.js +280 -0
  58. package/dist/src/models/index.js +1 -1
  59. package/dist/src/models/model-catalog.v1.json +62 -12
  60. package/dist/src/models/policy-resources.js +28 -0
  61. package/dist/src/models/provider-policy.js +10 -546
  62. package/dist/src/models/resolver.js +26 -8
  63. package/dist/src/models/router.js +15 -2
  64. package/dist/src/models/tiers.js +13 -0
  65. package/dist/src/models/types.js +0 -6
  66. package/dist/src/models/worker-ceilings.js +59 -0
  67. package/dist/src/models/worker-invocation-policy.js +243 -0
  68. package/dist/src/models/worker-plan.js +16 -20
  69. package/dist/src/models/worker-review-lineage.js +55 -0
  70. package/dist/src/models/worker-selection-policy.js +1 -1
  71. package/dist/src/models/wrapper-route.js +6 -3
  72. package/dist/src/policy/authorization.js +1 -0
  73. package/dist/src/policy/hitl-config.js +63 -0
  74. package/dist/src/policy/hitl-receipt.js +57 -0
  75. package/dist/src/policy/hitl-types.js +4 -0
  76. package/dist/src/policy/hitl-validation.js +39 -0
  77. package/dist/src/policy/hitl.js +117 -0
  78. package/dist/src/rules/activation.d.mts +1 -1
  79. package/dist/src/rules/activation.mjs +0 -2
  80. package/dist/src/rules/gate-coverage.js +51 -0
  81. package/dist/src/rules/gate-references.js +16 -0
  82. package/dist/src/rules/rule-frontmatter.schema.json +7 -1
  83. package/dist/src/serve/worker-command-request.js +21 -4
  84. package/dist/src/serve/worker-invocation-preparation.js +3 -1
  85. package/dist/src/serve/worker-invocation.js +21 -10
  86. package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
  87. package/dist/src/sessions/adapters/grok-build-native.js +269 -0
  88. package/dist/src/sessions/adapters/grok-build.js +57 -15
  89. package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
  90. package/dist/src/sessions/adapters/hermes.js +42 -3
  91. package/dist/src/sessions/adapters/warp.js +1 -1
  92. package/dist/src/sessions/batch-import.js +10 -0
  93. package/dist/src/sessions/contracts.js +1 -1
  94. package/dist/src/sessions/discovery-capability.js +38 -0
  95. package/dist/src/sessions/importer.js +8 -251
  96. package/dist/src/sessions/index.js +4 -0
  97. package/dist/src/sessions/intake-dir.js +71 -0
  98. package/dist/src/sessions/normalization.js +251 -0
  99. package/dist/src/sessions/tail.js +91 -0
  100. package/dist/src/sessions/workspace-discovery.js +83 -7
  101. package/dist/src/skills/agent-skill-sidecar.js +28 -0
  102. package/dist/src/skills/agent-skills.js +4 -23
  103. package/dist/src/skills/claude-surface-projection.js +25 -0
  104. package/dist/src/skills/deployer.js +8 -288
  105. package/dist/src/skills/deployment-policy.js +114 -0
  106. package/dist/src/skills/deployment-projection.js +152 -0
  107. package/dist/src/skills/deployment-types.js +20 -0
  108. package/dist/src/skills/exporter.js +8 -31
  109. package/dist/src/skills/provider-extensions.js +41 -0
  110. package/dist/src/skills/run.js +6 -0
  111. package/dist/src/skills/types.js +0 -8
  112. package/dist/src/skills/validator-resources.js +68 -0
  113. package/dist/src/skills/validator.js +6 -55
  114. package/dist/src/storage/cli-migrate-options.js +125 -0
  115. package/dist/src/storage/cli-migrate.js +111 -0
  116. package/dist/src/storage/cli.js +4 -279
  117. package/dist/src/storage/migration-adapter.js +79 -0
  118. package/dist/src/storage/migration-copy.js +108 -0
  119. package/dist/src/storage/migration-protocol.js +14 -197
  120. package/dist/src/storage/migration-records.js +186 -0
  121. package/dist/src/storage/migration-types.js +12 -0
  122. package/dist/src/tracker/forge-http.js +2 -1
  123. package/dist/src/tracker/issue-pages.js +110 -0
  124. package/package.json +1 -1
  125. package/schemas/mission-v1.schema.json +335 -0
  126. package/schemas/models/model-catalog.v1.schema.json +19 -1
  127. package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
  128. package/schemas/models/model-policy.v1.schema.json +1 -1
  129. package/tools/agents/providers/provider-paths-staging.mjs +35 -0
  130. package/tools/agents/providers/rule-binding-targets.mjs +146 -0
  131. package/tools/agents/providers/rule-delivery.mjs +7 -126
  132. package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
@@ -15,12 +15,10 @@
15
15
  * @issue #955
16
16
  */
17
17
  import { randomUUID } from 'crypto';
18
- import { existsSync } from 'fs';
19
- import { mkdir, readFile, writeFile, appendFile } from 'fs/promises';
20
- import { dirname, extname, join, resolve as resolvePath } from 'path';
21
- import { parseFrontmatter } from '../artifacts/index-builder.js';
22
- import { BACKEND_TYPES, FilesystemAdapter, ObsidianAdapter, LogseqAdapter, FortemiAdapter, SUBSYSTEM_KEYS, getLoadedConfig, initStorage, resolveStorage, resolveSubsystemRoot, storageConfigPath, } from './index.js';
23
- import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
18
+ import { handleMigrate } from './cli-migrate.js';
19
+ export { isTextCorpusEntry } from './migration-adapter.js';
20
+ import { BACKEND_TYPES, SUBSYSTEM_KEYS, getLoadedConfig, initStorage, resolveStorage, resolveSubsystemRoot, storageConfigPath, } from './index.js';
21
+ import { resolveProjectAiwgDir } from '../config/project-artifacts.js';
24
22
  export async function main(args, projectRoot = process.cwd()) {
25
23
  const subcommand = args[0];
26
24
  const subArgs = args.slice(1);
@@ -141,166 +139,6 @@ async function handleTest(projectRoot, args) {
141
139
  ` Subsystem: ${subsystem}\n Probe path: ${probePath}`);
142
140
  }
143
141
  }
144
- /**
145
- * `aiwg storage migrate <subsystem> --from <spec> --to <spec> [--dry-run]`
146
- *
147
- * Spec format: `<type>:<location>` where type is one of fs/obsidian/
148
- * logseq/fortemi and location is:
149
- * fs → directory path (relative to projectRoot or absolute)
150
- * obsidian → vault directory (folder via `--from-folder`/`--to-folder`)
151
- * logseq → graph directory
152
- * fortemi → MCP server name (default: 'fortemi')
153
- *
154
- * Examples:
155
- * aiwg storage migrate memory --from fs:.aiwg/memory --to obsidian:~/vault --to-folder AIWG/memory
156
- * aiwg storage migrate kb --from fs:.aiwg/kb --to fortemi:fortemi --dry-run
157
- *
158
- * Walks source.list(''), reads each entry, writes to destination. Tracks
159
- * progress in `.aiwg/.storage-cache/migrations/<id>.jsonl` so re-running
160
- * resumes from the last successfully-migrated entry.
161
- *
162
- * @issue #955
163
- */
164
- async function handleMigrate(projectRoot, args) {
165
- const opts = parseMigrateArgs(args);
166
- const source = buildAdapter(opts.subsystem, opts.from, projectRoot);
167
- const destination = buildAdapter(opts.subsystem, opts.to, projectRoot);
168
- if (locationsEqual(opts.from, opts.to, projectRoot)) {
169
- throw new Error(`Refusing to migrate: source and destination resolve to the same location.\n ${describeSpec(opts.from)}\n ${describeSpec(opts.to)}`);
170
- }
171
- if (source.init)
172
- await source.init();
173
- if (!opts.dryRun && destination.init)
174
- await destination.init();
175
- console.log(`storage migrate (${opts.dryRun ? 'DRY RUN' : 'live'})`);
176
- console.log(` subsystem: ${opts.subsystem}`);
177
- console.log(` from: ${describeSpec(opts.from)}`);
178
- console.log(` to: ${describeSpec(opts.to)}`);
179
- console.log('');
180
- const entries = await source.list('');
181
- if (entries.length === 0) {
182
- console.log('No entries to migrate.');
183
- return;
184
- }
185
- // Resume support: read the per-migration log; skip already-done paths
186
- const migrationLogPath = projectAiwgPath(projectRoot, '.storage-cache', 'migrations', `${opts.subsystem}-${specSlug(opts.from)}-to-${specSlug(opts.to)}.jsonl`);
187
- const completed = await readCompletedSet(migrationLogPath);
188
- let copied = 0;
189
- let skipped = 0;
190
- let errored = 0;
191
- let unsupported = 0;
192
- for (const entry of entries) {
193
- if (opts.textOnly && !isTextCorpusEntry(entry.path)) {
194
- unsupported++;
195
- console.log(` · ${entry.path} (non-text attachment skipped)`);
196
- continue;
197
- }
198
- if (completed.has(entry.path)) {
199
- skipped++;
200
- console.log(` ✓ ${entry.path} (already migrated, skipped)`);
201
- continue;
202
- }
203
- if (opts.dryRun) {
204
- copied++;
205
- console.log(` → ${entry.path} (would copy)`);
206
- continue;
207
- }
208
- try {
209
- const content = await source.read(entry.path);
210
- if (content === null) {
211
- errored++;
212
- console.log(` ✗ ${entry.path} (read returned null)`);
213
- continue;
214
- }
215
- await destination.write(entry.path, content, migrationMetadata(entry.path, content));
216
- await recordCompletion(migrationLogPath, entry.path);
217
- copied++;
218
- console.log(` ✓ ${entry.path}`);
219
- }
220
- catch (err) {
221
- errored++;
222
- console.log(` ✗ ${entry.path} (${err.message})`);
223
- }
224
- }
225
- if (source.close)
226
- await source.close();
227
- if (destination.close)
228
- await destination.close();
229
- console.log('');
230
- console.log(`Summary: copied=${copied} skipped=${skipped} unsupported=${unsupported} ` +
231
- `errored=${errored} total=${entries.length}`);
232
- if (!opts.dryRun) {
233
- console.log(`Migration log: ${migrationLogPath}`);
234
- }
235
- if (errored > 0) {
236
- throw new Error(`${errored} entr${errored === 1 ? 'y' : 'ies'} failed to migrate.`);
237
- }
238
- }
239
- function parseMigrateArgs(args) {
240
- let subsystem;
241
- let from;
242
- let to;
243
- let fromFolder;
244
- let toFolder;
245
- let dryRun = false;
246
- let textOnly = false;
247
- for (let i = 0; i < args.length; i++) {
248
- const a = args[i];
249
- if (a === '--from')
250
- from = args[++i];
251
- else if (a === '--to')
252
- to = args[++i];
253
- else if (a === '--from-folder')
254
- fromFolder = args[++i];
255
- else if (a === '--to-folder')
256
- toFolder = args[++i];
257
- else if (a === '--dry-run')
258
- dryRun = true;
259
- else if (a === '--text-only')
260
- textOnly = true;
261
- else if (!a.startsWith('--') && !subsystem)
262
- subsystem = a;
263
- else
264
- throw new Error(`Unknown migrate flag: ${a}`);
265
- }
266
- if (!subsystem || !SUBSYSTEM_KEYS.includes(subsystem)) {
267
- throw new Error(`Usage: aiwg storage migrate <subsystem> --from <type>:<location> --to <type>:<location> [--dry-run]\n Valid subsystems: ${SUBSYSTEM_KEYS.join(', ')}`);
268
- }
269
- if (!from)
270
- throw new Error('storage migrate: --from <type>:<location> is required');
271
- if (!to)
272
- throw new Error('storage migrate: --to <type>:<location> is required');
273
- return {
274
- subsystem: subsystem,
275
- from: { ...parseSpec(from), ...(fromFolder ? { folder: fromFolder } : {}) },
276
- to: { ...parseSpec(to), ...(toFolder ? { folder: toFolder } : {}) },
277
- dryRun,
278
- textOnly,
279
- };
280
- }
281
- const TEXT_CORPUS_EXTENSIONS = new Set([
282
- '.bib',
283
- '.csv',
284
- '.htm',
285
- '.html',
286
- '.json',
287
- '.md',
288
- '.ris',
289
- '.txt',
290
- '.xml',
291
- '.yaml',
292
- '.yml',
293
- ]);
294
- export function isTextCorpusEntry(entryPath) {
295
- return TEXT_CORPUS_EXTENSIONS.has(extname(entryPath).toLowerCase());
296
- }
297
- function migrationMetadata(entryPath, content) {
298
- const extension = extname(entryPath).toLowerCase();
299
- const contentType = extension === '.md' ? 'text/markdown' : 'text/plain';
300
- if (extension !== '.md')
301
- return { contentType };
302
- return { contentType, frontmatter: parseFrontmatter(content).data };
303
- }
304
142
  async function handleImportCorpus(projectRoot, args) {
305
143
  let server = 'fortemi';
306
144
  let destination;
@@ -342,119 +180,6 @@ async function handleImportCorpus(projectRoot, args) {
342
180
  ...(dryRun ? ['--dry-run'] : []),
343
181
  ]);
344
182
  }
345
- function parseSpec(raw) {
346
- const idx = raw.indexOf(':');
347
- if (idx === -1) {
348
- throw new Error(`Invalid backend spec "${raw}" — expected <type>:<location>`);
349
- }
350
- const type = raw.slice(0, idx);
351
- const location = raw.slice(idx + 1);
352
- if (!BACKEND_TYPES.includes(type)) {
353
- throw new Error(`Unknown backend type "${type}" in spec "${raw}". Valid: ${BACKEND_TYPES.join(', ')}`);
354
- }
355
- if (location.length === 0) {
356
- throw new Error(`Empty location in backend spec "${raw}"`);
357
- }
358
- return { type: type, location };
359
- }
360
- function buildAdapter(subsystem, spec, projectRoot) {
361
- switch (spec.type) {
362
- case 'fs': {
363
- const root = expandFsLocation(spec.location, projectRoot);
364
- return new FilesystemAdapter(root);
365
- }
366
- case 'obsidian':
367
- return new ObsidianAdapter({
368
- type: 'obsidian',
369
- vault: expandFsLocation(spec.location, projectRoot),
370
- ...(spec.folder ? { folder: spec.folder } : {}),
371
- useCli: false,
372
- });
373
- case 'logseq':
374
- return new LogseqAdapter({
375
- type: 'logseq',
376
- graph: expandFsLocation(spec.location, projectRoot),
377
- useApi: false,
378
- });
379
- case 'fortemi':
380
- return new FortemiAdapter({
381
- subsystem,
382
- config: { type: 'fortemi', mcpServer: spec.location },
383
- });
384
- case 'notion':
385
- case 'anythingllm':
386
- case 's3':
387
- case 'webdav':
388
- throw new Error(`storage migrate: backend "${spec.type}" not yet implemented. ` +
389
- `See issues #959, #960, #962, #963 for tracking.`);
390
- default: {
391
- const _exhaustive = spec.type;
392
- void _exhaustive;
393
- throw new Error(`Unhandled backend type ${spec.type}`);
394
- }
395
- }
396
- }
397
- function expandFsLocation(loc, projectRoot) {
398
- if (loc.startsWith('~/')) {
399
- const { homedir } = require('os');
400
- return join(homedir(), loc.slice(2));
401
- }
402
- if (loc === '~') {
403
- const { homedir } = require('os');
404
- return homedir();
405
- }
406
- if (loc.startsWith('/'))
407
- return loc;
408
- return resolvePath(projectRoot, loc);
409
- }
410
- function locationsEqual(a, b, projectRoot) {
411
- if (a.type !== b.type)
412
- return false;
413
- if (a.type === 'fortemi')
414
- return a.location === b.location;
415
- // For file-shaped backends, compare resolved fs paths
416
- return expandFsLocation(a.location, projectRoot) === expandFsLocation(b.location, projectRoot);
417
- }
418
- function describeSpec(spec) {
419
- return spec.folder ? `${spec.type}:${spec.location} (folder=${spec.folder})` : `${spec.type}:${spec.location}`;
420
- }
421
- function specSlug(spec) {
422
- return `${spec.type}-${spec.location.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 32)}`;
423
- }
424
- async function readCompletedSet(logPath) {
425
- if (!existsSync(logPath))
426
- return new Set();
427
- try {
428
- const content = await readFile(logPath, 'utf-8');
429
- const out = new Set();
430
- for (const line of content.split('\n')) {
431
- if (!line.trim())
432
- continue;
433
- try {
434
- const obj = JSON.parse(line);
435
- if (obj.path)
436
- out.add(obj.path);
437
- }
438
- catch {
439
- /* ignore malformed line */
440
- }
441
- }
442
- return out;
443
- }
444
- catch {
445
- return new Set();
446
- }
447
- }
448
- async function recordCompletion(logPath, path) {
449
- await mkdir(dirname(logPath), { recursive: true });
450
- const line = JSON.stringify({ path, ts: new Date().toISOString() }) + '\n';
451
- if (existsSync(logPath)) {
452
- await appendFile(logPath, line, 'utf-8');
453
- }
454
- else {
455
- await writeFile(logPath, line, 'utf-8');
456
- }
457
- }
458
183
  function describeBackendLocation(subsystem, config) {
459
184
  const b = config?.backends?.[subsystem];
460
185
  if (!b)
@@ -0,0 +1,79 @@
1
+ // Bridges text storage adapters to protocol records and single-record commits.
2
+ import { extname } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { parseFrontmatter } from '../artifacts/index-builder.js';
5
+ import { digestRecords, sha256 } from './migration-records.js';
6
+ export function migrationEndpoint(adapter, identity) {
7
+ const readRecord = async (path) => {
8
+ const value = await adapter.read(path);
9
+ if (value === null)
10
+ return null;
11
+ const digest = sha256(value);
12
+ return { identity: { tenant: identity.tenant, subsystem: identity.subsystem, path }, value, digest, sourceRevision: digest };
13
+ };
14
+ const readAll = async () => {
15
+ const records = [];
16
+ for (const { path } of await adapter.list('')) {
17
+ const record = await readRecord(path);
18
+ if (record === null)
19
+ throw new Error(`source changed during enumeration: ${path} (read returned null)`);
20
+ records.push(record);
21
+ }
22
+ return records;
23
+ };
24
+ return {
25
+ identity, readRecord, readAll,
26
+ async snapshot() {
27
+ const records = await readAll();
28
+ return { id: randomUUID(), highWaterMark: digestRecords(records), records };
29
+ },
30
+ async commitBatch(mutations) {
31
+ // Baseline adapters provide single-path idempotent writes, not multi-path transactions.
32
+ if (mutations.length !== 1)
33
+ throw new Error('storage adapter copy requires single-record batches');
34
+ const mutation = mutations[0];
35
+ const path = mutation.record.identity.path;
36
+ if ((await readRecord(path))?.sourceRevision !== mutation.expectedRevision) {
37
+ throw new Error(`destination changed before commit: ${path}`);
38
+ }
39
+ if (mutation.operation === 'delete')
40
+ await adapter.delete(path);
41
+ else
42
+ await adapter.write(path, mutation.record.value, migrationMetadata(path, mutation.record.value));
43
+ const committed = await readRecord(path);
44
+ if (mutation.operation === 'delete' && committed !== null)
45
+ throw new Error(`delete not applied: ${path}`);
46
+ if (mutation.operation === 'upsert' && committed === null)
47
+ throw new Error(`write not applied: ${path}`);
48
+ return {
49
+ batchId: randomUUID(), committed: true, highWaterMark: mutation.record.sourceRevision,
50
+ recordReceipts: [{ identity: mutation.record.identity, digest: mutation.record.digest,
51
+ sourceRevision: committed?.sourceRevision ?? mutation.record.sourceRevision }],
52
+ };
53
+ },
54
+ };
55
+ }
56
+ const TEXT_CORPUS_EXTENSIONS = new Set([
57
+ '.bib',
58
+ '.csv',
59
+ '.htm',
60
+ '.html',
61
+ '.json',
62
+ '.md',
63
+ '.ris',
64
+ '.txt',
65
+ '.xml',
66
+ '.yaml',
67
+ '.yml',
68
+ ]);
69
+ export function isTextCorpusEntry(entryPath) {
70
+ return TEXT_CORPUS_EXTENSIONS.has(extname(entryPath).toLowerCase());
71
+ }
72
+ export function migrationMetadata(entryPath, content) {
73
+ const extension = extname(entryPath).toLowerCase();
74
+ const contentType = extension === '.md' ? 'text/markdown' : 'text/plain';
75
+ if (extension !== '.md')
76
+ return { contentType };
77
+ return { contentType, frontmatter: parseFrontmatter(content).data };
78
+ }
79
+ //# sourceMappingURL=migration-adapter.js.map
@@ -0,0 +1,108 @@
1
+ // Plans and checkpoints copy-only migrations without changing backend routing.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { STORAGE_MIGRATION_PROTOCOL, MigrationProtocolError, } from './migration-types.js';
4
+ import { commitMigrationRecords, sha256, stableStringify } from './migration-records.js';
5
+ export async function runMigrationCopy(request) {
6
+ const { source, destination } = request;
7
+ const prior = request.checkpoint;
8
+ if (prior && (prior.protocol !== STORAGE_MIGRATION_PROTOCOL || !prior.migrationId ||
9
+ stableStringify(prior.source) !== stableStringify(source.identity) ||
10
+ stableStringify(prior.destination) !== stableStringify(destination.identity))) {
11
+ throw new MigrationProtocolError('AIWG_MIGRATION_CHECKPOINT_MISMATCH', 'copy checkpoint does not match migration endpoints');
12
+ }
13
+ const latest = new Map(prior?.records.map(record => [record.identity.path, record]));
14
+ const sourceRecords = (await source.snapshot()).records;
15
+ // Preview must remain usable without connecting to the destination (e.g. corpus ingest).
16
+ const destinationRecords = request.dryRun ? [] : (await destination.snapshot()).records;
17
+ const sources = new Map(sourceRecords.map(record => [record.identity.path, record]));
18
+ const destinations = new Map(destinationRecords.map(record => [record.identity.path, record]));
19
+ const plan = [];
20
+ for (const record of sourceRecords) {
21
+ const path = record.identity.path;
22
+ const previous = latest.get(path);
23
+ const current = destinations.get(path);
24
+ const excluded = request.exclude?.(path);
25
+ const unchanged = previous && !previous.tombstone && previous.contentDigest === record.digest &&
26
+ previous.sourceRevision === record.sourceRevision &&
27
+ (request.dryRun || current?.sourceRevision === previous.destinationRevision);
28
+ plan.push({
29
+ path, sourceDigest: record.digest, destinationRevision: current?.sourceRevision,
30
+ action: excluded || unchanged ? 'skipped' : previous ? 'recopied' : 'copied',
31
+ reason: excluded ?? (unchanged ? 'already migrated, skipped' : previous ?
32
+ previous.contentDigest !== record.digest || previous.sourceRevision !== record.sourceRevision ? 'source changed since checkpoint' : 'destination changed since checkpoint' :
33
+ 'not yet migrated'),
34
+ });
35
+ }
36
+ for (const record of destinationRecords) {
37
+ const path = record.identity.path;
38
+ if (sources.has(path))
39
+ continue;
40
+ const previous = latest.get(path);
41
+ const excluded = request.exclude?.(path);
42
+ const authorized = !excluded && previous !== undefined && !previous.tombstone &&
43
+ record.sourceRevision === previous.destinationRevision;
44
+ plan.push({
45
+ path, action: authorized ? 'deleted' : 'skipped', deleteAuthorized: authorized,
46
+ destinationRevision: record.sourceRevision,
47
+ reason: excluded ?? (authorized ? 'source removed; unchanged migration-owned destination' :
48
+ !previous || previous.tombstone ? 'delete not authorized: destination has no live migration receipt' :
49
+ 'delete not authorized: destination changed since checkpoint'),
50
+ });
51
+ }
52
+ plan.sort((a, b) => a.path.localeCompare(b.path));
53
+ const checkpoint = {
54
+ protocol: STORAGE_MIGRATION_PROTOCOL, migrationId: prior?.migrationId ?? randomUUID(),
55
+ source: source.identity, destination: destination.identity,
56
+ records: structuredClone(prior?.records ?? []), plan,
57
+ receipt: { copied: [], recopied: [], deleted: [], skipped: [], errored: [] },
58
+ };
59
+ if (!request.dryRun)
60
+ await request.save(structuredClone(checkpoint));
61
+ for (const item of plan) {
62
+ let action = item.action;
63
+ let reason = item.reason;
64
+ if (!request.dryRun && action !== 'skipped') {
65
+ try {
66
+ const sourceNow = await source.readRecord(item.path);
67
+ const destinationNow = await destination.readRecord(item.path);
68
+ if (destinationNow?.sourceRevision !== item.destinationRevision) {
69
+ action = 'skipped';
70
+ reason = 'destination changed after plan; retry to replan';
71
+ }
72
+ else if (item.action === 'deleted' && (!item.deleteAuthorized || sourceNow)) {
73
+ action = 'skipped';
74
+ reason = 'delete not authorized: source reappeared or plan denies deletion';
75
+ }
76
+ else if (item.action !== 'deleted' && sourceNow?.tombstone) {
77
+ action = 'skipped';
78
+ reason = 'delete not authorized: source tombstone is not a planned delete';
79
+ }
80
+ else if (item.action !== 'deleted' && sourceNow?.digest !== item.sourceDigest) {
81
+ action = 'skipped';
82
+ reason = 'source changed after plan; retry to replan';
83
+ }
84
+ else {
85
+ const record = item.action === 'deleted' ? {
86
+ identity: destinationNow.identity, sourceRevision: `deleted:${item.destinationRevision}`,
87
+ digest: sha256(`deleted:${item.destinationRevision}`),
88
+ tombstone: { deletedAt: new Date().toISOString(), reason },
89
+ } : sourceNow;
90
+ const committed = await commitMigrationRecords(`${checkpoint.migrationId}:${checkpoint.records.length}`, [record], () => item.destinationRevision, mutations => destination.commitBatch(mutations), () => new Date());
91
+ checkpoint.records.push(...committed.records);
92
+ }
93
+ }
94
+ catch (error) {
95
+ action = 'errored';
96
+ reason = error instanceof Error ? error.message : String(error);
97
+ }
98
+ }
99
+ const receiptItem = { path: item.path, reason };
100
+ checkpoint.receipt[action].push(receiptItem);
101
+ // A failed checkpoint write stops the run: no later writes may outrun durable progress.
102
+ if (!request.dryRun)
103
+ await request.save(structuredClone(checkpoint));
104
+ await request.report?.(action, receiptItem);
105
+ }
106
+ return checkpoint;
107
+ }
108
+ //# sourceMappingURL=migration-copy.js.map