@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,412 @@
1
+ /**
2
+ * @file Hotspot analyzer - identify high-risk features by complexity metrics
3
+ * @module project-engine/hotspot-analyzer
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 NS = {
12
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
14
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
15
+ fs: 'http://example.org/unrdf/filesystem#',
16
+ proj: 'http://example.org/unrdf/project#',
17
+ };
18
+
19
+ /**
20
+ * Scoring weights for hotspot calculation
21
+ * @type {{fileCount: number, testCoverage: number, dependencies: number, complexity: number}}
22
+ */
23
+ const SCORING_WEIGHTS = {
24
+ fileCount: 0.3,
25
+ testCoverage: 0.4,
26
+ dependencies: 0.2,
27
+ complexity: 0.1,
28
+ };
29
+
30
+ /**
31
+ * Risk thresholds
32
+ */
33
+ const RISK_THRESHOLDS = {
34
+ HIGH: 70,
35
+ MEDIUM: 40,
36
+ LOW: 0,
37
+ };
38
+
39
+ const HotspotOptionsSchema = z.object({
40
+ projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
41
+ message: 'projectStore must be an RDF store with getQuads method',
42
+ }),
43
+ domainStore: z
44
+ .custom(val => val && typeof val.getQuads === 'function', {
45
+ message: 'domainStore must be an RDF store with getQuads method',
46
+ })
47
+ .optional(),
48
+ stackProfile: z
49
+ .object({
50
+ testFramework: z.string().nullable().optional(),
51
+ sourceRoot: z.string().optional(),
52
+ })
53
+ .optional(),
54
+ baseIri: z.string().default('http://example.org/unrdf/hotspot#'),
55
+ });
56
+
57
+ const FeatureMetricsSchema = z.object({
58
+ fileCount: z.number().min(0),
59
+ lineCount: z.number().min(0),
60
+ testCount: z.number().min(0),
61
+ testCoverage: z.number().min(0).max(100),
62
+ dependencies: z.number().min(0),
63
+ });
64
+
65
+ /**
66
+ * @typedef {Object} FeatureMetrics
67
+ * @property {number} fileCount - Total files in the feature
68
+ * @property {number} lineCount - Approximate line count (from byte size)
69
+ * @property {number} testCount - Number of test files
70
+ * @property {number} testCoverage - Test coverage percentage (testCount / fileCount * 100)
71
+ * @property {number} dependencies - Number of dependencies/relationships
72
+ */
73
+
74
+ /**
75
+ * @typedef {Object} HotspotEntry
76
+ * @property {string} feature - Feature name
77
+ * @property {number} score - Hotspot score (0-100)
78
+ * @property {'HIGH'|'MEDIUM'|'LOW'} risk - Risk level
79
+ * @property {FeatureMetrics} metrics - Detailed metrics
80
+ * @property {string} recommendation - Actionable recommendation
81
+ */
82
+
83
+ /**
84
+ * @typedef {Object} HotspotResult
85
+ * @property {HotspotEntry[]} hotspots - All features with scores
86
+ * @property {{feature: string, score: number, reason: string}[]} topRisks - Top risk features
87
+ * @property {string} summary - Human-readable summary
88
+ */
89
+
90
+ /**
91
+ * Analyze project for hotspots - high-risk or high-complexity features
92
+ *
93
+ * @param {Object} options
94
+ * @param {Store} options.projectStore - Project model store (with features, file roles)
95
+ * @param {Store} [options.domainStore] - Domain model store (optional, for relationships)
96
+ * @param {Object} [options.stackProfile] - Stack information
97
+ * @param {string} [options.baseIri] - Base IRI for hotspot resources
98
+ * @returns {HotspotResult} Hotspot analysis results
99
+ */
100
+ export function analyzeHotspots(options) {
101
+ const validated = HotspotOptionsSchema.parse(options);
102
+ const { projectStore, domainStore, _baseIri } = validated;
103
+
104
+ // Extract all features from the project store
105
+ const features = extractFeatures(projectStore);
106
+
107
+ // Calculate metrics for each feature
108
+ const hotspots = [];
109
+ for (const [featureName, featureData] of Object.entries(features)) {
110
+ const metrics = calculateFeatureMetrics(featureName, featureData, projectStore, domainStore);
111
+ const score = scoreFeature(featureName, metrics);
112
+ const risk = getRiskLevel(score);
113
+ const recommendation = generateRecommendation(featureName, metrics, risk);
114
+
115
+ hotspots.push({
116
+ feature: featureName,
117
+ score,
118
+ risk,
119
+ metrics,
120
+ recommendation,
121
+ });
122
+ }
123
+
124
+ // Sort by score descending (highest risk first)
125
+ hotspots.sort((a, b) => b.score - a.score);
126
+
127
+ // Extract top risks (score > 40 or top 5)
128
+ const topRisks = hotspots
129
+ .filter(h => h.score > RISK_THRESHOLDS.MEDIUM)
130
+ .slice(0, 5)
131
+ .map(h => ({
132
+ feature: h.feature,
133
+ score: h.score,
134
+ reason: `${h.metrics.fileCount} files, ${h.metrics.testCoverage}% coverage`,
135
+ }));
136
+
137
+ // Generate summary
138
+ const highRiskCount = hotspots.filter(h => h.risk === 'HIGH').length;
139
+ const summary =
140
+ highRiskCount > 0
141
+ ? `${highRiskCount} high-risk feature${highRiskCount > 1 ? 's' : ''} identified`
142
+ : 'No high-risk features identified';
143
+
144
+ return {
145
+ hotspots,
146
+ topRisks,
147
+ summary,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Score a feature based on its metrics
153
+ *
154
+ * Formula:
155
+ * - File count: 30% weight - more files = higher score
156
+ * - Test coverage: 40% weight - LESS coverage = higher score
157
+ * - Dependencies: 20% weight - more deps = higher score
158
+ * - Complexity: 10% weight - larger files = higher score
159
+ *
160
+ * @param {string} feature - Feature name
161
+ * @param {FeatureMetrics} metrics - Feature metrics
162
+ * @returns {number} Score 0-100 (higher = more risk)
163
+ */
164
+ export function scoreFeature(feature, metrics) {
165
+ const validatedMetrics = FeatureMetricsSchema.parse(metrics);
166
+
167
+ // File count score: normalized to 0-100 (40 files = 100)
168
+ const fileCountScore = Math.min(100, (validatedMetrics.fileCount / 40) * 100);
169
+
170
+ // Test coverage score: inverted (less coverage = higher score)
171
+ const testCoverageScore = 100 - validatedMetrics.testCoverage;
172
+
173
+ // Dependencies score: normalized to 0-100 (15 deps = 100)
174
+ const dependencyScore = Math.min(100, (validatedMetrics.dependencies / 15) * 100);
175
+
176
+ // Complexity score: based on average lines per file (200 lines/file = 100)
177
+ const avgLinesPerFile =
178
+ validatedMetrics.fileCount > 0 ? validatedMetrics.lineCount / validatedMetrics.fileCount : 0;
179
+ const complexityScore = Math.min(100, (avgLinesPerFile / 200) * 100);
180
+
181
+ // Weighted average
182
+ const score =
183
+ fileCountScore * SCORING_WEIGHTS.fileCount +
184
+ testCoverageScore * SCORING_WEIGHTS.testCoverage +
185
+ dependencyScore * SCORING_WEIGHTS.dependencies +
186
+ complexityScore * SCORING_WEIGHTS.complexity;
187
+
188
+ return Math.round(score);
189
+ }
190
+
191
+ /**
192
+ * Extract features from project store
193
+ *
194
+ * @private
195
+ * @param {Store} projectStore
196
+ * @returns {Object<string, {iri: string, files: string[]}>}
197
+ */
198
+ function extractFeatures(projectStore) {
199
+ const features = {};
200
+
201
+ // Get all feature IRIs
202
+ const featureQuads = projectStore.getQuads(
203
+ null,
204
+ namedNode(`${NS.rdf}type`),
205
+ namedNode(`${NS.proj}Feature`)
206
+ );
207
+
208
+ for (const quad of featureQuads) {
209
+ const featureIri = quad.subject.value;
210
+
211
+ // Get feature label
212
+ const labelQuads = projectStore.getQuads(quad.subject, namedNode(`${NS.rdfs}label`), null);
213
+ const featureName =
214
+ labelQuads.length > 0 ? labelQuads[0].object.value : extractNameFromIri(featureIri);
215
+
216
+ // Get files belonging to this feature
217
+ const fileQuads = projectStore.getQuads(
218
+ null,
219
+ namedNode(`${NS.proj}belongsToFeature`),
220
+ quad.subject
221
+ );
222
+
223
+ const files = fileQuads.map(fq => {
224
+ const pathQuads = projectStore.getQuads(fq.subject, namedNode(`${NS.fs}relativePath`), null);
225
+ return pathQuads.length > 0 ? pathQuads[0].object.value : fq.subject.value;
226
+ });
227
+
228
+ features[featureName] = {
229
+ iri: featureIri,
230
+ files,
231
+ };
232
+ }
233
+
234
+ return features;
235
+ }
236
+
237
+ /**
238
+ * Calculate metrics for a feature
239
+ *
240
+ * @private
241
+ * @param {string} featureName
242
+ * @param {{iri: string, files: string[]}} featureData
243
+ * @param {Store} projectStore
244
+ * @param {Store} [domainStore]
245
+ * @returns {FeatureMetrics}
246
+ */
247
+ function calculateFeatureMetrics(featureName, featureData, projectStore, domainStore) {
248
+ const files = featureData.files;
249
+ const fileCount = files.length;
250
+
251
+ // Count test files (files with Test role or .test./.spec. in name)
252
+ let testCount = 0;
253
+ let totalByteSize = 0;
254
+
255
+ for (const filePath of files) {
256
+ // Check if test file
257
+ if (isTestFile(filePath, projectStore)) {
258
+ testCount++;
259
+ }
260
+
261
+ // Get byte size for line count approximation
262
+ const fileIri = namedNode(`http://example.org/unrdf/fs#${encodeURIComponent(filePath)}`);
263
+ const sizeQuads = projectStore.getQuads(fileIri, namedNode(`${NS.fs}byteSize`), null);
264
+ if (sizeQuads.length > 0) {
265
+ totalByteSize += parseInt(sizeQuads[0].object.value, 10) || 0;
266
+ }
267
+ }
268
+
269
+ // Approximate line count (assume ~40 bytes per line average)
270
+ const lineCount = Math.round(totalByteSize / 40);
271
+
272
+ // Test coverage: percentage of test files
273
+ const testCoverage = fileCount > 0 ? Math.round((testCount / fileCount) * 100) : 100; // No files = 100% coverage (nothing to test)
274
+
275
+ // Count dependencies from domain store or project relationships
276
+ const dependencies = countDependencies(featureData, projectStore, domainStore);
277
+
278
+ return {
279
+ fileCount,
280
+ lineCount,
281
+ testCount,
282
+ testCoverage,
283
+ dependencies,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Check if a file is a test file
289
+ *
290
+ * @private
291
+ * @param {string} filePath
292
+ * @param {Store} projectStore
293
+ * @returns {boolean}
294
+ */
295
+ function isTestFile(filePath, projectStore) {
296
+ // Check by file path pattern
297
+ if (/\.(test|spec)\.(tsx?|jsx?|mjs)$/.test(filePath)) {
298
+ return true;
299
+ }
300
+ if (/^(test|tests|__tests__|spec)\//.test(filePath)) {
301
+ return true;
302
+ }
303
+
304
+ // Check by role in store
305
+ const fileIri = namedNode(`http://example.org/unrdf/fs#${encodeURIComponent(filePath)}`);
306
+ const roleQuads = projectStore.getQuads(fileIri, namedNode(`${NS.proj}roleString`), null);
307
+
308
+ return roleQuads.some(q => q.object.value === 'Test');
309
+ }
310
+
311
+ /**
312
+ * Count dependencies for a feature
313
+ *
314
+ * @private
315
+ * @param {{iri: string, files: string[]}} featureData
316
+ * @param {Store} projectStore
317
+ * @param {Store} [domainStore]
318
+ * @returns {number}
319
+ */
320
+ function countDependencies(featureData, projectStore, domainStore) {
321
+ let deps = 0;
322
+
323
+ // Count relationships from project store
324
+ const featureIri = namedNode(featureData.iri);
325
+ const relQuads = projectStore.getQuads(featureIri, null, null);
326
+
327
+ // Count outgoing relationships (excluding type and label)
328
+ for (const quad of relQuads) {
329
+ if (quad.predicate.value !== `${NS.rdf}type` && quad.predicate.value !== `${NS.rdfs}label`) {
330
+ deps++;
331
+ }
332
+ }
333
+
334
+ // If domain store provided, count entity relationships
335
+ if (domainStore) {
336
+ const domainRels = domainStore.getQuads(
337
+ null,
338
+ namedNode('http://example.org/unrdf/domain#relatesTo'),
339
+ null
340
+ );
341
+ // Count unique relationships (simplified - just total)
342
+ deps += Math.min(domainRels.length, 20);
343
+ }
344
+
345
+ return deps;
346
+ }
347
+
348
+ /**
349
+ * Get risk level from score
350
+ *
351
+ * @private
352
+ * @param {number} score
353
+ * @returns {'HIGH'|'MEDIUM'|'LOW'}
354
+ */
355
+ function getRiskLevel(score) {
356
+ if (score >= RISK_THRESHOLDS.HIGH) return 'HIGH';
357
+ if (score >= RISK_THRESHOLDS.MEDIUM) return 'MEDIUM';
358
+ return 'LOW';
359
+ }
360
+
361
+ /**
362
+ * Generate recommendation based on metrics and risk
363
+ *
364
+ * @private
365
+ * @param {string} featureName
366
+ * @param {FeatureMetrics} metrics
367
+ * @param {'HIGH'|'MEDIUM'|'LOW'} risk
368
+ * @returns {string}
369
+ */
370
+ function generateRecommendation(featureName, metrics, risk) {
371
+ const issues = [];
372
+
373
+ if (metrics.fileCount > 30) {
374
+ issues.push('high file count');
375
+ }
376
+ if (metrics.testCoverage < 50) {
377
+ issues.push('low test coverage');
378
+ }
379
+ if (metrics.dependencies > 10) {
380
+ issues.push('many dependencies');
381
+ }
382
+ if (metrics.lineCount > 5000) {
383
+ issues.push('large codebase');
384
+ }
385
+
386
+ if (issues.length === 0) {
387
+ return 'Feature is within acceptable complexity thresholds.';
388
+ }
389
+
390
+ const issueStr = issues.join(' + ');
391
+ const action =
392
+ risk === 'HIGH'
393
+ ? 'Add tests or refactor.'
394
+ : risk === 'MEDIUM'
395
+ ? 'Consider adding tests.'
396
+ : 'Monitor for growth.';
397
+
398
+ return `${issueStr.charAt(0).toUpperCase() + issueStr.slice(1)}. ${action}`;
399
+ }
400
+
401
+ /**
402
+ * Extract name from IRI
403
+ *
404
+ * @private
405
+ * @param {string} iri
406
+ * @returns {string}
407
+ */
408
+ function extractNameFromIri(iri) {
409
+ const parts = iri.split(/[#/]/);
410
+ const last = parts[parts.length - 1];
411
+ return decodeURIComponent(last);
412
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @file Project engine exports
3
+ * @module project-engine
4
+ */
5
+
6
+ export { scanFileSystemToStore } from './fs-scan.mjs';
7
+ export { buildProjectModelFromFs } from './project-model.mjs';
8
+ export { detectStackFromFs } from './stack-detect.mjs';
9
+ export { classifyFiles } from './file-roles.mjs';
10
+ // Removed unused export: generateGoldenStructure (never imported/used)
11
+ export { diffProjectStructure } from './project-diff.mjs';
12
+ export { materializeArtifacts } from './materialize.mjs';
13
+ export { ProjectStructureLens } from './lens/project-structure.mjs';
14
+ export { getProjectEngineConfig, ProjectEngineConfigSchema } from './project-config.mjs';
15
+ export { buildProjectReport } from './project-report.mjs';
16
+ export { createProjectInitializationPipeline } from './initialize.mjs';
17
+ export {
18
+ deriveHooksFromStructure,
19
+ analyzePatternViolations,
20
+ createCustomPatternHook,
21
+ } from './policy-derivation.mjs';
22
+ export { inferDomainModel, inferDomainModelFromPath, DomainModelLens } from './domain-infer.mjs';
23
+ export {
24
+ inferTemplatesFromProject,
25
+ inferTemplatesWithDomainBinding,
26
+ getTemplatesByKind,
27
+ serializeTemplates,
28
+ } from './template-infer.mjs';
29
+
30
+ // Materialization planning and execution
31
+ export {
32
+ planMaterialization,
33
+ validatePlan,
34
+ createEmptyPlan,
35
+ mergePlans,
36
+ } from './materialize-plan.mjs';
37
+
38
+ export {
39
+ applyMaterializationPlan,
40
+ rollbackMaterialization,
41
+ previewPlan,
42
+ checkPlanApplicability,
43
+ } from './materialize-apply.mjs';
44
+
45
+ // Drift detection
46
+ export {
47
+ createStructureSnapshot,
48
+ computeDrift,
49
+ createEmptyBaseline,
50
+ serializeSnapshot,
51
+ deserializeSnapshot,
52
+ } from './drift-snapshot.mjs';
53
+
54
+ // Hotspot analysis
55
+ export { analyzeHotspots, scoreFeature } from './hotspot-analyzer.mjs';
56
+
57
+ // Gap detection
58
+ export { findMissingRoles, scoreMissingRole } from './gap-finder.mjs';
59
+
60
+ // Type-safety auditing
61
+ export {
62
+ auditTypeConsistency,
63
+ auditEntityTypes,
64
+ compareTypes,
65
+ FieldInfoSchema,
66
+ MismatchSchema,
67
+ AuditResultSchema,
68
+ CompareTypesResultSchema,
69
+ } from './type-auditor.mjs';
70
+
71
+ // Autonomic MAPEK Loop - Full autonomics with Knowledge Hooks
72
+ export {
73
+ runMapekIteration,
74
+ createAutonomicHooks,
75
+ runContinuousMapekLoop,
76
+ reportMapekStatus,
77
+ } from './autonomic-mapek.mjs';
78
+
79
+ // API Contract Validation
80
+ export {
81
+ generateAPISchema,
82
+ generateAllAPISchemas,
83
+ validateAPIFiles,
84
+ detectContractBreaks,
85
+ detectAllContractBreaks,
86
+ FieldSchemaSchema,
87
+ EntitySchemaSchema,
88
+ ViolationSchema,
89
+ ValidationResultSchema,
90
+ BreakingChangeSchema,
91
+ ContractBreaksSchema,
92
+ } from './api-contract-validator.mjs';
93
+
94
+ // Stack-Aware Linter Rules
95
+ export { deriveLinterRules, analyzeCodePatterns, generateESLintConfig } from './stack-linter.mjs';
96
+
97
+ // Removed unused exports: planEntityRename, planEntityMerge, planServiceExtraction, validateRefactoringPlan (never imported/used)
98
+
99
+ // JavaScript Code Complexity Analysis
100
+ export { analyzeJsComplexity } from './code-complexity-js.mjs';
101
+
102
+ // Capabilities Manifest - Feature flags and metadata
103
+ export {
104
+ CODE_COMPLEXITY_JS,
105
+ CAPABILITIES,
106
+ FEATURE_FLAGS,
107
+ isCapabilityEnabled,
108
+ getCapabilityMetadata,
109
+ getEnabledCapabilities,
110
+ setCapabilityEnabled,
111
+ } from './capabilities-manifest.mjs';
112
+
113
+ // Feature Dependency Graph
114
+ export {
115
+ buildDependencyGraph,
116
+ detectCircularDependencies,
117
+ topologicalSort,
118
+ analyzeDependencyPath,
119
+ getTransitiveDependencies,
120
+ // Removed unused export: getTransitiveDependents (never imported/used, getTransitiveDependencies is used)
121
+ calculateImpactScore,
122
+ } from './dependency-graph.mjs';
123
+
124
+ // Auto-Test Generator
125
+ export {
126
+ inferTestPatterns,
127
+ generateTestSkeleton,
128
+ scoreTestCoverage,
129
+ generateTestFactory,
130
+ } from './auto-test-generator.mjs';
131
+
132
+ // Removed unused exports: generateEntityReference, generateAPIReference, generateArchitectureDiagram, generateCompleteDocumentation (never imported/used)
133
+ // Kept DocGenerationResultSchema as it may be used by checkDocConsistency
134
+ export { DocGenerationResultSchema } from './doc-generator.mjs';
135
+
136
+ // Documentation Drift Checker - validate documentation consistency against domain model
137
+ export {
138
+ checkDocConsistency,
139
+ extractDocReferences,
140
+ scoreDocDrift,
141
+ checkDocDrift,
142
+ DriftEntrySchema,
143
+ } from './doc-drift-checker.mjs';
144
+
145
+ // MAPEK Orchestration - Unified execution with all innovations
146
+ export {
147
+ runFullMapekWithAllInnovations,
148
+ runInnovationsParallel,
149
+ aggregateInnovationFindings,
150
+ ALL_INNOVATIONS,
151
+ } from './mapek-orchestration.mjs';