qfai 1.0.6 → 1.1.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 +327 -245
- package/assets/init/.qfai/README.md +2 -1
- package/assets/init/.qfai/assistant/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-configure.md +197 -0
- package/assets/init/.qfai/assistant/prompts/qfai-verify.md +1 -1
- package/assets/init/.qfai/assistant/steering/README.md +6 -0
- package/assets/init/.qfai/assistant/steering/manifest.md +43 -0
- package/assets/init/.qfai/contracts/db/README.md +10 -3
- package/assets/init/.qfai/samples/guardrails/delta_with_guardrails.md +19 -0
- package/assets/init/.qfai/specs/README.md +4 -0
- package/assets/init/root/.claude/commands/qfai-configure.md +14 -0
- package/assets/init/root/.claude/commands/qfai-discuss.md +14 -0
- package/assets/init/root/.claude/commands/qfai-implement.md +14 -0
- package/assets/init/root/.claude/commands/qfai-require.md +14 -0
- package/assets/init/root/.claude/commands/qfai-scenario-test.md +14 -0
- package/assets/init/root/.claude/commands/qfai-spec.md +14 -0
- package/assets/init/root/.claude/commands/qfai-unit-test.md +14 -0
- package/assets/init/root/.claude/commands/qfai-verify.md +14 -0
- package/assets/init/root/.codex/README.md +16 -0
- package/assets/init/root/.codex/skills/qfai-configure/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-discuss/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-implement/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-require/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-scenario-test/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-spec/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-unit-test/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-verify/SKILL.md +18 -0
- package/assets/init/root/.github/copilot-instructions.md +14 -0
- package/assets/init/root/.github/prompts/qfai-configure.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-discuss.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-implement.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-require.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-scenario-test.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-spec.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-unit-test.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-verify.prompt.md +17 -0
- package/assets/init/root/.github/workflows/qfai.yml +0 -2
- package/assets/init/root/qfai.config.yaml +1 -8
- package/dist/cli/index.cjs +880 -196
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +866 -182
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +731 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.mjs +719 -216
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-pr.md +0 -209
package/dist/index.cjs
CHANGED
|
@@ -30,19 +30,26 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
checkDecisionGuardrails: () => checkDecisionGuardrails,
|
|
33
34
|
createReportData: () => createReportData,
|
|
34
35
|
defaultConfig: () => defaultConfig,
|
|
35
36
|
extractAllIds: () => extractAllIds,
|
|
37
|
+
extractDecisionGuardrailsFromMarkdown: () => extractDecisionGuardrailsFromMarkdown,
|
|
36
38
|
extractIds: () => extractIds,
|
|
37
39
|
extractInvalidIds: () => extractInvalidIds,
|
|
40
|
+
filterDecisionGuardrailsByKeyword: () => filterDecisionGuardrailsByKeyword,
|
|
38
41
|
findConfigRoot: () => findConfigRoot,
|
|
42
|
+
formatGuardrailsForLlm: () => formatGuardrailsForLlm,
|
|
39
43
|
formatReportJson: () => formatReportJson,
|
|
40
44
|
formatReportMarkdown: () => formatReportMarkdown,
|
|
41
45
|
getConfigPath: () => getConfigPath,
|
|
42
46
|
lintSql: () => lintSql,
|
|
43
47
|
loadConfig: () => loadConfig,
|
|
48
|
+
loadDecisionGuardrails: () => loadDecisionGuardrails,
|
|
49
|
+
normalizeDecisionGuardrails: () => normalizeDecisionGuardrails,
|
|
44
50
|
resolvePath: () => resolvePath,
|
|
45
51
|
resolveToolVersion: () => resolveToolVersion,
|
|
52
|
+
sortDecisionGuardrails: () => sortDecisionGuardrails,
|
|
46
53
|
validateContracts: () => validateContracts,
|
|
47
54
|
validateDefinedIds: () => validateDefinedIds,
|
|
48
55
|
validateDeltas: () => validateDeltas,
|
|
@@ -71,15 +78,7 @@ var defaultConfig = {
|
|
|
71
78
|
validation: {
|
|
72
79
|
failOn: "error",
|
|
73
80
|
require: {
|
|
74
|
-
specSections: [
|
|
75
|
-
"\u80CC\u666F",
|
|
76
|
-
"\u30B9\u30B3\u30FC\u30D7",
|
|
77
|
-
"\u975E\u30B4\u30FC\u30EB",
|
|
78
|
-
"\u7528\u8A9E",
|
|
79
|
-
"\u524D\u63D0",
|
|
80
|
-
"\u6C7A\u5B9A\u4E8B\u9805",
|
|
81
|
-
"\u696D\u52D9\u30EB\u30FC\u30EB"
|
|
82
|
-
]
|
|
81
|
+
specSections: []
|
|
83
82
|
},
|
|
84
83
|
traceability: {
|
|
85
84
|
brMustHaveSc: true,
|
|
@@ -447,80 +446,9 @@ function isRecord(value) {
|
|
|
447
446
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
448
447
|
}
|
|
449
448
|
|
|
450
|
-
// src/core/
|
|
451
|
-
var
|
|
452
|
-
|
|
453
|
-
"BR",
|
|
454
|
-
"SC",
|
|
455
|
-
"UI",
|
|
456
|
-
"API",
|
|
457
|
-
"DB",
|
|
458
|
-
"THEMA"
|
|
459
|
-
];
|
|
460
|
-
var STRICT_ID_PATTERNS = {
|
|
461
|
-
SPEC: /\bSPEC-\d{4}\b/g,
|
|
462
|
-
BR: /\bBR-\d{4}\b/g,
|
|
463
|
-
SC: /\bSC-\d{4}\b/g,
|
|
464
|
-
UI: /\bUI-\d{4}\b/g,
|
|
465
|
-
API: /\bAPI-\d{4}\b/g,
|
|
466
|
-
DB: /\bDB-\d{4}\b/g,
|
|
467
|
-
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
468
|
-
ADR: /\bADR-\d{4}\b/g
|
|
469
|
-
};
|
|
470
|
-
var LOOSE_ID_PATTERNS = {
|
|
471
|
-
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
472
|
-
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
473
|
-
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
474
|
-
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
475
|
-
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
476
|
-
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
477
|
-
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
478
|
-
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
479
|
-
};
|
|
480
|
-
function extractIds(text, prefix) {
|
|
481
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
482
|
-
const matches = text.match(pattern);
|
|
483
|
-
return unique(matches ?? []);
|
|
484
|
-
}
|
|
485
|
-
function extractAllIds(text) {
|
|
486
|
-
const all = [];
|
|
487
|
-
ID_PREFIXES.forEach((prefix) => {
|
|
488
|
-
all.push(...extractIds(text, prefix));
|
|
489
|
-
});
|
|
490
|
-
return unique(all);
|
|
491
|
-
}
|
|
492
|
-
function extractInvalidIds(text, prefixes) {
|
|
493
|
-
const invalid = [];
|
|
494
|
-
for (const prefix of prefixes) {
|
|
495
|
-
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
496
|
-
for (const candidate of candidates) {
|
|
497
|
-
if (!isValidId(candidate, prefix)) {
|
|
498
|
-
invalid.push(candidate);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return unique(invalid);
|
|
503
|
-
}
|
|
504
|
-
function unique(values) {
|
|
505
|
-
return Array.from(new Set(values));
|
|
506
|
-
}
|
|
507
|
-
function isValidId(value, prefix) {
|
|
508
|
-
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
509
|
-
const strict = new RegExp(pattern.source);
|
|
510
|
-
return strict.test(value);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// src/core/report.ts
|
|
514
|
-
var import_promises15 = require("fs/promises");
|
|
515
|
-
var import_node_path16 = __toESM(require("path"), 1);
|
|
516
|
-
|
|
517
|
-
// src/core/contractIndex.ts
|
|
518
|
-
var import_promises5 = require("fs/promises");
|
|
519
|
-
var import_node_path5 = __toESM(require("path"), 1);
|
|
520
|
-
|
|
521
|
-
// src/core/discovery.ts
|
|
522
|
-
var import_promises4 = require("fs/promises");
|
|
523
|
-
var import_node_path4 = __toESM(require("path"), 1);
|
|
449
|
+
// src/core/decisionGuardrails.ts
|
|
450
|
+
var import_promises3 = require("fs/promises");
|
|
451
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
524
452
|
|
|
525
453
|
// src/core/fs.ts
|
|
526
454
|
var import_promises2 = require("fs/promises");
|
|
@@ -623,24 +551,549 @@ function destroyStream(stream) {
|
|
|
623
551
|
}
|
|
624
552
|
}
|
|
625
553
|
|
|
554
|
+
// src/core/parse/markdown.ts
|
|
555
|
+
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
556
|
+
function parseHeadings(md) {
|
|
557
|
+
const lines = md.split(/\r?\n/);
|
|
558
|
+
const headings = [];
|
|
559
|
+
for (let i = 0; i < lines.length; i++) {
|
|
560
|
+
const line = lines[i] ?? "";
|
|
561
|
+
const match = line.match(HEADING_RE);
|
|
562
|
+
if (!match) continue;
|
|
563
|
+
const levelToken = match[1];
|
|
564
|
+
const title = match[2];
|
|
565
|
+
if (!levelToken || !title) continue;
|
|
566
|
+
headings.push({
|
|
567
|
+
level: levelToken.length,
|
|
568
|
+
title: title.trim(),
|
|
569
|
+
line: i + 1
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return headings;
|
|
573
|
+
}
|
|
574
|
+
function extractH2Sections(md) {
|
|
575
|
+
const lines = md.split(/\r?\n/);
|
|
576
|
+
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
577
|
+
const sections = /* @__PURE__ */ new Map();
|
|
578
|
+
for (let i = 0; i < headings.length; i++) {
|
|
579
|
+
const current = headings[i];
|
|
580
|
+
if (!current) continue;
|
|
581
|
+
const next = headings[i + 1];
|
|
582
|
+
const startLine = current.line + 1;
|
|
583
|
+
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
584
|
+
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
585
|
+
sections.set(current.title.trim(), {
|
|
586
|
+
title: current.title.trim(),
|
|
587
|
+
startLine,
|
|
588
|
+
endLine,
|
|
589
|
+
body
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return sections;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/core/decisionGuardrails.ts
|
|
596
|
+
var DEFAULT_DECISION_GUARDRAILS_GLOBS = [".qfai/specs/**/delta.md"];
|
|
597
|
+
var DEFAULT_GUARDRAILS_IGNORE_GLOBS = [
|
|
598
|
+
"**/node_modules/**",
|
|
599
|
+
"**/.git/**",
|
|
600
|
+
"**/dist/**",
|
|
601
|
+
"**/build/**",
|
|
602
|
+
"**/.pnpm/**",
|
|
603
|
+
"**/tmp/**",
|
|
604
|
+
"**/.mcp-tools/**"
|
|
605
|
+
];
|
|
606
|
+
var SECTION_TITLE = "decision guardrails";
|
|
607
|
+
var ENTRY_START_RE = /^\s*[-*]\s+ID:\s*(.+?)\s*$/i;
|
|
608
|
+
var FIELD_RE = /^\s{2,}([A-Za-z][A-Za-z0-9 _-]*):\s*(.*)$/;
|
|
609
|
+
var CONTINUATION_RE = /^\s{4,}(.+)$/;
|
|
610
|
+
var ID_FORMAT_RE = /^DG-\d{4}$/;
|
|
611
|
+
var TYPE_ORDER = {
|
|
612
|
+
"non-goal": 0,
|
|
613
|
+
"not-now": 1,
|
|
614
|
+
"trade-off": 2
|
|
615
|
+
};
|
|
616
|
+
async function loadDecisionGuardrails(root, options = {}) {
|
|
617
|
+
const errors = [];
|
|
618
|
+
const files = await scanDecisionGuardrailFiles(
|
|
619
|
+
root,
|
|
620
|
+
options.paths,
|
|
621
|
+
errors,
|
|
622
|
+
options.specsRoot
|
|
623
|
+
);
|
|
624
|
+
const entries = [];
|
|
625
|
+
for (const filePath of files) {
|
|
626
|
+
try {
|
|
627
|
+
const content = await (0, import_promises3.readFile)(filePath, "utf-8");
|
|
628
|
+
const parsed = extractDecisionGuardrailsFromMarkdown(content, filePath);
|
|
629
|
+
entries.push(...parsed);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
errors.push({ path: filePath, message: String(error) });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return { entries, errors, files };
|
|
635
|
+
}
|
|
636
|
+
function extractDecisionGuardrailsFromMarkdown(markdown, filePath) {
|
|
637
|
+
const sections = extractH2Sections(markdown);
|
|
638
|
+
const section = findDecisionGuardrailsSection(sections);
|
|
639
|
+
if (!section) {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
const lines = section.body.split(/\r?\n/);
|
|
643
|
+
const entries = [];
|
|
644
|
+
let current = null;
|
|
645
|
+
const flush = () => {
|
|
646
|
+
if (!current) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const entry = {
|
|
650
|
+
keywords: current.keywords,
|
|
651
|
+
source: { file: filePath, line: current.startLine },
|
|
652
|
+
...current.fields.id ? { id: current.fields.id } : {},
|
|
653
|
+
...current.fields.type ? { type: current.fields.type } : {},
|
|
654
|
+
...current.fields.guardrail ? { guardrail: current.fields.guardrail } : {},
|
|
655
|
+
...current.fields.rationale ? { rationale: current.fields.rationale } : {},
|
|
656
|
+
...current.fields.reconsider ? { reconsider: current.fields.reconsider } : {},
|
|
657
|
+
...current.fields.related ? { related: current.fields.related } : {},
|
|
658
|
+
...current.fields.title ? { title: current.fields.title } : {}
|
|
659
|
+
};
|
|
660
|
+
entries.push(entry);
|
|
661
|
+
current = null;
|
|
662
|
+
};
|
|
663
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
664
|
+
const rawLine = lines[i] ?? "";
|
|
665
|
+
const lineNumber = section.startLine + i;
|
|
666
|
+
const entryMatch = rawLine.match(ENTRY_START_RE);
|
|
667
|
+
if (entryMatch) {
|
|
668
|
+
flush();
|
|
669
|
+
const id = entryMatch[1]?.trim() ?? "";
|
|
670
|
+
current = {
|
|
671
|
+
startLine: lineNumber,
|
|
672
|
+
fields: { id },
|
|
673
|
+
keywords: []
|
|
674
|
+
};
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (!current) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const fieldMatch = rawLine.match(FIELD_RE);
|
|
681
|
+
if (fieldMatch) {
|
|
682
|
+
const rawKey = fieldMatch[1] ?? "";
|
|
683
|
+
const value = fieldMatch[2] ?? "";
|
|
684
|
+
const key = normalizeFieldKey(rawKey);
|
|
685
|
+
if (key) {
|
|
686
|
+
if (key === "keywords") {
|
|
687
|
+
current.keywords.push(
|
|
688
|
+
...value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
|
|
689
|
+
);
|
|
690
|
+
} else {
|
|
691
|
+
const trimmed = value.trim();
|
|
692
|
+
if (trimmed.length > 0) {
|
|
693
|
+
const existing = current.fields[key];
|
|
694
|
+
current.fields[key] = existing ? `${existing}
|
|
695
|
+
${trimmed}` : trimmed;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
current.lastKey = key;
|
|
699
|
+
} else {
|
|
700
|
+
delete current.lastKey;
|
|
701
|
+
}
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const continuationMatch = rawLine.match(CONTINUATION_RE);
|
|
705
|
+
if (continuationMatch && current.lastKey) {
|
|
706
|
+
const value = continuationMatch[1]?.trim() ?? "";
|
|
707
|
+
if (value.length > 0) {
|
|
708
|
+
const existing = current.fields[current.lastKey];
|
|
709
|
+
current.fields[current.lastKey] = existing ? `${existing}
|
|
710
|
+
${value}` : value;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
flush();
|
|
715
|
+
return entries;
|
|
716
|
+
}
|
|
717
|
+
function normalizeDecisionGuardrails(entries) {
|
|
718
|
+
const items = [];
|
|
719
|
+
for (const entry of entries) {
|
|
720
|
+
const id = entry.id?.trim();
|
|
721
|
+
const type = normalizeGuardrailType(entry.type);
|
|
722
|
+
const guardrail = entry.guardrail?.trim();
|
|
723
|
+
if (!id || !type || !guardrail) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const item = {
|
|
727
|
+
id,
|
|
728
|
+
type,
|
|
729
|
+
guardrail,
|
|
730
|
+
keywords: entry.keywords?.filter((word) => word.length > 0) ?? [],
|
|
731
|
+
source: entry.source
|
|
732
|
+
};
|
|
733
|
+
const rationale = entry.rationale?.trim();
|
|
734
|
+
if (rationale) {
|
|
735
|
+
item.rationale = rationale;
|
|
736
|
+
}
|
|
737
|
+
const reconsider = entry.reconsider?.trim();
|
|
738
|
+
if (reconsider) {
|
|
739
|
+
item.reconsider = reconsider;
|
|
740
|
+
}
|
|
741
|
+
const related = entry.related?.trim();
|
|
742
|
+
if (related) {
|
|
743
|
+
item.related = related;
|
|
744
|
+
}
|
|
745
|
+
const title = entry.title?.trim();
|
|
746
|
+
if (title) {
|
|
747
|
+
item.title = title;
|
|
748
|
+
}
|
|
749
|
+
items.push(item);
|
|
750
|
+
}
|
|
751
|
+
return items;
|
|
752
|
+
}
|
|
753
|
+
function sortDecisionGuardrails(items) {
|
|
754
|
+
return [...items].sort((a, b) => {
|
|
755
|
+
const typeOrder = (TYPE_ORDER[a.type] ?? 999) - (TYPE_ORDER[b.type] ?? 999);
|
|
756
|
+
if (typeOrder !== 0) {
|
|
757
|
+
return typeOrder;
|
|
758
|
+
}
|
|
759
|
+
return a.id.localeCompare(b.id);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
function filterDecisionGuardrailsByKeyword(items, keyword) {
|
|
763
|
+
const needle = keyword?.trim().toLowerCase();
|
|
764
|
+
if (!needle) {
|
|
765
|
+
return items;
|
|
766
|
+
}
|
|
767
|
+
return items.filter((item) => {
|
|
768
|
+
const haystack = [
|
|
769
|
+
item.title,
|
|
770
|
+
item.guardrail,
|
|
771
|
+
item.rationale,
|
|
772
|
+
item.related,
|
|
773
|
+
item.keywords.join(" ")
|
|
774
|
+
].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
|
|
775
|
+
return haystack.some((value) => value.includes(needle));
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function formatGuardrailsForLlm(items, max) {
|
|
779
|
+
const limit = Math.max(0, Math.floor(max));
|
|
780
|
+
const lines = ["# Decision Guardrails (extract)", ""];
|
|
781
|
+
const slice = limit > 0 ? items.slice(0, limit) : [];
|
|
782
|
+
if (slice.length === 0) {
|
|
783
|
+
lines.push("- (none)");
|
|
784
|
+
return lines.join("\n");
|
|
785
|
+
}
|
|
786
|
+
for (const item of slice) {
|
|
787
|
+
lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
|
|
788
|
+
if (item.rationale) {
|
|
789
|
+
lines.push(` Rationale: ${item.rationale}`);
|
|
790
|
+
}
|
|
791
|
+
if (item.reconsider) {
|
|
792
|
+
lines.push(` Reconsider: ${item.reconsider}`);
|
|
793
|
+
}
|
|
794
|
+
if (item.related) {
|
|
795
|
+
lines.push(` Related: ${item.related}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return lines.join("\n");
|
|
799
|
+
}
|
|
800
|
+
function checkDecisionGuardrails(entries) {
|
|
801
|
+
const errors = [];
|
|
802
|
+
const warnings = [];
|
|
803
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const entry of entries) {
|
|
805
|
+
const file = entry.source.file;
|
|
806
|
+
const line = entry.source.line;
|
|
807
|
+
const id = entry.id?.trim();
|
|
808
|
+
const typeRaw = entry.type?.trim();
|
|
809
|
+
const guardrail = entry.guardrail?.trim();
|
|
810
|
+
const rationale = entry.rationale?.trim();
|
|
811
|
+
const reconsider = entry.reconsider?.trim();
|
|
812
|
+
if (!id) {
|
|
813
|
+
errors.push({
|
|
814
|
+
severity: "error",
|
|
815
|
+
code: "QFAI-GR-001",
|
|
816
|
+
message: "ID is missing",
|
|
817
|
+
file,
|
|
818
|
+
line
|
|
819
|
+
});
|
|
820
|
+
} else {
|
|
821
|
+
const list = idMap.get(id) ?? [];
|
|
822
|
+
list.push(entry);
|
|
823
|
+
idMap.set(id, list);
|
|
824
|
+
if (!ID_FORMAT_RE.test(id)) {
|
|
825
|
+
warnings.push({
|
|
826
|
+
severity: "warning",
|
|
827
|
+
code: "QFAI-GR-002",
|
|
828
|
+
message: `ID format is not standard: ${id}`,
|
|
829
|
+
file,
|
|
830
|
+
line,
|
|
831
|
+
id
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (!typeRaw) {
|
|
836
|
+
errors.push({
|
|
837
|
+
severity: "error",
|
|
838
|
+
code: "QFAI-GR-003",
|
|
839
|
+
message: "Type is missing",
|
|
840
|
+
file,
|
|
841
|
+
line,
|
|
842
|
+
...id ? { id } : {}
|
|
843
|
+
});
|
|
844
|
+
} else if (!normalizeGuardrailType(typeRaw)) {
|
|
845
|
+
errors.push({
|
|
846
|
+
severity: "error",
|
|
847
|
+
code: "QFAI-GR-004",
|
|
848
|
+
message: `Type is invalid: ${typeRaw}`,
|
|
849
|
+
file,
|
|
850
|
+
line,
|
|
851
|
+
...id ? { id } : {}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (!guardrail) {
|
|
855
|
+
errors.push({
|
|
856
|
+
severity: "error",
|
|
857
|
+
code: "QFAI-GR-005",
|
|
858
|
+
message: "Guardrail is missing",
|
|
859
|
+
file,
|
|
860
|
+
line,
|
|
861
|
+
...id ? { id } : {}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
if (!rationale) {
|
|
865
|
+
warnings.push({
|
|
866
|
+
severity: "warning",
|
|
867
|
+
code: "QFAI-GR-006",
|
|
868
|
+
message: "Rationale is missing",
|
|
869
|
+
file,
|
|
870
|
+
line,
|
|
871
|
+
...id ? { id } : {}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (!reconsider) {
|
|
875
|
+
warnings.push({
|
|
876
|
+
severity: "warning",
|
|
877
|
+
code: "QFAI-GR-007",
|
|
878
|
+
message: "Reconsider is missing",
|
|
879
|
+
file,
|
|
880
|
+
line,
|
|
881
|
+
...id ? { id } : {}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
for (const [id, list] of idMap.entries()) {
|
|
886
|
+
if (list.length > 1) {
|
|
887
|
+
const locations = list.map((entry) => `${entry.source.file}:${entry.source.line}`).join(", ");
|
|
888
|
+
const first = list[0];
|
|
889
|
+
const file = first?.source.file ?? "";
|
|
890
|
+
const line = first?.source.line;
|
|
891
|
+
errors.push({
|
|
892
|
+
severity: "error",
|
|
893
|
+
code: "QFAI-GR-008",
|
|
894
|
+
message: `ID is duplicated: ${id} (${locations})`,
|
|
895
|
+
file,
|
|
896
|
+
...line !== void 0 ? { line } : {},
|
|
897
|
+
id
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return { errors, warnings };
|
|
902
|
+
}
|
|
903
|
+
function normalizeGuardrailType(raw) {
|
|
904
|
+
if (!raw) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const normalized = raw.trim().toLowerCase().replace(/[_\s]+/g, "-");
|
|
908
|
+
if (normalized === "non-goal") {
|
|
909
|
+
return "non-goal";
|
|
910
|
+
}
|
|
911
|
+
if (normalized === "not-now") {
|
|
912
|
+
return "not-now";
|
|
913
|
+
}
|
|
914
|
+
if (normalized === "trade-off") {
|
|
915
|
+
return "trade-off";
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
function normalizeFieldKey(raw) {
|
|
920
|
+
const normalized = raw.trim().toLowerCase().replace(/[_\s-]+/g, "");
|
|
921
|
+
switch (normalized) {
|
|
922
|
+
case "id":
|
|
923
|
+
return "id";
|
|
924
|
+
case "type":
|
|
925
|
+
return "type";
|
|
926
|
+
case "guardrail":
|
|
927
|
+
return "guardrail";
|
|
928
|
+
case "rationale":
|
|
929
|
+
case "reason":
|
|
930
|
+
return "rationale";
|
|
931
|
+
case "reconsider":
|
|
932
|
+
return "reconsider";
|
|
933
|
+
case "related":
|
|
934
|
+
return "related";
|
|
935
|
+
case "keywords":
|
|
936
|
+
case "keyword":
|
|
937
|
+
return "keywords";
|
|
938
|
+
case "title":
|
|
939
|
+
case "heading":
|
|
940
|
+
return "title";
|
|
941
|
+
default:
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function scanDecisionGuardrailFiles(root, rawPaths, errors, specsRoot) {
|
|
946
|
+
if (!rawPaths || rawPaths.length === 0) {
|
|
947
|
+
const scanRoot = specsRoot ? import_node_path3.default.isAbsolute(specsRoot) ? specsRoot : import_node_path3.default.resolve(root, specsRoot) : root;
|
|
948
|
+
const globs = specsRoot ? ["**/delta.md"] : DEFAULT_DECISION_GUARDRAILS_GLOBS;
|
|
949
|
+
try {
|
|
950
|
+
const result = await collectFilesByGlobs(scanRoot, {
|
|
951
|
+
globs,
|
|
952
|
+
ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
|
|
953
|
+
});
|
|
954
|
+
return result.files.sort((a, b) => a.localeCompare(b));
|
|
955
|
+
} catch (error) {
|
|
956
|
+
errors.push({ path: scanRoot, message: String(error) });
|
|
957
|
+
return [];
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const files = /* @__PURE__ */ new Set();
|
|
961
|
+
for (const rawPath of rawPaths) {
|
|
962
|
+
const resolved = import_node_path3.default.isAbsolute(rawPath) ? rawPath : import_node_path3.default.resolve(root, rawPath);
|
|
963
|
+
const stats = await safeStat(resolved);
|
|
964
|
+
if (!stats) {
|
|
965
|
+
errors.push({ path: resolved, message: "Path does not exist" });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (stats.isFile()) {
|
|
969
|
+
files.add(resolved);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (stats.isDirectory()) {
|
|
973
|
+
try {
|
|
974
|
+
const result = await collectFilesByGlobs(resolved, {
|
|
975
|
+
globs: ["**/delta.md"],
|
|
976
|
+
ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
|
|
977
|
+
});
|
|
978
|
+
result.files.forEach((file) => files.add(file));
|
|
979
|
+
} catch (error) {
|
|
980
|
+
errors.push({ path: resolved, message: String(error) });
|
|
981
|
+
}
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
errors.push({ path: resolved, message: "Unsupported path type" });
|
|
985
|
+
}
|
|
986
|
+
return Array.from(files).sort((a, b) => a.localeCompare(b));
|
|
987
|
+
}
|
|
988
|
+
async function safeStat(target) {
|
|
989
|
+
try {
|
|
990
|
+
return await (0, import_promises3.stat)(target);
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function findDecisionGuardrailsSection(sections) {
|
|
996
|
+
for (const [title, section] of sections.entries()) {
|
|
997
|
+
if (title.trim().toLowerCase() === SECTION_TITLE) {
|
|
998
|
+
return section;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/core/ids.ts
|
|
1005
|
+
var ID_PREFIXES = [
|
|
1006
|
+
"SPEC",
|
|
1007
|
+
"BR",
|
|
1008
|
+
"SC",
|
|
1009
|
+
"UI",
|
|
1010
|
+
"API",
|
|
1011
|
+
"DB",
|
|
1012
|
+
"THEMA"
|
|
1013
|
+
];
|
|
1014
|
+
var STRICT_ID_PATTERNS = {
|
|
1015
|
+
SPEC: /\bSPEC-\d{4}\b/g,
|
|
1016
|
+
BR: /\bBR-\d{4}\b/g,
|
|
1017
|
+
SC: /\bSC-\d{4}\b/g,
|
|
1018
|
+
UI: /\bUI-\d{4}\b/g,
|
|
1019
|
+
API: /\bAPI-\d{4}\b/g,
|
|
1020
|
+
DB: /\bDB-\d{4}\b/g,
|
|
1021
|
+
THEMA: /\bTHEMA-\d{3}\b/g,
|
|
1022
|
+
ADR: /\bADR-\d{4}\b/g
|
|
1023
|
+
};
|
|
1024
|
+
var LOOSE_ID_PATTERNS = {
|
|
1025
|
+
SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
|
|
1026
|
+
BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
|
|
1027
|
+
SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
|
|
1028
|
+
UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
|
|
1029
|
+
API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
|
|
1030
|
+
DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
|
|
1031
|
+
THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
|
|
1032
|
+
ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
|
|
1033
|
+
};
|
|
1034
|
+
function extractIds(text, prefix) {
|
|
1035
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1036
|
+
const matches = text.match(pattern);
|
|
1037
|
+
return unique(matches ?? []);
|
|
1038
|
+
}
|
|
1039
|
+
function extractAllIds(text) {
|
|
1040
|
+
const all = [];
|
|
1041
|
+
ID_PREFIXES.forEach((prefix) => {
|
|
1042
|
+
all.push(...extractIds(text, prefix));
|
|
1043
|
+
});
|
|
1044
|
+
return unique(all);
|
|
1045
|
+
}
|
|
1046
|
+
function extractInvalidIds(text, prefixes) {
|
|
1047
|
+
const invalid = [];
|
|
1048
|
+
for (const prefix of prefixes) {
|
|
1049
|
+
const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
|
|
1050
|
+
for (const candidate of candidates) {
|
|
1051
|
+
if (!isValidId(candidate, prefix)) {
|
|
1052
|
+
invalid.push(candidate);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return unique(invalid);
|
|
1057
|
+
}
|
|
1058
|
+
function unique(values) {
|
|
1059
|
+
return Array.from(new Set(values));
|
|
1060
|
+
}
|
|
1061
|
+
function isValidId(value, prefix) {
|
|
1062
|
+
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
1063
|
+
const strict = new RegExp(pattern.source);
|
|
1064
|
+
return strict.test(value);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/core/report.ts
|
|
1068
|
+
var import_promises16 = require("fs/promises");
|
|
1069
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
1070
|
+
|
|
1071
|
+
// src/core/contractIndex.ts
|
|
1072
|
+
var import_promises6 = require("fs/promises");
|
|
1073
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1074
|
+
|
|
1075
|
+
// src/core/discovery.ts
|
|
1076
|
+
var import_promises5 = require("fs/promises");
|
|
1077
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1078
|
+
|
|
626
1079
|
// src/core/specLayout.ts
|
|
627
|
-
var
|
|
628
|
-
var
|
|
1080
|
+
var import_promises4 = require("fs/promises");
|
|
1081
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
629
1082
|
var SPEC_DIR_RE = /^spec-\d{4}$/;
|
|
630
1083
|
async function collectSpecEntries(specsRoot) {
|
|
631
1084
|
const dirs = await listSpecDirs(specsRoot);
|
|
632
1085
|
const entries = dirs.map((dir) => ({
|
|
633
1086
|
dir,
|
|
634
|
-
specPath:
|
|
635
|
-
deltaPath:
|
|
636
|
-
scenarioPath:
|
|
1087
|
+
specPath: import_node_path4.default.join(dir, "spec.md"),
|
|
1088
|
+
deltaPath: import_node_path4.default.join(dir, "delta.md"),
|
|
1089
|
+
scenarioPath: import_node_path4.default.join(dir, "scenario.feature")
|
|
637
1090
|
}));
|
|
638
1091
|
return entries.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
639
1092
|
}
|
|
640
1093
|
async function listSpecDirs(specsRoot) {
|
|
641
1094
|
try {
|
|
642
|
-
const items = await (0,
|
|
643
|
-
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) =>
|
|
1095
|
+
const items = await (0, import_promises4.readdir)(specsRoot, { withFileTypes: true });
|
|
1096
|
+
return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path4.default.join(specsRoot, name));
|
|
644
1097
|
} catch (error) {
|
|
645
1098
|
if (isMissingFileError(error)) {
|
|
646
1099
|
return [];
|
|
@@ -702,7 +1155,7 @@ async function filterExisting(files) {
|
|
|
702
1155
|
}
|
|
703
1156
|
async function exists3(target) {
|
|
704
1157
|
try {
|
|
705
|
-
await (0,
|
|
1158
|
+
await (0, import_promises5.access)(target);
|
|
706
1159
|
return true;
|
|
707
1160
|
} catch {
|
|
708
1161
|
return false;
|
|
@@ -711,7 +1164,7 @@ async function exists3(target) {
|
|
|
711
1164
|
function filterByBasenamePrefix(files, prefix) {
|
|
712
1165
|
const lowerPrefix = prefix.toLowerCase();
|
|
713
1166
|
return files.filter(
|
|
714
|
-
(file) =>
|
|
1167
|
+
(file) => import_node_path5.default.basename(file).toLowerCase().startsWith(lowerPrefix)
|
|
715
1168
|
);
|
|
716
1169
|
}
|
|
717
1170
|
|
|
@@ -735,9 +1188,9 @@ function stripContractDeclarationLines(text) {
|
|
|
735
1188
|
// src/core/contractIndex.ts
|
|
736
1189
|
async function buildContractIndex(root, config) {
|
|
737
1190
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
738
|
-
const uiRoot =
|
|
739
|
-
const apiRoot =
|
|
740
|
-
const dbRoot =
|
|
1191
|
+
const uiRoot = import_node_path6.default.join(contractsRoot, "ui");
|
|
1192
|
+
const apiRoot = import_node_path6.default.join(contractsRoot, "api");
|
|
1193
|
+
const dbRoot = import_node_path6.default.join(contractsRoot, "db");
|
|
741
1194
|
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
742
1195
|
collectUiContractFiles(uiRoot),
|
|
743
1196
|
collectThemaContractFiles(uiRoot),
|
|
@@ -757,7 +1210,7 @@ async function buildContractIndex(root, config) {
|
|
|
757
1210
|
}
|
|
758
1211
|
async function indexContractFiles(files, index) {
|
|
759
1212
|
for (const file of files) {
|
|
760
|
-
const text = await (0,
|
|
1213
|
+
const text = await (0, import_promises6.readFile)(file, "utf-8");
|
|
761
1214
|
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
762
1215
|
}
|
|
763
1216
|
}
|
|
@@ -769,15 +1222,15 @@ function record(index, id, file) {
|
|
|
769
1222
|
}
|
|
770
1223
|
|
|
771
1224
|
// src/core/paths.ts
|
|
772
|
-
var
|
|
1225
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
773
1226
|
function toRelativePath(root, target) {
|
|
774
1227
|
if (!target) {
|
|
775
1228
|
return target;
|
|
776
1229
|
}
|
|
777
|
-
if (!
|
|
1230
|
+
if (!import_node_path7.default.isAbsolute(target)) {
|
|
778
1231
|
return toPosixPath(target);
|
|
779
1232
|
}
|
|
780
|
-
const relative =
|
|
1233
|
+
const relative = import_node_path7.default.relative(root, target);
|
|
781
1234
|
if (!relative) {
|
|
782
1235
|
return ".";
|
|
783
1236
|
}
|
|
@@ -875,53 +1328,11 @@ function unique2(values) {
|
|
|
875
1328
|
return Array.from(new Set(values));
|
|
876
1329
|
}
|
|
877
1330
|
|
|
878
|
-
// src/core/parse/markdown.ts
|
|
879
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
880
|
-
function parseHeadings(md) {
|
|
881
|
-
const lines = md.split(/\r?\n/);
|
|
882
|
-
const headings = [];
|
|
883
|
-
for (let i = 0; i < lines.length; i++) {
|
|
884
|
-
const line = lines[i] ?? "";
|
|
885
|
-
const match = line.match(HEADING_RE);
|
|
886
|
-
if (!match) continue;
|
|
887
|
-
const levelToken = match[1];
|
|
888
|
-
const title = match[2];
|
|
889
|
-
if (!levelToken || !title) continue;
|
|
890
|
-
headings.push({
|
|
891
|
-
level: levelToken.length,
|
|
892
|
-
title: title.trim(),
|
|
893
|
-
line: i + 1
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
return headings;
|
|
897
|
-
}
|
|
898
|
-
function extractH2Sections(md) {
|
|
899
|
-
const lines = md.split(/\r?\n/);
|
|
900
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
901
|
-
const sections = /* @__PURE__ */ new Map();
|
|
902
|
-
for (let i = 0; i < headings.length; i++) {
|
|
903
|
-
const current = headings[i];
|
|
904
|
-
if (!current) continue;
|
|
905
|
-
const next = headings[i + 1];
|
|
906
|
-
const startLine = current.line + 1;
|
|
907
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
908
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
909
|
-
sections.set(current.title.trim(), {
|
|
910
|
-
title: current.title.trim(),
|
|
911
|
-
startLine,
|
|
912
|
-
endLine,
|
|
913
|
-
body
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
return sections;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
1331
|
// src/core/parse/spec.ts
|
|
920
1332
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
921
1333
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
922
1334
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
923
1335
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
924
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
925
1336
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
926
1337
|
function parseSpec(md, file) {
|
|
927
1338
|
const headings = parseHeadings(md);
|
|
@@ -929,15 +1340,13 @@ function parseSpec(md, file) {
|
|
|
929
1340
|
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
930
1341
|
const sections = extractH2Sections(md);
|
|
931
1342
|
const sectionNames = new Set(Array.from(sections.keys()));
|
|
932
|
-
const
|
|
933
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
934
|
-
const startLine = brSection?.startLine ?? 1;
|
|
1343
|
+
const lines = md.split(/\r?\n/);
|
|
935
1344
|
const brs = [];
|
|
936
1345
|
const brsWithoutPriority = [];
|
|
937
1346
|
const brsWithInvalidPriority = [];
|
|
938
|
-
for (let i = 0; i <
|
|
939
|
-
const lineText =
|
|
940
|
-
const lineNumber =
|
|
1347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1348
|
+
const lineText = lines[i] ?? "";
|
|
1349
|
+
const lineNumber = i + 1;
|
|
941
1350
|
const validMatch = lineText.match(BR_LINE_RE);
|
|
942
1351
|
if (validMatch) {
|
|
943
1352
|
const id = validMatch[1];
|
|
@@ -995,8 +1404,8 @@ function parseSpec(md, file) {
|
|
|
995
1404
|
}
|
|
996
1405
|
|
|
997
1406
|
// src/core/traceability.ts
|
|
998
|
-
var
|
|
999
|
-
var
|
|
1407
|
+
var import_promises7 = require("fs/promises");
|
|
1408
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1000
1409
|
|
|
1001
1410
|
// src/core/gherkin/parse.ts
|
|
1002
1411
|
var import_gherkin = require("@cucumber/gherkin");
|
|
@@ -1148,7 +1557,7 @@ function extractAnnotatedScIds(text) {
|
|
|
1148
1557
|
async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
1149
1558
|
const scIds = /* @__PURE__ */ new Set();
|
|
1150
1559
|
for (const file of scenarioFiles) {
|
|
1151
|
-
const text = await (0,
|
|
1560
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1152
1561
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1153
1562
|
if (!document || errors.length > 0) {
|
|
1154
1563
|
continue;
|
|
@@ -1166,7 +1575,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
|
|
|
1166
1575
|
async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
|
|
1167
1576
|
const sources = /* @__PURE__ */ new Map();
|
|
1168
1577
|
for (const file of scenarioFiles) {
|
|
1169
|
-
const text = await (0,
|
|
1578
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1170
1579
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
1171
1580
|
if (!document || errors.length > 0) {
|
|
1172
1581
|
continue;
|
|
@@ -1224,10 +1633,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
|
|
|
1224
1633
|
};
|
|
1225
1634
|
}
|
|
1226
1635
|
const normalizedFiles = Array.from(
|
|
1227
|
-
new Set(scanResult.files.map((file) =>
|
|
1636
|
+
new Set(scanResult.files.map((file) => import_node_path8.default.normalize(file)))
|
|
1228
1637
|
);
|
|
1229
1638
|
for (const file of normalizedFiles) {
|
|
1230
|
-
const text = await (0,
|
|
1639
|
+
const text = await (0, import_promises7.readFile)(file, "utf-8");
|
|
1231
1640
|
const scIds = extractAnnotatedScIds(text);
|
|
1232
1641
|
if (scIds.length === 0) {
|
|
1233
1642
|
continue;
|
|
@@ -1286,16 +1695,16 @@ function formatError3(error) {
|
|
|
1286
1695
|
}
|
|
1287
1696
|
|
|
1288
1697
|
// src/core/version.ts
|
|
1289
|
-
var
|
|
1290
|
-
var
|
|
1698
|
+
var import_promises8 = require("fs/promises");
|
|
1699
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1291
1700
|
var import_node_url = require("url");
|
|
1292
1701
|
async function resolveToolVersion() {
|
|
1293
|
-
if ("1.0
|
|
1294
|
-
return "1.0
|
|
1702
|
+
if ("1.1.0".length > 0) {
|
|
1703
|
+
return "1.1.0";
|
|
1295
1704
|
}
|
|
1296
1705
|
try {
|
|
1297
1706
|
const packagePath = resolvePackageJsonPath();
|
|
1298
|
-
const raw = await (0,
|
|
1707
|
+
const raw = await (0, import_promises8.readFile)(packagePath, "utf-8");
|
|
1299
1708
|
const parsed = JSON.parse(raw);
|
|
1300
1709
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
1301
1710
|
return version.length > 0 ? version : "unknown";
|
|
@@ -1306,18 +1715,18 @@ async function resolveToolVersion() {
|
|
|
1306
1715
|
function resolvePackageJsonPath() {
|
|
1307
1716
|
const base = __filename;
|
|
1308
1717
|
const basePath = base.startsWith("file:") ? (0, import_node_url.fileURLToPath)(base) : base;
|
|
1309
|
-
return
|
|
1718
|
+
return import_node_path9.default.resolve(import_node_path9.default.dirname(basePath), "../../package.json");
|
|
1310
1719
|
}
|
|
1311
1720
|
|
|
1312
1721
|
// src/core/validators/contracts.ts
|
|
1313
|
-
var
|
|
1314
|
-
var
|
|
1722
|
+
var import_promises9 = require("fs/promises");
|
|
1723
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1315
1724
|
|
|
1316
1725
|
// src/core/contracts.ts
|
|
1317
|
-
var
|
|
1726
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1318
1727
|
var import_yaml2 = require("yaml");
|
|
1319
1728
|
function parseStructuredContract(file, text) {
|
|
1320
|
-
const ext =
|
|
1729
|
+
const ext = import_node_path10.default.extname(file).toLowerCase();
|
|
1321
1730
|
if (ext === ".json") {
|
|
1322
1731
|
return JSON.parse(text);
|
|
1323
1732
|
}
|
|
@@ -1339,14 +1748,14 @@ async function validateContracts(root, config) {
|
|
|
1339
1748
|
const issues = [];
|
|
1340
1749
|
const contractIndex = await buildContractIndex(root, config);
|
|
1341
1750
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1342
|
-
const uiRoot =
|
|
1751
|
+
const uiRoot = import_node_path11.default.join(contractsRoot, "ui");
|
|
1343
1752
|
const themaIds = new Set(
|
|
1344
1753
|
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
1345
1754
|
);
|
|
1346
1755
|
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
1347
1756
|
issues.push(...await validateThemaContracts(uiRoot));
|
|
1348
|
-
issues.push(...await validateApiContracts(
|
|
1349
|
-
issues.push(...await validateDbContracts(
|
|
1757
|
+
issues.push(...await validateApiContracts(import_node_path11.default.join(contractsRoot, "api")));
|
|
1758
|
+
issues.push(...await validateDbContracts(import_node_path11.default.join(contractsRoot, "db")));
|
|
1350
1759
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
1351
1760
|
return issues;
|
|
1352
1761
|
}
|
|
@@ -1365,7 +1774,7 @@ async function validateUiContracts(uiRoot, themaIds) {
|
|
|
1365
1774
|
}
|
|
1366
1775
|
const issues = [];
|
|
1367
1776
|
for (const file of files) {
|
|
1368
|
-
const text = await (0,
|
|
1777
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1369
1778
|
const declaredIds = extractDeclaredContractIds(text);
|
|
1370
1779
|
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
1371
1780
|
let doc = null;
|
|
@@ -1419,7 +1828,7 @@ async function validateThemaContracts(uiRoot) {
|
|
|
1419
1828
|
}
|
|
1420
1829
|
const issues = [];
|
|
1421
1830
|
for (const file of files) {
|
|
1422
|
-
const text = await (0,
|
|
1831
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1423
1832
|
const invalidIds = extractInvalidIds(text, [
|
|
1424
1833
|
"SPEC",
|
|
1425
1834
|
"BR",
|
|
@@ -1553,7 +1962,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
1553
1962
|
}
|
|
1554
1963
|
const issues = [];
|
|
1555
1964
|
for (const file of files) {
|
|
1556
|
-
const text = await (0,
|
|
1965
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1557
1966
|
const invalidIds = extractInvalidIds(text, [
|
|
1558
1967
|
"SPEC",
|
|
1559
1968
|
"BR",
|
|
@@ -1622,7 +2031,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
1622
2031
|
}
|
|
1623
2032
|
const issues = [];
|
|
1624
2033
|
for (const file of files) {
|
|
1625
|
-
const text = await (0,
|
|
2034
|
+
const text = await (0, import_promises9.readFile)(file, "utf-8");
|
|
1626
2035
|
const invalidIds = extractInvalidIds(text, [
|
|
1627
2036
|
"SPEC",
|
|
1628
2037
|
"BR",
|
|
@@ -1819,9 +2228,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
1819
2228
|
);
|
|
1820
2229
|
return issues;
|
|
1821
2230
|
}
|
|
1822
|
-
const packDir =
|
|
1823
|
-
const packRelative =
|
|
1824
|
-
if (packRelative.startsWith("..") ||
|
|
2231
|
+
const packDir = import_node_path11.default.resolve(uiRoot, packValue);
|
|
2232
|
+
const packRelative = import_node_path11.default.relative(uiRoot, packDir);
|
|
2233
|
+
if (packRelative.startsWith("..") || import_node_path11.default.isAbsolute(packRelative)) {
|
|
1825
2234
|
issues.push(
|
|
1826
2235
|
issue(
|
|
1827
2236
|
"QFAI-ASSET-001",
|
|
@@ -1847,7 +2256,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
1847
2256
|
);
|
|
1848
2257
|
return issues;
|
|
1849
2258
|
}
|
|
1850
|
-
const assetsYamlPath =
|
|
2259
|
+
const assetsYamlPath = import_node_path11.default.join(packDir, "assets.yaml");
|
|
1851
2260
|
if (!await exists4(assetsYamlPath)) {
|
|
1852
2261
|
issues.push(
|
|
1853
2262
|
issue(
|
|
@@ -1862,7 +2271,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
1862
2271
|
}
|
|
1863
2272
|
let manifest;
|
|
1864
2273
|
try {
|
|
1865
|
-
const manifestText = await (0,
|
|
2274
|
+
const manifestText = await (0, import_promises9.readFile)(assetsYamlPath, "utf-8");
|
|
1866
2275
|
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
1867
2276
|
} catch (error) {
|
|
1868
2277
|
issues.push(
|
|
@@ -1935,9 +2344,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
1935
2344
|
);
|
|
1936
2345
|
continue;
|
|
1937
2346
|
}
|
|
1938
|
-
const assetPath =
|
|
1939
|
-
const assetRelative =
|
|
1940
|
-
if (assetRelative.startsWith("..") ||
|
|
2347
|
+
const assetPath = import_node_path11.default.resolve(packDir, entry.path);
|
|
2348
|
+
const assetRelative = import_node_path11.default.relative(packDir, assetPath);
|
|
2349
|
+
if (assetRelative.startsWith("..") || import_node_path11.default.isAbsolute(assetRelative)) {
|
|
1941
2350
|
issues.push(
|
|
1942
2351
|
issue(
|
|
1943
2352
|
"QFAI-ASSET-004",
|
|
@@ -1978,7 +2387,7 @@ function shouldIgnoreInvalidId(value, doc) {
|
|
|
1978
2387
|
return false;
|
|
1979
2388
|
}
|
|
1980
2389
|
const normalized = packValue.replace(/\\/g, "/");
|
|
1981
|
-
const basename =
|
|
2390
|
+
const basename = import_node_path11.default.posix.basename(normalized);
|
|
1982
2391
|
if (!basename) {
|
|
1983
2392
|
return false;
|
|
1984
2393
|
}
|
|
@@ -1988,7 +2397,7 @@ function isSafeRelativePath(value) {
|
|
|
1988
2397
|
if (!value) {
|
|
1989
2398
|
return false;
|
|
1990
2399
|
}
|
|
1991
|
-
if (
|
|
2400
|
+
if (import_node_path11.default.isAbsolute(value)) {
|
|
1992
2401
|
return false;
|
|
1993
2402
|
}
|
|
1994
2403
|
const normalized = value.replace(/\\/g, "/");
|
|
@@ -2003,7 +2412,7 @@ function isSafeRelativePath(value) {
|
|
|
2003
2412
|
}
|
|
2004
2413
|
async function exists4(target) {
|
|
2005
2414
|
try {
|
|
2006
|
-
await (0,
|
|
2415
|
+
await (0, import_promises9.access)(target);
|
|
2007
2416
|
return true;
|
|
2008
2417
|
} catch {
|
|
2009
2418
|
return false;
|
|
@@ -2038,8 +2447,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2038
2447
|
}
|
|
2039
2448
|
|
|
2040
2449
|
// src/core/validators/delta.ts
|
|
2041
|
-
var
|
|
2042
|
-
var
|
|
2450
|
+
var import_promises10 = require("fs/promises");
|
|
2451
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2043
2452
|
async function validateDeltas(root, config) {
|
|
2044
2453
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2045
2454
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2048,9 +2457,9 @@ async function validateDeltas(root, config) {
|
|
|
2048
2457
|
}
|
|
2049
2458
|
const issues = [];
|
|
2050
2459
|
for (const pack of packs) {
|
|
2051
|
-
const deltaPath =
|
|
2460
|
+
const deltaPath = import_node_path12.default.join(pack, "delta.md");
|
|
2052
2461
|
try {
|
|
2053
|
-
await (0,
|
|
2462
|
+
await (0, import_promises10.readFile)(deltaPath, "utf-8");
|
|
2054
2463
|
} catch (error) {
|
|
2055
2464
|
if (isMissingFileError2(error)) {
|
|
2056
2465
|
issues.push(
|
|
@@ -2101,8 +2510,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2101
2510
|
}
|
|
2102
2511
|
|
|
2103
2512
|
// src/core/validators/ids.ts
|
|
2104
|
-
var
|
|
2105
|
-
var
|
|
2513
|
+
var import_promises11 = require("fs/promises");
|
|
2514
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2106
2515
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2107
2516
|
async function validateDefinedIds(root, config) {
|
|
2108
2517
|
const issues = [];
|
|
@@ -2137,7 +2546,7 @@ async function validateDefinedIds(root, config) {
|
|
|
2137
2546
|
}
|
|
2138
2547
|
async function collectSpecDefinitionIds(files, out) {
|
|
2139
2548
|
for (const file of files) {
|
|
2140
|
-
const text = await (0,
|
|
2549
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
2141
2550
|
const parsed = parseSpec(text, file);
|
|
2142
2551
|
if (parsed.specId) {
|
|
2143
2552
|
recordId(out, parsed.specId, file);
|
|
@@ -2147,7 +2556,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
2147
2556
|
}
|
|
2148
2557
|
async function collectScenarioDefinitionIds(files, out) {
|
|
2149
2558
|
for (const file of files) {
|
|
2150
|
-
const text = await (0,
|
|
2559
|
+
const text = await (0, import_promises11.readFile)(file, "utf-8");
|
|
2151
2560
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2152
2561
|
if (!document || errors.length > 0) {
|
|
2153
2562
|
continue;
|
|
@@ -2168,7 +2577,7 @@ function recordId(out, id, file) {
|
|
|
2168
2577
|
}
|
|
2169
2578
|
function formatFileList(files, root) {
|
|
2170
2579
|
return files.map((file) => {
|
|
2171
|
-
const relative =
|
|
2580
|
+
const relative = import_node_path13.default.relative(root, file);
|
|
2172
2581
|
return relative.length > 0 ? relative : file;
|
|
2173
2582
|
}).join(", ");
|
|
2174
2583
|
}
|
|
@@ -2195,20 +2604,20 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
|
|
|
2195
2604
|
}
|
|
2196
2605
|
|
|
2197
2606
|
// src/core/promptsIntegrity.ts
|
|
2198
|
-
var
|
|
2199
|
-
var
|
|
2607
|
+
var import_promises12 = require("fs/promises");
|
|
2608
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2200
2609
|
|
|
2201
2610
|
// src/shared/assets.ts
|
|
2202
2611
|
var import_node_fs = require("fs");
|
|
2203
|
-
var
|
|
2612
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2204
2613
|
var import_node_url2 = require("url");
|
|
2205
2614
|
function getInitAssetsDir() {
|
|
2206
2615
|
const base = __filename;
|
|
2207
2616
|
const basePath = base.startsWith("file:") ? (0, import_node_url2.fileURLToPath)(base) : base;
|
|
2208
|
-
const baseDir =
|
|
2617
|
+
const baseDir = import_node_path14.default.dirname(basePath);
|
|
2209
2618
|
const candidates = [
|
|
2210
|
-
|
|
2211
|
-
|
|
2619
|
+
import_node_path14.default.resolve(baseDir, "../../../assets/init"),
|
|
2620
|
+
import_node_path14.default.resolve(baseDir, "../../assets/init")
|
|
2212
2621
|
];
|
|
2213
2622
|
for (const candidate of candidates) {
|
|
2214
2623
|
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
@@ -2228,10 +2637,10 @@ function getInitAssetsDir() {
|
|
|
2228
2637
|
var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
|
|
2229
2638
|
async function diffProjectPromptsAgainstInitAssets(root, config) {
|
|
2230
2639
|
const promptsDirConfig = config.paths.promptsDir;
|
|
2231
|
-
const promptsDir =
|
|
2640
|
+
const promptsDir = import_node_path15.default.isAbsolute(promptsDirConfig) ? promptsDirConfig : import_node_path15.default.resolve(root, promptsDirConfig);
|
|
2232
2641
|
let templateDir;
|
|
2233
2642
|
try {
|
|
2234
|
-
const rel =
|
|
2643
|
+
const rel = import_node_path15.default.isAbsolute(promptsDirConfig) ? import_node_path15.default.relative(root, promptsDirConfig) : promptsDirConfig;
|
|
2235
2644
|
const normalized = rel.replace(/^[\\/]+/, "");
|
|
2236
2645
|
if (normalized.length === 0 || normalized.startsWith("..")) {
|
|
2237
2646
|
return {
|
|
@@ -2243,7 +2652,7 @@ async function diffProjectPromptsAgainstInitAssets(root, config) {
|
|
|
2243
2652
|
changed: []
|
|
2244
2653
|
};
|
|
2245
2654
|
}
|
|
2246
|
-
templateDir =
|
|
2655
|
+
templateDir = import_node_path15.default.join(getInitAssetsDir(), normalized);
|
|
2247
2656
|
} catch {
|
|
2248
2657
|
return {
|
|
2249
2658
|
status: "skipped_missing_assets",
|
|
@@ -2297,8 +2706,8 @@ async function diffProjectPromptsAgainstInitAssets(root, config) {
|
|
|
2297
2706
|
}
|
|
2298
2707
|
try {
|
|
2299
2708
|
const [a, b] = await Promise.all([
|
|
2300
|
-
(0,
|
|
2301
|
-
(0,
|
|
2709
|
+
(0, import_promises12.readFile)(templateAbs, "utf-8"),
|
|
2710
|
+
(0, import_promises12.readFile)(projectAbs, "utf-8")
|
|
2302
2711
|
]);
|
|
2303
2712
|
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
2304
2713
|
changed.push(rel);
|
|
@@ -2321,7 +2730,7 @@ function normalizeNewlines(text) {
|
|
|
2321
2730
|
return text.replace(/\r\n/g, "\n");
|
|
2322
2731
|
}
|
|
2323
2732
|
function toRel(base, abs) {
|
|
2324
|
-
const rel =
|
|
2733
|
+
const rel = import_node_path15.default.relative(base, abs);
|
|
2325
2734
|
return rel.replace(/[\\/]+/g, "/");
|
|
2326
2735
|
}
|
|
2327
2736
|
function intersectKeys(a, b) {
|
|
@@ -2366,8 +2775,8 @@ async function validatePromptsIntegrity(root, config) {
|
|
|
2366
2775
|
}
|
|
2367
2776
|
|
|
2368
2777
|
// src/core/validators/scenario.ts
|
|
2369
|
-
var
|
|
2370
|
-
var
|
|
2778
|
+
var import_promises13 = require("fs/promises");
|
|
2779
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2371
2780
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2372
2781
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2373
2782
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2390,7 +2799,7 @@ async function validateScenarios(root, config) {
|
|
|
2390
2799
|
}
|
|
2391
2800
|
const issues = [];
|
|
2392
2801
|
for (const entry of entries) {
|
|
2393
|
-
const legacyScenarioPath =
|
|
2802
|
+
const legacyScenarioPath = import_node_path16.default.join(entry.dir, "scenario.md");
|
|
2394
2803
|
if (await fileExists(legacyScenarioPath)) {
|
|
2395
2804
|
issues.push(
|
|
2396
2805
|
issue4(
|
|
@@ -2404,7 +2813,7 @@ async function validateScenarios(root, config) {
|
|
|
2404
2813
|
}
|
|
2405
2814
|
let text;
|
|
2406
2815
|
try {
|
|
2407
|
-
text = await (0,
|
|
2816
|
+
text = await (0, import_promises13.readFile)(entry.scenarioPath, "utf-8");
|
|
2408
2817
|
} catch (error) {
|
|
2409
2818
|
if (isMissingFileError3(error)) {
|
|
2410
2819
|
issues.push(
|
|
@@ -2579,7 +2988,7 @@ function isMissingFileError3(error) {
|
|
|
2579
2988
|
}
|
|
2580
2989
|
async function fileExists(target) {
|
|
2581
2990
|
try {
|
|
2582
|
-
await (0,
|
|
2991
|
+
await (0, import_promises13.access)(target);
|
|
2583
2992
|
return true;
|
|
2584
2993
|
} catch {
|
|
2585
2994
|
return false;
|
|
@@ -2587,7 +2996,7 @@ async function fileExists(target) {
|
|
|
2587
2996
|
}
|
|
2588
2997
|
|
|
2589
2998
|
// src/core/validators/spec.ts
|
|
2590
|
-
var
|
|
2999
|
+
var import_promises14 = require("fs/promises");
|
|
2591
3000
|
async function validateSpecs(root, config) {
|
|
2592
3001
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2593
3002
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -2608,7 +3017,7 @@ async function validateSpecs(root, config) {
|
|
|
2608
3017
|
for (const entry of entries) {
|
|
2609
3018
|
let text;
|
|
2610
3019
|
try {
|
|
2611
|
-
text = await (0,
|
|
3020
|
+
text = await (0, import_promises14.readFile)(entry.specPath, "utf-8");
|
|
2612
3021
|
} catch (error) {
|
|
2613
3022
|
if (isMissingFileError4(error)) {
|
|
2614
3023
|
issues.push(
|
|
@@ -2762,7 +3171,7 @@ function isMissingFileError4(error) {
|
|
|
2762
3171
|
}
|
|
2763
3172
|
|
|
2764
3173
|
// src/core/validators/traceability.ts
|
|
2765
|
-
var
|
|
3174
|
+
var import_promises15 = require("fs/promises");
|
|
2766
3175
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
2767
3176
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
2768
3177
|
async function validateTraceability(root, config) {
|
|
@@ -2782,7 +3191,7 @@ async function validateTraceability(root, config) {
|
|
|
2782
3191
|
const contractIndex = await buildContractIndex(root, config);
|
|
2783
3192
|
const contractIds = contractIndex.ids;
|
|
2784
3193
|
for (const file of specFiles) {
|
|
2785
|
-
const text = await (0,
|
|
3194
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2786
3195
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2787
3196
|
const parsed = parseSpec(text, file);
|
|
2788
3197
|
if (parsed.specId) {
|
|
@@ -2855,7 +3264,7 @@ async function validateTraceability(root, config) {
|
|
|
2855
3264
|
}
|
|
2856
3265
|
}
|
|
2857
3266
|
for (const file of scenarioFiles) {
|
|
2858
|
-
const text = await (0,
|
|
3267
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
2859
3268
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
2860
3269
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
2861
3270
|
allowCommentPrefix: true
|
|
@@ -3177,7 +3586,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
3177
3586
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
3178
3587
|
let found = false;
|
|
3179
3588
|
for (const file of targetFiles) {
|
|
3180
|
-
const text = await (0,
|
|
3589
|
+
const text = await (0, import_promises15.readFile)(file, "utf-8");
|
|
3181
3590
|
if (pattern.test(text)) {
|
|
3182
3591
|
found = true;
|
|
3183
3592
|
break;
|
|
@@ -3276,16 +3685,17 @@ var ID_PREFIXES2 = [
|
|
|
3276
3685
|
"DB",
|
|
3277
3686
|
"THEMA"
|
|
3278
3687
|
];
|
|
3688
|
+
var REPORT_GUARDRAILS_MAX = 20;
|
|
3279
3689
|
async function createReportData(root, validation, configResult) {
|
|
3280
|
-
const resolvedRoot =
|
|
3690
|
+
const resolvedRoot = import_node_path17.default.resolve(root);
|
|
3281
3691
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3282
3692
|
const config = resolved.config;
|
|
3283
3693
|
const configPath = resolved.configPath;
|
|
3284
3694
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3285
3695
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3286
|
-
const apiRoot =
|
|
3287
|
-
const uiRoot =
|
|
3288
|
-
const dbRoot =
|
|
3696
|
+
const apiRoot = import_node_path17.default.join(contractsRoot, "api");
|
|
3697
|
+
const uiRoot = import_node_path17.default.join(contractsRoot, "ui");
|
|
3698
|
+
const dbRoot = import_node_path17.default.join(contractsRoot, "db");
|
|
3289
3699
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3290
3700
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3291
3701
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3344,6 +3754,27 @@ async function createReportData(root, validation, configResult) {
|
|
|
3344
3754
|
const scSourceRecord = mapToSortedRecord(
|
|
3345
3755
|
normalizeScSources(resolvedRoot, scSources)
|
|
3346
3756
|
);
|
|
3757
|
+
const guardrailsLoad = await loadDecisionGuardrails(resolvedRoot, {
|
|
3758
|
+
specsRoot
|
|
3759
|
+
});
|
|
3760
|
+
const guardrailsAll = sortDecisionGuardrails(
|
|
3761
|
+
normalizeDecisionGuardrails(guardrailsLoad.entries)
|
|
3762
|
+
);
|
|
3763
|
+
const guardrailsDisplay = guardrailsAll.slice(0, REPORT_GUARDRAILS_MAX);
|
|
3764
|
+
const guardrailsByType = { nonGoal: 0, notNow: 0, tradeOff: 0 };
|
|
3765
|
+
for (const item of guardrailsAll) {
|
|
3766
|
+
if (item.type === "non-goal") {
|
|
3767
|
+
guardrailsByType.nonGoal += 1;
|
|
3768
|
+
} else if (item.type === "not-now") {
|
|
3769
|
+
guardrailsByType.notNow += 1;
|
|
3770
|
+
} else if (item.type === "trade-off") {
|
|
3771
|
+
guardrailsByType.tradeOff += 1;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
const guardrailsErrors = guardrailsLoad.errors.map((item) => ({
|
|
3775
|
+
path: toRelativePath(resolvedRoot, item.path),
|
|
3776
|
+
message: item.message
|
|
3777
|
+
}));
|
|
3347
3778
|
const version = await resolveToolVersion();
|
|
3348
3779
|
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
3349
3780
|
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
@@ -3391,6 +3822,34 @@ async function createReportData(root, validation, configResult) {
|
|
|
3391
3822
|
specToContracts: specToContractsRecord
|
|
3392
3823
|
}
|
|
3393
3824
|
},
|
|
3825
|
+
guardrails: {
|
|
3826
|
+
total: guardrailsAll.length,
|
|
3827
|
+
max: REPORT_GUARDRAILS_MAX,
|
|
3828
|
+
truncated: guardrailsAll.length > guardrailsDisplay.length,
|
|
3829
|
+
byType: guardrailsByType,
|
|
3830
|
+
items: guardrailsDisplay.map((item) => {
|
|
3831
|
+
const entry = {
|
|
3832
|
+
id: item.id,
|
|
3833
|
+
type: item.type,
|
|
3834
|
+
guardrail: item.guardrail,
|
|
3835
|
+
source: {
|
|
3836
|
+
file: toRelativePath(resolvedRoot, item.source.file),
|
|
3837
|
+
line: item.source.line
|
|
3838
|
+
}
|
|
3839
|
+
};
|
|
3840
|
+
if (item.rationale) {
|
|
3841
|
+
entry.rationale = item.rationale;
|
|
3842
|
+
}
|
|
3843
|
+
if (item.reconsider) {
|
|
3844
|
+
entry.reconsider = item.reconsider;
|
|
3845
|
+
}
|
|
3846
|
+
if (item.related) {
|
|
3847
|
+
entry.related = item.related;
|
|
3848
|
+
}
|
|
3849
|
+
return entry;
|
|
3850
|
+
}),
|
|
3851
|
+
scanErrors: guardrailsErrors
|
|
3852
|
+
},
|
|
3394
3853
|
issues: normalizedValidation.issues
|
|
3395
3854
|
};
|
|
3396
3855
|
}
|
|
@@ -3486,6 +3945,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3486
3945
|
lines.push("");
|
|
3487
3946
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
3488
3947
|
lines.push("- [Change Issues](#change-issues)");
|
|
3948
|
+
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
3489
3949
|
lines.push("- [IDs](#ids)");
|
|
3490
3950
|
lines.push("- [Traceability](#traceability)");
|
|
3491
3951
|
lines.push("");
|
|
@@ -3577,6 +4037,49 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
3577
4037
|
lines.push("### Issues");
|
|
3578
4038
|
lines.push("");
|
|
3579
4039
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
4040
|
+
lines.push("## Decision Guardrails");
|
|
4041
|
+
lines.push("");
|
|
4042
|
+
lines.push(`- total: ${data.guardrails.total}`);
|
|
4043
|
+
lines.push(
|
|
4044
|
+
`- types: non-goal ${data.guardrails.byType.nonGoal} / not-now ${data.guardrails.byType.notNow} / trade-off ${data.guardrails.byType.tradeOff}`
|
|
4045
|
+
);
|
|
4046
|
+
if (data.guardrails.truncated) {
|
|
4047
|
+
lines.push(`- truncated: true (max=${data.guardrails.max})`);
|
|
4048
|
+
}
|
|
4049
|
+
if (data.guardrails.scanErrors.length > 0) {
|
|
4050
|
+
lines.push(`- scanErrors: ${data.guardrails.scanErrors.length}`);
|
|
4051
|
+
}
|
|
4052
|
+
lines.push("");
|
|
4053
|
+
if (data.guardrails.items.length === 0) {
|
|
4054
|
+
lines.push("- (none)");
|
|
4055
|
+
} else {
|
|
4056
|
+
for (const item of data.guardrails.items) {
|
|
4057
|
+
lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
|
|
4058
|
+
lines.push(
|
|
4059
|
+
` - source: ${formatPathWithLine(item.source.file, { line: item.source.line }, baseUrl)}`
|
|
4060
|
+
);
|
|
4061
|
+
if (item.rationale) {
|
|
4062
|
+
lines.push(` - Rationale: ${item.rationale}`);
|
|
4063
|
+
}
|
|
4064
|
+
if (item.reconsider) {
|
|
4065
|
+
lines.push(` - Reconsider: ${item.reconsider}`);
|
|
4066
|
+
}
|
|
4067
|
+
if (item.related) {
|
|
4068
|
+
lines.push(` - Related: ${item.related}`);
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
if (data.guardrails.scanErrors.length > 0) {
|
|
4073
|
+
lines.push("");
|
|
4074
|
+
lines.push("### Scan errors");
|
|
4075
|
+
lines.push("");
|
|
4076
|
+
for (const errorItem of data.guardrails.scanErrors) {
|
|
4077
|
+
lines.push(
|
|
4078
|
+
`- ${formatPathLink(errorItem.path, baseUrl)}: ${errorItem.message}`
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
lines.push("");
|
|
3580
4083
|
lines.push("## IDs");
|
|
3581
4084
|
lines.push("");
|
|
3582
4085
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
@@ -3767,7 +4270,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
3767
4270
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
3768
4271
|
}
|
|
3769
4272
|
for (const file of specFiles) {
|
|
3770
|
-
const text = await (0,
|
|
4273
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
3771
4274
|
const parsed = parseSpec(text, file);
|
|
3772
4275
|
const specKey = parsed.specId;
|
|
3773
4276
|
if (!specKey) {
|
|
@@ -3809,7 +4312,7 @@ async function collectIds(files) {
|
|
|
3809
4312
|
THEMA: /* @__PURE__ */ new Set()
|
|
3810
4313
|
};
|
|
3811
4314
|
for (const file of files) {
|
|
3812
|
-
const text = await (0,
|
|
4315
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
3813
4316
|
for (const prefix of ID_PREFIXES2) {
|
|
3814
4317
|
const ids = extractIds(text, prefix);
|
|
3815
4318
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -3828,7 +4331,7 @@ async function collectIds(files) {
|
|
|
3828
4331
|
async function collectUpstreamIds(files) {
|
|
3829
4332
|
const ids = /* @__PURE__ */ new Set();
|
|
3830
4333
|
for (const file of files) {
|
|
3831
|
-
const text = await (0,
|
|
4334
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
3832
4335
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
3833
4336
|
}
|
|
3834
4337
|
return ids;
|
|
@@ -3849,7 +4352,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
3849
4352
|
}
|
|
3850
4353
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
3851
4354
|
for (const file of targetFiles) {
|
|
3852
|
-
const text = await (0,
|
|
4355
|
+
const text = await (0, import_promises16.readFile)(file, "utf-8");
|
|
3853
4356
|
if (pattern.test(text)) {
|
|
3854
4357
|
return true;
|
|
3855
4358
|
}
|
|
@@ -3975,19 +4478,26 @@ function buildHotspots(issues) {
|
|
|
3975
4478
|
}
|
|
3976
4479
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3977
4480
|
0 && (module.exports = {
|
|
4481
|
+
checkDecisionGuardrails,
|
|
3978
4482
|
createReportData,
|
|
3979
4483
|
defaultConfig,
|
|
3980
4484
|
extractAllIds,
|
|
4485
|
+
extractDecisionGuardrailsFromMarkdown,
|
|
3981
4486
|
extractIds,
|
|
3982
4487
|
extractInvalidIds,
|
|
4488
|
+
filterDecisionGuardrailsByKeyword,
|
|
3983
4489
|
findConfigRoot,
|
|
4490
|
+
formatGuardrailsForLlm,
|
|
3984
4491
|
formatReportJson,
|
|
3985
4492
|
formatReportMarkdown,
|
|
3986
4493
|
getConfigPath,
|
|
3987
4494
|
lintSql,
|
|
3988
4495
|
loadConfig,
|
|
4496
|
+
loadDecisionGuardrails,
|
|
4497
|
+
normalizeDecisionGuardrails,
|
|
3989
4498
|
resolvePath,
|
|
3990
4499
|
resolveToolVersion,
|
|
4500
|
+
sortDecisionGuardrails,
|
|
3991
4501
|
validateContracts,
|
|
3992
4502
|
validateDefinedIds,
|
|
3993
4503
|
validateDeltas,
|