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/README.md +71 -10
- package/assets/init/.qfai/README.md +1 -1
- package/assets/init/.qfai/assistant/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-configure.md +33 -8
- package/assets/init/.qfai/assistant/prompts/qfai-verify.md +1 -1
- package/assets/init/.qfai/assistant/steering/README.md +6 -0
- package/assets/init/.qfai/assistant/steering/manifest.md +45 -0
- package/assets/init/.qfai/contracts/db/README.md +10 -3
- package/assets/init/.qfai/samples/guardrails/delta_with_guardrails.md +19 -0
- package/assets/init/.qfai/specs/README.md +6 -0
- package/assets/init/root/.github/workflows/qfai.yml +0 -2
- package/assets/init/root/qfai.config.yaml +1 -8
- package/dist/cli/index.cjs +873 -195
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +859 -181
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +731 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.mjs +719 -216
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-pr.md +0 -209
package/dist/index.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/
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
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:
|
|
578
|
-
deltaPath:
|
|
579
|
-
scenarioPath:
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
682
|
-
const apiRoot =
|
|
683
|
-
const dbRoot =
|
|
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
|
|
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
|
|
1161
|
+
import path7 from "path";
|
|
716
1162
|
function toRelativePath(root, target) {
|
|
717
1163
|
if (!target) {
|
|
718
1164
|
return target;
|
|
719
1165
|
}
|
|
720
|
-
if (!
|
|
1166
|
+
if (!path7.isAbsolute(target)) {
|
|
721
1167
|
return toPosixPath(target);
|
|
722
1168
|
}
|
|
723
|
-
const relative =
|
|
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
|
|
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 <
|
|
882
|
-
const lineText =
|
|
883
|
-
const lineNumber =
|
|
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
|
|
942
|
-
import
|
|
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
|
|
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
|
|
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) =>
|
|
1576
|
+
new Set(scanResult.files.map((file) => path8.normalize(file)))
|
|
1175
1577
|
);
|
|
1176
1578
|
for (const file of normalizedFiles) {
|
|
1177
|
-
const text = await
|
|
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
|
|
1237
|
-
import
|
|
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.
|
|
1241
|
-
return "1.
|
|
1642
|
+
if ("1.1.1".length > 0) {
|
|
1643
|
+
return "1.1.1";
|
|
1242
1644
|
}
|
|
1243
1645
|
try {
|
|
1244
1646
|
const packagePath = resolvePackageJsonPath();
|
|
1245
|
-
const raw = await
|
|
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
|
|
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
|
|
1261
|
-
import
|
|
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
|
|
1666
|
+
import path10 from "path";
|
|
1265
1667
|
import { parse as parseYaml2 } from "yaml";
|
|
1266
1668
|
function parseStructuredContract(file, text) {
|
|
1267
|
-
const ext =
|
|
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 =
|
|
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(
|
|
1296
|
-
issues.push(...await validateDbContracts(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1770
|
-
const packRelative =
|
|
1771
|
-
if (packRelative.startsWith("..") ||
|
|
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 =
|
|
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
|
|
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 =
|
|
1886
|
-
const assetRelative =
|
|
1887
|
-
if (assetRelative.startsWith("..") ||
|
|
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 =
|
|
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 (
|
|
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
|
|
1989
|
-
import
|
|
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 =
|
|
2400
|
+
const deltaPath = path12.join(pack, "delta.md");
|
|
1999
2401
|
try {
|
|
2000
|
-
await
|
|
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
|
|
2052
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2146
|
-
import
|
|
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
|
|
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 =
|
|
2557
|
+
const baseDir = path14.dirname(basePath);
|
|
2156
2558
|
const candidates = [
|
|
2157
|
-
|
|
2158
|
-
|
|
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 =
|
|
2580
|
+
const promptsDir = path15.isAbsolute(promptsDirConfig) ? promptsDirConfig : path15.resolve(root, promptsDirConfig);
|
|
2179
2581
|
let templateDir;
|
|
2180
2582
|
try {
|
|
2181
|
-
const rel =
|
|
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 =
|
|
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
|
-
|
|
2248
|
-
|
|
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 =
|
|
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
|
|
2317
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
3234
|
-
const uiRoot =
|
|
3235
|
-
const dbRoot =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|