qfai 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/init/.qfai/assistant/skills/qfai-implement/SKILL.md +7 -0
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/SKILL.md +7 -0
- package/assets/init/.qfai/evidence/README.md +30 -0
- package/assets/validators/designSlopPatterns.json +56 -0
- package/dist/cli/index.cjs +966 -122
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +956 -112
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +777 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.mjs +778 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -12,6 +12,22 @@ import path10 from "path";
|
|
|
12
12
|
import { access, readFile } from "fs/promises";
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
|
+
|
|
16
|
+
// src/core/uiux/renderEvidenceTypes.ts
|
|
17
|
+
var DEFAULT_RENDER_VIEWPORTS = ["desktop", "mobile"];
|
|
18
|
+
function normalizeRenderViewports(viewports) {
|
|
19
|
+
const normalized = Array.isArray(viewports) ? viewports.map((item) => item.trim()).filter((item) => item.length > 0) : [];
|
|
20
|
+
if (normalized.length > 0) {
|
|
21
|
+
return Array.from(new Set(normalized));
|
|
22
|
+
}
|
|
23
|
+
return [...DEFAULT_RENDER_VIEWPORTS];
|
|
24
|
+
}
|
|
25
|
+
function looksLikeInlineRenderPayload(value) {
|
|
26
|
+
const trimmed = value.trim().toLowerCase();
|
|
27
|
+
return trimmed.startsWith("data:image") || trimmed.includes("<html");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/core/config.ts
|
|
15
31
|
var defaultConfig = {
|
|
16
32
|
paths: {
|
|
17
33
|
contractsDir: ".qfai/contracts",
|
|
@@ -461,6 +477,133 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
461
477
|
);
|
|
462
478
|
}
|
|
463
479
|
}
|
|
480
|
+
if (raw.renderEvidence !== void 0) {
|
|
481
|
+
const renderEvidence = normalizeRenderEvidence(raw.renderEvidence, configPath, issues);
|
|
482
|
+
if (renderEvidence) {
|
|
483
|
+
result.renderEvidence = renderEvidence;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (raw.audit !== void 0) {
|
|
487
|
+
const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
|
|
488
|
+
if (audit) {
|
|
489
|
+
result.audit = audit;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
493
|
+
}
|
|
494
|
+
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
495
|
+
if (!isRecord(raw)) {
|
|
496
|
+
issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
const result = {};
|
|
500
|
+
if (raw.enabled !== void 0) {
|
|
501
|
+
if (typeof raw.enabled === "boolean") {
|
|
502
|
+
result.enabled = raw.enabled;
|
|
503
|
+
} else {
|
|
504
|
+
issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (raw.slopDetection !== void 0) {
|
|
508
|
+
if (typeof raw.slopDetection === "boolean") {
|
|
509
|
+
result.slopDetection = raw.slopDetection;
|
|
510
|
+
} else {
|
|
511
|
+
issues.push(
|
|
512
|
+
configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (raw.maxPrimaryCtas !== void 0) {
|
|
517
|
+
if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
|
|
518
|
+
result.maxPrimaryCtas = raw.maxPrimaryCtas;
|
|
519
|
+
} else {
|
|
520
|
+
issues.push(
|
|
521
|
+
configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (raw.maxRawTokenLiteralWarnings !== void 0) {
|
|
526
|
+
if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
|
|
527
|
+
result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
|
|
528
|
+
} else {
|
|
529
|
+
issues.push(
|
|
530
|
+
configIssue(
|
|
531
|
+
configPath,
|
|
532
|
+
"uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
533
|
+
)
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (raw.maxDuplicateFindingsPerRule !== void 0) {
|
|
538
|
+
if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
|
|
539
|
+
result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
|
|
540
|
+
} else {
|
|
541
|
+
issues.push(
|
|
542
|
+
configIssue(
|
|
543
|
+
configPath,
|
|
544
|
+
"uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
545
|
+
)
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
550
|
+
}
|
|
551
|
+
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
552
|
+
if (!isRecord(raw)) {
|
|
553
|
+
issues.push(
|
|
554
|
+
configIssue(configPath, "uiux.renderEvidence \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
555
|
+
);
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
const result = {};
|
|
559
|
+
if (raw.enabled !== void 0) {
|
|
560
|
+
if (typeof raw.enabled === "boolean") {
|
|
561
|
+
result.enabled = raw.enabled;
|
|
562
|
+
} else {
|
|
563
|
+
issues.push(
|
|
564
|
+
configIssue(configPath, "uiux.renderEvidence.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (raw.viewports !== void 0) {
|
|
569
|
+
if (Array.isArray(raw.viewports) && raw.viewports.every((item) => typeof item === "string")) {
|
|
570
|
+
result.viewports = normalizeRenderViewports(raw.viewports);
|
|
571
|
+
} else {
|
|
572
|
+
issues.push(
|
|
573
|
+
configIssue(configPath, "uiux.renderEvidence.viewports \u306F\u6587\u5B57\u5217\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (raw.out !== void 0) {
|
|
578
|
+
if (typeof raw.out === "string" && raw.out.trim().length > 0) {
|
|
579
|
+
result.out = raw.out.trim();
|
|
580
|
+
} else {
|
|
581
|
+
issues.push(
|
|
582
|
+
configIssue(configPath, "uiux.renderEvidence.out \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (raw.baseUrl !== void 0) {
|
|
587
|
+
if (typeof raw.baseUrl === "string" && raw.baseUrl.trim().length > 0) {
|
|
588
|
+
result.baseUrl = raw.baseUrl.trim();
|
|
589
|
+
} else {
|
|
590
|
+
issues.push(
|
|
591
|
+
configIssue(
|
|
592
|
+
configPath,
|
|
593
|
+
"uiux.renderEvidence.baseUrl \u306F\u7A7A\u3067\u306A\u3044\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (raw.failOpen !== void 0) {
|
|
599
|
+
if (typeof raw.failOpen === "boolean") {
|
|
600
|
+
result.failOpen = raw.failOpen;
|
|
601
|
+
} else {
|
|
602
|
+
issues.push(
|
|
603
|
+
configIssue(configPath, "uiux.renderEvidence.failOpen \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
464
607
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
465
608
|
}
|
|
466
609
|
function configIssue(file, message) {
|
|
@@ -1541,8 +1684,8 @@ import { readFile as readFile5 } from "fs/promises";
|
|
|
1541
1684
|
import path8 from "path";
|
|
1542
1685
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1543
1686
|
async function resolveToolVersion() {
|
|
1544
|
-
if ("1.7.
|
|
1545
|
-
return "1.7.
|
|
1687
|
+
if ("1.7.2".length > 0) {
|
|
1688
|
+
return "1.7.2";
|
|
1546
1689
|
}
|
|
1547
1690
|
try {
|
|
1548
1691
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -3311,6 +3454,7 @@ var ID_PREFIXES = [
|
|
|
3311
3454
|
"DB",
|
|
3312
3455
|
"THEMA"
|
|
3313
3456
|
];
|
|
3457
|
+
var DIGIT_AHEAD = "(?=[A-Za-z0-9_-]*\\d)";
|
|
3314
3458
|
var STRICT_ID_PATTERNS = {
|
|
3315
3459
|
CAP: /\bCAP-\d{4}\b/g,
|
|
3316
3460
|
SPEC: /\bSPEC-\d{4}\b/g,
|
|
@@ -3326,18 +3470,18 @@ var STRICT_ID_PATTERNS = {
|
|
|
3326
3470
|
ADR: /\bADR-\d{4}\b/g
|
|
3327
3471
|
};
|
|
3328
3472
|
var LOOSE_ID_PATTERNS = {
|
|
3329
|
-
CAP:
|
|
3330
|
-
SPEC:
|
|
3331
|
-
US:
|
|
3332
|
-
BR:
|
|
3333
|
-
SC:
|
|
3334
|
-
AC:
|
|
3335
|
-
CASE:
|
|
3336
|
-
UI:
|
|
3337
|
-
API:
|
|
3338
|
-
DB:
|
|
3339
|
-
THEMA:
|
|
3340
|
-
ADR:
|
|
3473
|
+
CAP: new RegExp(`\\bCAP-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3474
|
+
SPEC: new RegExp(`\\bSPEC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3475
|
+
US: new RegExp(`\\bUS-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3476
|
+
BR: new RegExp(`\\bBR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3477
|
+
SC: new RegExp(`\\bSC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3478
|
+
AC: new RegExp(`\\bAC-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3479
|
+
CASE: new RegExp(`\\bCASE-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3480
|
+
UI: new RegExp(`\\bUI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3481
|
+
API: new RegExp(`\\bAPI-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3482
|
+
DB: new RegExp(`\\bDB-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3483
|
+
THEMA: new RegExp(`\\bTHEMA-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi"),
|
|
3484
|
+
ADR: new RegExp(`\\bADR-${DIGIT_AHEAD}[A-Za-z0-9_-]+\\b`, "gi")
|
|
3341
3485
|
};
|
|
3342
3486
|
function extractIds(text, prefix) {
|
|
3343
3487
|
const pattern = STRICT_ID_PATTERNS[prefix];
|
|
@@ -3919,6 +4063,8 @@ function formatError4(error2) {
|
|
|
3919
4063
|
var ENV_AUTOGEN = "QFAI_PROTOTYPE_FIDELITY_AUTOGEN";
|
|
3920
4064
|
var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
|
|
3921
4065
|
async function runPrototyping(options) {
|
|
4066
|
+
const { config } = await loadConfig(options.root);
|
|
4067
|
+
const renderOptions = mergeRenderOptions(options, config.uiux?.renderEvidence);
|
|
3922
4068
|
const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
|
|
3923
4069
|
if (!autogenEnabled) {
|
|
3924
4070
|
if (options.autogenOnly) {
|
|
@@ -3949,16 +4095,24 @@ async function runPrototyping(options) {
|
|
|
3949
4095
|
status: "skipped",
|
|
3950
4096
|
reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
|
|
3951
4097
|
});
|
|
3952
|
-
await
|
|
4098
|
+
const renderBundle2 = await maybeWriteRenderBundle(
|
|
4099
|
+
toolVersion2,
|
|
4100
|
+
"qfai prototyping --render-evidence",
|
|
4101
|
+
renderOptions,
|
|
4102
|
+
false
|
|
4103
|
+
);
|
|
4104
|
+
await writeEvidence(
|
|
4105
|
+
evidencePath2,
|
|
4106
|
+
applyRenderEvidence(skippedEvidence, renderOptions, false, renderBundle2?.path)
|
|
4107
|
+
);
|
|
3953
4108
|
return 0;
|
|
3954
4109
|
}
|
|
3955
|
-
const baseUrl = resolveBaseUrl(
|
|
4110
|
+
const baseUrl = resolveBaseUrl(renderOptions);
|
|
3956
4111
|
if (!baseUrl) {
|
|
3957
4112
|
error(`prototyping: --base-url \u307E\u305F\u306F QFAI_PROTOTYPE_BASE_URL \u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002`);
|
|
3958
4113
|
return 1;
|
|
3959
4114
|
}
|
|
3960
4115
|
const toolVersion = await resolveToolVersion();
|
|
3961
|
-
const { config } = await loadConfig(options.root);
|
|
3962
4116
|
const evidencePath = resolveEvidencePath(options.root, options.evidenceOut);
|
|
3963
4117
|
info(`prototyping: autogen uiFidelity \u3092\u5B9F\u884C\u3057\u307E\u3059 (baseUrl=${baseUrl})`);
|
|
3964
4118
|
let existingEvidence = {};
|
|
@@ -3985,7 +4139,16 @@ async function runPrototyping(options) {
|
|
|
3985
4139
|
status: "failed",
|
|
3986
4140
|
reason
|
|
3987
4141
|
});
|
|
3988
|
-
await
|
|
4142
|
+
const renderBundle2 = await maybeWriteRenderBundle(
|
|
4143
|
+
toolVersion,
|
|
4144
|
+
"qfai prototyping --render-evidence",
|
|
4145
|
+
renderOptions,
|
|
4146
|
+
true
|
|
4147
|
+
);
|
|
4148
|
+
await writeEvidence(
|
|
4149
|
+
evidencePath,
|
|
4150
|
+
applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
|
|
4151
|
+
);
|
|
3989
4152
|
info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
|
|
3990
4153
|
return options.autogenOnly ? 1 : 0;
|
|
3991
4154
|
}
|
|
@@ -4004,7 +4167,16 @@ async function runPrototyping(options) {
|
|
|
4004
4167
|
crawled: result.crawled,
|
|
4005
4168
|
reason
|
|
4006
4169
|
});
|
|
4007
|
-
await
|
|
4170
|
+
const renderBundle2 = await maybeWriteRenderBundle(
|
|
4171
|
+
toolVersion,
|
|
4172
|
+
"qfai prototyping --render-evidence",
|
|
4173
|
+
renderOptions,
|
|
4174
|
+
true
|
|
4175
|
+
);
|
|
4176
|
+
await writeEvidence(
|
|
4177
|
+
evidencePath,
|
|
4178
|
+
applyRenderEvidence(failedEvidence, renderOptions, true, renderBundle2?.path)
|
|
4179
|
+
);
|
|
4008
4180
|
info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
|
|
4009
4181
|
return options.autogenOnly ? 1 : 0;
|
|
4010
4182
|
}
|
|
@@ -4017,7 +4189,16 @@ async function runPrototyping(options) {
|
|
|
4017
4189
|
screens: result.screens,
|
|
4018
4190
|
crawled: result.crawled
|
|
4019
4191
|
});
|
|
4020
|
-
await
|
|
4192
|
+
const renderBundle = await maybeWriteRenderBundle(
|
|
4193
|
+
toolVersion,
|
|
4194
|
+
"qfai prototyping --render-evidence",
|
|
4195
|
+
renderOptions,
|
|
4196
|
+
true
|
|
4197
|
+
);
|
|
4198
|
+
await writeEvidence(
|
|
4199
|
+
evidencePath,
|
|
4200
|
+
applyRenderEvidence(successEvidence, renderOptions, true, renderBundle?.path)
|
|
4201
|
+
);
|
|
4021
4202
|
const routeOkCount = result.crawled.filter((r) => r.status === "ok").length;
|
|
4022
4203
|
const routeFailCount = result.crawled.filter((r) => r.status === "failed").length;
|
|
4023
4204
|
const avgCoverage = result.screens.length > 0 ? (result.screens.reduce((sum, s) => sum + s.coverage, 0) / result.screens.length).toFixed(2) : "N/A";
|
|
@@ -4047,6 +4228,75 @@ async function writeEvidence(filePath, evidence) {
|
|
|
4047
4228
|
await mkdir4(path17.dirname(filePath), { recursive: true });
|
|
4048
4229
|
await writeFile3(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
|
|
4049
4230
|
}
|
|
4231
|
+
async function writeRenderBundle(filePath, bundle) {
|
|
4232
|
+
await mkdir4(path17.dirname(filePath), { recursive: true });
|
|
4233
|
+
await writeFile3(filePath, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
|
|
4234
|
+
}
|
|
4235
|
+
async function maybeWriteRenderBundle(toolVersion, command, options, autogenEnabled) {
|
|
4236
|
+
if (!options.renderEvidence) {
|
|
4237
|
+
return void 0;
|
|
4238
|
+
}
|
|
4239
|
+
const renderBundle = buildRenderBundle(toolVersion, command, options, autogenEnabled);
|
|
4240
|
+
await writeRenderBundle(renderBundle.path, renderBundle.bundle);
|
|
4241
|
+
return renderBundle;
|
|
4242
|
+
}
|
|
4243
|
+
function applyRenderEvidence(evidence, options, autogenEnabled, outputPath) {
|
|
4244
|
+
if (!options.renderEvidence) {
|
|
4245
|
+
return evidence;
|
|
4246
|
+
}
|
|
4247
|
+
const viewports = normalizeRenderViewports(options.renderViewports);
|
|
4248
|
+
return {
|
|
4249
|
+
...evidence,
|
|
4250
|
+
renderEvidence: {
|
|
4251
|
+
status: autogenEnabled ? "requested" : "skipped",
|
|
4252
|
+
requested: true,
|
|
4253
|
+
autogenEnabled,
|
|
4254
|
+
viewports,
|
|
4255
|
+
outputPath: outputPath ?? resolveRenderOutPath(options.root, options.renderOut),
|
|
4256
|
+
reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
|
|
4257
|
+
}
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
function buildRenderBundle(toolVersion, command, options, autogenEnabled) {
|
|
4261
|
+
const path64 = resolveRenderOutPath(options.root, options.renderOut);
|
|
4262
|
+
return {
|
|
4263
|
+
path: path64,
|
|
4264
|
+
bundle: {
|
|
4265
|
+
meta: {
|
|
4266
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4267
|
+
toolVersion,
|
|
4268
|
+
commands: [command]
|
|
4269
|
+
},
|
|
4270
|
+
renderEvidence: {
|
|
4271
|
+
status: autogenEnabled ? "requested" : "skipped",
|
|
4272
|
+
requested: true,
|
|
4273
|
+
autogenEnabled,
|
|
4274
|
+
viewports: normalizeRenderViewports(options.renderViewports),
|
|
4275
|
+
outputPath: path64,
|
|
4276
|
+
reason: autogenEnabled ? "render evidence capture not implemented in this slice" : "render requested without autogen-ui-fidelity"
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
function mergeRenderOptions(options, configRenderEvidence) {
|
|
4282
|
+
const cliViewportsSpecified = Array.isArray(options.renderViewports);
|
|
4283
|
+
const mergedViewports = cliViewportsSpecified ? normalizeRenderViewports(options.renderViewports) : normalizeRenderViewports(configRenderEvidence?.viewports);
|
|
4284
|
+
const renderOut = options.renderOut ?? configRenderEvidence?.out;
|
|
4285
|
+
const baseUrl = options.baseUrl ?? configRenderEvidence?.baseUrl;
|
|
4286
|
+
return {
|
|
4287
|
+
...options,
|
|
4288
|
+
renderEvidence: options.renderEvidence || configRenderEvidence?.enabled === true,
|
|
4289
|
+
renderViewports: mergedViewports,
|
|
4290
|
+
...renderOut ? { renderOut } : {},
|
|
4291
|
+
...baseUrl ? { baseUrl } : {}
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
function resolveRenderOutPath(root, explicit) {
|
|
4295
|
+
if (explicit) {
|
|
4296
|
+
return path17.isAbsolute(explicit) ? explicit : path17.resolve(root, explicit);
|
|
4297
|
+
}
|
|
4298
|
+
return path17.resolve(root, ".qfai/evidence/render.json");
|
|
4299
|
+
}
|
|
4050
4300
|
function extractRouteHintsFromEvidence(evidence) {
|
|
4051
4301
|
const routes = /* @__PURE__ */ new Set();
|
|
4052
4302
|
const runtimeGate = evidence.runtimeGate;
|
|
@@ -4088,8 +4338,8 @@ function extractRouteHintsFromEvidence(evidence) {
|
|
|
4088
4338
|
}
|
|
4089
4339
|
|
|
4090
4340
|
// src/cli/commands/report.ts
|
|
4091
|
-
import { mkdir as mkdir8, readFile as
|
|
4092
|
-
import
|
|
4341
|
+
import { mkdir as mkdir8, readFile as readFile45, writeFile as writeFile7 } from "fs/promises";
|
|
4342
|
+
import path61 from "path";
|
|
4093
4343
|
|
|
4094
4344
|
// src/core/normalize.ts
|
|
4095
4345
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -4184,8 +4434,8 @@ async function createPhaseGuardResult(phase, blockedIssue) {
|
|
|
4184
4434
|
}
|
|
4185
4435
|
|
|
4186
4436
|
// src/core/report.ts
|
|
4187
|
-
import { readFile as
|
|
4188
|
-
import
|
|
4437
|
+
import { readFile as readFile43 } from "fs/promises";
|
|
4438
|
+
import path59 from "path";
|
|
4189
4439
|
|
|
4190
4440
|
// src/core/contractIndex.ts
|
|
4191
4441
|
import { readFile as readFile10 } from "fs/promises";
|
|
@@ -8365,11 +8615,11 @@ import path29 from "path";
|
|
|
8365
8615
|
// src/core/atddTraceability.ts
|
|
8366
8616
|
import { readFile as readFile20 } from "fs/promises";
|
|
8367
8617
|
import path28 from "path";
|
|
8368
|
-
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4})\b/g;
|
|
8369
|
-
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4})\b/g;
|
|
8618
|
+
var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
|
|
8619
|
+
var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
|
|
8370
8620
|
var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
|
|
8371
|
-
var
|
|
8372
|
-
var
|
|
8621
|
+
var US_ID_RE2 = /^US-\d{4}(?:-\d{4})?$/;
|
|
8622
|
+
var TC_ID_RE = /^TC-\d{4}(?:-\d{4})?$/;
|
|
8373
8623
|
var API_CONTRACT_ID_RE = /^CON-API-\d+$/;
|
|
8374
8624
|
var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
|
|
8375
8625
|
async function evaluateAtddCodeTraceability(root, config) {
|
|
@@ -8517,11 +8767,11 @@ async function collectApiContractIds(apiRoot) {
|
|
|
8517
8767
|
function collectShortIds(text, prefix) {
|
|
8518
8768
|
const ids = /* @__PURE__ */ new Set();
|
|
8519
8769
|
const headingIds = collectMarkdownItems(text, prefix).map((item) => item.id);
|
|
8520
|
-
const pattern = prefix === "US" ? /\bUS-\d{4}
|
|
8770
|
+
const pattern = prefix === "US" ? /\bUS-\d{4}(?:-\d{4})?\b/g : /\bTC-\d{4}(?:-\d{4})?\b/g;
|
|
8521
8771
|
const looseIds = uniqueMatches(text, pattern);
|
|
8522
8772
|
for (const id of [...headingIds, ...looseIds]) {
|
|
8523
8773
|
const normalized = id.toUpperCase();
|
|
8524
|
-
if (prefix === "US" &&
|
|
8774
|
+
if (prefix === "US" && US_ID_RE2.test(normalized) || prefix === "TC" && TC_ID_RE.test(normalized)) {
|
|
8525
8775
|
ids.add(normalized);
|
|
8526
8776
|
}
|
|
8527
8777
|
}
|
|
@@ -9530,15 +9780,15 @@ import path33 from "path";
|
|
|
9530
9780
|
import { mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
|
|
9531
9781
|
import path34 from "path";
|
|
9532
9782
|
var ID_PATTERNS = {
|
|
9533
|
-
us: /^US-\d{4}
|
|
9534
|
-
ac: /^AC-\d{4}
|
|
9535
|
-
br: /^BR-\d{4}
|
|
9536
|
-
ex: /^EX-\d{4}
|
|
9783
|
+
us: /^US-\d{4}(?:-\d{4})?$/,
|
|
9784
|
+
ac: /^AC-\d{4}(?:-\d{4})?$/,
|
|
9785
|
+
br: /^BR-\d{4}(?:-\d{4})?$/,
|
|
9786
|
+
ex: /^EX-\d{4}(?:-\d{4})?$/
|
|
9537
9787
|
};
|
|
9538
9788
|
var V1421_REFS = {
|
|
9539
|
-
ac: /\bAC-\d{4}
|
|
9540
|
-
br: /\bBR-\d{4}
|
|
9541
|
-
ex: /\bEX-\d{4}
|
|
9789
|
+
ac: /\bAC-\d{4}(?:-\d{4})?\b/gi,
|
|
9790
|
+
br: /\bBR-\d{4}(?:-\d{4})?\b/gi,
|
|
9791
|
+
ex: /\bEX-\d{4}(?:-\d{4})?\b/gi
|
|
9542
9792
|
};
|
|
9543
9793
|
async function validateLayerCoverage(root, config) {
|
|
9544
9794
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -9811,11 +10061,11 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
9811
10061
|
const ids = /* @__PURE__ */ new Set();
|
|
9812
10062
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
9813
10063
|
for (const line of lines) {
|
|
9814
|
-
const headingMatch = /^##\s*(AC-\d{4})\b/i.exec(line.trim());
|
|
10064
|
+
const headingMatch = /^##\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line.trim());
|
|
9815
10065
|
if (headingMatch?.[1]) {
|
|
9816
10066
|
ids.add(headingMatch[1].toUpperCase());
|
|
9817
10067
|
}
|
|
9818
|
-
const commentMatch = /^\s*#\s*(AC-\d{4})\b/i.exec(line);
|
|
10068
|
+
const commentMatch = /^\s*#\s*(AC-\d{4}(?:-\d{4})?)\b/i.exec(line);
|
|
9819
10069
|
if (commentMatch?.[1]) {
|
|
9820
10070
|
ids.add(commentMatch[1].toUpperCase());
|
|
9821
10071
|
}
|
|
@@ -9835,8 +10085,8 @@ function parseAcceptanceCriteriaIds2(text) {
|
|
|
9835
10085
|
function parseDefinitionRefs(text, prefix, refPattern, options = {}) {
|
|
9836
10086
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
9837
10087
|
const refsById = /* @__PURE__ */ new Map();
|
|
9838
|
-
const idPattern = new RegExp(`^${prefix}-\\d{4}
|
|
9839
|
-
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4})\\b`, "i");
|
|
10088
|
+
const idPattern = new RegExp(`^${prefix}-\\d{4}(?:-\\d{4})?$`);
|
|
10089
|
+
const headingPattern = new RegExp(`^##\\s*(${prefix}-\\d{4}(?:-\\d{4})?)\\b`, "i");
|
|
9840
10090
|
const referenceColumns = new Set(
|
|
9841
10091
|
(options.referenceColumns ?? []).map((column) => normalizeColumnName(column))
|
|
9842
10092
|
);
|
|
@@ -10237,7 +10487,7 @@ var US_DOWNSTREAM_RE = /\b(?:AC|BR|EX|TC)-\d{4}\b/g;
|
|
|
10237
10487
|
var AC_DOWNSTREAM_RE = /\b(?:BR|EX|TC)-\d{4}\b/g;
|
|
10238
10488
|
var BR_DOWNSTREAM_RE = /\b(?:EX|TC)-\d{4}\b/g;
|
|
10239
10489
|
var CAP_ID_RE = /^CAP-\d{4}$/;
|
|
10240
|
-
var
|
|
10490
|
+
var US_ID_RE3 = /^US-\d{4}$/;
|
|
10241
10491
|
var AC_ID_RE4 = /^AC-\d{4}$/;
|
|
10242
10492
|
var BR_OR_AC_ID_RE = /^(?:BR|AC)-\d{4}$/;
|
|
10243
10493
|
var EX_ID_RE2 = /^EX-\d{4}$/;
|
|
@@ -10276,7 +10526,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
10276
10526
|
...await validateMarkdownParentFormat(entry.userStoriesPath, "US", CAP_ID_RE, "CAP")
|
|
10277
10527
|
);
|
|
10278
10528
|
issues.push(
|
|
10279
|
-
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC",
|
|
10529
|
+
...await validateMarkdownParentFormat(entry.acceptanceCriteriaPath, "AC", US_ID_RE3, "US")
|
|
10280
10530
|
);
|
|
10281
10531
|
issues.push(
|
|
10282
10532
|
...await validateMarkdownParentFormat(entry.businessRulesPath, "BR", AC_ID_RE4, "AC")
|
|
@@ -11082,7 +11332,7 @@ function validateExParentExists(filePath, exItems, acIds, brIds) {
|
|
|
11082
11332
|
}
|
|
11083
11333
|
|
|
11084
11334
|
// src/core/validators/prototypingEvidence.ts
|
|
11085
|
-
import { readFile as readFile26 } from "fs/promises";
|
|
11335
|
+
import { access as access12, readFile as readFile26 } from "fs/promises";
|
|
11086
11336
|
import path39 from "path";
|
|
11087
11337
|
var EVIDENCE_MARKDOWN_FILE = "prototyping.md";
|
|
11088
11338
|
var EVIDENCE_JSON_FILE = "prototyping.json";
|
|
@@ -11588,6 +11838,100 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
|
|
|
11588
11838
|
)
|
|
11589
11839
|
);
|
|
11590
11840
|
}
|
|
11841
|
+
const renderIssues = await validateRenderEvidenceScreens(
|
|
11842
|
+
root,
|
|
11843
|
+
config,
|
|
11844
|
+
evidenceJsonPath,
|
|
11845
|
+
uiFidelity.screens
|
|
11846
|
+
);
|
|
11847
|
+
issues.push(...renderIssues);
|
|
11848
|
+
return issues;
|
|
11849
|
+
}
|
|
11850
|
+
async function validateRenderEvidenceScreens(root, config, evidenceJsonPath, screens) {
|
|
11851
|
+
const issues = [];
|
|
11852
|
+
const hasAnyRenderEvidence = screens.some((screen) => screen.renders.length > 0);
|
|
11853
|
+
if (!hasAnyRenderEvidence) {
|
|
11854
|
+
return issues;
|
|
11855
|
+
}
|
|
11856
|
+
const qualityProfile = config.uiux?.qualityProfile ?? "default";
|
|
11857
|
+
for (const screen of screens) {
|
|
11858
|
+
if (screen.renders.length === 0) {
|
|
11859
|
+
continue;
|
|
11860
|
+
}
|
|
11861
|
+
const viewports = new Set(screen.renders.map((render) => render.viewport));
|
|
11862
|
+
const missingDefaultViewports = DEFAULT_RENDER_VIEWPORTS.filter(
|
|
11863
|
+
(viewport) => !viewports.has(viewport)
|
|
11864
|
+
);
|
|
11865
|
+
const allSkipped = screen.renders.every((render) => render.status === "skipped");
|
|
11866
|
+
for (const render of screen.renders) {
|
|
11867
|
+
if (render.status !== "captured") {
|
|
11868
|
+
continue;
|
|
11869
|
+
}
|
|
11870
|
+
const invalidPaths = [
|
|
11871
|
+
{ label: "imagePath", value: render.imagePath },
|
|
11872
|
+
{ label: "htmlPath", value: render.htmlPath }
|
|
11873
|
+
].filter((entry) => looksLikeInlineRenderPayload(entry.value));
|
|
11874
|
+
if (invalidPaths.length > 0) {
|
|
11875
|
+
issues.push(
|
|
11876
|
+
issue(
|
|
11877
|
+
"QFAI-PROT-244",
|
|
11878
|
+
`QFAI-PROT-244: render evidence must be path-only. route=${screen.route}, viewport=${render.viewport}, invalid=${invalidPaths.map((entry) => entry.label).join("|")}`,
|
|
11879
|
+
"error",
|
|
11880
|
+
evidenceJsonPath,
|
|
11881
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
11882
|
+
[
|
|
11883
|
+
`route=${screen.route}`,
|
|
11884
|
+
`viewport=${render.viewport}`,
|
|
11885
|
+
...invalidPaths.map((entry) => `artifact=${entry.label}`)
|
|
11886
|
+
],
|
|
11887
|
+
"change",
|
|
11888
|
+
"imagePath/htmlPath \u306B\u306F\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9\u306E\u307F\u3092\u4FDD\u5B58\u3057\u3001data URI \u3084 HTML \u672C\u6587\u3092 JSON \u306B\u57CB\u3081\u8FBC\u307E\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002"
|
|
11889
|
+
)
|
|
11890
|
+
);
|
|
11891
|
+
continue;
|
|
11892
|
+
}
|
|
11893
|
+
const missingArtifacts = await collectMissingRenderArtifacts(root, render);
|
|
11894
|
+
if (missingArtifacts.length > 0) {
|
|
11895
|
+
issues.push(
|
|
11896
|
+
issue(
|
|
11897
|
+
"QFAI-PROT-244",
|
|
11898
|
+
`QFAI-PROT-244: captured render artifact is missing. route=${screen.route}, viewport=${render.viewport}, missing=${missingArtifacts.join("|")}`,
|
|
11899
|
+
"error",
|
|
11900
|
+
evidenceJsonPath,
|
|
11901
|
+
"prototypingEvidence.renderArtifactPresence",
|
|
11902
|
+
[
|
|
11903
|
+
`route=${screen.route}`,
|
|
11904
|
+
`viewport=${render.viewport}`,
|
|
11905
|
+
...missingArtifacts.map((artifact) => `artifact=${artifact}`)
|
|
11906
|
+
],
|
|
11907
|
+
"change",
|
|
11908
|
+
"render capture \u3092\u518D\u5B9F\u884C\u3057\u3001screenshot \u3068 HTML snapshot \u306E\u4E21\u65B9\u304C\u4FDD\u5B58\u3055\u308C\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11909
|
+
)
|
|
11910
|
+
);
|
|
11911
|
+
}
|
|
11912
|
+
}
|
|
11913
|
+
if (missingDefaultViewports.length === 0 && !allSkipped) {
|
|
11914
|
+
continue;
|
|
11915
|
+
}
|
|
11916
|
+
const severity = qualityProfile === "default" ? "warning" : "error";
|
|
11917
|
+
const reason = allSkipped ? "all renders are skipped" : `missing default viewports=${missingDefaultViewports.join("|")}`;
|
|
11918
|
+
issues.push(
|
|
11919
|
+
issue(
|
|
11920
|
+
"QFAI-PROT-245",
|
|
11921
|
+
`QFAI-PROT-245: render coverage is incomplete for ${screen.route}. ${reason}. qualityProfile=${qualityProfile}`,
|
|
11922
|
+
severity,
|
|
11923
|
+
evidenceJsonPath,
|
|
11924
|
+
"prototypingEvidence.renderCoverage",
|
|
11925
|
+
[
|
|
11926
|
+
`route=${screen.route}`,
|
|
11927
|
+
...missingDefaultViewports.map((viewport) => `viewport=${viewport}`),
|
|
11928
|
+
`qualityProfile=${qualityProfile}`
|
|
11929
|
+
],
|
|
11930
|
+
"change",
|
|
11931
|
+
allSkipped ? "\u5C11\u306A\u304F\u3068\u3082 desktop/mobile \u306E\u3044\u305A\u308C\u304B\u3067 captured \u307E\u305F\u306F failed \u306E\u660E\u793A\u7684\u306A render outcome \u3092\u6B8B\u3057\u3066\u304F\u3060\u3055\u3044\u3002" : "desktop/mobile \u306E default viewport \u3092\u63C3\u3048\u308B\u304B\u3001profile \u8A2D\u5B9A\u3068 scope \u3092\u898B\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11932
|
+
)
|
|
11933
|
+
);
|
|
11934
|
+
}
|
|
11591
11935
|
return issues;
|
|
11592
11936
|
}
|
|
11593
11937
|
function formatUiFidelityMismatch(mismatch) {
|
|
@@ -11800,6 +12144,10 @@ function normalizeUiFidelityScreen(value) {
|
|
|
11800
12144
|
if (!mockPaths.ok) {
|
|
11801
12145
|
return mockPaths;
|
|
11802
12146
|
}
|
|
12147
|
+
const renders = normalizeRenderEntries(value.renders);
|
|
12148
|
+
if (!renders.ok) {
|
|
12149
|
+
return renders;
|
|
12150
|
+
}
|
|
11803
12151
|
return {
|
|
11804
12152
|
ok: true,
|
|
11805
12153
|
value: {
|
|
@@ -11810,8 +12158,103 @@ function normalizeUiFidelityScreen(value) {
|
|
|
11810
12158
|
...normalizeOptionalMissingBlock(value.missing),
|
|
11811
12159
|
...typeof value.coverage === "number" ? { coverage: value.coverage } : {},
|
|
11812
12160
|
observed: observed.value,
|
|
11813
|
-
mockPaths: mockPaths.value
|
|
12161
|
+
mockPaths: mockPaths.value,
|
|
12162
|
+
renders: renders.value
|
|
12163
|
+
}
|
|
12164
|
+
};
|
|
12165
|
+
}
|
|
12166
|
+
function normalizeRenderEntries(value) {
|
|
12167
|
+
if (value === void 0) {
|
|
12168
|
+
return { ok: true, value: [] };
|
|
12169
|
+
}
|
|
12170
|
+
if (!Array.isArray(value)) {
|
|
12171
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders` must be an array" };
|
|
12172
|
+
}
|
|
12173
|
+
const renders = [];
|
|
12174
|
+
for (const entry of value) {
|
|
12175
|
+
const normalized = normalizeRenderEntry(entry);
|
|
12176
|
+
if (!normalized.ok) {
|
|
12177
|
+
return normalized;
|
|
12178
|
+
}
|
|
12179
|
+
renders.push(normalized.value);
|
|
12180
|
+
}
|
|
12181
|
+
return { ok: true, value: renders };
|
|
12182
|
+
}
|
|
12183
|
+
function normalizeRenderEntry(value) {
|
|
12184
|
+
if (!isRecord5(value)) {
|
|
12185
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[]` must be objects" };
|
|
12186
|
+
}
|
|
12187
|
+
if (typeof value.viewport !== "string" || value.viewport.trim().length === 0) {
|
|
12188
|
+
return { ok: false, reason: "`uiFidelity.screens[].renders[].viewport` is required" };
|
|
12189
|
+
}
|
|
12190
|
+
if (!isNonNegativeInteger(value.width) || !isNonNegativeInteger(value.height) || value.width === 0 || value.height === 0) {
|
|
12191
|
+
return {
|
|
12192
|
+
ok: false,
|
|
12193
|
+
reason: "`uiFidelity.screens[].renders[]` requires positive integers for width/height"
|
|
12194
|
+
};
|
|
12195
|
+
}
|
|
12196
|
+
const viewport = value.viewport.trim();
|
|
12197
|
+
const width = value.width;
|
|
12198
|
+
const height = value.height;
|
|
12199
|
+
const status = typeof value.status === "string" ? value.status.trim().toLowerCase() : "";
|
|
12200
|
+
if (status === "captured") {
|
|
12201
|
+
if (typeof value.imagePath !== "string" || value.imagePath.trim().length === 0 || typeof value.htmlPath !== "string" || value.htmlPath.trim().length === 0) {
|
|
12202
|
+
return {
|
|
12203
|
+
ok: false,
|
|
12204
|
+
reason: "`captured` render entries require imagePath and htmlPath"
|
|
12205
|
+
};
|
|
12206
|
+
}
|
|
12207
|
+
return {
|
|
12208
|
+
ok: true,
|
|
12209
|
+
value: {
|
|
12210
|
+
viewport,
|
|
12211
|
+
status: "captured",
|
|
12212
|
+
width,
|
|
12213
|
+
height,
|
|
12214
|
+
imagePath: value.imagePath.trim(),
|
|
12215
|
+
htmlPath: value.htmlPath.trim()
|
|
12216
|
+
}
|
|
12217
|
+
};
|
|
12218
|
+
}
|
|
12219
|
+
if (status === "skipped") {
|
|
12220
|
+
if (typeof value.skippedReason !== "string" || value.skippedReason.trim().length === 0) {
|
|
12221
|
+
return {
|
|
12222
|
+
ok: false,
|
|
12223
|
+
reason: "`skipped` render entries require skippedReason"
|
|
12224
|
+
};
|
|
12225
|
+
}
|
|
12226
|
+
return {
|
|
12227
|
+
ok: true,
|
|
12228
|
+
value: {
|
|
12229
|
+
viewport,
|
|
12230
|
+
status: "skipped",
|
|
12231
|
+
width,
|
|
12232
|
+
height,
|
|
12233
|
+
skippedReason: value.skippedReason.trim()
|
|
12234
|
+
}
|
|
12235
|
+
};
|
|
12236
|
+
}
|
|
12237
|
+
if (status === "failed") {
|
|
12238
|
+
if (typeof value.error !== "string" || value.error.trim().length === 0) {
|
|
12239
|
+
return {
|
|
12240
|
+
ok: false,
|
|
12241
|
+
reason: "`failed` render entries require error"
|
|
12242
|
+
};
|
|
11814
12243
|
}
|
|
12244
|
+
return {
|
|
12245
|
+
ok: true,
|
|
12246
|
+
value: {
|
|
12247
|
+
viewport,
|
|
12248
|
+
status: "failed",
|
|
12249
|
+
width,
|
|
12250
|
+
height,
|
|
12251
|
+
error: value.error.trim()
|
|
12252
|
+
}
|
|
12253
|
+
};
|
|
12254
|
+
}
|
|
12255
|
+
return {
|
|
12256
|
+
ok: false,
|
|
12257
|
+
reason: "`uiFidelity.screens[].renders[].status` must be captured|skipped|failed"
|
|
11815
12258
|
};
|
|
11816
12259
|
}
|
|
11817
12260
|
function normalizeUiFidelityExpected(value) {
|
|
@@ -12037,6 +12480,22 @@ function normalizeOptionalMissingBlock(value) {
|
|
|
12037
12480
|
function isRecord5(value) {
|
|
12038
12481
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12039
12482
|
}
|
|
12483
|
+
async function collectMissingRenderArtifacts(root, render) {
|
|
12484
|
+
const missing = [];
|
|
12485
|
+
const candidates = [
|
|
12486
|
+
{ label: "imagePath", target: render.imagePath },
|
|
12487
|
+
{ label: "htmlPath", target: render.htmlPath }
|
|
12488
|
+
];
|
|
12489
|
+
for (const candidate of candidates) {
|
|
12490
|
+
const resolved = path39.isAbsolute(candidate.target) ? candidate.target : path39.resolve(root, candidate.target);
|
|
12491
|
+
try {
|
|
12492
|
+
await access12(resolved);
|
|
12493
|
+
} catch {
|
|
12494
|
+
missing.push(candidate.label);
|
|
12495
|
+
}
|
|
12496
|
+
}
|
|
12497
|
+
return missing;
|
|
12498
|
+
}
|
|
12040
12499
|
function isInteger(value) {
|
|
12041
12500
|
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
|
|
12042
12501
|
}
|
|
@@ -12385,7 +12844,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
12385
12844
|
}
|
|
12386
12845
|
function flattenTokens(obj, prefix, target, errors) {
|
|
12387
12846
|
for (const [key, value] of Object.entries(obj)) {
|
|
12388
|
-
const
|
|
12847
|
+
const path64 = `${prefix}.${key}`;
|
|
12389
12848
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
12390
12849
|
const record2 = value;
|
|
12391
12850
|
if ("$value" in record2) {
|
|
@@ -12401,9 +12860,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
12401
12860
|
if (typeof record2.platform === "string") {
|
|
12402
12861
|
token.platform = record2.platform;
|
|
12403
12862
|
}
|
|
12404
|
-
target.set(
|
|
12863
|
+
target.set(path64, token);
|
|
12405
12864
|
} else {
|
|
12406
|
-
flattenTokens(record2,
|
|
12865
|
+
flattenTokens(record2, path64, target, errors);
|
|
12407
12866
|
}
|
|
12408
12867
|
}
|
|
12409
12868
|
}
|
|
@@ -12413,44 +12872,44 @@ function resolveAllReferences(result) {
|
|
|
12413
12872
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
12414
12873
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
12415
12874
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
12416
|
-
for (const [
|
|
12417
|
-
resolveTokenRef(
|
|
12875
|
+
for (const [path64] of allTokens) {
|
|
12876
|
+
resolveTokenRef(path64, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
12418
12877
|
}
|
|
12419
12878
|
}
|
|
12420
|
-
function resolveTokenRef(
|
|
12421
|
-
if (result.resolved.has(
|
|
12422
|
-
return result.resolved.get(
|
|
12879
|
+
function resolveTokenRef(path64, allTokens, visited, depth, result) {
|
|
12880
|
+
if (result.resolved.has(path64)) {
|
|
12881
|
+
return result.resolved.get(path64);
|
|
12423
12882
|
}
|
|
12424
12883
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
12425
12884
|
result.errors.push({
|
|
12426
|
-
message: `Max reference depth exceeded at: ${
|
|
12427
|
-
path:
|
|
12885
|
+
message: `Max reference depth exceeded at: ${path64}`,
|
|
12886
|
+
path: path64
|
|
12428
12887
|
});
|
|
12429
12888
|
return void 0;
|
|
12430
12889
|
}
|
|
12431
|
-
if (visited.has(
|
|
12890
|
+
if (visited.has(path64)) {
|
|
12432
12891
|
result.errors.push({
|
|
12433
|
-
message: `Circular reference detected: ${
|
|
12434
|
-
path:
|
|
12892
|
+
message: `Circular reference detected: ${path64}`,
|
|
12893
|
+
path: path64
|
|
12435
12894
|
});
|
|
12436
12895
|
return void 0;
|
|
12437
12896
|
}
|
|
12438
|
-
const token = allTokens.get(
|
|
12897
|
+
const token = allTokens.get(path64);
|
|
12439
12898
|
if (!token) {
|
|
12440
12899
|
return void 0;
|
|
12441
12900
|
}
|
|
12442
12901
|
if (typeof token.$value !== "string") {
|
|
12443
12902
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
12444
|
-
result.resolved.set(
|
|
12903
|
+
result.resolved.set(path64, rawValue2);
|
|
12445
12904
|
return rawValue2;
|
|
12446
12905
|
}
|
|
12447
12906
|
const rawValue = stringifyTokenValue(token.$value);
|
|
12448
12907
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
12449
12908
|
if (refs.length === 0) {
|
|
12450
|
-
result.resolved.set(
|
|
12909
|
+
result.resolved.set(path64, rawValue);
|
|
12451
12910
|
return rawValue;
|
|
12452
12911
|
}
|
|
12453
|
-
visited.add(
|
|
12912
|
+
visited.add(path64);
|
|
12454
12913
|
let resolved = rawValue;
|
|
12455
12914
|
for (const ref of refs) {
|
|
12456
12915
|
const refPath = ref[1];
|
|
@@ -12458,8 +12917,8 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
|
|
|
12458
12917
|
const refToken = allTokens.get(refPath);
|
|
12459
12918
|
if (!refToken) {
|
|
12460
12919
|
result.errors.push({
|
|
12461
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
12462
|
-
path:
|
|
12920
|
+
message: `Unresolved token reference: {${refPath}} at ${path64}`,
|
|
12921
|
+
path: path64
|
|
12463
12922
|
});
|
|
12464
12923
|
continue;
|
|
12465
12924
|
}
|
|
@@ -12468,7 +12927,7 @@ function resolveTokenRef(path62, allTokens, visited, depth, result) {
|
|
|
12468
12927
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
12469
12928
|
}
|
|
12470
12929
|
}
|
|
12471
|
-
result.resolved.set(
|
|
12930
|
+
result.resolved.set(path64, resolved);
|
|
12472
12931
|
return resolved;
|
|
12473
12932
|
}
|
|
12474
12933
|
function stringifyTokenValue(value) {
|
|
@@ -15580,6 +16039,7 @@ async function validateNavigationFlow(root, config) {
|
|
|
15580
16039
|
|
|
15581
16040
|
// src/core/validators/renderCritique.ts
|
|
15582
16041
|
import path54 from "path";
|
|
16042
|
+
import { readFile as readFile40 } from "fs/promises";
|
|
15583
16043
|
import fg9 from "fast-glob";
|
|
15584
16044
|
var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
|
|
15585
16045
|
var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
|
|
@@ -15611,6 +16071,7 @@ async function validateRenderCritique(root, config) {
|
|
|
15611
16071
|
if (!hasDdp) return issues;
|
|
15612
16072
|
const skillsDir = path54.join(root, config.paths.skillsDir).replace(/\\/g, "/");
|
|
15613
16073
|
const evidenceDir = path54.join(root, ".qfai", "evidence").replace(/\\/g, "/");
|
|
16074
|
+
const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
|
|
15614
16075
|
const skillPromptPattern = path54.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
15615
16076
|
const skillFiles = await fg9(skillPromptPattern, { dot: true });
|
|
15616
16077
|
const evidencePattern = path54.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
|
|
@@ -15650,7 +16111,7 @@ async function validateRenderCritique(root, config) {
|
|
|
15650
16111
|
}
|
|
15651
16112
|
}
|
|
15652
16113
|
const allEvidenceContent = await collectContent(evidenceFiles);
|
|
15653
|
-
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent)) {
|
|
16114
|
+
if (evidenceFiles.length > 0 && !DESKTOP_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("desktop")) {
|
|
15654
16115
|
issues.push(
|
|
15655
16116
|
issue(
|
|
15656
16117
|
"QFAI-CRIT-003",
|
|
@@ -15664,7 +16125,7 @@ async function validateRenderCritique(root, config) {
|
|
|
15664
16125
|
)
|
|
15665
16126
|
);
|
|
15666
16127
|
}
|
|
15667
|
-
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent)) {
|
|
16128
|
+
if (evidenceFiles.length > 0 && !MOBILE_RE.test(allEvidenceContent) && !renderEvidenceViewports.has("mobile")) {
|
|
15668
16129
|
issues.push(
|
|
15669
16130
|
issue(
|
|
15670
16131
|
"QFAI-CRIT-004",
|
|
@@ -15825,9 +16286,49 @@ async function collectContent(files) {
|
|
|
15825
16286
|
}
|
|
15826
16287
|
return contents.join("\n---\n");
|
|
15827
16288
|
}
|
|
16289
|
+
async function collectRenderEvidenceViewports(root) {
|
|
16290
|
+
const prototypingJsonPath = path54.join(root, ".qfai", "evidence", "prototyping.json");
|
|
16291
|
+
try {
|
|
16292
|
+
const raw = await readFile40(prototypingJsonPath, "utf-8");
|
|
16293
|
+
const parsed = JSON.parse(raw);
|
|
16294
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16295
|
+
return /* @__PURE__ */ new Set();
|
|
16296
|
+
}
|
|
16297
|
+
const uiFidelity = parsed.uiFidelity;
|
|
16298
|
+
if (!uiFidelity || typeof uiFidelity !== "object" || Array.isArray(uiFidelity)) {
|
|
16299
|
+
return /* @__PURE__ */ new Set();
|
|
16300
|
+
}
|
|
16301
|
+
const screens = uiFidelity.screens;
|
|
16302
|
+
if (!Array.isArray(screens)) {
|
|
16303
|
+
return /* @__PURE__ */ new Set();
|
|
16304
|
+
}
|
|
16305
|
+
const viewports = /* @__PURE__ */ new Set();
|
|
16306
|
+
for (const screen of screens) {
|
|
16307
|
+
if (!screen || typeof screen !== "object" || Array.isArray(screen)) {
|
|
16308
|
+
continue;
|
|
16309
|
+
}
|
|
16310
|
+
const renders = screen.renders;
|
|
16311
|
+
if (!Array.isArray(renders)) {
|
|
16312
|
+
continue;
|
|
16313
|
+
}
|
|
16314
|
+
for (const render of renders) {
|
|
16315
|
+
if (!render || typeof render !== "object" || Array.isArray(render)) {
|
|
16316
|
+
continue;
|
|
16317
|
+
}
|
|
16318
|
+
const viewport = render.viewport;
|
|
16319
|
+
if (typeof viewport === "string" && viewport.trim().length > 0) {
|
|
16320
|
+
viewports.add(viewport.trim().toLowerCase());
|
|
16321
|
+
}
|
|
16322
|
+
}
|
|
16323
|
+
}
|
|
16324
|
+
return viewports;
|
|
16325
|
+
} catch {
|
|
16326
|
+
return /* @__PURE__ */ new Set();
|
|
16327
|
+
}
|
|
16328
|
+
}
|
|
15828
16329
|
|
|
15829
16330
|
// src/core/validators/designFidelity.ts
|
|
15830
|
-
import { readFile as
|
|
16331
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
15831
16332
|
import path55 from "path";
|
|
15832
16333
|
import fg10 from "fast-glob";
|
|
15833
16334
|
var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
|
|
@@ -15860,7 +16361,7 @@ async function validateDesignFidelity(root, config) {
|
|
|
15860
16361
|
for (const filePath of allFiles) {
|
|
15861
16362
|
let content;
|
|
15862
16363
|
try {
|
|
15863
|
-
content = await
|
|
16364
|
+
content = await readFile41(filePath, "utf-8");
|
|
15864
16365
|
} catch {
|
|
15865
16366
|
continue;
|
|
15866
16367
|
}
|
|
@@ -16483,6 +16984,252 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
16483
16984
|
return issues;
|
|
16484
16985
|
}
|
|
16485
16986
|
|
|
16987
|
+
// src/core/validators/designAudit.ts
|
|
16988
|
+
import { readdir as readdir11 } from "fs/promises";
|
|
16989
|
+
import path57 from "path";
|
|
16990
|
+
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
16991
|
+
function resolveAuditConfig(config) {
|
|
16992
|
+
const audit = config.uiux?.audit;
|
|
16993
|
+
const profile = config.uiux?.qualityProfile ?? "default";
|
|
16994
|
+
return {
|
|
16995
|
+
enabled: audit?.enabled ?? true,
|
|
16996
|
+
slopDetection: audit?.slopDetection ?? true,
|
|
16997
|
+
qualityProfile: profile,
|
|
16998
|
+
maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
|
|
16999
|
+
maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
|
|
17000
|
+
maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
|
|
17001
|
+
};
|
|
17002
|
+
}
|
|
17003
|
+
function mapSeverity(tier, profile, category) {
|
|
17004
|
+
if (tier === 1) return "error";
|
|
17005
|
+
if (tier === 2) return profile === "strict" ? "error" : "warning";
|
|
17006
|
+
if (profile === "default") {
|
|
17007
|
+
return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
|
|
17008
|
+
}
|
|
17009
|
+
return "warning";
|
|
17010
|
+
}
|
|
17011
|
+
function findingToIssue(finding, profile, rulePrefix = "audit") {
|
|
17012
|
+
const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
|
|
17013
|
+
return issue(
|
|
17014
|
+
finding.ruleId,
|
|
17015
|
+
finding.message,
|
|
17016
|
+
severity,
|
|
17017
|
+
finding.file,
|
|
17018
|
+
`${rulePrefix}.${finding.dimension}`,
|
|
17019
|
+
finding.evidence.length > 0 ? finding.evidence : void 0,
|
|
17020
|
+
"compatibility",
|
|
17021
|
+
finding.guidance
|
|
17022
|
+
);
|
|
17023
|
+
}
|
|
17024
|
+
function extractSection2(content, heading) {
|
|
17025
|
+
const idx = content.indexOf(heading);
|
|
17026
|
+
if (idx === -1) return null;
|
|
17027
|
+
const start = idx + heading.length;
|
|
17028
|
+
const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
|
|
17029
|
+
const rest = content.slice(start);
|
|
17030
|
+
const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
|
|
17031
|
+
const nextHeadingMatch = headingPattern.exec(rest);
|
|
17032
|
+
const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
17033
|
+
return sectionContent.trim() || null;
|
|
17034
|
+
}
|
|
17035
|
+
function checkCtaHierarchy(content, auditConfig, file) {
|
|
17036
|
+
const findings = [];
|
|
17037
|
+
const ctaSection = extractSection2(content, "### CTA Hierarchy");
|
|
17038
|
+
if (!ctaSection) return findings;
|
|
17039
|
+
const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
|
|
17040
|
+
const primaryCount = primaryLines.length;
|
|
17041
|
+
if (primaryCount === 0) {
|
|
17042
|
+
findings.push({
|
|
17043
|
+
ruleId: "QFAI-AUD-001",
|
|
17044
|
+
dimension: "visualHierarchy",
|
|
17045
|
+
severityTier: 1,
|
|
17046
|
+
message: "No primary CTA defined in CTA Hierarchy",
|
|
17047
|
+
why: "Every UI screen needs a clear primary action to guide users",
|
|
17048
|
+
evidence: [],
|
|
17049
|
+
guidance: "Define at least one primary CTA in the CTA Hierarchy section",
|
|
17050
|
+
file
|
|
17051
|
+
});
|
|
17052
|
+
}
|
|
17053
|
+
if (primaryCount > auditConfig.maxPrimaryCtas) {
|
|
17054
|
+
findings.push({
|
|
17055
|
+
ruleId: "QFAI-AUD-020",
|
|
17056
|
+
dimension: "visualHierarchy",
|
|
17057
|
+
severityTier: 2,
|
|
17058
|
+
message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
|
|
17059
|
+
why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
|
|
17060
|
+
evidence: primaryLines.map((l) => l.trim()),
|
|
17061
|
+
guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
|
|
17062
|
+
file
|
|
17063
|
+
});
|
|
17064
|
+
}
|
|
17065
|
+
return findings;
|
|
17066
|
+
}
|
|
17067
|
+
var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
|
|
17068
|
+
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
17069
|
+
const findings = [];
|
|
17070
|
+
const configuredDir = cfg.uiux?.designTokensDir;
|
|
17071
|
+
const tokensDir = configuredDir ? path57.resolve(root, configuredDir) : path57.join(root, cfg.paths.contractsDir, "design");
|
|
17072
|
+
let hasTokenFiles = false;
|
|
17073
|
+
try {
|
|
17074
|
+
const entries = await readdir11(tokensDir);
|
|
17075
|
+
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
17076
|
+
} catch {
|
|
17077
|
+
return findings;
|
|
17078
|
+
}
|
|
17079
|
+
if (!hasTokenFiles) return findings;
|
|
17080
|
+
const contractsUiDir = path57.join(root, cfg.paths.contractsDir, "ui");
|
|
17081
|
+
let htmlFiles = [];
|
|
17082
|
+
try {
|
|
17083
|
+
const entries = await readdir11(contractsUiDir);
|
|
17084
|
+
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
17085
|
+
} catch {
|
|
17086
|
+
return findings;
|
|
17087
|
+
}
|
|
17088
|
+
let rawCount = 0;
|
|
17089
|
+
const sampleLiterals = [];
|
|
17090
|
+
for (const htmlFile of htmlFiles) {
|
|
17091
|
+
const content = await readSafe(path57.join(contractsUiDir, htmlFile));
|
|
17092
|
+
if (!content) continue;
|
|
17093
|
+
const matches = content.match(RAW_COLOR_RE);
|
|
17094
|
+
if (matches) {
|
|
17095
|
+
rawCount += matches.length;
|
|
17096
|
+
for (const m of matches) {
|
|
17097
|
+
if (sampleLiterals.length < 10) {
|
|
17098
|
+
sampleLiterals.push(m.toLowerCase());
|
|
17099
|
+
}
|
|
17100
|
+
}
|
|
17101
|
+
}
|
|
17102
|
+
}
|
|
17103
|
+
if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
|
|
17104
|
+
findings.push({
|
|
17105
|
+
ruleId: "QFAI-AUD-004",
|
|
17106
|
+
dimension: "tokenDiscipline",
|
|
17107
|
+
severityTier: 1,
|
|
17108
|
+
message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
|
|
17109
|
+
why: "Raw color values bypass design tokens, causing visual inconsistency",
|
|
17110
|
+
evidence: sampleLiterals,
|
|
17111
|
+
guidance: "Replace raw color literals with design token references"
|
|
17112
|
+
});
|
|
17113
|
+
}
|
|
17114
|
+
return findings;
|
|
17115
|
+
}
|
|
17116
|
+
function deduplicateFindings(issues, maxPerRule) {
|
|
17117
|
+
const counts = /* @__PURE__ */ new Map();
|
|
17118
|
+
const result = [];
|
|
17119
|
+
for (const iss of issues) {
|
|
17120
|
+
const count = counts.get(iss.code) ?? 0;
|
|
17121
|
+
if (count < maxPerRule) {
|
|
17122
|
+
result.push(iss);
|
|
17123
|
+
}
|
|
17124
|
+
counts.set(iss.code, count + 1);
|
|
17125
|
+
}
|
|
17126
|
+
for (const [code, count] of counts) {
|
|
17127
|
+
if (count > maxPerRule) {
|
|
17128
|
+
result.push({
|
|
17129
|
+
code,
|
|
17130
|
+
severity: "info",
|
|
17131
|
+
category: "compatibility",
|
|
17132
|
+
message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
|
|
17133
|
+
rule: `audit.dedup.${code}`
|
|
17134
|
+
});
|
|
17135
|
+
}
|
|
17136
|
+
}
|
|
17137
|
+
return result;
|
|
17138
|
+
}
|
|
17139
|
+
async function validateDesignAudit(root, config) {
|
|
17140
|
+
const auditConfig = resolveAuditConfig(config);
|
|
17141
|
+
if (!auditConfig.enabled) return [];
|
|
17142
|
+
const discussionDir = path57.join(root, config.paths.discussionDir);
|
|
17143
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
17144
|
+
if (!packRoot) return [];
|
|
17145
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
17146
|
+
if (!uiBearing) return [];
|
|
17147
|
+
const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
|
|
17148
|
+
const content = await readSafe(storyPath);
|
|
17149
|
+
if (!content) return [];
|
|
17150
|
+
const findings = [];
|
|
17151
|
+
findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
|
|
17152
|
+
findings.push(...await checkTokenDrift(root, auditConfig, config));
|
|
17153
|
+
const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
|
|
17154
|
+
return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
|
|
17155
|
+
}
|
|
17156
|
+
|
|
17157
|
+
// src/core/validators/designSlop.ts
|
|
17158
|
+
import { existsSync as existsSync2 } from "fs";
|
|
17159
|
+
import { readFile as readFile42 } from "fs/promises";
|
|
17160
|
+
import path58 from "path";
|
|
17161
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
17162
|
+
function isValidSlopPattern(rule) {
|
|
17163
|
+
if (typeof rule !== "object" || rule === null) return false;
|
|
17164
|
+
const r = rule;
|
|
17165
|
+
return typeof r.id === "string" && typeof r.category === "string" && typeof r.tier === "number" && Array.isArray(r.scopes) && typeof r.match === "string" && typeof r.message === "string" && typeof r.guidance === "string";
|
|
17166
|
+
}
|
|
17167
|
+
async function loadSlopPatterns(jsonPath) {
|
|
17168
|
+
const raw = await readFile42(jsonPath, "utf-8");
|
|
17169
|
+
const parsed = JSON.parse(raw);
|
|
17170
|
+
if (!Array.isArray(parsed)) return [];
|
|
17171
|
+
return parsed.filter((r) => isValidSlopPattern(r));
|
|
17172
|
+
}
|
|
17173
|
+
function defaultPatternsPath() {
|
|
17174
|
+
const base = import.meta.url;
|
|
17175
|
+
const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
|
|
17176
|
+
const baseDir = path58.dirname(basePath);
|
|
17177
|
+
const candidates = [
|
|
17178
|
+
path58.join(baseDir, "designSlopPatterns.json"),
|
|
17179
|
+
path58.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
17180
|
+
path58.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
17181
|
+
];
|
|
17182
|
+
for (const c of candidates) {
|
|
17183
|
+
if (existsSync2(c)) return c;
|
|
17184
|
+
}
|
|
17185
|
+
return candidates[0];
|
|
17186
|
+
}
|
|
17187
|
+
async function validateDesignSlop(root, config) {
|
|
17188
|
+
const auditConfig = resolveAuditConfig(config);
|
|
17189
|
+
if (!auditConfig.enabled) return [];
|
|
17190
|
+
if (!auditConfig.slopDetection) return [];
|
|
17191
|
+
const discussionDir = path58.join(root, config.paths.discussionDir);
|
|
17192
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
17193
|
+
if (!packRoot) return [];
|
|
17194
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
17195
|
+
if (!uiBearing) return [];
|
|
17196
|
+
let patterns;
|
|
17197
|
+
try {
|
|
17198
|
+
patterns = await loadSlopPatterns(defaultPatternsPath());
|
|
17199
|
+
} catch {
|
|
17200
|
+
return [];
|
|
17201
|
+
}
|
|
17202
|
+
const findings = [];
|
|
17203
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
17204
|
+
for (const pattern of patterns) {
|
|
17205
|
+
let regex;
|
|
17206
|
+
try {
|
|
17207
|
+
regex = new RegExp(pattern.match, "gi");
|
|
17208
|
+
} catch {
|
|
17209
|
+
continue;
|
|
17210
|
+
}
|
|
17211
|
+
for (const scope of pattern.scopes) {
|
|
17212
|
+
const filePath = path58.join(packRoot, scope);
|
|
17213
|
+
const content = await readSafe(filePath);
|
|
17214
|
+
if (!content) continue;
|
|
17215
|
+
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
17216
|
+
seenRules.add(pattern.id);
|
|
17217
|
+
findings.push({
|
|
17218
|
+
ruleId: pattern.id,
|
|
17219
|
+
dimension: pattern.category,
|
|
17220
|
+
severityTier: pattern.tier,
|
|
17221
|
+
message: pattern.message,
|
|
17222
|
+
why: `Slop pattern "${pattern.id}" matched in ${scope}`,
|
|
17223
|
+
evidence: [],
|
|
17224
|
+
guidance: pattern.guidance,
|
|
17225
|
+
file: scope
|
|
17226
|
+
});
|
|
17227
|
+
}
|
|
17228
|
+
}
|
|
17229
|
+
}
|
|
17230
|
+
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
17231
|
+
}
|
|
17232
|
+
|
|
16486
17233
|
// src/core/validate.ts
|
|
16487
17234
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
16488
17235
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -16500,7 +17247,9 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
16500
17247
|
() => validateBpApDb(root, config),
|
|
16501
17248
|
() => validateUiDefinitionConsistency(root, config),
|
|
16502
17249
|
() => validateResearchSummary(root, config),
|
|
16503
|
-
() => validateAgentDefinition(root, config)
|
|
17250
|
+
() => validateAgentDefinition(root, config),
|
|
17251
|
+
() => validateDesignAudit(root, config),
|
|
17252
|
+
() => validateDesignSlop(root, config)
|
|
16504
17253
|
];
|
|
16505
17254
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
16506
17255
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -16587,15 +17336,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
16587
17336
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
16588
17337
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
16589
17338
|
async function createReportData(root, validation, configResult) {
|
|
16590
|
-
const resolvedRoot =
|
|
17339
|
+
const resolvedRoot = path59.resolve(root);
|
|
16591
17340
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
16592
17341
|
const config = resolved.config;
|
|
16593
17342
|
const configPath = resolved.configPath;
|
|
16594
17343
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
16595
17344
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
16596
|
-
const apiRoot =
|
|
16597
|
-
const uiRoot =
|
|
16598
|
-
const dbRoot =
|
|
17345
|
+
const apiRoot = path59.join(contractsRoot, "api");
|
|
17346
|
+
const uiRoot = path59.join(contractsRoot, "ui");
|
|
17347
|
+
const dbRoot = path59.join(contractsRoot, "db");
|
|
16599
17348
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
16600
17349
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
16601
17350
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -16923,6 +17672,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16923
17672
|
lines.push("");
|
|
16924
17673
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
16925
17674
|
lines.push("- [Change Issues](#change-issues)");
|
|
17675
|
+
lines.push("- [Design Audit Findings](#design-audit-findings)");
|
|
17676
|
+
lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
|
|
16926
17677
|
lines.push("- [Change Type](#change-type)");
|
|
16927
17678
|
lines.push("- [Waivers](#waivers)");
|
|
16928
17679
|
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
@@ -17071,6 +17822,46 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
17071
17822
|
lines.push("### Issues");
|
|
17072
17823
|
lines.push("");
|
|
17073
17824
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
17825
|
+
const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
|
|
17826
|
+
if (auditIssues.length > 0) {
|
|
17827
|
+
lines.push("## Design Audit Findings");
|
|
17828
|
+
lines.push("");
|
|
17829
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
17830
|
+
for (const iss of auditIssues) {
|
|
17831
|
+
const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
|
|
17832
|
+
const group = byDimension.get(dim) ?? [];
|
|
17833
|
+
group.push(iss);
|
|
17834
|
+
byDimension.set(dim, group);
|
|
17835
|
+
}
|
|
17836
|
+
for (const [dim, dimIssues] of byDimension) {
|
|
17837
|
+
lines.push(`### ${dim}`);
|
|
17838
|
+
lines.push("");
|
|
17839
|
+
for (const iss of dimIssues) {
|
|
17840
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
17841
|
+
}
|
|
17842
|
+
lines.push("");
|
|
17843
|
+
}
|
|
17844
|
+
}
|
|
17845
|
+
const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
|
|
17846
|
+
if (slopIssues.length > 0) {
|
|
17847
|
+
lines.push("## Slop Guardrails Findings");
|
|
17848
|
+
lines.push("");
|
|
17849
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
17850
|
+
for (const iss of slopIssues) {
|
|
17851
|
+
const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
|
|
17852
|
+
const group = byCategory.get(cat) ?? [];
|
|
17853
|
+
group.push(iss);
|
|
17854
|
+
byCategory.set(cat, group);
|
|
17855
|
+
}
|
|
17856
|
+
for (const [cat, catIssues] of byCategory) {
|
|
17857
|
+
lines.push(`### ${cat}`);
|
|
17858
|
+
lines.push("");
|
|
17859
|
+
for (const iss of catIssues) {
|
|
17860
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
17861
|
+
}
|
|
17862
|
+
lines.push("");
|
|
17863
|
+
}
|
|
17864
|
+
}
|
|
17074
17865
|
lines.push("## Change Type");
|
|
17075
17866
|
lines.push("");
|
|
17076
17867
|
lines.push("### Summary");
|
|
@@ -17442,6 +18233,20 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
17442
18233
|
} else {
|
|
17443
18234
|
lines.push("- issue \u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u904B\u7528\u30C6\u30F3\u30D7\u30EC\u306B\u6CBF\u3063\u3066\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
|
|
17444
18235
|
}
|
|
18236
|
+
const renderEvidenceIssues = data.issues.filter(
|
|
18237
|
+
(item) => ["QFAI-PROT-101", "QFAI-PROT-244", "QFAI-PROT-245"].includes(item.code)
|
|
18238
|
+
);
|
|
18239
|
+
if (renderEvidenceIssues.length > 0) {
|
|
18240
|
+
lines.push(
|
|
18241
|
+
"- render evidence \u304C\u4E0D\u8DB3\u307E\u305F\u306F\u4E0D\u5B8C\u5168\u3067\u3059\u3002viewport coverage \u3068 artifact path \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
18242
|
+
);
|
|
18243
|
+
lines.push(
|
|
18244
|
+
"- recover: `qfai prototyping --autogen-ui-fidelity --render-evidence --viewports desktop,mobile` \u3092\u5B9F\u884C\u3057\u3001`.qfai/evidence/prototyping.json` \u3068 render bundle \u3092\u66F4\u65B0\u3057\u307E\u3059\u3002"
|
|
18245
|
+
);
|
|
18246
|
+
lines.push(
|
|
18247
|
+
"- why it matters: render evidence \u306F viewport coverage \u3068 missing artifact \u306E\u5207\u308A\u5206\u3051\u306B\u4F7F\u308F\u308C\u3001strict/high profile \u3067\u306F gate \u306B\u5F71\u97FF\u3057\u307E\u3059\u3002"
|
|
18248
|
+
);
|
|
18249
|
+
}
|
|
17445
18250
|
lines.push("- \u5909\u66F4\u5185\u5BB9\u30FB\u53D7\u5165\u89B3\u70B9\u306F `.qfai/specs/*/18_delta.md` \u306B\u8A18\u9332\u3057\u307E\u3059\u3002");
|
|
17446
18251
|
lines.push("- \u53C2\u7167\u30EB\u30FC\u30EB\u306E\u6B63\u672C: `.qfai/assistant/instructions/constitution.md`");
|
|
17447
18252
|
return lines.join("\n");
|
|
@@ -17476,7 +18281,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
17476
18281
|
};
|
|
17477
18282
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
17478
18283
|
for (const deltaFile of deltaFiles) {
|
|
17479
|
-
const text = await
|
|
18284
|
+
const text = await readFile43(deltaFile, "utf-8");
|
|
17480
18285
|
const parsed = parseDeltaV1(text);
|
|
17481
18286
|
for (const entry of parsed.entries) {
|
|
17482
18287
|
if (!entry.meta) {
|
|
@@ -17513,7 +18318,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
17513
18318
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
17514
18319
|
}
|
|
17515
18320
|
for (const file of specFiles) {
|
|
17516
|
-
const text = await
|
|
18321
|
+
const text = await readFile43(file, "utf-8");
|
|
17517
18322
|
const parsed = parseSpec(text, file);
|
|
17518
18323
|
const specKey = parsed.specId;
|
|
17519
18324
|
if (!specKey) {
|
|
@@ -17550,7 +18355,7 @@ async function collectIds(files) {
|
|
|
17550
18355
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
17551
18356
|
}
|
|
17552
18357
|
for (const file of files) {
|
|
17553
|
-
const text = await
|
|
18358
|
+
const text = await readFile43(file, "utf-8");
|
|
17554
18359
|
for (const prefix of ID_PREFIXES) {
|
|
17555
18360
|
const ids = extractIds(text, prefix);
|
|
17556
18361
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -17565,7 +18370,7 @@ async function collectIds(files) {
|
|
|
17565
18370
|
async function collectUpstreamIds(files) {
|
|
17566
18371
|
const ids = /* @__PURE__ */ new Set();
|
|
17567
18372
|
for (const file of files) {
|
|
17568
|
-
const text = await
|
|
18373
|
+
const text = await readFile43(file, "utf-8");
|
|
17569
18374
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
17570
18375
|
}
|
|
17571
18376
|
return ids;
|
|
@@ -17586,7 +18391,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
17586
18391
|
}
|
|
17587
18392
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
17588
18393
|
for (const file of targetFiles) {
|
|
17589
|
-
const text = await
|
|
18394
|
+
const text = await readFile43(file, "utf-8");
|
|
17590
18395
|
if (pattern.test(text)) {
|
|
17591
18396
|
return true;
|
|
17592
18397
|
}
|
|
@@ -17705,7 +18510,7 @@ function normalizeScSources(root, sources) {
|
|
|
17705
18510
|
async function countScenarios(scenarioFiles) {
|
|
17706
18511
|
let total = 0;
|
|
17707
18512
|
for (const file of scenarioFiles) {
|
|
17708
|
-
const text = await
|
|
18513
|
+
const text = await readFile43(file, "utf-8");
|
|
17709
18514
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
17710
18515
|
if (!document || errors.length > 0) {
|
|
17711
18516
|
continue;
|
|
@@ -17736,7 +18541,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
17736
18541
|
let totalScenarios = 0;
|
|
17737
18542
|
let e2eCount = 0;
|
|
17738
18543
|
for (const file of scenarioFiles) {
|
|
17739
|
-
const text = await
|
|
18544
|
+
const text = await readFile43(file, "utf-8");
|
|
17740
18545
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
17741
18546
|
if (!document || errors.length > 0) {
|
|
17742
18547
|
continue;
|
|
@@ -17824,10 +18629,10 @@ function buildHotspots(issues) {
|
|
|
17824
18629
|
async function collectTddCoverage(entries) {
|
|
17825
18630
|
const specs = [];
|
|
17826
18631
|
for (const entry of entries) {
|
|
17827
|
-
const testCasesPath =
|
|
18632
|
+
const testCasesPath = path59.join(entry.dir, "06_Test-Cases.md");
|
|
17828
18633
|
let tcContent;
|
|
17829
18634
|
try {
|
|
17830
|
-
tcContent = await
|
|
18635
|
+
tcContent = await readFile43(testCasesPath, "utf-8");
|
|
17831
18636
|
} catch {
|
|
17832
18637
|
continue;
|
|
17833
18638
|
}
|
|
@@ -17859,10 +18664,10 @@ async function collectTddCoverage(entries) {
|
|
|
17859
18664
|
});
|
|
17860
18665
|
continue;
|
|
17861
18666
|
}
|
|
17862
|
-
const tddListPath =
|
|
18667
|
+
const tddListPath = path59.join(entry.dir, "tdd", "test-list.md");
|
|
17863
18668
|
let tddContent;
|
|
17864
18669
|
try {
|
|
17865
|
-
tddContent = await
|
|
18670
|
+
tddContent = await readFile43(tddListPath, "utf-8");
|
|
17866
18671
|
} catch {
|
|
17867
18672
|
specs.push({
|
|
17868
18673
|
specNumber: entry.specNumber,
|
|
@@ -17945,8 +18750,8 @@ async function collectTddCoverage(entries) {
|
|
|
17945
18750
|
}
|
|
17946
18751
|
|
|
17947
18752
|
// src/core/specPackReport.ts
|
|
17948
|
-
import { mkdir as mkdir7, readFile as
|
|
17949
|
-
import
|
|
18753
|
+
import { mkdir as mkdir7, readFile as readFile44, writeFile as writeFile6 } from "fs/promises";
|
|
18754
|
+
import path60 from "path";
|
|
17950
18755
|
var REQUIRED_LEDGER_COLUMNS = [
|
|
17951
18756
|
"trace_id",
|
|
17952
18757
|
"obj_id",
|
|
@@ -17964,8 +18769,8 @@ async function writeSpecPackReports(root, config) {
|
|
|
17964
18769
|
const entries = await collectSpecEntries(specsRoot);
|
|
17965
18770
|
const contractIndex = await buildContractIndex(root, config);
|
|
17966
18771
|
for (const entry of entries) {
|
|
17967
|
-
const specName =
|
|
17968
|
-
const outputDir =
|
|
18772
|
+
const specName = path60.basename(entry.dir);
|
|
18773
|
+
const outputDir = path60.join(outRoot, specName);
|
|
17969
18774
|
await mkdir7(outputDir, { recursive: true });
|
|
17970
18775
|
const [acText, tcText, exText, ledgerText] = await Promise.all([
|
|
17971
18776
|
readSafe12(entry.acceptanceCriteriaPath),
|
|
@@ -17992,13 +18797,13 @@ async function writeSpecPackReports(root, config) {
|
|
|
17992
18797
|
});
|
|
17993
18798
|
const graph = buildTraceabilityGraph(ledgerRows);
|
|
17994
18799
|
await writeFile6(
|
|
17995
|
-
|
|
18800
|
+
path60.join(outputDir, "coverage.md"),
|
|
17996
18801
|
`${formatCoverageMarkdown(specName, coverage)}
|
|
17997
18802
|
`,
|
|
17998
18803
|
"utf-8"
|
|
17999
18804
|
);
|
|
18000
18805
|
await writeFile6(
|
|
18001
|
-
|
|
18806
|
+
path60.join(outputDir, "traceability-graph.json"),
|
|
18002
18807
|
`${JSON.stringify(graph, null, 2)}
|
|
18003
18808
|
`,
|
|
18004
18809
|
"utf-8"
|
|
@@ -18173,7 +18978,7 @@ function getCell(row, indexByColumn, column) {
|
|
|
18173
18978
|
}
|
|
18174
18979
|
async function readSafe12(filePath) {
|
|
18175
18980
|
try {
|
|
18176
|
-
return await
|
|
18981
|
+
return await readFile44(filePath, "utf-8");
|
|
18177
18982
|
} catch {
|
|
18178
18983
|
return "";
|
|
18179
18984
|
}
|
|
@@ -18191,7 +18996,7 @@ function warnIfTruncated(scan, context) {
|
|
|
18191
18996
|
|
|
18192
18997
|
// src/cli/commands/report.ts
|
|
18193
18998
|
async function runReport(options) {
|
|
18194
|
-
const root =
|
|
18999
|
+
const root = path61.resolve(options.root);
|
|
18195
19000
|
const configResult = await loadConfig(root);
|
|
18196
19001
|
let validation;
|
|
18197
19002
|
let blockedByPhaseGuard = false;
|
|
@@ -18207,7 +19012,7 @@ async function runReport(options) {
|
|
|
18207
19012
|
validation = normalized;
|
|
18208
19013
|
} else {
|
|
18209
19014
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
18210
|
-
const inputPath =
|
|
19015
|
+
const inputPath = path61.isAbsolute(input) ? input : path61.resolve(root, input);
|
|
18211
19016
|
try {
|
|
18212
19017
|
validation = await readValidationResult(inputPath);
|
|
18213
19018
|
} catch (err) {
|
|
@@ -18234,10 +19039,10 @@ async function runReport(options) {
|
|
|
18234
19039
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
18235
19040
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
18236
19041
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
18237
|
-
const defaultOut = options.format === "json" ?
|
|
19042
|
+
const defaultOut = options.format === "json" ? path61.join(outRoot, "report.json") : path61.join(outRoot, "report.md");
|
|
18238
19043
|
const out = options.outPath ?? defaultOut;
|
|
18239
|
-
const outPath =
|
|
18240
|
-
await mkdir8(
|
|
19044
|
+
const outPath = path61.isAbsolute(out) ? out : path61.resolve(root, out);
|
|
19045
|
+
await mkdir8(path61.dirname(outPath), { recursive: true });
|
|
18241
19046
|
await writeFile7(outPath, `${output}
|
|
18242
19047
|
`, "utf-8");
|
|
18243
19048
|
await writeSpecPackReports(root, configResult.config);
|
|
@@ -18253,7 +19058,7 @@ async function runReport(options) {
|
|
|
18253
19058
|
info(`wrote report: ${outPath}`);
|
|
18254
19059
|
}
|
|
18255
19060
|
async function readValidationResult(inputPath) {
|
|
18256
|
-
const raw = await
|
|
19061
|
+
const raw = await readFile45(inputPath, "utf-8");
|
|
18257
19062
|
const parsed = JSON.parse(raw);
|
|
18258
19063
|
if (!isValidationResult(parsed)) {
|
|
18259
19064
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -18313,21 +19118,21 @@ function isMissingFileError2(error2) {
|
|
|
18313
19118
|
return record2.code === "ENOENT";
|
|
18314
19119
|
}
|
|
18315
19120
|
async function writeValidationResult(root, outputPath, result) {
|
|
18316
|
-
const abs =
|
|
18317
|
-
await mkdir8(
|
|
19121
|
+
const abs = path61.isAbsolute(outputPath) ? outputPath : path61.resolve(root, outputPath);
|
|
19122
|
+
await mkdir8(path61.dirname(abs), { recursive: true });
|
|
18318
19123
|
await writeFile7(abs, `${JSON.stringify(result, null, 2)}
|
|
18319
19124
|
`, "utf-8");
|
|
18320
19125
|
}
|
|
18321
19126
|
|
|
18322
19127
|
// src/cli/commands/validate.ts
|
|
18323
19128
|
import { mkdir as mkdir10, writeFile as writeFile9 } from "fs/promises";
|
|
18324
|
-
import
|
|
19129
|
+
import path63 from "path";
|
|
18325
19130
|
|
|
18326
19131
|
// src/core/runLog.ts
|
|
18327
19132
|
import { mkdir as mkdir9, writeFile as writeFile8 } from "fs/promises";
|
|
18328
|
-
import
|
|
19133
|
+
import path62 from "path";
|
|
18329
19134
|
async function writeValidateRunLog(input) {
|
|
18330
|
-
const root =
|
|
19135
|
+
const root = path62.resolve(input.root);
|
|
18331
19136
|
const outDir = resolvePath(root, input.config, "outDir");
|
|
18332
19137
|
await mkdir9(outDir, { recursive: true });
|
|
18333
19138
|
const { runId, reportDir } = await allocateRunReportDir(outDir, input.startedAt);
|
|
@@ -18374,10 +19179,10 @@ async function writeValidateRunLog(input) {
|
|
|
18374
19179
|
errors,
|
|
18375
19180
|
warnings
|
|
18376
19181
|
});
|
|
18377
|
-
await writeJson(
|
|
18378
|
-
await writeJson(
|
|
18379
|
-
await writeJson(
|
|
18380
|
-
await writeFile8(
|
|
19182
|
+
await writeJson(path62.join(reportDir, "run.json"), runJson);
|
|
19183
|
+
await writeJson(path62.join(reportDir, "validator.json"), validatorJson);
|
|
19184
|
+
await writeJson(path62.join(reportDir, "traceability.json"), traceabilityJson);
|
|
19185
|
+
await writeFile8(path62.join(reportDir, "summary.md"), `${summaryMd}
|
|
18381
19186
|
`, "utf-8");
|
|
18382
19187
|
return {
|
|
18383
19188
|
runId,
|
|
@@ -18501,7 +19306,7 @@ async function allocateRunReportDir(outDir, startedAt) {
|
|
|
18501
19306
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
18502
19307
|
const candidateDate = new Date(startedAt.getTime() + attempt);
|
|
18503
19308
|
const runId = `run-${formatTimestamp17(candidateDate)}`;
|
|
18504
|
-
const reportDir =
|
|
19309
|
+
const reportDir = path62.join(outDir, runId);
|
|
18505
19310
|
try {
|
|
18506
19311
|
await mkdir9(reportDir);
|
|
18507
19312
|
return { runId, reportDir };
|
|
@@ -18532,7 +19337,7 @@ function shouldFail(result, failOn) {
|
|
|
18532
19337
|
// src/cli/commands/validate.ts
|
|
18533
19338
|
async function runValidate(options) {
|
|
18534
19339
|
const startedAt = /* @__PURE__ */ new Date();
|
|
18535
|
-
const root =
|
|
19340
|
+
const root = path63.resolve(options.root);
|
|
18536
19341
|
const configResult = await loadConfig(root);
|
|
18537
19342
|
const blockedIssue = buildCiRefinementIssue(options.phase);
|
|
18538
19343
|
const blockedByPhaseGuard = blockedIssue !== null;
|
|
@@ -18688,12 +19493,12 @@ function issueKey(issue2) {
|
|
|
18688
19493
|
}
|
|
18689
19494
|
async function emitJson(result, root, jsonPath) {
|
|
18690
19495
|
const abs = resolveJsonPath(root, jsonPath);
|
|
18691
|
-
await mkdir10(
|
|
19496
|
+
await mkdir10(path63.dirname(abs), { recursive: true });
|
|
18692
19497
|
await writeFile9(abs, `${JSON.stringify(result, null, 2)}
|
|
18693
19498
|
`, "utf-8");
|
|
18694
19499
|
}
|
|
18695
19500
|
function resolveJsonPath(root, jsonPath) {
|
|
18696
|
-
return
|
|
19501
|
+
return path63.isAbsolute(jsonPath) ? jsonPath : path63.resolve(root, jsonPath);
|
|
18697
19502
|
}
|
|
18698
19503
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
18699
19504
|
var ISSUE_EXPECTED_BY_CODE = {
|
|
@@ -18812,6 +19617,8 @@ function parseArgs(argv, cwd) {
|
|
|
18812
19617
|
guardrailsPaths: [],
|
|
18813
19618
|
prototypingAutogen: false,
|
|
18814
19619
|
prototypingAutogenOnly: false,
|
|
19620
|
+
prototypingRenderEvidence: false,
|
|
19621
|
+
prototypingRenderViewports: [],
|
|
18815
19622
|
help: false,
|
|
18816
19623
|
invalidExitCode: 1
|
|
18817
19624
|
};
|
|
@@ -19023,6 +19830,37 @@ function parseArgs(argv, cwd) {
|
|
|
19023
19830
|
i += 1;
|
|
19024
19831
|
break;
|
|
19025
19832
|
}
|
|
19833
|
+
case "--render-evidence":
|
|
19834
|
+
if (command === "prototyping") {
|
|
19835
|
+
options.prototypingRenderEvidence = true;
|
|
19836
|
+
}
|
|
19837
|
+
break;
|
|
19838
|
+
case "--viewports": {
|
|
19839
|
+
if (command !== "prototyping") {
|
|
19840
|
+
break;
|
|
19841
|
+
}
|
|
19842
|
+
const next = readOptionValue(args, i);
|
|
19843
|
+
if (next === null) {
|
|
19844
|
+
markInvalid();
|
|
19845
|
+
break;
|
|
19846
|
+
}
|
|
19847
|
+
options.prototypingRenderViewports = next.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
19848
|
+
i += 1;
|
|
19849
|
+
break;
|
|
19850
|
+
}
|
|
19851
|
+
case "--render-out": {
|
|
19852
|
+
if (command !== "prototyping") {
|
|
19853
|
+
break;
|
|
19854
|
+
}
|
|
19855
|
+
const next = readOptionValue(args, i);
|
|
19856
|
+
if (next === null) {
|
|
19857
|
+
markInvalid();
|
|
19858
|
+
break;
|
|
19859
|
+
}
|
|
19860
|
+
options.prototypingRenderOut = next;
|
|
19861
|
+
i += 1;
|
|
19862
|
+
break;
|
|
19863
|
+
}
|
|
19026
19864
|
case "--platform": {
|
|
19027
19865
|
if (command !== "validate") {
|
|
19028
19866
|
break;
|
|
@@ -19175,7 +20013,10 @@ async function run(argv, cwd) {
|
|
|
19175
20013
|
autogenUiFidelity: options.prototypingAutogen,
|
|
19176
20014
|
autogenOnly: options.prototypingAutogenOnly,
|
|
19177
20015
|
...options.prototypingBaseUrl !== void 0 ? { baseUrl: options.prototypingBaseUrl } : {},
|
|
19178
|
-
...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {}
|
|
20016
|
+
...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {},
|
|
20017
|
+
renderEvidence: options.prototypingRenderEvidence,
|
|
20018
|
+
renderViewports: options.prototypingRenderViewports,
|
|
20019
|
+
...options.prototypingRenderOut !== void 0 ? { renderOut: options.prototypingRenderOut } : {}
|
|
19179
20020
|
});
|
|
19180
20021
|
process.exitCode = exitCode;
|
|
19181
20022
|
}
|
|
@@ -19221,6 +20062,9 @@ Options:
|
|
|
19221
20062
|
--autogen-ui-fidelity prototyping: uiFidelity \u81EA\u52D5\u751F\u6210\u3092\u6709\u52B9\u5316
|
|
19222
20063
|
--autogen-only prototyping: \u81EA\u52D5\u751F\u6210\u306E\u307F\u5B9F\u884C\uFF08\u5931\u6557\u6642exit 1\uFF09
|
|
19223
20064
|
--evidence-out <path> prototyping: \u51FA\u529B\u5148\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8 .qfai/evidence/prototyping.json\uFF09
|
|
20065
|
+
--render-evidence prototyping: render evidence \u306E\u53CE\u96C6\u3092\u6709\u52B9\u5316
|
|
20066
|
+
--viewports <list> prototyping: render \u5BFE\u8C61 viewport \u3092\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u6307\u5B9A
|
|
20067
|
+
--render-out <path> prototyping: render evidence \u306E\u51FA\u529B\u5148
|
|
19224
20068
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
19225
20069
|
|
|
19226
20070
|
Environment:
|