mustard-claude 3.1.4 → 3.1.6
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/package.json +1 -1
- package/templates/commands/mustard/scan/SKILL.md +34 -4
- package/templates/scripts/registry/file-utils.js +126 -0
- package/templates/scripts/registry/pluralize.js +167 -0
- package/templates/scripts/registry/scanner-contract.js +188 -0
- package/templates/scripts/registry/scanner-loader.js +120 -0
- package/templates/scripts/registry/scanners/.gitkeep +0 -0
- package/templates/scripts/registry/scanners/dart-scanner.js +694 -0
- package/templates/scripts/registry/scanners/dotnet-scanner.js +1093 -0
- package/templates/scripts/registry/scanners/go-scanner.js +837 -0
- package/templates/scripts/registry/scanners/java-scanner.js +977 -0
- package/templates/scripts/registry/scanners/php-scanner.js +757 -0
- package/templates/scripts/registry/scanners/python-scanner.js +774 -0
- package/templates/scripts/registry/scanners/rust-scanner.js +872 -0
- package/templates/scripts/registry/scanners/typescript-scanner.js +1259 -0
- package/templates/scripts/registry/schema-builder.js +145 -0
- package/templates/scripts/skill-generator.js +2090 -0
- package/templates/scripts/sync-registry.js +100 -378
package/package.json
CHANGED
|
@@ -140,9 +140,13 @@ Never search in:
|
|
|
140
140
|
- `node_modules/`, `.next/`, `bin/`, `obj/`, `dist/`, `migrations/`
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
**`.claude/entity-registry.json`** —
|
|
143
|
+
**`.claude/entity-registry.json`** — generate via registry scanner:
|
|
144
|
+
```bash
|
|
145
|
+
node .claude/scripts/sync-registry.js --force
|
|
146
|
+
```
|
|
147
|
+
If `sync-registry.js` fails or is not available, create empty skeleton:
|
|
144
148
|
```json
|
|
145
|
-
{ "_patterns": {}, "_enums": {}, "e": {} }
|
|
149
|
+
{ "_meta": { "version": "4.0" }, "_patterns": {}, "_enums": {}, "e": {} }
|
|
146
150
|
```
|
|
147
151
|
|
|
148
152
|
**`{subproject}/CLAUDE.md`** — per subproject (skip if exists):
|
|
@@ -310,6 +314,31 @@ See `scan-format.md` §10 for decomposition rules, SKILL.md format, and descript
|
|
|
310
314
|
Skills are generated ONLY in `{subproject}/.claude/skills/{skill-name}/` (NOT in root `.claude/skills/`).
|
|
311
315
|
Mark all with `<!-- mustard:generated -->`. Overwrite on next scan.
|
|
312
316
|
|
|
317
|
+
### 4.7. Generate Pattern Skills from Registry (OODA: Observe → Act)
|
|
318
|
+
|
|
319
|
+
After agent-generated skills (4.6), run the registry-based skill generator to create structural pattern skills from `_patterns`:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
node .claude/scripts/sync-registry.js --force
|
|
323
|
+
node .claude/scripts/skill-generator.js --force
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
This generates skills that the agents in Step 3 may have missed — particularly:
|
|
327
|
+
- `{role}-entity-creation` — entity folder, base class, interfaces, namespace
|
|
328
|
+
- `{role}-enum-placement` — enum folder, decorators, NEVER inline in entities
|
|
329
|
+
- `{role}-route-conventions` — route naming, auth pattern, CRUD standard
|
|
330
|
+
- `{role}-service-pattern` — interface-first, base interface, DI
|
|
331
|
+
- `{role}-repository-pattern` — base class, interface, DI
|
|
332
|
+
- `{role}-dto-conventions` — folder, naming, validation pattern
|
|
333
|
+
- `{role}-module-registration` — DI registration, route wiring
|
|
334
|
+
|
|
335
|
+
These skills are derived from **detected patterns** (not hardcoded). They complement agent-generated skills by covering structural conventions that agents may not explicitly document.
|
|
336
|
+
|
|
337
|
+
**Skip conditions:**
|
|
338
|
+
- `entity-registry.json` version < 4.0 → skip (registry not populated)
|
|
339
|
+
- `skill-generator.js` not present → skip
|
|
340
|
+
- Pattern skill already exists and was NOT generated by mustard → skip (user-edited)
|
|
341
|
+
|
|
313
342
|
### 4. Update CLAUDE.md files
|
|
314
343
|
|
|
315
344
|
After agents complete:
|
|
@@ -358,8 +387,9 @@ Include findings in scan output under a `## Security` section:
|
|
|
358
387
|
5. Old files backed up in `_backup/`
|
|
359
388
|
6. Each subproject's CLAUDE.md has `## Scan References`
|
|
360
389
|
7. Root CLAUDE.md has `## Project Structure` with all subprojects
|
|
361
|
-
8. `.claude/entity-registry.json` exists
|
|
362
|
-
9.
|
|
390
|
+
8. `.claude/entity-registry.json` exists and is v4.0
|
|
391
|
+
9. Pattern skills generated from registry (entity-creation, enum-placement, route-conventions, etc.)
|
|
392
|
+
10. Each generated skill has valid YAML frontmatter (name + description)
|
|
363
393
|
10. Each skill's description is "pushy" — includes casual trigger phrases
|
|
364
394
|
11. If security scan ran: findings summarized in `## Security` section of output
|
|
365
395
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* file-utils.js
|
|
5
|
+
*
|
|
6
|
+
* Single Responsibility: file collection and path helpers shared across scanners.
|
|
7
|
+
* No scanning logic, no schema building — only filesystem utilities.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { collectFiles, relativePath, readFileSafe } = require('./registry/file-utils');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Constants
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const DEFAULT_IGNORE = [
|
|
21
|
+
'node_modules', 'bin', 'obj', 'dist', '.next',
|
|
22
|
+
'__pycache__', '.venv', 'venv', 'target', 'build',
|
|
23
|
+
'.git', 'migrations', 'Migrations',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// collectFiles
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Recursively collect all files with the given extension under a directory.
|
|
32
|
+
* Skips DEFAULT_IGNORE directories, dot-directories, and any extra ignore dirs.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} dir - root directory to walk
|
|
35
|
+
* @param {string} extension - file extension including dot, e.g. '.cs', '.ts'
|
|
36
|
+
* @param {string[]} [ignore] - additional directory names to skip
|
|
37
|
+
* @returns {string[]} - absolute file paths
|
|
38
|
+
*/
|
|
39
|
+
function collectFiles(dir, extension, ignore = []) {
|
|
40
|
+
const allIgnore = new Set([...DEFAULT_IGNORE, ...ignore]);
|
|
41
|
+
const results = [];
|
|
42
|
+
|
|
43
|
+
function walk(currentDir) {
|
|
44
|
+
try {
|
|
45
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (allIgnore.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
49
|
+
walk(path.join(currentDir, entry.name));
|
|
50
|
+
} else if (entry.name.endsWith(extension)) {
|
|
51
|
+
results.push(path.join(currentDir, entry.name));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch { /* ignore permission errors */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
walk(dir);
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// relativePath
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a relative path from a base directory, normalised with forward slashes.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} base - absolute base directory
|
|
69
|
+
* @param {string} filePath - absolute file path
|
|
70
|
+
* @returns {string} - relative path with forward slashes
|
|
71
|
+
*/
|
|
72
|
+
function relativePath(base, filePath) {
|
|
73
|
+
return path.relative(base, filePath).replace(/\\/g, '/');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// readFileSafe
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read a file as UTF-8 string. Returns null on any error.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} filePath - absolute path to file
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
function readFileSafe(filePath) {
|
|
87
|
+
try {
|
|
88
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// inferCommonFolder
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Detect the most common parent folder from a list of relative file paths.
|
|
100
|
+
* Useful for pattern inference (e.g., "most entities live in Domain/Entities/").
|
|
101
|
+
*
|
|
102
|
+
* @param {string[]} filePaths - relative paths (forward slashes)
|
|
103
|
+
* @returns {string|null} - most common parent folder with trailing slash, or null
|
|
104
|
+
*/
|
|
105
|
+
function inferCommonFolder(filePaths) {
|
|
106
|
+
if (!filePaths.length) return null;
|
|
107
|
+
|
|
108
|
+
const counts = new Map();
|
|
109
|
+
for (const fp of filePaths) {
|
|
110
|
+
const dir = path.dirname(fp).replace(/\\/g, '/');
|
|
111
|
+
counts.set(dir, (counts.get(dir) || 0) + 1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let maxDir = null;
|
|
115
|
+
let maxCount = 0;
|
|
116
|
+
for (const [dir, count] of counts) {
|
|
117
|
+
if (count > maxCount) {
|
|
118
|
+
maxDir = dir;
|
|
119
|
+
maxCount = count;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return maxDir ? maxDir + '/' : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { collectFiles, relativePath, readFileSafe, inferCommonFolder, DEFAULT_IGNORE };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pluralize.js
|
|
5
|
+
*
|
|
6
|
+
* Single Responsibility: English pluralization helpers for converting
|
|
7
|
+
* snake_case plural database table names to PascalCase singular entity names.
|
|
8
|
+
*
|
|
9
|
+
* Previously inlined in sync-registry.js. Extracted here so scanners can
|
|
10
|
+
* reuse without depending on the top-level CLI script.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { snakeToPascalSingular, singularize, snakeToPascal } = require('./registry/pluralize');
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// IRREGULAR_PLURALS
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lookup for common irregular plurals.
|
|
22
|
+
* Key: lowercase plural form (snake_case table name or single word)
|
|
23
|
+
* Value: PascalCase singular entity name
|
|
24
|
+
*/
|
|
25
|
+
const IRREGULAR_PLURALS = {
|
|
26
|
+
people: 'Person',
|
|
27
|
+
children: 'Child',
|
|
28
|
+
men: 'Man',
|
|
29
|
+
women: 'Woman',
|
|
30
|
+
mice: 'Mouse',
|
|
31
|
+
geese: 'Goose',
|
|
32
|
+
teeth: 'Tooth',
|
|
33
|
+
feet: 'Foot',
|
|
34
|
+
data: 'Datum',
|
|
35
|
+
indices: 'Index',
|
|
36
|
+
matrices: 'Matrix',
|
|
37
|
+
vertices: 'Vertex',
|
|
38
|
+
analyses: 'Analysis',
|
|
39
|
+
bases: 'Base',
|
|
40
|
+
crises: 'Crisis',
|
|
41
|
+
diagnoses: 'Diagnosis',
|
|
42
|
+
hypotheses: 'Hypothesis',
|
|
43
|
+
parentheses: 'Parenthesis',
|
|
44
|
+
theses: 'Thesis',
|
|
45
|
+
criteria: 'Criterion',
|
|
46
|
+
phenomena: 'Phenomenon',
|
|
47
|
+
media: 'Medium',
|
|
48
|
+
statuses: 'Status',
|
|
49
|
+
addresses: 'Address',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// singularize
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Singularize a single lowercase English word using simple heuristics.
|
|
58
|
+
*
|
|
59
|
+
* Examples:
|
|
60
|
+
* companies -> company
|
|
61
|
+
* addresses -> address
|
|
62
|
+
* boxes -> box
|
|
63
|
+
* contracts -> contract
|
|
64
|
+
* queue -> queue (already singular)
|
|
65
|
+
*
|
|
66
|
+
* @param {string} word - lowercase word
|
|
67
|
+
* @returns {string} - singular form (lowercase)
|
|
68
|
+
*/
|
|
69
|
+
function singularize(word) {
|
|
70
|
+
// Check irregular
|
|
71
|
+
if (IRREGULAR_PLURALS[word]) {
|
|
72
|
+
return IRREGULAR_PLURALS[word].toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Already-singular indicators
|
|
76
|
+
if (
|
|
77
|
+
word.endsWith('ss') ||
|
|
78
|
+
word.endsWith('us') ||
|
|
79
|
+
word.endsWith('is') ||
|
|
80
|
+
word === 'queue'
|
|
81
|
+
) {
|
|
82
|
+
return word;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -ies -> -y (companies -> company, categories -> category)
|
|
86
|
+
if (word.endsWith('ies')) {
|
|
87
|
+
return word.slice(0, -3) + 'y';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// -sses -> -ss (addresses -> address)
|
|
91
|
+
if (word.endsWith('sses')) {
|
|
92
|
+
return word.slice(0, -2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// -es after sh, ch, x, z -> remove -es (boxes -> box, churches -> church)
|
|
96
|
+
if (
|
|
97
|
+
word.endsWith('shes') ||
|
|
98
|
+
word.endsWith('ches') ||
|
|
99
|
+
word.endsWith('xes') ||
|
|
100
|
+
word.endsWith('zes')
|
|
101
|
+
) {
|
|
102
|
+
return word.slice(0, -2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generic -s removal (contracts -> contract)
|
|
106
|
+
if (word.endsWith('s') && !word.endsWith('ss')) {
|
|
107
|
+
return word.slice(0, -1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return word;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// snakeToPascalSingular
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert a snake_case plural table name to PascalCase singular entity name.
|
|
119
|
+
*
|
|
120
|
+
* Examples:
|
|
121
|
+
* contracts -> Contract
|
|
122
|
+
* partner_types -> PartnerType
|
|
123
|
+
* people -> Person
|
|
124
|
+
* companies -> Company
|
|
125
|
+
* product_categories -> ProductCategory
|
|
126
|
+
* email_queue -> EmailQueue (already singular)
|
|
127
|
+
*
|
|
128
|
+
* @param {string} snakePlural - snake_case plural name (e.g., 'partner_types')
|
|
129
|
+
* @returns {string} - PascalCase singular entity name
|
|
130
|
+
*/
|
|
131
|
+
function snakeToPascalSingular(snakePlural) {
|
|
132
|
+
// Check irregular lookup for the full compound name
|
|
133
|
+
if (IRREGULAR_PLURALS[snakePlural]) {
|
|
134
|
+
return IRREGULAR_PLURALS[snakePlural];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Split by underscore; singularize only the LAST part (the noun)
|
|
138
|
+
const parts = snakePlural.split('_');
|
|
139
|
+
const result = parts.map((part, idx) => {
|
|
140
|
+
const word = idx === parts.length - 1 ? singularize(part) : part;
|
|
141
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return result.join('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// snakeToPascal
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convert a snake_case name to PascalCase (no singularization).
|
|
153
|
+
*
|
|
154
|
+
* Example:
|
|
155
|
+
* contract_status -> ContractStatus
|
|
156
|
+
*
|
|
157
|
+
* @param {string} snakeName
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
function snakeToPascal(snakeName) {
|
|
161
|
+
return snakeName
|
|
162
|
+
.split('_')
|
|
163
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
164
|
+
.join('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { snakeToPascalSingular, singularize, snakeToPascal, IRREGULAR_PLURALS };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scanner-contract.js
|
|
5
|
+
*
|
|
6
|
+
* Base contract for all stack scanners (Interface Segregation + Liskov Substitution).
|
|
7
|
+
* Every scanner extends ScannerContract and implements detect() and optionally
|
|
8
|
+
* the scan* methods. Each method has a single responsibility.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { ScannerContract } = require('./scanner-contract');
|
|
12
|
+
* class MyScanner extends ScannerContract { ... }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// JSDoc typedefs
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} EntityInfo
|
|
21
|
+
* @property {string} file - relative path from subproject root
|
|
22
|
+
* @property {string} [namespace]
|
|
23
|
+
* @property {string} [baseClass]
|
|
24
|
+
* @property {string[]} [interfaces]
|
|
25
|
+
* @property {string[]} [decorators] - class-level decorators/attributes
|
|
26
|
+
* @property {string[]} [refs] - referenced entities (FK/navigation)
|
|
27
|
+
* @property {string[]} [sub] - child/collection entities
|
|
28
|
+
* @property {string[]} [enums] - enum types used
|
|
29
|
+
* @property {string[]} [properties] - key property names with types
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} EnumInfo
|
|
34
|
+
* @property {string[]} values
|
|
35
|
+
* @property {string} file - relative path
|
|
36
|
+
* @property {string} [namespace]
|
|
37
|
+
* @property {string[]} [decorators] - enum-level decorators
|
|
38
|
+
* @property {string[]} [valueDecorators] - decorators found on values (e.g., Description, Display)
|
|
39
|
+
* @property {string} [valueConvention] - UPPER_CASE | PascalCase | camelCase
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} InterfaceInfo
|
|
44
|
+
* @property {string} file
|
|
45
|
+
* @property {string} [namespace]
|
|
46
|
+
* @property {string[]} [methods]
|
|
47
|
+
* @property {string[]} [extends] - parent interfaces
|
|
48
|
+
* @property {string[]} [implementedBy] - known implementing classes
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} RouteInfo
|
|
53
|
+
* @property {string} file
|
|
54
|
+
* @property {string} prefix - route group prefix (e.g., /contracts)
|
|
55
|
+
* @property {Object[]} endpoints - { method, path, name, auth }
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} DtoInfo
|
|
60
|
+
* @property {string} file
|
|
61
|
+
* @property {string} [namespace]
|
|
62
|
+
* @property {string} [entity] - linked entity name
|
|
63
|
+
* @property {string} [validationPattern] - FluentValidation, Zod, class-validator, etc.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} ServiceInfo
|
|
68
|
+
* @property {string} file
|
|
69
|
+
* @property {string} [interface] - interface it implements
|
|
70
|
+
* @property {string} [entity] - linked entity name
|
|
71
|
+
* @property {string[]} [dependencies] - injected interfaces
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {Object} RepoInfo
|
|
76
|
+
* @property {string} file
|
|
77
|
+
* @property {string} [interface]
|
|
78
|
+
* @property {string} [entity]
|
|
79
|
+
* @property {string} [baseClass]
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// ScannerContract
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Base contract for stack scanners (Interface Segregation + Liskov Substitution).
|
|
88
|
+
* Every scanner implements detect() and scan(). Each method has a single responsibility.
|
|
89
|
+
*/
|
|
90
|
+
class ScannerContract {
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} subprojectPath - absolute path to subproject root
|
|
93
|
+
* @param {Object} subprojectMeta - metadata from sync-detect.js output
|
|
94
|
+
*/
|
|
95
|
+
constructor(subprojectPath, subprojectMeta) {
|
|
96
|
+
if (new.target === ScannerContract) {
|
|
97
|
+
throw new Error('ScannerContract is abstract — extend it');
|
|
98
|
+
}
|
|
99
|
+
this.subprojectPath = subprojectPath;
|
|
100
|
+
this.meta = subprojectMeta;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detect if this scanner applies to the subproject.
|
|
105
|
+
* @returns {boolean}
|
|
106
|
+
*/
|
|
107
|
+
detect() { throw new Error('detect() not implemented'); }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect the architecture pattern used in the project.
|
|
111
|
+
* @returns {string} - e.g., 'solid', 'layered', 'minimal', 'mvvm', 'mvc'
|
|
112
|
+
*/
|
|
113
|
+
detectArchitecture() { return 'unknown'; }
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Scan entities (models, domain objects).
|
|
117
|
+
* @returns {Map<string, EntityInfo>}
|
|
118
|
+
*/
|
|
119
|
+
scanEntities() { return new Map(); }
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Scan enums/value types.
|
|
123
|
+
* @returns {Map<string, EnumInfo>}
|
|
124
|
+
*/
|
|
125
|
+
scanEnums() { return new Map(); }
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Scan interfaces/contracts.
|
|
129
|
+
* @returns {Map<string, InterfaceInfo>}
|
|
130
|
+
*/
|
|
131
|
+
scanInterfaces() { return new Map(); }
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Scan routes/endpoints.
|
|
135
|
+
* @returns {Map<string, RouteInfo>}
|
|
136
|
+
*/
|
|
137
|
+
scanRoutes() { return new Map(); }
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Scan DTOs/schemas/view models.
|
|
141
|
+
* @returns {Map<string, DtoInfo>}
|
|
142
|
+
*/
|
|
143
|
+
scanDtos() { return new Map(); }
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Scan services.
|
|
147
|
+
* @returns {Map<string, ServiceInfo>}
|
|
148
|
+
*/
|
|
149
|
+
scanServices() { return new Map(); }
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Scan repositories.
|
|
153
|
+
* @returns {Map<string, RepoInfo>}
|
|
154
|
+
*/
|
|
155
|
+
scanRepositories() { return new Map(); }
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Infer structural patterns from all scanned data.
|
|
159
|
+
* Called AFTER all scan methods. Receives the results.
|
|
160
|
+
* @param {{ entities: Map, enums: Map, interfaces: Map, routes: Map, dtos: Map, services: Map, repositories: Map }} scanResults
|
|
161
|
+
* @returns {Object} - patterns object for _patterns.{stack}
|
|
162
|
+
*/
|
|
163
|
+
inferPatterns(scanResults) { return {}; } // eslint-disable-line no-unused-vars
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Run the full scan pipeline.
|
|
167
|
+
* Calls all scan* methods in order, then inferPatterns, then returns the combined result.
|
|
168
|
+
* @returns {{ entities: Map, enums: Map, interfaces: Map, routes: Map, dtos: Map, services: Map, repositories: Map, patterns: Object }}
|
|
169
|
+
*/
|
|
170
|
+
scan() {
|
|
171
|
+
const entities = this.scanEntities();
|
|
172
|
+
const enums = this.scanEnums();
|
|
173
|
+
const interfaces = this.scanInterfaces();
|
|
174
|
+
const routes = this.scanRoutes();
|
|
175
|
+
const dtos = this.scanDtos();
|
|
176
|
+
const services = this.scanServices();
|
|
177
|
+
const repositories = this.scanRepositories();
|
|
178
|
+
const architecture = this.detectArchitecture();
|
|
179
|
+
|
|
180
|
+
const scanResults = { entities, enums, interfaces, routes, dtos, services, repositories };
|
|
181
|
+
const patterns = this.inferPatterns(scanResults);
|
|
182
|
+
patterns.architecture = architecture;
|
|
183
|
+
|
|
184
|
+
return { ...scanResults, patterns };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { ScannerContract };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scanner-loader.js
|
|
5
|
+
*
|
|
6
|
+
* Dependency Inversion: sync-registry.js depends on this loader, not on concrete scanners.
|
|
7
|
+
* Open/Closed: adding a new stack scanner = dropping a new file in scanners/, zero other changes.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const { loadScanner, detectStack } = require('./registry/scanner-loader');
|
|
11
|
+
* const scanner = loadScanner(subprojectPath, subprojectMeta);
|
|
12
|
+
* if (scanner) { const result = scanner.scan(); }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const SCANNERS_DIR = path.join(__dirname, 'scanners');
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Stack detection signals
|
|
22
|
+
// Each key is the stack ID that maps to a scanner file: {stackId}-scanner.js
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const STACK_SIGNALS = {
|
|
26
|
+
dotnet: { files: ['*.csproj', '*.sln'], dirs: [] },
|
|
27
|
+
typescript: { files: ['package.json', 'tsconfig.json'], dirs: [] },
|
|
28
|
+
dart: { files: ['pubspec.yaml'], dirs: ['lib'] },
|
|
29
|
+
php: { files: ['composer.json', 'artisan'], dirs: [] },
|
|
30
|
+
python: { files: ['pyproject.toml', 'setup.py', 'requirements.txt', 'manage.py'], dirs: [] },
|
|
31
|
+
java: { files: ['pom.xml', 'build.gradle', 'build.gradle.kts'], dirs: [] },
|
|
32
|
+
go: { files: ['go.mod'], dirs: [] },
|
|
33
|
+
rust: { files: ['Cargo.toml'], dirs: [] },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// detectStack
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect which stack a subproject uses via file-presence heuristics.
|
|
42
|
+
* Iterates STACK_SIGNALS in definition order (most specific first).
|
|
43
|
+
*
|
|
44
|
+
* @param {string} subprojectPath - absolute path to subproject root
|
|
45
|
+
* @returns {string|null} - stack ID (e.g., 'dotnet') or null if unrecognised
|
|
46
|
+
*/
|
|
47
|
+
function detectStack(subprojectPath) {
|
|
48
|
+
for (const [stackId, signals] of Object.entries(STACK_SIGNALS)) {
|
|
49
|
+
for (const pattern of signals.files) {
|
|
50
|
+
// Handle glob-like patterns (*.ext) — match any file with that extension
|
|
51
|
+
if (pattern.startsWith('*')) {
|
|
52
|
+
const ext = pattern.slice(1); // e.g., '.csproj'
|
|
53
|
+
try {
|
|
54
|
+
const entries = fs.readdirSync(subprojectPath);
|
|
55
|
+
if (entries.some(e => e.endsWith(ext))) return stackId;
|
|
56
|
+
} catch { /* ignore unreadable dirs */ }
|
|
57
|
+
} else {
|
|
58
|
+
if (fs.existsSync(path.join(subprojectPath, pattern))) return stackId;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// loadScanner
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load the appropriate scanner class for a subproject and return an instance,
|
|
71
|
+
* or null if no scanner is available or detect() returns false.
|
|
72
|
+
*
|
|
73
|
+
* Resolution order:
|
|
74
|
+
* 1. Use subprojectMeta.stack if provided
|
|
75
|
+
* 2. Fall back to detectStack()
|
|
76
|
+
* 3. Look for scanners/{stackId}-scanner.js
|
|
77
|
+
* 4. Instantiate and call detect() — return null if detect() is false
|
|
78
|
+
*
|
|
79
|
+
* @param {string} subprojectPath - absolute path to subproject root
|
|
80
|
+
* @param {Object} subprojectMeta - metadata from sync-detect.js output
|
|
81
|
+
* @returns {import('./scanner-contract').ScannerContract|null}
|
|
82
|
+
*/
|
|
83
|
+
function loadScanner(subprojectPath, subprojectMeta) {
|
|
84
|
+
const stackId = subprojectMeta.stack || detectStack(subprojectPath);
|
|
85
|
+
if (!stackId) return null;
|
|
86
|
+
|
|
87
|
+
const scannerFile = path.join(SCANNERS_DIR, `${stackId}-scanner.js`);
|
|
88
|
+
if (!fs.existsSync(scannerFile)) return null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const ScannerClass = require(scannerFile);
|
|
92
|
+
const scanner = new ScannerClass(subprojectPath, subprojectMeta);
|
|
93
|
+
if (scanner.detect()) return scanner;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
process.stderr.write(`[scanner-loader] Failed to load ${stackId} scanner: ${err.message}\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// listAvailableScanners
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* List all scanner files currently present in the scanners/ directory.
|
|
107
|
+
* Useful for diagnostics and --list-scanners flag.
|
|
108
|
+
* @returns {string[]} - array of stack IDs with scanners installed
|
|
109
|
+
*/
|
|
110
|
+
function listAvailableScanners() {
|
|
111
|
+
try {
|
|
112
|
+
return fs.readdirSync(SCANNERS_DIR)
|
|
113
|
+
.filter(f => f.endsWith('-scanner.js'))
|
|
114
|
+
.map(f => f.replace('-scanner.js', ''));
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { detectStack, loadScanner, listAvailableScanners, STACK_SIGNALS };
|
|
File without changes
|