qfai 1.7.0 → 1.7.2
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/assets/init/.qfai/assistant/skills/qfai-implement/SKILL.md +7 -0
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/SKILL.md +7 -0
- package/assets/init/.qfai/evidence/README.md +30 -0
- package/assets/validators/designSlopPatterns.json +56 -0
- package/dist/cli/index.cjs +966 -122
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +956 -112
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +777 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.mjs +778 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -83,6 +83,22 @@ module.exports = __toCommonJS(src_exports);
|
|
|
83
83
|
var import_promises = require("fs/promises");
|
|
84
84
|
var import_node_path = __toESM(require("path"), 1);
|
|
85
85
|
var import_yaml = require("yaml");
|
|
86
|
+
|
|
87
|
+
// src/core/uiux/renderEvidenceTypes.ts
|
|
88
|
+
var DEFAULT_RENDER_VIEWPORTS = ["desktop", "mobile"];
|
|
89
|
+
function normalizeRenderViewports(viewports) {
|
|
90
|
+
const normalized = Array.isArray(viewports) ? viewports.map((item) => item.trim()).filter((item) => item.length > 0) : [];
|
|
91
|
+
if (normalized.length > 0) {
|
|
92
|
+
return Array.from(new Set(normalized));
|
|
93
|
+
}
|
|
94
|
+
return [...DEFAULT_RENDER_VIEWPORTS];
|
|
95
|
+
}
|
|
96
|
+
function looksLikeInlineRenderPayload(value) {
|
|
97
|
+
const trimmed = value.trim().toLowerCase();
|
|
98
|
+
return trimmed.startsWith("data:image") || trimmed.includes("<html");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/core/config.ts
|
|
86
102
|
var defaultConfig = {
|
|
87
103
|
paths: {
|
|
88
104
|
contractsDir: ".qfai/contracts",
|
|
@@ -532,6 +548,133 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
532
548
|
);
|
|
533
549
|
}
|
|
534
550
|
}
|
|
551
|
+
if (raw.renderEvidence !== void 0) {
|
|
552
|
+
const renderEvidence = normalizeRenderEvidence(raw.renderEvidence, configPath, issues);
|
|
553
|
+
if (renderEvidence) {
|
|
554
|
+
result.renderEvidence = renderEvidence;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (raw.audit !== void 0) {
|
|
558
|
+
const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
|
|
559
|
+
if (audit) {
|
|
560
|
+
result.audit = audit;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
564
|
+
}
|
|
565
|
+
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
566
|
+
if (!isRecord(raw)) {
|
|
567
|
+
issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
const result = {};
|
|
571
|
+
if (raw.enabled !== void 0) {
|
|
572
|
+
if (typeof raw.enabled === "boolean") {
|
|
573
|
+
result.enabled = raw.enabled;
|
|
574
|
+
} else {
|
|
575
|
+
issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (raw.slopDetection !== void 0) {
|
|
579
|
+
if (typeof raw.slopDetection === "boolean") {
|
|
580
|
+
result.slopDetection = raw.slopDetection;
|
|
581
|
+
} else {
|
|
582
|
+
issues.push(
|
|
583
|
+
configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (raw.maxPrimaryCtas !== void 0) {
|
|
588
|
+
if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
|
|
589
|
+
result.maxPrimaryCtas = raw.maxPrimaryCtas;
|
|
590
|
+
} else {
|
|
591
|
+
issues.push(
|
|
592
|
+
configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (raw.maxRawTokenLiteralWarnings !== void 0) {
|
|
597
|
+
if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
|
|
598
|
+
result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
|
|
599
|
+
} else {
|
|
600
|
+
issues.push(
|
|
601
|
+
configIssue(
|
|
602
|
+
configPath,
|
|
603
|
+
"uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (raw.maxDuplicateFindingsPerRule !== void 0) {
|
|
609
|
+
if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
|
|
610
|
+
result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
|
|
611
|
+
} else {
|
|
612
|
+
issues.push(
|
|
613
|
+
configIssue(
|
|
614
|
+
configPath,
|
|
615
|
+
"uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
621
|
+
}
|
|
622
|
+
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
623
|
+
if (!isRecord(raw)) {
|
|
624
|
+
issues.push(
|
|
625
|
+
configIssue(configPath, "uiux.renderEvidence \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
626
|
+
);
|
|
627
|
+
return void 0;
|
|
628
|
+
}
|
|
629
|
+
const result = {};
|
|
630
|
+
if (raw.enabled !== void 0) {
|
|
631
|
+
if (typeof raw.enabled === "boolean") {
|
|
632
|
+
result.enabled = raw.enabled;
|
|
633
|
+
} else {
|
|
634
|
+
issues.push(
|
|
635
|
+
configIssue(configPath, "uiux.renderEvidence.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (raw.viewports !== void 0) {
|
|
640
|
+
if (Array.isArray(raw.viewports) && raw.viewports.every((item) => typeof item === "string")) {
|
|
641
|
+
result.viewports = normalizeRenderViewports(raw.viewports);
|
|
642
|
+
} else {
|
|
643
|
+
issues.push(
|
|
644
|
+
configIssue(configPath, "uiux.renderEvidence.viewports \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (raw.out !== void 0) {
|
|
649
|
+
if (typeof raw.out === "string" && raw.out.trim().length > 0) {
|
|
650
|
+
result.out = raw.out.trim();
|
|
651
|
+
} else {
|
|
652
|
+
issues.push(
|
|
653
|
+
configIssue(configPath, "uiux.renderEvidence.out \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (raw.baseUrl !== void 0) {
|
|
658
|
+
if (typeof raw.baseUrl === "string" && raw.baseUrl.trim().length > 0) {
|
|
659
|
+
result.baseUrl = raw.baseUrl.trim();
|
|
660
|
+
} else {
|
|
661
|
+
issues.push(
|
|
662
|
+
configIssue(
|
|
663
|
+
configPath,
|
|
664
|
+
"uiux.renderEvidence.baseUrl \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (raw.failOpen !== void 0) {
|
|
670
|
+
if (typeof raw.failOpen === "boolean") {
|
|
671
|
+
result.failOpen = raw.failOpen;
|
|
672
|
+
} else {
|
|
673
|
+
issues.push(
|
|
674
|
+
configIssue(configPath, "uiux.renderEvidence.failOpen \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
535
678
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
536
679
|
}
|
|
537
680
|
function configIssue(file, message) {
|
|
@@ -1595,11 +1738,11 @@ function cloneGlobal(pattern) {
|
|
|
1595
1738
|
}
|
|
1596
1739
|
|
|
1597
1740
|
// src/core/atddTraceability.ts
|
|
1598
|
-
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4})\b/g;
|
|
1599
|
-
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4})\b/g;
|
|
1741
|
+
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
|
|
1742
|
+
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
|
|
1600
1743
|
var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
|
|
1601
|
-
var
|
|
1602
|
-
var
|
|
1744
|
+
var US_ID_RE = /^US-\d{4}(?:-\d{4})?$/;
|
|
1745
|
+
var TC_ID_RE = /^TC-\d{4}(?:-\d{4})?$/;
|
|
1603
1746
|
var API_CONTRACT_ID_RE = /^CON-API-\d+$/;
|
|
1604
1747
|
var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
|
|
1605
1748
|
async function evaluateAtddCodeTraceability(root, config) {
|
|
@@ -1747,11 +1890,11 @@ async function collectApiContractIds(apiRoot) {
|
|
|
1747
1890
|
function collectShortIds(text, prefix) {
|
|
1748
1891
|
const ids = /* @__PURE__ */ new Set();
|
|
1749
1892
|
const headingIds = collectMarkdownItems(text, prefix).map((item) => item.id);
|
|
1750
|
-
const pattern = prefix === "US" ? /\bUS-\d{4}
|
|
1893
|
+
const pattern = prefix === "US" ? /\bUS-\d{4}(?:-\d{4})?\b/g : /\bTC-\d{4}(?:-\d{4})?\b/g;
|
|
1751
1894
|
const looseIds = uniqueMatches(text, pattern);
|
|
1752
1895
|
for (const id of [...headingIds, ...looseIds]) {
|
|
1753
1896
|
const normalized = id.toUpperCase();
|
|
1754
|
-
if (prefix === "US" &&
|
|
1897
|
+
if (prefix === "US" && US_ID_RE.test(normalized) || prefix === "TC" && TC_ID_RE.test(normalized)) {
|
|
1755
1898
|
ids.add(normalized);
|
|
1756
1899
|
}
|
|
1757
1900
|
}
|
|
@@ -2436,6 +2579,7 @@ var ID_PREFIXES = [
|
|
|
2436
2579
|
"DB",
|
|
2437
2580
|
"THEMA"
|
|
2438
2581
|
];
|
|
2582
|
+
var DIGIT_AHEAD = "(?=[A-Za-z0-9_-]*\\d)";
|
|
2439
2583
|
var STRICT_ID_PATTERNS = {
|
|
2440
2584
|
CAP: /\bCAP-\d{4}\b/g,
|
|
2441
2585
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
@@ -2451,18 +2595,18 @@ var STRICT_ID_PATTERNS = {
|
|
|
2451
2595
|
ADR: /\bADR-\d{4}\b/g
|
|
2452
2596
|
};
|
|
2453
2597
|
var LOOSE_ID_PATTERNS = {
|
|
2454
|
-
CAP:
|
|
2455
|
-
SPEC:
|
|
2456
|
-
US:
|
|
2457
|
-
BR:
|
|
2458
|
-
SC:
|
|
2459
|
-
AC:
|
|
2460
|
-
CASE:
|
|
2461
|
-
UI:
|
|
2462
|
-
API:
|
|
2463
|
-
DB:
|
|
2464
|
-
THEMA:
|
|
2465
|
-
ADR:
|
|
2598
|
+
CAP: new RegExp(`\\bCAP-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2599
|
+
SPEC: new RegExp(`\\bSPEC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2600
|
+
US: new RegExp(`\\bUS-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2601
|
+
BR: new RegExp(`\\bBR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2602
|
+
SC: new RegExp(`\\bSC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2603
|
+
AC: new RegExp(`\\bAC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2604
|
+
CASE: new RegExp(`\\bCASE-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2605
|
+
UI: new RegExp(`\\bUI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2606
|
+
API: new RegExp(`\\bAPI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2607
|
+
DB: new RegExp(`\\bDB-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2608
|
+
THEMA: new RegExp(`\\bTHEMA-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2609
|
+
ADR: new RegExp(`\\bADR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi")
|
|
2466
2610
|
};
|
|
2467
2611
|
function extractIds(text, prefix) {
|
|
2468
2612
|
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
@@ -3504,8 +3648,8 @@ function formatError4(error) {
|
|
|
3504
3648
|
}
|
|
3505
3649
|
|
|
3506
3650
|
// src/core/report.ts
|
|
3507
|
-
var
|
|
3508
|
-
var
|
|
3651
|
+
var import_promises52 = require("fs/promises");
|
|
3652
|
+
var import_node_path54 = __toESM(require("path"), 1);
|
|
3509
3653
|
|
|
3510
3654
|
// src/core/contractIndex.ts
|
|
3511
3655
|
var import_promises13 = require("fs/promises");
|
|
@@ -4263,8 +4407,8 @@ var import_promises14 = require("fs/promises");
|
|
|
4263
4407
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
4264
4408
|
var import_node_url = require("url");
|
|
4265
4409
|
async function resolveToolVersion() {
|
|
4266
|
-
if ("1.7.
|
|
4267
|
-
return "1.7.
|
|
4410
|
+
if ("1.7.2".length > 0) {
|
|
4411
|
+
return "1.7.2";
|
|
4268
4412
|
}
|
|
4269
4413
|
try {
|
|
4270
4414
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -7107,7 +7251,7 @@ var import_node_path24 = __toESM(require("path"), 1);
|
|
|
7107
7251
|
var SC_TAG_RE3 = /^SC-\d{4}-\d{4}$/;
|
|
7108
7252
|
var SC_TAG_RE_GLOBAL = /\bSC-\d{4}-\d{4}\b/g;
|
|
7109
7253
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
7110
|
-
var
|
|
7254
|
+
var US_ID_RE2 = /\bUS-\d{4}-\d{4}\b/g;
|
|
7111
7255
|
var AC_ID_RE3 = /\bAC-\d{4}-\d{4}\b/g;
|
|
7112
7256
|
var DOWNSTREAM_ID_RE = /\b(?:US|AC|BR|SC|CASE)-\d{4}-\d{4}\b/g;
|
|
7113
7257
|
async function validateTraceability(root, config, phase) {
|
|
@@ -7234,7 +7378,7 @@ async function collectLayeredEdgeData(entry, issues) {
|
|
|
7234
7378
|
const { definitions: acIds, refsByDefinition: acToUs } = extractTableDefinitionRefs(
|
|
7235
7379
|
acceptanceCriteriaText,
|
|
7236
7380
|
/^AC-\d{4}-\d{4}$/,
|
|
7237
|
-
|
|
7381
|
+
US_ID_RE2
|
|
7238
7382
|
);
|
|
7239
7383
|
const { definitions: brIds, refsByDefinition: brToAc } = extractTableDefinitionRefs(
|
|
7240
7384
|
businessRulesText,
|
|
@@ -8290,15 +8434,15 @@ var import_node_path28 = __toESM(require("path"), 1);
|
|
|
8290
8434
|
var import_promises29 = require("fs/promises");
|
|
8291
8435
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
8292
8436
|
var ID_PATTERNS = {
|
|
8293
|
-
us: /^US-\d{4}
|
|
8294
|
-
ac: /^AC-\d{4}
|
|
8295
|
-
br: /^BR-\d{4}
|
|
8296
|
-
ex: /^EX-\d{4}
|
|
8437
|
+
us: /^US-\d{4}(?:-\d{4})?$/,
|
|
8438
|
+
ac: /^AC-\d{4}(?:-\d{4})?$/,
|
|
8439
|
+
br: /^BR-\d{4}(?:-\d{4})?$/,
|
|
8440
|
+
ex: /^EX-\d{4}(?:-\d{4})?$/
|
|
8297
8441
|
};
|
|
8298
8442
|
var V1421_REFS = {
|
|
8299
|
-
ac: /\bAC-\d{4}
|
|
8300
|
-
br: /\bBR-\d{4}
|
|
8301
|
-
ex: /\bEX-\d{4}
|
|
8443
|
+
ac: /\bAC-\d{4}(?:-\d{4})?\b/gi,
|
|
8444
|
+
br: /\bBR-\d{4}(?:-\d{4})?\b/gi,
|
|
8445
|
+
ex: /\bEX-\d{4}(?:-\d{4})?\b/gi
|
|
8302
8446
|
};
|
|
8303
8447
|
async function validateLayerCoverage(root, config) {
|
|
8304
8448
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -8571,11 +8715,11 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
8571
8715
|
const ids = /* @__PURE__ */ new Set();
|
|
8572
8716
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
8573
8717
|
for (const line of lines) {
|
|
8574
|
-
const headingMatch = /^##\s*(AC-\d{4})\b/i.exec(line.trim());
|
|
8718
|
+
const headingMatch = /^##\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line.trim());
|
|
8575
8719
|
if (headingMatch?.[1]) {
|
|
8576
8720
|
ids.add(headingMatch[1].toUpperCase());
|
|
8577
8721
|
}
|
|
8578
|
-
const commentMatch = /^\s*#\s*(AC-\d{4})\b/i.exec(line);
|
|
8722
|
+
const commentMatch = /^\s*#\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line);
|
|
8579
8723
|
if (commentMatch?.[1]) {
|
|
8580
8724
|
ids.add(commentMatch[1].toUpperCase());
|
|
8581
8725
|
}
|
|
@@ -8595,8 +8739,8 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
8595
8739
|
function parseDefinitionRefs(text, prefix, refPattern, options = {}) {
|
|
8596
8740
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
8597
8741
|
const refsById = /* @__PURE__ */ new Map();
|
|
8598
|
-
const idPattern = new RegExp(`^${prefix}-\\d{4}
|
|
8599
|
-
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4})\\b`, "i");
|
|
8742
|
+
const idPattern = new RegExp(`^${prefix}-\\d{4}(?:-\\d{4})?$`);
|
|
8743
|
+
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4}(?:-\\d{4})?)\\b`, "i");
|
|
8600
8744
|
const referenceColumns = new Set(
|
|
8601
8745
|
(options.referenceColumns ?? []).map((column) => normalizeColumnName(column))
|
|
8602
8746
|
);
|
|
@@ -8997,7 +9141,7 @@ var US_DOWNSTREAM_RE = /\b(?:AC|BR|EX|TC)-\d{4}\b/g;
|
|
|
8997
9141
|
var AC_DOWNSTREAM_RE = /\b(?:BR|EX|TC)-\d{4}\b/g;
|
|
8998
9142
|
var BR_DOWNSTREAM_RE = /\b(?:EX|TC)-\d{4}\b/g;
|
|
8999
9143
|
var CAP_ID_RE = /^CAP-\d{4}$/;
|
|
9000
|
-
var
|
|
9144
|
+
var US_ID_RE3 = /^US-\d{4}$/;
|
|
9001
9145
|
var AC_ID_RE4 = /^AC-\d{4}$/;
|
|
9002
9146
|
var BR_OR_AC_ID_RE = /^(?:BR|AC)-\d{4}$/;
|
|
9003
9147
|
var EX_ID_RE2 = /^EX-\d{4}$/;
|
|
@@ -9036,7 +9180,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
9036
9180
|
...await validateMarkdownParentFormat(entry.userStoriesPath, "US", CAP_ID_RE, "CAP")
|
|
9037
9181
|
);
|
|
9038
9182
|
issues.push(
|
|
9039
|
-
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC",
|
|
9183
|
+
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE3, "US")
|
|
9040
9184
|
);
|
|
9041
9185
|
issues.push(
|
|
9042
9186
|
...await validateMarkdownParentFormat(entry.businessRulesPath, "BR", AC_ID_RE4, "AC")
|
|
@@ -10348,6 +10492,100 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
|
|
|
10348
10492
|
)
|
|
10349
10493
|
);
|
|
10350
10494
|
}
|
|
10495
|
+
const renderIssues = await validateRenderEvidenceScreens(
|
|
10496
|
+
root,
|
|
10497
|
+
config,
|
|
10498
|
+
evidenceJsonPath,
|
|
10499
|
+
uiFidelity.screens
|
|
10500
|
+
);
|
|
10501
|
+
issues.push(...renderIssues);
|
|
10502
|
+
return issues;
|
|
10503
|
+
}
|
|
10504
|
+
async function validateRenderEvidenceScreens(root, config, evidenceJsonPath, screens) {
|
|
10505
|
+
const issues = [];
|
|
10506
|
+
const hasAnyRenderEvidence = screens.some((screen) => screen.renders.length > 0);
|
|
10507
|
+
if (!hasAnyRenderEvidence) {
|
|
10508
|
+
return issues;
|
|
10509
|
+
}
|
|
10510
|
+
const qualityProfile = config.uiux?.qualityProfile ?? "default";
|
|
10511
|
+
for (const screen of screens) {
|
|
10512
|
+
if (screen.renders.length === 0) {
|
|
10513
|
+
continue;
|
|
10514
|
+
}
|
|
10515
|
+
const viewports = new Set(screen.renders.map((render) => render.viewport));
|
|
10516
|
+
const missingDefaultViewports = DEFAULT_RENDER_VIEWPORTS.filter(
|
|
10517
|
+
(viewport) => !viewports.has(viewport)
|
|
10518
|
+
);
|
|
10519
|
+
const allSkipped = screen.renders.every((render) => render.status === "skipped");
|
|
10520
|
+
for (const render of screen.renders) {
|
|
10521
|
+
if (render.status !== "captured") {
|
|
10522
|
+
continue;
|
|
10523
|
+
}
|
|
10524
|
+
const invalidPaths = [
|
|
10525
|
+
{ label: "imagePath", value: render.imagePath },
|
|
10526
|
+
{ label: "htmlPath", value: render.htmlPath }
|
|
10527
|
+
].filter((entry) => looksLikeInlineRenderPayload(entry.value));
|
|
10528
|
+
if (invalidPaths.length > 0) {
|
|
10529
|
+
issues.push(
|
|
10530
|
+
issue(
|
|
10531
|
+
"QFAI-PROT-244",
|
|
10532
|
+
`QFAI-PROT-244: render evidence must be path-only. route=${screen.route}, viewport=${render.viewport}, invalid=${invalidPaths.map((entry) => entry.label).join("|")}`,
|
|
10533
|
+
"error",
|
|
10534
|
+
evidenceJsonPath,
|
|
10535
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
10536
|
+
[
|
|
10537
|
+
`route=${screen.route}`,
|
|
10538
|
+
`viewport=${render.viewport}`,
|
|
10539
|
+
...invalidPaths.map((entry) => `artifact=${entry.label}`)
|
|
10540
|
+
],
|
|
10541
|
+
"change",
|
|
10542
|
+
"imagePath/htmlPath \u306B\u306F\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9\u306E\u307F\u3092\u4FDD\u5B58\u3057\u3001data URI \u3084 HTML \u672C\u6587\u3092 JSON \u306B\u57CB\u3081\u8FBC\u307E\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002"
|
|
10543
|
+
)
|
|
10544
|
+
);
|
|
10545
|
+
continue;
|
|
10546
|
+
}
|
|
10547
|
+
const missingArtifacts = await collectMissingRenderArtifacts(root, render);
|
|
10548
|
+
if (missingArtifacts.length > 0) {
|
|
10549
|
+
issues.push(
|
|
10550
|
+
issue(
|
|
10551
|
+
"QFAI-PROT-244",
|
|
10552
|
+
`QFAI-PROT-244: captured render artifact is missing. route=${screen.route}, viewport=${render.viewport}, missing=${missingArtifacts.join("|")}`,
|
|
10553
|
+
"error",
|
|
10554
|
+
evidenceJsonPath,
|
|
10555
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
10556
|
+
[
|
|
10557
|
+
`route=${screen.route}`,
|
|
10558
|
+
`viewport=${render.viewport}`,
|
|
10559
|
+
...missingArtifacts.map((artifact) => `artifact=${artifact}`)
|
|
10560
|
+
],
|
|
10561
|
+
"change",
|
|
10562
|
+
"render capture \u3092\u518D\u5B9F\u884C\u3057\u3001screenshot \u3068 HTML snapshot \u306E\u4E21\u65B9\u304C\u4FDD\u5B58\u3055\u308C\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10563
|
+
)
|
|
10564
|
+
);
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
if (missingDefaultViewports.length === 0 && !allSkipped) {
|
|
10568
|
+
continue;
|
|
10569
|
+
}
|
|
10570
|
+
const severity = qualityProfile === "default" ? "warning" : "error";
|
|
10571
|
+
const reason = allSkipped ? "all renders are skipped" : `missing default viewports=${missingDefaultViewports.join("|")}`;
|
|
10572
|
+
issues.push(
|
|
10573
|
+
issue(
|
|
10574
|
+
"QFAI-PROT-245",
|
|
10575
|
+
`QFAI-PROT-245: render coverage is incomplete for ${screen.route}. ${reason}. qualityProfile=${qualityProfile}`,
|
|
10576
|
+
severity,
|
|
10577
|
+
evidenceJsonPath,
|
|
10578
|
+
"prototypingEvidence.renderCoverage",
|
|
10579
|
+
[
|
|
10580
|
+
`route=${screen.route}`,
|
|
10581
|
+
...missingDefaultViewports.map((viewport) => `viewport=${viewport}`),
|
|
10582
|
+
`qualityProfile=${qualityProfile}`
|
|
10583
|
+
],
|
|
10584
|
+
"change",
|
|
10585
|
+
allSkipped ? "\u5C11\u306A\u304F\u3068\u3082 desktop/mobile \u306E\u3044\u305A\u308C\u304B\u3067 captured \u307E\u305F\u306F failed \u306E\u660E\u793A\u7684\u306A render outcome \u3092\u6B8B\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "desktop/mobile \u306E default viewport \u3092\u63C3\u3048\u308B\u304B\u3001profile \u8A2D\u5B9A\u3068 scope \u3092\u898B\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10586
|
+
)
|
|
10587
|
+
);
|
|
10588
|
+
}
|
|
10351
10589
|
return issues;
|
|
10352
10590
|
}
|
|
10353
10591
|
function formatUiFidelityMismatch(mismatch) {
|
|
@@ -10560,6 +10798,10 @@ function normalizeUiFidelityScreen(value) {
|
|
|
10560
10798
|
if (!mockPaths.ok) {
|
|
10561
10799
|
return mockPaths;
|
|
10562
10800
|
}
|
|
10801
|
+
const renders = normalizeRenderEntries(value.renders);
|
|
10802
|
+
if (!renders.ok) {
|
|
10803
|
+
return renders;
|
|
10804
|
+
}
|
|
10563
10805
|
return {
|
|
10564
10806
|
ok: true,
|
|
10565
10807
|
value: {
|
|
@@ -10570,10 +10812,105 @@ function normalizeUiFidelityScreen(value) {
|
|
|
10570
10812
|
...normalizeOptionalMissingBlock(value.missing),
|
|
10571
10813
|
...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
|
|
10572
10814
|
observed: observed.value,
|
|
10573
|
-
mockPaths: mockPaths.value
|
|
10815
|
+
mockPaths: mockPaths.value,
|
|
10816
|
+
renders: renders.value
|
|
10574
10817
|
}
|
|
10575
10818
|
};
|
|
10576
10819
|
}
|
|
10820
|
+
function normalizeRenderEntries(value) {
|
|
10821
|
+
if (value === void 0) {
|
|
10822
|
+
return { ok: true, value: [] };
|
|
10823
|
+
}
|
|
10824
|
+
if (!Array.isArray(value)) {
|
|
10825
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders` must be an array" };
|
|
10826
|
+
}
|
|
10827
|
+
const renders = [];
|
|
10828
|
+
for (const entry of value) {
|
|
10829
|
+
const normalized = normalizeRenderEntry(entry);
|
|
10830
|
+
if (!normalized.ok) {
|
|
10831
|
+
return normalized;
|
|
10832
|
+
}
|
|
10833
|
+
renders.push(normalized.value);
|
|
10834
|
+
}
|
|
10835
|
+
return { ok: true, value: renders };
|
|
10836
|
+
}
|
|
10837
|
+
function normalizeRenderEntry(value) {
|
|
10838
|
+
if (!isRecord5(value)) {
|
|
10839
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[]` must be objects" };
|
|
10840
|
+
}
|
|
10841
|
+
if (typeof value.viewport !== "string" || value.viewport.trim().length === 0) {
|
|
10842
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[].viewport` is required" };
|
|
10843
|
+
}
|
|
10844
|
+
if (!isNonNegativeInteger(value.width) || !isNonNegativeInteger(value.height) || value.width === 0 || value.height === 0) {
|
|
10845
|
+
return {
|
|
10846
|
+
ok: false,
|
|
10847
|
+
reason: "`uiFidelity.screens[].renders[]` requires positive integers for width/height"
|
|
10848
|
+
};
|
|
10849
|
+
}
|
|
10850
|
+
const viewport = value.viewport.trim();
|
|
10851
|
+
const width = value.width;
|
|
10852
|
+
const height = value.height;
|
|
10853
|
+
const status = typeof value.status === "string" ? value.status.trim().toLowerCase() : "";
|
|
10854
|
+
if (status === "captured") {
|
|
10855
|
+
if (typeof value.imagePath !== "string" || value.imagePath.trim().length === 0 || typeof value.htmlPath !== "string" || value.htmlPath.trim().length === 0) {
|
|
10856
|
+
return {
|
|
10857
|
+
ok: false,
|
|
10858
|
+
reason: "`captured` render entries require imagePath and htmlPath"
|
|
10859
|
+
};
|
|
10860
|
+
}
|
|
10861
|
+
return {
|
|
10862
|
+
ok: true,
|
|
10863
|
+
value: {
|
|
10864
|
+
viewport,
|
|
10865
|
+
status: "captured",
|
|
10866
|
+
width,
|
|
10867
|
+
height,
|
|
10868
|
+
imagePath: value.imagePath.trim(),
|
|
10869
|
+
htmlPath: value.htmlPath.trim()
|
|
10870
|
+
}
|
|
10871
|
+
};
|
|
10872
|
+
}
|
|
10873
|
+
if (status === "skipped") {
|
|
10874
|
+
if (typeof value.skippedReason !== "string" || value.skippedReason.trim().length === 0) {
|
|
10875
|
+
return {
|
|
10876
|
+
ok: false,
|
|
10877
|
+
reason: "`skipped` render entries require skippedReason"
|
|
10878
|
+
};
|
|
10879
|
+
}
|
|
10880
|
+
return {
|
|
10881
|
+
ok: true,
|
|
10882
|
+
value: {
|
|
10883
|
+
viewport,
|
|
10884
|
+
status: "skipped",
|
|
10885
|
+
width,
|
|
10886
|
+
height,
|
|
10887
|
+
skippedReason: value.skippedReason.trim()
|
|
10888
|
+
}
|
|
10889
|
+
};
|
|
10890
|
+
}
|
|
10891
|
+
if (status === "failed") {
|
|
10892
|
+
if (typeof value.error !== "string" || value.error.trim().length === 0) {
|
|
10893
|
+
return {
|
|
10894
|
+
ok: false,
|
|
10895
|
+
reason: "`failed` render entries require error"
|
|
10896
|
+
};
|
|
10897
|
+
}
|
|
10898
|
+
return {
|
|
10899
|
+
ok: true,
|
|
10900
|
+
value: {
|
|
10901
|
+
viewport,
|
|
10902
|
+
status: "failed",
|
|
10903
|
+
width,
|
|
10904
|
+
height,
|
|
10905
|
+
error: value.error.trim()
|
|
10906
|
+
}
|
|
10907
|
+
};
|
|
10908
|
+
}
|
|
10909
|
+
return {
|
|
10910
|
+
ok: false,
|
|
10911
|
+
reason: "`uiFidelity.screens[].renders[].status` must be captured|skipped|failed"
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10577
10914
|
function normalizeUiFidelityExpected(value) {
|
|
10578
10915
|
if (!isRecord5(value)) {
|
|
10579
10916
|
return {
|
|
@@ -10797,6 +11134,22 @@ function normalizeOptionalMissingBlock(value) {
|
|
|
10797
11134
|
function isRecord5(value) {
|
|
10798
11135
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10799
11136
|
}
|
|
11137
|
+
async function collectMissingRenderArtifacts(root, render) {
|
|
11138
|
+
const missing = [];
|
|
11139
|
+
const candidates = [
|
|
11140
|
+
{ label: "imagePath", target: render.imagePath },
|
|
11141
|
+
{ label: "htmlPath", target: render.htmlPath }
|
|
11142
|
+
];
|
|
11143
|
+
for (const candidate of candidates) {
|
|
11144
|
+
const resolved = import_node_path34.default.isAbsolute(candidate.target) ? candidate.target : import_node_path34.default.resolve(root, candidate.target);
|
|
11145
|
+
try {
|
|
11146
|
+
await (0, import_promises33.access)(resolved);
|
|
11147
|
+
} catch {
|
|
11148
|
+
missing.push(candidate.label);
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
return missing;
|
|
11152
|
+
}
|
|
10800
11153
|
function isInteger(value) {
|
|
10801
11154
|
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
10802
11155
|
}
|
|
@@ -11145,7 +11498,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
11145
11498
|
}
|
|
11146
11499
|
function flattenTokens(obj, prefix, target, errors) {
|
|
11147
11500
|
for (const [key, value] of Object.entries(obj)) {
|
|
11148
|
-
const
|
|
11501
|
+
const path55 = `${prefix}.${key}`;
|
|
11149
11502
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11150
11503
|
const record2 = value;
|
|
11151
11504
|
if ("$value" in record2) {
|
|
@@ -11161,9 +11514,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
11161
11514
|
if (typeof record2.platform === "string") {
|
|
11162
11515
|
token.platform = record2.platform;
|
|
11163
11516
|
}
|
|
11164
|
-
target.set(
|
|
11517
|
+
target.set(path55, token);
|
|
11165
11518
|
} else {
|
|
11166
|
-
flattenTokens(record2,
|
|
11519
|
+
flattenTokens(record2, path55, target, errors);
|
|
11167
11520
|
}
|
|
11168
11521
|
}
|
|
11169
11522
|
}
|
|
@@ -11173,44 +11526,44 @@ function resolveAllReferences(result) {
|
|
|
11173
11526
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
11174
11527
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
11175
11528
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
11176
|
-
for (const [
|
|
11177
|
-
resolveTokenRef(
|
|
11529
|
+
for (const [path55] of allTokens) {
|
|
11530
|
+
resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
11178
11531
|
}
|
|
11179
11532
|
}
|
|
11180
|
-
function resolveTokenRef(
|
|
11181
|
-
if (result.resolved.has(
|
|
11182
|
-
return result.resolved.get(
|
|
11533
|
+
function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
11534
|
+
if (result.resolved.has(path55)) {
|
|
11535
|
+
return result.resolved.get(path55);
|
|
11183
11536
|
}
|
|
11184
11537
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
11185
11538
|
result.errors.push({
|
|
11186
|
-
message: `Max reference depth exceeded at: ${
|
|
11187
|
-
path:
|
|
11539
|
+
message: `Max reference depth exceeded at: ${path55}`,
|
|
11540
|
+
path: path55
|
|
11188
11541
|
});
|
|
11189
11542
|
return void 0;
|
|
11190
11543
|
}
|
|
11191
|
-
if (visited.has(
|
|
11544
|
+
if (visited.has(path55)) {
|
|
11192
11545
|
result.errors.push({
|
|
11193
|
-
message: `Circular reference detected: ${
|
|
11194
|
-
path:
|
|
11546
|
+
message: `Circular reference detected: ${path55}`,
|
|
11547
|
+
path: path55
|
|
11195
11548
|
});
|
|
11196
11549
|
return void 0;
|
|
11197
11550
|
}
|
|
11198
|
-
const token = allTokens.get(
|
|
11551
|
+
const token = allTokens.get(path55);
|
|
11199
11552
|
if (!token) {
|
|
11200
11553
|
return void 0;
|
|
11201
11554
|
}
|
|
11202
11555
|
if (typeof token.$value !== "string") {
|
|
11203
11556
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11204
|
-
result.resolved.set(
|
|
11557
|
+
result.resolved.set(path55, rawValue2);
|
|
11205
11558
|
return rawValue2;
|
|
11206
11559
|
}
|
|
11207
11560
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11208
11561
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11209
11562
|
if (refs.length === 0) {
|
|
11210
|
-
result.resolved.set(
|
|
11563
|
+
result.resolved.set(path55, rawValue);
|
|
11211
11564
|
return rawValue;
|
|
11212
11565
|
}
|
|
11213
|
-
visited.add(
|
|
11566
|
+
visited.add(path55);
|
|
11214
11567
|
let resolved = rawValue;
|
|
11215
11568
|
for (const ref of refs) {
|
|
11216
11569
|
const refPath = ref[1];
|
|
@@ -11218,8 +11571,8 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11218
11571
|
const refToken = allTokens.get(refPath);
|
|
11219
11572
|
if (!refToken) {
|
|
11220
11573
|
result.errors.push({
|
|
11221
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11222
|
-
path:
|
|
11574
|
+
message: `Unresolved token reference: {${refPath}} at ${path55}`,
|
|
11575
|
+
path: path55
|
|
11223
11576
|
});
|
|
11224
11577
|
continue;
|
|
11225
11578
|
}
|
|
@@ -11228,7 +11581,7 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11228
11581
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11229
11582
|
}
|
|
11230
11583
|
}
|
|
11231
|
-
result.resolved.set(
|
|
11584
|
+
result.resolved.set(path55, resolved);
|
|
11232
11585
|
return resolved;
|
|
11233
11586
|
}
|
|
11234
11587
|
function stringifyTokenValue(value) {
|
|
@@ -14340,6 +14693,7 @@ async function validateNavigationFlow(root, config) {
|
|
|
14340
14693
|
|
|
14341
14694
|
// src/core/validators/renderCritique.ts
|
|
14342
14695
|
var import_node_path49 = __toESM(require("path"), 1);
|
|
14696
|
+
var import_promises48 = require("fs/promises");
|
|
14343
14697
|
var import_fast_glob9 = __toESM(require("fast-glob"), 1);
|
|
14344
14698
|
var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
|
|
14345
14699
|
var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
|
|
@@ -14371,6 +14725,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14371
14725
|
if (!hasDdp) return issues;
|
|
14372
14726
|
const skillsDir = import_node_path49.default.join(root, config.paths.skillsDir).replace(/\\/g, "/");
|
|
14373
14727
|
const evidenceDir = import_node_path49.default.join(root, ".qfai", "evidence").replace(/\\/g, "/");
|
|
14728
|
+
const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
|
|
14374
14729
|
const skillPromptPattern = import_node_path49.default.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
14375
14730
|
const skillFiles = await (0, import_fast_glob9.default)(skillPromptPattern, { dot: true });
|
|
14376
14731
|
const evidencePattern = import_node_path49.default.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
|
|
@@ -14410,7 +14765,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14410
14765
|
}
|
|
14411
14766
|
}
|
|
14412
14767
|
const allEvidenceContent = await collectContent(evidenceFiles);
|
|
14413
|
-
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
|
|
14768
|
+
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("desktop")) {
|
|
14414
14769
|
issues.push(
|
|
14415
14770
|
issue(
|
|
14416
14771
|
"QFAI-CRIT-003",
|
|
@@ -14424,7 +14779,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14424
14779
|
)
|
|
14425
14780
|
);
|
|
14426
14781
|
}
|
|
14427
|
-
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
|
|
14782
|
+
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("mobile")) {
|
|
14428
14783
|
issues.push(
|
|
14429
14784
|
issue(
|
|
14430
14785
|
"QFAI-CRIT-004",
|
|
@@ -14585,9 +14940,49 @@ async function collectContent(files) {
|
|
|
14585
14940
|
}
|
|
14586
14941
|
return contents.join("\n---\n");
|
|
14587
14942
|
}
|
|
14943
|
+
async function collectRenderEvidenceViewports(root) {
|
|
14944
|
+
const prototypingJsonPath = import_node_path49.default.join(root, ".qfai", "evidence", "prototyping.json");
|
|
14945
|
+
try {
|
|
14946
|
+
const raw = await (0, import_promises48.readFile)(prototypingJsonPath, "utf-8");
|
|
14947
|
+
const parsed = JSON.parse(raw);
|
|
14948
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14949
|
+
return /* @__PURE__ */ new Set();
|
|
14950
|
+
}
|
|
14951
|
+
const uiFidelity = parsed.uiFidelity;
|
|
14952
|
+
if (!uiFidelity || typeof uiFidelity !== "object" || Array.isArray(uiFidelity)) {
|
|
14953
|
+
return /* @__PURE__ */ new Set();
|
|
14954
|
+
}
|
|
14955
|
+
const screens = uiFidelity.screens;
|
|
14956
|
+
if (!Array.isArray(screens)) {
|
|
14957
|
+
return /* @__PURE__ */ new Set();
|
|
14958
|
+
}
|
|
14959
|
+
const viewports = /* @__PURE__ */ new Set();
|
|
14960
|
+
for (const screen of screens) {
|
|
14961
|
+
if (!screen || typeof screen !== "object" || Array.isArray(screen)) {
|
|
14962
|
+
continue;
|
|
14963
|
+
}
|
|
14964
|
+
const renders = screen.renders;
|
|
14965
|
+
if (!Array.isArray(renders)) {
|
|
14966
|
+
continue;
|
|
14967
|
+
}
|
|
14968
|
+
for (const render of renders) {
|
|
14969
|
+
if (!render || typeof render !== "object" || Array.isArray(render)) {
|
|
14970
|
+
continue;
|
|
14971
|
+
}
|
|
14972
|
+
const viewport = render.viewport;
|
|
14973
|
+
if (typeof viewport === "string" && viewport.trim().length > 0) {
|
|
14974
|
+
viewports.add(viewport.trim().toLowerCase());
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
}
|
|
14978
|
+
return viewports;
|
|
14979
|
+
} catch {
|
|
14980
|
+
return /* @__PURE__ */ new Set();
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14588
14983
|
|
|
14589
14984
|
// src/core/validators/designFidelity.ts
|
|
14590
|
-
var
|
|
14985
|
+
var import_promises49 = require("fs/promises");
|
|
14591
14986
|
var import_node_path50 = __toESM(require("path"), 1);
|
|
14592
14987
|
var import_fast_glob10 = __toESM(require("fast-glob"), 1);
|
|
14593
14988
|
var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
|
|
@@ -14620,7 +15015,7 @@ async function validateDesignFidelity(root, config) {
|
|
|
14620
15015
|
for (const filePath of allFiles) {
|
|
14621
15016
|
let content;
|
|
14622
15017
|
try {
|
|
14623
|
-
content = await (0,
|
|
15018
|
+
content = await (0, import_promises49.readFile)(filePath, "utf-8");
|
|
14624
15019
|
} catch {
|
|
14625
15020
|
continue;
|
|
14626
15021
|
}
|
|
@@ -15243,6 +15638,252 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
15243
15638
|
return issues;
|
|
15244
15639
|
}
|
|
15245
15640
|
|
|
15641
|
+
// src/core/validators/designAudit.ts
|
|
15642
|
+
var import_promises50 = require("fs/promises");
|
|
15643
|
+
var import_node_path52 = __toESM(require("path"), 1);
|
|
15644
|
+
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
15645
|
+
function resolveAuditConfig(config) {
|
|
15646
|
+
const audit = config.uiux?.audit;
|
|
15647
|
+
const profile = config.uiux?.qualityProfile ?? "default";
|
|
15648
|
+
return {
|
|
15649
|
+
enabled: audit?.enabled ?? true,
|
|
15650
|
+
slopDetection: audit?.slopDetection ?? true,
|
|
15651
|
+
qualityProfile: profile,
|
|
15652
|
+
maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
|
|
15653
|
+
maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
|
|
15654
|
+
maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
|
|
15655
|
+
};
|
|
15656
|
+
}
|
|
15657
|
+
function mapSeverity(tier, profile, category) {
|
|
15658
|
+
if (tier === 1) return "error";
|
|
15659
|
+
if (tier === 2) return profile === "strict" ? "error" : "warning";
|
|
15660
|
+
if (profile === "default") {
|
|
15661
|
+
return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
|
|
15662
|
+
}
|
|
15663
|
+
return "warning";
|
|
15664
|
+
}
|
|
15665
|
+
function findingToIssue(finding, profile, rulePrefix = "audit") {
|
|
15666
|
+
const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
|
|
15667
|
+
return issue(
|
|
15668
|
+
finding.ruleId,
|
|
15669
|
+
finding.message,
|
|
15670
|
+
severity,
|
|
15671
|
+
finding.file,
|
|
15672
|
+
`${rulePrefix}.${finding.dimension}`,
|
|
15673
|
+
finding.evidence.length > 0 ? finding.evidence : void 0,
|
|
15674
|
+
"compatibility",
|
|
15675
|
+
finding.guidance
|
|
15676
|
+
);
|
|
15677
|
+
}
|
|
15678
|
+
function extractSection2(content, heading) {
|
|
15679
|
+
const idx = content.indexOf(heading);
|
|
15680
|
+
if (idx === -1) return null;
|
|
15681
|
+
const start = idx + heading.length;
|
|
15682
|
+
const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
|
|
15683
|
+
const rest = content.slice(start);
|
|
15684
|
+
const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
|
|
15685
|
+
const nextHeadingMatch = headingPattern.exec(rest);
|
|
15686
|
+
const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
15687
|
+
return sectionContent.trim() || null;
|
|
15688
|
+
}
|
|
15689
|
+
function checkCtaHierarchy(content, auditConfig, file) {
|
|
15690
|
+
const findings = [];
|
|
15691
|
+
const ctaSection = extractSection2(content, "### CTA Hierarchy");
|
|
15692
|
+
if (!ctaSection) return findings;
|
|
15693
|
+
const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
|
|
15694
|
+
const primaryCount = primaryLines.length;
|
|
15695
|
+
if (primaryCount === 0) {
|
|
15696
|
+
findings.push({
|
|
15697
|
+
ruleId: "QFAI-AUD-001",
|
|
15698
|
+
dimension: "visualHierarchy",
|
|
15699
|
+
severityTier: 1,
|
|
15700
|
+
message: "No primary CTA defined in CTA Hierarchy",
|
|
15701
|
+
why: "Every UI screen needs a clear primary action to guide users",
|
|
15702
|
+
evidence: [],
|
|
15703
|
+
guidance: "Define at least one primary CTA in the CTA Hierarchy section",
|
|
15704
|
+
file
|
|
15705
|
+
});
|
|
15706
|
+
}
|
|
15707
|
+
if (primaryCount > auditConfig.maxPrimaryCtas) {
|
|
15708
|
+
findings.push({
|
|
15709
|
+
ruleId: "QFAI-AUD-020",
|
|
15710
|
+
dimension: "visualHierarchy",
|
|
15711
|
+
severityTier: 2,
|
|
15712
|
+
message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
|
|
15713
|
+
why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
|
|
15714
|
+
evidence: primaryLines.map((l) => l.trim()),
|
|
15715
|
+
guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
|
|
15716
|
+
file
|
|
15717
|
+
});
|
|
15718
|
+
}
|
|
15719
|
+
return findings;
|
|
15720
|
+
}
|
|
15721
|
+
var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
|
|
15722
|
+
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
15723
|
+
const findings = [];
|
|
15724
|
+
const configuredDir = cfg.uiux?.designTokensDir;
|
|
15725
|
+
const tokensDir = configuredDir ? import_node_path52.default.resolve(root, configuredDir) : import_node_path52.default.join(root, cfg.paths.contractsDir, "design");
|
|
15726
|
+
let hasTokenFiles = false;
|
|
15727
|
+
try {
|
|
15728
|
+
const entries = await (0, import_promises50.readdir)(tokensDir);
|
|
15729
|
+
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
15730
|
+
} catch {
|
|
15731
|
+
return findings;
|
|
15732
|
+
}
|
|
15733
|
+
if (!hasTokenFiles) return findings;
|
|
15734
|
+
const contractsUiDir = import_node_path52.default.join(root, cfg.paths.contractsDir, "ui");
|
|
15735
|
+
let htmlFiles = [];
|
|
15736
|
+
try {
|
|
15737
|
+
const entries = await (0, import_promises50.readdir)(contractsUiDir);
|
|
15738
|
+
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
15739
|
+
} catch {
|
|
15740
|
+
return findings;
|
|
15741
|
+
}
|
|
15742
|
+
let rawCount = 0;
|
|
15743
|
+
const sampleLiterals = [];
|
|
15744
|
+
for (const htmlFile of htmlFiles) {
|
|
15745
|
+
const content = await readSafe(import_node_path52.default.join(contractsUiDir, htmlFile));
|
|
15746
|
+
if (!content) continue;
|
|
15747
|
+
const matches = content.match(RAW_COLOR_RE);
|
|
15748
|
+
if (matches) {
|
|
15749
|
+
rawCount += matches.length;
|
|
15750
|
+
for (const m of matches) {
|
|
15751
|
+
if (sampleLiterals.length < 10) {
|
|
15752
|
+
sampleLiterals.push(m.toLowerCase());
|
|
15753
|
+
}
|
|
15754
|
+
}
|
|
15755
|
+
}
|
|
15756
|
+
}
|
|
15757
|
+
if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
|
|
15758
|
+
findings.push({
|
|
15759
|
+
ruleId: "QFAI-AUD-004",
|
|
15760
|
+
dimension: "tokenDiscipline",
|
|
15761
|
+
severityTier: 1,
|
|
15762
|
+
message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
|
|
15763
|
+
why: "Raw color values bypass design tokens, causing visual inconsistency",
|
|
15764
|
+
evidence: sampleLiterals,
|
|
15765
|
+
guidance: "Replace raw color literals with design token references"
|
|
15766
|
+
});
|
|
15767
|
+
}
|
|
15768
|
+
return findings;
|
|
15769
|
+
}
|
|
15770
|
+
function deduplicateFindings(issues, maxPerRule) {
|
|
15771
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15772
|
+
const result = [];
|
|
15773
|
+
for (const iss of issues) {
|
|
15774
|
+
const count = counts.get(iss.code) ?? 0;
|
|
15775
|
+
if (count < maxPerRule) {
|
|
15776
|
+
result.push(iss);
|
|
15777
|
+
}
|
|
15778
|
+
counts.set(iss.code, count + 1);
|
|
15779
|
+
}
|
|
15780
|
+
for (const [code, count] of counts) {
|
|
15781
|
+
if (count > maxPerRule) {
|
|
15782
|
+
result.push({
|
|
15783
|
+
code,
|
|
15784
|
+
severity: "info",
|
|
15785
|
+
category: "compatibility",
|
|
15786
|
+
message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
|
|
15787
|
+
rule: `audit.dedup.${code}`
|
|
15788
|
+
});
|
|
15789
|
+
}
|
|
15790
|
+
}
|
|
15791
|
+
return result;
|
|
15792
|
+
}
|
|
15793
|
+
async function validateDesignAudit(root, config) {
|
|
15794
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15795
|
+
if (!auditConfig.enabled) return [];
|
|
15796
|
+
const discussionDir = import_node_path52.default.join(root, config.paths.discussionDir);
|
|
15797
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15798
|
+
if (!packRoot) return [];
|
|
15799
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15800
|
+
if (!uiBearing) return [];
|
|
15801
|
+
const storyPath = import_node_path52.default.join(packRoot, "03_Story-Workshop.md");
|
|
15802
|
+
const content = await readSafe(storyPath);
|
|
15803
|
+
if (!content) return [];
|
|
15804
|
+
const findings = [];
|
|
15805
|
+
findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
|
|
15806
|
+
findings.push(...await checkTokenDrift(root, auditConfig, config));
|
|
15807
|
+
const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
|
|
15808
|
+
return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
|
|
15809
|
+
}
|
|
15810
|
+
|
|
15811
|
+
// src/core/validators/designSlop.ts
|
|
15812
|
+
var import_node_fs2 = require("fs");
|
|
15813
|
+
var import_promises51 = require("fs/promises");
|
|
15814
|
+
var import_node_path53 = __toESM(require("path"), 1);
|
|
15815
|
+
var import_node_url4 = require("url");
|
|
15816
|
+
function isValidSlopPattern(rule) {
|
|
15817
|
+
if (typeof rule !== "object" || rule === null) return false;
|
|
15818
|
+
const r = rule;
|
|
15819
|
+
return typeof r.id === "string" && typeof r.category === "string" && typeof r.tier === "number" && Array.isArray(r.scopes) && typeof r.match === "string" && typeof r.message === "string" && typeof r.guidance === "string";
|
|
15820
|
+
}
|
|
15821
|
+
async function loadSlopPatterns(jsonPath) {
|
|
15822
|
+
const raw = await (0, import_promises51.readFile)(jsonPath, "utf-8");
|
|
15823
|
+
const parsed = JSON.parse(raw);
|
|
15824
|
+
if (!Array.isArray(parsed)) return [];
|
|
15825
|
+
return parsed.filter((r) => isValidSlopPattern(r));
|
|
15826
|
+
}
|
|
15827
|
+
function defaultPatternsPath() {
|
|
15828
|
+
const base = __filename;
|
|
15829
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url4.fileURLToPath)(base) : base;
|
|
15830
|
+
const baseDir = import_node_path53.default.dirname(basePath);
|
|
15831
|
+
const candidates = [
|
|
15832
|
+
import_node_path53.default.join(baseDir, "designSlopPatterns.json"),
|
|
15833
|
+
import_node_path53.default.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
15834
|
+
import_node_path53.default.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
15835
|
+
];
|
|
15836
|
+
for (const c of candidates) {
|
|
15837
|
+
if ((0, import_node_fs2.existsSync)(c)) return c;
|
|
15838
|
+
}
|
|
15839
|
+
return candidates[0];
|
|
15840
|
+
}
|
|
15841
|
+
async function validateDesignSlop(root, config) {
|
|
15842
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15843
|
+
if (!auditConfig.enabled) return [];
|
|
15844
|
+
if (!auditConfig.slopDetection) return [];
|
|
15845
|
+
const discussionDir = import_node_path53.default.join(root, config.paths.discussionDir);
|
|
15846
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15847
|
+
if (!packRoot) return [];
|
|
15848
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15849
|
+
if (!uiBearing) return [];
|
|
15850
|
+
let patterns;
|
|
15851
|
+
try {
|
|
15852
|
+
patterns = await loadSlopPatterns(defaultPatternsPath());
|
|
15853
|
+
} catch {
|
|
15854
|
+
return [];
|
|
15855
|
+
}
|
|
15856
|
+
const findings = [];
|
|
15857
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
15858
|
+
for (const pattern of patterns) {
|
|
15859
|
+
let regex;
|
|
15860
|
+
try {
|
|
15861
|
+
regex = new RegExp(pattern.match, "gi");
|
|
15862
|
+
} catch {
|
|
15863
|
+
continue;
|
|
15864
|
+
}
|
|
15865
|
+
for (const scope of pattern.scopes) {
|
|
15866
|
+
const filePath = import_node_path53.default.join(packRoot, scope);
|
|
15867
|
+
const content = await readSafe(filePath);
|
|
15868
|
+
if (!content) continue;
|
|
15869
|
+
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
15870
|
+
seenRules.add(pattern.id);
|
|
15871
|
+
findings.push({
|
|
15872
|
+
ruleId: pattern.id,
|
|
15873
|
+
dimension: pattern.category,
|
|
15874
|
+
severityTier: pattern.tier,
|
|
15875
|
+
message: pattern.message,
|
|
15876
|
+
why: `Slop pattern "${pattern.id}" matched in ${scope}`,
|
|
15877
|
+
evidence: [],
|
|
15878
|
+
guidance: pattern.guidance,
|
|
15879
|
+
file: scope
|
|
15880
|
+
});
|
|
15881
|
+
}
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15246
15887
|
// src/core/validate.ts
|
|
15247
15888
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
15248
15889
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -15260,7 +15901,9 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
15260
15901
|
() => validateBpApDb(root, config),
|
|
15261
15902
|
() => validateUiDefinitionConsistency(root, config),
|
|
15262
15903
|
() => validateResearchSummary(root, config),
|
|
15263
|
-
() => validateAgentDefinition(root, config)
|
|
15904
|
+
() => validateAgentDefinition(root, config),
|
|
15905
|
+
() => validateDesignAudit(root, config),
|
|
15906
|
+
() => validateDesignSlop(root, config)
|
|
15264
15907
|
];
|
|
15265
15908
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
15266
15909
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -15347,15 +15990,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
15347
15990
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
15348
15991
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
15349
15992
|
async function createReportData(root, validation, configResult) {
|
|
15350
|
-
const resolvedRoot =
|
|
15993
|
+
const resolvedRoot = import_node_path54.default.resolve(root);
|
|
15351
15994
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
15352
15995
|
const config = resolved.config;
|
|
15353
15996
|
const configPath = resolved.configPath;
|
|
15354
15997
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
15355
15998
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
15356
|
-
const apiRoot =
|
|
15357
|
-
const uiRoot =
|
|
15358
|
-
const dbRoot =
|
|
15999
|
+
const apiRoot = import_node_path54.default.join(contractsRoot, "api");
|
|
16000
|
+
const uiRoot = import_node_path54.default.join(contractsRoot, "ui");
|
|
16001
|
+
const dbRoot = import_node_path54.default.join(contractsRoot, "db");
|
|
15359
16002
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
15360
16003
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
15361
16004
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -15683,6 +16326,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
15683
16326
|
lines.push("");
|
|
15684
16327
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
15685
16328
|
lines.push("- [Change Issues](#change-issues)");
|
|
16329
|
+
lines.push("- [Design Audit Findings](#design-audit-findings)");
|
|
16330
|
+
lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
|
|
15686
16331
|
lines.push("- [Change Type](#change-type)");
|
|
15687
16332
|
lines.push("- [Waivers](#waivers)");
|
|
15688
16333
|
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
@@ -15831,6 +16476,46 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
15831
16476
|
lines.push("### Issues");
|
|
15832
16477
|
lines.push("");
|
|
15833
16478
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
16479
|
+
const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
|
|
16480
|
+
if (auditIssues.length > 0) {
|
|
16481
|
+
lines.push("## Design Audit Findings");
|
|
16482
|
+
lines.push("");
|
|
16483
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
16484
|
+
for (const iss of auditIssues) {
|
|
16485
|
+
const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
|
|
16486
|
+
const group = byDimension.get(dim) ?? [];
|
|
16487
|
+
group.push(iss);
|
|
16488
|
+
byDimension.set(dim, group);
|
|
16489
|
+
}
|
|
16490
|
+
for (const [dim, dimIssues] of byDimension) {
|
|
16491
|
+
lines.push(`### ${dim}`);
|
|
16492
|
+
lines.push("");
|
|
16493
|
+
for (const iss of dimIssues) {
|
|
16494
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16495
|
+
}
|
|
16496
|
+
lines.push("");
|
|
16497
|
+
}
|
|
16498
|
+
}
|
|
16499
|
+
const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
|
|
16500
|
+
if (slopIssues.length > 0) {
|
|
16501
|
+
lines.push("## Slop Guardrails Findings");
|
|
16502
|
+
lines.push("");
|
|
16503
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
16504
|
+
for (const iss of slopIssues) {
|
|
16505
|
+
const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
|
|
16506
|
+
const group = byCategory.get(cat) ?? [];
|
|
16507
|
+
group.push(iss);
|
|
16508
|
+
byCategory.set(cat, group);
|
|
16509
|
+
}
|
|
16510
|
+
for (const [cat, catIssues] of byCategory) {
|
|
16511
|
+
lines.push(`### ${cat}`);
|
|
16512
|
+
lines.push("");
|
|
16513
|
+
for (const iss of catIssues) {
|
|
16514
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16515
|
+
}
|
|
16516
|
+
lines.push("");
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
15834
16519
|
lines.push("## Change Type");
|
|
15835
16520
|
lines.push("");
|
|
15836
16521
|
lines.push("### Summary");
|
|
@@ -16202,6 +16887,20 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16202
16887
|
} else {
|
|
16203
16888
|
lines.push("- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
16204
16889
|
}
|
|
16890
|
+
const renderEvidenceIssues = data.issues.filter(
|
|
16891
|
+
(item) => ["QFAI-PROT-101", "QFAI-PROT-244", "QFAI-PROT-245"].includes(item.code)
|
|
16892
|
+
);
|
|
16893
|
+
if (renderEvidenceIssues.length > 0) {
|
|
16894
|
+
lines.push(
|
|
16895
|
+
"- render evidence \u304C\u4E0D\u8DB3\u307E\u305F\u306F\u4E0D\u5B8C\u5168\u3067\u3059\u3002viewport coverage \u3068 artifact path \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
16896
|
+
);
|
|
16897
|
+
lines.push(
|
|
16898
|
+
"- recover: `qfai prototyping --autogen-ui-fidelity --render-evidence --viewports desktop,mobile` \u3092\u5B9F\u884C\u3057\u3001`.qfai/evidence/prototyping.json` \u3068 render bundle \u3092\u66F4\u65B0\u3057\u307E\u3059\u3002"
|
|
16899
|
+
);
|
|
16900
|
+
lines.push(
|
|
16901
|
+
"- why it matters: render evidence \u306F viewport coverage \u3068 missing artifact \u306E\u5207\u308A\u5206\u3051\u306B\u4F7F\u308F\u308C\u3001strict/high profile \u3067\u306F gate \u306B\u5F71\u97FF\u3057\u307E\u3059\u3002"
|
|
16902
|
+
);
|
|
16903
|
+
}
|
|
16205
16904
|
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/18_delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
16206
16905
|
lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`");
|
|
16207
16906
|
return lines.join("\n");
|
|
@@ -16236,7 +16935,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
16236
16935
|
};
|
|
16237
16936
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
16238
16937
|
for (const deltaFile of deltaFiles) {
|
|
16239
|
-
const text = await (0,
|
|
16938
|
+
const text = await (0, import_promises52.readFile)(deltaFile, "utf-8");
|
|
16240
16939
|
const parsed = parseDeltaV1(text);
|
|
16241
16940
|
for (const entry of parsed.entries) {
|
|
16242
16941
|
if (!entry.meta) {
|
|
@@ -16273,7 +16972,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
16273
16972
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
16274
16973
|
}
|
|
16275
16974
|
for (const file of specFiles) {
|
|
16276
|
-
const text = await (0,
|
|
16975
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16277
16976
|
const parsed = parseSpec(text, file);
|
|
16278
16977
|
const specKey = parsed.specId;
|
|
16279
16978
|
if (!specKey) {
|
|
@@ -16310,7 +17009,7 @@ async function collectIds(files) {
|
|
|
16310
17009
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
16311
17010
|
}
|
|
16312
17011
|
for (const file of files) {
|
|
16313
|
-
const text = await (0,
|
|
17012
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16314
17013
|
for (const prefix of ID_PREFIXES) {
|
|
16315
17014
|
const ids = extractIds(text, prefix);
|
|
16316
17015
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -16325,7 +17024,7 @@ async function collectIds(files) {
|
|
|
16325
17024
|
async function collectUpstreamIds(files) {
|
|
16326
17025
|
const ids = /* @__PURE__ */ new Set();
|
|
16327
17026
|
for (const file of files) {
|
|
16328
|
-
const text = await (0,
|
|
17027
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16329
17028
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
16330
17029
|
}
|
|
16331
17030
|
return ids;
|
|
@@ -16346,7 +17045,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
16346
17045
|
}
|
|
16347
17046
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
16348
17047
|
for (const file of targetFiles) {
|
|
16349
|
-
const text = await (0,
|
|
17048
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16350
17049
|
if (pattern.test(text)) {
|
|
16351
17050
|
return true;
|
|
16352
17051
|
}
|
|
@@ -16465,7 +17164,7 @@ function normalizeScSources(root, sources) {
|
|
|
16465
17164
|
async function countScenarios(scenarioFiles) {
|
|
16466
17165
|
let total = 0;
|
|
16467
17166
|
for (const file of scenarioFiles) {
|
|
16468
|
-
const text = await (0,
|
|
17167
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16469
17168
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16470
17169
|
if (!document || errors.length > 0) {
|
|
16471
17170
|
continue;
|
|
@@ -16496,7 +17195,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
16496
17195
|
let totalScenarios = 0;
|
|
16497
17196
|
let e2eCount = 0;
|
|
16498
17197
|
for (const file of scenarioFiles) {
|
|
16499
|
-
const text = await (0,
|
|
17198
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16500
17199
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16501
17200
|
if (!document || errors.length > 0) {
|
|
16502
17201
|
continue;
|
|
@@ -16584,10 +17283,10 @@ function buildHotspots(issues) {
|
|
|
16584
17283
|
async function collectTddCoverage(entries) {
|
|
16585
17284
|
const specs = [];
|
|
16586
17285
|
for (const entry of entries) {
|
|
16587
|
-
const testCasesPath =
|
|
17286
|
+
const testCasesPath = import_node_path54.default.join(entry.dir, "06_Test-Cases.md");
|
|
16588
17287
|
let tcContent;
|
|
16589
17288
|
try {
|
|
16590
|
-
tcContent = await (0,
|
|
17289
|
+
tcContent = await (0, import_promises52.readFile)(testCasesPath, "utf-8");
|
|
16591
17290
|
} catch {
|
|
16592
17291
|
continue;
|
|
16593
17292
|
}
|
|
@@ -16619,10 +17318,10 @@ async function collectTddCoverage(entries) {
|
|
|
16619
17318
|
});
|
|
16620
17319
|
continue;
|
|
16621
17320
|
}
|
|
16622
|
-
const tddListPath =
|
|
17321
|
+
const tddListPath = import_node_path54.default.join(entry.dir, "tdd", "test-list.md");
|
|
16623
17322
|
let tddContent;
|
|
16624
17323
|
try {
|
|
16625
|
-
tddContent = await (0,
|
|
17324
|
+
tddContent = await (0, import_promises52.readFile)(tddListPath, "utf-8");
|
|
16626
17325
|
} catch {
|
|
16627
17326
|
specs.push({
|
|
16628
17327
|
specNumber: entry.specNumber,
|