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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-meta
|
|
3
|
+
* @id module:ai-agent-infra:gen-side-effects
|
|
4
|
+
* @domain ai-agent-infra
|
|
5
|
+
* @type module
|
|
6
|
+
* @side-effects none
|
|
7
|
+
* @stability high
|
|
8
|
+
* @complexity moderate
|
|
9
|
+
* @token-budget 100
|
|
10
|
+
* @purpose Generates side-effects.json — side-effects graph from @side-effects annotations
|
|
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 side-effects graph from manifest metadata.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} manifest - File manifest with sideEffects field
|
|
22
|
+
* @returns {object} Side-effects registry
|
|
23
|
+
*/
|
|
24
|
+
export function generateSideEffects(manifest) {
|
|
25
|
+
const effects = {};
|
|
26
|
+
const mutations = [];
|
|
27
|
+
const missingSideEffects = [];
|
|
28
|
+
let documentedCount = 0;
|
|
29
|
+
|
|
30
|
+
for (const [relPath, meta] of Object.entries(manifest)) {
|
|
31
|
+
const semanticId = meta.id || generateSemanticId(relPath, meta);
|
|
32
|
+
|
|
33
|
+
if (meta.sideEffects !== undefined) {
|
|
34
|
+
documentedCount++;
|
|
35
|
+
const sideEffectsList = meta.sideEffects === 'none'
|
|
36
|
+
? []
|
|
37
|
+
: meta.sideEffects.split(',').map(s => s.trim()).filter(Boolean);
|
|
38
|
+
|
|
39
|
+
effects[semanticId] = {
|
|
40
|
+
id: semanticId,
|
|
41
|
+
file: relPath,
|
|
42
|
+
sideEffects: sideEffectsList,
|
|
43
|
+
downstreamOf: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const effect of sideEffectsList) {
|
|
47
|
+
if (effect.startsWith('writes:') || effect.startsWith('triggers:') || effect.startsWith('invalidates:')) {
|
|
48
|
+
mutations.push({ source: semanticId, effect, file: relPath });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
missingSideEffects.push(relPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
version: '1.0',
|
|
58
|
+
generated: new Date().toISOString(),
|
|
59
|
+
documentedCount,
|
|
60
|
+
missingCount: missingSideEffects.length,
|
|
61
|
+
effects,
|
|
62
|
+
mutations,
|
|
63
|
+
missing_side_effects: missingSideEffects,
|
|
64
|
+
};
|
|
65
|
+
}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-meta
|
|
3
|
+
* @id module:ai-agent-infra:index
|
|
4
|
+
* @domain ai-agent-infra
|
|
5
|
+
* @type index
|
|
6
|
+
* @side-effects none
|
|
7
|
+
* @stability high
|
|
8
|
+
* @complexity trivial
|
|
9
|
+
* @token-budget 50
|
|
10
|
+
* @purpose Main entry point for ai-codebase-registry package
|
|
11
|
+
*
|
|
12
|
+
* @filechangelog
|
|
13
|
+
* @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created package entry point
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { Registry } from './core/registry.mjs';
|
|
17
|
+
export { loadConfig } from './core/config-loader.mjs';
|
|
18
|
+
export { parseAiMeta } from './core/ai-meta-parser.mjs';
|
|
19
|
+
export { generateSemanticId } from './core/semantic-id.mjs';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-meta
|
|
3
|
+
* @id module:ai-agent-infra:imports-typescript
|
|
4
|
+
* @domain ai-agent-infra
|
|
5
|
+
* @type module
|
|
6
|
+
* @side-effects none
|
|
7
|
+
* @stability high
|
|
8
|
+
* @complexity moderate
|
|
9
|
+
* @token-budget 200
|
|
10
|
+
* @purpose Parses TypeScript/ESM import and export specifiers from source files; resolves aliases to project paths
|
|
11
|
+
*
|
|
12
|
+
* @filechangelog
|
|
13
|
+
* @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs, parameterized aliases
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { join, dirname } from 'path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract import/export specifiers from TypeScript/ESM file content.
|
|
20
|
+
* Returns an array of raw module specifiers.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} content - File content
|
|
23
|
+
* @returns {string[]} Array of module specifiers
|
|
24
|
+
*/
|
|
25
|
+
export function parseImports(content) {
|
|
26
|
+
const specifiers = new Set();
|
|
27
|
+
|
|
28
|
+
// Normalize multi-line imports
|
|
29
|
+
const normalized = content.replace(/import\s*\{[^}]*\}/gs, (match) =>
|
|
30
|
+
match.replace(/\n/g, ' ')
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Static imports: import ... from '...'
|
|
34
|
+
const staticRe = /(?:import|export)\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = staticRe.exec(normalized)) !== null) {
|
|
37
|
+
specifiers.add(match[1]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Side-effect imports: import '...'
|
|
41
|
+
const sideEffectRe = /import\s+['"]([^'"]+)['"]/g;
|
|
42
|
+
while ((match = sideEffectRe.exec(normalized)) !== null) {
|
|
43
|
+
specifiers.add(match[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Dynamic imports: import('...')
|
|
47
|
+
const dynamicRe = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
48
|
+
while ((match = dynamicRe.exec(content)) !== null) {
|
|
49
|
+
specifiers.add(match[1]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Re-exports
|
|
53
|
+
const reExportRe = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
|
|
54
|
+
while ((match = reExportRe.exec(content)) !== null) {
|
|
55
|
+
specifiers.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [...specifiers];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a module specifier resolver with project-specific aliases.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} aliasMap - Map of alias prefixes to target paths
|
|
65
|
+
* @param {Set<string>} allFilePaths - Set of all known file paths for resolution
|
|
66
|
+
* @returns {(specifier: string, fromRelPath: string) => string|null}
|
|
67
|
+
*/
|
|
68
|
+
export function createResolver(aliasMap, allFilePaths) {
|
|
69
|
+
const aliasEntries = Object.entries(aliasMap);
|
|
70
|
+
|
|
71
|
+
return function resolveSpecifier(specifier, fromRelPath) {
|
|
72
|
+
const isAliased = aliasEntries.some(([alias]) => specifier.startsWith(alias));
|
|
73
|
+
if (!specifier.startsWith('.') && !isAliased) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let basePath;
|
|
78
|
+
|
|
79
|
+
for (const [alias, target] of aliasEntries) {
|
|
80
|
+
if (specifier.startsWith(alias)) {
|
|
81
|
+
basePath = join(target, specifier.slice(alias.length));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!basePath && specifier.startsWith('.')) {
|
|
87
|
+
basePath = join(dirname(fromRelPath), specifier);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!basePath) return null;
|
|
91
|
+
basePath = basePath.replace(/\\/g, '/');
|
|
92
|
+
|
|
93
|
+
const candidates = [
|
|
94
|
+
basePath, basePath + '.ts', basePath + '.tsx',
|
|
95
|
+
basePath + '/index.ts', basePath + '/index.tsx',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (const c of candidates) {
|
|
99
|
+
if (allFilePaths.has(c)) return c;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-meta
|
|
3
|
+
* @id index:ai-agent-infra:parsers-index
|
|
4
|
+
* @domain ai-agent-infra
|
|
5
|
+
* @type index
|
|
6
|
+
* @side-effects none
|
|
7
|
+
* @stability high
|
|
8
|
+
* @complexity trivial
|
|
9
|
+
* @token-budget 20
|
|
10
|
+
* @purpose Barrel export for all parsers
|
|
11
|
+
*
|
|
12
|
+
* @filechangelog
|
|
13
|
+
* @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created parsers barrel export
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export { parseImports, createResolver } from './imports-typescript.mjs';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ai-meta
|
|
3
|
+
* @id module:ai-agent-infra:query-engine
|
|
4
|
+
* @domain ai-agent-infra
|
|
5
|
+
* @type module
|
|
6
|
+
* @side-effects none
|
|
7
|
+
* @stability medium
|
|
8
|
+
* @complexity complex
|
|
9
|
+
* @token-budget 300
|
|
10
|
+
* @purpose Query engine for registry data — supports domain/type/impact/coverage/orphaned-routes queries
|
|
11
|
+
*
|
|
12
|
+
* @filechangelog
|
|
13
|
+
* @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created query engine with all query modes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Registry } from '../core/registry.mjs';
|
|
17
|
+
|
|
18
|
+
// ANSI colors
|
|
19
|
+
const C = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute a query against the registry and return formatted results.
|
|
32
|
+
*
|
|
33
|
+
* @param {Registry} registry - Loaded registry instance
|
|
34
|
+
* @param {object} query - Query parameters
|
|
35
|
+
* @param {{ json?: boolean }} options
|
|
36
|
+
* @returns {{ output: string, data: any }}
|
|
37
|
+
*/
|
|
38
|
+
export function executeQuery(registry, query, options = {}) {
|
|
39
|
+
if (query.help) return formatHelp();
|
|
40
|
+
if (query.coverage) return formatCoverage(registry, options);
|
|
41
|
+
if (query.orphanedRoutes) return formatOrphanedRoutes(registry, options);
|
|
42
|
+
if (query.id) return formatIdLookup(registry, query.id, options);
|
|
43
|
+
if (query.consumers) return formatConsumers(registry, query.consumers, options);
|
|
44
|
+
if (query.deps) return formatDeps(registry, query.deps, options);
|
|
45
|
+
if (query.impact) return formatImpact(registry, query.impact, options);
|
|
46
|
+
if (query.sideEffects) return formatSideEffects(registry, query.sideEffects, options);
|
|
47
|
+
if (query.complexity) return formatComplexity(registry, query.complexity, options);
|
|
48
|
+
if (query.domain) return formatDomainQuery(registry, query.domain, query.type, options);
|
|
49
|
+
return formatHelp();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatHelp() {
|
|
53
|
+
const output = `
|
|
54
|
+
${C.bold}ai-codebase-registry query${C.reset}
|
|
55
|
+
|
|
56
|
+
${C.bold}Usage:${C.reset}
|
|
57
|
+
ai-registry query [options]
|
|
58
|
+
node scripts/query-registry.mjs [options]
|
|
59
|
+
|
|
60
|
+
${C.bold}Options:${C.reset}
|
|
61
|
+
--domain <name> Filter files by domain
|
|
62
|
+
--type <type> Filter files by type (combine with --domain)
|
|
63
|
+
--id <semantic-id> Lookup file by semantic ID
|
|
64
|
+
--consumers <filepath> Show who imports this file
|
|
65
|
+
--deps <filepath> Show what this file imports
|
|
66
|
+
--impact <filepath> Full impact analysis for changing a file
|
|
67
|
+
--side-effects <pattern> Find files with matching side-effects
|
|
68
|
+
--complexity <level> Find files at/above complexity (trivial|moderate|complex)
|
|
69
|
+
--orphaned-routes Find routes with no nav entry point
|
|
70
|
+
--coverage Show @ai-meta coverage statistics
|
|
71
|
+
--json Output as JSON (for piping)
|
|
72
|
+
--help Show this help
|
|
73
|
+
`;
|
|
74
|
+
return { output, data: null };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatDomainQuery(registry, domain, type, options) {
|
|
78
|
+
const results = registry.query({ domain, type });
|
|
79
|
+
if (options.json) return { output: JSON.stringify(results, null, 2), data: results };
|
|
80
|
+
|
|
81
|
+
const header = type
|
|
82
|
+
? `Registry Query: domain=${domain}, type=${type}`
|
|
83
|
+
: `Registry Query: domain=${domain}`;
|
|
84
|
+
|
|
85
|
+
let output = `\n${C.bold}${header}${C.reset}\n${'='.repeat(header.length)}\n`;
|
|
86
|
+
for (const r of results) {
|
|
87
|
+
const purpose = r.purpose ? ` ${C.dim}"${r.purpose}"${C.reset}` : '';
|
|
88
|
+
output += ` ${C.cyan}${r.path}${C.reset} ${C.yellow}${r.type}${C.reset} ${r.size} lines${purpose}\n`;
|
|
89
|
+
}
|
|
90
|
+
output += `\n${C.green}Found ${results.length} files${C.reset}\n`;
|
|
91
|
+
return { output, data: results };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatIdLookup(registry, id, options) {
|
|
95
|
+
const result = registry.resolveWithMeta(id);
|
|
96
|
+
if (!result) {
|
|
97
|
+
const output = `${C.red}No file found for semantic ID: ${id}${C.reset}\n`;
|
|
98
|
+
return { output, data: null };
|
|
99
|
+
}
|
|
100
|
+
if (options.json) return { output: JSON.stringify(result, null, 2), data: result };
|
|
101
|
+
|
|
102
|
+
let output = `\n${C.bold}Semantic ID: ${id}${C.reset}\n${'='.repeat(40)}\n`;
|
|
103
|
+
output += ` ${C.cyan}Path:${C.reset} ${result.path}\n`;
|
|
104
|
+
output += ` ${C.cyan}Domain:${C.reset} ${result.domain}\n`;
|
|
105
|
+
output += ` ${C.cyan}Type:${C.reset} ${result.type}\n`;
|
|
106
|
+
output += ` ${C.cyan}Size:${C.reset} ${result.size} lines\n`;
|
|
107
|
+
if (result.purpose) output += ` ${C.cyan}Purpose:${C.reset} ${result.purpose}\n`;
|
|
108
|
+
if (result.sideEffects) output += ` ${C.cyan}Effects:${C.reset} ${result.sideEffects}\n`;
|
|
109
|
+
return { output, data: result };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatConsumers(registry, filePath, options) {
|
|
113
|
+
const consumers = registry.consumers(filePath);
|
|
114
|
+
if (options.json) return { output: JSON.stringify(consumers, null, 2), data: consumers };
|
|
115
|
+
|
|
116
|
+
let output = `\n${C.bold}Consumers of: ${filePath}${C.reset}\n${'='.repeat(50)}\n`;
|
|
117
|
+
if (consumers.length === 0) {
|
|
118
|
+
output += ` ${C.dim}No consumers found (not imported by any file)${C.reset}\n`;
|
|
119
|
+
} else {
|
|
120
|
+
for (const c of consumers) output += ` ${C.cyan}${c}${C.reset}\n`;
|
|
121
|
+
}
|
|
122
|
+
output += `\n${C.green}${consumers.length} direct consumers${C.reset}\n`;
|
|
123
|
+
return { output, data: consumers };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatDeps(registry, filePath, options) {
|
|
127
|
+
const deps = registry.dependencies(filePath);
|
|
128
|
+
if (options.json) return { output: JSON.stringify(deps, null, 2), data: deps };
|
|
129
|
+
|
|
130
|
+
let output = `\n${C.bold}Dependencies of: ${filePath}${C.reset}\n${'='.repeat(50)}\n`;
|
|
131
|
+
if (deps.length === 0) {
|
|
132
|
+
output += ` ${C.dim}No internal dependencies${C.reset}\n`;
|
|
133
|
+
} else {
|
|
134
|
+
for (const d of deps) output += ` ${C.cyan}${d}${C.reset}\n`;
|
|
135
|
+
}
|
|
136
|
+
output += `\n${C.green}${deps.length} direct dependencies${C.reset}\n`;
|
|
137
|
+
return { output, data: deps };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatImpact(registry, filePath, options) {
|
|
141
|
+
const impact = registry.impact(filePath);
|
|
142
|
+
if (options.json) return { output: JSON.stringify(impact, null, 2), data: impact };
|
|
143
|
+
|
|
144
|
+
let output = `\n${C.bold}Impact Analysis: ${filePath}${C.reset}\n${'='.repeat(60)}\n`;
|
|
145
|
+
|
|
146
|
+
output += `\n${C.bold}Direct consumers (${impact.directConsumers.length}):${C.reset}\n`;
|
|
147
|
+
for (const c of impact.directConsumers) output += ` ${C.cyan}${c}${C.reset}\n`;
|
|
148
|
+
|
|
149
|
+
if (impact.transitiveConsumers.length > 0) {
|
|
150
|
+
output += `\n${C.bold}Transitive consumers (${impact.transitiveConsumers.length}):${C.reset}\n`;
|
|
151
|
+
for (const t of impact.transitiveConsumers) output += ` ${C.dim}${t}${C.reset}\n`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (impact.changeImpactRules.length > 0) {
|
|
155
|
+
output += `\n${C.bold}Change Impact Rules:${C.reset}\n`;
|
|
156
|
+
for (const rule of impact.changeImpactRules) {
|
|
157
|
+
output += ` ${C.yellow}! ${rule.then || rule.description || JSON.stringify(rule)}${C.reset}\n`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (impact.sideEffects.length > 0) {
|
|
162
|
+
output += `\n${C.bold}Side Effects:${C.reset}\n`;
|
|
163
|
+
for (const e of impact.sideEffects) output += ` ${C.magenta}${e}${C.reset}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const total = impact.directConsumers.length + impact.transitiveConsumers.length;
|
|
167
|
+
output += `\n${C.green}Total affected: ${total} files${C.reset}\n`;
|
|
168
|
+
return { output, data: impact };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatSideEffects(registry, pattern, options) {
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const [id, entry] of Object.entries(registry.sideEffects)) {
|
|
174
|
+
if (entry.sideEffects.some(e => e.includes(pattern))) {
|
|
175
|
+
results.push({ id, file: entry.file, sideEffects: entry.sideEffects });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (options.json) return { output: JSON.stringify(results, null, 2), data: results };
|
|
179
|
+
|
|
180
|
+
let output = `\n${C.bold}Side-effects matching: "${pattern}"${C.reset}\n${'='.repeat(50)}\n`;
|
|
181
|
+
for (const r of results) {
|
|
182
|
+
output += ` ${C.cyan}${r.file}${C.reset} ${C.dim}(${r.id})${C.reset}\n`;
|
|
183
|
+
for (const e of r.sideEffects) output += ` ${C.magenta}${e}${C.reset}\n`;
|
|
184
|
+
}
|
|
185
|
+
output += `\n${C.green}Found ${results.length} files${C.reset}\n`;
|
|
186
|
+
return { output, data: results };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatComplexity(registry, level, options) {
|
|
190
|
+
const LEVELS = { trivial: 1, moderate: 2, complex: 3 };
|
|
191
|
+
const threshold = LEVELS[level] || 2;
|
|
192
|
+
const results = [];
|
|
193
|
+
|
|
194
|
+
for (const [path, meta] of Object.entries(registry.manifest)) {
|
|
195
|
+
// Infer complexity from size if not explicitly set
|
|
196
|
+
const size = meta.size || 0;
|
|
197
|
+
let fileComplexity;
|
|
198
|
+
if (size < 50) fileComplexity = 1;
|
|
199
|
+
else if (size < 150) fileComplexity = 2;
|
|
200
|
+
else fileComplexity = 3;
|
|
201
|
+
|
|
202
|
+
if (fileComplexity >= threshold) {
|
|
203
|
+
results.push({ path, size, domain: meta.domain, type: meta.type });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
results.sort((a, b) => b.size - a.size);
|
|
208
|
+
if (options.json) return { output: JSON.stringify(results, null, 2), data: results };
|
|
209
|
+
|
|
210
|
+
let output = `\n${C.bold}Files with complexity >= ${level}${C.reset}\n${'='.repeat(50)}\n`;
|
|
211
|
+
for (const r of results.slice(0, 50)) {
|
|
212
|
+
output += ` ${C.cyan}${r.path}${C.reset} ${C.yellow}${r.size} lines${C.reset} ${r.domain}/${r.type}\n`;
|
|
213
|
+
}
|
|
214
|
+
if (results.length > 50) output += ` ${C.dim}... and ${results.length - 50} more${C.reset}\n`;
|
|
215
|
+
output += `\n${C.green}Found ${results.length} files${C.reset}\n`;
|
|
216
|
+
return { output, data: results };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatOrphanedRoutes(registry, options) {
|
|
220
|
+
const orphaned = registry.orphanedRoutes();
|
|
221
|
+
if (options.json) return { output: JSON.stringify(orphaned, null, 2), data: orphaned };
|
|
222
|
+
|
|
223
|
+
let output = `\n${C.bold}Orphaned Routes (no nav entry point)${C.reset}\n${'='.repeat(50)}\n`;
|
|
224
|
+
if (orphaned.length === 0) {
|
|
225
|
+
output += ` ${C.green}All routes have entry points!${C.reset}\n`;
|
|
226
|
+
} else {
|
|
227
|
+
for (const r of orphaned) {
|
|
228
|
+
output += ` ${C.red}${r.path}${C.reset} -> ${C.cyan}${r.component}${C.reset}\n`;
|
|
229
|
+
if (r.file) output += ` ${C.dim}${r.file}${C.reset}\n`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
output += `\n${C.yellow}${orphaned.length} orphaned routes${C.reset}\n`;
|
|
233
|
+
return { output, data: orphaned };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatCoverage(registry, options) {
|
|
237
|
+
const coverage = registry.coverage();
|
|
238
|
+
if (options.json) return { output: JSON.stringify(coverage, null, 2), data: coverage };
|
|
239
|
+
|
|
240
|
+
let output = `\n${C.bold}Registry Coverage${C.reset}\n${'='.repeat(40)}\n`;
|
|
241
|
+
output += ` ${C.cyan}Total files:${C.reset} ${coverage.totalFiles}\n`;
|
|
242
|
+
output += ` ${C.cyan}With @id:${C.reset} ${coverage.withId.count} (${coverage.withId.pct}%)\n`;
|
|
243
|
+
output += ` ${C.cyan}With @side-effects:${C.reset} ${coverage.withSideEffects.count} (${coverage.withSideEffects.pct}%)\n`;
|
|
244
|
+
output += ` ${C.cyan}With @purpose:${C.reset} ${coverage.withPurpose.count} (${coverage.withPurpose.pct}%)\n`;
|
|
245
|
+
|
|
246
|
+
if (coverage.missingId.length > 0 && coverage.missingId.length <= 20) {
|
|
247
|
+
output += `\n${C.yellow}Missing @id (${coverage.missingId.length}):${C.reset}\n`;
|
|
248
|
+
for (const p of coverage.missingId) output += ` ${C.dim}${p}${C.reset}\n`;
|
|
249
|
+
} else if (coverage.missingId.length > 20) {
|
|
250
|
+
output += `\n${C.yellow}Missing @id: ${coverage.missingId.length} files (use --json for full list)${C.reset}\n`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { output, data: coverage };
|
|
254
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "ai-codebase-registry Config",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["name", "scanDirs"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Project name"
|
|
10
|
+
},
|
|
11
|
+
"description": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"description": "Project description"
|
|
14
|
+
},
|
|
15
|
+
"scanDirs": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["dir"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"dir": { "type": "string" },
|
|
22
|
+
"prefix": { "type": "string" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"description": "Directories to scan for source files"
|
|
27
|
+
},
|
|
28
|
+
"extensions": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": { "type": "string" },
|
|
31
|
+
"default": [".ts", ".tsx"],
|
|
32
|
+
"description": "File extensions to include"
|
|
33
|
+
},
|
|
34
|
+
"skipDirs": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string" },
|
|
37
|
+
"description": "Directory names to skip during scanning"
|
|
38
|
+
},
|
|
39
|
+
"aliases": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": { "type": "string" },
|
|
42
|
+
"description": "Import alias map (e.g., { '@/': 'src/' })"
|
|
43
|
+
},
|
|
44
|
+
"router": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["react-router", "nextjs", "expo-router", "express", "none"],
|
|
47
|
+
"default": "none",
|
|
48
|
+
"description": "Router framework for route extraction"
|
|
49
|
+
},
|
|
50
|
+
"routerEntryFile": {
|
|
51
|
+
"type": ["string", "null"],
|
|
52
|
+
"description": "Path to router entry file (e.g., App.tsx)"
|
|
53
|
+
},
|
|
54
|
+
"routeBuilderFile": {
|
|
55
|
+
"type": ["string", "null"],
|
|
56
|
+
"description": "Path to route builder file (e.g., routes.ts)"
|
|
57
|
+
},
|
|
58
|
+
"dataStore": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"enum": ["firestore", "postgres", "mongodb", "none"],
|
|
61
|
+
"default": "none",
|
|
62
|
+
"description": "Data store for relationships template"
|
|
63
|
+
},
|
|
64
|
+
"registryDir": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"default": "_registry",
|
|
67
|
+
"description": "Output directory for registry files"
|
|
68
|
+
},
|
|
69
|
+
"agent": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"default": "claude-code",
|
|
72
|
+
"description": "Target AI agent"
|
|
73
|
+
},
|
|
74
|
+
"claudemd": {
|
|
75
|
+
"type": "boolean",
|
|
76
|
+
"default": true,
|
|
77
|
+
"description": "Whether to generate/update CLAUDE.md sections"
|
|
78
|
+
},
|
|
79
|
+
"rulesFile": {
|
|
80
|
+
"type": ["string", "null"],
|
|
81
|
+
"description": "Path to project-specific scoring rules"
|
|
82
|
+
},
|
|
83
|
+
"patternsFile": {
|
|
84
|
+
"type": ["string", "null"],
|
|
85
|
+
"description": "Path to canonical patterns file"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-codebase-registry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Registry-driven AI-agent infrastructure for codebases. Auto-generates machine-readable metadata, dependency graphs, side-effects tracking, and query APIs for AI coding agents. Supports TypeScript, Swift, Kotlin, and Python.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./lib/index.mjs",
|
|
8
|
+
"./core/file-discovery": "./lib/core/file-discovery.mjs",
|
|
9
|
+
"./core/ai-meta-parser": "./lib/core/ai-meta-parser.mjs",
|
|
10
|
+
"./core/domain-detector": "./lib/core/domain-detector.mjs",
|
|
11
|
+
"./core/type-detector": "./lib/core/type-detector.mjs",
|
|
12
|
+
"./core/semantic-id": "./lib/core/semantic-id.mjs",
|
|
13
|
+
"./parsers": "./lib/parsers/index.mjs",
|
|
14
|
+
"./parsers/imports-typescript": "./lib/parsers/imports-typescript.mjs",
|
|
15
|
+
"./generators": "./lib/generators/index.mjs",
|
|
16
|
+
"./query": "./lib/query/engine.mjs"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"ai-registry": "./bin/cli.mjs",
|
|
20
|
+
"ai-codebase-registry": "./bin/cli.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"lib/",
|
|
25
|
+
"templates/",
|
|
26
|
+
"schemas/",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test tests/",
|
|
31
|
+
"lint": "echo 'no lint configured'"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"ai",
|
|
35
|
+
"agent",
|
|
36
|
+
"codebase",
|
|
37
|
+
"registry",
|
|
38
|
+
"metadata",
|
|
39
|
+
"claude",
|
|
40
|
+
"ai-meta",
|
|
41
|
+
"dependency-graph",
|
|
42
|
+
"side-effects",
|
|
43
|
+
"swift",
|
|
44
|
+
"multi-language"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/ming0627/bellyfun"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @type {import('ai-codebase-registry').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
name: 'my-swift-project',
|
|
4
|
+
description: 'What this project does',
|
|
5
|
+
|
|
6
|
+
scanDirs: [
|
|
7
|
+
{ dir: 'Sources', prefix: 'Sources' },
|
|
8
|
+
{ dir: 'Tests', prefix: 'Tests' },
|
|
9
|
+
],
|
|
10
|
+
extensions: ['.swift'],
|
|
11
|
+
skipDirs: ['build', '.build', 'Pods', 'DerivedData', 'xcuserdata'],
|
|
12
|
+
|
|
13
|
+
language: 'swift',
|
|
14
|
+
fieldMap: {
|
|
15
|
+
mutations: 'side-effects',
|
|
16
|
+
dependencies: 'depends-on',
|
|
17
|
+
},
|
|
18
|
+
generators: ['manifest', 'semantic-ids', 'side-effects', 'features'],
|
|
19
|
+
|
|
20
|
+
aliases: {},
|
|
21
|
+
router: 'none',
|
|
22
|
+
dataStore: 'none', // 'firestore' | 'coredata' | 'realm' | 'none'
|
|
23
|
+
|
|
24
|
+
registryDir: '_registry',
|
|
25
|
+
agent: 'claude-code',
|
|
26
|
+
claudemd: true,
|
|
27
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** @type {import('ai-codebase-registry').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
name: 'my-project',
|
|
4
|
+
description: 'What this project does',
|
|
5
|
+
|
|
6
|
+
scanDirs: [
|
|
7
|
+
{ dir: 'src', prefix: 'src' },
|
|
8
|
+
],
|
|
9
|
+
extensions: ['.ts', '.tsx'],
|
|
10
|
+
skipDirs: ['node_modules', 'dist', '.next', 'coverage', '__tests__', '__mocks__'],
|
|
11
|
+
|
|
12
|
+
aliases: {
|
|
13
|
+
'@/': 'src/',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
router: 'none', // 'react-router' | 'nextjs' | 'expo-router' | 'express' | 'none'
|
|
17
|
+
routerEntryFile: null,
|
|
18
|
+
routeBuilderFile: null,
|
|
19
|
+
|
|
20
|
+
dataStore: 'none', // 'firestore' | 'postgres' | 'mongodb' | 'none'
|
|
21
|
+
|
|
22
|
+
registryDir: '_registry',
|
|
23
|
+
agent: 'claude-code',
|
|
24
|
+
claudemd: true,
|
|
25
|
+
};
|