speckeeper 0.4.1 → 0.6.0

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.
package/dist/dsl/index.js CHANGED
@@ -1,3 +1,633 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join, isAbsolute } from 'path';
4
+ import { glob } from 'glob';
5
+ import { parse } from 'yaml';
6
+ import nodeSqlParser from 'node-sql-parser';
1
7
 
8
+ // src/core/dsl/schema.ts
9
+ var RELATION_TYPES = [
10
+ "dependsOn",
11
+ "uses",
12
+ "includes",
13
+ "implements",
14
+ "refines",
15
+ "verifies",
16
+ "verifiedBy",
17
+ "satisfies",
18
+ "traces",
19
+ "relatedTo"
20
+ ];
21
+ var RelationSchema = z.object({
22
+ /** Relation type */
23
+ type: z.enum(RELATION_TYPES),
24
+ /** Target ID */
25
+ target: z.string(),
26
+ /** Description (optional) */
27
+ description: z.string().optional()
28
+ });
29
+ z.array(RelationSchema).optional();
30
+
31
+ // src/core/dsl/schema.ts
32
+ var baseSpecSchema = z.object({
33
+ id: z.string(),
34
+ name: z.string().min(1),
35
+ description: z.string(),
36
+ relations: z.array(RelationSchema).optional()
37
+ });
38
+
39
+ // src/core/dsl/lint-rules.ts
40
+ function requireField(field, severity = "warning") {
41
+ return {
42
+ id: `has-${field}`,
43
+ severity,
44
+ message: `${field} must not be empty`,
45
+ check: (spec) => {
46
+ const value = spec[field];
47
+ return !value || typeof value === "string" && value.trim() === "";
48
+ }
49
+ };
50
+ }
51
+ function arrayMinLength(field, min, severity = "error") {
52
+ return {
53
+ id: `${field}-min-${min}`,
54
+ severity,
55
+ message: `${field} must have at least ${min} item(s)`,
56
+ check: (spec) => {
57
+ const value = spec[field];
58
+ return !Array.isArray(value) || value.length < min;
59
+ }
60
+ };
61
+ }
62
+ function idFormat(prefix, opts) {
63
+ const pattern = opts?.pattern ?? new RegExp(`^${prefix}-\\d{${opts?.digits ?? 3}}$`);
64
+ return {
65
+ id: "id-format",
66
+ severity: "warning",
67
+ message: `ID should match format ${pattern}`,
68
+ check: (spec) => {
69
+ const id = spec["id"];
70
+ return typeof id !== "string" || !pattern.test(id);
71
+ }
72
+ };
73
+ }
74
+ function childIdFormat(arrayField, idField) {
75
+ return {
76
+ id: `${arrayField}-id-format`,
77
+ severity: "warning",
78
+ message: `${arrayField} IDs should follow parent ID pattern`,
79
+ check: (spec) => {
80
+ const parentId = spec["id"];
81
+ const children = spec[arrayField];
82
+ if (!Array.isArray(children) || !parentId) return false;
83
+ return children.some((child) => {
84
+ const childId = child[idField];
85
+ return typeof childId === "string" && !childId.startsWith(parentId + "-");
86
+ });
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/core/dsl/exporters.ts
92
+ function markdownExporter(config) {
93
+ const exporter = {
94
+ format: "markdown",
95
+ outputDir: config.outputDir,
96
+ filename: (spec) => spec.id
97
+ };
98
+ if (config.single) {
99
+ const singleCfg = config.single;
100
+ exporter.single = (spec) => {
101
+ const lines = [];
102
+ const title = typeof singleCfg.title === "function" ? singleCfg.title(spec) : String(spec[singleCfg.title] ?? "");
103
+ lines.push(`# ${title}`, "");
104
+ if (singleCfg.meta) {
105
+ for (const m of singleCfg.meta) {
106
+ const key = typeof m === "string" ? m : m.key;
107
+ const label = typeof m === "string" ? key : m.label ?? m.key;
108
+ const value = spec[key];
109
+ if (value !== void 0 && value !== null) {
110
+ lines.push(`**${label}**: ${String(value)}`);
111
+ }
112
+ }
113
+ lines.push("");
114
+ }
115
+ if (singleCfg.sections) {
116
+ for (const section of singleCfg.sections) {
117
+ let content;
118
+ if (section.render) {
119
+ content = section.render(spec);
120
+ } else if (section.field) {
121
+ const value = spec[section.field];
122
+ if (typeof value === "string") content = value;
123
+ }
124
+ if (section.optional && !content) continue;
125
+ lines.push(`## ${section.heading}`, "");
126
+ if (content) lines.push(content, "");
127
+ }
128
+ }
129
+ return lines.join("\n");
130
+ };
131
+ }
132
+ if (config.index) {
133
+ const indexCfg = config.index;
134
+ exporter.index = (specs) => {
135
+ const lines = [];
136
+ const title = indexCfg.title ?? "Index";
137
+ lines.push(`# ${title}`, "");
138
+ const columns = indexCfg.columns.map(
139
+ (c) => typeof c === "string" ? { key: c, label: c } : { key: c.key, label: c.label ?? c.key, render: c.render }
140
+ );
141
+ lines.push("| " + columns.map((c) => c.label).join(" | ") + " |");
142
+ lines.push("| " + columns.map(() => "---").join(" | ") + " |");
143
+ for (const spec of specs) {
144
+ const cells = columns.map((col) => {
145
+ const value = spec[col.key];
146
+ if ("render" in col && col.render) return col.render(value, spec);
147
+ if (col.key === "id") return `[${value}](./${value}.md)`;
148
+ return String(value ?? "-");
149
+ });
150
+ lines.push("| " + cells.join(" | ") + " |");
151
+ }
152
+ return lines.join("\n");
153
+ };
154
+ }
155
+ return exporter;
156
+ }
157
+
158
+ // src/core/dsl/type-compat.ts
159
+ var ALIASES = {
160
+ INTEGER: "INT",
161
+ BOOL: "BOOLEAN",
162
+ REAL: "FLOAT",
163
+ "DOUBLE PRECISION": "DOUBLE",
164
+ SERIAL: "INT",
165
+ BIGSERIAL: "BIGINT",
166
+ SMALLSERIAL: "SMALLINT",
167
+ TIMESTAMPTZ: "TIMESTAMP",
168
+ "TIMESTAMP WITH TIME ZONE": "TIMESTAMP",
169
+ "TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
170
+ "CHARACTER VARYING": "VARCHAR",
171
+ CHARACTER: "CHAR",
172
+ INT2: "SMALLINT",
173
+ INT4: "INT",
174
+ INT8: "BIGINT",
175
+ FLOAT4: "FLOAT",
176
+ FLOAT8: "DOUBLE"
177
+ };
178
+ var CONTAINMENT_CHAINS = [
179
+ ["SMALLINT", "INT", "BIGINT"],
180
+ ["FLOAT", "DOUBLE"],
181
+ ["CHAR", "VARCHAR", "TEXT"],
182
+ ["INTEGER", "NUMBER"]
183
+ ];
184
+ function parseType(raw) {
185
+ const trimmed = raw.trim().toUpperCase();
186
+ const match = trimmed.match(/^([A-Z\s]+?)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?$/);
187
+ if (!match) {
188
+ return { base: trimmed };
189
+ }
190
+ const base = match[1].trim();
191
+ return {
192
+ base,
193
+ length: match[2] !== void 0 ? Number(match[2]) : void 0,
194
+ precision: match[2] !== void 0 ? Number(match[2]) : void 0,
195
+ scale: match[3] !== void 0 ? Number(match[3]) : void 0
196
+ };
197
+ }
198
+ function normalize(base) {
199
+ return ALIASES[base] ?? base;
200
+ }
201
+ function findAllChainPositions(base, chains) {
202
+ const results = [];
203
+ const resolved = normalize(base);
204
+ for (const chain of chains) {
205
+ let idx = chain.indexOf(base);
206
+ if (idx === -1) idx = chain.indexOf(resolved);
207
+ if (idx !== -1) results.push({ chain, index: idx });
208
+ }
209
+ return results;
210
+ }
211
+ function isTypeContainedBy(specType, ddlType) {
212
+ const spec = parseType(specType);
213
+ const ddl = parseType(ddlType);
214
+ const specNorm = normalize(spec.base);
215
+ const ddlNorm = normalize(ddl.base);
216
+ if (specNorm === ddlNorm) {
217
+ if (specNorm === "DECIMAL" || specNorm === "NUMERIC") {
218
+ if (spec.precision !== void 0 && ddl.precision !== void 0) {
219
+ if (ddl.precision < spec.precision) return false;
220
+ }
221
+ if (spec.scale !== void 0 && ddl.scale !== void 0) {
222
+ if (ddl.scale < spec.scale) return false;
223
+ }
224
+ return true;
225
+ }
226
+ if (spec.length !== void 0 && ddl.length !== void 0) {
227
+ return ddl.length >= spec.length;
228
+ }
229
+ return true;
230
+ }
231
+ const specPositions = findAllChainPositions(spec.base, CONTAINMENT_CHAINS);
232
+ const ddlPositions = findAllChainPositions(ddl.base, CONTAINMENT_CHAINS);
233
+ for (const sp of specPositions) {
234
+ for (const dp of ddlPositions) {
235
+ if (sp.chain === dp.chain) {
236
+ return dp.index >= sp.index;
237
+ }
238
+ }
239
+ }
240
+ return false;
241
+ }
242
+
243
+ // src/core/dsl/checkers.ts
244
+ function testChecker(config) {
245
+ return {
246
+ targetType: "test",
247
+ sourcePath: config?.sourcePath ?? (() => "."),
248
+ check: (spec) => {
249
+ const errors = [];
250
+ const warnings = [];
251
+ const basePath = process.cwd();
252
+ const testPatterns = [
253
+ "test/**/*.test.ts",
254
+ "test/**/*.spec.ts",
255
+ "tests/**/*.test.ts",
256
+ "tests/**/*.spec.ts"
257
+ ];
258
+ let testFiles = [];
259
+ for (const pattern of testPatterns) {
260
+ testFiles = testFiles.concat(glob.sync(pattern, { cwd: basePath }));
261
+ }
262
+ if (testFiles.length === 0) {
263
+ warnings.push({ message: `No test files found for ${spec.id}`, specId: spec.id });
264
+ return { success: true, errors, warnings };
265
+ }
266
+ let specIdFound = false;
267
+ for (const testFile of testFiles) {
268
+ const fullPath = join(basePath, testFile);
269
+ try {
270
+ const content = readFileSync(fullPath, "utf-8");
271
+ const patterns = [
272
+ new RegExp(`describe\\s*\\(\\s*['"\`].*${spec.id}`, "m"),
273
+ new RegExp(`it\\s*\\(\\s*['"\`].*${spec.id}`, "m"),
274
+ new RegExp(`test\\s*\\(\\s*['"\`].*${spec.id}`, "m")
275
+ ];
276
+ if (patterns.some((p) => p.test(content))) {
277
+ specIdFound = true;
278
+ break;
279
+ }
280
+ } catch {
281
+ }
282
+ }
283
+ if (!specIdFound) {
284
+ warnings.push({
285
+ message: `Spec ID "${spec.id}" not found in any test file`,
286
+ specId: spec.id
287
+ });
288
+ }
289
+ return { success: errors.length === 0, errors, warnings };
290
+ }
291
+ };
292
+ }
293
+ function resolveRef(doc, ref) {
294
+ if (!ref.startsWith("#/")) return void 0;
295
+ const parts = ref.substring(2).split("/");
296
+ let current = doc;
297
+ for (const part of parts) {
298
+ if (current == null || typeof current !== "object") return void 0;
299
+ current = current[part];
300
+ }
301
+ return current;
302
+ }
303
+ function maybeResolveRef(doc, value) {
304
+ if (value && typeof value === "object" && typeof value.$ref === "string") {
305
+ return resolveRef(doc, value.$ref);
306
+ }
307
+ return value;
308
+ }
309
+ function findSpecIdInOpenAPI(doc, specId) {
310
+ const paths = doc.paths;
311
+ if (!paths || typeof paths !== "object") return null;
312
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
313
+ if (pathItem == null || typeof pathItem !== "object") continue;
314
+ const methods = ["get", "post", "put", "delete", "patch", "options", "head"];
315
+ for (const method of methods) {
316
+ const op = pathItem[method];
317
+ if (!op) continue;
318
+ if (op.operationId === specId) return { pathKey, operation: op, method };
319
+ if (op["x-spec-id"] === specId) return { pathKey, operation: op, method };
320
+ }
321
+ }
322
+ for (const pathKey of Object.keys(paths)) {
323
+ const segments = pathKey.split("/").filter(Boolean);
324
+ const cleanSegments = segments.map((s) => s.replace(/^\{|\}$/g, ""));
325
+ if (cleanSegments.includes(specId)) return { pathKey };
326
+ }
327
+ const schemas = doc.components?.schemas;
328
+ if (schemas && typeof schemas === "object") {
329
+ if (specId in schemas) return { pathKey: `#/components/schemas/${specId}` };
330
+ }
331
+ function searchXSpecId(obj) {
332
+ if (obj == null || typeof obj !== "object") return false;
333
+ if (obj["x-spec-id"] === specId) return true;
334
+ for (const val of Object.values(obj)) {
335
+ if (searchXSpecId(val)) return true;
336
+ }
337
+ return false;
338
+ }
339
+ if (searchXSpecId(doc)) return { pathKey: "(x-spec-id match)" };
340
+ return null;
341
+ }
342
+ function externalOpenAPIChecker(config) {
343
+ return {
344
+ targetType: "openapi",
345
+ sourcePath: config?.sourcePath ?? (() => "api/openapi.yaml"),
346
+ check: (spec) => {
347
+ const errors = [];
348
+ const warnings = [];
349
+ const filePath = (config?.sourcePath ?? (() => "api/openapi.yaml"))(spec);
350
+ const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
351
+ if (!existsSync(fullPath)) {
352
+ errors.push({ message: `OpenAPI file not found: ${filePath}`, specId: spec.id });
353
+ return { success: false, errors, warnings };
354
+ }
355
+ let content;
356
+ try {
357
+ content = readFileSync(fullPath, "utf-8");
358
+ } catch {
359
+ errors.push({ message: `Failed to read OpenAPI file: ${filePath}`, specId: spec.id });
360
+ return { success: false, errors, warnings };
361
+ }
362
+ if (content.length === 0) {
363
+ errors.push({ message: `OpenAPI file is empty: ${filePath}`, specId: spec.id });
364
+ return { success: false, errors, warnings };
365
+ }
366
+ let doc;
367
+ try {
368
+ if (fullPath.endsWith(".json")) {
369
+ doc = JSON.parse(content);
370
+ } else {
371
+ doc = parse(content);
372
+ }
373
+ } catch (e) {
374
+ const msg = e instanceof Error ? e.message : String(e);
375
+ errors.push({ message: `Failed to parse OpenAPI file ${filePath}: ${msg}`, specId: spec.id });
376
+ return { success: false, errors, warnings };
377
+ }
378
+ const match = findSpecIdInOpenAPI(doc, spec.id);
379
+ if (!match) {
380
+ warnings.push({ message: `Spec ID "${spec.id}" not found in OpenAPI document ${filePath}`, specId: spec.id });
381
+ return { success: true, errors, warnings };
382
+ }
383
+ if (!config?.mapper) {
384
+ return { success: errors.length === 0, errors, warnings };
385
+ }
386
+ const mapping = config.mapper(spec);
387
+ if (mapping.method && match.operation) {
388
+ const expectedMethod = mapping.method.toLowerCase();
389
+ const actualMethod = match.method?.toLowerCase();
390
+ if (actualMethod && actualMethod !== expectedMethod) {
391
+ warnings.push({
392
+ message: `Method mismatch for "${spec.id}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
393
+ specId: spec.id
394
+ });
395
+ }
396
+ }
397
+ if (match.operation && mapping.parameters) {
398
+ const opParams = (match.operation.parameters || []).map(
399
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
400
+ (p) => maybeResolveRef(doc, p)
401
+ );
402
+ for (const expectedParam of mapping.parameters) {
403
+ const found = opParams.find(
404
+ (p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
405
+ );
406
+ if (!found) {
407
+ warnings.push({
408
+ message: `Parameter "${expectedParam.name}" not found in operation "${spec.id}"`,
409
+ specId: spec.id,
410
+ field: expectedParam.name
411
+ });
412
+ } else if (expectedParam.type && found.schema?.type) {
413
+ if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
414
+ warnings.push({
415
+ message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
416
+ specId: spec.id,
417
+ field: expectedParam.name
418
+ });
419
+ }
420
+ }
421
+ }
422
+ }
423
+ if (match.operation && mapping.responseProperties) {
424
+ const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
425
+ let responseSchema = successResponse?.content?.["application/json"]?.schema;
426
+ responseSchema = maybeResolveRef(doc, responseSchema);
427
+ if (responseSchema?.type === "array") {
428
+ responseSchema = maybeResolveRef(doc, responseSchema.items);
429
+ }
430
+ const responseProps = responseSchema?.properties || {};
431
+ for (const expectedProp of mapping.responseProperties) {
432
+ const foundProp = responseProps[expectedProp.name];
433
+ if (!foundProp) {
434
+ warnings.push({
435
+ message: `Response property "${expectedProp.name}" not found in operation "${spec.id}"`,
436
+ specId: spec.id,
437
+ field: expectedProp.name
438
+ });
439
+ } else if (expectedProp.type && foundProp.type) {
440
+ if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
441
+ warnings.push({
442
+ message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
443
+ specId: spec.id,
444
+ field: expectedProp.name
445
+ });
446
+ }
447
+ }
448
+ }
449
+ }
450
+ return { success: errors.length === 0, errors, warnings };
451
+ }
452
+ };
453
+ }
454
+ function parseDDLWithParser(content) {
455
+ const parser = new nodeSqlParser.Parser();
456
+ const tables = [];
457
+ try {
458
+ const ast = parser.astify(content);
459
+ const statements = Array.isArray(ast) ? ast : [ast];
460
+ for (const stmt of statements) {
461
+ if (stmt.type !== "create" || stmt.keyword !== "table") continue;
462
+ const tableName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
463
+ const columns = (stmt.create_definitions || []).filter((def) => def.resource === "column").map((def) => {
464
+ const colName = def.column?.column ?? "";
465
+ let colType = def.definition?.dataType ?? "";
466
+ if (def.definition?.length) {
467
+ colType += `(${def.definition.length})`;
468
+ }
469
+ if (def.definition?.scale) {
470
+ colType = colType.replace(/\)$/, `,${def.definition.scale})`);
471
+ }
472
+ return { name: colName, type: colType };
473
+ });
474
+ if (tableName) {
475
+ tables.push({ name: tableName, columns });
476
+ }
477
+ }
478
+ return tables.length > 0 ? tables : null;
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+ function parseDDLWithRegex(content) {
484
+ const tables = [];
485
+ const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|"|')?(\w+)(?:`|"|')?\s*\(([\s\S]*?)\);/gi;
486
+ let match;
487
+ while ((match = tableRegex.exec(content)) !== null) {
488
+ const tableName = match[1];
489
+ const body = match[2];
490
+ const columns = [];
491
+ const lines = body.split(",").map((l) => l.trim()).filter(Boolean);
492
+ for (const line of lines) {
493
+ if (/^\s*(PRIMARY\s+KEY|UNIQUE|CHECK|CONSTRAINT|INDEX|FOREIGN\s+KEY)/i.test(line)) continue;
494
+ const colMatch = line.match(/^(?:`|"|')?(\w+)(?:`|"|')?\s+(\w+(?:\([^)]*\))?)/i);
495
+ if (colMatch) {
496
+ columns.push({ name: colMatch[1], type: colMatch[2] });
497
+ }
498
+ }
499
+ if (tableName) {
500
+ tables.push({ name: tableName, columns });
501
+ }
502
+ }
503
+ return tables;
504
+ }
505
+ function externalSqlSchemaChecker(config) {
506
+ return {
507
+ targetType: "ddl",
508
+ sourcePath: config?.sourcePath ?? (() => "db/schema.sql"),
509
+ check: (spec) => {
510
+ const errors = [];
511
+ const warnings = [];
512
+ const filePath = (config?.sourcePath ?? (() => "db/schema.sql"))(spec);
513
+ const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
514
+ if (!existsSync(fullPath)) {
515
+ errors.push({ message: `DDL file not found: ${filePath}`, specId: spec.id });
516
+ return { success: false, errors, warnings };
517
+ }
518
+ let content;
519
+ try {
520
+ content = readFileSync(fullPath, "utf-8");
521
+ } catch {
522
+ errors.push({ message: `Failed to read DDL file: ${filePath}`, specId: spec.id });
523
+ return { success: false, errors, warnings };
524
+ }
525
+ if (content.length === 0) {
526
+ errors.push({ message: `DDL file is empty: ${filePath}`, specId: spec.id });
527
+ return { success: false, errors, warnings };
528
+ }
529
+ let tables = parseDDLWithParser(content);
530
+ if (!tables) {
531
+ warnings.push({ message: `DDL parser failed for ${filePath}, using regex fallback`, specId: spec.id });
532
+ tables = parseDDLWithRegex(content);
533
+ }
534
+ if (!config?.mapper) {
535
+ return { success: errors.length === 0, errors, warnings };
536
+ }
537
+ const mapping = config.mapper(spec);
538
+ const table = tables.find(
539
+ (t) => t.name.toLowerCase() === mapping.tableName.toLowerCase()
540
+ );
541
+ if (!table) {
542
+ warnings.push({
543
+ message: `Table "${mapping.tableName}" not found in DDL file ${filePath}`,
544
+ specId: spec.id
545
+ });
546
+ return { success: errors.length === 0, errors, warnings };
547
+ }
548
+ if (!mapping.columns) {
549
+ return { success: errors.length === 0, errors, warnings };
550
+ }
551
+ for (const expectedCol of mapping.columns) {
552
+ const foundCol = table.columns.find(
553
+ (c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
554
+ );
555
+ if (!foundCol) {
556
+ warnings.push({
557
+ message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
558
+ specId: spec.id,
559
+ field: expectedCol.name
560
+ });
561
+ continue;
562
+ }
563
+ if (config.checkTypes && expectedCol.type && foundCol.type) {
564
+ if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
565
+ warnings.push({
566
+ message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
567
+ specId: spec.id,
568
+ field: expectedCol.name
569
+ });
570
+ }
571
+ }
572
+ }
573
+ return { success: errors.length === 0, errors, warnings };
574
+ }
575
+ };
576
+ }
577
+ function relationCoverage(config) {
578
+ return {
579
+ targetModel: config.targetModel,
580
+ description: config.description,
581
+ check: (specs, registry) => {
582
+ const targets = registry[config.targetModel];
583
+ if (!targets) {
584
+ return { total: 0, covered: 0, uncovered: 0, coveragePercent: 100, coveredItems: [], uncoveredItems: [] };
585
+ }
586
+ const allTargetIds = /* @__PURE__ */ new Set();
587
+ const targetDescriptions = /* @__PURE__ */ new Map();
588
+ for (const t of targets.values()) {
589
+ if (config.targetPrefix && !t.id.startsWith(config.targetPrefix)) continue;
590
+ allTargetIds.add(t.id);
591
+ targetDescriptions.set(t.id, t.name ?? t.description ?? t.id);
592
+ }
593
+ const coveredIds = /* @__PURE__ */ new Set();
594
+ for (const spec of specs) {
595
+ if (!spec.relations) continue;
596
+ for (const rel of spec.relations) {
597
+ if (config.relationType && rel.type !== config.relationType) continue;
598
+ if (allTargetIds.has(rel.target)) coveredIds.add(rel.target);
599
+ }
600
+ }
601
+ const coveredItems = [];
602
+ const uncoveredItems = [];
603
+ for (const id of allTargetIds) {
604
+ const desc = targetDescriptions.get(id);
605
+ if (coveredIds.has(id)) {
606
+ coveredItems.push({ id, description: desc });
607
+ } else {
608
+ uncoveredItems.push({ id, description: desc });
609
+ }
610
+ }
611
+ const total = allTargetIds.size;
612
+ const covered = coveredItems.length;
613
+ const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
614
+ return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
615
+ }
616
+ };
617
+ }
618
+ var ImplementsRelationSchema = z.object({
619
+ target: z.string(),
620
+ targetType: z.string(),
621
+ sourcePath: z.string().optional(),
622
+ description: z.string().optional()
623
+ });
624
+ var VerifiedByRelationSchema = z.object({
625
+ target: z.string(),
626
+ testPath: z.string().optional(),
627
+ framework: z.string().optional(),
628
+ description: z.string().optional()
629
+ });
630
+
631
+ export { ImplementsRelationSchema, VerifiedByRelationSchema, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, testChecker };
2
632
  //# sourceMappingURL=index.js.map
3
633
  //# sourceMappingURL=index.js.map