@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.
Files changed (39) hide show
  1. package/package.json +16 -15
  2. package/src/golden-structure.mjs +2 -2
  3. package/src/materialize-apply.mjs +2 -2
  4. package/README.md +0 -53
  5. package/src/api-contract-validator.mjs +0 -711
  6. package/src/auto-test-generator.mjs +0 -444
  7. package/src/autonomic-mapek.mjs +0 -511
  8. package/src/capabilities-manifest.mjs +0 -125
  9. package/src/code-complexity-js.mjs +0 -368
  10. package/src/dependency-graph.mjs +0 -276
  11. package/src/doc-drift-checker.mjs +0 -172
  12. package/src/doc-generator.mjs +0 -229
  13. package/src/domain-infer.mjs +0 -966
  14. package/src/drift-snapshot.mjs +0 -775
  15. package/src/file-roles.mjs +0 -94
  16. package/src/fs-scan.mjs +0 -305
  17. package/src/gap-finder.mjs +0 -376
  18. package/src/hotspot-analyzer.mjs +0 -412
  19. package/src/index.mjs +0 -151
  20. package/src/initialize.mjs +0 -957
  21. package/src/lens/project-structure.mjs +0 -74
  22. package/src/mapek-orchestration.mjs +0 -665
  23. package/src/materialize-plan.mjs +0 -422
  24. package/src/materialize.mjs +0 -137
  25. package/src/policy-derivation.mjs +0 -869
  26. package/src/project-config.mjs +0 -142
  27. package/src/project-diff.mjs +0 -28
  28. package/src/project-engine/build-utils.mjs +0 -237
  29. package/src/project-engine/code-analyzer.mjs +0 -248
  30. package/src/project-engine/doc-generator.mjs +0 -407
  31. package/src/project-engine/infrastructure.mjs +0 -213
  32. package/src/project-engine/metrics.mjs +0 -146
  33. package/src/project-model.mjs +0 -111
  34. package/src/project-report.mjs +0 -348
  35. package/src/refactoring-guide.mjs +0 -242
  36. package/src/stack-detect.mjs +0 -102
  37. package/src/stack-linter.mjs +0 -213
  38. package/src/template-infer.mjs +0 -674
  39. package/src/type-auditor.mjs +0 -609
@@ -1,966 +0,0 @@
1
- /**
2
- * @file Domain model inference engine - extract entities, fields, relations from code
3
- * @module project-engine/domain-infer
4
- */
5
-
6
- import { promises as fs } from 'fs';
7
- import path from 'path';
8
- import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
9
- import { createStore } from '@unrdf/oxigraph'; // TODO: Replace with Oxigraph Store
10
- import { z } from 'zod';
11
-
12
- const { namedNode, literal } = DataFactory;
13
-
14
- /* ========================================================================= */
15
- /* Namespace prefixes */
16
- /* ========================================================================= */
17
-
18
- const NS = {
19
- rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
20
- rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
21
- xsd: 'http://www.w3.org/2001/XMLSchema#',
22
- dom: 'http://example.org/unrdf/domain#',
23
- };
24
-
25
- /* ========================================================================= */
26
- /* Zod Schemas */
27
- /* ========================================================================= */
28
-
29
- const StackProfileSchema = z.object({
30
- hasZod: z.boolean().default(false),
31
- hasPrisma: z.boolean().default(false),
32
- hasTypeORM: z.boolean().default(false),
33
- hasSequelize: z.boolean().default(false),
34
- hasDrizzle: z.boolean().default(false),
35
- hasTypescript: z.boolean().default(false),
36
- sourceRoot: z.string().default('src'),
37
- });
38
-
39
- const InferOptionsSchema = z.object({
40
- fsStore: z.any(),
41
- stackProfile: StackProfileSchema.optional(),
42
- baseIri: z.string().default('http://example.org/unrdf/domain#'),
43
- projectRoot: z.string().optional(),
44
- });
45
-
46
- /**
47
- * @typedef {Object} DomainField
48
- * @property {string} name
49
- * @property {string} type
50
- * @property {boolean} optional
51
- * @property {boolean} array
52
- */
53
-
54
- /**
55
- * @typedef {Object} DomainEntity
56
- * @property {string} name
57
- * @property {string} source - 'zod' | 'prisma' | 'typescript' | 'typeorm' | 'sequelize'
58
- * @property {DomainField[]} fields
59
- * @property {string[]} relations
60
- */
61
-
62
- /**
63
- * @typedef {Object} DomainInferResult
64
- * @property {Store} store
65
- * @property {{entityCount: number, fieldCount: number, relationshipCount: number}} summary
66
- */
67
-
68
- /* ========================================================================= */
69
- /* Type mapping */
70
- /* ========================================================================= */
71
-
72
- /**
73
- * Map common type names to XSD types
74
- * @param {string} typeName
75
- * @returns {string}
76
- */
77
- function mapToXsdType(typeName) {
78
- const typeMap = {
79
- string: `${NS.xsd}string`,
80
- number: `${NS.xsd}decimal`,
81
- int: `${NS.xsd}integer`,
82
- integer: `${NS.xsd}integer`,
83
- float: `${NS.xsd}float`,
84
- double: `${NS.xsd}double`,
85
- boolean: `${NS.xsd}boolean`,
86
- bool: `${NS.xsd}boolean`,
87
- date: `${NS.xsd}date`,
88
- datetime: `${NS.xsd}dateTime`,
89
- timestamp: `${NS.xsd}dateTime`,
90
- bigint: `${NS.xsd}integer`,
91
- json: `${NS.xsd}string`,
92
- uuid: `${NS.xsd}string`,
93
- email: `${NS.xsd}string`,
94
- url: `${NS.xsd}anyURI`,
95
- };
96
-
97
- const normalized = typeName.toLowerCase().replace(/[\[\]?]/g, '');
98
- return typeMap[normalized] || `${NS.xsd}string`;
99
- }
100
-
101
- /* ========================================================================= */
102
- /* File reading utilities */
103
- /* ========================================================================= */
104
-
105
- /**
106
- * Read file content from filesystem
107
- * @param {string} filePath
108
- * @returns {Promise<string|null>}
109
- */
110
- async function readFileContent(filePath) {
111
- try {
112
- return await fs.readFile(filePath, 'utf-8');
113
- } catch {
114
- return null;
115
- }
116
- }
117
-
118
- /**
119
- * Extract file paths from fsStore
120
- * @param {Store} fsStore
121
- * @returns {Set<string>}
122
- */
123
- function extractFilePaths(fsStore) {
124
- const paths = new Set();
125
- const quads = fsStore.getQuads(
126
- null,
127
- namedNode('http://example.org/unrdf/filesystem#relativePath'),
128
- null
129
- );
130
- for (const quad of quads) {
131
- paths.add(quad.object.value);
132
- }
133
- return paths;
134
- }
135
-
136
- /* ========================================================================= */
137
- /* Stack profile detection */
138
- /* ========================================================================= */
139
-
140
- /**
141
- * Detect stack profile from package.json and file structure
142
- * @param {Store} fsStore
143
- * @param {string} [projectRoot]
144
- * @returns {Promise<z.infer<typeof StackProfileSchema>>}
145
- */
146
- async function detectStackProfile(fsStore, projectRoot) {
147
- const profile = {
148
- hasZod: false,
149
- hasPrisma: false,
150
- hasTypeORM: false,
151
- hasSequelize: false,
152
- hasDrizzle: false,
153
- hasTypescript: false,
154
- sourceRoot: 'src',
155
- };
156
-
157
- const filePaths = extractFilePaths(fsStore);
158
-
159
- // Check for TypeScript
160
- if (filePaths.has('tsconfig.json') || filePaths.has('tsconfig.base.json')) {
161
- profile.hasTypescript = true;
162
- }
163
-
164
- // Check for Prisma
165
- if (filePaths.has('prisma/schema.prisma') || filePaths.has('schema.prisma')) {
166
- profile.hasPrisma = true;
167
- }
168
-
169
- // Try to read package.json if projectRoot provided
170
- if (projectRoot) {
171
- const pkgContent = await readFileContent(path.join(projectRoot, 'package.json'));
172
- if (pkgContent) {
173
- try {
174
- const pkg = JSON.parse(pkgContent);
175
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
176
-
177
- if (allDeps.zod) profile.hasZod = true;
178
- if (allDeps['@prisma/client'] || allDeps.prisma) profile.hasPrisma = true;
179
- if (allDeps.typeorm) profile.hasTypeORM = true;
180
- if (allDeps.sequelize) profile.hasSequelize = true;
181
- if (allDeps.drizzle) profile.hasDrizzle = true;
182
- if (allDeps.typescript) profile.hasTypescript = true;
183
- } catch {
184
- // Ignore JSON parse errors
185
- }
186
- }
187
- }
188
-
189
- return profile;
190
- }
191
-
192
- /* ========================================================================= */
193
- /* Zod schema parsing */
194
- /* ========================================================================= */
195
-
196
- /**
197
- * Parse Zod schema definitions from file content
198
- * @param {string} content
199
- * @param {string} fileName
200
- * @returns {DomainEntity[]}
201
- */
202
- function parseZodSchemas(content, _fileName) {
203
- const entities = [];
204
-
205
- // Match export const XxxSchema = z.object({ ... })
206
- const schemaPattern =
207
- /(?:export\s+)?(?:const|let)\s+(\w+)Schema\s*=\s*z\.object\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\s*\)/g;
208
-
209
- let match;
210
- while ((match = schemaPattern.exec(content)) !== null) {
211
- const entityName = match[1];
212
- const fieldsBlock = match[2];
213
-
214
- const fields = parseZodFields(fieldsBlock);
215
-
216
- if (fields.length > 0) {
217
- entities.push({
218
- name: entityName,
219
- source: 'zod',
220
- fields,
221
- relations: extractZodRelations(fieldsBlock),
222
- });
223
- }
224
- }
225
-
226
- // Also match z.object inline (for simpler schemas)
227
- const inlinePattern =
228
- /(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\.object\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\s*\)/g;
229
-
230
- while ((match = inlinePattern.exec(content)) !== null) {
231
- const entityName = match[1];
232
- if (entityName.endsWith('Schema')) continue; // Already matched above
233
-
234
- const fieldsBlock = match[2];
235
- const fields = parseZodFields(fieldsBlock);
236
-
237
- if (fields.length > 0) {
238
- entities.push({
239
- name: entityName,
240
- source: 'zod',
241
- fields,
242
- relations: extractZodRelations(fieldsBlock),
243
- });
244
- }
245
- }
246
-
247
- return entities;
248
- }
249
-
250
- /**
251
- * Parse individual Zod fields from object block
252
- * @param {string} fieldsBlock
253
- * @returns {DomainField[]}
254
- */
255
- function parseZodFields(fieldsBlock) {
256
- const fields = [];
257
-
258
- // Match: fieldName: z.string(), z.number(), z.boolean(), z.array(), etc.
259
- const fieldPattern =
260
- /(\w+)\s*:\s*z\.(string|number|boolean|date|bigint|array|enum|object|union|optional|nullable)(\([^)]*\))?/g;
261
-
262
- let match;
263
- while ((match = fieldPattern.exec(fieldsBlock)) !== null) {
264
- const fieldName = match[1];
265
- let zodType = match[2];
266
- const _modifier = match[3] || '';
267
-
268
- // Check for optional/nullable chain
269
- const isOptional =
270
- fieldsBlock.includes(`${fieldName}:`) &&
271
- (fieldsBlock.includes('.optional()') || fieldsBlock.includes('.nullable()'));
272
- const isArray = zodType === 'array';
273
-
274
- // Map zod types to our types
275
- let type = 'string';
276
- if (zodType === 'number' || zodType === 'bigint') type = 'number';
277
- else if (zodType === 'boolean') type = 'boolean';
278
- else if (zodType === 'date') type = 'date';
279
- else if (zodType === 'array') type = 'array';
280
- else if (zodType === 'enum') type = 'enum';
281
-
282
- fields.push({
283
- name: fieldName,
284
- type,
285
- optional: isOptional,
286
- array: isArray,
287
- });
288
- }
289
-
290
- return fields;
291
- }
292
-
293
- /**
294
- * Extract relations from Zod schema
295
- * @param {string} fieldsBlock
296
- * @returns {string[]}
297
- */
298
- function extractZodRelations(fieldsBlock) {
299
- const relations = [];
300
-
301
- // Match: z.array(OtherSchema), z.lazy(() => OtherSchema)
302
- const relationPattern = /z\.(?:array|lazy)\s*\(\s*(?:\(\)\s*=>\s*)?(\w+)Schema\s*\)/g;
303
-
304
- let match;
305
- while ((match = relationPattern.exec(fieldsBlock)) !== null) {
306
- const relatedEntity = match[1];
307
- if (!relations.includes(relatedEntity)) {
308
- relations.push(relatedEntity);
309
- }
310
- }
311
-
312
- return relations;
313
- }
314
-
315
- /* ========================================================================= */
316
- /* Prisma schema parsing */
317
- /* ========================================================================= */
318
-
319
- /**
320
- * Parse Prisma schema file
321
- * @param {string} content
322
- * @returns {DomainEntity[]}
323
- */
324
- function parsePrismaSchema(content) {
325
- const entities = [];
326
-
327
- // Match model blocks
328
- const modelPattern = /model\s+(\w+)\s*\{([^}]+)\}/g;
329
-
330
- let match;
331
- while ((match = modelPattern.exec(content)) !== null) {
332
- const modelName = match[1];
333
- const fieldsBlock = match[2];
334
-
335
- const { fields, relations } = parsePrismaFields(fieldsBlock);
336
-
337
- entities.push({
338
- name: modelName,
339
- source: 'prisma',
340
- fields,
341
- relations,
342
- });
343
- }
344
-
345
- return entities;
346
- }
347
-
348
- /**
349
- * Parse Prisma fields from model block
350
- * @param {string} fieldsBlock
351
- * @returns {{fields: DomainField[], relations: string[]}}
352
- */
353
- function parsePrismaFields(fieldsBlock) {
354
- const fields = [];
355
- const relations = [];
356
-
357
- const lines = fieldsBlock.split('\n');
358
-
359
- for (const line of lines) {
360
- const trimmed = line.trim();
361
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('@@')) continue;
362
-
363
- // Match: fieldName Type? @relation(...) or fieldName Type[]
364
- const fieldMatch = trimmed.match(/^(\w+)\s+(\w+)(\[\])?\??(.*)$/);
365
-
366
- if (fieldMatch) {
367
- const fieldName = fieldMatch[1];
368
- const fieldType = fieldMatch[2];
369
- const isArray = !!fieldMatch[3];
370
- const modifiers = fieldMatch[4] || '';
371
-
372
- // Skip internal Prisma fields
373
- if (fieldName.startsWith('_')) continue;
374
-
375
- // Check if it's a relation
376
- if (modifiers.includes('@relation')) {
377
- if (!relations.includes(fieldType)) {
378
- relations.push(fieldType);
379
- }
380
- continue;
381
- }
382
-
383
- // Check if type is another model (relation without @relation)
384
- const isPrimitiveType = [
385
- 'String',
386
- 'Int',
387
- 'Float',
388
- 'Boolean',
389
- 'DateTime',
390
- 'BigInt',
391
- 'Decimal',
392
- 'Json',
393
- 'Bytes',
394
- ].includes(fieldType);
395
-
396
- if (!isPrimitiveType) {
397
- if (!relations.includes(fieldType)) {
398
- relations.push(fieldType);
399
- }
400
- continue;
401
- }
402
-
403
- const isOptional = trimmed.includes('?');
404
-
405
- fields.push({
406
- name: fieldName,
407
- type: fieldType.toLowerCase(),
408
- optional: isOptional,
409
- array: isArray,
410
- });
411
- }
412
- }
413
-
414
- return { fields, relations };
415
- }
416
-
417
- /* ========================================================================= */
418
- /* TypeScript type/interface parsing */
419
- /* ========================================================================= */
420
-
421
- /**
422
- * Parse TypeScript interfaces and types
423
- * @param {string} content
424
- * @param {string} fileName
425
- * @returns {DomainEntity[]}
426
- */
427
- function parseTypeScriptTypes(content, _fileName) {
428
- const entities = [];
429
-
430
- // Match interface declarations
431
- const interfacePattern =
432
- /(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+[\w,\s]+)?\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
433
-
434
- let match;
435
- while ((match = interfacePattern.exec(content)) !== null) {
436
- const entityName = match[1];
437
- const fieldsBlock = match[2];
438
-
439
- // Skip common non-entity interfaces
440
- if (
441
- entityName.endsWith('Props') ||
442
- entityName.endsWith('Options') ||
443
- entityName.endsWith('Config')
444
- ) {
445
- continue;
446
- }
447
-
448
- const { fields, relations } = parseTypeScriptFields(fieldsBlock);
449
-
450
- if (fields.length > 0) {
451
- entities.push({
452
- name: entityName,
453
- source: 'typescript',
454
- fields,
455
- relations,
456
- });
457
- }
458
- }
459
-
460
- // Match type declarations with object shape
461
- const typePattern = /(?:export\s+)?type\s+(\w+)\s*=\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
462
-
463
- while ((match = typePattern.exec(content)) !== null) {
464
- const entityName = match[1];
465
- const fieldsBlock = match[2];
466
-
467
- // Skip utility types
468
- if (
469
- entityName.endsWith('Props') ||
470
- entityName.endsWith('Options') ||
471
- entityName.endsWith('Config')
472
- ) {
473
- continue;
474
- }
475
-
476
- const { fields, relations } = parseTypeScriptFields(fieldsBlock);
477
-
478
- if (fields.length > 0) {
479
- entities.push({
480
- name: entityName,
481
- source: 'typescript',
482
- fields,
483
- relations,
484
- });
485
- }
486
- }
487
-
488
- return entities;
489
- }
490
-
491
- /**
492
- * Parse TypeScript fields from interface/type body
493
- * @param {string} fieldsBlock
494
- * @returns {{fields: DomainField[], relations: string[]}}
495
- */
496
- function parseTypeScriptFields(fieldsBlock) {
497
- const fields = [];
498
- const relations = [];
499
-
500
- // Match: fieldName: Type, fieldName?: Type, readonly fieldName: Type
501
- const fieldPattern = /(?:readonly\s+)?(\w+)(\?)?:\s*([^;,\n]+)/g;
502
-
503
- let match;
504
- while ((match = fieldPattern.exec(fieldsBlock)) !== null) {
505
- const fieldName = match[1];
506
- const isOptional = !!match[2];
507
- let rawType = match[3].trim();
508
-
509
- // Check for array types
510
- const isArray = rawType.endsWith('[]') || rawType.startsWith('Array<');
511
-
512
- // Clean up the type
513
- let baseType = rawType
514
- .replace(/\[\]$/, '')
515
- .replace(/^Array<(.+)>$/, '$1')
516
- .replace(/\s*\|\s*null$/, '')
517
- .replace(/\s*\|\s*undefined$/, '')
518
- .trim();
519
-
520
- // Check if it's a primitive type
521
- const primitiveTypes = [
522
- 'string',
523
- 'number',
524
- 'boolean',
525
- 'Date',
526
- 'bigint',
527
- 'symbol',
528
- 'any',
529
- 'unknown',
530
- 'never',
531
- 'void',
532
- ];
533
- const isPrimitive =
534
- primitiveTypes.includes(baseType) || primitiveTypes.includes(baseType.toLowerCase());
535
-
536
- if (
537
- !isPrimitive &&
538
- /^[A-Z]/.test(baseType) &&
539
- !baseType.includes('|') &&
540
- !baseType.includes('&')
541
- ) {
542
- // Looks like a relation to another entity
543
- if (!relations.includes(baseType)) {
544
- relations.push(baseType);
545
- }
546
- continue;
547
- }
548
-
549
- fields.push({
550
- name: fieldName,
551
- type: baseType.toLowerCase(),
552
- optional: isOptional,
553
- array: isArray,
554
- });
555
- }
556
-
557
- return { fields, relations };
558
- }
559
-
560
- /* ========================================================================= */
561
- /* TypeORM entity parsing */
562
- /* ========================================================================= */
563
-
564
- /**
565
- * Parse TypeORM entity decorators
566
- * @param {string} content
567
- * @param {string} fileName
568
- * @returns {DomainEntity[]}
569
- */
570
- function parseTypeORMEntities(content, _fileName) {
571
- const entities = [];
572
-
573
- // Check if this looks like a TypeORM entity file
574
- if (!content.includes('@Entity') && !content.includes('typeorm')) {
575
- return entities;
576
- }
577
-
578
- // Match @Entity() class declarations
579
- const entityPattern =
580
- /@Entity\([^)]*\)\s*(?:export\s+)?class\s+(\w+)(?:\s+extends\s+\w+)?\s*\{([^]*?)(?=\n\}|$)/g;
581
-
582
- let match;
583
- while ((match = entityPattern.exec(content)) !== null) {
584
- const entityName = match[1];
585
- const classBody = match[2];
586
-
587
- const { fields, relations } = parseTypeORMFields(classBody);
588
-
589
- entities.push({
590
- name: entityName,
591
- source: 'typeorm',
592
- fields,
593
- relations,
594
- });
595
- }
596
-
597
- return entities;
598
- }
599
-
600
- /**
601
- * Parse TypeORM fields from class body
602
- * @param {string} classBody
603
- * @returns {{fields: DomainField[], relations: string[]}}
604
- */
605
- function parseTypeORMFields(classBody) {
606
- const fields = [];
607
- const relations = [];
608
-
609
- // Match @Column() fieldName: Type
610
- const columnPattern = /@Column\([^)]*\)\s*(\w+)(?:\?)?:\s*([^;\n]+)/g;
611
-
612
- let match;
613
- while ((match = columnPattern.exec(classBody)) !== null) {
614
- const fieldName = match[1];
615
- const fieldType = match[2].trim();
616
-
617
- fields.push({
618
- name: fieldName,
619
- type: fieldType.toLowerCase(),
620
- optional: classBody.includes(`${fieldName}?:`),
621
- array: fieldType.endsWith('[]'),
622
- });
623
- }
624
-
625
- // Match relation decorators
626
- const relationPattern =
627
- /@(?:OneToMany|ManyToOne|OneToOne|ManyToMany)\([^)]*\)\s*\w+(?:\?)?:\s*(\w+)/g;
628
-
629
- while ((match = relationPattern.exec(classBody)) !== null) {
630
- const relatedType = match[1];
631
- if (!relations.includes(relatedType)) {
632
- relations.push(relatedType);
633
- }
634
- }
635
-
636
- return { fields, relations };
637
- }
638
-
639
- /* ========================================================================= */
640
- /* File discovery */
641
- /* ========================================================================= */
642
-
643
- /**
644
- * Find schema/type files in the project
645
- * @param {Set<string>} filePaths
646
- * @param {z.infer<typeof StackProfileSchema>} stackProfile
647
- * @returns {string[]}
648
- */
649
- function findSchemaFiles(filePaths, stackProfile) {
650
- const schemaFiles = [];
651
-
652
- for (const filePath of filePaths) {
653
- // Skip node_modules, dist, etc.
654
- if (
655
- filePath.includes('node_modules') ||
656
- filePath.includes('/dist/') ||
657
- filePath.includes('/build/')
658
- ) {
659
- continue;
660
- }
661
-
662
- const ext = path.extname(filePath);
663
- const basename = path.basename(filePath);
664
-
665
- // Check for Zod schema files
666
- if (stackProfile.hasZod) {
667
- if (
668
- filePath.includes('/schemas/') ||
669
- filePath.includes('/schema/') ||
670
- (filePath.includes('/types/') && (ext === '.ts' || ext === '.mjs')) ||
671
- basename.includes('schema') ||
672
- basename.includes('validation')
673
- ) {
674
- schemaFiles.push(filePath);
675
- }
676
- }
677
-
678
- // Check for TypeScript type files
679
- if (stackProfile.hasTypescript) {
680
- if (
681
- filePath.includes('/types/') ||
682
- filePath.includes('/interfaces/') ||
683
- filePath.includes('/models/') ||
684
- basename.endsWith('.d.ts') ||
685
- basename.includes('.types.')
686
- ) {
687
- schemaFiles.push(filePath);
688
- }
689
- }
690
-
691
- // Check for TypeORM entity files
692
- if (stackProfile.hasTypeORM) {
693
- if (
694
- filePath.includes('/entities/') ||
695
- filePath.includes('/entity/') ||
696
- basename.includes('.entity.')
697
- ) {
698
- schemaFiles.push(filePath);
699
- }
700
- }
701
-
702
- // Check for Prisma schema
703
- if (stackProfile.hasPrisma) {
704
- if (basename === 'schema.prisma') {
705
- schemaFiles.push(filePath);
706
- }
707
- }
708
- }
709
-
710
- // Dedupe
711
- return [...new Set(schemaFiles)];
712
- }
713
-
714
- /* ========================================================================= */
715
- /* Store building */
716
- /* ========================================================================= */
717
-
718
- /**
719
- * Add entity to RDF store
720
- * @param {Store} store
721
- * @param {DomainEntity} entity
722
- * @param {string} baseIri
723
- */
724
- function addEntityToStore(store, entity, baseIri) {
725
- const entityIri = namedNode(`${baseIri}${entity.name}`);
726
-
727
- // Add entity type
728
- store.addQuad(entityIri, namedNode(`${NS.rdf}type`), namedNode(`${NS.dom}Entity`));
729
-
730
- // Add label
731
- store.addQuad(entityIri, namedNode(`${NS.rdfs}label`), literal(entity.name));
732
-
733
- // Add source
734
- store.addQuad(entityIri, namedNode(`${NS.dom}source`), literal(entity.source));
735
-
736
- // Add fields
737
- for (const field of entity.fields) {
738
- const fieldIri = namedNode(`${baseIri}${entity.name}.${field.name}`);
739
-
740
- // Link entity to field
741
- store.addQuad(entityIri, namedNode(`${NS.dom}hasField`), fieldIri);
742
-
743
- // Add field type
744
- store.addQuad(fieldIri, namedNode(`${NS.rdf}type`), namedNode(`${NS.dom}Field`));
745
-
746
- // Add field name
747
- store.addQuad(fieldIri, namedNode(`${NS.dom}fieldName`), literal(field.name));
748
-
749
- // Add field type (XSD)
750
- store.addQuad(fieldIri, namedNode(`${NS.dom}fieldType`), namedNode(mapToXsdType(field.type)));
751
-
752
- // Add optional flag
753
- store.addQuad(
754
- fieldIri,
755
- namedNode(`${NS.dom}isOptional`),
756
- literal(field.optional, namedNode(`${NS.xsd}boolean`))
757
- );
758
-
759
- // Add array flag
760
- store.addQuad(
761
- fieldIri,
762
- namedNode(`${NS.dom}isArray`),
763
- literal(field.array, namedNode(`${NS.xsd}boolean`))
764
- );
765
- }
766
-
767
- // Add relations
768
- for (const relation of entity.relations) {
769
- const relationIri = namedNode(`${baseIri}${relation}`);
770
-
771
- store.addQuad(entityIri, namedNode(`${NS.dom}relatesTo`), relationIri);
772
- }
773
- }
774
-
775
- /* ========================================================================= */
776
- /* Main API */
777
- /* ========================================================================= */
778
-
779
- /**
780
- * Infer domain model from filesystem store
781
- *
782
- * @param {Object} options
783
- * @param {Store} options.fsStore - FS store from scanFileSystemToStore
784
- * @param {Object} [options.stackProfile] - Pre-computed stack profile
785
- * @param {string} [options.baseIri] - Base IRI for domain resources
786
- * @param {string} [options.projectRoot] - Project root path for reading files
787
- * @returns {Promise<DomainInferResult>}
788
- */
789
- export async function inferDomainModel(options) {
790
- const validated = InferOptionsSchema.parse(options);
791
- const { fsStore, baseIri, projectRoot } = validated;
792
-
793
- // Detect or use provided stack profile
794
- const stackProfile = validated.stackProfile || (await detectStackProfile(fsStore, projectRoot));
795
-
796
- const store = createStore();
797
- const allEntities = [];
798
-
799
- // Get file paths from fsStore
800
- const filePaths = extractFilePaths(fsStore);
801
-
802
- // Find schema files
803
- const schemaFiles = findSchemaFiles(filePaths, stackProfile);
804
-
805
- // Process each schema file
806
- for (const relativePath of schemaFiles) {
807
- if (!projectRoot) continue;
808
-
809
- const fullPath = path.join(projectRoot, relativePath);
810
- const content = await readFileContent(fullPath);
811
-
812
- if (!content) continue;
813
-
814
- const ext = path.extname(relativePath);
815
-
816
- // Parse based on file type and stack
817
- if (relativePath.endsWith('.prisma')) {
818
- const entities = parsePrismaSchema(content);
819
- allEntities.push(...entities);
820
- } else if (ext === '.ts' || ext === '.tsx') {
821
- // Check for Zod schemas first
822
- if (
823
- stackProfile.hasZod &&
824
- (content.includes("from 'zod'") || content.includes('from "zod"'))
825
- ) {
826
- const zodEntities = parseZodSchemas(content, relativePath);
827
- allEntities.push(...zodEntities);
828
- }
829
-
830
- // Check for TypeORM entities
831
- if (stackProfile.hasTypeORM && content.includes('@Entity')) {
832
- const typeormEntities = parseTypeORMEntities(content, relativePath);
833
- allEntities.push(...typeormEntities);
834
- }
835
-
836
- // Parse TypeScript types/interfaces
837
- if (stackProfile.hasTypescript) {
838
- const tsEntities = parseTypeScriptTypes(content, relativePath);
839
- allEntities.push(...tsEntities);
840
- }
841
- } else if (ext === '.mjs' || ext === '.js') {
842
- // Check for Zod schemas in JS/MJS files
843
- if (
844
- stackProfile.hasZod &&
845
- (content.includes("from 'zod'") || content.includes('from "zod"'))
846
- ) {
847
- const zodEntities = parseZodSchemas(content, relativePath);
848
- allEntities.push(...zodEntities);
849
- }
850
- }
851
- }
852
-
853
- // Deduplicate entities by name (prefer richer sources)
854
- const entityMap = new Map();
855
- const sourcePriority = { prisma: 4, typeorm: 3, zod: 2, typescript: 1 };
856
-
857
- for (const entity of allEntities) {
858
- const existing = entityMap.get(entity.name);
859
- if (
860
- !existing ||
861
- (sourcePriority[entity.source] || 0) > (sourcePriority[existing.source] || 0)
862
- ) {
863
- entityMap.set(entity.name, entity);
864
- }
865
- }
866
-
867
- // Add entities to store
868
- let fieldCount = 0;
869
- let relationshipCount = 0;
870
-
871
- for (const entity of entityMap.values()) {
872
- addEntityToStore(store, entity, baseIri);
873
- fieldCount += entity.fields.length;
874
- relationshipCount += entity.relations.length;
875
- }
876
-
877
- return {
878
- store,
879
- summary: {
880
- entityCount: entityMap.size,
881
- fieldCount,
882
- relationshipCount,
883
- },
884
- };
885
- }
886
-
887
- /**
888
- * Convenience function: infer from project path
889
- *
890
- * @param {string} projectRoot - Path to project root
891
- * @param {Object} [options]
892
- * @param {string} [options.baseIri] - Base IRI for domain resources
893
- * @returns {Promise<DomainInferResult>}
894
- */
895
- export async function inferDomainModelFromPath(projectRoot, options = {}) {
896
- // Import fs-scan dynamically to avoid circular deps
897
- const { scanFileSystemToStore } = await import('./fs-scan.mjs');
898
-
899
- const { store: fsStore } = await scanFileSystemToStore({ root: projectRoot });
900
-
901
- return inferDomainModel({
902
- fsStore,
903
- projectRoot,
904
- baseIri: options.baseIri,
905
- });
906
- }
907
-
908
- /* ========================================================================= */
909
- /* Domain Model Lens */
910
- /* ========================================================================= */
911
-
912
- /**
913
- * Ontology lens for domain model changes
914
- *
915
- * @param {import('../diff.mjs').DiffTriple} triple
916
- * @param {'added' | 'removed'} direction
917
- * @returns {import('../diff.mjs').OntologyChange | null}
918
- */
919
- export function DomainModelLens(triple, direction) {
920
- const { subject, predicate, object } = triple;
921
-
922
- // Entity added/removed
923
- if (predicate === `${NS.rdf}type` && object === `${NS.dom}Entity`) {
924
- const entityName = subject.split('#').pop() || subject.split('/').pop();
925
- return {
926
- kind: direction === 'added' ? 'EntityAdded' : 'EntityRemoved',
927
- entity: subject,
928
- details: { name: entityName },
929
- };
930
- }
931
-
932
- // Field added/removed
933
- if (predicate === `${NS.dom}hasField`) {
934
- const entityName = subject.split('#').pop() || subject.split('/').pop();
935
- const fieldName = object.split('.').pop();
936
- return {
937
- kind: direction === 'added' ? 'FieldAdded' : 'FieldRemoved',
938
- entity: subject,
939
- role: fieldName,
940
- details: { entityName, fieldName },
941
- };
942
- }
943
-
944
- // Relation added/removed
945
- if (predicate === `${NS.dom}relatesTo`) {
946
- const fromEntity = subject.split('#').pop() || subject.split('/').pop();
947
- const toEntity = object.split('#').pop() || object.split('/').pop();
948
- return {
949
- kind: direction === 'added' ? 'RelationAdded' : 'RelationRemoved',
950
- entity: subject,
951
- details: { from: fromEntity, to: toEntity },
952
- };
953
- }
954
-
955
- // Field type changed (detected as removed + added)
956
- if (predicate === `${NS.dom}fieldType`) {
957
- const fieldPath = subject.split('#').pop() || subject.split('/').pop();
958
- return {
959
- kind: direction === 'added' ? 'FieldTypeSet' : 'FieldTypeUnset',
960
- entity: subject,
961
- details: { field: fieldPath, type: object },
962
- };
963
- }
964
-
965
- return null;
966
- }