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.
- package/assets/init/.qfai/contracts/ui/README.md +8 -0
- package/dist/cli/index.cjs +318 -16
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +318 -16
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +244 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.mjs +243 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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.
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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.
|
|
1593
|
-
return "1.4.
|
|
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
|
-
|
|
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
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
const
|
|
3906
|
-
|
|
3907
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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) {
|