qfai 1.0.7 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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/ids.ts
451
- var ID_PREFIXES = [
452
- "SPEC",
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 import_promises3 = require("fs/promises");
628
- var import_node_path3 = __toESM(require("path"), 1);
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: import_node_path3.default.join(dir, "spec.md"),
635
- deltaPath: import_node_path3.default.join(dir, "delta.md"),
636
- scenarioPath: import_node_path3.default.join(dir, "scenario.feature")
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, import_promises3.readdir)(specsRoot, { withFileTypes: true });
643
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => import_node_path3.default.join(specsRoot, 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, import_promises4.access)(target);
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) => import_node_path4.default.basename(file).toLowerCase().startsWith(lowerPrefix)
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 = import_node_path5.default.join(contractsRoot, "ui");
739
- const apiRoot = import_node_path5.default.join(contractsRoot, "api");
740
- const dbRoot = import_node_path5.default.join(contractsRoot, "db");
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, import_promises5.readFile)(file, "utf-8");
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 import_node_path6 = __toESM(require("path"), 1);
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 (!import_node_path6.default.isAbsolute(target)) {
1230
+ if (!import_node_path7.default.isAbsolute(target)) {
778
1231
  return toPosixPath(target);
779
1232
  }
780
- const relative = import_node_path6.default.relative(root, target);
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 brSection = sections.get(BR_SECTION_TITLE);
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 < brLines.length; i++) {
939
- const lineText = brLines[i] ?? "";
940
- const lineNumber = startLine + i;
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 import_promises6 = require("fs/promises");
999
- var import_node_path7 = __toESM(require("path"), 1);
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, import_promises6.readFile)(file, "utf-8");
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, import_promises6.readFile)(file, "utf-8");
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) => import_node_path7.default.normalize(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, import_promises6.readFile)(file, "utf-8");
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 import_promises7 = require("fs/promises");
1290
- var import_node_path8 = __toESM(require("path"), 1);
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.7".length > 0) {
1294
- return "1.0.7";
1702
+ if ("1.1.1".length > 0) {
1703
+ return "1.1.1";
1295
1704
  }
1296
1705
  try {
1297
1706
  const packagePath = resolvePackageJsonPath();
1298
- const raw = await (0, import_promises7.readFile)(packagePath, "utf-8");
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 import_node_path8.default.resolve(import_node_path8.default.dirname(basePath), "../../package.json");
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 import_promises8 = require("fs/promises");
1314
- var import_node_path10 = __toESM(require("path"), 1);
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 import_node_path9 = __toESM(require("path"), 1);
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 = import_node_path9.default.extname(file).toLowerCase();
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 = import_node_path10.default.join(contractsRoot, "ui");
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(import_node_path10.default.join(contractsRoot, "api")));
1349
- issues.push(...await validateDbContracts(import_node_path10.default.join(contractsRoot, "db")));
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, import_promises8.readFile)(file, "utf-8");
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, import_promises8.readFile)(file, "utf-8");
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, import_promises8.readFile)(file, "utf-8");
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, import_promises8.readFile)(file, "utf-8");
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 = import_node_path10.default.resolve(uiRoot, packValue);
1823
- const packRelative = import_node_path10.default.relative(uiRoot, packDir);
1824
- if (packRelative.startsWith("..") || import_node_path10.default.isAbsolute(packRelative)) {
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 = import_node_path10.default.join(packDir, "assets.yaml");
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, import_promises8.readFile)(assetsYamlPath, "utf-8");
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 = import_node_path10.default.resolve(packDir, entry.path);
1939
- const assetRelative = import_node_path10.default.relative(packDir, assetPath);
1940
- if (assetRelative.startsWith("..") || import_node_path10.default.isAbsolute(assetRelative)) {
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 = import_node_path10.default.posix.basename(normalized);
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 (import_node_path10.default.isAbsolute(value)) {
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, import_promises8.access)(target);
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 import_promises9 = require("fs/promises");
2042
- var import_node_path11 = __toESM(require("path"), 1);
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 = import_node_path11.default.join(pack, "delta.md");
2460
+ const deltaPath = import_node_path12.default.join(pack, "delta.md");
2052
2461
  try {
2053
- await (0, import_promises9.readFile)(deltaPath, "utf-8");
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 import_promises10 = require("fs/promises");
2105
- var import_node_path12 = __toESM(require("path"), 1);
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, import_promises10.readFile)(file, "utf-8");
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, import_promises10.readFile)(file, "utf-8");
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 = import_node_path12.default.relative(root, file);
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 import_promises11 = require("fs/promises");
2199
- var import_node_path14 = __toESM(require("path"), 1);
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 import_node_path13 = __toESM(require("path"), 1);
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 = import_node_path13.default.dirname(basePath);
2617
+ const baseDir = import_node_path14.default.dirname(basePath);
2209
2618
  const candidates = [
2210
- import_node_path13.default.resolve(baseDir, "../../../assets/init"),
2211
- import_node_path13.default.resolve(baseDir, "../../assets/init")
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 = import_node_path14.default.isAbsolute(promptsDirConfig) ? promptsDirConfig : import_node_path14.default.resolve(root, promptsDirConfig);
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 = import_node_path14.default.isAbsolute(promptsDirConfig) ? import_node_path14.default.relative(root, promptsDirConfig) : promptsDirConfig;
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 = import_node_path14.default.join(getInitAssetsDir(), normalized);
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, import_promises11.readFile)(templateAbs, "utf-8"),
2301
- (0, import_promises11.readFile)(projectAbs, "utf-8")
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 = import_node_path14.default.relative(base, abs);
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 import_promises12 = require("fs/promises");
2370
- var import_node_path15 = __toESM(require("path"), 1);
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 = import_node_path15.default.join(entry.dir, "scenario.md");
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, import_promises12.readFile)(entry.scenarioPath, "utf-8");
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, import_promises12.access)(target);
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 import_promises13 = require("fs/promises");
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, import_promises13.readFile)(entry.specPath, "utf-8");
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 import_promises14 = require("fs/promises");
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, import_promises14.readFile)(file, "utf-8");
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, import_promises14.readFile)(file, "utf-8");
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, import_promises14.readFile)(file, "utf-8");
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 = import_node_path16.default.resolve(root);
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 = import_node_path16.default.join(contractsRoot, "api");
3287
- const uiRoot = import_node_path16.default.join(contractsRoot, "ui");
3288
- const dbRoot = import_node_path16.default.join(contractsRoot, "db");
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, import_promises15.readFile)(file, "utf-8");
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, import_promises15.readFile)(file, "utf-8");
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, import_promises15.readFile)(file, "utf-8");
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, import_promises15.readFile)(file, "utf-8");
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,