ai-codebase-registry 1.0.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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:registry
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity complex
9
+ * @token-budget 300
10
+ * @purpose Main Registry class — loads registry JSON and provides query/impact/resolve APIs
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created Registry class with query, impact, resolve APIs
14
+ */
15
+
16
+ import { readFileSync } from 'fs';
17
+ import { join } from 'path';
18
+
19
+ /**
20
+ * Registry provides a programmatic query API over the generated registry files.
21
+ *
22
+ * Usage:
23
+ * const registry = await Registry.load('./_registry');
24
+ * const tripHooks = registry.query({ domain: 'trip', type: 'hook' });
25
+ * const impact = registry.impact('functions/src/services/projectionService.ts');
26
+ */
27
+ export class Registry {
28
+ constructor(data) {
29
+ this._manifest = data.manifest;
30
+ this._deps = data.deps;
31
+ this._semanticIds = data.semanticIds;
32
+ this._sideEffects = data.sideEffects;
33
+ this._relationships = data.relationships;
34
+ this._routes = data.routes;
35
+ }
36
+
37
+ /**
38
+ * Load registry from a directory containing JSON files.
39
+ * @param {string} registryDir - Path to _registry/ directory
40
+ * @returns {Registry}
41
+ */
42
+ static load(registryDir) {
43
+ const read = (name) => {
44
+ try {
45
+ return JSON.parse(readFileSync(join(registryDir, name), 'utf-8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ };
50
+
51
+ return new Registry({
52
+ manifest: read('manifest.json'),
53
+ deps: read('deps.json'),
54
+ semanticIds: read('semantic-ids.json'),
55
+ sideEffects: read('side-effects.json'),
56
+ relationships: read('relationships.json'),
57
+ routes: read('routes.json'),
58
+ });
59
+ }
60
+
61
+ /** Get the raw manifest data */
62
+ get manifest() { return this._manifest?.files || {}; }
63
+
64
+ /** Get the raw deps graph */
65
+ get deps() { return this._deps?.graph || {}; }
66
+
67
+ /** Get the raw semantic IDs map */
68
+ get semanticIds() { return this._semanticIds?.ids || {}; }
69
+
70
+ /** Get the raw side-effects data */
71
+ get sideEffects() { return this._sideEffects?.effects || {}; }
72
+
73
+ /** Get the raw routes data */
74
+ get routes() { return this._routes?.routes || {}; }
75
+
76
+ /**
77
+ * Query manifest files by domain and/or type.
78
+ * @param {{ domain?: string, type?: string }} filters
79
+ * @returns {{ path: string, meta: object }[]}
80
+ */
81
+ query(filters = {}) {
82
+ const results = [];
83
+ for (const [path, meta] of Object.entries(this.manifest)) {
84
+ if (filters.domain && meta.domain !== filters.domain) continue;
85
+ if (filters.type && meta.type !== filters.type) continue;
86
+ results.push({ path, ...meta });
87
+ }
88
+ return results;
89
+ }
90
+
91
+ /**
92
+ * Resolve a semantic ID to a file path.
93
+ * @param {string} semanticId - e.g., 'hook:trip:diary-entries'
94
+ * @returns {string|null} File path or null
95
+ */
96
+ resolve(semanticId) {
97
+ return this.semanticIds[semanticId] || null;
98
+ }
99
+
100
+ /**
101
+ * Get full metadata for a semantic ID.
102
+ * @param {string} semanticId
103
+ * @returns {object|null}
104
+ */
105
+ resolveWithMeta(semanticId) {
106
+ const path = this.resolve(semanticId);
107
+ if (!path) return null;
108
+ return { path, ...this.manifest[path] };
109
+ }
110
+
111
+ /**
112
+ * Get direct consumers (importedBy) for a file.
113
+ * @param {string} filePath
114
+ * @returns {string[]}
115
+ */
116
+ consumers(filePath) {
117
+ return this.deps[filePath]?.importedBy || [];
118
+ }
119
+
120
+ /**
121
+ * Get direct dependencies (imports) for a file.
122
+ * @param {string} filePath
123
+ * @returns {string[]}
124
+ */
125
+ dependencies(filePath) {
126
+ return this.deps[filePath]?.imports || [];
127
+ }
128
+
129
+ /**
130
+ * Comprehensive impact analysis for changing a file.
131
+ * Returns direct consumers, transitive consumers (2 levels), change impact rules, and side-effects.
132
+ *
133
+ * @param {string} filePath
134
+ * @returns {object}
135
+ */
136
+ impact(filePath) {
137
+ const direct = this.consumers(filePath);
138
+
139
+ // Transitive consumers (2 levels deep)
140
+ const transitiveSet = new Set();
141
+ for (const consumer of direct) {
142
+ const level2 = this.consumers(consumer);
143
+ for (const t of level2) {
144
+ if (t !== filePath && !direct.includes(t)) {
145
+ transitiveSet.add(t);
146
+ }
147
+ }
148
+ }
149
+
150
+ // Check change_impact_rules from relationships.json
151
+ const impactRules = [];
152
+ const cir = this._relationships?.change_impact_rules;
153
+ if (Array.isArray(cir)) {
154
+ for (const rule of cir) {
155
+ if (rule.when && filePath.includes(rule.when.replace(/\*/g, ''))) {
156
+ impactRules.push(rule);
157
+ }
158
+ }
159
+ }
160
+
161
+ // Get side-effects chain
162
+ const meta = this.manifest[filePath];
163
+ const semanticId = meta?.id;
164
+ const sideEffectEntry = semanticId ? this.sideEffects[semanticId] : null;
165
+
166
+ return {
167
+ file: filePath,
168
+ directConsumers: direct,
169
+ transitiveConsumers: [...transitiveSet],
170
+ changeImpactRules: impactRules,
171
+ sideEffects: sideEffectEntry?.sideEffects || [],
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Get side-effects chain for a semantic ID.
177
+ * @param {string} semanticId
178
+ * @returns {object|null}
179
+ */
180
+ sideEffectChain(semanticId) {
181
+ return this.sideEffects[semanticId] || null;
182
+ }
183
+
184
+ /**
185
+ * Find orphaned routes (no nav entry point).
186
+ * @returns {{ path: string, component: string }[]}
187
+ */
188
+ orphanedRoutes() {
189
+ const results = [];
190
+ for (const [routePath, routeData] of Object.entries(this.routes)) {
191
+ if (routeData.nav_entry_points?.status === 'orphaned') {
192
+ results.push({ path: routePath, component: routeData.component, file: routeData.file });
193
+ }
194
+ }
195
+ return results;
196
+ }
197
+
198
+ /**
199
+ * Get coverage statistics.
200
+ * @returns {object}
201
+ */
202
+ coverage() {
203
+ const files = Object.entries(this.manifest);
204
+ const total = files.length;
205
+ let withId = 0, withSideEffects = 0, withPurpose = 0;
206
+
207
+ for (const [, meta] of files) {
208
+ if (meta.id) withId++;
209
+ if (meta.sideEffects !== undefined) withSideEffects++;
210
+ if (meta.purpose) withPurpose++;
211
+ }
212
+
213
+ return {
214
+ totalFiles: total,
215
+ withId: { count: withId, pct: ((withId / total) * 100).toFixed(1) },
216
+ withSideEffects: { count: withSideEffects, pct: ((withSideEffects / total) * 100).toFixed(1) },
217
+ withPurpose: { count: withPurpose, pct: ((withPurpose / total) * 100).toFixed(1) },
218
+ missingId: files.filter(([, m]) => !m.id).map(([p]) => p),
219
+ missingSideEffects: files.filter(([, m]) => m.sideEffects === undefined).map(([p]) => p),
220
+ };
221
+ }
222
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:semantic-id
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 120
10
+ * @purpose Generates and resolves semantic IDs (type:domain:kebab-name) for codebase entities
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-23T10:00 [feat] [Claude Opus 4.6] Add Swift suffix stripping (ViewModel, View, Service) for cleaner IDs
14
+ * @changelog 2026-02-22T16:00 [fix] [Claude Opus 4.6] Remove Context suffix stripping and index-file handling to align with local deriveSemanticId
15
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
16
+ */
17
+
18
+ import { basename, extname } from 'path';
19
+
20
+ /** Suffixes to strip from filenames when generating semantic IDs, ordered longest-first */
21
+ const SUFFIX_STRIP = ['ViewModel', 'Page', 'View', 'Service'];
22
+
23
+ /**
24
+ * Generate a semantic ID from file metadata.
25
+ * Format: {type}:{domain}:{kebab-filename}
26
+ *
27
+ * @param {string} relPath - Relative file path
28
+ * @param {{ type: string, domain: string }} meta - File metadata with type and domain
29
+ * @returns {string} Semantic ID
30
+ */
31
+ export function generateSemanticId(relPath, meta) {
32
+ const name = basename(relPath);
33
+ const ext = extname(name);
34
+ let nameNoExt = name.replace(ext, '');
35
+
36
+ // Strip common prefixes (hooks: useXxx)
37
+ if (nameNoExt.startsWith('use') && nameNoExt[3] === nameNoExt[3]?.toUpperCase()) {
38
+ nameNoExt = nameNoExt.slice(3);
39
+ }
40
+
41
+ // Strip common suffixes (Page, View, ViewModel, Service)
42
+ for (const suffix of SUFFIX_STRIP) {
43
+ if (nameNoExt.endsWith(suffix) && nameNoExt.length > suffix.length) {
44
+ nameNoExt = nameNoExt.slice(0, -suffix.length);
45
+ break; // Only strip one suffix
46
+ }
47
+ }
48
+
49
+ // Convert PascalCase/camelCase to kebab-case
50
+ const kebab = nameNoExt
51
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
52
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
53
+ .toLowerCase();
54
+
55
+ return `${meta.type}:${meta.domain}:${kebab}`;
56
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:type-detector
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 200
10
+ * @purpose Detects file type (hook, component, page, etc.) from file path — supports TypeScript and Swift conventions
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-23T10:00 [feat] [Claude Opus 4.6] Add Swift-specific type detection rules (ViewModel, View, Service, Tests)
14
+ * @changelog 2026-02-22T16:00 [fix] [Claude Opus 4.6] Add missing /agents/ and /services/ rules for functions/src/ block
15
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs, made configurable
16
+ */
17
+
18
+ import { basename, extname } from 'path';
19
+
20
+ /**
21
+ * Detect file type from path for Swift files.
22
+ *
23
+ * @param {string} relPath - Relative file path
24
+ * @param {string} nameNoExt - Filename without extension
25
+ * @returns {string} Detected type
26
+ */
27
+ function detectSwiftFileType(relPath, nameNoExt) {
28
+ // Test files
29
+ if (relPath.includes('Tests/') || nameNoExt.endsWith('Tests')) return 'test';
30
+
31
+ // ViewModel: XxxViewModel
32
+ if (nameNoExt.endsWith('ViewModel')) return 'viewmodel';
33
+
34
+ // Service: XxxService
35
+ if (nameNoExt.endsWith('Service') || relPath.includes('/Services/')) return 'service';
36
+
37
+ // View: XxxView (must come after ViewModel check)
38
+ if (nameNoExt.endsWith('View')) return 'view';
39
+
40
+ // Models directory
41
+ if (relPath.includes('/Models/')) return 'model';
42
+
43
+ // Theme directory
44
+ if (relPath.includes('/Theme/')) return 'config';
45
+
46
+ // Validators
47
+ if (nameNoExt.endsWith('Validator') || nameNoExt === 'Validators') return 'util';
48
+
49
+ // App entry point
50
+ if (nameNoExt.endsWith('App') || nameNoExt === 'AppDelegate') return 'config';
51
+
52
+ return 'module';
53
+ }
54
+
55
+ /**
56
+ * Detect file type from path using default heuristics.
57
+ *
58
+ * @param {string} relPath - Relative file path
59
+ * @returns {string} Detected type
60
+ */
61
+ export function detectFileType(relPath) {
62
+ const name = basename(relPath);
63
+ const ext = extname(name);
64
+ const nameNoExt = name.replace(ext, '');
65
+
66
+ // Swift files use a dedicated detector
67
+ if (ext === '.swift') {
68
+ return detectSwiftFileType(relPath, nameNoExt);
69
+ }
70
+
71
+ // Test files
72
+ if (name.includes('.test.') || name.includes('.spec.')) return 'test';
73
+ if (relPath.includes('/e2e/') || relPath.includes('/__tests__/')) return 'test';
74
+
75
+ // Type definitions
76
+ if (name.includes('.types.') || relPath.includes('/types/') || name === 'types.ts') return 'types';
77
+
78
+ // Hooks: useXxx
79
+ if (nameNoExt.startsWith('use') && nameNoExt[3] === nameNoExt[3]?.toUpperCase()) return 'hook';
80
+
81
+ // Pages: XxxPage
82
+ if (nameNoExt.endsWith('Page')) return 'page';
83
+
84
+ // Contexts: XxxContext
85
+ if (nameNoExt.endsWith('Context')) return 'context';
86
+
87
+ // Services
88
+ if (nameNoExt.endsWith('Service') || relPath.includes('/services/')) return 'service';
89
+
90
+ // Layout components
91
+ if (relPath.includes('/components/layout/')) return 'layout';
92
+
93
+ // Backend-specific types
94
+ if (relPath.startsWith('functions/src/') || relPath.includes('/server/')) {
95
+ if (relPath.includes('operations') || relPath.includes('Operations')) return 'operations';
96
+ if (relPath.includes('triggers') || relPath.includes('Triggers')) return 'trigger';
97
+ if (relPath.includes('jobs') || relPath.includes('Jobs')) return 'job';
98
+ if (relPath.includes('/config/')) return 'config';
99
+ if (relPath.includes('/utils/') || relPath.includes('/helpers/')) return 'util';
100
+ if (relPath.includes('/agents/')) return 'module';
101
+ if (relPath.includes('/services/')) return 'service';
102
+ if (relPath.includes('/middleware/')) return 'module';
103
+ if (relPath.includes('/testing/')) return 'test';
104
+ }
105
+
106
+ // General patterns
107
+ if (relPath.includes('/utils/')) return 'util';
108
+ if (nameNoExt.endsWith('Config') || nameNoExt.endsWith('config')) return 'config';
109
+ if (nameNoExt === 'constants' || nameNoExt.endsWith('Constants')) return 'constants';
110
+ if (nameNoExt === 'index') return 'index';
111
+ if (ext === '.tsx') return 'component';
112
+
113
+ return 'module';
114
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:gen-deps
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 80
10
+ * @purpose Generates deps.json — forward and reverse dependency graph from import analysis
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
14
+ */
15
+
16
+ /**
17
+ * Build dependency graph from import relationships.
18
+ *
19
+ * @param {object} importGraph - Map of filePath → Set of imported file paths
20
+ * @returns {object} Dependency graph with imports and importedBy for each file
21
+ */
22
+ export function generateDeps(importGraph) {
23
+ const depsGraph = {};
24
+
25
+ // Forward edges
26
+ for (const [filePath, importSet] of Object.entries(importGraph)) {
27
+ depsGraph[filePath] = {
28
+ imports: [...importSet].sort(),
29
+ importedBy: [],
30
+ };
31
+ }
32
+
33
+ // Reverse edges
34
+ for (const [filePath, importSet] of Object.entries(importGraph)) {
35
+ for (const target of importSet) {
36
+ if (depsGraph[target]) {
37
+ depsGraph[target].importedBy.push(filePath);
38
+ }
39
+ }
40
+ }
41
+
42
+ // Sort reverse edges
43
+ for (const deps of Object.values(depsGraph)) {
44
+ deps.importedBy.sort();
45
+ }
46
+
47
+ const edgeCount = Object.values(depsGraph).reduce((sum, d) => sum + d.imports.length, 0);
48
+
49
+ return {
50
+ generated: new Date().toISOString(),
51
+ nodeCount: Object.keys(depsGraph).length,
52
+ edgeCount,
53
+ graph: depsGraph,
54
+ };
55
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:gen-features
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 80
10
+ * @purpose Generates features.json — groups manifest files by domain into feature inventory
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
14
+ */
15
+
16
+ /**
17
+ * Group manifest files by domain into a feature inventory.
18
+ *
19
+ * @param {object} manifest - File manifest
20
+ * @returns {object} Domain → file type buckets
21
+ */
22
+ export function generateFeatures(manifest) {
23
+ const domains = {};
24
+
25
+ const TYPE_TO_BUCKET = {
26
+ component: 'components', hook: 'hooks', page: 'pages', context: 'contexts',
27
+ service: 'services', layout: 'layouts', types: 'types', util: 'utils',
28
+ operations: 'operations', trigger: 'triggers', job: 'jobs', test: 'tests',
29
+ };
30
+
31
+ for (const [relPath, meta] of Object.entries(manifest)) {
32
+ const domain = meta.domain;
33
+ if (!domains[domain]) {
34
+ domains[domain] = {};
35
+ }
36
+
37
+ const bucket = TYPE_TO_BUCKET[meta.type] || 'other';
38
+ if (!domains[domain][bucket]) {
39
+ domains[domain][bucket] = [];
40
+ }
41
+ domains[domain][bucket].push(relPath);
42
+ }
43
+
44
+ // Remove empty arrays
45
+ for (const domain of Object.values(domains)) {
46
+ for (const key of Object.keys(domain)) {
47
+ if (Array.isArray(domain[key]) && domain[key].length === 0) {
48
+ delete domain[key];
49
+ }
50
+ }
51
+ }
52
+
53
+ return {
54
+ generated: new Date().toISOString(),
55
+ domainCount: Object.keys(domains).length,
56
+ domains,
57
+ };
58
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id index:ai-agent-infra:generators-index
4
+ * @domain ai-agent-infra
5
+ * @type index
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity trivial
9
+ * @token-budget 30
10
+ * @purpose Barrel export for all registry generators
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created generators barrel export
14
+ */
15
+
16
+ export { generateManifest } from './manifest.mjs';
17
+ export { generateDeps } from './deps.mjs';
18
+ export { generateSemanticIds } from './semantic-ids.mjs';
19
+ export { generateSideEffects } from './side-effects.mjs';
20
+ export { generateFeatures } from './features.mjs';
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:gen-manifest
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity complex
9
+ * @token-budget 280
10
+ * @purpose Generates manifest.json — file metadata index with domain, type, imports, purpose from @ai-meta
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-23T10:00 [feat] [Claude Opus 4.6] Add fieldMap + skipImportParsing options for multi-language support
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
15
+ */
16
+
17
+ import { readFileSync, statSync } from 'fs';
18
+ import { parseAiMeta, normalizeType } from '../core/ai-meta-parser.mjs';
19
+ import { detectDomain as defaultDetectDomain } from '../core/domain-detector.mjs';
20
+ import { detectFileType } from '../core/type-detector.mjs';
21
+ import { parseImports, createResolver } from '../parsers/imports-typescript.mjs';
22
+
23
+ /**
24
+ * Build file manifest from discovered files.
25
+ *
26
+ * @param {{ fullPath: string, relPath: string }[]} allFiles
27
+ * @param {object} options
28
+ * @param {object} [options.aliasMap] - Import alias map
29
+ * @param {(relPath: string) => string} [options.detectDomain] - Custom domain detector
30
+ * @param {object} [options.fieldMap] - Field name normalization map for @ai-meta parsing
31
+ * @param {boolean} [options.skipImportParsing] - Skip TypeScript import parsing (for non-TS languages)
32
+ * @returns {{ manifest: object, importGraph: object, stats: object }}
33
+ */
34
+ export function generateManifest(allFiles, options = {}) {
35
+ const aliasMap = options.aliasMap || {};
36
+ const detectDomain = options.detectDomain || defaultDetectDomain;
37
+ const fieldMap = options.fieldMap || {};
38
+ const skipImportParsing = options.skipImportParsing || false;
39
+
40
+ const allFilePathsSet = new Set(allFiles.map(f => f.relPath));
41
+ const resolveSpecifier = skipImportParsing ? null : createResolver(aliasMap, allFilePathsSet);
42
+
43
+ const manifest = {};
44
+ const importGraph = {};
45
+ let aiMetaCount = 0;
46
+ let purposeCount = 0;
47
+ let firestoreCount = 0;
48
+
49
+ for (const { fullPath, relPath } of allFiles) {
50
+ let content;
51
+ try {
52
+ content = readFileSync(fullPath, 'utf-8');
53
+ } catch (err) {
54
+ console.warn(` Warning: Cannot read file: ${relPath} (${err.code || err.message})`);
55
+ continue;
56
+ }
57
+
58
+ const stat = statSync(fullPath);
59
+ const lineCount = content.split('\n').length;
60
+
61
+ const resolvedImports = [];
62
+ const rawSpecifiers = [];
63
+
64
+ if (!skipImportParsing) {
65
+ const rawImports = parseImports(content);
66
+ for (const spec of rawImports) {
67
+ const resolved = resolveSpecifier(spec, relPath);
68
+ if (resolved) resolvedImports.push(resolved);
69
+ if (spec.startsWith('.') || Object.keys(aliasMap).some(a => spec.startsWith(a))) {
70
+ rawSpecifiers.push(spec);
71
+ }
72
+ }
73
+ }
74
+
75
+ importGraph[relPath] = new Set(resolvedImports);
76
+
77
+ const aiMeta = parseAiMeta(content, { fieldMap });
78
+ if (aiMeta) aiMetaCount++;
79
+
80
+ const rawType = aiMeta?.type || detectFileType(relPath);
81
+ const finalType = normalizeType(rawType);
82
+
83
+ const entry = {
84
+ domain: aiMeta?.domain || detectDomain(relPath),
85
+ type: finalType,
86
+ imports: rawSpecifiers,
87
+ importedBy: [],
88
+ size: lineCount,
89
+ lastModified: stat.mtime.toISOString().split('T')[0],
90
+ };
91
+
92
+ if (aiMeta?.purpose) { entry.purpose = aiMeta.purpose; purposeCount++; }
93
+ if (aiMeta?.firestore) { entry.firestorePaths = aiMeta.firestore; firestoreCount++; }
94
+ if (aiMeta?.['depends-on']) { entry.curatedDeps = aiMeta['depends-on']; }
95
+ if (aiMeta?.id) { entry.id = aiMeta.id; }
96
+ if (aiMeta?.['side-effects'] !== undefined) { entry.sideEffects = aiMeta['side-effects']; }
97
+
98
+ manifest[relPath] = entry;
99
+ }
100
+
101
+ // Build reverse edges (only meaningful when import parsing is active)
102
+ if (!skipImportParsing) {
103
+ for (const [filePath, importSet] of Object.entries(importGraph)) {
104
+ for (const target of importSet) {
105
+ if (manifest[target]) {
106
+ manifest[target].importedBy.push(filePath);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // Sort importedBy
113
+ for (const entry of Object.values(manifest)) {
114
+ entry.importedBy.sort();
115
+ }
116
+
117
+ return {
118
+ manifest,
119
+ importGraph,
120
+ stats: {
121
+ totalFiles: allFiles.length,
122
+ aiMetaCount,
123
+ purposeCount,
124
+ firestoreCount,
125
+ coverage: `${aiMetaCount}/${allFiles.length} (${(aiMetaCount / allFiles.length * 100).toFixed(1)}%)`,
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:gen-semantic-ids
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity trivial
9
+ * @token-budget 60
10
+ * @purpose Generates semantic-ids.json — maps semantic IDs to file paths for instant lookup
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
14
+ */
15
+
16
+ import { generateSemanticId } from '../core/semantic-id.mjs';
17
+
18
+ /**
19
+ * Generate semantic ID → file path mapping from manifest.
20
+ *
21
+ * @param {object} manifest - File manifest
22
+ * @returns {object} Semantic IDs registry
23
+ */
24
+ export function generateSemanticIds(manifest) {
25
+ const ids = {};
26
+ let explicitCount = 0;
27
+ let derivedCount = 0;
28
+
29
+ for (const [relPath, meta] of Object.entries(manifest)) {
30
+ const semanticId = meta.id || generateSemanticId(relPath, meta);
31
+ ids[semanticId] = relPath;
32
+ if (meta.id) explicitCount++;
33
+ else derivedCount++;
34
+ }
35
+
36
+ return {
37
+ version: '1.0',
38
+ generated: new Date().toISOString(),
39
+ explicitCount,
40
+ derivedCount,
41
+ ids,
42
+ };
43
+ }