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/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 SHORT_US_ID_RE = /^US-\d{4}$/;
1602
- var SHORT_TC_ID_RE = /^TC-\d{4}$/;
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}\b/g : /\bTC-\d{4}\b/g;
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" && SHORT_US_ID_RE.test(normalized) || prefix === "TC" && SHORT_TC_ID_RE.test(normalized)) {
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: /\bCAP-[A-Za-z0-9_-]+\b/gi,
2455
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
2456
- US: /\bUS-[A-Za-z0-9_-]+\b/gi,
2457
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
2458
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
2459
- AC: /\bAC-[A-Za-z0-9_-]+\b/gi,
2460
- CASE: /\bCASE-[A-Za-z0-9_-]+\b/gi,
2461
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
2462
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
2463
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
2464
- THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
2465
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
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 import_promises49 = require("fs/promises");
3508
- var import_node_path52 = __toESM(require("path"), 1);
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.0".length > 0) {
4267
- return "1.7.0";
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 US_ID_RE = /\bUS-\d{4}-\d{4}\b/g;
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
- US_ID_RE
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}\b/gi,
8300
- br: /\bBR-\d{4}\b/gi,
8301
- ex: /\bEX-\d{4}\b/gi
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 US_ID_RE2 = /^US-\d{4}$/;
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", US_ID_RE2, "US")
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 path53 = `${prefix}.${key}`;
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(path53, token);
11517
+ target.set(path55, token);
11165
11518
  } else {
11166
- flattenTokens(record2, path53, target, errors);
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 [path53] of allTokens) {
11177
- resolveTokenRef(path53, allTokens, /* @__PURE__ */ new Set(), 0, result);
11529
+ for (const [path55] of allTokens) {
11530
+ resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
11178
11531
  }
11179
11532
  }
11180
- function resolveTokenRef(path53, allTokens, visited, depth, result) {
11181
- if (result.resolved.has(path53)) {
11182
- return result.resolved.get(path53);
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: ${path53}`,
11187
- path: path53
11539
+ message: `Max reference depth exceeded at: ${path55}`,
11540
+ path: path55
11188
11541
  });
11189
11542
  return void 0;
11190
11543
  }
11191
- if (visited.has(path53)) {
11544
+ if (visited.has(path55)) {
11192
11545
  result.errors.push({
11193
- message: `Circular reference detected: ${path53}`,
11194
- path: path53
11546
+ message: `Circular reference detected: ${path55}`,
11547
+ path: path55
11195
11548
  });
11196
11549
  return void 0;
11197
11550
  }
11198
- const token = allTokens.get(path53);
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(path53, rawValue2);
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(path53, rawValue);
11563
+ result.resolved.set(path55, rawValue);
11211
11564
  return rawValue;
11212
11565
  }
11213
- visited.add(path53);
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 ${path53}`,
11222
- path: path53
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(path53, resolved);
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 import_promises48 = require("fs/promises");
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, import_promises48.readFile)(filePath, "utf-8");
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 = import_node_path52.default.resolve(root);
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 = import_node_path52.default.join(contractsRoot, "api");
15357
- const uiRoot = import_node_path52.default.join(contractsRoot, "ui");
15358
- const dbRoot = import_node_path52.default.join(contractsRoot, "db");
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, import_promises49.readFile)(deltaFile, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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, import_promises49.readFile)(file, "utf-8");
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 = import_node_path52.default.join(entry.dir, "06_Test-Cases.md");
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, import_promises49.readFile)(testCasesPath, "utf-8");
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 = import_node_path52.default.join(entry.dir, "tdd", "test-list.md");
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, import_promises49.readFile)(tddListPath, "utf-8");
17324
+ tddContent = await (0, import_promises52.readFile)(tddListPath, "utf-8");
16626
17325
  } catch {
16627
17326
  specs.push({
16628
17327
  specNumber: entry.specNumber,