speckeeper 0.8.0 → 0.9.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
@@ -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,117 +430,27 @@ 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
- };
448
+ return matches;
449
+ }
450
+ };
451
+ function stripSchemaPrefix(name) {
452
+ const dotIndex = name.lastIndexOf(".");
453
+ return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
642
454
  }
643
455
  function parseDDLWithParser(content) {
644
456
  const parser = new nodeSqlParser.Parser();
@@ -648,7 +460,8 @@ function parseDDLWithParser(content) {
648
460
  const statements = Array.isArray(ast) ? ast : [ast];
649
461
  for (const stmt of statements) {
650
462
  if (stmt.type !== "create" || stmt.keyword !== "table") continue;
651
- const tableName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
463
+ const rawName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
464
+ const tableName = stripSchemaPrefix(rawName);
652
465
  const columns = (stmt.create_definitions || []).filter((def) => def.resource === "column").map((def) => {
653
466
  const colName = def.column?.column ?? "";
654
467
  let colType = def.definition?.dataType ?? "";
@@ -671,7 +484,7 @@ function parseDDLWithParser(content) {
671
484
  }
672
485
  function parseDDLWithRegex(content) {
673
486
  const tables = [];
674
- const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`|"|')?(\w+)(?:`|"|')?\s*\(([\s\S]*?)\);/gi;
487
+ const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:[`"']?\w+[`"']?\s*\.\s*)?)[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\);/gi;
675
488
  let match;
676
489
  while ((match = tableRegex.exec(content)) !== null) {
677
490
  const tableName = match[1];
@@ -691,132 +504,296 @@ function parseDDLWithRegex(content) {
691
504
  }
692
505
  return tables;
693
506
  }
694
- function externalSqlSchemaChecker(config) {
695
- return {
696
- targetType: "ddl",
697
- sourcePath: config?.sourcePath ?? (() => "db/schema.sql"),
698
- check: (spec) => {
699
- const errors = [];
700
- const warnings = [];
701
- const filePath = (config?.sourcePath ?? (() => "db/schema.sql"))(spec);
702
- const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
703
- if (!existsSync(fullPath)) {
704
- errors.push({ message: `DDL file not found: ${filePath}`, specId: spec.id });
705
- return { success: false, errors, warnings };
706
- }
707
- let content;
708
- try {
709
- content = readFileSync(fullPath, "utf-8");
710
- } catch {
711
- errors.push({ message: `Failed to read DDL file: ${filePath}`, specId: spec.id });
712
- return { success: false, errors, warnings };
713
- }
714
- if (content.length === 0) {
715
- errors.push({ message: `DDL file is empty: ${filePath}`, specId: spec.id });
716
- return { success: false, errors, warnings };
717
- }
718
- let tables = parseDDLWithParser(content);
719
- if (!tables) {
720
- warnings.push({ message: `DDL parser failed for ${filePath}, using regex fallback`, specId: spec.id });
721
- tables = parseDDLWithRegex(content);
722
- }
723
- if (!config?.mapper) {
724
- return { success: errors.length === 0, errors, warnings };
725
- }
726
- 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) {
727
516
  const table = tables.find(
728
- (t) => t.name.toLowerCase() === mapping.tableName.toLowerCase()
517
+ (t) => t.name.toLowerCase() === specId.toLowerCase()
729
518
  );
730
- if (!table) {
731
- warnings.push({
732
- message: `Table "${mapping.tableName}" not found in DDL file ${filePath}`,
733
- specId: spec.id
519
+ if (table) {
520
+ matches.push({
521
+ specId,
522
+ location: `${filePath}:${table.name}`,
523
+ context: { table }
734
524
  });
735
- return { success: errors.length === 0, errors, warnings };
736
525
  }
737
- if (!mapping.columns) {
738
- return { success: errors.length === 0, errors, warnings };
739
- }
740
- for (const expectedCol of mapping.columns) {
741
- const foundCol = table.columns.find(
742
- (c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
743
- );
744
- if (!foundCol) {
745
- warnings.push({
746
- message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
747
- specId: spec.id,
748
- field: expectedCol.name
749
- });
750
- continue;
751
- }
752
- if (config.checkTypes && expectedCol.type && foundCol.type) {
753
- if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
754
- warnings.push({
755
- message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
756
- specId: spec.id,
757
- 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 }
758
552
  });
759
553
  }
760
554
  }
761
555
  }
762
- return { success: errors.length === 0, errors, warnings };
763
556
  }
764
- };
765
- }
766
- function relationCoverage(config) {
557
+ return matches;
558
+ }
559
+ };
560
+ function createAnnotationScanner(contentPatterns) {
767
561
  return {
768
- targetModel: config.targetModel,
769
- description: config.description,
770
- check: (specs, registry) => {
771
- const targets = registry[config.targetModel];
772
- if (!targets) {
773
- 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
+ }
774
585
  }
775
- const allTargetIds = /* @__PURE__ */ new Set();
776
- const targetDescriptions = /* @__PURE__ */ new Map();
777
- for (const t of targets.values()) {
778
- if (config.targetPrefix && !t.id.startsWith(config.targetPrefix)) continue;
779
- allTargetIds.add(t.id);
780
- 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) {
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 allFiles = /* @__PURE__ */ new Set();
627
+ for (const pattern of source.paths) {
628
+ const found = glob.sync(pattern, {
629
+ cwd,
630
+ ignore: source.exclude ?? []
631
+ });
632
+ for (const f of found) {
633
+ allFiles.add(f);
781
634
  }
782
- const coveredIds = /* @__PURE__ */ new Set();
783
- for (const spec of specs) {
784
- if (!spec.relations) continue;
785
- for (const rel of spec.relations) {
786
- if (config.relationType && rel.type !== config.relationType) continue;
787
- if (allTargetIds.has(rel.target)) coveredIds.add(rel.target);
788
- }
635
+ }
636
+ for (const relPath of allFiles) {
637
+ const fullPath = isAbsolute(relPath) ? relPath : join(cwd, relPath);
638
+ if (!existsSync(fullPath)) continue;
639
+ let content;
640
+ try {
641
+ content = loadFileContent(fullPath, source.type);
642
+ } catch (e) {
643
+ const msg = e instanceof Error ? e.message : String(e);
644
+ warnings.push({
645
+ message: `Failed to load ${relPath}: ${msg}`,
646
+ sourceType: source.type,
647
+ filePath: relPath
648
+ });
649
+ continue;
789
650
  }
790
- const coveredItems = [];
791
- const uncoveredItems = [];
792
- for (const id of allTargetIds) {
793
- const desc = targetDescriptions.get(id);
794
- if (coveredIds.has(id)) {
795
- coveredItems.push({ id, description: desc });
651
+ if (content == null) continue;
652
+ let matches;
653
+ try {
654
+ matches = scanner.findSpecIds(content, specIds, relPath);
655
+ } catch (e) {
656
+ const msg = e instanceof Error ? e.message : String(e);
657
+ warnings.push({
658
+ message: `Scanner error for ${relPath}: ${msg}`,
659
+ sourceType: source.type,
660
+ filePath: relPath
661
+ });
662
+ continue;
663
+ }
664
+ for (const match of matches) {
665
+ const globalMatch = {
666
+ ...match,
667
+ sourceType: source.type,
668
+ relation: source.relation,
669
+ filePath: relPath
670
+ };
671
+ const existing = result.get(match.specId);
672
+ if (existing) {
673
+ existing.push(globalMatch);
796
674
  } else {
797
- uncoveredItems.push({ id, description: desc });
675
+ result.set(match.specId, [globalMatch]);
798
676
  }
799
677
  }
800
- const total = allTargetIds.size;
801
- const covered = coveredItems.length;
802
- const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
803
- return { total, covered, uncovered: total - covered, coveragePercent, coveredItems, uncoveredItems };
804
678
  }
679
+ }
680
+ return { matches: result, warnings };
681
+ }
682
+ function runDeepValidation(specId, matches, deepValidation, spec) {
683
+ const errors = [];
684
+ const warnings = [];
685
+ for (const match of matches) {
686
+ const rule = deepValidation[match.sourceType];
687
+ if (!rule) continue;
688
+ if (match.sourceType === "openapi") {
689
+ const mapping = rule.mapper(spec);
690
+ const ctx = match.context;
691
+ if (!ctx?.match?.operation) continue;
692
+ validateOpenAPIDeep(specId, mapping, ctx.doc, ctx.match, warnings);
693
+ } else if (match.sourceType === "ddl") {
694
+ const mapping = rule.mapper(spec);
695
+ const ctx = match.context;
696
+ if (!ctx?.table) continue;
697
+ validateDDLDeep(specId, mapping, ctx.table, warnings);
698
+ }
699
+ }
700
+ return {
701
+ success: errors.length === 0,
702
+ errors,
703
+ warnings
805
704
  };
806
705
  }
807
- var ImplementsRelationSchema = z.object({
808
- target: z.string(),
809
- targetType: z.string(),
810
- sourcePath: z.string().optional(),
811
- description: z.string().optional()
812
- });
813
- var VerifiedByRelationSchema = z.object({
814
- target: z.string(),
815
- testPath: z.string().optional(),
816
- framework: z.string().optional(),
817
- description: z.string().optional()
818
- });
706
+ function validateOpenAPIDeep(specId, mapping, doc, match, warnings) {
707
+ if (mapping.method && match.operation) {
708
+ const expectedMethod = mapping.method.toLowerCase();
709
+ const actualMethod = match.method?.toLowerCase();
710
+ if (actualMethod && actualMethod !== expectedMethod) {
711
+ warnings.push({
712
+ message: `Method mismatch for "${specId}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
713
+ specId
714
+ });
715
+ }
716
+ }
717
+ if (match.operation && mapping.parameters) {
718
+ const opParams = (match.operation.parameters || []).map(
719
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
720
+ (p) => maybeResolveRef(doc, p)
721
+ );
722
+ for (const expectedParam of mapping.parameters) {
723
+ const found = opParams.find(
724
+ (p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
725
+ );
726
+ if (!found) {
727
+ warnings.push({
728
+ message: `Parameter "${expectedParam.name}" not found in operation "${specId}"`,
729
+ specId,
730
+ field: expectedParam.name
731
+ });
732
+ } else if (expectedParam.type && found.schema?.type) {
733
+ if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
734
+ warnings.push({
735
+ message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
736
+ specId,
737
+ field: expectedParam.name
738
+ });
739
+ }
740
+ }
741
+ }
742
+ }
743
+ if (match.operation && mapping.responseProperties) {
744
+ const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
745
+ let responseSchema = successResponse?.content?.["application/json"]?.schema;
746
+ responseSchema = maybeResolveRef(doc, responseSchema);
747
+ if (responseSchema?.type === "array") {
748
+ responseSchema = maybeResolveRef(doc, responseSchema.items);
749
+ }
750
+ const responseProps = responseSchema?.properties || {};
751
+ for (const expectedProp of mapping.responseProperties) {
752
+ const foundProp = responseProps[expectedProp.name];
753
+ if (!foundProp) {
754
+ warnings.push({
755
+ message: `Response property "${expectedProp.name}" not found in operation "${specId}"`,
756
+ specId,
757
+ field: expectedProp.name
758
+ });
759
+ } else if (expectedProp.type && foundProp.type) {
760
+ if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
761
+ warnings.push({
762
+ message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
763
+ specId,
764
+ field: expectedProp.name
765
+ });
766
+ }
767
+ }
768
+ }
769
+ }
770
+ }
771
+ function validateDDLDeep(specId, mapping, table, warnings) {
772
+ if (!mapping.columns) return;
773
+ for (const expectedCol of mapping.columns) {
774
+ const foundCol = table.columns.find(
775
+ (c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
776
+ );
777
+ if (!foundCol) {
778
+ warnings.push({
779
+ message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
780
+ specId,
781
+ field: expectedCol.name
782
+ });
783
+ continue;
784
+ }
785
+ if (mapping.checkTypes && expectedCol.type && foundCol.type) {
786
+ if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
787
+ warnings.push({
788
+ message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
789
+ specId,
790
+ field: expectedCol.name
791
+ });
792
+ }
793
+ }
794
+ }
795
+ }
819
796
 
820
- export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationChecker, annotationCoverage, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, setArtifactsConfig, testChecker };
797
+ export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationCoverage, annotationScanner, arrayMinLength, baseSpecSchema, childIdFormat, createAnnotationScanner, ddlScanner, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, openapiScanner, relationCoverage, requireField, runDeepValidation, runGlobalScan, setArtifactsConfig };
821
798
  //# sourceMappingURL=index.js.map
822
799
  //# sourceMappingURL=index.js.map