aiwg 2026.7.3 → 2026.7.5

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/README.md CHANGED
@@ -13,6 +13,11 @@ npm i -g aiwg # install globally
13
13
  aiwg use sdlc # deploy SDLC framework
14
14
  ```
15
15
 
16
+ Then ask your AI assistant to set up the project for AIWG. The agent-led setup
17
+ conversation should establish remotes, issue storage, delivery behavior,
18
+ signing policy, and provider choices; the assistant may call `aiwg setup project`
19
+ as the underlying CLI helper.
20
+
16
21
  macOS users: if npm fails with `EACCES` under `/usr/local/lib/node_modules`,
17
22
  use the [macOS Install Guide](docs/getting-started/macos-install.md).
18
23
  Agents and stewards setting up AIWG end-to-end should use the
@@ -133,7 +138,8 @@ The user surface is the conversation with your AI tool. You install AIWG, deploy
133
138
  The CLI exists mostly for the agent to call under the hood. The commands a user typically runs by hand are a short list:
134
139
 
135
140
  - `aiwg use <framework>` — deploy AIWG to your project (one-time per framework, per project)
136
- - `aiwg wizard` — guided first-run setup
141
+ - Project setup agent/skill — recommended guided setup conversation for repo, tracker, delivery, signing, and provider policy
142
+ - `aiwg wizard` — guided first-run goal routing
137
143
  - `aiwg new <project>` — scaffold a new project
138
144
  - `aiwg status` — what's deployed and engaged in this workspace
139
145
  - `aiwg doctor` — health check
@@ -65,6 +65,7 @@ function relationshipsForEntry(entry, graph, recordTypesByPath) {
65
65
  source_path: edge.path,
66
66
  target_path: edge.path,
67
67
  direction: prefix,
68
+ metadata: Object.fromEntries(Object.entries(edge).filter(([key]) => key !== "path" && key !== "type")),
68
69
  });
69
70
  };
70
71
  for (const edge of edges.upstream)
@@ -125,6 +126,20 @@ function recordTypeForEntry(entry, schemaVersion) {
125
126
  return "aiwg.memory.entry";
126
127
  case "issue":
127
128
  return "aiwg.issue";
129
+ case "source.file":
130
+ return "aiwg.source.file";
131
+ case "source.module":
132
+ return "aiwg.source.module";
133
+ case "source.package":
134
+ return "aiwg.source.package";
135
+ case "source.builtin":
136
+ return "aiwg.source.builtin";
137
+ case "source.asset":
138
+ return "aiwg.source.asset";
139
+ case "source.unresolved":
140
+ return "aiwg.source.unresolved";
141
+ case "source.entrypoint":
142
+ return "aiwg.source.entrypoint";
128
143
  default:
129
144
  if (pathText.includes("/research/references/") ||
130
145
  /^ref[-_]/i.test(entry.name ?? entry.title))
@@ -16,7 +16,7 @@
16
16
  * @source @src/cli/handlers/subcommands.ts
17
17
  * @tests @test/unit/artifacts/cli.test.ts
18
18
  */
19
- import { GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
19
+ import { GRAPH_CONFIGS, loadUserGraphConfigs, loadGlobalGraphConfigs } from './types.js';
20
20
  import { SUPPORTED_VIEWS } from './corpus-views/renderers.js';
21
21
  /** Parse --graph flag from args, returns undefined for "all graphs" */
22
22
  function parseGraphFlag(args) {
@@ -30,6 +30,7 @@ function parseGraphFlag(args) {
30
30
  const val = args[idx + 1];
31
31
  // Load user-defined graphs so validation is complete
32
32
  loadUserGraphConfigs(process.cwd());
33
+ loadGlobalGraphConfigs();
33
34
  if (val in GRAPH_CONFIGS)
34
35
  return val;
35
36
  // Corpus markdown views (#1490) are valid --graph targets for `index build`.
@@ -135,6 +136,9 @@ export async function main(args) {
135
136
  case 'sync':
136
137
  await handleSync(subcommandArgs);
137
138
  break;
139
+ case 'migrate-legacy':
140
+ await handleMigrateLegacy(subcommandArgs);
141
+ break;
138
142
  case 'deps':
139
143
  await handleDeps(subcommandArgs);
140
144
  break;
@@ -186,7 +190,7 @@ export async function main(args) {
186
190
  break;
187
191
  default:
188
192
  console.error(`Error: Unknown index subcommand '${subcommand}'`);
189
- console.log('Available: build, query, discover, show, export, sync, deps, stats, status, list, neighbors, set, embed, similar, dedup-report, watch');
193
+ console.log('Available: build, query, discover, show, export, sync, migrate-legacy, deps, stats, status, list, neighbors, set, embed, similar, dedup-report, watch');
190
194
  process.exit(1);
191
195
  }
192
196
  }
@@ -200,6 +204,7 @@ function printIndexUsage() {
200
204
  console.log(' show Print the full text of a specific skill/agent/command/rule');
201
205
  console.log(' export Export a browser-consumable index contract');
202
206
  console.log(' sync Materialize the Fortemi Core static index cache');
207
+ console.log(' migrate-legacy Move legacy root indexes into graph sidecar indexes');
203
208
  console.log(' deps Show artifact dependency graph');
204
209
  console.log(' stats Show index statistics');
205
210
  console.log(' status Enumerate the index-graph registry (freshness + drift); alias: list');
@@ -211,7 +216,7 @@ function printIndexUsage() {
211
216
  console.log(' watch Start a filesystem watcher for automatic incremental index updates');
212
217
  console.log('');
213
218
  console.log('Options:');
214
- console.log(' --graph <name> Target a specific graph (framework, project, codebase, or user-defined)');
219
+ console.log(' --graph <name> Target a specific graph (framework, project, codebase, source, user, or user-defined)');
215
220
  console.log(' --all Build all known graphs (including user-defined)');
216
221
  console.log('');
217
222
  console.log('Examples:');
@@ -225,6 +230,7 @@ function printIndexUsage() {
225
230
  console.log(' aiwg index show skill flow-deploy-to-production --json');
226
231
  console.log(' aiwg index show agent aiwg-steward');
227
232
  console.log(' aiwg index export --format fortemi --graph project --out aiwg-fortemi-index.json');
233
+ console.log(' aiwg index migrate-legacy --scope project --dry-run');
228
234
  console.log(' aiwg index query "authentication" --type use-case');
229
235
  console.log(' aiwg index query "security rules" --graph framework --json');
230
236
  console.log(' aiwg index query "mixture of experts" --fulltext --graph papers # body text, BM25');
@@ -332,7 +338,7 @@ async function handleBuild(args) {
332
338
  console.log(' --scope <dir> Limit scan to a specific subdirectory');
333
339
  console.log(' --graph <name> Build a specific graph only (built-in or user-defined)');
334
340
  console.log('');
335
- console.log('Built-in graph names: project, codebase, framework');
341
+ console.log('Built-in graph names: project, codebase, source, user, framework');
336
342
  console.log('User-defined graphs: configure under index.graphs in .aiwg/aiwg.config');
337
343
  console.log('');
338
344
  console.log('Default behavior (no --graph): builds all graphs with defaultBuild: true');
@@ -342,6 +348,8 @@ async function handleBuild(args) {
342
348
  console.log(' aiwg index build');
343
349
  console.log(' aiwg index build --force');
344
350
  console.log(' aiwg index build --graph codebase --force');
351
+ console.log(' aiwg index build --graph source --force');
352
+ console.log(' aiwg index build --graph user --force');
345
353
  console.log(' aiwg index build --graph references # user-defined graph');
346
354
  console.log(' aiwg index build --scope documentation/references');
347
355
  console.log(' aiwg index build --all');
@@ -363,6 +371,7 @@ async function handleBuild(args) {
363
371
  }
364
372
  // Load user-defined graphs
365
373
  loadUserGraphConfigs(cwd);
374
+ loadGlobalGraphConfigs();
366
375
  let jsonBuilt = false;
367
376
  if (graph) {
368
377
  // --graph X: build the JSON graph if X is one; otherwise X may be a
@@ -821,6 +830,83 @@ async function handleSync(args) {
821
830
  process.exit(1);
822
831
  }
823
832
  }
833
+ /**
834
+ * Handle 'index migrate-legacy' command.
835
+ */
836
+ async function handleMigrateLegacy(args) {
837
+ if (args.includes('--help') || args.includes('-h')) {
838
+ console.log('Usage: aiwg index migrate-legacy [--scope project|user|global | --all] [options]');
839
+ console.log('');
840
+ console.log('Migrates compatible legacy root index files into graph sidecar index');
841
+ console.log('directories. Project scope moves .aiwg/.index/*.json to');
842
+ console.log('.aiwg/.index/project/*.json and refreshes the Fortemi Core static');
843
+ console.log('cache. User/global scopes report or migrate their corresponding');
844
+ console.log('sidecar locations without modifying packaged/prebuilt AIWG indexes.');
845
+ console.log('');
846
+ console.log('Options:');
847
+ console.log(' --scope <name> Scope to migrate: project, user, or global (default: project)');
848
+ console.log(' --all Migrate project, user, and global scopes');
849
+ console.log(' --dry-run Print planned changes without writing files');
850
+ console.log(' --no-fortemi-sync Do not refresh the project Fortemi Core static cache');
851
+ console.log(' --generated-at <iso> Override generated timestamp for deterministic fixtures');
852
+ console.log(' --json Print the migration report as JSON');
853
+ console.log('');
854
+ console.log('Examples:');
855
+ console.log(' aiwg index migrate-legacy --scope project --dry-run');
856
+ console.log(' aiwg index migrate-legacy --all --json');
857
+ return;
858
+ }
859
+ const all = args.includes('--all');
860
+ const scopeValue = parseFlagValue(args, '--scope', 'Error: --scope requires project, user, or global');
861
+ if (all && scopeValue) {
862
+ console.error('Error: pass either --all or --scope, not both');
863
+ process.exit(1);
864
+ }
865
+ const allowedScopes = ['project', 'user', 'global'];
866
+ const scopes = all
867
+ ? [...allowedScopes]
868
+ : scopeValue
869
+ ? [scopeValue]
870
+ : ['project'];
871
+ const invalidScope = scopes.find((scope) => !allowedScopes.includes(scope));
872
+ if (invalidScope) {
873
+ console.error('Error: --scope must be project, user, or global');
874
+ process.exit(1);
875
+ }
876
+ const generatedAt = parseFlagValue(args, '--generated-at', 'Error: --generated-at requires an ISO timestamp value');
877
+ const { migrateLegacyIndex } = await import('./legacy-index-migration.js');
878
+ try {
879
+ const report = migrateLegacyIndex(process.cwd(), {
880
+ scopes: scopes,
881
+ dryRun: args.includes('--dry-run'),
882
+ syncFortemi: !args.includes('--no-fortemi-sync'),
883
+ generatedAt,
884
+ });
885
+ if (args.includes('--json')) {
886
+ console.log(JSON.stringify(report, null, 2));
887
+ return;
888
+ }
889
+ console.log(`Legacy index migration ${report.dryRun ? '(DRY RUN)' : 'complete'}`);
890
+ for (const result of report.results) {
891
+ const entries = result.entries === null ? 'unknown' : String(result.entries);
892
+ const detail = result.reason ? ` — ${result.reason}` : '';
893
+ console.log(` ${result.scope}: ${result.status} (${entries} entries)${detail}`);
894
+ for (const file of result.files) {
895
+ console.log(` ${file.name}: ${file.status}`);
896
+ }
897
+ if (result.fortemiCore) {
898
+ console.log(` fortemi-core: ${result.fortemiCore.status} (${result.fortemiCore.itemCount} item(s)) → ${result.fortemiCore.exportPath}`);
899
+ }
900
+ }
901
+ if (report.reportPath) {
902
+ console.log(` report: ${report.reportPath}`);
903
+ }
904
+ }
905
+ catch (err) {
906
+ console.error('Error: ' + (err instanceof Error ? err.message : String(err)));
907
+ process.exit(1);
908
+ }
909
+ }
824
910
  /**
825
911
  * Handle 'index deps' command
826
912
  *
@@ -1,9 +1,16 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { GRAPH_CONFIGS, loadGlobalGraphConfigs } from "./types.js";
4
5
  import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
5
6
  import { loadGraphIndexFile } from "./index-reader.js";
6
7
  function syncDir(cwd, graph) {
8
+ loadGlobalGraphConfigs();
9
+ const config = GRAPH_CONFIGS[graph];
10
+ if (graph === "framework" || config?.shared) {
11
+ const xdgData = process.env.XDG_DATA_HOME ?? path.join(process.env.HOME ?? cwd, ".local", "share");
12
+ return path.join(xdgData, "aiwg", "index", "fortemi-core", graph);
13
+ }
7
14
  return path.join(cwd, ".aiwg", ".index", "fortemi-core", graph);
8
15
  }
9
16
  function findPackageRoot(startDir) {
@@ -13,10 +13,29 @@ import fs from 'fs';
13
13
  import path from 'path';
14
14
  import { createHash } from 'crypto';
15
15
  import { load as loadYaml } from 'js-yaml';
16
- import { INDEX_VERSION, INDEX_DIR, PHASE_DIRECTORIES, GRAPH_CONFIGS, loadUserGraphConfigs } from './types.js';
16
+ import { INDEX_VERSION, INDEX_DIR, PHASE_DIRECTORIES, GRAPH_CONFIGS, loadUserGraphConfigs, loadGlobalGraphConfigs } from './types.js';
17
17
  import { parseCitationSidecar, citationResultToEdges, buildRefToPathMap } from './citation-parser.js';
18
18
  import { writeIndexFile, resolveIndexDir, loadGraphIndexFile } from './index-reader.js';
19
19
  import { loadManifest, writeManifest, statMatches, makeEntry } from './checksum-manifest.js';
20
+ function expandScanDir(cwd, scanDir) {
21
+ if (scanDir === '~')
22
+ return process.env.HOME ?? scanDir;
23
+ if (scanDir.startsWith('~/')) {
24
+ return path.join(process.env.HOME ?? '', scanDir.slice(2));
25
+ }
26
+ if (path.isAbsolute(scanDir))
27
+ return scanDir;
28
+ return path.join(cwd, scanDir);
29
+ }
30
+ function indexPathFor(cwd, fullPath) {
31
+ const rel = path.relative(cwd, fullPath);
32
+ if (!rel.startsWith('..') && !path.isAbsolute(rel))
33
+ return rel;
34
+ return fullPath;
35
+ }
36
+ function absoluteEntryPath(cwd, entryPath) {
37
+ return path.isAbsolute(entryPath) ? entryPath : path.join(cwd, entryPath);
38
+ }
20
39
  /**
21
40
  * Parse YAML frontmatter from markdown content
22
41
  */
@@ -498,6 +517,7 @@ export async function buildIndex(cwd, options = {}) {
498
517
  const startTime = Date.now();
499
518
  // Ensure user-defined graphs are loaded
500
519
  loadUserGraphConfigs(cwd);
520
+ loadGlobalGraphConfigs();
501
521
  // Determine scan directories based on graph type
502
522
  const graphConfig = graph ? GRAPH_CONFIGS[graph] : undefined;
503
523
  let scanDirs;
@@ -508,7 +528,7 @@ export async function buildIndex(cwd, options = {}) {
508
528
  fileExtensions = ['.md', '.yaml', '.json'];
509
529
  }
510
530
  else if (graphConfig) {
511
- scanDirs = graphConfig.scanDirs.map(d => path.join(cwd, d));
531
+ scanDirs = graphConfig.scanDirs.map(d => expandScanDir(cwd, d));
512
532
  fileExtensions = graphConfig.extensions;
513
533
  }
514
534
  else {
@@ -548,6 +568,19 @@ export async function buildIndex(cwd, options = {}) {
548
568
  fs.mkdirSync(indexOutputDir, { recursive: true });
549
569
  // effectiveOutputCwd is used for backward-compat loadMetadataIndex calls
550
570
  const effectiveOutputCwd = outputDir ?? cwd;
571
+ if (graph === 'source') {
572
+ const { buildSourceGraphIndex } = await import('./source-graph.js');
573
+ await buildSourceGraphIndex({
574
+ cwd,
575
+ outputDir: indexOutputDir,
576
+ effectiveOutputCwd,
577
+ verbose,
578
+ });
579
+ const buildTimeMs = Date.now() - startTime;
580
+ console.log(`Source graph built in ${buildTimeMs}ms`);
581
+ console.log(` Output: ${INDEX_DIR}/source/`);
582
+ return;
583
+ }
551
584
  // Load existing index for incremental updates
552
585
  const existingIndex = force ? null : loadGraphIndexFile(effectiveOutputCwd, 'metadata.json', graph);
553
586
  const existingEntries = existingIndex?.entries ?? {};
@@ -577,7 +610,7 @@ export async function buildIndex(cwd, options = {}) {
577
610
  let unchangedCount = 0;
578
611
  const useFilenameMetadata = graphConfig?.nodeStrategy === 'filename-metadata';
579
612
  for (const fullPath of files) {
580
- const relativePath = path.relative(cwd, fullPath);
613
+ const relativePath = indexPathFor(cwd, fullPath);
581
614
  let entry;
582
615
  if (useFilenameMetadata) {
583
616
  // Filename-metadata strategy: derive metadata from filename, skip content read.
@@ -728,7 +761,7 @@ export async function buildIndex(cwd, options = {}) {
728
761
  // Build REF-XXX → path map from all entries with ref frontmatter
729
762
  const entryFrontmatter = new Map();
730
763
  for (const entryPath of Object.keys(entries)) {
731
- const fullPath = path.join(cwd, entryPath);
764
+ const fullPath = absoluteEntryPath(cwd, entryPath);
732
765
  if (fs.existsSync(fullPath)) {
733
766
  const content = fs.readFileSync(fullPath, 'utf-8');
734
767
  const { data } = parseFrontmatter(content);
@@ -739,7 +772,7 @@ export async function buildIndex(cwd, options = {}) {
739
772
  // Parse each entry as a citation sidecar and extract edges
740
773
  let citationEdgeCount = 0;
741
774
  for (const entryPath of Object.keys(entries)) {
742
- const fullPath = path.join(cwd, entryPath);
775
+ const fullPath = absoluteEntryPath(cwd, entryPath);
743
776
  if (!fs.existsSync(fullPath))
744
777
  continue;
745
778
  const content = fs.readFileSync(fullPath, 'utf-8');
@@ -848,7 +881,7 @@ export async function buildIndex(cwd, options = {}) {
848
881
  console.log(` Pruned ${manifestStats.pruned} stale manifest entries (files no longer on disk)`);
849
882
  }
850
883
  }
851
- const displayDir = graph ? `${INDEX_DIR}/${graph}/` : `${INDEX_DIR}/`;
884
+ const displayDir = graph ? indexOutputDir : `${INDEX_DIR}/`;
852
885
  console.log(` Output: ${displayDir}`);
853
886
  }
854
887
  //# sourceMappingURL=index-builder.js.map
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import * as fs from 'node:fs';
20
20
  import * as path from 'node:path';
21
- import { GRAPH_CONFIGS, BUILTIN_GRAPH_CONFIGS, getGraphIndexDir, loadUserGraphConfigs, } from './types.js';
21
+ import { GRAPH_CONFIGS, BUILTIN_GRAPH_CONFIGS, getGraphIndexDir, loadGlobalGraphConfigs, loadUserGraphConfigs, } from './types.js';
22
22
  import { getFortemiCoreSyncStatus, } from './fortemi-core-sync.js';
23
23
  function readBuiltMeta(indexDir) {
24
24
  try {
@@ -47,6 +47,7 @@ export function collectIndexStatus(cwd, nowMs) {
47
47
  // instead of letting malformed defs vanish (#1624).
48
48
  const warnings = [];
49
49
  loadUserGraphConfigs(cwd, warnings);
50
+ loadGlobalGraphConfigs(warnings);
50
51
  const now = nowMs ?? Date.now();
51
52
  const graphs = [];
52
53
  for (const [name, config] of Object.entries(GRAPH_CONFIGS)) {
@@ -0,0 +1,169 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { INDEX_VERSION, getGraphIndexDir } from './types.js';
6
+ import { syncFortemiCoreIndex } from './fortemi-core-sync.js';
7
+ const INDEX_FILES = ['metadata.json', 'tags.json', 'dependencies.json', 'stats.json'];
8
+ function sha256File(filePath) {
9
+ if (!fs.existsSync(filePath))
10
+ return null;
11
+ return createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
12
+ }
13
+ function readJson(filePath) {
14
+ try {
15
+ return { value: JSON.parse(fs.readFileSync(filePath, 'utf-8')), reason: null };
16
+ }
17
+ catch (err) {
18
+ return {
19
+ value: null,
20
+ reason: err instanceof Error ? err.message : String(err),
21
+ };
22
+ }
23
+ }
24
+ function resolveHome(options) {
25
+ return options.homeDir ?? process.env.AIWG_HOME ?? os.homedir();
26
+ }
27
+ function resolveXdgData(options) {
28
+ return options.xdgDataHome ?? process.env.XDG_DATA_HOME ?? path.join(resolveHome(options), '.local', 'share');
29
+ }
30
+ function scopePaths(cwd, scope, options) {
31
+ switch (scope) {
32
+ case 'project':
33
+ return {
34
+ sourceDir: path.join(cwd, '.aiwg', '.index'),
35
+ destinationDir: getGraphIndexDir(cwd, 'project'),
36
+ syncRoot: cwd,
37
+ graph: 'project',
38
+ };
39
+ case 'user': {
40
+ const home = resolveHome(options);
41
+ return {
42
+ sourceDir: path.join(home, '.aiwg', '.index'),
43
+ destinationDir: path.join(home, '.aiwg', '.index', 'user'),
44
+ syncRoot: null,
45
+ graph: 'user',
46
+ };
47
+ }
48
+ case 'global': {
49
+ const xdg = resolveXdgData(options);
50
+ return {
51
+ sourceDir: path.join(xdg, 'aiwg', 'index'),
52
+ destinationDir: path.join(xdg, 'aiwg', 'index', 'global'),
53
+ syncRoot: null,
54
+ graph: 'global',
55
+ };
56
+ }
57
+ }
58
+ }
59
+ function validateLegacyMetadata(sourceDir) {
60
+ const metadataPath = path.join(sourceDir, 'metadata.json');
61
+ if (!fs.existsSync(metadataPath)) {
62
+ return { index: null, reason: 'metadata.json not found' };
63
+ }
64
+ const parsed = readJson(metadataPath);
65
+ if (!parsed.value) {
66
+ return { index: null, reason: `metadata.json is unreadable: ${parsed.reason}` };
67
+ }
68
+ if (parsed.value.version !== INDEX_VERSION) {
69
+ return {
70
+ index: null,
71
+ reason: `metadata.json schema version ${String(parsed.value.version ?? 'missing')} is not compatible with ${INDEX_VERSION}`,
72
+ };
73
+ }
74
+ if (!parsed.value.entries || typeof parsed.value.entries !== 'object' || Array.isArray(parsed.value.entries)) {
75
+ return { index: null, reason: 'metadata.json entries must be an object' };
76
+ }
77
+ return { index: parsed.value, reason: null };
78
+ }
79
+ function migrateOneScope(cwd, scope, options) {
80
+ const { sourceDir, destinationDir, syncRoot, graph } = scopePaths(cwd, scope, options);
81
+ const baseResult = {
82
+ scope,
83
+ sourceDir,
84
+ destinationDir,
85
+ status: 'skipped',
86
+ files: [],
87
+ entries: null,
88
+ reason: null,
89
+ fortemiCore: null,
90
+ };
91
+ if (!fs.existsSync(sourceDir)) {
92
+ return { ...baseResult, reason: 'legacy index directory not found' };
93
+ }
94
+ if (path.resolve(sourceDir) === path.resolve(destinationDir)) {
95
+ return {
96
+ ...baseResult,
97
+ reason: 'source and destination are the same directory; no migration needed',
98
+ };
99
+ }
100
+ const validation = validateLegacyMetadata(sourceDir);
101
+ if (!validation.index) {
102
+ return {
103
+ ...baseResult,
104
+ status: 'needs-rebuild',
105
+ reason: validation.reason,
106
+ };
107
+ }
108
+ let changed = false;
109
+ let anyCreated = false;
110
+ for (const name of INDEX_FILES) {
111
+ const sourcePath = path.join(sourceDir, name);
112
+ const destinationPath = path.join(destinationDir, name);
113
+ if (!fs.existsSync(sourcePath)) {
114
+ baseResult.files.push({ name, status: 'skipped' });
115
+ continue;
116
+ }
117
+ const sourceHash = sha256File(sourcePath);
118
+ const destinationHash = sha256File(destinationPath);
119
+ const exists = destinationHash !== null;
120
+ const same = sourceHash !== null && sourceHash === destinationHash;
121
+ const status = same ? 'unchanged' : exists ? 'updated' : 'created';
122
+ baseResult.files.push({ name, status });
123
+ if (!same) {
124
+ changed = true;
125
+ if (!exists)
126
+ anyCreated = true;
127
+ if (!options.dryRun) {
128
+ fs.mkdirSync(destinationDir, { recursive: true });
129
+ fs.copyFileSync(sourcePath, destinationPath);
130
+ }
131
+ }
132
+ }
133
+ baseResult.entries = Object.keys(validation.index.entries).length;
134
+ baseResult.status = changed ? (anyCreated ? 'created' : 'updated') : 'unchanged';
135
+ if (!options.dryRun && options.syncFortemi !== false && syncRoot) {
136
+ const manifest = syncFortemiCoreIndex(syncRoot, {
137
+ graph,
138
+ generatedAt: options.generatedAt,
139
+ });
140
+ baseResult.fortemiCore = {
141
+ status: manifest.status,
142
+ exportPath: manifest.export_path,
143
+ itemCount: manifest.item_count,
144
+ };
145
+ }
146
+ return baseResult;
147
+ }
148
+ function defaultReportPath(cwd) {
149
+ return path.join(cwd, '.aiwg', '.index', 'migrations', 'legacy-index-migration.json');
150
+ }
151
+ export function migrateLegacyIndex(cwd, options = {}) {
152
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
153
+ const scopes = options.scopes?.length ? options.scopes : ['project'];
154
+ const results = scopes.map((scope) => migrateOneScope(cwd, scope, { ...options, generatedAt }));
155
+ const report = {
156
+ schemaVersion: 'aiwg.legacy-index-migration.v1',
157
+ dryRun: options.dryRun === true,
158
+ generatedAt,
159
+ results,
160
+ reportPath: options.dryRun ? null : defaultReportPath(cwd),
161
+ };
162
+ if (!options.dryRun) {
163
+ const reportPath = defaultReportPath(cwd);
164
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true });
165
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n', 'utf-8');
166
+ }
167
+ return report;
168
+ }
169
+ //# sourceMappingURL=legacy-index-migration.js.map