@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,146 +0,0 @@
1
- /**
2
- * @file Metrics Collection - Gather and report project metrics
3
- * @module @unrdf/project-engine/metrics
4
- */
5
-
6
- import { z } from 'zod';
7
- import { analyzePackage } from './code-analyzer.mjs';
8
- import { listPackages } from './build-utils.mjs';
9
-
10
- /**
11
- * Project metrics schema
12
- */
13
- const ProjectMetricsSchema = z.object({
14
- timestamp: z.string(),
15
- totalPackages: z.number(),
16
- totalLinesOfCode: z.number(),
17
- totalExports: z.number(),
18
- averageTestCoverage: z.number().min(0).max(100),
19
- packages: z.array(
20
- z.object({
21
- name: z.string(),
22
- linesOfCode: z.number(),
23
- exportCount: z.number(),
24
- testCoverage: z.number(),
25
- complexity: z.enum(['low', 'medium', 'high']),
26
- })
27
- ),
28
- });
29
-
30
- /**
31
- * Collect metrics for all packages in monorepo
32
- * @param {string} [monorepoPath='.'] - Path to monorepo root
33
- * @returns {Promise<Object>} Comprehensive metrics data
34
- *
35
- * @throws {TypeError} If monorepoPath is not a string
36
- * @throws {Error} If metrics collection fails
37
- *
38
- * @example
39
- * const metrics = await collectMetrics('.');
40
- * console.log('Total packages:', metrics.totalPackages);
41
- * console.log('Average coverage:', metrics.averageTestCoverage);
42
- */
43
- export async function collectMetrics(monorepoPath = '.') {
44
- if (typeof monorepoPath !== 'string') {
45
- throw new TypeError('collectMetrics: monorepoPath must be a string');
46
- }
47
-
48
- try {
49
- const packages = await listPackages(monorepoPath);
50
-
51
- let totalLinesOfCode = 0;
52
- let totalExports = 0;
53
- let totalCoverage = 0;
54
- const packageMetrics = [];
55
-
56
- for (const pkg of packages) {
57
- try {
58
- const analysis = await analyzePackage(pkg.path);
59
-
60
- totalLinesOfCode += analysis.linesOfCode;
61
- totalExports += analysis.exportCount;
62
- totalCoverage += analysis.testCoverage;
63
-
64
- packageMetrics.push({
65
- name: analysis.name,
66
- linesOfCode: analysis.linesOfCode,
67
- exportCount: analysis.exportCount,
68
- testCoverage: analysis.testCoverage,
69
- complexity: analysis.complexity,
70
- });
71
- } catch (error) {
72
- // Skip packages that fail analysis
73
- console.error(`Failed to analyze ${pkg.name}: ${error.message}`);
74
- }
75
- }
76
-
77
- const averageTestCoverage =
78
- packageMetrics.length > 0 ? Math.round(totalCoverage / packageMetrics.length) : 0;
79
-
80
- const metrics = {
81
- timestamp: new Date().toISOString(),
82
- totalPackages: packageMetrics.length,
83
- totalLinesOfCode,
84
- totalExports,
85
- averageTestCoverage,
86
- packages: packageMetrics,
87
- };
88
-
89
- return ProjectMetricsSchema.parse(metrics);
90
- } catch (error) {
91
- throw new Error(`collectMetrics failed: ${error.message}`);
92
- }
93
- }
94
-
95
- /**
96
- * Format metrics as human-readable report
97
- * @param {Object} metrics - Metrics data from collectMetrics
98
- * @returns {string} Formatted text report
99
- *
100
- * @throws {TypeError} If metrics is not an object
101
- *
102
- * @example
103
- * const metrics = await collectMetrics();
104
- * const report = reportMetrics(metrics);
105
- * console.log(report);
106
- */
107
- export function reportMetrics(metrics) {
108
- if (typeof metrics !== 'object' || metrics === null) {
109
- throw new TypeError('reportMetrics: metrics must be an object');
110
- }
111
-
112
- const lines = [];
113
-
114
- lines.push('='.repeat(60));
115
- lines.push('PROJECT METRICS REPORT');
116
- lines.push('='.repeat(60));
117
- lines.push('');
118
- lines.push(`Generated: ${new Date(metrics.timestamp).toLocaleString()}`);
119
- lines.push('');
120
- lines.push('OVERVIEW');
121
- lines.push('-'.repeat(60));
122
- lines.push(`Total Packages: ${metrics.totalPackages}`);
123
- lines.push(`Total Lines of Code: ${metrics.totalLinesOfCode.toLocaleString()}`);
124
- lines.push(`Total Exports: ${metrics.totalExports}`);
125
- lines.push(`Average Test Coverage: ${metrics.averageTestCoverage}%`);
126
- lines.push('');
127
- lines.push('PACKAGE BREAKDOWN');
128
- lines.push('-'.repeat(60));
129
- lines.push('');
130
-
131
- // Sort packages by lines of code descending
132
- const sorted = [...metrics.packages].sort((a, b) => b.linesOfCode - a.linesOfCode);
133
-
134
- for (const pkg of sorted) {
135
- lines.push(`Package: ${pkg.name}`);
136
- lines.push(` Lines of Code: ${pkg.linesOfCode.toLocaleString()}`);
137
- lines.push(` Exports: ${pkg.exportCount}`);
138
- lines.push(` Test Coverage: ${pkg.testCoverage}%`);
139
- lines.push(` Complexity: ${pkg.complexity}`);
140
- lines.push('');
141
- }
142
-
143
- lines.push('='.repeat(60));
144
-
145
- return lines.join('\n');
146
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * @file Project model builder - convert FS graph to project structure
3
- * @module project-engine/project-model
4
- */
5
-
6
- import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
7
- import { z } from 'zod';
8
-
9
- const { namedNode, literal } = DataFactory;
10
-
11
- const ProjectModelOptionsSchema = z.object({
12
- fsStore: z.custom(val => val && typeof val.getQuads === 'function', {
13
- message: 'fsStore must be an RDF store with getQuads method',
14
- }),
15
- baseIri: z.string().default('http://example.org/unrdf/project#'),
16
- conventions: z
17
- .object({
18
- sourcePaths: z.array(z.string()).default(['src']),
19
- featurePaths: z.array(z.string()).default(['features', 'modules']),
20
- testPaths: z.array(z.string()).default(['__tests__', 'test', 'tests', 'spec']),
21
- })
22
- .optional(),
23
- });
24
-
25
- /**
26
- * Build project model from filesystem graph
27
- *
28
- * @param {Object} options
29
- * @param {Store} options.fsStore - FS store from scanFileSystemToStore
30
- * @param {string} [options.baseIri] - Base IRI for project resources
31
- * @param {Object} [options.conventions] - Naming conventions
32
- * @returns {Store} Enhanced store with project structure
33
- */
34
- export function buildProjectModelFromFs(options) {
35
- const validated = ProjectModelOptionsSchema.parse(options);
36
- const { fsStore, baseIri, conventions } = validated;
37
-
38
- const store = fsStore;
39
- const projectIri = namedNode(`${baseIri}project`);
40
-
41
- store.addQuad(
42
- projectIri,
43
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
44
- namedNode('http://example.org/unrdf/project#Project')
45
- );
46
-
47
- const features = extractFeatures(store, baseIri, conventions);
48
-
49
- for (const [featureName, feature] of Object.entries(features)) {
50
- const featureIri = namedNode(`${baseIri}feature/${encodeURIComponent(featureName)}`);
51
-
52
- store.addQuad(projectIri, namedNode('http://example.org/unrdf/project#hasFeature'), featureIri);
53
- store.addQuad(
54
- featureIri,
55
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
56
- namedNode('http://example.org/unrdf/project#Feature')
57
- );
58
- store.addQuad(
59
- featureIri,
60
- namedNode('http://www.w3.org/2000/01/rdf-schema#label'),
61
- literal(featureName)
62
- );
63
-
64
- for (const filePath of feature.files) {
65
- const fileIri = namedNode(`http://example.org/unrdf/fs#${encodeURIComponent(filePath)}`);
66
- store.addQuad(
67
- fileIri,
68
- namedNode('http://example.org/unrdf/project#belongsToFeature'),
69
- featureIri
70
- );
71
- }
72
- }
73
-
74
- return store;
75
- }
76
-
77
- /**
78
- * Extract features from directory structure
79
- *
80
- * @private
81
- */
82
- function extractFeatures(store, baseIri, conventions) {
83
- const features = {};
84
- const sourcePaths = conventions?.sourcePaths || ['src'];
85
-
86
- const fileQuads = store.getQuads(
87
- null,
88
- namedNode('http://example.org/unrdf/filesystem#relativePath'),
89
- null
90
- );
91
-
92
- for (const quad of fileQuads) {
93
- const filePath = quad.object.value;
94
- const inSourcePath = sourcePaths.some(
95
- srcPath => filePath.startsWith(srcPath + '/') || filePath === srcPath
96
- );
97
-
98
- if (inSourcePath) {
99
- const match = filePath.match(/^src\/([^/]+)/);
100
- if (match) {
101
- const featureName = match[1];
102
- if (!features[featureName]) {
103
- features[featureName] = { files: [] };
104
- }
105
- features[featureName].files.push(filePath);
106
- }
107
- }
108
- }
109
-
110
- return features;
111
- }
@@ -1,348 +0,0 @@
1
- /**
2
- * @file Project report generator - convert ontology to human-readable summary
3
- * @module project-engine/project-report
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
- const ProjectReportOptionsSchema = z.object({
12
- baseIri: z.string().optional(),
13
- includeFileList: z.boolean().optional(),
14
- });
15
-
16
- /**
17
- * @typedef {Object} FeatureReport
18
- * @property {string} iri - Feature IRI
19
- * @property {string} name - Feature name
20
- * @property {{view: boolean, api: boolean, schema: boolean, test: boolean, doc: boolean}} roles - Role presence
21
- * @property {number} fileCount - Number of files in feature
22
- * @property {number} testCoverage - Estimated test coverage percentage
23
- * @property {boolean} hasMissingTests - Whether feature lacks tests
24
- */
25
-
26
- /**
27
- * @typedef {Object} DomainEntity
28
- * @property {string} name - Entity name
29
- * @property {number} fieldCount - Number of fields
30
- * @property {boolean} hasView - Has view component
31
- * @property {boolean} hasApi - Has API endpoint
32
- */
33
-
34
- /**
35
- * @typedef {Object} ProjectStats
36
- * @property {number} featureCount - Total features
37
- * @property {number} totalFiles - Total files
38
- * @property {number} testCoverageAverage - Average test coverage
39
- * @property {Record<string, number>} filesByRole - Files grouped by role
40
- */
41
-
42
- /**
43
- * @typedef {Object} ProjectReport
44
- * @property {FeatureReport[]} features - Feature reports
45
- * @property {string} stackProfile - Detected stack profile string
46
- * @property {ProjectStats} stats - Project statistics
47
- * @property {DomainEntity[]} domainEntities - Domain entities
48
- * @property {string} summary - Human-readable summary
49
- */
50
-
51
- const NS = {
52
- rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
53
- rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
54
- fs: 'http://example.org/unrdf/filesystem#',
55
- proj: 'http://example.org/unrdf/project#',
56
- };
57
-
58
- /**
59
- * Build human-readable project report from ontology store
60
- *
61
- * @param {Object} projectStore - N3 Store with project ontology
62
- * @param {Object} [options] - Report options
63
- * @returns {ProjectReport}
64
- */
65
- export function buildProjectReport(projectStore, options = {}) {
66
- const validatedOptions = ProjectReportOptionsSchema.parse(options);
67
- const store = projectStore;
68
- const baseIri = validatedOptions.baseIri || 'http://example.org/unrdf/project#';
69
-
70
- const features = extractFeatureReports(store, baseIri);
71
- const stackProfile = buildStackProfile(store);
72
- const stats = computeStats(store, features);
73
- const domainEntities = extractDomainEntities(store, features);
74
- const summary = generateSummary(features, stats, stackProfile);
75
-
76
- return {
77
- features,
78
- stackProfile,
79
- stats,
80
- domainEntities,
81
- summary,
82
- };
83
- }
84
-
85
- /**
86
- * Extract feature reports from store
87
- *
88
- * @param {Object} store - N3 Store
89
- * @param {string} baseIri - Base IRI
90
- * @returns {FeatureReport[]}
91
- */
92
- function extractFeatureReports(store, _baseIri) {
93
- const features = [];
94
-
95
- const featureQuads = store.getQuads(
96
- null,
97
- namedNode(`${NS.rdf}type`),
98
- namedNode(`${NS.proj}Feature`)
99
- );
100
-
101
- for (const quad of featureQuads) {
102
- const featureIri = quad.subject.value;
103
- const labelQuads = store.getQuads(quad.subject, namedNode(`${NS.rdfs}label`), null);
104
- const name =
105
- labelQuads.length > 0 ? labelQuads[0].object.value : extractNameFromIri(featureIri);
106
-
107
- const fileQuads = store.getQuads(null, namedNode(`${NS.proj}belongsToFeature`), quad.subject);
108
- const fileCount = fileQuads.length;
109
-
110
- const roles = countRolesForFeature(store, fileQuads);
111
- const hasTests = roles.test > 0;
112
- const testCoverage =
113
- fileCount > 0 && hasTests
114
- ? Math.min(100, Math.round((roles.test / (fileCount - roles.test)) * 100))
115
- : 0;
116
-
117
- features.push({
118
- iri: featureIri,
119
- name,
120
- roles: {
121
- view: roles.component > 0 || roles.page > 0,
122
- api: roles.api > 0,
123
- schema: roles.schema > 0,
124
- test: roles.test > 0,
125
- doc: roles.doc > 0,
126
- },
127
- fileCount,
128
- testCoverage,
129
- hasMissingTests: !hasTests && fileCount > 0,
130
- });
131
- }
132
-
133
- return features.sort((a, b) => a.name.localeCompare(b.name));
134
- }
135
-
136
- /**
137
- * Count roles for files in a feature
138
- *
139
- * @param {Object} store - N3 Store
140
- * @param {Array} fileQuads - File quads belonging to feature
141
- * @returns {Object} Role counts
142
- */
143
- function countRolesForFeature(store, fileQuads) {
144
- const counts = {
145
- component: 0,
146
- page: 0,
147
- api: 0,
148
- schema: 0,
149
- test: 0,
150
- doc: 0,
151
- hook: 0,
152
- service: 0,
153
- state: 0,
154
- config: 0,
155
- other: 0,
156
- };
157
-
158
- for (const fq of fileQuads) {
159
- const roleQuads = store.getQuads(fq.subject, namedNode(`${NS.proj}roleString`), null);
160
-
161
- for (const rq of roleQuads) {
162
- const role = rq.object.value.toLowerCase();
163
- if (counts[role] !== undefined) {
164
- counts[role]++;
165
- } else {
166
- counts.other++;
167
- }
168
- }
169
- }
170
-
171
- return counts;
172
- }
173
-
174
- /**
175
- * Build stack profile string
176
- *
177
- * @param {Object} store - N3 Store
178
- * @returns {string}
179
- */
180
- function buildStackProfile(store) {
181
- const parts = [];
182
-
183
- const stackQuads = store.getQuads(null, namedNode(`${NS.proj}hasStack`), null);
184
-
185
- if (stackQuads.length > 0) {
186
- for (const sq of stackQuads) {
187
- parts.push(sq.object.value);
188
- }
189
- }
190
-
191
- if (parts.length === 0) {
192
- const filePaths = new Set();
193
- const pathQuads = store.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
194
- for (const pq of pathQuads) {
195
- filePaths.add(pq.object.value);
196
- }
197
-
198
- if (filePaths.has('next.config.js') || filePaths.has('next.config.mjs')) {
199
- if (filePaths.has('src/app') || [...filePaths].some(p => p.startsWith('src/app/'))) {
200
- parts.push('react-next-app-router');
201
- } else {
202
- parts.push('react-next-pages');
203
- }
204
- }
205
- if (filePaths.has('nest-cli.json')) {
206
- parts.push('nest');
207
- }
208
- if (filePaths.has('vitest.config.js') || filePaths.has('vitest.config.mjs')) {
209
- parts.push('vitest');
210
- } else if (filePaths.has('jest.config.js')) {
211
- parts.push('jest');
212
- }
213
- }
214
-
215
- return parts.join(' + ') || 'unknown';
216
- }
217
-
218
- /**
219
- * Compute project statistics
220
- *
221
- * @param {Object} store - N3 Store
222
- * @param {FeatureReport[]} features - Feature reports
223
- * @returns {ProjectStats}
224
- */
225
- function computeStats(store, features) {
226
- const roleQuads = store.getQuads(null, namedNode(`${NS.proj}roleString`), null);
227
-
228
- const filesByRole = {};
229
- for (const rq of roleQuads) {
230
- const role = rq.object.value;
231
- filesByRole[role] = (filesByRole[role] || 0) + 1;
232
- }
233
-
234
- const pathQuads = store.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
235
- const totalFiles = pathQuads.length;
236
-
237
- const coverages = features.filter(f => f.fileCount > 0).map(f => f.testCoverage);
238
- const testCoverageAverage =
239
- coverages.length > 0 ? Math.round(coverages.reduce((a, b) => a + b, 0) / coverages.length) : 0;
240
-
241
- return {
242
- featureCount: features.length,
243
- totalFiles,
244
- testCoverageAverage,
245
- filesByRole,
246
- };
247
- }
248
-
249
- /**
250
- * Extract domain entities from features
251
- *
252
- * @param {Object} store - N3 Store
253
- * @param {FeatureReport[]} features - Feature reports
254
- * @returns {DomainEntity[]}
255
- */
256
- function extractDomainEntities(store, features) {
257
- const entities = [];
258
-
259
- const allRoleQuads = store.getQuads(null, namedNode(`${NS.proj}roleString`), null);
260
- const schemaQuads = allRoleQuads.filter(q => q.object.value === 'Schema');
261
-
262
- for (const sq of schemaQuads) {
263
- const pathQuads = store.getQuads(sq.subject, namedNode(`${NS.fs}relativePath`), null);
264
-
265
- if (pathQuads.length > 0) {
266
- const filePath = pathQuads[0].object.value;
267
- const nameMatch = filePath.match(/([A-Z][a-z]+(?:[A-Z][a-z]+)*)/);
268
- const name = nameMatch ? nameMatch[1] : extractNameFromPath(filePath);
269
-
270
- const featureMatch = features.find(
271
- f => filePath.includes(f.name) || f.name.toLowerCase() === name.toLowerCase()
272
- );
273
-
274
- entities.push({
275
- name,
276
- fieldCount: 8,
277
- hasView: featureMatch?.roles.view || false,
278
- hasApi: featureMatch?.roles.api || false,
279
- });
280
- }
281
- }
282
-
283
- const seen = new Set();
284
- return entities.filter(e => {
285
- if (seen.has(e.name)) return false;
286
- seen.add(e.name);
287
- return true;
288
- });
289
- }
290
-
291
- /**
292
- * Generate human-readable summary
293
- *
294
- * @param {FeatureReport[]} features - Feature reports
295
- * @param {ProjectStats} stats - Stats
296
- * @param {string} stackProfile - Stack profile
297
- * @returns {string}
298
- */
299
- function generateSummary(features, stats, _stackProfile) {
300
- const parts = [];
301
-
302
- const structure =
303
- stats.featureCount > 10 ? 'Large' : stats.featureCount > 5 ? 'Well-structured' : 'Compact';
304
- parts.push(`${structure} ${stats.featureCount}-feature project`);
305
-
306
- if (stats.testCoverageAverage > 0) {
307
- parts[0] += ` with ${stats.testCoverageAverage}% test coverage`;
308
- }
309
-
310
- const missingTests = features.filter(f => f.hasMissingTests).map(f => f.name);
311
- if (missingTests.length > 0 && missingTests.length <= 3) {
312
- parts.push(`Missing tests: ${missingTests.join(', ')}`);
313
- } else if (missingTests.length > 3) {
314
- parts.push(
315
- `Missing tests: ${missingTests.slice(0, 2).join(', ')} and ${missingTests.length - 2} more`
316
- );
317
- }
318
-
319
- const missingDocs = features.filter(f => !f.roles.doc && f.fileCount > 3).map(f => f.name);
320
- if (missingDocs.length > 0 && missingDocs.length <= 2) {
321
- parts.push(`Consider docs for: ${missingDocs.join(', ')}`);
322
- }
323
-
324
- return parts.join('. ') + '.';
325
- }
326
-
327
- /**
328
- * Extract name from IRI
329
- *
330
- * @param {string} iri - Feature IRI
331
- * @returns {string}
332
- */
333
- function extractNameFromIri(iri) {
334
- const match = iri.match(/\/([^/]+)$/);
335
- return match ? decodeURIComponent(match[1]) : 'unknown';
336
- }
337
-
338
- /**
339
- * Extract name from file path
340
- *
341
- * @param {string} filePath - File path
342
- * @returns {string}
343
- */
344
- function extractNameFromPath(filePath) {
345
- const parts = filePath.split('/');
346
- const fileName = parts[parts.length - 1];
347
- return fileName.replace(/\.(tsx?|jsx?|mjs|json)$/, '');
348
- }