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.mjs
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
import { access, readFile } from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
+
|
|
6
|
+
// src/core/uiux/renderEvidenceTypes.ts
|
|
7
|
+
var DEFAULT_RENDER_VIEWPORTS = ["desktop", "mobile"];
|
|
8
|
+
function normalizeRenderViewports(viewports) {
|
|
9
|
+
const normalized = Array.isArray(viewports) ? viewports.map((item) => item.trim()).filter((item) => item.length > 0) : [];
|
|
10
|
+
if (normalized.length > 0) {
|
|
11
|
+
return Array.from(new Set(normalized));
|
|
12
|
+
}
|
|
13
|
+
return [...DEFAULT_RENDER_VIEWPORTS];
|
|
14
|
+
}
|
|
15
|
+
function looksLikeInlineRenderPayload(value) {
|
|
16
|
+
const trimmed = value.trim().toLowerCase();
|
|
17
|
+
return trimmed.startsWith("data:image") || trimmed.includes("<html");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/core/config.ts
|
|
5
21
|
var defaultConfig = {
|
|
6
22
|
paths: {
|
|
7
23
|
contractsDir: ".qfai/contracts",
|
|
@@ -451,6 +467,133 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
451
467
|
);
|
|
452
468
|
}
|
|
453
469
|
}
|
|
470
|
+
if (raw.renderEvidence !== void 0) {
|
|
471
|
+
const renderEvidence = normalizeRenderEvidence(raw.renderEvidence, configPath, issues);
|
|
472
|
+
if (renderEvidence) {
|
|
473
|
+
result.renderEvidence = renderEvidence;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (raw.audit !== void 0) {
|
|
477
|
+
const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
|
|
478
|
+
if (audit) {
|
|
479
|
+
result.audit = audit;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
483
|
+
}
|
|
484
|
+
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
485
|
+
if (!isRecord(raw)) {
|
|
486
|
+
issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
const result = {};
|
|
490
|
+
if (raw.enabled !== void 0) {
|
|
491
|
+
if (typeof raw.enabled === "boolean") {
|
|
492
|
+
result.enabled = raw.enabled;
|
|
493
|
+
} else {
|
|
494
|
+
issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (raw.slopDetection !== void 0) {
|
|
498
|
+
if (typeof raw.slopDetection === "boolean") {
|
|
499
|
+
result.slopDetection = raw.slopDetection;
|
|
500
|
+
} else {
|
|
501
|
+
issues.push(
|
|
502
|
+
configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (raw.maxPrimaryCtas !== void 0) {
|
|
507
|
+
if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
|
|
508
|
+
result.maxPrimaryCtas = raw.maxPrimaryCtas;
|
|
509
|
+
} else {
|
|
510
|
+
issues.push(
|
|
511
|
+
configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (raw.maxRawTokenLiteralWarnings !== void 0) {
|
|
516
|
+
if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
|
|
517
|
+
result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
|
|
518
|
+
} else {
|
|
519
|
+
issues.push(
|
|
520
|
+
configIssue(
|
|
521
|
+
configPath,
|
|
522
|
+
"uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
523
|
+
)
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (raw.maxDuplicateFindingsPerRule !== void 0) {
|
|
528
|
+
if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
|
|
529
|
+
result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
|
|
530
|
+
} else {
|
|
531
|
+
issues.push(
|
|
532
|
+
configIssue(
|
|
533
|
+
configPath,
|
|
534
|
+
"uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
540
|
+
}
|
|
541
|
+
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
542
|
+
if (!isRecord(raw)) {
|
|
543
|
+
issues.push(
|
|
544
|
+
configIssue(configPath, "uiux.renderEvidence \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
545
|
+
);
|
|
546
|
+
return void 0;
|
|
547
|
+
}
|
|
548
|
+
const result = {};
|
|
549
|
+
if (raw.enabled !== void 0) {
|
|
550
|
+
if (typeof raw.enabled === "boolean") {
|
|
551
|
+
result.enabled = raw.enabled;
|
|
552
|
+
} else {
|
|
553
|
+
issues.push(
|
|
554
|
+
configIssue(configPath, "uiux.renderEvidence.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (raw.viewports !== void 0) {
|
|
559
|
+
if (Array.isArray(raw.viewports) && raw.viewports.every((item) => typeof item === "string")) {
|
|
560
|
+
result.viewports = normalizeRenderViewports(raw.viewports);
|
|
561
|
+
} else {
|
|
562
|
+
issues.push(
|
|
563
|
+
configIssue(configPath, "uiux.renderEvidence.viewports \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (raw.out !== void 0) {
|
|
568
|
+
if (typeof raw.out === "string" && raw.out.trim().length > 0) {
|
|
569
|
+
result.out = raw.out.trim();
|
|
570
|
+
} else {
|
|
571
|
+
issues.push(
|
|
572
|
+
configIssue(configPath, "uiux.renderEvidence.out \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (raw.baseUrl !== void 0) {
|
|
577
|
+
if (typeof raw.baseUrl === "string" && raw.baseUrl.trim().length > 0) {
|
|
578
|
+
result.baseUrl = raw.baseUrl.trim();
|
|
579
|
+
} else {
|
|
580
|
+
issues.push(
|
|
581
|
+
configIssue(
|
|
582
|
+
configPath,
|
|
583
|
+
"uiux.renderEvidence.baseUrl \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
584
|
+
)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (raw.failOpen !== void 0) {
|
|
589
|
+
if (typeof raw.failOpen === "boolean") {
|
|
590
|
+
result.failOpen = raw.failOpen;
|
|
591
|
+
} else {
|
|
592
|
+
issues.push(
|
|
593
|
+
configIssue(configPath, "uiux.renderEvidence.failOpen \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
454
597
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
455
598
|
}
|
|
456
599
|
function configIssue(file, message) {
|
|
@@ -1514,11 +1657,11 @@ function cloneGlobal(pattern) {
|
|
|
1514
1657
|
}
|
|
1515
1658
|
|
|
1516
1659
|
// src/core/atddTraceability.ts
|
|
1517
|
-
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4})\b/g;
|
|
1518
|
-
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4})\b/g;
|
|
1660
|
+
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
|
|
1661
|
+
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
|
|
1519
1662
|
var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
|
|
1520
|
-
var
|
|
1521
|
-
var
|
|
1663
|
+
var US_ID_RE = /^US-\d{4}(?:-\d{4})?$/;
|
|
1664
|
+
var TC_ID_RE = /^TC-\d{4}(?:-\d{4})?$/;
|
|
1522
1665
|
var API_CONTRACT_ID_RE = /^CON-API-\d+$/;
|
|
1523
1666
|
var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
|
|
1524
1667
|
async function evaluateAtddCodeTraceability(root, config) {
|
|
@@ -1666,11 +1809,11 @@ async function collectApiContractIds(apiRoot) {
|
|
|
1666
1809
|
function collectShortIds(text, prefix) {
|
|
1667
1810
|
const ids = /* @__PURE__ */ new Set();
|
|
1668
1811
|
const headingIds = collectMarkdownItems(text, prefix).map((item) => item.id);
|
|
1669
|
-
const pattern = prefix === "US" ? /\bUS-\d{4}
|
|
1812
|
+
const pattern = prefix === "US" ? /\bUS-\d{4}(?:-\d{4})?\b/g : /\bTC-\d{4}(?:-\d{4})?\b/g;
|
|
1670
1813
|
const looseIds = uniqueMatches(text, pattern);
|
|
1671
1814
|
for (const id of [...headingIds, ...looseIds]) {
|
|
1672
1815
|
const normalized = id.toUpperCase();
|
|
1673
|
-
if (prefix === "US" &&
|
|
1816
|
+
if (prefix === "US" && US_ID_RE.test(normalized) || prefix === "TC" && TC_ID_RE.test(normalized)) {
|
|
1674
1817
|
ids.add(normalized);
|
|
1675
1818
|
}
|
|
1676
1819
|
}
|
|
@@ -2355,6 +2498,7 @@ var ID_PREFIXES = [
|
|
|
2355
2498
|
"DB",
|
|
2356
2499
|
"THEMA"
|
|
2357
2500
|
];
|
|
2501
|
+
var DIGIT_AHEAD = "(?=[A-Za-z0-9_-]*\\d)";
|
|
2358
2502
|
var STRICT_ID_PATTERNS = {
|
|
2359
2503
|
CAP: /\bCAP-\d{4}\b/g,
|
|
2360
2504
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
@@ -2370,18 +2514,18 @@ var STRICT_ID_PATTERNS = {
|
|
|
2370
2514
|
ADR: /\bADR-\d{4}\b/g
|
|
2371
2515
|
};
|
|
2372
2516
|
var LOOSE_ID_PATTERNS = {
|
|
2373
|
-
CAP:
|
|
2374
|
-
SPEC:
|
|
2375
|
-
US:
|
|
2376
|
-
BR:
|
|
2377
|
-
SC:
|
|
2378
|
-
AC:
|
|
2379
|
-
CASE:
|
|
2380
|
-
UI:
|
|
2381
|
-
API:
|
|
2382
|
-
DB:
|
|
2383
|
-
THEMA:
|
|
2384
|
-
ADR:
|
|
2517
|
+
CAP: new RegExp(`\\bCAP-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2518
|
+
SPEC: new RegExp(`\\bSPEC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2519
|
+
US: new RegExp(`\\bUS-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2520
|
+
BR: new RegExp(`\\bBR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2521
|
+
SC: new RegExp(`\\bSC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2522
|
+
AC: new RegExp(`\\bAC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2523
|
+
CASE: new RegExp(`\\bCASE-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2524
|
+
UI: new RegExp(`\\bUI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2525
|
+
API: new RegExp(`\\bAPI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2526
|
+
DB: new RegExp(`\\bDB-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2527
|
+
THEMA: new RegExp(`\\bTHEMA-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
2528
|
+
ADR: new RegExp(`\\bADR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi")
|
|
2385
2529
|
};
|
|
2386
2530
|
function extractIds(text, prefix) {
|
|
2387
2531
|
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
@@ -3423,8 +3567,8 @@ function formatError4(error) {
|
|
|
3423
3567
|
}
|
|
3424
3568
|
|
|
3425
3569
|
// src/core/report.ts
|
|
3426
|
-
import { readFile as
|
|
3427
|
-
import
|
|
3570
|
+
import { readFile as readFile42 } from "fs/promises";
|
|
3571
|
+
import path54 from "path";
|
|
3428
3572
|
|
|
3429
3573
|
// src/core/contractIndex.ts
|
|
3430
3574
|
import { readFile as readFile10 } from "fs/promises";
|
|
@@ -4182,8 +4326,8 @@ import { readFile as readFile11 } from "fs/promises";
|
|
|
4182
4326
|
import path14 from "path";
|
|
4183
4327
|
import { fileURLToPath } from "url";
|
|
4184
4328
|
async function resolveToolVersion() {
|
|
4185
|
-
if ("1.7.
|
|
4186
|
-
return "1.7.
|
|
4329
|
+
if ("1.7.2".length > 0) {
|
|
4330
|
+
return "1.7.2";
|
|
4187
4331
|
}
|
|
4188
4332
|
try {
|
|
4189
4333
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -7026,7 +7170,7 @@ import path24 from "path";
|
|
|
7026
7170
|
var SC_TAG_RE3 = /^SC-\d{4}-\d{4}$/;
|
|
7027
7171
|
var SC_TAG_RE_GLOBAL = /\bSC-\d{4}-\d{4}\b/g;
|
|
7028
7172
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
7029
|
-
var
|
|
7173
|
+
var US_ID_RE2 = /\bUS-\d{4}-\d{4}\b/g;
|
|
7030
7174
|
var AC_ID_RE3 = /\bAC-\d{4}-\d{4}\b/g;
|
|
7031
7175
|
var DOWNSTREAM_ID_RE = /\b(?:US|AC|BR|SC|CASE)-\d{4}-\d{4}\b/g;
|
|
7032
7176
|
async function validateTraceability(root, config, phase) {
|
|
@@ -7153,7 +7297,7 @@ async function collectLayeredEdgeData(entry, issues) {
|
|
|
7153
7297
|
const { definitions: acIds, refsByDefinition: acToUs } = extractTableDefinitionRefs(
|
|
7154
7298
|
acceptanceCriteriaText,
|
|
7155
7299
|
/^AC-\d{4}-\d{4}$/,
|
|
7156
|
-
|
|
7300
|
+
US_ID_RE2
|
|
7157
7301
|
);
|
|
7158
7302
|
const { definitions: brIds, refsByDefinition: brToAc } = extractTableDefinitionRefs(
|
|
7159
7303
|
businessRulesText,
|
|
@@ -8209,15 +8353,15 @@ import path28 from "path";
|
|
|
8209
8353
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
8210
8354
|
import path29 from "path";
|
|
8211
8355
|
var ID_PATTERNS = {
|
|
8212
|
-
us: /^US-\d{4}
|
|
8213
|
-
ac: /^AC-\d{4}
|
|
8214
|
-
br: /^BR-\d{4}
|
|
8215
|
-
ex: /^EX-\d{4}
|
|
8356
|
+
us: /^US-\d{4}(?:-\d{4})?$/,
|
|
8357
|
+
ac: /^AC-\d{4}(?:-\d{4})?$/,
|
|
8358
|
+
br: /^BR-\d{4}(?:-\d{4})?$/,
|
|
8359
|
+
ex: /^EX-\d{4}(?:-\d{4})?$/
|
|
8216
8360
|
};
|
|
8217
8361
|
var V1421_REFS = {
|
|
8218
|
-
ac: /\bAC-\d{4}
|
|
8219
|
-
br: /\bBR-\d{4}
|
|
8220
|
-
ex: /\bEX-\d{4}
|
|
8362
|
+
ac: /\bAC-\d{4}(?:-\d{4})?\b/gi,
|
|
8363
|
+
br: /\bBR-\d{4}(?:-\d{4})?\b/gi,
|
|
8364
|
+
ex: /\bEX-\d{4}(?:-\d{4})?\b/gi
|
|
8221
8365
|
};
|
|
8222
8366
|
async function validateLayerCoverage(root, config) {
|
|
8223
8367
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -8490,11 +8634,11 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
8490
8634
|
const ids = /* @__PURE__ */ new Set();
|
|
8491
8635
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
8492
8636
|
for (const line of lines) {
|
|
8493
|
-
const headingMatch = /^##\s*(AC-\d{4})\b/i.exec(line.trim());
|
|
8637
|
+
const headingMatch = /^##\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line.trim());
|
|
8494
8638
|
if (headingMatch?.[1]) {
|
|
8495
8639
|
ids.add(headingMatch[1].toUpperCase());
|
|
8496
8640
|
}
|
|
8497
|
-
const commentMatch = /^\s*#\s*(AC-\d{4})\b/i.exec(line);
|
|
8641
|
+
const commentMatch = /^\s*#\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line);
|
|
8498
8642
|
if (commentMatch?.[1]) {
|
|
8499
8643
|
ids.add(commentMatch[1].toUpperCase());
|
|
8500
8644
|
}
|
|
@@ -8514,8 +8658,8 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
8514
8658
|
function parseDefinitionRefs(text, prefix, refPattern, options = {}) {
|
|
8515
8659
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
8516
8660
|
const refsById = /* @__PURE__ */ new Map();
|
|
8517
|
-
const idPattern = new RegExp(`^${prefix}-\\d{4}
|
|
8518
|
-
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4})\\b`, "i");
|
|
8661
|
+
const idPattern = new RegExp(`^${prefix}-\\d{4}(?:-\\d{4})?$`);
|
|
8662
|
+
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4}(?:-\\d{4})?)\\b`, "i");
|
|
8519
8663
|
const referenceColumns = new Set(
|
|
8520
8664
|
(options.referenceColumns ?? []).map((column) => normalizeColumnName(column))
|
|
8521
8665
|
);
|
|
@@ -8916,7 +9060,7 @@ var US_DOWNSTREAM_RE = /\b(?:AC|BR|EX|TC)-\d{4}\b/g;
|
|
|
8916
9060
|
var AC_DOWNSTREAM_RE = /\b(?:BR|EX|TC)-\d{4}\b/g;
|
|
8917
9061
|
var BR_DOWNSTREAM_RE = /\b(?:EX|TC)-\d{4}\b/g;
|
|
8918
9062
|
var CAP_ID_RE = /^CAP-\d{4}$/;
|
|
8919
|
-
var
|
|
9063
|
+
var US_ID_RE3 = /^US-\d{4}$/;
|
|
8920
9064
|
var AC_ID_RE4 = /^AC-\d{4}$/;
|
|
8921
9065
|
var BR_OR_AC_ID_RE = /^(?:BR|AC)-\d{4}$/;
|
|
8922
9066
|
var EX_ID_RE2 = /^EX-\d{4}$/;
|
|
@@ -8955,7 +9099,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
8955
9099
|
...await validateMarkdownParentFormat(entry.userStoriesPath, "US", CAP_ID_RE, "CAP")
|
|
8956
9100
|
);
|
|
8957
9101
|
issues.push(
|
|
8958
|
-
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC",
|
|
9102
|
+
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE3, "US")
|
|
8959
9103
|
);
|
|
8960
9104
|
issues.push(
|
|
8961
9105
|
...await validateMarkdownParentFormat(entry.businessRulesPath, "BR", AC_ID_RE4, "AC")
|
|
@@ -9761,7 +9905,7 @@ function validateExParentExists(filePath, exItems, acIds, brIds) {
|
|
|
9761
9905
|
}
|
|
9762
9906
|
|
|
9763
9907
|
// src/core/validators/prototypingEvidence.ts
|
|
9764
|
-
import { readFile as readFile25 } from "fs/promises";
|
|
9908
|
+
import { access as access9, readFile as readFile25 } from "fs/promises";
|
|
9765
9909
|
import path34 from "path";
|
|
9766
9910
|
var EVIDENCE_MARKDOWN_FILE = "prototyping.md";
|
|
9767
9911
|
var EVIDENCE_JSON_FILE = "prototyping.json";
|
|
@@ -10267,6 +10411,100 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
|
|
|
10267
10411
|
)
|
|
10268
10412
|
);
|
|
10269
10413
|
}
|
|
10414
|
+
const renderIssues = await validateRenderEvidenceScreens(
|
|
10415
|
+
root,
|
|
10416
|
+
config,
|
|
10417
|
+
evidenceJsonPath,
|
|
10418
|
+
uiFidelity.screens
|
|
10419
|
+
);
|
|
10420
|
+
issues.push(...renderIssues);
|
|
10421
|
+
return issues;
|
|
10422
|
+
}
|
|
10423
|
+
async function validateRenderEvidenceScreens(root, config, evidenceJsonPath, screens) {
|
|
10424
|
+
const issues = [];
|
|
10425
|
+
const hasAnyRenderEvidence = screens.some((screen) => screen.renders.length > 0);
|
|
10426
|
+
if (!hasAnyRenderEvidence) {
|
|
10427
|
+
return issues;
|
|
10428
|
+
}
|
|
10429
|
+
const qualityProfile = config.uiux?.qualityProfile ?? "default";
|
|
10430
|
+
for (const screen of screens) {
|
|
10431
|
+
if (screen.renders.length === 0) {
|
|
10432
|
+
continue;
|
|
10433
|
+
}
|
|
10434
|
+
const viewports = new Set(screen.renders.map((render) => render.viewport));
|
|
10435
|
+
const missingDefaultViewports = DEFAULT_RENDER_VIEWPORTS.filter(
|
|
10436
|
+
(viewport) => !viewports.has(viewport)
|
|
10437
|
+
);
|
|
10438
|
+
const allSkipped = screen.renders.every((render) => render.status === "skipped");
|
|
10439
|
+
for (const render of screen.renders) {
|
|
10440
|
+
if (render.status !== "captured") {
|
|
10441
|
+
continue;
|
|
10442
|
+
}
|
|
10443
|
+
const invalidPaths = [
|
|
10444
|
+
{ label: "imagePath", value: render.imagePath },
|
|
10445
|
+
{ label: "htmlPath", value: render.htmlPath }
|
|
10446
|
+
].filter((entry) => looksLikeInlineRenderPayload(entry.value));
|
|
10447
|
+
if (invalidPaths.length > 0) {
|
|
10448
|
+
issues.push(
|
|
10449
|
+
issue(
|
|
10450
|
+
"QFAI-PROT-244",
|
|
10451
|
+
`QFAI-PROT-244: render evidence must be path-only. route=${screen.route}, viewport=${render.viewport}, invalid=${invalidPaths.map((entry) => entry.label).join("|")}`,
|
|
10452
|
+
"error",
|
|
10453
|
+
evidenceJsonPath,
|
|
10454
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
10455
|
+
[
|
|
10456
|
+
`route=${screen.route}`,
|
|
10457
|
+
`viewport=${render.viewport}`,
|
|
10458
|
+
...invalidPaths.map((entry) => `artifact=${entry.label}`)
|
|
10459
|
+
],
|
|
10460
|
+
"change",
|
|
10461
|
+
"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"
|
|
10462
|
+
)
|
|
10463
|
+
);
|
|
10464
|
+
continue;
|
|
10465
|
+
}
|
|
10466
|
+
const missingArtifacts = await collectMissingRenderArtifacts(root, render);
|
|
10467
|
+
if (missingArtifacts.length > 0) {
|
|
10468
|
+
issues.push(
|
|
10469
|
+
issue(
|
|
10470
|
+
"QFAI-PROT-244",
|
|
10471
|
+
`QFAI-PROT-244: captured render artifact is missing. route=${screen.route}, viewport=${render.viewport}, missing=${missingArtifacts.join("|")}`,
|
|
10472
|
+
"error",
|
|
10473
|
+
evidenceJsonPath,
|
|
10474
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
10475
|
+
[
|
|
10476
|
+
`route=${screen.route}`,
|
|
10477
|
+
`viewport=${render.viewport}`,
|
|
10478
|
+
...missingArtifacts.map((artifact) => `artifact=${artifact}`)
|
|
10479
|
+
],
|
|
10480
|
+
"change",
|
|
10481
|
+
"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"
|
|
10482
|
+
)
|
|
10483
|
+
);
|
|
10484
|
+
}
|
|
10485
|
+
}
|
|
10486
|
+
if (missingDefaultViewports.length === 0 && !allSkipped) {
|
|
10487
|
+
continue;
|
|
10488
|
+
}
|
|
10489
|
+
const severity = qualityProfile === "default" ? "warning" : "error";
|
|
10490
|
+
const reason = allSkipped ? "all renders are skipped" : `missing default viewports=${missingDefaultViewports.join("|")}`;
|
|
10491
|
+
issues.push(
|
|
10492
|
+
issue(
|
|
10493
|
+
"QFAI-PROT-245",
|
|
10494
|
+
`QFAI-PROT-245: render coverage is incomplete for ${screen.route}. ${reason}. qualityProfile=${qualityProfile}`,
|
|
10495
|
+
severity,
|
|
10496
|
+
evidenceJsonPath,
|
|
10497
|
+
"prototypingEvidence.renderCoverage",
|
|
10498
|
+
[
|
|
10499
|
+
`route=${screen.route}`,
|
|
10500
|
+
...missingDefaultViewports.map((viewport) => `viewport=${viewport}`),
|
|
10501
|
+
`qualityProfile=${qualityProfile}`
|
|
10502
|
+
],
|
|
10503
|
+
"change",
|
|
10504
|
+
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"
|
|
10505
|
+
)
|
|
10506
|
+
);
|
|
10507
|
+
}
|
|
10270
10508
|
return issues;
|
|
10271
10509
|
}
|
|
10272
10510
|
function formatUiFidelityMismatch(mismatch) {
|
|
@@ -10479,6 +10717,10 @@ function normalizeUiFidelityScreen(value) {
|
|
|
10479
10717
|
if (!mockPaths.ok) {
|
|
10480
10718
|
return mockPaths;
|
|
10481
10719
|
}
|
|
10720
|
+
const renders = normalizeRenderEntries(value.renders);
|
|
10721
|
+
if (!renders.ok) {
|
|
10722
|
+
return renders;
|
|
10723
|
+
}
|
|
10482
10724
|
return {
|
|
10483
10725
|
ok: true,
|
|
10484
10726
|
value: {
|
|
@@ -10489,10 +10731,105 @@ function normalizeUiFidelityScreen(value) {
|
|
|
10489
10731
|
...normalizeOptionalMissingBlock(value.missing),
|
|
10490
10732
|
...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
|
|
10491
10733
|
observed: observed.value,
|
|
10492
|
-
mockPaths: mockPaths.value
|
|
10734
|
+
mockPaths: mockPaths.value,
|
|
10735
|
+
renders: renders.value
|
|
10493
10736
|
}
|
|
10494
10737
|
};
|
|
10495
10738
|
}
|
|
10739
|
+
function normalizeRenderEntries(value) {
|
|
10740
|
+
if (value === void 0) {
|
|
10741
|
+
return { ok: true, value: [] };
|
|
10742
|
+
}
|
|
10743
|
+
if (!Array.isArray(value)) {
|
|
10744
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders` must be an array" };
|
|
10745
|
+
}
|
|
10746
|
+
const renders = [];
|
|
10747
|
+
for (const entry of value) {
|
|
10748
|
+
const normalized = normalizeRenderEntry(entry);
|
|
10749
|
+
if (!normalized.ok) {
|
|
10750
|
+
return normalized;
|
|
10751
|
+
}
|
|
10752
|
+
renders.push(normalized.value);
|
|
10753
|
+
}
|
|
10754
|
+
return { ok: true, value: renders };
|
|
10755
|
+
}
|
|
10756
|
+
function normalizeRenderEntry(value) {
|
|
10757
|
+
if (!isRecord5(value)) {
|
|
10758
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[]` must be objects" };
|
|
10759
|
+
}
|
|
10760
|
+
if (typeof value.viewport !== "string" || value.viewport.trim().length === 0) {
|
|
10761
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[].viewport` is required" };
|
|
10762
|
+
}
|
|
10763
|
+
if (!isNonNegativeInteger(value.width) || !isNonNegativeInteger(value.height) || value.width === 0 || value.height === 0) {
|
|
10764
|
+
return {
|
|
10765
|
+
ok: false,
|
|
10766
|
+
reason: "`uiFidelity.screens[].renders[]` requires positive integers for width/height"
|
|
10767
|
+
};
|
|
10768
|
+
}
|
|
10769
|
+
const viewport = value.viewport.trim();
|
|
10770
|
+
const width = value.width;
|
|
10771
|
+
const height = value.height;
|
|
10772
|
+
const status = typeof value.status === "string" ? value.status.trim().toLowerCase() : "";
|
|
10773
|
+
if (status === "captured") {
|
|
10774
|
+
if (typeof value.imagePath !== "string" || value.imagePath.trim().length === 0 || typeof value.htmlPath !== "string" || value.htmlPath.trim().length === 0) {
|
|
10775
|
+
return {
|
|
10776
|
+
ok: false,
|
|
10777
|
+
reason: "`captured` render entries require imagePath and htmlPath"
|
|
10778
|
+
};
|
|
10779
|
+
}
|
|
10780
|
+
return {
|
|
10781
|
+
ok: true,
|
|
10782
|
+
value: {
|
|
10783
|
+
viewport,
|
|
10784
|
+
status: "captured",
|
|
10785
|
+
width,
|
|
10786
|
+
height,
|
|
10787
|
+
imagePath: value.imagePath.trim(),
|
|
10788
|
+
htmlPath: value.htmlPath.trim()
|
|
10789
|
+
}
|
|
10790
|
+
};
|
|
10791
|
+
}
|
|
10792
|
+
if (status === "skipped") {
|
|
10793
|
+
if (typeof value.skippedReason !== "string" || value.skippedReason.trim().length === 0) {
|
|
10794
|
+
return {
|
|
10795
|
+
ok: false,
|
|
10796
|
+
reason: "`skipped` render entries require skippedReason"
|
|
10797
|
+
};
|
|
10798
|
+
}
|
|
10799
|
+
return {
|
|
10800
|
+
ok: true,
|
|
10801
|
+
value: {
|
|
10802
|
+
viewport,
|
|
10803
|
+
status: "skipped",
|
|
10804
|
+
width,
|
|
10805
|
+
height,
|
|
10806
|
+
skippedReason: value.skippedReason.trim()
|
|
10807
|
+
}
|
|
10808
|
+
};
|
|
10809
|
+
}
|
|
10810
|
+
if (status === "failed") {
|
|
10811
|
+
if (typeof value.error !== "string" || value.error.trim().length === 0) {
|
|
10812
|
+
return {
|
|
10813
|
+
ok: false,
|
|
10814
|
+
reason: "`failed` render entries require error"
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
10817
|
+
return {
|
|
10818
|
+
ok: true,
|
|
10819
|
+
value: {
|
|
10820
|
+
viewport,
|
|
10821
|
+
status: "failed",
|
|
10822
|
+
width,
|
|
10823
|
+
height,
|
|
10824
|
+
error: value.error.trim()
|
|
10825
|
+
}
|
|
10826
|
+
};
|
|
10827
|
+
}
|
|
10828
|
+
return {
|
|
10829
|
+
ok: false,
|
|
10830
|
+
reason: "`uiFidelity.screens[].renders[].status` must be captured|skipped|failed"
|
|
10831
|
+
};
|
|
10832
|
+
}
|
|
10496
10833
|
function normalizeUiFidelityExpected(value) {
|
|
10497
10834
|
if (!isRecord5(value)) {
|
|
10498
10835
|
return {
|
|
@@ -10716,6 +11053,22 @@ function normalizeOptionalMissingBlock(value) {
|
|
|
10716
11053
|
function isRecord5(value) {
|
|
10717
11054
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10718
11055
|
}
|
|
11056
|
+
async function collectMissingRenderArtifacts(root, render) {
|
|
11057
|
+
const missing = [];
|
|
11058
|
+
const candidates = [
|
|
11059
|
+
{ label: "imagePath", target: render.imagePath },
|
|
11060
|
+
{ label: "htmlPath", target: render.htmlPath }
|
|
11061
|
+
];
|
|
11062
|
+
for (const candidate of candidates) {
|
|
11063
|
+
const resolved = path34.isAbsolute(candidate.target) ? candidate.target : path34.resolve(root, candidate.target);
|
|
11064
|
+
try {
|
|
11065
|
+
await access9(resolved);
|
|
11066
|
+
} catch {
|
|
11067
|
+
missing.push(candidate.label);
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
return missing;
|
|
11071
|
+
}
|
|
10719
11072
|
function isInteger(value) {
|
|
10720
11073
|
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
10721
11074
|
}
|
|
@@ -11064,7 +11417,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
11064
11417
|
}
|
|
11065
11418
|
function flattenTokens(obj, prefix, target, errors) {
|
|
11066
11419
|
for (const [key, value] of Object.entries(obj)) {
|
|
11067
|
-
const
|
|
11420
|
+
const path55 = `${prefix}.${key}`;
|
|
11068
11421
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11069
11422
|
const record2 = value;
|
|
11070
11423
|
if ("$value" in record2) {
|
|
@@ -11080,9 +11433,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
11080
11433
|
if (typeof record2.platform === "string") {
|
|
11081
11434
|
token.platform = record2.platform;
|
|
11082
11435
|
}
|
|
11083
|
-
target.set(
|
|
11436
|
+
target.set(path55, token);
|
|
11084
11437
|
} else {
|
|
11085
|
-
flattenTokens(record2,
|
|
11438
|
+
flattenTokens(record2, path55, target, errors);
|
|
11086
11439
|
}
|
|
11087
11440
|
}
|
|
11088
11441
|
}
|
|
@@ -11092,44 +11445,44 @@ function resolveAllReferences(result) {
|
|
|
11092
11445
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
11093
11446
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
11094
11447
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
11095
|
-
for (const [
|
|
11096
|
-
resolveTokenRef(
|
|
11448
|
+
for (const [path55] of allTokens) {
|
|
11449
|
+
resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
11097
11450
|
}
|
|
11098
11451
|
}
|
|
11099
|
-
function resolveTokenRef(
|
|
11100
|
-
if (result.resolved.has(
|
|
11101
|
-
return result.resolved.get(
|
|
11452
|
+
function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
11453
|
+
if (result.resolved.has(path55)) {
|
|
11454
|
+
return result.resolved.get(path55);
|
|
11102
11455
|
}
|
|
11103
11456
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
11104
11457
|
result.errors.push({
|
|
11105
|
-
message: `Max reference depth exceeded at: ${
|
|
11106
|
-
path:
|
|
11458
|
+
message: `Max reference depth exceeded at: ${path55}`,
|
|
11459
|
+
path: path55
|
|
11107
11460
|
});
|
|
11108
11461
|
return void 0;
|
|
11109
11462
|
}
|
|
11110
|
-
if (visited.has(
|
|
11463
|
+
if (visited.has(path55)) {
|
|
11111
11464
|
result.errors.push({
|
|
11112
|
-
message: `Circular reference detected: ${
|
|
11113
|
-
path:
|
|
11465
|
+
message: `Circular reference detected: ${path55}`,
|
|
11466
|
+
path: path55
|
|
11114
11467
|
});
|
|
11115
11468
|
return void 0;
|
|
11116
11469
|
}
|
|
11117
|
-
const token = allTokens.get(
|
|
11470
|
+
const token = allTokens.get(path55);
|
|
11118
11471
|
if (!token) {
|
|
11119
11472
|
return void 0;
|
|
11120
11473
|
}
|
|
11121
11474
|
if (typeof token.$value !== "string") {
|
|
11122
11475
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11123
|
-
result.resolved.set(
|
|
11476
|
+
result.resolved.set(path55, rawValue2);
|
|
11124
11477
|
return rawValue2;
|
|
11125
11478
|
}
|
|
11126
11479
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11127
11480
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11128
11481
|
if (refs.length === 0) {
|
|
11129
|
-
result.resolved.set(
|
|
11482
|
+
result.resolved.set(path55, rawValue);
|
|
11130
11483
|
return rawValue;
|
|
11131
11484
|
}
|
|
11132
|
-
visited.add(
|
|
11485
|
+
visited.add(path55);
|
|
11133
11486
|
let resolved = rawValue;
|
|
11134
11487
|
for (const ref of refs) {
|
|
11135
11488
|
const refPath = ref[1];
|
|
@@ -11137,8 +11490,8 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11137
11490
|
const refToken = allTokens.get(refPath);
|
|
11138
11491
|
if (!refToken) {
|
|
11139
11492
|
result.errors.push({
|
|
11140
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11141
|
-
path:
|
|
11493
|
+
message: `Unresolved token reference: {${refPath}} at ${path55}`,
|
|
11494
|
+
path: path55
|
|
11142
11495
|
});
|
|
11143
11496
|
continue;
|
|
11144
11497
|
}
|
|
@@ -11147,7 +11500,7 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11147
11500
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11148
11501
|
}
|
|
11149
11502
|
}
|
|
11150
|
-
result.resolved.set(
|
|
11503
|
+
result.resolved.set(path55, resolved);
|
|
11151
11504
|
return resolved;
|
|
11152
11505
|
}
|
|
11153
11506
|
function stringifyTokenValue(value) {
|
|
@@ -14259,6 +14612,7 @@ async function validateNavigationFlow(root, config) {
|
|
|
14259
14612
|
|
|
14260
14613
|
// src/core/validators/renderCritique.ts
|
|
14261
14614
|
import path49 from "path";
|
|
14615
|
+
import { readFile as readFile39 } from "fs/promises";
|
|
14262
14616
|
import fg9 from "fast-glob";
|
|
14263
14617
|
var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
|
|
14264
14618
|
var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
|
|
@@ -14290,6 +14644,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14290
14644
|
if (!hasDdp) return issues;
|
|
14291
14645
|
const skillsDir = path49.join(root, config.paths.skillsDir).replace(/\\/g, "/");
|
|
14292
14646
|
const evidenceDir = path49.join(root, ".qfai", "evidence").replace(/\\/g, "/");
|
|
14647
|
+
const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
|
|
14293
14648
|
const skillPromptPattern = path49.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
14294
14649
|
const skillFiles = await fg9(skillPromptPattern, { dot: true });
|
|
14295
14650
|
const evidencePattern = path49.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
|
|
@@ -14329,7 +14684,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14329
14684
|
}
|
|
14330
14685
|
}
|
|
14331
14686
|
const allEvidenceContent = await collectContent(evidenceFiles);
|
|
14332
|
-
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
|
|
14687
|
+
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("desktop")) {
|
|
14333
14688
|
issues.push(
|
|
14334
14689
|
issue(
|
|
14335
14690
|
"QFAI-CRIT-003",
|
|
@@ -14343,7 +14698,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14343
14698
|
)
|
|
14344
14699
|
);
|
|
14345
14700
|
}
|
|
14346
|
-
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
|
|
14701
|
+
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("mobile")) {
|
|
14347
14702
|
issues.push(
|
|
14348
14703
|
issue(
|
|
14349
14704
|
"QFAI-CRIT-004",
|
|
@@ -14504,9 +14859,49 @@ async function collectContent(files) {
|
|
|
14504
14859
|
}
|
|
14505
14860
|
return contents.join("\n---\n");
|
|
14506
14861
|
}
|
|
14862
|
+
async function collectRenderEvidenceViewports(root) {
|
|
14863
|
+
const prototypingJsonPath = path49.join(root, ".qfai", "evidence", "prototyping.json");
|
|
14864
|
+
try {
|
|
14865
|
+
const raw = await readFile39(prototypingJsonPath, "utf-8");
|
|
14866
|
+
const parsed = JSON.parse(raw);
|
|
14867
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14868
|
+
return /* @__PURE__ */ new Set();
|
|
14869
|
+
}
|
|
14870
|
+
const uiFidelity = parsed.uiFidelity;
|
|
14871
|
+
if (!uiFidelity || typeof uiFidelity !== "object" || Array.isArray(uiFidelity)) {
|
|
14872
|
+
return /* @__PURE__ */ new Set();
|
|
14873
|
+
}
|
|
14874
|
+
const screens = uiFidelity.screens;
|
|
14875
|
+
if (!Array.isArray(screens)) {
|
|
14876
|
+
return /* @__PURE__ */ new Set();
|
|
14877
|
+
}
|
|
14878
|
+
const viewports = /* @__PURE__ */ new Set();
|
|
14879
|
+
for (const screen of screens) {
|
|
14880
|
+
if (!screen || typeof screen !== "object" || Array.isArray(screen)) {
|
|
14881
|
+
continue;
|
|
14882
|
+
}
|
|
14883
|
+
const renders = screen.renders;
|
|
14884
|
+
if (!Array.isArray(renders)) {
|
|
14885
|
+
continue;
|
|
14886
|
+
}
|
|
14887
|
+
for (const render of renders) {
|
|
14888
|
+
if (!render || typeof render !== "object" || Array.isArray(render)) {
|
|
14889
|
+
continue;
|
|
14890
|
+
}
|
|
14891
|
+
const viewport = render.viewport;
|
|
14892
|
+
if (typeof viewport === "string" && viewport.trim().length > 0) {
|
|
14893
|
+
viewports.add(viewport.trim().toLowerCase());
|
|
14894
|
+
}
|
|
14895
|
+
}
|
|
14896
|
+
}
|
|
14897
|
+
return viewports;
|
|
14898
|
+
} catch {
|
|
14899
|
+
return /* @__PURE__ */ new Set();
|
|
14900
|
+
}
|
|
14901
|
+
}
|
|
14507
14902
|
|
|
14508
14903
|
// src/core/validators/designFidelity.ts
|
|
14509
|
-
import { readFile as
|
|
14904
|
+
import { readFile as readFile40 } from "fs/promises";
|
|
14510
14905
|
import path50 from "path";
|
|
14511
14906
|
import fg10 from "fast-glob";
|
|
14512
14907
|
var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
|
|
@@ -14539,7 +14934,7 @@ async function validateDesignFidelity(root, config) {
|
|
|
14539
14934
|
for (const filePath of allFiles) {
|
|
14540
14935
|
let content;
|
|
14541
14936
|
try {
|
|
14542
|
-
content = await
|
|
14937
|
+
content = await readFile40(filePath, "utf-8");
|
|
14543
14938
|
} catch {
|
|
14544
14939
|
continue;
|
|
14545
14940
|
}
|
|
@@ -15162,6 +15557,252 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
15162
15557
|
return issues;
|
|
15163
15558
|
}
|
|
15164
15559
|
|
|
15560
|
+
// src/core/validators/designAudit.ts
|
|
15561
|
+
import { readdir as readdir9 } from "fs/promises";
|
|
15562
|
+
import path52 from "path";
|
|
15563
|
+
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
15564
|
+
function resolveAuditConfig(config) {
|
|
15565
|
+
const audit = config.uiux?.audit;
|
|
15566
|
+
const profile = config.uiux?.qualityProfile ?? "default";
|
|
15567
|
+
return {
|
|
15568
|
+
enabled: audit?.enabled ?? true,
|
|
15569
|
+
slopDetection: audit?.slopDetection ?? true,
|
|
15570
|
+
qualityProfile: profile,
|
|
15571
|
+
maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
|
|
15572
|
+
maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
|
|
15573
|
+
maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
|
|
15574
|
+
};
|
|
15575
|
+
}
|
|
15576
|
+
function mapSeverity(tier, profile, category) {
|
|
15577
|
+
if (tier === 1) return "error";
|
|
15578
|
+
if (tier === 2) return profile === "strict" ? "error" : "warning";
|
|
15579
|
+
if (profile === "default") {
|
|
15580
|
+
return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
|
|
15581
|
+
}
|
|
15582
|
+
return "warning";
|
|
15583
|
+
}
|
|
15584
|
+
function findingToIssue(finding, profile, rulePrefix = "audit") {
|
|
15585
|
+
const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
|
|
15586
|
+
return issue(
|
|
15587
|
+
finding.ruleId,
|
|
15588
|
+
finding.message,
|
|
15589
|
+
severity,
|
|
15590
|
+
finding.file,
|
|
15591
|
+
`${rulePrefix}.${finding.dimension}`,
|
|
15592
|
+
finding.evidence.length > 0 ? finding.evidence : void 0,
|
|
15593
|
+
"compatibility",
|
|
15594
|
+
finding.guidance
|
|
15595
|
+
);
|
|
15596
|
+
}
|
|
15597
|
+
function extractSection2(content, heading) {
|
|
15598
|
+
const idx = content.indexOf(heading);
|
|
15599
|
+
if (idx === -1) return null;
|
|
15600
|
+
const start = idx + heading.length;
|
|
15601
|
+
const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
|
|
15602
|
+
const rest = content.slice(start);
|
|
15603
|
+
const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
|
|
15604
|
+
const nextHeadingMatch = headingPattern.exec(rest);
|
|
15605
|
+
const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
15606
|
+
return sectionContent.trim() || null;
|
|
15607
|
+
}
|
|
15608
|
+
function checkCtaHierarchy(content, auditConfig, file) {
|
|
15609
|
+
const findings = [];
|
|
15610
|
+
const ctaSection = extractSection2(content, "### CTA Hierarchy");
|
|
15611
|
+
if (!ctaSection) return findings;
|
|
15612
|
+
const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
|
|
15613
|
+
const primaryCount = primaryLines.length;
|
|
15614
|
+
if (primaryCount === 0) {
|
|
15615
|
+
findings.push({
|
|
15616
|
+
ruleId: "QFAI-AUD-001",
|
|
15617
|
+
dimension: "visualHierarchy",
|
|
15618
|
+
severityTier: 1,
|
|
15619
|
+
message: "No primary CTA defined in CTA Hierarchy",
|
|
15620
|
+
why: "Every UI screen needs a clear primary action to guide users",
|
|
15621
|
+
evidence: [],
|
|
15622
|
+
guidance: "Define at least one primary CTA in the CTA Hierarchy section",
|
|
15623
|
+
file
|
|
15624
|
+
});
|
|
15625
|
+
}
|
|
15626
|
+
if (primaryCount > auditConfig.maxPrimaryCtas) {
|
|
15627
|
+
findings.push({
|
|
15628
|
+
ruleId: "QFAI-AUD-020",
|
|
15629
|
+
dimension: "visualHierarchy",
|
|
15630
|
+
severityTier: 2,
|
|
15631
|
+
message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
|
|
15632
|
+
why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
|
|
15633
|
+
evidence: primaryLines.map((l) => l.trim()),
|
|
15634
|
+
guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
|
|
15635
|
+
file
|
|
15636
|
+
});
|
|
15637
|
+
}
|
|
15638
|
+
return findings;
|
|
15639
|
+
}
|
|
15640
|
+
var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
|
|
15641
|
+
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
15642
|
+
const findings = [];
|
|
15643
|
+
const configuredDir = cfg.uiux?.designTokensDir;
|
|
15644
|
+
const tokensDir = configuredDir ? path52.resolve(root, configuredDir) : path52.join(root, cfg.paths.contractsDir, "design");
|
|
15645
|
+
let hasTokenFiles = false;
|
|
15646
|
+
try {
|
|
15647
|
+
const entries = await readdir9(tokensDir);
|
|
15648
|
+
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
15649
|
+
} catch {
|
|
15650
|
+
return findings;
|
|
15651
|
+
}
|
|
15652
|
+
if (!hasTokenFiles) return findings;
|
|
15653
|
+
const contractsUiDir = path52.join(root, cfg.paths.contractsDir, "ui");
|
|
15654
|
+
let htmlFiles = [];
|
|
15655
|
+
try {
|
|
15656
|
+
const entries = await readdir9(contractsUiDir);
|
|
15657
|
+
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
15658
|
+
} catch {
|
|
15659
|
+
return findings;
|
|
15660
|
+
}
|
|
15661
|
+
let rawCount = 0;
|
|
15662
|
+
const sampleLiterals = [];
|
|
15663
|
+
for (const htmlFile of htmlFiles) {
|
|
15664
|
+
const content = await readSafe(path52.join(contractsUiDir, htmlFile));
|
|
15665
|
+
if (!content) continue;
|
|
15666
|
+
const matches = content.match(RAW_COLOR_RE);
|
|
15667
|
+
if (matches) {
|
|
15668
|
+
rawCount += matches.length;
|
|
15669
|
+
for (const m of matches) {
|
|
15670
|
+
if (sampleLiterals.length < 10) {
|
|
15671
|
+
sampleLiterals.push(m.toLowerCase());
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
}
|
|
15675
|
+
}
|
|
15676
|
+
if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
|
|
15677
|
+
findings.push({
|
|
15678
|
+
ruleId: "QFAI-AUD-004",
|
|
15679
|
+
dimension: "tokenDiscipline",
|
|
15680
|
+
severityTier: 1,
|
|
15681
|
+
message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
|
|
15682
|
+
why: "Raw color values bypass design tokens, causing visual inconsistency",
|
|
15683
|
+
evidence: sampleLiterals,
|
|
15684
|
+
guidance: "Replace raw color literals with design token references"
|
|
15685
|
+
});
|
|
15686
|
+
}
|
|
15687
|
+
return findings;
|
|
15688
|
+
}
|
|
15689
|
+
function deduplicateFindings(issues, maxPerRule) {
|
|
15690
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15691
|
+
const result = [];
|
|
15692
|
+
for (const iss of issues) {
|
|
15693
|
+
const count = counts.get(iss.code) ?? 0;
|
|
15694
|
+
if (count < maxPerRule) {
|
|
15695
|
+
result.push(iss);
|
|
15696
|
+
}
|
|
15697
|
+
counts.set(iss.code, count + 1);
|
|
15698
|
+
}
|
|
15699
|
+
for (const [code, count] of counts) {
|
|
15700
|
+
if (count > maxPerRule) {
|
|
15701
|
+
result.push({
|
|
15702
|
+
code,
|
|
15703
|
+
severity: "info",
|
|
15704
|
+
category: "compatibility",
|
|
15705
|
+
message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
|
|
15706
|
+
rule: `audit.dedup.${code}`
|
|
15707
|
+
});
|
|
15708
|
+
}
|
|
15709
|
+
}
|
|
15710
|
+
return result;
|
|
15711
|
+
}
|
|
15712
|
+
async function validateDesignAudit(root, config) {
|
|
15713
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15714
|
+
if (!auditConfig.enabled) return [];
|
|
15715
|
+
const discussionDir = path52.join(root, config.paths.discussionDir);
|
|
15716
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15717
|
+
if (!packRoot) return [];
|
|
15718
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15719
|
+
if (!uiBearing) return [];
|
|
15720
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15721
|
+
const content = await readSafe(storyPath);
|
|
15722
|
+
if (!content) return [];
|
|
15723
|
+
const findings = [];
|
|
15724
|
+
findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
|
|
15725
|
+
findings.push(...await checkTokenDrift(root, auditConfig, config));
|
|
15726
|
+
const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
|
|
15727
|
+
return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
|
|
15728
|
+
}
|
|
15729
|
+
|
|
15730
|
+
// src/core/validators/designSlop.ts
|
|
15731
|
+
import { existsSync as existsSync2 } from "fs";
|
|
15732
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
15733
|
+
import path53 from "path";
|
|
15734
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
15735
|
+
function isValidSlopPattern(rule) {
|
|
15736
|
+
if (typeof rule !== "object" || rule === null) return false;
|
|
15737
|
+
const r = rule;
|
|
15738
|
+
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";
|
|
15739
|
+
}
|
|
15740
|
+
async function loadSlopPatterns(jsonPath) {
|
|
15741
|
+
const raw = await readFile41(jsonPath, "utf-8");
|
|
15742
|
+
const parsed = JSON.parse(raw);
|
|
15743
|
+
if (!Array.isArray(parsed)) return [];
|
|
15744
|
+
return parsed.filter((r) => isValidSlopPattern(r));
|
|
15745
|
+
}
|
|
15746
|
+
function defaultPatternsPath() {
|
|
15747
|
+
const base = import.meta.url;
|
|
15748
|
+
const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
|
|
15749
|
+
const baseDir = path53.dirname(basePath);
|
|
15750
|
+
const candidates = [
|
|
15751
|
+
path53.join(baseDir, "designSlopPatterns.json"),
|
|
15752
|
+
path53.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
15753
|
+
path53.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
15754
|
+
];
|
|
15755
|
+
for (const c of candidates) {
|
|
15756
|
+
if (existsSync2(c)) return c;
|
|
15757
|
+
}
|
|
15758
|
+
return candidates[0];
|
|
15759
|
+
}
|
|
15760
|
+
async function validateDesignSlop(root, config) {
|
|
15761
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15762
|
+
if (!auditConfig.enabled) return [];
|
|
15763
|
+
if (!auditConfig.slopDetection) return [];
|
|
15764
|
+
const discussionDir = path53.join(root, config.paths.discussionDir);
|
|
15765
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15766
|
+
if (!packRoot) return [];
|
|
15767
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15768
|
+
if (!uiBearing) return [];
|
|
15769
|
+
let patterns;
|
|
15770
|
+
try {
|
|
15771
|
+
patterns = await loadSlopPatterns(defaultPatternsPath());
|
|
15772
|
+
} catch {
|
|
15773
|
+
return [];
|
|
15774
|
+
}
|
|
15775
|
+
const findings = [];
|
|
15776
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
15777
|
+
for (const pattern of patterns) {
|
|
15778
|
+
let regex;
|
|
15779
|
+
try {
|
|
15780
|
+
regex = new RegExp(pattern.match, "gi");
|
|
15781
|
+
} catch {
|
|
15782
|
+
continue;
|
|
15783
|
+
}
|
|
15784
|
+
for (const scope of pattern.scopes) {
|
|
15785
|
+
const filePath = path53.join(packRoot, scope);
|
|
15786
|
+
const content = await readSafe(filePath);
|
|
15787
|
+
if (!content) continue;
|
|
15788
|
+
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
15789
|
+
seenRules.add(pattern.id);
|
|
15790
|
+
findings.push({
|
|
15791
|
+
ruleId: pattern.id,
|
|
15792
|
+
dimension: pattern.category,
|
|
15793
|
+
severityTier: pattern.tier,
|
|
15794
|
+
message: pattern.message,
|
|
15795
|
+
why: `Slop pattern "${pattern.id}" matched in ${scope}`,
|
|
15796
|
+
evidence: [],
|
|
15797
|
+
guidance: pattern.guidance,
|
|
15798
|
+
file: scope
|
|
15799
|
+
});
|
|
15800
|
+
}
|
|
15801
|
+
}
|
|
15802
|
+
}
|
|
15803
|
+
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
15804
|
+
}
|
|
15805
|
+
|
|
15165
15806
|
// src/core/validate.ts
|
|
15166
15807
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
15167
15808
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -15179,7 +15820,9 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
15179
15820
|
() => validateBpApDb(root, config),
|
|
15180
15821
|
() => validateUiDefinitionConsistency(root, config),
|
|
15181
15822
|
() => validateResearchSummary(root, config),
|
|
15182
|
-
() => validateAgentDefinition(root, config)
|
|
15823
|
+
() => validateAgentDefinition(root, config),
|
|
15824
|
+
() => validateDesignAudit(root, config),
|
|
15825
|
+
() => validateDesignSlop(root, config)
|
|
15183
15826
|
];
|
|
15184
15827
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
15185
15828
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -15266,15 +15909,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
15266
15909
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
15267
15910
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
15268
15911
|
async function createReportData(root, validation, configResult) {
|
|
15269
|
-
const resolvedRoot =
|
|
15912
|
+
const resolvedRoot = path54.resolve(root);
|
|
15270
15913
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
15271
15914
|
const config = resolved.config;
|
|
15272
15915
|
const configPath = resolved.configPath;
|
|
15273
15916
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
15274
15917
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
15275
|
-
const apiRoot =
|
|
15276
|
-
const uiRoot =
|
|
15277
|
-
const dbRoot =
|
|
15918
|
+
const apiRoot = path54.join(contractsRoot, "api");
|
|
15919
|
+
const uiRoot = path54.join(contractsRoot, "ui");
|
|
15920
|
+
const dbRoot = path54.join(contractsRoot, "db");
|
|
15278
15921
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
15279
15922
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
15280
15923
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -15602,6 +16245,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
15602
16245
|
lines.push("");
|
|
15603
16246
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
15604
16247
|
lines.push("- [Change Issues](#change-issues)");
|
|
16248
|
+
lines.push("- [Design Audit Findings](#design-audit-findings)");
|
|
16249
|
+
lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
|
|
15605
16250
|
lines.push("- [Change Type](#change-type)");
|
|
15606
16251
|
lines.push("- [Waivers](#waivers)");
|
|
15607
16252
|
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
@@ -15750,6 +16395,46 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
15750
16395
|
lines.push("### Issues");
|
|
15751
16396
|
lines.push("");
|
|
15752
16397
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
16398
|
+
const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
|
|
16399
|
+
if (auditIssues.length > 0) {
|
|
16400
|
+
lines.push("## Design Audit Findings");
|
|
16401
|
+
lines.push("");
|
|
16402
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
16403
|
+
for (const iss of auditIssues) {
|
|
16404
|
+
const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
|
|
16405
|
+
const group = byDimension.get(dim) ?? [];
|
|
16406
|
+
group.push(iss);
|
|
16407
|
+
byDimension.set(dim, group);
|
|
16408
|
+
}
|
|
16409
|
+
for (const [dim, dimIssues] of byDimension) {
|
|
16410
|
+
lines.push(`### ${dim}`);
|
|
16411
|
+
lines.push("");
|
|
16412
|
+
for (const iss of dimIssues) {
|
|
16413
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16414
|
+
}
|
|
16415
|
+
lines.push("");
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
|
|
16419
|
+
if (slopIssues.length > 0) {
|
|
16420
|
+
lines.push("## Slop Guardrails Findings");
|
|
16421
|
+
lines.push("");
|
|
16422
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
16423
|
+
for (const iss of slopIssues) {
|
|
16424
|
+
const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
|
|
16425
|
+
const group = byCategory.get(cat) ?? [];
|
|
16426
|
+
group.push(iss);
|
|
16427
|
+
byCategory.set(cat, group);
|
|
16428
|
+
}
|
|
16429
|
+
for (const [cat, catIssues] of byCategory) {
|
|
16430
|
+
lines.push(`### ${cat}`);
|
|
16431
|
+
lines.push("");
|
|
16432
|
+
for (const iss of catIssues) {
|
|
16433
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16434
|
+
}
|
|
16435
|
+
lines.push("");
|
|
16436
|
+
}
|
|
16437
|
+
}
|
|
15753
16438
|
lines.push("## Change Type");
|
|
15754
16439
|
lines.push("");
|
|
15755
16440
|
lines.push("### Summary");
|
|
@@ -16121,6 +16806,20 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16121
16806
|
} else {
|
|
16122
16807
|
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");
|
|
16123
16808
|
}
|
|
16809
|
+
const renderEvidenceIssues = data.issues.filter(
|
|
16810
|
+
(item) => ["QFAI-PROT-101", "QFAI-PROT-244", "QFAI-PROT-245"].includes(item.code)
|
|
16811
|
+
);
|
|
16812
|
+
if (renderEvidenceIssues.length > 0) {
|
|
16813
|
+
lines.push(
|
|
16814
|
+
"- 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"
|
|
16815
|
+
);
|
|
16816
|
+
lines.push(
|
|
16817
|
+
"- 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"
|
|
16818
|
+
);
|
|
16819
|
+
lines.push(
|
|
16820
|
+
"- 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"
|
|
16821
|
+
);
|
|
16822
|
+
}
|
|
16124
16823
|
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/18_delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
16125
16824
|
lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`");
|
|
16126
16825
|
return lines.join("\n");
|
|
@@ -16155,7 +16854,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
16155
16854
|
};
|
|
16156
16855
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
16157
16856
|
for (const deltaFile of deltaFiles) {
|
|
16158
|
-
const text = await
|
|
16857
|
+
const text = await readFile42(deltaFile, "utf-8");
|
|
16159
16858
|
const parsed = parseDeltaV1(text);
|
|
16160
16859
|
for (const entry of parsed.entries) {
|
|
16161
16860
|
if (!entry.meta) {
|
|
@@ -16192,7 +16891,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
16192
16891
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
16193
16892
|
}
|
|
16194
16893
|
for (const file of specFiles) {
|
|
16195
|
-
const text = await
|
|
16894
|
+
const text = await readFile42(file, "utf-8");
|
|
16196
16895
|
const parsed = parseSpec(text, file);
|
|
16197
16896
|
const specKey = parsed.specId;
|
|
16198
16897
|
if (!specKey) {
|
|
@@ -16229,7 +16928,7 @@ async function collectIds(files) {
|
|
|
16229
16928
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
16230
16929
|
}
|
|
16231
16930
|
for (const file of files) {
|
|
16232
|
-
const text = await
|
|
16931
|
+
const text = await readFile42(file, "utf-8");
|
|
16233
16932
|
for (const prefix of ID_PREFIXES) {
|
|
16234
16933
|
const ids = extractIds(text, prefix);
|
|
16235
16934
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -16244,7 +16943,7 @@ async function collectIds(files) {
|
|
|
16244
16943
|
async function collectUpstreamIds(files) {
|
|
16245
16944
|
const ids = /* @__PURE__ */ new Set();
|
|
16246
16945
|
for (const file of files) {
|
|
16247
|
-
const text = await
|
|
16946
|
+
const text = await readFile42(file, "utf-8");
|
|
16248
16947
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
16249
16948
|
}
|
|
16250
16949
|
return ids;
|
|
@@ -16265,7 +16964,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
16265
16964
|
}
|
|
16266
16965
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
16267
16966
|
for (const file of targetFiles) {
|
|
16268
|
-
const text = await
|
|
16967
|
+
const text = await readFile42(file, "utf-8");
|
|
16269
16968
|
if (pattern.test(text)) {
|
|
16270
16969
|
return true;
|
|
16271
16970
|
}
|
|
@@ -16384,7 +17083,7 @@ function normalizeScSources(root, sources) {
|
|
|
16384
17083
|
async function countScenarios(scenarioFiles) {
|
|
16385
17084
|
let total = 0;
|
|
16386
17085
|
for (const file of scenarioFiles) {
|
|
16387
|
-
const text = await
|
|
17086
|
+
const text = await readFile42(file, "utf-8");
|
|
16388
17087
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16389
17088
|
if (!document || errors.length > 0) {
|
|
16390
17089
|
continue;
|
|
@@ -16415,7 +17114,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
16415
17114
|
let totalScenarios = 0;
|
|
16416
17115
|
let e2eCount = 0;
|
|
16417
17116
|
for (const file of scenarioFiles) {
|
|
16418
|
-
const text = await
|
|
17117
|
+
const text = await readFile42(file, "utf-8");
|
|
16419
17118
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16420
17119
|
if (!document || errors.length > 0) {
|
|
16421
17120
|
continue;
|
|
@@ -16503,10 +17202,10 @@ function buildHotspots(issues) {
|
|
|
16503
17202
|
async function collectTddCoverage(entries) {
|
|
16504
17203
|
const specs = [];
|
|
16505
17204
|
for (const entry of entries) {
|
|
16506
|
-
const testCasesPath =
|
|
17205
|
+
const testCasesPath = path54.join(entry.dir, "06_Test-Cases.md");
|
|
16507
17206
|
let tcContent;
|
|
16508
17207
|
try {
|
|
16509
|
-
tcContent = await
|
|
17208
|
+
tcContent = await readFile42(testCasesPath, "utf-8");
|
|
16510
17209
|
} catch {
|
|
16511
17210
|
continue;
|
|
16512
17211
|
}
|
|
@@ -16538,10 +17237,10 @@ async function collectTddCoverage(entries) {
|
|
|
16538
17237
|
});
|
|
16539
17238
|
continue;
|
|
16540
17239
|
}
|
|
16541
|
-
const tddListPath =
|
|
17240
|
+
const tddListPath = path54.join(entry.dir, "tdd", "test-list.md");
|
|
16542
17241
|
let tddContent;
|
|
16543
17242
|
try {
|
|
16544
|
-
tddContent = await
|
|
17243
|
+
tddContent = await readFile42(tddListPath, "utf-8");
|
|
16545
17244
|
} catch {
|
|
16546
17245
|
specs.push({
|
|
16547
17246
|
specNumber: entry.specNumber,
|