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/bin/query.mjs ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ai-meta
4
+ * @id module:ai-agent-infra:cli-query
5
+ * @domain ai-agent-infra
6
+ * @type module
7
+ * @side-effects none
8
+ * @stability medium
9
+ * @complexity moderate
10
+ * @token-budget 100
11
+ * @purpose CLI command: query registry data
12
+ *
13
+ * @filechangelog
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created query CLI command
15
+ */
16
+
17
+ import { resolve } from 'path';
18
+ import { Registry } from '../lib/core/registry.mjs';
19
+ import { executeQuery } from '../lib/query/engine.mjs';
20
+
21
+ function parseArgs(argv) {
22
+ const args = {};
23
+ const raw = argv.slice(2);
24
+ for (let i = 0; i < raw.length; i++) {
25
+ const arg = raw[i];
26
+ if (arg.startsWith('--')) {
27
+ const key = arg.slice(2);
28
+ // Boolean flags
29
+ if (['json', 'help', 'orphaned-routes', 'coverage'].includes(key)) {
30
+ args[key] = true;
31
+ } else if (i + 1 < raw.length && !raw[i + 1].startsWith('--')) {
32
+ args[key] = raw[++i];
33
+ } else {
34
+ args[key] = true;
35
+ }
36
+ }
37
+ }
38
+ return args;
39
+ }
40
+
41
+ const args = parseArgs(process.argv);
42
+ const registryDir = resolve(args['registry-dir'] || '_registry');
43
+
44
+ const registry = Registry.load(registryDir);
45
+
46
+ const query = {
47
+ domain: args.domain,
48
+ type: args.type,
49
+ id: args.id,
50
+ consumers: args.consumers,
51
+ deps: args.deps,
52
+ impact: args.impact,
53
+ sideEffects: args['side-effects'],
54
+ complexity: args.complexity,
55
+ orphanedRoutes: args['orphaned-routes'],
56
+ coverage: args.coverage,
57
+ help: args.help,
58
+ };
59
+
60
+ const options = { json: args.json };
61
+ const { output } = executeQuery(registry, query, options);
62
+ console.log(output);
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ai-meta
4
+ * @id module:ai-agent-infra:cli-validate
5
+ * @domain ai-agent-infra
6
+ * @type module
7
+ * @side-effects none
8
+ * @stability medium
9
+ * @complexity moderate
10
+ * @token-budget 120
11
+ * @purpose CLI command: validate @ai-meta coverage and report gaps
12
+ *
13
+ * @filechangelog
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created validate CLI command
15
+ */
16
+
17
+ import { resolve } from 'path';
18
+ import { Registry } from '../lib/core/registry.mjs';
19
+
20
+ const C = {
21
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
22
+ green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m',
23
+ };
24
+
25
+ function parseArgs(argv) {
26
+ const args = {};
27
+ const raw = argv.slice(2);
28
+ for (let i = 0; i < raw.length; i++) {
29
+ const arg = raw[i];
30
+ if (arg.startsWith('--')) {
31
+ const key = arg.slice(2);
32
+ if (['json', 'strict'].includes(key)) {
33
+ args[key] = true;
34
+ } else if (i + 1 < raw.length && !raw[i + 1].startsWith('--')) {
35
+ args[key] = raw[++i];
36
+ } else {
37
+ args[key] = true;
38
+ }
39
+ }
40
+ }
41
+ return args;
42
+ }
43
+
44
+ const args = parseArgs(process.argv);
45
+ const registryDir = resolve(args['registry-dir'] || '_registry');
46
+ const registry = Registry.load(registryDir);
47
+ const coverage = registry.coverage();
48
+
49
+ if (args.json) {
50
+ console.log(JSON.stringify(coverage, null, 2));
51
+ process.exit(coverage.missingId.length > 0 ? 1 : 0);
52
+ }
53
+
54
+ console.log(`\n${C.bold}Registry Validation${C.reset}`);
55
+ console.log('='.repeat(40));
56
+
57
+ const checks = [
58
+ {
59
+ label: '@id coverage',
60
+ count: coverage.withId.count,
61
+ total: coverage.totalFiles,
62
+ pct: coverage.withId.pct,
63
+ missing: coverage.missingId,
64
+ },
65
+ {
66
+ label: '@side-effects coverage',
67
+ count: coverage.withSideEffects.count,
68
+ total: coverage.totalFiles,
69
+ pct: coverage.withSideEffects.pct,
70
+ missing: coverage.missingSideEffects,
71
+ },
72
+ {
73
+ label: '@purpose coverage',
74
+ count: coverage.withPurpose.count,
75
+ total: coverage.totalFiles,
76
+ pct: coverage.withPurpose.pct,
77
+ missing: [],
78
+ },
79
+ ];
80
+
81
+ let hasFailures = false;
82
+
83
+ for (const check of checks) {
84
+ const pct = parseFloat(check.pct);
85
+ const icon = pct === 100 ? `${C.green}PASS${C.reset}` : pct >= 95 ? `${C.yellow}WARN${C.reset}` : `${C.red}FAIL${C.reset}`;
86
+ console.log(`\n ${icon} ${check.label}: ${check.count}/${check.total} (${check.pct}%)`);
87
+
88
+ if (check.missing.length > 0) {
89
+ hasFailures = true;
90
+ const shown = check.missing.slice(0, 10);
91
+ for (const p of shown) console.log(` ${C.dim}${p}${C.reset}`);
92
+ if (check.missing.length > 10) {
93
+ console.log(` ${C.dim}... and ${check.missing.length - 10} more${C.reset}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ console.log(`\n${C.bold}Summary:${C.reset} ${coverage.totalFiles} files, ${hasFailures ? `${C.red}gaps found${C.reset}` : `${C.green}100% coverage${C.reset}`}\n`);
99
+
100
+ if (args.strict && hasFailures) {
101
+ process.exit(1);
102
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:ai-meta-parser
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 @ai-meta headers from source files — supports JSDoc, triple-slash (Swift), and line-comment styles
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-23T10:00 [feat] [Claude Opus 4.6] Add triple-slash + line-comment parsing, fieldMap normalization for multi-language support
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
15
+ */
16
+
17
+ const FIELDS = [
18
+ 'domain', 'type', 'purpose', 'depends-on', 'firestore',
19
+ 'id', 'side-effects', 'stability', 'complexity', 'token-budget', 'nav-entry',
20
+ 'mutations', 'dependencies',
21
+ ];
22
+ const SINGLE_TOKEN_FIELDS = new Set(['domain', 'type', 'id', 'stability', 'complexity']);
23
+
24
+ /**
25
+ * Extract @ai-meta block from JSDoc comment (/** ... * /).
26
+ * @param {string} content
27
+ * @returns {string[]|null} Lines of the block, or null
28
+ */
29
+ function extractJsDocBlock(content) {
30
+ const match = content.match(/\/\*\*[\s\S]*?@ai-meta[\s\S]*?\*\//);
31
+ if (!match) return null;
32
+ return match[0].split('\n');
33
+ }
34
+
35
+ /**
36
+ * Extract @ai-meta block from consecutive triple-slash comments (/// ...).
37
+ * Used by Swift files. Block must start within the first 10 lines.
38
+ * @param {string} content
39
+ * @returns {string[]|null} Lines of the block, or null
40
+ */
41
+ function extractTripleSlashBlock(content) {
42
+ const lines = content.split('\n');
43
+ let inBlock = false;
44
+ const blockLines = [];
45
+
46
+ for (let i = 0; i < lines.length && i < 50; i++) {
47
+ const trimmed = lines[i].trim();
48
+ if (trimmed.startsWith('///')) {
49
+ const inner = trimmed.slice(3).trim();
50
+ if (inner === '@ai-meta' || inner.startsWith('@ai-meta')) {
51
+ inBlock = true;
52
+ blockLines.push(inner);
53
+ continue;
54
+ }
55
+ if (inBlock) {
56
+ blockLines.push(inner);
57
+ }
58
+ } else if (inBlock) {
59
+ break; // Non-/// line ends the block
60
+ }
61
+ }
62
+
63
+ return inBlock && blockLines.length > 1 ? blockLines : null;
64
+ }
65
+
66
+ /**
67
+ * Extract @ai-meta block from consecutive line comments (// ...).
68
+ * For Kotlin/Go/future languages. Block must start within first 10 lines.
69
+ * @param {string} content
70
+ * @returns {string[]|null} Lines of the block, or null
71
+ */
72
+ function extractLineCommentBlock(content) {
73
+ const lines = content.split('\n');
74
+ let inBlock = false;
75
+ const blockLines = [];
76
+
77
+ for (let i = 0; i < lines.length && i < 50; i++) {
78
+ const trimmed = lines[i].trim();
79
+ if (trimmed.startsWith('//') && !trimmed.startsWith('///')) {
80
+ const inner = trimmed.slice(2).trim();
81
+ if (inner === '@ai-meta' || inner.startsWith('@ai-meta')) {
82
+ inBlock = true;
83
+ blockLines.push(inner);
84
+ continue;
85
+ }
86
+ if (inBlock) {
87
+ blockLines.push(inner);
88
+ }
89
+ } else if (inBlock) {
90
+ break;
91
+ }
92
+ }
93
+
94
+ return inBlock && blockLines.length > 1 ? blockLines : null;
95
+ }
96
+
97
+ /**
98
+ * Parse a single line for a field value, supporting both `@field value` and `field: value` syntax.
99
+ * @param {string} line
100
+ * @param {string} fieldPattern - Regex alternation of field names
101
+ * @returns {{ field: string, value: string }|null}
102
+ */
103
+ function parseFieldLine(line, fieldPattern) {
104
+ // Try @field value syntax (JSDoc style)
105
+ let match = line.match(new RegExp(`@(${fieldPattern})\\s+(.+)`));
106
+ if (match) {
107
+ return { field: match[1], value: match[2] };
108
+ }
109
+
110
+ // Try field: value syntax (Swift/YAML style)
111
+ match = line.match(new RegExp(`^(${fieldPattern}):\\s*(.+)`));
112
+ if (match) {
113
+ return { field: match[1], value: match[2] };
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Parse @ai-meta header from file content.
121
+ * Returns an object with extracted fields, or null if no @ai-meta block found.
122
+ *
123
+ * Supports three comment styles:
124
+ * - JSDoc: /** @ai-meta ... * / (TypeScript/JavaScript)
125
+ * - Triple-slash: /// @ai-meta ... (Swift)
126
+ * - Line-comment: // @ai-meta ... (Kotlin/Go)
127
+ *
128
+ * Supports two field syntaxes:
129
+ * - @field value (JSDoc style)
130
+ * - field: value (YAML-like style)
131
+ *
132
+ * @param {string} content - File content to parse
133
+ * @param {object} [options]
134
+ * @param {object} [options.fieldMap] - Map of source field names to canonical names (e.g., { mutations: 'side-effects' })
135
+ * @returns {object|null} Parsed metadata or null
136
+ */
137
+ export function parseAiMeta(content, options = {}) {
138
+ const { fieldMap = {} } = options;
139
+
140
+ // Try extraction strategies in order: JSDoc → triple-slash → line-comment
141
+ const blockLines = extractJsDocBlock(content)
142
+ || extractTripleSlashBlock(content)
143
+ || extractLineCommentBlock(content);
144
+
145
+ if (!blockLines) return null;
146
+
147
+ const result = {};
148
+ const fieldPattern = FIELDS.join('|');
149
+
150
+ for (const line of blockLines) {
151
+ // Clean JSDoc artifacts (* at start/end of line)
152
+ const cleaned = line.replace(/^\s*\*\s?/, '').replace(/\s*\*\/?$/, '').trim();
153
+
154
+ const parsed = parseFieldLine(cleaned, fieldPattern);
155
+ if (!parsed) continue;
156
+
157
+ let { field, value } = parsed;
158
+
159
+ // Clean trailing JSDoc artifacts and em-dash descriptions
160
+ value = value
161
+ .replace(/\s*\*\/?$/, '')
162
+ .replace(/\s*—\s*.*$/, '')
163
+ .trim();
164
+
165
+ // Apply field name normalization
166
+ if (fieldMap[field]) {
167
+ field = fieldMap[field];
168
+ }
169
+
170
+ if (SINGLE_TOKEN_FIELDS.has(field)) {
171
+ value = value.split(/\s+/)[0];
172
+ }
173
+
174
+ if (value) {
175
+ result[field] = value;
176
+ }
177
+ }
178
+
179
+ return Object.keys(result).length > 0 ? result : null;
180
+ }
181
+
182
+ /** Non-canonical @type values that should map to canonical types */
183
+ const TYPE_NORMALIZATION = {
184
+ card: 'component', modal: 'component', sheet: 'component', form: 'component',
185
+ skeleton: 'component', banner: 'component', dialog: 'component',
186
+ panel: 'component', section: 'component', barrel: 'index',
187
+ };
188
+
189
+ /**
190
+ * Normalize a @type value to a canonical type.
191
+ * @param {string} type - Raw type value
192
+ * @returns {string} Canonical type
193
+ */
194
+ export function normalizeType(type) {
195
+ return TYPE_NORMALIZATION[type] || type;
196
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:config-loader
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 200
10
+ * @purpose Loads and validates ai-codebase-registry project config from ai-registry.config.mjs
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-23T10:00 [feat] [Claude Opus 4.6] Add language, fieldMap, generators defaults for multi-language support
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Created config loader with defaults and validation
15
+ */
16
+
17
+ import { existsSync } from 'fs';
18
+ import { join, resolve } from 'path';
19
+ import { pathToFileURL } from 'url';
20
+
21
+ /** Default configuration — merged with user config */
22
+ const DEFAULTS = {
23
+ name: 'my-project',
24
+ description: '',
25
+ scanDirs: [{ dir: 'src', prefix: 'src' }],
26
+ extensions: ['.ts', '.tsx'],
27
+ skipDirs: ['node_modules', 'dist', '.next', 'coverage', '__tests__', '__mocks__'],
28
+ aliases: {},
29
+ language: 'typescript', // 'typescript' | 'swift' | 'kotlin' | 'python'
30
+ fieldMap: {}, // field name normalization (e.g., { mutations: 'side-effects' })
31
+ generators: null, // null = all, or ['manifest', 'semantic-ids', 'deps', 'side-effects', 'features']
32
+ router: 'none',
33
+ routerEntryFile: null,
34
+ routeBuilderFile: null,
35
+ dataStore: 'none',
36
+ registryDir: '_registry',
37
+ agent: 'claude-code',
38
+ claudemd: true,
39
+ rulesFile: null,
40
+ patternsFile: null,
41
+ };
42
+
43
+ /**
44
+ * Load project configuration from `ai-registry.config.mjs` in the given root directory.
45
+ * Falls back to defaults for any missing fields.
46
+ *
47
+ * @param {string} rootDir - Project root directory
48
+ * @returns {Promise<object>} Merged configuration
49
+ */
50
+ export async function loadConfig(rootDir) {
51
+ const configPath = join(rootDir, 'ai-registry.config.mjs');
52
+ let userConfig = {};
53
+
54
+ if (existsSync(configPath)) {
55
+ const configUrl = pathToFileURL(resolve(configPath)).href;
56
+ const mod = await import(configUrl);
57
+ userConfig = mod.default || mod;
58
+ }
59
+
60
+ const config = { ...DEFAULTS, ...userConfig };
61
+
62
+ // Resolve relative paths
63
+ config.scanDirs = config.scanDirs.map(entry => ({
64
+ dir: resolve(rootDir, entry.dir),
65
+ prefix: entry.prefix || entry.dir,
66
+ }));
67
+ config.registryDir = resolve(rootDir, config.registryDir);
68
+ config.rootDir = rootDir;
69
+
70
+ if (config.routerEntryFile) {
71
+ config.routerEntryFile = resolve(rootDir, config.routerEntryFile);
72
+ }
73
+ if (config.routeBuilderFile) {
74
+ config.routeBuilderFile = resolve(rootDir, config.routeBuilderFile);
75
+ }
76
+
77
+ // Validate required fields
78
+ if (!config.scanDirs.length) {
79
+ throw new Error('ai-registry.config.mjs: scanDirs must have at least one entry');
80
+ }
81
+
82
+ return config;
83
+ }
84
+
85
+ export { DEFAULTS };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:domain-detector
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 150
10
+ * @purpose Detects domain from file path using configurable pattern rules
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs, made configurable
14
+ */
15
+
16
+ /**
17
+ * Default domain detection rules.
18
+ * Each rule is a { pattern: RegExp, group: number } that extracts a domain from a path.
19
+ * Rules are tried in order — first match wins.
20
+ */
21
+ const DEFAULT_RULES = [
22
+ // Backend domain directories: functions/src/domains/{domain}/
23
+ { pattern: /^functions\/src\/domains\/([^/]+)\//, group: 1 },
24
+ // Frontend domain directories: apps/web/{components|hooks|pages|utils|types}/{domain}/
25
+ { pattern: /^apps\/web\/(?:components|hooks|pages|utils|types)\/([^/]+)\//, group: 1 },
26
+ // Context files: apps/web/contexts/{Name}Context.tsx
27
+ { pattern: /^apps\/web\/contexts\/([A-Z]\w+?)Context\.tsx$/, group: 1, transform: 'lowercase' },
28
+ // Service files by name: apps/web/services/.../{Name}Service.ts
29
+ { pattern: /^apps\/web\/services\/(?:[^/]+\/)?(\w+?)Service\.ts$/, group: 1, transform: 'lowercase' },
30
+ // Service directories: apps/web/services/{domain}/
31
+ { pattern: /^apps\/web\/services\/([^/]+)\//, group: 1, exclude: '_shared' },
32
+ // Feature directories: apps/web/features/{domain}/
33
+ { pattern: /^apps\/web\/features\/([^/]+)\//, group: 1 },
34
+ ];
35
+
36
+ /** Static path-prefix → domain mappings */
37
+ const DEFAULT_PREFIX_MAP = {
38
+ 'packages/shared-types/': 'shared-types',
39
+ 'packages/shared-constants/': 'shared-constants',
40
+ 'packages/shared-schemas/': 'shared-schemas',
41
+ 'apps/web/': 'core',
42
+ 'functions/src/': 'backend-core',
43
+ 'scripts/': 'infrastructure',
44
+ };
45
+
46
+ /**
47
+ * Create a domain detector with custom rules and prefix map.
48
+ *
49
+ * @param {object} [options]
50
+ * @param {Array} [options.rules] - Custom detection rules
51
+ * @param {object} [options.prefixMap] - Custom prefix → domain mapping
52
+ * @returns {(relPath: string) => string}
53
+ */
54
+ export function createDomainDetector(options = {}) {
55
+ const rules = options.rules || DEFAULT_RULES;
56
+ const prefixMap = options.prefixMap || DEFAULT_PREFIX_MAP;
57
+
58
+ return function detectDomain(relPath) {
59
+ // Try pattern rules first
60
+ for (const rule of rules) {
61
+ const match = relPath.match(rule.pattern);
62
+ if (match) {
63
+ const value = match[rule.group];
64
+ if (rule.exclude && value === rule.exclude) continue;
65
+ return rule.transform === 'lowercase' ? value.toLowerCase() : value;
66
+ }
67
+ }
68
+
69
+ // Try prefix map
70
+ for (const [prefix, domain] of Object.entries(prefixMap)) {
71
+ if (relPath.startsWith(prefix)) return domain;
72
+ }
73
+
74
+ return 'unknown';
75
+ };
76
+ }
77
+
78
+ /** Default domain detector (BellyFun-style paths) */
79
+ export const detectDomain = createDomainDetector();
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @ai-meta
3
+ * @id module:ai-agent-infra:file-discovery
4
+ * @domain ai-agent-infra
5
+ * @type module
6
+ * @side-effects none
7
+ * @stability high
8
+ * @complexity moderate
9
+ * @token-budget 120
10
+ * @purpose Recursively discovers source files in configured directories, respecting skip rules
11
+ *
12
+ * @filechangelog
13
+ * @changelog 2026-02-22T16:00 [feat] [Claude Opus 4.6] Add excludePatterns option for .d.ts filtering
14
+ * @changelog 2026-02-22T14:00 [feat] [Claude Opus 4.6] Extracted from generate-registry.mjs
15
+ */
16
+
17
+ import { readdirSync } from 'fs';
18
+ import { join, relative, extname } from 'path';
19
+
20
+ /**
21
+ * Recursively collect files under a directory, skipping excluded dirs.
22
+ *
23
+ * @param {string} dir - Absolute directory path to scan
24
+ * @param {string} prefix - Logical prefix for relative paths
25
+ * @param {{ extensions: Set<string>, skipDirs: Set<string>, excludePatterns?: string[] }} options
26
+ * @returns {{ fullPath: string, relPath: string }[]}
27
+ */
28
+ export function collectFiles(dir, prefix, options) {
29
+ const { extensions, skipDirs, excludePatterns = [] } = options;
30
+ const results = [];
31
+
32
+ function walk(currentDir) {
33
+ let entries;
34
+ try {
35
+ entries = readdirSync(currentDir, { withFileTypes: true });
36
+ } catch (err) {
37
+ console.warn(` Warning: Cannot read directory: ${currentDir} (${err.code || err.message})`);
38
+ return;
39
+ }
40
+
41
+ for (const entry of entries) {
42
+ if (entry.name.startsWith('.')) continue;
43
+ const fullPath = join(currentDir, entry.name);
44
+
45
+ if (entry.isDirectory()) {
46
+ if (!skipDirs.has(entry.name)) {
47
+ walk(fullPath);
48
+ }
49
+ } else if (entry.isFile() && extensions.has(extname(entry.name))) {
50
+ if (excludePatterns.some(p => entry.name.endsWith(p))) continue;
51
+ const relFromScanDir = relative(dir, fullPath);
52
+ const relFromRoot = join(prefix, relFromScanDir);
53
+ results.push({ fullPath, relPath: relFromRoot });
54
+ }
55
+ }
56
+ }
57
+
58
+ walk(dir);
59
+ return results;
60
+ }