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.
Files changed (51) hide show
  1. package/README.md +327 -245
  2. package/assets/init/.qfai/README.md +2 -1
  3. package/assets/init/.qfai/assistant/README.md +1 -1
  4. package/assets/init/.qfai/assistant/prompts/README.md +1 -1
  5. package/assets/init/.qfai/assistant/prompts/qfai-configure.md +197 -0
  6. package/assets/init/.qfai/assistant/prompts/qfai-verify.md +1 -1
  7. package/assets/init/.qfai/assistant/steering/README.md +6 -0
  8. package/assets/init/.qfai/assistant/steering/manifest.md +43 -0
  9. package/assets/init/.qfai/contracts/db/README.md +10 -3
  10. package/assets/init/.qfai/samples/guardrails/delta_with_guardrails.md +19 -0
  11. package/assets/init/.qfai/specs/README.md +4 -0
  12. package/assets/init/root/.claude/commands/qfai-configure.md +14 -0
  13. package/assets/init/root/.claude/commands/qfai-discuss.md +14 -0
  14. package/assets/init/root/.claude/commands/qfai-implement.md +14 -0
  15. package/assets/init/root/.claude/commands/qfai-require.md +14 -0
  16. package/assets/init/root/.claude/commands/qfai-scenario-test.md +14 -0
  17. package/assets/init/root/.claude/commands/qfai-spec.md +14 -0
  18. package/assets/init/root/.claude/commands/qfai-unit-test.md +14 -0
  19. package/assets/init/root/.claude/commands/qfai-verify.md +14 -0
  20. package/assets/init/root/.codex/README.md +16 -0
  21. package/assets/init/root/.codex/skills/qfai-configure/SKILL.md +18 -0
  22. package/assets/init/root/.codex/skills/qfai-discuss/SKILL.md +18 -0
  23. package/assets/init/root/.codex/skills/qfai-implement/SKILL.md +18 -0
  24. package/assets/init/root/.codex/skills/qfai-require/SKILL.md +18 -0
  25. package/assets/init/root/.codex/skills/qfai-scenario-test/SKILL.md +18 -0
  26. package/assets/init/root/.codex/skills/qfai-spec/SKILL.md +18 -0
  27. package/assets/init/root/.codex/skills/qfai-unit-test/SKILL.md +18 -0
  28. package/assets/init/root/.codex/skills/qfai-verify/SKILL.md +18 -0
  29. package/assets/init/root/.github/copilot-instructions.md +14 -0
  30. package/assets/init/root/.github/prompts/qfai-configure.prompt.md +17 -0
  31. package/assets/init/root/.github/prompts/qfai-discuss.prompt.md +17 -0
  32. package/assets/init/root/.github/prompts/qfai-implement.prompt.md +17 -0
  33. package/assets/init/root/.github/prompts/qfai-require.prompt.md +17 -0
  34. package/assets/init/root/.github/prompts/qfai-scenario-test.prompt.md +17 -0
  35. package/assets/init/root/.github/prompts/qfai-spec.prompt.md +17 -0
  36. package/assets/init/root/.github/prompts/qfai-unit-test.prompt.md +17 -0
  37. package/assets/init/root/.github/prompts/qfai-verify.prompt.md +17 -0
  38. package/assets/init/root/.github/workflows/qfai.yml +0 -2
  39. package/assets/init/root/qfai.config.yaml +1 -8
  40. package/dist/cli/index.cjs +880 -196
  41. package/dist/cli/index.cjs.map +1 -1
  42. package/dist/cli/index.mjs +866 -182
  43. package/dist/cli/index.mjs.map +1 -1
  44. package/dist/index.cjs +731 -221
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +91 -1
  47. package/dist/index.d.ts +91 -1
  48. package/dist/index.mjs +719 -216
  49. package/dist/index.mjs.map +1 -1
  50. package/package.json +1 -1
  51. package/assets/init/.qfai/assistant/prompts/qfai-pr.md +0 -209
package/dist/index.mjs CHANGED
@@ -14,15 +14,7 @@ var defaultConfig = {
14
14
  validation: {
15
15
  failOn: "error",
16
16
  require: {
17
- specSections: [
18
- "\u80CC\u666F",
19
- "\u30B9\u30B3\u30FC\u30D7",
20
- "\u975E\u30B4\u30FC\u30EB",
21
- "\u7528\u8A9E",
22
- "\u524D\u63D0",
23
- "\u6C7A\u5B9A\u4E8B\u9805",
24
- "\u696D\u52D9\u30EB\u30FC\u30EB"
25
- ]
17
+ specSections: []
26
18
  },
27
19
  traceability: {
28
20
  brMustHaveSc: true,
@@ -390,80 +382,9 @@ function isRecord(value) {
390
382
  return value !== null && typeof value === "object" && !Array.isArray(value);
391
383
  }
392
384
 
393
- // src/core/ids.ts
394
- var ID_PREFIXES = [
395
- "SPEC",
396
- "BR",
397
- "SC",
398
- "UI",
399
- "API",
400
- "DB",
401
- "THEMA"
402
- ];
403
- var STRICT_ID_PATTERNS = {
404
- SPEC: /\bSPEC-\d{4}\b/g,
405
- BR: /\bBR-\d{4}\b/g,
406
- SC: /\bSC-\d{4}\b/g,
407
- UI: /\bUI-\d{4}\b/g,
408
- API: /\bAPI-\d{4}\b/g,
409
- DB: /\bDB-\d{4}\b/g,
410
- THEMA: /\bTHEMA-\d{3}\b/g,
411
- ADR: /\bADR-\d{4}\b/g
412
- };
413
- var LOOSE_ID_PATTERNS = {
414
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
415
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
416
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
417
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
418
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
419
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
420
- THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
421
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
422
- };
423
- function extractIds(text, prefix) {
424
- const pattern = STRICT_ID_PATTERNS[prefix];
425
- const matches = text.match(pattern);
426
- return unique(matches ?? []);
427
- }
428
- function extractAllIds(text) {
429
- const all = [];
430
- ID_PREFIXES.forEach((prefix) => {
431
- all.push(...extractIds(text, prefix));
432
- });
433
- return unique(all);
434
- }
435
- function extractInvalidIds(text, prefixes) {
436
- const invalid = [];
437
- for (const prefix of prefixes) {
438
- const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
439
- for (const candidate of candidates) {
440
- if (!isValidId(candidate, prefix)) {
441
- invalid.push(candidate);
442
- }
443
- }
444
- }
445
- return unique(invalid);
446
- }
447
- function unique(values) {
448
- return Array.from(new Set(values));
449
- }
450
- function isValidId(value, prefix) {
451
- const pattern = STRICT_ID_PATTERNS[prefix];
452
- const strict = new RegExp(pattern.source);
453
- return strict.test(value);
454
- }
455
-
456
- // src/core/report.ts
457
- import { readFile as readFile12 } from "fs/promises";
458
- import path16 from "path";
459
-
460
- // src/core/contractIndex.ts
461
- import { readFile as readFile2 } from "fs/promises";
462
- import path5 from "path";
463
-
464
- // src/core/discovery.ts
465
- import { access as access3 } from "fs/promises";
466
- import path4 from "path";
385
+ // src/core/decisionGuardrails.ts
386
+ import { readFile as readFile2, stat } from "fs/promises";
387
+ import path3 from "path";
467
388
 
468
389
  // src/core/fs.ts
469
390
  import { access as access2, readdir } from "fs/promises";
@@ -566,24 +487,549 @@ function destroyStream(stream) {
566
487
  }
567
488
  }
568
489
 
490
+ // src/core/parse/markdown.ts
491
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
492
+ function parseHeadings(md) {
493
+ const lines = md.split(/\r?\n/);
494
+ const headings = [];
495
+ for (let i = 0; i < lines.length; i++) {
496
+ const line = lines[i] ?? "";
497
+ const match = line.match(HEADING_RE);
498
+ if (!match) continue;
499
+ const levelToken = match[1];
500
+ const title = match[2];
501
+ if (!levelToken || !title) continue;
502
+ headings.push({
503
+ level: levelToken.length,
504
+ title: title.trim(),
505
+ line: i + 1
506
+ });
507
+ }
508
+ return headings;
509
+ }
510
+ function extractH2Sections(md) {
511
+ const lines = md.split(/\r?\n/);
512
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
513
+ const sections = /* @__PURE__ */ new Map();
514
+ for (let i = 0; i < headings.length; i++) {
515
+ const current = headings[i];
516
+ if (!current) continue;
517
+ const next = headings[i + 1];
518
+ const startLine = current.line + 1;
519
+ const endLine = (next?.line ?? lines.length + 1) - 1;
520
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
521
+ sections.set(current.title.trim(), {
522
+ title: current.title.trim(),
523
+ startLine,
524
+ endLine,
525
+ body
526
+ });
527
+ }
528
+ return sections;
529
+ }
530
+
531
+ // src/core/decisionGuardrails.ts
532
+ var DEFAULT_DECISION_GUARDRAILS_GLOBS = [".qfai/specs/**/delta.md"];
533
+ var DEFAULT_GUARDRAILS_IGNORE_GLOBS = [
534
+ "**/node_modules/**",
535
+ "**/.git/**",
536
+ "**/dist/**",
537
+ "**/build/**",
538
+ "**/.pnpm/**",
539
+ "**/tmp/**",
540
+ "**/.mcp-tools/**"
541
+ ];
542
+ var SECTION_TITLE = "decision guardrails";
543
+ var ENTRY_START_RE = /^\s*[-*]\s+ID:\s*(.+?)\s*$/i;
544
+ var FIELD_RE = /^\s{2,}([A-Za-z][A-Za-z0-9 _-]*):\s*(.*)$/;
545
+ var CONTINUATION_RE = /^\s{4,}(.+)$/;
546
+ var ID_FORMAT_RE = /^DG-\d{4}$/;
547
+ var TYPE_ORDER = {
548
+ "non-goal": 0,
549
+ "not-now": 1,
550
+ "trade-off": 2
551
+ };
552
+ async function loadDecisionGuardrails(root, options = {}) {
553
+ const errors = [];
554
+ const files = await scanDecisionGuardrailFiles(
555
+ root,
556
+ options.paths,
557
+ errors,
558
+ options.specsRoot
559
+ );
560
+ const entries = [];
561
+ for (const filePath of files) {
562
+ try {
563
+ const content = await readFile2(filePath, "utf-8");
564
+ const parsed = extractDecisionGuardrailsFromMarkdown(content, filePath);
565
+ entries.push(...parsed);
566
+ } catch (error) {
567
+ errors.push({ path: filePath, message: String(error) });
568
+ }
569
+ }
570
+ return { entries, errors, files };
571
+ }
572
+ function extractDecisionGuardrailsFromMarkdown(markdown, filePath) {
573
+ const sections = extractH2Sections(markdown);
574
+ const section = findDecisionGuardrailsSection(sections);
575
+ if (!section) {
576
+ return [];
577
+ }
578
+ const lines = section.body.split(/\r?\n/);
579
+ const entries = [];
580
+ let current = null;
581
+ const flush = () => {
582
+ if (!current) {
583
+ return;
584
+ }
585
+ const entry = {
586
+ keywords: current.keywords,
587
+ source: { file: filePath, line: current.startLine },
588
+ ...current.fields.id ? { id: current.fields.id } : {},
589
+ ...current.fields.type ? { type: current.fields.type } : {},
590
+ ...current.fields.guardrail ? { guardrail: current.fields.guardrail } : {},
591
+ ...current.fields.rationale ? { rationale: current.fields.rationale } : {},
592
+ ...current.fields.reconsider ? { reconsider: current.fields.reconsider } : {},
593
+ ...current.fields.related ? { related: current.fields.related } : {},
594
+ ...current.fields.title ? { title: current.fields.title } : {}
595
+ };
596
+ entries.push(entry);
597
+ current = null;
598
+ };
599
+ for (let i = 0; i < lines.length; i += 1) {
600
+ const rawLine = lines[i] ?? "";
601
+ const lineNumber = section.startLine + i;
602
+ const entryMatch = rawLine.match(ENTRY_START_RE);
603
+ if (entryMatch) {
604
+ flush();
605
+ const id = entryMatch[1]?.trim() ?? "";
606
+ current = {
607
+ startLine: lineNumber,
608
+ fields: { id },
609
+ keywords: []
610
+ };
611
+ continue;
612
+ }
613
+ if (!current) {
614
+ continue;
615
+ }
616
+ const fieldMatch = rawLine.match(FIELD_RE);
617
+ if (fieldMatch) {
618
+ const rawKey = fieldMatch[1] ?? "";
619
+ const value = fieldMatch[2] ?? "";
620
+ const key = normalizeFieldKey(rawKey);
621
+ if (key) {
622
+ if (key === "keywords") {
623
+ current.keywords.push(
624
+ ...value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
625
+ );
626
+ } else {
627
+ const trimmed = value.trim();
628
+ if (trimmed.length > 0) {
629
+ const existing = current.fields[key];
630
+ current.fields[key] = existing ? `${existing}
631
+ ${trimmed}` : trimmed;
632
+ }
633
+ }
634
+ current.lastKey = key;
635
+ } else {
636
+ delete current.lastKey;
637
+ }
638
+ continue;
639
+ }
640
+ const continuationMatch = rawLine.match(CONTINUATION_RE);
641
+ if (continuationMatch && current.lastKey) {
642
+ const value = continuationMatch[1]?.trim() ?? "";
643
+ if (value.length > 0) {
644
+ const existing = current.fields[current.lastKey];
645
+ current.fields[current.lastKey] = existing ? `${existing}
646
+ ${value}` : value;
647
+ }
648
+ }
649
+ }
650
+ flush();
651
+ return entries;
652
+ }
653
+ function normalizeDecisionGuardrails(entries) {
654
+ const items = [];
655
+ for (const entry of entries) {
656
+ const id = entry.id?.trim();
657
+ const type = normalizeGuardrailType(entry.type);
658
+ const guardrail = entry.guardrail?.trim();
659
+ if (!id || !type || !guardrail) {
660
+ continue;
661
+ }
662
+ const item = {
663
+ id,
664
+ type,
665
+ guardrail,
666
+ keywords: entry.keywords?.filter((word) => word.length > 0) ?? [],
667
+ source: entry.source
668
+ };
669
+ const rationale = entry.rationale?.trim();
670
+ if (rationale) {
671
+ item.rationale = rationale;
672
+ }
673
+ const reconsider = entry.reconsider?.trim();
674
+ if (reconsider) {
675
+ item.reconsider = reconsider;
676
+ }
677
+ const related = entry.related?.trim();
678
+ if (related) {
679
+ item.related = related;
680
+ }
681
+ const title = entry.title?.trim();
682
+ if (title) {
683
+ item.title = title;
684
+ }
685
+ items.push(item);
686
+ }
687
+ return items;
688
+ }
689
+ function sortDecisionGuardrails(items) {
690
+ return [...items].sort((a, b) => {
691
+ const typeOrder = (TYPE_ORDER[a.type] ?? 999) - (TYPE_ORDER[b.type] ?? 999);
692
+ if (typeOrder !== 0) {
693
+ return typeOrder;
694
+ }
695
+ return a.id.localeCompare(b.id);
696
+ });
697
+ }
698
+ function filterDecisionGuardrailsByKeyword(items, keyword) {
699
+ const needle = keyword?.trim().toLowerCase();
700
+ if (!needle) {
701
+ return items;
702
+ }
703
+ return items.filter((item) => {
704
+ const haystack = [
705
+ item.title,
706
+ item.guardrail,
707
+ item.rationale,
708
+ item.related,
709
+ item.keywords.join(" ")
710
+ ].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
711
+ return haystack.some((value) => value.includes(needle));
712
+ });
713
+ }
714
+ function formatGuardrailsForLlm(items, max) {
715
+ const limit = Math.max(0, Math.floor(max));
716
+ const lines = ["# Decision Guardrails (extract)", ""];
717
+ const slice = limit > 0 ? items.slice(0, limit) : [];
718
+ if (slice.length === 0) {
719
+ lines.push("- (none)");
720
+ return lines.join("\n");
721
+ }
722
+ for (const item of slice) {
723
+ lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
724
+ if (item.rationale) {
725
+ lines.push(` Rationale: ${item.rationale}`);
726
+ }
727
+ if (item.reconsider) {
728
+ lines.push(` Reconsider: ${item.reconsider}`);
729
+ }
730
+ if (item.related) {
731
+ lines.push(` Related: ${item.related}`);
732
+ }
733
+ }
734
+ return lines.join("\n");
735
+ }
736
+ function checkDecisionGuardrails(entries) {
737
+ const errors = [];
738
+ const warnings = [];
739
+ const idMap = /* @__PURE__ */ new Map();
740
+ for (const entry of entries) {
741
+ const file = entry.source.file;
742
+ const line = entry.source.line;
743
+ const id = entry.id?.trim();
744
+ const typeRaw = entry.type?.trim();
745
+ const guardrail = entry.guardrail?.trim();
746
+ const rationale = entry.rationale?.trim();
747
+ const reconsider = entry.reconsider?.trim();
748
+ if (!id) {
749
+ errors.push({
750
+ severity: "error",
751
+ code: "QFAI-GR-001",
752
+ message: "ID is missing",
753
+ file,
754
+ line
755
+ });
756
+ } else {
757
+ const list = idMap.get(id) ?? [];
758
+ list.push(entry);
759
+ idMap.set(id, list);
760
+ if (!ID_FORMAT_RE.test(id)) {
761
+ warnings.push({
762
+ severity: "warning",
763
+ code: "QFAI-GR-002",
764
+ message: `ID format is not standard: ${id}`,
765
+ file,
766
+ line,
767
+ id
768
+ });
769
+ }
770
+ }
771
+ if (!typeRaw) {
772
+ errors.push({
773
+ severity: "error",
774
+ code: "QFAI-GR-003",
775
+ message: "Type is missing",
776
+ file,
777
+ line,
778
+ ...id ? { id } : {}
779
+ });
780
+ } else if (!normalizeGuardrailType(typeRaw)) {
781
+ errors.push({
782
+ severity: "error",
783
+ code: "QFAI-GR-004",
784
+ message: `Type is invalid: ${typeRaw}`,
785
+ file,
786
+ line,
787
+ ...id ? { id } : {}
788
+ });
789
+ }
790
+ if (!guardrail) {
791
+ errors.push({
792
+ severity: "error",
793
+ code: "QFAI-GR-005",
794
+ message: "Guardrail is missing",
795
+ file,
796
+ line,
797
+ ...id ? { id } : {}
798
+ });
799
+ }
800
+ if (!rationale) {
801
+ warnings.push({
802
+ severity: "warning",
803
+ code: "QFAI-GR-006",
804
+ message: "Rationale is missing",
805
+ file,
806
+ line,
807
+ ...id ? { id } : {}
808
+ });
809
+ }
810
+ if (!reconsider) {
811
+ warnings.push({
812
+ severity: "warning",
813
+ code: "QFAI-GR-007",
814
+ message: "Reconsider is missing",
815
+ file,
816
+ line,
817
+ ...id ? { id } : {}
818
+ });
819
+ }
820
+ }
821
+ for (const [id, list] of idMap.entries()) {
822
+ if (list.length > 1) {
823
+ const locations = list.map((entry) => `${entry.source.file}:${entry.source.line}`).join(", ");
824
+ const first = list[0];
825
+ const file = first?.source.file ?? "";
826
+ const line = first?.source.line;
827
+ errors.push({
828
+ severity: "error",
829
+ code: "QFAI-GR-008",
830
+ message: `ID is duplicated: ${id} (${locations})`,
831
+ file,
832
+ ...line !== void 0 ? { line } : {},
833
+ id
834
+ });
835
+ }
836
+ }
837
+ return { errors, warnings };
838
+ }
839
+ function normalizeGuardrailType(raw) {
840
+ if (!raw) {
841
+ return null;
842
+ }
843
+ const normalized = raw.trim().toLowerCase().replace(/[_\s]+/g, "-");
844
+ if (normalized === "non-goal") {
845
+ return "non-goal";
846
+ }
847
+ if (normalized === "not-now") {
848
+ return "not-now";
849
+ }
850
+ if (normalized === "trade-off") {
851
+ return "trade-off";
852
+ }
853
+ return null;
854
+ }
855
+ function normalizeFieldKey(raw) {
856
+ const normalized = raw.trim().toLowerCase().replace(/[_\s-]+/g, "");
857
+ switch (normalized) {
858
+ case "id":
859
+ return "id";
860
+ case "type":
861
+ return "type";
862
+ case "guardrail":
863
+ return "guardrail";
864
+ case "rationale":
865
+ case "reason":
866
+ return "rationale";
867
+ case "reconsider":
868
+ return "reconsider";
869
+ case "related":
870
+ return "related";
871
+ case "keywords":
872
+ case "keyword":
873
+ return "keywords";
874
+ case "title":
875
+ case "heading":
876
+ return "title";
877
+ default:
878
+ return null;
879
+ }
880
+ }
881
+ async function scanDecisionGuardrailFiles(root, rawPaths, errors, specsRoot) {
882
+ if (!rawPaths || rawPaths.length === 0) {
883
+ const scanRoot = specsRoot ? path3.isAbsolute(specsRoot) ? specsRoot : path3.resolve(root, specsRoot) : root;
884
+ const globs = specsRoot ? ["**/delta.md"] : DEFAULT_DECISION_GUARDRAILS_GLOBS;
885
+ try {
886
+ const result = await collectFilesByGlobs(scanRoot, {
887
+ globs,
888
+ ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
889
+ });
890
+ return result.files.sort((a, b) => a.localeCompare(b));
891
+ } catch (error) {
892
+ errors.push({ path: scanRoot, message: String(error) });
893
+ return [];
894
+ }
895
+ }
896
+ const files = /* @__PURE__ */ new Set();
897
+ for (const rawPath of rawPaths) {
898
+ const resolved = path3.isAbsolute(rawPath) ? rawPath : path3.resolve(root, rawPath);
899
+ const stats = await safeStat(resolved);
900
+ if (!stats) {
901
+ errors.push({ path: resolved, message: "Path does not exist" });
902
+ continue;
903
+ }
904
+ if (stats.isFile()) {
905
+ files.add(resolved);
906
+ continue;
907
+ }
908
+ if (stats.isDirectory()) {
909
+ try {
910
+ const result = await collectFilesByGlobs(resolved, {
911
+ globs: ["**/delta.md"],
912
+ ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
913
+ });
914
+ result.files.forEach((file) => files.add(file));
915
+ } catch (error) {
916
+ errors.push({ path: resolved, message: String(error) });
917
+ }
918
+ continue;
919
+ }
920
+ errors.push({ path: resolved, message: "Unsupported path type" });
921
+ }
922
+ return Array.from(files).sort((a, b) => a.localeCompare(b));
923
+ }
924
+ async function safeStat(target) {
925
+ try {
926
+ return await stat(target);
927
+ } catch {
928
+ return null;
929
+ }
930
+ }
931
+ function findDecisionGuardrailsSection(sections) {
932
+ for (const [title, section] of sections.entries()) {
933
+ if (title.trim().toLowerCase() === SECTION_TITLE) {
934
+ return section;
935
+ }
936
+ }
937
+ return null;
938
+ }
939
+
940
+ // src/core/ids.ts
941
+ var ID_PREFIXES = [
942
+ "SPEC",
943
+ "BR",
944
+ "SC",
945
+ "UI",
946
+ "API",
947
+ "DB",
948
+ "THEMA"
949
+ ];
950
+ var STRICT_ID_PATTERNS = {
951
+ SPEC: /\bSPEC-\d{4}\b/g,
952
+ BR: /\bBR-\d{4}\b/g,
953
+ SC: /\bSC-\d{4}\b/g,
954
+ UI: /\bUI-\d{4}\b/g,
955
+ API: /\bAPI-\d{4}\b/g,
956
+ DB: /\bDB-\d{4}\b/g,
957
+ THEMA: /\bTHEMA-\d{3}\b/g,
958
+ ADR: /\bADR-\d{4}\b/g
959
+ };
960
+ var LOOSE_ID_PATTERNS = {
961
+ SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
962
+ BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
963
+ SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
964
+ UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
965
+ API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
966
+ DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
967
+ THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
968
+ ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
969
+ };
970
+ function extractIds(text, prefix) {
971
+ const pattern = STRICT_ID_PATTERNS[prefix];
972
+ const matches = text.match(pattern);
973
+ return unique(matches ?? []);
974
+ }
975
+ function extractAllIds(text) {
976
+ const all = [];
977
+ ID_PREFIXES.forEach((prefix) => {
978
+ all.push(...extractIds(text, prefix));
979
+ });
980
+ return unique(all);
981
+ }
982
+ function extractInvalidIds(text, prefixes) {
983
+ const invalid = [];
984
+ for (const prefix of prefixes) {
985
+ const candidates = text.match(LOOSE_ID_PATTERNS[prefix]) ?? [];
986
+ for (const candidate of candidates) {
987
+ if (!isValidId(candidate, prefix)) {
988
+ invalid.push(candidate);
989
+ }
990
+ }
991
+ }
992
+ return unique(invalid);
993
+ }
994
+ function unique(values) {
995
+ return Array.from(new Set(values));
996
+ }
997
+ function isValidId(value, prefix) {
998
+ const pattern = STRICT_ID_PATTERNS[prefix];
999
+ const strict = new RegExp(pattern.source);
1000
+ return strict.test(value);
1001
+ }
1002
+
1003
+ // src/core/report.ts
1004
+ import { readFile as readFile13 } from "fs/promises";
1005
+ import path17 from "path";
1006
+
1007
+ // src/core/contractIndex.ts
1008
+ import { readFile as readFile3 } from "fs/promises";
1009
+ import path6 from "path";
1010
+
1011
+ // src/core/discovery.ts
1012
+ import { access as access3 } from "fs/promises";
1013
+ import path5 from "path";
1014
+
569
1015
  // src/core/specLayout.ts
570
1016
  import { readdir as readdir2 } from "fs/promises";
571
- import path3 from "path";
1017
+ import path4 from "path";
572
1018
  var SPEC_DIR_RE = /^spec-\d{4}$/;
573
1019
  async function collectSpecEntries(specsRoot) {
574
1020
  const dirs = await listSpecDirs(specsRoot);
575
1021
  const entries = dirs.map((dir) => ({
576
1022
  dir,
577
- specPath: path3.join(dir, "spec.md"),
578
- deltaPath: path3.join(dir, "delta.md"),
579
- scenarioPath: path3.join(dir, "scenario.feature")
1023
+ specPath: path4.join(dir, "spec.md"),
1024
+ deltaPath: path4.join(dir, "delta.md"),
1025
+ scenarioPath: path4.join(dir, "scenario.feature")
580
1026
  }));
581
1027
  return entries.sort((a, b) => a.dir.localeCompare(b.dir));
582
1028
  }
583
1029
  async function listSpecDirs(specsRoot) {
584
1030
  try {
585
1031
  const items = await readdir2(specsRoot, { withFileTypes: true });
586
- return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path3.join(specsRoot, name));
1032
+ return items.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => SPEC_DIR_RE.test(name.toLowerCase())).map((name) => path4.join(specsRoot, name));
587
1033
  } catch (error) {
588
1034
  if (isMissingFileError(error)) {
589
1035
  return [];
@@ -654,7 +1100,7 @@ async function exists3(target) {
654
1100
  function filterByBasenamePrefix(files, prefix) {
655
1101
  const lowerPrefix = prefix.toLowerCase();
656
1102
  return files.filter(
657
- (file) => path4.basename(file).toLowerCase().startsWith(lowerPrefix)
1103
+ (file) => path5.basename(file).toLowerCase().startsWith(lowerPrefix)
658
1104
  );
659
1105
  }
660
1106
 
@@ -678,9 +1124,9 @@ function stripContractDeclarationLines(text) {
678
1124
  // src/core/contractIndex.ts
679
1125
  async function buildContractIndex(root, config) {
680
1126
  const contractsRoot = resolvePath(root, config, "contractsDir");
681
- const uiRoot = path5.join(contractsRoot, "ui");
682
- const apiRoot = path5.join(contractsRoot, "api");
683
- const dbRoot = path5.join(contractsRoot, "db");
1127
+ const uiRoot = path6.join(contractsRoot, "ui");
1128
+ const apiRoot = path6.join(contractsRoot, "api");
1129
+ const dbRoot = path6.join(contractsRoot, "db");
684
1130
  const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
685
1131
  collectUiContractFiles(uiRoot),
686
1132
  collectThemaContractFiles(uiRoot),
@@ -700,7 +1146,7 @@ async function buildContractIndex(root, config) {
700
1146
  }
701
1147
  async function indexContractFiles(files, index) {
702
1148
  for (const file of files) {
703
- const text = await readFile2(file, "utf-8");
1149
+ const text = await readFile3(file, "utf-8");
704
1150
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
705
1151
  }
706
1152
  }
@@ -712,15 +1158,15 @@ function record(index, id, file) {
712
1158
  }
713
1159
 
714
1160
  // src/core/paths.ts
715
- import path6 from "path";
1161
+ import path7 from "path";
716
1162
  function toRelativePath(root, target) {
717
1163
  if (!target) {
718
1164
  return target;
719
1165
  }
720
- if (!path6.isAbsolute(target)) {
1166
+ if (!path7.isAbsolute(target)) {
721
1167
  return toPosixPath(target);
722
1168
  }
723
- const relative = path6.relative(root, target);
1169
+ const relative = path7.relative(root, target);
724
1170
  if (!relative) {
725
1171
  return ".";
726
1172
  }
@@ -818,53 +1264,11 @@ function unique2(values) {
818
1264
  return Array.from(new Set(values));
819
1265
  }
820
1266
 
821
- // src/core/parse/markdown.ts
822
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
823
- function parseHeadings(md) {
824
- const lines = md.split(/\r?\n/);
825
- const headings = [];
826
- for (let i = 0; i < lines.length; i++) {
827
- const line = lines[i] ?? "";
828
- const match = line.match(HEADING_RE);
829
- if (!match) continue;
830
- const levelToken = match[1];
831
- const title = match[2];
832
- if (!levelToken || !title) continue;
833
- headings.push({
834
- level: levelToken.length,
835
- title: title.trim(),
836
- line: i + 1
837
- });
838
- }
839
- return headings;
840
- }
841
- function extractH2Sections(md) {
842
- const lines = md.split(/\r?\n/);
843
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
844
- const sections = /* @__PURE__ */ new Map();
845
- for (let i = 0; i < headings.length; i++) {
846
- const current = headings[i];
847
- if (!current) continue;
848
- const next = headings[i + 1];
849
- const startLine = current.line + 1;
850
- const endLine = (next?.line ?? lines.length + 1) - 1;
851
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
852
- sections.set(current.title.trim(), {
853
- title: current.title.trim(),
854
- startLine,
855
- endLine,
856
- body
857
- });
858
- }
859
- return sections;
860
- }
861
-
862
1267
  // src/core/parse/spec.ts
863
1268
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
864
1269
  var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
865
1270
  var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
866
1271
  var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
867
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
868
1272
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
869
1273
  function parseSpec(md, file) {
870
1274
  const headings = parseHeadings(md);
@@ -872,15 +1276,13 @@ function parseSpec(md, file) {
872
1276
  const specId = h1?.title.match(SPEC_ID_RE)?.[0];
873
1277
  const sections = extractH2Sections(md);
874
1278
  const sectionNames = new Set(Array.from(sections.keys()));
875
- const brSection = sections.get(BR_SECTION_TITLE);
876
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
877
- const startLine = brSection?.startLine ?? 1;
1279
+ const lines = md.split(/\r?\n/);
878
1280
  const brs = [];
879
1281
  const brsWithoutPriority = [];
880
1282
  const brsWithInvalidPriority = [];
881
- for (let i = 0; i < brLines.length; i++) {
882
- const lineText = brLines[i] ?? "";
883
- const lineNumber = startLine + i;
1283
+ for (let i = 0; i < lines.length; i++) {
1284
+ const lineText = lines[i] ?? "";
1285
+ const lineNumber = i + 1;
884
1286
  const validMatch = lineText.match(BR_LINE_RE);
885
1287
  if (validMatch) {
886
1288
  const id = validMatch[1];
@@ -938,8 +1340,8 @@ function parseSpec(md, file) {
938
1340
  }
939
1341
 
940
1342
  // src/core/traceability.ts
941
- import { readFile as readFile3 } from "fs/promises";
942
- import path7 from "path";
1343
+ import { readFile as readFile4 } from "fs/promises";
1344
+ import path8 from "path";
943
1345
 
944
1346
  // src/core/gherkin/parse.ts
945
1347
  import {
@@ -1095,7 +1497,7 @@ function extractAnnotatedScIds(text) {
1095
1497
  async function collectScIdsFromScenarioFiles(scenarioFiles) {
1096
1498
  const scIds = /* @__PURE__ */ new Set();
1097
1499
  for (const file of scenarioFiles) {
1098
- const text = await readFile3(file, "utf-8");
1500
+ const text = await readFile4(file, "utf-8");
1099
1501
  const { document, errors } = parseScenarioDocument(text, file);
1100
1502
  if (!document || errors.length > 0) {
1101
1503
  continue;
@@ -1113,7 +1515,7 @@ async function collectScIdsFromScenarioFiles(scenarioFiles) {
1113
1515
  async function collectScIdSourcesFromScenarioFiles(scenarioFiles) {
1114
1516
  const sources = /* @__PURE__ */ new Map();
1115
1517
  for (const file of scenarioFiles) {
1116
- const text = await readFile3(file, "utf-8");
1518
+ const text = await readFile4(file, "utf-8");
1117
1519
  const { document, errors } = parseScenarioDocument(text, file);
1118
1520
  if (!document || errors.length > 0) {
1119
1521
  continue;
@@ -1171,10 +1573,10 @@ async function collectScTestReferences(root, globs, excludeGlobs) {
1171
1573
  };
1172
1574
  }
1173
1575
  const normalizedFiles = Array.from(
1174
- new Set(scanResult.files.map((file) => path7.normalize(file)))
1576
+ new Set(scanResult.files.map((file) => path8.normalize(file)))
1175
1577
  );
1176
1578
  for (const file of normalizedFiles) {
1177
- const text = await readFile3(file, "utf-8");
1579
+ const text = await readFile4(file, "utf-8");
1178
1580
  const scIds = extractAnnotatedScIds(text);
1179
1581
  if (scIds.length === 0) {
1180
1582
  continue;
@@ -1233,16 +1635,16 @@ function formatError3(error) {
1233
1635
  }
1234
1636
 
1235
1637
  // src/core/version.ts
1236
- import { readFile as readFile4 } from "fs/promises";
1237
- import path8 from "path";
1638
+ import { readFile as readFile5 } from "fs/promises";
1639
+ import path9 from "path";
1238
1640
  import { fileURLToPath } from "url";
1239
1641
  async function resolveToolVersion() {
1240
- if ("1.0.6".length > 0) {
1241
- return "1.0.6";
1642
+ if ("1.1.0".length > 0) {
1643
+ return "1.1.0";
1242
1644
  }
1243
1645
  try {
1244
1646
  const packagePath = resolvePackageJsonPath();
1245
- const raw = await readFile4(packagePath, "utf-8");
1647
+ const raw = await readFile5(packagePath, "utf-8");
1246
1648
  const parsed = JSON.parse(raw);
1247
1649
  const version = typeof parsed.version === "string" ? parsed.version : "";
1248
1650
  return version.length > 0 ? version : "unknown";
@@ -1253,18 +1655,18 @@ async function resolveToolVersion() {
1253
1655
  function resolvePackageJsonPath() {
1254
1656
  const base = import.meta.url;
1255
1657
  const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
1256
- return path8.resolve(path8.dirname(basePath), "../../package.json");
1658
+ return path9.resolve(path9.dirname(basePath), "../../package.json");
1257
1659
  }
1258
1660
 
1259
1661
  // src/core/validators/contracts.ts
1260
- import { access as access4, readFile as readFile5 } from "fs/promises";
1261
- import path10 from "path";
1662
+ import { access as access4, readFile as readFile6 } from "fs/promises";
1663
+ import path11 from "path";
1262
1664
 
1263
1665
  // src/core/contracts.ts
1264
- import path9 from "path";
1666
+ import path10 from "path";
1265
1667
  import { parse as parseYaml2 } from "yaml";
1266
1668
  function parseStructuredContract(file, text) {
1267
- const ext = path9.extname(file).toLowerCase();
1669
+ const ext = path10.extname(file).toLowerCase();
1268
1670
  if (ext === ".json") {
1269
1671
  return JSON.parse(text);
1270
1672
  }
@@ -1286,14 +1688,14 @@ async function validateContracts(root, config) {
1286
1688
  const issues = [];
1287
1689
  const contractIndex = await buildContractIndex(root, config);
1288
1690
  const contractsRoot = resolvePath(root, config, "contractsDir");
1289
- const uiRoot = path10.join(contractsRoot, "ui");
1691
+ const uiRoot = path11.join(contractsRoot, "ui");
1290
1692
  const themaIds = new Set(
1291
1693
  Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
1292
1694
  );
1293
1695
  issues.push(...await validateUiContracts(uiRoot, themaIds));
1294
1696
  issues.push(...await validateThemaContracts(uiRoot));
1295
- issues.push(...await validateApiContracts(path10.join(contractsRoot, "api")));
1296
- issues.push(...await validateDbContracts(path10.join(contractsRoot, "db")));
1697
+ issues.push(...await validateApiContracts(path11.join(contractsRoot, "api")));
1698
+ issues.push(...await validateDbContracts(path11.join(contractsRoot, "db")));
1297
1699
  issues.push(...validateDuplicateContractIds(contractIndex));
1298
1700
  return issues;
1299
1701
  }
@@ -1312,7 +1714,7 @@ async function validateUiContracts(uiRoot, themaIds) {
1312
1714
  }
1313
1715
  const issues = [];
1314
1716
  for (const file of files) {
1315
- const text = await readFile5(file, "utf-8");
1717
+ const text = await readFile6(file, "utf-8");
1316
1718
  const declaredIds = extractDeclaredContractIds(text);
1317
1719
  issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
1318
1720
  let doc = null;
@@ -1366,7 +1768,7 @@ async function validateThemaContracts(uiRoot) {
1366
1768
  }
1367
1769
  const issues = [];
1368
1770
  for (const file of files) {
1369
- const text = await readFile5(file, "utf-8");
1771
+ const text = await readFile6(file, "utf-8");
1370
1772
  const invalidIds = extractInvalidIds(text, [
1371
1773
  "SPEC",
1372
1774
  "BR",
@@ -1500,7 +1902,7 @@ async function validateApiContracts(apiRoot) {
1500
1902
  }
1501
1903
  const issues = [];
1502
1904
  for (const file of files) {
1503
- const text = await readFile5(file, "utf-8");
1905
+ const text = await readFile6(file, "utf-8");
1504
1906
  const invalidIds = extractInvalidIds(text, [
1505
1907
  "SPEC",
1506
1908
  "BR",
@@ -1569,7 +1971,7 @@ async function validateDbContracts(dbRoot) {
1569
1971
  }
1570
1972
  const issues = [];
1571
1973
  for (const file of files) {
1572
- const text = await readFile5(file, "utf-8");
1974
+ const text = await readFile6(file, "utf-8");
1573
1975
  const invalidIds = extractInvalidIds(text, [
1574
1976
  "SPEC",
1575
1977
  "BR",
@@ -1766,9 +2168,9 @@ async function validateUiAssets(assets, file, uiRoot) {
1766
2168
  );
1767
2169
  return issues;
1768
2170
  }
1769
- const packDir = path10.resolve(uiRoot, packValue);
1770
- const packRelative = path10.relative(uiRoot, packDir);
1771
- if (packRelative.startsWith("..") || path10.isAbsolute(packRelative)) {
2171
+ const packDir = path11.resolve(uiRoot, packValue);
2172
+ const packRelative = path11.relative(uiRoot, packDir);
2173
+ if (packRelative.startsWith("..") || path11.isAbsolute(packRelative)) {
1772
2174
  issues.push(
1773
2175
  issue(
1774
2176
  "QFAI-ASSET-001",
@@ -1794,7 +2196,7 @@ async function validateUiAssets(assets, file, uiRoot) {
1794
2196
  );
1795
2197
  return issues;
1796
2198
  }
1797
- const assetsYamlPath = path10.join(packDir, "assets.yaml");
2199
+ const assetsYamlPath = path11.join(packDir, "assets.yaml");
1798
2200
  if (!await exists4(assetsYamlPath)) {
1799
2201
  issues.push(
1800
2202
  issue(
@@ -1809,7 +2211,7 @@ async function validateUiAssets(assets, file, uiRoot) {
1809
2211
  }
1810
2212
  let manifest;
1811
2213
  try {
1812
- const manifestText = await readFile5(assetsYamlPath, "utf-8");
2214
+ const manifestText = await readFile6(assetsYamlPath, "utf-8");
1813
2215
  manifest = parseStructuredContract(assetsYamlPath, manifestText);
1814
2216
  } catch (error) {
1815
2217
  issues.push(
@@ -1882,9 +2284,9 @@ async function validateUiAssets(assets, file, uiRoot) {
1882
2284
  );
1883
2285
  continue;
1884
2286
  }
1885
- const assetPath = path10.resolve(packDir, entry.path);
1886
- const assetRelative = path10.relative(packDir, assetPath);
1887
- if (assetRelative.startsWith("..") || path10.isAbsolute(assetRelative)) {
2287
+ const assetPath = path11.resolve(packDir, entry.path);
2288
+ const assetRelative = path11.relative(packDir, assetPath);
2289
+ if (assetRelative.startsWith("..") || path11.isAbsolute(assetRelative)) {
1888
2290
  issues.push(
1889
2291
  issue(
1890
2292
  "QFAI-ASSET-004",
@@ -1925,7 +2327,7 @@ function shouldIgnoreInvalidId(value, doc) {
1925
2327
  return false;
1926
2328
  }
1927
2329
  const normalized = packValue.replace(/\\/g, "/");
1928
- const basename = path10.posix.basename(normalized);
2330
+ const basename = path11.posix.basename(normalized);
1929
2331
  if (!basename) {
1930
2332
  return false;
1931
2333
  }
@@ -1935,7 +2337,7 @@ function isSafeRelativePath(value) {
1935
2337
  if (!value) {
1936
2338
  return false;
1937
2339
  }
1938
- if (path10.isAbsolute(value)) {
2340
+ if (path11.isAbsolute(value)) {
1939
2341
  return false;
1940
2342
  }
1941
2343
  const normalized = value.replace(/\\/g, "/");
@@ -1985,8 +2387,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
1985
2387
  }
1986
2388
 
1987
2389
  // src/core/validators/delta.ts
1988
- import { readFile as readFile6 } from "fs/promises";
1989
- import path11 from "path";
2390
+ import { readFile as readFile7 } from "fs/promises";
2391
+ import path12 from "path";
1990
2392
  async function validateDeltas(root, config) {
1991
2393
  const specsRoot = resolvePath(root, config, "specsDir");
1992
2394
  const packs = await collectSpecPackDirs(specsRoot);
@@ -1995,9 +2397,9 @@ async function validateDeltas(root, config) {
1995
2397
  }
1996
2398
  const issues = [];
1997
2399
  for (const pack of packs) {
1998
- const deltaPath = path11.join(pack, "delta.md");
2400
+ const deltaPath = path12.join(pack, "delta.md");
1999
2401
  try {
2000
- await readFile6(deltaPath, "utf-8");
2402
+ await readFile7(deltaPath, "utf-8");
2001
2403
  } catch (error) {
2002
2404
  if (isMissingFileError2(error)) {
2003
2405
  issues.push(
@@ -2048,8 +2450,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2048
2450
  }
2049
2451
 
2050
2452
  // src/core/validators/ids.ts
2051
- import { readFile as readFile7 } from "fs/promises";
2052
- import path12 from "path";
2453
+ import { readFile as readFile8 } from "fs/promises";
2454
+ import path13 from "path";
2053
2455
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2054
2456
  async function validateDefinedIds(root, config) {
2055
2457
  const issues = [];
@@ -2084,7 +2486,7 @@ async function validateDefinedIds(root, config) {
2084
2486
  }
2085
2487
  async function collectSpecDefinitionIds(files, out) {
2086
2488
  for (const file of files) {
2087
- const text = await readFile7(file, "utf-8");
2489
+ const text = await readFile8(file, "utf-8");
2088
2490
  const parsed = parseSpec(text, file);
2089
2491
  if (parsed.specId) {
2090
2492
  recordId(out, parsed.specId, file);
@@ -2094,7 +2496,7 @@ async function collectSpecDefinitionIds(files, out) {
2094
2496
  }
2095
2497
  async function collectScenarioDefinitionIds(files, out) {
2096
2498
  for (const file of files) {
2097
- const text = await readFile7(file, "utf-8");
2499
+ const text = await readFile8(file, "utf-8");
2098
2500
  const { document, errors } = parseScenarioDocument(text, file);
2099
2501
  if (!document || errors.length > 0) {
2100
2502
  continue;
@@ -2115,7 +2517,7 @@ function recordId(out, id, file) {
2115
2517
  }
2116
2518
  function formatFileList(files, root) {
2117
2519
  return files.map((file) => {
2118
- const relative = path12.relative(root, file);
2520
+ const relative = path13.relative(root, file);
2119
2521
  return relative.length > 0 ? relative : file;
2120
2522
  }).join(", ");
2121
2523
  }
@@ -2142,20 +2544,20 @@ function issue3(code, message, severity, file, rule, refs, category = "compatibi
2142
2544
  }
2143
2545
 
2144
2546
  // src/core/promptsIntegrity.ts
2145
- import { readFile as readFile8 } from "fs/promises";
2146
- import path14 from "path";
2547
+ import { readFile as readFile9 } from "fs/promises";
2548
+ import path15 from "path";
2147
2549
 
2148
2550
  // src/shared/assets.ts
2149
2551
  import { existsSync } from "fs";
2150
- import path13 from "path";
2552
+ import path14 from "path";
2151
2553
  import { fileURLToPath as fileURLToPath2 } from "url";
2152
2554
  function getInitAssetsDir() {
2153
2555
  const base = import.meta.url;
2154
2556
  const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
2155
- const baseDir = path13.dirname(basePath);
2557
+ const baseDir = path14.dirname(basePath);
2156
2558
  const candidates = [
2157
- path13.resolve(baseDir, "../../../assets/init"),
2158
- path13.resolve(baseDir, "../../assets/init")
2559
+ path14.resolve(baseDir, "../../../assets/init"),
2560
+ path14.resolve(baseDir, "../../assets/init")
2159
2561
  ];
2160
2562
  for (const candidate of candidates) {
2161
2563
  if (existsSync(candidate)) {
@@ -2175,10 +2577,10 @@ function getInitAssetsDir() {
2175
2577
  var LEGACY_OK_EXTRA = /* @__PURE__ */ new Set(["qfai-classify-change.md"]);
2176
2578
  async function diffProjectPromptsAgainstInitAssets(root, config) {
2177
2579
  const promptsDirConfig = config.paths.promptsDir;
2178
- const promptsDir = path14.isAbsolute(promptsDirConfig) ? promptsDirConfig : path14.resolve(root, promptsDirConfig);
2580
+ const promptsDir = path15.isAbsolute(promptsDirConfig) ? promptsDirConfig : path15.resolve(root, promptsDirConfig);
2179
2581
  let templateDir;
2180
2582
  try {
2181
- const rel = path14.isAbsolute(promptsDirConfig) ? path14.relative(root, promptsDirConfig) : promptsDirConfig;
2583
+ const rel = path15.isAbsolute(promptsDirConfig) ? path15.relative(root, promptsDirConfig) : promptsDirConfig;
2182
2584
  const normalized = rel.replace(/^[\\/]+/, "");
2183
2585
  if (normalized.length === 0 || normalized.startsWith("..")) {
2184
2586
  return {
@@ -2190,7 +2592,7 @@ async function diffProjectPromptsAgainstInitAssets(root, config) {
2190
2592
  changed: []
2191
2593
  };
2192
2594
  }
2193
- templateDir = path14.join(getInitAssetsDir(), normalized);
2595
+ templateDir = path15.join(getInitAssetsDir(), normalized);
2194
2596
  } catch {
2195
2597
  return {
2196
2598
  status: "skipped_missing_assets",
@@ -2244,8 +2646,8 @@ async function diffProjectPromptsAgainstInitAssets(root, config) {
2244
2646
  }
2245
2647
  try {
2246
2648
  const [a, b] = await Promise.all([
2247
- readFile8(templateAbs, "utf-8"),
2248
- readFile8(projectAbs, "utf-8")
2649
+ readFile9(templateAbs, "utf-8"),
2650
+ readFile9(projectAbs, "utf-8")
2249
2651
  ]);
2250
2652
  if (normalizeNewlines(a) !== normalizeNewlines(b)) {
2251
2653
  changed.push(rel);
@@ -2268,7 +2670,7 @@ function normalizeNewlines(text) {
2268
2670
  return text.replace(/\r\n/g, "\n");
2269
2671
  }
2270
2672
  function toRel(base, abs) {
2271
- const rel = path14.relative(base, abs);
2673
+ const rel = path15.relative(base, abs);
2272
2674
  return rel.replace(/[\\/]+/g, "/");
2273
2675
  }
2274
2676
  function intersectKeys(a, b) {
@@ -2313,8 +2715,8 @@ async function validatePromptsIntegrity(root, config) {
2313
2715
  }
2314
2716
 
2315
2717
  // src/core/validators/scenario.ts
2316
- import { access as access5, readFile as readFile9 } from "fs/promises";
2317
- import path15 from "path";
2718
+ import { access as access5, readFile as readFile10 } from "fs/promises";
2719
+ import path16 from "path";
2318
2720
  var GIVEN_PATTERN = /\bGiven\b/;
2319
2721
  var WHEN_PATTERN = /\bWhen\b/;
2320
2722
  var THEN_PATTERN = /\bThen\b/;
@@ -2337,7 +2739,7 @@ async function validateScenarios(root, config) {
2337
2739
  }
2338
2740
  const issues = [];
2339
2741
  for (const entry of entries) {
2340
- const legacyScenarioPath = path15.join(entry.dir, "scenario.md");
2742
+ const legacyScenarioPath = path16.join(entry.dir, "scenario.md");
2341
2743
  if (await fileExists(legacyScenarioPath)) {
2342
2744
  issues.push(
2343
2745
  issue4(
@@ -2351,7 +2753,7 @@ async function validateScenarios(root, config) {
2351
2753
  }
2352
2754
  let text;
2353
2755
  try {
2354
- text = await readFile9(entry.scenarioPath, "utf-8");
2756
+ text = await readFile10(entry.scenarioPath, "utf-8");
2355
2757
  } catch (error) {
2356
2758
  if (isMissingFileError3(error)) {
2357
2759
  issues.push(
@@ -2534,7 +2936,7 @@ async function fileExists(target) {
2534
2936
  }
2535
2937
 
2536
2938
  // src/core/validators/spec.ts
2537
- import { readFile as readFile10 } from "fs/promises";
2939
+ import { readFile as readFile11 } from "fs/promises";
2538
2940
  async function validateSpecs(root, config) {
2539
2941
  const specsRoot = resolvePath(root, config, "specsDir");
2540
2942
  const entries = await collectSpecEntries(specsRoot);
@@ -2555,7 +2957,7 @@ async function validateSpecs(root, config) {
2555
2957
  for (const entry of entries) {
2556
2958
  let text;
2557
2959
  try {
2558
- text = await readFile10(entry.specPath, "utf-8");
2960
+ text = await readFile11(entry.specPath, "utf-8");
2559
2961
  } catch (error) {
2560
2962
  if (isMissingFileError4(error)) {
2561
2963
  issues.push(
@@ -2709,7 +3111,7 @@ function isMissingFileError4(error) {
2709
3111
  }
2710
3112
 
2711
3113
  // src/core/validators/traceability.ts
2712
- import { readFile as readFile11 } from "fs/promises";
3114
+ import { readFile as readFile12 } from "fs/promises";
2713
3115
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
2714
3116
  var BR_TAG_RE2 = /^BR-\d{4}$/;
2715
3117
  async function validateTraceability(root, config) {
@@ -2729,7 +3131,7 @@ async function validateTraceability(root, config) {
2729
3131
  const contractIndex = await buildContractIndex(root, config);
2730
3132
  const contractIds = contractIndex.ids;
2731
3133
  for (const file of specFiles) {
2732
- const text = await readFile11(file, "utf-8");
3134
+ const text = await readFile12(file, "utf-8");
2733
3135
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2734
3136
  const parsed = parseSpec(text, file);
2735
3137
  if (parsed.specId) {
@@ -2802,7 +3204,7 @@ async function validateTraceability(root, config) {
2802
3204
  }
2803
3205
  }
2804
3206
  for (const file of scenarioFiles) {
2805
- const text = await readFile11(file, "utf-8");
3207
+ const text = await readFile12(file, "utf-8");
2806
3208
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
2807
3209
  const scenarioContractRefs = parseContractRefs(text, {
2808
3210
  allowCommentPrefix: true
@@ -3124,7 +3526,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
3124
3526
  const pattern = buildIdPattern(Array.from(upstreamIds));
3125
3527
  let found = false;
3126
3528
  for (const file of targetFiles) {
3127
- const text = await readFile11(file, "utf-8");
3529
+ const text = await readFile12(file, "utf-8");
3128
3530
  if (pattern.test(text)) {
3129
3531
  found = true;
3130
3532
  break;
@@ -3223,16 +3625,17 @@ var ID_PREFIXES2 = [
3223
3625
  "DB",
3224
3626
  "THEMA"
3225
3627
  ];
3628
+ var REPORT_GUARDRAILS_MAX = 20;
3226
3629
  async function createReportData(root, validation, configResult) {
3227
- const resolvedRoot = path16.resolve(root);
3630
+ const resolvedRoot = path17.resolve(root);
3228
3631
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3229
3632
  const config = resolved.config;
3230
3633
  const configPath = resolved.configPath;
3231
3634
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3232
3635
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3233
- const apiRoot = path16.join(contractsRoot, "api");
3234
- const uiRoot = path16.join(contractsRoot, "ui");
3235
- const dbRoot = path16.join(contractsRoot, "db");
3636
+ const apiRoot = path17.join(contractsRoot, "api");
3637
+ const uiRoot = path17.join(contractsRoot, "ui");
3638
+ const dbRoot = path17.join(contractsRoot, "db");
3236
3639
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3237
3640
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3238
3641
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3291,6 +3694,27 @@ async function createReportData(root, validation, configResult) {
3291
3694
  const scSourceRecord = mapToSortedRecord(
3292
3695
  normalizeScSources(resolvedRoot, scSources)
3293
3696
  );
3697
+ const guardrailsLoad = await loadDecisionGuardrails(resolvedRoot, {
3698
+ specsRoot
3699
+ });
3700
+ const guardrailsAll = sortDecisionGuardrails(
3701
+ normalizeDecisionGuardrails(guardrailsLoad.entries)
3702
+ );
3703
+ const guardrailsDisplay = guardrailsAll.slice(0, REPORT_GUARDRAILS_MAX);
3704
+ const guardrailsByType = { nonGoal: 0, notNow: 0, tradeOff: 0 };
3705
+ for (const item of guardrailsAll) {
3706
+ if (item.type === "non-goal") {
3707
+ guardrailsByType.nonGoal += 1;
3708
+ } else if (item.type === "not-now") {
3709
+ guardrailsByType.notNow += 1;
3710
+ } else if (item.type === "trade-off") {
3711
+ guardrailsByType.tradeOff += 1;
3712
+ }
3713
+ }
3714
+ const guardrailsErrors = guardrailsLoad.errors.map((item) => ({
3715
+ path: toRelativePath(resolvedRoot, item.path),
3716
+ message: item.message
3717
+ }));
3294
3718
  const version = await resolveToolVersion();
3295
3719
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
3296
3720
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
@@ -3338,6 +3762,34 @@ async function createReportData(root, validation, configResult) {
3338
3762
  specToContracts: specToContractsRecord
3339
3763
  }
3340
3764
  },
3765
+ guardrails: {
3766
+ total: guardrailsAll.length,
3767
+ max: REPORT_GUARDRAILS_MAX,
3768
+ truncated: guardrailsAll.length > guardrailsDisplay.length,
3769
+ byType: guardrailsByType,
3770
+ items: guardrailsDisplay.map((item) => {
3771
+ const entry = {
3772
+ id: item.id,
3773
+ type: item.type,
3774
+ guardrail: item.guardrail,
3775
+ source: {
3776
+ file: toRelativePath(resolvedRoot, item.source.file),
3777
+ line: item.source.line
3778
+ }
3779
+ };
3780
+ if (item.rationale) {
3781
+ entry.rationale = item.rationale;
3782
+ }
3783
+ if (item.reconsider) {
3784
+ entry.reconsider = item.reconsider;
3785
+ }
3786
+ if (item.related) {
3787
+ entry.related = item.related;
3788
+ }
3789
+ return entry;
3790
+ }),
3791
+ scanErrors: guardrailsErrors
3792
+ },
3341
3793
  issues: normalizedValidation.issues
3342
3794
  };
3343
3795
  }
@@ -3433,6 +3885,7 @@ function formatReportMarkdown(data, options = {}) {
3433
3885
  lines.push("");
3434
3886
  lines.push("- [Compatibility Issues](#compatibility-issues)");
3435
3887
  lines.push("- [Change Issues](#change-issues)");
3888
+ lines.push("- [Decision Guardrails](#decision-guardrails)");
3436
3889
  lines.push("- [IDs](#ids)");
3437
3890
  lines.push("- [Traceability](#traceability)");
3438
3891
  lines.push("");
@@ -3524,6 +3977,49 @@ function formatReportMarkdown(data, options = {}) {
3524
3977
  lines.push("### Issues");
3525
3978
  lines.push("");
3526
3979
  lines.push(...formatIssueCards(issuesByCategory.change));
3980
+ lines.push("## Decision Guardrails");
3981
+ lines.push("");
3982
+ lines.push(`- total: ${data.guardrails.total}`);
3983
+ lines.push(
3984
+ `- types: non-goal ${data.guardrails.byType.nonGoal} / not-now ${data.guardrails.byType.notNow} / trade-off ${data.guardrails.byType.tradeOff}`
3985
+ );
3986
+ if (data.guardrails.truncated) {
3987
+ lines.push(`- truncated: true (max=${data.guardrails.max})`);
3988
+ }
3989
+ if (data.guardrails.scanErrors.length > 0) {
3990
+ lines.push(`- scanErrors: ${data.guardrails.scanErrors.length}`);
3991
+ }
3992
+ lines.push("");
3993
+ if (data.guardrails.items.length === 0) {
3994
+ lines.push("- (none)");
3995
+ } else {
3996
+ for (const item of data.guardrails.items) {
3997
+ lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
3998
+ lines.push(
3999
+ ` - source: ${formatPathWithLine(item.source.file, { line: item.source.line }, baseUrl)}`
4000
+ );
4001
+ if (item.rationale) {
4002
+ lines.push(` - Rationale: ${item.rationale}`);
4003
+ }
4004
+ if (item.reconsider) {
4005
+ lines.push(` - Reconsider: ${item.reconsider}`);
4006
+ }
4007
+ if (item.related) {
4008
+ lines.push(` - Related: ${item.related}`);
4009
+ }
4010
+ }
4011
+ }
4012
+ if (data.guardrails.scanErrors.length > 0) {
4013
+ lines.push("");
4014
+ lines.push("### Scan errors");
4015
+ lines.push("");
4016
+ for (const errorItem of data.guardrails.scanErrors) {
4017
+ lines.push(
4018
+ `- ${formatPathLink(errorItem.path, baseUrl)}: ${errorItem.message}`
4019
+ );
4020
+ }
4021
+ }
4022
+ lines.push("");
3527
4023
  lines.push("## IDs");
3528
4024
  lines.push("");
3529
4025
  lines.push(formatIdLine("SPEC", data.ids.spec));
@@ -3714,7 +4210,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
3714
4210
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
3715
4211
  }
3716
4212
  for (const file of specFiles) {
3717
- const text = await readFile12(file, "utf-8");
4213
+ const text = await readFile13(file, "utf-8");
3718
4214
  const parsed = parseSpec(text, file);
3719
4215
  const specKey = parsed.specId;
3720
4216
  if (!specKey) {
@@ -3756,7 +4252,7 @@ async function collectIds(files) {
3756
4252
  THEMA: /* @__PURE__ */ new Set()
3757
4253
  };
3758
4254
  for (const file of files) {
3759
- const text = await readFile12(file, "utf-8");
4255
+ const text = await readFile13(file, "utf-8");
3760
4256
  for (const prefix of ID_PREFIXES2) {
3761
4257
  const ids = extractIds(text, prefix);
3762
4258
  ids.forEach((id) => result[prefix].add(id));
@@ -3775,7 +4271,7 @@ async function collectIds(files) {
3775
4271
  async function collectUpstreamIds(files) {
3776
4272
  const ids = /* @__PURE__ */ new Set();
3777
4273
  for (const file of files) {
3778
- const text = await readFile12(file, "utf-8");
4274
+ const text = await readFile13(file, "utf-8");
3779
4275
  extractAllIds(text).forEach((id) => ids.add(id));
3780
4276
  }
3781
4277
  return ids;
@@ -3796,7 +4292,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
3796
4292
  }
3797
4293
  const pattern = buildIdPattern2(Array.from(upstreamIds));
3798
4294
  for (const file of targetFiles) {
3799
- const text = await readFile12(file, "utf-8");
4295
+ const text = await readFile13(file, "utf-8");
3800
4296
  if (pattern.test(text)) {
3801
4297
  return true;
3802
4298
  }
@@ -3921,19 +4417,26 @@ function buildHotspots(issues) {
3921
4417
  );
3922
4418
  }
3923
4419
  export {
4420
+ checkDecisionGuardrails,
3924
4421
  createReportData,
3925
4422
  defaultConfig,
3926
4423
  extractAllIds,
4424
+ extractDecisionGuardrailsFromMarkdown,
3927
4425
  extractIds,
3928
4426
  extractInvalidIds,
4427
+ filterDecisionGuardrailsByKeyword,
3929
4428
  findConfigRoot,
4429
+ formatGuardrailsForLlm,
3930
4430
  formatReportJson,
3931
4431
  formatReportMarkdown,
3932
4432
  getConfigPath,
3933
4433
  lintSql,
3934
4434
  loadConfig,
4435
+ loadDecisionGuardrails,
4436
+ normalizeDecisionGuardrails,
3935
4437
  resolvePath,
3936
4438
  resolveToolVersion,
4439
+ sortDecisionGuardrails,
3937
4440
  validateContracts,
3938
4441
  validateDefinedIds,
3939
4442
  validateDeltas,