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.
- package/README.md +137 -0
- package/bin/add-headers.mjs +74 -0
- package/bin/cli.mjs +82 -0
- package/bin/generate.mjs +153 -0
- package/bin/init.mjs +198 -0
- package/bin/query.mjs +62 -0
- package/bin/validate.mjs +102 -0
- package/lib/core/ai-meta-parser.mjs +196 -0
- package/lib/core/config-loader.mjs +85 -0
- package/lib/core/domain-detector.mjs +79 -0
- package/lib/core/file-discovery.mjs +60 -0
- package/lib/core/registry.mjs +222 -0
- package/lib/core/semantic-id.mjs +56 -0
- package/lib/core/type-detector.mjs +114 -0
- package/lib/generators/deps.mjs +55 -0
- package/lib/generators/features.mjs +58 -0
- package/lib/generators/index.mjs +20 -0
- package/lib/generators/manifest.mjs +128 -0
- package/lib/generators/semantic-ids.mjs +43 -0
- package/lib/generators/side-effects.mjs +65 -0
- package/lib/index.mjs +19 -0
- package/lib/parsers/imports-typescript.mjs +104 -0
- package/lib/parsers/index.mjs +16 -0
- package/lib/query/engine.mjs +254 -0
- package/lib/schemas/config.schema.json +88 -0
- package/package.json +57 -0
- package/templates/config-swift.mjs +27 -0
- package/templates/config.mjs +25 -0
|
@@ -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
|
+
}
|