@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,711 +0,0 @@
1
- /**
2
- * @file API Contract Validator - validates API files against domain schemas
3
- * @module project-engine/api-contract-validator
4
- */
5
-
6
- import { z } from 'zod';
7
- import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
8
-
9
- const { namedNode } = DataFactory;
10
-
11
- /* ========================================================================= */
12
- /* Namespace prefixes */
13
- /* ========================================================================= */
14
-
15
- const NS = {
16
- rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
17
- rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
18
- xsd: 'http://www.w3.org/2001/XMLSchema#',
19
- dom: 'http://example.org/unrdf/domain#',
20
- };
21
-
22
- /* ========================================================================= */
23
- /* Zod Schemas */
24
- /* ========================================================================= */
25
-
26
- const FieldSchemaSchema = z.object({
27
- name: z.string(),
28
- type: z.string(),
29
- optional: z.boolean().default(false),
30
- array: z.boolean().default(false),
31
- });
32
-
33
- const EntitySchemaSchema = z.object({
34
- entityName: z.string(),
35
- fields: z.array(FieldSchemaSchema),
36
- zodSchema: z.string().optional(),
37
- });
38
-
39
- const ViolationSchema = z.object({
40
- type: z.enum([
41
- 'missing_field',
42
- 'extra_field',
43
- 'type_mismatch',
44
- 'optionality_mismatch',
45
- 'missing_validation',
46
- 'missing_response_field',
47
- ]),
48
- field: z.string(),
49
- expected: z.string().optional(),
50
- actual: z.string().optional(),
51
- severity: z.enum(['low', 'medium', 'high', 'critical']),
52
- message: z.string(),
53
- });
54
-
55
- const ValidationResultSchema = z.object({
56
- violations: z.array(ViolationSchema),
57
- coverage: z.number().min(0).max(100),
58
- breaking: z.boolean(),
59
- summary: z.string(),
60
- });
61
-
62
- const BreakingChangeSchema = z.object({
63
- type: z.enum(['field_removed', 'field_required', 'type_changed', 'entity_removed']),
64
- entity: z.string(),
65
- field: z.string().optional(),
66
- oldValue: z.string().optional(),
67
- newValue: z.string().optional(),
68
- message: z.string(),
69
- });
70
-
71
- const ContractBreaksSchema = z.object({
72
- breakingChanges: z.array(BreakingChangeSchema),
73
- affectedAPIs: z.array(z.string()),
74
- safe: z.boolean(),
75
- });
76
-
77
- /**
78
- * @typedef {z.infer<typeof FieldSchemaSchema>} FieldSchema
79
- * @typedef {z.infer<typeof EntitySchemaSchema>} EntitySchema
80
- * @typedef {z.infer<typeof ViolationSchema>} Violation
81
- * @typedef {z.infer<typeof ValidationResultSchema>} ValidationResult
82
- * @typedef {z.infer<typeof BreakingChangeSchema>} BreakingChange
83
- * @typedef {z.infer<typeof ContractBreaksSchema>} ContractBreaks
84
- */
85
-
86
- /* ========================================================================= */
87
- /* Type mapping from XSD to Zod */
88
- /* ========================================================================= */
89
-
90
- /**
91
- * Map XSD type to Zod type name
92
- * @param {string} xsdType
93
- * @returns {string}
94
- */
95
- function xsdToZodType(xsdType) {
96
- const typeMap = {
97
- [`${NS.xsd}string`]: 'z.string()',
98
- [`${NS.xsd}integer`]: 'z.number()',
99
- [`${NS.xsd}decimal`]: 'z.number()',
100
- [`${NS.xsd}float`]: 'z.number()',
101
- [`${NS.xsd}double`]: 'z.number()',
102
- [`${NS.xsd}boolean`]: 'z.boolean()',
103
- [`${NS.xsd}date`]: 'z.string().date()',
104
- [`${NS.xsd}dateTime`]: 'z.string().datetime()',
105
- [`${NS.xsd}anyURI`]: 'z.string().url()',
106
- string: 'z.string()',
107
- number: 'z.number()',
108
- boolean: 'z.boolean()',
109
- date: 'z.string().date()',
110
- };
111
-
112
- return typeMap[xsdType] || 'z.string()';
113
- }
114
-
115
- /**
116
- * Map type string to simple type name for comparison
117
- * @param {string} xsdType
118
- * @returns {string}
119
- */
120
- function normalizeTypeName(xsdType) {
121
- if (
122
- xsdType.includes('integer') ||
123
- xsdType.includes('decimal') ||
124
- xsdType.includes('float') ||
125
- xsdType.includes('double')
126
- ) {
127
- return 'number';
128
- }
129
- if (xsdType.includes('boolean')) {
130
- return 'boolean';
131
- }
132
- if (xsdType.includes('date')) {
133
- return 'date';
134
- }
135
- return 'string';
136
- }
137
-
138
- /* ========================================================================= */
139
- /* Domain store extraction */
140
- /* ========================================================================= */
141
-
142
- /**
143
- * Extract entity fields from domain model store
144
- * @param {import('n3').Store} domainStore
145
- * @param {string} entityName
146
- * @param {string} [baseIri]
147
- * @returns {FieldSchema[]}
148
- */
149
- function extractEntityFields(
150
- domainStore,
151
- entityName,
152
- baseIri = 'http://example.org/unrdf/domain#'
153
- ) {
154
- /** @type {FieldSchema[]} */
155
- const fields = [];
156
-
157
- const entityIri = `${baseIri}${entityName}`;
158
-
159
- // Get all fields for this entity
160
- const fieldQuads = domainStore.getQuads(
161
- namedNode(entityIri),
162
- namedNode(`${NS.dom}hasField`),
163
- null
164
- );
165
-
166
- for (const fieldQuad of fieldQuads) {
167
- const fieldIri = fieldQuad.object.value;
168
-
169
- // Extract field name
170
- const nameQuads = domainStore.getQuads(
171
- namedNode(fieldIri),
172
- namedNode(`${NS.dom}fieldName`),
173
- null
174
- );
175
- const fieldName = nameQuads[0]?.object.value || fieldIri.split('.').pop() || '';
176
-
177
- // Extract field type
178
- const typeQuads = domainStore.getQuads(
179
- namedNode(fieldIri),
180
- namedNode(`${NS.dom}fieldType`),
181
- null
182
- );
183
- const fieldType = normalizeTypeName(typeQuads[0]?.object.value || 'string');
184
-
185
- // Extract optional flag
186
- const optionalQuads = domainStore.getQuads(
187
- namedNode(fieldIri),
188
- namedNode(`${NS.dom}isOptional`),
189
- null
190
- );
191
- const isOptional = optionalQuads[0]?.object.value === 'true';
192
-
193
- // Extract array flag
194
- const arrayQuads = domainStore.getQuads(
195
- namedNode(fieldIri),
196
- namedNode(`${NS.dom}isArray`),
197
- null
198
- );
199
- const isArray = arrayQuads[0]?.object.value === 'true';
200
-
201
- fields.push({
202
- name: fieldName,
203
- type: fieldType,
204
- optional: isOptional,
205
- array: isArray,
206
- });
207
- }
208
-
209
- return fields;
210
- }
211
-
212
- /**
213
- * Get all entity names from domain store
214
- * @param {import('n3').Store} domainStore
215
- * @param {string} [baseIri]
216
- * @returns {string[]}
217
- */
218
- function getEntityNames(domainStore, baseIri = 'http://example.org/unrdf/domain#') {
219
- const entityQuads = domainStore.getQuads(
220
- null,
221
- namedNode(`${NS.rdf}type`),
222
- namedNode(`${NS.dom}Entity`)
223
- );
224
-
225
- return entityQuads.map(quad => {
226
- const iri = quad.subject.value;
227
- return iri.replace(baseIri, '');
228
- });
229
- }
230
-
231
- /* ========================================================================= */
232
- /* API file parsing */
233
- /* ========================================================================= */
234
-
235
- /**
236
- * Extract request/response fields from Next.js API route
237
- * @param {string} content
238
- * @returns {{request: string[], response: string[], validations: string[]}}
239
- */
240
- function parseNextJsRoute(content) {
241
- const request = [];
242
- const response = [];
243
- const validations = [];
244
-
245
- // Match destructuring from request body: const { field1, field2 } = await req.json()
246
- const bodyPattern =
247
- /(?:const|let)\s*\{\s*([^}]+)\s*\}\s*=\s*(?:await\s+)?(?:req\.json|request\.json|body)/g;
248
- let match;
249
- while ((match = bodyPattern.exec(content)) !== null) {
250
- const fields = match[1].split(',').map(f => f.trim().split(':')[0].trim());
251
- request.push(...fields.filter(f => f && !f.startsWith('...')));
252
- }
253
-
254
- // Match direct body property access: body.fieldName, req.body.fieldName
255
- const directBodyPattern = /(?:body|req\.body)\.(\w+)/g;
256
- while ((match = directBodyPattern.exec(content)) !== null) {
257
- if (!request.includes(match[1])) {
258
- request.push(match[1]);
259
- }
260
- }
261
-
262
- // Match NextResponse.json({ ... }) or res.json({ ... })
263
- const responsePattern =
264
- /(?:NextResponse\.json|Response\.json|res\.(?:json|send))\s*\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
265
- while ((match = responsePattern.exec(content)) !== null) {
266
- const responseBlock = match[1];
267
- // Extract field names from response object
268
- const fieldPattern = /(\w+)\s*:/g;
269
- let fieldMatch;
270
- while ((fieldMatch = fieldPattern.exec(responseBlock)) !== null) {
271
- if (!response.includes(fieldMatch[1])) {
272
- response.push(fieldMatch[1]);
273
- }
274
- }
275
- }
276
-
277
- // Match Zod schema validation: schema.parse(), schema.safeParse()
278
- const zodPattern = /(\w+)Schema\.(?:parse|safeParse|parseAsync)/g;
279
- while ((match = zodPattern.exec(content)) !== null) {
280
- validations.push(match[1]);
281
- }
282
-
283
- // Match z.object validation inline
284
- const zodInlinePattern = /z\.object\(\s*\{([^}]+)\}\s*\)\.(?:parse|safeParse)/g;
285
- while ((match = zodInlinePattern.exec(content)) !== null) {
286
- validations.push('inline');
287
- }
288
-
289
- return { request, response, validations };
290
- }
291
-
292
- /**
293
- * Extract request/response fields from Express route handler
294
- * @param {string} content
295
- * @returns {{request: string[], response: string[], validations: string[]}}
296
- */
297
- function parseExpressRoute(content) {
298
- const request = [];
299
- const response = [];
300
- const validations = [];
301
-
302
- // Match destructuring from req.body: const { field1, field2 } = req.body
303
- const bodyPattern = /(?:const|let)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
304
- let match;
305
- while ((match = bodyPattern.exec(content)) !== null) {
306
- const fields = match[1].split(',').map(f => f.trim().split(':')[0].trim());
307
- request.push(...fields.filter(f => f && !f.startsWith('...')));
308
- }
309
-
310
- // Match direct req.body property access
311
- const directBodyPattern = /req\.body\.(\w+)/g;
312
- while ((match = directBodyPattern.exec(content)) !== null) {
313
- if (!request.includes(match[1])) {
314
- request.push(match[1]);
315
- }
316
- }
317
-
318
- // Match res.json({ ... }) or res.send({ ... })
319
- const responsePattern = /res\.(?:json|send)\s*\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
320
- while ((match = responsePattern.exec(content)) !== null) {
321
- const responseBlock = match[1];
322
- const fieldPattern = /(\w+)\s*:/g;
323
- let fieldMatch;
324
- while ((fieldMatch = fieldPattern.exec(responseBlock)) !== null) {
325
- if (!response.includes(fieldMatch[1])) {
326
- response.push(fieldMatch[1]);
327
- }
328
- }
329
- }
330
-
331
- // Match Zod/Joi validation
332
- const zodPattern = /(\w+)Schema\.(?:parse|safeParse|validate)/g;
333
- while ((match = zodPattern.exec(content)) !== null) {
334
- validations.push(match[1]);
335
- }
336
-
337
- // Match express-validator
338
- const validatorPattern = /body\(['"](\w+)['"]\)/g;
339
- while ((match = validatorPattern.exec(content)) !== null) {
340
- validations.push(match[1]);
341
- }
342
-
343
- return { request, response, validations };
344
- }
345
-
346
- /**
347
- * Parse API file content and extract request/response fields
348
- * @param {string} content
349
- * @param {string} [framework]
350
- * @returns {{request: string[], response: string[], validations: string[]}}
351
- */
352
- function parseAPIFile(content, framework) {
353
- // Auto-detect framework if not specified
354
- if (!framework) {
355
- if (
356
- content.includes('NextResponse') ||
357
- content.includes('NextRequest') ||
358
- content.includes('export async function')
359
- ) {
360
- framework = 'nextjs';
361
- } else if (
362
- content.includes('express') ||
363
- content.includes('req, res') ||
364
- content.includes('Router')
365
- ) {
366
- framework = 'express';
367
- } else {
368
- framework = 'nextjs'; // Default
369
- }
370
- }
371
-
372
- if (framework === 'nextjs') {
373
- return parseNextJsRoute(content);
374
- } else {
375
- return parseExpressRoute(content);
376
- }
377
- }
378
-
379
- /* ========================================================================= */
380
- /* Public API: generateAPISchema */
381
- /* ========================================================================= */
382
-
383
- /**
384
- * Generate API schema from domain store for an entity
385
- *
386
- * @param {import('n3').Store} domainStore - Domain model RDF store
387
- * @param {string} entity - Entity name to generate schema for
388
- * @param {Object} [options]
389
- * @param {string} [options.baseIri] - Base IRI for domain resources
390
- * @returns {EntitySchema}
391
- */
392
- export function generateAPISchema(domainStore, entity, options = {}) {
393
- const { baseIri = 'http://example.org/unrdf/domain#' } = options;
394
-
395
- const fields = extractEntityFields(domainStore, entity, baseIri);
396
-
397
- // Generate Zod schema string
398
- const fieldDefs = fields.map(f => {
399
- let zodType = xsdToZodType(f.type);
400
- if (f.array) {
401
- zodType = `z.array(${zodType})`;
402
- }
403
- if (f.optional) {
404
- zodType = `${zodType}.optional()`;
405
- }
406
- return ` ${f.name}: ${zodType}`;
407
- });
408
-
409
- const zodSchema = `const ${entity}Schema = z.object({\n${fieldDefs.join(',\n')}\n})`;
410
-
411
- return EntitySchemaSchema.parse({
412
- entityName: entity,
413
- fields,
414
- zodSchema,
415
- });
416
- }
417
-
418
- /**
419
- * Generate API schemas for all entities in domain store
420
- *
421
- * @param {import('n3').Store} domainStore
422
- * @param {Object} [options]
423
- * @param {string} [options.baseIri]
424
- * @returns {EntitySchema[]}
425
- */
426
- export function generateAllAPISchemas(domainStore, options = {}) {
427
- const { baseIri = 'http://example.org/unrdf/domain#' } = options;
428
-
429
- const entityNames = getEntityNames(domainStore, baseIri);
430
-
431
- return entityNames.map(entity => generateAPISchema(domainStore, entity, options));
432
- }
433
-
434
- /* ========================================================================= */
435
- /* Public API: validateAPIFiles */
436
- /* ========================================================================= */
437
-
438
- /**
439
- * Validate API files against expected schema
440
- *
441
- * @param {Array<{path: string, content: string}>} apiFiles - API file contents
442
- * @param {EntitySchema} expectedSchema - Expected schema from domain model
443
- * @param {Object} [options]
444
- * @param {string} [options.framework] - 'nextjs' or 'express'
445
- * @returns {ValidationResult}
446
- */
447
- export function validateAPIFiles(apiFiles, expectedSchema, options = {}) {
448
- const { framework } = options;
449
-
450
- /** @type {Violation[]} */
451
- const violations = [];
452
- const expectedFields = new Set(expectedSchema.fields.map(f => f.name));
453
- const foundRequestFields = new Set();
454
- const foundResponseFields = new Set();
455
- let hasValidation = false;
456
-
457
- for (const file of apiFiles) {
458
- const parsed = parseAPIFile(file.content, framework);
459
-
460
- // Track found fields
461
- for (const field of parsed.request) {
462
- foundRequestFields.add(field);
463
- }
464
- for (const field of parsed.response) {
465
- foundResponseFields.add(field);
466
- }
467
-
468
- // Check for validation
469
- if (parsed.validations.length > 0) {
470
- hasValidation = true;
471
- }
472
-
473
- // Check for extra fields not in schema
474
- for (const field of parsed.request) {
475
- if (!expectedFields.has(field)) {
476
- violations.push({
477
- type: 'extra_field',
478
- field,
479
- expected: 'not defined',
480
- actual: 'present in request',
481
- severity: 'medium',
482
- message: `Field "${field}" in API request is not defined in domain schema`,
483
- });
484
- }
485
- }
486
- }
487
-
488
- // Check for missing required fields
489
- for (const field of expectedSchema.fields) {
490
- if (!field.optional && !foundRequestFields.has(field.name)) {
491
- violations.push({
492
- type: 'missing_field',
493
- field: field.name,
494
- expected: 'required',
495
- actual: 'not found in API',
496
- severity: 'high',
497
- message: `Required field "${field.name}" not found in API request handling`,
498
- });
499
- }
500
- }
501
-
502
- // Check for missing response fields
503
- for (const field of expectedSchema.fields) {
504
- if (!foundResponseFields.has(field.name) && !field.optional) {
505
- violations.push({
506
- type: 'missing_response_field',
507
- field: field.name,
508
- expected: 'in response',
509
- actual: 'not found',
510
- severity: 'medium',
511
- message: `Field "${field.name}" not found in API response`,
512
- });
513
- }
514
- }
515
-
516
- // Check for missing validation
517
- if (!hasValidation && apiFiles.length > 0) {
518
- violations.push({
519
- type: 'missing_validation',
520
- field: 'request body',
521
- expected: 'schema validation',
522
- actual: 'no validation found',
523
- severity: 'high',
524
- message: 'No schema validation found in API handlers',
525
- });
526
- }
527
-
528
- // Calculate coverage
529
- const totalExpectedFields = expectedSchema.fields.length;
530
- const coveredFields = expectedSchema.fields.filter(
531
- f => foundRequestFields.has(f.name) || foundResponseFields.has(f.name)
532
- ).length;
533
- const coverage =
534
- totalExpectedFields > 0 ? Math.round((coveredFields / totalExpectedFields) * 100) : 100;
535
-
536
- // Determine if breaking
537
- const breaking = violations.some(
538
- v => v.severity === 'critical' || (v.severity === 'high' && v.type === 'missing_field')
539
- );
540
-
541
- // Generate summary
542
- const criticalCount = violations.filter(v => v.severity === 'critical').length;
543
- const highCount = violations.filter(v => v.severity === 'high').length;
544
- let summary = `${violations.length} violations found`;
545
- if (criticalCount > 0) {
546
- summary += ` (${criticalCount} critical)`;
547
- } else if (highCount > 0) {
548
- summary += ` (${highCount} high severity)`;
549
- }
550
- if (violations.length === 0) {
551
- summary = 'API contract is valid';
552
- }
553
-
554
- return ValidationResultSchema.parse({
555
- violations,
556
- coverage,
557
- breaking,
558
- summary,
559
- });
560
- }
561
-
562
- /* ========================================================================= */
563
- /* Public API: detectContractBreaks */
564
- /* ========================================================================= */
565
-
566
- /**
567
- * Detect breaking changes between old and new schemas
568
- *
569
- * @param {EntitySchema} oldSchema - Previous version schema
570
- * @param {EntitySchema} newSchema - New version schema
571
- * @param {Array<{path: string, content: string}>} [implementations] - API implementation files
572
- * @returns {ContractBreaks}
573
- */
574
- export function detectContractBreaks(oldSchema, newSchema, implementations = []) {
575
- /** @type {BreakingChange[]} */
576
- const breakingChanges = [];
577
- /** @type {Set<string>} */
578
- const affectedAPIs = new Set();
579
-
580
- const oldFields = new Map(oldSchema.fields.map(f => [f.name, f]));
581
- const newFields = new Map(newSchema.fields.map(f => [f.name, f]));
582
-
583
- // Check for removed fields (breaking)
584
- for (const [fieldName, oldField] of oldFields) {
585
- if (!newFields.has(fieldName)) {
586
- breakingChanges.push({
587
- type: 'field_removed',
588
- entity: oldSchema.entityName,
589
- field: fieldName,
590
- oldValue: oldField.type,
591
- newValue: undefined,
592
- message: `Field "${fieldName}" was removed from ${oldSchema.entityName}`,
593
- });
594
- }
595
- }
596
-
597
- // Check for fields that became required (breaking)
598
- for (const [fieldName, newField] of newFields) {
599
- const oldField = oldFields.get(fieldName);
600
- if (oldField && oldField.optional && !newField.optional) {
601
- breakingChanges.push({
602
- type: 'field_required',
603
- entity: newSchema.entityName,
604
- field: fieldName,
605
- oldValue: 'optional',
606
- newValue: 'required',
607
- message: `Field "${fieldName}" changed from optional to required in ${newSchema.entityName}`,
608
- });
609
- }
610
- }
611
-
612
- // Check for type changes (breaking)
613
- for (const [fieldName, newField] of newFields) {
614
- const oldField = oldFields.get(fieldName);
615
- if (oldField && oldField.type !== newField.type) {
616
- breakingChanges.push({
617
- type: 'type_changed',
618
- entity: newSchema.entityName,
619
- field: fieldName,
620
- oldValue: oldField.type,
621
- newValue: newField.type,
622
- message: `Field "${fieldName}" type changed from ${oldField.type} to ${newField.type} in ${newSchema.entityName}`,
623
- });
624
- }
625
- }
626
-
627
- // Find affected API files
628
- for (const impl of implementations) {
629
- const parsed = parseAPIFile(impl.content);
630
- const allFields = [...parsed.request, ...parsed.response];
631
-
632
- for (const change of breakingChanges) {
633
- if (change.field && allFields.includes(change.field)) {
634
- affectedAPIs.add(impl.path);
635
- }
636
- }
637
- }
638
-
639
- return ContractBreaksSchema.parse({
640
- breakingChanges,
641
- affectedAPIs: Array.from(affectedAPIs),
642
- safe: breakingChanges.length === 0,
643
- });
644
- }
645
-
646
- /**
647
- * Compare two domain stores and detect API contract breaks
648
- *
649
- * @param {import('n3').Store} oldStore - Previous domain model
650
- * @param {import('n3').Store} newStore - New domain model
651
- * @param {Array<{path: string, content: string}>} [implementations] - API files
652
- * @param {Object} [options]
653
- * @param {string} [options.baseIri]
654
- * @returns {Map<string, ContractBreaks>}
655
- */
656
- export function detectAllContractBreaks(oldStore, newStore, implementations = [], options = {}) {
657
- const { baseIri = 'http://example.org/unrdf/domain#' } = options;
658
-
659
- const oldEntities = getEntityNames(oldStore, baseIri);
660
- const newEntities = getEntityNames(newStore, baseIri);
661
-
662
- const allEntities = new Set([...oldEntities, ...newEntities]);
663
- /** @type {Map<string, ContractBreaks>} */
664
- const results = new Map();
665
-
666
- for (const entity of allEntities) {
667
- const oldSchema = oldEntities.includes(entity)
668
- ? generateAPISchema(oldStore, entity, options)
669
- : { entityName: entity, fields: [], zodSchema: '' };
670
-
671
- const newSchema = newEntities.includes(entity)
672
- ? generateAPISchema(newStore, entity, options)
673
- : { entityName: entity, fields: [], zodSchema: '' };
674
-
675
- // Check for entity removal
676
- if (oldEntities.includes(entity) && !newEntities.includes(entity)) {
677
- results.set(entity, {
678
- breakingChanges: [
679
- {
680
- type: 'entity_removed',
681
- entity,
682
- message: `Entity "${entity}" was removed`,
683
- },
684
- ],
685
- affectedAPIs: implementations.map(i => i.path),
686
- safe: false,
687
- });
688
- continue;
689
- }
690
-
691
- const breaks = detectContractBreaks(oldSchema, newSchema, implementations);
692
- if (breaks.breakingChanges.length > 0) {
693
- results.set(entity, breaks);
694
- }
695
- }
696
-
697
- return results;
698
- }
699
-
700
- /* ========================================================================= */
701
- /* Exports for module */
702
- /* ========================================================================= */
703
-
704
- export {
705
- FieldSchemaSchema,
706
- EntitySchemaSchema,
707
- ViolationSchema,
708
- ValidationResultSchema,
709
- BreakingChangeSchema,
710
- ContractBreaksSchema,
711
- };