@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,775 +0,0 @@
1
- /**
2
- * @file Drift detection system - baseline snapshots and deviation detection
3
- * @module project-engine/drift-snapshot
4
- */
5
-
6
- import { createHash } from 'crypto';
7
- import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
8
- import { createStore } from '@unrdf/oxigraph'; // TODO: Replace with Oxigraph Store
9
- import { z } from 'zod';
10
- import {
11
- diffGraphFromStores,
12
- summarizeChangesByKind as _summarizeChangesByKind,
13
- } from '../diff.mjs';
14
- import { ProjectStructureLens } from './lens/project-structure.mjs';
15
-
16
- const { namedNode, literal } = DataFactory;
17
-
18
- /* ========================================================================= */
19
- /* Zod Schemas */
20
- /* ========================================================================= */
21
-
22
- const CreateSnapshotOptionsSchema = z.object({
23
- fsStore: z.object({}).passthrough(),
24
- domainStore: z.object({}).passthrough().optional().nullable(),
25
- templateMappings: z.record(z.string(), z.string()).optional(),
26
- baseIri: z.string().default('http://example.org/unrdf/snapshot#'),
27
- });
28
-
29
- const SnapshotReceiptSchema = z.object({
30
- hash: z.string(),
31
- createdAt: z.string(),
32
- summary: z.object({
33
- fileCount: z.number(),
34
- featureCount: z.number(),
35
- roleCount: z.number(),
36
- domainEntityCount: z.number(),
37
- templateMappingCount: z.number(),
38
- }),
39
- });
40
-
41
- const DriftResultSchema = z.object({
42
- ontologyDiff: z.object({
43
- triples: z.object({
44
- added: z.array(z.any()),
45
- removed: z.array(z.any()),
46
- }),
47
- changes: z.array(z.any()),
48
- }),
49
- summary: z.array(z.string()),
50
- driftSeverity: z.enum(['none', 'minor', 'major']),
51
- });
52
-
53
- /* ========================================================================= */
54
- /* Snapshot Creation */
55
- /* ========================================================================= */
56
-
57
- /**
58
- * Create a structure snapshot from filesystem and domain stores
59
- * Encodes: FS structure + domain + template mappings
60
- *
61
- * @param {Store} fsStore - Filesystem structure store
62
- * @param {Store} [domainStore] - Domain ontology store (entities + fields)
63
- * @param {Object} [options] - Snapshot options
64
- * @param {Record<string, string>} [options.templateMappings] - File to template mappings
65
- * @param {string} [options.baseIri] - Base IRI for snapshot resources
66
- * @returns {{snapshotStore: Store, receipt: {hash: string, createdAt: string, summary: Object}}}
67
- */
68
- export function createStructureSnapshot(fsStore, domainStore, options = {}) {
69
- const validated = CreateSnapshotOptionsSchema.parse({
70
- fsStore,
71
- domainStore,
72
- ...options,
73
- });
74
-
75
- const { baseIri, templateMappings = {} } = validated;
76
- const snapshotStore = createStore();
77
- const createdAt = new Date().toISOString();
78
-
79
- // Summary counters
80
- const summary = {
81
- fileCount: 0,
82
- featureCount: 0,
83
- roleCount: 0,
84
- domainEntityCount: 0,
85
- templateMappingCount: 0,
86
- };
87
-
88
- // 1. Copy FS structure graph (file -> feature -> role)
89
- const fsQuads = fsStore.getQuads(null, null, null, null);
90
- for (const quad of fsQuads) {
91
- snapshotStore.addQuad(quad.subject, quad.predicate, quad.object, quad.graph);
92
-
93
- // Count files
94
- if (
95
- quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
96
- quad.object.value ===
97
- 'http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject'
98
- ) {
99
- summary.fileCount++;
100
- }
101
-
102
- // Count features
103
- if (
104
- quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
105
- quad.object.value === 'http://example.org/unrdf/project#Feature'
106
- ) {
107
- summary.featureCount++;
108
- }
109
-
110
- // Count roles
111
- if (quad.predicate.value === 'http://example.org/unrdf/project#hasRole') {
112
- summary.roleCount++;
113
- }
114
- }
115
-
116
- // 2. Copy domain ontology if provided
117
- if (domainStore) {
118
- const domainQuads = domainStore.getQuads(null, null, null, null);
119
- for (const quad of domainQuads) {
120
- snapshotStore.addQuad(quad.subject, quad.predicate, quad.object, quad.graph);
121
-
122
- // Count domain entities
123
- if (
124
- quad.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
125
- (quad.object.value.includes('Entity') || quad.object.value.includes('Class'))
126
- ) {
127
- summary.domainEntityCount++;
128
- }
129
- }
130
- }
131
-
132
- // 3. Encode template mappings (file -> template -> entity)
133
- for (const [filePath, templateId] of Object.entries(templateMappings)) {
134
- const fileIri = namedNode(`${baseIri}file/${encodeURIComponent(filePath)}`);
135
- const templateIri = namedNode(`${baseIri}template/${encodeURIComponent(templateId)}`);
136
-
137
- snapshotStore.addQuad(
138
- fileIri,
139
- namedNode('http://example.org/unrdf/project#usesTemplate'),
140
- templateIri
141
- );
142
- summary.templateMappingCount++;
143
- }
144
-
145
- // 4. Add snapshot metadata
146
- const snapshotIri = namedNode(`${baseIri}snapshot`);
147
- snapshotStore.addQuad(
148
- snapshotIri,
149
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
150
- namedNode('http://example.org/unrdf/snapshot#StructureSnapshot')
151
- );
152
- snapshotStore.addQuad(
153
- snapshotIri,
154
- namedNode('http://example.org/unrdf/snapshot#createdAt'),
155
- literal(createdAt, namedNode('http://www.w3.org/2001/XMLSchema#dateTime'))
156
- );
157
- snapshotStore.addQuad(
158
- snapshotIri,
159
- namedNode('http://example.org/unrdf/snapshot#fileCount'),
160
- literal(summary.fileCount, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
161
- );
162
- snapshotStore.addQuad(
163
- snapshotIri,
164
- namedNode('http://example.org/unrdf/snapshot#featureCount'),
165
- literal(summary.featureCount, namedNode('http://www.w3.org/2001/XMLSchema#integer'))
166
- );
167
-
168
- // 5. Compute hash of combined state
169
- const hash = computeSnapshotHash(snapshotStore, createdAt);
170
-
171
- snapshotStore.addQuad(
172
- snapshotIri,
173
- namedNode('http://example.org/unrdf/snapshot#contentHash'),
174
- literal(hash)
175
- );
176
-
177
- const receipt = SnapshotReceiptSchema.parse({
178
- hash,
179
- createdAt,
180
- summary,
181
- });
182
-
183
- return { snapshotStore, receipt };
184
- }
185
-
186
- /* ========================================================================= */
187
- /* Drift Detection */
188
- /* ========================================================================= */
189
-
190
- /**
191
- * Compute drift between current snapshot and baseline
192
- * Uses diffOntologyFromDelta + ProjectStructureLens
193
- *
194
- * @param {Store} currentSnapshot - Current project state snapshot
195
- * @param {Store} baselineSnapshot - Baseline snapshot to compare against
196
- * @returns {{ontologyDiff: Object, summary: string[], driftSeverity: 'none'|'minor'|'major'}}
197
- */
198
- export function computeDrift(currentSnapshot, baselineSnapshot) {
199
- // Compute graph-level diff
200
- const graphDiff = diffGraphFromStores(baselineSnapshot, currentSnapshot);
201
-
202
- // Apply ProjectStructureLens to get semantic changes
203
- const changes = [];
204
-
205
- for (const triple of graphDiff.added) {
206
- const change = ProjectStructureLens(triple, 'added');
207
- if (change) {
208
- changes.push(change);
209
- }
210
- }
211
-
212
- for (const triple of graphDiff.removed) {
213
- const change = ProjectStructureLens(triple, 'removed');
214
- if (change) {
215
- changes.push(change);
216
- }
217
- }
218
-
219
- const ontologyDiff = {
220
- triples: graphDiff,
221
- changes,
222
- };
223
-
224
- // Generate human-readable summary
225
- const summary = generateDriftSummary(graphDiff, changes, currentSnapshot, baselineSnapshot);
226
-
227
- // Compute severity
228
- const driftSeverity = computeDriftSeverity(graphDiff, changes);
229
-
230
- return DriftResultSchema.parse({
231
- ontologyDiff,
232
- summary,
233
- driftSeverity,
234
- });
235
- }
236
-
237
- /* ========================================================================= */
238
- /* Drift Summary Generation */
239
- /* ========================================================================= */
240
-
241
- /**
242
- * Generate human-readable drift summary
243
- *
244
- * @param {Object} graphDiff - Low-level triple diff
245
- * @param {Object[]} changes - Semantic changes from lens
246
- * @param {Store} currentSnapshot - Current snapshot
247
- * @param {Store} baselineSnapshot - Baseline snapshot
248
- * @returns {string[]}
249
- */
250
- function generateDriftSummary(graphDiff, changes, currentSnapshot, baselineSnapshot) {
251
- const summary = [];
252
-
253
- // Group changes by kind
254
- const changesByKind = {};
255
- for (const change of changes) {
256
- if (!changesByKind[change.kind]) {
257
- changesByKind[change.kind] = [];
258
- }
259
- changesByKind[change.kind].push(change);
260
- }
261
-
262
- // Missing tests detection
263
- const missingTests = detectMissingTests(graphDiff, changes, currentSnapshot, baselineSnapshot);
264
- for (const msg of missingTests) {
265
- summary.push(msg);
266
- }
267
-
268
- // Unmatched template patterns
269
- const unmatchedPatterns = detectUnmatchedPatterns(graphDiff, changes, currentSnapshot);
270
- for (const msg of unmatchedPatterns) {
271
- summary.push(msg);
272
- }
273
-
274
- // Domain entity coverage gaps
275
- const coverageGaps = detectCoverageGaps(graphDiff, changes, currentSnapshot, baselineSnapshot);
276
- for (const msg of coverageGaps) {
277
- summary.push(msg);
278
- }
279
-
280
- // Test coverage changes
281
- const coverageChanges = detectTestCoverageChanges(currentSnapshot, baselineSnapshot);
282
- for (const msg of coverageChanges) {
283
- summary.push(msg);
284
- }
285
-
286
- // Feature additions/removals
287
- if (changesByKind['FeatureAdded']) {
288
- for (const change of changesByKind['FeatureAdded']) {
289
- const name = extractNameFromIri(change.entity);
290
- summary.push(`Feature "${name}" added since baseline`);
291
- }
292
- }
293
-
294
- if (changesByKind['FeatureRemoved']) {
295
- for (const change of changesByKind['FeatureRemoved']) {
296
- const name = extractNameFromIri(change.entity);
297
- summary.push(`Feature "${name}" removed (was in baseline)`);
298
- }
299
- }
300
-
301
- // Role changes
302
- if (changesByKind['RoleRemoved']) {
303
- for (const change of changesByKind['RoleRemoved']) {
304
- const entity = extractNameFromIri(change.entity);
305
- const role = extractNameFromIri(change.role);
306
- summary.push(`Role "${role}" removed from "${entity}"`);
307
- }
308
- }
309
-
310
- // If no drift detected
311
- if (summary.length === 0 && graphDiff.added.length === 0 && graphDiff.removed.length === 0) {
312
- summary.push('No structural drift detected - code matches baseline model');
313
- }
314
-
315
- return summary;
316
- }
317
-
318
- /**
319
- * Detect features missing required tests
320
- *
321
- * @param {Object} graphDiff
322
- * @param {Object[]} changes
323
- * @param {Store} currentSnapshot
324
- * @param {Store} baselineSnapshot
325
- * @returns {string[]}
326
- */
327
- function detectMissingTests(graphDiff, changes, currentSnapshot, baselineSnapshot) {
328
- const messages = [];
329
-
330
- // Get features in current snapshot
331
- const currentFeatures = new Set();
332
- const featureQuads = currentSnapshot.getQuads(
333
- null,
334
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
335
- namedNode('http://example.org/unrdf/project#Feature')
336
- );
337
- for (const quad of featureQuads) {
338
- currentFeatures.add(quad.subject.value);
339
- }
340
-
341
- // Check which features have tests
342
- const featuresWithTests = new Set();
343
- const testRoleQuads = currentSnapshot.getQuads(
344
- null,
345
- namedNode('http://example.org/unrdf/project#hasRole'),
346
- namedNode('http://example.org/unrdf/project#Test')
347
- );
348
- for (const quad of testRoleQuads) {
349
- // Find feature this file belongs to
350
- const belongsQuads = currentSnapshot.getQuads(
351
- quad.subject,
352
- namedNode('http://example.org/unrdf/project#belongsToFeature'),
353
- null
354
- );
355
- for (const bq of belongsQuads) {
356
- featuresWithTests.add(bq.object.value);
357
- }
358
- }
359
-
360
- // Check baseline for features that should have tests
361
- const baselineTestRequirements = new Set();
362
- const baselineTestQuads = baselineSnapshot.getQuads(
363
- null,
364
- namedNode('http://example.org/unrdf/project#hasRole'),
365
- namedNode('http://example.org/unrdf/project#Test')
366
- );
367
- for (const quad of baselineTestQuads) {
368
- const belongsQuads = baselineSnapshot.getQuads(
369
- quad.subject,
370
- namedNode('http://example.org/unrdf/project#belongsToFeature'),
371
- null
372
- );
373
- for (const bq of belongsQuads) {
374
- baselineTestRequirements.add(bq.object.value);
375
- }
376
- }
377
-
378
- // Report features that had tests in baseline but not now
379
- for (const featureIri of baselineTestRequirements) {
380
- if (currentFeatures.has(featureIri) && !featuresWithTests.has(featureIri)) {
381
- const name = extractNameFromIri(featureIri);
382
- messages.push(`Feature "${name}" missing tests when model requires them`);
383
- }
384
- }
385
-
386
- return messages;
387
- }
388
-
389
- /**
390
- * Detect files that don't match any template pattern
391
- *
392
- * @param {Object} graphDiff
393
- * @param {Object[]} changes
394
- * @param {Store} currentSnapshot
395
- * @returns {string[]}
396
- */
397
- function detectUnmatchedPatterns(graphDiff, changes, currentSnapshot) {
398
- const messages = [];
399
-
400
- // Find files without roles
401
- const filesWithRoles = new Set();
402
- const roleQuads = currentSnapshot.getQuads(
403
- null,
404
- namedNode('http://example.org/unrdf/project#hasRole'),
405
- null
406
- );
407
- for (const quad of roleQuads) {
408
- filesWithRoles.add(quad.subject.value);
409
- }
410
-
411
- // Find all files in feature directories
412
- const featureFiles = [];
413
- const pathQuads = currentSnapshot.getQuads(
414
- null,
415
- namedNode('http://example.org/unrdf/filesystem#relativePath'),
416
- null
417
- );
418
- for (const quad of pathQuads) {
419
- const path = quad.object.value;
420
- if (path.includes('/features/') || path.includes('/modules/')) {
421
- if (!filesWithRoles.has(quad.subject.value)) {
422
- featureFiles.push(path);
423
- }
424
- }
425
- }
426
-
427
- // Group unmatched files by feature
428
- const unmatchedByFeature = {};
429
- for (const path of featureFiles) {
430
- const match = path.match(/\/(features|modules)\/([^/]+)\//);
431
- if (match) {
432
- const featureName = match[2];
433
- if (!unmatchedByFeature[featureName]) {
434
- unmatchedByFeature[featureName] = [];
435
- }
436
- unmatchedByFeature[featureName].push(path);
437
- }
438
- }
439
-
440
- for (const [featureName, files] of Object.entries(unmatchedByFeature)) {
441
- if (files.length > 0) {
442
- messages.push(
443
- `Files in features/${featureName} don't match any template pattern (${files.length} files)`
444
- );
445
- }
446
- }
447
-
448
- return messages;
449
- }
450
-
451
- /**
452
- * Detect domain entities without corresponding views/APIs
453
- *
454
- * @param {Object} graphDiff
455
- * @param {Object[]} changes
456
- * @param {Store} currentSnapshot
457
- * @param {Store} baselineSnapshot
458
- * @returns {string[]}
459
- */
460
- function detectCoverageGaps(graphDiff, changes, currentSnapshot, baselineSnapshot) {
461
- const messages = [];
462
-
463
- // Find domain entities in current snapshot
464
- const currentEntities = new Set();
465
- const entityQuads = currentSnapshot.getQuads(
466
- null,
467
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
468
- null
469
- );
470
- for (const quad of entityQuads) {
471
- if (quad.object.value.includes('Entity') || quad.object.value.includes('DomainClass')) {
472
- currentEntities.add(quad.subject.value);
473
- }
474
- }
475
-
476
- // Find entities in baseline
477
- const baselineEntities = new Set();
478
- const baselineEntityQuads = baselineSnapshot.getQuads(
479
- null,
480
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
481
- null
482
- );
483
- for (const quad of baselineEntityQuads) {
484
- if (quad.object.value.includes('Entity') || quad.object.value.includes('DomainClass')) {
485
- baselineEntities.add(quad.subject.value);
486
- }
487
- }
488
-
489
- // Find new entities (in current but not baseline)
490
- for (const entityIri of currentEntities) {
491
- if (!baselineEntities.has(entityIri)) {
492
- // Check if entity has views/APIs
493
- const hasViews = checkEntityHasViews(entityIri, currentSnapshot);
494
- const hasApis = checkEntityHasApis(entityIri, currentSnapshot);
495
-
496
- if (!hasViews && !hasApis) {
497
- const name = extractNameFromIri(entityIri);
498
- messages.push(`Domain entity "${name}" added but no views/APIs for it`);
499
- }
500
- }
501
- }
502
-
503
- return messages;
504
- }
505
-
506
- /**
507
- * Detect test coverage changes
508
- *
509
- * @param {Store} currentSnapshot
510
- * @param {Store} baselineSnapshot
511
- * @returns {string[]}
512
- */
513
- function detectTestCoverageChanges(currentSnapshot, baselineSnapshot) {
514
- const messages = [];
515
-
516
- // Count tests in baseline
517
- const baselineTestCount = baselineSnapshot.getQuads(
518
- null,
519
- namedNode('http://example.org/unrdf/project#hasRole'),
520
- namedNode('http://example.org/unrdf/project#Test')
521
- ).length;
522
-
523
- // Count tests in current
524
- const currentTestCount = currentSnapshot.getQuads(
525
- null,
526
- namedNode('http://example.org/unrdf/project#hasRole'),
527
- namedNode('http://example.org/unrdf/project#Test')
528
- ).length;
529
-
530
- // Count total files
531
- const baselineFileCount = baselineSnapshot.getQuads(
532
- null,
533
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
534
- namedNode('http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject')
535
- ).length;
536
-
537
- const currentFileCount = currentSnapshot.getQuads(
538
- null,
539
- namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
540
- namedNode('http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject')
541
- ).length;
542
-
543
- // Compute coverage ratios
544
- const baselineCoverage = baselineFileCount > 0 ? baselineTestCount / baselineFileCount : 0;
545
- const currentCoverage = currentFileCount > 0 ? currentTestCount / currentFileCount : 0;
546
-
547
- // Report if coverage dropped
548
- if (currentCoverage < baselineCoverage * 0.9) {
549
- const baselinePct = (baselineCoverage * 100).toFixed(1);
550
- const currentPct = (currentCoverage * 100).toFixed(1);
551
- messages.push(`Test coverage dropped below baseline (was ${baselinePct}%, now ${currentPct}%)`);
552
- }
553
-
554
- return messages;
555
- }
556
-
557
- /**
558
- * Check if entity has view files
559
- *
560
- * @param {string} entityIri
561
- * @param {Store} store
562
- * @returns {boolean}
563
- */
564
- function checkEntityHasViews(entityIri, store) {
565
- const entityName = extractNameFromIri(entityIri).toLowerCase();
566
-
567
- // Look for view/component files containing entity name
568
- const pathQuads = store.getQuads(
569
- null,
570
- namedNode('http://example.org/unrdf/filesystem#relativePath'),
571
- null
572
- );
573
-
574
- for (const quad of pathQuads) {
575
- const path = quad.object.value.toLowerCase();
576
- if (
577
- path.includes(entityName) &&
578
- (path.includes('view') || path.includes('page') || path.includes('component'))
579
- ) {
580
- return true;
581
- }
582
- }
583
-
584
- return false;
585
- }
586
-
587
- /**
588
- * Check if entity has API files
589
- *
590
- * @param {string} entityIri
591
- * @param {Store} store
592
- * @returns {boolean}
593
- */
594
- function checkEntityHasApis(entityIri, store) {
595
- const entityName = extractNameFromIri(entityIri).toLowerCase();
596
-
597
- // Look for API/route files containing entity name
598
- const pathQuads = store.getQuads(
599
- null,
600
- namedNode('http://example.org/unrdf/filesystem#relativePath'),
601
- null
602
- );
603
-
604
- for (const quad of pathQuads) {
605
- const path = quad.object.value.toLowerCase();
606
- if (
607
- path.includes(entityName) &&
608
- (path.includes('api') || path.includes('route') || path.includes('controller'))
609
- ) {
610
- return true;
611
- }
612
- }
613
-
614
- return false;
615
- }
616
-
617
- /* ========================================================================= */
618
- /* Drift Severity Computation */
619
- /* ========================================================================= */
620
-
621
- /**
622
- * Compute drift severity based on changes
623
- *
624
- * @param {Object} graphDiff
625
- * @param {Object[]} changes
626
- * @returns {'none'|'minor'|'major'}
627
- */
628
- function computeDriftSeverity(graphDiff, changes) {
629
- const totalTripleChanges = graphDiff.added.length + graphDiff.removed.length;
630
-
631
- // No changes = no drift
632
- if (totalTripleChanges === 0 && changes.length === 0) {
633
- return 'none';
634
- }
635
-
636
- // Count critical changes
637
- let criticalCount = 0;
638
- let minorCount = 0;
639
-
640
- for (const change of changes) {
641
- if (change.kind === 'FeatureRemoved' || change.kind === 'ModuleRemoved') {
642
- criticalCount++;
643
- } else if (change.kind === 'RoleRemoved') {
644
- // Removed tests or docs is critical
645
- if (change.role && (change.role.includes('Test') || change.role.includes('Doc'))) {
646
- criticalCount++;
647
- } else {
648
- minorCount++;
649
- }
650
- } else {
651
- minorCount++;
652
- }
653
- }
654
-
655
- // Major if any critical changes or many minor changes
656
- if (criticalCount > 0 || minorCount > 10) {
657
- return 'major';
658
- }
659
-
660
- // Minor if few changes
661
- if (minorCount > 0 || totalTripleChanges > 0) {
662
- return 'minor';
663
- }
664
-
665
- return 'none';
666
- }
667
-
668
- /* ========================================================================= */
669
- /* Utility Functions */
670
- /* ========================================================================= */
671
-
672
- /**
673
- * Compute SHA-256 hash of snapshot
674
- *
675
- * @param {Store} store
676
- * @param {string} timestamp
677
- * @returns {string}
678
- */
679
- function computeSnapshotHash(store, timestamp) {
680
- const hash = createHash('sha256');
681
-
682
- // Hash store size and timestamp
683
- hash.update(String(store.size));
684
- hash.update(timestamp);
685
-
686
- // Hash a sample of quads for content fingerprint
687
- const quads = store.getQuads(null, null, null, null);
688
- const sample = quads.slice(0, Math.min(100, quads.length));
689
-
690
- for (const quad of sample) {
691
- hash.update(quad.subject.value);
692
- hash.update(quad.predicate.value);
693
- hash.update(quad.object.value);
694
- }
695
-
696
- return hash.digest('hex').substring(0, 32);
697
- }
698
-
699
- /**
700
- * Extract name from IRI
701
- *
702
- * @param {string} iri
703
- * @returns {string}
704
- */
705
- function extractNameFromIri(iri) {
706
- if (!iri) return 'unknown';
707
-
708
- // Try hash fragment
709
- const hashMatch = iri.match(/#([^#]+)$/);
710
- if (hashMatch) return decodeURIComponent(hashMatch[1]);
711
-
712
- // Try last path segment
713
- const slashMatch = iri.match(/\/([^/]+)$/);
714
- if (slashMatch) return decodeURIComponent(slashMatch[1]);
715
-
716
- return iri;
717
- }
718
-
719
- /* ========================================================================= */
720
- /* Convenience Exports */
721
- /* ========================================================================= */
722
-
723
- /**
724
- * Create empty baseline snapshot
725
- *
726
- * @returns {Store}
727
- */
728
- export function createEmptyBaseline() {
729
- return createStore();
730
- }
731
-
732
- /**
733
- * Serialize snapshot to JSON for persistence
734
- *
735
- * @param {Store} snapshotStore
736
- * @param {Object} receipt
737
- * @returns {string}
738
- */
739
- export function serializeSnapshot(snapshotStore, receipt) {
740
- const quads = snapshotStore.getQuads(null, null, null, null);
741
- const serialized = {
742
- version: '1.0.0',
743
- receipt,
744
- quads: quads.map(q => ({
745
- subject: q.subject.value,
746
- predicate: q.predicate.value,
747
- object: q.object.value,
748
- })),
749
- };
750
- return JSON.stringify(serialized, null, 2);
751
- }
752
-
753
- /**
754
- * Deserialize snapshot from JSON
755
- *
756
- * @param {string} json
757
- * @returns {{snapshotStore: Store, receipt: Object}}
758
- */
759
- export function deserializeSnapshot(json) {
760
- const data = JSON.parse(json);
761
- const store = createStore();
762
-
763
- for (const q of data.quads) {
764
- store.addQuad(
765
- namedNode(q.subject),
766
- namedNode(q.predicate),
767
- q.object.startsWith('http') ? namedNode(q.object) : literal(q.object)
768
- );
769
- }
770
-
771
- return {
772
- snapshotStore: store,
773
- receipt: data.receipt,
774
- };
775
- }