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
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);
|
package/bin/validate.mjs
ADDED
|
@@ -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
|
+
}
|