@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
|
@@ -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
|
-
}
|
package/src/project-model.mjs
DELETED
|
@@ -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
|
-
}
|
package/src/project-report.mjs
DELETED
|
@@ -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
|
-
}
|