qfai 1.6.4 → 1.6.5

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.
@@ -442,6 +442,48 @@ function normalizeUiux(raw, configPath, issues) {
442
442
  issues.push(configIssue(configPath, "uiux.htmlMockTimeout \u306F\u6B63\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
443
443
  }
444
444
  }
445
+ if (raw.qualityProfile !== void 0) {
446
+ if (typeof raw.qualityProfile === "string" && ["strict", "high", "default"].includes(raw.qualityProfile)) {
447
+ result.qualityProfile = raw.qualityProfile;
448
+ } else {
449
+ issues.push(
450
+ configIssue(
451
+ configPath,
452
+ "uiux.qualityProfile \u306F strict|high|default \u306E\u3044\u305A\u308C\u304B\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
453
+ )
454
+ );
455
+ }
456
+ }
457
+ if (raw.requireResearchSummary !== void 0) {
458
+ if (typeof raw.requireResearchSummary === "boolean") {
459
+ result.requireResearchSummary = raw.requireResearchSummary;
460
+ } else {
461
+ issues.push(
462
+ configIssue(configPath, "uiux.requireResearchSummary \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
463
+ );
464
+ }
465
+ }
466
+ if (raw.competitive_refs_min !== void 0) {
467
+ if (typeof raw.competitive_refs_min === "number" && Number.isFinite(raw.competitive_refs_min) && raw.competitive_refs_min >= 0) {
468
+ result.competitive_refs_min = raw.competitive_refs_min;
469
+ } else {
470
+ issues.push(
471
+ configIssue(configPath, "uiux.competitive_refs_min \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
472
+ );
473
+ }
474
+ }
475
+ if (raw.warning_as_error_override !== void 0) {
476
+ if (Array.isArray(raw.warning_as_error_override) && raw.warning_as_error_override.every((v) => typeof v === "string")) {
477
+ result.warning_as_error_override = raw.warning_as_error_override;
478
+ } else {
479
+ issues.push(
480
+ configIssue(
481
+ configPath,
482
+ "uiux.warning_as_error_override \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
483
+ )
484
+ );
485
+ }
486
+ }
445
487
  return Object.keys(result).length > 0 ? result : void 0;
446
488
  }
447
489
  function configIssue(file, message) {
@@ -1522,8 +1564,8 @@ var import_promises7 = require("fs/promises");
1522
1564
  var import_node_path8 = __toESM(require("path"), 1);
1523
1565
  var import_node_url2 = require("url");
1524
1566
  async function resolveToolVersion() {
1525
- if ("1.6.4".length > 0) {
1526
- return "1.6.4";
1567
+ if ("1.6.5".length > 0) {
1568
+ return "1.6.5";
1527
1569
  }
1528
1570
  try {
1529
1571
  const packagePath = resolvePackageJsonPath();
@@ -4059,8 +4101,8 @@ function extractRouteHintsFromEvidence(evidence) {
4059
4101
  }
4060
4102
 
4061
4103
  // src/cli/commands/report.ts
4062
- var import_promises52 = require("fs/promises");
4063
- var import_node_path55 = __toESM(require("path"), 1);
4104
+ var import_promises55 = require("fs/promises");
4105
+ var import_node_path58 = __toESM(require("path"), 1);
4064
4106
 
4065
4107
  // src/core/normalize.ts
4066
4108
  function normalizeIssuePaths(root, issues) {
@@ -4155,8 +4197,8 @@ async function createPhaseGuardResult(phase, blockedIssue) {
4155
4197
  }
4156
4198
 
4157
4199
  // src/core/report.ts
4158
- var import_promises50 = require("fs/promises");
4159
- var import_node_path53 = __toESM(require("path"), 1);
4200
+ var import_promises53 = require("fs/promises");
4201
+ var import_node_path56 = __toESM(require("path"), 1);
4160
4202
 
4161
4203
  // src/core/contractIndex.ts
4162
4204
  var import_promises15 = require("fs/promises");
@@ -12356,7 +12398,7 @@ function collectLayer(layer, layerName, target, errors) {
12356
12398
  }
12357
12399
  function flattenTokens(obj, prefix, target, errors) {
12358
12400
  for (const [key, value] of Object.entries(obj)) {
12359
- const path58 = `${prefix}.${key}`;
12401
+ const path61 = `${prefix}.${key}`;
12360
12402
  if (value && typeof value === "object" && !Array.isArray(value)) {
12361
12403
  const record2 = value;
12362
12404
  if ("$value" in record2) {
@@ -12372,9 +12414,9 @@ function flattenTokens(obj, prefix, target, errors) {
12372
12414
  if (typeof record2.platform === "string") {
12373
12415
  token.platform = record2.platform;
12374
12416
  }
12375
- target.set(path58, token);
12417
+ target.set(path61, token);
12376
12418
  } else {
12377
- flattenTokens(record2, path58, target, errors);
12419
+ flattenTokens(record2, path61, target, errors);
12378
12420
  }
12379
12421
  }
12380
12422
  }
@@ -12384,44 +12426,44 @@ function resolveAllReferences(result) {
12384
12426
  for (const [key, val] of result.primitives) allTokens.set(key, val);
12385
12427
  for (const [key, val] of result.semantics) allTokens.set(key, val);
12386
12428
  for (const [key, val] of result.components) allTokens.set(key, val);
12387
- for (const [path58] of allTokens) {
12388
- resolveTokenRef(path58, allTokens, /* @__PURE__ */ new Set(), 0, result);
12429
+ for (const [path61] of allTokens) {
12430
+ resolveTokenRef(path61, allTokens, /* @__PURE__ */ new Set(), 0, result);
12389
12431
  }
12390
12432
  }
12391
- function resolveTokenRef(path58, allTokens, visited, depth, result) {
12392
- if (result.resolved.has(path58)) {
12393
- return result.resolved.get(path58);
12433
+ function resolveTokenRef(path61, allTokens, visited, depth, result) {
12434
+ if (result.resolved.has(path61)) {
12435
+ return result.resolved.get(path61);
12394
12436
  }
12395
12437
  if (depth > MAX_RESOLVE_DEPTH) {
12396
12438
  result.errors.push({
12397
- message: `Max reference depth exceeded at: ${path58}`,
12398
- path: path58
12439
+ message: `Max reference depth exceeded at: ${path61}`,
12440
+ path: path61
12399
12441
  });
12400
12442
  return void 0;
12401
12443
  }
12402
- if (visited.has(path58)) {
12444
+ if (visited.has(path61)) {
12403
12445
  result.errors.push({
12404
- message: `Circular reference detected: ${path58}`,
12405
- path: path58
12446
+ message: `Circular reference detected: ${path61}`,
12447
+ path: path61
12406
12448
  });
12407
12449
  return void 0;
12408
12450
  }
12409
- const token = allTokens.get(path58);
12451
+ const token = allTokens.get(path61);
12410
12452
  if (!token) {
12411
12453
  return void 0;
12412
12454
  }
12413
12455
  if (typeof token.$value !== "string") {
12414
12456
  const rawValue2 = stringifyTokenValue(token.$value);
12415
- result.resolved.set(path58, rawValue2);
12457
+ result.resolved.set(path61, rawValue2);
12416
12458
  return rawValue2;
12417
12459
  }
12418
12460
  const rawValue = stringifyTokenValue(token.$value);
12419
12461
  const refs = [...rawValue.matchAll(REF_PATTERN)];
12420
12462
  if (refs.length === 0) {
12421
- result.resolved.set(path58, rawValue);
12463
+ result.resolved.set(path61, rawValue);
12422
12464
  return rawValue;
12423
12465
  }
12424
- visited.add(path58);
12466
+ visited.add(path61);
12425
12467
  let resolved = rawValue;
12426
12468
  for (const ref of refs) {
12427
12469
  const refPath = ref[1];
@@ -12429,8 +12471,8 @@ function resolveTokenRef(path58, allTokens, visited, depth, result) {
12429
12471
  const refToken = allTokens.get(refPath);
12430
12472
  if (!refToken) {
12431
12473
  result.errors.push({
12432
- message: `Unresolved token reference: {${refPath}} at ${path58}`,
12433
- path: path58
12474
+ message: `Unresolved token reference: {${refPath}} at ${path61}`,
12475
+ path: path61
12434
12476
  });
12435
12477
  continue;
12436
12478
  }
@@ -12439,7 +12481,7 @@ function resolveTokenRef(path58, allTokens, visited, depth, result) {
12439
12481
  resolved = resolved.split(`{${refPath}}`).join(refValue);
12440
12482
  }
12441
12483
  }
12442
- result.resolved.set(path58, resolved);
12484
+ result.resolved.set(path61, resolved);
12443
12485
  return resolved;
12444
12486
  }
12445
12487
  function stringifyTokenValue(value) {
@@ -12677,8 +12719,8 @@ var import_node_path45 = __toESM(require("path"), 1);
12677
12719
  var import_fast_glob3 = __toESM(require("fast-glob"), 1);
12678
12720
 
12679
12721
  // src/core/uiux/contrastRatio.ts
12680
- function computeContrastRatio(fg8, bg) {
12681
- const fgLum = relativeLuminance(fg8);
12722
+ function computeContrastRatio(fg11, bg) {
12723
+ const fgLum = relativeLuminance(fg11);
12682
12724
  const bgLum = relativeLuminance(bg);
12683
12725
  if (fgLum === null || bgLum === null) return null;
12684
12726
  const lighter = Math.max(fgLum, bgLum);
@@ -14618,216 +14660,1729 @@ async function collectTestCaseIds(specDir) {
14618
14660
  return { knownTcIds, unitComponentTcIds };
14619
14661
  }
14620
14662
 
14621
- // src/core/validate.ts
14622
- var UIUX_VALIDATION_BUDGET_MS = 2e3;
14623
- async function validateProject(root, configResult, options = {}) {
14624
- const resolved = configResult ?? await loadConfig(root);
14625
- const { config, issues: configIssues } = resolved;
14626
- const phase = options.phase ?? "full";
14627
- const atddCodeTraceabilityIssues = phase === "refinement" ? [] : await validateAtddCodeTraceability(root, config);
14628
- const uiuxStart = performance.now();
14629
- const platformResult = await detectPlatform(root, config, options.platform);
14630
- const platform = platformResult.platform;
14631
- const uiuxValidators = [
14632
- () => validateDesignToken(root, config),
14633
- () => validateHtmlMock(root, platform, config),
14634
- () => validateMermaidScreenFlow(root, config),
14635
- () => validateBpApDb(root, config),
14636
- () => validateUiDefinitionConsistency(root, config),
14637
- () => validateResearchSummary(root, config),
14638
- () => validateAgentDefinition(root, config)
14639
- ];
14640
- const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
14641
- const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
14642
- const uiuxElapsed = performance.now() - uiuxStart;
14643
- if (uiuxElapsed > UIUX_VALIDATION_BUDGET_MS) {
14644
- uiuxIssues.push({
14645
- code: "QFAI-UIUX-PERF",
14646
- severity: "warning",
14647
- category: "compatibility",
14648
- message: `UI/UX validation exceeded budget (${UIUX_VALIDATION_BUDGET_MS}ms). All validators were executed.`,
14649
- rule: "uiux.performanceBudget"
14650
- });
14663
+ // src/core/validators/ddpValidation.ts
14664
+ var import_promises50 = require("fs/promises");
14665
+ var import_node_path53 = __toESM(require("path"), 1);
14666
+ var import_node_url3 = require("url");
14667
+ var import_fast_glob8 = __toESM(require("fast-glob"), 1);
14668
+ var DDP_HEADING_RE = /^#{1,3}\s+Design\s+Direction\s+Pack/im;
14669
+ var DDP_REQUIRED_FIELDS = [
14670
+ "visual_thesis",
14671
+ "content_plan",
14672
+ "interaction_thesis",
14673
+ "anti_goals",
14674
+ "cta_hierarchy"
14675
+ ];
14676
+ var DDP_THEME_FIELDS = ["theme", "mood", "taste", "material", "energy", "visual_anchor"];
14677
+ var UI_BEARING_KEYWORDS_RE = /\b(screen|ui|interface|mock|layout|design)\b/i;
14678
+ var FIGMA_URL_RE = /https?:\/\/(?:www\.)?figma\.com\/[^\s)]+/i;
14679
+ var _bannedPatterns = null;
14680
+ async function loadBannedPatterns() {
14681
+ if (_bannedPatterns !== null) return _bannedPatterns;
14682
+ try {
14683
+ const thisDir = import_node_path53.default.dirname((0, import_node_url3.fileURLToPath)(__filename));
14684
+ const filePath = import_node_path53.default.join(thisDir, "ddpBannedPatterns.txt");
14685
+ const content = await (0, import_promises50.readFile)(filePath, "utf-8");
14686
+ _bannedPatterns = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
14687
+ } catch {
14688
+ _bannedPatterns = [];
14651
14689
  }
14652
- const findings = [
14653
- ...configIssues,
14654
- ...await validateRepositoryHygiene(root, config),
14655
- ...await validateSkillsIntegrity(root, config),
14656
- ...await validateAssistantAssets(root, config),
14657
- ...await validateDiscussionMermaid(root),
14658
- ...await validateMermaidEnforcement(root),
14659
- ...await validateSpecPacks(root, config),
14660
- ...await validateDiscussionPackReadiness(root, config),
14661
- ...await validateDiscussionVisuals(root),
14662
- ...await validateLegacyStatusDir(root),
14663
- ...await validateStatusInSpecs(root, config),
14664
- ...await validateDensityHints(root, config),
14665
- ...await validateReviewArtifacts(root),
14666
- ...await validatePrototypingEvidence(root, config),
14667
- ...await validateSpecSplitByCapability(root, config),
14668
- ...await validateLayeredTraceability(root, config),
14669
- ...await validateOrphanProhibition(root, config),
14670
- ...await validateLayerCoverage(root, config),
14671
- ...atddCodeTraceabilityIssues,
14672
- ...await validateContractReferences(root, config),
14673
- ...await validateTraceability(root, config, phase),
14674
- ...await validateDefinedIds(root, config),
14675
- ...await validateContracts(root, config),
14676
- ...await validateTddList(root, config),
14677
- ...uiuxIssues
14678
- ];
14679
- const { issues, waivers } = await applyWaivers(root, findings);
14680
- const specsRoot = resolvePath(root, config, "specsDir");
14681
- const scenarioFiles = await collectScenarioFiles(specsRoot);
14682
- const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
14683
- const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
14684
- root,
14685
- config.validation.traceability.testFileGlobs,
14686
- config.validation.traceability.testFileExcludeGlobs
14687
- );
14688
- const scCoverage = buildScCoverage(scIds, scTestRefs);
14689
- const toolVersion = await resolveToolVersion();
14690
- return {
14691
- toolVersion,
14692
- phase,
14693
- issues,
14694
- counts: countIssues(issues),
14695
- traceability: {
14696
- sc: scCoverage,
14697
- testFiles
14698
- },
14699
- waivers
14700
- };
14690
+ return _bannedPatterns;
14701
14691
  }
14702
- function countIssues(issues) {
14703
- return issues.reduce(
14704
- (acc, issue2) => {
14705
- if (issue2.suppressed) {
14706
- return acc;
14692
+ async function validateDdpFields(root, config) {
14693
+ const issues = [];
14694
+ const pattern = import_node_path53.default.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
14695
+ const files = await (0, import_fast_glob8.default)(pattern, { absolute: true });
14696
+ let ddpFound = false;
14697
+ let isUiBearing = false;
14698
+ for (const filePath of files) {
14699
+ const basename = import_node_path53.default.basename(filePath);
14700
+ if (basename === "03_Story-Workshop.md") {
14701
+ try {
14702
+ const storyContent = await (0, import_promises50.readFile)(filePath, "utf-8");
14703
+ if (UI_BEARING_KEYWORDS_RE.test(storyContent)) {
14704
+ isUiBearing = true;
14705
+ }
14706
+ } catch {
14707
14707
  }
14708
- acc[issue2.severity] += 1;
14709
- return acc;
14710
- },
14711
- { info: 0, warning: 0, error: 0 }
14712
- );
14713
- }
14714
-
14715
- // src/core/report.ts
14716
- var REPORT_GUARDRAILS_MAX = 20;
14717
- var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
14718
- var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
14719
- async function createReportData(root, validation, configResult) {
14720
- const resolvedRoot = import_node_path53.default.resolve(root);
14721
- const resolved = configResult ?? await loadConfig(resolvedRoot);
14722
- const config = resolved.config;
14723
- const configPath = resolved.configPath;
14724
- const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
14725
- const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
14726
- const apiRoot = import_node_path53.default.join(contractsRoot, "api");
14727
- const uiRoot = import_node_path53.default.join(contractsRoot, "ui");
14728
- const dbRoot = import_node_path53.default.join(contractsRoot, "db");
14729
- const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
14730
- const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
14731
- const specEntries = await collectSpecEntries(specsRoot);
14732
- const specFiles = await collectSpecFiles(specsRoot);
14733
- const scenarioFiles = await collectScenarioFiles(specsRoot);
14734
- const scenarioCount = await countScenarios(scenarioFiles);
14735
- const testStrategy = await collectTestStrategy(
14736
- scenarioFiles,
14737
- resolvedRoot,
14738
- config,
14739
- REPORT_TEST_STRATEGY_SAMPLE_LIMIT
14740
- );
14741
- const {
14742
- api: apiFiles,
14743
- ui: uiFiles,
14744
- db: dbFiles,
14745
- thema: themaFiles
14746
- } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
14747
- const contractIndex = await buildContractIndex(resolvedRoot, config);
14748
- const contractIdList = Array.from(contractIndex.ids);
14749
- const specContractRefs = await collectSpecContractRefs(specFiles, contractIdList);
14750
- const referencedContracts = /* @__PURE__ */ new Set();
14751
- for (const entry of specContractRefs.specToContracts.values()) {
14752
- entry.ids.forEach((id) => referencedContracts.add(id));
14753
- }
14754
- const referencedContractCount = contractIdList.filter((id) => referencedContracts.has(id)).length;
14755
- const orphanContractCount = contractIdList.filter((id) => !referencedContracts.has(id)).length;
14756
- const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
14757
- const specToContractsRecord = mapToSpecContractRecord(specContractRefs.specToContracts);
14758
- const idsByPrefix = await collectIds([
14759
- ...specFiles,
14760
- ...scenarioFiles,
14761
- ...apiFiles,
14762
- ...uiFiles,
14763
- ...dbFiles,
14764
- ...themaFiles
14765
- ]);
14766
- const upstreamIds = await collectUpstreamIds([...specFiles, ...scenarioFiles]);
14767
- const traceability = await evaluateTraceability(upstreamIds, srcRoot, testsRoot);
14768
- const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
14769
- const normalizedValidation = normalizeValidationResult(resolvedRoot, resolvedValidationRaw);
14770
- const scCoverage = normalizedValidation.traceability.sc;
14771
- const testFiles = normalizedValidation.traceability.testFiles;
14772
- const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
14773
- const scSourceRecord = mapToSortedRecord(normalizeScSources(resolvedRoot, scSources));
14774
- const guardrailsLoad = await loadDecisionGuardrails(resolvedRoot, {
14775
- specsRoot
14776
- });
14777
- const guardrailsAll = sortDecisionGuardrails(normalizeDecisionGuardrails(guardrailsLoad.entries));
14778
- const guardrailsDisplay = guardrailsAll.slice(0, REPORT_GUARDRAILS_MAX);
14779
- const guardrailsByType = { nonGoal: 0, notNow: 0, tradeOff: 0 };
14780
- for (const item of guardrailsAll) {
14781
- if (item.type === "non-goal") {
14782
- guardrailsByType.nonGoal += 1;
14783
- } else if (item.type === "not-now") {
14784
- guardrailsByType.notNow += 1;
14785
- } else {
14786
- guardrailsByType.tradeOff += 1;
14787
14708
  }
14788
14709
  }
14789
- const guardrailsErrors = guardrailsLoad.errors.map((item) => ({
14790
- path: toRelativePath(resolvedRoot, item.path),
14791
- message: item.message
14792
- }));
14793
- const changeTypeSummary = await collectChangeTypeSummary(specsRoot);
14794
- const ctypeWarnings = normalizedValidation.issues.filter((item) => item.code === "QFAI-CTYPE-002").map((item) => {
14795
- const warning = {
14796
- file: item.file ? toRelativePath(resolvedRoot, item.file) : "(unknown)",
14797
- suspectedMismatch: item.message,
14798
- refs: item.refs ?? []
14799
- };
14800
- if (item.suggested_action) {
14801
- warning.suggestion = item.suggested_action;
14710
+ for (const filePath of files) {
14711
+ let content;
14712
+ try {
14713
+ content = await (0, import_promises50.readFile)(filePath, "utf-8");
14714
+ } catch {
14715
+ continue;
14802
14716
  }
14803
- return warning;
14804
- });
14805
- const toReportRuleFinding = (item) => {
14806
- const finding = {
14807
- code: item.code,
14808
- severity: item.severity,
14809
- message: item.message,
14810
- refs: item.refs ?? []
14811
- };
14812
- if (item.file) {
14813
- finding.file = toRelativePath(resolvedRoot, item.file);
14717
+ if (!DDP_HEADING_RE.test(content)) continue;
14718
+ ddpFound = true;
14719
+ const rel = import_node_path53.default.relative(root, filePath).replace(/\\/g, "/");
14720
+ const section = extractDdpSection(content);
14721
+ if (!section) continue;
14722
+ for (const field of DDP_REQUIRED_FIELDS) {
14723
+ if (!hasNonEmptyField(section, field)) {
14724
+ issues.push(
14725
+ issue(
14726
+ "QFAI-DDP-001",
14727
+ `DDP required field missing: ${field}`,
14728
+ "error",
14729
+ rel,
14730
+ `ddp.requiredField.${field}`
14731
+ )
14732
+ );
14733
+ }
14814
14734
  }
14815
- if (item.suggested_action) {
14816
- finding.suggestion = item.suggested_action;
14735
+ const visualThesisValue = getFieldValue(section, "visual_thesis");
14736
+ if (visualThesisValue) {
14737
+ const trimmed = visualThesisValue.trim();
14738
+ if (/^-\s/.test(trimmed) && !/[.!?]/.test(trimmed)) {
14739
+ issues.push(
14740
+ issue(
14741
+ "QFAI-DDP-002",
14742
+ "Visual thesis should be a single descriptive sentence, not bullet points",
14743
+ "warning",
14744
+ rel,
14745
+ "ddp.visualThesisFormat"
14746
+ )
14747
+ );
14748
+ }
14817
14749
  }
14818
- return finding;
14819
- };
14820
- const compatFindings = normalizedValidation.issues.filter((item) => /^QFAI-COMPAT-\d+$/.test(item.code)).map((item) => toReportRuleFinding(item));
14821
- const scopeMismatches = normalizedValidation.issues.filter((item) => item.code === "QFAI-SCOPE-001" || item.code === "QFAI-SCOPE-002").map((item) => toReportRuleFinding(item));
14822
- const verificationFindings = normalizedValidation.issues.filter((item) => /^QFAI-VFY-\d+$/.test(item.code)).map((item) => toReportRuleFinding(item));
14823
- const missingDeltaUpdateIssues = normalizedValidation.issues.filter(
14824
- (item) => item.code === "QFAI-CTYPE-003"
14825
- ).length;
14826
- const waiverState = normalizedValidation.waivers ?? {
14827
- active: [],
14828
- suppressed: {
14829
- total: 0,
14830
- byWaiver: {},
14750
+ const themeBlock = extractNestedBlock(section, "theme");
14751
+ if (themeBlock !== null) {
14752
+ for (const tf of DDP_THEME_FIELDS) {
14753
+ if (!hasSubField(themeBlock, tf)) {
14754
+ issues.push(
14755
+ issue(
14756
+ "QFAI-DDP-003",
14757
+ `DDP theme field missing: ${tf}`,
14758
+ "error",
14759
+ rel,
14760
+ `ddp.themeField.${tf}`
14761
+ )
14762
+ );
14763
+ }
14764
+ }
14765
+ }
14766
+ const ctaBlock = extractNestedBlock(section, "cta_hierarchy");
14767
+ if (ctaBlock !== null) {
14768
+ if (!hasSubField(ctaBlock, "primary")) {
14769
+ issues.push(
14770
+ issue(
14771
+ "QFAI-DDP-004",
14772
+ "CTA hierarchy must have at least one primary CTA",
14773
+ "error",
14774
+ rel,
14775
+ "ddp.ctaPrimaryMissing"
14776
+ )
14777
+ );
14778
+ }
14779
+ if (!hasSubField(ctaBlock, "placement")) {
14780
+ issues.push(
14781
+ issue(
14782
+ "QFAI-DDP-005",
14783
+ "CTA placement not specified",
14784
+ "warning",
14785
+ rel,
14786
+ "ddp.ctaPlacementMissing"
14787
+ )
14788
+ );
14789
+ }
14790
+ } else {
14791
+ const ctaInline = getFieldValue(section, "cta_hierarchy");
14792
+ if (ctaInline && !ctaInline.toLowerCase().includes("primary")) {
14793
+ issues.push(
14794
+ issue(
14795
+ "QFAI-DDP-004",
14796
+ "CTA hierarchy must have at least one primary CTA",
14797
+ "error",
14798
+ rel,
14799
+ "ddp.ctaPrimaryMissing"
14800
+ )
14801
+ );
14802
+ }
14803
+ }
14804
+ const contentPlanItems = collectListItems(section, "content_plan");
14805
+ if (contentPlanItems.length > 0 && contentPlanItems.length < 2) {
14806
+ issues.push(
14807
+ issue(
14808
+ "QFAI-DDP-007",
14809
+ "Content plan should have at least 2 sections",
14810
+ "warning",
14811
+ rel,
14812
+ "ddp.contentPlanSections"
14813
+ )
14814
+ );
14815
+ }
14816
+ const interactionItems = collectListItems(section, "interaction_thesis");
14817
+ if (interactionItems.length > 0 && interactionItems.length < 2) {
14818
+ issues.push(
14819
+ issue(
14820
+ "QFAI-DDP-008",
14821
+ "Interaction thesis should contain 2-3 motion principles",
14822
+ "warning",
14823
+ rel,
14824
+ "ddp.interactionThesisPrinciples"
14825
+ )
14826
+ );
14827
+ }
14828
+ const antiGoalItems = collectListItems(section, "anti_goals");
14829
+ const bannedPatterns = await loadBannedPatterns();
14830
+ const hasBannedPattern = antiGoalItems.some(
14831
+ (item) => bannedPatterns.some((bp) => item.toLowerCase().includes(bp.toLowerCase()))
14832
+ );
14833
+ if (hasNonEmptyField(section, "anti_goals") && antiGoalItems.length > 0 && !hasBannedPattern) {
14834
+ issues.push(
14835
+ issue(
14836
+ "QFAI-DDP-009",
14837
+ "DDP anti-goals should contain at least one banned generic pattern",
14838
+ "warning",
14839
+ rel,
14840
+ "ddp.antiGoalsBannedPattern"
14841
+ )
14842
+ );
14843
+ }
14844
+ if (FIGMA_URL_RE.test(section)) {
14845
+ issues.push(
14846
+ issue(
14847
+ "QFAI-DDP-010",
14848
+ "DDP contains hard reference to external tool (Figma URL); DDP should be tool-independent",
14849
+ "error",
14850
+ rel,
14851
+ "ddp.externalToolDependency"
14852
+ )
14853
+ );
14854
+ }
14855
+ }
14856
+ if (isUiBearing && !ddpFound) {
14857
+ issues.push(
14858
+ issue(
14859
+ "QFAI-DDP-006",
14860
+ "UI-bearing artifact detected but no Design Direction Pack found",
14861
+ "error",
14862
+ void 0,
14863
+ "ddp.uiBearingNoDdp"
14864
+ )
14865
+ );
14866
+ }
14867
+ await validateResearchTraceability(root, config, issues);
14868
+ await validateStoryWorkshopTemplates(root, config, issues, files);
14869
+ validateQualityProfile(config, issues);
14870
+ await validateOptionComparison(root, config, issues);
14871
+ await validateCompetitiveRefs(root, config, issues);
14872
+ return issues;
14873
+ }
14874
+ var LIST_TEMPLATE_REQUIRED_FIELDS = [
14875
+ "screen_type",
14876
+ "items_source",
14877
+ "empty_state",
14878
+ "sort_filter_controls",
14879
+ "pagination_or_infinite_scroll",
14880
+ "primary_cta"
14881
+ ];
14882
+ var FORM_TEMPLATE_REQUIRED_FIELDS = [
14883
+ "screen_type",
14884
+ "fields",
14885
+ "required_fields_count",
14886
+ "submit_cta",
14887
+ "validation_feedback",
14888
+ "cancel_or_back_action"
14889
+ ];
14890
+ var ANTI_PATTERN_CHECKS = [
14891
+ {
14892
+ pattern: /dual\s+primary\s+CTA|two\s+primary\s+CTA|2\s+primary\s+CTA/i,
14893
+ id: "dual-primary-cta",
14894
+ message: "Anti-pattern detected: dual primary CTA",
14895
+ guidance: "A screen should have exactly one primary CTA to avoid user confusion."
14896
+ },
14897
+ {
14898
+ pattern: /required.fields.*(?:[89]|\d{2,})|required_fields_count\s*:\s*(?:[89]|\d{2,})/i,
14899
+ id: "too-many-required-fields",
14900
+ message: "Anti-pattern detected: form has more than 7 required fields",
14901
+ guidance: "Keep required fields to 7 or fewer to reduce form abandonment."
14902
+ },
14903
+ {
14904
+ pattern: /empty.state(?:(?!action|cta|button|link).)*$/im,
14905
+ id: "empty-state-no-action",
14906
+ message: "Anti-pattern detected: empty state without action",
14907
+ guidance: "Empty states should include an actionable CTA to guide users forward."
14908
+ }
14909
+ ];
14910
+ async function validateResearchTraceability(root, config, issues) {
14911
+ const requireResearchSummary = config.uiux?.requireResearchSummary === true;
14912
+ const discussionPattern = import_node_path53.default.posix.join(
14913
+ root.replace(/\\/g, "/"),
14914
+ config.paths.discussionDir,
14915
+ "**/*.md"
14916
+ );
14917
+ const discussionFiles = await (0, import_fast_glob8.default)(discussionPattern, { absolute: true });
14918
+ let hasResearchSummary = false;
14919
+ for (const filePath of discussionFiles) {
14920
+ try {
14921
+ const content = await (0, import_promises50.readFile)(filePath, "utf-8");
14922
+ if (/research_summary\s*:/m.test(content)) {
14923
+ hasResearchSummary = true;
14924
+ break;
14925
+ }
14926
+ } catch {
14927
+ }
14928
+ }
14929
+ if (requireResearchSummary && !hasResearchSummary) {
14930
+ issues.push(
14931
+ issue(
14932
+ "QFAI-DDP-012",
14933
+ "Research summary required by config (requireResearchSummary=true) but not found in discussion pack",
14934
+ "error",
14935
+ void 0,
14936
+ "ddp.researchSummaryRequired"
14937
+ )
14938
+ );
14939
+ }
14940
+ const contractPattern = import_node_path53.default.posix.join(
14941
+ root.replace(/\\/g, "/"),
14942
+ config.paths.contractsDir,
14943
+ "design",
14944
+ "*.yaml"
14945
+ );
14946
+ const contractFiles = await (0, import_fast_glob8.default)(contractPattern, { absolute: true });
14947
+ for (const filePath of contractFiles) {
14948
+ let content;
14949
+ try {
14950
+ content = await (0, import_promises50.readFile)(filePath, "utf-8");
14951
+ } catch {
14952
+ continue;
14953
+ }
14954
+ const rel = import_node_path53.default.relative(root, filePath).replace(/\\/g, "/");
14955
+ const ruleBlocks = content.split(/(?=^-\s+(?:rule|id)\s*:)/m).filter((b) => b.trim());
14956
+ for (const block of ruleBlocks) {
14957
+ const idMatch = /(?:^|\n)\s*(?:-\s+)?id\s*:\s*(\S+)/m.exec(block);
14958
+ const ruleId = idMatch?.[1] ?? "unknown";
14959
+ if (!/source_research\s*:/m.test(block) && /(?:rule|id)\s*:/m.test(block)) {
14960
+ issues.push(
14961
+ issue(
14962
+ "QFAI-DDP-011",
14963
+ `Design contract rule '${ruleId}' missing source_research traceability field`,
14964
+ "warning",
14965
+ rel,
14966
+ `ddp.researchTraceability.${ruleId}`
14967
+ )
14968
+ );
14969
+ }
14970
+ }
14971
+ }
14972
+ }
14973
+ async function validateStoryWorkshopTemplates(root, config, issues, discussionFiles) {
14974
+ for (const filePath of discussionFiles) {
14975
+ let content;
14976
+ try {
14977
+ content = await (0, import_promises50.readFile)(filePath, "utf-8");
14978
+ } catch {
14979
+ continue;
14980
+ }
14981
+ const rel = import_node_path53.default.relative(root, filePath).replace(/\\/g, "/");
14982
+ if (/screen_type\s*:\s*list/im.test(content)) {
14983
+ for (const field of LIST_TEMPLATE_REQUIRED_FIELDS) {
14984
+ if (!hasNonEmptyField(content, field)) {
14985
+ issues.push(
14986
+ issue(
14987
+ "QFAI-DDP-013",
14988
+ `List template missing required field: ${field}`,
14989
+ "error",
14990
+ rel,
14991
+ `ddp.listTemplate.${field}`
14992
+ )
14993
+ );
14994
+ }
14995
+ }
14996
+ if (hasFieldKey(content, "density_rationale") && !hasNonEmptyField(content, "density_rationale")) {
14997
+ issues.push(
14998
+ issue(
14999
+ "QFAI-DDP-013",
15000
+ "List template density_rationale is empty",
15001
+ "error",
15002
+ rel,
15003
+ "ddp.listTemplate.density_rationale"
15004
+ )
15005
+ );
15006
+ }
15007
+ }
15008
+ if (/screen_type\s*:\s*form/im.test(content)) {
15009
+ for (const field of FORM_TEMPLATE_REQUIRED_FIELDS) {
15010
+ if (!hasNonEmptyField(content, field)) {
15011
+ issues.push(
15012
+ issue(
15013
+ "QFAI-DDP-013",
15014
+ `Form template missing required field: ${field}`,
15015
+ "error",
15016
+ rel,
15017
+ `ddp.formTemplate.${field}`
15018
+ )
15019
+ );
15020
+ }
15021
+ }
15022
+ const statesBlock = extractNestedBlock(content, "states");
15023
+ if (statesBlock !== null && !hasSubField(statesBlock, "error")) {
15024
+ issues.push(
15025
+ issue(
15026
+ "QFAI-DDP-013",
15027
+ "Form template states.error is missing",
15028
+ "error",
15029
+ rel,
15030
+ "ddp.formTemplate.states.error"
15031
+ )
15032
+ );
15033
+ }
15034
+ }
15035
+ detectAntiPatterns(content, rel, issues);
15036
+ }
15037
+ }
15038
+ function detectAntiPatterns(content, rel, issues) {
15039
+ for (const check of ANTI_PATTERN_CHECKS) {
15040
+ if (check.pattern.test(content)) {
15041
+ issues.push(
15042
+ issue(
15043
+ "QFAI-DDP-014",
15044
+ check.message,
15045
+ "error",
15046
+ rel,
15047
+ `ddp.antiPattern.${check.id}`,
15048
+ void 0,
15049
+ "compatibility",
15050
+ check.guidance
15051
+ )
15052
+ );
15053
+ }
15054
+ }
15055
+ }
15056
+ function validateQualityProfile(config, issues) {
15057
+ const profile = config.uiux?.qualityProfile;
15058
+ if (profile === "strict") {
15059
+ issues.push(
15060
+ issue(
15061
+ "QFAI-DDP-015",
15062
+ "Quality profile is set to 'strict': all DDP warnings are enforced as errors",
15063
+ "info",
15064
+ void 0,
15065
+ "ddp.qualityProfile.strict"
15066
+ )
15067
+ );
15068
+ } else if (profile === "high") {
15069
+ issues.push(
15070
+ issue(
15071
+ "QFAI-DDP-015",
15072
+ "Quality profile is set to 'high': all DDP warnings are shown",
15073
+ "info",
15074
+ void 0,
15075
+ "ddp.qualityProfile.high"
15076
+ )
15077
+ );
15078
+ }
15079
+ }
15080
+ async function validateOptionComparison(root, config, issues) {
15081
+ const contractPattern = import_node_path53.default.posix.join(
15082
+ root.replace(/\\/g, "/"),
15083
+ config.paths.contractsDir,
15084
+ "design",
15085
+ "*.yaml"
15086
+ );
15087
+ const contractFiles = await (0, import_fast_glob8.default)(contractPattern, { absolute: true });
15088
+ for (const filePath of contractFiles) {
15089
+ let content;
15090
+ try {
15091
+ content = await (0, import_promises50.readFile)(filePath, "utf-8");
15092
+ } catch {
15093
+ continue;
15094
+ }
15095
+ const rel = import_node_path53.default.relative(root, filePath).replace(/\\/g, "/");
15096
+ const optionMatches = content.match(/(?:^|\n)\s*-\s+option\s*:/gm) ?? [];
15097
+ const numberedOptions = content.match(/(?:^|\n)\s*option_\d+\s*:/gm) ?? [];
15098
+ const totalOptions = optionMatches.length + numberedOptions.length;
15099
+ if (totalOptions === 1) {
15100
+ issues.push(
15101
+ issue(
15102
+ "QFAI-DDP-016",
15103
+ "Design contract has only 1 option; at least 2 options are required for comparison",
15104
+ "error",
15105
+ rel,
15106
+ "ddp.optionComparison.insufficient"
15107
+ )
15108
+ );
15109
+ } else if (totalOptions >= 2) {
15110
+ const lines = content.split(/\r?\n/);
15111
+ for (const line of lines) {
15112
+ const prosMatch = /^\s*pros\s*:\s*(.*)$/m.exec(line);
15113
+ if (prosMatch && prosMatch[1]?.trim() === "") {
15114
+ issues.push(
15115
+ issue(
15116
+ "QFAI-DDP-016",
15117
+ "Design option has empty pros field",
15118
+ "warning",
15119
+ rel,
15120
+ "ddp.optionComparison.emptyPros"
15121
+ )
15122
+ );
15123
+ }
15124
+ }
15125
+ }
15126
+ }
15127
+ }
15128
+ async function validateCompetitiveRefs(root, config, issues) {
15129
+ const minRefs = config.uiux?.competitive_refs_min ?? 3;
15130
+ const contractPattern = import_node_path53.default.posix.join(
15131
+ root.replace(/\\/g, "/"),
15132
+ config.paths.contractsDir,
15133
+ "design",
15134
+ "*.yaml"
15135
+ );
15136
+ const contractFiles = await (0, import_fast_glob8.default)(contractPattern, { absolute: true });
15137
+ for (const filePath of contractFiles) {
15138
+ let content;
15139
+ try {
15140
+ content = await (0, import_promises50.readFile)(filePath, "utf-8");
15141
+ } catch {
15142
+ continue;
15143
+ }
15144
+ const rel = import_node_path53.default.relative(root, filePath).replace(/\\/g, "/");
15145
+ const refsBlock = extractNestedBlock(content, "competitive_refs");
15146
+ if (refsBlock !== null) {
15147
+ const refItems = refsBlock.split(/\n/).filter((l) => /^\s*-\s/.test(l));
15148
+ if (refItems.length < minRefs) {
15149
+ issues.push(
15150
+ issue(
15151
+ "QFAI-DDP-017",
15152
+ `Competitive references insufficient: found ${refItems.length}, minimum ${minRefs} required`,
15153
+ "error",
15154
+ rel,
15155
+ "ddp.competitiveRefs.insufficient"
15156
+ )
15157
+ );
15158
+ }
15159
+ } else {
15160
+ const inlineRefs = getFieldValue(content, "competitive_refs");
15161
+ if (inlineRefs !== null) {
15162
+ if (1 < minRefs) {
15163
+ issues.push(
15164
+ issue(
15165
+ "QFAI-DDP-017",
15166
+ `Competitive references insufficient: found 1, minimum ${minRefs} required`,
15167
+ "error",
15168
+ rel,
15169
+ "ddp.competitiveRefs.insufficient"
15170
+ )
15171
+ );
15172
+ }
15173
+ }
15174
+ }
15175
+ if (/competitive_refs/m.test(content) && !/translation_policy\s*:/m.test(content)) {
15176
+ issues.push(
15177
+ issue(
15178
+ "QFAI-DDP-018",
15179
+ "Translation policy missing in design contract with competitive references",
15180
+ "warning",
15181
+ rel,
15182
+ "ddp.translationPolicy.missing"
15183
+ )
15184
+ );
15185
+ }
15186
+ }
15187
+ }
15188
+ function extractDdpSection(content) {
15189
+ const heading = DDP_HEADING_RE.exec(content);
15190
+ if (!heading) return null;
15191
+ const start = heading.index + heading[0].length;
15192
+ const remainder = content.slice(start);
15193
+ const nextHeadingOffset = remainder.search(/^#{1,3}\s+/m);
15194
+ const section = nextHeadingOffset === -1 ? remainder : remainder.slice(0, nextHeadingOffset);
15195
+ return section.trim();
15196
+ }
15197
+ function hasFieldKey(section, field) {
15198
+ const re = new RegExp(`^${field}\\s*:`, "m");
15199
+ return re.test(section);
15200
+ }
15201
+ function hasNonEmptyField(section, field) {
15202
+ const re = new RegExp(`^${field}\\s*:(.*)`, "m");
15203
+ const match = re.exec(section);
15204
+ if (!match) return false;
15205
+ const inlineValue = (match[1] ?? "").trim();
15206
+ if (inlineValue) return true;
15207
+ const matchEnd = match.index + match[0].length;
15208
+ const remainder = section.slice(matchEnd);
15209
+ const lines = remainder.split(/\r?\n/);
15210
+ for (const line of lines) {
15211
+ const trimmed = line.trim();
15212
+ if (!trimmed) continue;
15213
+ if (/^\s+/.test(line) && trimmed.length > 0) return true;
15214
+ break;
15215
+ }
15216
+ return false;
15217
+ }
15218
+ function getFieldValue(section, field) {
15219
+ const re = new RegExp(`^${field}\\s*:(.*)`, "m");
15220
+ const match = re.exec(section);
15221
+ if (!match) return null;
15222
+ const inlineValue = (match[1] ?? "").trim();
15223
+ if (inlineValue) return inlineValue;
15224
+ const matchEnd = match.index + match[0].length;
15225
+ const remainder = section.slice(matchEnd);
15226
+ const lines = remainder.split(/\r?\n/);
15227
+ const blockLines = [];
15228
+ for (const line of lines) {
15229
+ const trimmed = line.trim();
15230
+ if (!trimmed && blockLines.length === 0) continue;
15231
+ if (/^\s+/.test(line) && trimmed.length > 0) {
15232
+ blockLines.push(trimmed);
15233
+ } else if (blockLines.length > 0 || !trimmed && blockLines.length === 0) {
15234
+ if (!trimmed && blockLines.length === 0) continue;
15235
+ break;
15236
+ } else {
15237
+ break;
15238
+ }
15239
+ }
15240
+ return blockLines.length > 0 ? blockLines.join("\n") : null;
15241
+ }
15242
+ function extractNestedBlock(section, field) {
15243
+ const re = new RegExp(`^${field}\\s*:(.*)`, "m");
15244
+ const match = re.exec(section);
15245
+ if (!match) return null;
15246
+ const inlineValue = (match[1] ?? "").trim();
15247
+ if (inlineValue) return null;
15248
+ const matchEnd = match.index + match[0].length;
15249
+ const remainder = section.slice(matchEnd);
15250
+ const lines = remainder.split(/\r?\n/);
15251
+ const blockLines = [];
15252
+ for (const line of lines) {
15253
+ const trimmed = line.trim();
15254
+ if (!trimmed && blockLines.length === 0) continue;
15255
+ if (/^\s+/.test(line) && trimmed.length > 0) {
15256
+ blockLines.push(line);
15257
+ } else if (blockLines.length > 0) {
15258
+ break;
15259
+ } else if (trimmed) {
15260
+ break;
15261
+ }
15262
+ }
15263
+ return blockLines.length > 0 ? blockLines.join("\n") : null;
15264
+ }
15265
+ function hasSubField(block, subField) {
15266
+ const re = new RegExp(`^\\s*${subField}\\s*:\\s*\\S`, "m");
15267
+ return re.test(block);
15268
+ }
15269
+ function collectListItems(section, field) {
15270
+ const re = new RegExp(`^${field}\\s*:(.*)`, "m");
15271
+ const match = re.exec(section);
15272
+ if (!match) return [];
15273
+ const matchEnd = match.index + match[0].length;
15274
+ const remainder = section.slice(matchEnd);
15275
+ const lines = remainder.split(/\r?\n/);
15276
+ const items = [];
15277
+ for (const line of lines) {
15278
+ const trimmed = line.trim();
15279
+ if (!trimmed && items.length === 0) continue;
15280
+ if (/^\s+-\s/.test(line)) {
15281
+ items.push(trimmed.replace(/^-\s*/, ""));
15282
+ } else if (/^\s+\S/.test(line) && items.length > 0) {
15283
+ continue;
15284
+ } else if (trimmed && !/^\s/.test(line)) {
15285
+ break;
15286
+ } else if (!trimmed && items.length > 0) {
15287
+ break;
15288
+ }
15289
+ }
15290
+ return items;
15291
+ }
15292
+
15293
+ // src/core/validators/navigationFlow.ts
15294
+ var import_promises51 = require("fs/promises");
15295
+ var NODE_DEF_RE = /([A-Za-z_][\w-]*)\s*(?:\[.*?\]|\(.*?\)|\{.*?\})/g;
15296
+ var EDGE_RE = /([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?\s*(?:--+>|==+>|-.->|~~>)\s*(?:\|"?([^"|]*)"?\|)?\s*([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?/g;
15297
+ var SUBGRAPH_RE = /^\s*subgraph\s+([\w-]+)/gm;
15298
+ var FLOWCHART_DECL_RE = /^\s*(?:flowchart|graph)\s+(TD|TB|BT|RL|LR)\s*$/m;
15299
+ var MODERN_FLOWCHART_RE = /^\s*flowchart\s+(TD|TB|BT|RL|LR)\s*$/m;
15300
+ var TERMINAL_TEXT_RE = /\b(?:end|terminal|finish|complete|done|終了|完了)\b/i;
15301
+ var ERROR_NODE_CLASS_RE = /:::\s*error\b/;
15302
+ var ERROR_PREFIX_RE = /^err-/;
15303
+ function parseFlowchart(content) {
15304
+ const nodes = /* @__PURE__ */ new Map();
15305
+ const edges = [];
15306
+ const subgraphs = [];
15307
+ for (const match of content.matchAll(SUBGRAPH_RE)) {
15308
+ if (match[1]) subgraphs.push(match[1]);
15309
+ }
15310
+ const lines = content.split("\n");
15311
+ for (const line of lines) {
15312
+ const trimmed = line.trim();
15313
+ if (trimmed.startsWith("%%") || trimmed.startsWith("subgraph") || trimmed === "end" || FLOWCHART_DECL_RE.test(trimmed)) {
15314
+ continue;
15315
+ }
15316
+ for (const match of line.matchAll(NODE_DEF_RE)) {
15317
+ const id = match[1] ?? "";
15318
+ const fullMatch = match[0];
15319
+ const bracketContent = fullMatch.match(/\[([^\]]*)\]/)?.[1] ?? fullMatch.match(/\(([^)]*)\)/)?.[1] ?? fullMatch.match(/\{([^}]*)\}/)?.[1] ?? id;
15320
+ const displayText = bracketContent.replace(/^["']|["']$/g, "");
15321
+ const isError = ERROR_PREFIX_RE.test(id) || ERROR_NODE_CLASS_RE.test(line);
15322
+ const isTerminal = TERMINAL_TEXT_RE.test(displayText);
15323
+ if (!nodes.has(id)) {
15324
+ nodes.set(id, { id, displayText, isTerminal, isError });
15325
+ }
15326
+ }
15327
+ }
15328
+ for (const match of content.matchAll(EDGE_RE)) {
15329
+ const from = match[1] ?? "";
15330
+ const label = match[2] !== void 0 && match[2] !== "" ? match[2] : null;
15331
+ const to = match[3] ?? "";
15332
+ edges.push({ from, to, label });
15333
+ if (!nodes.has(from)) {
15334
+ nodes.set(from, {
15335
+ id: from,
15336
+ displayText: from,
15337
+ isTerminal: false,
15338
+ isError: ERROR_PREFIX_RE.test(from)
15339
+ });
15340
+ }
15341
+ if (!nodes.has(to)) {
15342
+ nodes.set(to, {
15343
+ id: to,
15344
+ displayText: to,
15345
+ isTerminal: false,
15346
+ isError: ERROR_PREFIX_RE.test(to)
15347
+ });
15348
+ }
15349
+ }
15350
+ return { nodes, edges, subgraphs, raw: content };
15351
+ }
15352
+ function hasFlowchartDeclaration(content) {
15353
+ return FLOWCHART_DECL_RE.test(content);
15354
+ }
15355
+ function checkMermaidSyntax(content, file) {
15356
+ const issues = [];
15357
+ if (hasFlowchartDeclaration(content) && !MODERN_FLOWCHART_RE.test(content)) {
15358
+ issues.push(
15359
+ issue(
15360
+ "QFAI-NAV-001",
15361
+ `Mermaid block uses legacy "graph" keyword. Use "flowchart" instead.`,
15362
+ "error",
15363
+ file,
15364
+ "navigationFlow.mermaid.syntax"
15365
+ )
15366
+ );
15367
+ return issues;
15368
+ }
15369
+ if (!hasFlowchartDeclaration(content)) {
15370
+ issues.push(
15371
+ issue(
15372
+ "QFAI-NAV-001",
15373
+ `Mermaid block does not contain a valid flowchart declaration.`,
15374
+ "error",
15375
+ file,
15376
+ "navigationFlow.mermaid.syntax"
15377
+ )
15378
+ );
15379
+ return issues;
15380
+ }
15381
+ const subgraphOpens = (content.match(/^\s*subgraph\b/gm) ?? []).length;
15382
+ const subgraphCloses = (content.match(/^\s*end\b/gm) ?? []).length;
15383
+ if (subgraphOpens !== subgraphCloses) {
15384
+ issues.push(
15385
+ issue(
15386
+ "QFAI-NAV-001",
15387
+ `Mermaid flowchart has mismatched subgraph/end blocks (${subgraphOpens} subgraph vs ${subgraphCloses} end).`,
15388
+ "error",
15389
+ file,
15390
+ "navigationFlow.mermaid.syntax"
15391
+ )
15392
+ );
15393
+ }
15394
+ return issues;
15395
+ }
15396
+ function checkUnlabeledEdges(parsed, file) {
15397
+ const issues = [];
15398
+ for (const edge of parsed.edges) {
15399
+ if (edge.label === null) {
15400
+ issues.push(
15401
+ issue(
15402
+ "QFAI-NAV-002",
15403
+ `Edge ${edge.from} \u2192 ${edge.to} has no label. All edges must be labeled.`,
15404
+ "warning",
15405
+ file,
15406
+ "navigationFlow.edge.label"
15407
+ )
15408
+ );
15409
+ }
15410
+ }
15411
+ return issues;
15412
+ }
15413
+ function checkDeadEndNodes(parsed, file) {
15414
+ const issues = [];
15415
+ const outgoingCount = /* @__PURE__ */ new Map();
15416
+ for (const [id] of parsed.nodes) {
15417
+ outgoingCount.set(id, 0);
15418
+ }
15419
+ for (const edge of parsed.edges) {
15420
+ outgoingCount.set(edge.from, (outgoingCount.get(edge.from) ?? 0) + 1);
15421
+ }
15422
+ for (const [id, count] of outgoingCount) {
15423
+ if (count === 0) {
15424
+ const node = parsed.nodes.get(id);
15425
+ if (node && !node.isTerminal) {
15426
+ issues.push(
15427
+ issue(
15428
+ "QFAI-NAV-003",
15429
+ `Node "${id}" has no outgoing edges and is not marked as terminal.`,
15430
+ "warning",
15431
+ file,
15432
+ "navigationFlow.node.deadEnd"
15433
+ )
15434
+ );
15435
+ }
15436
+ }
15437
+ }
15438
+ return issues;
15439
+ }
15440
+ function checkReachability(parsed, file) {
15441
+ const issues = [];
15442
+ if (parsed.nodes.size === 0) return issues;
15443
+ const firstEdge = parsed.edges[0];
15444
+ const entryId = firstEdge?.from ?? parsed.nodes.keys().next().value;
15445
+ if (!entryId) return issues;
15446
+ const visited = /* @__PURE__ */ new Set();
15447
+ const queue = [entryId];
15448
+ visited.add(entryId);
15449
+ while (queue.length > 0) {
15450
+ const current = queue.shift() ?? "";
15451
+ for (const edge of parsed.edges) {
15452
+ if (edge.from === current && !visited.has(edge.to)) {
15453
+ visited.add(edge.to);
15454
+ queue.push(edge.to);
15455
+ }
15456
+ }
15457
+ }
15458
+ for (const [id] of parsed.nodes) {
15459
+ if (!visited.has(id)) {
15460
+ issues.push(
15461
+ issue(
15462
+ "QFAI-NAV-004",
15463
+ `Node "${id}" is unreachable from entry point "${entryId}".`,
15464
+ "error",
15465
+ file,
15466
+ "navigationFlow.node.unreachable"
15467
+ )
15468
+ );
15469
+ }
15470
+ }
15471
+ return issues;
15472
+ }
15473
+ function checkErrorRecovery(parsed, file) {
15474
+ const issues = [];
15475
+ for (const [id, node] of parsed.nodes) {
15476
+ if (!node.isError) continue;
15477
+ const hasOutgoing = parsed.edges.some((e) => e.from === id);
15478
+ if (!hasOutgoing) {
15479
+ issues.push(
15480
+ issue(
15481
+ "QFAI-NAV-005",
15482
+ `Error node "${id}" has no outgoing edge to recover to normal flow.`,
15483
+ "error",
15484
+ file,
15485
+ "navigationFlow.errorNode.recovery"
15486
+ )
15487
+ );
15488
+ }
15489
+ }
15490
+ return issues;
15491
+ }
15492
+ function checkViewportDiffs(content, parsed, file) {
15493
+ const issues = [];
15494
+ const hasViewportSubgraph = parsed.subgraphs.some(
15495
+ (s) => /desktop|mobile|tablet|responsive/i.test(s)
15496
+ );
15497
+ const hasViewportAnnotation = /%%\s*(?:desktop|mobile|tablet|viewport|responsive)/i.test(content) || /\bdesktop\b.*\bmobile\b/i.test(content) || /\bmobile\b.*\bdesktop\b/i.test(content);
15498
+ const hasSharedDeclaration = /\b(?:shared|common)\b/i.test(content);
15499
+ if (!hasViewportSubgraph && !hasViewportAnnotation && !hasSharedDeclaration) {
15500
+ issues.push(
15501
+ issue(
15502
+ "QFAI-NAV-006",
15503
+ `Flowchart does not document viewport differences. Use subgraphs for desktop/mobile or annotate as "shared".`,
15504
+ "warning",
15505
+ file,
15506
+ "navigationFlow.viewport.diff"
15507
+ )
15508
+ );
15509
+ }
15510
+ return issues;
15511
+ }
15512
+ function checkImplementationAlignment(specContent, parsed, file) {
15513
+ const issues = [];
15514
+ const screenListMatch = specContent.match(/## Screen List\s*\n((?:.*\n)*?)(?=## |$)/);
15515
+ if (!screenListMatch) {
15516
+ return issues;
15517
+ }
15518
+ const screenListBlock = screenListMatch[1] ?? "";
15519
+ const screenNames = [];
15520
+ for (const line of screenListBlock.split("\n")) {
15521
+ const match = line.match(/^\s*[-*]\s+(.+)/);
15522
+ if (match?.[1]) {
15523
+ screenNames.push(match[1].trim());
15524
+ }
15525
+ }
15526
+ if (screenNames.length === 0) return issues;
15527
+ const flowNodeTexts = /* @__PURE__ */ new Set();
15528
+ for (const [, node] of parsed.nodes) {
15529
+ flowNodeTexts.add(node.displayText.toLowerCase());
15530
+ }
15531
+ for (const screen of screenNames) {
15532
+ const screenLower = screen.toLowerCase();
15533
+ const found = [...flowNodeTexts].some(
15534
+ (t) => t.includes(screenLower) || screenLower.includes(t)
15535
+ );
15536
+ if (!found) {
15537
+ issues.push(
15538
+ issue(
15539
+ "QFAI-NAV-007",
15540
+ `Screen "${screen}" in Screen List is not found in flowchart nodes.`,
15541
+ "warning",
15542
+ file,
15543
+ "navigationFlow.alignment.mismatch"
15544
+ )
15545
+ );
15546
+ }
15547
+ }
15548
+ const screenNamesLower = screenNames.map((s) => s.toLowerCase());
15549
+ for (const [, node] of parsed.nodes) {
15550
+ if (node.isTerminal || node.isError) continue;
15551
+ const textLower = node.displayText.toLowerCase();
15552
+ const found = screenNamesLower.some((s) => s.includes(textLower) || textLower.includes(s));
15553
+ if (!found) {
15554
+ issues.push(
15555
+ issue(
15556
+ "QFAI-NAV-007",
15557
+ `Flowchart node "${node.displayText}" has no matching entry in Screen List.`,
15558
+ "warning",
15559
+ file,
15560
+ "navigationFlow.alignment.mismatch"
15561
+ )
15562
+ );
15563
+ }
15564
+ }
15565
+ return issues;
15566
+ }
15567
+ async function validateNavigationFlow(root, config) {
15568
+ const specsDir = resolvePath(root, config, "specsDir");
15569
+ const allFiles = await collectFiles(specsDir, { extensions: [".md"] });
15570
+ const specFiles = allFiles.filter((f) => /[\\/]spec-\d{4}[\\/]/.test(f));
15571
+ const issues = [];
15572
+ for (const file of specFiles) {
15573
+ const content = await (0, import_promises51.readFile)(file, "utf-8");
15574
+ const blocks = extractFencedCodeBlocks(content);
15575
+ const mermaidBlocks = blocks.filter((b) => b.language === "mermaid");
15576
+ const _flowchartBlocks = mermaidBlocks.filter((b) => hasFlowchartDeclaration(b.content));
15577
+ for (const block of mermaidBlocks) {
15578
+ if (!hasFlowchartDeclaration(block.content)) {
15579
+ continue;
15580
+ }
15581
+ issues.push(...checkMermaidSyntax(block.content, file));
15582
+ const parsed = parseFlowchart(block.content);
15583
+ issues.push(...checkUnlabeledEdges(parsed, file));
15584
+ issues.push(...checkDeadEndNodes(parsed, file));
15585
+ issues.push(...checkReachability(parsed, file));
15586
+ issues.push(...checkErrorRecovery(parsed, file));
15587
+ issues.push(...checkViewportDiffs(block.content, parsed, file));
15588
+ issues.push(...checkImplementationAlignment(content, parsed, file));
15589
+ }
15590
+ }
15591
+ return issues;
15592
+ }
15593
+
15594
+ // src/core/validators/renderCritique.ts
15595
+ var import_node_path54 = __toESM(require("path"), 1);
15596
+ var import_fast_glob9 = __toESM(require("fast-glob"), 1);
15597
+ var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
15598
+ var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
15599
+ var READ_ORDER_RE = /DDP[\s\S]{0,40}Design\s*Token[\s\S]{0,40}UI\s*Contract[\s\S]{0,40}HTML\s*Mock[\s\S]{0,40}Flow/i;
15600
+ var DESKTOP_RE = /\b(desktop|1024\s*px|1280\s*px|1440\s*px|viewport\s*[≥>=]+\s*1024)\b/i;
15601
+ var MOBILE_RE = /\b(mobile|480\s*px|375\s*px|390\s*px|viewport\s*[≤<=]+\s*480)\b/i;
15602
+ var EVIDENCE_DATE_RE = /\bdate\s*:/i;
15603
+ var EVIDENCE_VIEWPORT_RE = /\bviewport\s*:/i;
15604
+ var EVIDENCE_VERDICT_RE = /\bverdict\s*:\s*(PASS|REVISE)\b/i;
15605
+ var EVIDENCE_FINDINGS_RE = /\bfindings\s*:/i;
15606
+ var RUBRIC_RE = /\b(rubric|evaluation\s*criteria|scoring\s*guide)\b/i;
15607
+ var TASK_FIDELITY_SECTION_RE = /\btaskFidelity\b/i;
15608
+ var STEP_COUNT_RE = /\bstep_count\s*:\s*(\d+)/i;
15609
+ var CTA_VISIBILITY_RE = /\bcta_visibility\s*:/i;
15610
+ var FOUR_STATE_CHECK_RE = /\bfour_state_check\s*:/i;
15611
+ var MAX_PRIMARY_STEPS_RE = /\bmax_primary_steps\s*:\s*(\d+)/i;
15612
+ async function validateRenderCritique(root, config) {
15613
+ const issues = [];
15614
+ const discussionDir = import_node_path54.default.join(root, config.paths.discussionDir).replace(/\\/g, "/");
15615
+ const discussionFiles = await (0, import_fast_glob9.default)(import_node_path54.default.posix.join(discussionDir, "**/*.md"), { absolute: true });
15616
+ let hasDdp = false;
15617
+ for (const df of discussionFiles) {
15618
+ const content = await readSafe(df);
15619
+ if (/^#{1,3}\s+Design\s+Direction\s+Pack/im.test(content)) {
15620
+ hasDdp = true;
15621
+ break;
15622
+ }
15623
+ }
15624
+ if (!hasDdp) return issues;
15625
+ const skillsDir = import_node_path54.default.join(root, config.paths.skillsDir).replace(/\\/g, "/");
15626
+ const evidenceDir = import_node_path54.default.join(root, ".qfai", "evidence").replace(/\\/g, "/");
15627
+ const skillPromptPattern = import_node_path54.default.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
15628
+ const skillFiles = await (0, import_fast_glob9.default)(skillPromptPattern, { dot: true });
15629
+ const evidencePattern = import_node_path54.default.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
15630
+ const evidenceFiles = await (0, import_fast_glob9.default)(evidencePattern, { dot: true });
15631
+ for (const sf of skillFiles) {
15632
+ const content = await readSafe(sf);
15633
+ if (content.length > 0 && !RENDERED_KEYWORDS_RE.test(content)) {
15634
+ issues.push(
15635
+ issue(
15636
+ "QFAI-CRIT-001",
15637
+ `Skill prompt does not mention rendered/screenshot/HTML review: ${import_node_path54.default.relative(root, sf)}`,
15638
+ "error",
15639
+ sf,
15640
+ "renderCritique.codeOnly",
15641
+ void 0,
15642
+ "change",
15643
+ "Add rendered output review requirement (e.g. 'screenshot', 'rendered', 'HTML') to the skill prompt."
15644
+ )
15645
+ );
15646
+ }
15647
+ }
15648
+ for (const sf of skillFiles) {
15649
+ const content = await readSafe(sf);
15650
+ if (content.length > 0 && !DDP_REFERENCE_RE.test(content)) {
15651
+ issues.push(
15652
+ issue(
15653
+ "QFAI-CRIT-002",
15654
+ `Downstream skill prompt missing DDP reference: ${import_node_path54.default.relative(root, sf)}`,
15655
+ "error",
15656
+ sf,
15657
+ "renderCritique.ddpMissing",
15658
+ void 0,
15659
+ "change",
15660
+ "Add DDP (Design Direction Pack) reference to the downstream skill prompt."
15661
+ )
15662
+ );
15663
+ }
15664
+ }
15665
+ const allEvidenceContent = await collectContent(evidenceFiles);
15666
+ if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
15667
+ issues.push(
15668
+ issue(
15669
+ "QFAI-CRIT-003",
15670
+ "No desktop viewport critique found in evidence files (viewport >= 1024px required).",
15671
+ "error",
15672
+ evidenceDir,
15673
+ "renderCritique.desktopMissing",
15674
+ void 0,
15675
+ "change",
15676
+ "Add desktop viewport critique (>= 1024px) to evidence files."
15677
+ )
15678
+ );
15679
+ }
15680
+ if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
15681
+ issues.push(
15682
+ issue(
15683
+ "QFAI-CRIT-004",
15684
+ "No mobile viewport critique found in evidence files (viewport <= 480px required).",
15685
+ "error",
15686
+ evidenceDir,
15687
+ "renderCritique.mobileMissing",
15688
+ void 0,
15689
+ "change",
15690
+ "Add mobile viewport critique (<= 480px) to evidence files."
15691
+ )
15692
+ );
15693
+ }
15694
+ for (const sf of skillFiles) {
15695
+ const content = await readSafe(sf);
15696
+ if (content.length > 0 && !READ_ORDER_RE.test(content)) {
15697
+ issues.push(
15698
+ issue(
15699
+ "QFAI-CRIT-005",
15700
+ `Read order not specified (DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow): ${import_node_path54.default.relative(root, sf)}`,
15701
+ "error",
15702
+ sf,
15703
+ "renderCritique.readOrder",
15704
+ void 0,
15705
+ "change",
15706
+ "Specify the read order: DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow."
15707
+ )
15708
+ );
15709
+ }
15710
+ }
15711
+ for (const ef of evidenceFiles) {
15712
+ const content = await readSafe(ef);
15713
+ if (content.length === 0) continue;
15714
+ const hasDate = EVIDENCE_DATE_RE.test(content);
15715
+ const hasViewport = EVIDENCE_VIEWPORT_RE.test(content);
15716
+ const hasVerdict = EVIDENCE_VERDICT_RE.test(content);
15717
+ const hasFindings = EVIDENCE_FINDINGS_RE.test(content);
15718
+ if (!hasDate || !hasViewport || !hasVerdict || !hasFindings) {
15719
+ const missing = [];
15720
+ if (!hasDate) missing.push("date");
15721
+ if (!hasViewport) missing.push("viewport");
15722
+ if (!hasVerdict) missing.push("verdict");
15723
+ if (!hasFindings) missing.push("findings");
15724
+ issues.push(
15725
+ issue(
15726
+ "QFAI-CRIT-006",
15727
+ `Critique evidence incomplete (missing: ${missing.join(", ")}): ${import_node_path54.default.relative(root, ef)}`,
15728
+ "error",
15729
+ ef,
15730
+ "renderCritique.incompleteEvidence",
15731
+ void 0,
15732
+ "change",
15733
+ "Ensure evidence includes date, viewport, verdict (PASS/REVISE), and findings."
15734
+ )
15735
+ );
15736
+ }
15737
+ }
15738
+ if (evidenceFiles.length > 0 && !RUBRIC_RE.test(allEvidenceContent)) {
15739
+ issues.push(
15740
+ issue(
15741
+ "QFAI-CRIT-007",
15742
+ "Rubric / evaluation criteria not documented in evidence files.",
15743
+ "warning",
15744
+ evidenceDir,
15745
+ "renderCritique.rubricMissing",
15746
+ void 0,
15747
+ "change",
15748
+ "Document the rubric or evaluation criteria in evidence files for reproducibility."
15749
+ )
15750
+ );
15751
+ }
15752
+ if (evidenceFiles.length > 0) {
15753
+ const desktopPass = hasViewportPass(allEvidenceContent, "desktop");
15754
+ const mobilePass = hasViewportPass(allEvidenceContent, "mobile");
15755
+ if (!desktopPass || !mobilePass) {
15756
+ issues.push(
15757
+ issue(
15758
+ "QFAI-CRIT-008",
15759
+ `Iterative loop not completed: ${!desktopPass ? "desktop" : ""}${!desktopPass && !mobilePass ? " and " : ""}${!mobilePass ? "mobile" : ""} viewport not PASS.`,
15760
+ "error",
15761
+ evidenceDir,
15762
+ "renderCritique.loopNotCompleted",
15763
+ void 0,
15764
+ "change",
15765
+ "Both desktop and mobile viewports must have verdict: PASS for the loop to complete."
15766
+ )
15767
+ );
15768
+ }
15769
+ }
15770
+ if (evidenceFiles.length > 0 && !TASK_FIDELITY_SECTION_RE.test(allEvidenceContent)) {
15771
+ issues.push(
15772
+ issue(
15773
+ "QFAI-CRIT-009",
15774
+ "taskFidelity evaluation not recorded in evidence files.",
15775
+ "error",
15776
+ evidenceDir,
15777
+ "renderCritique.taskFidelityMissing",
15778
+ void 0,
15779
+ "change",
15780
+ "Add taskFidelity section with step_count, cta_visibility, and four_state_check."
15781
+ )
15782
+ );
15783
+ }
15784
+ if (evidenceFiles.length > 0 && TASK_FIDELITY_SECTION_RE.test(allEvidenceContent)) {
15785
+ const stepCountMatch = STEP_COUNT_RE.exec(allEvidenceContent);
15786
+ const maxStepsMatch = MAX_PRIMARY_STEPS_RE.exec(allEvidenceContent);
15787
+ const hasCta = CTA_VISIBILITY_RE.test(allEvidenceContent);
15788
+ const hasFourState = FOUR_STATE_CHECK_RE.test(allEvidenceContent);
15789
+ if (!hasCta || !hasFourState) {
15790
+ issues.push(
15791
+ issue(
15792
+ "QFAI-CRIT-009",
15793
+ `taskFidelity section incomplete (missing: ${[!hasCta ? "cta_visibility" : "", !hasFourState ? "four_state_check" : ""].filter(Boolean).join(", ")}).`,
15794
+ "error",
15795
+ evidenceDir,
15796
+ "renderCritique.taskFidelityMissing",
15797
+ void 0,
15798
+ "change",
15799
+ "Add missing taskFidelity fields: step_count, cta_visibility, four_state_check."
15800
+ )
15801
+ );
15802
+ }
15803
+ if (stepCountMatch && maxStepsMatch) {
15804
+ const stepCount = parseInt(stepCountMatch[1] ?? "0", 10);
15805
+ const maxSteps = parseInt(maxStepsMatch[1] ?? "0", 10);
15806
+ if (stepCount > maxSteps) {
15807
+ issues.push(
15808
+ issue(
15809
+ "QFAI-CRIT-010",
15810
+ `taskFidelity FAIL: step_count (${stepCount}) exceeds max_primary_steps (${maxSteps}).`,
15811
+ "error",
15812
+ evidenceDir,
15813
+ "renderCritique.taskFidelityFail",
15814
+ void 0,
15815
+ "change",
15816
+ "Reduce step_count to be within max_primary_steps, or revise the flow."
15817
+ )
15818
+ );
15819
+ }
15820
+ }
15821
+ }
15822
+ return issues;
15823
+ }
15824
+ function hasViewportPass(content, viewport) {
15825
+ const vpRe = viewport === "desktop" ? DESKTOP_RE : MOBILE_RE;
15826
+ const sections = content.split(/(?=^#{1,3}\s)/m);
15827
+ for (const section of sections) {
15828
+ if (vpRe.test(section) && /verdict\s*:\s*PASS/i.test(section)) {
15829
+ return true;
15830
+ }
15831
+ }
15832
+ return false;
15833
+ }
15834
+ async function collectContent(files) {
15835
+ const contents = [];
15836
+ for (const f of files) {
15837
+ contents.push(await readSafe(f));
15838
+ }
15839
+ return contents.join("\n---\n");
15840
+ }
15841
+
15842
+ // src/core/validators/designFidelity.ts
15843
+ var import_promises52 = require("fs/promises");
15844
+ var import_node_path55 = __toESM(require("path"), 1);
15845
+ var import_fast_glob10 = __toESM(require("fast-glob"), 1);
15846
+ var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
15847
+ var BASE_DIMENSIONS = ["hierarchy", "clarity", "accessibility", "responsive"];
15848
+ var TASK_FIDELITY_REQUIRED_FIELDS = [
15849
+ "step_count",
15850
+ "cta_visibility",
15851
+ "empty_state",
15852
+ "error_state",
15853
+ "primary_flow_clicks"
15854
+ ];
15855
+ var PASS_THRESHOLD = 70;
15856
+ var ESCALATED_CODES = [
15857
+ "dual_primary_cta",
15858
+ "empty_state_without_action",
15859
+ "error_without_recovery",
15860
+ "placeholder_or_lorem",
15861
+ "cta_visibility_fail",
15862
+ "scorecard_dimension_missing"
15863
+ ];
15864
+ async function validateDesignFidelity(root, config) {
15865
+ const issues = [];
15866
+ const evidenceDirs = [".qfai/evidence", ".qfai/review"];
15867
+ const allFiles = [];
15868
+ for (const dir of evidenceDirs) {
15869
+ const pattern = import_node_path55.default.posix.join(root.replace(/\\/g, "/"), dir, "**/*.md");
15870
+ const files = await (0, import_fast_glob10.default)(pattern, { absolute: true });
15871
+ allFiles.push(...files);
15872
+ }
15873
+ for (const filePath of allFiles) {
15874
+ let content;
15875
+ try {
15876
+ content = await (0, import_promises52.readFile)(filePath, "utf-8");
15877
+ } catch {
15878
+ continue;
15879
+ }
15880
+ if (!SCORECARD_HEADING_RE.test(content)) continue;
15881
+ const rel = import_node_path55.default.relative(root, filePath).replace(/\\/g, "/");
15882
+ const section = extractScorecardSection(content);
15883
+ if (!section) continue;
15884
+ const dimensions = parseDimensions(section);
15885
+ const _verdict = parseVerdict(section);
15886
+ for (const dim of BASE_DIMENSIONS) {
15887
+ if (!dimensions.has(dim)) {
15888
+ issues.push(
15889
+ issue(
15890
+ "QFAI-FID-001",
15891
+ `Fidelity scorecard missing dimension: ${dim}`,
15892
+ "error",
15893
+ rel,
15894
+ `fidelity.dimension.${dim}`
15895
+ )
15896
+ );
15897
+ } else {
15898
+ const d = dimensions.get(dim);
15899
+ if (d && (d.score === null || d.comment === null)) {
15900
+ issues.push(
15901
+ issue(
15902
+ "QFAI-FID-002",
15903
+ `Fidelity dimension '${dim}' missing ${d.score === null ? "score" : "prose comment"}`,
15904
+ "error",
15905
+ rel,
15906
+ `fidelity.scoreOrProse.${dim}`
15907
+ )
15908
+ );
15909
+ }
15910
+ }
15911
+ }
15912
+ const scores = [];
15913
+ for (const dim of BASE_DIMENSIONS) {
15914
+ const d = dimensions.get(dim);
15915
+ if (d?.score !== null && d?.score !== void 0) {
15916
+ scores.push(d.score);
15917
+ if (d.score < PASS_THRESHOLD) {
15918
+ issues.push(
15919
+ issue(
15920
+ "QFAI-FID-003",
15921
+ `Dimension '${dim}' score ${d.score} is below threshold ${PASS_THRESHOLD}`,
15922
+ "error",
15923
+ rel,
15924
+ `fidelity.threshold.${dim}`
15925
+ )
15926
+ );
15927
+ }
15928
+ }
15929
+ }
15930
+ const taskFid = dimensions.get("taskFidelity");
15931
+ if (taskFid?.score !== null && taskFid?.score !== void 0) {
15932
+ scores.push(taskFid.score);
15933
+ if (taskFid.score < PASS_THRESHOLD) {
15934
+ issues.push(
15935
+ issue(
15936
+ "QFAI-FID-003",
15937
+ `Dimension 'taskFidelity' score ${taskFid.score} is below threshold ${PASS_THRESHOLD}`,
15938
+ "error",
15939
+ rel,
15940
+ "fidelity.threshold.taskFidelity"
15941
+ )
15942
+ );
15943
+ }
15944
+ }
15945
+ if (scores.length > 0) {
15946
+ const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
15947
+ if (avg < PASS_THRESHOLD) {
15948
+ issues.push(
15949
+ issue(
15950
+ "QFAI-FID-003",
15951
+ `Overall average score ${avg.toFixed(1)} is below threshold ${PASS_THRESHOLD}`,
15952
+ "error",
15953
+ rel,
15954
+ "fidelity.threshold.average"
15955
+ )
15956
+ );
15957
+ }
15958
+ }
15959
+ for (const dim of BASE_DIMENSIONS) {
15960
+ const d = dimensions.get(dim);
15961
+ if (d?.score !== null && d?.score !== void 0 && d.score < PASS_THRESHOLD) {
15962
+ if (!d.improvement || !d.alternative) {
15963
+ issues.push(
15964
+ issue(
15965
+ "QFAI-FID-004",
15966
+ `FAIL dimension '${dim}' missing ${!d.improvement ? "improvement instructions" : "alternative suggestion"}`,
15967
+ "error",
15968
+ rel,
15969
+ `fidelity.improvement.${dim}`
15970
+ )
15971
+ );
15972
+ }
15973
+ }
15974
+ }
15975
+ const responsive = dimensions.get("responsive");
15976
+ if (responsive) {
15977
+ if (!responsive.desktop || !responsive.mobile) {
15978
+ issues.push(
15979
+ issue(
15980
+ "QFAI-FID-005",
15981
+ `Responsive dimension missing ${!responsive.desktop ? "desktop" : "mobile"} viewport results`,
15982
+ "error",
15983
+ rel,
15984
+ "fidelity.viewport.responsive"
15985
+ )
15986
+ );
15987
+ }
15988
+ }
15989
+ const delta = parseDelta(section);
15990
+ if (delta.hasBreaking) {
15991
+ if (!delta.content || !delta.impact || !delta.migration) {
15992
+ const missing = [];
15993
+ if (!delta.content) missing.push("content");
15994
+ if (!delta.impact) missing.push("impact");
15995
+ if (!delta.migration) missing.push("migration steps");
15996
+ issues.push(
15997
+ issue(
15998
+ "QFAI-FID-006",
15999
+ `Breaking delta missing: ${missing.join(", ")}`,
16000
+ "error",
16001
+ rel,
16002
+ "fidelity.breakingDelta"
16003
+ )
16004
+ );
16005
+ }
16006
+ }
16007
+ if (!hasRubric(section)) {
16008
+ issues.push(
16009
+ issue(
16010
+ "QFAI-FID-007",
16011
+ "Scorecard rubric not documented; results are not reproducible",
16012
+ "error",
16013
+ rel,
16014
+ "fidelity.rubricMissing"
16015
+ )
16016
+ );
16017
+ }
16018
+ const requiresTaskFidelity = hasTaskFidelityRequirement(content);
16019
+ if (requiresTaskFidelity) {
16020
+ if (!dimensions.has("taskFidelity")) {
16021
+ issues.push(
16022
+ issue(
16023
+ "QFAI-FID-008",
16024
+ "Spec requires taskFidelity dimension but it is not present in scorecard",
16025
+ "error",
16026
+ rel,
16027
+ "fidelity.taskFidelity.missing"
16028
+ )
16029
+ );
16030
+ } else {
16031
+ const tf = dimensions.get("taskFidelity");
16032
+ for (const field of TASK_FIDELITY_REQUIRED_FIELDS) {
16033
+ if (tf && !tf.fields.has(field)) {
16034
+ issues.push(
16035
+ issue(
16036
+ "QFAI-FID-009",
16037
+ `taskFidelity missing required field: ${field}`,
16038
+ "error",
16039
+ rel,
16040
+ `fidelity.taskFidelity.field.${field}`
16041
+ )
16042
+ );
16043
+ }
16044
+ }
16045
+ }
16046
+ }
16047
+ const overrides = config.uiux?.warning_as_error_override ?? [];
16048
+ for (const code of ESCALATED_CODES) {
16049
+ if (hasAntiPatternMention(section, code)) {
16050
+ const isOverridden = overrides.includes(code);
16051
+ const severity = isOverridden ? "warning" : "error";
16052
+ const issueCode = isOverridden ? "QFAI-FID-011" : "QFAI-FID-010";
16053
+ const suffix = isOverridden ? " (overridden to warning by config)" : "";
16054
+ issues.push(
16055
+ issue(
16056
+ issueCode,
16057
+ `Escalated validation: ${code}${suffix}`,
16058
+ severity,
16059
+ rel,
16060
+ `fidelity.escalation.${code}`
16061
+ )
16062
+ );
16063
+ }
16064
+ }
16065
+ }
16066
+ return issues;
16067
+ }
16068
+ function extractScorecardSection(content) {
16069
+ const heading = SCORECARD_HEADING_RE.exec(content);
16070
+ if (!heading) return null;
16071
+ const start = heading.index + heading[0].length;
16072
+ const remainder = content.slice(start);
16073
+ const nextHeadingOffset = remainder.search(/^#{1,3}\s+/m);
16074
+ const section = nextHeadingOffset === -1 ? remainder : remainder.slice(0, nextHeadingOffset);
16075
+ return section.trim();
16076
+ }
16077
+ function parseDimensions(section) {
16078
+ const dims = /* @__PURE__ */ new Map();
16079
+ const lines = section.split(/\r?\n/);
16080
+ let currentDim = null;
16081
+ let currentInfo = null;
16082
+ for (const line of lines) {
16083
+ const dimMatch = /^(\w+)\s*:(.*)$/.exec(line);
16084
+ if (dimMatch && !line.startsWith(" ") && !line.startsWith(" ")) {
16085
+ if (currentDim && currentInfo) {
16086
+ dims.set(currentDim, currentInfo);
16087
+ }
16088
+ const name = dimMatch[1] ?? "";
16089
+ const inlineValue = (dimMatch[2] ?? "").trim();
16090
+ if (["verdict", "rubric", "delta", "breaking_delta"].includes(name)) {
16091
+ currentDim = null;
16092
+ currentInfo = null;
16093
+ continue;
16094
+ }
16095
+ currentDim = name;
16096
+ currentInfo = {
16097
+ score: null,
16098
+ comment: null,
16099
+ improvement: null,
16100
+ alternative: null,
16101
+ desktop: null,
16102
+ mobile: null,
16103
+ fields: /* @__PURE__ */ new Map()
16104
+ };
16105
+ if (inlineValue && /^\d+$/.test(inlineValue)) {
16106
+ currentInfo.score = parseInt(inlineValue, 10);
16107
+ }
16108
+ continue;
16109
+ }
16110
+ if (currentDim && currentInfo && /^\s+/.test(line)) {
16111
+ const subMatch = /^\s+(\w+)\s*:\s*(.*)$/.exec(line);
16112
+ if (subMatch) {
16113
+ const key = subMatch[1] ?? "";
16114
+ const value = (subMatch[2] ?? "").trim();
16115
+ switch (key) {
16116
+ case "score":
16117
+ currentInfo.score = value ? parseInt(value, 10) : null;
16118
+ break;
16119
+ case "comment":
16120
+ currentInfo.comment = value || null;
16121
+ break;
16122
+ case "improvement":
16123
+ currentInfo.improvement = value || null;
16124
+ break;
16125
+ case "alternative":
16126
+ currentInfo.alternative = value || null;
16127
+ break;
16128
+ case "desktop":
16129
+ currentInfo.desktop = value || null;
16130
+ break;
16131
+ case "mobile":
16132
+ currentInfo.mobile = value || null;
16133
+ break;
16134
+ default:
16135
+ currentInfo.fields.set(key, value);
16136
+ break;
16137
+ }
16138
+ }
16139
+ }
16140
+ }
16141
+ if (currentDim && currentInfo) {
16142
+ dims.set(currentDim, currentInfo);
16143
+ }
16144
+ return dims;
16145
+ }
16146
+ function parseVerdict(section) {
16147
+ const match = /^verdict\s*:\s*(\S+)/m.exec(section);
16148
+ return match?.[1] ?? null;
16149
+ }
16150
+ function parseDelta(section) {
16151
+ const hasBreaking = /breaking_delta\s*:/m.test(section) || /delta\s*:/m.test(section) && /breaking/im.test(section);
16152
+ if (!hasBreaking) {
16153
+ return { hasBreaking: false, content: false, impact: false, migration: false };
16154
+ }
16155
+ return {
16156
+ hasBreaking: true,
16157
+ content: /content\s*:/m.test(section),
16158
+ impact: /impact\s*:/m.test(section),
16159
+ migration: /migration\s*:/m.test(section)
16160
+ };
16161
+ }
16162
+ function hasRubric(section) {
16163
+ return /rubric\s*:/m.test(section);
16164
+ }
16165
+ function hasTaskFidelityRequirement(content) {
16166
+ return /taskFidelity\s*:\s*required/im.test(content) || /require.*taskFidelity/im.test(content);
16167
+ }
16168
+ function hasAntiPatternMention(section, code) {
16169
+ return new RegExp(`\\b${code.replace(/_/g, "[_ ]")}\\b`, "i").test(section);
16170
+ }
16171
+
16172
+ // src/core/validate.ts
16173
+ var UIUX_VALIDATION_BUDGET_MS = 2e3;
16174
+ async function validateProject(root, configResult, options = {}) {
16175
+ const resolved = configResult ?? await loadConfig(root);
16176
+ const { config, issues: configIssues } = resolved;
16177
+ const phase = options.phase ?? "full";
16178
+ const atddCodeTraceabilityIssues = phase === "refinement" ? [] : await validateAtddCodeTraceability(root, config);
16179
+ const uiuxStart = performance.now();
16180
+ const platformResult = await detectPlatform(root, config, options.platform);
16181
+ const platform = platformResult.platform;
16182
+ const uiuxValidators = [
16183
+ () => validateDesignToken(root, config),
16184
+ () => validateHtmlMock(root, platform, config),
16185
+ () => validateMermaidScreenFlow(root, config),
16186
+ () => validateBpApDb(root, config),
16187
+ () => validateUiDefinitionConsistency(root, config),
16188
+ () => validateResearchSummary(root, config),
16189
+ () => validateAgentDefinition(root, config)
16190
+ ];
16191
+ const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
16192
+ const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
16193
+ const uiuxElapsed = performance.now() - uiuxStart;
16194
+ if (uiuxElapsed > UIUX_VALIDATION_BUDGET_MS) {
16195
+ uiuxIssues.push({
16196
+ code: "QFAI-UIUX-PERF",
16197
+ severity: "warning",
16198
+ category: "compatibility",
16199
+ message: `UI/UX validation exceeded budget (${UIUX_VALIDATION_BUDGET_MS}ms). All validators were executed.`,
16200
+ rule: "uiux.performanceBudget"
16201
+ });
16202
+ }
16203
+ const findings = [
16204
+ ...configIssues,
16205
+ ...await validateRepositoryHygiene(root, config),
16206
+ ...await validateSkillsIntegrity(root, config),
16207
+ ...await validateAssistantAssets(root, config),
16208
+ ...await validateDiscussionMermaid(root),
16209
+ ...await validateMermaidEnforcement(root),
16210
+ ...await validateSpecPacks(root, config),
16211
+ ...await validateDiscussionPackReadiness(root, config),
16212
+ ...await validateDiscussionVisuals(root),
16213
+ ...await validateLegacyStatusDir(root),
16214
+ ...await validateStatusInSpecs(root, config),
16215
+ ...await validateDensityHints(root, config),
16216
+ ...await validateReviewArtifacts(root),
16217
+ ...await validatePrototypingEvidence(root, config),
16218
+ ...await validateSpecSplitByCapability(root, config),
16219
+ ...await validateLayeredTraceability(root, config),
16220
+ ...await validateOrphanProhibition(root, config),
16221
+ ...await validateLayerCoverage(root, config),
16222
+ ...atddCodeTraceabilityIssues,
16223
+ ...await validateContractReferences(root, config),
16224
+ ...await validateTraceability(root, config, phase),
16225
+ ...await validateDefinedIds(root, config),
16226
+ ...await validateContracts(root, config),
16227
+ ...await validateTddList(root, config),
16228
+ ...await validateDdpFields(root, config),
16229
+ ...await validateNavigationFlow(root, config),
16230
+ ...await validateRenderCritique(root, config),
16231
+ ...await validateDesignFidelity(root, config),
16232
+ ...uiuxIssues
16233
+ ];
16234
+ const { issues, waivers } = await applyWaivers(root, findings);
16235
+ const specsRoot = resolvePath(root, config, "specsDir");
16236
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
16237
+ const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
16238
+ const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
16239
+ root,
16240
+ config.validation.traceability.testFileGlobs,
16241
+ config.validation.traceability.testFileExcludeGlobs
16242
+ );
16243
+ const scCoverage = buildScCoverage(scIds, scTestRefs);
16244
+ const toolVersion = await resolveToolVersion();
16245
+ return {
16246
+ toolVersion,
16247
+ phase,
16248
+ issues,
16249
+ counts: countIssues(issues),
16250
+ traceability: {
16251
+ sc: scCoverage,
16252
+ testFiles
16253
+ },
16254
+ waivers
16255
+ };
16256
+ }
16257
+ function countIssues(issues) {
16258
+ return issues.reduce(
16259
+ (acc, issue2) => {
16260
+ if (issue2.suppressed) {
16261
+ return acc;
16262
+ }
16263
+ acc[issue2.severity] += 1;
16264
+ return acc;
16265
+ },
16266
+ { info: 0, warning: 0, error: 0 }
16267
+ );
16268
+ }
16269
+
16270
+ // src/core/report.ts
16271
+ var REPORT_GUARDRAILS_MAX = 20;
16272
+ var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
16273
+ var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
16274
+ async function createReportData(root, validation, configResult) {
16275
+ const resolvedRoot = import_node_path56.default.resolve(root);
16276
+ const resolved = configResult ?? await loadConfig(resolvedRoot);
16277
+ const config = resolved.config;
16278
+ const configPath = resolved.configPath;
16279
+ const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
16280
+ const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
16281
+ const apiRoot = import_node_path56.default.join(contractsRoot, "api");
16282
+ const uiRoot = import_node_path56.default.join(contractsRoot, "ui");
16283
+ const dbRoot = import_node_path56.default.join(contractsRoot, "db");
16284
+ const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
16285
+ const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
16286
+ const specEntries = await collectSpecEntries(specsRoot);
16287
+ const specFiles = await collectSpecFiles(specsRoot);
16288
+ const scenarioFiles = await collectScenarioFiles(specsRoot);
16289
+ const scenarioCount = await countScenarios(scenarioFiles);
16290
+ const testStrategy = await collectTestStrategy(
16291
+ scenarioFiles,
16292
+ resolvedRoot,
16293
+ config,
16294
+ REPORT_TEST_STRATEGY_SAMPLE_LIMIT
16295
+ );
16296
+ const {
16297
+ api: apiFiles,
16298
+ ui: uiFiles,
16299
+ db: dbFiles,
16300
+ thema: themaFiles
16301
+ } = await collectContractFiles(uiRoot, apiRoot, dbRoot);
16302
+ const contractIndex = await buildContractIndex(resolvedRoot, config);
16303
+ const contractIdList = Array.from(contractIndex.ids);
16304
+ const specContractRefs = await collectSpecContractRefs(specFiles, contractIdList);
16305
+ const referencedContracts = /* @__PURE__ */ new Set();
16306
+ for (const entry of specContractRefs.specToContracts.values()) {
16307
+ entry.ids.forEach((id) => referencedContracts.add(id));
16308
+ }
16309
+ const referencedContractCount = contractIdList.filter((id) => referencedContracts.has(id)).length;
16310
+ const orphanContractCount = contractIdList.filter((id) => !referencedContracts.has(id)).length;
16311
+ const contractIdToSpecsRecord = mapToSortedRecord(specContractRefs.idToSpecs);
16312
+ const specToContractsRecord = mapToSpecContractRecord(specContractRefs.specToContracts);
16313
+ const idsByPrefix = await collectIds([
16314
+ ...specFiles,
16315
+ ...scenarioFiles,
16316
+ ...apiFiles,
16317
+ ...uiFiles,
16318
+ ...dbFiles,
16319
+ ...themaFiles
16320
+ ]);
16321
+ const upstreamIds = await collectUpstreamIds([...specFiles, ...scenarioFiles]);
16322
+ const traceability = await evaluateTraceability(upstreamIds, srcRoot, testsRoot);
16323
+ const resolvedValidationRaw = validation ?? await validateProject(resolvedRoot, resolved);
16324
+ const normalizedValidation = normalizeValidationResult(resolvedRoot, resolvedValidationRaw);
16325
+ const scCoverage = normalizedValidation.traceability.sc;
16326
+ const testFiles = normalizedValidation.traceability.testFiles;
16327
+ const scSources = await collectScIdSourcesFromScenarioFiles(scenarioFiles);
16328
+ const scSourceRecord = mapToSortedRecord(normalizeScSources(resolvedRoot, scSources));
16329
+ const guardrailsLoad = await loadDecisionGuardrails(resolvedRoot, {
16330
+ specsRoot
16331
+ });
16332
+ const guardrailsAll = sortDecisionGuardrails(normalizeDecisionGuardrails(guardrailsLoad.entries));
16333
+ const guardrailsDisplay = guardrailsAll.slice(0, REPORT_GUARDRAILS_MAX);
16334
+ const guardrailsByType = { nonGoal: 0, notNow: 0, tradeOff: 0 };
16335
+ for (const item of guardrailsAll) {
16336
+ if (item.type === "non-goal") {
16337
+ guardrailsByType.nonGoal += 1;
16338
+ } else if (item.type === "not-now") {
16339
+ guardrailsByType.notNow += 1;
16340
+ } else {
16341
+ guardrailsByType.tradeOff += 1;
16342
+ }
16343
+ }
16344
+ const guardrailsErrors = guardrailsLoad.errors.map((item) => ({
16345
+ path: toRelativePath(resolvedRoot, item.path),
16346
+ message: item.message
16347
+ }));
16348
+ const changeTypeSummary = await collectChangeTypeSummary(specsRoot);
16349
+ const ctypeWarnings = normalizedValidation.issues.filter((item) => item.code === "QFAI-CTYPE-002").map((item) => {
16350
+ const warning = {
16351
+ file: item.file ? toRelativePath(resolvedRoot, item.file) : "(unknown)",
16352
+ suspectedMismatch: item.message,
16353
+ refs: item.refs ?? []
16354
+ };
16355
+ if (item.suggested_action) {
16356
+ warning.suggestion = item.suggested_action;
16357
+ }
16358
+ return warning;
16359
+ });
16360
+ const toReportRuleFinding = (item) => {
16361
+ const finding = {
16362
+ code: item.code,
16363
+ severity: item.severity,
16364
+ message: item.message,
16365
+ refs: item.refs ?? []
16366
+ };
16367
+ if (item.file) {
16368
+ finding.file = toRelativePath(resolvedRoot, item.file);
16369
+ }
16370
+ if (item.suggested_action) {
16371
+ finding.suggestion = item.suggested_action;
16372
+ }
16373
+ return finding;
16374
+ };
16375
+ const compatFindings = normalizedValidation.issues.filter((item) => /^QFAI-COMPAT-\d+$/.test(item.code)).map((item) => toReportRuleFinding(item));
16376
+ const scopeMismatches = normalizedValidation.issues.filter((item) => item.code === "QFAI-SCOPE-001" || item.code === "QFAI-SCOPE-002").map((item) => toReportRuleFinding(item));
16377
+ const verificationFindings = normalizedValidation.issues.filter((item) => /^QFAI-VFY-\d+$/.test(item.code)).map((item) => toReportRuleFinding(item));
16378
+ const missingDeltaUpdateIssues = normalizedValidation.issues.filter(
16379
+ (item) => item.code === "QFAI-CTYPE-003"
16380
+ ).length;
16381
+ const waiverState = normalizedValidation.waivers ?? {
16382
+ active: [],
16383
+ suppressed: {
16384
+ total: 0,
16385
+ byWaiver: {},
14831
16386
  byRule: {}
14832
16387
  }
14833
16388
  };
@@ -15606,7 +17161,7 @@ async function collectChangeTypeSummary(specsRoot) {
15606
17161
  };
15607
17162
  const deltaFiles = await collectDeltaFiles(specsRoot);
15608
17163
  for (const deltaFile of deltaFiles) {
15609
- const text = await (0, import_promises50.readFile)(deltaFile, "utf-8");
17164
+ const text = await (0, import_promises53.readFile)(deltaFile, "utf-8");
15610
17165
  const parsed = parseDeltaV1(text);
15611
17166
  for (const entry of parsed.entries) {
15612
17167
  if (!entry.meta) {
@@ -15643,7 +17198,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
15643
17198
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
15644
17199
  }
15645
17200
  for (const file of specFiles) {
15646
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17201
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15647
17202
  const parsed = parseSpec(text, file);
15648
17203
  const specKey = parsed.specId;
15649
17204
  if (!specKey) {
@@ -15680,7 +17235,7 @@ async function collectIds(files) {
15680
17235
  result[prefix] = /* @__PURE__ */ new Set();
15681
17236
  }
15682
17237
  for (const file of files) {
15683
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17238
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15684
17239
  for (const prefix of ID_PREFIXES) {
15685
17240
  const ids = extractIds(text, prefix);
15686
17241
  ids.forEach((id) => result[prefix].add(id));
@@ -15695,7 +17250,7 @@ async function collectIds(files) {
15695
17250
  async function collectUpstreamIds(files) {
15696
17251
  const ids = /* @__PURE__ */ new Set();
15697
17252
  for (const file of files) {
15698
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17253
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15699
17254
  extractAllIds(text).forEach((id) => ids.add(id));
15700
17255
  }
15701
17256
  return ids;
@@ -15716,7 +17271,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
15716
17271
  }
15717
17272
  const pattern = buildIdPattern(Array.from(upstreamIds));
15718
17273
  for (const file of targetFiles) {
15719
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17274
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15720
17275
  if (pattern.test(text)) {
15721
17276
  return true;
15722
17277
  }
@@ -15835,7 +17390,7 @@ function normalizeScSources(root, sources) {
15835
17390
  async function countScenarios(scenarioFiles) {
15836
17391
  let total = 0;
15837
17392
  for (const file of scenarioFiles) {
15838
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17393
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15839
17394
  const { document, errors } = parseScenarioDocument(text, file);
15840
17395
  if (!document || errors.length > 0) {
15841
17396
  continue;
@@ -15866,7 +17421,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
15866
17421
  let totalScenarios = 0;
15867
17422
  let e2eCount = 0;
15868
17423
  for (const file of scenarioFiles) {
15869
- const text = await (0, import_promises50.readFile)(file, "utf-8");
17424
+ const text = await (0, import_promises53.readFile)(file, "utf-8");
15870
17425
  const { document, errors } = parseScenarioDocument(text, file);
15871
17426
  if (!document || errors.length > 0) {
15872
17427
  continue;
@@ -15954,10 +17509,10 @@ function buildHotspots(issues) {
15954
17509
  async function collectTddCoverage(entries) {
15955
17510
  const specs = [];
15956
17511
  for (const entry of entries) {
15957
- const testCasesPath = import_node_path53.default.join(entry.dir, "06_Test-Cases.md");
17512
+ const testCasesPath = import_node_path56.default.join(entry.dir, "06_Test-Cases.md");
15958
17513
  let tcContent;
15959
17514
  try {
15960
- tcContent = await (0, import_promises50.readFile)(testCasesPath, "utf-8");
17515
+ tcContent = await (0, import_promises53.readFile)(testCasesPath, "utf-8");
15961
17516
  } catch {
15962
17517
  continue;
15963
17518
  }
@@ -15989,10 +17544,10 @@ async function collectTddCoverage(entries) {
15989
17544
  });
15990
17545
  continue;
15991
17546
  }
15992
- const tddListPath = import_node_path53.default.join(entry.dir, "tdd", "test-list.md");
17547
+ const tddListPath = import_node_path56.default.join(entry.dir, "tdd", "test-list.md");
15993
17548
  let tddContent;
15994
17549
  try {
15995
- tddContent = await (0, import_promises50.readFile)(tddListPath, "utf-8");
17550
+ tddContent = await (0, import_promises53.readFile)(tddListPath, "utf-8");
15996
17551
  } catch {
15997
17552
  specs.push({
15998
17553
  specNumber: entry.specNumber,
@@ -16075,8 +17630,8 @@ async function collectTddCoverage(entries) {
16075
17630
  }
16076
17631
 
16077
17632
  // src/core/specPackReport.ts
16078
- var import_promises51 = require("fs/promises");
16079
- var import_node_path54 = __toESM(require("path"), 1);
17633
+ var import_promises54 = require("fs/promises");
17634
+ var import_node_path57 = __toESM(require("path"), 1);
16080
17635
  var REQUIRED_LEDGER_COLUMNS = [
16081
17636
  "trace_id",
16082
17637
  "obj_id",
@@ -16094,9 +17649,9 @@ async function writeSpecPackReports(root, config) {
16094
17649
  const entries = await collectSpecEntries(specsRoot);
16095
17650
  const contractIndex = await buildContractIndex(root, config);
16096
17651
  for (const entry of entries) {
16097
- const specName = import_node_path54.default.basename(entry.dir);
16098
- const outputDir = import_node_path54.default.join(outRoot, specName);
16099
- await (0, import_promises51.mkdir)(outputDir, { recursive: true });
17652
+ const specName = import_node_path57.default.basename(entry.dir);
17653
+ const outputDir = import_node_path57.default.join(outRoot, specName);
17654
+ await (0, import_promises54.mkdir)(outputDir, { recursive: true });
16100
17655
  const [acText, tcText, exText, ledgerText] = await Promise.all([
16101
17656
  readSafe12(entry.acceptanceCriteriaPath),
16102
17657
  readSafe12(entry.testCasesPath),
@@ -16121,14 +17676,14 @@ async function writeSpecPackReports(root, config) {
16121
17676
  contractIds: contractIndex.ids
16122
17677
  });
16123
17678
  const graph = buildTraceabilityGraph(ledgerRows);
16124
- await (0, import_promises51.writeFile)(
16125
- import_node_path54.default.join(outputDir, "coverage.md"),
17679
+ await (0, import_promises54.writeFile)(
17680
+ import_node_path57.default.join(outputDir, "coverage.md"),
16126
17681
  `${formatCoverageMarkdown(specName, coverage)}
16127
17682
  `,
16128
17683
  "utf-8"
16129
17684
  );
16130
- await (0, import_promises51.writeFile)(
16131
- import_node_path54.default.join(outputDir, "traceability-graph.json"),
17685
+ await (0, import_promises54.writeFile)(
17686
+ import_node_path57.default.join(outputDir, "traceability-graph.json"),
16132
17687
  `${JSON.stringify(graph, null, 2)}
16133
17688
  `,
16134
17689
  "utf-8"
@@ -16303,7 +17858,7 @@ function getCell(row, indexByColumn, column) {
16303
17858
  }
16304
17859
  async function readSafe12(filePath) {
16305
17860
  try {
16306
- return await (0, import_promises51.readFile)(filePath, "utf-8");
17861
+ return await (0, import_promises54.readFile)(filePath, "utf-8");
16307
17862
  } catch {
16308
17863
  return "";
16309
17864
  }
@@ -16321,7 +17876,7 @@ function warnIfTruncated(scan, context) {
16321
17876
 
16322
17877
  // src/cli/commands/report.ts
16323
17878
  async function runReport(options) {
16324
- const root = import_node_path55.default.resolve(options.root);
17879
+ const root = import_node_path58.default.resolve(options.root);
16325
17880
  const configResult = await loadConfig(root);
16326
17881
  let validation;
16327
17882
  let blockedByPhaseGuard = false;
@@ -16337,7 +17892,7 @@ async function runReport(options) {
16337
17892
  validation = normalized;
16338
17893
  } else {
16339
17894
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
16340
- const inputPath = import_node_path55.default.isAbsolute(input) ? input : import_node_path55.default.resolve(root, input);
17895
+ const inputPath = import_node_path58.default.isAbsolute(input) ? input : import_node_path58.default.resolve(root, input);
16341
17896
  try {
16342
17897
  validation = await readValidationResult(inputPath);
16343
17898
  } catch (err) {
@@ -16364,11 +17919,11 @@ async function runReport(options) {
16364
17919
  warnIfTruncated(data.traceability.testFiles, "report");
16365
17920
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
16366
17921
  const outRoot = resolvePath(root, configResult.config, "outDir");
16367
- const defaultOut = options.format === "json" ? import_node_path55.default.join(outRoot, "report.json") : import_node_path55.default.join(outRoot, "report.md");
17922
+ const defaultOut = options.format === "json" ? import_node_path58.default.join(outRoot, "report.json") : import_node_path58.default.join(outRoot, "report.md");
16368
17923
  const out = options.outPath ?? defaultOut;
16369
- const outPath = import_node_path55.default.isAbsolute(out) ? out : import_node_path55.default.resolve(root, out);
16370
- await (0, import_promises52.mkdir)(import_node_path55.default.dirname(outPath), { recursive: true });
16371
- await (0, import_promises52.writeFile)(outPath, `${output}
17924
+ const outPath = import_node_path58.default.isAbsolute(out) ? out : import_node_path58.default.resolve(root, out);
17925
+ await (0, import_promises55.mkdir)(import_node_path58.default.dirname(outPath), { recursive: true });
17926
+ await (0, import_promises55.writeFile)(outPath, `${output}
16372
17927
  `, "utf-8");
16373
17928
  await writeSpecPackReports(root, configResult.config);
16374
17929
  if (blockedByPhaseGuard) {
@@ -16383,7 +17938,7 @@ async function runReport(options) {
16383
17938
  info(`wrote report: ${outPath}`);
16384
17939
  }
16385
17940
  async function readValidationResult(inputPath) {
16386
- const raw = await (0, import_promises52.readFile)(inputPath, "utf-8");
17941
+ const raw = await (0, import_promises55.readFile)(inputPath, "utf-8");
16387
17942
  const parsed = JSON.parse(raw);
16388
17943
  if (!isValidationResult(parsed)) {
16389
17944
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -16443,23 +17998,23 @@ function isMissingFileError2(error2) {
16443
17998
  return record2.code === "ENOENT";
16444
17999
  }
16445
18000
  async function writeValidationResult(root, outputPath, result) {
16446
- const abs = import_node_path55.default.isAbsolute(outputPath) ? outputPath : import_node_path55.default.resolve(root, outputPath);
16447
- await (0, import_promises52.mkdir)(import_node_path55.default.dirname(abs), { recursive: true });
16448
- await (0, import_promises52.writeFile)(abs, `${JSON.stringify(result, null, 2)}
18001
+ const abs = import_node_path58.default.isAbsolute(outputPath) ? outputPath : import_node_path58.default.resolve(root, outputPath);
18002
+ await (0, import_promises55.mkdir)(import_node_path58.default.dirname(abs), { recursive: true });
18003
+ await (0, import_promises55.writeFile)(abs, `${JSON.stringify(result, null, 2)}
16449
18004
  `, "utf-8");
16450
18005
  }
16451
18006
 
16452
18007
  // src/cli/commands/validate.ts
16453
- var import_promises54 = require("fs/promises");
16454
- var import_node_path57 = __toESM(require("path"), 1);
18008
+ var import_promises57 = require("fs/promises");
18009
+ var import_node_path60 = __toESM(require("path"), 1);
16455
18010
 
16456
18011
  // src/core/runLog.ts
16457
- var import_promises53 = require("fs/promises");
16458
- var import_node_path56 = __toESM(require("path"), 1);
18012
+ var import_promises56 = require("fs/promises");
18013
+ var import_node_path59 = __toESM(require("path"), 1);
16459
18014
  async function writeValidateRunLog(input) {
16460
- const root = import_node_path56.default.resolve(input.root);
18015
+ const root = import_node_path59.default.resolve(input.root);
16461
18016
  const outDir = resolvePath(root, input.config, "outDir");
16462
- await (0, import_promises53.mkdir)(outDir, { recursive: true });
18017
+ await (0, import_promises56.mkdir)(outDir, { recursive: true });
16463
18018
  const { runId, reportDir } = await allocateRunReportDir(outDir, input.startedAt);
16464
18019
  const relativeSpecsRoot = toRelativePath(root, resolvePath(root, input.config, "specsDir"));
16465
18020
  const latestDiscussion = await findLatestPack(
@@ -16504,10 +18059,10 @@ async function writeValidateRunLog(input) {
16504
18059
  errors,
16505
18060
  warnings
16506
18061
  });
16507
- await writeJson(import_node_path56.default.join(reportDir, "run.json"), runJson);
16508
- await writeJson(import_node_path56.default.join(reportDir, "validator.json"), validatorJson);
16509
- await writeJson(import_node_path56.default.join(reportDir, "traceability.json"), traceabilityJson);
16510
- await (0, import_promises53.writeFile)(import_node_path56.default.join(reportDir, "summary.md"), `${summaryMd}
18062
+ await writeJson(import_node_path59.default.join(reportDir, "run.json"), runJson);
18063
+ await writeJson(import_node_path59.default.join(reportDir, "validator.json"), validatorJson);
18064
+ await writeJson(import_node_path59.default.join(reportDir, "traceability.json"), traceabilityJson);
18065
+ await (0, import_promises56.writeFile)(import_node_path59.default.join(reportDir, "summary.md"), `${summaryMd}
16511
18066
  `, "utf-8");
16512
18067
  return {
16513
18068
  runId,
@@ -16515,7 +18070,7 @@ async function writeValidateRunLog(input) {
16515
18070
  };
16516
18071
  }
16517
18072
  async function writeJson(filePath, value) {
16518
- await (0, import_promises53.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
18073
+ await (0, import_promises56.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
16519
18074
  `, "utf-8");
16520
18075
  }
16521
18076
  function resolveStatus(result, override) {
@@ -16631,9 +18186,9 @@ async function allocateRunReportDir(outDir, startedAt) {
16631
18186
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
16632
18187
  const candidateDate = new Date(startedAt.getTime() + attempt);
16633
18188
  const runId = `run-${formatTimestamp17(candidateDate)}`;
16634
- const reportDir = import_node_path56.default.join(outDir, runId);
18189
+ const reportDir = import_node_path59.default.join(outDir, runId);
16635
18190
  try {
16636
- await (0, import_promises53.mkdir)(reportDir);
18191
+ await (0, import_promises56.mkdir)(reportDir);
16637
18192
  return { runId, reportDir };
16638
18193
  } catch (error2) {
16639
18194
  if (isAlreadyExistsError(error2)) {
@@ -16662,7 +18217,7 @@ function shouldFail(result, failOn) {
16662
18217
  // src/cli/commands/validate.ts
16663
18218
  async function runValidate(options) {
16664
18219
  const startedAt = /* @__PURE__ */ new Date();
16665
- const root = import_node_path57.default.resolve(options.root);
18220
+ const root = import_node_path60.default.resolve(options.root);
16666
18221
  const configResult = await loadConfig(root);
16667
18222
  const blockedIssue = buildCiRefinementIssue(options.phase);
16668
18223
  const blockedByPhaseGuard = blockedIssue !== null;
@@ -16818,12 +18373,12 @@ function issueKey(issue2) {
16818
18373
  }
16819
18374
  async function emitJson(result, root, jsonPath) {
16820
18375
  const abs = resolveJsonPath(root, jsonPath);
16821
- await (0, import_promises54.mkdir)(import_node_path57.default.dirname(abs), { recursive: true });
16822
- await (0, import_promises54.writeFile)(abs, `${JSON.stringify(result, null, 2)}
18376
+ await (0, import_promises57.mkdir)(import_node_path60.default.dirname(abs), { recursive: true });
18377
+ await (0, import_promises57.writeFile)(abs, `${JSON.stringify(result, null, 2)}
16823
18378
  `, "utf-8");
16824
18379
  }
16825
18380
  function resolveJsonPath(root, jsonPath) {
16826
- return import_node_path57.default.isAbsolute(jsonPath) ? jsonPath : import_node_path57.default.resolve(root, jsonPath);
18381
+ return import_node_path60.default.isAbsolute(jsonPath) ? jsonPath : import_node_path60.default.resolve(root, jsonPath);
16827
18382
  }
16828
18383
  var GITHUB_ANNOTATION_LIMIT = 100;
16829
18384
  var ISSUE_EXPECTED_BY_CODE = {