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.
@@ -12,6 +12,22 @@ import path10 from "path";
12
12
  import { access, readFile } from "fs/promises";
13
13
  import path from "path";
14
14
  import { parse as parseYaml } from "yaml";
15
+
16
+ // src/core/uiux/renderEvidenceTypes.ts
17
+ var DEFAULT_RENDER_VIEWPORTS = ["desktop", "mobile"];
18
+ function normalizeRenderViewports(viewports) {
19
+ const normalized = Array.isArray(viewports) ? viewports.map((item) => item.trim()).filter((item) => item.length > 0) : [];
20
+ if (normalized.length > 0) {
21
+ return Array.from(new Set(normalized));
22
+ }
23
+ return [...DEFAULT_RENDER_VIEWPORTS];
24
+ }
25
+ function looksLikeInlineRenderPayload(value) {
26
+ const trimmed = value.trim().toLowerCase();
27
+ return trimmed.startsWith("data:image") || trimmed.includes("<html");
28
+ }
29
+
30
+ // src/core/config.ts
15
31
  var defaultConfig = {
16
32
  paths: {
17
33
  contractsDir: ".qfai/contracts",
@@ -461,6 +477,133 @@ function normalizeUiux(raw, configPath, issues) {
461
477
  );
462
478
  }
463
479
  }
480
+ if (raw.renderEvidence !== void 0) {
481
+ const renderEvidence = normalizeRenderEvidence(raw.renderEvidence, configPath, issues);
482
+ if (renderEvidence) {
483
+ result.renderEvidence = renderEvidence;
484
+ }
485
+ }
486
+ if (raw.audit !== void 0) {
487
+ const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
488
+ if (audit) {
489
+ result.audit = audit;
490
+ }
491
+ }
492
+ return Object.keys(result).length > 0 ? result : void 0;
493
+ }
494
+ function normalizeUiuxAudit(raw, configPath, issues) {
495
+ if (!isRecord(raw)) {
496
+ issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
497
+ return void 0;
498
+ }
499
+ const result = {};
500
+ if (raw.enabled !== void 0) {
501
+ if (typeof raw.enabled === "boolean") {
502
+ result.enabled = raw.enabled;
503
+ } else {
504
+ issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
505
+ }
506
+ }
507
+ if (raw.slopDetection !== void 0) {
508
+ if (typeof raw.slopDetection === "boolean") {
509
+ result.slopDetection = raw.slopDetection;
510
+ } else {
511
+ issues.push(
512
+ configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
513
+ );
514
+ }
515
+ }
516
+ if (raw.maxPrimaryCtas !== void 0) {
517
+ if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
518
+ result.maxPrimaryCtas = raw.maxPrimaryCtas;
519
+ } else {
520
+ issues.push(
521
+ configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
522
+ );
523
+ }
524
+ }
525
+ if (raw.maxRawTokenLiteralWarnings !== void 0) {
526
+ if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
527
+ result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
528
+ } else {
529
+ issues.push(
530
+ configIssue(
531
+ configPath,
532
+ "uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
533
+ )
534
+ );
535
+ }
536
+ }
537
+ if (raw.maxDuplicateFindingsPerRule !== void 0) {
538
+ if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
539
+ result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
540
+ } else {
541
+ issues.push(
542
+ configIssue(
543
+ configPath,
544
+ "uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
545
+ )
546
+ );
547
+ }
548
+ }
549
+ return Object.keys(result).length > 0 ? result : void 0;
550
+ }
551
+ function normalizeRenderEvidence(raw, configPath, issues) {
552
+ if (!isRecord(raw)) {
553
+ issues.push(
554
+ configIssue(configPath, "uiux.renderEvidence \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
555
+ );
556
+ return void 0;
557
+ }
558
+ const result = {};
559
+ if (raw.enabled !== void 0) {
560
+ if (typeof raw.enabled === "boolean") {
561
+ result.enabled = raw.enabled;
562
+ } else {
563
+ issues.push(
564
+ configIssue(configPath, "uiux.renderEvidence.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
565
+ );
566
+ }
567
+ }
568
+ if (raw.viewports !== void 0) {
569
+ if (Array.isArray(raw.viewports) && raw.viewports.every((item) => typeof item === "string")) {
570
+ result.viewports = normalizeRenderViewports(raw.viewports);
571
+ } else {
572
+ issues.push(
573
+ configIssue(configPath, "uiux.renderEvidence.viewports \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
574
+ );
575
+ }
576
+ }
577
+ if (raw.out !== void 0) {
578
+ if (typeof raw.out === "string" && raw.out.trim().length > 0) {
579
+ result.out = raw.out.trim();
580
+ } else {
581
+ issues.push(
582
+ configIssue(configPath, "uiux.renderEvidence.out \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
583
+ );
584
+ }
585
+ }
586
+ if (raw.baseUrl !== void 0) {
587
+ if (typeof raw.baseUrl === "string" && raw.baseUrl.trim().length > 0) {
588
+ result.baseUrl = raw.baseUrl.trim();
589
+ } else {
590
+ issues.push(
591
+ configIssue(
592
+ configPath,
593
+ "uiux.renderEvidence.baseUrl \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
594
+ )
595
+ );
596
+ }
597
+ }
598
+ if (raw.failOpen !== void 0) {
599
+ if (typeof raw.failOpen === "boolean") {
600
+ result.failOpen = raw.failOpen;
601
+ } else {
602
+ issues.push(
603
+ configIssue(configPath, "uiux.renderEvidence.failOpen \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
604
+ );
605
+ }
606
+ }
464
607
  return Object.keys(result).length > 0 ? result : void 0;
465
608
  }
466
609
  function configIssue(file, message) {
@@ -1541,8 +1684,8 @@ import { readFile as readFile5 } from "fs/promises";
1541
1684
  import path8 from "path";
1542
1685
  import { fileURLToPath as fileURLToPath2 } from "url";
1543
1686
  async function resolveToolVersion() {
1544
- if ("1.7.0".length > 0) {
1545
- return "1.7.0";
1687
+ if ("1.7.2".length > 0) {
1688
+ return "1.7.2";
1546
1689
  }
1547
1690
  try {
1548
1691
  const packagePath = resolvePackageJsonPath();
@@ -3311,6 +3454,7 @@ var ID_PREFIXES = [
3311
3454
  "DB",
3312
3455
  "THEMA"
3313
3456
  ];
3457
+ var DIGIT_AHEAD = "(?=[A-Za-z0-9_-]*\\d)";
3314
3458
  var STRICT_ID_PATTERNS = {
3315
3459
  CAP: /\bCAP-\d{4}\b/g,
3316
3460
  SPEC: /\bSPEC-\d{4}\b/g,
@@ -3326,18 +3470,18 @@ var STRICT_ID_PATTERNS = {
3326
3470
  ADR: /\bADR-\d{4}\b/g
3327
3471
  };
3328
3472
  var LOOSE_ID_PATTERNS = {
3329
- CAP: /\bCAP-[A-Za-z0-9_-]+\b/gi,
3330
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
3331
- US: /\bUS-[A-Za-z0-9_-]+\b/gi,
3332
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
3333
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
3334
- AC: /\bAC-[A-Za-z0-9_-]+\b/gi,
3335
- CASE: /\bCASE-[A-Za-z0-9_-]+\b/gi,
3336
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
3337
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
3338
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
3339
- THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
3340
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
3473
+ CAP: new RegExp(`\\bCAP-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3474
+ SPEC: new RegExp(`\\bSPEC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3475
+ US: new RegExp(`\\bUS-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3476
+ BR: new RegExp(`\\bBR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3477
+ SC: new RegExp(`\\bSC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3478
+ AC: new RegExp(`\\bAC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3479
+ CASE: new RegExp(`\\bCASE-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3480
+ UI: new RegExp(`\\bUI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3481
+ API: new RegExp(`\\bAPI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3482
+ DB: new RegExp(`\\bDB-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3483
+ THEMA: new RegExp(`\\bTHEMA-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3484
+ ADR: new RegExp(`\\bADR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi")
3341
3485
  };
3342
3486
  function extractIds(text, prefix) {
3343
3487
  const pattern = STRICT_ID_PATTERNS[prefix];
@@ -3919,6 +4063,8 @@ function formatError4(error2) {
3919
4063
  var ENV_AUTOGEN = "QFAI_PROTOTYPE_FIDELITY_AUTOGEN";
3920
4064
  var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
3921
4065
  async function runPrototyping(options) {
4066
+ const { config } = await loadConfig(options.root);
4067
+ const renderOptions = mergeRenderOptions(options, config.uiux?.renderEvidence);
3922
4068
  const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
3923
4069
  if (!autogenEnabled) {
3924
4070
  if (options.autogenOnly) {
@@ -3949,16 +4095,24 @@ async function runPrototyping(options) {
3949
4095
  status: "skipped",
3950
4096
  reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
3951
4097
  });
3952
- await writeEvidence(evidencePath2, skippedEvidence);
4098
+ const renderBundle2 = await maybeWriteRenderBundle(
4099
+ toolVersion2,
4100
+ "qfai prototyping --render-evidence",
4101
+ renderOptions,
4102
+ false
4103
+ );
4104
+ await writeEvidence(
4105
+ evidencePath2,
4106
+ applyRenderEvidence(skippedEvidence, renderOptions, false, renderBundle2?.path)
4107
+ );
3953
4108
  return 0;
3954
4109
  }
3955
- const baseUrl = resolveBaseUrl(options);
4110
+ const baseUrl = resolveBaseUrl(renderOptions);
3956
4111
  if (!baseUrl) {
3957
4112
  error(`prototyping: --base-url \u307E\u305F\u306F QFAI_PROTOTYPE_BASE_URL \u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002`);
3958
4113
  return 1;
3959
4114
  }
3960
4115
  const toolVersion = await resolveToolVersion();
3961
- const { config } = await loadConfig(options.root);
3962
4116
  const evidencePath = resolveEvidencePath(options.root, options.evidenceOut);
3963
4117
  info(`prototyping: autogen uiFidelity \u3092\u5B9F\u884C\u3057\u307E\u3059 (baseUrl=${baseUrl})`);
3964
4118
  let existingEvidence = {};
@@ -3985,7 +4139,16 @@ async function runPrototyping(options) {
3985
4139
  status: "failed",
3986
4140
  reason
3987
4141
  });
3988
- await writeEvidence(evidencePath, failedEvidence);
4142
+ const renderBundle2 = await maybeWriteRenderBundle(
4143
+ toolVersion,
4144
+ "qfai prototyping --render-evidence",
4145
+ renderOptions,
4146
+ true
4147
+ );
4148
+ await writeEvidence(
4149
+ evidencePath,
4150
+ applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
4151
+ );
3989
4152
  info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
3990
4153
  return options.autogenOnly ? 1 : 0;
3991
4154
  }
@@ -4004,7 +4167,16 @@ async function runPrototyping(options) {
4004
4167
  crawled: result.crawled,
4005
4168
  reason
4006
4169
  });
4007
- await writeEvidence(evidencePath, failedEvidence);
4170
+ const renderBundle2 = await maybeWriteRenderBundle(
4171
+ toolVersion,
4172
+ "qfai prototyping --render-evidence",
4173
+ renderOptions,
4174
+ true
4175
+ );
4176
+ await writeEvidence(
4177
+ evidencePath,
4178
+ applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
4179
+ );
4008
4180
  info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4009
4181
  return options.autogenOnly ? 1 : 0;
4010
4182
  }
@@ -4017,7 +4189,16 @@ async function runPrototyping(options) {
4017
4189
  screens: result.screens,
4018
4190
  crawled: result.crawled
4019
4191
  });
4020
- await writeEvidence(evidencePath, successEvidence);
4192
+ const renderBundle = await maybeWriteRenderBundle(
4193
+ toolVersion,
4194
+ "qfai prototyping --render-evidence",
4195
+ renderOptions,
4196
+ true
4197
+ );
4198
+ await writeEvidence(
4199
+ evidencePath,
4200
+ applyRenderEvidence(successEvidence, renderOptions, true, renderBundle?.path)
4201
+ );
4021
4202
  const routeOkCount = result.crawled.filter((r) => r.status === "ok").length;
4022
4203
  const routeFailCount = result.crawled.filter((r) => r.status === "failed").length;
4023
4204
  const avgCoverage = result.screens.length > 0 ? (result.screens.reduce((sum, s) => sum + s.coverage, 0) / result.screens.length).toFixed(2) : "N/A";
@@ -4047,6 +4228,75 @@ async function writeEvidence(filePath, evidence) {
4047
4228
  await mkdir4(path17.dirname(filePath), { recursive: true });
4048
4229
  await writeFile3(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4049
4230
  }
4231
+ async function writeRenderBundle(filePath, bundle) {
4232
+ await mkdir4(path17.dirname(filePath), { recursive: true });
4233
+ await writeFile3(filePath, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
4234
+ }
4235
+ async function maybeWriteRenderBundle(toolVersion, command, options, autogenEnabled) {
4236
+ if (!options.renderEvidence) {
4237
+ return void 0;
4238
+ }
4239
+ const renderBundle = buildRenderBundle(toolVersion, command, options, autogenEnabled);
4240
+ await writeRenderBundle(renderBundle.path, renderBundle.bundle);
4241
+ return renderBundle;
4242
+ }
4243
+ function applyRenderEvidence(evidence, options, autogenEnabled, outputPath) {
4244
+ if (!options.renderEvidence) {
4245
+ return evidence;
4246
+ }
4247
+ const viewports = normalizeRenderViewports(options.renderViewports);
4248
+ return {
4249
+ ...evidence,
4250
+ renderEvidence: {
4251
+ status: autogenEnabled ? "requested" : "skipped",
4252
+ requested: true,
4253
+ autogenEnabled,
4254
+ viewports,
4255
+ outputPath: outputPath ?? resolveRenderOutPath(options.root, options.renderOut),
4256
+ reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
4257
+ }
4258
+ };
4259
+ }
4260
+ function buildRenderBundle(toolVersion, command, options, autogenEnabled) {
4261
+ const path64 = resolveRenderOutPath(options.root, options.renderOut);
4262
+ return {
4263
+ path: path64,
4264
+ bundle: {
4265
+ meta: {
4266
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4267
+ toolVersion,
4268
+ commands: [command]
4269
+ },
4270
+ renderEvidence: {
4271
+ status: autogenEnabled ? "requested" : "skipped",
4272
+ requested: true,
4273
+ autogenEnabled,
4274
+ viewports: normalizeRenderViewports(options.renderViewports),
4275
+ outputPath: path64,
4276
+ reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
4277
+ }
4278
+ }
4279
+ };
4280
+ }
4281
+ function mergeRenderOptions(options, configRenderEvidence) {
4282
+ const cliViewportsSpecified = Array.isArray(options.renderViewports);
4283
+ const mergedViewports = cliViewportsSpecified ? normalizeRenderViewports(options.renderViewports) : normalizeRenderViewports(configRenderEvidence?.viewports);
4284
+ const renderOut = options.renderOut ?? configRenderEvidence?.out;
4285
+ const baseUrl = options.baseUrl ?? configRenderEvidence?.baseUrl;
4286
+ return {
4287
+ ...options,
4288
+ renderEvidence: options.renderEvidence || configRenderEvidence?.enabled === true,
4289
+ renderViewports: mergedViewports,
4290
+ ...renderOut ? { renderOut } : {},
4291
+ ...baseUrl ? { baseUrl } : {}
4292
+ };
4293
+ }
4294
+ function resolveRenderOutPath(root, explicit) {
4295
+ if (explicit) {
4296
+ return path17.isAbsolute(explicit) ? explicit : path17.resolve(root, explicit);
4297
+ }
4298
+ return path17.resolve(root, ".qfai/evidence/render.json");
4299
+ }
4050
4300
  function extractRouteHintsFromEvidence(evidence) {
4051
4301
  const routes = /* @__PURE__ */ new Set();
4052
4302
  const runtimeGate = evidence.runtimeGate;
@@ -4088,8 +4338,8 @@ function extractRouteHintsFromEvidence(evidence) {
4088
4338
  }
4089
4339
 
4090
4340
  // src/cli/commands/report.ts
4091
- import { mkdir as mkdir8, readFile as readFile43, writeFile as writeFile7 } from "fs/promises";
4092
- import path59 from "path";
4341
+ import { mkdir as mkdir8, readFile as readFile45, writeFile as writeFile7 } from "fs/promises";
4342
+ import path61 from "path";
4093
4343
 
4094
4344
  // src/core/normalize.ts
4095
4345
  function normalizeIssuePaths(root, issues) {
@@ -4184,8 +4434,8 @@ async function createPhaseGuardResult(phase, blockedIssue) {
4184
4434
  }
4185
4435
 
4186
4436
  // src/core/report.ts
4187
- import { readFile as readFile41 } from "fs/promises";
4188
- import path57 from "path";
4437
+ import { readFile as readFile43 } from "fs/promises";
4438
+ import path59 from "path";
4189
4439
 
4190
4440
  // src/core/contractIndex.ts
4191
4441
  import { readFile as readFile10 } from "fs/promises";
@@ -8365,11 +8615,11 @@ import path29 from "path";
8365
8615
  // src/core/atddTraceability.ts
8366
8616
  import { readFile as readFile20 } from "fs/promises";
8367
8617
  import path28 from "path";
8368
- var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4})\b/g;
8369
- var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4})\b/g;
8618
+ var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
8619
+ var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
8370
8620
  var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
8371
- var SHORT_US_ID_RE = /^US-\d{4}$/;
8372
- var SHORT_TC_ID_RE = /^TC-\d{4}$/;
8621
+ var US_ID_RE2 = /^US-\d{4}(?:-\d{4})?$/;
8622
+ var TC_ID_RE = /^TC-\d{4}(?:-\d{4})?$/;
8373
8623
  var API_CONTRACT_ID_RE = /^CON-API-\d+$/;
8374
8624
  var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
8375
8625
  async function evaluateAtddCodeTraceability(root, config) {
@@ -8517,11 +8767,11 @@ async function collectApiContractIds(apiRoot) {
8517
8767
  function collectShortIds(text, prefix) {
8518
8768
  const ids = /* @__PURE__ */ new Set();
8519
8769
  const headingIds = collectMarkdownItems(text, prefix).map((item) => item.id);
8520
- const pattern = prefix === "US" ? /\bUS-\d{4}\b/g : /\bTC-\d{4}\b/g;
8770
+ const pattern = prefix === "US" ? /\bUS-\d{4}(?:-\d{4})?\b/g : /\bTC-\d{4}(?:-\d{4})?\b/g;
8521
8771
  const looseIds = uniqueMatches(text, pattern);
8522
8772
  for (const id of [...headingIds, ...looseIds]) {
8523
8773
  const normalized = id.toUpperCase();
8524
- if (prefix === "US" && SHORT_US_ID_RE.test(normalized) || prefix === "TC" && SHORT_TC_ID_RE.test(normalized)) {
8774
+ if (prefix === "US" && US_ID_RE2.test(normalized) || prefix === "TC" && TC_ID_RE.test(normalized)) {
8525
8775
  ids.add(normalized);
8526
8776
  }
8527
8777
  }
@@ -9530,15 +9780,15 @@ import path33 from "path";
9530
9780
  import { mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
9531
9781
  import path34 from "path";
9532
9782
  var ID_PATTERNS = {
9533
- us: /^US-\d{4}$/,
9534
- ac: /^AC-\d{4}$/,
9535
- br: /^BR-\d{4}$/,
9536
- ex: /^EX-\d{4}$/
9783
+ us: /^US-\d{4}(?:-\d{4})?$/,
9784
+ ac: /^AC-\d{4}(?:-\d{4})?$/,
9785
+ br: /^BR-\d{4}(?:-\d{4})?$/,
9786
+ ex: /^EX-\d{4}(?:-\d{4})?$/
9537
9787
  };
9538
9788
  var V1421_REFS = {
9539
- ac: /\bAC-\d{4}\b/gi,
9540
- br: /\bBR-\d{4}\b/gi,
9541
- ex: /\bEX-\d{4}\b/gi
9789
+ ac: /\bAC-\d{4}(?:-\d{4})?\b/gi,
9790
+ br: /\bBR-\d{4}(?:-\d{4})?\b/gi,
9791
+ ex: /\bEX-\d{4}(?:-\d{4})?\b/gi
9542
9792
  };
9543
9793
  async function validateLayerCoverage(root, config) {
9544
9794
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -9811,11 +10061,11 @@ function parseAcceptanceCriteriaIds2(text) {
9811
10061
  const ids = /* @__PURE__ */ new Set();
9812
10062
  const lines = text.replace(/\r\n/g, "\n").split("\n");
9813
10063
  for (const line of lines) {
9814
- const headingMatch = /^##\s*(AC-\d{4})\b/i.exec(line.trim());
10064
+ const headingMatch = /^##\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line.trim());
9815
10065
  if (headingMatch?.[1]) {
9816
10066
  ids.add(headingMatch[1].toUpperCase());
9817
10067
  }
9818
- const commentMatch = /^\s*#\s*(AC-\d{4})\b/i.exec(line);
10068
+ const commentMatch = /^\s*#\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line);
9819
10069
  if (commentMatch?.[1]) {
9820
10070
  ids.add(commentMatch[1].toUpperCase());
9821
10071
  }
@@ -9835,8 +10085,8 @@ function parseAcceptanceCriteriaIds2(text) {
9835
10085
  function parseDefinitionRefs(text, prefix, refPattern, options = {}) {
9836
10086
  const lines = text.replace(/\r\n/g, "\n").split("\n");
9837
10087
  const refsById = /* @__PURE__ */ new Map();
9838
- const idPattern = new RegExp(`^${prefix}-\\d{4}$`);
9839
- const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4})\\b`, "i");
10088
+ const idPattern = new RegExp(`^${prefix}-\\d{4}(?:-\\d{4})?$`);
10089
+ const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4}(?:-\\d{4})?)\\b`, "i");
9840
10090
  const referenceColumns = new Set(
9841
10091
  (options.referenceColumns ?? []).map((column) => normalizeColumnName(column))
9842
10092
  );
@@ -10237,7 +10487,7 @@ var US_DOWNSTREAM_RE = /\b(?:AC|BR|EX|TC)-\d{4}\b/g;
10237
10487
  var AC_DOWNSTREAM_RE = /\b(?:BR|EX|TC)-\d{4}\b/g;
10238
10488
  var BR_DOWNSTREAM_RE = /\b(?:EX|TC)-\d{4}\b/g;
10239
10489
  var CAP_ID_RE = /^CAP-\d{4}$/;
10240
- var US_ID_RE2 = /^US-\d{4}$/;
10490
+ var US_ID_RE3 = /^US-\d{4}$/;
10241
10491
  var AC_ID_RE4 = /^AC-\d{4}$/;
10242
10492
  var BR_OR_AC_ID_RE = /^(?:BR|AC)-\d{4}$/;
10243
10493
  var EX_ID_RE2 = /^EX-\d{4}$/;
@@ -10276,7 +10526,7 @@ async function validateLayeredTraceability(root, config) {
10276
10526
  ...await validateMarkdownParentFormat(entry.userStoriesPath, "US", CAP_ID_RE, "CAP")
10277
10527
  );
10278
10528
  issues.push(
10279
- ...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE2, "US")
10529
+ ...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE3, "US")
10280
10530
  );
10281
10531
  issues.push(
10282
10532
  ...await validateMarkdownParentFormat(entry.businessRulesPath, "BR", AC_ID_RE4, "AC")
@@ -11082,7 +11332,7 @@ function validateExParentExists(filePath, exItems, acIds, brIds) {
11082
11332
  }
11083
11333
 
11084
11334
  // src/core/validators/prototypingEvidence.ts
11085
- import { readFile as readFile26 } from "fs/promises";
11335
+ import { access as access12, readFile as readFile26 } from "fs/promises";
11086
11336
  import path39 from "path";
11087
11337
  var EVIDENCE_MARKDOWN_FILE = "prototyping.md";
11088
11338
  var EVIDENCE_JSON_FILE = "prototyping.json";
@@ -11588,6 +11838,100 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
11588
11838
  )
11589
11839
  );
11590
11840
  }
11841
+ const renderIssues = await validateRenderEvidenceScreens(
11842
+ root,
11843
+ config,
11844
+ evidenceJsonPath,
11845
+ uiFidelity.screens
11846
+ );
11847
+ issues.push(...renderIssues);
11848
+ return issues;
11849
+ }
11850
+ async function validateRenderEvidenceScreens(root, config, evidenceJsonPath, screens) {
11851
+ const issues = [];
11852
+ const hasAnyRenderEvidence = screens.some((screen) => screen.renders.length > 0);
11853
+ if (!hasAnyRenderEvidence) {
11854
+ return issues;
11855
+ }
11856
+ const qualityProfile = config.uiux?.qualityProfile ?? "default";
11857
+ for (const screen of screens) {
11858
+ if (screen.renders.length === 0) {
11859
+ continue;
11860
+ }
11861
+ const viewports = new Set(screen.renders.map((render) => render.viewport));
11862
+ const missingDefaultViewports = DEFAULT_RENDER_VIEWPORTS.filter(
11863
+ (viewport) => !viewports.has(viewport)
11864
+ );
11865
+ const allSkipped = screen.renders.every((render) => render.status === "skipped");
11866
+ for (const render of screen.renders) {
11867
+ if (render.status !== "captured") {
11868
+ continue;
11869
+ }
11870
+ const invalidPaths = [
11871
+ { label: "imagePath", value: render.imagePath },
11872
+ { label: "htmlPath", value: render.htmlPath }
11873
+ ].filter((entry) => looksLikeInlineRenderPayload(entry.value));
11874
+ if (invalidPaths.length > 0) {
11875
+ issues.push(
11876
+ issue(
11877
+ "QFAI-PROT-244",
11878
+ `QFAI-PROT-244: render evidence must be path-only. route=${screen.route}, viewport=${render.viewport}, invalid=${invalidPaths.map((entry) => entry.label).join("|")}`,
11879
+ "error",
11880
+ evidenceJsonPath,
11881
+ "prototypingEvidence.renderArtifactPresence",
11882
+ [
11883
+ `route=${screen.route}`,
11884
+ `viewport=${render.viewport}`,
11885
+ ...invalidPaths.map((entry) => `artifact=${entry.label}`)
11886
+ ],
11887
+ "change",
11888
+ "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"
11889
+ )
11890
+ );
11891
+ continue;
11892
+ }
11893
+ const missingArtifacts = await collectMissingRenderArtifacts(root, render);
11894
+ if (missingArtifacts.length > 0) {
11895
+ issues.push(
11896
+ issue(
11897
+ "QFAI-PROT-244",
11898
+ `QFAI-PROT-244: captured render artifact is missing. route=${screen.route}, viewport=${render.viewport}, missing=${missingArtifacts.join("|")}`,
11899
+ "error",
11900
+ evidenceJsonPath,
11901
+ "prototypingEvidence.renderArtifactPresence",
11902
+ [
11903
+ `route=${screen.route}`,
11904
+ `viewport=${render.viewport}`,
11905
+ ...missingArtifacts.map((artifact) => `artifact=${artifact}`)
11906
+ ],
11907
+ "change",
11908
+ "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"
11909
+ )
11910
+ );
11911
+ }
11912
+ }
11913
+ if (missingDefaultViewports.length === 0 && !allSkipped) {
11914
+ continue;
11915
+ }
11916
+ const severity = qualityProfile === "default" ? "warning" : "error";
11917
+ const reason = allSkipped ? "all renders are skipped" : `missing default viewports=${missingDefaultViewports.join("|")}`;
11918
+ issues.push(
11919
+ issue(
11920
+ "QFAI-PROT-245",
11921
+ `QFAI-PROT-245: render coverage is incomplete for ${screen.route}. ${reason}. qualityProfile=${qualityProfile}`,
11922
+ severity,
11923
+ evidenceJsonPath,
11924
+ "prototypingEvidence.renderCoverage",
11925
+ [
11926
+ `route=${screen.route}`,
11927
+ ...missingDefaultViewports.map((viewport) => `viewport=${viewport}`),
11928
+ `qualityProfile=${qualityProfile}`
11929
+ ],
11930
+ "change",
11931
+ 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"
11932
+ )
11933
+ );
11934
+ }
11591
11935
  return issues;
11592
11936
  }
11593
11937
  function formatUiFidelityMismatch(mismatch) {
@@ -11800,6 +12144,10 @@ function normalizeUiFidelityScreen(value) {
11800
12144
  if (!mockPaths.ok) {
11801
12145
  return mockPaths;
11802
12146
  }
12147
+ const renders = normalizeRenderEntries(value.renders);
12148
+ if (!renders.ok) {
12149
+ return renders;
12150
+ }
11803
12151
  return {
11804
12152
  ok: true,
11805
12153
  value: {
@@ -11810,8 +12158,103 @@ function normalizeUiFidelityScreen(value) {
11810
12158
  ...normalizeOptionalMissingBlock(value.missing),
11811
12159
  ...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
11812
12160
  observed: observed.value,
11813
- mockPaths: mockPaths.value
12161
+ mockPaths: mockPaths.value,
12162
+ renders: renders.value
12163
+ }
12164
+ };
12165
+ }
12166
+ function normalizeRenderEntries(value) {
12167
+ if (value === void 0) {
12168
+ return { ok: true, value: [] };
12169
+ }
12170
+ if (!Array.isArray(value)) {
12171
+ return { ok: false, reason: "`uiFidelity.screens[].renders` must be an array" };
12172
+ }
12173
+ const renders = [];
12174
+ for (const entry of value) {
12175
+ const normalized = normalizeRenderEntry(entry);
12176
+ if (!normalized.ok) {
12177
+ return normalized;
12178
+ }
12179
+ renders.push(normalized.value);
12180
+ }
12181
+ return { ok: true, value: renders };
12182
+ }
12183
+ function normalizeRenderEntry(value) {
12184
+ if (!isRecord5(value)) {
12185
+ return { ok: false, reason: "`uiFidelity.screens[].renders[]` must be objects" };
12186
+ }
12187
+ if (typeof value.viewport !== "string" || value.viewport.trim().length === 0) {
12188
+ return { ok: false, reason: "`uiFidelity.screens[].renders[].viewport` is required" };
12189
+ }
12190
+ if (!isNonNegativeInteger(value.width) || !isNonNegativeInteger(value.height) || value.width === 0 || value.height === 0) {
12191
+ return {
12192
+ ok: false,
12193
+ reason: "`uiFidelity.screens[].renders[]` requires positive integers for width/height"
12194
+ };
12195
+ }
12196
+ const viewport = value.viewport.trim();
12197
+ const width = value.width;
12198
+ const height = value.height;
12199
+ const status = typeof value.status === "string" ? value.status.trim().toLowerCase() : "";
12200
+ if (status === "captured") {
12201
+ if (typeof value.imagePath !== "string" || value.imagePath.trim().length === 0 || typeof value.htmlPath !== "string" || value.htmlPath.trim().length === 0) {
12202
+ return {
12203
+ ok: false,
12204
+ reason: "`captured` render entries require imagePath and htmlPath"
12205
+ };
12206
+ }
12207
+ return {
12208
+ ok: true,
12209
+ value: {
12210
+ viewport,
12211
+ status: "captured",
12212
+ width,
12213
+ height,
12214
+ imagePath: value.imagePath.trim(),
12215
+ htmlPath: value.htmlPath.trim()
12216
+ }
12217
+ };
12218
+ }
12219
+ if (status === "skipped") {
12220
+ if (typeof value.skippedReason !== "string" || value.skippedReason.trim().length === 0) {
12221
+ return {
12222
+ ok: false,
12223
+ reason: "`skipped` render entries require skippedReason"
12224
+ };
12225
+ }
12226
+ return {
12227
+ ok: true,
12228
+ value: {
12229
+ viewport,
12230
+ status: "skipped",
12231
+ width,
12232
+ height,
12233
+ skippedReason: value.skippedReason.trim()
12234
+ }
12235
+ };
12236
+ }
12237
+ if (status === "failed") {
12238
+ if (typeof value.error !== "string" || value.error.trim().length === 0) {
12239
+ return {
12240
+ ok: false,
12241
+ reason: "`failed` render entries require error"
12242
+ };
11814
12243
  }
12244
+ return {
12245
+ ok: true,
12246
+ value: {
12247
+ viewport,
12248
+ status: "failed",
12249
+ width,
12250
+ height,
12251
+ error: value.error.trim()
12252
+ }
12253
+ };
12254
+ }
12255
+ return {
12256
+ ok: false,
12257
+ reason: "`uiFidelity.screens[].renders[].status` must be captured|skipped|failed"
11815
12258
  };
11816
12259
  }
11817
12260
  function normalizeUiFidelityExpected(value) {
@@ -12037,6 +12480,22 @@ function normalizeOptionalMissingBlock(value) {
12037
12480
  function isRecord5(value) {
12038
12481
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12039
12482
  }
12483
+ async function collectMissingRenderArtifacts(root, render) {
12484
+ const missing = [];
12485
+ const candidates = [
12486
+ { label: "imagePath", target: render.imagePath },
12487
+ { label: "htmlPath", target: render.htmlPath }
12488
+ ];
12489
+ for (const candidate of candidates) {
12490
+ const resolved = path39.isAbsolute(candidate.target) ? candidate.target : path39.resolve(root, candidate.target);
12491
+ try {
12492
+ await access12(resolved);
12493
+ } catch {
12494
+ missing.push(candidate.label);
12495
+ }
12496
+ }
12497
+ return missing;
12498
+ }
12040
12499
  function isInteger(value) {
12041
12500
  return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
12042
12501
  }
@@ -12385,7 +12844,7 @@ function collectLayer(layer, layerName, target, errors) {
12385
12844
  }
12386
12845
  function flattenTokens(obj, prefix, target, errors) {
12387
12846
  for (const [key, value] of Object.entries(obj)) {
12388
- const path62 = `${prefix}.${key}`;
12847
+ const path64 = `${prefix}.${key}`;
12389
12848
  if (value && typeof value === "object" && !Array.isArray(value)) {
12390
12849
  const record2 = value;
12391
12850
  if ("$value" in record2) {
@@ -12401,9 +12860,9 @@ function flattenTokens(obj, prefix, target, errors) {
12401
12860
  if (typeof record2.platform === "string") {
12402
12861
  token.platform = record2.platform;
12403
12862
  }
12404
- target.set(path62, token);
12863
+ target.set(path64, token);
12405
12864
  } else {
12406
- flattenTokens(record2, path62, target, errors);
12865
+ flattenTokens(record2, path64, target, errors);
12407
12866
  }
12408
12867
  }
12409
12868
  }
@@ -12413,44 +12872,44 @@ function resolveAllReferences(result) {
12413
12872
  for (const [key, val] of result.primitives) allTokens.set(key, val);
12414
12873
  for (const [key, val] of result.semantics) allTokens.set(key, val);
12415
12874
  for (const [key, val] of result.components) allTokens.set(key, val);
12416
- for (const [path62] of allTokens) {
12417
- resolveTokenRef(path62, allTokens, /* @__PURE__ */ new Set(), 0, result);
12875
+ for (const [path64] of allTokens) {
12876
+ resolveTokenRef(path64, allTokens, /* @__PURE__ */ new Set(), 0, result);
12418
12877
  }
12419
12878
  }
12420
- function resolveTokenRef(path62, allTokens, visited, depth, result) {
12421
- if (result.resolved.has(path62)) {
12422
- return result.resolved.get(path62);
12879
+ function resolveTokenRef(path64, allTokens, visited, depth, result) {
12880
+ if (result.resolved.has(path64)) {
12881
+ return result.resolved.get(path64);
12423
12882
  }
12424
12883
  if (depth > MAX_RESOLVE_DEPTH) {
12425
12884
  result.errors.push({
12426
- message: `Max reference depth exceeded at: ${path62}`,
12427
- path: path62
12885
+ message: `Max reference depth exceeded at: ${path64}`,
12886
+ path: path64
12428
12887
  });
12429
12888
  return void 0;
12430
12889
  }
12431
- if (visited.has(path62)) {
12890
+ if (visited.has(path64)) {
12432
12891
  result.errors.push({
12433
- message: `Circular reference detected: ${path62}`,
12434
- path: path62
12892
+ message: `Circular reference detected: ${path64}`,
12893
+ path: path64
12435
12894
  });
12436
12895
  return void 0;
12437
12896
  }
12438
- const token = allTokens.get(path62);
12897
+ const token = allTokens.get(path64);
12439
12898
  if (!token) {
12440
12899
  return void 0;
12441
12900
  }
12442
12901
  if (typeof token.$value !== "string") {
12443
12902
  const rawValue2 = stringifyTokenValue(token.$value);
12444
- result.resolved.set(path62, rawValue2);
12903
+ result.resolved.set(path64, rawValue2);
12445
12904
  return rawValue2;
12446
12905
  }
12447
12906
  const rawValue = stringifyTokenValue(token.$value);
12448
12907
  const refs = [...rawValue.matchAll(REF_PATTERN)];
12449
12908
  if (refs.length === 0) {
12450
- result.resolved.set(path62, rawValue);
12909
+ result.resolved.set(path64, rawValue);
12451
12910
  return rawValue;
12452
12911
  }
12453
- visited.add(path62);
12912
+ visited.add(path64);
12454
12913
  let resolved = rawValue;
12455
12914
  for (const ref of refs) {
12456
12915
  const refPath = ref[1];
@@ -12458,8 +12917,8 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
12458
12917
  const refToken = allTokens.get(refPath);
12459
12918
  if (!refToken) {
12460
12919
  result.errors.push({
12461
- message: `Unresolved token reference: {${refPath}} at ${path62}`,
12462
- path: path62
12920
+ message: `Unresolved token reference: {${refPath}} at ${path64}`,
12921
+ path: path64
12463
12922
  });
12464
12923
  continue;
12465
12924
  }
@@ -12468,7 +12927,7 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
12468
12927
  resolved = resolved.split(`{${refPath}}`).join(refValue);
12469
12928
  }
12470
12929
  }
12471
- result.resolved.set(path62, resolved);
12930
+ result.resolved.set(path64, resolved);
12472
12931
  return resolved;
12473
12932
  }
12474
12933
  function stringifyTokenValue(value) {
@@ -15580,6 +16039,7 @@ async function validateNavigationFlow(root, config) {
15580
16039
 
15581
16040
  // src/core/validators/renderCritique.ts
15582
16041
  import path54 from "path";
16042
+ import { readFile as readFile40 } from "fs/promises";
15583
16043
  import fg9 from "fast-glob";
15584
16044
  var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
15585
16045
  var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
@@ -15611,6 +16071,7 @@ async function validateRenderCritique(root, config) {
15611
16071
  if (!hasDdp) return issues;
15612
16072
  const skillsDir = path54.join(root, config.paths.skillsDir).replace(/\\/g, "/");
15613
16073
  const evidenceDir = path54.join(root, ".qfai", "evidence").replace(/\\/g, "/");
16074
+ const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
15614
16075
  const skillPromptPattern = path54.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
15615
16076
  const skillFiles = await fg9(skillPromptPattern, { dot: true });
15616
16077
  const evidencePattern = path54.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
@@ -15650,7 +16111,7 @@ async function validateRenderCritique(root, config) {
15650
16111
  }
15651
16112
  }
15652
16113
  const allEvidenceContent = await collectContent(evidenceFiles);
15653
- if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
16114
+ if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("desktop")) {
15654
16115
  issues.push(
15655
16116
  issue(
15656
16117
  "QFAI-CRIT-003",
@@ -15664,7 +16125,7 @@ async function validateRenderCritique(root, config) {
15664
16125
  )
15665
16126
  );
15666
16127
  }
15667
- if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
16128
+ if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("mobile")) {
15668
16129
  issues.push(
15669
16130
  issue(
15670
16131
  "QFAI-CRIT-004",
@@ -15825,9 +16286,49 @@ async function collectContent(files) {
15825
16286
  }
15826
16287
  return contents.join("\n---\n");
15827
16288
  }
16289
+ async function collectRenderEvidenceViewports(root) {
16290
+ const prototypingJsonPath = path54.join(root, ".qfai", "evidence", "prototyping.json");
16291
+ try {
16292
+ const raw = await readFile40(prototypingJsonPath, "utf-8");
16293
+ const parsed = JSON.parse(raw);
16294
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16295
+ return /* @__PURE__ */ new Set();
16296
+ }
16297
+ const uiFidelity = parsed.uiFidelity;
16298
+ if (!uiFidelity || typeof uiFidelity !== "object" || Array.isArray(uiFidelity)) {
16299
+ return /* @__PURE__ */ new Set();
16300
+ }
16301
+ const screens = uiFidelity.screens;
16302
+ if (!Array.isArray(screens)) {
16303
+ return /* @__PURE__ */ new Set();
16304
+ }
16305
+ const viewports = /* @__PURE__ */ new Set();
16306
+ for (const screen of screens) {
16307
+ if (!screen || typeof screen !== "object" || Array.isArray(screen)) {
16308
+ continue;
16309
+ }
16310
+ const renders = screen.renders;
16311
+ if (!Array.isArray(renders)) {
16312
+ continue;
16313
+ }
16314
+ for (const render of renders) {
16315
+ if (!render || typeof render !== "object" || Array.isArray(render)) {
16316
+ continue;
16317
+ }
16318
+ const viewport = render.viewport;
16319
+ if (typeof viewport === "string" && viewport.trim().length > 0) {
16320
+ viewports.add(viewport.trim().toLowerCase());
16321
+ }
16322
+ }
16323
+ }
16324
+ return viewports;
16325
+ } catch {
16326
+ return /* @__PURE__ */ new Set();
16327
+ }
16328
+ }
15828
16329
 
15829
16330
  // src/core/validators/designFidelity.ts
15830
- import { readFile as readFile40 } from "fs/promises";
16331
+ import { readFile as readFile41 } from "fs/promises";
15831
16332
  import path55 from "path";
15832
16333
  import fg10 from "fast-glob";
15833
16334
  var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
@@ -15860,7 +16361,7 @@ async function validateDesignFidelity(root, config) {
15860
16361
  for (const filePath of allFiles) {
15861
16362
  let content;
15862
16363
  try {
15863
- content = await readFile40(filePath, "utf-8");
16364
+ content = await readFile41(filePath, "utf-8");
15864
16365
  } catch {
15865
16366
  continue;
15866
16367
  }
@@ -16483,6 +16984,252 @@ async function validateDiscussionDesignHardening(root, config) {
16483
16984
  return issues;
16484
16985
  }
16485
16986
 
16987
+ // src/core/validators/designAudit.ts
16988
+ import { readdir as readdir11 } from "fs/promises";
16989
+ import path57 from "path";
16990
+ var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
16991
+ function resolveAuditConfig(config) {
16992
+ const audit = config.uiux?.audit;
16993
+ const profile = config.uiux?.qualityProfile ?? "default";
16994
+ return {
16995
+ enabled: audit?.enabled ?? true,
16996
+ slopDetection: audit?.slopDetection ?? true,
16997
+ qualityProfile: profile,
16998
+ maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
16999
+ maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
17000
+ maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
17001
+ };
17002
+ }
17003
+ function mapSeverity(tier, profile, category) {
17004
+ if (tier === 1) return "error";
17005
+ if (tier === 2) return profile === "strict" ? "error" : "warning";
17006
+ if (profile === "default") {
17007
+ return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
17008
+ }
17009
+ return "warning";
17010
+ }
17011
+ function findingToIssue(finding, profile, rulePrefix = "audit") {
17012
+ const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
17013
+ return issue(
17014
+ finding.ruleId,
17015
+ finding.message,
17016
+ severity,
17017
+ finding.file,
17018
+ `${rulePrefix}.${finding.dimension}`,
17019
+ finding.evidence.length > 0 ? finding.evidence : void 0,
17020
+ "compatibility",
17021
+ finding.guidance
17022
+ );
17023
+ }
17024
+ function extractSection2(content, heading) {
17025
+ const idx = content.indexOf(heading);
17026
+ if (idx === -1) return null;
17027
+ const start = idx + heading.length;
17028
+ const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
17029
+ const rest = content.slice(start);
17030
+ const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
17031
+ const nextHeadingMatch = headingPattern.exec(rest);
17032
+ const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
17033
+ return sectionContent.trim() || null;
17034
+ }
17035
+ function checkCtaHierarchy(content, auditConfig, file) {
17036
+ const findings = [];
17037
+ const ctaSection = extractSection2(content, "### CTA Hierarchy");
17038
+ if (!ctaSection) return findings;
17039
+ const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
17040
+ const primaryCount = primaryLines.length;
17041
+ if (primaryCount === 0) {
17042
+ findings.push({
17043
+ ruleId: "QFAI-AUD-001",
17044
+ dimension: "visualHierarchy",
17045
+ severityTier: 1,
17046
+ message: "No primary CTA defined in CTA Hierarchy",
17047
+ why: "Every UI screen needs a clear primary action to guide users",
17048
+ evidence: [],
17049
+ guidance: "Define at least one primary CTA in the CTA Hierarchy section",
17050
+ file
17051
+ });
17052
+ }
17053
+ if (primaryCount > auditConfig.maxPrimaryCtas) {
17054
+ findings.push({
17055
+ ruleId: "QFAI-AUD-020",
17056
+ dimension: "visualHierarchy",
17057
+ severityTier: 2,
17058
+ message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
17059
+ why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
17060
+ evidence: primaryLines.map((l) => l.trim()),
17061
+ guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
17062
+ file
17063
+ });
17064
+ }
17065
+ return findings;
17066
+ }
17067
+ var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
17068
+ async function checkTokenDrift(root, auditConfig, cfg) {
17069
+ const findings = [];
17070
+ const configuredDir = cfg.uiux?.designTokensDir;
17071
+ const tokensDir = configuredDir ? path57.resolve(root, configuredDir) : path57.join(root, cfg.paths.contractsDir, "design");
17072
+ let hasTokenFiles = false;
17073
+ try {
17074
+ const entries = await readdir11(tokensDir);
17075
+ hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
17076
+ } catch {
17077
+ return findings;
17078
+ }
17079
+ if (!hasTokenFiles) return findings;
17080
+ const contractsUiDir = path57.join(root, cfg.paths.contractsDir, "ui");
17081
+ let htmlFiles = [];
17082
+ try {
17083
+ const entries = await readdir11(contractsUiDir);
17084
+ htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
17085
+ } catch {
17086
+ return findings;
17087
+ }
17088
+ let rawCount = 0;
17089
+ const sampleLiterals = [];
17090
+ for (const htmlFile of htmlFiles) {
17091
+ const content = await readSafe(path57.join(contractsUiDir, htmlFile));
17092
+ if (!content) continue;
17093
+ const matches = content.match(RAW_COLOR_RE);
17094
+ if (matches) {
17095
+ rawCount += matches.length;
17096
+ for (const m of matches) {
17097
+ if (sampleLiterals.length < 10) {
17098
+ sampleLiterals.push(m.toLowerCase());
17099
+ }
17100
+ }
17101
+ }
17102
+ }
17103
+ if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
17104
+ findings.push({
17105
+ ruleId: "QFAI-AUD-004",
17106
+ dimension: "tokenDiscipline",
17107
+ severityTier: 1,
17108
+ message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
17109
+ why: "Raw color values bypass design tokens, causing visual inconsistency",
17110
+ evidence: sampleLiterals,
17111
+ guidance: "Replace raw color literals with design token references"
17112
+ });
17113
+ }
17114
+ return findings;
17115
+ }
17116
+ function deduplicateFindings(issues, maxPerRule) {
17117
+ const counts = /* @__PURE__ */ new Map();
17118
+ const result = [];
17119
+ for (const iss of issues) {
17120
+ const count = counts.get(iss.code) ?? 0;
17121
+ if (count < maxPerRule) {
17122
+ result.push(iss);
17123
+ }
17124
+ counts.set(iss.code, count + 1);
17125
+ }
17126
+ for (const [code, count] of counts) {
17127
+ if (count > maxPerRule) {
17128
+ result.push({
17129
+ code,
17130
+ severity: "info",
17131
+ category: "compatibility",
17132
+ message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
17133
+ rule: `audit.dedup.${code}`
17134
+ });
17135
+ }
17136
+ }
17137
+ return result;
17138
+ }
17139
+ async function validateDesignAudit(root, config) {
17140
+ const auditConfig = resolveAuditConfig(config);
17141
+ if (!auditConfig.enabled) return [];
17142
+ const discussionDir = path57.join(root, config.paths.discussionDir);
17143
+ const packRoot = await findLatestDiscussionPackDir(discussionDir);
17144
+ if (!packRoot) return [];
17145
+ const uiBearing = await isUiBearing(packRoot);
17146
+ if (!uiBearing) return [];
17147
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17148
+ const content = await readSafe(storyPath);
17149
+ if (!content) return [];
17150
+ const findings = [];
17151
+ findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
17152
+ findings.push(...await checkTokenDrift(root, auditConfig, config));
17153
+ const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
17154
+ return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
17155
+ }
17156
+
17157
+ // src/core/validators/designSlop.ts
17158
+ import { existsSync as existsSync2 } from "fs";
17159
+ import { readFile as readFile42 } from "fs/promises";
17160
+ import path58 from "path";
17161
+ import { fileURLToPath as fileURLToPath4 } from "url";
17162
+ function isValidSlopPattern(rule) {
17163
+ if (typeof rule !== "object" || rule === null) return false;
17164
+ const r = rule;
17165
+ 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";
17166
+ }
17167
+ async function loadSlopPatterns(jsonPath) {
17168
+ const raw = await readFile42(jsonPath, "utf-8");
17169
+ const parsed = JSON.parse(raw);
17170
+ if (!Array.isArray(parsed)) return [];
17171
+ return parsed.filter((r) => isValidSlopPattern(r));
17172
+ }
17173
+ function defaultPatternsPath() {
17174
+ const base = import.meta.url;
17175
+ const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
17176
+ const baseDir = path58.dirname(basePath);
17177
+ const candidates = [
17178
+ path58.join(baseDir, "designSlopPatterns.json"),
17179
+ path58.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
17180
+ path58.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
17181
+ ];
17182
+ for (const c of candidates) {
17183
+ if (existsSync2(c)) return c;
17184
+ }
17185
+ return candidates[0];
17186
+ }
17187
+ async function validateDesignSlop(root, config) {
17188
+ const auditConfig = resolveAuditConfig(config);
17189
+ if (!auditConfig.enabled) return [];
17190
+ if (!auditConfig.slopDetection) return [];
17191
+ const discussionDir = path58.join(root, config.paths.discussionDir);
17192
+ const packRoot = await findLatestDiscussionPackDir(discussionDir);
17193
+ if (!packRoot) return [];
17194
+ const uiBearing = await isUiBearing(packRoot);
17195
+ if (!uiBearing) return [];
17196
+ let patterns;
17197
+ try {
17198
+ patterns = await loadSlopPatterns(defaultPatternsPath());
17199
+ } catch {
17200
+ return [];
17201
+ }
17202
+ const findings = [];
17203
+ const seenRules = /* @__PURE__ */ new Set();
17204
+ for (const pattern of patterns) {
17205
+ let regex;
17206
+ try {
17207
+ regex = new RegExp(pattern.match, "gi");
17208
+ } catch {
17209
+ continue;
17210
+ }
17211
+ for (const scope of pattern.scopes) {
17212
+ const filePath = path58.join(packRoot, scope);
17213
+ const content = await readSafe(filePath);
17214
+ if (!content) continue;
17215
+ if (regex.test(content) && !seenRules.has(pattern.id)) {
17216
+ seenRules.add(pattern.id);
17217
+ findings.push({
17218
+ ruleId: pattern.id,
17219
+ dimension: pattern.category,
17220
+ severityTier: pattern.tier,
17221
+ message: pattern.message,
17222
+ why: `Slop pattern "${pattern.id}" matched in ${scope}`,
17223
+ evidence: [],
17224
+ guidance: pattern.guidance,
17225
+ file: scope
17226
+ });
17227
+ }
17228
+ }
17229
+ }
17230
+ return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
17231
+ }
17232
+
16486
17233
  // src/core/validate.ts
16487
17234
  var UIUX_VALIDATION_BUDGET_MS = 2e3;
16488
17235
  async function validateProject(root, configResult, options = {}) {
@@ -16500,7 +17247,9 @@ async function validateProject(root, configResult, options = {}) {
16500
17247
  () => validateBpApDb(root, config),
16501
17248
  () => validateUiDefinitionConsistency(root, config),
16502
17249
  () => validateResearchSummary(root, config),
16503
- () => validateAgentDefinition(root, config)
17250
+ () => validateAgentDefinition(root, config),
17251
+ () => validateDesignAudit(root, config),
17252
+ () => validateDesignSlop(root, config)
16504
17253
  ];
16505
17254
  const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
16506
17255
  const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
@@ -16587,15 +17336,15 @@ var REPORT_GUARDRAILS_MAX = 20;
16587
17336
  var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
16588
17337
  var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
16589
17338
  async function createReportData(root, validation, configResult) {
16590
- const resolvedRoot = path57.resolve(root);
17339
+ const resolvedRoot = path59.resolve(root);
16591
17340
  const resolved = configResult ?? await loadConfig(resolvedRoot);
16592
17341
  const config = resolved.config;
16593
17342
  const configPath = resolved.configPath;
16594
17343
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
16595
17344
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
16596
- const apiRoot = path57.join(contractsRoot, "api");
16597
- const uiRoot = path57.join(contractsRoot, "ui");
16598
- const dbRoot = path57.join(contractsRoot, "db");
17345
+ const apiRoot = path59.join(contractsRoot, "api");
17346
+ const uiRoot = path59.join(contractsRoot, "ui");
17347
+ const dbRoot = path59.join(contractsRoot, "db");
16599
17348
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
16600
17349
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
16601
17350
  const specEntries = await collectSpecEntries(specsRoot);
@@ -16923,6 +17672,8 @@ function formatReportMarkdown(data, options = {}) {
16923
17672
  lines.push("");
16924
17673
  lines.push("- [Compatibility Issues](#compatibility-issues)");
16925
17674
  lines.push("- [Change Issues](#change-issues)");
17675
+ lines.push("- [Design Audit Findings](#design-audit-findings)");
17676
+ lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
16926
17677
  lines.push("- [Change Type](#change-type)");
16927
17678
  lines.push("- [Waivers](#waivers)");
16928
17679
  lines.push("- [Decision Guardrails](#decision-guardrails)");
@@ -17071,6 +17822,46 @@ function formatReportMarkdown(data, options = {}) {
17071
17822
  lines.push("### Issues");
17072
17823
  lines.push("");
17073
17824
  lines.push(...formatIssueCards(issuesByCategory.change));
17825
+ const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
17826
+ if (auditIssues.length > 0) {
17827
+ lines.push("## Design Audit Findings");
17828
+ lines.push("");
17829
+ const byDimension = /* @__PURE__ */ new Map();
17830
+ for (const iss of auditIssues) {
17831
+ const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
17832
+ const group = byDimension.get(dim) ?? [];
17833
+ group.push(iss);
17834
+ byDimension.set(dim, group);
17835
+ }
17836
+ for (const [dim, dimIssues] of byDimension) {
17837
+ lines.push(`### ${dim}`);
17838
+ lines.push("");
17839
+ for (const iss of dimIssues) {
17840
+ lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
17841
+ }
17842
+ lines.push("");
17843
+ }
17844
+ }
17845
+ const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
17846
+ if (slopIssues.length > 0) {
17847
+ lines.push("## Slop Guardrails Findings");
17848
+ lines.push("");
17849
+ const byCategory = /* @__PURE__ */ new Map();
17850
+ for (const iss of slopIssues) {
17851
+ const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
17852
+ const group = byCategory.get(cat) ?? [];
17853
+ group.push(iss);
17854
+ byCategory.set(cat, group);
17855
+ }
17856
+ for (const [cat, catIssues] of byCategory) {
17857
+ lines.push(`### ${cat}`);
17858
+ lines.push("");
17859
+ for (const iss of catIssues) {
17860
+ lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
17861
+ }
17862
+ lines.push("");
17863
+ }
17864
+ }
17074
17865
  lines.push("## Change Type");
17075
17866
  lines.push("");
17076
17867
  lines.push("### Summary");
@@ -17442,6 +18233,20 @@ function formatReportMarkdown(data, options = {}) {
17442
18233
  } else {
17443
18234
  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");
17444
18235
  }
18236
+ const renderEvidenceIssues = data.issues.filter(
18237
+ (item) => ["QFAI-PROT-101", "QFAI-PROT-244", "QFAI-PROT-245"].includes(item.code)
18238
+ );
18239
+ if (renderEvidenceIssues.length > 0) {
18240
+ lines.push(
18241
+ "- 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"
18242
+ );
18243
+ lines.push(
18244
+ "- 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"
18245
+ );
18246
+ lines.push(
18247
+ "- 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"
18248
+ );
18249
+ }
17445
18250
  lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/18_delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
17446
18251
  lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`");
17447
18252
  return lines.join("\n");
@@ -17476,7 +18281,7 @@ async function collectChangeTypeSummary(specsRoot) {
17476
18281
  };
17477
18282
  const deltaFiles = await collectDeltaFiles(specsRoot);
17478
18283
  for (const deltaFile of deltaFiles) {
17479
- const text = await readFile41(deltaFile, "utf-8");
18284
+ const text = await readFile43(deltaFile, "utf-8");
17480
18285
  const parsed = parseDeltaV1(text);
17481
18286
  for (const entry of parsed.entries) {
17482
18287
  if (!entry.meta) {
@@ -17513,7 +18318,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
17513
18318
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
17514
18319
  }
17515
18320
  for (const file of specFiles) {
17516
- const text = await readFile41(file, "utf-8");
18321
+ const text = await readFile43(file, "utf-8");
17517
18322
  const parsed = parseSpec(text, file);
17518
18323
  const specKey = parsed.specId;
17519
18324
  if (!specKey) {
@@ -17550,7 +18355,7 @@ async function collectIds(files) {
17550
18355
  result[prefix] = /* @__PURE__ */ new Set();
17551
18356
  }
17552
18357
  for (const file of files) {
17553
- const text = await readFile41(file, "utf-8");
18358
+ const text = await readFile43(file, "utf-8");
17554
18359
  for (const prefix of ID_PREFIXES) {
17555
18360
  const ids = extractIds(text, prefix);
17556
18361
  ids.forEach((id) => result[prefix].add(id));
@@ -17565,7 +18370,7 @@ async function collectIds(files) {
17565
18370
  async function collectUpstreamIds(files) {
17566
18371
  const ids = /* @__PURE__ */ new Set();
17567
18372
  for (const file of files) {
17568
- const text = await readFile41(file, "utf-8");
18373
+ const text = await readFile43(file, "utf-8");
17569
18374
  extractAllIds(text).forEach((id) => ids.add(id));
17570
18375
  }
17571
18376
  return ids;
@@ -17586,7 +18391,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
17586
18391
  }
17587
18392
  const pattern = buildIdPattern(Array.from(upstreamIds));
17588
18393
  for (const file of targetFiles) {
17589
- const text = await readFile41(file, "utf-8");
18394
+ const text = await readFile43(file, "utf-8");
17590
18395
  if (pattern.test(text)) {
17591
18396
  return true;
17592
18397
  }
@@ -17705,7 +18510,7 @@ function normalizeScSources(root, sources) {
17705
18510
  async function countScenarios(scenarioFiles) {
17706
18511
  let total = 0;
17707
18512
  for (const file of scenarioFiles) {
17708
- const text = await readFile41(file, "utf-8");
18513
+ const text = await readFile43(file, "utf-8");
17709
18514
  const { document, errors } = parseScenarioDocument(text, file);
17710
18515
  if (!document || errors.length > 0) {
17711
18516
  continue;
@@ -17736,7 +18541,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
17736
18541
  let totalScenarios = 0;
17737
18542
  let e2eCount = 0;
17738
18543
  for (const file of scenarioFiles) {
17739
- const text = await readFile41(file, "utf-8");
18544
+ const text = await readFile43(file, "utf-8");
17740
18545
  const { document, errors } = parseScenarioDocument(text, file);
17741
18546
  if (!document || errors.length > 0) {
17742
18547
  continue;
@@ -17824,10 +18629,10 @@ function buildHotspots(issues) {
17824
18629
  async function collectTddCoverage(entries) {
17825
18630
  const specs = [];
17826
18631
  for (const entry of entries) {
17827
- const testCasesPath = path57.join(entry.dir, "06_Test-Cases.md");
18632
+ const testCasesPath = path59.join(entry.dir, "06_Test-Cases.md");
17828
18633
  let tcContent;
17829
18634
  try {
17830
- tcContent = await readFile41(testCasesPath, "utf-8");
18635
+ tcContent = await readFile43(testCasesPath, "utf-8");
17831
18636
  } catch {
17832
18637
  continue;
17833
18638
  }
@@ -17859,10 +18664,10 @@ async function collectTddCoverage(entries) {
17859
18664
  });
17860
18665
  continue;
17861
18666
  }
17862
- const tddListPath = path57.join(entry.dir, "tdd", "test-list.md");
18667
+ const tddListPath = path59.join(entry.dir, "tdd", "test-list.md");
17863
18668
  let tddContent;
17864
18669
  try {
17865
- tddContent = await readFile41(tddListPath, "utf-8");
18670
+ tddContent = await readFile43(tddListPath, "utf-8");
17866
18671
  } catch {
17867
18672
  specs.push({
17868
18673
  specNumber: entry.specNumber,
@@ -17945,8 +18750,8 @@ async function collectTddCoverage(entries) {
17945
18750
  }
17946
18751
 
17947
18752
  // src/core/specPackReport.ts
17948
- import { mkdir as mkdir7, readFile as readFile42, writeFile as writeFile6 } from "fs/promises";
17949
- import path58 from "path";
18753
+ import { mkdir as mkdir7, readFile as readFile44, writeFile as writeFile6 } from "fs/promises";
18754
+ import path60 from "path";
17950
18755
  var REQUIRED_LEDGER_COLUMNS = [
17951
18756
  "trace_id",
17952
18757
  "obj_id",
@@ -17964,8 +18769,8 @@ async function writeSpecPackReports(root, config) {
17964
18769
  const entries = await collectSpecEntries(specsRoot);
17965
18770
  const contractIndex = await buildContractIndex(root, config);
17966
18771
  for (const entry of entries) {
17967
- const specName = path58.basename(entry.dir);
17968
- const outputDir = path58.join(outRoot, specName);
18772
+ const specName = path60.basename(entry.dir);
18773
+ const outputDir = path60.join(outRoot, specName);
17969
18774
  await mkdir7(outputDir, { recursive: true });
17970
18775
  const [acText, tcText, exText, ledgerText] = await Promise.all([
17971
18776
  readSafe12(entry.acceptanceCriteriaPath),
@@ -17992,13 +18797,13 @@ async function writeSpecPackReports(root, config) {
17992
18797
  });
17993
18798
  const graph = buildTraceabilityGraph(ledgerRows);
17994
18799
  await writeFile6(
17995
- path58.join(outputDir, "coverage.md"),
18800
+ path60.join(outputDir, "coverage.md"),
17996
18801
  `${formatCoverageMarkdown(specName, coverage)}
17997
18802
  `,
17998
18803
  "utf-8"
17999
18804
  );
18000
18805
  await writeFile6(
18001
- path58.join(outputDir, "traceability-graph.json"),
18806
+ path60.join(outputDir, "traceability-graph.json"),
18002
18807
  `${JSON.stringify(graph, null, 2)}
18003
18808
  `,
18004
18809
  "utf-8"
@@ -18173,7 +18978,7 @@ function getCell(row, indexByColumn, column) {
18173
18978
  }
18174
18979
  async function readSafe12(filePath) {
18175
18980
  try {
18176
- return await readFile42(filePath, "utf-8");
18981
+ return await readFile44(filePath, "utf-8");
18177
18982
  } catch {
18178
18983
  return "";
18179
18984
  }
@@ -18191,7 +18996,7 @@ function warnIfTruncated(scan, context) {
18191
18996
 
18192
18997
  // src/cli/commands/report.ts
18193
18998
  async function runReport(options) {
18194
- const root = path59.resolve(options.root);
18999
+ const root = path61.resolve(options.root);
18195
19000
  const configResult = await loadConfig(root);
18196
19001
  let validation;
18197
19002
  let blockedByPhaseGuard = false;
@@ -18207,7 +19012,7 @@ async function runReport(options) {
18207
19012
  validation = normalized;
18208
19013
  } else {
18209
19014
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
18210
- const inputPath = path59.isAbsolute(input) ? input : path59.resolve(root, input);
19015
+ const inputPath = path61.isAbsolute(input) ? input : path61.resolve(root, input);
18211
19016
  try {
18212
19017
  validation = await readValidationResult(inputPath);
18213
19018
  } catch (err) {
@@ -18234,10 +19039,10 @@ async function runReport(options) {
18234
19039
  warnIfTruncated(data.traceability.testFiles, "report");
18235
19040
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
18236
19041
  const outRoot = resolvePath(root, configResult.config, "outDir");
18237
- const defaultOut = options.format === "json" ? path59.join(outRoot, "report.json") : path59.join(outRoot, "report.md");
19042
+ const defaultOut = options.format === "json" ? path61.join(outRoot, "report.json") : path61.join(outRoot, "report.md");
18238
19043
  const out = options.outPath ?? defaultOut;
18239
- const outPath = path59.isAbsolute(out) ? out : path59.resolve(root, out);
18240
- await mkdir8(path59.dirname(outPath), { recursive: true });
19044
+ const outPath = path61.isAbsolute(out) ? out : path61.resolve(root, out);
19045
+ await mkdir8(path61.dirname(outPath), { recursive: true });
18241
19046
  await writeFile7(outPath, `${output}
18242
19047
  `, "utf-8");
18243
19048
  await writeSpecPackReports(root, configResult.config);
@@ -18253,7 +19058,7 @@ async function runReport(options) {
18253
19058
  info(`wrote report: ${outPath}`);
18254
19059
  }
18255
19060
  async function readValidationResult(inputPath) {
18256
- const raw = await readFile43(inputPath, "utf-8");
19061
+ const raw = await readFile45(inputPath, "utf-8");
18257
19062
  const parsed = JSON.parse(raw);
18258
19063
  if (!isValidationResult(parsed)) {
18259
19064
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -18313,21 +19118,21 @@ function isMissingFileError2(error2) {
18313
19118
  return record2.code === "ENOENT";
18314
19119
  }
18315
19120
  async function writeValidationResult(root, outputPath, result) {
18316
- const abs = path59.isAbsolute(outputPath) ? outputPath : path59.resolve(root, outputPath);
18317
- await mkdir8(path59.dirname(abs), { recursive: true });
19121
+ const abs = path61.isAbsolute(outputPath) ? outputPath : path61.resolve(root, outputPath);
19122
+ await mkdir8(path61.dirname(abs), { recursive: true });
18318
19123
  await writeFile7(abs, `${JSON.stringify(result, null, 2)}
18319
19124
  `, "utf-8");
18320
19125
  }
18321
19126
 
18322
19127
  // src/cli/commands/validate.ts
18323
19128
  import { mkdir as mkdir10, writeFile as writeFile9 } from "fs/promises";
18324
- import path61 from "path";
19129
+ import path63 from "path";
18325
19130
 
18326
19131
  // src/core/runLog.ts
18327
19132
  import { mkdir as mkdir9, writeFile as writeFile8 } from "fs/promises";
18328
- import path60 from "path";
19133
+ import path62 from "path";
18329
19134
  async function writeValidateRunLog(input) {
18330
- const root = path60.resolve(input.root);
19135
+ const root = path62.resolve(input.root);
18331
19136
  const outDir = resolvePath(root, input.config, "outDir");
18332
19137
  await mkdir9(outDir, { recursive: true });
18333
19138
  const { runId, reportDir } = await allocateRunReportDir(outDir, input.startedAt);
@@ -18374,10 +19179,10 @@ async function writeValidateRunLog(input) {
18374
19179
  errors,
18375
19180
  warnings
18376
19181
  });
18377
- await writeJson(path60.join(reportDir, "run.json"), runJson);
18378
- await writeJson(path60.join(reportDir, "validator.json"), validatorJson);
18379
- await writeJson(path60.join(reportDir, "traceability.json"), traceabilityJson);
18380
- await writeFile8(path60.join(reportDir, "summary.md"), `${summaryMd}
19182
+ await writeJson(path62.join(reportDir, "run.json"), runJson);
19183
+ await writeJson(path62.join(reportDir, "validator.json"), validatorJson);
19184
+ await writeJson(path62.join(reportDir, "traceability.json"), traceabilityJson);
19185
+ await writeFile8(path62.join(reportDir, "summary.md"), `${summaryMd}
18381
19186
  `, "utf-8");
18382
19187
  return {
18383
19188
  runId,
@@ -18501,7 +19306,7 @@ async function allocateRunReportDir(outDir, startedAt) {
18501
19306
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
18502
19307
  const candidateDate = new Date(startedAt.getTime() + attempt);
18503
19308
  const runId = `run-${formatTimestamp17(candidateDate)}`;
18504
- const reportDir = path60.join(outDir, runId);
19309
+ const reportDir = path62.join(outDir, runId);
18505
19310
  try {
18506
19311
  await mkdir9(reportDir);
18507
19312
  return { runId, reportDir };
@@ -18532,7 +19337,7 @@ function shouldFail(result, failOn) {
18532
19337
  // src/cli/commands/validate.ts
18533
19338
  async function runValidate(options) {
18534
19339
  const startedAt = /* @__PURE__ */ new Date();
18535
- const root = path61.resolve(options.root);
19340
+ const root = path63.resolve(options.root);
18536
19341
  const configResult = await loadConfig(root);
18537
19342
  const blockedIssue = buildCiRefinementIssue(options.phase);
18538
19343
  const blockedByPhaseGuard = blockedIssue !== null;
@@ -18688,12 +19493,12 @@ function issueKey(issue2) {
18688
19493
  }
18689
19494
  async function emitJson(result, root, jsonPath) {
18690
19495
  const abs = resolveJsonPath(root, jsonPath);
18691
- await mkdir10(path61.dirname(abs), { recursive: true });
19496
+ await mkdir10(path63.dirname(abs), { recursive: true });
18692
19497
  await writeFile9(abs, `${JSON.stringify(result, null, 2)}
18693
19498
  `, "utf-8");
18694
19499
  }
18695
19500
  function resolveJsonPath(root, jsonPath) {
18696
- return path61.isAbsolute(jsonPath) ? jsonPath : path61.resolve(root, jsonPath);
19501
+ return path63.isAbsolute(jsonPath) ? jsonPath : path63.resolve(root, jsonPath);
18697
19502
  }
18698
19503
  var GITHUB_ANNOTATION_LIMIT = 100;
18699
19504
  var ISSUE_EXPECTED_BY_CODE = {
@@ -18812,6 +19617,8 @@ function parseArgs(argv, cwd) {
18812
19617
  guardrailsPaths: [],
18813
19618
  prototypingAutogen: false,
18814
19619
  prototypingAutogenOnly: false,
19620
+ prototypingRenderEvidence: false,
19621
+ prototypingRenderViewports: [],
18815
19622
  help: false,
18816
19623
  invalidExitCode: 1
18817
19624
  };
@@ -19023,6 +19830,37 @@ function parseArgs(argv, cwd) {
19023
19830
  i += 1;
19024
19831
  break;
19025
19832
  }
19833
+ case "--render-evidence":
19834
+ if (command === "prototyping") {
19835
+ options.prototypingRenderEvidence = true;
19836
+ }
19837
+ break;
19838
+ case "--viewports": {
19839
+ if (command !== "prototyping") {
19840
+ break;
19841
+ }
19842
+ const next = readOptionValue(args, i);
19843
+ if (next === null) {
19844
+ markInvalid();
19845
+ break;
19846
+ }
19847
+ options.prototypingRenderViewports = next.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
19848
+ i += 1;
19849
+ break;
19850
+ }
19851
+ case "--render-out": {
19852
+ if (command !== "prototyping") {
19853
+ break;
19854
+ }
19855
+ const next = readOptionValue(args, i);
19856
+ if (next === null) {
19857
+ markInvalid();
19858
+ break;
19859
+ }
19860
+ options.prototypingRenderOut = next;
19861
+ i += 1;
19862
+ break;
19863
+ }
19026
19864
  case "--platform": {
19027
19865
  if (command !== "validate") {
19028
19866
  break;
@@ -19175,7 +20013,10 @@ async function run(argv, cwd) {
19175
20013
  autogenUiFidelity: options.prototypingAutogen,
19176
20014
  autogenOnly: options.prototypingAutogenOnly,
19177
20015
  ...options.prototypingBaseUrl !== void 0 ? { baseUrl: options.prototypingBaseUrl } : {},
19178
- ...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {}
20016
+ ...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {},
20017
+ renderEvidence: options.prototypingRenderEvidence,
20018
+ renderViewports: options.prototypingRenderViewports,
20019
+ ...options.prototypingRenderOut !== void 0 ? { renderOut: options.prototypingRenderOut } : {}
19179
20020
  });
19180
20021
  process.exitCode = exitCode;
19181
20022
  }
@@ -19221,6 +20062,9 @@ Options:
19221
20062
  --autogen-ui-fidelity prototyping: uiFidelity \u81EA\u52D5\u751F\u6210\u3092\u6709\u52B9\u5316
19222
20063
  --autogen-only prototyping: \u81EA\u52D5\u751F\u6210\u306E\u307F\u5B9F\u884C\uFF08\u5931\u6557\u6642exit 1\uFF09
19223
20064
  --evidence-out <path> prototyping: \u51FA\u529B\u5148\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8 .qfai/evidence/prototyping.json\uFF09
20065
+ --render-evidence prototyping: render evidence \u306E\u53CE\u96C6\u3092\u6709\u52B9\u5316
20066
+ --viewports <list> prototyping: render \u5BFE\u8C61 viewport \u3092\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u6307\u5B9A
20067
+ --render-out <path> prototyping: render evidence \u306E\u51FA\u529B\u5148
19224
20068
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
19225
20069
 
19226
20070
  Environment: