speckeeper 0.7.2 → 0.8.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/README.md +70 -15
- package/dist/cli.js +53 -36
- package/dist/cli.js.map +1 -1
- package/dist/config-api-U2pt1aHJ.d.ts +970 -0
- package/dist/dsl/index.d.ts +33 -2
- package/dist/dsl/index.js +197 -3
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +3 -504
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/model-BPZMYasZ.d.ts +0 -451
package/dist/dsl/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { q as LintRule, m as Exporter, n as ExternalChecker, k as CoverageChecker, A as ArtifactConfig } from '../config-api-U2pt1aHJ.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Core DSL — Base spec schema
|
|
@@ -125,6 +125,37 @@ interface TestCheckerConfig<T> {
|
|
|
125
125
|
declare function testChecker<T extends {
|
|
126
126
|
id: string;
|
|
127
127
|
}>(config?: TestCheckerConfig<T>): ExternalChecker<T>;
|
|
128
|
+
type AnnotationRelationType = 'verifiedBy' | 'implements' | 'traces';
|
|
129
|
+
interface AnnotationCheckEntry {
|
|
130
|
+
artifact: string;
|
|
131
|
+
relationType: AnnotationRelationType;
|
|
132
|
+
contentPatterns?: RegExp[];
|
|
133
|
+
checker?: ExternalChecker<{
|
|
134
|
+
id: string;
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
137
|
+
interface AnnotationCheckerConfig<_T extends {
|
|
138
|
+
id: string;
|
|
139
|
+
}> {
|
|
140
|
+
artifact?: string;
|
|
141
|
+
relationType?: AnnotationRelationType;
|
|
142
|
+
checks?: AnnotationCheckEntry[];
|
|
143
|
+
contentPatterns?: RegExp[];
|
|
144
|
+
}
|
|
145
|
+
declare function setArtifactsConfig(config: Record<string, ArtifactConfig>): void;
|
|
146
|
+
declare function getArtifactsConfig(): Record<string, ArtifactConfig> | undefined;
|
|
147
|
+
declare function annotationChecker<T extends {
|
|
148
|
+
id: string;
|
|
149
|
+
}>(config?: AnnotationCheckerConfig<T>): ExternalChecker<T>;
|
|
150
|
+
interface AnnotationCoverageConfig {
|
|
151
|
+
artifact: string;
|
|
152
|
+
relationType: AnnotationRelationType;
|
|
153
|
+
description: string;
|
|
154
|
+
contentPatterns?: RegExp[];
|
|
155
|
+
}
|
|
156
|
+
declare function annotationCoverage<T extends {
|
|
157
|
+
id: string;
|
|
158
|
+
}>(config: AnnotationCoverageConfig): CoverageChecker<T>;
|
|
128
159
|
interface OpenAPICheckerConfig<T> {
|
|
129
160
|
sourcePath?: (spec: T) => string;
|
|
130
161
|
mapper: (spec: T) => {
|
|
@@ -250,4 +281,4 @@ declare const VerifiedByRelationSchema: z.ZodObject<{
|
|
|
250
281
|
}>;
|
|
251
282
|
type VerifiedByRelation = z.infer<typeof VerifiedByRelationSchema>;
|
|
252
283
|
|
|
253
|
-
export { type BaseSpec, type ImplementsRelation, ImplementsRelationSchema, type MarkdownExporterConfig, type VerifiedByRelation, VerifiedByRelationSchema, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, testChecker };
|
|
284
|
+
export { type AnnotationCheckEntry, type AnnotationCheckerConfig, type AnnotationCoverageConfig, type BaseSpec, type ImplementsRelation, ImplementsRelationSchema, type MarkdownExporterConfig, type VerifiedByRelation, VerifiedByRelationSchema, annotationChecker, annotationCoverage, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, setArtifactsConfig, testChecker };
|
package/dist/dsl/index.js
CHANGED
|
@@ -290,6 +290,195 @@ function testChecker(config) {
|
|
|
290
290
|
}
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
|
+
function relationTypeToAnnotation(relationType) {
|
|
294
|
+
return relationType === "verifiedBy" ? "verifies" : relationType;
|
|
295
|
+
}
|
|
296
|
+
function getDefaultPatternForRelation(relationType) {
|
|
297
|
+
const ann = relationTypeToAnnotation(relationType);
|
|
298
|
+
return new RegExp(`@${ann}\\s+([\\w-]+(?:[,\\s]+[\\w-]+)*)`, "g");
|
|
299
|
+
}
|
|
300
|
+
function extractSpecIds(captured) {
|
|
301
|
+
return captured.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
302
|
+
}
|
|
303
|
+
var _artifactsConfig;
|
|
304
|
+
function setArtifactsConfig(config) {
|
|
305
|
+
_artifactsConfig = config;
|
|
306
|
+
}
|
|
307
|
+
function getArtifactsConfig() {
|
|
308
|
+
return _artifactsConfig;
|
|
309
|
+
}
|
|
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
|
+
function annotationCoverage(config) {
|
|
411
|
+
return {
|
|
412
|
+
targetModel: "annotation",
|
|
413
|
+
description: config.description,
|
|
414
|
+
check: (specs, _registry) => {
|
|
415
|
+
const basePath = process.cwd();
|
|
416
|
+
const allSpecIds = new Set(specs.map((s) => s.id));
|
|
417
|
+
const coveredIds = /* @__PURE__ */ new Set();
|
|
418
|
+
const artifactsConfig = getArtifactsConfig();
|
|
419
|
+
const artifactConfig = artifactsConfig?.[config.artifact];
|
|
420
|
+
if (!artifactConfig) {
|
|
421
|
+
return {
|
|
422
|
+
total: allSpecIds.size,
|
|
423
|
+
covered: 0,
|
|
424
|
+
uncovered: allSpecIds.size,
|
|
425
|
+
coveragePercent: 0,
|
|
426
|
+
coveredItems: [],
|
|
427
|
+
uncoveredItems: Array.from(allSpecIds).map((id) => ({ id }))
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
431
|
+
for (const pattern of artifactConfig.globs) {
|
|
432
|
+
const found = glob.sync(pattern, {
|
|
433
|
+
cwd: basePath,
|
|
434
|
+
ignore: artifactConfig.exclude ?? []
|
|
435
|
+
});
|
|
436
|
+
for (const f of found) {
|
|
437
|
+
allFiles.add(f);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const patterns = config.contentPatterns ?? artifactConfig.contentPatterns ?? [getDefaultPatternForRelation(config.relationType)];
|
|
441
|
+
const patternList = Array.isArray(patterns) ? patterns : [patterns];
|
|
442
|
+
for (const filePath of allFiles) {
|
|
443
|
+
const fullPath = join(basePath, filePath);
|
|
444
|
+
let content;
|
|
445
|
+
try {
|
|
446
|
+
content = readFileSync(fullPath, "utf-8");
|
|
447
|
+
} catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const lines = content.split("\n");
|
|
451
|
+
for (const line of lines) {
|
|
452
|
+
for (const re of patternList) {
|
|
453
|
+
const copy = new RegExp(re.source, re.flags);
|
|
454
|
+
let m;
|
|
455
|
+
while ((m = copy.exec(line)) !== null) {
|
|
456
|
+
const ids = extractSpecIds(m[1] ?? "");
|
|
457
|
+
for (const id of ids) {
|
|
458
|
+
if (allSpecIds.has(id)) {
|
|
459
|
+
coveredIds.add(id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const coveredItems = Array.from(coveredIds).map((id) => ({ id }));
|
|
467
|
+
const uncoveredItems = Array.from(allSpecIds).filter((id) => !coveredIds.has(id)).map((id) => ({ id }));
|
|
468
|
+
const total = allSpecIds.size;
|
|
469
|
+
const covered = coveredItems.length;
|
|
470
|
+
const coveragePercent = total > 0 ? Math.round(covered / total * 100) : 100;
|
|
471
|
+
return {
|
|
472
|
+
total,
|
|
473
|
+
covered,
|
|
474
|
+
uncovered: total - covered,
|
|
475
|
+
coveragePercent,
|
|
476
|
+
coveredItems,
|
|
477
|
+
uncoveredItems
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
293
482
|
function resolveRef(doc, ref) {
|
|
294
483
|
if (!ref.startsWith("#/")) return void 0;
|
|
295
484
|
const parts = ref.substring(2).split("/");
|
|
@@ -451,6 +640,10 @@ function externalOpenAPIChecker(config) {
|
|
|
451
640
|
}
|
|
452
641
|
};
|
|
453
642
|
}
|
|
643
|
+
function stripSchemaPrefix(name) {
|
|
644
|
+
const dotIndex = name.lastIndexOf(".");
|
|
645
|
+
return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
|
|
646
|
+
}
|
|
454
647
|
function parseDDLWithParser(content) {
|
|
455
648
|
const parser = new nodeSqlParser.Parser();
|
|
456
649
|
const tables = [];
|
|
@@ -459,7 +652,8 @@ function parseDDLWithParser(content) {
|
|
|
459
652
|
const statements = Array.isArray(ast) ? ast : [ast];
|
|
460
653
|
for (const stmt of statements) {
|
|
461
654
|
if (stmt.type !== "create" || stmt.keyword !== "table") continue;
|
|
462
|
-
const
|
|
655
|
+
const rawName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
|
|
656
|
+
const tableName = stripSchemaPrefix(rawName);
|
|
463
657
|
const columns = (stmt.create_definitions || []).filter((def) => def.resource === "column").map((def) => {
|
|
464
658
|
const colName = def.column?.column ?? "";
|
|
465
659
|
let colType = def.definition?.dataType ?? "";
|
|
@@ -482,7 +676,7 @@ function parseDDLWithParser(content) {
|
|
|
482
676
|
}
|
|
483
677
|
function parseDDLWithRegex(content) {
|
|
484
678
|
const tables = [];
|
|
485
|
-
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(
|
|
679
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:[`"']?\w+[`"']?\s*\.\s*)?)[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\);/gi;
|
|
486
680
|
let match;
|
|
487
681
|
while ((match = tableRegex.exec(content)) !== null) {
|
|
488
682
|
const tableName = match[1];
|
|
@@ -628,6 +822,6 @@ var VerifiedByRelationSchema = z.object({
|
|
|
628
822
|
description: z.string().optional()
|
|
629
823
|
});
|
|
630
824
|
|
|
631
|
-
export { ImplementsRelationSchema, VerifiedByRelationSchema, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, testChecker };
|
|
825
|
+
export { ImplementsRelationSchema, VerifiedByRelationSchema, annotationChecker, annotationCoverage, arrayMinLength, baseSpecSchema, childIdFormat, externalOpenAPIChecker, externalSqlSchemaChecker, getArtifactsConfig, idFormat, isTypeContainedBy, markdownExporter, relationCoverage, requireField, setArtifactsConfig, testChecker };
|
|
632
826
|
//# sourceMappingURL=index.js.map
|
|
633
827
|
//# sourceMappingURL=index.js.map
|