@unrdf/project-engine 5.0.1

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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/package.json +58 -0
  4. package/src/api-contract-validator.mjs +711 -0
  5. package/src/auto-test-generator.mjs +444 -0
  6. package/src/autonomic-mapek.mjs +511 -0
  7. package/src/capabilities-manifest.mjs +125 -0
  8. package/src/code-complexity-js.mjs +368 -0
  9. package/src/dependency-graph.mjs +276 -0
  10. package/src/doc-drift-checker.mjs +172 -0
  11. package/src/doc-generator.mjs +229 -0
  12. package/src/domain-infer.mjs +966 -0
  13. package/src/drift-snapshot.mjs +775 -0
  14. package/src/file-roles.mjs +94 -0
  15. package/src/fs-scan.mjs +305 -0
  16. package/src/gap-finder.mjs +376 -0
  17. package/src/golden-structure.mjs +149 -0
  18. package/src/hotspot-analyzer.mjs +412 -0
  19. package/src/index.mjs +151 -0
  20. package/src/initialize.mjs +957 -0
  21. package/src/lens/project-structure.mjs +74 -0
  22. package/src/mapek-orchestration.mjs +665 -0
  23. package/src/materialize-apply.mjs +505 -0
  24. package/src/materialize-plan.mjs +422 -0
  25. package/src/materialize.mjs +137 -0
  26. package/src/policy-derivation.mjs +869 -0
  27. package/src/project-config.mjs +142 -0
  28. package/src/project-diff.mjs +28 -0
  29. package/src/project-engine/build-utils.mjs +237 -0
  30. package/src/project-engine/code-analyzer.mjs +248 -0
  31. package/src/project-engine/doc-generator.mjs +407 -0
  32. package/src/project-engine/infrastructure.mjs +213 -0
  33. package/src/project-engine/metrics.mjs +146 -0
  34. package/src/project-model.mjs +111 -0
  35. package/src/project-report.mjs +348 -0
  36. package/src/refactoring-guide.mjs +242 -0
  37. package/src/stack-detect.mjs +102 -0
  38. package/src/stack-linter.mjs +213 -0
  39. package/src/template-infer.mjs +674 -0
  40. package/src/type-auditor.mjs +609 -0
@@ -0,0 +1,376 @@
1
+ /**
2
+ * @file Predictive Gap Finder - analyze domain model + project files to identify missing roles
3
+ * @module project-engine/gap-finder
4
+ */
5
+
6
+ import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
7
+ import { z } from 'zod';
8
+
9
+ const { namedNode } = DataFactory;
10
+
11
+ /* ========================================================================= */
12
+ /* Namespace prefixes */
13
+ /* ========================================================================= */
14
+
15
+ const NS = {
16
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
17
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
18
+ dom: 'http://example.org/unrdf/domain#',
19
+ fs: 'http://example.org/unrdf/filesystem#',
20
+ proj: 'http://example.org/unrdf/project#',
21
+ };
22
+
23
+ /* ========================================================================= */
24
+ /* Zod Schemas */
25
+ /* ========================================================================= */
26
+
27
+ const StackProfileSchema = z
28
+ .object({
29
+ webFramework: z.string().nullable().optional(),
30
+ uiFramework: z.string().nullable().optional(),
31
+ apiFramework: z.string().nullable().optional(),
32
+ testFramework: z.string().nullable().optional(),
33
+ })
34
+ .passthrough()
35
+ .optional();
36
+
37
+ const FindMissingRolesOptionsSchema = z.object({
38
+ domainStore: z.custom(val => val && typeof val.getQuads === 'function', {
39
+ message: 'domainStore must be an RDF store with getQuads method',
40
+ }),
41
+ projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
42
+ message: 'projectStore must be an RDF store with getQuads method',
43
+ }),
44
+ templateGraph: z.custom(val => val && typeof val.getQuads === 'function').optional(),
45
+ stackProfile: StackProfileSchema,
46
+ });
47
+
48
+ /* ========================================================================= */
49
+ /* Role configuration */
50
+ /* ========================================================================= */
51
+
52
+ /**
53
+ * Base role importance scores (0-100)
54
+ */
55
+ const ROLE_BASE_SCORES = {
56
+ Api: 95,
57
+ Route: 95,
58
+ Test: 90,
59
+ Component: 80,
60
+ Page: 80,
61
+ View: 80,
62
+ Schema: 70,
63
+ Service: 60,
64
+ Hook: 55,
65
+ Doc: 50,
66
+ };
67
+
68
+ /**
69
+ * Required roles per framework
70
+ */
71
+ const FRAMEWORK_ROLES = {
72
+ next: ['Page', 'Api', 'Component', 'Test', 'Schema'],
73
+ 'next-app-router': ['Page', 'Api', 'Component', 'Test', 'Schema'],
74
+ 'next-pages': ['Page', 'Api', 'Component', 'Test', 'Schema'],
75
+ express: ['Route', 'Service', 'Test', 'Schema'],
76
+ nest: ['Controller', 'Service', 'Test', 'Schema'],
77
+ react: ['Component', 'Test', 'Schema'],
78
+ default: ['Api', 'Component', 'Test', 'Schema'],
79
+ };
80
+
81
+ /**
82
+ * Framework-specific role score boosts
83
+ */
84
+ const FRAMEWORK_BOOSTS = {
85
+ next: { Page: 15, Api: 10 },
86
+ 'next-app-router': { Page: 15, Api: 10 },
87
+ 'next-pages': { Page: 15, Api: 10 },
88
+ express: { Route: 15, Service: 10 },
89
+ nest: { Controller: 15, Service: 10 },
90
+ };
91
+
92
+ /* ========================================================================= */
93
+ /* Entity extraction */
94
+ /* ========================================================================= */
95
+
96
+ /**
97
+ * Extract entity names from domain store
98
+ * @param {Store} domainStore
99
+ * @returns {string[]}
100
+ */
101
+ function extractEntities(domainStore) {
102
+ const entities = [];
103
+ const quads = domainStore.getQuads(
104
+ null,
105
+ namedNode(`${NS.rdf}type`),
106
+ namedNode(`${NS.dom}Entity`)
107
+ );
108
+
109
+ for (const quad of quads) {
110
+ const iri = quad.subject.value;
111
+ const name = iri.split('#').pop() || iri.split('/').pop();
112
+ if (name) entities.push(name);
113
+ }
114
+
115
+ return entities;
116
+ }
117
+
118
+ /* ========================================================================= */
119
+ /* File role extraction */
120
+ /* ========================================================================= */
121
+
122
+ /**
123
+ * Extract file paths with their roles from project store
124
+ * @param {Store} projectStore
125
+ * @returns {Array<{path: string, role: string|null}>}
126
+ */
127
+ function extractFilesWithRoles(projectStore) {
128
+ const files = [];
129
+
130
+ const pathQuads = projectStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
131
+
132
+ for (const quad of pathQuads) {
133
+ const fileIri = quad.subject;
134
+ const path = quad.object.value;
135
+
136
+ // Get role for this file
137
+ const roleQuads = projectStore.getQuads(fileIri, namedNode(`${NS.proj}roleString`), null);
138
+
139
+ const role = roleQuads.length > 0 ? roleQuads[0].object.value : null;
140
+ files.push({ path, role });
141
+ }
142
+
143
+ return files;
144
+ }
145
+
146
+ /* ========================================================================= */
147
+ /* Entity-file matching */
148
+ /* ========================================================================= */
149
+
150
+ /**
151
+ * Match file to entity by name pattern
152
+ * @param {string} filePath
153
+ * @param {string} entityName
154
+ * @returns {boolean}
155
+ */
156
+ function fileMatchesEntity(filePath, entityName) {
157
+ const lowerPath = filePath.toLowerCase();
158
+ const lowerEntity = entityName.toLowerCase();
159
+
160
+ // Direct match
161
+ if (lowerPath.includes(lowerEntity)) return true;
162
+
163
+ // Plural form: User -> users
164
+ if (lowerPath.includes(lowerEntity + 's')) return true;
165
+
166
+ // Kebab-case: UserProfile -> user-profile
167
+ const kebabEntity = entityName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
168
+ if (lowerPath.includes(kebabEntity)) return true;
169
+
170
+ // Snake_case: UserProfile -> user_profile
171
+ const snakeEntity = entityName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
172
+ if (lowerPath.includes(snakeEntity)) return true;
173
+
174
+ return false;
175
+ }
176
+
177
+ /**
178
+ * Find roles present for an entity
179
+ * @param {string} entityName
180
+ * @param {Array<{path: string, role: string|null}>} files
181
+ * @returns {Set<string>}
182
+ */
183
+ function findPresentRoles(entityName, files) {
184
+ const roles = new Set();
185
+
186
+ for (const { path, role } of files) {
187
+ if (role && fileMatchesEntity(path, entityName)) {
188
+ roles.add(role);
189
+ }
190
+ }
191
+
192
+ return roles;
193
+ }
194
+
195
+ /* ========================================================================= */
196
+ /* Gap detection */
197
+ /* ========================================================================= */
198
+
199
+ /**
200
+ * Determine required roles based on stack profile
201
+ * @param {Object} stackProfile
202
+ * @returns {string[]}
203
+ */
204
+ function getRequiredRoles(stackProfile) {
205
+ if (!stackProfile) return FRAMEWORK_ROLES.default;
206
+
207
+ const framework = stackProfile.webFramework || stackProfile.apiFramework;
208
+ if (framework && FRAMEWORK_ROLES[framework]) {
209
+ return FRAMEWORK_ROLES[framework];
210
+ }
211
+
212
+ return FRAMEWORK_ROLES.default;
213
+ }
214
+
215
+ /**
216
+ * Calculate missing roles for an entity
217
+ * @param {string} entityName
218
+ * @param {Set<string>} presentRoles
219
+ * @param {string[]} requiredRoles
220
+ * @returns {string[]}
221
+ */
222
+ function calculateMissingRoles(entityName, presentRoles, requiredRoles) {
223
+ return requiredRoles.filter(role => !presentRoles.has(role));
224
+ }
225
+
226
+ /**
227
+ * Generate suggestion for a missing role
228
+ * @param {string} entityName
229
+ * @param {string} role
230
+ * @returns {string}
231
+ */
232
+ function generateSuggestion(entityName, role) {
233
+ const suggestions = {
234
+ Api: `${entityName}Api is needed for /${entityName.toLowerCase()} endpoint`,
235
+ Route: `${entityName}Route handler needed for /${entityName.toLowerCase()} routes`,
236
+ Page: `${entityName}Page needed for /${entityName.toLowerCase()} view`,
237
+ Component: `${entityName}View component needed for UI`,
238
+ Test: `${entityName} test suite needed for coverage`,
239
+ Schema: `${entityName}Schema needed for validation`,
240
+ Service: `${entityName}Service needed for business logic`,
241
+ Doc: `${entityName} documentation needed`,
242
+ };
243
+
244
+ return suggestions[role] || `${entityName}${role} needed`;
245
+ }
246
+
247
+ /* ========================================================================= */
248
+ /* Main API */
249
+ /* ========================================================================= */
250
+
251
+ /**
252
+ * @typedef {Object} GapResult
253
+ * @property {string} entity - Entity name
254
+ * @property {string[]} missingRoles - Roles that are missing
255
+ * @property {number} score - Priority score (0-100)
256
+ * @property {string} suggestion - Human-readable suggestion
257
+ * @property {string[]} [files] - Related files if they exist but aren't classified
258
+ */
259
+
260
+ /**
261
+ * @typedef {Object} FindMissingRolesResult
262
+ * @property {GapResult[]} gaps - Array of gap results
263
+ * @property {string} summary - Human-readable summary
264
+ * @property {string} callToAction - CLI command suggestion
265
+ */
266
+
267
+ /**
268
+ * Analyze domain model + project files to predict which features are missing roles
269
+ *
270
+ * @param {Object} options
271
+ * @param {Store} options.domainStore - Domain model store (from inferDomainModel)
272
+ * @param {Store} options.projectStore - Project structure store (from buildProjectModelFromFs + classifyFiles)
273
+ * @param {Store} [options.templateGraph] - Optional template graph
274
+ * @param {Object} [options.stackProfile] - Stack profile for framework-specific rules
275
+ * @returns {FindMissingRolesResult}
276
+ */
277
+ export function findMissingRoles(options) {
278
+ const validated = FindMissingRolesOptionsSchema.parse(options);
279
+ const { domainStore, projectStore, stackProfile } = validated;
280
+
281
+ // Extract entities from domain model
282
+ const entities = extractEntities(domainStore);
283
+
284
+ // Early exit if no entities
285
+ if (entities.length === 0) {
286
+ return {
287
+ gaps: [],
288
+ summary: '0 gaps found (no entities in domain model)',
289
+ callToAction: 'Run: unrdf infer-domain to detect domain entities',
290
+ };
291
+ }
292
+
293
+ // Extract files with roles from project store
294
+ const files = extractFilesWithRoles(projectStore);
295
+
296
+ // Determine required roles based on stack
297
+ const requiredRoles = getRequiredRoles(stackProfile);
298
+
299
+ // Find gaps for each entity
300
+ const gaps = [];
301
+ const gapCounts = { Api: 0, Test: 0, Component: 0, Schema: 0, other: 0 };
302
+
303
+ for (const entityName of entities) {
304
+ const presentRoles = findPresentRoles(entityName, files);
305
+ const missingRoles = calculateMissingRoles(entityName, presentRoles, requiredRoles);
306
+
307
+ // Calculate max score for this entity's gaps
308
+ const maxScore = missingRoles.reduce((max, role) => {
309
+ const score = scoreMissingRole(entityName, role, stackProfile || {});
310
+ return Math.max(max, score);
311
+ }, 0);
312
+
313
+ // Count gaps by type
314
+ for (const role of missingRoles) {
315
+ if (gapCounts[role] !== undefined) {
316
+ gapCounts[role]++;
317
+ } else {
318
+ gapCounts.other++;
319
+ }
320
+ }
321
+
322
+ gaps.push({
323
+ entity: entityName,
324
+ missingRoles,
325
+ score: maxScore,
326
+ suggestion: missingRoles.length > 0 ? generateSuggestion(entityName, missingRoles[0]) : '',
327
+ });
328
+ }
329
+
330
+ // Sort by score (highest first)
331
+ gaps.sort((a, b) => b.score - a.score);
332
+
333
+ // Generate summary
334
+ const summaryParts = [];
335
+ if (gapCounts.Api > 0) summaryParts.push(`${gapCounts.Api} missing APIs`);
336
+ if (gapCounts.Test > 0) summaryParts.push(`${gapCounts.Test} missing tests`);
337
+ if (gapCounts.Component > 0) summaryParts.push(`${gapCounts.Component} missing components`);
338
+ if (gapCounts.Schema > 0) summaryParts.push(`${gapCounts.Schema} missing schemas`);
339
+ if (gapCounts.other > 0) summaryParts.push(`${gapCounts.other} other gaps`);
340
+
341
+ const summary =
342
+ summaryParts.length > 0
343
+ ? summaryParts.join(', ')
344
+ : '0 gaps found - all entities have required roles';
345
+
346
+ // Generate call to action
347
+ const topGap = gaps.find(g => g.missingRoles.length > 0);
348
+ const callToAction = topGap
349
+ ? `Run: unrdf generate --entity ${topGap.entity} --roles ${topGap.missingRoles.join(',')}`
350
+ : 'Run: unrdf analyze to check for other issues';
351
+
352
+ return { gaps, summary, callToAction };
353
+ }
354
+
355
+ /**
356
+ * Score the importance of a missing role for an entity
357
+ *
358
+ * @param {string} entity - Entity name
359
+ * @param {string} role - Missing role name
360
+ * @param {Object} stackProfile - Stack profile for framework-specific boosts
361
+ * @returns {number} Score from 0-100
362
+ */
363
+ export function scoreMissingRole(entity, role, stackProfile) {
364
+ // Get base score
365
+ let score = ROLE_BASE_SCORES[role] || 40;
366
+
367
+ // Apply framework-specific boosts
368
+ const framework = stackProfile?.webFramework || stackProfile?.apiFramework;
369
+ if (framework && FRAMEWORK_BOOSTS[framework]) {
370
+ const boost = FRAMEWORK_BOOSTS[framework][role] || 0;
371
+ score += boost;
372
+ }
373
+
374
+ // Cap at 100
375
+ return Math.min(score, 100);
376
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @file Golden structure ontologies - canonical best-practice structures
3
+ * @module project-engine/golden-structure
4
+ */
5
+
6
+ import { promises as _fs } from 'fs';
7
+ import { createStore as _createStore } from '@unrdf/oxigraph'; // TODO: Replace with Oxigraph Store
8
+ import { parseTurtle } from '../knowledge-engine/parse.mjs';
9
+ import { trace, SpanStatusCode } from '@opentelemetry/api';
10
+ import { z } from 'zod';
11
+
12
+ const tracer = trace.getTracer('unrdf/golden-structure');
13
+
14
+ const GoldenStructureOptionsSchema = z.object({
15
+ profile: z.enum([
16
+ 'react-feature-v1',
17
+ 'next-app-router-v1',
18
+ 'next-pages-v1',
19
+ 'nest-api-v1',
20
+ 'express-api-v1',
21
+ ]),
22
+ });
23
+
24
+ /**
25
+ * Generate or load golden structure from profile
26
+ *
27
+ * @param {Object} options
28
+ * @param {string} options.profile - Profile ID (e.g., 'react-feature-v1')
29
+ * @returns {Promise<Store>} Store with golden structure
30
+ */
31
+ export async function generateGoldenStructure(options) {
32
+ const validated = GoldenStructureOptionsSchema.parse(options);
33
+ const { profile } = validated;
34
+
35
+ return tracer.startActiveSpan('golden.structure.generate', async span => {
36
+ try {
37
+ span.setAttribute('golden.profile', profile);
38
+
39
+ const ttl = getGoldenStructureTtl(profile);
40
+ const store = await parseTurtle(ttl, 'http://example.org/unrdf/golden#');
41
+
42
+ span.setAttribute('golden.store_size', store.size);
43
+ span.setStatus({ code: SpanStatusCode.OK });
44
+
45
+ return store;
46
+ } catch (error) {
47
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
48
+ throw error;
49
+ }
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Get golden structure TTL for a profile
55
+ *
56
+ * @private
57
+ */
58
+ function getGoldenStructureTtl(profile) {
59
+ const baseGolden = `
60
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
61
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
62
+ @prefix unproj: <http://example.org/unrdf/project#> .
63
+ @prefix golden: <http://example.org/unrdf/golden#> .
64
+
65
+ golden:project a unproj:Project ;
66
+ rdfs:label "Golden Structure" .
67
+ `;
68
+
69
+ // Profile-specific structures
70
+ if (profile === 'react-feature-v1') {
71
+ return (
72
+ baseGolden +
73
+ `
74
+ golden:featureExample a unproj:Feature ;
75
+ rdfs:label "Example Feature" ;
76
+ rdfs:comment "Standard React feature structure" .
77
+
78
+ golden:featureExample unproj:hasRole unproj:Component ;
79
+ unproj:hasRole unproj:Hook ;
80
+ unproj:hasRole unproj:Schema ;
81
+ unproj:hasRole unproj:Test ;
82
+ unproj:hasRole unproj:Doc .
83
+
84
+ golden:pathComponentPattern a rdf:Property ;
85
+ rdfs:value "src/features/*/components/*.tsx" .
86
+
87
+ golden:pathHookPattern a rdf:Property ;
88
+ rdfs:value "src/features/*/hooks/use*.ts" .
89
+
90
+ golden:pathTestPattern a rdf:Property ;
91
+ rdfs:value "src/features/**/*.test.tsx" .
92
+ `
93
+ );
94
+ } else if (profile === 'next-app-router-v1') {
95
+ return (
96
+ baseGolden +
97
+ `
98
+ golden:project unproj:webFramework "next-app-router" .
99
+
100
+ golden:pageExample a unproj:Page ;
101
+ rdfs:label "Page Route" .
102
+
103
+ golden:layoutExample a unproj:Component ;
104
+ rdfs:label "Layout Component" .
105
+
106
+ golden:apiExample a unproj:Api ;
107
+ rdfs:label "API Route" .
108
+
109
+ golden:pathPagePattern a rdf:Property ;
110
+ rdfs:value "src/app/**/page.tsx" .
111
+
112
+ golden:pathLayoutPattern a rdf:Property ;
113
+ rdfs:value "src/app/**/layout.tsx" .
114
+
115
+ golden:pathApiPattern a rdf:Property ;
116
+ rdfs:value "src/app/**/route.ts" .
117
+ `
118
+ );
119
+ } else if (profile === 'nest-api-v1') {
120
+ return (
121
+ baseGolden +
122
+ `
123
+ golden:project unproj:webFramework "nest" ;
124
+ unproj:apiFramework "nest" .
125
+
126
+ golden:controllerExample a unproj:Api ;
127
+ rdfs:label "NestJS Controller" .
128
+
129
+ golden:serviceExample a unproj:Service ;
130
+ rdfs:label "NestJS Service" .
131
+
132
+ golden:schemaExample a unproj:Schema ;
133
+ rdfs:label "NestJS DTO" .
134
+
135
+ golden:pathControllerPattern a rdf:Property ;
136
+ rdfs:value "src/**/*.controller.ts" .
137
+
138
+ golden:pathServicePattern a rdf:Property ;
139
+ rdfs:value "src/**/*.service.ts" .
140
+
141
+ golden:pathDtoPattern a rdf:Property ;
142
+ rdfs:value "src/**/dto/*.dto.ts" .
143
+ `
144
+ );
145
+ } else {
146
+ // Default fallback
147
+ return baseGolden;
148
+ }
149
+ }