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.
@@ -35,6 +35,22 @@ var import_node_path10 = __toESM(require("path"), 1);
35
35
  var import_promises = require("fs/promises");
36
36
  var import_node_path = __toESM(require("path"), 1);
37
37
  var import_yaml = require("yaml");
38
+
39
+ // src/core/uiux/renderEvidenceTypes.ts
40
+ var DEFAULT_RENDER_VIEWPORTS = ["desktop", "mobile"];
41
+ function normalizeRenderViewports(viewports) {
42
+ const normalized = Array.isArray(viewports) ? viewports.map((item) => item.trim()).filter((item) => item.length > 0) : [];
43
+ if (normalized.length > 0) {
44
+ return Array.from(new Set(normalized));
45
+ }
46
+ return [...DEFAULT_RENDER_VIEWPORTS];
47
+ }
48
+ function looksLikeInlineRenderPayload(value) {
49
+ const trimmed = value.trim().toLowerCase();
50
+ return trimmed.startsWith("data:image") || trimmed.includes("<html");
51
+ }
52
+
53
+ // src/core/config.ts
38
54
  var defaultConfig = {
39
55
  paths: {
40
56
  contractsDir: ".qfai/contracts",
@@ -484,6 +500,133 @@ function normalizeUiux(raw, configPath, issues) {
484
500
  );
485
501
  }
486
502
  }
503
+ if (raw.renderEvidence !== void 0) {
504
+ const renderEvidence = normalizeRenderEvidence(raw.renderEvidence, configPath, issues);
505
+ if (renderEvidence) {
506
+ result.renderEvidence = renderEvidence;
507
+ }
508
+ }
509
+ if (raw.audit !== void 0) {
510
+ const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
511
+ if (audit) {
512
+ result.audit = audit;
513
+ }
514
+ }
515
+ return Object.keys(result).length > 0 ? result : void 0;
516
+ }
517
+ function normalizeUiuxAudit(raw, configPath, issues) {
518
+ if (!isRecord(raw)) {
519
+ issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
520
+ return void 0;
521
+ }
522
+ const result = {};
523
+ if (raw.enabled !== void 0) {
524
+ if (typeof raw.enabled === "boolean") {
525
+ result.enabled = raw.enabled;
526
+ } else {
527
+ issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
528
+ }
529
+ }
530
+ if (raw.slopDetection !== void 0) {
531
+ if (typeof raw.slopDetection === "boolean") {
532
+ result.slopDetection = raw.slopDetection;
533
+ } else {
534
+ issues.push(
535
+ configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
536
+ );
537
+ }
538
+ }
539
+ if (raw.maxPrimaryCtas !== void 0) {
540
+ if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
541
+ result.maxPrimaryCtas = raw.maxPrimaryCtas;
542
+ } else {
543
+ issues.push(
544
+ configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
545
+ );
546
+ }
547
+ }
548
+ if (raw.maxRawTokenLiteralWarnings !== void 0) {
549
+ if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
550
+ result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
551
+ } else {
552
+ issues.push(
553
+ configIssue(
554
+ configPath,
555
+ "uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
556
+ )
557
+ );
558
+ }
559
+ }
560
+ if (raw.maxDuplicateFindingsPerRule !== void 0) {
561
+ if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
562
+ result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
563
+ } else {
564
+ issues.push(
565
+ configIssue(
566
+ configPath,
567
+ "uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
568
+ )
569
+ );
570
+ }
571
+ }
572
+ return Object.keys(result).length > 0 ? result : void 0;
573
+ }
574
+ function normalizeRenderEvidence(raw, configPath, issues) {
575
+ if (!isRecord(raw)) {
576
+ issues.push(
577
+ configIssue(configPath, "uiux.renderEvidence \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
578
+ );
579
+ return void 0;
580
+ }
581
+ const result = {};
582
+ if (raw.enabled !== void 0) {
583
+ if (typeof raw.enabled === "boolean") {
584
+ result.enabled = raw.enabled;
585
+ } else {
586
+ issues.push(
587
+ configIssue(configPath, "uiux.renderEvidence.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
588
+ );
589
+ }
590
+ }
591
+ if (raw.viewports !== void 0) {
592
+ if (Array.isArray(raw.viewports) && raw.viewports.every((item) => typeof item === "string")) {
593
+ result.viewports = normalizeRenderViewports(raw.viewports);
594
+ } else {
595
+ issues.push(
596
+ configIssue(configPath, "uiux.renderEvidence.viewports \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
597
+ );
598
+ }
599
+ }
600
+ if (raw.out !== void 0) {
601
+ if (typeof raw.out === "string" && raw.out.trim().length > 0) {
602
+ result.out = raw.out.trim();
603
+ } else {
604
+ issues.push(
605
+ configIssue(configPath, "uiux.renderEvidence.out \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
606
+ );
607
+ }
608
+ }
609
+ if (raw.baseUrl !== void 0) {
610
+ if (typeof raw.baseUrl === "string" && raw.baseUrl.trim().length > 0) {
611
+ result.baseUrl = raw.baseUrl.trim();
612
+ } else {
613
+ issues.push(
614
+ configIssue(
615
+ configPath,
616
+ "uiux.renderEvidence.baseUrl \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
617
+ )
618
+ );
619
+ }
620
+ }
621
+ if (raw.failOpen !== void 0) {
622
+ if (typeof raw.failOpen === "boolean") {
623
+ result.failOpen = raw.failOpen;
624
+ } else {
625
+ issues.push(
626
+ configIssue(configPath, "uiux.renderEvidence.failOpen \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
627
+ );
628
+ }
629
+ }
487
630
  return Object.keys(result).length > 0 ? result : void 0;
488
631
  }
489
632
  function configIssue(file, message) {
@@ -1564,8 +1707,8 @@ var import_promises7 = require("fs/promises");
1564
1707
  var import_node_path8 = __toESM(require("path"), 1);
1565
1708
  var import_node_url2 = require("url");
1566
1709
  async function resolveToolVersion() {
1567
- if ("1.7.0".length > 0) {
1568
- return "1.7.0";
1710
+ if ("1.7.2".length > 0) {
1711
+ return "1.7.2";
1569
1712
  }
1570
1713
  try {
1571
1714
  const packagePath = resolvePackageJsonPath();
@@ -3324,6 +3467,7 @@ var ID_PREFIXES = [
3324
3467
  "DB",
3325
3468
  "THEMA"
3326
3469
  ];
3470
+ var DIGIT_AHEAD = "(?=[A-Za-z0-9_-]*\\d)";
3327
3471
  var STRICT_ID_PATTERNS = {
3328
3472
  CAP: /\bCAP-\d{4}\b/g,
3329
3473
  SPEC: /\bSPEC-\d{4}\b/g,
@@ -3339,18 +3483,18 @@ var STRICT_ID_PATTERNS = {
3339
3483
  ADR: /\bADR-\d{4}\b/g
3340
3484
  };
3341
3485
  var LOOSE_ID_PATTERNS = {
3342
- CAP: /\bCAP-[A-Za-z0-9_-]+\b/gi,
3343
- SPEC: /\bSPEC-[A-Za-z0-9_-]+\b/gi,
3344
- US: /\bUS-[A-Za-z0-9_-]+\b/gi,
3345
- BR: /\bBR-[A-Za-z0-9_-]+\b/gi,
3346
- SC: /\bSC-[A-Za-z0-9_-]+\b/gi,
3347
- AC: /\bAC-[A-Za-z0-9_-]+\b/gi,
3348
- CASE: /\bCASE-[A-Za-z0-9_-]+\b/gi,
3349
- UI: /\bUI-[A-Za-z0-9_-]+\b/gi,
3350
- API: /\bAPI-[A-Za-z0-9_-]+\b/gi,
3351
- DB: /\bDB-[A-Za-z0-9_-]+\b/gi,
3352
- THEMA: /\bTHEMA-[A-Za-z0-9_-]+\b/gi,
3353
- ADR: /\bADR-[A-Za-z0-9_-]+\b/gi
3486
+ CAP: new RegExp(`\\bCAP-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3487
+ SPEC: new RegExp(`\\bSPEC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3488
+ US: new RegExp(`\\bUS-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3489
+ BR: new RegExp(`\\bBR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3490
+ SC: new RegExp(`\\bSC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3491
+ AC: new RegExp(`\\bAC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3492
+ CASE: new RegExp(`\\bCASE-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3493
+ UI: new RegExp(`\\bUI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3494
+ API: new RegExp(`\\bAPI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3495
+ DB: new RegExp(`\\bDB-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3496
+ THEMA: new RegExp(`\\bTHEMA-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
3497
+ ADR: new RegExp(`\\bADR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi")
3354
3498
  };
3355
3499
  function extractIds(text, prefix) {
3356
3500
  const pattern = STRICT_ID_PATTERNS[prefix];
@@ -3932,6 +4076,8 @@ function formatError4(error2) {
3932
4076
  var ENV_AUTOGEN = "QFAI_PROTOTYPE_FIDELITY_AUTOGEN";
3933
4077
  var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
3934
4078
  async function runPrototyping(options) {
4079
+ const { config } = await loadConfig(options.root);
4080
+ const renderOptions = mergeRenderOptions(options, config.uiux?.renderEvidence);
3935
4081
  const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
3936
4082
  if (!autogenEnabled) {
3937
4083
  if (options.autogenOnly) {
@@ -3962,16 +4108,24 @@ async function runPrototyping(options) {
3962
4108
  status: "skipped",
3963
4109
  reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
3964
4110
  });
3965
- await writeEvidence(evidencePath2, skippedEvidence);
4111
+ const renderBundle2 = await maybeWriteRenderBundle(
4112
+ toolVersion2,
4113
+ "qfai prototyping --render-evidence",
4114
+ renderOptions,
4115
+ false
4116
+ );
4117
+ await writeEvidence(
4118
+ evidencePath2,
4119
+ applyRenderEvidence(skippedEvidence, renderOptions, false, renderBundle2?.path)
4120
+ );
3966
4121
  return 0;
3967
4122
  }
3968
- const baseUrl = resolveBaseUrl(options);
4123
+ const baseUrl = resolveBaseUrl(renderOptions);
3969
4124
  if (!baseUrl) {
3970
4125
  error(`prototyping: --base-url \u307E\u305F\u306F QFAI_PROTOTYPE_BASE_URL \u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002`);
3971
4126
  return 1;
3972
4127
  }
3973
4128
  const toolVersion = await resolveToolVersion();
3974
- const { config } = await loadConfig(options.root);
3975
4129
  const evidencePath = resolveEvidencePath(options.root, options.evidenceOut);
3976
4130
  info(`prototyping: autogen uiFidelity \u3092\u5B9F\u884C\u3057\u307E\u3059 (baseUrl=${baseUrl})`);
3977
4131
  let existingEvidence = {};
@@ -3998,7 +4152,16 @@ async function runPrototyping(options) {
3998
4152
  status: "failed",
3999
4153
  reason
4000
4154
  });
4001
- await writeEvidence(evidencePath, failedEvidence);
4155
+ const renderBundle2 = await maybeWriteRenderBundle(
4156
+ toolVersion,
4157
+ "qfai prototyping --render-evidence",
4158
+ renderOptions,
4159
+ true
4160
+ );
4161
+ await writeEvidence(
4162
+ evidencePath,
4163
+ applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
4164
+ );
4002
4165
  info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4003
4166
  return options.autogenOnly ? 1 : 0;
4004
4167
  }
@@ -4017,7 +4180,16 @@ async function runPrototyping(options) {
4017
4180
  crawled: result.crawled,
4018
4181
  reason
4019
4182
  });
4020
- await writeEvidence(evidencePath, failedEvidence);
4183
+ const renderBundle2 = await maybeWriteRenderBundle(
4184
+ toolVersion,
4185
+ "qfai prototyping --render-evidence",
4186
+ renderOptions,
4187
+ true
4188
+ );
4189
+ await writeEvidence(
4190
+ evidencePath,
4191
+ applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
4192
+ );
4021
4193
  info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4022
4194
  return options.autogenOnly ? 1 : 0;
4023
4195
  }
@@ -4030,7 +4202,16 @@ async function runPrototyping(options) {
4030
4202
  screens: result.screens,
4031
4203
  crawled: result.crawled
4032
4204
  });
4033
- await writeEvidence(evidencePath, successEvidence);
4205
+ const renderBundle = await maybeWriteRenderBundle(
4206
+ toolVersion,
4207
+ "qfai prototyping --render-evidence",
4208
+ renderOptions,
4209
+ true
4210
+ );
4211
+ await writeEvidence(
4212
+ evidencePath,
4213
+ applyRenderEvidence(successEvidence, renderOptions, true, renderBundle?.path)
4214
+ );
4034
4215
  const routeOkCount = result.crawled.filter((r) => r.status === "ok").length;
4035
4216
  const routeFailCount = result.crawled.filter((r) => r.status === "failed").length;
4036
4217
  const avgCoverage = result.screens.length > 0 ? (result.screens.reduce((sum, s) => sum + s.coverage, 0) / result.screens.length).toFixed(2) : "N/A";
@@ -4060,6 +4241,75 @@ async function writeEvidence(filePath, evidence) {
4060
4241
  await (0, import_promises14.mkdir)(import_node_path17.default.dirname(filePath), { recursive: true });
4061
4242
  await (0, import_promises14.writeFile)(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4062
4243
  }
4244
+ async function writeRenderBundle(filePath, bundle) {
4245
+ await (0, import_promises14.mkdir)(import_node_path17.default.dirname(filePath), { recursive: true });
4246
+ await (0, import_promises14.writeFile)(filePath, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
4247
+ }
4248
+ async function maybeWriteRenderBundle(toolVersion, command, options, autogenEnabled) {
4249
+ if (!options.renderEvidence) {
4250
+ return void 0;
4251
+ }
4252
+ const renderBundle = buildRenderBundle(toolVersion, command, options, autogenEnabled);
4253
+ await writeRenderBundle(renderBundle.path, renderBundle.bundle);
4254
+ return renderBundle;
4255
+ }
4256
+ function applyRenderEvidence(evidence, options, autogenEnabled, outputPath) {
4257
+ if (!options.renderEvidence) {
4258
+ return evidence;
4259
+ }
4260
+ const viewports = normalizeRenderViewports(options.renderViewports);
4261
+ return {
4262
+ ...evidence,
4263
+ renderEvidence: {
4264
+ status: autogenEnabled ? "requested" : "skipped",
4265
+ requested: true,
4266
+ autogenEnabled,
4267
+ viewports,
4268
+ outputPath: outputPath ?? resolveRenderOutPath(options.root, options.renderOut),
4269
+ reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
4270
+ }
4271
+ };
4272
+ }
4273
+ function buildRenderBundle(toolVersion, command, options, autogenEnabled) {
4274
+ const path64 = resolveRenderOutPath(options.root, options.renderOut);
4275
+ return {
4276
+ path: path64,
4277
+ bundle: {
4278
+ meta: {
4279
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4280
+ toolVersion,
4281
+ commands: [command]
4282
+ },
4283
+ renderEvidence: {
4284
+ status: autogenEnabled ? "requested" : "skipped",
4285
+ requested: true,
4286
+ autogenEnabled,
4287
+ viewports: normalizeRenderViewports(options.renderViewports),
4288
+ outputPath: path64,
4289
+ reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
4290
+ }
4291
+ }
4292
+ };
4293
+ }
4294
+ function mergeRenderOptions(options, configRenderEvidence) {
4295
+ const cliViewportsSpecified = Array.isArray(options.renderViewports);
4296
+ const mergedViewports = cliViewportsSpecified ? normalizeRenderViewports(options.renderViewports) : normalizeRenderViewports(configRenderEvidence?.viewports);
4297
+ const renderOut = options.renderOut ?? configRenderEvidence?.out;
4298
+ const baseUrl = options.baseUrl ?? configRenderEvidence?.baseUrl;
4299
+ return {
4300
+ ...options,
4301
+ renderEvidence: options.renderEvidence || configRenderEvidence?.enabled === true,
4302
+ renderViewports: mergedViewports,
4303
+ ...renderOut ? { renderOut } : {},
4304
+ ...baseUrl ? { baseUrl } : {}
4305
+ };
4306
+ }
4307
+ function resolveRenderOutPath(root, explicit) {
4308
+ if (explicit) {
4309
+ return import_node_path17.default.isAbsolute(explicit) ? explicit : import_node_path17.default.resolve(root, explicit);
4310
+ }
4311
+ return import_node_path17.default.resolve(root, ".qfai/evidence/render.json");
4312
+ }
4063
4313
  function extractRouteHintsFromEvidence(evidence) {
4064
4314
  const routes = /* @__PURE__ */ new Set();
4065
4315
  const runtimeGate = evidence.runtimeGate;
@@ -4101,8 +4351,8 @@ function extractRouteHintsFromEvidence(evidence) {
4101
4351
  }
4102
4352
 
4103
4353
  // src/cli/commands/report.ts
4104
- var import_promises55 = require("fs/promises");
4105
- var import_node_path59 = __toESM(require("path"), 1);
4354
+ var import_promises58 = require("fs/promises");
4355
+ var import_node_path61 = __toESM(require("path"), 1);
4106
4356
 
4107
4357
  // src/core/normalize.ts
4108
4358
  function normalizeIssuePaths(root, issues) {
@@ -4197,8 +4447,8 @@ async function createPhaseGuardResult(phase, blockedIssue) {
4197
4447
  }
4198
4448
 
4199
4449
  // src/core/report.ts
4200
- var import_promises53 = require("fs/promises");
4201
- var import_node_path57 = __toESM(require("path"), 1);
4450
+ var import_promises56 = require("fs/promises");
4451
+ var import_node_path59 = __toESM(require("path"), 1);
4202
4452
 
4203
4453
  // src/core/contractIndex.ts
4204
4454
  var import_promises15 = require("fs/promises");
@@ -8378,11 +8628,11 @@ var import_node_path29 = __toESM(require("path"), 1);
8378
8628
  // src/core/atddTraceability.ts
8379
8629
  var import_promises26 = require("fs/promises");
8380
8630
  var import_node_path28 = __toESM(require("path"), 1);
8381
- var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4})\b/g;
8382
- var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4})\b/g;
8631
+ var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
8632
+ var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
8383
8633
  var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
8384
- var SHORT_US_ID_RE = /^US-\d{4}$/;
8385
- var SHORT_TC_ID_RE = /^TC-\d{4}$/;
8634
+ var US_ID_RE2 = /^US-\d{4}(?:-\d{4})?$/;
8635
+ var TC_ID_RE = /^TC-\d{4}(?:-\d{4})?$/;
8386
8636
  var API_CONTRACT_ID_RE = /^CON-API-\d+$/;
8387
8637
  var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
8388
8638
  async function evaluateAtddCodeTraceability(root, config) {
@@ -8530,11 +8780,11 @@ async function collectApiContractIds(apiRoot) {
8530
8780
  function collectShortIds(text, prefix) {
8531
8781
  const ids = /* @__PURE__ */ new Set();
8532
8782
  const headingIds = collectMarkdownItems(text, prefix).map((item) => item.id);
8533
- const pattern = prefix === "US" ? /\bUS-\d{4}\b/g : /\bTC-\d{4}\b/g;
8783
+ const pattern = prefix === "US" ? /\bUS-\d{4}(?:-\d{4})?\b/g : /\bTC-\d{4}(?:-\d{4})?\b/g;
8534
8784
  const looseIds = uniqueMatches(text, pattern);
8535
8785
  for (const id of [...headingIds, ...looseIds]) {
8536
8786
  const normalized = id.toUpperCase();
8537
- if (prefix === "US" && SHORT_US_ID_RE.test(normalized) || prefix === "TC" && SHORT_TC_ID_RE.test(normalized)) {
8787
+ if (prefix === "US" && US_ID_RE2.test(normalized) || prefix === "TC" && TC_ID_RE.test(normalized)) {
8538
8788
  ids.add(normalized);
8539
8789
  }
8540
8790
  }
@@ -9543,15 +9793,15 @@ var import_node_path33 = __toESM(require("path"), 1);
9543
9793
  var import_promises33 = require("fs/promises");
9544
9794
  var import_node_path34 = __toESM(require("path"), 1);
9545
9795
  var ID_PATTERNS = {
9546
- us: /^US-\d{4}$/,
9547
- ac: /^AC-\d{4}$/,
9548
- br: /^BR-\d{4}$/,
9549
- ex: /^EX-\d{4}$/
9796
+ us: /^US-\d{4}(?:-\d{4})?$/,
9797
+ ac: /^AC-\d{4}(?:-\d{4})?$/,
9798
+ br: /^BR-\d{4}(?:-\d{4})?$/,
9799
+ ex: /^EX-\d{4}(?:-\d{4})?$/
9550
9800
  };
9551
9801
  var V1421_REFS = {
9552
- ac: /\bAC-\d{4}\b/gi,
9553
- br: /\bBR-\d{4}\b/gi,
9554
- ex: /\bEX-\d{4}\b/gi
9802
+ ac: /\bAC-\d{4}(?:-\d{4})?\b/gi,
9803
+ br: /\bBR-\d{4}(?:-\d{4})?\b/gi,
9804
+ ex: /\bEX-\d{4}(?:-\d{4})?\b/gi
9555
9805
  };
9556
9806
  async function validateLayerCoverage(root, config) {
9557
9807
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -9824,11 +10074,11 @@ function parseAcceptanceCriteriaIds2(text) {
9824
10074
  const ids = /* @__PURE__ */ new Set();
9825
10075
  const lines = text.replace(/\r\n/g, "\n").split("\n");
9826
10076
  for (const line of lines) {
9827
- const headingMatch = /^##\s*(AC-\d{4})\b/i.exec(line.trim());
10077
+ const headingMatch = /^##\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line.trim());
9828
10078
  if (headingMatch?.[1]) {
9829
10079
  ids.add(headingMatch[1].toUpperCase());
9830
10080
  }
9831
- const commentMatch = /^\s*#\s*(AC-\d{4})\b/i.exec(line);
10081
+ const commentMatch = /^\s*#\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line);
9832
10082
  if (commentMatch?.[1]) {
9833
10083
  ids.add(commentMatch[1].toUpperCase());
9834
10084
  }
@@ -9848,8 +10098,8 @@ function parseAcceptanceCriteriaIds2(text) {
9848
10098
  function parseDefinitionRefs(text, prefix, refPattern, options = {}) {
9849
10099
  const lines = text.replace(/\r\n/g, "\n").split("\n");
9850
10100
  const refsById = /* @__PURE__ */ new Map();
9851
- const idPattern = new RegExp(`^${prefix}-\\d{4}$`);
9852
- const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4})\\b`, "i");
10101
+ const idPattern = new RegExp(`^${prefix}-\\d{4}(?:-\\d{4})?$`);
10102
+ const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4}(?:-\\d{4})?)\\b`, "i");
9853
10103
  const referenceColumns = new Set(
9854
10104
  (options.referenceColumns ?? []).map((column) => normalizeColumnName(column))
9855
10105
  );
@@ -10250,7 +10500,7 @@ var US_DOWNSTREAM_RE = /\b(?:AC|BR|EX|TC)-\d{4}\b/g;
10250
10500
  var AC_DOWNSTREAM_RE = /\b(?:BR|EX|TC)-\d{4}\b/g;
10251
10501
  var BR_DOWNSTREAM_RE = /\b(?:EX|TC)-\d{4}\b/g;
10252
10502
  var CAP_ID_RE = /^CAP-\d{4}$/;
10253
- var US_ID_RE2 = /^US-\d{4}$/;
10503
+ var US_ID_RE3 = /^US-\d{4}$/;
10254
10504
  var AC_ID_RE4 = /^AC-\d{4}$/;
10255
10505
  var BR_OR_AC_ID_RE = /^(?:BR|AC)-\d{4}$/;
10256
10506
  var EX_ID_RE2 = /^EX-\d{4}$/;
@@ -10289,7 +10539,7 @@ async function validateLayeredTraceability(root, config) {
10289
10539
  ...await validateMarkdownParentFormat(entry.userStoriesPath, "US", CAP_ID_RE, "CAP")
10290
10540
  );
10291
10541
  issues.push(
10292
- ...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE2, "US")
10542
+ ...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE3, "US")
10293
10543
  );
10294
10544
  issues.push(
10295
10545
  ...await validateMarkdownParentFormat(entry.businessRulesPath, "BR", AC_ID_RE4, "AC")
@@ -11601,6 +11851,100 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
11601
11851
  )
11602
11852
  );
11603
11853
  }
11854
+ const renderIssues = await validateRenderEvidenceScreens(
11855
+ root,
11856
+ config,
11857
+ evidenceJsonPath,
11858
+ uiFidelity.screens
11859
+ );
11860
+ issues.push(...renderIssues);
11861
+ return issues;
11862
+ }
11863
+ async function validateRenderEvidenceScreens(root, config, evidenceJsonPath, screens) {
11864
+ const issues = [];
11865
+ const hasAnyRenderEvidence = screens.some((screen) => screen.renders.length > 0);
11866
+ if (!hasAnyRenderEvidence) {
11867
+ return issues;
11868
+ }
11869
+ const qualityProfile = config.uiux?.qualityProfile ?? "default";
11870
+ for (const screen of screens) {
11871
+ if (screen.renders.length === 0) {
11872
+ continue;
11873
+ }
11874
+ const viewports = new Set(screen.renders.map((render) => render.viewport));
11875
+ const missingDefaultViewports = DEFAULT_RENDER_VIEWPORTS.filter(
11876
+ (viewport) => !viewports.has(viewport)
11877
+ );
11878
+ const allSkipped = screen.renders.every((render) => render.status === "skipped");
11879
+ for (const render of screen.renders) {
11880
+ if (render.status !== "captured") {
11881
+ continue;
11882
+ }
11883
+ const invalidPaths = [
11884
+ { label: "imagePath", value: render.imagePath },
11885
+ { label: "htmlPath", value: render.htmlPath }
11886
+ ].filter((entry) => looksLikeInlineRenderPayload(entry.value));
11887
+ if (invalidPaths.length > 0) {
11888
+ issues.push(
11889
+ issue(
11890
+ "QFAI-PROT-244",
11891
+ `QFAI-PROT-244: render evidence must be path-only. route=${screen.route}, viewport=${render.viewport}, invalid=${invalidPaths.map((entry) => entry.label).join("|")}`,
11892
+ "error",
11893
+ evidenceJsonPath,
11894
+ "prototypingEvidence.renderArtifactPresence",
11895
+ [
11896
+ `route=${screen.route}`,
11897
+ `viewport=${render.viewport}`,
11898
+ ...invalidPaths.map((entry) => `artifact=${entry.label}`)
11899
+ ],
11900
+ "change",
11901
+ "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"
11902
+ )
11903
+ );
11904
+ continue;
11905
+ }
11906
+ const missingArtifacts = await collectMissingRenderArtifacts(root, render);
11907
+ if (missingArtifacts.length > 0) {
11908
+ issues.push(
11909
+ issue(
11910
+ "QFAI-PROT-244",
11911
+ `QFAI-PROT-244: captured render artifact is missing. route=${screen.route}, viewport=${render.viewport}, missing=${missingArtifacts.join("|")}`,
11912
+ "error",
11913
+ evidenceJsonPath,
11914
+ "prototypingEvidence.renderArtifactPresence",
11915
+ [
11916
+ `route=${screen.route}`,
11917
+ `viewport=${render.viewport}`,
11918
+ ...missingArtifacts.map((artifact) => `artifact=${artifact}`)
11919
+ ],
11920
+ "change",
11921
+ "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"
11922
+ )
11923
+ );
11924
+ }
11925
+ }
11926
+ if (missingDefaultViewports.length === 0 && !allSkipped) {
11927
+ continue;
11928
+ }
11929
+ const severity = qualityProfile === "default" ? "warning" : "error";
11930
+ const reason = allSkipped ? "all renders are skipped" : `missing default viewports=${missingDefaultViewports.join("|")}`;
11931
+ issues.push(
11932
+ issue(
11933
+ "QFAI-PROT-245",
11934
+ `QFAI-PROT-245: render coverage is incomplete for ${screen.route}. ${reason}. qualityProfile=${qualityProfile}`,
11935
+ severity,
11936
+ evidenceJsonPath,
11937
+ "prototypingEvidence.renderCoverage",
11938
+ [
11939
+ `route=${screen.route}`,
11940
+ ...missingDefaultViewports.map((viewport) => `viewport=${viewport}`),
11941
+ `qualityProfile=${qualityProfile}`
11942
+ ],
11943
+ "change",
11944
+ 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"
11945
+ )
11946
+ );
11947
+ }
11604
11948
  return issues;
11605
11949
  }
11606
11950
  function formatUiFidelityMismatch(mismatch) {
@@ -11813,6 +12157,10 @@ function normalizeUiFidelityScreen(value) {
11813
12157
  if (!mockPaths.ok) {
11814
12158
  return mockPaths;
11815
12159
  }
12160
+ const renders = normalizeRenderEntries(value.renders);
12161
+ if (!renders.ok) {
12162
+ return renders;
12163
+ }
11816
12164
  return {
11817
12165
  ok: true,
11818
12166
  value: {
@@ -11823,8 +12171,103 @@ function normalizeUiFidelityScreen(value) {
11823
12171
  ...normalizeOptionalMissingBlock(value.missing),
11824
12172
  ...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
11825
12173
  observed: observed.value,
11826
- mockPaths: mockPaths.value
12174
+ mockPaths: mockPaths.value,
12175
+ renders: renders.value
12176
+ }
12177
+ };
12178
+ }
12179
+ function normalizeRenderEntries(value) {
12180
+ if (value === void 0) {
12181
+ return { ok: true, value: [] };
12182
+ }
12183
+ if (!Array.isArray(value)) {
12184
+ return { ok: false, reason: "`uiFidelity.screens[].renders` must be an array" };
12185
+ }
12186
+ const renders = [];
12187
+ for (const entry of value) {
12188
+ const normalized = normalizeRenderEntry(entry);
12189
+ if (!normalized.ok) {
12190
+ return normalized;
12191
+ }
12192
+ renders.push(normalized.value);
12193
+ }
12194
+ return { ok: true, value: renders };
12195
+ }
12196
+ function normalizeRenderEntry(value) {
12197
+ if (!isRecord5(value)) {
12198
+ return { ok: false, reason: "`uiFidelity.screens[].renders[]` must be objects" };
12199
+ }
12200
+ if (typeof value.viewport !== "string" || value.viewport.trim().length === 0) {
12201
+ return { ok: false, reason: "`uiFidelity.screens[].renders[].viewport` is required" };
12202
+ }
12203
+ if (!isNonNegativeInteger(value.width) || !isNonNegativeInteger(value.height) || value.width === 0 || value.height === 0) {
12204
+ return {
12205
+ ok: false,
12206
+ reason: "`uiFidelity.screens[].renders[]` requires positive integers for width/height"
12207
+ };
12208
+ }
12209
+ const viewport = value.viewport.trim();
12210
+ const width = value.width;
12211
+ const height = value.height;
12212
+ const status = typeof value.status === "string" ? value.status.trim().toLowerCase() : "";
12213
+ if (status === "captured") {
12214
+ if (typeof value.imagePath !== "string" || value.imagePath.trim().length === 0 || typeof value.htmlPath !== "string" || value.htmlPath.trim().length === 0) {
12215
+ return {
12216
+ ok: false,
12217
+ reason: "`captured` render entries require imagePath and htmlPath"
12218
+ };
12219
+ }
12220
+ return {
12221
+ ok: true,
12222
+ value: {
12223
+ viewport,
12224
+ status: "captured",
12225
+ width,
12226
+ height,
12227
+ imagePath: value.imagePath.trim(),
12228
+ htmlPath: value.htmlPath.trim()
12229
+ }
12230
+ };
12231
+ }
12232
+ if (status === "skipped") {
12233
+ if (typeof value.skippedReason !== "string" || value.skippedReason.trim().length === 0) {
12234
+ return {
12235
+ ok: false,
12236
+ reason: "`skipped` render entries require skippedReason"
12237
+ };
12238
+ }
12239
+ return {
12240
+ ok: true,
12241
+ value: {
12242
+ viewport,
12243
+ status: "skipped",
12244
+ width,
12245
+ height,
12246
+ skippedReason: value.skippedReason.trim()
12247
+ }
12248
+ };
12249
+ }
12250
+ if (status === "failed") {
12251
+ if (typeof value.error !== "string" || value.error.trim().length === 0) {
12252
+ return {
12253
+ ok: false,
12254
+ reason: "`failed` render entries require error"
12255
+ };
11827
12256
  }
12257
+ return {
12258
+ ok: true,
12259
+ value: {
12260
+ viewport,
12261
+ status: "failed",
12262
+ width,
12263
+ height,
12264
+ error: value.error.trim()
12265
+ }
12266
+ };
12267
+ }
12268
+ return {
12269
+ ok: false,
12270
+ reason: "`uiFidelity.screens[].renders[].status` must be captured|skipped|failed"
11828
12271
  };
11829
12272
  }
11830
12273
  function normalizeUiFidelityExpected(value) {
@@ -12050,6 +12493,22 @@ function normalizeOptionalMissingBlock(value) {
12050
12493
  function isRecord5(value) {
12051
12494
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12052
12495
  }
12496
+ async function collectMissingRenderArtifacts(root, render) {
12497
+ const missing = [];
12498
+ const candidates = [
12499
+ { label: "imagePath", target: render.imagePath },
12500
+ { label: "htmlPath", target: render.htmlPath }
12501
+ ];
12502
+ for (const candidate of candidates) {
12503
+ const resolved = import_node_path39.default.isAbsolute(candidate.target) ? candidate.target : import_node_path39.default.resolve(root, candidate.target);
12504
+ try {
12505
+ await (0, import_promises37.access)(resolved);
12506
+ } catch {
12507
+ missing.push(candidate.label);
12508
+ }
12509
+ }
12510
+ return missing;
12511
+ }
12053
12512
  function isInteger(value) {
12054
12513
  return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
12055
12514
  }
@@ -12398,7 +12857,7 @@ function collectLayer(layer, layerName, target, errors) {
12398
12857
  }
12399
12858
  function flattenTokens(obj, prefix, target, errors) {
12400
12859
  for (const [key, value] of Object.entries(obj)) {
12401
- const path62 = `${prefix}.${key}`;
12860
+ const path64 = `${prefix}.${key}`;
12402
12861
  if (value && typeof value === "object" && !Array.isArray(value)) {
12403
12862
  const record2 = value;
12404
12863
  if ("$value" in record2) {
@@ -12414,9 +12873,9 @@ function flattenTokens(obj, prefix, target, errors) {
12414
12873
  if (typeof record2.platform === "string") {
12415
12874
  token.platform = record2.platform;
12416
12875
  }
12417
- target.set(path62, token);
12876
+ target.set(path64, token);
12418
12877
  } else {
12419
- flattenTokens(record2, path62, target, errors);
12878
+ flattenTokens(record2, path64, target, errors);
12420
12879
  }
12421
12880
  }
12422
12881
  }
@@ -12426,44 +12885,44 @@ function resolveAllReferences(result) {
12426
12885
  for (const [key, val] of result.primitives) allTokens.set(key, val);
12427
12886
  for (const [key, val] of result.semantics) allTokens.set(key, val);
12428
12887
  for (const [key, val] of result.components) allTokens.set(key, val);
12429
- for (const [path62] of allTokens) {
12430
- resolveTokenRef(path62, allTokens, /* @__PURE__ */ new Set(), 0, result);
12888
+ for (const [path64] of allTokens) {
12889
+ resolveTokenRef(path64, allTokens, /* @__PURE__ */ new Set(), 0, result);
12431
12890
  }
12432
12891
  }
12433
- function resolveTokenRef(path62, allTokens, visited, depth, result) {
12434
- if (result.resolved.has(path62)) {
12435
- return result.resolved.get(path62);
12892
+ function resolveTokenRef(path64, allTokens, visited, depth, result) {
12893
+ if (result.resolved.has(path64)) {
12894
+ return result.resolved.get(path64);
12436
12895
  }
12437
12896
  if (depth > MAX_RESOLVE_DEPTH) {
12438
12897
  result.errors.push({
12439
- message: `Max reference depth exceeded at: ${path62}`,
12440
- path: path62
12898
+ message: `Max reference depth exceeded at: ${path64}`,
12899
+ path: path64
12441
12900
  });
12442
12901
  return void 0;
12443
12902
  }
12444
- if (visited.has(path62)) {
12903
+ if (visited.has(path64)) {
12445
12904
  result.errors.push({
12446
- message: `Circular reference detected: ${path62}`,
12447
- path: path62
12905
+ message: `Circular reference detected: ${path64}`,
12906
+ path: path64
12448
12907
  });
12449
12908
  return void 0;
12450
12909
  }
12451
- const token = allTokens.get(path62);
12910
+ const token = allTokens.get(path64);
12452
12911
  if (!token) {
12453
12912
  return void 0;
12454
12913
  }
12455
12914
  if (typeof token.$value !== "string") {
12456
12915
  const rawValue2 = stringifyTokenValue(token.$value);
12457
- result.resolved.set(path62, rawValue2);
12916
+ result.resolved.set(path64, rawValue2);
12458
12917
  return rawValue2;
12459
12918
  }
12460
12919
  const rawValue = stringifyTokenValue(token.$value);
12461
12920
  const refs = [...rawValue.matchAll(REF_PATTERN)];
12462
12921
  if (refs.length === 0) {
12463
- result.resolved.set(path62, rawValue);
12922
+ result.resolved.set(path64, rawValue);
12464
12923
  return rawValue;
12465
12924
  }
12466
- visited.add(path62);
12925
+ visited.add(path64);
12467
12926
  let resolved = rawValue;
12468
12927
  for (const ref of refs) {
12469
12928
  const refPath = ref[1];
@@ -12471,8 +12930,8 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
12471
12930
  const refToken = allTokens.get(refPath);
12472
12931
  if (!refToken) {
12473
12932
  result.errors.push({
12474
- message: `Unresolved token reference: {${refPath}} at ${path62}`,
12475
- path: path62
12933
+ message: `Unresolved token reference: {${refPath}} at ${path64}`,
12934
+ path: path64
12476
12935
  });
12477
12936
  continue;
12478
12937
  }
@@ -12481,7 +12940,7 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
12481
12940
  resolved = resolved.split(`{${refPath}}`).join(refValue);
12482
12941
  }
12483
12942
  }
12484
- result.resolved.set(path62, resolved);
12943
+ result.resolved.set(path64, resolved);
12485
12944
  return resolved;
12486
12945
  }
12487
12946
  function stringifyTokenValue(value) {
@@ -15593,6 +16052,7 @@ async function validateNavigationFlow(root, config) {
15593
16052
 
15594
16053
  // src/core/validators/renderCritique.ts
15595
16054
  var import_node_path54 = __toESM(require("path"), 1);
16055
+ var import_promises52 = require("fs/promises");
15596
16056
  var import_fast_glob9 = __toESM(require("fast-glob"), 1);
15597
16057
  var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
15598
16058
  var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
@@ -15624,6 +16084,7 @@ async function validateRenderCritique(root, config) {
15624
16084
  if (!hasDdp) return issues;
15625
16085
  const skillsDir = import_node_path54.default.join(root, config.paths.skillsDir).replace(/\\/g, "/");
15626
16086
  const evidenceDir = import_node_path54.default.join(root, ".qfai", "evidence").replace(/\\/g, "/");
16087
+ const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
15627
16088
  const skillPromptPattern = import_node_path54.default.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
15628
16089
  const skillFiles = await (0, import_fast_glob9.default)(skillPromptPattern, { dot: true });
15629
16090
  const evidencePattern = import_node_path54.default.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
@@ -15663,7 +16124,7 @@ async function validateRenderCritique(root, config) {
15663
16124
  }
15664
16125
  }
15665
16126
  const allEvidenceContent = await collectContent(evidenceFiles);
15666
- if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
16127
+ if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("desktop")) {
15667
16128
  issues.push(
15668
16129
  issue(
15669
16130
  "QFAI-CRIT-003",
@@ -15677,7 +16138,7 @@ async function validateRenderCritique(root, config) {
15677
16138
  )
15678
16139
  );
15679
16140
  }
15680
- if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
16141
+ if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("mobile")) {
15681
16142
  issues.push(
15682
16143
  issue(
15683
16144
  "QFAI-CRIT-004",
@@ -15838,9 +16299,49 @@ async function collectContent(files) {
15838
16299
  }
15839
16300
  return contents.join("\n---\n");
15840
16301
  }
16302
+ async function collectRenderEvidenceViewports(root) {
16303
+ const prototypingJsonPath = import_node_path54.default.join(root, ".qfai", "evidence", "prototyping.json");
16304
+ try {
16305
+ const raw = await (0, import_promises52.readFile)(prototypingJsonPath, "utf-8");
16306
+ const parsed = JSON.parse(raw);
16307
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16308
+ return /* @__PURE__ */ new Set();
16309
+ }
16310
+ const uiFidelity = parsed.uiFidelity;
16311
+ if (!uiFidelity || typeof uiFidelity !== "object" || Array.isArray(uiFidelity)) {
16312
+ return /* @__PURE__ */ new Set();
16313
+ }
16314
+ const screens = uiFidelity.screens;
16315
+ if (!Array.isArray(screens)) {
16316
+ return /* @__PURE__ */ new Set();
16317
+ }
16318
+ const viewports = /* @__PURE__ */ new Set();
16319
+ for (const screen of screens) {
16320
+ if (!screen || typeof screen !== "object" || Array.isArray(screen)) {
16321
+ continue;
16322
+ }
16323
+ const renders = screen.renders;
16324
+ if (!Array.isArray(renders)) {
16325
+ continue;
16326
+ }
16327
+ for (const render of renders) {
16328
+ if (!render || typeof render !== "object" || Array.isArray(render)) {
16329
+ continue;
16330
+ }
16331
+ const viewport = render.viewport;
16332
+ if (typeof viewport === "string" && viewport.trim().length > 0) {
16333
+ viewports.add(viewport.trim().toLowerCase());
16334
+ }
16335
+ }
16336
+ }
16337
+ return viewports;
16338
+ } catch {
16339
+ return /* @__PURE__ */ new Set();
16340
+ }
16341
+ }
15841
16342
 
15842
16343
  // src/core/validators/designFidelity.ts
15843
- var import_promises52 = require("fs/promises");
16344
+ var import_promises53 = require("fs/promises");
15844
16345
  var import_node_path55 = __toESM(require("path"), 1);
15845
16346
  var import_fast_glob10 = __toESM(require("fast-glob"), 1);
15846
16347
  var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
@@ -15873,7 +16374,7 @@ async function validateDesignFidelity(root, config) {
15873
16374
  for (const filePath of allFiles) {
15874
16375
  let content;
15875
16376
  try {
15876
- content = await (0, import_promises52.readFile)(filePath, "utf-8");
16377
+ content = await (0, import_promises53.readFile)(filePath, "utf-8");
15877
16378
  } catch {
15878
16379
  continue;
15879
16380
  }
@@ -16496,6 +16997,252 @@ async function validateDiscussionDesignHardening(root, config) {
16496
16997
  return issues;
16497
16998
  }
16498
16999
 
17000
+ // src/core/validators/designAudit.ts
17001
+ var import_promises54 = require("fs/promises");
17002
+ var import_node_path57 = __toESM(require("path"), 1);
17003
+ var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
17004
+ function resolveAuditConfig(config) {
17005
+ const audit = config.uiux?.audit;
17006
+ const profile = config.uiux?.qualityProfile ?? "default";
17007
+ return {
17008
+ enabled: audit?.enabled ?? true,
17009
+ slopDetection: audit?.slopDetection ?? true,
17010
+ qualityProfile: profile,
17011
+ maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
17012
+ maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
17013
+ maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
17014
+ };
17015
+ }
17016
+ function mapSeverity(tier, profile, category) {
17017
+ if (tier === 1) return "error";
17018
+ if (tier === 2) return profile === "strict" ? "error" : "warning";
17019
+ if (profile === "default") {
17020
+ return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
17021
+ }
17022
+ return "warning";
17023
+ }
17024
+ function findingToIssue(finding, profile, rulePrefix = "audit") {
17025
+ const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
17026
+ return issue(
17027
+ finding.ruleId,
17028
+ finding.message,
17029
+ severity,
17030
+ finding.file,
17031
+ `${rulePrefix}.${finding.dimension}`,
17032
+ finding.evidence.length > 0 ? finding.evidence : void 0,
17033
+ "compatibility",
17034
+ finding.guidance
17035
+ );
17036
+ }
17037
+ function extractSection2(content, heading) {
17038
+ const idx = content.indexOf(heading);
17039
+ if (idx === -1) return null;
17040
+ const start = idx + heading.length;
17041
+ const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
17042
+ const rest = content.slice(start);
17043
+ const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
17044
+ const nextHeadingMatch = headingPattern.exec(rest);
17045
+ const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
17046
+ return sectionContent.trim() || null;
17047
+ }
17048
+ function checkCtaHierarchy(content, auditConfig, file) {
17049
+ const findings = [];
17050
+ const ctaSection = extractSection2(content, "### CTA Hierarchy");
17051
+ if (!ctaSection) return findings;
17052
+ const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
17053
+ const primaryCount = primaryLines.length;
17054
+ if (primaryCount === 0) {
17055
+ findings.push({
17056
+ ruleId: "QFAI-AUD-001",
17057
+ dimension: "visualHierarchy",
17058
+ severityTier: 1,
17059
+ message: "No primary CTA defined in CTA Hierarchy",
17060
+ why: "Every UI screen needs a clear primary action to guide users",
17061
+ evidence: [],
17062
+ guidance: "Define at least one primary CTA in the CTA Hierarchy section",
17063
+ file
17064
+ });
17065
+ }
17066
+ if (primaryCount > auditConfig.maxPrimaryCtas) {
17067
+ findings.push({
17068
+ ruleId: "QFAI-AUD-020",
17069
+ dimension: "visualHierarchy",
17070
+ severityTier: 2,
17071
+ message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
17072
+ why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
17073
+ evidence: primaryLines.map((l) => l.trim()),
17074
+ guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
17075
+ file
17076
+ });
17077
+ }
17078
+ return findings;
17079
+ }
17080
+ var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
17081
+ async function checkTokenDrift(root, auditConfig, cfg) {
17082
+ const findings = [];
17083
+ const configuredDir = cfg.uiux?.designTokensDir;
17084
+ const tokensDir = configuredDir ? import_node_path57.default.resolve(root, configuredDir) : import_node_path57.default.join(root, cfg.paths.contractsDir, "design");
17085
+ let hasTokenFiles = false;
17086
+ try {
17087
+ const entries = await (0, import_promises54.readdir)(tokensDir);
17088
+ hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
17089
+ } catch {
17090
+ return findings;
17091
+ }
17092
+ if (!hasTokenFiles) return findings;
17093
+ const contractsUiDir = import_node_path57.default.join(root, cfg.paths.contractsDir, "ui");
17094
+ let htmlFiles = [];
17095
+ try {
17096
+ const entries = await (0, import_promises54.readdir)(contractsUiDir);
17097
+ htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
17098
+ } catch {
17099
+ return findings;
17100
+ }
17101
+ let rawCount = 0;
17102
+ const sampleLiterals = [];
17103
+ for (const htmlFile of htmlFiles) {
17104
+ const content = await readSafe(import_node_path57.default.join(contractsUiDir, htmlFile));
17105
+ if (!content) continue;
17106
+ const matches = content.match(RAW_COLOR_RE);
17107
+ if (matches) {
17108
+ rawCount += matches.length;
17109
+ for (const m of matches) {
17110
+ if (sampleLiterals.length < 10) {
17111
+ sampleLiterals.push(m.toLowerCase());
17112
+ }
17113
+ }
17114
+ }
17115
+ }
17116
+ if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
17117
+ findings.push({
17118
+ ruleId: "QFAI-AUD-004",
17119
+ dimension: "tokenDiscipline",
17120
+ severityTier: 1,
17121
+ message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
17122
+ why: "Raw color values bypass design tokens, causing visual inconsistency",
17123
+ evidence: sampleLiterals,
17124
+ guidance: "Replace raw color literals with design token references"
17125
+ });
17126
+ }
17127
+ return findings;
17128
+ }
17129
+ function deduplicateFindings(issues, maxPerRule) {
17130
+ const counts = /* @__PURE__ */ new Map();
17131
+ const result = [];
17132
+ for (const iss of issues) {
17133
+ const count = counts.get(iss.code) ?? 0;
17134
+ if (count < maxPerRule) {
17135
+ result.push(iss);
17136
+ }
17137
+ counts.set(iss.code, count + 1);
17138
+ }
17139
+ for (const [code, count] of counts) {
17140
+ if (count > maxPerRule) {
17141
+ result.push({
17142
+ code,
17143
+ severity: "info",
17144
+ category: "compatibility",
17145
+ message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
17146
+ rule: `audit.dedup.${code}`
17147
+ });
17148
+ }
17149
+ }
17150
+ return result;
17151
+ }
17152
+ async function validateDesignAudit(root, config) {
17153
+ const auditConfig = resolveAuditConfig(config);
17154
+ if (!auditConfig.enabled) return [];
17155
+ const discussionDir = import_node_path57.default.join(root, config.paths.discussionDir);
17156
+ const packRoot = await findLatestDiscussionPackDir(discussionDir);
17157
+ if (!packRoot) return [];
17158
+ const uiBearing = await isUiBearing(packRoot);
17159
+ if (!uiBearing) return [];
17160
+ const storyPath = import_node_path57.default.join(packRoot, "03_Story-Workshop.md");
17161
+ const content = await readSafe(storyPath);
17162
+ if (!content) return [];
17163
+ const findings = [];
17164
+ findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
17165
+ findings.push(...await checkTokenDrift(root, auditConfig, config));
17166
+ const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
17167
+ return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
17168
+ }
17169
+
17170
+ // src/core/validators/designSlop.ts
17171
+ var import_node_fs2 = require("fs");
17172
+ var import_promises55 = require("fs/promises");
17173
+ var import_node_path58 = __toESM(require("path"), 1);
17174
+ var import_node_url4 = require("url");
17175
+ function isValidSlopPattern(rule) {
17176
+ if (typeof rule !== "object" || rule === null) return false;
17177
+ const r = rule;
17178
+ 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";
17179
+ }
17180
+ async function loadSlopPatterns(jsonPath) {
17181
+ const raw = await (0, import_promises55.readFile)(jsonPath, "utf-8");
17182
+ const parsed = JSON.parse(raw);
17183
+ if (!Array.isArray(parsed)) return [];
17184
+ return parsed.filter((r) => isValidSlopPattern(r));
17185
+ }
17186
+ function defaultPatternsPath() {
17187
+ const base = __filename;
17188
+ const basePath = base.startsWith("file:") ? (0, import_node_url4.fileURLToPath)(base) : base;
17189
+ const baseDir = import_node_path58.default.dirname(basePath);
17190
+ const candidates = [
17191
+ import_node_path58.default.join(baseDir, "designSlopPatterns.json"),
17192
+ import_node_path58.default.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
17193
+ import_node_path58.default.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
17194
+ ];
17195
+ for (const c of candidates) {
17196
+ if ((0, import_node_fs2.existsSync)(c)) return c;
17197
+ }
17198
+ return candidates[0];
17199
+ }
17200
+ async function validateDesignSlop(root, config) {
17201
+ const auditConfig = resolveAuditConfig(config);
17202
+ if (!auditConfig.enabled) return [];
17203
+ if (!auditConfig.slopDetection) return [];
17204
+ const discussionDir = import_node_path58.default.join(root, config.paths.discussionDir);
17205
+ const packRoot = await findLatestDiscussionPackDir(discussionDir);
17206
+ if (!packRoot) return [];
17207
+ const uiBearing = await isUiBearing(packRoot);
17208
+ if (!uiBearing) return [];
17209
+ let patterns;
17210
+ try {
17211
+ patterns = await loadSlopPatterns(defaultPatternsPath());
17212
+ } catch {
17213
+ return [];
17214
+ }
17215
+ const findings = [];
17216
+ const seenRules = /* @__PURE__ */ new Set();
17217
+ for (const pattern of patterns) {
17218
+ let regex;
17219
+ try {
17220
+ regex = new RegExp(pattern.match, "gi");
17221
+ } catch {
17222
+ continue;
17223
+ }
17224
+ for (const scope of pattern.scopes) {
17225
+ const filePath = import_node_path58.default.join(packRoot, scope);
17226
+ const content = await readSafe(filePath);
17227
+ if (!content) continue;
17228
+ if (regex.test(content) && !seenRules.has(pattern.id)) {
17229
+ seenRules.add(pattern.id);
17230
+ findings.push({
17231
+ ruleId: pattern.id,
17232
+ dimension: pattern.category,
17233
+ severityTier: pattern.tier,
17234
+ message: pattern.message,
17235
+ why: `Slop pattern "${pattern.id}" matched in ${scope}`,
17236
+ evidence: [],
17237
+ guidance: pattern.guidance,
17238
+ file: scope
17239
+ });
17240
+ }
17241
+ }
17242
+ }
17243
+ return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
17244
+ }
17245
+
16499
17246
  // src/core/validate.ts
16500
17247
  var UIUX_VALIDATION_BUDGET_MS = 2e3;
16501
17248
  async function validateProject(root, configResult, options = {}) {
@@ -16513,7 +17260,9 @@ async function validateProject(root, configResult, options = {}) {
16513
17260
  () => validateBpApDb(root, config),
16514
17261
  () => validateUiDefinitionConsistency(root, config),
16515
17262
  () => validateResearchSummary(root, config),
16516
- () => validateAgentDefinition(root, config)
17263
+ () => validateAgentDefinition(root, config),
17264
+ () => validateDesignAudit(root, config),
17265
+ () => validateDesignSlop(root, config)
16517
17266
  ];
16518
17267
  const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
16519
17268
  const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
@@ -16600,15 +17349,15 @@ var REPORT_GUARDRAILS_MAX = 20;
16600
17349
  var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
16601
17350
  var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
16602
17351
  async function createReportData(root, validation, configResult) {
16603
- const resolvedRoot = import_node_path57.default.resolve(root);
17352
+ const resolvedRoot = import_node_path59.default.resolve(root);
16604
17353
  const resolved = configResult ?? await loadConfig(resolvedRoot);
16605
17354
  const config = resolved.config;
16606
17355
  const configPath = resolved.configPath;
16607
17356
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
16608
17357
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
16609
- const apiRoot = import_node_path57.default.join(contractsRoot, "api");
16610
- const uiRoot = import_node_path57.default.join(contractsRoot, "ui");
16611
- const dbRoot = import_node_path57.default.join(contractsRoot, "db");
17358
+ const apiRoot = import_node_path59.default.join(contractsRoot, "api");
17359
+ const uiRoot = import_node_path59.default.join(contractsRoot, "ui");
17360
+ const dbRoot = import_node_path59.default.join(contractsRoot, "db");
16612
17361
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
16613
17362
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
16614
17363
  const specEntries = await collectSpecEntries(specsRoot);
@@ -16936,6 +17685,8 @@ function formatReportMarkdown(data, options = {}) {
16936
17685
  lines.push("");
16937
17686
  lines.push("- [Compatibility Issues](#compatibility-issues)");
16938
17687
  lines.push("- [Change Issues](#change-issues)");
17688
+ lines.push("- [Design Audit Findings](#design-audit-findings)");
17689
+ lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
16939
17690
  lines.push("- [Change Type](#change-type)");
16940
17691
  lines.push("- [Waivers](#waivers)");
16941
17692
  lines.push("- [Decision Guardrails](#decision-guardrails)");
@@ -17084,6 +17835,46 @@ function formatReportMarkdown(data, options = {}) {
17084
17835
  lines.push("### Issues");
17085
17836
  lines.push("");
17086
17837
  lines.push(...formatIssueCards(issuesByCategory.change));
17838
+ const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
17839
+ if (auditIssues.length > 0) {
17840
+ lines.push("## Design Audit Findings");
17841
+ lines.push("");
17842
+ const byDimension = /* @__PURE__ */ new Map();
17843
+ for (const iss of auditIssues) {
17844
+ const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
17845
+ const group = byDimension.get(dim) ?? [];
17846
+ group.push(iss);
17847
+ byDimension.set(dim, group);
17848
+ }
17849
+ for (const [dim, dimIssues] of byDimension) {
17850
+ lines.push(`### ${dim}`);
17851
+ lines.push("");
17852
+ for (const iss of dimIssues) {
17853
+ lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
17854
+ }
17855
+ lines.push("");
17856
+ }
17857
+ }
17858
+ const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
17859
+ if (slopIssues.length > 0) {
17860
+ lines.push("## Slop Guardrails Findings");
17861
+ lines.push("");
17862
+ const byCategory = /* @__PURE__ */ new Map();
17863
+ for (const iss of slopIssues) {
17864
+ const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
17865
+ const group = byCategory.get(cat) ?? [];
17866
+ group.push(iss);
17867
+ byCategory.set(cat, group);
17868
+ }
17869
+ for (const [cat, catIssues] of byCategory) {
17870
+ lines.push(`### ${cat}`);
17871
+ lines.push("");
17872
+ for (const iss of catIssues) {
17873
+ lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
17874
+ }
17875
+ lines.push("");
17876
+ }
17877
+ }
17087
17878
  lines.push("## Change Type");
17088
17879
  lines.push("");
17089
17880
  lines.push("### Summary");
@@ -17455,6 +18246,20 @@ function formatReportMarkdown(data, options = {}) {
17455
18246
  } else {
17456
18247
  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");
17457
18248
  }
18249
+ const renderEvidenceIssues = data.issues.filter(
18250
+ (item) => ["QFAI-PROT-101", "QFAI-PROT-244", "QFAI-PROT-245"].includes(item.code)
18251
+ );
18252
+ if (renderEvidenceIssues.length > 0) {
18253
+ lines.push(
18254
+ "- 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"
18255
+ );
18256
+ lines.push(
18257
+ "- 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"
18258
+ );
18259
+ lines.push(
18260
+ "- 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"
18261
+ );
18262
+ }
17458
18263
  lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/18_delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
17459
18264
  lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`");
17460
18265
  return lines.join("\n");
@@ -17489,7 +18294,7 @@ async function collectChangeTypeSummary(specsRoot) {
17489
18294
  };
17490
18295
  const deltaFiles = await collectDeltaFiles(specsRoot);
17491
18296
  for (const deltaFile of deltaFiles) {
17492
- const text = await (0, import_promises53.readFile)(deltaFile, "utf-8");
18297
+ const text = await (0, import_promises56.readFile)(deltaFile, "utf-8");
17493
18298
  const parsed = parseDeltaV1(text);
17494
18299
  for (const entry of parsed.entries) {
17495
18300
  if (!entry.meta) {
@@ -17526,7 +18331,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
17526
18331
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
17527
18332
  }
17528
18333
  for (const file of specFiles) {
17529
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18334
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17530
18335
  const parsed = parseSpec(text, file);
17531
18336
  const specKey = parsed.specId;
17532
18337
  if (!specKey) {
@@ -17563,7 +18368,7 @@ async function collectIds(files) {
17563
18368
  result[prefix] = /* @__PURE__ */ new Set();
17564
18369
  }
17565
18370
  for (const file of files) {
17566
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18371
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17567
18372
  for (const prefix of ID_PREFIXES) {
17568
18373
  const ids = extractIds(text, prefix);
17569
18374
  ids.forEach((id) => result[prefix].add(id));
@@ -17578,7 +18383,7 @@ async function collectIds(files) {
17578
18383
  async function collectUpstreamIds(files) {
17579
18384
  const ids = /* @__PURE__ */ new Set();
17580
18385
  for (const file of files) {
17581
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18386
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17582
18387
  extractAllIds(text).forEach((id) => ids.add(id));
17583
18388
  }
17584
18389
  return ids;
@@ -17599,7 +18404,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
17599
18404
  }
17600
18405
  const pattern = buildIdPattern(Array.from(upstreamIds));
17601
18406
  for (const file of targetFiles) {
17602
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18407
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17603
18408
  if (pattern.test(text)) {
17604
18409
  return true;
17605
18410
  }
@@ -17718,7 +18523,7 @@ function normalizeScSources(root, sources) {
17718
18523
  async function countScenarios(scenarioFiles) {
17719
18524
  let total = 0;
17720
18525
  for (const file of scenarioFiles) {
17721
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18526
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17722
18527
  const { document, errors } = parseScenarioDocument(text, file);
17723
18528
  if (!document || errors.length > 0) {
17724
18529
  continue;
@@ -17749,7 +18554,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
17749
18554
  let totalScenarios = 0;
17750
18555
  let e2eCount = 0;
17751
18556
  for (const file of scenarioFiles) {
17752
- const text = await (0, import_promises53.readFile)(file, "utf-8");
18557
+ const text = await (0, import_promises56.readFile)(file, "utf-8");
17753
18558
  const { document, errors } = parseScenarioDocument(text, file);
17754
18559
  if (!document || errors.length > 0) {
17755
18560
  continue;
@@ -17837,10 +18642,10 @@ function buildHotspots(issues) {
17837
18642
  async function collectTddCoverage(entries) {
17838
18643
  const specs = [];
17839
18644
  for (const entry of entries) {
17840
- const testCasesPath = import_node_path57.default.join(entry.dir, "06_Test-Cases.md");
18645
+ const testCasesPath = import_node_path59.default.join(entry.dir, "06_Test-Cases.md");
17841
18646
  let tcContent;
17842
18647
  try {
17843
- tcContent = await (0, import_promises53.readFile)(testCasesPath, "utf-8");
18648
+ tcContent = await (0, import_promises56.readFile)(testCasesPath, "utf-8");
17844
18649
  } catch {
17845
18650
  continue;
17846
18651
  }
@@ -17872,10 +18677,10 @@ async function collectTddCoverage(entries) {
17872
18677
  });
17873
18678
  continue;
17874
18679
  }
17875
- const tddListPath = import_node_path57.default.join(entry.dir, "tdd", "test-list.md");
18680
+ const tddListPath = import_node_path59.default.join(entry.dir, "tdd", "test-list.md");
17876
18681
  let tddContent;
17877
18682
  try {
17878
- tddContent = await (0, import_promises53.readFile)(tddListPath, "utf-8");
18683
+ tddContent = await (0, import_promises56.readFile)(tddListPath, "utf-8");
17879
18684
  } catch {
17880
18685
  specs.push({
17881
18686
  specNumber: entry.specNumber,
@@ -17958,8 +18763,8 @@ async function collectTddCoverage(entries) {
17958
18763
  }
17959
18764
 
17960
18765
  // src/core/specPackReport.ts
17961
- var import_promises54 = require("fs/promises");
17962
- var import_node_path58 = __toESM(require("path"), 1);
18766
+ var import_promises57 = require("fs/promises");
18767
+ var import_node_path60 = __toESM(require("path"), 1);
17963
18768
  var REQUIRED_LEDGER_COLUMNS = [
17964
18769
  "trace_id",
17965
18770
  "obj_id",
@@ -17977,9 +18782,9 @@ async function writeSpecPackReports(root, config) {
17977
18782
  const entries = await collectSpecEntries(specsRoot);
17978
18783
  const contractIndex = await buildContractIndex(root, config);
17979
18784
  for (const entry of entries) {
17980
- const specName = import_node_path58.default.basename(entry.dir);
17981
- const outputDir = import_node_path58.default.join(outRoot, specName);
17982
- await (0, import_promises54.mkdir)(outputDir, { recursive: true });
18785
+ const specName = import_node_path60.default.basename(entry.dir);
18786
+ const outputDir = import_node_path60.default.join(outRoot, specName);
18787
+ await (0, import_promises57.mkdir)(outputDir, { recursive: true });
17983
18788
  const [acText, tcText, exText, ledgerText] = await Promise.all([
17984
18789
  readSafe12(entry.acceptanceCriteriaPath),
17985
18790
  readSafe12(entry.testCasesPath),
@@ -18004,14 +18809,14 @@ async function writeSpecPackReports(root, config) {
18004
18809
  contractIds: contractIndex.ids
18005
18810
  });
18006
18811
  const graph = buildTraceabilityGraph(ledgerRows);
18007
- await (0, import_promises54.writeFile)(
18008
- import_node_path58.default.join(outputDir, "coverage.md"),
18812
+ await (0, import_promises57.writeFile)(
18813
+ import_node_path60.default.join(outputDir, "coverage.md"),
18009
18814
  `${formatCoverageMarkdown(specName, coverage)}
18010
18815
  `,
18011
18816
  "utf-8"
18012
18817
  );
18013
- await (0, import_promises54.writeFile)(
18014
- import_node_path58.default.join(outputDir, "traceability-graph.json"),
18818
+ await (0, import_promises57.writeFile)(
18819
+ import_node_path60.default.join(outputDir, "traceability-graph.json"),
18015
18820
  `${JSON.stringify(graph, null, 2)}
18016
18821
  `,
18017
18822
  "utf-8"
@@ -18186,7 +18991,7 @@ function getCell(row, indexByColumn, column) {
18186
18991
  }
18187
18992
  async function readSafe12(filePath) {
18188
18993
  try {
18189
- return await (0, import_promises54.readFile)(filePath, "utf-8");
18994
+ return await (0, import_promises57.readFile)(filePath, "utf-8");
18190
18995
  } catch {
18191
18996
  return "";
18192
18997
  }
@@ -18204,7 +19009,7 @@ function warnIfTruncated(scan, context) {
18204
19009
 
18205
19010
  // src/cli/commands/report.ts
18206
19011
  async function runReport(options) {
18207
- const root = import_node_path59.default.resolve(options.root);
19012
+ const root = import_node_path61.default.resolve(options.root);
18208
19013
  const configResult = await loadConfig(root);
18209
19014
  let validation;
18210
19015
  let blockedByPhaseGuard = false;
@@ -18220,7 +19025,7 @@ async function runReport(options) {
18220
19025
  validation = normalized;
18221
19026
  } else {
18222
19027
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
18223
- const inputPath = import_node_path59.default.isAbsolute(input) ? input : import_node_path59.default.resolve(root, input);
19028
+ const inputPath = import_node_path61.default.isAbsolute(input) ? input : import_node_path61.default.resolve(root, input);
18224
19029
  try {
18225
19030
  validation = await readValidationResult(inputPath);
18226
19031
  } catch (err) {
@@ -18247,11 +19052,11 @@ async function runReport(options) {
18247
19052
  warnIfTruncated(data.traceability.testFiles, "report");
18248
19053
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
18249
19054
  const outRoot = resolvePath(root, configResult.config, "outDir");
18250
- const defaultOut = options.format === "json" ? import_node_path59.default.join(outRoot, "report.json") : import_node_path59.default.join(outRoot, "report.md");
19055
+ const defaultOut = options.format === "json" ? import_node_path61.default.join(outRoot, "report.json") : import_node_path61.default.join(outRoot, "report.md");
18251
19056
  const out = options.outPath ?? defaultOut;
18252
- const outPath = import_node_path59.default.isAbsolute(out) ? out : import_node_path59.default.resolve(root, out);
18253
- await (0, import_promises55.mkdir)(import_node_path59.default.dirname(outPath), { recursive: true });
18254
- await (0, import_promises55.writeFile)(outPath, `${output}
19057
+ const outPath = import_node_path61.default.isAbsolute(out) ? out : import_node_path61.default.resolve(root, out);
19058
+ await (0, import_promises58.mkdir)(import_node_path61.default.dirname(outPath), { recursive: true });
19059
+ await (0, import_promises58.writeFile)(outPath, `${output}
18255
19060
  `, "utf-8");
18256
19061
  await writeSpecPackReports(root, configResult.config);
18257
19062
  if (blockedByPhaseGuard) {
@@ -18266,7 +19071,7 @@ async function runReport(options) {
18266
19071
  info(`wrote report: ${outPath}`);
18267
19072
  }
18268
19073
  async function readValidationResult(inputPath) {
18269
- const raw = await (0, import_promises55.readFile)(inputPath, "utf-8");
19074
+ const raw = await (0, import_promises58.readFile)(inputPath, "utf-8");
18270
19075
  const parsed = JSON.parse(raw);
18271
19076
  if (!isValidationResult(parsed)) {
18272
19077
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -18326,23 +19131,23 @@ function isMissingFileError2(error2) {
18326
19131
  return record2.code === "ENOENT";
18327
19132
  }
18328
19133
  async function writeValidationResult(root, outputPath, result) {
18329
- const abs = import_node_path59.default.isAbsolute(outputPath) ? outputPath : import_node_path59.default.resolve(root, outputPath);
18330
- await (0, import_promises55.mkdir)(import_node_path59.default.dirname(abs), { recursive: true });
18331
- await (0, import_promises55.writeFile)(abs, `${JSON.stringify(result, null, 2)}
19134
+ const abs = import_node_path61.default.isAbsolute(outputPath) ? outputPath : import_node_path61.default.resolve(root, outputPath);
19135
+ await (0, import_promises58.mkdir)(import_node_path61.default.dirname(abs), { recursive: true });
19136
+ await (0, import_promises58.writeFile)(abs, `${JSON.stringify(result, null, 2)}
18332
19137
  `, "utf-8");
18333
19138
  }
18334
19139
 
18335
19140
  // src/cli/commands/validate.ts
18336
- var import_promises57 = require("fs/promises");
18337
- var import_node_path61 = __toESM(require("path"), 1);
19141
+ var import_promises60 = require("fs/promises");
19142
+ var import_node_path63 = __toESM(require("path"), 1);
18338
19143
 
18339
19144
  // src/core/runLog.ts
18340
- var import_promises56 = require("fs/promises");
18341
- var import_node_path60 = __toESM(require("path"), 1);
19145
+ var import_promises59 = require("fs/promises");
19146
+ var import_node_path62 = __toESM(require("path"), 1);
18342
19147
  async function writeValidateRunLog(input) {
18343
- const root = import_node_path60.default.resolve(input.root);
19148
+ const root = import_node_path62.default.resolve(input.root);
18344
19149
  const outDir = resolvePath(root, input.config, "outDir");
18345
- await (0, import_promises56.mkdir)(outDir, { recursive: true });
19150
+ await (0, import_promises59.mkdir)(outDir, { recursive: true });
18346
19151
  const { runId, reportDir } = await allocateRunReportDir(outDir, input.startedAt);
18347
19152
  const relativeSpecsRoot = toRelativePath(root, resolvePath(root, input.config, "specsDir"));
18348
19153
  const latestDiscussion = await findLatestPack(
@@ -18387,10 +19192,10 @@ async function writeValidateRunLog(input) {
18387
19192
  errors,
18388
19193
  warnings
18389
19194
  });
18390
- await writeJson(import_node_path60.default.join(reportDir, "run.json"), runJson);
18391
- await writeJson(import_node_path60.default.join(reportDir, "validator.json"), validatorJson);
18392
- await writeJson(import_node_path60.default.join(reportDir, "traceability.json"), traceabilityJson);
18393
- await (0, import_promises56.writeFile)(import_node_path60.default.join(reportDir, "summary.md"), `${summaryMd}
19195
+ await writeJson(import_node_path62.default.join(reportDir, "run.json"), runJson);
19196
+ await writeJson(import_node_path62.default.join(reportDir, "validator.json"), validatorJson);
19197
+ await writeJson(import_node_path62.default.join(reportDir, "traceability.json"), traceabilityJson);
19198
+ await (0, import_promises59.writeFile)(import_node_path62.default.join(reportDir, "summary.md"), `${summaryMd}
18394
19199
  `, "utf-8");
18395
19200
  return {
18396
19201
  runId,
@@ -18398,7 +19203,7 @@ async function writeValidateRunLog(input) {
18398
19203
  };
18399
19204
  }
18400
19205
  async function writeJson(filePath, value) {
18401
- await (0, import_promises56.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
19206
+ await (0, import_promises59.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
18402
19207
  `, "utf-8");
18403
19208
  }
18404
19209
  function resolveStatus(result, override) {
@@ -18514,9 +19319,9 @@ async function allocateRunReportDir(outDir, startedAt) {
18514
19319
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
18515
19320
  const candidateDate = new Date(startedAt.getTime() + attempt);
18516
19321
  const runId = `run-${formatTimestamp17(candidateDate)}`;
18517
- const reportDir = import_node_path60.default.join(outDir, runId);
19322
+ const reportDir = import_node_path62.default.join(outDir, runId);
18518
19323
  try {
18519
- await (0, import_promises56.mkdir)(reportDir);
19324
+ await (0, import_promises59.mkdir)(reportDir);
18520
19325
  return { runId, reportDir };
18521
19326
  } catch (error2) {
18522
19327
  if (isAlreadyExistsError(error2)) {
@@ -18545,7 +19350,7 @@ function shouldFail(result, failOn) {
18545
19350
  // src/cli/commands/validate.ts
18546
19351
  async function runValidate(options) {
18547
19352
  const startedAt = /* @__PURE__ */ new Date();
18548
- const root = import_node_path61.default.resolve(options.root);
19353
+ const root = import_node_path63.default.resolve(options.root);
18549
19354
  const configResult = await loadConfig(root);
18550
19355
  const blockedIssue = buildCiRefinementIssue(options.phase);
18551
19356
  const blockedByPhaseGuard = blockedIssue !== null;
@@ -18701,12 +19506,12 @@ function issueKey(issue2) {
18701
19506
  }
18702
19507
  async function emitJson(result, root, jsonPath) {
18703
19508
  const abs = resolveJsonPath(root, jsonPath);
18704
- await (0, import_promises57.mkdir)(import_node_path61.default.dirname(abs), { recursive: true });
18705
- await (0, import_promises57.writeFile)(abs, `${JSON.stringify(result, null, 2)}
19509
+ await (0, import_promises60.mkdir)(import_node_path63.default.dirname(abs), { recursive: true });
19510
+ await (0, import_promises60.writeFile)(abs, `${JSON.stringify(result, null, 2)}
18706
19511
  `, "utf-8");
18707
19512
  }
18708
19513
  function resolveJsonPath(root, jsonPath) {
18709
- return import_node_path61.default.isAbsolute(jsonPath) ? jsonPath : import_node_path61.default.resolve(root, jsonPath);
19514
+ return import_node_path63.default.isAbsolute(jsonPath) ? jsonPath : import_node_path63.default.resolve(root, jsonPath);
18710
19515
  }
18711
19516
  var GITHUB_ANNOTATION_LIMIT = 100;
18712
19517
  var ISSUE_EXPECTED_BY_CODE = {
@@ -18825,6 +19630,8 @@ function parseArgs(argv, cwd) {
18825
19630
  guardrailsPaths: [],
18826
19631
  prototypingAutogen: false,
18827
19632
  prototypingAutogenOnly: false,
19633
+ prototypingRenderEvidence: false,
19634
+ prototypingRenderViewports: [],
18828
19635
  help: false,
18829
19636
  invalidExitCode: 1
18830
19637
  };
@@ -19036,6 +19843,37 @@ function parseArgs(argv, cwd) {
19036
19843
  i += 1;
19037
19844
  break;
19038
19845
  }
19846
+ case "--render-evidence":
19847
+ if (command === "prototyping") {
19848
+ options.prototypingRenderEvidence = true;
19849
+ }
19850
+ break;
19851
+ case "--viewports": {
19852
+ if (command !== "prototyping") {
19853
+ break;
19854
+ }
19855
+ const next = readOptionValue(args, i);
19856
+ if (next === null) {
19857
+ markInvalid();
19858
+ break;
19859
+ }
19860
+ options.prototypingRenderViewports = next.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
19861
+ i += 1;
19862
+ break;
19863
+ }
19864
+ case "--render-out": {
19865
+ if (command !== "prototyping") {
19866
+ break;
19867
+ }
19868
+ const next = readOptionValue(args, i);
19869
+ if (next === null) {
19870
+ markInvalid();
19871
+ break;
19872
+ }
19873
+ options.prototypingRenderOut = next;
19874
+ i += 1;
19875
+ break;
19876
+ }
19039
19877
  case "--platform": {
19040
19878
  if (command !== "validate") {
19041
19879
  break;
@@ -19188,7 +20026,10 @@ async function run(argv, cwd) {
19188
20026
  autogenUiFidelity: options.prototypingAutogen,
19189
20027
  autogenOnly: options.prototypingAutogenOnly,
19190
20028
  ...options.prototypingBaseUrl !== void 0 ? { baseUrl: options.prototypingBaseUrl } : {},
19191
- ...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {}
20029
+ ...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {},
20030
+ renderEvidence: options.prototypingRenderEvidence,
20031
+ renderViewports: options.prototypingRenderViewports,
20032
+ ...options.prototypingRenderOut !== void 0 ? { renderOut: options.prototypingRenderOut } : {}
19192
20033
  });
19193
20034
  process.exitCode = exitCode;
19194
20035
  }
@@ -19234,6 +20075,9 @@ Options:
19234
20075
  --autogen-ui-fidelity prototyping: uiFidelity \u81EA\u52D5\u751F\u6210\u3092\u6709\u52B9\u5316
19235
20076
  --autogen-only prototyping: \u81EA\u52D5\u751F\u6210\u306E\u307F\u5B9F\u884C\uFF08\u5931\u6557\u6642exit 1\uFF09
19236
20077
  --evidence-out <path> prototyping: \u51FA\u529B\u5148\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8 .qfai/evidence/prototyping.json\uFF09
20078
+ --render-evidence prototyping: render evidence \u306E\u53CE\u96C6\u3092\u6709\u52B9\u5316
20079
+ --viewports <list> prototyping: render \u5BFE\u8C61 viewport \u3092\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u6307\u5B9A
20080
+ --render-out <path> prototyping: render evidence \u306E\u51FA\u529B\u5148
19237
20081
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
19238
20082
 
19239
20083
  Environment: