qfai 1.4.36 → 1.4.38

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.
@@ -31,6 +31,14 @@ The contract must describe both screen structure and minimum mockable behavior.
31
31
  3. `.qfai/evidence/prototyping.json` (`uiFidelity` snapshot)
32
32
  - If label text is intentionally hidden (icon-only, aria-only), add stable `data-qfai` markers and document mapping in the contract comments.
33
33
 
34
+ ### `data-qfai` marker convention
35
+
36
+ - Recommended marker value: `CONTRACT_ID:ELEMENT_ID` (example: `data-qfai="CON-UI-0001:search_input"`).
37
+ - Use `elements[].id` (stable ID) for the marker suffix, not `elements[].label`.
38
+ - Even when label text is not visible in the UI, markers ensure fidelity coverage.
39
+ - autogen generates expected markers from `elements[].id` automatically.
40
+ - Legacy format (`CONTRACT_ID:ELEMENT_LABEL`) is accepted for backward compatibility but new implementations should use the id-based format.
41
+
34
42
  ## Mockable prototype minimum (L2)
35
43
 
36
44
  Add `prototype` at the top level.
@@ -1589,8 +1589,8 @@ var import_promises7 = require("fs/promises");
1589
1589
  var import_node_path8 = __toESM(require("path"), 1);
1590
1590
  var import_node_url2 = require("url");
1591
1591
  async function resolveToolVersion() {
1592
- if ("1.4.36".length > 0) {
1593
- return "1.4.36";
1592
+ if ("1.4.38".length > 0) {
1593
+ return "1.4.38";
1594
1594
  }
1595
1595
  try {
1596
1596
  const packagePath = resolvePackageJsonPath();
@@ -3541,6 +3541,7 @@ async function crawlRoutesAndCollectFoundLabels(baseUrl, routes) {
3541
3541
  status: "failed",
3542
3542
  httpStatus: null,
3543
3543
  labels: [],
3544
+ markers: [],
3544
3545
  error: `invalid baseUrl or route: baseUrl=${baseUrl}, route=${route}`
3545
3546
  });
3546
3547
  continue;
@@ -3556,11 +3557,13 @@ async function crawlRoutesAndCollectFoundLabels(baseUrl, routes) {
3556
3557
  clearTimeout(timeoutId);
3557
3558
  const html = await response.text();
3558
3559
  const labels = extractDomLabels(html);
3560
+ const markers = extractDomMarkers(html);
3559
3561
  results.push({
3560
3562
  route,
3561
3563
  status: response.ok ? "ok" : "failed",
3562
3564
  httpStatus: response.status,
3563
3565
  labels,
3566
+ markers,
3564
3567
  ...response.ok ? {} : { error: `http status ${response.status} for ${targetUrl}` }
3565
3568
  });
3566
3569
  } finally {
@@ -3572,6 +3575,7 @@ async function crawlRoutesAndCollectFoundLabels(baseUrl, routes) {
3572
3575
  status: "failed",
3573
3576
  httpStatus: null,
3574
3577
  labels: [],
3578
+ markers: [],
3575
3579
  error: formatError4(error2)
3576
3580
  });
3577
3581
  }
@@ -3644,19 +3648,46 @@ function buildUiFidelityScreens(expectedScreens, crawledRoutes, mockPathResults)
3644
3648
  const hasPass = mockPath?.entries.some(
3645
3649
  (entry) => entry.status.toLowerCase() === "pass"
3646
3650
  ) ?? false;
3651
+ const expectedMarkers = screen.elementIds.map(
3652
+ (id) => `${screen.uiContractId}:${id}`
3653
+ );
3654
+ const crawledMarkersSet = new Set(crawl?.markers ?? []);
3655
+ const labelToIdMarker = /* @__PURE__ */ new Map();
3656
+ for (const pair of screen.elementPairs) {
3657
+ const labelMarker = `${screen.uiContractId}:${pair.label}`;
3658
+ const idMarker = `${screen.uiContractId}:${pair.id}`;
3659
+ if (labelMarker !== idMarker) {
3660
+ labelToIdMarker.set(labelMarker, idMarker);
3661
+ }
3662
+ }
3663
+ const foundMarkers = expectedMarkers.filter((marker) => {
3664
+ if (crawledMarkersSet.has(marker)) return true;
3665
+ for (const [labelMarker, idMarker] of labelToIdMarker) {
3666
+ if (idMarker === marker && crawledMarkersSet.has(labelMarker)) {
3667
+ return true;
3668
+ }
3669
+ }
3670
+ return false;
3671
+ });
3672
+ const missingMarkers = expectedMarkers.filter(
3673
+ (marker) => !foundMarkers.includes(marker)
3674
+ );
3647
3675
  return {
3648
3676
  route: screen.route,
3649
3677
  uiContractId: screen.uiContractId,
3650
3678
  expected: {
3651
3679
  elements: screen.elementCount,
3652
3680
  actions: screen.actionIds.length,
3653
- labels: screen.labels
3681
+ labels: screen.labels,
3682
+ ids: screen.elementIds
3654
3683
  },
3655
3684
  found: {
3656
- labels: coverage.found
3685
+ labels: coverage.found,
3686
+ markers: foundMarkers
3657
3687
  },
3658
3688
  missing: {
3659
- labels: coverage.missing
3689
+ labels: coverage.missing,
3690
+ markers: missingMarkers
3660
3691
  },
3661
3692
  coverage: coverage.coverage,
3662
3693
  observed: {
@@ -3768,6 +3799,8 @@ function collectContractScreens(contractId, doc, mockPathIds) {
3768
3799
  route,
3769
3800
  uiContractId: contractId,
3770
3801
  expectedLabels: elements.labels,
3802
+ elementIds: elements.ids,
3803
+ elementPairs: elements.pairs,
3771
3804
  elementCount: elements.count,
3772
3805
  actionIds,
3773
3806
  mockPathIds,
@@ -3779,8 +3812,15 @@ function collectContractScreens(contractId, doc, mockPathIds) {
3779
3812
  function collectElements(value) {
3780
3813
  const entries = Array.isArray(value) ? value : [];
3781
3814
  const validEntries = entries.map((entry) => readRecord(entry)).filter((entry) => Boolean(entry));
3815
+ const ids = validEntries.map((entry) => readText(entry.id)).filter((id) => id.length > 0);
3782
3816
  const labels = validEntries.map((entry) => readText(entry.label)).filter((label) => label.length > 0);
3783
- return { labels: dedupeLabels(labels), count: validEntries.length };
3817
+ const pairs = validEntries.map((entry) => ({ id: readText(entry.id), label: readText(entry.label) })).filter((pair) => pair.id.length > 0 && pair.label.length > 0);
3818
+ return {
3819
+ ids: Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b)),
3820
+ labels: dedupeLabels(labels),
3821
+ pairs,
3822
+ count: validEntries.length
3823
+ };
3784
3824
  }
3785
3825
  function collectActionIds(value) {
3786
3826
  const entries = Array.isArray(value) ? value : [];
@@ -3826,6 +3866,10 @@ function dedupeExpectedScreens(screens) {
3826
3866
  route: screen.route,
3827
3867
  uiContractId: screen.uiContractId,
3828
3868
  labels: dedupeLabels(screen.expectedLabels),
3869
+ elementIds: Array.from(new Set(screen.elementIds)).sort(
3870
+ (a, b) => a.localeCompare(b)
3871
+ ),
3872
+ elementPairs: [...screen.elementPairs],
3829
3873
  elementCount: screen.elementCount,
3830
3874
  actionIds: Array.from(new Set(screen.actionIds)).sort(
3831
3875
  (a, b) => a.localeCompare(b)
@@ -3843,6 +3887,16 @@ function dedupeExpectedScreens(screens) {
3843
3887
  ...current.labels,
3844
3888
  ...screen.expectedLabels
3845
3889
  ]);
3890
+ current.elementIds = Array.from(
3891
+ /* @__PURE__ */ new Set([...current.elementIds, ...screen.elementIds])
3892
+ ).sort((a, b) => a.localeCompare(b));
3893
+ const existingIds = new Set(current.elementPairs.map((p) => p.id));
3894
+ for (const pair of screen.elementPairs) {
3895
+ if (!existingIds.has(pair.id)) {
3896
+ current.elementPairs.push(pair);
3897
+ existingIds.add(pair.id);
3898
+ }
3899
+ }
3846
3900
  current.elementCount += screen.elementCount;
3847
3901
  current.actionIds = Array.from(
3848
3902
  /* @__PURE__ */ new Set([...current.actionIds, ...screen.actionIds])
@@ -3899,16 +3953,31 @@ function extractDomLabels(html) {
3899
3953
  }
3900
3954
  }
3901
3955
  }
3902
- const rawBodyText = document.body?.textContent ?? "";
3903
- const bodyTokens = rawBodyText.split(/\s{2,}|\n+/);
3904
- for (const token of bodyTokens) {
3905
- const trimmed = normalizeDomLabel(token);
3906
- if (trimmed) {
3907
- labels.push(trimmed);
3956
+ if (process.env.QFAI_AUTOGEN_BODY_TOKENS === "1") {
3957
+ const rawBodyText = document.body?.textContent ?? "";
3958
+ const bodyTokens = rawBodyText.split(/\s{2,}|\n+/);
3959
+ for (const token of bodyTokens) {
3960
+ const trimmed = normalizeDomLabel(token);
3961
+ if (trimmed) {
3962
+ labels.push(trimmed);
3963
+ }
3908
3964
  }
3909
3965
  }
3910
3966
  return dedupeLabels(labels);
3911
3967
  }
3968
+ function extractDomMarkers(html) {
3969
+ const dom = new import_jsdom.JSDOM(html);
3970
+ const document = dom.window.document;
3971
+ const markers = [];
3972
+ for (const element of document.querySelectorAll("[data-qfai]")) {
3973
+ const value = element.getAttribute("data-qfai") ?? "";
3974
+ const trimmed = value.trim();
3975
+ if (trimmed.length > 0) {
3976
+ markers.push(trimmed);
3977
+ }
3978
+ }
3979
+ return Array.from(new Set(markers)).sort((a, b) => a.localeCompare(b));
3980
+ }
3912
3981
  function normalizeDomLabel(value) {
3913
3982
  const normalized = value.replace(/\s+/g, " ").trim();
3914
3983
  if (normalized.length === 0) {
@@ -3926,7 +3995,7 @@ function hasLabelMatch(foundLabels, expectedLabel) {
3926
3995
  }
3927
3996
  return foundLabels.some((label) => {
3928
3997
  const normalizedFound = normalizeComparableText(label);
3929
- return normalizedFound === normalizedExpected || normalizedFound.includes(normalizedExpected) || normalizedExpected.includes(normalizedFound);
3998
+ return normalizedFound === normalizedExpected;
3930
3999
  });
3931
4000
  }
3932
4001
  function normalizeComparableText(value) {
@@ -3992,9 +4061,35 @@ var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
3992
4061
  async function runPrototyping(options) {
3993
4062
  const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
3994
4063
  if (!autogenEnabled) {
4064
+ if (options.autogenOnly) {
4065
+ error(
4066
+ `prototyping: --autogen-only \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u3059\u304C --autogen-ui-fidelity / ${ENV_AUTOGEN}=1 \u304C\u3042\u308A\u307E\u305B\u3093\u3002`
4067
+ );
4068
+ return 2;
4069
+ }
3995
4070
  info(
3996
4071
  `prototyping: --autogen-ui-fidelity or ${ENV_AUTOGEN}=1 \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u4F55\u3082\u5B9F\u884C\u3057\u307E\u305B\u3093\u3002`
3997
4072
  );
4073
+ const evidencePath2 = resolveEvidencePath(options.root, options.evidenceOut);
4074
+ const toolVersion2 = await resolveToolVersion();
4075
+ let existingEvidence2 = {};
4076
+ try {
4077
+ const raw = await (0, import_promises14.readFile)(evidencePath2, "utf-8");
4078
+ const parsed = JSON.parse(raw);
4079
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4080
+ existingEvidence2 = parsed;
4081
+ }
4082
+ } catch {
4083
+ }
4084
+ const skippedEvidence = emitUiFidelity({
4085
+ evidence: existingEvidence2,
4086
+ toolVersion: toolVersion2,
4087
+ command: "qfai prototyping",
4088
+ baseUrl: "",
4089
+ status: "skipped",
4090
+ reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
4091
+ });
4092
+ await writeEvidence(evidencePath2, skippedEvidence);
3998
4093
  return 0;
3999
4094
  }
4000
4095
  const baseUrl = resolveBaseUrl(options);
@@ -4017,9 +4112,15 @@ async function runPrototyping(options) {
4017
4112
  }
4018
4113
  } catch {
4019
4114
  }
4115
+ const routeHints = extractRouteHintsFromEvidence(existingEvidence);
4020
4116
  let result;
4021
4117
  try {
4022
- result = await autogenerateUiFidelity(options.root, config, baseUrl, []);
4118
+ result = await autogenerateUiFidelity(
4119
+ options.root,
4120
+ config,
4121
+ baseUrl,
4122
+ routeHints
4123
+ );
4023
4124
  } catch (err) {
4024
4125
  const reason = err instanceof Error ? err.message : String(err);
4025
4126
  warn(`prototyping: autogen failed - ${reason}`);
@@ -4098,6 +4199,45 @@ async function writeEvidence(filePath, evidence) {
4098
4199
  await (0, import_promises14.mkdir)(import_node_path17.default.dirname(filePath), { recursive: true });
4099
4200
  await (0, import_promises14.writeFile)(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4100
4201
  }
4202
+ function extractRouteHintsFromEvidence(evidence) {
4203
+ const routes = /* @__PURE__ */ new Set();
4204
+ const runtimeGate = evidence.runtimeGate;
4205
+ if (runtimeGate && typeof runtimeGate === "object" && !Array.isArray(runtimeGate)) {
4206
+ const gate = runtimeGate;
4207
+ const uiRows = gate.ui;
4208
+ if (Array.isArray(uiRows)) {
4209
+ for (const row of uiRows) {
4210
+ if (row && typeof row === "object" && !Array.isArray(row)) {
4211
+ const r = row;
4212
+ if (typeof r.route === "string" && r.route.trim().length > 0) {
4213
+ routes.add(r.route.trim());
4214
+ }
4215
+ }
4216
+ }
4217
+ }
4218
+ }
4219
+ const specs = evidence.specs;
4220
+ if (Array.isArray(specs)) {
4221
+ for (const spec of specs) {
4222
+ if (spec && typeof spec === "object" && !Array.isArray(spec)) {
4223
+ const s = spec;
4224
+ const missing = s.missing;
4225
+ if (missing && typeof missing === "object" && !Array.isArray(missing)) {
4226
+ const m = missing;
4227
+ const uiRoutes = m.uiRoutes;
4228
+ if (Array.isArray(uiRoutes)) {
4229
+ for (const route of uiRoutes) {
4230
+ if (typeof route === "string" && route.trim().length > 0) {
4231
+ routes.add(route.trim());
4232
+ }
4233
+ }
4234
+ }
4235
+ }
4236
+ }
4237
+ }
4238
+ }
4239
+ return Array.from(routes).sort((a, b) => a.localeCompare(b));
4240
+ }
4101
4241
 
4102
4242
  // src/cli/commands/report.ts
4103
4243
  var import_promises42 = require("fs/promises");
@@ -11577,6 +11717,83 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
11577
11717
  )
11578
11718
  );
11579
11719
  }
11720
+ const screensWithMissingLabels = uiFidelity.screens.filter(
11721
+ (screen) => screen.expected.labels && screen.expected.labels.length > 0 && screen.missing?.labels && screen.missing.labels.length > 0
11722
+ );
11723
+ if (screensWithMissingLabels.length > 0) {
11724
+ const details = screensWithMissingLabels.map((screen) => {
11725
+ const missing = screen.missing?.labels ?? [];
11726
+ return `${screen.route}:${screen.uiContractId}(missing_labels=${missing.join("|")})`;
11727
+ }).sort((a, b) => a.localeCompare(b));
11728
+ const refs = collectLabelMismatchRefs(screensWithMissingLabels);
11729
+ issues.push(
11730
+ issue(
11731
+ "QFAI-PROT-241",
11732
+ `QFAI-PROT-241: uiFidelity screens have missing labels. ${details.join("; ")}`,
11733
+ "error",
11734
+ evidenceJsonPath,
11735
+ "prototypingEvidence.uiFidelityMissingLabels",
11736
+ refs,
11737
+ "change",
11738
+ [
11739
+ "contracts/ui \u306E elements[].label \u3092\u753B\u9762\u306B\u3059\u3079\u3066\u63CF\u753B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
11740
+ "\u63CF\u753B\u304C\u96E3\u3057\u3044\u8981\u7D20\u306F data-qfai \u30DE\u30FC\u30AB\u30FC\u3067\u4EE3\u66FF\u3057\u3001autogen \u3092\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
11741
+ ].join("\n")
11742
+ )
11743
+ );
11744
+ }
11745
+ const screensWithMissingMarkers = uiFidelity.screens.filter(
11746
+ (screen) => screen.expected.elements > 0 && screen.missing?.markers && screen.missing.markers.length > 0
11747
+ );
11748
+ if (screensWithMissingMarkers.length > 0) {
11749
+ const details = screensWithMissingMarkers.map((screen) => {
11750
+ const missing = screen.missing?.markers ?? [];
11751
+ return `${screen.route}:${screen.uiContractId}(missing_markers=${missing.join("|")})`;
11752
+ }).sort((a, b) => a.localeCompare(b));
11753
+ const refs = collectMarkerMismatchRefs(screensWithMissingMarkers);
11754
+ issues.push(
11755
+ issue(
11756
+ "QFAI-PROT-242",
11757
+ `QFAI-PROT-242: uiFidelity screens have missing markers. ${details.join("; ")}`,
11758
+ "error",
11759
+ evidenceJsonPath,
11760
+ "prototypingEvidence.uiFidelityMissingMarkers",
11761
+ refs,
11762
+ "change",
11763
+ [
11764
+ '\u753B\u9762\u306E\u5404\u8981\u7D20\u306B data-qfai="CONTRACT_ID:ELEMENT_ID" \u30DE\u30FC\u30AB\u30FC\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002',
11765
+ '\u30DE\u30FC\u30AB\u30FC\u306E\u5024\u306F contracts/ui \u306E elements[].id \u3092\u4F7F\u3044\u307E\u3059\uFF08\u4F8B: data-qfai="CON-UI-0001:search_input"\uFF09\u3002',
11766
+ "autogen \u3092\u518D\u5B9F\u884C\u3057\u3001missing.markers \u304C\u7A7A\u306B\u306A\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
11767
+ ].join("\n")
11768
+ )
11769
+ );
11770
+ }
11771
+ const placeholderScreens = uiFidelity.screens.filter((screen) => {
11772
+ if (!screen.found?.labels) return false;
11773
+ const expectedElements = screen.expected.elements;
11774
+ const observedElements = screen.observed.elementsPlaced;
11775
+ const foundLabels = screen.found.labels.length;
11776
+ return expectedElements > 2 && observedElements <= 1 && foundLabels <= 1;
11777
+ });
11778
+ if (placeholderScreens.length > 0) {
11779
+ const details = placeholderScreens.map(
11780
+ (screen) => `${screen.route}:${screen.uiContractId}(expected=${screen.expected.elements},observed=${screen.observed.elementsPlaced})`
11781
+ ).sort((a, b) => a.localeCompare(b));
11782
+ issues.push(
11783
+ issue(
11784
+ "QFAI-PROT-243",
11785
+ `QFAI-PROT-243: placeholder/single-text pages detected. ${details.join("; ")}`,
11786
+ "warning",
11787
+ evidenceJsonPath,
11788
+ "prototypingEvidence.placeholderPages",
11789
+ placeholderScreens.map(
11790
+ (screen) => `${screen.uiContractId}|${screen.route}`
11791
+ ),
11792
+ "change",
11793
+ "\u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u30DA\u30FC\u30B8\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002contracts/ui \u306E\u5168\u8981\u7D20\u3092\u753B\u9762\u306B\u914D\u7F6E\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
11794
+ )
11795
+ );
11796
+ }
11580
11797
  const hasMockPaths = uiFidelity.screens.some(
11581
11798
  (screen) => screen.mockPaths.length > 0
11582
11799
  );
@@ -11647,6 +11864,40 @@ function collectUiFidelityMismatchRefs(mismatches) {
11647
11864
  }
11648
11865
  return Array.from(refs).sort((left, right) => left.localeCompare(right));
11649
11866
  }
11867
+ function collectLabelMismatchRefs(screens) {
11868
+ const refs = /* @__PURE__ */ new Set();
11869
+ for (const screen of screens) {
11870
+ refs.add(`contract_id=${screen.uiContractId}`);
11871
+ refs.add(`route=${screen.route}`);
11872
+ refs.add(`contract_route=${screen.uiContractId}|${screen.route}`);
11873
+ const missingLabels = screen.missing?.labels ?? [];
11874
+ if (missingLabels.length > 0) {
11875
+ const labels = missingLabels.sort((a, b) => a.localeCompare(b)).join("|");
11876
+ refs.add(`missing_labels=${labels}`);
11877
+ refs.add(
11878
+ `missing_labels_by_contract_route=${screen.uiContractId}|${screen.route}:${labels}`
11879
+ );
11880
+ }
11881
+ }
11882
+ return Array.from(refs).sort((l, r) => l.localeCompare(r));
11883
+ }
11884
+ function collectMarkerMismatchRefs(screens) {
11885
+ const refs = /* @__PURE__ */ new Set();
11886
+ for (const screen of screens) {
11887
+ refs.add(`contract_id=${screen.uiContractId}`);
11888
+ refs.add(`route=${screen.route}`);
11889
+ refs.add(`contract_route=${screen.uiContractId}|${screen.route}`);
11890
+ const missingMarkers = screen.missing?.markers ?? [];
11891
+ if (missingMarkers.length > 0) {
11892
+ const markers = missingMarkers.sort((a, b) => a.localeCompare(b)).join("|");
11893
+ refs.add(`missing_markers=${markers}`);
11894
+ refs.add(
11895
+ `missing_markers_by_contract_route=${screen.uiContractId}|${screen.route}:${markers}`
11896
+ );
11897
+ }
11898
+ }
11899
+ return Array.from(refs).sort((l, r) => l.localeCompare(r));
11900
+ }
11650
11901
  async function collectUiContractScreens(contractIndex) {
11651
11902
  const result = /* @__PURE__ */ new Map();
11652
11903
  for (const [contractId, fileSet] of contractIndex.idToFiles.entries()) {
@@ -11799,6 +12050,9 @@ function normalizeUiFidelityScreen(value) {
11799
12050
  route: value.route.trim(),
11800
12051
  uiContractId: value.uiContractId.trim().toUpperCase(),
11801
12052
  expected: expected.value,
12053
+ ...normalizeOptionalFoundBlock(value.found),
12054
+ ...normalizeOptionalMissingBlock(value.missing),
12055
+ ...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
11802
12056
  observed: observed.value,
11803
12057
  mockPaths: mockPaths.value
11804
12058
  }
@@ -11817,11 +12071,15 @@ function normalizeUiFidelityExpected(value) {
11817
12071
  reason: "`uiFidelity.screens[].expected` requires non-negative integers for elements/actions"
11818
12072
  };
11819
12073
  }
12074
+ const labels = toOptionalStringArray(value.labels);
12075
+ const ids = toOptionalStringArray(value.ids);
11820
12076
  return {
11821
12077
  ok: true,
11822
12078
  value: {
11823
12079
  elements: value.elements,
11824
- actions: value.actions
12080
+ actions: value.actions,
12081
+ ...labels ? { labels } : {},
12082
+ ...ids ? { ids } : {}
11825
12083
  }
11826
12084
  };
11827
12085
  }
@@ -11979,6 +12237,47 @@ function toStringArray2(value) {
11979
12237
  }
11980
12238
  return value.map((item) => item.trim()).filter((item) => item.length > 0);
11981
12239
  }
12240
+ function toOptionalStringArray(value) {
12241
+ if (value === void 0 || value === null) {
12242
+ return void 0;
12243
+ }
12244
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
12245
+ return void 0;
12246
+ }
12247
+ return value.map((item) => item.trim()).filter((item) => item.length > 0);
12248
+ }
12249
+ function normalizeOptionalFoundBlock(value) {
12250
+ if (!isRecord5(value)) {
12251
+ return {};
12252
+ }
12253
+ const labels = toOptionalStringArray(value.labels);
12254
+ const markers = toOptionalStringArray(value.markers);
12255
+ if (!labels && !markers) {
12256
+ return {};
12257
+ }
12258
+ return {
12259
+ found: {
12260
+ ...labels ? { labels } : {},
12261
+ ...markers ? { markers } : {}
12262
+ }
12263
+ };
12264
+ }
12265
+ function normalizeOptionalMissingBlock(value) {
12266
+ if (!isRecord5(value)) {
12267
+ return {};
12268
+ }
12269
+ const labels = toOptionalStringArray(value.labels);
12270
+ const markers = toOptionalStringArray(value.markers);
12271
+ if (!labels && !markers) {
12272
+ return {};
12273
+ }
12274
+ return {
12275
+ missing: {
12276
+ ...labels ? { labels } : {},
12277
+ ...markers ? { markers } : {}
12278
+ }
12279
+ };
12280
+ }
11982
12281
  function isRecord5(value) {
11983
12282
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11984
12283
  }
@@ -14728,7 +15027,10 @@ var ISSUE_EXPECTED_BY_CODE = {
14728
15027
  "QFAI-PROT-114": "Per-spec DB checks satisfy declared object counts and leave no unresolved DB objects.",
14729
15028
  "QFAI-PROT-231": "Interactive prototyping evidence includes uiFidelity with required screen-level fields.",
14730
15029
  "QFAI-PROT-232": "uiFidelity screen observations satisfy referenced UI contract elements/actions coverage.",
14731
- "QFAI-PROT-233": "Interactive uiFidelity records at least one mockPaths status=pass entry (warning in v1.4.36).",
15030
+ "QFAI-PROT-233": "Interactive uiFidelity records at least one mockPaths status=pass entry (warning in v1.4.37).",
15031
+ "QFAI-PROT-241": "uiFidelity screens must have no missing labels when expected.labels is present.",
15032
+ "QFAI-PROT-242": "uiFidelity screens must have no missing markers when expected.elements > 0 and markers are tracked.",
15033
+ "QFAI-PROT-243": "Placeholder/single-text pages are detected when expected elements > 2, observed <= 1, and found.labels <= 1 (warning in v1.4.37).",
14732
15034
  "QFAI-CONTRACT-030": "Contract index references must match declared contract IDs in .qfai/contracts/**."
14733
15035
  };
14734
15036
  function resolveIssueTarget(issue2) {