qfai 1.4.36 → 1.4.37

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.
@@ -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.37".length > 0) {
1593
+ return "1.4.37";
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,6 +3648,16 @@ 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.labels.map(
3652
+ (label) => `${screen.uiContractId}:${label}`
3653
+ );
3654
+ const crawledMarkers = crawl?.markers ?? [];
3655
+ const foundMarkers = expectedMarkers.filter(
3656
+ (marker) => crawledMarkers.includes(marker)
3657
+ );
3658
+ const missingMarkers = expectedMarkers.filter(
3659
+ (marker) => !crawledMarkers.includes(marker)
3660
+ );
3647
3661
  return {
3648
3662
  route: screen.route,
3649
3663
  uiContractId: screen.uiContractId,
@@ -3653,10 +3667,12 @@ function buildUiFidelityScreens(expectedScreens, crawledRoutes, mockPathResults)
3653
3667
  labels: screen.labels
3654
3668
  },
3655
3669
  found: {
3656
- labels: coverage.found
3670
+ labels: coverage.found,
3671
+ markers: foundMarkers
3657
3672
  },
3658
3673
  missing: {
3659
- labels: coverage.missing
3674
+ labels: coverage.missing,
3675
+ markers: missingMarkers
3660
3676
  },
3661
3677
  coverage: coverage.coverage,
3662
3678
  observed: {
@@ -3899,16 +3915,31 @@ function extractDomLabels(html) {
3899
3915
  }
3900
3916
  }
3901
3917
  }
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);
3918
+ if (process.env.QFAI_AUTOGEN_BODY_TOKENS === "1") {
3919
+ const rawBodyText = document.body?.textContent ?? "";
3920
+ const bodyTokens = rawBodyText.split(/\s{2,}|\n+/);
3921
+ for (const token of bodyTokens) {
3922
+ const trimmed = normalizeDomLabel(token);
3923
+ if (trimmed) {
3924
+ labels.push(trimmed);
3925
+ }
3908
3926
  }
3909
3927
  }
3910
3928
  return dedupeLabels(labels);
3911
3929
  }
3930
+ function extractDomMarkers(html) {
3931
+ const dom = new import_jsdom.JSDOM(html);
3932
+ const document = dom.window.document;
3933
+ const markers = [];
3934
+ for (const element of document.querySelectorAll("[data-qfai]")) {
3935
+ const value = element.getAttribute("data-qfai") ?? "";
3936
+ const trimmed = value.trim();
3937
+ if (trimmed.length > 0) {
3938
+ markers.push(trimmed);
3939
+ }
3940
+ }
3941
+ return Array.from(new Set(markers)).sort((a, b) => a.localeCompare(b));
3942
+ }
3912
3943
  function normalizeDomLabel(value) {
3913
3944
  const normalized = value.replace(/\s+/g, " ").trim();
3914
3945
  if (normalized.length === 0) {
@@ -3926,7 +3957,7 @@ function hasLabelMatch(foundLabels, expectedLabel) {
3926
3957
  }
3927
3958
  return foundLabels.some((label) => {
3928
3959
  const normalizedFound = normalizeComparableText(label);
3929
- return normalizedFound === normalizedExpected || normalizedFound.includes(normalizedExpected) || normalizedExpected.includes(normalizedFound);
3960
+ return normalizedFound === normalizedExpected;
3930
3961
  });
3931
3962
  }
3932
3963
  function normalizeComparableText(value) {
@@ -3992,9 +4023,35 @@ var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
3992
4023
  async function runPrototyping(options) {
3993
4024
  const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
3994
4025
  if (!autogenEnabled) {
4026
+ if (options.autogenOnly) {
4027
+ error(
4028
+ `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`
4029
+ );
4030
+ return 2;
4031
+ }
3995
4032
  info(
3996
4033
  `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
4034
  );
4035
+ const evidencePath2 = resolveEvidencePath(options.root, options.evidenceOut);
4036
+ const toolVersion2 = await resolveToolVersion();
4037
+ let existingEvidence2 = {};
4038
+ try {
4039
+ const raw = await (0, import_promises14.readFile)(evidencePath2, "utf-8");
4040
+ const parsed = JSON.parse(raw);
4041
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4042
+ existingEvidence2 = parsed;
4043
+ }
4044
+ } catch {
4045
+ }
4046
+ const skippedEvidence = emitUiFidelity({
4047
+ evidence: existingEvidence2,
4048
+ toolVersion: toolVersion2,
4049
+ command: "qfai prototyping",
4050
+ baseUrl: "",
4051
+ status: "skipped",
4052
+ reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
4053
+ });
4054
+ await writeEvidence(evidencePath2, skippedEvidence);
3998
4055
  return 0;
3999
4056
  }
4000
4057
  const baseUrl = resolveBaseUrl(options);
@@ -4017,9 +4074,15 @@ async function runPrototyping(options) {
4017
4074
  }
4018
4075
  } catch {
4019
4076
  }
4077
+ const routeHints = extractRouteHintsFromEvidence(existingEvidence);
4020
4078
  let result;
4021
4079
  try {
4022
- result = await autogenerateUiFidelity(options.root, config, baseUrl, []);
4080
+ result = await autogenerateUiFidelity(
4081
+ options.root,
4082
+ config,
4083
+ baseUrl,
4084
+ routeHints
4085
+ );
4023
4086
  } catch (err) {
4024
4087
  const reason = err instanceof Error ? err.message : String(err);
4025
4088
  warn(`prototyping: autogen failed - ${reason}`);
@@ -4098,6 +4161,45 @@ async function writeEvidence(filePath, evidence) {
4098
4161
  await (0, import_promises14.mkdir)(import_node_path17.default.dirname(filePath), { recursive: true });
4099
4162
  await (0, import_promises14.writeFile)(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4100
4163
  }
4164
+ function extractRouteHintsFromEvidence(evidence) {
4165
+ const routes = /* @__PURE__ */ new Set();
4166
+ const runtimeGate = evidence.runtimeGate;
4167
+ if (runtimeGate && typeof runtimeGate === "object" && !Array.isArray(runtimeGate)) {
4168
+ const gate = runtimeGate;
4169
+ const uiRows = gate.ui;
4170
+ if (Array.isArray(uiRows)) {
4171
+ for (const row of uiRows) {
4172
+ if (row && typeof row === "object" && !Array.isArray(row)) {
4173
+ const r = row;
4174
+ if (typeof r.route === "string" && r.route.trim().length > 0) {
4175
+ routes.add(r.route.trim());
4176
+ }
4177
+ }
4178
+ }
4179
+ }
4180
+ }
4181
+ const specs = evidence.specs;
4182
+ if (Array.isArray(specs)) {
4183
+ for (const spec of specs) {
4184
+ if (spec && typeof spec === "object" && !Array.isArray(spec)) {
4185
+ const s = spec;
4186
+ const missing = s.missing;
4187
+ if (missing && typeof missing === "object" && !Array.isArray(missing)) {
4188
+ const m = missing;
4189
+ const uiRoutes = m.uiRoutes;
4190
+ if (Array.isArray(uiRoutes)) {
4191
+ for (const route of uiRoutes) {
4192
+ if (typeof route === "string" && route.trim().length > 0) {
4193
+ routes.add(route.trim());
4194
+ }
4195
+ }
4196
+ }
4197
+ }
4198
+ }
4199
+ }
4200
+ }
4201
+ return Array.from(routes).sort((a, b) => a.localeCompare(b));
4202
+ }
4101
4203
 
4102
4204
  // src/cli/commands/report.ts
4103
4205
  var import_promises42 = require("fs/promises");
@@ -11577,6 +11679,82 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
11577
11679
  )
11578
11680
  );
11579
11681
  }
11682
+ const screensWithMissingLabels = uiFidelity.screens.filter(
11683
+ (screen) => screen.expected.labels && screen.expected.labels.length > 0 && screen.missing?.labels && screen.missing.labels.length > 0
11684
+ );
11685
+ if (screensWithMissingLabels.length > 0) {
11686
+ const details = screensWithMissingLabels.map((screen) => {
11687
+ const missing = screen.missing?.labels ?? [];
11688
+ return `${screen.route}:${screen.uiContractId}(missing_labels=${missing.join("|")})`;
11689
+ }).sort((a, b) => a.localeCompare(b));
11690
+ const refs = collectLabelMismatchRefs(screensWithMissingLabels);
11691
+ issues.push(
11692
+ issue(
11693
+ "QFAI-PROT-241",
11694
+ `QFAI-PROT-241: uiFidelity screens have missing labels. ${details.join("; ")}`,
11695
+ "error",
11696
+ evidenceJsonPath,
11697
+ "prototypingEvidence.uiFidelityMissingLabels",
11698
+ refs,
11699
+ "change",
11700
+ [
11701
+ "contracts/ui \u306E elements[].label \u3092\u753B\u9762\u306B\u3059\u3079\u3066\u63CF\u753B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
11702
+ "\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"
11703
+ ].join("\n")
11704
+ )
11705
+ );
11706
+ }
11707
+ const screensWithMissingMarkers = uiFidelity.screens.filter(
11708
+ (screen) => screen.expected.elements > 0 && screen.missing?.markers && screen.missing.markers.length > 0
11709
+ );
11710
+ if (screensWithMissingMarkers.length > 0) {
11711
+ const details = screensWithMissingMarkers.map((screen) => {
11712
+ const missing = screen.missing?.markers ?? [];
11713
+ return `${screen.route}:${screen.uiContractId}(missing_markers=${missing.join("|")})`;
11714
+ }).sort((a, b) => a.localeCompare(b));
11715
+ const refs = collectMarkerMismatchRefs(screensWithMissingMarkers);
11716
+ issues.push(
11717
+ issue(
11718
+ "QFAI-PROT-242",
11719
+ `QFAI-PROT-242: uiFidelity screens have missing markers. ${details.join("; ")}`,
11720
+ "error",
11721
+ evidenceJsonPath,
11722
+ "prototypingEvidence.uiFidelityMissingMarkers",
11723
+ refs,
11724
+ "change",
11725
+ [
11726
+ '\u753B\u9762\u306E\u5404\u8981\u7D20\u306B data-qfai="CONTRACT_ID:ELEMENT_LABEL" \u30DE\u30FC\u30AB\u30FC\u3092\u8FFD\u52A0\u3057\u3066\u304F\u3060\u3055\u3044\u3002',
11727
+ "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"
11728
+ ].join("\n")
11729
+ )
11730
+ );
11731
+ }
11732
+ const placeholderScreens = uiFidelity.screens.filter((screen) => {
11733
+ if (!screen.found?.labels) return false;
11734
+ const expectedElements = screen.expected.elements;
11735
+ const observedElements = screen.observed.elementsPlaced;
11736
+ const foundLabels = screen.found.labels.length;
11737
+ return expectedElements > 2 && observedElements <= 1 && foundLabels <= 1;
11738
+ });
11739
+ if (placeholderScreens.length > 0) {
11740
+ const details = placeholderScreens.map(
11741
+ (screen) => `${screen.route}:${screen.uiContractId}(expected=${screen.expected.elements},observed=${screen.observed.elementsPlaced})`
11742
+ ).sort((a, b) => a.localeCompare(b));
11743
+ issues.push(
11744
+ issue(
11745
+ "QFAI-PROT-243",
11746
+ `QFAI-PROT-243: placeholder/single-text pages detected. ${details.join("; ")}`,
11747
+ "warning",
11748
+ evidenceJsonPath,
11749
+ "prototypingEvidence.placeholderPages",
11750
+ placeholderScreens.map(
11751
+ (screen) => `${screen.uiContractId}|${screen.route}`
11752
+ ),
11753
+ "change",
11754
+ "\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"
11755
+ )
11756
+ );
11757
+ }
11580
11758
  const hasMockPaths = uiFidelity.screens.some(
11581
11759
  (screen) => screen.mockPaths.length > 0
11582
11760
  );
@@ -11647,6 +11825,40 @@ function collectUiFidelityMismatchRefs(mismatches) {
11647
11825
  }
11648
11826
  return Array.from(refs).sort((left, right) => left.localeCompare(right));
11649
11827
  }
11828
+ function collectLabelMismatchRefs(screens) {
11829
+ const refs = /* @__PURE__ */ new Set();
11830
+ for (const screen of screens) {
11831
+ refs.add(`contract_id=${screen.uiContractId}`);
11832
+ refs.add(`route=${screen.route}`);
11833
+ refs.add(`contract_route=${screen.uiContractId}|${screen.route}`);
11834
+ const missingLabels = screen.missing?.labels ?? [];
11835
+ if (missingLabels.length > 0) {
11836
+ const labels = missingLabels.sort((a, b) => a.localeCompare(b)).join("|");
11837
+ refs.add(`missing_labels=${labels}`);
11838
+ refs.add(
11839
+ `missing_labels_by_contract_route=${screen.uiContractId}|${screen.route}:${labels}`
11840
+ );
11841
+ }
11842
+ }
11843
+ return Array.from(refs).sort((l, r) => l.localeCompare(r));
11844
+ }
11845
+ function collectMarkerMismatchRefs(screens) {
11846
+ const refs = /* @__PURE__ */ new Set();
11847
+ for (const screen of screens) {
11848
+ refs.add(`contract_id=${screen.uiContractId}`);
11849
+ refs.add(`route=${screen.route}`);
11850
+ refs.add(`contract_route=${screen.uiContractId}|${screen.route}`);
11851
+ const missingMarkers = screen.missing?.markers ?? [];
11852
+ if (missingMarkers.length > 0) {
11853
+ const markers = missingMarkers.sort((a, b) => a.localeCompare(b)).join("|");
11854
+ refs.add(`missing_markers=${markers}`);
11855
+ refs.add(
11856
+ `missing_markers_by_contract_route=${screen.uiContractId}|${screen.route}:${markers}`
11857
+ );
11858
+ }
11859
+ }
11860
+ return Array.from(refs).sort((l, r) => l.localeCompare(r));
11861
+ }
11650
11862
  async function collectUiContractScreens(contractIndex) {
11651
11863
  const result = /* @__PURE__ */ new Map();
11652
11864
  for (const [contractId, fileSet] of contractIndex.idToFiles.entries()) {
@@ -11799,6 +12011,9 @@ function normalizeUiFidelityScreen(value) {
11799
12011
  route: value.route.trim(),
11800
12012
  uiContractId: value.uiContractId.trim().toUpperCase(),
11801
12013
  expected: expected.value,
12014
+ ...normalizeOptionalFoundBlock(value.found),
12015
+ ...normalizeOptionalMissingBlock(value.missing),
12016
+ ...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
11802
12017
  observed: observed.value,
11803
12018
  mockPaths: mockPaths.value
11804
12019
  }
@@ -11817,11 +12032,13 @@ function normalizeUiFidelityExpected(value) {
11817
12032
  reason: "`uiFidelity.screens[].expected` requires non-negative integers for elements/actions"
11818
12033
  };
11819
12034
  }
12035
+ const labels = toOptionalStringArray(value.labels);
11820
12036
  return {
11821
12037
  ok: true,
11822
12038
  value: {
11823
12039
  elements: value.elements,
11824
- actions: value.actions
12040
+ actions: value.actions,
12041
+ ...labels ? { labels } : {}
11825
12042
  }
11826
12043
  };
11827
12044
  }
@@ -11979,6 +12196,47 @@ function toStringArray2(value) {
11979
12196
  }
11980
12197
  return value.map((item) => item.trim()).filter((item) => item.length > 0);
11981
12198
  }
12199
+ function toOptionalStringArray(value) {
12200
+ if (value === void 0 || value === null) {
12201
+ return void 0;
12202
+ }
12203
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
12204
+ return void 0;
12205
+ }
12206
+ return value.map((item) => item.trim()).filter((item) => item.length > 0);
12207
+ }
12208
+ function normalizeOptionalFoundBlock(value) {
12209
+ if (!isRecord5(value)) {
12210
+ return {};
12211
+ }
12212
+ const labels = toOptionalStringArray(value.labels);
12213
+ const markers = toOptionalStringArray(value.markers);
12214
+ if (!labels && !markers) {
12215
+ return {};
12216
+ }
12217
+ return {
12218
+ found: {
12219
+ ...labels ? { labels } : {},
12220
+ ...markers ? { markers } : {}
12221
+ }
12222
+ };
12223
+ }
12224
+ function normalizeOptionalMissingBlock(value) {
12225
+ if (!isRecord5(value)) {
12226
+ return {};
12227
+ }
12228
+ const labels = toOptionalStringArray(value.labels);
12229
+ const markers = toOptionalStringArray(value.markers);
12230
+ if (!labels && !markers) {
12231
+ return {};
12232
+ }
12233
+ return {
12234
+ missing: {
12235
+ ...labels ? { labels } : {},
12236
+ ...markers ? { markers } : {}
12237
+ }
12238
+ };
12239
+ }
11982
12240
  function isRecord5(value) {
11983
12241
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11984
12242
  }
@@ -14728,7 +14986,10 @@ var ISSUE_EXPECTED_BY_CODE = {
14728
14986
  "QFAI-PROT-114": "Per-spec DB checks satisfy declared object counts and leave no unresolved DB objects.",
14729
14987
  "QFAI-PROT-231": "Interactive prototyping evidence includes uiFidelity with required screen-level fields.",
14730
14988
  "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).",
14989
+ "QFAI-PROT-233": "Interactive uiFidelity records at least one mockPaths status=pass entry (warning in v1.4.37).",
14990
+ "QFAI-PROT-241": "uiFidelity screens must have no missing labels when expected.labels is present.",
14991
+ "QFAI-PROT-242": "uiFidelity screens must have no missing markers when expected.elements > 0 and markers are tracked.",
14992
+ "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
14993
  "QFAI-CONTRACT-030": "Contract index references must match declared contract IDs in .qfai/contracts/**."
14733
14994
  };
14734
14995
  function resolveIssueTarget(issue2) {