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.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 SHORT_US_ID_RE = /^US-\d{4}$/;
1521
- var SHORT_TC_ID_RE = /^TC-\d{4}$/;
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}\b/g : /\bTC-\d{4}\b/g;
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" && SHORT_US_ID_RE.test(normalized) || prefix === "TC" && SHORT_TC_ID_RE.test(normalized)) {
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: /\bCAP-[A-Za-z0-9_-]+\b/gi,
2374
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
2375
- US: /\bUS-[A-Za-z0-9_-]+\b/gi,
2376
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
2377
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
2378
- AC: /\bAC-[A-Za-z0-9_-]+\b/gi,
2379
- CASE: /\bCASE-[A-Za-z0-9_-]+\b/gi,
2380
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
2381
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
2382
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
2383
- THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
2384
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
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 readFile40 } from "fs/promises";
3427
- import path52 from "path";
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.0".length > 0) {
4186
- return "1.7.0";
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 US_ID_RE = /\bUS-\d{4}-\d{4}\b/g;
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
- US_ID_RE
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}\b/gi,
8219
- br: /\bBR-\d{4}\b/gi,
8220
- ex: /\bEX-\d{4}\b/gi
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 US_ID_RE2 = /^US-\d{4}$/;
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", US_ID_RE2, "US")
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 path53 = `${prefix}.${key}`;
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(path53, token);
11436
+ target.set(path55, token);
11084
11437
  } else {
11085
- flattenTokens(record2, path53, target, errors);
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 [path53] of allTokens) {
11096
- resolveTokenRef(path53, allTokens, /* @__PURE__ */ new Set(), 0, result);
11448
+ for (const [path55] of allTokens) {
11449
+ resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
11097
11450
  }
11098
11451
  }
11099
- function resolveTokenRef(path53, allTokens, visited, depth, result) {
11100
- if (result.resolved.has(path53)) {
11101
- return result.resolved.get(path53);
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: ${path53}`,
11106
- path: path53
11458
+ message: `Max reference depth exceeded at: ${path55}`,
11459
+ path: path55
11107
11460
  });
11108
11461
  return void 0;
11109
11462
  }
11110
- if (visited.has(path53)) {
11463
+ if (visited.has(path55)) {
11111
11464
  result.errors.push({
11112
- message: `Circular reference detected: ${path53}`,
11113
- path: path53
11465
+ message: `Circular reference detected: ${path55}`,
11466
+ path: path55
11114
11467
  });
11115
11468
  return void 0;
11116
11469
  }
11117
- const token = allTokens.get(path53);
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(path53, rawValue2);
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(path53, rawValue);
11482
+ result.resolved.set(path55, rawValue);
11130
11483
  return rawValue;
11131
11484
  }
11132
- visited.add(path53);
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 ${path53}`,
11141
- path: path53
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(path53, resolved);
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 readFile39 } from "fs/promises";
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 readFile39(filePath, "utf-8");
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 = path52.resolve(root);
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 = path52.join(contractsRoot, "api");
15276
- const uiRoot = path52.join(contractsRoot, "ui");
15277
- const dbRoot = path52.join(contractsRoot, "db");
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 readFile40(deltaFile, "utf-8");
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 readFile40(file, "utf-8");
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 readFile40(file, "utf-8");
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 readFile40(file, "utf-8");
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 readFile40(file, "utf-8");
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 readFile40(file, "utf-8");
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 readFile40(file, "utf-8");
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 = path52.join(entry.dir, "06_Test-Cases.md");
17205
+ const testCasesPath = path54.join(entry.dir, "06_Test-Cases.md");
16507
17206
  let tcContent;
16508
17207
  try {
16509
- tcContent = await readFile40(testCasesPath, "utf-8");
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 = path52.join(entry.dir, "tdd", "test-list.md");
17240
+ const tddListPath = path54.join(entry.dir, "tdd", "test-list.md");
16542
17241
  let tddContent;
16543
17242
  try {
16544
- tddContent = await readFile40(tddListPath, "utf-8");
17243
+ tddContent = await readFile42(tddListPath, "utf-8");
16545
17244
  } catch {
16546
17245
  specs.push({
16547
17246
  specNumber: entry.specNumber,