@unrdf/project-engine 5.0.1 → 26.4.3

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 +6 -5
  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,444 +0,0 @@
1
- /**
2
- * @file Auto Test Generator - generates test suggestions from code analysis
3
- * @module project-engine/auto-test-generator
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 NS = {
12
- rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
- rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
14
- fs: 'http://example.org/unrdf/filesystem#',
15
- proj: 'http://example.org/unrdf/project#',
16
- dom: 'http://example.org/unrdf/domain#',
17
- };
18
-
19
- const InferTestPatternsOptionsSchema = z.object({
20
- fsStore: z
21
- .custom(val => val && typeof val.getQuads === 'function', {
22
- message: 'fsStore must be an RDF store with getQuads method',
23
- })
24
- .optional(),
25
- projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
26
- message: 'projectStore must be an RDF store with getQuads method',
27
- }),
28
- });
29
-
30
- const AutoTestOptionsSchema = z.object({
31
- projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
32
- message: 'projectStore must be an RDF store with getQuads method',
33
- }),
34
- domainStore: z
35
- .custom(val => val && typeof val.getQuads === 'function', {
36
- message: 'domainStore must be an RDF store with getQuads method',
37
- })
38
- .optional(),
39
- stackProfile: z
40
- .object({
41
- testFramework: z.string().nullable().optional(),
42
- })
43
- .passthrough()
44
- .optional(),
45
- });
46
-
47
- const TestSuggestionSchema = z.object({
48
- file: z.string(),
49
- testFile: z.string(),
50
- testType: z.enum(['unit', 'integration', 'e2e']),
51
- priority: z.enum(['critical', 'high', 'medium', 'low']),
52
- reason: z.string(),
53
- suggestedTests: z.array(z.string()),
54
- });
55
-
56
- /**
57
- * Generate test suggestions based on code analysis
58
- * @param {Object} options
59
- * @param {Store} options.projectStore - Project RDF store
60
- * @param {Store} [options.domainStore] - Domain model store
61
- * @param {Object} [options.stackProfile] - Stack profile
62
- * @returns {{ suggestions: Array, summary: string, coverage: number }}
63
- */
64
- export function generateTestSuggestions(options) {
65
- const validated = AutoTestOptionsSchema.parse(options);
66
- const { projectStore, stackProfile } = validated;
67
-
68
- const suggestions = [];
69
- const fileQuads = projectStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
70
-
71
- let totalFiles = 0;
72
- let filesWithTests = 0;
73
-
74
- for (const quad of fileQuads) {
75
- const filePath = quad.object.value;
76
- if (isTestFile(filePath) || isConfigFile(filePath) || !isSourceFile(filePath)) continue;
77
-
78
- totalFiles++;
79
- const testPath = generateTestPath(filePath, stackProfile);
80
- const hasTest = hasExistingTest(filePath, projectStore);
81
-
82
- if (hasTest) {
83
- filesWithTests++;
84
- continue;
85
- }
86
-
87
- const roleQuads = projectStore.getQuads(quad.subject, namedNode(`${NS.proj}roleString`), null);
88
- const role = roleQuads.length > 0 ? roleQuads[0].object.value : 'Unknown';
89
- const { testType, priority } = determineTestType(filePath, role);
90
- const suggestedTests = generateSuggestedTestCases(filePath, role);
91
-
92
- suggestions.push({
93
- file: filePath,
94
- testFile: testPath,
95
- testType,
96
- priority,
97
- reason: `${role} file without test`,
98
- suggestedTests,
99
- });
100
- }
101
-
102
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
103
- suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
104
-
105
- const coverage = totalFiles > 0 ? Math.round((filesWithTests / totalFiles) * 100) : 100;
106
- const summary =
107
- suggestions.length > 0
108
- ? `${suggestions.length} files need tests (${coverage}% coverage)`
109
- : `All files have tests (${coverage}% coverage)`;
110
-
111
- return { suggestions, summary, coverage };
112
- }
113
-
114
- function isTestFile(filePath) {
115
- return (
116
- /\.(test|spec)\.(tsx?|jsx?|mjs)$/.test(filePath) ||
117
- /^(test|tests|__tests__|spec)\//.test(filePath)
118
- );
119
- }
120
-
121
- function isConfigFile(filePath) {
122
- return /\.(config|rc)\.(tsx?|jsx?|mjs|json)$/.test(filePath);
123
- }
124
-
125
- function isSourceFile(filePath) {
126
- return /\.(tsx?|jsx?|mjs)$/.test(filePath);
127
- }
128
-
129
- function generateTestPath(filePath, _stackProfile) {
130
- const ext = filePath.match(/\.(tsx?|jsx?|mjs)$/)?.[1] || 'mjs';
131
- const baseName = filePath.replace(/\.(tsx?|jsx?|mjs)$/, '');
132
- return `${baseName}.test.${ext}`;
133
- }
134
-
135
- function hasExistingTest(filePath, projectStore) {
136
- const baseName = filePath.replace(/\.(tsx?|jsx?|mjs)$/, '');
137
- const testPatterns = [`${baseName}.test.`, `${baseName}.spec.`];
138
- const allPaths = projectStore
139
- .getQuads(null, namedNode(`${NS.fs}relativePath`), null)
140
- .map(q => q.object.value);
141
- return testPatterns.some(pattern => allPaths.some(p => p.includes(pattern)));
142
- }
143
-
144
- function determineTestType(filePath, role) {
145
- const roleMap = {
146
- Api: { testType: 'integration', priority: 'critical' },
147
- Route: { testType: 'integration', priority: 'critical' },
148
- Service: { testType: 'unit', priority: 'high' },
149
- Schema: { testType: 'unit', priority: 'high' },
150
- Component: { testType: 'unit', priority: 'medium' },
151
- };
152
- return roleMap[role] || { testType: 'unit', priority: 'medium' };
153
- }
154
-
155
- function generateSuggestedTestCases(filePath, role) {
156
- const baseName = filePath
157
- .split('/')
158
- .pop()
159
- ?.replace(/\.(tsx?|jsx?|mjs)$/, '');
160
- const tests =
161
- role === 'Api' || role === 'Route'
162
- ? [
163
- 'should handle GET requests',
164
- 'should handle POST requests',
165
- 'should return 400 for invalid input',
166
- ]
167
- : ['should work correctly with valid input', 'should handle edge cases'];
168
- return tests.map(t => `${baseName}: ${t}`);
169
- }
170
-
171
- /**
172
- * Infer test patterns from existing test files
173
- * @param {Object} options
174
- * @param {import('n3').Store} [options.fsStore] - Filesystem RDF store
175
- * @param {import('n3').Store} options.projectStore - Project RDF store
176
- * @returns {Object} Test patterns object
177
- */
178
- export function inferTestPatterns(options) {
179
- const validated = InferTestPatternsOptionsSchema.parse(options);
180
- const { fsStore, _projectStore } = validated;
181
-
182
- // Default patterns
183
- const defaultPatterns = {
184
- testFramework: 'vitest',
185
- fileExtension: 'mjs',
186
- testSuffix: 'test',
187
- assertionPatterns: ['toBe', 'toEqual', 'toBeDefined', 'toContain'],
188
- describeBlocks: [],
189
- setupTeardown: {
190
- hasBeforeEach: false,
191
- hasAfterEach: false,
192
- hasBeforeAll: false,
193
- hasAfterAll: false,
194
- },
195
- imports: [],
196
- };
197
-
198
- // If no fsStore, return defaults
199
- if (!fsStore) {
200
- return defaultPatterns;
201
- }
202
-
203
- // Find test files
204
- const testFileQuads = fsStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
205
-
206
- const testFiles = testFileQuads
207
- .map(q => ({
208
- path: q.object.value,
209
- subject: q.subject,
210
- }))
211
- .filter(f => isTestFile(f.path));
212
-
213
- // If no test files found, return defaults
214
- if (testFiles.length === 0) {
215
- return defaultPatterns;
216
- }
217
-
218
- // Analyze first test file content
219
- const firstTestFile = testFiles[0];
220
- const contentQuads = fsStore.getQuads(firstTestFile.subject, namedNode(`${NS.fs}content`), null);
221
-
222
- if (contentQuads.length === 0) {
223
- return defaultPatterns;
224
- }
225
-
226
- const content = contentQuads[0].object.value;
227
- const patterns = { ...defaultPatterns };
228
-
229
- // Detect test framework
230
- if (content.includes("from 'vitest'") || content.includes('from "vitest"')) {
231
- patterns.testFramework = 'vitest';
232
- } else if (content.includes("from 'jest'") || content.includes('from "jest"')) {
233
- patterns.testFramework = 'jest';
234
- } else if (content.includes("from 'mocha'") || content.includes('from "mocha"')) {
235
- patterns.testFramework = 'mocha';
236
- }
237
-
238
- // Detect file extension
239
- const extMatch = firstTestFile.path.match(/\.(tsx?|jsx?|mjs)$/);
240
- if (extMatch) {
241
- patterns.fileExtension = extMatch[1];
242
- }
243
-
244
- // Detect test suffix
245
- if (firstTestFile.path.includes('.spec.')) {
246
- patterns.testSuffix = 'spec';
247
- }
248
-
249
- // Extract describe blocks
250
- const describeMatches = content.matchAll(/describe\(['"]([^'"]+)['"]/g);
251
- for (const match of describeMatches) {
252
- patterns.describeBlocks.push(match[1]);
253
- }
254
-
255
- // Detect assertion patterns
256
- const assertionPatterns = [
257
- 'toBe',
258
- 'toEqual',
259
- 'toBeDefined',
260
- 'toBeUndefined',
261
- 'toBeNull',
262
- 'toBeTruthy',
263
- 'toBeFalsy',
264
- 'toContain',
265
- 'toHaveLength',
266
- 'toThrow',
267
- 'toMatch',
268
- ];
269
- for (const pattern of assertionPatterns) {
270
- if (content.includes(`.${pattern}(`)) {
271
- if (!patterns.assertionPatterns.includes(pattern)) {
272
- patterns.assertionPatterns.push(pattern);
273
- }
274
- }
275
- }
276
-
277
- // Detect setup/teardown
278
- patterns.setupTeardown.hasBeforeEach = content.includes('beforeEach');
279
- patterns.setupTeardown.hasAfterEach = content.includes('afterEach');
280
- patterns.setupTeardown.hasBeforeAll = content.includes('beforeAll');
281
- patterns.setupTeardown.hasAfterAll = content.includes('afterAll');
282
-
283
- // Extract imports
284
- const importMatches = content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g);
285
- for (const match of importMatches) {
286
- if (!patterns.imports.includes(match[1])) {
287
- patterns.imports.push(match[1]);
288
- }
289
- }
290
-
291
- return patterns;
292
- }
293
-
294
- /**
295
- * Generate test skeleton from entity and patterns
296
- * @param {Object} options
297
- * @param {string} options.entity - Entity name
298
- * @param {Object} options.existingTestPatterns - Test patterns from inferTestPatterns
299
- * @param {import('n3').Store} [options.domainStore] - Domain model store
300
- * @returns {Object} Test skeleton object
301
- */
302
- export function generateTestSkeleton(options) {
303
- const { entity, existingTestPatterns, domainStore } = options;
304
-
305
- // Use provided patterns or defaults
306
- const patterns = existingTestPatterns || {
307
- testFramework: 'vitest',
308
- fileExtension: 'mjs',
309
- testSuffix: 'test',
310
- assertionPatterns: ['toBe', 'toEqual', 'toBeDefined'],
311
- describeBlocks: [],
312
- setupTeardown: {
313
- hasBeforeEach: false,
314
- hasAfterEach: false,
315
- hasBeforeAll: false,
316
- hasAfterAll: false,
317
- },
318
- imports: [],
319
- };
320
- const ext = patterns.fileExtension || 'mjs';
321
- const suffix = patterns.testSuffix || 'test';
322
- const entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
323
-
324
- const filename = `${entityLower}.${suffix}.${ext}`;
325
-
326
- // Generate imports
327
- const imports =
328
- patterns.imports.length > 0
329
- ? patterns.imports.map(imp => `import { } from '${imp}'`).join('\n')
330
- : `import { describe, it, expect${patterns.setupTeardown.hasBeforeEach ? ', beforeEach' : ''} } from '${patterns.testFramework || 'vitest'}'`;
331
-
332
- // Generate content
333
- let content = `${imports}\n`;
334
- content += `import { ${entity} } from '../../src/${entityLower}.${ext}'\n\n`;
335
- content += `describe('${entity}', () => {\n`;
336
-
337
- if (patterns.setupTeardown.hasBeforeEach) {
338
- content += ` let ${entityLower}\n`;
339
- content += ` beforeEach(() => { ${entityLower} = new ${entity}() })\n`;
340
- }
341
-
342
- content += ` describe('creation', () => {\n`;
343
- content += ` it('should create ${entityLower} instance', () => { expect(${entityLower || `new ${entity}()`}).toBeDefined() })\n`;
344
- content += ` })\n`;
345
- content += `})\n`;
346
-
347
- // Generate suggested tests
348
- const suggestedTests = [`should create ${entity} instance`];
349
- const fieldTests = [];
350
-
351
- // Add field tests if domainStore provided
352
- if (domainStore) {
353
- const fieldQuads = domainStore.getQuads(
354
- namedNode(`${NS.dom}${entity}`),
355
- namedNode(`${NS.dom}hasField`),
356
- null
357
- );
358
- for (const quad of fieldQuads) {
359
- const fieldName = quad.object.value.split('.').pop();
360
- const testName = `should have ${fieldName} property`;
361
- suggestedTests.push(testName);
362
- fieldTests.push({ name: testName, field: fieldName });
363
- }
364
- }
365
-
366
- // Add field tests to content
367
- if (fieldTests.length > 0) {
368
- content += ` describe('properties', () => {\n`;
369
- for (const fieldTest of fieldTests) {
370
- content += ` it('${fieldTest.name}', () => { expect(${entityLower}.${fieldTest.field}).toBeDefined() })\n`;
371
- }
372
- content += ` })\n`;
373
- }
374
-
375
- return {
376
- filename,
377
- content,
378
- suggestedTests,
379
- };
380
- }
381
-
382
- /**
383
- * Score test coverage for an entity
384
- * @param {Object} options
385
- * @param {string} options.entity - Entity name
386
- * @param {string[]} [options.testFiles] - Array of test file paths
387
- * @param {string[]} [options.sourceFiles] - Array of source file paths
388
- * @returns {Object} Coverage score object
389
- */
390
- export function scoreTestCoverage(options) {
391
- const { entity, testFiles = [], sourceFiles = [] } = options;
392
-
393
- const entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
394
- const entityKebab = entity
395
- .replace(/([A-Z])/g, '-$1')
396
- .toLowerCase()
397
- .slice(1);
398
-
399
- const matchingTests = testFiles.filter(f => {
400
- const lower = f.toLowerCase();
401
- return (
402
- lower.includes(entityLower) ||
403
- lower.includes(entityKebab) ||
404
- lower.includes(entity.toLowerCase())
405
- );
406
- });
407
-
408
- // If no source files provided, assume entity needs tests if no matching tests found
409
- const needsTests =
410
- sourceFiles.length > 0 ? matchingTests.length === 0 : matchingTests.length === 0;
411
- // Coverage: if source files exist, 100% if any tests found, 0% otherwise
412
- // Cap at 100% to handle cases where multiple test files match one source file
413
- const coverage =
414
- sourceFiles.length > 0
415
- ? matchingTests.length > 0
416
- ? 100
417
- : 0
418
- : matchingTests.length > 0
419
- ? 100
420
- : 0;
421
-
422
- return {
423
- coverage,
424
- needsTests,
425
- existingTests: matchingTests,
426
- };
427
- }
428
-
429
- /**
430
- * Generate test factory function
431
- * @param {string} entity - Entity name
432
- * @returns {string} Factory function code
433
- */
434
- export function generateTestFactory(entity) {
435
- const _entityLower = entity.charAt(0).toLowerCase() + entity.slice(1);
436
- return `function create${entity}(overrides = {}) {
437
- return {
438
- id: 'test-id',
439
- ...overrides,
440
- };
441
- }`;
442
- }
443
-
444
- export { TestSuggestionSchema };