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/README.md +137 -33
- package/dist/cli.js +622 -106
- package/dist/cli.js.map +1 -1
- package/dist/{config-api-Bh0zX8W7.d.ts → config-api-BDl4otlv.d.ts} +92 -2
- package/dist/dsl/index.d.ts +38 -82
- package/dist/dsl/index.js +431 -454
- package/dist/dsl/index.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
4
|
-
import { dirname, join, resolve } from 'path';
|
|
4
|
+
import { dirname, join, resolve, isAbsolute } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import chalk4 from 'chalk';
|
|
7
7
|
import { parse } from 'yaml';
|
|
8
8
|
import 'crypto';
|
|
9
|
-
import 'glob';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
+
import nodeSqlParser from 'node-sql-parser';
|
|
11
12
|
|
|
12
13
|
var defaultConfig = {
|
|
13
14
|
srcDir: "src",
|
|
@@ -196,7 +197,9 @@ async function buildCommand(options) {
|
|
|
196
197
|
}
|
|
197
198
|
for (const exporter of exporters) {
|
|
198
199
|
const outputDir = exporter.outputDir ? join(cwd, config.docsDir, exporter.outputDir) : join(cwd, config.docsDir);
|
|
199
|
-
|
|
200
|
+
if (exporter.single) {
|
|
201
|
+
ensureDir(outputDir);
|
|
202
|
+
}
|
|
200
203
|
if (exporter.single) {
|
|
201
204
|
for (const spec of modelSpecs) {
|
|
202
205
|
const content = exporter.single(spec);
|
|
@@ -209,8 +212,12 @@ async function buildCommand(options) {
|
|
|
209
212
|
}
|
|
210
213
|
if (exporter.index) {
|
|
211
214
|
const indexContent = exporter.index(modelSpecs);
|
|
215
|
+
const indexPath = exporter.outputFile ? join(cwd, config.docsDir, exporter.outputFile) : join(outputDir, "index.md");
|
|
216
|
+
if (!exporter.outputFile) {
|
|
217
|
+
ensureDir(outputDir);
|
|
218
|
+
}
|
|
212
219
|
files.push({
|
|
213
|
-
path:
|
|
220
|
+
path: indexPath,
|
|
214
221
|
content: indexContent
|
|
215
222
|
});
|
|
216
223
|
}
|
|
@@ -351,7 +358,7 @@ async function driftCommand(options) {
|
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
if (exporter.index) {
|
|
354
|
-
const indexPath = join(outputDir, "index.md");
|
|
361
|
+
const indexPath = exporter.outputFile ? join(cwd, config.docsDir, exporter.outputFile) : join(outputDir, "index.md");
|
|
355
362
|
if (!existsSync(indexPath)) {
|
|
356
363
|
results.push({ file: indexPath, status: "missing" });
|
|
357
364
|
} else {
|
|
@@ -404,6 +411,507 @@ function outputDriftResults(results, _options) {
|
|
|
404
411
|
console.log("");
|
|
405
412
|
console.log(chalk4.gray(` Summary: ${ok.length} ok, ${drifted.length} drifted, ${missing.length} missing`));
|
|
406
413
|
}
|
|
414
|
+
|
|
415
|
+
// src/core/dsl/type-compat.ts
|
|
416
|
+
var ALIASES = {
|
|
417
|
+
INTEGER: "INT",
|
|
418
|
+
BOOL: "BOOLEAN",
|
|
419
|
+
REAL: "FLOAT",
|
|
420
|
+
"DOUBLE PRECISION": "DOUBLE",
|
|
421
|
+
SERIAL: "INT",
|
|
422
|
+
BIGSERIAL: "BIGINT",
|
|
423
|
+
SMALLSERIAL: "SMALLINT",
|
|
424
|
+
TIMESTAMPTZ: "TIMESTAMP",
|
|
425
|
+
"TIMESTAMP WITH TIME ZONE": "TIMESTAMP",
|
|
426
|
+
"TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
|
|
427
|
+
"CHARACTER VARYING": "VARCHAR",
|
|
428
|
+
CHARACTER: "CHAR",
|
|
429
|
+
INT2: "SMALLINT",
|
|
430
|
+
INT4: "INT",
|
|
431
|
+
INT8: "BIGINT",
|
|
432
|
+
FLOAT4: "FLOAT",
|
|
433
|
+
FLOAT8: "DOUBLE"
|
|
434
|
+
};
|
|
435
|
+
var CONTAINMENT_CHAINS = [
|
|
436
|
+
["SMALLINT", "INT", "BIGINT"],
|
|
437
|
+
["FLOAT", "DOUBLE"],
|
|
438
|
+
["CHAR", "VARCHAR", "TEXT"],
|
|
439
|
+
["INTEGER", "NUMBER"]
|
|
440
|
+
];
|
|
441
|
+
function parseType(raw) {
|
|
442
|
+
const trimmed = raw.trim().toUpperCase();
|
|
443
|
+
const match = trimmed.match(/^([A-Z\s]+?)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?$/);
|
|
444
|
+
if (!match) {
|
|
445
|
+
return { base: trimmed };
|
|
446
|
+
}
|
|
447
|
+
const base = match[1].trim();
|
|
448
|
+
return {
|
|
449
|
+
base,
|
|
450
|
+
length: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
451
|
+
precision: match[2] !== void 0 ? Number(match[2]) : void 0,
|
|
452
|
+
scale: match[3] !== void 0 ? Number(match[3]) : void 0
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function normalize(base) {
|
|
456
|
+
return ALIASES[base] ?? base;
|
|
457
|
+
}
|
|
458
|
+
function findAllChainPositions(base, chains) {
|
|
459
|
+
const results = [];
|
|
460
|
+
const resolved = normalize(base);
|
|
461
|
+
for (const chain of chains) {
|
|
462
|
+
let idx = chain.indexOf(base);
|
|
463
|
+
if (idx === -1) idx = chain.indexOf(resolved);
|
|
464
|
+
if (idx !== -1) results.push({ chain, index: idx });
|
|
465
|
+
}
|
|
466
|
+
return results;
|
|
467
|
+
}
|
|
468
|
+
function isTypeContainedBy(specType, ddlType) {
|
|
469
|
+
const spec = parseType(specType);
|
|
470
|
+
const ddl = parseType(ddlType);
|
|
471
|
+
const specNorm = normalize(spec.base);
|
|
472
|
+
const ddlNorm = normalize(ddl.base);
|
|
473
|
+
if (specNorm === ddlNorm) {
|
|
474
|
+
if (specNorm === "DECIMAL" || specNorm === "NUMERIC") {
|
|
475
|
+
if (spec.precision !== void 0 && ddl.precision !== void 0) {
|
|
476
|
+
if (ddl.precision < spec.precision) return false;
|
|
477
|
+
}
|
|
478
|
+
if (spec.scale !== void 0 && ddl.scale !== void 0) {
|
|
479
|
+
if (ddl.scale < spec.scale) return false;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (spec.length !== void 0 && ddl.length !== void 0) {
|
|
484
|
+
return ddl.length >= spec.length;
|
|
485
|
+
}
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
const specPositions = findAllChainPositions(spec.base, CONTAINMENT_CHAINS);
|
|
489
|
+
const ddlPositions = findAllChainPositions(ddl.base, CONTAINMENT_CHAINS);
|
|
490
|
+
for (const sp of specPositions) {
|
|
491
|
+
for (const dp of ddlPositions) {
|
|
492
|
+
if (sp.chain === dp.chain) {
|
|
493
|
+
return dp.index >= sp.index;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/core/global-scanner.ts
|
|
501
|
+
function resolveRef(doc, ref) {
|
|
502
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
503
|
+
const parts = ref.substring(2).split("/");
|
|
504
|
+
let current = doc;
|
|
505
|
+
for (const part of parts) {
|
|
506
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
507
|
+
current = current[part];
|
|
508
|
+
}
|
|
509
|
+
return current;
|
|
510
|
+
}
|
|
511
|
+
function maybeResolveRef(doc, value) {
|
|
512
|
+
if (value && typeof value === "object" && typeof value.$ref === "string") {
|
|
513
|
+
return resolveRef(doc, value.$ref);
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
function findSpecIdInOpenAPI(doc, specId) {
|
|
518
|
+
const paths = doc.paths;
|
|
519
|
+
if (!paths || typeof paths !== "object") return null;
|
|
520
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
521
|
+
if (pathItem == null || typeof pathItem !== "object") continue;
|
|
522
|
+
const methods = ["get", "post", "put", "delete", "patch", "options", "head"];
|
|
523
|
+
for (const method of methods) {
|
|
524
|
+
const op = pathItem[method];
|
|
525
|
+
if (!op) continue;
|
|
526
|
+
if (op.operationId === specId) return { pathKey, operation: op, method };
|
|
527
|
+
if (op["x-spec-id"] === specId) return { pathKey, operation: op, method };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
for (const pathKey of Object.keys(paths)) {
|
|
531
|
+
const segments = pathKey.split("/").filter(Boolean);
|
|
532
|
+
const cleanSegments = segments.map((s) => s.replace(/^\{|\}$/g, ""));
|
|
533
|
+
if (cleanSegments.includes(specId)) return { pathKey };
|
|
534
|
+
}
|
|
535
|
+
const schemas = doc.components?.schemas;
|
|
536
|
+
if (schemas && typeof schemas === "object") {
|
|
537
|
+
if (specId in schemas) return { pathKey: `#/components/schemas/${specId}` };
|
|
538
|
+
}
|
|
539
|
+
function searchXSpecId(obj) {
|
|
540
|
+
if (obj == null || typeof obj !== "object") return false;
|
|
541
|
+
if (obj["x-spec-id"] === specId) return true;
|
|
542
|
+
for (const val of Object.values(obj)) {
|
|
543
|
+
if (searchXSpecId(val)) return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
if (searchXSpecId(doc)) return { pathKey: "(x-spec-id match)" };
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
var openapiScanner = {
|
|
551
|
+
findSpecIds(content, specIds, filePath) {
|
|
552
|
+
const doc = content;
|
|
553
|
+
if (!doc || typeof doc !== "object") return [];
|
|
554
|
+
const matches = [];
|
|
555
|
+
for (const specId of specIds) {
|
|
556
|
+
const match = findSpecIdInOpenAPI(doc, specId);
|
|
557
|
+
if (match) {
|
|
558
|
+
matches.push({
|
|
559
|
+
specId,
|
|
560
|
+
location: `${filePath}:${match.pathKey}`,
|
|
561
|
+
context: { doc, match }
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return matches;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
function stripSchemaPrefix(name) {
|
|
569
|
+
const dotIndex = name.lastIndexOf(".");
|
|
570
|
+
return dotIndex >= 0 ? name.substring(dotIndex + 1) : name;
|
|
571
|
+
}
|
|
572
|
+
function parseDDLWithParser(content) {
|
|
573
|
+
const parser = new nodeSqlParser.Parser();
|
|
574
|
+
const tables = [];
|
|
575
|
+
try {
|
|
576
|
+
const ast = parser.astify(content);
|
|
577
|
+
const statements = Array.isArray(ast) ? ast : [ast];
|
|
578
|
+
for (const stmt of statements) {
|
|
579
|
+
if (stmt.type !== "create" || stmt.keyword !== "table") continue;
|
|
580
|
+
const rawName = typeof stmt.table === "string" ? stmt.table : stmt.table?.[0]?.table ?? "";
|
|
581
|
+
const tableName = stripSchemaPrefix(rawName);
|
|
582
|
+
const columns = (stmt.create_definitions || []).filter((def) => def.resource === "column").map((def) => {
|
|
583
|
+
const colName = def.column?.column ?? "";
|
|
584
|
+
let colType = def.definition?.dataType ?? "";
|
|
585
|
+
if (def.definition?.length) {
|
|
586
|
+
colType += `(${def.definition.length})`;
|
|
587
|
+
}
|
|
588
|
+
if (def.definition?.scale) {
|
|
589
|
+
colType = colType.replace(/\)$/, `,${def.definition.scale})`);
|
|
590
|
+
}
|
|
591
|
+
return { name: colName, type: colType };
|
|
592
|
+
});
|
|
593
|
+
if (tableName) {
|
|
594
|
+
tables.push({ name: tableName, columns });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return tables.length > 0 ? tables : null;
|
|
598
|
+
} catch {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function parseDDLWithRegex(content) {
|
|
603
|
+
const tables = [];
|
|
604
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:[`"']?\w+[`"']?\s*\.\s*)?)[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\);/gi;
|
|
605
|
+
let match;
|
|
606
|
+
while ((match = tableRegex.exec(content)) !== null) {
|
|
607
|
+
const tableName = match[1];
|
|
608
|
+
const body = match[2];
|
|
609
|
+
const columns = [];
|
|
610
|
+
const lines = body.split(",").map((l) => l.trim()).filter(Boolean);
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
if (/^\s*(PRIMARY\s+KEY|UNIQUE|CHECK|CONSTRAINT|INDEX|FOREIGN\s+KEY)/i.test(line)) continue;
|
|
613
|
+
const colMatch = line.match(/^(?:`|"|')?(\w+)(?:`|"|')?\s+(\w+(?:\([^)]*\))?)/i);
|
|
614
|
+
if (colMatch) {
|
|
615
|
+
columns.push({ name: colMatch[1], type: colMatch[2] });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (tableName) {
|
|
619
|
+
tables.push({ name: tableName, columns });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return tables;
|
|
623
|
+
}
|
|
624
|
+
var ddlScanner = {
|
|
625
|
+
findSpecIds(content, specIds, filePath) {
|
|
626
|
+
if (typeof content !== "string") return [];
|
|
627
|
+
let tables = parseDDLWithParser(content);
|
|
628
|
+
if (!tables) {
|
|
629
|
+
tables = parseDDLWithRegex(content);
|
|
630
|
+
}
|
|
631
|
+
const matches = [];
|
|
632
|
+
for (const specId of specIds) {
|
|
633
|
+
const table = tables.find(
|
|
634
|
+
(t) => t.name.toLowerCase() === specId.toLowerCase()
|
|
635
|
+
);
|
|
636
|
+
if (table) {
|
|
637
|
+
matches.push({
|
|
638
|
+
specId,
|
|
639
|
+
location: `${filePath}:${table.name}`,
|
|
640
|
+
context: { table }
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return matches;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
function extractSpecIds(captured) {
|
|
648
|
+
return captured.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
649
|
+
}
|
|
650
|
+
var annotationScanner = {
|
|
651
|
+
findSpecIds(content, specIds, filePath) {
|
|
652
|
+
if (typeof content !== "string") return [];
|
|
653
|
+
const specIdSet = new Set(specIds);
|
|
654
|
+
const matches = [];
|
|
655
|
+
const defaultPattern = /@(?:verifies|implements|traces)\s+([\w-]+(?:[,\s]+[\w-]+)*)/g;
|
|
656
|
+
const lines = content.split("\n");
|
|
657
|
+
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
658
|
+
const line = lines[lineNum - 1];
|
|
659
|
+
const copy = new RegExp(defaultPattern.source, defaultPattern.flags);
|
|
660
|
+
let m;
|
|
661
|
+
while ((m = copy.exec(line)) !== null) {
|
|
662
|
+
const ids = extractSpecIds(m[1] ?? "");
|
|
663
|
+
for (const id of ids) {
|
|
664
|
+
if (specIdSet.has(id)) {
|
|
665
|
+
matches.push({
|
|
666
|
+
specId: id,
|
|
667
|
+
location: `${filePath}:${lineNum}`,
|
|
668
|
+
context: { filePath, line: lineNum }
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return matches;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
function createAnnotationScanner(contentPatterns) {
|
|
678
|
+
return {
|
|
679
|
+
findSpecIds(content, specIds, filePath) {
|
|
680
|
+
if (typeof content !== "string") return [];
|
|
681
|
+
const specIdSet = new Set(specIds);
|
|
682
|
+
const matches = [];
|
|
683
|
+
const lines = content.split("\n");
|
|
684
|
+
for (let lineNum = 1; lineNum <= lines.length; lineNum++) {
|
|
685
|
+
const line = lines[lineNum - 1];
|
|
686
|
+
for (const re of contentPatterns) {
|
|
687
|
+
const copy = new RegExp(re.source, re.flags);
|
|
688
|
+
let m;
|
|
689
|
+
while ((m = copy.exec(line)) !== null) {
|
|
690
|
+
const ids = extractSpecIds(m[1] ?? "");
|
|
691
|
+
for (const id of ids) {
|
|
692
|
+
if (specIdSet.has(id)) {
|
|
693
|
+
matches.push({
|
|
694
|
+
specId: id,
|
|
695
|
+
location: `${filePath}:${lineNum}`,
|
|
696
|
+
context: { filePath, line: lineNum }
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return matches;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
var BUILT_IN_SCANNERS = {
|
|
708
|
+
openapi: openapiScanner,
|
|
709
|
+
ddl: ddlScanner,
|
|
710
|
+
annotation: annotationScanner
|
|
711
|
+
};
|
|
712
|
+
function getScannerForSource(source) {
|
|
713
|
+
if (source.scanner) return source.scanner;
|
|
714
|
+
if (source.type === "annotation" && source.contentPatterns && source.contentPatterns.length > 0) {
|
|
715
|
+
return createAnnotationScanner(source.contentPatterns);
|
|
716
|
+
}
|
|
717
|
+
return BUILT_IN_SCANNERS[source.type] ?? null;
|
|
718
|
+
}
|
|
719
|
+
function loadFileContent(filePath, sourceType) {
|
|
720
|
+
const content = readFileSync(filePath, "utf-8");
|
|
721
|
+
if (!content || content.length === 0) return null;
|
|
722
|
+
if (sourceType === "openapi") {
|
|
723
|
+
if (filePath.endsWith(".json")) {
|
|
724
|
+
return JSON.parse(content);
|
|
725
|
+
}
|
|
726
|
+
return parse(content);
|
|
727
|
+
}
|
|
728
|
+
return content;
|
|
729
|
+
}
|
|
730
|
+
function runGlobalScan(sources, specIds, basePath) {
|
|
731
|
+
const cwd = basePath ?? process.cwd();
|
|
732
|
+
const result = /* @__PURE__ */ new Map();
|
|
733
|
+
const warnings = [];
|
|
734
|
+
for (const source of sources) {
|
|
735
|
+
const scanner = getScannerForSource(source);
|
|
736
|
+
if (!scanner) {
|
|
737
|
+
warnings.push({
|
|
738
|
+
message: `No scanner found for source type "${source.type}". Provide a custom scanner.`,
|
|
739
|
+
sourceType: source.type
|
|
740
|
+
});
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
744
|
+
for (const pattern of source.paths) {
|
|
745
|
+
const found = glob.sync(pattern, {
|
|
746
|
+
cwd,
|
|
747
|
+
ignore: source.exclude ?? []
|
|
748
|
+
});
|
|
749
|
+
for (const f of found) {
|
|
750
|
+
allFiles.add(f);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
for (const relPath of allFiles) {
|
|
754
|
+
const fullPath = isAbsolute(relPath) ? relPath : join(cwd, relPath);
|
|
755
|
+
if (!existsSync(fullPath)) continue;
|
|
756
|
+
let content;
|
|
757
|
+
try {
|
|
758
|
+
content = loadFileContent(fullPath, source.type);
|
|
759
|
+
} catch (e) {
|
|
760
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
761
|
+
warnings.push({
|
|
762
|
+
message: `Failed to load ${relPath}: ${msg}`,
|
|
763
|
+
sourceType: source.type,
|
|
764
|
+
filePath: relPath
|
|
765
|
+
});
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (content == null) continue;
|
|
769
|
+
let matches;
|
|
770
|
+
try {
|
|
771
|
+
matches = scanner.findSpecIds(content, specIds, relPath);
|
|
772
|
+
} catch (e) {
|
|
773
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
774
|
+
warnings.push({
|
|
775
|
+
message: `Scanner error for ${relPath}: ${msg}`,
|
|
776
|
+
sourceType: source.type,
|
|
777
|
+
filePath: relPath
|
|
778
|
+
});
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
for (const match of matches) {
|
|
782
|
+
const globalMatch = {
|
|
783
|
+
...match,
|
|
784
|
+
sourceType: source.type,
|
|
785
|
+
relation: source.relation,
|
|
786
|
+
filePath: relPath
|
|
787
|
+
};
|
|
788
|
+
const existing = result.get(match.specId);
|
|
789
|
+
if (existing) {
|
|
790
|
+
existing.push(globalMatch);
|
|
791
|
+
} else {
|
|
792
|
+
result.set(match.specId, [globalMatch]);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { matches: result, warnings };
|
|
798
|
+
}
|
|
799
|
+
function runDeepValidation(specId, matches, deepValidation, spec) {
|
|
800
|
+
const errors = [];
|
|
801
|
+
const warnings = [];
|
|
802
|
+
for (const match of matches) {
|
|
803
|
+
const rule = deepValidation[match.sourceType];
|
|
804
|
+
if (!rule) continue;
|
|
805
|
+
if (match.sourceType === "openapi") {
|
|
806
|
+
const mapping = rule.mapper(spec);
|
|
807
|
+
const ctx = match.context;
|
|
808
|
+
if (!ctx?.match?.operation) continue;
|
|
809
|
+
validateOpenAPIDeep(specId, mapping, ctx.doc, ctx.match, warnings);
|
|
810
|
+
} else if (match.sourceType === "ddl") {
|
|
811
|
+
const mapping = rule.mapper(spec);
|
|
812
|
+
const ctx = match.context;
|
|
813
|
+
if (!ctx?.table) continue;
|
|
814
|
+
validateDDLDeep(specId, mapping, ctx.table, warnings);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
success: errors.length === 0,
|
|
819
|
+
errors,
|
|
820
|
+
warnings
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function validateOpenAPIDeep(specId, mapping, doc, match, warnings) {
|
|
824
|
+
if (mapping.method && match.operation) {
|
|
825
|
+
const expectedMethod = mapping.method.toLowerCase();
|
|
826
|
+
const actualMethod = match.method?.toLowerCase();
|
|
827
|
+
if (actualMethod && actualMethod !== expectedMethod) {
|
|
828
|
+
warnings.push({
|
|
829
|
+
message: `Method mismatch for "${specId}": expected ${expectedMethod.toUpperCase()}, found ${actualMethod.toUpperCase()}`,
|
|
830
|
+
specId
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (match.operation && mapping.parameters) {
|
|
835
|
+
const opParams = (match.operation.parameters || []).map(
|
|
836
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
837
|
+
(p) => maybeResolveRef(doc, p)
|
|
838
|
+
);
|
|
839
|
+
for (const expectedParam of mapping.parameters) {
|
|
840
|
+
const found = opParams.find(
|
|
841
|
+
(p) => p?.name === expectedParam.name && (!expectedParam.in || p?.in === expectedParam.in)
|
|
842
|
+
);
|
|
843
|
+
if (!found) {
|
|
844
|
+
warnings.push({
|
|
845
|
+
message: `Parameter "${expectedParam.name}" not found in operation "${specId}"`,
|
|
846
|
+
specId,
|
|
847
|
+
field: expectedParam.name
|
|
848
|
+
});
|
|
849
|
+
} else if (expectedParam.type && found.schema?.type) {
|
|
850
|
+
if (!isTypeContainedBy(expectedParam.type, found.schema.type)) {
|
|
851
|
+
warnings.push({
|
|
852
|
+
message: `Parameter "${expectedParam.name}" type mismatch: expected ${expectedParam.type}, found ${found.schema.type}`,
|
|
853
|
+
specId,
|
|
854
|
+
field: expectedParam.name
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (match.operation && mapping.responseProperties) {
|
|
861
|
+
const successResponse = match.operation.responses?.["200"] || match.operation.responses?.["201"];
|
|
862
|
+
let responseSchema = successResponse?.content?.["application/json"]?.schema;
|
|
863
|
+
responseSchema = maybeResolveRef(doc, responseSchema);
|
|
864
|
+
if (responseSchema?.type === "array") {
|
|
865
|
+
responseSchema = maybeResolveRef(doc, responseSchema.items);
|
|
866
|
+
}
|
|
867
|
+
const responseProps = responseSchema?.properties || {};
|
|
868
|
+
for (const expectedProp of mapping.responseProperties) {
|
|
869
|
+
const foundProp = responseProps[expectedProp.name];
|
|
870
|
+
if (!foundProp) {
|
|
871
|
+
warnings.push({
|
|
872
|
+
message: `Response property "${expectedProp.name}" not found in operation "${specId}"`,
|
|
873
|
+
specId,
|
|
874
|
+
field: expectedProp.name
|
|
875
|
+
});
|
|
876
|
+
} else if (expectedProp.type && foundProp.type) {
|
|
877
|
+
if (!isTypeContainedBy(expectedProp.type, foundProp.type)) {
|
|
878
|
+
warnings.push({
|
|
879
|
+
message: `Response property "${expectedProp.name}" type mismatch: expected ${expectedProp.type}, found ${foundProp.type}`,
|
|
880
|
+
specId,
|
|
881
|
+
field: expectedProp.name
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function validateDDLDeep(specId, mapping, table, warnings) {
|
|
889
|
+
if (!mapping.columns) return;
|
|
890
|
+
for (const expectedCol of mapping.columns) {
|
|
891
|
+
const foundCol = table.columns.find(
|
|
892
|
+
(c) => c.name.toLowerCase() === expectedCol.name.toLowerCase()
|
|
893
|
+
);
|
|
894
|
+
if (!foundCol) {
|
|
895
|
+
warnings.push({
|
|
896
|
+
message: `Column "${expectedCol.name}" not found in table "${mapping.tableName}"`,
|
|
897
|
+
specId,
|
|
898
|
+
field: expectedCol.name
|
|
899
|
+
});
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (mapping.checkTypes && expectedCol.type && foundCol.type) {
|
|
903
|
+
if (!isTypeContainedBy(expectedCol.type, foundCol.type)) {
|
|
904
|
+
warnings.push({
|
|
905
|
+
message: `Column "${expectedCol.name}" type mismatch in table "${mapping.tableName}": expected ${expectedCol.type}, found ${foundCol.type}`,
|
|
906
|
+
specId,
|
|
907
|
+
field: expectedCol.name
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/cli/check.ts
|
|
407
915
|
async function checkCommand(type, options) {
|
|
408
916
|
console.log(chalk4.blue("speckeeper check"));
|
|
409
917
|
console.log("");
|
|
@@ -416,43 +924,83 @@ async function checkCommand(type, options) {
|
|
|
416
924
|
try {
|
|
417
925
|
const models = config.models || [];
|
|
418
926
|
const specs = config.specs;
|
|
927
|
+
const sources = config.sources ?? [];
|
|
419
928
|
if (options.verbose) {
|
|
420
929
|
console.log(chalk4.gray(` Registered models: ${models.map((m) => m.id).join(", ")}`));
|
|
930
|
+
console.log(chalk4.gray(` Sources: ${sources.length} configured`));
|
|
421
931
|
}
|
|
422
932
|
const results = [];
|
|
933
|
+
const allSpecIds = [];
|
|
934
|
+
const specIdToModel = /* @__PURE__ */ new Map();
|
|
423
935
|
for (const model of models) {
|
|
424
936
|
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
425
|
-
if (modelSpecs.length === 0) continue;
|
|
426
937
|
for (const spec of modelSpecs) {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
938
|
+
const specId = spec.id;
|
|
939
|
+
allSpecIds.push(specId);
|
|
940
|
+
specIdToModel.set(specId, { modelId: model.id, spec });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (sources.length > 0 && allSpecIds.length > 0) {
|
|
944
|
+
const filteredSources = checkType === "all" ? sources : sources.filter((s) => s.type === checkType);
|
|
945
|
+
const { matches, warnings: scanWarnings } = runGlobalScan(filteredSources, allSpecIds, cwd);
|
|
946
|
+
for (const sw of scanWarnings) {
|
|
947
|
+
results.push({
|
|
948
|
+
type: sw.sourceType,
|
|
949
|
+
success: true,
|
|
950
|
+
issues: [{
|
|
951
|
+
severity: "warning",
|
|
952
|
+
message: sw.message
|
|
953
|
+
}]
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
for (const [specId, entry] of specIdToModel) {
|
|
957
|
+
const specMatches = matches.get(specId);
|
|
958
|
+
if (specMatches && specMatches.length > 0) {
|
|
959
|
+
const model = models.find((m) => m.id === entry.modelId);
|
|
960
|
+
const deepValidation = model?.getDeepValidation?.();
|
|
961
|
+
if (deepValidation) {
|
|
962
|
+
const deepResult = runDeepValidation(specId, specMatches, deepValidation, entry.spec);
|
|
963
|
+
if (deepResult.errors.length > 0 || deepResult.warnings.length > 0) {
|
|
964
|
+
results.push({
|
|
965
|
+
type: entry.modelId,
|
|
966
|
+
success: deepResult.success,
|
|
967
|
+
issues: [
|
|
968
|
+
...deepResult.errors.map((e) => ({
|
|
969
|
+
severity: "error",
|
|
970
|
+
message: e.message,
|
|
971
|
+
specId: e.specId,
|
|
972
|
+
field: e.field
|
|
973
|
+
})),
|
|
974
|
+
...deepResult.warnings.map((w) => ({
|
|
975
|
+
severity: "warning",
|
|
976
|
+
message: w.message,
|
|
977
|
+
specId: w.specId,
|
|
978
|
+
field: w.field
|
|
979
|
+
}))
|
|
980
|
+
]
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
441
984
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
985
|
+
}
|
|
986
|
+
if (options.verbose) {
|
|
987
|
+
for (const specId of allSpecIds) {
|
|
988
|
+
if (!matches.has(specId)) {
|
|
989
|
+
const entry = specIdToModel.get(specId);
|
|
990
|
+
results.push({
|
|
991
|
+
type: entry?.modelId ?? "unknown",
|
|
992
|
+
success: true,
|
|
993
|
+
issues: [{
|
|
994
|
+
severity: "warning",
|
|
995
|
+
message: `Spec ID "${specId}" not found in any configured source`,
|
|
996
|
+
specId
|
|
997
|
+
}]
|
|
998
|
+
});
|
|
999
|
+
}
|
|
453
1000
|
}
|
|
454
1001
|
}
|
|
455
1002
|
}
|
|
1003
|
+
runLegacyChecks(models, specs, cwd, results);
|
|
456
1004
|
outputCheckResults(results);
|
|
457
1005
|
if (options.coverage) {
|
|
458
1006
|
console.log("");
|
|
@@ -505,6 +1053,41 @@ function loadExternalData(filePath) {
|
|
|
505
1053
|
}
|
|
506
1054
|
return content;
|
|
507
1055
|
}
|
|
1056
|
+
function runLegacyChecks(models, specs, cwd, results) {
|
|
1057
|
+
for (const model of models) {
|
|
1058
|
+
const modelSpecs = getSpecsFromConfig(specs, model.id);
|
|
1059
|
+
if (modelSpecs.length === 0) continue;
|
|
1060
|
+
for (const spec of modelSpecs) {
|
|
1061
|
+
const sourcePath = model.getExternalSourcePath?.(spec);
|
|
1062
|
+
if (!sourcePath) continue;
|
|
1063
|
+
const fullPath = join(cwd, sourcePath);
|
|
1064
|
+
if (!existsSync(fullPath)) {
|
|
1065
|
+
results.push({
|
|
1066
|
+
type: model.id,
|
|
1067
|
+
success: false,
|
|
1068
|
+
issues: [{
|
|
1069
|
+
severity: "error",
|
|
1070
|
+
message: `External source not found: ${sourcePath}`,
|
|
1071
|
+
specId: spec.id
|
|
1072
|
+
}]
|
|
1073
|
+
});
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const externalData = loadExternalData(fullPath);
|
|
1077
|
+
const checkResult = model.check(spec, externalData);
|
|
1078
|
+
if (!checkResult.success || checkResult.errors.length > 0 || checkResult.warnings.length > 0) {
|
|
1079
|
+
results.push({
|
|
1080
|
+
type: model.id,
|
|
1081
|
+
success: checkResult.success,
|
|
1082
|
+
issues: [
|
|
1083
|
+
...checkResult.errors.map((e) => ({ severity: "error", message: e.message, specId: e.specId, field: e.field })),
|
|
1084
|
+
...checkResult.warnings.map((w) => ({ severity: "warning", message: w.message, specId: w.specId, field: w.field }))
|
|
1085
|
+
]
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
508
1091
|
function outputCheckResults(results) {
|
|
509
1092
|
console.log("");
|
|
510
1093
|
if (results.length === 0) {
|
|
@@ -1218,9 +1801,6 @@ function resolveEdges(edges, nodes, speckeeperClassName) {
|
|
|
1218
1801
|
}
|
|
1219
1802
|
return { resolved, diagnostics };
|
|
1220
1803
|
}
|
|
1221
|
-
function isCheckEdge(entry) {
|
|
1222
|
-
return entry.category === "check";
|
|
1223
|
-
}
|
|
1224
1804
|
|
|
1225
1805
|
// src/scaffold/template-registry.ts
|
|
1226
1806
|
var LEVEL_PATTERNS = [
|
|
@@ -1264,56 +1844,11 @@ function toKebabCase(s) {
|
|
|
1264
1844
|
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
1265
1845
|
}
|
|
1266
1846
|
|
|
1267
|
-
// src/scaffold/artifact-defaults.ts
|
|
1268
|
-
var SPECIALIZED_CHECKER_MAP = {
|
|
1269
|
-
openapi: "externalOpenAPIChecker",
|
|
1270
|
-
sqlschema: "externalSqlSchemaChecker"
|
|
1271
|
-
};
|
|
1272
|
-
function generateCheckerCode(bindings, modelName) {
|
|
1273
|
-
const dslImports = /* @__PURE__ */ new Set(["annotationChecker"]);
|
|
1274
|
-
for (const b of bindings) {
|
|
1275
|
-
const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
|
|
1276
|
-
if (specialized) dslImports.add(specialized);
|
|
1277
|
-
}
|
|
1278
|
-
const checks = bindings.map((b) => {
|
|
1279
|
-
const specialized = SPECIALIZED_CHECKER_MAP[b.targetClass];
|
|
1280
|
-
const artifact = `'${b.targetClass}'`;
|
|
1281
|
-
const relationType = `'${b.edgeType}'`;
|
|
1282
|
-
if (specialized) {
|
|
1283
|
-
return `{ artifact: ${artifact}, relationType: ${relationType}, checker: ${specialized}() }`;
|
|
1284
|
-
}
|
|
1285
|
-
return `{ artifact: ${artifact}, relationType: ${relationType} }`;
|
|
1286
|
-
});
|
|
1287
|
-
let config;
|
|
1288
|
-
if (checks.length === 1) {
|
|
1289
|
-
config = checks[0];
|
|
1290
|
-
} else {
|
|
1291
|
-
config = `{
|
|
1292
|
-
checks: [
|
|
1293
|
-
${checks.join(",\n ")}
|
|
1294
|
-
],
|
|
1295
|
-
}`;
|
|
1296
|
-
}
|
|
1297
|
-
const code = `protected externalChecker = annotationChecker<${modelName}>(${config});`;
|
|
1298
|
-
return {
|
|
1299
|
-
code,
|
|
1300
|
-
dslImports: Array.from(dslImports)
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
1847
|
// src/scaffold/templates/base.ts
|
|
1305
1848
|
function generateBaseModel(params) {
|
|
1306
1849
|
const schemaName = `${params.modelName}Schema`;
|
|
1307
1850
|
const className = `${params.modelName}Model`;
|
|
1308
|
-
const
|
|
1309
|
-
const checkerGen = hasCheckerBindings ? generateCheckerCode(params.checkerBindings, params.modelName) : null;
|
|
1310
|
-
const dslImportItems = ["requireField"];
|
|
1311
|
-
if (checkerGen) {
|
|
1312
|
-
dslImportItems.push(...checkerGen.dslImports);
|
|
1313
|
-
}
|
|
1314
|
-
const dslImportLine = `import { ${dslImportItems.join(", ")} } from 'speckeeper/dsl';`;
|
|
1315
|
-
const externalCheckerLine = checkerGen ? `
|
|
1316
|
-
${checkerGen.code}` : "";
|
|
1851
|
+
const dslImportLine = `import { requireField } from 'speckeeper/dsl';`;
|
|
1317
1852
|
return `/**
|
|
1318
1853
|
* ${params.modelName} Model Definition
|
|
1319
1854
|
*
|
|
@@ -1358,7 +1893,7 @@ class ${className} extends Model<typeof ${schemaName}> {
|
|
|
1358
1893
|
requireField<${params.modelName}>('description'),
|
|
1359
1894
|
];
|
|
1360
1895
|
|
|
1361
|
-
protected exporters: Exporter<${params.modelName}>[] = []
|
|
1896
|
+
protected exporters: Exporter<${params.modelName}>[] = [];
|
|
1362
1897
|
}
|
|
1363
1898
|
|
|
1364
1899
|
export { ${className} };
|
|
@@ -1371,34 +1906,15 @@ var MODEL_TEMPLATE_FUNCTIONS = {
|
|
|
1371
1906
|
};
|
|
1372
1907
|
|
|
1373
1908
|
// src/scaffold/model-generator.ts
|
|
1374
|
-
function
|
|
1375
|
-
const isSpk = (id) => nodes.get(id)?.classes.includes(speckeeperClassName) ?? false;
|
|
1376
|
-
const bindings = [];
|
|
1377
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1378
|
-
for (const edge of outgoingEdges) {
|
|
1379
|
-
if (!isCheckEdge(edge.vocabulary)) continue;
|
|
1380
|
-
if (isSpk(edge.targetId)) continue;
|
|
1381
|
-
if (seen.has(edge.targetId)) continue;
|
|
1382
|
-
seen.add(edge.targetId);
|
|
1383
|
-
const targetNode = nodes.get(edge.targetId);
|
|
1384
|
-
const targetClass = targetNode?.classes.find((c) => c !== speckeeperClassName) ?? edge.targetId.toLowerCase();
|
|
1385
|
-
const edgeType = edge.normalizedLabel === "verifiedBy" ? "verifiedBy" : "implements";
|
|
1386
|
-
bindings.push({ edgeType, targetNodeId: edge.targetId, targetClass });
|
|
1387
|
-
}
|
|
1388
|
-
return bindings;
|
|
1389
|
-
}
|
|
1390
|
-
function generateModelFile(node, _incomingEdges, outgoingEdges, allNodes) {
|
|
1909
|
+
function generateModelFile(node, _incomingEdges, _outgoingEdges, _allNodes) {
|
|
1391
1910
|
const templateInfo = resolveModelTemplate(node.id, node.classes, node.subgraph);
|
|
1392
1911
|
const templateFn = MODEL_TEMPLATE_FUNCTIONS[templateInfo.templateName];
|
|
1393
|
-
const bindings = allNodes ? resolveCheckerBindings(node.id, outgoingEdges, allNodes, "speckeeper") : [];
|
|
1394
|
-
const checkerBindings = bindings.length > 0 ? bindings.map((b) => ({ edgeType: b.edgeType, targetClass: b.targetClass })) : void 0;
|
|
1395
1912
|
const params = {
|
|
1396
1913
|
modelId: templateInfo.fileName,
|
|
1397
1914
|
modelName: templateInfo.modelName,
|
|
1398
1915
|
idPrefix: templateInfo.defaultIdPrefix,
|
|
1399
1916
|
level: templateInfo.defaultLevel,
|
|
1400
|
-
description: node.label ?? node.id
|
|
1401
|
-
checkerBindings
|
|
1917
|
+
description: node.label ?? node.id
|
|
1402
1918
|
};
|
|
1403
1919
|
const content = !templateFn ? MODEL_TEMPLATE_FUNCTIONS["base"](params) : templateFn(params);
|
|
1404
1920
|
return {
|
|
@@ -1486,9 +2002,9 @@ function generateAllModelFiles(speckeeperNodes, resolvedEdges, allNodes) {
|
|
|
1486
2002
|
generated.add(key);
|
|
1487
2003
|
const groupNodes = templateNodeGroups.get(key) ?? [node];
|
|
1488
2004
|
const groupNodeIds = new Set(groupNodes.map((n) => n.id));
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
files.push(generateModelFile(node
|
|
2005
|
+
resolvedEdges.filter((e) => groupNodeIds.has(e.targetId));
|
|
2006
|
+
resolvedEdges.filter((e) => groupNodeIds.has(e.sourceId));
|
|
2007
|
+
files.push(generateModelFile(node));
|
|
1492
2008
|
files.push(generateSpecDataFile(node));
|
|
1493
2009
|
}
|
|
1494
2010
|
files.push(generateDesignIndex(speckeeperNodes));
|
|
@@ -1648,7 +2164,7 @@ var program = new Command();
|
|
|
1648
2164
|
program.name("speckeeper").description("Requirements and design management framework with TypeScript DSL").version(getVersion());
|
|
1649
2165
|
program.command("build").description("Generate docs/ and specs/ from TypeScript models").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Base output directory path", ".").option("-f, --format <format>", "Output format: markdown, json, both", "both").option("-w, --watch", "Watch for changes and auto-regenerate").option("-v, --verbose", "Show detailed output").action(buildCommand);
|
|
1650
2166
|
program.command("lint").description("Check design integrity (ID duplicates, references, layer violations, etc.)").option("-c, --config <path>", "Path to config file").option("-p, --phase <phase>", "Phase gate to check against (REQ, HLD, LLD, OPS)").option("-s, --strict", "Treat warnings as errors").option("--fix", "Attempt to fix auto-fixable issues").option("-f, --format <format>", "Output format: text, json, github", "text").action(lintCommand);
|
|
1651
|
-
program.command("drift").description("Check if generated files have been manually edited").option("-c, --config <path>", "Path to config file").option("-u, --update", "Auto-update if differences are found").option("-f, --format <format>", "Output format: text, json, diff", "text").action(driftCommand);
|
|
2167
|
+
program.command("drift").description("Check if generated files have been manually edited").option("-c, --config <path>", "Path to config file").option("-u, --update", "Auto-update if differences are found").option("-f, --format <format>", "Output format: text, json, diff", "text").option("--fail-on-drift", "Exit with code 1 if drift is detected (for CI)").action(driftCommand);
|
|
1652
2168
|
program.command("check").description("Check external SSOT conformance (including custom models)").argument("[type]", "Type of check: external-ssot, openapi, ddl, iac, custom, all, test").option("-c, --config <path>", "Path to config file").option("--strict", "Treat warnings as errors").option("-v, --verbose", "Show detailed output").option("--coverage", "Check if all testable acceptance criteria are covered by TestRefs").action(checkCommand);
|
|
1653
2169
|
program.command("new").description("Create a new element with auto-generated ID").argument("<type>", "Type: requirement, usecase, entity, component, screen, flow, error-case, term").option("-k, --kind <kind>", "Sub-kind (e.g., functional, non-functional for requirements)").option("-n, --name <name>", "Name of the element").option("-o, --output <path>", "Output directory path").option("-t, --template <path>", "Path to template file").action(newCommand);
|
|
1654
2170
|
program.command("impact").description("Analyze impact of changes to an ID").argument("<id>", "ID to analyze (e.g., REQ-001, ENT-ORDER)").option("-c, --config <path>", "Path to config file").option("-d, --depth <depth>", "Analysis depth (reference tracking level)", "3").option("--direction <direction>", "Analysis direction: upstream, downstream, both", "both").option("-f, --format <format>", "Output format: text, json, mermaid", "text").action(impactCommand);
|