@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,869 +0,0 @@
1
- /**
2
- * @file Policy derivation from observed project patterns
3
- * @module project-engine/policy-derivation
4
- *
5
- * @description
6
- * Automatically derives transaction hook policies from observed project patterns.
7
- * Analyzes the project store and stack profile to detect invariant violations
8
- * and generates hooks that enforce these patterns on future transactions.
9
- */
10
-
11
- import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
12
- import { z } from 'zod';
13
-
14
- const { namedNode } = DataFactory;
15
-
16
- // ============================================================================
17
- // Zod Schemas
18
- // ============================================================================
19
-
20
- const StackProfileSchema = z.object({
21
- uiFramework: z.string().nullable().optional(),
22
- webFramework: z.string().nullable().optional(),
23
- apiFramework: z.string().nullable().optional(),
24
- testFramework: z.string().nullable().optional(),
25
- packageManager: z.string().nullable().optional(),
26
- });
27
-
28
- const DeriveOptionsSchema = z.object({
29
- enableFeatureViewPolicy: z.boolean().default(true),
30
- enableApiTestPolicy: z.boolean().default(true),
31
- enableOrphanFilePolicy: z.boolean().default(true),
32
- enableTestCompanionPolicy: z.boolean().default(true),
33
- enableStackPolicies: z.boolean().default(true),
34
- strictMode: z.boolean().default(false),
35
- });
36
-
37
- const HookConditionSchema = z.object({
38
- kind: z.enum(['sparql-ask', 'sparql-select', 'shacl', 'delta', 'threshold', 'count']),
39
- ref: z
40
- .object({
41
- uri: z.string(),
42
- sha256: z.string().optional(),
43
- mediaType: z.string().optional(),
44
- })
45
- .optional(),
46
- query: z.string().optional(),
47
- spec: z.any().optional(),
48
- });
49
-
50
- /**
51
- * @typedef {Object} DerivedHook
52
- * @property {Object} meta - Hook metadata
53
- * @property {string} meta.name - Unique hook name
54
- * @property {string} [meta.description] - Hook description
55
- * @property {string[]} [meta.ontology] - Related ontologies
56
- * @property {Object} [channel] - Observation channel
57
- * @property {Object} when - Trigger condition
58
- * @property {Function} run - Main execution body
59
- * @property {Function} [before] - Pre-condition gate
60
- * @property {Function} [after] - Post-execution step
61
- * @property {Object} [determinism] - Determinism config
62
- * @property {Object} [receipt] - Receipt config
63
- */
64
-
65
- /**
66
- * @typedef {Object} PatternViolation
67
- * @property {string} type - Violation type
68
- * @property {string} subject - Subject IRI
69
- * @property {string} message - Human-readable message
70
- * @property {string} [relatedEntity] - Related entity IRI
71
- */
72
-
73
- // ============================================================================
74
- // Pattern Detection
75
- // ============================================================================
76
-
77
- /**
78
- * Detect features without views
79
- * @param {Object} store - N3 Store
80
- * @returns {PatternViolation[]}
81
- */
82
- function detectFeaturesWithoutViews(store) {
83
- const violations = [];
84
- const projectNs = 'http://example.org/unrdf/project#';
85
- const rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
86
-
87
- // Find all features
88
- const featureQuads = store.getQuads(null, namedNode(rdfType), namedNode(`${projectNs}Feature`));
89
-
90
- for (const featureQuad of featureQuads) {
91
- const featureIri = featureQuad.subject.value;
92
-
93
- // Check if feature has any file with Component or Page role
94
- const roleQuads = store.getQuads(
95
- null,
96
- namedNode(`${projectNs}belongsToFeature`),
97
- namedNode(featureIri)
98
- );
99
-
100
- let hasView = false;
101
- for (const roleQuad of roleQuads) {
102
- const fileIri = roleQuad.subject;
103
- const fileRoles = store.getQuads(fileIri, namedNode(`${projectNs}roleString`), null);
104
-
105
- for (const fileRole of fileRoles) {
106
- if (['Component', 'Page'].includes(fileRole.object.value)) {
107
- hasView = true;
108
- break;
109
- }
110
- }
111
- if (hasView) break;
112
- }
113
-
114
- if (!hasView) {
115
- violations.push({
116
- type: 'feature-without-view',
117
- subject: featureIri,
118
- message: `Feature "${featureIri}" has no Component or Page view`,
119
- });
120
- }
121
- }
122
-
123
- return violations;
124
- }
125
-
126
- /**
127
- * Detect features with API but no test
128
- * @param {Object} store - N3 Store
129
- * @returns {PatternViolation[]}
130
- */
131
- function detectApiWithoutTest(store) {
132
- const violations = [];
133
- const projectNs = 'http://example.org/unrdf/project#';
134
- const rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
135
-
136
- const featureQuads = store.getQuads(null, namedNode(rdfType), namedNode(`${projectNs}Feature`));
137
-
138
- for (const featureQuad of featureQuads) {
139
- const featureIri = featureQuad.subject.value;
140
-
141
- const roleQuads = store.getQuads(
142
- null,
143
- namedNode(`${projectNs}belongsToFeature`),
144
- namedNode(featureIri)
145
- );
146
-
147
- let hasApi = false;
148
- let hasTest = false;
149
-
150
- for (const roleQuad of roleQuads) {
151
- const fileIri = roleQuad.subject;
152
- const fileRoles = store.getQuads(fileIri, namedNode(`${projectNs}roleString`), null);
153
-
154
- for (const fileRole of fileRoles) {
155
- if (fileRole.object.value === 'Api') hasApi = true;
156
- if (fileRole.object.value === 'Test') hasTest = true;
157
- }
158
- }
159
-
160
- if (hasApi && !hasTest) {
161
- violations.push({
162
- type: 'api-without-test',
163
- subject: featureIri,
164
- message: `Feature "${featureIri}" has API but no test`,
165
- });
166
- }
167
- }
168
-
169
- return violations;
170
- }
171
-
172
- /**
173
- * Detect orphan files (files under features/* without Feature association)
174
- * @param {Object} store - N3 Store
175
- * @returns {PatternViolation[]}
176
- */
177
- function detectOrphanFiles(store) {
178
- const violations = [];
179
- const projectNs = 'http://example.org/unrdf/project#';
180
- const fsNs = 'http://example.org/unrdf/filesystem#';
181
-
182
- const fileQuads = store.getQuads(null, namedNode(`${fsNs}relativePath`), null);
183
-
184
- for (const fileQuad of fileQuads) {
185
- const filePath = fileQuad.object.value;
186
- const fileIri = fileQuad.subject;
187
-
188
- // Check if file is under features/ directory
189
- if (filePath.match(/^src\/features\/[^/]+\//)) {
190
- // Check if file belongs to a feature
191
- const belongsQuads = store.getQuads(fileIri, namedNode(`${projectNs}belongsToFeature`), null);
192
-
193
- if (belongsQuads.length === 0) {
194
- violations.push({
195
- type: 'orphan-file',
196
- subject: fileIri.value,
197
- message: `File "${filePath}" is under features/ but has no Feature association`,
198
- relatedEntity: filePath,
199
- });
200
- }
201
- }
202
- }
203
-
204
- return violations;
205
- }
206
-
207
- /**
208
- * Detect main files without test companions
209
- * @param {Object} store - N3 Store
210
- * @returns {PatternViolation[]}
211
- */
212
- function detectFilesWithoutTests(store) {
213
- const violations = [];
214
- const _projectNs = 'http://example.org/unrdf/project#';
215
- const fsNs = 'http://example.org/unrdf/filesystem#';
216
-
217
- const fileQuads = store.getQuads(null, namedNode(`${fsNs}relativePath`), null);
218
-
219
- const filePaths = new Set();
220
- const testPaths = new Set();
221
-
222
- for (const fileQuad of fileQuads) {
223
- const filePath = fileQuad.object.value;
224
- filePaths.add(filePath);
225
-
226
- if (filePath.match(/\.(test|spec)\.(tsx?|jsx?|mjs)$/)) {
227
- testPaths.add(filePath);
228
- }
229
- }
230
-
231
- for (const filePath of filePaths) {
232
- // Skip test files, config, docs
233
- if (filePath.match(/\.(test|spec|config|d)\.(tsx?|jsx?|mjs)$/)) continue;
234
- if (filePath.match(/\.(md|json|yaml|yml)$/)) continue;
235
- if (!filePath.match(/\.(tsx?|jsx?|mjs)$/)) continue;
236
-
237
- // Only check src files
238
- if (!filePath.startsWith('src/')) continue;
239
-
240
- // Check for corresponding test file
241
- const baseName = filePath.replace(/\.(tsx?|jsx?|mjs)$/, '');
242
- const possibleTests = [
243
- `${baseName}.test.ts`,
244
- `${baseName}.test.tsx`,
245
- `${baseName}.test.js`,
246
- `${baseName}.test.jsx`,
247
- `${baseName}.test.mjs`,
248
- `${baseName}.spec.ts`,
249
- `${baseName}.spec.tsx`,
250
- `${baseName}.spec.js`,
251
- `${baseName}.spec.jsx`,
252
- `${baseName}.spec.mjs`,
253
- ];
254
-
255
- const hasTest = possibleTests.some(t => testPaths.has(t));
256
-
257
- if (!hasTest) {
258
- violations.push({
259
- type: 'file-without-test',
260
- subject: filePath,
261
- message: `Source file "${filePath}" has no corresponding test file`,
262
- });
263
- }
264
- }
265
-
266
- return violations;
267
- }
268
-
269
- /**
270
- * Detect Next.js app router violations
271
- * @param {Object} store - N3 Store
272
- * @param {Object} stackProfile - Stack profile
273
- * @returns {PatternViolation[]}
274
- */
275
- function detectNextAppRouterViolations(store, stackProfile) {
276
- const violations = [];
277
-
278
- if (stackProfile.webFramework !== 'next-app-router') {
279
- return violations;
280
- }
281
-
282
- const fsNs = 'http://example.org/unrdf/filesystem#';
283
-
284
- const fileQuads = store.getQuads(null, namedNode(`${fsNs}relativePath`), null);
285
-
286
- const filePaths = new Set();
287
- for (const fileQuad of fileQuads) {
288
- filePaths.add(fileQuad.object.value);
289
- }
290
-
291
- // Find page.tsx files and check for route.ts
292
- for (const filePath of filePaths) {
293
- if (filePath.match(/app\/.*\/page\.(tsx?|jsx?)$/)) {
294
- const routeDir = filePath.replace(/page\.(tsx?|jsx?)$/, '');
295
- const routeFile = `${routeDir}route.ts`;
296
- const routeFileTsx = `${routeDir}route.tsx`;
297
-
298
- // Note: route.ts is optional, but if it's an API route it should exist
299
- // This is a soft check - only flag if directory looks like API route
300
- if (routeDir.match(/\/api\//)) {
301
- if (!filePaths.has(routeFile) && !filePaths.has(routeFileTsx)) {
302
- violations.push({
303
- type: 'next-app-router-missing-route',
304
- subject: filePath,
305
- message: `API route "${routeDir}" has page.tsx but no route.ts`,
306
- relatedEntity: routeFile,
307
- });
308
- }
309
- }
310
- }
311
- }
312
-
313
- return violations;
314
- }
315
-
316
- // ============================================================================
317
- // Hook Generation
318
- // ============================================================================
319
-
320
- /**
321
- * Create hook for feature-must-have-view invariant
322
- * @param {PatternViolation[]} violations - Detected violations
323
- * @returns {DerivedHook}
324
- */
325
- function createFeatureViewHook(_violations) {
326
- return {
327
- meta: {
328
- name: 'derived:feature-must-have-view',
329
- description: 'Every Feature must have at least one Component or Page view',
330
- ontology: ['unrdf-project'],
331
- },
332
- channel: {
333
- graphs: ['urn:graph:project'],
334
- view: 'after',
335
- },
336
- when: {
337
- kind: 'sparql-ask',
338
- query: `
339
- PREFIX unproj: <http://example.org/unrdf/project#>
340
- PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
341
- ASK {
342
- ?feature rdf:type unproj:Feature .
343
- FILTER NOT EXISTS {
344
- ?file unproj:belongsToFeature ?feature ;
345
- unproj:roleString ?role .
346
- FILTER(?role IN ("Component", "Page"))
347
- }
348
- }
349
- `,
350
- },
351
- determinism: { seed: 42 },
352
- receipt: { anchor: 'none' },
353
-
354
- before({ payload }) {
355
- return payload;
356
- },
357
-
358
- run({ _payload, context }) {
359
- const store = context.graph;
360
- if (!store) {
361
- return { result: { valid: true, violations: [] } };
362
- }
363
-
364
- const projectNs = 'http://example.org/unrdf/project#';
365
- const rdfType = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
366
-
367
- const currentViolations = [];
368
- const featureQuads = store.getQuads(
369
- null,
370
- namedNode(rdfType),
371
- namedNode(`${projectNs}Feature`)
372
- );
373
-
374
- for (const featureQuad of featureQuads) {
375
- const featureIri = featureQuad.subject.value;
376
- const roleQuads = store.getQuads(
377
- null,
378
- namedNode(`${projectNs}belongsToFeature`),
379
- namedNode(featureIri)
380
- );
381
-
382
- let hasView = false;
383
- for (const roleQuad of roleQuads) {
384
- const fileRoles = store.getQuads(
385
- roleQuad.subject,
386
- namedNode(`${projectNs}roleString`),
387
- null
388
- );
389
- for (const fileRole of fileRoles) {
390
- if (['Component', 'Page'].includes(fileRole.object.value)) {
391
- hasView = true;
392
- break;
393
- }
394
- }
395
- if (hasView) break;
396
- }
397
-
398
- if (!hasView) {
399
- currentViolations.push({
400
- type: 'feature-without-view',
401
- subject: featureIri,
402
- message: `Feature "${featureIri}" must have at least one view`,
403
- });
404
- }
405
- }
406
-
407
- return {
408
- result: {
409
- valid: currentViolations.length === 0,
410
- violations: currentViolations,
411
- },
412
- };
413
- },
414
-
415
- after({ result, cancelled, reason }) {
416
- if (cancelled) {
417
- return { result: { status: 'cancelled', reason } };
418
- }
419
- return { result: { status: 'completed', ...result } };
420
- },
421
- };
422
- }
423
-
424
- /**
425
- * Create hook for api-must-have-test invariant
426
- * @param {PatternViolation[]} violations - Detected violations
427
- * @returns {DerivedHook}
428
- */
429
- function createApiTestHook(_violations) {
430
- return {
431
- meta: {
432
- name: 'derived:api-must-have-test',
433
- description: 'Every Feature with API must have Test',
434
- ontology: ['unrdf-project'],
435
- },
436
- channel: {
437
- graphs: ['urn:graph:project'],
438
- view: 'after',
439
- },
440
- when: {
441
- kind: 'sparql-ask',
442
- query: `
443
- PREFIX unproj: <http://example.org/unrdf/project#>
444
- PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
445
- ASK {
446
- ?feature rdf:type unproj:Feature .
447
- ?apiFile unproj:belongsToFeature ?feature ;
448
- unproj:roleString "Api" .
449
- FILTER NOT EXISTS {
450
- ?testFile unproj:belongsToFeature ?feature ;
451
- unproj:roleString "Test" .
452
- }
453
- }
454
- `,
455
- },
456
- determinism: { seed: 42 },
457
- receipt: { anchor: 'none' },
458
-
459
- before({ payload }) {
460
- return payload;
461
- },
462
-
463
- run({ _payload, context }) {
464
- const store = context.graph;
465
- if (!store) {
466
- return { result: { valid: true, violations: [] } };
467
- }
468
-
469
- const currentViolations = detectApiWithoutTest(store);
470
-
471
- return {
472
- result: {
473
- valid: currentViolations.length === 0,
474
- violations: currentViolations,
475
- },
476
- };
477
- },
478
-
479
- after({ result, cancelled, reason }) {
480
- if (cancelled) {
481
- return { result: { status: 'cancelled', reason } };
482
- }
483
- return { result: { status: 'completed', ...result } };
484
- },
485
- };
486
- }
487
-
488
- /**
489
- * Create hook for orphan-file invariant
490
- * @param {PatternViolation[]} violations - Detected violations
491
- * @returns {DerivedHook}
492
- */
493
- function createOrphanFileHook(_violations) {
494
- return {
495
- meta: {
496
- name: 'derived:no-orphan-files',
497
- description: 'Files under features/* must belong to some Feature',
498
- ontology: ['unrdf-project', 'unrdf-filesystem'],
499
- },
500
- channel: {
501
- graphs: ['urn:graph:project', 'urn:graph:fs'],
502
- view: 'after',
503
- },
504
- when: {
505
- kind: 'sparql-ask',
506
- query: `
507
- PREFIX unproj: <http://example.org/unrdf/project#>
508
- PREFIX fs: <http://example.org/unrdf/filesystem#>
509
- ASK {
510
- ?file fs:relativePath ?path .
511
- FILTER(REGEX(?path, "^src/features/[^/]+/"))
512
- FILTER NOT EXISTS {
513
- ?file unproj:belongsToFeature ?feature .
514
- }
515
- }
516
- `,
517
- },
518
- determinism: { seed: 42 },
519
- receipt: { anchor: 'none' },
520
-
521
- before({ payload }) {
522
- return payload;
523
- },
524
-
525
- run({ _payload, context }) {
526
- const store = context.graph;
527
- if (!store) {
528
- return { result: { valid: true, violations: [] } };
529
- }
530
-
531
- const currentViolations = detectOrphanFiles(store);
532
-
533
- return {
534
- result: {
535
- valid: currentViolations.length === 0,
536
- violations: currentViolations,
537
- },
538
- };
539
- },
540
-
541
- after({ result, cancelled, reason }) {
542
- if (cancelled) {
543
- return { result: { status: 'cancelled', reason } };
544
- }
545
- return { result: { status: 'completed', ...result } };
546
- },
547
- };
548
- }
549
-
550
- /**
551
- * Create hook for test-companion invariant
552
- * @param {PatternViolation[]} violations - Detected violations
553
- * @returns {DerivedHook}
554
- */
555
- function createTestCompanionHook(_violations) {
556
- return {
557
- meta: {
558
- name: 'derived:test-companion-required',
559
- description: 'Test file must accompany main file',
560
- ontology: ['unrdf-project', 'unrdf-filesystem'],
561
- },
562
- channel: {
563
- graphs: ['urn:graph:project', 'urn:graph:fs'],
564
- view: 'after',
565
- },
566
- when: {
567
- kind: 'count',
568
- spec: {
569
- op: '>',
570
- value: 0,
571
- query:
572
- 'SELECT (COUNT(?file) AS ?count) WHERE { ?file a <http://example.org/unrdf/filesystem#File> }',
573
- },
574
- },
575
- determinism: { seed: 42 },
576
- receipt: { anchor: 'none' },
577
-
578
- before({ payload }) {
579
- return payload;
580
- },
581
-
582
- run({ _payload, context }) {
583
- const store = context.graph;
584
- if (!store) {
585
- return { result: { valid: true, violations: [] } };
586
- }
587
-
588
- const currentViolations = detectFilesWithoutTests(store);
589
-
590
- return {
591
- result: {
592
- valid: currentViolations.length === 0,
593
- violations: currentViolations,
594
- },
595
- };
596
- },
597
-
598
- after({ result, cancelled, reason }) {
599
- if (cancelled) {
600
- return { result: { status: 'cancelled', reason } };
601
- }
602
- return { result: { status: 'completed', ...result } };
603
- },
604
- };
605
- }
606
-
607
- /**
608
- * Create hook for Next.js app router structure
609
- * @param {PatternViolation[]} violations - Detected violations
610
- * @param {Object} stackProfile - Stack profile
611
- * @returns {DerivedHook}
612
- */
613
- function createNextAppRouterHook(violations, stackProfile) {
614
- return {
615
- meta: {
616
- name: 'derived:next-app-router-structure',
617
- description: 'Next.js app/[route]/page.tsx must have route.ts for API routes',
618
- ontology: ['unrdf-project', 'next-app-router'],
619
- },
620
- channel: {
621
- graphs: ['urn:graph:project', 'urn:graph:fs'],
622
- view: 'after',
623
- },
624
- when: {
625
- kind: 'sparql-ask',
626
- query: `
627
- PREFIX fs: <http://example.org/unrdf/filesystem#>
628
- ASK {
629
- ?file fs:relativePath ?path .
630
- FILTER(REGEX(?path, "app/.*/api/.*/page\\\\.(tsx?|jsx?)$"))
631
- }
632
- `,
633
- },
634
- determinism: { seed: 42 },
635
- receipt: { anchor: 'none' },
636
-
637
- before({ payload }) {
638
- return payload;
639
- },
640
-
641
- run({ _payload, context }) {
642
- const store = context.graph;
643
- if (!store) {
644
- return { result: { valid: true, violations: [] } };
645
- }
646
-
647
- const currentViolations = detectNextAppRouterViolations(store, stackProfile);
648
-
649
- return {
650
- result: {
651
- valid: currentViolations.length === 0,
652
- violations: currentViolations,
653
- stackProfile,
654
- },
655
- };
656
- },
657
-
658
- after({ result, cancelled, reason }) {
659
- if (cancelled) {
660
- return { result: { status: 'cancelled', reason } };
661
- }
662
- return { result: { status: 'completed', ...result } };
663
- },
664
- };
665
- }
666
-
667
- // ============================================================================
668
- // Main API
669
- // ============================================================================
670
-
671
- /**
672
- * Derive transaction hook policies from observed project patterns
673
- *
674
- * Analyzes the project store to detect pattern violations and generates
675
- * hooks that enforce these patterns on future transactions.
676
- *
677
- * @param {Object} projectStore - N3 Store with project structure
678
- * @param {Object} stackProfile - Stack profile from detectStackFromFs
679
- * @param {Object} [options] - Derivation options
680
- * @param {boolean} [options.enableFeatureViewPolicy=true] - Enable feature-view policy
681
- * @param {boolean} [options.enableApiTestPolicy=true] - Enable api-test policy
682
- * @param {boolean} [options.enableOrphanFilePolicy=true] - Enable orphan-file policy
683
- * @param {boolean} [options.enableTestCompanionPolicy=true] - Enable test-companion policy
684
- * @param {boolean} [options.enableStackPolicies=true] - Enable stack-specific policies
685
- * @param {boolean} [options.strictMode=false] - Fail on validation errors
686
- * @returns {DerivedHook[]} Array of Hook objects ready for KnowledgeHookManager.registerHook()
687
- *
688
- * @example
689
- * import { deriveHooksFromStructure } from './policy-derivation.mjs'
690
- * import { KnowledgeHookManager } from '../knowledge-engine/knowledge-hook-manager.mjs'
691
- *
692
- * const hooks = deriveHooksFromStructure(projectStore, stackProfile)
693
- * const manager = new KnowledgeHookManager()
694
- *
695
- * for (const hook of hooks) {
696
- * manager.addKnowledgeHook(hook)
697
- * }
698
- */
699
- export function deriveHooksFromStructure(projectStore, stackProfile, options = {}) {
700
- // Validate inputs
701
- if (!projectStore || typeof projectStore.getQuads !== 'function') {
702
- throw new TypeError('deriveHooksFromStructure: projectStore must be an N3 Store');
703
- }
704
-
705
- const validatedStack = StackProfileSchema.parse(stackProfile || {});
706
- const validatedOptions = DeriveOptionsSchema.parse(options);
707
-
708
- const hooks = [];
709
-
710
- // Detect violations for each pattern type
711
- if (validatedOptions.enableFeatureViewPolicy) {
712
- const featureViewViolations = detectFeaturesWithoutViews(projectStore);
713
- hooks.push(createFeatureViewHook(featureViewViolations));
714
- }
715
-
716
- if (validatedOptions.enableApiTestPolicy) {
717
- const apiTestViolations = detectApiWithoutTest(projectStore);
718
- hooks.push(createApiTestHook(apiTestViolations));
719
- }
720
-
721
- if (validatedOptions.enableOrphanFilePolicy) {
722
- const orphanViolations = detectOrphanFiles(projectStore);
723
- hooks.push(createOrphanFileHook(orphanViolations));
724
- }
725
-
726
- if (validatedOptions.enableTestCompanionPolicy) {
727
- const testCompanionViolations = detectFilesWithoutTests(projectStore);
728
- hooks.push(createTestCompanionHook(testCompanionViolations));
729
- }
730
-
731
- // Stack-specific policies
732
- if (validatedOptions.enableStackPolicies) {
733
- if (validatedStack.webFramework === 'next-app-router') {
734
- const nextViolations = detectNextAppRouterViolations(projectStore, validatedStack);
735
- hooks.push(createNextAppRouterHook(nextViolations, validatedStack));
736
- }
737
- }
738
-
739
- return hooks;
740
- }
741
-
742
- /**
743
- * Analyze project store and return violation report without generating hooks
744
- *
745
- * @param {Object} projectStore - N3 Store with project structure
746
- * @param {Object} stackProfile - Stack profile from detectStackFromFs
747
- * @param {Object} [options] - Analysis options
748
- * @returns {Object} Violation report
749
- */
750
- export function analyzePatternViolations(projectStore, stackProfile, options = {}) {
751
- if (!projectStore || typeof projectStore.getQuads !== 'function') {
752
- throw new TypeError('analyzePatternViolations: projectStore must be an N3 Store');
753
- }
754
-
755
- const validatedStack = StackProfileSchema.parse(stackProfile || {});
756
- const validatedOptions = DeriveOptionsSchema.parse(options);
757
-
758
- const report = {
759
- timestamp: new Date().toISOString(),
760
- stackProfile: validatedStack,
761
- violations: {
762
- featureWithoutView: [],
763
- apiWithoutTest: [],
764
- orphanFiles: [],
765
- filesWithoutTests: [],
766
- stackSpecific: [],
767
- },
768
- summary: {
769
- total: 0,
770
- byType: {},
771
- },
772
- };
773
-
774
- if (validatedOptions.enableFeatureViewPolicy) {
775
- report.violations.featureWithoutView = detectFeaturesWithoutViews(projectStore);
776
- }
777
-
778
- if (validatedOptions.enableApiTestPolicy) {
779
- report.violations.apiWithoutTest = detectApiWithoutTest(projectStore);
780
- }
781
-
782
- if (validatedOptions.enableOrphanFilePolicy) {
783
- report.violations.orphanFiles = detectOrphanFiles(projectStore);
784
- }
785
-
786
- if (validatedOptions.enableTestCompanionPolicy) {
787
- report.violations.filesWithoutTests = detectFilesWithoutTests(projectStore);
788
- }
789
-
790
- if (validatedOptions.enableStackPolicies) {
791
- if (validatedStack.webFramework === 'next-app-router') {
792
- report.violations.stackSpecific = detectNextAppRouterViolations(projectStore, validatedStack);
793
- }
794
- }
795
-
796
- // Compute summary
797
- for (const [type, violations] of Object.entries(report.violations)) {
798
- report.summary.byType[type] = violations.length;
799
- report.summary.total += violations.length;
800
- }
801
-
802
- return report;
803
- }
804
-
805
- /**
806
- * Create a custom hook from a pattern specification
807
- *
808
- * @param {Object} spec - Pattern specification
809
- * @param {string} spec.name - Hook name
810
- * @param {string} spec.description - Hook description
811
- * @param {string} spec.pattern - Pattern type
812
- * @param {Object} spec.condition - Trigger condition
813
- * @param {Function} spec.validator - Validation function
814
- * @returns {DerivedHook} Hook object
815
- */
816
- export function createCustomPatternHook(spec) {
817
- const SpecSchema = z.object({
818
- name: z.string().min(1).max(100),
819
- description: z.string().min(1).max(500),
820
- pattern: z.string().min(1),
821
- condition: HookConditionSchema,
822
- validator: z.function(),
823
- });
824
-
825
- const validated = SpecSchema.parse(spec);
826
-
827
- return {
828
- meta: {
829
- name: `custom:${validated.name}`,
830
- description: validated.description,
831
- ontology: ['unrdf-project'],
832
- },
833
- channel: {
834
- graphs: ['urn:graph:project'],
835
- view: 'after',
836
- },
837
- when: validated.condition,
838
- determinism: { seed: 42 },
839
- receipt: { anchor: 'none' },
840
-
841
- before({ payload }) {
842
- return payload;
843
- },
844
-
845
- run({ payload, context }) {
846
- const store = context.graph;
847
- if (!store) {
848
- return { result: { valid: true, violations: [] } };
849
- }
850
-
851
- const violations = validated.validator(store, payload);
852
-
853
- return {
854
- result: {
855
- valid: Array.isArray(violations) ? violations.length === 0 : violations,
856
- violations: Array.isArray(violations) ? violations : [],
857
- pattern: validated.pattern,
858
- },
859
- };
860
- },
861
-
862
- after({ result, cancelled, reason }) {
863
- if (cancelled) {
864
- return { result: { status: 'cancelled', reason } };
865
- }
866
- return { result: { status: 'completed', ...result } };
867
- },
868
- };
869
- }