@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,94 @@
1
+ /**
2
+ * @file File role classification
3
+ * @module project-engine/file-roles
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 FileRolesOptionsSchema = 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
+ stackInfo: z
16
+ .object({
17
+ uiFramework: z.string().nullable(),
18
+ webFramework: z.string().nullable(),
19
+ testFramework: z.string().nullable(),
20
+ })
21
+ .optional(),
22
+ baseIri: z.string().default('http://example.org/unrdf/project#'),
23
+ });
24
+
25
+ const ROLE_PATTERNS = {
26
+ Component: [/\.(tsx?|jsx?)$/, /(Component|View)\.tsx?$/],
27
+ Page: [/^(pages|src\/pages|src\/app|app)\//, /page\.(tsx?|jsx?)$/],
28
+ Api: [/(api|server|route)\.(tsx?|js)$/, /^(api|server|routes)\//],
29
+ Hook: [/use[A-Z]\w+\.(tsx?|jsx?)$/, /^hooks?\//],
30
+ Service: [/(service|client|repository)\.(tsx?|js)$/, /^(services?|clients?)\//],
31
+ Schema: [/(schema|type|interface)\.(tsx?|js)$/, /(schema|types?|interfaces?)\.(tsx?|json)$/],
32
+ State: [/(store|state|reducer|context)\.(tsx?|js)$/, /^(store|state|redux)\//],
33
+ Test: [/\.(test|spec)\.(tsx?|jsx?)$/, /^(__tests__|test|tests|spec)\//],
34
+ Doc: [/\.(md|mdx)$/, /^(docs?|README)/],
35
+ Config: [/\.(json|yaml|yml|toml|conf)$/, /(package\.json|tsconfig|eslintrc|prettier)/],
36
+ Build: [/(build|webpack|rollup|esbuild|vite|next\.config)/, /^scripts\//],
37
+ Other: [],
38
+ };
39
+
40
+ /**
41
+ * Classify files with semantic roles
42
+ *
43
+ * @param {Object} options
44
+ * @param {Store} options.fsStore - FS store with project model
45
+ * @param {Object} [options.stackInfo] - Stack information from detectStackFromFs
46
+ * @param {string} [options.baseIri] - Base IRI for roles
47
+ * @returns {Store} Store with role classifications
48
+ */
49
+ export function classifyFiles(options) {
50
+ const validated = FileRolesOptionsSchema.parse(options);
51
+ const { fsStore, baseIri } = validated;
52
+
53
+ const store = fsStore;
54
+ let _roleCount = 0;
55
+
56
+ const fileQuads = store.getQuads(
57
+ null,
58
+ namedNode('http://example.org/unrdf/filesystem#relativePath'),
59
+ null
60
+ );
61
+
62
+ for (const quad of fileQuads) {
63
+ const filePath = quad.object.value;
64
+ const fileIri = quad.subject;
65
+ const role = classifyPath(filePath);
66
+
67
+ if (role && role !== 'Other') {
68
+ const roleIri = namedNode(`${baseIri}${role}`);
69
+ store.addQuad(fileIri, namedNode('http://example.org/unrdf/project#hasRole'), roleIri);
70
+ store.addQuad(
71
+ fileIri,
72
+ namedNode('http://example.org/unrdf/project#roleString'),
73
+ literal(role)
74
+ );
75
+ _roleCount++;
76
+ }
77
+ }
78
+
79
+ return store;
80
+ }
81
+
82
+ /**
83
+ * Classify a file path to determine its role
84
+ *
85
+ * @private
86
+ */
87
+ function classifyPath(filePath) {
88
+ for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
89
+ if (patterns.some(pattern => pattern.test(filePath))) {
90
+ return role;
91
+ }
92
+ }
93
+ return 'Other';
94
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * @file Filesystem scanner - walk directory tree and emit RDF graph
3
+ * @module project-engine/fs-scan
4
+ */
5
+
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
9
+ import { createStore } from '@unrdf/oxigraph'; // TODO: Replace with Oxigraph Store
10
+ import { trace, SpanStatusCode } from '@opentelemetry/api';
11
+ import { z } from 'zod';
12
+
13
+ const tracer = trace.getTracer('unrdf/fs-scan');
14
+ const { namedNode, literal } = DataFactory;
15
+
16
+ /**
17
+ * Default ignore patterns for filesystem scan
18
+ */
19
+ const DEFAULT_IGNORE_PATTERNS = [
20
+ 'node_modules',
21
+ '.git',
22
+ '.gitignore',
23
+ 'dist',
24
+ 'build',
25
+ '.next',
26
+ '.turbo',
27
+ 'coverage',
28
+ '.cache',
29
+ '.venv',
30
+ 'env',
31
+ '*.swp',
32
+ '.DS_Store',
33
+ '.env.local',
34
+ '.env.*.local',
35
+ ];
36
+
37
+ const ScanOptionsSchema = z.object({
38
+ root: z.string(),
39
+ ignorePatterns: z.array(z.string()).optional(),
40
+ baseIri: z.string().default('http://example.org/unrdf/fs#'),
41
+ });
42
+
43
+ /**
44
+ * Scan a filesystem directory and create RDF graph with NFO + UNRDF FS ontology
45
+ *
46
+ * @param {Object} options
47
+ * @param {string} options.root - Directory to scan
48
+ * @param {string[]} [options.ignorePatterns] - Glob patterns to ignore
49
+ * @param {string} [options.baseIri] - Base IRI for resource identifiers
50
+ * @returns {Promise<{store: Store, summary: {fileCount: number, folderCount: number, ignoredCount: number, rootIri: string}}>}
51
+ */
52
+ export async function scanFileSystemToStore(options) {
53
+ const validated = ScanOptionsSchema.parse(options);
54
+ const { root, baseIri } = validated;
55
+ const ignorePatterns = validated.ignorePatterns || DEFAULT_IGNORE_PATTERNS;
56
+
57
+ return tracer.startActiveSpan('fs.scan', async span => {
58
+ try {
59
+ span.setAttributes({
60
+ 'fs.root': root,
61
+ 'fs.ignore_count': ignorePatterns.length,
62
+ });
63
+
64
+ const store = createStore();
65
+ const stats = {
66
+ fileCount: 0,
67
+ folderCount: 0,
68
+ ignoredCount: 0,
69
+ rootIri: `${baseIri}root`,
70
+ };
71
+
72
+ // Add root as ProjectRoot
73
+ const rootIri = namedNode(`${baseIri}root`);
74
+ store.addQuad(
75
+ rootIri,
76
+ namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
77
+ namedNode('http://example.org/unrdf/filesystem#ProjectRoot')
78
+ );
79
+ store.addQuad(
80
+ rootIri,
81
+ namedNode('http://example.org/unrdf/filesystem#relativePath'),
82
+ literal('.')
83
+ );
84
+ store.addQuad(
85
+ rootIri,
86
+ namedNode('http://example.org/unrdf/filesystem#depth'),
87
+ literal(0, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
88
+ );
89
+
90
+ // Recursive walk
91
+ await walkDirectory(root, '.', rootIri, store, ignorePatterns, baseIri, stats);
92
+
93
+ span.setAttributes({
94
+ 'fs.file_count': stats.fileCount,
95
+ 'fs.folder_count': stats.folderCount,
96
+ 'fs.ignored_count': stats.ignoredCount,
97
+ });
98
+ span.setStatus({ code: SpanStatusCode.OK });
99
+
100
+ return { store, summary: stats };
101
+ } catch (error) {
102
+ span.setStatus({
103
+ code: SpanStatusCode.ERROR,
104
+ message: error.message,
105
+ });
106
+ throw error;
107
+ }
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Recursively walk directory and add quads to store
113
+ *
114
+ * @private
115
+ */
116
+ async function walkDirectory(
117
+ diskPath,
118
+ relativePath,
119
+ parentIri,
120
+ store,
121
+ ignorePatterns,
122
+ baseIri,
123
+ stats
124
+ ) {
125
+ try {
126
+ const entries = await fs.readdir(diskPath, { withFileTypes: true });
127
+
128
+ for (const entry of entries) {
129
+ // Check ignore patterns
130
+ if (shouldIgnore(entry.name, ignorePatterns)) {
131
+ stats.ignoredCount++;
132
+ continue;
133
+ }
134
+
135
+ const entryRelPath = relativePath === '.' ? entry.name : `${relativePath}/${entry.name}`;
136
+ const entryDiskPath = path.join(diskPath, entry.name);
137
+ const entryIri = namedNode(`${baseIri}${encodeURIComponent(entryRelPath)}`);
138
+
139
+ // Determine type
140
+ if (entry.isDirectory()) {
141
+ await handleDirectory(
142
+ entryDiskPath,
143
+ entryRelPath,
144
+ entryIri,
145
+ parentIri,
146
+ store,
147
+ ignorePatterns,
148
+ baseIri,
149
+ stats
150
+ );
151
+ } else if (entry.isFile()) {
152
+ await handleFile(entryDiskPath, entryRelPath, entryIri, parentIri, store, stats);
153
+ }
154
+ }
155
+ } catch (error) {
156
+ // Skip unreadable directories
157
+ console.warn(`Skipped: ${diskPath} (${error.message})`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Handle a directory entry
163
+ *
164
+ * @private
165
+ */
166
+ async function handleDirectory(
167
+ diskPath,
168
+ relativePath,
169
+ dirIri,
170
+ parentIri,
171
+ store,
172
+ ignorePatterns,
173
+ baseIri,
174
+ stats
175
+ ) {
176
+ const depth = (relativePath.match(/\//g) || []).length;
177
+
178
+ // Determine folder type
179
+ let folderType = 'http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#Folder';
180
+ if (relativePath.includes('/src') || relativePath === 'src') {
181
+ folderType = 'http://example.org/unrdf/filesystem#SourceFolder';
182
+ } else if (
183
+ relativePath.includes('/dist') ||
184
+ relativePath === 'dist' ||
185
+ relativePath.includes('/.next') ||
186
+ relativePath === '.next' ||
187
+ relativePath.includes('/build') ||
188
+ relativePath === 'build'
189
+ ) {
190
+ folderType = 'http://example.org/unrdf/filesystem#BuildFolder';
191
+ } else if (
192
+ relativePath.includes('/config') ||
193
+ relativePath === 'config' ||
194
+ relativePath.includes('/.') ||
195
+ relativePath.startsWith('.')
196
+ ) {
197
+ folderType = 'http://example.org/unrdf/filesystem#ConfigFolder';
198
+ }
199
+
200
+ // Add folder quads
201
+ store.addQuad(
202
+ dirIri,
203
+ namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
204
+ namedNode(folderType)
205
+ );
206
+ store.addQuad(
207
+ dirIri,
208
+ namedNode('http://example.org/unrdf/filesystem#relativePath'),
209
+ literal(relativePath)
210
+ );
211
+ store.addQuad(
212
+ dirIri,
213
+ namedNode('http://example.org/unrdf/filesystem#depth'),
214
+ literal(depth, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
215
+ );
216
+ store.addQuad(
217
+ dirIri,
218
+ namedNode('http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#containedFolders'),
219
+ namedNode('http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject')
220
+ );
221
+
222
+ // Link to parent
223
+ store.addQuad(dirIri, namedNode('http://example.org/unrdf/filesystem#containedIn'), parentIri);
224
+
225
+ stats.folderCount++;
226
+
227
+ // Recurse
228
+ await walkDirectory(diskPath, relativePath, dirIri, store, ignorePatterns, baseIri, stats);
229
+ }
230
+
231
+ /**
232
+ * Handle a file entry
233
+ *
234
+ * @private
235
+ */
236
+ async function handleFile(diskPath, relativePath, fileIri, parentIri, store, stats) {
237
+ try {
238
+ const stat = await fs.stat(diskPath);
239
+ const depth = (relativePath.match(/\//g) || []).length;
240
+ const ext = path.extname(relativePath).replace(/^\./, '');
241
+ const isHidden = path.basename(relativePath).startsWith('.');
242
+
243
+ store.addQuad(
244
+ fileIri,
245
+ namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
246
+ namedNode('http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject')
247
+ );
248
+ store.addQuad(
249
+ fileIri,
250
+ namedNode('http://example.org/unrdf/filesystem#relativePath'),
251
+ literal(relativePath)
252
+ );
253
+ store.addQuad(
254
+ fileIri,
255
+ namedNode('http://example.org/unrdf/filesystem#depth'),
256
+ literal(depth, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
257
+ );
258
+ store.addQuad(
259
+ fileIri,
260
+ namedNode('http://example.org/unrdf/filesystem#byteSize'),
261
+ literal(stat.size, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
262
+ );
263
+ store.addQuad(
264
+ fileIri,
265
+ namedNode('http://example.org/unrdf/filesystem#lastModified'),
266
+ literal(stat.mtime.toISOString(), namedNode('http://www.w3.org/2001/XMLSchema#dateTime'))
267
+ );
268
+ store.addQuad(
269
+ fileIri,
270
+ namedNode('http://example.org/unrdf/filesystem#isHidden'),
271
+ literal(isHidden, namedNode('http://www.w3.org/2001/XMLSchema#boolean'))
272
+ );
273
+
274
+ if (ext) {
275
+ store.addQuad(
276
+ fileIri,
277
+ namedNode('http://example.org/unrdf/filesystem#extension'),
278
+ literal(ext)
279
+ );
280
+ }
281
+
282
+ // Link to parent
283
+ store.addQuad(fileIri, namedNode('http://example.org/unrdf/filesystem#containedIn'), parentIri);
284
+
285
+ stats.fileCount++;
286
+ } catch (error) {
287
+ console.warn(`Skipped file: ${diskPath} (${error.message})`);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Check if path matches ignore patterns
293
+ *
294
+ * @private
295
+ */
296
+ function shouldIgnore(name, patterns) {
297
+ return patterns.some(pattern => {
298
+ if (pattern.includes('*')) {
299
+ // Simple glob: *.swp, .*, etc.
300
+ const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`);
301
+ return regex.test(name);
302
+ }
303
+ return name === pattern;
304
+ });
305
+ }