@unrdf/project-engine 5.0.1 → 26.4.2
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 +16 -15
- package/src/golden-structure.mjs +2 -2
- package/src/materialize-apply.mjs +2 -2
- package/README.md +0 -53
- package/src/api-contract-validator.mjs +0 -711
- package/src/auto-test-generator.mjs +0 -444
- package/src/autonomic-mapek.mjs +0 -511
- package/src/capabilities-manifest.mjs +0 -125
- package/src/code-complexity-js.mjs +0 -368
- package/src/dependency-graph.mjs +0 -276
- package/src/doc-drift-checker.mjs +0 -172
- package/src/doc-generator.mjs +0 -229
- package/src/domain-infer.mjs +0 -966
- package/src/drift-snapshot.mjs +0 -775
- package/src/file-roles.mjs +0 -94
- package/src/fs-scan.mjs +0 -305
- package/src/gap-finder.mjs +0 -376
- package/src/hotspot-analyzer.mjs +0 -412
- package/src/index.mjs +0 -151
- package/src/initialize.mjs +0 -957
- package/src/lens/project-structure.mjs +0 -74
- package/src/mapek-orchestration.mjs +0 -665
- package/src/materialize-plan.mjs +0 -422
- package/src/materialize.mjs +0 -137
- package/src/policy-derivation.mjs +0 -869
- package/src/project-config.mjs +0 -142
- package/src/project-diff.mjs +0 -28
- package/src/project-engine/build-utils.mjs +0 -237
- package/src/project-engine/code-analyzer.mjs +0 -248
- package/src/project-engine/doc-generator.mjs +0 -407
- package/src/project-engine/infrastructure.mjs +0 -213
- package/src/project-engine/metrics.mjs +0 -146
- package/src/project-model.mjs +0 -111
- package/src/project-report.mjs +0 -348
- package/src/refactoring-guide.mjs +0 -242
- package/src/stack-detect.mjs +0 -102
- package/src/stack-linter.mjs +0 -213
- package/src/template-infer.mjs +0 -674
- package/src/type-auditor.mjs +0 -609
package/src/template-infer.mjs
DELETED
|
@@ -1,674 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Pattern induction - learns generator templates from existing project code
|
|
3
|
-
* @module project-engine/template-infer
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
7
|
-
import { createStore } from '@unrdf/oxigraph'; // TODO: Replace with Oxigraph Store
|
|
8
|
-
import { z } from 'zod';
|
|
9
|
-
|
|
10
|
-
const { namedNode, literal } = DataFactory;
|
|
11
|
-
|
|
12
|
-
/* ========================================================================= */
|
|
13
|
-
/* Zod Schemas */
|
|
14
|
-
/* ========================================================================= */
|
|
15
|
-
|
|
16
|
-
const InferOptionsSchema = z.object({
|
|
17
|
-
fsStore: z.any().refine(val => val && typeof val.getQuads === 'function', {
|
|
18
|
-
message: 'fsStore must be an RDF store with getQuads method',
|
|
19
|
-
}),
|
|
20
|
-
domainStore: z.any().nullable().optional(),
|
|
21
|
-
stackProfile: z
|
|
22
|
-
.object({
|
|
23
|
-
uiFramework: z.string().nullable().optional(),
|
|
24
|
-
webFramework: z.string().nullable().optional(),
|
|
25
|
-
testFramework: z.string().nullable().optional(),
|
|
26
|
-
language: z.string().nullable().optional(),
|
|
27
|
-
})
|
|
28
|
-
.optional(),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export const TemplateSchema = z.object({
|
|
32
|
-
id: z.string(),
|
|
33
|
-
kind: z.string(),
|
|
34
|
-
outputPattern: z.string(),
|
|
35
|
-
variables: z.array(z.string()),
|
|
36
|
-
invariants: z.array(z.string()),
|
|
37
|
-
variantCount: z.number(),
|
|
38
|
-
examples: z.array(z.string()),
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
export const InferSummarySchema = z.object({
|
|
42
|
-
templateCount: z.number(),
|
|
43
|
-
byKind: z.record(z.string(), z.number()),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
/* ========================================================================= */
|
|
47
|
-
/* Namespaces */
|
|
48
|
-
/* ========================================================================= */
|
|
49
|
-
|
|
50
|
-
const NS = {
|
|
51
|
-
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
52
|
-
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
53
|
-
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
54
|
-
gen: 'http://example.org/unrdf/generator#',
|
|
55
|
-
dom: 'http://example.org/unrdf/domain#',
|
|
56
|
-
unproj: 'http://example.org/unrdf/project#',
|
|
57
|
-
fs: 'http://example.org/unrdf/filesystem#',
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/* ========================================================================= */
|
|
61
|
-
/* File Family Patterns */
|
|
62
|
-
/* ========================================================================= */
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* File family detection patterns
|
|
66
|
-
* Each pattern defines how to identify and extract templates from file groups
|
|
67
|
-
*/
|
|
68
|
-
const FILE_FAMILY_PATTERNS = [
|
|
69
|
-
{
|
|
70
|
-
kind: 'Component',
|
|
71
|
-
patterns: [
|
|
72
|
-
/^src\/features\/([^/]+)\/([A-Z][a-zA-Z]+)(Component|View|Page)?\.(tsx?|jsx?)$/,
|
|
73
|
-
/^src\/components\/([^/]+)\/(index|[A-Z][a-zA-Z]+)\.(tsx?|jsx?)$/,
|
|
74
|
-
/^components\/([^/]+)\/(index|[A-Z][a-zA-Z]+)\.(tsx?|jsx?)$/,
|
|
75
|
-
],
|
|
76
|
-
outputTemplate: 'src/features/{{entity}}/{{Entity}}{{suffix}}.{{ext}}',
|
|
77
|
-
extractVars: (match, _path) => ({
|
|
78
|
-
entity: match[1],
|
|
79
|
-
Entity: capitalize(match[1]),
|
|
80
|
-
suffix: match[3] || '',
|
|
81
|
-
ext: match[4] || 'tsx',
|
|
82
|
-
}),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
kind: 'Page',
|
|
86
|
-
patterns: [
|
|
87
|
-
/^src\/app\/([^/]+)\/page\.(tsx?|jsx?)$/,
|
|
88
|
-
/^src\/pages\/([^/]+)\/(index|page)\.(tsx?|jsx?)$/,
|
|
89
|
-
/^pages\/([^/]+)\/(index|page)\.(tsx?|jsx?)$/,
|
|
90
|
-
/^app\/([^/]+)\/page\.(tsx?|jsx?)$/,
|
|
91
|
-
],
|
|
92
|
-
outputTemplate: 'src/app/{{route}}/page.{{ext}}',
|
|
93
|
-
extractVars: (match, _path) => ({
|
|
94
|
-
route: match[1],
|
|
95
|
-
ext: match[2] || 'tsx',
|
|
96
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
kind: 'Route',
|
|
100
|
-
patterns: [
|
|
101
|
-
/^src\/app\/([^/]+)\/route\.(tsx?|ts)$/,
|
|
102
|
-
/^src\/routes\/([^/]+)\.(tsx?|ts)$/,
|
|
103
|
-
/^app\/api\/([^/]+)\/route\.(tsx?|ts)$/,
|
|
104
|
-
],
|
|
105
|
-
outputTemplate: 'src/app/{{route}}/route.{{ext}}',
|
|
106
|
-
extractVars: (match, _path) => ({
|
|
107
|
-
route: match[1],
|
|
108
|
-
ext: match[2] || 'ts',
|
|
109
|
-
}),
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
kind: 'Test',
|
|
113
|
-
patterns: [
|
|
114
|
-
/^src\/([^/]+)\/([^/]+)\.(test|spec)\.(tsx?|jsx?)$/,
|
|
115
|
-
/^test\/([^/]+)\/([^/]+)\.(test|spec)\.(tsx?|jsx?)$/,
|
|
116
|
-
/^__tests__\/([^/]+)\.(test|spec)\.(tsx?|jsx?)$/,
|
|
117
|
-
/^([^/]+)\.(test|spec)\.(tsx?|jsx?)$/,
|
|
118
|
-
],
|
|
119
|
-
outputTemplate: 'test/{{module}}/{{name}}.{{testType}}.{{ext}}',
|
|
120
|
-
extractVars: (match, _path) => ({
|
|
121
|
-
module: match[1] || 'unit',
|
|
122
|
-
name: match[2] || match[1],
|
|
123
|
-
testType: match[3] || 'test',
|
|
124
|
-
ext: match[4] || 'ts',
|
|
125
|
-
}),
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
kind: 'Api',
|
|
129
|
-
patterns: [
|
|
130
|
-
/^src\/api\/([^/]+)\/(route|handler|controller)\.(tsx?|ts)$/,
|
|
131
|
-
/^api\/([^/]+)\.(tsx?|ts)$/,
|
|
132
|
-
/^src\/server\/([^/]+)\.(tsx?|ts)$/,
|
|
133
|
-
],
|
|
134
|
-
outputTemplate: 'src/api/{{endpoint}}/route.{{ext}}',
|
|
135
|
-
extractVars: (match, _path) => ({
|
|
136
|
-
endpoint: match[1],
|
|
137
|
-
ext: match[3] || match[2] || 'ts',
|
|
138
|
-
}),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
kind: 'Hook',
|
|
142
|
-
patterns: [
|
|
143
|
-
/^src\/hooks\/(use[A-Z][a-zA-Z]+)\.(tsx?|ts)$/,
|
|
144
|
-
/^hooks\/(use[A-Z][a-zA-Z]+)\.(tsx?|ts)$/,
|
|
145
|
-
/^src\/features\/([^/]+)\/hooks\/(use[A-Z][a-zA-Z]+)\.(tsx?|ts)$/,
|
|
146
|
-
],
|
|
147
|
-
outputTemplate: 'src/hooks/{{hookName}}.{{ext}}',
|
|
148
|
-
extractVars: (match, _path) => ({
|
|
149
|
-
hookName: match[1] || match[2],
|
|
150
|
-
feature: match[1] && match[2] ? match[1] : null,
|
|
151
|
-
ext: match[2] || match[3] || 'ts',
|
|
152
|
-
}),
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
kind: 'Service',
|
|
156
|
-
patterns: [
|
|
157
|
-
/^src\/services\/([^/]+)\.(service|client)\.(tsx?|ts)$/,
|
|
158
|
-
/^src\/lib\/([^/]+)\.(tsx?|ts)$/,
|
|
159
|
-
/^lib\/([^/]+)\.(tsx?|ts)$/,
|
|
160
|
-
],
|
|
161
|
-
outputTemplate: 'src/services/{{serviceName}}.service.{{ext}}',
|
|
162
|
-
extractVars: (match, _path) => ({
|
|
163
|
-
serviceName: match[1],
|
|
164
|
-
ext: match[3] || match[2] || 'ts',
|
|
165
|
-
}),
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
kind: 'Schema',
|
|
169
|
-
patterns: [
|
|
170
|
-
/^src\/schemas?\/([^/]+)\.(schema|types?)\.(tsx?|ts)$/,
|
|
171
|
-
/^src\/types\/([^/]+)\.(tsx?|ts)$/,
|
|
172
|
-
/^types\/([^/]+)\.(tsx?|ts)$/,
|
|
173
|
-
],
|
|
174
|
-
outputTemplate: 'src/schemas/{{schemaName}}.schema.{{ext}}',
|
|
175
|
-
extractVars: (match, _path) => ({
|
|
176
|
-
schemaName: match[1],
|
|
177
|
-
ext: match[3] || match[2] || 'ts',
|
|
178
|
-
}),
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
kind: 'Doc',
|
|
182
|
-
patterns: [
|
|
183
|
-
/^docs?\/([^/]+)\.(md|mdx)$/,
|
|
184
|
-
/^src\/docs?\/([^/]+)\.(md|mdx)$/,
|
|
185
|
-
/^([A-Z][A-Z_]+)\.(md|mdx)$/,
|
|
186
|
-
],
|
|
187
|
-
outputTemplate: 'docs/{{docName}}.{{ext}}',
|
|
188
|
-
extractVars: (match, _path) => ({
|
|
189
|
-
docName: match[1],
|
|
190
|
-
ext: match[2] || 'md',
|
|
191
|
-
}),
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
kind: 'Config',
|
|
195
|
-
patterns: [
|
|
196
|
-
/^\.?([a-z]+)rc\.(json|yaml|yml|js|cjs|mjs)$/,
|
|
197
|
-
/^([a-z]+)\.config\.(tsx?|ts|js|cjs|mjs)$/,
|
|
198
|
-
/^config\/([^/]+)\.(json|yaml|yml)$/,
|
|
199
|
-
],
|
|
200
|
-
outputTemplate: '{{configName}}.config.{{ext}}',
|
|
201
|
-
extractVars: (match, _path) => ({
|
|
202
|
-
configName: match[1],
|
|
203
|
-
ext: match[2] || 'js',
|
|
204
|
-
}),
|
|
205
|
-
},
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
/* ========================================================================= */
|
|
209
|
-
/* Helper Functions */
|
|
210
|
-
/* ========================================================================= */
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Capitalize first letter
|
|
214
|
-
* @param {string} str
|
|
215
|
-
* @returns {string}
|
|
216
|
-
*/
|
|
217
|
-
function capitalize(str) {
|
|
218
|
-
if (!str) return '';
|
|
219
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Convert camelCase to kebab-case
|
|
224
|
-
* @param {string} str
|
|
225
|
-
* @returns {string}
|
|
226
|
-
*/
|
|
227
|
-
function _toKebabCase(str) {
|
|
228
|
-
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Extract file paths from fs store
|
|
233
|
-
* @param {Store} fsStore
|
|
234
|
-
* @returns {string[]}
|
|
235
|
-
*/
|
|
236
|
-
function extractFilePathsFromStore(fsStore) {
|
|
237
|
-
const paths = [];
|
|
238
|
-
|
|
239
|
-
// Validate fsStore has getQuads method
|
|
240
|
-
if (!fsStore || typeof fsStore.getQuads !== 'function') {
|
|
241
|
-
return paths;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const quads = fsStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
|
|
246
|
-
|
|
247
|
-
for (const quad of quads) {
|
|
248
|
-
paths.push(quad.object.value);
|
|
249
|
-
}
|
|
250
|
-
} catch (e) {
|
|
251
|
-
// Handle stores that don't support getQuads properly
|
|
252
|
-
return paths;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return paths;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Group files by family pattern
|
|
260
|
-
* @param {string[]} paths
|
|
261
|
-
* @returns {Map<string, {pattern: Object, matches: Array<{path: string, match: RegExpMatchArray, vars: Object}>}>}
|
|
262
|
-
*/
|
|
263
|
-
function groupFilesByFamily(paths) {
|
|
264
|
-
const groups = new Map();
|
|
265
|
-
|
|
266
|
-
for (const pattern of FILE_FAMILY_PATTERNS) {
|
|
267
|
-
groups.set(pattern.kind, {
|
|
268
|
-
pattern,
|
|
269
|
-
matches: [],
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
for (const path of paths) {
|
|
274
|
-
for (const familyPattern of FILE_FAMILY_PATTERNS) {
|
|
275
|
-
for (const regex of familyPattern.patterns) {
|
|
276
|
-
const match = path.match(regex);
|
|
277
|
-
if (match) {
|
|
278
|
-
const vars = familyPattern.extractVars(match, path);
|
|
279
|
-
const group = groups.get(familyPattern.kind);
|
|
280
|
-
group.matches.push({ path, match, vars });
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return groups;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Extract invariants from a group of similar files
|
|
292
|
-
* @param {Array<{path: string, match: RegExpMatchArray, vars: Object}>} matches
|
|
293
|
-
* @returns {string[]}
|
|
294
|
-
*/
|
|
295
|
-
function extractInvariants(matches) {
|
|
296
|
-
if (matches.length < 2) return [];
|
|
297
|
-
|
|
298
|
-
const invariants = [];
|
|
299
|
-
|
|
300
|
-
// Find common path prefixes
|
|
301
|
-
const prefixes = matches.map(m => {
|
|
302
|
-
const parts = m.path.split('/');
|
|
303
|
-
return parts.slice(0, -1).join('/');
|
|
304
|
-
});
|
|
305
|
-
const commonPrefix = findCommonPrefix(prefixes);
|
|
306
|
-
if (commonPrefix) {
|
|
307
|
-
invariants.push(`path_prefix:${commonPrefix}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Find common extensions
|
|
311
|
-
const extensions = matches.map(m => {
|
|
312
|
-
const parts = m.path.split('.');
|
|
313
|
-
return parts[parts.length - 1];
|
|
314
|
-
});
|
|
315
|
-
const uniqueExts = [...new Set(extensions)];
|
|
316
|
-
if (uniqueExts.length === 1) {
|
|
317
|
-
invariants.push(`extension:${uniqueExts[0]}`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Find common naming patterns
|
|
321
|
-
const baseNames = matches.map(m => {
|
|
322
|
-
const parts = m.path.split('/');
|
|
323
|
-
const fileName = parts[parts.length - 1];
|
|
324
|
-
return fileName.split('.')[0];
|
|
325
|
-
});
|
|
326
|
-
const namingPattern = detectNamingPattern(baseNames);
|
|
327
|
-
if (namingPattern) {
|
|
328
|
-
invariants.push(`naming:${namingPattern}`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return invariants;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Find common prefix among strings
|
|
336
|
-
* @param {string[]} strings
|
|
337
|
-
* @returns {string}
|
|
338
|
-
*/
|
|
339
|
-
function findCommonPrefix(strings) {
|
|
340
|
-
if (strings.length === 0) return '';
|
|
341
|
-
if (strings.length === 1) return strings[0];
|
|
342
|
-
|
|
343
|
-
let prefix = strings[0];
|
|
344
|
-
for (let i = 1; i < strings.length; i++) {
|
|
345
|
-
while (!strings[i].startsWith(prefix) && prefix.length > 0) {
|
|
346
|
-
prefix = prefix.slice(0, -1);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return prefix;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Detect naming pattern from base names
|
|
354
|
-
* @param {string[]} names
|
|
355
|
-
* @returns {string|null}
|
|
356
|
-
*/
|
|
357
|
-
function detectNamingPattern(names) {
|
|
358
|
-
const patterns = {
|
|
359
|
-
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
360
|
-
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
361
|
-
'kebab-case': /^[a-z][a-z0-9-]*$/,
|
|
362
|
-
snake_case: /^[a-z][a-z0-9_]*$/,
|
|
363
|
-
index: /^index$/,
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
for (const [name, regex] of Object.entries(patterns)) {
|
|
367
|
-
if (names.every(n => regex.test(n))) {
|
|
368
|
-
return name;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Extract variable names from output pattern
|
|
377
|
-
* @param {string} pattern
|
|
378
|
-
* @returns {string[]}
|
|
379
|
-
*/
|
|
380
|
-
function extractVariablesFromPattern(pattern) {
|
|
381
|
-
const matches = pattern.match(/\{\{(\w+)\}\}/g) || [];
|
|
382
|
-
return matches.map(m => m.replace(/\{\{|\}\}/g, ''));
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Generate template ID
|
|
387
|
-
* @param {string} kind
|
|
388
|
-
* @param {number} index
|
|
389
|
-
* @returns {string}
|
|
390
|
-
*/
|
|
391
|
-
function generateTemplateId(kind, index) {
|
|
392
|
-
return `${kind}Template${index > 0 ? index + 1 : ''}`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/* ========================================================================= */
|
|
396
|
-
/* RDF Generation */
|
|
397
|
-
/* ========================================================================= */
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Add template triples to store
|
|
401
|
-
* @param {Store} store
|
|
402
|
-
* @param {Object} template
|
|
403
|
-
*/
|
|
404
|
-
function addTemplateToStore(store, template) {
|
|
405
|
-
const templateIri = namedNode(`${NS.gen}${template.id}`);
|
|
406
|
-
|
|
407
|
-
// rdf:type gen:Template
|
|
408
|
-
store.addQuad(templateIri, namedNode(`${NS.rdf}type`), namedNode(`${NS.gen}Template`));
|
|
409
|
-
|
|
410
|
-
// gen:templateKind
|
|
411
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}templateKind`), literal(template.kind));
|
|
412
|
-
|
|
413
|
-
// gen:outputPattern
|
|
414
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}outputPattern`), literal(template.outputPattern));
|
|
415
|
-
|
|
416
|
-
// gen:variantCount
|
|
417
|
-
store.addQuad(
|
|
418
|
-
templateIri,
|
|
419
|
-
namedNode(`${NS.gen}variantCount`),
|
|
420
|
-
literal(template.variantCount, namedNode(`${NS.xsd}integer`))
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
// gen:variable (multiple)
|
|
424
|
-
for (const variable of template.variables) {
|
|
425
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}variable`), literal(variable));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// gen:invariant (multiple)
|
|
429
|
-
for (const invariant of template.invariants) {
|
|
430
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}invariant`), literal(invariant));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// gen:example (multiple, max 3)
|
|
434
|
-
for (const example of template.examples.slice(0, 3)) {
|
|
435
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}example`), literal(example));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// gen:producesRole
|
|
439
|
-
store.addQuad(
|
|
440
|
-
templateIri,
|
|
441
|
-
namedNode(`${NS.gen}producesRole`),
|
|
442
|
-
namedNode(`${NS.unproj}${template.kind}`)
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/* ========================================================================= */
|
|
447
|
-
/* Public API */
|
|
448
|
-
/* ========================================================================= */
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Infer generator templates from existing project code
|
|
452
|
-
*
|
|
453
|
-
* Scans project files, groups them by family, extracts patterns,
|
|
454
|
-
* and returns RDF triples describing the inferred templates.
|
|
455
|
-
*
|
|
456
|
-
* @param {Object} fsStore - Filesystem store with project files
|
|
457
|
-
* @param {Object} [domainStore] - Optional domain ontology store
|
|
458
|
-
* @param {Object} [stackProfile] - Optional stack detection info
|
|
459
|
-
* @returns {{store: Store, summary: {templateCount: number, byKind: Record<string, number>}}}
|
|
460
|
-
*
|
|
461
|
-
* @example
|
|
462
|
-
* const { store, summary } = inferTemplatesFromProject(fsStore)
|
|
463
|
-
* console.log(`Found ${summary.templateCount} templates`)
|
|
464
|
-
* // store contains RDF triples like:
|
|
465
|
-
* // gen:UserViewTemplate rdf:type gen:Template
|
|
466
|
-
* // gen:UserViewTemplate gen:outputPattern "src/features/{{entity}}/{{Entity}}Page.tsx"
|
|
467
|
-
*/
|
|
468
|
-
export function inferTemplatesFromProject(fsStore, domainStore, stackProfile) {
|
|
469
|
-
const validated = InferOptionsSchema.parse({
|
|
470
|
-
fsStore,
|
|
471
|
-
domainStore,
|
|
472
|
-
stackProfile,
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
const store = createStore();
|
|
476
|
-
const summary = {
|
|
477
|
-
templateCount: 0,
|
|
478
|
-
byKind: {},
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
// Extract file paths from fsStore
|
|
482
|
-
const paths = extractFilePathsFromStore(validated.fsStore);
|
|
483
|
-
|
|
484
|
-
// Group files by family pattern
|
|
485
|
-
const groups = groupFilesByFamily(paths);
|
|
486
|
-
|
|
487
|
-
// Process each family group
|
|
488
|
-
for (const [kind, group] of groups) {
|
|
489
|
-
const { pattern, matches } = group;
|
|
490
|
-
|
|
491
|
-
// Skip families with too few matches (need >= 2 for pattern detection)
|
|
492
|
-
if (matches.length < 2) continue;
|
|
493
|
-
|
|
494
|
-
// Extract invariants from the matched files
|
|
495
|
-
const invariants = extractInvariants(matches);
|
|
496
|
-
|
|
497
|
-
// Extract variables from the output pattern
|
|
498
|
-
const variables = extractVariablesFromPattern(pattern.outputTemplate);
|
|
499
|
-
|
|
500
|
-
// Create template
|
|
501
|
-
const templateId = generateTemplateId(kind, 0);
|
|
502
|
-
const template = {
|
|
503
|
-
id: templateId,
|
|
504
|
-
kind,
|
|
505
|
-
outputPattern: pattern.outputTemplate,
|
|
506
|
-
variables,
|
|
507
|
-
invariants,
|
|
508
|
-
variantCount: matches.length,
|
|
509
|
-
examples: matches.map(m => m.path),
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// Add to store
|
|
513
|
-
addTemplateToStore(store, template);
|
|
514
|
-
|
|
515
|
-
// Update summary
|
|
516
|
-
summary.templateCount++;
|
|
517
|
-
summary.byKind[kind] = (summary.byKind[kind] || 0) + 1;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Validate summary
|
|
521
|
-
InferSummarySchema.parse(summary);
|
|
522
|
-
|
|
523
|
-
return { store, summary };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Infer templates with domain entity binding
|
|
528
|
-
*
|
|
529
|
-
* Enhanced version that also attempts to bind templates to domain entities
|
|
530
|
-
* from the domain ontology store.
|
|
531
|
-
*
|
|
532
|
-
* @param {Object} fsStore - Filesystem store with project files
|
|
533
|
-
* @param {Object} domainStore - Domain ontology store with entities
|
|
534
|
-
* @param {Object} [stackProfile] - Optional stack detection info
|
|
535
|
-
* @returns {{store: Store, summary: {templateCount: number, byKind: Record<string, number>, boundEntities: number}}}
|
|
536
|
-
*/
|
|
537
|
-
export function inferTemplatesWithDomainBinding(fsStore, domainStore, stackProfile) {
|
|
538
|
-
const { store, summary } = inferTemplatesFromProject(fsStore, domainStore, stackProfile);
|
|
539
|
-
|
|
540
|
-
let boundEntities = 0;
|
|
541
|
-
|
|
542
|
-
// Try to bind templates to domain entities
|
|
543
|
-
if (domainStore) {
|
|
544
|
-
try {
|
|
545
|
-
// Get domain entities (classes)
|
|
546
|
-
const entityQuads = domainStore.getQuads(
|
|
547
|
-
null,
|
|
548
|
-
namedNode(`${NS.rdf}type`),
|
|
549
|
-
namedNode(`${NS.rdfs}Class`)
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
const entities = entityQuads.map(q => q.subject.value);
|
|
553
|
-
|
|
554
|
-
// Get all templates from store
|
|
555
|
-
const templateQuads = store.getQuads(
|
|
556
|
-
null,
|
|
557
|
-
namedNode(`${NS.rdf}type`),
|
|
558
|
-
namedNode(`${NS.gen}Template`)
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
// For each template, try to find matching entities
|
|
562
|
-
for (const templateQuad of templateQuads) {
|
|
563
|
-
const templateIri = templateQuad.subject;
|
|
564
|
-
|
|
565
|
-
// Get examples for this template
|
|
566
|
-
const exampleQuads = store.getQuads(templateIri, namedNode(`${NS.gen}example`), null);
|
|
567
|
-
|
|
568
|
-
for (const exampleQuad of exampleQuads) {
|
|
569
|
-
const examplePath = exampleQuad.object.value;
|
|
570
|
-
|
|
571
|
-
// Try to match entity names in the path
|
|
572
|
-
for (const entityIri of entities) {
|
|
573
|
-
const entityName = entityIri.split(/[#/]/).pop();
|
|
574
|
-
if (entityName && examplePath.toLowerCase().includes(entityName.toLowerCase())) {
|
|
575
|
-
// Add binding
|
|
576
|
-
store.addQuad(templateIri, namedNode(`${NS.gen}targetsClass`), namedNode(entityIri));
|
|
577
|
-
boundEntities++;
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
} catch (e) {
|
|
584
|
-
// Ignore binding errors, templates still work
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
store,
|
|
590
|
-
summary: {
|
|
591
|
-
...summary,
|
|
592
|
-
boundEntities,
|
|
593
|
-
},
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Get templates by kind from store
|
|
599
|
-
*
|
|
600
|
-
* @param {Store} store - Template store
|
|
601
|
-
* @param {string} kind - Template kind (Component, Page, Test, etc.)
|
|
602
|
-
* @returns {Array<{iri: string, outputPattern: string, variantCount: number}>}
|
|
603
|
-
*/
|
|
604
|
-
export function getTemplatesByKind(store, kind) {
|
|
605
|
-
const templates = [];
|
|
606
|
-
|
|
607
|
-
const templateQuads = store.getQuads(null, namedNode(`${NS.gen}templateKind`), literal(kind));
|
|
608
|
-
|
|
609
|
-
for (const quad of templateQuads) {
|
|
610
|
-
const templateIri = quad.subject.value;
|
|
611
|
-
|
|
612
|
-
// Get output pattern
|
|
613
|
-
const patternQuads = store.getQuads(quad.subject, namedNode(`${NS.gen}outputPattern`), null);
|
|
614
|
-
const outputPattern = patternQuads[0]?.object.value || '';
|
|
615
|
-
|
|
616
|
-
// Get variant count
|
|
617
|
-
const countQuads = store.getQuads(quad.subject, namedNode(`${NS.gen}variantCount`), null);
|
|
618
|
-
const variantCount = parseInt(countQuads[0]?.object.value || '0', 10);
|
|
619
|
-
|
|
620
|
-
templates.push({
|
|
621
|
-
iri: templateIri,
|
|
622
|
-
outputPattern,
|
|
623
|
-
variantCount,
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return templates;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Serialize templates to a plain object for debugging/export
|
|
632
|
-
*
|
|
633
|
-
* @param {Store} store - Template store
|
|
634
|
-
* @returns {Object[]}
|
|
635
|
-
*/
|
|
636
|
-
export function serializeTemplates(store) {
|
|
637
|
-
const templates = [];
|
|
638
|
-
|
|
639
|
-
const templateQuads = store.getQuads(
|
|
640
|
-
null,
|
|
641
|
-
namedNode(`${NS.rdf}type`),
|
|
642
|
-
namedNode(`${NS.gen}Template`)
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
for (const quad of templateQuads) {
|
|
646
|
-
const iri = quad.subject;
|
|
647
|
-
|
|
648
|
-
const template = {
|
|
649
|
-
id: iri.value.split('#').pop(),
|
|
650
|
-
iri: iri.value,
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
// Get all properties
|
|
654
|
-
const propQuads = store.getQuads(iri, null, null);
|
|
655
|
-
|
|
656
|
-
for (const pq of propQuads) {
|
|
657
|
-
const propName = pq.predicate.value.split('#').pop();
|
|
658
|
-
const value = pq.object.value;
|
|
659
|
-
|
|
660
|
-
if (propName === 'type') continue;
|
|
661
|
-
|
|
662
|
-
if (['variable', 'invariant', 'example'].includes(propName)) {
|
|
663
|
-
if (!template[propName]) template[propName] = [];
|
|
664
|
-
template[propName].push(value);
|
|
665
|
-
} else {
|
|
666
|
-
template[propName] = value;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
templates.push(template);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
return templates;
|
|
674
|
-
}
|