speckeeper 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/dsl/index.js CHANGED
@@ -154,142 +154,6 @@ function markdownExporter(config) {
154
154
  }
155
155
  return exporter;
156
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
157
  function relationTypeToAnnotation(relationType) {
294
158
  return relationType === "verifiedBy" ? "verifies" : relationType;
295
159
  }
@@ -307,106 +171,6 @@ function setArtifactsConfig(config) {
307
171
  function getArtifactsConfig() {
308
172
  return _artifactsConfig;
309
173
  }
310
- function runAnnotationScan(specId, artifact, relationType, contentPatterns, basePath) {
311
- const matchedFiles = [];
312
- const warnings = [];
313
- const artifactsConfig = getArtifactsConfig();
314
- const artifactConfig = artifactsConfig?.[artifact];
315
- if (!artifactConfig) {
316
- warnings.push({
317
- message: `Artifact config "${artifact}" not found; set artifacts in config and call setArtifactsConfig()`,
318
- specId
319
- });
320
- return { matchedFiles, warnings };
321
- }
322
- const allFiles = /* @__PURE__ */ new Set();
323
- for (const pattern of artifactConfig.globs) {
324
- const found = glob.sync(pattern, {
325
- cwd: basePath,
326
- ignore: artifactConfig.exclude ?? []
327
- });
328
- for (const f of found) {
329
- allFiles.add(f);
330
- }
331
- }
332
- const patterns = contentPatterns.length > 0 ? contentPatterns : [getDefaultPatternForRelation(relationType)];
333
- for (const filePath of allFiles) {
334
- const fullPath = join(basePath, filePath);
335
- let content;
336
- try {
337
- content = readFileSync(fullPath, "utf-8");
338
- } catch {
339
- continue;
340
- }
341
- const lines = content.split("\n");
342
- for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
343
- const line = lines[lineNum - 1];
344
- for (const re of patterns) {
345
- const copy = new RegExp(re.source, re.flags);
346
- let m;
347
- while ((m = copy.exec(line)) !== null) {
348
- const ids = extractSpecIds(m[1] ?? "");
349
- if (ids.includes(specId)) {
350
- matchedFiles.push({ specId, filePath, line: lineNum, relationType });
351
- }
352
- }
353
- }
354
- }
355
- }
356
- return { matchedFiles, warnings };
357
- }
358
- function annotationChecker(config) {
359
- return {
360
- targetType: "annotation",
361
- sourcePath: () => ".",
362
- check: (spec, _externalData) => {
363
- const errors = [];
364
- const warnings = [];
365
- const allMatchedFiles = [];
366
- const basePath = process.cwd();
367
- const checks = config?.artifact && config?.relationType ? [
368
- {
369
- artifact: config.artifact,
370
- relationType: config.relationType,
371
- contentPatterns: config.contentPatterns
372
- }
373
- ] : config?.checks ?? [];
374
- for (const entry of checks) {
375
- if (entry.checker) {
376
- const result = entry.checker.check(spec, void 0);
377
- errors.push(...result.errors);
378
- warnings.push(...result.warnings);
379
- if (result.matchedFiles) {
380
- allMatchedFiles.push(...result.matchedFiles);
381
- }
382
- } else {
383
- const patterns = entry.contentPatterns ?? getArtifactsConfig()?.[entry.artifact]?.contentPatterns ?? [];
384
- const { matchedFiles, warnings: w } = runAnnotationScan(
385
- spec.id,
386
- entry.artifact,
387
- entry.relationType,
388
- Array.isArray(patterns) ? patterns : [patterns],
389
- basePath
390
- );
391
- warnings.push(...w);
392
- allMatchedFiles.push(...matchedFiles);
393
- }
394
- }
395
- if (checks.length > 0 && allMatchedFiles.length === 0) {
396
- warnings.push({
397
- message: `No annotation matches found for spec "${spec.id}"`,
398
- specId: spec.id
399
- });
400
- }
401
- return {
402
- success: errors.length === 0,
403
- errors,
404
- warnings,
405
- matchedFiles: allMatchedFiles.length > 0 ? allMatchedFiles : void 0
406
- };
407
- }
408
- };
409
- }
410
174
  function annotationCoverage(config) {
411
175
  return {
412
176
  targetModel: "annotation",
@@ -479,6 +243,144 @@ function annotationCoverage(config) {
479
243
  }
480
244
  };
481
245
  }
246
+ function relationCoverage(config) {
247
+ return {
248
+ targetModel: config.targetModel,
249
+ description: config.description,
250
+ check: (specs, registry) => {
251
+ const targets = registry[config.targetModel];
252
+ if (!targets) {
253
+ return { total: 0, covered: 0, uncovered: 0, coveragePercent: 100, coveredItems: [], uncoveredItems: [] };
254
+ }
255
+ const allTargetIds = /* @__PURE__ */ new Set();
256
+ const targetDescriptions = /* @__PURE__ */ new Map();
257
+ for (const t of targets.values()) {
258
+ if (config.targetPrefix && !t.id.startsWith(config.targetPrefix)) continue;
259
+ allTargetIds.add(t.id);
260
+ targetDescriptions.set(t.id, t.name ?? t.description ?? t.id);
261
+ }
262
+ const coveredIds = /* @__PURE__ */ new Set();
263
+ for (const spec of specs) {
264
+ if (!spec.relations) continue;
265
+ for (const rel of spec.relations) {
266
+ if (config.relationType && rel.type !== config.relationType) continue;
267
+ if (allTargetIds.has(rel.target)) coveredIds.add(rel.target);
268
+ }
269
+ }
270
+ const coveredItems = [];
271
+ const uncoveredItems = [];
272
+ for (const id of allTargetIds) {
273
+ const desc = targetDescriptions.get(id);
274
+ if (coveredIds.has(id)) {
275
+ coveredItems.push({ id, description: desc });
276
+ } else {
277
+ uncoveredItems.push({ id, description: desc });
278
+ }
279
+ }
280
+ const total = allTargetIds.size;
281
+ const covered = coveredItems.length;
282
+ const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
283
+ return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
284
+ }
285
+ };
286
+ }
287
+
288
+ // src/core/dsl/type-compat.ts
289
+ var ALIASES = {
290
+ INTEGER: "INT",
291
+ BOOL: "BOOLEAN",
292
+ REAL: "FLOAT",
293
+ "DOUBLE PRECISION": "DOUBLE",
294
+ SERIAL: "INT",
295
+ BIGSERIAL: "BIGINT",
296
+ SMALLSERIAL: "SMALLINT",
297
+ TIMESTAMPTZ: "TIMESTAMP",
298
+ "TIMESTAMP WITH TIME ZONE": "TIMESTAMP",
299
+ "TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
300
+ "CHARACTER VARYING": "VARCHAR",
301
+ CHARACTER: "CHAR",
302
+ INT2: "SMALLINT",
303
+ INT4: "INT",
304
+ INT8: "BIGINT",
305
+ FLOAT4: "FLOAT",
306
+ FLOAT8: "DOUBLE"
307
+ };
308
+ var CONTAINMENT_CHAINS = [
309
+ ["SMALLINT", "INT", "BIGINT"],
310
+ ["FLOAT", "DOUBLE"],
311
+ ["CHAR", "VARCHAR", "TEXT"],
312
+ ["INTEGER", "NUMBER"]
313
+ ];
314
+ function parseType(raw) {
315
+ const trimmed = raw.trim().toUpperCase();
316
+ const match = trimmed.match(/^([A-Z\s]+?)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?$/);
317
+ if (!match) {
318
+ return { base: trimmed };
319
+ }
320
+ const base = match[1].trim();
321
+ return {
322
+ base,
323
+ length: match[2] !== void 0 ? Number(match[2]) : void 0,
324
+ precision: match[2] !== void 0 ? Number(match[2]) : void 0,
325
+ scale: match[3] !== void 0 ? Number(match[3]) : void 0
326
+ };
327
+ }
328
+ function normalize(base) {
329
+ return ALIASES[base] ?? base;
330
+ }
331
+ function findAllChainPositions(base, chains) {
332
+ const results = [];
333
+ const resolved = normalize(base);
334
+ for (const chain of chains) {
335
+ let idx = chain.indexOf(base);
336
+ if (idx === -1) idx = chain.indexOf(resolved);
337
+ if (idx !== -1) results.push({ chain, index: idx });
338
+ }
339
+ return results;
340
+ }
341
+ function isTypeContainedBy(specType, ddlType) {
342
+ const spec = parseType(specType);
343
+ const ddl = parseType(ddlType);
344
+ const specNorm = normalize(spec.base);
345
+ const ddlNorm = normalize(ddl.base);
346
+ if (specNorm === ddlNorm) {
347
+ if (specNorm === "DECIMAL" || specNorm === "NUMERIC") {
348
+ if (spec.precision !== void 0 && ddl.precision !== void 0) {
349
+ if (ddl.precision < spec.precision) return false;
350
+ }
351
+ if (spec.scale !== void 0 && ddl.scale !== void 0) {
352
+ if (ddl.scale < spec.scale) return false;
353
+ }
354
+ return true;
355
+ }
356
+ if (spec.length !== void 0 && ddl.length !== void 0) {
357
+ return ddl.length >= spec.length;
358
+ }
359
+ return true;
360
+ }
361
+ const specPositions = findAllChainPositions(spec.base, CONTAINMENT_CHAINS);
362
+ const ddlPositions = findAllChainPositions(ddl.base, CONTAINMENT_CHAINS);
363
+ for (const sp of specPositions) {
364
+ for (const dp of ddlPositions) {
365
+ if (sp.chain === dp.chain) {
366
+ return dp.index >= sp.index;
367
+ }
368
+ }
369
+ }
370
+ return false;
371
+ }
372
+ var ImplementsRelationSchema = z.object({
373
+ target: z.string(),
374
+ targetType: z.string(),
375
+ sourcePath: z.string().optional(),
376
+ description: z.string().optional()
377
+ });
378
+ var VerifiedByRelationSchema = z.object({
379
+ target: z.string(),
380
+ testPath: z.string().optional(),
381
+ framework: z.string().optional(),
382
+ description: z.string().optional()
383
+ });
482
384
  function resolveRef(doc, ref) {
483
385
  if (!ref.startsWith("#/")) return void 0;
484
386
  const parts = ref.substring(2).split("/");
@@ -528,118 +430,24 @@ function findSpecIdInOpenAPI(doc, specId) {
528
430
  if (searchXSpecId(doc)) return { pathKey: "(x-spec-id match)" };
529
431
  return null;
530
432
  }
531
- function externalOpenAPIChecker(config) {
532
- return {
533
- targetType: "openapi",
534
- sourcePath: config?.sourcePath ?? (() => "api/openapi.yaml"),
535
- check: (spec) => {
536
- const errors = [];
537
- const warnings = [];
538
- const filePath = (config?.sourcePath ?? (() => "api/openapi.yaml"))(spec);
539
- const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
540
- if (!existsSync(fullPath)) {
541
- errors.push({ message: `OpenAPI file not found: ${filePath}`, specId: spec.id });
542
- return { success: false, errors, warnings };
543
- }
544
- let content;
545
- try {
546
- content = readFileSync(fullPath, "utf-8");
547
- } catch {
548
- errors.push({ message: `Failed to read OpenAPI file: ${filePath}`, specId: spec.id });
549
- return { success: false, errors, warnings };
550
- }
551
- if (content.length === 0) {
552
- errors.push({ message: `OpenAPI file is empty: ${filePath}`, specId: spec.id });
553
- return { success: false, errors, warnings };
554
- }
555
- let doc;
556
- try {
557
- if (fullPath.endsWith(".json")) {
558
- doc = JSON.parse(content);
559
- } else {
560
- doc = parse(content);
561
- }
562
- } catch (e) {
563
- const msg = e instanceof Error ? e.message : String(e);
564
- errors.push({ message: `Failed to parse OpenAPI file ${filePath}: ${msg}`, specId: spec.id });
565
- return { success: false, errors, warnings };
566
- }
567
- const match = findSpecIdInOpenAPI(doc, spec.id);
568
- if (!match) {
569
- warnings.push({ message: `Spec ID "${spec.id}" not found in OpenAPI document ${filePath}`, specId: spec.id });
570
- return { success: true, errors, warnings };
571
- }
572
- if (!config?.mapper) {
573
- return { success: errors.length === 0, errors, warnings };
574
- }
575
- const mapping = config.mapper(spec);
576
- if (mapping.method && match.operation) {
577
- const expectedMethod = mapping.method.toLowerCase();
578
- const actualMethod = match.method?.toLowerCase();
579
- if (actualMethod && actualMethod !== expectedMethod) {
580
- warnings.push({
581
- message: `Method mismatch for "${spec.id}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
582
- specId: spec.id
583
- });
584
- }
585
- }
586
- if (match.operation && mapping.parameters) {
587
- const opParams = (match.operation.parameters || []).map(
588
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
589
- (p) => maybeResolveRef(doc, p)
590
- );
591
- for (const expectedParam of mapping.parameters) {
592
- const found = opParams.find(
593
- (p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
594
- );
595
- if (!found) {
596
- warnings.push({
597
- message: `Parameter "${expectedParam.name}" not found in operation "${spec.id}"`,
598
- specId: spec.id,
599
- field: expectedParam.name
600
- });
601
- } else if (expectedParam.type && found.schema?.type) {
602
- if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
603
- warnings.push({
604
- message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
605
- specId: spec.id,
606
- field: expectedParam.name
607
- });
608
- }
609
- }
610
- }
611
- }
612
- if (match.operation && mapping.responseProperties) {
613
- const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
614
- let responseSchema = successResponse?.content?.["application/json"]?.schema;
615
- responseSchema = maybeResolveRef(doc, responseSchema);
616
- if (responseSchema?.type === "array") {
617
- responseSchema = maybeResolveRef(doc, responseSchema.items);
618
- }
619
- const responseProps = responseSchema?.properties || {};
620
- for (const expectedProp of mapping.responseProperties) {
621
- const foundProp = responseProps[expectedProp.name];
622
- if (!foundProp) {
623
- warnings.push({
624
- message: `Response property "${expectedProp.name}" not found in operation "${spec.id}"`,
625
- specId: spec.id,
626
- field: expectedProp.name
627
- });
628
- } else if (expectedProp.type && foundProp.type) {
629
- if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
630
- warnings.push({
631
- message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
632
- specId: spec.id,
633
- field: expectedProp.name
634
- });
635
- }
636
- }
637
- }
433
+ var openapiScanner = {
434
+ findSpecIds(content, specIds, filePath) {
435
+ const doc = content;
436
+ if (!doc || typeof doc !== "object") return [];
437
+ const matches = [];
438
+ for (const specId of specIds) {
439
+ const match = findSpecIdInOpenAPI(doc, specId);
440
+ if (match) {
441
+ matches.push({
442
+ specId,
443
+ location: `${filePath}:${match.pathKey}`,
444
+ context: { doc, match }
445
+ });
638
446
  }
639
- return { success: errors.length === 0, errors, warnings };
640
447
  }
641
- };
642
- }
448
+ return matches;
449
+ }
450
+ };
643
451
  function stripSchemaPrefix(name) {
644
452
  const dotIndex = name.lastIndexOf(".");
645
453
  return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
@@ -696,132 +504,305 @@ function parseDDLWithRegex(content) {
696
504
  }
697
505
  return tables;
698
506
  }
699
- function externalSqlSchemaChecker(config) {
700
- return {
701
- targetType: "ddl",
702
- sourcePath: config?.sourcePath ?? (() => "db/schema.sql"),
703
- check: (spec) => {
704
- const errors = [];
705
- const warnings = [];
706
- const filePath = (config?.sourcePath ?? (() => "db/schema.sql"))(spec);
707
- const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
708
- if (!existsSync(fullPath)) {
709
- errors.push({ message: `DDL file not found: ${filePath}`, specId: spec.id });
710
- return { success: false, errors, warnings };
711
- }
712
- let content;
713
- try {
714
- content = readFileSync(fullPath, "utf-8");
715
- } catch {
716
- errors.push({ message: `Failed to read DDL file: ${filePath}`, specId: spec.id });
717
- return { success: false, errors, warnings };
718
- }
719
- if (content.length === 0) {
720
- errors.push({ message: `DDL file is empty: ${filePath}`, specId: spec.id });
721
- return { success: false, errors, warnings };
722
- }
723
- let tables = parseDDLWithParser(content);
724
- if (!tables) {
725
- warnings.push({ message: `DDL parser failed for ${filePath}, using regex fallback`, specId: spec.id });
726
- tables = parseDDLWithRegex(content);
727
- }
728
- if (!config?.mapper) {
729
- return { success: errors.length === 0, errors, warnings };
730
- }
731
- const mapping = config.mapper(spec);
507
+ var ddlScanner = {
508
+ findSpecIds(content, specIds, filePath) {
509
+ if (typeof content !== "string") return [];
510
+ let tables = parseDDLWithParser(content);
511
+ if (!tables) {
512
+ tables = parseDDLWithRegex(content);
513
+ }
514
+ const matches = [];
515
+ for (const specId of specIds) {
732
516
  const table = tables.find(
733
- (t) => t.name.toLowerCase() === mapping.tableName.toLowerCase()
517
+ (t) => t.name.toLowerCase() === specId.toLowerCase()
734
518
  );
735
- if (!table) {
736
- warnings.push({
737
- message: `Table "${mapping.tableName}" not found in DDL file ${filePath}`,
738
- specId: spec.id
519
+ if (table) {
520
+ matches.push({
521
+ specId,
522
+ location: `${filePath}:${table.name}`,
523
+ context: { table }
739
524
  });
740
- return { success: errors.length === 0, errors, warnings };
741
525
  }
742
- if (!mapping.columns) {
743
- return { success: errors.length === 0, errors, warnings };
744
- }
745
- for (const expectedCol of mapping.columns) {
746
- const foundCol = table.columns.find(
747
- (c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
748
- );
749
- if (!foundCol) {
750
- warnings.push({
751
- message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
752
- specId: spec.id,
753
- field: expectedCol.name
754
- });
755
- continue;
756
- }
757
- if (config.checkTypes && expectedCol.type && foundCol.type) {
758
- if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
759
- warnings.push({
760
- message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
761
- specId: spec.id,
762
- field: expectedCol.name
526
+ }
527
+ return matches;
528
+ }
529
+ };
530
+ function extractSpecIds2(captured) {
531
+ return captured.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
532
+ }
533
+ var annotationScanner = {
534
+ findSpecIds(content, specIds, filePath) {
535
+ if (typeof content !== "string") return [];
536
+ const specIdSet = new Set(specIds);
537
+ const matches = [];
538
+ const defaultPattern = /@(?:verifies|implements|traces)\s+([\w-]+(?:[,\s]+[\w-]+)*)/g;
539
+ const lines = content.split("\n");
540
+ for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
541
+ const line = lines[lineNum - 1];
542
+ const copy = new RegExp(defaultPattern.source, defaultPattern.flags);
543
+ let m;
544
+ while ((m = copy.exec(line)) !== null) {
545
+ const ids = extractSpecIds2(m[1] ?? "");
546
+ for (const id of ids) {
547
+ if (specIdSet.has(id)) {
548
+ matches.push({
549
+ specId: id,
550
+ location: `${filePath}:${lineNum}`,
551
+ context: { filePath, line: lineNum }
763
552
  });
764
553
  }
765
554
  }
766
555
  }
767
- return { success: errors.length === 0, errors, warnings };
768
556
  }
769
- };
770
- }
771
- function relationCoverage(config) {
557
+ return matches;
558
+ }
559
+ };
560
+ function createAnnotationScanner(contentPatterns) {
772
561
  return {
773
- targetModel: config.targetModel,
774
- description: config.description,
775
- check: (specs, registry) => {
776
- const targets = registry[config.targetModel];
777
- if (!targets) {
778
- return { total: 0, covered: 0, uncovered: 0, coveragePercent: 100, coveredItems: [], uncoveredItems: [] };
562
+ findSpecIds(content, specIds, filePath) {
563
+ if (typeof content !== "string") return [];
564
+ const specIdSet = new Set(specIds);
565
+ const matches = [];
566
+ const lines = content.split("\n");
567
+ for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
568
+ const line = lines[lineNum - 1];
569
+ for (const re of contentPatterns) {
570
+ const copy = new RegExp(re.source, re.flags);
571
+ let m;
572
+ while ((m = copy.exec(line)) !== null) {
573
+ const ids = extractSpecIds2(m[1] ?? "");
574
+ for (const id of ids) {
575
+ if (specIdSet.has(id)) {
576
+ matches.push({
577
+ specId: id,
578
+ location: `${filePath}:${lineNum}`,
579
+ context: { filePath, line: lineNum }
580
+ });
581
+ }
582
+ }
583
+ }
584
+ }
779
585
  }
780
- const allTargetIds = /* @__PURE__ */ new Set();
781
- const targetDescriptions = /* @__PURE__ */ new Map();
782
- for (const t of targets.values()) {
783
- if (config.targetPrefix && !t.id.startsWith(config.targetPrefix)) continue;
784
- allTargetIds.add(t.id);
785
- targetDescriptions.set(t.id, t.name ?? t.description ?? t.id);
586
+ return matches;
587
+ }
588
+ };
589
+ }
590
+ var BUILT_IN_SCANNERS = {
591
+ openapi: openapiScanner,
592
+ ddl: ddlScanner,
593
+ annotation: annotationScanner
594
+ };
595
+ function getScannerForSource(source) {
596
+ if (source.scanner) return source.scanner;
597
+ if (source.type === "annotation" && source.contentPatterns && source.contentPatterns.length > 0) {
598
+ return createAnnotationScanner(source.contentPatterns);
599
+ }
600
+ return BUILT_IN_SCANNERS[source.type] ?? null;
601
+ }
602
+ function loadFileContent(filePath, sourceType) {
603
+ const content = readFileSync(filePath, "utf-8");
604
+ if (!content || content.length === 0) return null;
605
+ if (sourceType === "openapi") {
606
+ if (filePath.endsWith(".json")) {
607
+ return JSON.parse(content);
608
+ }
609
+ return parse(content);
610
+ }
611
+ return content;
612
+ }
613
+ function runGlobalScan(sources, specIds, basePath, lookupKeyMap) {
614
+ const cwd = basePath ?? process.cwd();
615
+ const result = /* @__PURE__ */ new Map();
616
+ const warnings = [];
617
+ for (const source of sources) {
618
+ const scanner = getScannerForSource(source);
619
+ if (!scanner) {
620
+ warnings.push({
621
+ message: `No scanner found for source type "${source.type}". Provide a custom scanner.`,
622
+ sourceType: source.type
623
+ });
624
+ continue;
625
+ }
626
+ const keyToSpecId = /* @__PURE__ */ new Map();
627
+ for (const specId of specIds) {
628
+ const overrides = lookupKeyMap?.get(specId);
629
+ const key = overrides?.[source.type] ?? specId;
630
+ keyToSpecId.set(key, specId);
631
+ }
632
+ const keysToSearch = Array.from(keyToSpecId.keys());
633
+ const allFiles = /* @__PURE__ */ new Set();
634
+ for (const pattern of source.paths) {
635
+ const found = glob.sync(pattern, {
636
+ cwd,
637
+ ignore: source.exclude ?? []
638
+ });
639
+ for (const f of found) {
640
+ allFiles.add(f);
786
641
  }
787
- const coveredIds = /* @__PURE__ */ new Set();
788
- for (const spec of specs) {
789
- if (!spec.relations) continue;
790
- for (const rel of spec.relations) {
791
- if (config.relationType && rel.type !== config.relationType) continue;
792
- if (allTargetIds.has(rel.target)) coveredIds.add(rel.target);
793
- }
642
+ }
643
+ for (const relPath of allFiles) {
644
+ const fullPath = isAbsolute(relPath) ? relPath : join(cwd, relPath);
645
+ if (!existsSync(fullPath)) continue;
646
+ let content;
647
+ try {
648
+ content = loadFileContent(fullPath, source.type);
649
+ } catch (e) {
650
+ const msg = e instanceof Error ? e.message : String(e);
651
+ warnings.push({
652
+ message: `Failed to load ${relPath}: ${msg}`,
653
+ sourceType: source.type,
654
+ filePath: relPath
655
+ });
656
+ continue;
794
657
  }
795
- const coveredItems = [];
796
- const uncoveredItems = [];
797
- for (const id of allTargetIds) {
798
- const desc = targetDescriptions.get(id);
799
- if (coveredIds.has(id)) {
800
- coveredItems.push({ id, description: desc });
658
+ if (content == null) continue;
659
+ let matches;
660
+ try {
661
+ matches = scanner.findSpecIds(content, keysToSearch, relPath);
662
+ } catch (e) {
663
+ const msg = e instanceof Error ? e.message : String(e);
664
+ warnings.push({
665
+ message: `Scanner error for ${relPath}: ${msg}`,
666
+ sourceType: source.type,
667
+ filePath: relPath
668
+ });
669
+ continue;
670
+ }
671
+ for (const match of matches) {
672
+ const originalSpecId = keyToSpecId.get(match.specId) ?? match.specId;
673
+ const globalMatch = {
674
+ ...match,
675
+ specId: originalSpecId,
676
+ sourceType: source.type,
677
+ relation: source.relation,
678
+ filePath: relPath
679
+ };
680
+ const existing = result.get(originalSpecId);
681
+ if (existing) {
682
+ existing.push(globalMatch);
801
683
  } else {
802
- uncoveredItems.push({ id, description: desc });
684
+ result.set(originalSpecId, [globalMatch]);
803
685
  }
804
686
  }
805
- const total = allTargetIds.size;
806
- const covered = coveredItems.length;
807
- const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
808
- return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
809
687
  }
688
+ }
689
+ return { matches: result, warnings };
690
+ }
691
+ function runDeepValidation(specId, matches, deepValidation, spec) {
692
+ const errors = [];
693
+ const warnings = [];
694
+ for (const match of matches) {
695
+ const rule = deepValidation[match.sourceType];
696
+ if (!rule) continue;
697
+ if (match.sourceType === "openapi") {
698
+ const mapping = rule.mapper(spec);
699
+ const ctx = match.context;
700
+ if (!ctx?.match?.operation) continue;
701
+ validateOpenAPIDeep(specId, mapping, ctx.doc, ctx.match, warnings);
702
+ } else if (match.sourceType === "ddl") {
703
+ const mapping = rule.mapper(spec);
704
+ const ctx = match.context;
705
+ if (!ctx?.table) continue;
706
+ validateDDLDeep(specId, mapping, ctx.table, warnings);
707
+ }
708
+ }
709
+ return {
710
+ success: errors.length === 0,
711
+ errors,
712
+ warnings
810
713
  };
811
714
  }
812
- var ImplementsRelationSchema = z.object({
813
- target: z.string(),
814
- targetType: z.string(),
815
- sourcePath: z.string().optional(),
816
- description: z.string().optional()
817
- });
818
- var VerifiedByRelationSchema = z.object({
819
- target: z.string(),
820
- testPath: z.string().optional(),
821
- framework: z.string().optional(),
822
- description: z.string().optional()
823
- });
715
+ function validateOpenAPIDeep(specId, mapping, doc, match, warnings) {
716
+ if (mapping.method && match.operation) {
717
+ const expectedMethod = mapping.method.toLowerCase();
718
+ const actualMethod = match.method?.toLowerCase();
719
+ if (actualMethod && actualMethod !== expectedMethod) {
720
+ warnings.push({
721
+ message: `Method mismatch for "${specId}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
722
+ specId
723
+ });
724
+ }
725
+ }
726
+ if (match.operation && mapping.parameters) {
727
+ const opParams = (match.operation.parameters || []).map(
728
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
729
+ (p) => maybeResolveRef(doc, p)
730
+ );
731
+ for (const expectedParam of mapping.parameters) {
732
+ const found = opParams.find(
733
+ (p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
734
+ );
735
+ if (!found) {
736
+ warnings.push({
737
+ message: `Parameter "${expectedParam.name}" not found in operation "${specId}"`,
738
+ specId,
739
+ field: expectedParam.name
740
+ });
741
+ } else if (expectedParam.type && found.schema?.type) {
742
+ if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
743
+ warnings.push({
744
+ message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
745
+ specId,
746
+ field: expectedParam.name
747
+ });
748
+ }
749
+ }
750
+ }
751
+ }
752
+ if (match.operation && mapping.responseProperties) {
753
+ const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
754
+ let responseSchema = successResponse?.content?.["application/json"]?.schema;
755
+ responseSchema = maybeResolveRef(doc, responseSchema);
756
+ if (responseSchema?.type === "array") {
757
+ responseSchema = maybeResolveRef(doc, responseSchema.items);
758
+ }
759
+ const responseProps = responseSchema?.properties || {};
760
+ for (const expectedProp of mapping.responseProperties) {
761
+ const foundProp = responseProps[expectedProp.name];
762
+ if (!foundProp) {
763
+ warnings.push({
764
+ message: `Response property "${expectedProp.name}" not found in operation "${specId}"`,
765
+ specId,
766
+ field: expectedProp.name
767
+ });
768
+ } else if (expectedProp.type && foundProp.type) {
769
+ if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
770
+ warnings.push({
771
+ message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
772
+ specId,
773
+ field: expectedProp.name
774
+ });
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ function validateDDLDeep(specId, mapping, table, warnings) {
781
+ if (!mapping.columns) return;
782
+ for (const expectedCol of mapping.columns) {
783
+ const foundCol = table.columns.find(
784
+ (c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
785
+ );
786
+ if (!foundCol) {
787
+ warnings.push({
788
+ message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
789
+ specId,
790
+ field: expectedCol.name
791
+ });
792
+ continue;
793
+ }
794
+ if (mapping.checkTypes && expectedCol.type && foundCol.type) {
795
+ if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
796
+ warnings.push({
797
+ message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
798
+ specId,
799
+ field: expectedCol.name
800
+ });
801
+ }
802
+ }
803
+ }
804
+ }
824
805
 
825
- export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationChecker, annotationCoverage, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, setArtifactsConfig, testChecker };
806
+ export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationCoverage, annotationScanner, arrayMinLength, baseSpecSchema, childIdFormat, createAnnotationScanner, ddlScanner, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, openapiScanner, relationCoverage, requireField, runDeepValidation, runGlobalScan, setArtifactsConfig };
826
807
  //# sourceMappingURL=index.js.map
827
808
  //# sourceMappingURL=index.js.map