qfai 1.8.2 → 1.8.4
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/README.md +9 -4
- package/assets/init/.qfai/assistant/agents/product-experience-architect.md +2 -1
- package/assets/init/.qfai/assistant/skills/qfai-atdd/SKILL.md +4 -4
- package/assets/init/.qfai/assistant/skills/qfai-configure/SKILL.md +1 -1
- package/assets/init/.qfai/assistant/skills/qfai-discussion/SKILL.md +1 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/references/rcp_footer.md +1 -1
- package/assets/init/.qfai/assistant/skills/qfai-implement/SKILL.md +3 -1
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/SKILL.md +126 -62
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/references/evidence-requirements.md +43 -12
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/references/iteration-cycle.md +46 -14
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/references/l1-review-guide.md +13 -12
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/references/l2-review-guide.md +16 -10
- package/assets/init/.qfai/assistant/skills/qfai-prototyping/references/reviewer-gate.md +25 -4
- package/assets/init/.qfai/assistant/skills/qfai-sdd/SKILL.md +3 -3
- package/assets/init/.qfai/assistant/skills/qfai-sdd/references/rcp_footer.md +1 -1
- package/assets/init/.qfai/assistant/skills/qfai-sdd/references/sdd-quality-gate.md +1 -1
- package/assets/init/.qfai/assistant/skills/qfai-sdd/templates/contracts/absorption-policy.sample.yaml +7 -0
- package/assets/init/.qfai/assistant/skills/qfai-sdd/templates/contracts/evaluation-rubric.sample.yaml +22 -3
- package/assets/init/.qfai/assistant/skills/qfai-sdd/templates/contracts/evaluator-calibration.sample.yaml +6 -0
- package/assets/init/.qfai/assistant/skills/qfai-sdd/templates/contracts/exploration-brief.sample.yaml +9 -0
- package/assets/init/.qfai/assistant/skills/qfai-verify/SKILL.md +6 -6
- package/assets/init/.qfai/contracts/design/README.md +6 -1
- package/assets/init/.qfai/contracts/ui/README.md +3 -3
- package/assets/init/.qfai/discussion/README.md +14 -9
- package/assets/init/.qfai/evidence/README.md +66 -46
- package/assets/init/root/.github/workflows/qfai-validate.yml +39 -0
- package/assets/init/root/qfai.config.yaml +1 -2
- package/dist/cli/index.cjs +8787 -5641
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +5993 -2847
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +2677 -702
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -23
- package/dist/index.d.ts +102 -23
- package/dist/index.mjs +2651 -675
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/assets/scripts/capture-screenshots.js +0 -128
package/dist/index.mjs
CHANGED
|
@@ -475,7 +475,8 @@ var defaultConfig = {
|
|
|
475
475
|
requireLayerTags: false,
|
|
476
476
|
requireSizeTags: false,
|
|
477
477
|
maxE2eScenarioRatio: null,
|
|
478
|
-
maxE2eScenarioCount: null
|
|
478
|
+
maxE2eScenarioCount: null,
|
|
479
|
+
forbidTestTodoStubs: true
|
|
479
480
|
},
|
|
480
481
|
traceability: {
|
|
481
482
|
brMustHaveSc: true,
|
|
@@ -496,8 +497,7 @@ var defaultConfig = {
|
|
|
496
497
|
},
|
|
497
498
|
execution: {
|
|
498
499
|
targetUrl: null,
|
|
499
|
-
|
|
500
|
-
renderProvider: "playwright"
|
|
500
|
+
browserTool: "playwright-cli"
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
};
|
|
@@ -688,6 +688,13 @@ function normalizeValidation(raw, configPath, issues) {
|
|
|
688
688
|
"validation.testStrategy.maxE2eScenarioCount",
|
|
689
689
|
configPath,
|
|
690
690
|
issues
|
|
691
|
+
),
|
|
692
|
+
forbidTestTodoStubs: readBoolean(
|
|
693
|
+
testStrategyRaw?.forbidTestTodoStubs,
|
|
694
|
+
base.testStrategy.forbidTestTodoStubs,
|
|
695
|
+
"validation.testStrategy.forbidTestTodoStubs",
|
|
696
|
+
configPath,
|
|
697
|
+
issues
|
|
691
698
|
)
|
|
692
699
|
},
|
|
693
700
|
traceability: {
|
|
@@ -772,14 +779,40 @@ function normalizePrototyping(raw, configPath, issues) {
|
|
|
772
779
|
}
|
|
773
780
|
const calibration = normalizePrototypingCalibration(raw.calibration, configPath, issues);
|
|
774
781
|
const execution = normalizePrototypingExecution(raw.execution, configPath, issues);
|
|
775
|
-
|
|
782
|
+
const primarySpecId = normalizePrimarySpecId(raw.primarySpecId, configPath, issues);
|
|
783
|
+
if (!calibration && !execution && primarySpecId === void 0) {
|
|
776
784
|
return void 0;
|
|
777
785
|
}
|
|
778
786
|
return {
|
|
779
787
|
...calibration ? { calibration } : {},
|
|
780
|
-
...execution ? { execution } : {}
|
|
788
|
+
...execution ? { execution } : {},
|
|
789
|
+
...primarySpecId !== void 0 ? { primarySpecId } : {}
|
|
781
790
|
};
|
|
782
791
|
}
|
|
792
|
+
function normalizePrimarySpecId(raw, configPath, issues) {
|
|
793
|
+
if (raw === void 0 || raw === null) {
|
|
794
|
+
return void 0;
|
|
795
|
+
}
|
|
796
|
+
if (typeof raw !== "string") {
|
|
797
|
+
issues.push(
|
|
798
|
+
configIssue(
|
|
799
|
+
configPath,
|
|
800
|
+
'prototyping.primarySpecId \u306F4\u6841\u306E\u6587\u5B57\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044 (\u4F8B: "0012")\u3002'
|
|
801
|
+
)
|
|
802
|
+
);
|
|
803
|
+
return void 0;
|
|
804
|
+
}
|
|
805
|
+
if (!/^\d{4}$/.test(raw)) {
|
|
806
|
+
issues.push(
|
|
807
|
+
configIssue(
|
|
808
|
+
configPath,
|
|
809
|
+
`prototyping.primarySpecId \u306F4\u6841\u306E\u6570\u5B57\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059 (got "${raw}")\u3002`
|
|
810
|
+
)
|
|
811
|
+
);
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
return raw;
|
|
815
|
+
}
|
|
783
816
|
function normalizePrototypingCalibration(raw, configPath, issues) {
|
|
784
817
|
const base = defaultConfig.prototyping?.calibration;
|
|
785
818
|
if (raw === void 0 || raw === null) {
|
|
@@ -813,6 +846,30 @@ function normalizePrototypingExecution(raw, configPath, issues) {
|
|
|
813
846
|
);
|
|
814
847
|
return base ? { ...base } : void 0;
|
|
815
848
|
}
|
|
849
|
+
for (const legacyKey of ["browserProvider", "renderProvider"]) {
|
|
850
|
+
if (raw[legacyKey] !== void 0) {
|
|
851
|
+
issues.push(
|
|
852
|
+
configIssue(
|
|
853
|
+
configPath,
|
|
854
|
+
`prototyping.execution.${legacyKey} \u306F\u5EC3\u6B62\u3055\u308C\u307E\u3057\u305F (spec-0012)\u3002 prototyping.execution.browserTool: playwright-cli \u306B\u7F6E\u304D\u63DB\u3048\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const browserToolRaw = raw.browserTool;
|
|
860
|
+
let browserTool = "playwright-cli";
|
|
861
|
+
if (browserToolRaw !== void 0) {
|
|
862
|
+
if (browserToolRaw !== "playwright-cli") {
|
|
863
|
+
issues.push(
|
|
864
|
+
configIssue(
|
|
865
|
+
configPath,
|
|
866
|
+
`prototyping.execution.browserTool \u306F "playwright-cli" \u306E\u307F\u6709\u52B9\u3067\u3059 (spec-0012)\u3002 \u53D7\u3051\u53D6\u3063\u305F\u5024: ${JSON.stringify(browserToolRaw)}`
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
} else {
|
|
870
|
+
browserTool = browserToolRaw;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
816
873
|
return {
|
|
817
874
|
targetUrl: raw.targetUrl === null ? null : readOptionalString(
|
|
818
875
|
raw.targetUrl,
|
|
@@ -820,20 +877,7 @@ function normalizePrototypingExecution(raw, configPath, issues) {
|
|
|
820
877
|
configPath,
|
|
821
878
|
issues
|
|
822
879
|
) ?? null,
|
|
823
|
-
|
|
824
|
-
raw.browserProvider,
|
|
825
|
-
base?.browserProvider ?? "playwright",
|
|
826
|
-
"prototyping.execution.browserProvider",
|
|
827
|
-
configPath,
|
|
828
|
-
issues
|
|
829
|
-
),
|
|
830
|
-
renderProvider: readString(
|
|
831
|
-
raw.renderProvider,
|
|
832
|
-
base?.renderProvider ?? "playwright",
|
|
833
|
-
"prototyping.execution.renderProvider",
|
|
834
|
-
configPath,
|
|
835
|
-
issues
|
|
836
|
-
)
|
|
880
|
+
browserTool
|
|
837
881
|
};
|
|
838
882
|
}
|
|
839
883
|
function validateObsoleteCalibrationFields(raw, configPath, issues) {
|
|
@@ -3028,11 +3072,12 @@ function isValidId(value, prefix) {
|
|
|
3028
3072
|
init_surface();
|
|
3029
3073
|
var PROTOTYPING_MODES = ["low-cost", "standard", "full-harness"];
|
|
3030
3074
|
var DEFAULT_PROTOTYPING_MODE = "standard";
|
|
3031
|
-
var
|
|
3075
|
+
var PROTOTYPING_MAX_CYCLES = {
|
|
3032
3076
|
"low-cost": 1,
|
|
3033
3077
|
standard: 3,
|
|
3034
3078
|
"full-harness": 20
|
|
3035
3079
|
};
|
|
3080
|
+
var PROTOTYPING_MAX_ITERATIONS = PROTOTYPING_MAX_CYCLES;
|
|
3036
3081
|
var PROTOTYPING_SUPPORTED_SURFACES = ["web", "mobile", "desktop", "mixed"];
|
|
3037
3082
|
var VALID_MODE_SET = new Set(PROTOTYPING_MODES);
|
|
3038
3083
|
var VALID_SURFACE_SET = new Set(CANONICAL_PROTOTYPING_SURFACES);
|
|
@@ -3062,14 +3107,24 @@ function resolvePrototypingMode(requested) {
|
|
|
3062
3107
|
};
|
|
3063
3108
|
}
|
|
3064
3109
|
function derivePrototypingObligations(input) {
|
|
3065
|
-
const
|
|
3110
|
+
const maxCycles = PROTOTYPING_MAX_CYCLES[input.effectiveMode];
|
|
3066
3111
|
return {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3112
|
+
browserTool: "playwright-cli",
|
|
3113
|
+
requireExecutionPlan: true,
|
|
3114
|
+
requirePlaywrightEvidence: true,
|
|
3115
|
+
requireReviewBundle: true,
|
|
3116
|
+
requireEvaluatorReview: true,
|
|
3117
|
+
requireBestOfHistory: true,
|
|
3118
|
+
requireBreakthrough: true,
|
|
3119
|
+
requireIndependentReviewerGate: true,
|
|
3120
|
+
requirePerfect100Completion: true,
|
|
3121
|
+
requireRuntimeGate: true,
|
|
3122
|
+
requireUiFidelity: true,
|
|
3123
|
+
requireRenderBundle: true,
|
|
3124
|
+
requireBrowserQaBundle: true,
|
|
3071
3125
|
requireIterations: true,
|
|
3072
|
-
|
|
3126
|
+
maxCycles,
|
|
3127
|
+
maxIterations: maxCycles,
|
|
3073
3128
|
validCombination: true
|
|
3074
3129
|
};
|
|
3075
3130
|
}
|
|
@@ -3536,8 +3591,8 @@ async function readSafe4(filePath) {
|
|
|
3536
3591
|
}
|
|
3537
3592
|
|
|
3538
3593
|
// src/core/report.ts
|
|
3539
|
-
import { readFile as
|
|
3540
|
-
import
|
|
3594
|
+
import { readFile as readFile53 } from "fs/promises";
|
|
3595
|
+
import path79 from "path";
|
|
3541
3596
|
|
|
3542
3597
|
// src/core/contractIndex.ts
|
|
3543
3598
|
import { readFile as readFile9 } from "fs/promises";
|
|
@@ -3579,6 +3634,31 @@ function record(index, id, file) {
|
|
|
3579
3634
|
index.idToFiles.set(id, current);
|
|
3580
3635
|
}
|
|
3581
3636
|
|
|
3637
|
+
// src/core/prototyping/round.ts
|
|
3638
|
+
var EXPLORATION_ROUNDS = ["r5", "r3", "r2", "r1"];
|
|
3639
|
+
var ABSORPTION_ROUNDS = ["r3", "r2", "r1"];
|
|
3640
|
+
var ROUND_SURVIVOR_COUNT = {
|
|
3641
|
+
r5: 5,
|
|
3642
|
+
r3: 3,
|
|
3643
|
+
r2: 2,
|
|
3644
|
+
r1: 1
|
|
3645
|
+
};
|
|
3646
|
+
function isExplorationRound(value) {
|
|
3647
|
+
return typeof value === "string" && EXPLORATION_ROUNDS.includes(value);
|
|
3648
|
+
}
|
|
3649
|
+
function prototypingRoundsDir() {
|
|
3650
|
+
return ".qfai/evidence/prototyping/rounds";
|
|
3651
|
+
}
|
|
3652
|
+
function roundDir(round) {
|
|
3653
|
+
return `${prototypingRoundsDir()}/${round}`;
|
|
3654
|
+
}
|
|
3655
|
+
function roundReviewBundlePath(round) {
|
|
3656
|
+
return `${roundDir(round)}/review-bundle.json`;
|
|
3657
|
+
}
|
|
3658
|
+
function roundEvaluatorReviewsDir(round) {
|
|
3659
|
+
return `${roundDir(round)}/evaluator-reviews`;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3582
3662
|
// src/core/specPackIds.ts
|
|
3583
3663
|
var STRICT_ID_PATTERNS2 = {
|
|
3584
3664
|
OBJ: /\bOBJ-\d+\b/g,
|
|
@@ -4291,15 +4371,15 @@ function asTagArray(value) {
|
|
|
4291
4371
|
}
|
|
4292
4372
|
|
|
4293
4373
|
// src/core/validate.ts
|
|
4294
|
-
import
|
|
4374
|
+
import path78 from "path";
|
|
4295
4375
|
|
|
4296
4376
|
// src/core/version.ts
|
|
4297
4377
|
import { readFile as readFile10 } from "fs/promises";
|
|
4298
4378
|
import path13 from "path";
|
|
4299
4379
|
import { fileURLToPath } from "url";
|
|
4300
4380
|
async function resolveToolVersion() {
|
|
4301
|
-
if ("1.8.
|
|
4302
|
-
return "1.8.
|
|
4381
|
+
if ("1.8.4".length > 0) {
|
|
4382
|
+
return "1.8.4";
|
|
4303
4383
|
}
|
|
4304
4384
|
try {
|
|
4305
4385
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -7230,7 +7310,7 @@ var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
|
7230
7310
|
var US_ID_RE2 = /\bUS-\d{4}-\d{4}\b/g;
|
|
7231
7311
|
var AC_ID_RE3 = /\bAC-\d{4}-\d{4}\b/g;
|
|
7232
7312
|
var DOWNSTREAM_ID_RE = /\b(?:US|AC|BR|SC|CASE)-\d{4}-\d{4}\b/g;
|
|
7233
|
-
async function validateTraceability(root, config,
|
|
7313
|
+
async function validateTraceability(root, config, options) {
|
|
7234
7314
|
const issues = [];
|
|
7235
7315
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
7236
7316
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -7306,7 +7386,7 @@ async function validateTraceability(root, config, phase) {
|
|
|
7306
7386
|
issues.push(...validateLayeredEdges(entry, edgeData));
|
|
7307
7387
|
issues.push(...validateLayeredHeuristics(entry, edgeData));
|
|
7308
7388
|
}
|
|
7309
|
-
if (
|
|
7389
|
+
if (options.includeCodeReferences && allLayeredScIds.size > 0) {
|
|
7310
7390
|
issues.push(
|
|
7311
7391
|
...await validateLayeredScCodeReferences(root, config, allLayeredScIds, specsRoot)
|
|
7312
7392
|
);
|
|
@@ -10086,8 +10166,109 @@ function normalizeOptionalFragment(fragment, originalRef) {
|
|
|
10086
10166
|
return normalized;
|
|
10087
10167
|
}
|
|
10088
10168
|
|
|
10169
|
+
// src/core/prototyping/candidate.ts
|
|
10170
|
+
var CANDIDATE_ID_PATTERN = /^c[1-9]\d*$/;
|
|
10171
|
+
function isCandidateId(value) {
|
|
10172
|
+
return typeof value === "string" && CANDIDATE_ID_PATTERN.test(value);
|
|
10173
|
+
}
|
|
10174
|
+
|
|
10089
10175
|
// src/core/validators/prototypingEvidence.ts
|
|
10090
10176
|
init_utils();
|
|
10177
|
+
|
|
10178
|
+
// src/core/validators/prototyping/modeInvariant.ts
|
|
10179
|
+
init_utils();
|
|
10180
|
+
function isRecord5(v) {
|
|
10181
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
10182
|
+
}
|
|
10183
|
+
function detectMode(raw) {
|
|
10184
|
+
if (!isRecord5(raw)) return null;
|
|
10185
|
+
if (typeof raw.mode === "string" && isValidPrototypingMode(raw.mode)) {
|
|
10186
|
+
return { mode: raw.mode, source: "string" };
|
|
10187
|
+
}
|
|
10188
|
+
if (isRecord5(raw.mode) && typeof raw.mode.effective === "string") {
|
|
10189
|
+
const effective = raw.mode.effective;
|
|
10190
|
+
if (isValidPrototypingMode(effective)) {
|
|
10191
|
+
return { mode: effective, source: "object" };
|
|
10192
|
+
}
|
|
10193
|
+
}
|
|
10194
|
+
return null;
|
|
10195
|
+
}
|
|
10196
|
+
function validateModeInvariant(raw, evidencePathForIssue = ".qfai/evidence/prototyping.json") {
|
|
10197
|
+
if (!isRecord5(raw)) {
|
|
10198
|
+
return [];
|
|
10199
|
+
}
|
|
10200
|
+
const detected = detectMode(raw);
|
|
10201
|
+
if (!detected) {
|
|
10202
|
+
return [];
|
|
10203
|
+
}
|
|
10204
|
+
const issues = [];
|
|
10205
|
+
const expectedMaxCycles = PROTOTYPING_MAX_CYCLES[detected.mode];
|
|
10206
|
+
if (raw.maxCycles !== void 0 && raw.maxIterations !== void 0 && raw.maxCycles !== raw.maxIterations) {
|
|
10207
|
+
issues.push(
|
|
10208
|
+
issue(
|
|
10209
|
+
"QFAI-PROT-MODE-001",
|
|
10210
|
+
`prototyping.json declares both maxCycles and maxIterations with conflicting values (maxCycles=${JSON.stringify(raw.maxCycles)}, maxIterations=${JSON.stringify(raw.maxIterations)}). They must agree (maxIterations is a deprecated alias).`,
|
|
10211
|
+
"error",
|
|
10212
|
+
evidencePathForIssue,
|
|
10213
|
+
"prototyping.modeInvariant.maxCyclesAlias",
|
|
10214
|
+
[JSON.stringify(raw.maxCycles), JSON.stringify(raw.maxIterations)],
|
|
10215
|
+
"canonical",
|
|
10216
|
+
"Remove the redundant key or align both values; prefer maxCycles (spec-0017 REQ-0001)."
|
|
10217
|
+
)
|
|
10218
|
+
);
|
|
10219
|
+
}
|
|
10220
|
+
const maxCyclesRaw = raw.maxCycles ?? raw.maxIterations;
|
|
10221
|
+
if (maxCyclesRaw !== void 0) {
|
|
10222
|
+
if (typeof maxCyclesRaw !== "number" || !Number.isInteger(maxCyclesRaw) || maxCyclesRaw !== expectedMaxCycles) {
|
|
10223
|
+
const maxCyclesDisplay = JSON.stringify(maxCyclesRaw);
|
|
10224
|
+
issues.push(
|
|
10225
|
+
issue(
|
|
10226
|
+
"QFAI-PROT-MODE-001",
|
|
10227
|
+
`Mode differences are limited to maxCycles only. Expected maxCycles=${expectedMaxCycles} for mode=${detected.mode}, got ${maxCyclesDisplay}.`,
|
|
10228
|
+
"error",
|
|
10229
|
+
evidencePathForIssue,
|
|
10230
|
+
"prototyping.modeInvariant",
|
|
10231
|
+
[detected.mode, String(expectedMaxCycles), maxCyclesDisplay],
|
|
10232
|
+
"canonical",
|
|
10233
|
+
`Set prototyping.json maxCycles to ${expectedMaxCycles} to match PROTOTYPING_MAX_CYCLES[${detected.mode}], or switch the mode if a different budget is desired (spec-0017 REQ-0001).`
|
|
10234
|
+
)
|
|
10235
|
+
);
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
if (raw.browserTool !== void 0) {
|
|
10239
|
+
if (raw.browserTool !== "playwright-cli") {
|
|
10240
|
+
const browserToolDisplay = JSON.stringify(raw.browserTool);
|
|
10241
|
+
issues.push(
|
|
10242
|
+
issue(
|
|
10243
|
+
"QFAI-PROT-MODE-001",
|
|
10244
|
+
`browserTool must be "playwright-cli" (spec-0017 REQ-0002). Got: ${browserToolDisplay}`,
|
|
10245
|
+
"error",
|
|
10246
|
+
evidencePathForIssue,
|
|
10247
|
+
"prototyping.modeInvariant.browserTool",
|
|
10248
|
+
[browserToolDisplay],
|
|
10249
|
+
"canonical",
|
|
10250
|
+
'Set prototyping.json browserTool to "playwright-cli". Playwright MCP and legacy providers are not supported in the standard harness.'
|
|
10251
|
+
)
|
|
10252
|
+
);
|
|
10253
|
+
}
|
|
10254
|
+
}
|
|
10255
|
+
return issues;
|
|
10256
|
+
}
|
|
10257
|
+
|
|
10258
|
+
// src/core/validators/prototypingEvidence.ts
|
|
10259
|
+
var VALID_ITERATION_KIND_SET = /* @__PURE__ */ new Set(["explore", "remix", "select", "polish", "branch"]);
|
|
10260
|
+
var VALID_PHASE_SET = /* @__PURE__ */ new Set([
|
|
10261
|
+
"planning",
|
|
10262
|
+
"explore",
|
|
10263
|
+
"remix",
|
|
10264
|
+
"select",
|
|
10265
|
+
"polish",
|
|
10266
|
+
"breakthrough",
|
|
10267
|
+
"reviewer_gate",
|
|
10268
|
+
"completed"
|
|
10269
|
+
]);
|
|
10270
|
+
var REQUIRED_POLISH_CHECKS = ["critique", "fix", "recapture", "rereview", "breakthrough"];
|
|
10271
|
+
var LEGACY_PASS_95_FIELD = "allItemsPass" + String(95);
|
|
10091
10272
|
var VALID_MODE_SOURCE_SET = /* @__PURE__ */ new Set([
|
|
10092
10273
|
"explicit-request",
|
|
10093
10274
|
"system-default",
|
|
@@ -10156,6 +10337,24 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10156
10337
|
return issues;
|
|
10157
10338
|
}
|
|
10158
10339
|
const obligations = surface && isSupportedPrototypingSurface(surface) ? derivePrototypingObligations({ surface, effectiveMode: mode.effective }) : void 0;
|
|
10340
|
+
issues.push(
|
|
10341
|
+
...validateModeInvariant(record2, path35.relative(root, evidencePath).replace(/\\/g, "/"))
|
|
10342
|
+
);
|
|
10343
|
+
const isV2Record = record2.schemaVersion === "2.0" || record2.rounds !== void 0 || record2.polishCycles !== void 0;
|
|
10344
|
+
if (isV2Record) {
|
|
10345
|
+
issues.push(...validateV2Lifecycle(root, evidencePath, record2, mode, obligations));
|
|
10346
|
+
if (obligations?.requireRuntimeGate && !isRecord6(record2.runtimeGate)) {
|
|
10347
|
+
issues.push(
|
|
10348
|
+
makeSchemaIssue(root, evidencePath, "runtimeGate is required in full-harness mode.")
|
|
10349
|
+
);
|
|
10350
|
+
}
|
|
10351
|
+
if (obligations?.requireUiFidelity && !isRecord6(record2.uiFidelity)) {
|
|
10352
|
+
issues.push(
|
|
10353
|
+
makeSchemaIssue(root, evidencePath, "uiFidelity is required in full-harness mode.")
|
|
10354
|
+
);
|
|
10355
|
+
}
|
|
10356
|
+
return issues;
|
|
10357
|
+
}
|
|
10159
10358
|
const iterations = normalizeIterations(record2.iterations);
|
|
10160
10359
|
if (!iterations || iterations.length === 0) {
|
|
10161
10360
|
issues.push(
|
|
@@ -10186,10 +10385,14 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10186
10385
|
)
|
|
10187
10386
|
);
|
|
10188
10387
|
}
|
|
10189
|
-
let
|
|
10388
|
+
let latestPerfect100 = false;
|
|
10389
|
+
let latestIterationKind = null;
|
|
10390
|
+
let completedPolishCount = 0;
|
|
10391
|
+
let polishWithBreakthroughCheck = false;
|
|
10392
|
+
let hasLegacyPass95CompletionMarker = false;
|
|
10190
10393
|
for (const [index, candidate] of iterations.entries()) {
|
|
10191
10394
|
const prefix = `iterations[${index}]`;
|
|
10192
|
-
if (!
|
|
10395
|
+
if (!isRecord6(candidate)) {
|
|
10193
10396
|
issues.push(makeSchemaIssue(root, evidencePath, `${prefix} must be an object.`));
|
|
10194
10397
|
continue;
|
|
10195
10398
|
}
|
|
@@ -10200,12 +10403,43 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10200
10403
|
makeSchemaIssue(root, evidencePath, `${prefix}.iteration must be a positive integer.`)
|
|
10201
10404
|
);
|
|
10202
10405
|
}
|
|
10203
|
-
if (typeof iteration.
|
|
10406
|
+
if (typeof iteration.allReviewerAxesPerfect100 !== "boolean") {
|
|
10204
10407
|
issues.push(
|
|
10205
|
-
makeSchemaIssue(
|
|
10408
|
+
makeSchemaIssue(
|
|
10409
|
+
root,
|
|
10410
|
+
evidencePath,
|
|
10411
|
+
`${prefix}.allReviewerAxesPerfect100 must be a boolean.`
|
|
10412
|
+
)
|
|
10206
10413
|
);
|
|
10207
|
-
} else if (iteration.
|
|
10208
|
-
|
|
10414
|
+
} else if (iteration.allReviewerAxesPerfect100) {
|
|
10415
|
+
latestPerfect100 = true;
|
|
10416
|
+
} else {
|
|
10417
|
+
latestPerfect100 = false;
|
|
10418
|
+
}
|
|
10419
|
+
if (iteration[LEGACY_PASS_95_FIELD] === true) {
|
|
10420
|
+
hasLegacyPass95CompletionMarker = true;
|
|
10421
|
+
}
|
|
10422
|
+
if (iteration.kind !== void 0) {
|
|
10423
|
+
if (typeof iteration.kind !== "string" || !VALID_ITERATION_KIND_SET.has(iteration.kind)) {
|
|
10424
|
+
issues.push(
|
|
10425
|
+
issue(
|
|
10426
|
+
"QFAI-PROT-285",
|
|
10427
|
+
`${prefix}.kind must be one of explore|remix|select|polish|branch.`,
|
|
10428
|
+
"error",
|
|
10429
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10430
|
+
"prototypingEvidence.phase",
|
|
10431
|
+
void 0,
|
|
10432
|
+
"canonical",
|
|
10433
|
+
"iteration kind \u3092 phase state machine \u306B\u5408\u308F\u305B\u3066\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10434
|
+
)
|
|
10435
|
+
);
|
|
10436
|
+
} else {
|
|
10437
|
+
latestIterationKind = iteration.kind;
|
|
10438
|
+
if (iteration.kind === "polish" && hasCompletePolishChecks(iteration.checks)) {
|
|
10439
|
+
completedPolishCount++;
|
|
10440
|
+
polishWithBreakthroughCheck = true;
|
|
10441
|
+
}
|
|
10442
|
+
}
|
|
10209
10443
|
}
|
|
10210
10444
|
if (!Array.isArray(iteration.reviewerScores) || iteration.reviewerScores.length === 0) {
|
|
10211
10445
|
issues.push(
|
|
@@ -10215,7 +10449,7 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10215
10449
|
}
|
|
10216
10450
|
for (const [reviewerIndex, reviewer] of iteration.reviewerScores.entries()) {
|
|
10217
10451
|
const reviewerPath = `${prefix}.reviewerScores[${reviewerIndex}]`;
|
|
10218
|
-
if (!
|
|
10452
|
+
if (!isRecord6(reviewer)) {
|
|
10219
10453
|
issues.push(makeSchemaIssue(root, evidencePath, `${reviewerPath} must be an object.`));
|
|
10220
10454
|
continue;
|
|
10221
10455
|
}
|
|
@@ -10232,7 +10466,7 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10232
10466
|
}
|
|
10233
10467
|
for (const [scoreIndex, score] of reviewer.scores.entries()) {
|
|
10234
10468
|
const scorePath = `${reviewerPath}.scores[${scoreIndex}]`;
|
|
10235
|
-
if (!
|
|
10469
|
+
if (!isRecord6(score)) {
|
|
10236
10470
|
issues.push(makeSchemaIssue(root, evidencePath, `${scorePath} must be an object.`));
|
|
10237
10471
|
continue;
|
|
10238
10472
|
}
|
|
@@ -10272,27 +10506,131 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
10272
10506
|
}
|
|
10273
10507
|
}
|
|
10274
10508
|
}
|
|
10509
|
+
const phaseCurrent = normalizePhaseCurrent(record2.phase);
|
|
10510
|
+
if (phaseCurrent !== null && !VALID_PHASE_SET.has(phaseCurrent)) {
|
|
10511
|
+
issues.push(
|
|
10512
|
+
issue(
|
|
10513
|
+
"QFAI-PROT-285",
|
|
10514
|
+
"phase.current must follow the prototyping phase state machine.",
|
|
10515
|
+
"error",
|
|
10516
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10517
|
+
"prototypingEvidence.phase",
|
|
10518
|
+
void 0,
|
|
10519
|
+
"canonical",
|
|
10520
|
+
"phase.current \u306F planning|explore|remix|select|polish|breakthrough|reviewer_gate|completed \u306E\u3044\u305A\u308C\u304B\u3067\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10521
|
+
)
|
|
10522
|
+
);
|
|
10523
|
+
}
|
|
10524
|
+
const fullHarnessStatus = normalizeFullHarnessStatus(record2.fullHarness);
|
|
10525
|
+
const completionClaimed = record2.completionClaimed === true || record2.completionEligible === true || phaseCurrent === "completed" || fullHarnessStatus === "completed";
|
|
10526
|
+
if (hasLegacyPass95CompletionMarker && completionClaimed) {
|
|
10527
|
+
issues.push(
|
|
10528
|
+
issue(
|
|
10529
|
+
"QFAI-PROT-288",
|
|
10530
|
+
"The legacy 95-point completion field is no longer a completion border; completion requires allReviewerAxesPerfect100.",
|
|
10531
|
+
"error",
|
|
10532
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10533
|
+
"prototypingEvidence.perfect100",
|
|
10534
|
+
void 0,
|
|
10535
|
+
"canonical",
|
|
10536
|
+
"95 \u70B9\u5230\u9054\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u5B8C\u4E86\u6761\u4EF6\u3068\u3057\u3066\u4F7F\u308F\u305A\u3001\u5168 reviewer / \u5168 axis 100 \u70B9\u3092 allReviewerAxesPerfect100 \u3067\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10537
|
+
)
|
|
10538
|
+
);
|
|
10539
|
+
}
|
|
10540
|
+
if (completionClaimed) {
|
|
10541
|
+
if (!latestPerfect100 || !allReviewerScoresArePerfect100(iterations)) {
|
|
10542
|
+
issues.push(
|
|
10543
|
+
issue(
|
|
10544
|
+
"QFAI-PROT-287",
|
|
10545
|
+
"Completion requires every reviewer to score every evaluation axis at 100.",
|
|
10546
|
+
"error",
|
|
10547
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10548
|
+
"prototypingEvidence.perfect100",
|
|
10549
|
+
void 0,
|
|
10550
|
+
"canonical",
|
|
10551
|
+
"completionClaimed/completed \u3092\u8A18\u9332\u3059\u308B\u524D\u306B\u3001\u5168 reviewerScores[].scores[].score \u3092 100 \u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10552
|
+
)
|
|
10553
|
+
);
|
|
10554
|
+
}
|
|
10555
|
+
if (latestIterationKind === "select") {
|
|
10556
|
+
issues.push(
|
|
10557
|
+
issue(
|
|
10558
|
+
"QFAI-PROT-285",
|
|
10559
|
+
"Selection funnel completion is not stage completion; latest iteration cannot remain select when completion is claimed.",
|
|
10560
|
+
"error",
|
|
10561
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10562
|
+
"prototypingEvidence.phase",
|
|
10563
|
+
void 0,
|
|
10564
|
+
"canonical",
|
|
10565
|
+
"winner \u9078\u5B9A\u5F8C\u306B post-selection polish iteration \u3092\u5B8C\u4E86\u3057\u3066\u304B\u3089 completion \u3092 claim \u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10566
|
+
)
|
|
10567
|
+
);
|
|
10568
|
+
}
|
|
10569
|
+
const postSelectionPolishCount = typeof record2.postSelectionPolishCount === "number" ? record2.postSelectionPolishCount : completedPolishCount;
|
|
10570
|
+
if (!Number.isInteger(postSelectionPolishCount) || postSelectionPolishCount < 1) {
|
|
10571
|
+
issues.push(
|
|
10572
|
+
issue(
|
|
10573
|
+
"QFAI-PROT-286",
|
|
10574
|
+
"Completion requires at least one completed post-selection polish iteration.",
|
|
10575
|
+
"error",
|
|
10576
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10577
|
+
"prototypingEvidence.postSelectionPolish",
|
|
10578
|
+
void 0,
|
|
10579
|
+
"canonical",
|
|
10580
|
+
"postSelectionPolishCount >= 1 \u3068\u3057\u3001polish iteration \u306B critique/fix/recapture/rereview/breakthrough checks \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10581
|
+
)
|
|
10582
|
+
);
|
|
10583
|
+
}
|
|
10584
|
+
if (!polishWithBreakthroughCheck) {
|
|
10585
|
+
issues.push(
|
|
10586
|
+
issue(
|
|
10587
|
+
"QFAI-PROT-286",
|
|
10588
|
+
"Completion requires a polish iteration with critique/fix/recapture/rereview/breakthrough checks.",
|
|
10589
|
+
"error",
|
|
10590
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10591
|
+
"prototypingEvidence.postSelectionPolish",
|
|
10592
|
+
void 0,
|
|
10593
|
+
"canonical",
|
|
10594
|
+
"polish iteration \u306E checks.critique/fix/recapture/rereview/breakthrough \u3092 true \u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10595
|
+
)
|
|
10596
|
+
);
|
|
10597
|
+
}
|
|
10598
|
+
if (!isRecord6(record2.completionCertificate)) {
|
|
10599
|
+
issues.push(
|
|
10600
|
+
issue(
|
|
10601
|
+
"QFAI-PROT-289",
|
|
10602
|
+
"completionCertificate is required when completion is claimed.",
|
|
10603
|
+
"error",
|
|
10604
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10605
|
+
"prototypingEvidence.completionCertificate",
|
|
10606
|
+
void 0,
|
|
10607
|
+
"canonical",
|
|
10608
|
+
"reviewerGateResult/validateCommand/bestOfHistoryRef/breakthroughRef \u3092\u542B\u3080 completionCertificate \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10609
|
+
)
|
|
10610
|
+
);
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10275
10613
|
const maxIterations = PROTOTYPING_MAX_ITERATIONS[mode.effective];
|
|
10276
|
-
if (!
|
|
10614
|
+
if (!latestPerfect100 && iterations.length < maxIterations) {
|
|
10277
10615
|
issues.push(
|
|
10278
10616
|
issue(
|
|
10279
10617
|
"QFAI-PROT-282",
|
|
10280
|
-
`mode=${mode.effective} has not reached all-
|
|
10618
|
+
`mode=${mode.effective} has not reached all-reviewer-axes-perfect-100 and has remaining iterations.`,
|
|
10281
10619
|
"warning",
|
|
10282
10620
|
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10283
10621
|
"prototypingEvidence.convergence",
|
|
10284
10622
|
[String(iterations.length), String(maxIterations)],
|
|
10285
10623
|
"canonical",
|
|
10286
|
-
"
|
|
10624
|
+
"\u5168 reviewer / \u5168 axis \u304C 100 \u70B9\u306B\u9054\u3057\u3066\u3044\u306A\u3044\u305F\u3081\u3001mode \u4E0A\u9650\u306B\u9054\u3059\u308B\u307E\u3067\u53CD\u5FA9\u3092\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10287
10625
|
)
|
|
10288
10626
|
);
|
|
10289
10627
|
}
|
|
10290
|
-
if (obligations?.requireRuntimeGate && !
|
|
10628
|
+
if (obligations?.requireRuntimeGate && !isRecord6(record2.runtimeGate)) {
|
|
10291
10629
|
issues.push(
|
|
10292
10630
|
makeSchemaIssue(root, evidencePath, "runtimeGate is required in full-harness mode.")
|
|
10293
10631
|
);
|
|
10294
10632
|
}
|
|
10295
|
-
if (obligations?.requireUiFidelity && !
|
|
10633
|
+
if (obligations?.requireUiFidelity && !isRecord6(record2.uiFidelity)) {
|
|
10296
10634
|
issues.push(
|
|
10297
10635
|
makeSchemaIssue(root, evidencePath, "uiFidelity is required in full-harness mode.")
|
|
10298
10636
|
);
|
|
@@ -10331,98 +10669,446 @@ function normalizeIterations(value) {
|
|
|
10331
10669
|
}
|
|
10332
10670
|
return value;
|
|
10333
10671
|
}
|
|
10334
|
-
function
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
async function readJsonFile(filePath) {
|
|
10338
|
-
try {
|
|
10339
|
-
const raw = await readFile24(filePath, "utf-8");
|
|
10340
|
-
const parsed = JSON.parse(raw);
|
|
10341
|
-
return isRecord5(parsed) ? { status: "ok", value: parsed } : { status: "invalid" };
|
|
10342
|
-
} catch {
|
|
10343
|
-
try {
|
|
10344
|
-
await readFile24(filePath, "utf-8");
|
|
10345
|
-
return { status: "invalid" };
|
|
10346
|
-
} catch {
|
|
10347
|
-
return { status: "missing" };
|
|
10348
|
-
}
|
|
10672
|
+
function normalizeRounds(value) {
|
|
10673
|
+
if (!Array.isArray(value)) {
|
|
10674
|
+
return null;
|
|
10349
10675
|
}
|
|
10676
|
+
return value;
|
|
10350
10677
|
}
|
|
10351
|
-
function
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
message,
|
|
10355
|
-
"error",
|
|
10356
|
-
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10357
|
-
"prototypingEvidence.schema"
|
|
10358
|
-
);
|
|
10359
|
-
}
|
|
10360
|
-
|
|
10361
|
-
// src/core/validators/prototypingDesignSystem.ts
|
|
10362
|
-
import { access as access9, readFile as readFile25 } from "fs/promises";
|
|
10363
|
-
import path36 from "path";
|
|
10364
|
-
var RULE_CODE = "PROT-DS01";
|
|
10365
|
-
var RULE_NAME = "prototyping.designSystemCompliance";
|
|
10366
|
-
var MESSAGE = "prototyping.json scoringTrace is missing required `designSystemCompliance` score.";
|
|
10367
|
-
var SUGGESTED_ACTION = "Add `scoringTrace.designSystemCompliance` (numeric score 0..100 or null with rationale) to prototyping.json.";
|
|
10368
|
-
function isRecord6(value) {
|
|
10369
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10370
|
-
}
|
|
10371
|
-
function detectMode(raw) {
|
|
10372
|
-
if (!isRecord6(raw)) {
|
|
10373
|
-
return "other";
|
|
10678
|
+
function normalizePolishCycles(value) {
|
|
10679
|
+
if (!Array.isArray(value)) {
|
|
10680
|
+
return null;
|
|
10374
10681
|
}
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10682
|
+
return value;
|
|
10683
|
+
}
|
|
10684
|
+
function normalizePhaseCurrent(value) {
|
|
10685
|
+
if (!isRecord6(value)) {
|
|
10686
|
+
return null;
|
|
10378
10687
|
}
|
|
10379
|
-
return "
|
|
10688
|
+
return typeof value.current === "string" ? value.current : null;
|
|
10380
10689
|
}
|
|
10381
|
-
function
|
|
10382
|
-
if (!isRecord6(
|
|
10383
|
-
return
|
|
10690
|
+
function normalizeFullHarnessStatus(value) {
|
|
10691
|
+
if (!isRecord6(value)) {
|
|
10692
|
+
return null;
|
|
10384
10693
|
}
|
|
10385
|
-
|
|
10386
|
-
|
|
10694
|
+
return typeof value.status === "string" ? value.status : null;
|
|
10695
|
+
}
|
|
10696
|
+
function hasCompletePolishChecks(value) {
|
|
10697
|
+
if (!isRecord6(value)) {
|
|
10387
10698
|
return false;
|
|
10388
10699
|
}
|
|
10389
|
-
return
|
|
10700
|
+
return REQUIRED_POLISH_CHECKS.every((key) => value[key] === true);
|
|
10390
10701
|
}
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
return true;
|
|
10395
|
-
} catch (err) {
|
|
10396
|
-
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
10397
|
-
return false;
|
|
10398
|
-
}
|
|
10702
|
+
function allReviewerScoresArePerfect100(iterations) {
|
|
10703
|
+
const candidate = iterations[iterations.length - 1];
|
|
10704
|
+
if (!isRecord6(candidate) || !Array.isArray(candidate.reviewerScores)) {
|
|
10399
10705
|
return false;
|
|
10400
10706
|
}
|
|
10401
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
return await readFile25(filePath, "utf-8");
|
|
10405
|
-
} catch (err) {
|
|
10406
|
-
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
10407
|
-
return null;
|
|
10707
|
+
return candidate.reviewerScores.every((reviewer) => {
|
|
10708
|
+
if (!isRecord6(reviewer) || !Array.isArray(reviewer.scores) || reviewer.scores.length === 0) {
|
|
10709
|
+
return false;
|
|
10408
10710
|
}
|
|
10409
|
-
return
|
|
10410
|
-
}
|
|
10711
|
+
return reviewer.scores.every((score) => isRecord6(score) && score.score === 100);
|
|
10712
|
+
});
|
|
10411
10713
|
}
|
|
10412
|
-
|
|
10413
|
-
const
|
|
10414
|
-
const
|
|
10415
|
-
if (
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10714
|
+
function validateV2Lifecycle(root, evidencePath, record2, mode, obligations) {
|
|
10715
|
+
const issues = [];
|
|
10716
|
+
const rounds = normalizeRounds(record2.rounds);
|
|
10717
|
+
if (!rounds || rounds.length === 0) {
|
|
10718
|
+
issues.push(
|
|
10719
|
+
issue(
|
|
10720
|
+
"QFAI-PROT-280",
|
|
10721
|
+
"prototyping evidence requires at least one round entry.",
|
|
10722
|
+
"error",
|
|
10723
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10724
|
+
"prototypingEvidence.rounds",
|
|
10725
|
+
void 0,
|
|
10726
|
+
"canonical",
|
|
10727
|
+
"rounds[] \u306B\u5C11\u306A\u304F\u3068\u3082 1 \u4EF6\u306E\u63A2\u7D22 round \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10728
|
+
)
|
|
10729
|
+
);
|
|
10730
|
+
return issues;
|
|
10731
|
+
}
|
|
10732
|
+
const polishCycles = normalizePolishCycles(record2.polishCycles) ?? [];
|
|
10733
|
+
if (obligations && polishCycles.length > obligations.maxIterations) {
|
|
10734
|
+
issues.push(
|
|
10735
|
+
issue(
|
|
10736
|
+
"QFAI-PROT-281",
|
|
10737
|
+
`mode=${mode.effective} exceeds max polish cycles (${obligations.maxIterations}).`,
|
|
10738
|
+
"error",
|
|
10739
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10740
|
+
"prototypingEvidence.maxIterations",
|
|
10741
|
+
[String(polishCycles.length), String(obligations.maxIterations)],
|
|
10742
|
+
"canonical",
|
|
10743
|
+
`mode=${mode.effective} \u306E polish cycle \u4E0A\u9650 ${obligations.maxIterations} \u3092\u8D85\u3048\u306A\u3044\u3088\u3046\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
10744
|
+
)
|
|
10745
|
+
);
|
|
10746
|
+
}
|
|
10747
|
+
let latestPerfect100 = false;
|
|
10748
|
+
let completedPolishCount = 0;
|
|
10749
|
+
let polishWithBreakthroughCheck = false;
|
|
10750
|
+
for (const [index, candidate] of rounds.entries()) {
|
|
10751
|
+
const prefix = `rounds[${index}]`;
|
|
10752
|
+
if (!isRecord6(candidate)) {
|
|
10753
|
+
issues.push(makeSchemaIssue(root, evidencePath, `${prefix} must be an object.`));
|
|
10754
|
+
continue;
|
|
10755
|
+
}
|
|
10756
|
+
let validRound = null;
|
|
10757
|
+
if (!isExplorationRound(candidate.round)) {
|
|
10758
|
+
issues.push(makeSchemaIssue(root, evidencePath, `${prefix}.round must be r5|r3|r2|r1.`));
|
|
10759
|
+
} else {
|
|
10760
|
+
validRound = candidate.round;
|
|
10761
|
+
if (index >= EXPLORATION_ROUNDS.length) {
|
|
10762
|
+
issues.push(
|
|
10763
|
+
makeSchemaIssue(
|
|
10764
|
+
root,
|
|
10765
|
+
evidencePath,
|
|
10766
|
+
`${prefix} exceeds the funnel limit (rounds[] must contain at most ${EXPLORATION_ROUNDS.length} entries: ${EXPLORATION_ROUNDS.join("\u2192")}).`
|
|
10767
|
+
)
|
|
10768
|
+
);
|
|
10769
|
+
} else {
|
|
10770
|
+
const expectedRound = EXPLORATION_ROUNDS[index];
|
|
10771
|
+
if (expectedRound !== void 0 && validRound !== expectedRound) {
|
|
10772
|
+
issues.push(
|
|
10773
|
+
makeSchemaIssue(
|
|
10774
|
+
root,
|
|
10775
|
+
evidencePath,
|
|
10776
|
+
`${prefix}.round must be ${expectedRound} (rounds[] must follow ${EXPLORATION_ROUNDS.join("\u2192")}); got ${validRound}.`
|
|
10777
|
+
)
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
const candidateIds = [];
|
|
10783
|
+
const seenCandidateIds = /* @__PURE__ */ new Set();
|
|
10784
|
+
if (!Array.isArray(candidate.candidates) || candidate.candidates.length === 0) {
|
|
10785
|
+
issues.push(
|
|
10786
|
+
makeSchemaIssue(root, evidencePath, `${prefix}.candidates must be a non-empty array.`)
|
|
10787
|
+
);
|
|
10788
|
+
} else {
|
|
10789
|
+
for (const [candidateIndex, roundCandidate] of candidate.candidates.entries()) {
|
|
10790
|
+
if (!isRecord6(roundCandidate)) {
|
|
10791
|
+
issues.push(
|
|
10792
|
+
makeSchemaIssue(
|
|
10793
|
+
root,
|
|
10794
|
+
evidencePath,
|
|
10795
|
+
`${prefix}.candidates[${candidateIndex}] must be an object.`
|
|
10796
|
+
)
|
|
10797
|
+
);
|
|
10798
|
+
continue;
|
|
10799
|
+
}
|
|
10800
|
+
if (!isCandidateId(roundCandidate.candidateId)) {
|
|
10801
|
+
issues.push(
|
|
10802
|
+
makeSchemaIssue(
|
|
10803
|
+
root,
|
|
10804
|
+
evidencePath,
|
|
10805
|
+
`${prefix}.candidates[${candidateIndex}].candidateId must be a valid candidate id.`
|
|
10806
|
+
)
|
|
10807
|
+
);
|
|
10808
|
+
} else if (seenCandidateIds.has(roundCandidate.candidateId)) {
|
|
10809
|
+
issues.push(
|
|
10810
|
+
makeSchemaIssue(
|
|
10811
|
+
root,
|
|
10812
|
+
evidencePath,
|
|
10813
|
+
`${prefix}.candidates[${candidateIndex}].candidateId must be unique within the round; ${roundCandidate.candidateId} already appears earlier.`
|
|
10814
|
+
)
|
|
10815
|
+
);
|
|
10816
|
+
} else {
|
|
10817
|
+
seenCandidateIds.add(roundCandidate.candidateId);
|
|
10818
|
+
candidateIds.push(roundCandidate.candidateId);
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
if (validRound) {
|
|
10822
|
+
const expectedCandidateCount = ROUND_SURVIVOR_COUNT[validRound];
|
|
10823
|
+
if (candidate.candidates.length !== expectedCandidateCount) {
|
|
10824
|
+
issues.push(
|
|
10825
|
+
makeSchemaIssue(
|
|
10826
|
+
root,
|
|
10827
|
+
evidencePath,
|
|
10828
|
+
`${prefix}.candidates must contain exactly ${expectedCandidateCount} entries for round ${validRound}; got ${candidate.candidates.length}.`
|
|
10829
|
+
)
|
|
10830
|
+
);
|
|
10831
|
+
}
|
|
10832
|
+
}
|
|
10833
|
+
}
|
|
10834
|
+
const screenMap = isRecord6(candidate.screenEvidenceByCandidate) ? candidate.screenEvidenceByCandidate : null;
|
|
10835
|
+
if (screenMap === null) {
|
|
10836
|
+
issues.push(
|
|
10837
|
+
makeSchemaIssue(
|
|
10838
|
+
root,
|
|
10839
|
+
evidencePath,
|
|
10840
|
+
`${prefix}.screenEvidenceByCandidate must be an object keyed by candidateId.`
|
|
10841
|
+
)
|
|
10842
|
+
);
|
|
10843
|
+
}
|
|
10844
|
+
const reviewMap = isRecord6(candidate.evaluatorReviewRefsByCandidate) ? candidate.evaluatorReviewRefsByCandidate : null;
|
|
10845
|
+
if (reviewMap === null) {
|
|
10846
|
+
issues.push(
|
|
10847
|
+
makeSchemaIssue(
|
|
10848
|
+
root,
|
|
10849
|
+
evidencePath,
|
|
10850
|
+
`${prefix}.evaluatorReviewRefsByCandidate must be an object keyed by candidateId.`
|
|
10851
|
+
)
|
|
10852
|
+
);
|
|
10853
|
+
}
|
|
10854
|
+
for (const candidateId of candidateIds) {
|
|
10855
|
+
if (screenMap) {
|
|
10856
|
+
const screens = screenMap[candidateId];
|
|
10857
|
+
if (!Array.isArray(screens) || screens.length === 0) {
|
|
10858
|
+
issues.push(
|
|
10859
|
+
makeSchemaIssue(
|
|
10860
|
+
root,
|
|
10861
|
+
evidencePath,
|
|
10862
|
+
`${prefix}.screenEvidenceByCandidate.${candidateId} must be a non-empty array of screen artifact refs.`
|
|
10863
|
+
)
|
|
10864
|
+
);
|
|
10865
|
+
}
|
|
10866
|
+
}
|
|
10867
|
+
if (reviewMap) {
|
|
10868
|
+
const reviewRef = reviewMap[candidateId];
|
|
10869
|
+
if (typeof reviewRef !== "string" || reviewRef.trim().length === 0) {
|
|
10870
|
+
issues.push(
|
|
10871
|
+
makeSchemaIssue(
|
|
10872
|
+
root,
|
|
10873
|
+
evidencePath,
|
|
10874
|
+
`${prefix}.evaluatorReviewRefsByCandidate.${candidateId} must be a non-empty string.`
|
|
10875
|
+
)
|
|
10876
|
+
);
|
|
10877
|
+
}
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10880
|
+
if (typeof candidate.allAxesPerfect100 !== "boolean") {
|
|
10881
|
+
issues.push(
|
|
10882
|
+
makeSchemaIssue(root, evidencePath, `${prefix}.allAxesPerfect100 must be a boolean.`)
|
|
10883
|
+
);
|
|
10884
|
+
} else {
|
|
10885
|
+
latestPerfect100 = candidate.allAxesPerfect100;
|
|
10886
|
+
}
|
|
10887
|
+
}
|
|
10888
|
+
for (const [index, cycle] of polishCycles.entries()) {
|
|
10889
|
+
const prefix = `polishCycles[${index}]`;
|
|
10890
|
+
if (!isRecord6(cycle)) {
|
|
10891
|
+
issues.push(makeSchemaIssue(root, evidencePath, `${prefix} must be an object.`));
|
|
10892
|
+
continue;
|
|
10893
|
+
}
|
|
10894
|
+
if (typeof cycle.cycle !== "number" || !Number.isInteger(cycle.cycle) || cycle.cycle <= 0) {
|
|
10895
|
+
issues.push(
|
|
10896
|
+
makeSchemaIssue(root, evidencePath, `${prefix}.cycle must be a positive integer.`)
|
|
10897
|
+
);
|
|
10898
|
+
}
|
|
10899
|
+
if (typeof cycle.kind !== "string" || !["polish", "branch", "reviewer_gate", "completed"].includes(cycle.kind)) {
|
|
10900
|
+
issues.push(
|
|
10901
|
+
makeSchemaIssue(
|
|
10902
|
+
root,
|
|
10903
|
+
evidencePath,
|
|
10904
|
+
`${prefix}.kind must be polish|branch|reviewer_gate|completed.`
|
|
10905
|
+
)
|
|
10906
|
+
);
|
|
10907
|
+
}
|
|
10908
|
+
if (typeof cycle.evaluatorReviewRef !== "string" || cycle.evaluatorReviewRef.trim().length === 0) {
|
|
10909
|
+
issues.push(
|
|
10910
|
+
makeSchemaIssue(root, evidencePath, `${prefix}.evaluatorReviewRef must be non-empty.`)
|
|
10911
|
+
);
|
|
10912
|
+
}
|
|
10913
|
+
if (typeof cycle.allAxesPerfect100 !== "boolean") {
|
|
10914
|
+
issues.push(
|
|
10915
|
+
makeSchemaIssue(root, evidencePath, `${prefix}.allAxesPerfect100 must be a boolean.`)
|
|
10916
|
+
);
|
|
10917
|
+
} else {
|
|
10918
|
+
latestPerfect100 = cycle.allAxesPerfect100;
|
|
10919
|
+
}
|
|
10920
|
+
if (cycle.kind === "polish") {
|
|
10921
|
+
completedPolishCount += 1;
|
|
10922
|
+
if (typeof cycle.breakthroughRef === "string" && cycle.breakthroughRef.trim().length > 0) {
|
|
10923
|
+
polishWithBreakthroughCheck = true;
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
const phaseCurrent = normalizePhaseCurrent(record2.phase);
|
|
10928
|
+
if (phaseCurrent !== null && !VALID_PHASE_SET.has(phaseCurrent)) {
|
|
10929
|
+
issues.push(
|
|
10930
|
+
issue(
|
|
10931
|
+
"QFAI-PROT-285",
|
|
10932
|
+
"phase.current must follow the prototyping phase state machine.",
|
|
10933
|
+
"error",
|
|
10934
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10935
|
+
"prototypingEvidence.phase",
|
|
10936
|
+
void 0,
|
|
10937
|
+
"canonical",
|
|
10938
|
+
"phase.current \u306F planning|explore|remix|select|polish|breakthrough|reviewer_gate|completed \u306E\u3044\u305A\u308C\u304B\u3067\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10939
|
+
)
|
|
10940
|
+
);
|
|
10941
|
+
}
|
|
10942
|
+
const fullHarnessStatus = normalizeFullHarnessStatus(record2.fullHarness);
|
|
10943
|
+
const completionClaimed = record2.completionClaimed === true || record2.completionEligible === true || phaseCurrent === "completed" || fullHarnessStatus === "completed";
|
|
10944
|
+
if (completionClaimed) {
|
|
10945
|
+
if (!latestPerfect100) {
|
|
10946
|
+
issues.push(
|
|
10947
|
+
issue(
|
|
10948
|
+
"QFAI-PROT-287",
|
|
10949
|
+
"Completion requires every reviewer to score every evaluation axis at 100.",
|
|
10950
|
+
"error",
|
|
10951
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10952
|
+
"prototypingEvidence.perfect100",
|
|
10953
|
+
void 0,
|
|
10954
|
+
"canonical",
|
|
10955
|
+
"completionClaimed/completed \u3092\u8A18\u9332\u3059\u308B\u524D\u306B\u3001\u6700\u65B0 round \u307E\u305F\u306F polish cycle \u306E allAxesPerfect100 \u3092 true \u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10956
|
+
)
|
|
10957
|
+
);
|
|
10958
|
+
}
|
|
10959
|
+
const postSelectionPolishCount = typeof record2.postSelectionPolishCount === "number" ? record2.postSelectionPolishCount : completedPolishCount;
|
|
10960
|
+
if (!Number.isInteger(postSelectionPolishCount) || postSelectionPolishCount < 1) {
|
|
10961
|
+
issues.push(
|
|
10962
|
+
issue(
|
|
10963
|
+
"QFAI-PROT-286",
|
|
10964
|
+
"Completion requires at least one completed post-selection polish cycle.",
|
|
10965
|
+
"error",
|
|
10966
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10967
|
+
"prototypingEvidence.postSelectionPolish",
|
|
10968
|
+
void 0,
|
|
10969
|
+
"canonical",
|
|
10970
|
+
"postSelectionPolishCount >= 1 \u3068\u3057\u3001polish cycle \u3092\u5C11\u306A\u304F\u3068\u3082 1 \u4EF6\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10971
|
+
)
|
|
10972
|
+
);
|
|
10973
|
+
}
|
|
10974
|
+
if (!polishWithBreakthroughCheck) {
|
|
10975
|
+
issues.push(
|
|
10976
|
+
issue(
|
|
10977
|
+
"QFAI-PROT-286",
|
|
10978
|
+
"Completion requires a polish cycle with breakthrough evidence.",
|
|
10979
|
+
"error",
|
|
10980
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10981
|
+
"prototypingEvidence.postSelectionPolish",
|
|
10982
|
+
void 0,
|
|
10983
|
+
"canonical",
|
|
10984
|
+
"polish cycle \u306B breakthroughRef \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10985
|
+
)
|
|
10986
|
+
);
|
|
10987
|
+
}
|
|
10988
|
+
if (!isRecord6(record2.completionCertificate)) {
|
|
10989
|
+
issues.push(
|
|
10990
|
+
issue(
|
|
10991
|
+
"QFAI-PROT-289",
|
|
10992
|
+
"completionCertificate is required when completion is claimed.",
|
|
10993
|
+
"error",
|
|
10994
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
10995
|
+
"prototypingEvidence.completionCertificate",
|
|
10996
|
+
void 0,
|
|
10997
|
+
"canonical",
|
|
10998
|
+
"reviewerGateResult/validateCommand/bestOfHistoryRef/breakthroughRef \u3092\u542B\u3080 completionCertificate \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
10999
|
+
)
|
|
11000
|
+
);
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
const maxIterations = PROTOTYPING_MAX_ITERATIONS[mode.effective];
|
|
11004
|
+
if (!latestPerfect100 && polishCycles.length < maxIterations) {
|
|
11005
|
+
issues.push(
|
|
11006
|
+
issue(
|
|
11007
|
+
"QFAI-PROT-282",
|
|
11008
|
+
`mode=${mode.effective} has not reached all-reviewer-axes-perfect-100 and has remaining polish cycles.`,
|
|
11009
|
+
"warning",
|
|
11010
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
11011
|
+
"prototypingEvidence.convergence",
|
|
11012
|
+
[String(polishCycles.length), String(maxIterations)],
|
|
11013
|
+
"canonical",
|
|
11014
|
+
"\u5168 reviewer / \u5168 axis \u304C 100 \u70B9\u306B\u9054\u3057\u3066\u3044\u306A\u3044\u305F\u3081\u3001mode \u4E0A\u9650\u306B\u9054\u3059\u308B\u307E\u3067 polish cycle \u3092\u7D99\u7D9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11015
|
+
)
|
|
11016
|
+
);
|
|
11017
|
+
}
|
|
11018
|
+
return issues;
|
|
11019
|
+
}
|
|
11020
|
+
function isRecord6(value) {
|
|
11021
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11022
|
+
}
|
|
11023
|
+
async function readJsonFile(filePath) {
|
|
11024
|
+
try {
|
|
11025
|
+
const raw = await readFile24(filePath, "utf-8");
|
|
11026
|
+
const parsed = JSON.parse(raw);
|
|
11027
|
+
return isRecord6(parsed) ? { status: "ok", value: parsed } : { status: "invalid" };
|
|
11028
|
+
} catch {
|
|
11029
|
+
try {
|
|
11030
|
+
await readFile24(filePath, "utf-8");
|
|
11031
|
+
return { status: "invalid" };
|
|
11032
|
+
} catch {
|
|
11033
|
+
return { status: "missing" };
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
function makeSchemaIssue(root, evidencePath, message) {
|
|
11038
|
+
return issue(
|
|
11039
|
+
"QFAI-PROT-299",
|
|
11040
|
+
message,
|
|
11041
|
+
"error",
|
|
11042
|
+
path35.relative(root, evidencePath).replace(/\\/g, "/"),
|
|
11043
|
+
"prototypingEvidence.schema"
|
|
11044
|
+
);
|
|
11045
|
+
}
|
|
11046
|
+
|
|
11047
|
+
// src/core/validators/prototypingDesignSystem.ts
|
|
11048
|
+
import { access as access9, readFile as readFile25 } from "fs/promises";
|
|
11049
|
+
import path36 from "path";
|
|
11050
|
+
var RULE_CODE = "PROT-DS01";
|
|
11051
|
+
var RULE_NAME = "prototyping.designSystemCompliance";
|
|
11052
|
+
var MESSAGE = "prototyping.json scoringTrace is missing required `designSystemCompliance` score.";
|
|
11053
|
+
var SUGGESTED_ACTION = "Add `scoringTrace.designSystemCompliance` (numeric score 0..100 or null with rationale) to prototyping.json.";
|
|
11054
|
+
function isRecord7(value) {
|
|
11055
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11056
|
+
}
|
|
11057
|
+
function detectMode2(raw) {
|
|
11058
|
+
if (!isRecord7(raw)) {
|
|
11059
|
+
return "other";
|
|
11060
|
+
}
|
|
11061
|
+
const topLevel = raw.mode;
|
|
11062
|
+
if (isRecord7(topLevel) && topLevel.effective === "full-harness") {
|
|
11063
|
+
return "full-harness";
|
|
11064
|
+
}
|
|
11065
|
+
return "other";
|
|
11066
|
+
}
|
|
11067
|
+
function detectScorePresence(raw) {
|
|
11068
|
+
if (!isRecord7(raw)) {
|
|
11069
|
+
return false;
|
|
11070
|
+
}
|
|
11071
|
+
const trace = raw.scoringTrace;
|
|
11072
|
+
if (!isRecord7(trace)) {
|
|
11073
|
+
return false;
|
|
11074
|
+
}
|
|
11075
|
+
return Object.prototype.hasOwnProperty.call(trace, "designSystemCompliance");
|
|
11076
|
+
}
|
|
11077
|
+
async function fileExists2(filePath) {
|
|
11078
|
+
try {
|
|
11079
|
+
await access9(filePath);
|
|
11080
|
+
return true;
|
|
11081
|
+
} catch (err) {
|
|
11082
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
11083
|
+
return false;
|
|
11084
|
+
}
|
|
11085
|
+
return false;
|
|
11086
|
+
}
|
|
11087
|
+
}
|
|
11088
|
+
async function readFileSafe(filePath) {
|
|
11089
|
+
try {
|
|
11090
|
+
return await readFile25(filePath, "utf-8");
|
|
11091
|
+
} catch (err) {
|
|
11092
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
11093
|
+
return null;
|
|
11094
|
+
}
|
|
11095
|
+
return null;
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
11098
|
+
async function loadPrototypingArtifact(evidenceDir) {
|
|
11099
|
+
const jsonPath = path36.join(evidenceDir, "prototyping.json");
|
|
11100
|
+
const jsonRaw = await readFileSafe(jsonPath);
|
|
11101
|
+
if (jsonRaw !== null) {
|
|
11102
|
+
let parsed = null;
|
|
11103
|
+
try {
|
|
11104
|
+
parsed = JSON.parse(jsonRaw);
|
|
11105
|
+
} catch {
|
|
10420
11106
|
parsed = null;
|
|
10421
11107
|
}
|
|
10422
11108
|
return {
|
|
10423
11109
|
source: "json",
|
|
10424
11110
|
fileName: "prototyping.json",
|
|
10425
|
-
mode:
|
|
11111
|
+
mode: detectMode2(parsed),
|
|
10426
11112
|
designSystemCompliancePresent: detectScorePresence(parsed)
|
|
10427
11113
|
};
|
|
10428
11114
|
}
|
|
@@ -10444,38 +11130,1060 @@ function toIssue(severity, file) {
|
|
|
10444
11130
|
suggested_action: SUGGESTED_ACTION
|
|
10445
11131
|
};
|
|
10446
11132
|
}
|
|
10447
|
-
function resolveEvidenceRoot(root, config) {
|
|
10448
|
-
return path36.join(path36.dirname(resolvePath(root, config, "specsDir")), "evidence");
|
|
11133
|
+
function resolveEvidenceRoot(root, config) {
|
|
11134
|
+
return path36.join(path36.dirname(resolvePath(root, config, "specsDir")), "evidence");
|
|
11135
|
+
}
|
|
11136
|
+
async function validatePrototypingDesignSystem(root, config) {
|
|
11137
|
+
const screens = await readUiContractScreenContracts(root, config.paths.contractsDir);
|
|
11138
|
+
if (screens.length === 0) {
|
|
11139
|
+
return [];
|
|
11140
|
+
}
|
|
11141
|
+
const evidenceDir = resolveEvidenceRoot(root, config);
|
|
11142
|
+
const artifact = await loadPrototypingArtifact(evidenceDir);
|
|
11143
|
+
if (artifact.designSystemCompliancePresent) {
|
|
11144
|
+
return [];
|
|
11145
|
+
}
|
|
11146
|
+
const designSystemPath = path36.join(
|
|
11147
|
+
root,
|
|
11148
|
+
config.paths.contractsDir,
|
|
11149
|
+
"design",
|
|
11150
|
+
"design-system.yaml"
|
|
11151
|
+
);
|
|
11152
|
+
const designSystemPresent = await fileExists2(designSystemPath);
|
|
11153
|
+
const severity = designSystemPresent && artifact.mode === "full-harness" ? "error" : "warning";
|
|
11154
|
+
return [toIssue(severity, artifact.fileName)];
|
|
11155
|
+
}
|
|
11156
|
+
|
|
11157
|
+
// src/core/validators/prototyping/executionPlan.ts
|
|
11158
|
+
init_utils();
|
|
11159
|
+
var FIELD_SHAPE_LABEL = {
|
|
11160
|
+
number: "finite number",
|
|
11161
|
+
"non-empty-string": "non-empty string",
|
|
11162
|
+
record: "object (not array)"
|
|
11163
|
+
};
|
|
11164
|
+
var REQUIRED_FIELDS = [
|
|
11165
|
+
{ key: "targetIterations", shape: "number" },
|
|
11166
|
+
{ key: "evaluationAxesSource", shape: "non-empty-string" },
|
|
11167
|
+
{ key: "delegationMap", shape: "record" },
|
|
11168
|
+
{ key: "plannedAt", shape: "non-empty-string" }
|
|
11169
|
+
];
|
|
11170
|
+
function describeType(value) {
|
|
11171
|
+
if (value === null) return "null";
|
|
11172
|
+
if (Array.isArray(value)) return "array";
|
|
11173
|
+
return typeof value;
|
|
11174
|
+
}
|
|
11175
|
+
function matchesShape(value, shape) {
|
|
11176
|
+
switch (shape) {
|
|
11177
|
+
case "number":
|
|
11178
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
11179
|
+
case "non-empty-string":
|
|
11180
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
11181
|
+
case "record":
|
|
11182
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
function isRecord8(v) {
|
|
11186
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11187
|
+
}
|
|
11188
|
+
function detectMode3(raw) {
|
|
11189
|
+
if (!isRecord8(raw)) return "other";
|
|
11190
|
+
if (typeof raw.mode === "string") return raw.mode;
|
|
11191
|
+
if (isRecord8(raw.mode) && typeof raw.mode.effective === "string") {
|
|
11192
|
+
return raw.mode.effective;
|
|
11193
|
+
}
|
|
11194
|
+
return "other";
|
|
11195
|
+
}
|
|
11196
|
+
function validateExecutionPlanIssues(prototypingJson, prototypingJsonPath) {
|
|
11197
|
+
if (!isRecord8(prototypingJson)) {
|
|
11198
|
+
return [];
|
|
11199
|
+
}
|
|
11200
|
+
if (detectMode3(prototypingJson) !== "full-harness") {
|
|
11201
|
+
return [];
|
|
11202
|
+
}
|
|
11203
|
+
if (!isRecord8(prototypingJson.executionPlan)) {
|
|
11204
|
+
return [
|
|
11205
|
+
buildIssue(
|
|
11206
|
+
"executionPlan is required in full-harness mode but is absent or not an object in prototyping.json.",
|
|
11207
|
+
prototypingJsonPath
|
|
11208
|
+
)
|
|
11209
|
+
];
|
|
11210
|
+
}
|
|
11211
|
+
const issues = [];
|
|
11212
|
+
const executionPlan = prototypingJson.executionPlan;
|
|
11213
|
+
for (const { key, shape } of REQUIRED_FIELDS) {
|
|
11214
|
+
const value = executionPlan[key];
|
|
11215
|
+
if (!matchesShape(value, shape)) {
|
|
11216
|
+
issues.push(
|
|
11217
|
+
buildIssue(
|
|
11218
|
+
`executionPlan.${key} is required in full-harness mode and must be a ${FIELD_SHAPE_LABEL[shape]} (got: ${describeType(value)}).`,
|
|
11219
|
+
prototypingJsonPath
|
|
11220
|
+
)
|
|
11221
|
+
);
|
|
11222
|
+
}
|
|
11223
|
+
}
|
|
11224
|
+
return issues;
|
|
11225
|
+
}
|
|
11226
|
+
function buildIssue(message, prototypingJsonPath) {
|
|
11227
|
+
return issue(
|
|
11228
|
+
"QFAI-PROT-310",
|
|
11229
|
+
message,
|
|
11230
|
+
"error",
|
|
11231
|
+
prototypingJsonPath,
|
|
11232
|
+
"prototyping.executionPlan.presence",
|
|
11233
|
+
void 0,
|
|
11234
|
+
"canonical",
|
|
11235
|
+
"full-harness \u30E2\u30FC\u30C9\u3067\u306F `prototyping.json.executionPlan` \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044 (targetIterations / evaluationAxesSource / delegationMap / plannedAt)\u3002"
|
|
11236
|
+
);
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
// src/core/validators/prototyping/delegationMap.ts
|
|
11240
|
+
init_utils();
|
|
11241
|
+
var DELEGATION_SCOPE = {
|
|
11242
|
+
UI\u5B9F\u88C5: ["frontend-engineer", "product-experience-architect"],
|
|
11243
|
+
\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8: ["devops-ci-engineer"],
|
|
11244
|
+
\u8A55\u4FA1\u30B9\u30B3\u30A2\u30EA\u30F3\u30B0: ["product-surface-reviewer", "product-experience-architect"],
|
|
11245
|
+
\u30D3\u30EB\u30C9: ["devops-ci-engineer", "backend-engineer"]
|
|
11246
|
+
};
|
|
11247
|
+
var DELEGATION_CATEGORIES = Object.keys(DELEGATION_SCOPE);
|
|
11248
|
+
function validateDelegationMapIssues(delegationMap, prototypingJsonPath) {
|
|
11249
|
+
if (!delegationMap) {
|
|
11250
|
+
return [];
|
|
11251
|
+
}
|
|
11252
|
+
const issues = [];
|
|
11253
|
+
for (const [category, rawRole] of Object.entries(delegationMap)) {
|
|
11254
|
+
const allowedRoles = DELEGATION_SCOPE[category];
|
|
11255
|
+
if (allowedRoles === void 0) {
|
|
11256
|
+
continue;
|
|
11257
|
+
}
|
|
11258
|
+
if (typeof rawRole !== "string") {
|
|
11259
|
+
issues.push(
|
|
11260
|
+
issue(
|
|
11261
|
+
"QFAI-PROT-311",
|
|
11262
|
+
`Delegation violation: category "${category}" assigned to non-string value (got: ${describeRoleType(rawRole)}). Allowed roles: ${allowedRoles.join(", ")}.`,
|
|
11263
|
+
"error",
|
|
11264
|
+
prototypingJsonPath,
|
|
11265
|
+
"prototyping.executionPlan.delegationMap",
|
|
11266
|
+
void 0,
|
|
11267
|
+
"canonical",
|
|
11268
|
+
`category "${category}" \u306B\u306F string \u306E role \u3092\u5272\u308A\u5F53\u3066\u3066\u304F\u3060\u3055\u3044 (allowed: ${allowedRoles.join(", ")})\u3002`
|
|
11269
|
+
)
|
|
11270
|
+
);
|
|
11271
|
+
continue;
|
|
11272
|
+
}
|
|
11273
|
+
if (!allowedRoles.includes(rawRole)) {
|
|
11274
|
+
issues.push(
|
|
11275
|
+
issue(
|
|
11276
|
+
"QFAI-PROT-311",
|
|
11277
|
+
`Delegation violation: category "${category}" assigned to undefined/invalid role "${rawRole}". Allowed roles: ${allowedRoles.join(", ")}.`,
|
|
11278
|
+
"error",
|
|
11279
|
+
prototypingJsonPath,
|
|
11280
|
+
"prototyping.executionPlan.delegationMap",
|
|
11281
|
+
void 0,
|
|
11282
|
+
"canonical",
|
|
11283
|
+
`category "${category}" \u3092\u8A31\u53EF\u3055\u308C\u305F role \u306B\u5272\u308A\u5F53\u3066\u3066\u304F\u3060\u3055\u3044 (allowed: ${allowedRoles.join(", ")})\u3002`
|
|
11284
|
+
)
|
|
11285
|
+
);
|
|
11286
|
+
}
|
|
11287
|
+
}
|
|
11288
|
+
return issues;
|
|
11289
|
+
}
|
|
11290
|
+
function describeRoleType(value) {
|
|
11291
|
+
if (value === null) return "null";
|
|
11292
|
+
if (Array.isArray(value)) return "array";
|
|
11293
|
+
return typeof value;
|
|
11294
|
+
}
|
|
11295
|
+
|
|
11296
|
+
// src/core/validators/prototyping/screenshotDir.ts
|
|
11297
|
+
init_utils();
|
|
11298
|
+
function isRecord9(v) {
|
|
11299
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11300
|
+
}
|
|
11301
|
+
function validateScreenshotDirIssues(scoringTrace, mode, prototypingJsonPath) {
|
|
11302
|
+
if (mode !== "full-harness") {
|
|
11303
|
+
return [];
|
|
11304
|
+
}
|
|
11305
|
+
const issues = [];
|
|
11306
|
+
const suggestion = "full-harness \u30E2\u30FC\u30C9\u3067\u306F scoringTrace \u306E\u5404 entry \u306B screenshotDir \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
|
|
11307
|
+
for (let i = 0; i < scoringTrace.length; i++) {
|
|
11308
|
+
const entry = scoringTrace[i];
|
|
11309
|
+
if (!isRecord9(entry)) {
|
|
11310
|
+
issues.push(
|
|
11311
|
+
issue(
|
|
11312
|
+
"QFAI-PROT-331",
|
|
11313
|
+
`scoringTrace[${i}] is not an object; screenshotDir cannot be verified.`,
|
|
11314
|
+
"error",
|
|
11315
|
+
prototypingJsonPath,
|
|
11316
|
+
"prototyping.fullHarness.scoringTrace.screenshotDir",
|
|
11317
|
+
void 0,
|
|
11318
|
+
"canonical",
|
|
11319
|
+
suggestion
|
|
11320
|
+
)
|
|
11321
|
+
);
|
|
11322
|
+
continue;
|
|
11323
|
+
}
|
|
11324
|
+
if (typeof entry.screenshotDir !== "string" || entry.screenshotDir.trim() === "") {
|
|
11325
|
+
issues.push(
|
|
11326
|
+
issue(
|
|
11327
|
+
"QFAI-PROT-331",
|
|
11328
|
+
`scoringTrace[${i}].screenshotDir is missing or empty; full-harness requires screenshot evidence per iteration.`,
|
|
11329
|
+
"error",
|
|
11330
|
+
prototypingJsonPath,
|
|
11331
|
+
"prototyping.fullHarness.scoringTrace.screenshotDir",
|
|
11332
|
+
void 0,
|
|
11333
|
+
"canonical",
|
|
11334
|
+
suggestion
|
|
11335
|
+
)
|
|
11336
|
+
);
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
return issues;
|
|
11340
|
+
}
|
|
11341
|
+
|
|
11342
|
+
// src/core/validators/prototyping/lighthouseGate.ts
|
|
11343
|
+
init_utils();
|
|
11344
|
+
function isRecord10(v) {
|
|
11345
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11346
|
+
}
|
|
11347
|
+
function detectMode4(raw) {
|
|
11348
|
+
if (!isRecord10(raw)) return "other";
|
|
11349
|
+
if (typeof raw.mode === "string") return raw.mode;
|
|
11350
|
+
if (isRecord10(raw.mode) && typeof raw.mode.effective === "string") {
|
|
11351
|
+
return raw.mode.effective;
|
|
11352
|
+
}
|
|
11353
|
+
return "other";
|
|
11354
|
+
}
|
|
11355
|
+
function detectSurface(raw) {
|
|
11356
|
+
if (!isRecord10(raw)) return "unknown";
|
|
11357
|
+
if (typeof raw.surface === "string") return raw.surface;
|
|
11358
|
+
return "unknown";
|
|
11359
|
+
}
|
|
11360
|
+
function hasLighthouseReport(raw) {
|
|
11361
|
+
if (!isRecord10(raw)) return false;
|
|
11362
|
+
const report = raw.lighthouseReport ?? raw.lighthouse;
|
|
11363
|
+
if (!report) return false;
|
|
11364
|
+
if (isRecord10(report)) return Object.keys(report).length > 0;
|
|
11365
|
+
if (typeof report === "string") return report.trim().length > 0;
|
|
11366
|
+
return false;
|
|
11367
|
+
}
|
|
11368
|
+
function validateLighthouseGateIssues(prototypingJson, prototypingJsonPath) {
|
|
11369
|
+
const mode = detectMode4(prototypingJson);
|
|
11370
|
+
const surface = detectSurface(prototypingJson);
|
|
11371
|
+
if (mode !== "full-harness" || surface !== "web") {
|
|
11372
|
+
return [];
|
|
11373
|
+
}
|
|
11374
|
+
if (hasLighthouseReport(prototypingJson)) {
|
|
11375
|
+
return [];
|
|
11376
|
+
}
|
|
11377
|
+
return [
|
|
11378
|
+
issue(
|
|
11379
|
+
"QFAI-PROT-332",
|
|
11380
|
+
"Lighthouse Gate is MUST for full-harness + web surface: no Lighthouse report found in prototyping evidence.",
|
|
11381
|
+
"error",
|
|
11382
|
+
prototypingJsonPath,
|
|
11383
|
+
"prototyping.lighthouseReport",
|
|
11384
|
+
void 0,
|
|
11385
|
+
"canonical",
|
|
11386
|
+
"full-harness + web surface \u3067\u306F `prototyping.json.lighthouseReport` (\u307E\u305F\u306F `lighthouse`) \u3092\u8A18\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11387
|
+
)
|
|
11388
|
+
];
|
|
11389
|
+
}
|
|
11390
|
+
|
|
11391
|
+
// src/core/validators/prototyping/iterationGate.ts
|
|
11392
|
+
init_utils();
|
|
11393
|
+
function validateIterationGateIssues(iterations, prototypingJsonPath) {
|
|
11394
|
+
if (iterations.length === 0) {
|
|
11395
|
+
return [];
|
|
11396
|
+
}
|
|
11397
|
+
for (const entry of iterations) {
|
|
11398
|
+
if (entry.iterationCount === 1 && entry.converged === true) {
|
|
11399
|
+
return [
|
|
11400
|
+
issue(
|
|
11401
|
+
"QFAI-PROT-333",
|
|
11402
|
+
"minimum 2 iterations required before convergence: iteration 1 cannot be marked converged.",
|
|
11403
|
+
"error",
|
|
11404
|
+
prototypingJsonPath,
|
|
11405
|
+
"prototyping.fullHarness.iterations.convergence",
|
|
11406
|
+
void 0,
|
|
11407
|
+
"canonical",
|
|
11408
|
+
"iteration 1 \u3067 converged=true \u306F\u8A31\u5BB9\u3055\u308C\u307E\u305B\u3093\u3002\u5C11\u306A\u304F\u3068\u3082 2 iteration \u4EE5\u4E0A \u8D70\u3089\u305B\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11409
|
+
)
|
|
11410
|
+
];
|
|
11411
|
+
}
|
|
11412
|
+
}
|
|
11413
|
+
return [];
|
|
11414
|
+
}
|
|
11415
|
+
|
|
11416
|
+
// src/core/validators/prototyping/designSystemThreshold.ts
|
|
11417
|
+
init_utils();
|
|
11418
|
+
import { access as access10 } from "fs/promises";
|
|
11419
|
+
import path37 from "path";
|
|
11420
|
+
var DS_PASS_THRESHOLD = 0.75;
|
|
11421
|
+
function isRecord11(v) {
|
|
11422
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11423
|
+
}
|
|
11424
|
+
async function designSystemMdPresent(packDir) {
|
|
11425
|
+
try {
|
|
11426
|
+
await access10(path37.join(packDir, "uiux", "12_design_system.md"));
|
|
11427
|
+
return true;
|
|
11428
|
+
} catch {
|
|
11429
|
+
return false;
|
|
11430
|
+
}
|
|
11431
|
+
}
|
|
11432
|
+
function extractScore(prototypingRecord) {
|
|
11433
|
+
if (!isRecord11(prototypingRecord)) return null;
|
|
11434
|
+
const trace = prototypingRecord.scoringTrace;
|
|
11435
|
+
if (!isRecord11(trace)) return null;
|
|
11436
|
+
const score = trace.designSystemCompliance;
|
|
11437
|
+
if (typeof score !== "number") return null;
|
|
11438
|
+
return score;
|
|
11439
|
+
}
|
|
11440
|
+
async function validateDesignSystemThresholdIssues(packDir, prototypingRecord, prototypingJsonPath) {
|
|
11441
|
+
const hasDesignSystem = await designSystemMdPresent(packDir);
|
|
11442
|
+
if (!hasDesignSystem) {
|
|
11443
|
+
return [];
|
|
11444
|
+
}
|
|
11445
|
+
const score = extractScore(prototypingRecord);
|
|
11446
|
+
if (score === null) {
|
|
11447
|
+
return [];
|
|
11448
|
+
}
|
|
11449
|
+
if (score >= DS_PASS_THRESHOLD) {
|
|
11450
|
+
return [];
|
|
11451
|
+
}
|
|
11452
|
+
return [
|
|
11453
|
+
issue(
|
|
11454
|
+
"QFAI-PROT-334",
|
|
11455
|
+
`designSystemCompliance score ${(score * 100).toFixed(0)}% is below threshold ${(DS_PASS_THRESHOLD * 100).toFixed(0)}%; immediate fix required for next iteration.`,
|
|
11456
|
+
"error",
|
|
11457
|
+
prototypingJsonPath,
|
|
11458
|
+
"prototyping.scoringTrace.designSystemCompliance.threshold",
|
|
11459
|
+
void 0,
|
|
11460
|
+
"canonical",
|
|
11461
|
+
`scoringTrace.designSystemCompliance \u3092\u95BE\u5024 ${(DS_PASS_THRESHOLD * 100).toFixed(0)}% \u4EE5\u4E0A\u306B\u4E0A\u3052\u308B\u4FEE\u6B63\u3092\u6B21\u30A4\u30C6\u30EC\u30FC\u30B7\u30E7\u30F3\u306B\u542B\u3081\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
11462
|
+
)
|
|
11463
|
+
];
|
|
11464
|
+
}
|
|
11465
|
+
|
|
11466
|
+
// src/core/validators/prototyping/stateGate.ts
|
|
11467
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
11468
|
+
import path38 from "path";
|
|
11469
|
+
function isRecord12(value) {
|
|
11470
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11471
|
+
}
|
|
11472
|
+
function extractDelegationMap(prototypingJson) {
|
|
11473
|
+
if (!isRecord12(prototypingJson)) return void 0;
|
|
11474
|
+
const executionPlan = prototypingJson.executionPlan;
|
|
11475
|
+
if (!isRecord12(executionPlan)) return void 0;
|
|
11476
|
+
const delegationMap = executionPlan.delegationMap;
|
|
11477
|
+
if (!isRecord12(delegationMap)) return void 0;
|
|
11478
|
+
return delegationMap;
|
|
11479
|
+
}
|
|
11480
|
+
function extractMode(prototypingJson) {
|
|
11481
|
+
if (!isRecord12(prototypingJson)) return "other";
|
|
11482
|
+
if (typeof prototypingJson.mode === "string") return prototypingJson.mode;
|
|
11483
|
+
if (isRecord12(prototypingJson.mode) && typeof prototypingJson.mode.effective === "string") {
|
|
11484
|
+
return prototypingJson.mode.effective;
|
|
11485
|
+
}
|
|
11486
|
+
return "other";
|
|
11487
|
+
}
|
|
11488
|
+
function extractScoringTrace(prototypingJson) {
|
|
11489
|
+
if (!isRecord12(prototypingJson)) return [];
|
|
11490
|
+
const fullHarness = prototypingJson.fullHarness;
|
|
11491
|
+
if (!isRecord12(fullHarness)) return [];
|
|
11492
|
+
const trace = fullHarness.scoringTrace;
|
|
11493
|
+
return Array.isArray(trace) ? trace : [];
|
|
11494
|
+
}
|
|
11495
|
+
function extractFullHarnessIterations(prototypingJson) {
|
|
11496
|
+
if (!isRecord12(prototypingJson)) return [];
|
|
11497
|
+
const fullHarness = prototypingJson.fullHarness;
|
|
11498
|
+
if (!isRecord12(fullHarness)) return [];
|
|
11499
|
+
const iterations = fullHarness.iterations;
|
|
11500
|
+
if (!Array.isArray(iterations)) return [];
|
|
11501
|
+
return iterations.filter(
|
|
11502
|
+
(entry) => isRecord12(entry) && typeof entry.iterationCount === "number"
|
|
11503
|
+
);
|
|
11504
|
+
}
|
|
11505
|
+
function resolveCalibrationPackDir(root, config) {
|
|
11506
|
+
const packPath = config.prototyping?.calibration?.packPath;
|
|
11507
|
+
if (!packPath || typeof packPath !== "string") return void 0;
|
|
11508
|
+
return path38.resolve(root, packPath);
|
|
11509
|
+
}
|
|
11510
|
+
async function validateStateGate(root, config) {
|
|
11511
|
+
const evidenceRoot = path38.join(
|
|
11512
|
+
path38.dirname(path38.resolve(root, config.paths.specsDir)),
|
|
11513
|
+
"evidence"
|
|
11514
|
+
);
|
|
11515
|
+
const prototypingJsonPath = path38.join(evidenceRoot, "prototyping.json");
|
|
11516
|
+
let raw;
|
|
11517
|
+
try {
|
|
11518
|
+
raw = await readFile26(prototypingJsonPath, "utf-8");
|
|
11519
|
+
} catch {
|
|
11520
|
+
return [];
|
|
11521
|
+
}
|
|
11522
|
+
let parsed;
|
|
11523
|
+
try {
|
|
11524
|
+
parsed = JSON.parse(raw);
|
|
11525
|
+
} catch {
|
|
11526
|
+
return [];
|
|
11527
|
+
}
|
|
11528
|
+
const relPath = path38.relative(root, prototypingJsonPath).replace(/\\/g, "/");
|
|
11529
|
+
const mode = extractMode(parsed);
|
|
11530
|
+
const issues = [];
|
|
11531
|
+
issues.push(...validateExecutionPlanIssues(parsed, relPath));
|
|
11532
|
+
issues.push(...validateDelegationMapIssues(extractDelegationMap(parsed), relPath));
|
|
11533
|
+
issues.push(...validateScreenshotDirIssues(extractScoringTrace(parsed), mode, relPath));
|
|
11534
|
+
issues.push(...validateIterationGateIssues(extractFullHarnessIterations(parsed), relPath));
|
|
11535
|
+
issues.push(...validateLighthouseGateIssues(parsed, relPath));
|
|
11536
|
+
const packDir = resolveCalibrationPackDir(root, config);
|
|
11537
|
+
if (packDir) {
|
|
11538
|
+
issues.push(...await validateDesignSystemThresholdIssues(packDir, parsed, relPath));
|
|
11539
|
+
}
|
|
11540
|
+
return issues;
|
|
11541
|
+
}
|
|
11542
|
+
|
|
11543
|
+
// src/core/validators/prototyping/completionCertificate.ts
|
|
11544
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
11545
|
+
import path40 from "path";
|
|
11546
|
+
|
|
11547
|
+
// src/core/prototyping/certificate.ts
|
|
11548
|
+
import { createHash } from "crypto";
|
|
11549
|
+
import { mkdir as mkdir4, readFile as readFile27, readdir as readdir8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
11550
|
+
import path39 from "path";
|
|
11551
|
+
var COMPLETION_CERTIFICATE_REL_PATH = ".qfai/evidence/prototyping/completion-certificate.json";
|
|
11552
|
+
async function loadCompletionCertificate(root) {
|
|
11553
|
+
const fullPath = path39.join(root, COMPLETION_CERTIFICATE_REL_PATH);
|
|
11554
|
+
try {
|
|
11555
|
+
const raw = await readFile27(fullPath, "utf-8");
|
|
11556
|
+
const parsed = JSON.parse(raw);
|
|
11557
|
+
if (!isMinimallyValidCertificate(parsed)) {
|
|
11558
|
+
return null;
|
|
11559
|
+
}
|
|
11560
|
+
return parsed;
|
|
11561
|
+
} catch {
|
|
11562
|
+
return null;
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
function isMinimallyValidCertificate(value) {
|
|
11566
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
11567
|
+
const v = value;
|
|
11568
|
+
if (v.schemaVersion !== "1.0") return false;
|
|
11569
|
+
if (typeof v.runId !== "string") return false;
|
|
11570
|
+
if (!Array.isArray(v.evidenceDigests)) return false;
|
|
11571
|
+
for (const entry of v.evidenceDigests) {
|
|
11572
|
+
if (!entry || typeof entry !== "object") return false;
|
|
11573
|
+
const e = entry;
|
|
11574
|
+
if (typeof e.path !== "string" || typeof e.sha256 !== "string") return false;
|
|
11575
|
+
}
|
|
11576
|
+
if (!v.validateRun || typeof v.validateRun !== "object") return false;
|
|
11577
|
+
if (!v.verifyRun || typeof v.verifyRun !== "object") return false;
|
|
11578
|
+
if (!v.reviewerSignoff || typeof v.reviewerSignoff !== "object") return false;
|
|
11579
|
+
return true;
|
|
11580
|
+
}
|
|
11581
|
+
async function checkCompletionCertificate(root) {
|
|
11582
|
+
const cert = await loadCompletionCertificate(root);
|
|
11583
|
+
if (!cert) {
|
|
11584
|
+
return {
|
|
11585
|
+
ok: false,
|
|
11586
|
+
reasons: [
|
|
11587
|
+
`${COMPLETION_CERTIFICATE_REL_PATH} not found. Run \`qfai prototyping certify\` after all gates pass.`
|
|
11588
|
+
]
|
|
11589
|
+
};
|
|
11590
|
+
}
|
|
11591
|
+
const reasons = [];
|
|
11592
|
+
const evidenceRoot = path39.join(root, ".qfai/evidence/prototyping");
|
|
11593
|
+
const currentDigests = await scanEvidenceDigests(evidenceRoot);
|
|
11594
|
+
const certMap = new Map(cert.evidenceDigests.map((entry) => [entry.path, entry.sha256]));
|
|
11595
|
+
const currMap = new Map(currentDigests.map((entry) => [entry.path, entry.sha256]));
|
|
11596
|
+
for (const [p, hash] of certMap) {
|
|
11597
|
+
const observed = currMap.get(p);
|
|
11598
|
+
if (observed === void 0) {
|
|
11599
|
+
reasons.push(`evidence file removed since certify: ${p}`);
|
|
11600
|
+
} else if (observed !== hash) {
|
|
11601
|
+
reasons.push(`digest mismatch (file modified since certify): ${p}`);
|
|
11602
|
+
}
|
|
11603
|
+
}
|
|
11604
|
+
for (const [p] of currMap) {
|
|
11605
|
+
if (!certMap.has(p)) {
|
|
11606
|
+
reasons.push(`new evidence file present but not in certificate: ${p}`);
|
|
11607
|
+
}
|
|
11608
|
+
}
|
|
11609
|
+
if (cert.validateRun.errorCount !== 0) {
|
|
11610
|
+
reasons.push("validateRun.errorCount must be 0");
|
|
11611
|
+
}
|
|
11612
|
+
if (cert.verifyRun.status !== "PASS") {
|
|
11613
|
+
reasons.push("verifyRun.status must be PASS");
|
|
11614
|
+
}
|
|
11615
|
+
if (!cert.reviewerSignoff.approved) {
|
|
11616
|
+
reasons.push("reviewerSignoff.approved must be true");
|
|
11617
|
+
}
|
|
11618
|
+
return reasons.length === 0 ? { ok: true } : { ok: false, reasons };
|
|
11619
|
+
}
|
|
11620
|
+
async function scanEvidenceDigests(evidenceRoot) {
|
|
11621
|
+
const out = [];
|
|
11622
|
+
await walk2(evidenceRoot, evidenceRoot, out);
|
|
11623
|
+
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
11624
|
+
return out;
|
|
11625
|
+
}
|
|
11626
|
+
async function walk2(rootDir, dir, out) {
|
|
11627
|
+
let entries;
|
|
11628
|
+
try {
|
|
11629
|
+
entries = await readdir8(dir);
|
|
11630
|
+
} catch {
|
|
11631
|
+
return;
|
|
11632
|
+
}
|
|
11633
|
+
for (const name of entries) {
|
|
11634
|
+
if (name === "completion-certificate.json") continue;
|
|
11635
|
+
const full = path39.join(dir, name);
|
|
11636
|
+
let s;
|
|
11637
|
+
try {
|
|
11638
|
+
s = await stat5(full);
|
|
11639
|
+
} catch {
|
|
11640
|
+
continue;
|
|
11641
|
+
}
|
|
11642
|
+
if (s.isDirectory()) {
|
|
11643
|
+
await walk2(rootDir, full, out);
|
|
11644
|
+
} else if (s.isFile()) {
|
|
11645
|
+
const buf = await readFile27(full);
|
|
11646
|
+
const hash = createHash("sha256").update(buf).digest("hex");
|
|
11647
|
+
const rel = path39.relative(rootDir, full).replace(/\\/g, "/");
|
|
11648
|
+
out.push({ path: rel, sha256: hash });
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
}
|
|
11652
|
+
|
|
11653
|
+
// src/core/validators/prototyping/completionCertificate.ts
|
|
11654
|
+
init_utils();
|
|
11655
|
+
var CERT_REL_PATH = ".qfai/evidence/prototyping/completion-certificate.json";
|
|
11656
|
+
function isRecord13(value) {
|
|
11657
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11658
|
+
}
|
|
11659
|
+
async function isCompletionClaimed(root, config) {
|
|
11660
|
+
const evidenceRoot = path40.join(
|
|
11661
|
+
path40.dirname(path40.resolve(root, config.paths.specsDir)),
|
|
11662
|
+
"evidence"
|
|
11663
|
+
);
|
|
11664
|
+
let raw;
|
|
11665
|
+
try {
|
|
11666
|
+
raw = await readFile28(path40.join(evidenceRoot, "prototyping.json"), "utf-8");
|
|
11667
|
+
} catch {
|
|
11668
|
+
return false;
|
|
11669
|
+
}
|
|
11670
|
+
let parsed;
|
|
11671
|
+
try {
|
|
11672
|
+
parsed = JSON.parse(raw);
|
|
11673
|
+
} catch {
|
|
11674
|
+
return false;
|
|
11675
|
+
}
|
|
11676
|
+
if (!isRecord13(parsed)) return false;
|
|
11677
|
+
if (parsed.completionClaimed === true) return true;
|
|
11678
|
+
if (parsed.phase === "completed") return true;
|
|
11679
|
+
if (isRecord13(parsed.completionCertificate)) return true;
|
|
11680
|
+
return false;
|
|
11681
|
+
}
|
|
11682
|
+
async function validateCompletionCertificateIssues(root, config) {
|
|
11683
|
+
const certificate = await loadCompletionCertificate(root);
|
|
11684
|
+
const claimed = await isCompletionClaimed(root, config);
|
|
11685
|
+
if (!certificate) {
|
|
11686
|
+
if (claimed) {
|
|
11687
|
+
return [
|
|
11688
|
+
issue(
|
|
11689
|
+
"QFAI-PROT-335",
|
|
11690
|
+
`${CERT_REL_PATH} is required when completion is claimed. Run \`qfai prototyping certify\` after all gates pass.`,
|
|
11691
|
+
"error",
|
|
11692
|
+
CERT_REL_PATH,
|
|
11693
|
+
"prototyping.completionCertificate.presence",
|
|
11694
|
+
void 0,
|
|
11695
|
+
"canonical",
|
|
11696
|
+
"completion \u4E3B\u5F35\u306E\u524D\u306B `qfai prototyping certify` \u3092\u6210\u529F\u3055\u305B\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11697
|
+
)
|
|
11698
|
+
];
|
|
11699
|
+
}
|
|
11700
|
+
return [];
|
|
11701
|
+
}
|
|
11702
|
+
if (!claimed) return [];
|
|
11703
|
+
const result = await checkCompletionCertificate(root);
|
|
11704
|
+
if (result.ok) return [];
|
|
11705
|
+
return [
|
|
11706
|
+
issue(
|
|
11707
|
+
"QFAI-PROT-336",
|
|
11708
|
+
`${CERT_REL_PATH} digest mismatch \u2014 evidence has been modified since certify: ${result.reasons.join("; ")}`,
|
|
11709
|
+
"error",
|
|
11710
|
+
CERT_REL_PATH,
|
|
11711
|
+
"prototyping.completionCertificate.digest",
|
|
11712
|
+
void 0,
|
|
11713
|
+
"canonical",
|
|
11714
|
+
"evidence \u3092\u5909\u66F4\u3057\u305F\u5834\u5408\u306F `qfai prototyping certify` \u3092\u518D\u5B9F\u884C\u3057\u3066 certificate \u3092\u66F4\u65B0\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11715
|
+
)
|
|
11716
|
+
];
|
|
11717
|
+
}
|
|
11718
|
+
|
|
11719
|
+
// src/core/validators/configReferenceIntegrity.ts
|
|
11720
|
+
init_utils();
|
|
11721
|
+
import { stat as stat6 } from "fs/promises";
|
|
11722
|
+
import path41 from "path";
|
|
11723
|
+
async function isDirectory(absolutePath) {
|
|
11724
|
+
try {
|
|
11725
|
+
const s = await stat6(absolutePath);
|
|
11726
|
+
return s.isDirectory();
|
|
11727
|
+
} catch {
|
|
11728
|
+
return false;
|
|
11729
|
+
}
|
|
11730
|
+
}
|
|
11731
|
+
async function pathExists(absolutePath) {
|
|
11732
|
+
try {
|
|
11733
|
+
await stat6(absolutePath);
|
|
11734
|
+
return true;
|
|
11735
|
+
} catch {
|
|
11736
|
+
return false;
|
|
11737
|
+
}
|
|
11738
|
+
}
|
|
11739
|
+
var VERIFIED_PATH_KEYS = [
|
|
11740
|
+
"specsDir",
|
|
11741
|
+
"contractsDir",
|
|
11742
|
+
"discussionDir",
|
|
11743
|
+
"skillsDir",
|
|
11744
|
+
"srcDir",
|
|
11745
|
+
"testsDir"
|
|
11746
|
+
];
|
|
11747
|
+
async function validateConfigReferenceIntegrity(root, config) {
|
|
11748
|
+
const issues = [];
|
|
11749
|
+
const primarySpecId = config.prototyping?.primarySpecId;
|
|
11750
|
+
if (primarySpecId !== void 0) {
|
|
11751
|
+
const specDir = path41.join(path41.resolve(root, config.paths.specsDir), `spec-${primarySpecId}`);
|
|
11752
|
+
if (!await isDirectory(specDir)) {
|
|
11753
|
+
const relSpecDir = path41.relative(root, specDir).replace(/\\/g, "/");
|
|
11754
|
+
issues.push(
|
|
11755
|
+
issue(
|
|
11756
|
+
"QFAI-CFG-LINK-001",
|
|
11757
|
+
`qfai.config.yaml: prototyping.primarySpecId="${primarySpecId}" but ${relSpecDir} does not exist.`,
|
|
11758
|
+
"error",
|
|
11759
|
+
"qfai.config.yaml",
|
|
11760
|
+
"config.prototyping.primarySpecId.reality",
|
|
11761
|
+
void 0,
|
|
11762
|
+
"canonical",
|
|
11763
|
+
"prototyping.primarySpecId \u3092\u5B9F\u5728\u3059\u308B spec-NNNN \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u5408\u308F\u305B\u308B\u304B\u3001\u5BFE\u5FDC\u3059\u308B spec \u3092 `.qfai/specs/spec-NNNN/` \u306B\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11764
|
+
)
|
|
11765
|
+
);
|
|
11766
|
+
}
|
|
11767
|
+
}
|
|
11768
|
+
for (const key of VERIFIED_PATH_KEYS) {
|
|
11769
|
+
const relPath = config.paths[key];
|
|
11770
|
+
const absolutePath = path41.resolve(root, relPath);
|
|
11771
|
+
if (!await isDirectory(absolutePath)) {
|
|
11772
|
+
issues.push(
|
|
11773
|
+
issue(
|
|
11774
|
+
"QFAI-CFG-LINK-002",
|
|
11775
|
+
`qfai.config.yaml: paths.${key}="${relPath}" but the directory does not exist.`,
|
|
11776
|
+
"warning",
|
|
11777
|
+
"qfai.config.yaml",
|
|
11778
|
+
`config.paths.${key}.reality`,
|
|
11779
|
+
void 0,
|
|
11780
|
+
"canonical",
|
|
11781
|
+
`paths.${key} \u3092\u5B9F\u5728\u3059\u308B\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u5408\u308F\u305B\u308B\u304B\u3001\u5BFE\u5FDC\u3059\u308B\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
11782
|
+
)
|
|
11783
|
+
);
|
|
11784
|
+
}
|
|
11785
|
+
}
|
|
11786
|
+
const packPath = config.prototyping?.calibration?.packPath;
|
|
11787
|
+
if (packPath !== void 0) {
|
|
11788
|
+
const absolutePackPath = path41.resolve(root, packPath);
|
|
11789
|
+
const exists9 = await pathExists(absolutePackPath);
|
|
11790
|
+
if (!exists9) {
|
|
11791
|
+
issues.push(
|
|
11792
|
+
issue(
|
|
11793
|
+
"QFAI-CFG-LINK-003",
|
|
11794
|
+
`qfai.config.yaml: prototyping.calibration.packPath="${packPath}" but the path does not exist on disk.`,
|
|
11795
|
+
"error",
|
|
11796
|
+
"qfai.config.yaml",
|
|
11797
|
+
"config.prototyping.calibration.packPath.reality",
|
|
11798
|
+
void 0,
|
|
11799
|
+
"canonical",
|
|
11800
|
+
"prototyping.calibration.packPath \u3092\u5B9F\u5728\u3059\u308B calibration pack (YAML \u30D5\u30A1\u30A4\u30EB \u307E\u305F\u306F \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA) \u306B\u5408\u308F\u305B\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
11801
|
+
)
|
|
11802
|
+
);
|
|
11803
|
+
}
|
|
11804
|
+
}
|
|
11805
|
+
return issues;
|
|
11806
|
+
}
|
|
11807
|
+
|
|
11808
|
+
// src/core/validators/prototyping/refIntegrity.ts
|
|
11809
|
+
import { readFile as readFile29, stat as stat7 } from "fs/promises";
|
|
11810
|
+
import path42 from "path";
|
|
11811
|
+
init_utils();
|
|
11812
|
+
function isRecord14(value) {
|
|
11813
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11814
|
+
}
|
|
11815
|
+
async function fileExists3(absolutePath) {
|
|
11816
|
+
try {
|
|
11817
|
+
const s = await stat7(absolutePath);
|
|
11818
|
+
return s.isFile();
|
|
11819
|
+
} catch {
|
|
11820
|
+
return false;
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
async function loadJson(absolutePath) {
|
|
11824
|
+
try {
|
|
11825
|
+
const raw = await readFile29(absolutePath, "utf-8");
|
|
11826
|
+
return JSON.parse(raw);
|
|
11827
|
+
} catch {
|
|
11828
|
+
return null;
|
|
11829
|
+
}
|
|
11830
|
+
}
|
|
11831
|
+
function collectPrototypingJsonRefs(prototypingJson, sourcePath) {
|
|
11832
|
+
const out = [];
|
|
11833
|
+
if (!isRecord14(prototypingJson)) return out;
|
|
11834
|
+
const push = (ref, field) => {
|
|
11835
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
11836
|
+
out.push({ ref, source: sourcePath, field });
|
|
11837
|
+
}
|
|
11838
|
+
};
|
|
11839
|
+
const rounds = prototypingJson.rounds;
|
|
11840
|
+
if (Array.isArray(rounds)) {
|
|
11841
|
+
for (let i = 0; i < rounds.length; i++) {
|
|
11842
|
+
const round = rounds[i];
|
|
11843
|
+
if (!isRecord14(round)) continue;
|
|
11844
|
+
push(round.harvestRef, `rounds[${i}].harvestRef`);
|
|
11845
|
+
push(round.narrowDecisionRef, `rounds[${i}].narrowDecisionRef`);
|
|
11846
|
+
push(round.absorptionPlanRef, `rounds[${i}].absorptionPlanRef`);
|
|
11847
|
+
push(round.reimplementationRef, `rounds[${i}].reimplementationRef`);
|
|
11848
|
+
const evalMap = round.evaluatorReviewRefsByCandidate;
|
|
11849
|
+
if (isRecord14(evalMap)) {
|
|
11850
|
+
for (const [cid, ref] of Object.entries(evalMap)) {
|
|
11851
|
+
push(ref, `rounds[${i}].evaluatorReviewRefsByCandidate.${cid}`);
|
|
11852
|
+
}
|
|
11853
|
+
}
|
|
11854
|
+
const screenMap = round.screenEvidenceByCandidate;
|
|
11855
|
+
if (isRecord14(screenMap)) {
|
|
11856
|
+
for (const [cid, candidateScreens] of Object.entries(screenMap)) {
|
|
11857
|
+
if (!isRecord14(candidateScreens)) continue;
|
|
11858
|
+
for (const [screenId, refs] of Object.entries(candidateScreens)) {
|
|
11859
|
+
if (!isRecord14(refs)) continue;
|
|
11860
|
+
push(
|
|
11861
|
+
refs.screenshotRef,
|
|
11862
|
+
`rounds[${i}].screenEvidenceByCandidate.${cid}.${screenId}.screenshotRef`
|
|
11863
|
+
);
|
|
11864
|
+
push(refs.htmlRef, `rounds[${i}].screenEvidenceByCandidate.${cid}.${screenId}.htmlRef`);
|
|
11865
|
+
push(
|
|
11866
|
+
refs.snapshotRef,
|
|
11867
|
+
`rounds[${i}].screenEvidenceByCandidate.${cid}.${screenId}.snapshotRef`
|
|
11868
|
+
);
|
|
11869
|
+
push(
|
|
11870
|
+
refs.commandLogRef,
|
|
11871
|
+
`rounds[${i}].screenEvidenceByCandidate.${cid}.${screenId}.commandLogRef`
|
|
11872
|
+
);
|
|
11873
|
+
}
|
|
11874
|
+
}
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11877
|
+
}
|
|
11878
|
+
const polishCycles = prototypingJson.polishCycles;
|
|
11879
|
+
if (Array.isArray(polishCycles)) {
|
|
11880
|
+
for (let i = 0; i < polishCycles.length; i++) {
|
|
11881
|
+
const cycle = polishCycles[i];
|
|
11882
|
+
if (!isRecord14(cycle)) continue;
|
|
11883
|
+
push(cycle.breakthroughRef, `polishCycles[${i}].breakthroughRef`);
|
|
11884
|
+
push(cycle.reviewerGateRef, `polishCycles[${i}].reviewerGateRef`);
|
|
11885
|
+
push(cycle.evaluatorReviewRef, `polishCycles[${i}].evaluatorReviewRef`);
|
|
11886
|
+
}
|
|
11887
|
+
}
|
|
11888
|
+
const bestOfHistory = prototypingJson.bestOfHistory;
|
|
11889
|
+
if (isRecord14(bestOfHistory)) {
|
|
11890
|
+
push(bestOfHistory.evidenceRef, "bestOfHistory.evidenceRef");
|
|
11891
|
+
}
|
|
11892
|
+
const breakthrough = prototypingJson.breakthrough;
|
|
11893
|
+
if (isRecord14(breakthrough)) {
|
|
11894
|
+
push(breakthrough.evidenceRef, "breakthrough.evidenceRef");
|
|
11895
|
+
}
|
|
11896
|
+
const reviewerGate = prototypingJson.reviewerGate;
|
|
11897
|
+
if (isRecord14(reviewerGate)) {
|
|
11898
|
+
push(reviewerGate.evidenceRef, "reviewerGate.evidenceRef");
|
|
11899
|
+
const signoff = reviewerGate.signoff;
|
|
11900
|
+
if (isRecord14(signoff)) {
|
|
11901
|
+
push(signoff.evidenceRef, "reviewerGate.signoff.evidenceRef");
|
|
11902
|
+
}
|
|
11903
|
+
}
|
|
11904
|
+
const fullHarness = prototypingJson.fullHarness;
|
|
11905
|
+
if (isRecord14(fullHarness) && Array.isArray(fullHarness.iterations)) {
|
|
11906
|
+
const iterations = fullHarness.iterations;
|
|
11907
|
+
for (let i = 0; i < iterations.length; i++) {
|
|
11908
|
+
const it = iterations[i];
|
|
11909
|
+
if (!isRecord14(it)) continue;
|
|
11910
|
+
const er = it.evidenceRefs;
|
|
11911
|
+
if (!isRecord14(er)) continue;
|
|
11912
|
+
for (const [key, refs] of Object.entries(er)) {
|
|
11913
|
+
if (!Array.isArray(refs)) continue;
|
|
11914
|
+
for (let j = 0; j < refs.length; j++) {
|
|
11915
|
+
const refValue = refs[j];
|
|
11916
|
+
push(refValue, `fullHarness.iterations[${i}].evidenceRefs.${key}[${j}]`);
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
}
|
|
11920
|
+
}
|
|
11921
|
+
return out;
|
|
11922
|
+
}
|
|
11923
|
+
function collectReviewBundleRefs(bundle, sourcePath) {
|
|
11924
|
+
const out = [];
|
|
11925
|
+
if (!isRecord14(bundle)) return out;
|
|
11926
|
+
const push = (ref, field) => {
|
|
11927
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
11928
|
+
out.push({ ref, source: sourcePath, field });
|
|
11929
|
+
}
|
|
11930
|
+
};
|
|
11931
|
+
push(bundle.axisDefsRef, "axisDefsRef");
|
|
11932
|
+
push(bundle.designSystemChecklistRef, "designSystemChecklistRef");
|
|
11933
|
+
push(bundle.commandPlanRef, "commandPlanRef");
|
|
11934
|
+
const candidates = bundle.candidates;
|
|
11935
|
+
if (Array.isArray(candidates)) {
|
|
11936
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
11937
|
+
const candidate = candidates[i];
|
|
11938
|
+
if (!isRecord14(candidate)) continue;
|
|
11939
|
+
push(candidate.conceptRef, `candidates[${i}].conceptRef`);
|
|
11940
|
+
push(candidate.previousEvaluatorReviewRef, `candidates[${i}].previousEvaluatorReviewRef`);
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
return out;
|
|
11944
|
+
}
|
|
11945
|
+
function collectBreakthroughRefs(breakthroughJson, sourcePath) {
|
|
11946
|
+
const out = [];
|
|
11947
|
+
if (!isRecord14(breakthroughJson)) return out;
|
|
11948
|
+
const branchRefs = breakthroughJson.branchRefs;
|
|
11949
|
+
if (Array.isArray(branchRefs)) {
|
|
11950
|
+
for (let i = 0; i < branchRefs.length; i++) {
|
|
11951
|
+
const ref = branchRefs[i];
|
|
11952
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
11953
|
+
out.push({ ref, source: sourcePath, field: `branchRefs[${i}]` });
|
|
11954
|
+
}
|
|
11955
|
+
}
|
|
11956
|
+
}
|
|
11957
|
+
return out;
|
|
11958
|
+
}
|
|
11959
|
+
async function validatePrototypingArtifactRefIntegrity(root, config) {
|
|
11960
|
+
const issues = [];
|
|
11961
|
+
const evidenceRoot = path42.join(path42.dirname(resolvePath(root, config, "specsDir")), "evidence");
|
|
11962
|
+
const collectedRefs = [];
|
|
11963
|
+
const protoPath = path42.join(evidenceRoot, "prototyping.json");
|
|
11964
|
+
const protoJson = await loadJson(protoPath);
|
|
11965
|
+
if (protoJson) {
|
|
11966
|
+
const protoRel = path42.relative(root, protoPath).replace(/\\/g, "/");
|
|
11967
|
+
collectedRefs.push(...collectPrototypingJsonRefs(protoJson, protoRel));
|
|
11968
|
+
}
|
|
11969
|
+
for (const round of EXPLORATION_ROUNDS) {
|
|
11970
|
+
const bundlePath = path42.join(
|
|
11971
|
+
evidenceRoot,
|
|
11972
|
+
"prototyping",
|
|
11973
|
+
"rounds",
|
|
11974
|
+
round,
|
|
11975
|
+
"review-bundle.json"
|
|
11976
|
+
);
|
|
11977
|
+
const bundleJson = await loadJson(bundlePath);
|
|
11978
|
+
if (bundleJson) {
|
|
11979
|
+
const bundleRel = path42.relative(root, bundlePath).replace(/\\/g, "/");
|
|
11980
|
+
collectedRefs.push(...collectReviewBundleRefs(bundleJson, bundleRel));
|
|
11981
|
+
}
|
|
11982
|
+
}
|
|
11983
|
+
const breakthroughPath = path42.join(evidenceRoot, "prototyping", "breakthrough.json");
|
|
11984
|
+
const breakthroughJson = await loadJson(breakthroughPath);
|
|
11985
|
+
if (breakthroughJson) {
|
|
11986
|
+
const breakthroughRel = path42.relative(root, breakthroughPath).replace(/\\/g, "/");
|
|
11987
|
+
collectedRefs.push(...collectBreakthroughRefs(breakthroughJson, breakthroughRel));
|
|
11988
|
+
}
|
|
11989
|
+
for (const ref of collectedRefs) {
|
|
11990
|
+
const candidatePath = path42.isAbsolute(ref.ref) ? ref.ref : path42.join(root, ref.ref);
|
|
11991
|
+
if (!await fileExists3(candidatePath)) {
|
|
11992
|
+
issues.push(
|
|
11993
|
+
issue(
|
|
11994
|
+
"QFAI-PROT-REF-001",
|
|
11995
|
+
`${ref.source}: ${ref.field}="${ref.ref}" points to a file that does not exist on disk.`,
|
|
11996
|
+
"warning",
|
|
11997
|
+
ref.source,
|
|
11998
|
+
"prototyping.artifactRef.reality",
|
|
11999
|
+
void 0,
|
|
12000
|
+
"canonical",
|
|
12001
|
+
"ref \u3092\u5B9F\u5728\u3059\u308B\u30D5\u30A1\u30A4\u30EB\u306B\u5408\u308F\u305B\u308B\u304B\u3001\u6B20\u843D artifact \u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044 (round-* / verify CLI \u3092\u518D\u5B9F\u884C)\u3002"
|
|
12002
|
+
)
|
|
12003
|
+
);
|
|
12004
|
+
}
|
|
12005
|
+
}
|
|
12006
|
+
return issues;
|
|
12007
|
+
}
|
|
12008
|
+
|
|
12009
|
+
// src/core/validators/prototyping/specIdLinkage.ts
|
|
12010
|
+
import { readFile as readFile30, stat as stat8 } from "fs/promises";
|
|
12011
|
+
import path43 from "path";
|
|
12012
|
+
init_utils();
|
|
12013
|
+
function isRecord15(value) {
|
|
12014
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12015
|
+
}
|
|
12016
|
+
async function isDirectory2(absolutePath) {
|
|
12017
|
+
try {
|
|
12018
|
+
const s = await stat8(absolutePath);
|
|
12019
|
+
return s.isDirectory();
|
|
12020
|
+
} catch {
|
|
12021
|
+
return false;
|
|
12022
|
+
}
|
|
12023
|
+
}
|
|
12024
|
+
async function loadJson2(absolutePath) {
|
|
12025
|
+
try {
|
|
12026
|
+
const raw = await readFile30(absolutePath, "utf-8");
|
|
12027
|
+
return JSON.parse(raw);
|
|
12028
|
+
} catch {
|
|
12029
|
+
return null;
|
|
12030
|
+
}
|
|
10449
12031
|
}
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
12032
|
+
function normalizeSpecId(raw) {
|
|
12033
|
+
if (typeof raw !== "string") return void 0;
|
|
12034
|
+
const trimmed = raw.trim();
|
|
12035
|
+
const direct = /^(\d{4})$/.exec(trimmed);
|
|
12036
|
+
if (direct?.[1]) return direct[1];
|
|
12037
|
+
const prefixed = /^spec-(\d{4})$/i.exec(trimmed);
|
|
12038
|
+
if (prefixed?.[1]) return prefixed[1];
|
|
12039
|
+
return void 0;
|
|
12040
|
+
}
|
|
12041
|
+
async function validateSpecIdLinkage(root, config) {
|
|
12042
|
+
const issues = [];
|
|
12043
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
12044
|
+
const evidenceRoot = path43.join(path43.dirname(specsRoot), "evidence");
|
|
12045
|
+
const specEntries = await collectSpecEntries(specsRoot);
|
|
12046
|
+
const knownSpecIds = new Set(specEntries.map((e) => e.specNumber));
|
|
12047
|
+
const protoPath = path43.join(evidenceRoot, "prototyping.json");
|
|
12048
|
+
const protoJson = await loadJson2(protoPath);
|
|
12049
|
+
if (isRecord15(protoJson)) {
|
|
12050
|
+
const protoRel = path43.relative(root, protoPath).replace(/\\/g, "/");
|
|
12051
|
+
const specs = protoJson.specs;
|
|
12052
|
+
if (Array.isArray(specs)) {
|
|
12053
|
+
for (let i = 0; i < specs.length; i++) {
|
|
12054
|
+
const entry = specs[i];
|
|
12055
|
+
if (!isRecord15(entry)) continue;
|
|
12056
|
+
const rawSpecId = entry.specId;
|
|
12057
|
+
if (rawSpecId === void 0) continue;
|
|
12058
|
+
const id = normalizeSpecId(rawSpecId);
|
|
12059
|
+
if (id === void 0) {
|
|
12060
|
+
issues.push(
|
|
12061
|
+
issue(
|
|
12062
|
+
"QFAI-PROT-LINK-001",
|
|
12063
|
+
`${protoRel}: specs[${i}].specId="${JSON.stringify(rawSpecId)}" is malformed (expected 4-digit "NNNN" or "spec-NNNN").`,
|
|
12064
|
+
"warning",
|
|
12065
|
+
protoRel,
|
|
12066
|
+
"prototyping.specs.idReality",
|
|
12067
|
+
void 0,
|
|
12068
|
+
"canonical",
|
|
12069
|
+
'specs[].specId \u306F 4 \u6841\u306E "NNNN" \u5F62\u5F0F\u307E\u305F\u306F "spec-NNNN" \u5F62\u5F0F\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'
|
|
12070
|
+
)
|
|
12071
|
+
);
|
|
12072
|
+
continue;
|
|
12073
|
+
}
|
|
12074
|
+
if (!knownSpecIds.has(id)) {
|
|
12075
|
+
issues.push(
|
|
12076
|
+
issue(
|
|
12077
|
+
"QFAI-PROT-LINK-001",
|
|
12078
|
+
`${protoRel}: specs[${i}].specId="${JSON.stringify(rawSpecId)}" but spec-${id} does not exist under ${path43.relative(root, specsRoot).replace(/\\/g, "/")}/.`,
|
|
12079
|
+
"warning",
|
|
12080
|
+
protoRel,
|
|
12081
|
+
"prototyping.specs.idReality",
|
|
12082
|
+
void 0,
|
|
12083
|
+
"canonical",
|
|
12084
|
+
"\u5BFE\u5FDC\u3059\u308B spec-NNNN \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u4F5C\u6210\u3059\u308B\u304B\u3001specs[].specId \u5024\u3092\u5B9F\u5728\u306E spec \u306B\u5408\u308F\u305B\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
12085
|
+
)
|
|
12086
|
+
);
|
|
12087
|
+
}
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
12090
|
+
const rounds = protoJson.rounds;
|
|
12091
|
+
if (Array.isArray(rounds)) {
|
|
12092
|
+
for (let i = 0; i < rounds.length; i++) {
|
|
12093
|
+
const round = rounds[i];
|
|
12094
|
+
if (!isRecord15(round)) continue;
|
|
12095
|
+
const roundName = round.round;
|
|
12096
|
+
const candidates = round.candidates;
|
|
12097
|
+
if (typeof roundName !== "string" || !Array.isArray(candidates)) continue;
|
|
12098
|
+
for (let j = 0; j < candidates.length; j++) {
|
|
12099
|
+
const candidate = candidates[j];
|
|
12100
|
+
if (!isRecord15(candidate)) continue;
|
|
12101
|
+
const candidateId = candidate.candidateId;
|
|
12102
|
+
if (typeof candidateId !== "string") continue;
|
|
12103
|
+
const candidateDir = path43.join(
|
|
12104
|
+
evidenceRoot,
|
|
12105
|
+
"prototyping",
|
|
12106
|
+
"rounds",
|
|
12107
|
+
roundName,
|
|
12108
|
+
"candidates",
|
|
12109
|
+
candidateId
|
|
12110
|
+
);
|
|
12111
|
+
if (!await isDirectory2(candidateDir)) {
|
|
12112
|
+
const relCandidateDir = path43.relative(root, candidateDir).replace(/\\/g, "/");
|
|
12113
|
+
issues.push(
|
|
12114
|
+
issue(
|
|
12115
|
+
"QFAI-PROT-LINK-003",
|
|
12116
|
+
`${protoRel}: rounds[${i}].candidates[${j}].candidateId="${candidateId}" but ${relCandidateDir} does not exist.`,
|
|
12117
|
+
"warning",
|
|
12118
|
+
protoRel,
|
|
12119
|
+
"prototyping.candidates.dirReality",
|
|
12120
|
+
void 0,
|
|
12121
|
+
"canonical",
|
|
12122
|
+
"candidateId \u306B\u5BFE\u5FDC\u3059\u308B artifact \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092 `qfai prototyping round-start` \u3067\u751F\u6210\u3059\u308B\u304B\u3001\u7121\u52B9\u306A candidate \u3092 index \u304B\u3089\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
12123
|
+
)
|
|
12124
|
+
);
|
|
12125
|
+
}
|
|
12126
|
+
}
|
|
12127
|
+
}
|
|
12128
|
+
}
|
|
12129
|
+
const polishCycles = protoJson.polishCycles;
|
|
12130
|
+
if (Array.isArray(polishCycles)) {
|
|
12131
|
+
for (let i = 0; i < polishCycles.length; i++) {
|
|
12132
|
+
const cycle = polishCycles[i];
|
|
12133
|
+
if (!isRecord15(cycle)) continue;
|
|
12134
|
+
const cycleNum = cycle.cycle;
|
|
12135
|
+
if (typeof cycleNum !== "number" || cycleNum < 1) continue;
|
|
12136
|
+
const iterationDir = path43.join(evidenceRoot, "prototyping", "iterations", String(cycleNum));
|
|
12137
|
+
if (!await isDirectory2(iterationDir)) {
|
|
12138
|
+
const relIterationDir = path43.relative(root, iterationDir).replace(/\\/g, "/");
|
|
12139
|
+
issues.push(
|
|
12140
|
+
issue(
|
|
12141
|
+
"QFAI-PROT-LINK-004",
|
|
12142
|
+
`${protoRel}: polishCycles[${i}].cycle=${cycleNum} but ${relIterationDir} does not exist.`,
|
|
12143
|
+
"warning",
|
|
12144
|
+
protoRel,
|
|
12145
|
+
"prototyping.polishCycles.iterationDirReality",
|
|
12146
|
+
void 0,
|
|
12147
|
+
"canonical",
|
|
12148
|
+
"polish cycle \u306B\u5BFE\u5FDC\u3059\u308B iteration \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u518D\u751F\u6210\u3059\u308B\u304B\u3001\u7121\u52B9\u306A polish cycle \u3092 index \u304B\u3089\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
12149
|
+
)
|
|
12150
|
+
);
|
|
12151
|
+
}
|
|
12152
|
+
}
|
|
12153
|
+
}
|
|
10454
12154
|
}
|
|
10455
|
-
const
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
12155
|
+
for (const round of EXPLORATION_ROUNDS) {
|
|
12156
|
+
const bundlePath = path43.join(root, ...roundReviewBundlePath(round).split("/"));
|
|
12157
|
+
const bundleJson = await loadJson2(bundlePath);
|
|
12158
|
+
if (!isRecord15(bundleJson)) continue;
|
|
12159
|
+
const id = normalizeSpecId(bundleJson.spec);
|
|
12160
|
+
if (id !== void 0 && !knownSpecIds.has(id)) {
|
|
12161
|
+
const bundleRel = path43.relative(root, bundlePath).replace(/\\/g, "/");
|
|
12162
|
+
issues.push(
|
|
12163
|
+
issue(
|
|
12164
|
+
"QFAI-PROT-LINK-002",
|
|
12165
|
+
`${bundleRel}: spec="${String(bundleJson.spec)}" but spec-${id} does not exist under ${path43.relative(root, specsRoot).replace(/\\/g, "/")}/.`,
|
|
12166
|
+
"error",
|
|
12167
|
+
bundleRel,
|
|
12168
|
+
"prototyping.reviewBundle.specReality",
|
|
12169
|
+
void 0,
|
|
12170
|
+
"canonical",
|
|
12171
|
+
"review-bundle.json \u306E spec \u3092 `qfai prototyping show-spec` \u306E\u7D50\u679C\u306B\u5408\u308F\u305B\u3066\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
12172
|
+
)
|
|
12173
|
+
);
|
|
12174
|
+
}
|
|
10459
12175
|
}
|
|
10460
|
-
|
|
10461
|
-
root,
|
|
10462
|
-
config.paths.contractsDir,
|
|
10463
|
-
"design",
|
|
10464
|
-
"design-system.yaml"
|
|
10465
|
-
);
|
|
10466
|
-
const designSystemPresent = await fileExists2(designSystemPath);
|
|
10467
|
-
const severity = designSystemPresent && artifact.mode === "full-harness" ? "error" : "warning";
|
|
10468
|
-
return [toIssue(severity, artifact.fileName)];
|
|
12176
|
+
return issues;
|
|
10469
12177
|
}
|
|
10470
12178
|
|
|
10471
12179
|
// src/core/validators/requireIndex.ts
|
|
10472
|
-
import { readFile as
|
|
10473
|
-
import
|
|
12180
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
12181
|
+
import path44 from "path";
|
|
10474
12182
|
init_utils();
|
|
10475
12183
|
|
|
10476
12184
|
// src/core/validators/repositoryHygiene.ts
|
|
10477
|
-
import { readdir as
|
|
10478
|
-
import
|
|
12185
|
+
import { readdir as readdir9, stat as stat9 } from "fs/promises";
|
|
12186
|
+
import path45 from "path";
|
|
10479
12187
|
init_utils();
|
|
10480
12188
|
var LEGACY_DIR_RULES = [
|
|
10481
12189
|
{ legacy: "discussions", canonical: "discussion" },
|
|
@@ -10487,12 +12195,12 @@ var LEGACY_DIR_RULES = [
|
|
|
10487
12195
|
];
|
|
10488
12196
|
var SUSPICIOUS_TEMPLATE_NAME_RE = /^(?:_?templates?|_?sample(?:s)?|sample-template)$/i;
|
|
10489
12197
|
async function validateRepositoryHygiene(root, config) {
|
|
10490
|
-
const qfaiRoot =
|
|
12198
|
+
const qfaiRoot = path45.join(root, ".qfai");
|
|
10491
12199
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
10492
12200
|
const issues = [];
|
|
10493
12201
|
for (const rule of LEGACY_DIR_RULES) {
|
|
10494
|
-
const legacyPath =
|
|
10495
|
-
if (!await
|
|
12202
|
+
const legacyPath = path45.join(qfaiRoot, rule.legacy);
|
|
12203
|
+
if (!await isDirectory3(legacyPath)) {
|
|
10496
12204
|
continue;
|
|
10497
12205
|
}
|
|
10498
12206
|
issues.push(
|
|
@@ -10526,7 +12234,7 @@ async function validateRepositoryHygiene(root, config) {
|
|
|
10526
12234
|
return issues;
|
|
10527
12235
|
}
|
|
10528
12236
|
async function collectSuspiciousTemplatePaths(root) {
|
|
10529
|
-
if (!await
|
|
12237
|
+
if (!await isDirectory3(root)) {
|
|
10530
12238
|
return [];
|
|
10531
12239
|
}
|
|
10532
12240
|
const matches = [];
|
|
@@ -10538,7 +12246,7 @@ async function collectSuspiciousTemplatePaths(root) {
|
|
|
10538
12246
|
}
|
|
10539
12247
|
let entries = [];
|
|
10540
12248
|
try {
|
|
10541
|
-
entries = await
|
|
12249
|
+
entries = await readdir9(current, {
|
|
10542
12250
|
withFileTypes: true,
|
|
10543
12251
|
encoding: "utf8"
|
|
10544
12252
|
});
|
|
@@ -10546,10 +12254,10 @@ async function collectSuspiciousTemplatePaths(root) {
|
|
|
10546
12254
|
continue;
|
|
10547
12255
|
}
|
|
10548
12256
|
for (const entry of entries) {
|
|
10549
|
-
const absolute =
|
|
12257
|
+
const absolute = path45.join(current, entry.name);
|
|
10550
12258
|
if (entry.isDirectory()) {
|
|
10551
12259
|
if (SUSPICIOUS_TEMPLATE_NAME_RE.test(entry.name)) {
|
|
10552
|
-
matches.push(toPosix(
|
|
12260
|
+
matches.push(toPosix(path45.relative(root, absolute)));
|
|
10553
12261
|
}
|
|
10554
12262
|
queue.push(absolute);
|
|
10555
12263
|
continue;
|
|
@@ -10557,17 +12265,17 @@ async function collectSuspiciousTemplatePaths(root) {
|
|
|
10557
12265
|
if (!entry.isFile()) {
|
|
10558
12266
|
continue;
|
|
10559
12267
|
}
|
|
10560
|
-
const baseName =
|
|
12268
|
+
const baseName = path45.parse(entry.name).name;
|
|
10561
12269
|
if (SUSPICIOUS_TEMPLATE_NAME_RE.test(baseName)) {
|
|
10562
|
-
matches.push(toPosix(
|
|
12270
|
+
matches.push(toPosix(path45.relative(root, absolute)));
|
|
10563
12271
|
}
|
|
10564
12272
|
}
|
|
10565
12273
|
}
|
|
10566
12274
|
return matches.sort((left, right) => left.localeCompare(right));
|
|
10567
12275
|
}
|
|
10568
|
-
async function
|
|
12276
|
+
async function isDirectory3(target) {
|
|
10569
12277
|
try {
|
|
10570
|
-
return (await
|
|
12278
|
+
return (await stat9(target)).isDirectory();
|
|
10571
12279
|
} catch {
|
|
10572
12280
|
return false;
|
|
10573
12281
|
}
|
|
@@ -10577,7 +12285,7 @@ function toPosix(value) {
|
|
|
10577
12285
|
}
|
|
10578
12286
|
|
|
10579
12287
|
// src/core/validators/specSplitByCapability.ts
|
|
10580
|
-
import
|
|
12288
|
+
import path46 from "path";
|
|
10581
12289
|
init_utils();
|
|
10582
12290
|
var CAP_ID_RE2 = /\bCAP-\d{4}\b/g;
|
|
10583
12291
|
async function validateSpecSplitByCapability(root, config) {
|
|
@@ -10589,8 +12297,8 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
10589
12297
|
if (layeredEntries.length === 0) {
|
|
10590
12298
|
return [];
|
|
10591
12299
|
}
|
|
10592
|
-
const policiesDir = layeredEntries[0]?.sharedDir ??
|
|
10593
|
-
const capabilitiesPath =
|
|
12300
|
+
const policiesDir = layeredEntries[0]?.sharedDir ?? path46.join(specsRoot, "_policies");
|
|
12301
|
+
const capabilitiesPath = path46.join(policiesDir, "03_Capabilities.md");
|
|
10594
12302
|
const capabilityText = await readSafe(capabilitiesPath);
|
|
10595
12303
|
const issues = [];
|
|
10596
12304
|
if (!await exists5(capabilitiesPath)) {
|
|
@@ -10630,7 +12338,7 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
10630
12338
|
);
|
|
10631
12339
|
}
|
|
10632
12340
|
const actualSpecIds = new Set(
|
|
10633
|
-
layeredEntries.map((entry) =>
|
|
12341
|
+
layeredEntries.map((entry) => path46.basename(entry.dir).toLowerCase())
|
|
10634
12342
|
);
|
|
10635
12343
|
const expectedSpecIds = capIds.map((_, index) => `spec-${to4(index + 1)}`);
|
|
10636
12344
|
const missingSpecIds = expectedSpecIds.filter((specId) => !actualSpecIds.has(specId));
|
|
@@ -10667,11 +12375,11 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
10667
12375
|
continue;
|
|
10668
12376
|
}
|
|
10669
12377
|
const specId = `spec-${to4(index + 1)}`;
|
|
10670
|
-
const entry = layeredEntries.find((value) =>
|
|
12378
|
+
const entry = layeredEntries.find((value) => path46.basename(value.dir).toLowerCase() === specId);
|
|
10671
12379
|
if (!entry) {
|
|
10672
12380
|
continue;
|
|
10673
12381
|
}
|
|
10674
|
-
const specFilePath =
|
|
12382
|
+
const specFilePath = path46.join(entry.dir, "01_Spec.md");
|
|
10675
12383
|
const specText = await readSafe(specFilePath);
|
|
10676
12384
|
if (specText.trim().length === 0 || !specText.includes(capId)) {
|
|
10677
12385
|
issues.push(
|
|
@@ -10690,8 +12398,8 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
10690
12398
|
}
|
|
10691
12399
|
|
|
10692
12400
|
// src/core/validators/statusInSpecs.ts
|
|
10693
|
-
import { readFile as
|
|
10694
|
-
import
|
|
12401
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
12402
|
+
import path47 from "path";
|
|
10695
12403
|
init_utils();
|
|
10696
12404
|
var STRONG_PATTERNS = [
|
|
10697
12405
|
{ label: "release_candidate:", pattern: /\brelease_candidate\s*:/i },
|
|
@@ -10749,32 +12457,32 @@ function hasMatch(text, pattern) {
|
|
|
10749
12457
|
}
|
|
10750
12458
|
async function readSafe11(filePath) {
|
|
10751
12459
|
try {
|
|
10752
|
-
return await
|
|
12460
|
+
return await readFile32(filePath, "utf-8");
|
|
10753
12461
|
} catch {
|
|
10754
12462
|
return "";
|
|
10755
12463
|
}
|
|
10756
12464
|
}
|
|
10757
12465
|
function isOpenQuestionsFile(filePath) {
|
|
10758
|
-
return /open-questions\.md$/i.test(
|
|
12466
|
+
return /open-questions\.md$/i.test(path47.basename(filePath));
|
|
10759
12467
|
}
|
|
10760
12468
|
function isDecisionFile(filePath) {
|
|
10761
|
-
return /decisions\.md$/i.test(
|
|
12469
|
+
return /decisions\.md$/i.test(path47.basename(filePath));
|
|
10762
12470
|
}
|
|
10763
12471
|
|
|
10764
12472
|
// src/core/validators/breakthroughEvidence.ts
|
|
10765
|
-
import { readFile as
|
|
10766
|
-
import
|
|
12473
|
+
import { readFile as readFile33 } from "fs/promises";
|
|
12474
|
+
import path48 from "path";
|
|
10767
12475
|
init_utils();
|
|
10768
12476
|
function toPosixRelative(root, targetPath) {
|
|
10769
|
-
return
|
|
12477
|
+
return path48.relative(root, targetPath).replace(/\\/g, "/");
|
|
10770
12478
|
}
|
|
10771
12479
|
async function validateBreakthroughEvidence(root, config) {
|
|
10772
12480
|
const screens = await readUiContractScreenContracts(root, config.paths.contractsDir);
|
|
10773
12481
|
if (screens.length === 0) {
|
|
10774
12482
|
return [];
|
|
10775
12483
|
}
|
|
10776
|
-
const evidenceRoot =
|
|
10777
|
-
const evidencePath =
|
|
12484
|
+
const evidenceRoot = path48.join(path48.dirname(resolvePath(root, config, "specsDir")), "evidence");
|
|
12485
|
+
const evidencePath = path48.join(evidenceRoot, "breakthrough.json");
|
|
10778
12486
|
const evidenceRelativePath = toPosixRelative(root, evidencePath);
|
|
10779
12487
|
const raw = await readJsonFile2(evidencePath);
|
|
10780
12488
|
if (raw.status === "missing") {
|
|
@@ -10890,7 +12598,7 @@ function isNonEmptyString2(value) {
|
|
|
10890
12598
|
}
|
|
10891
12599
|
async function readJsonFile2(filePath) {
|
|
10892
12600
|
try {
|
|
10893
|
-
const raw = await
|
|
12601
|
+
const raw = await readFile33(filePath, "utf-8");
|
|
10894
12602
|
const parsed = JSON.parse(raw);
|
|
10895
12603
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
10896
12604
|
return { status: "invalid" };
|
|
@@ -10898,7 +12606,7 @@ async function readJsonFile2(filePath) {
|
|
|
10898
12606
|
return { status: "ok", value: parsed };
|
|
10899
12607
|
} catch {
|
|
10900
12608
|
try {
|
|
10901
|
-
await
|
|
12609
|
+
await readFile33(filePath, "utf-8");
|
|
10902
12610
|
return { status: "invalid" };
|
|
10903
12611
|
} catch {
|
|
10904
12612
|
return { status: "missing" };
|
|
@@ -10907,8 +12615,8 @@ async function readJsonFile2(filePath) {
|
|
|
10907
12615
|
}
|
|
10908
12616
|
|
|
10909
12617
|
// src/core/validators/designToken.ts
|
|
10910
|
-
import { readFile as
|
|
10911
|
-
import
|
|
12618
|
+
import { readFile as readFile34 } from "fs/promises";
|
|
12619
|
+
import path49 from "path";
|
|
10912
12620
|
import fg3 from "fast-glob";
|
|
10913
12621
|
import { parse as parseYaml7 } from "yaml";
|
|
10914
12622
|
|
|
@@ -10955,7 +12663,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
10955
12663
|
}
|
|
10956
12664
|
function flattenTokens(obj, prefix, target, errors) {
|
|
10957
12665
|
for (const [key, value] of Object.entries(obj)) {
|
|
10958
|
-
const
|
|
12666
|
+
const path84 = `${prefix}.${key}`;
|
|
10959
12667
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10960
12668
|
const record2 = value;
|
|
10961
12669
|
if ("$value" in record2) {
|
|
@@ -10971,9 +12679,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
10971
12679
|
if (typeof record2.platform === "string") {
|
|
10972
12680
|
token.platform = record2.platform;
|
|
10973
12681
|
}
|
|
10974
|
-
target.set(
|
|
12682
|
+
target.set(path84, token);
|
|
10975
12683
|
} else {
|
|
10976
|
-
flattenTokens(record2,
|
|
12684
|
+
flattenTokens(record2, path84, target, errors);
|
|
10977
12685
|
}
|
|
10978
12686
|
}
|
|
10979
12687
|
}
|
|
@@ -10983,44 +12691,44 @@ function resolveAllReferences(result) {
|
|
|
10983
12691
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
10984
12692
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
10985
12693
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
10986
|
-
for (const [
|
|
10987
|
-
resolveTokenRef(
|
|
12694
|
+
for (const [path84] of allTokens) {
|
|
12695
|
+
resolveTokenRef(path84, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
10988
12696
|
}
|
|
10989
12697
|
}
|
|
10990
|
-
function resolveTokenRef(
|
|
10991
|
-
if (result.resolved.has(
|
|
10992
|
-
return result.resolved.get(
|
|
12698
|
+
function resolveTokenRef(path84, allTokens, visited, depth, result) {
|
|
12699
|
+
if (result.resolved.has(path84)) {
|
|
12700
|
+
return result.resolved.get(path84);
|
|
10993
12701
|
}
|
|
10994
12702
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
10995
12703
|
result.errors.push({
|
|
10996
|
-
message: `Max reference depth exceeded at: ${
|
|
10997
|
-
path:
|
|
12704
|
+
message: `Max reference depth exceeded at: ${path84}`,
|
|
12705
|
+
path: path84
|
|
10998
12706
|
});
|
|
10999
12707
|
return void 0;
|
|
11000
12708
|
}
|
|
11001
|
-
if (visited.has(
|
|
12709
|
+
if (visited.has(path84)) {
|
|
11002
12710
|
result.errors.push({
|
|
11003
|
-
message: `Circular reference detected: ${
|
|
11004
|
-
path:
|
|
12711
|
+
message: `Circular reference detected: ${path84}`,
|
|
12712
|
+
path: path84
|
|
11005
12713
|
});
|
|
11006
12714
|
return void 0;
|
|
11007
12715
|
}
|
|
11008
|
-
const token = allTokens.get(
|
|
12716
|
+
const token = allTokens.get(path84);
|
|
11009
12717
|
if (!token) {
|
|
11010
12718
|
return void 0;
|
|
11011
12719
|
}
|
|
11012
12720
|
if (typeof token.$value !== "string") {
|
|
11013
12721
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11014
|
-
result.resolved.set(
|
|
12722
|
+
result.resolved.set(path84, rawValue2);
|
|
11015
12723
|
return rawValue2;
|
|
11016
12724
|
}
|
|
11017
12725
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11018
12726
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11019
12727
|
if (refs.length === 0) {
|
|
11020
|
-
result.resolved.set(
|
|
12728
|
+
result.resolved.set(path84, rawValue);
|
|
11021
12729
|
return rawValue;
|
|
11022
12730
|
}
|
|
11023
|
-
visited.add(
|
|
12731
|
+
visited.add(path84);
|
|
11024
12732
|
let resolved = rawValue;
|
|
11025
12733
|
for (const ref of refs) {
|
|
11026
12734
|
const refPath = ref[1];
|
|
@@ -11028,8 +12736,8 @@ function resolveTokenRef(path76, allTokens, visited, depth, result) {
|
|
|
11028
12736
|
const refToken = allTokens.get(refPath);
|
|
11029
12737
|
if (!refToken) {
|
|
11030
12738
|
result.errors.push({
|
|
11031
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11032
|
-
path:
|
|
12739
|
+
message: `Unresolved token reference: {${refPath}} at ${path84}`,
|
|
12740
|
+
path: path84
|
|
11033
12741
|
});
|
|
11034
12742
|
continue;
|
|
11035
12743
|
}
|
|
@@ -11038,7 +12746,7 @@ function resolveTokenRef(path76, allTokens, visited, depth, result) {
|
|
|
11038
12746
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11039
12747
|
}
|
|
11040
12748
|
}
|
|
11041
|
-
result.resolved.set(
|
|
12749
|
+
result.resolved.set(path84, resolved);
|
|
11042
12750
|
return resolved;
|
|
11043
12751
|
}
|
|
11044
12752
|
function stringifyTokenValue(value) {
|
|
@@ -11094,8 +12802,8 @@ var VALID_TYPES = [
|
|
|
11094
12802
|
var VALID_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
|
|
11095
12803
|
async function validateDesignToken(root, config) {
|
|
11096
12804
|
const configuredDir = config.uiux?.designTokensDir;
|
|
11097
|
-
const designDir = configuredDir ?
|
|
11098
|
-
const pattern =
|
|
12805
|
+
const designDir = configuredDir ? path49.resolve(root, configuredDir) : path49.join(root, config.paths.contractsDir, "design");
|
|
12806
|
+
const pattern = path49.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
|
|
11099
12807
|
const files = await fg3(pattern, {
|
|
11100
12808
|
absolute: true,
|
|
11101
12809
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -11105,11 +12813,11 @@ async function validateDesignToken(root, config) {
|
|
|
11105
12813
|
}
|
|
11106
12814
|
const issues = [];
|
|
11107
12815
|
for (const filePath of files) {
|
|
11108
|
-
const rel =
|
|
12816
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
11109
12817
|
let hasRootObjectError = false;
|
|
11110
12818
|
let content;
|
|
11111
12819
|
try {
|
|
11112
|
-
content = await
|
|
12820
|
+
content = await readFile34(filePath, "utf-8");
|
|
11113
12821
|
} catch {
|
|
11114
12822
|
issues.push(
|
|
11115
12823
|
issue(
|
|
@@ -11272,8 +12980,8 @@ function normalizePlatform(value) {
|
|
|
11272
12980
|
}
|
|
11273
12981
|
|
|
11274
12982
|
// src/core/validators/htmlMock.ts
|
|
11275
|
-
import { readFile as
|
|
11276
|
-
import
|
|
12983
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
12984
|
+
import path50 from "path";
|
|
11277
12985
|
import fg4 from "fast-glob";
|
|
11278
12986
|
|
|
11279
12987
|
// src/core/uiux/contrastRatio.ts
|
|
@@ -11654,19 +13362,19 @@ async function validateHtmlMock(root, platform, config) {
|
|
|
11654
13362
|
const startTime = performance.now();
|
|
11655
13363
|
const budget = config.uiux?.htmlMockTimeout ?? 2e3;
|
|
11656
13364
|
const patterns = [
|
|
11657
|
-
|
|
11658
|
-
|
|
13365
|
+
path50.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
|
|
13366
|
+
path50.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
|
|
11659
13367
|
];
|
|
11660
13368
|
const files = await fg4(patterns, { absolute: true });
|
|
11661
13369
|
const mockBlocks = [];
|
|
11662
13370
|
for (const filePath of files) {
|
|
11663
13371
|
let content;
|
|
11664
13372
|
try {
|
|
11665
|
-
content = await
|
|
13373
|
+
content = await readFile35(filePath, "utf-8");
|
|
11666
13374
|
} catch {
|
|
11667
13375
|
continue;
|
|
11668
13376
|
}
|
|
11669
|
-
const rel =
|
|
13377
|
+
const rel = path50.relative(root, filePath).replace(/\\/g, "/");
|
|
11670
13378
|
for (const block of collectHtmlMockBlocks(content)) {
|
|
11671
13379
|
mockBlocks.push({ file: rel, html: block.html, rawBlock: block.rawBlock });
|
|
11672
13380
|
}
|
|
@@ -11832,8 +13540,8 @@ async function validateHtmlMock(root, platform, config) {
|
|
|
11832
13540
|
}
|
|
11833
13541
|
|
|
11834
13542
|
// src/core/validators/mermaidScreenFlow.ts
|
|
11835
|
-
import { readFile as
|
|
11836
|
-
import
|
|
13543
|
+
import { readFile as readFile36 } from "fs/promises";
|
|
13544
|
+
import path51 from "path";
|
|
11837
13545
|
import fg5 from "fast-glob";
|
|
11838
13546
|
|
|
11839
13547
|
// src/core/validators/mermaidUtils.ts
|
|
@@ -11887,19 +13595,19 @@ async function validateMermaidScreenFlow(root, config) {
|
|
|
11887
13595
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
11888
13596
|
const discussionRoot = resolvePath(root, config, "discussionDir");
|
|
11889
13597
|
const latestDiscussionPackDir = await findLatestDiscussionPackDir(discussionRoot);
|
|
11890
|
-
const patterns = [
|
|
13598
|
+
const patterns = [path51.posix.join(specsRoot.replace(/\\/g, "/"), "**/*.md")];
|
|
11891
13599
|
if (latestDiscussionPackDir) {
|
|
11892
|
-
patterns.push(
|
|
13600
|
+
patterns.push(path51.posix.join(latestDiscussionPackDir.replace(/\\/g, "/"), "**/*.md"));
|
|
11893
13601
|
}
|
|
11894
13602
|
const files = await fg5(patterns, { absolute: true });
|
|
11895
13603
|
for (const filePath of files) {
|
|
11896
13604
|
let content;
|
|
11897
13605
|
try {
|
|
11898
|
-
content = await
|
|
13606
|
+
content = await readFile36(filePath, "utf-8");
|
|
11899
13607
|
} catch {
|
|
11900
13608
|
continue;
|
|
11901
13609
|
}
|
|
11902
|
-
const rel =
|
|
13610
|
+
const rel = path51.relative(root, filePath).replace(/\\/g, "/");
|
|
11903
13611
|
const blocks = extractFencedCodeBlocks(content);
|
|
11904
13612
|
for (const block of blocks) {
|
|
11905
13613
|
if (block.language !== "mermaid") continue;
|
|
@@ -11998,8 +13706,8 @@ function parseTransitions(content) {
|
|
|
11998
13706
|
|
|
11999
13707
|
// src/core/validators/bpApDb.ts
|
|
12000
13708
|
init_utils();
|
|
12001
|
-
import { readFile as
|
|
12002
|
-
import
|
|
13709
|
+
import { readFile as readFile37 } from "fs/promises";
|
|
13710
|
+
import path52 from "path";
|
|
12003
13711
|
import fg6 from "fast-glob";
|
|
12004
13712
|
import { parse as parseYaml8 } from "yaml";
|
|
12005
13713
|
var BP_ID_RE = /^BP-\d{4}$/;
|
|
@@ -12036,9 +13744,9 @@ var AP_REQUIRED_FIELDS = [
|
|
|
12036
13744
|
];
|
|
12037
13745
|
async function validateBpApDb(root, config) {
|
|
12038
13746
|
const issues = [];
|
|
12039
|
-
const designDir =
|
|
12040
|
-
const bpPattern =
|
|
12041
|
-
const apPattern =
|
|
13747
|
+
const designDir = path52.join(root, config.paths.contractsDir, "design");
|
|
13748
|
+
const bpPattern = path52.posix.join(designDir.replace(/\\/g, "/"), "best-practices*.yaml");
|
|
13749
|
+
const apPattern = path52.posix.join(designDir.replace(/\\/g, "/"), "anti-patterns*.yaml");
|
|
12042
13750
|
const globOptions = {
|
|
12043
13751
|
absolute: true,
|
|
12044
13752
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -12051,14 +13759,14 @@ async function validateBpApDb(root, config) {
|
|
|
12051
13759
|
const seenBpIds = /* @__PURE__ */ new Set();
|
|
12052
13760
|
const seenApIds = /* @__PURE__ */ new Set();
|
|
12053
13761
|
for (const filePath of bpFiles) {
|
|
12054
|
-
const rel =
|
|
13762
|
+
const rel = path52.relative(root, filePath).replace(/\\/g, "/");
|
|
12055
13763
|
const entries = await parseRuleFile(filePath, rel, issues);
|
|
12056
13764
|
for (const entry of entries) {
|
|
12057
13765
|
validateBpEntry(entry, rel, seenBpIds, issues);
|
|
12058
13766
|
}
|
|
12059
13767
|
}
|
|
12060
13768
|
for (const filePath of apFiles) {
|
|
12061
|
-
const rel =
|
|
13769
|
+
const rel = path52.relative(root, filePath).replace(/\\/g, "/");
|
|
12062
13770
|
const entries = await parseRuleFile(filePath, rel, issues);
|
|
12063
13771
|
for (const entry of entries) {
|
|
12064
13772
|
validateApEntry(entry, rel, seenApIds, issues);
|
|
@@ -12069,7 +13777,7 @@ async function validateBpApDb(root, config) {
|
|
|
12069
13777
|
async function parseRuleFile(filePath, rel, issues) {
|
|
12070
13778
|
let content;
|
|
12071
13779
|
try {
|
|
12072
|
-
content = await
|
|
13780
|
+
content = await readFile37(filePath, "utf-8");
|
|
12073
13781
|
} catch {
|
|
12074
13782
|
issues.push(
|
|
12075
13783
|
issue("QFAI-BPAP-001", `BP/AP file unreadable: ${rel}`, "error", rel, "bpApDb.readFile")
|
|
@@ -12234,8 +13942,8 @@ function toSafeString(value) {
|
|
|
12234
13942
|
|
|
12235
13943
|
// src/core/validators/platformDetection.ts
|
|
12236
13944
|
init_utils();
|
|
12237
|
-
import { readFile as
|
|
12238
|
-
import
|
|
13945
|
+
import { readFile as readFile38 } from "fs/promises";
|
|
13946
|
+
import path53 from "path";
|
|
12239
13947
|
var KNOWN_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
|
|
12240
13948
|
async function detectPlatform(root, config, cliPlatform) {
|
|
12241
13949
|
const issues = [];
|
|
@@ -12278,13 +13986,13 @@ async function detectPlatform(root, config, cliPlatform) {
|
|
|
12278
13986
|
return { platform: "web", source: "fallback", issues };
|
|
12279
13987
|
}
|
|
12280
13988
|
async function inferPlatform(root, issues) {
|
|
12281
|
-
if (await exists5(
|
|
12282
|
-
const hasAndroid = await exists5(
|
|
12283
|
-
const hasIos = await exists5(
|
|
12284
|
-
const hasWeb = await exists5(
|
|
12285
|
-
const hasWindows = await exists5(
|
|
12286
|
-
const hasMacos = await exists5(
|
|
12287
|
-
const hasLinux = await exists5(
|
|
13989
|
+
if (await exists5(path53.join(root, "pubspec.yaml"))) {
|
|
13990
|
+
const hasAndroid = await exists5(path53.join(root, "android"));
|
|
13991
|
+
const hasIos = await exists5(path53.join(root, "ios"));
|
|
13992
|
+
const hasWeb = await exists5(path53.join(root, "web"));
|
|
13993
|
+
const hasWindows = await exists5(path53.join(root, "windows"));
|
|
13994
|
+
const hasMacos = await exists5(path53.join(root, "macos"));
|
|
13995
|
+
const hasLinux = await exists5(path53.join(root, "linux"));
|
|
12288
13996
|
const mobileTargets = [hasAndroid, hasIos].filter(Boolean).length;
|
|
12289
13997
|
const desktopTargets = [hasWeb, hasWindows, hasMacos, hasLinux].filter(Boolean).length;
|
|
12290
13998
|
if (mobileTargets + desktopTargets > 1) {
|
|
@@ -12304,10 +14012,10 @@ async function inferPlatform(root, issues) {
|
|
|
12304
14012
|
}
|
|
12305
14013
|
return null;
|
|
12306
14014
|
}
|
|
12307
|
-
const pkgJsonPath =
|
|
14015
|
+
const pkgJsonPath = path53.join(root, "package.json");
|
|
12308
14016
|
if (await exists5(pkgJsonPath)) {
|
|
12309
14017
|
try {
|
|
12310
|
-
const raw = await
|
|
14018
|
+
const raw = await readFile38(pkgJsonPath, "utf-8");
|
|
12311
14019
|
const pkg = JSON.parse(raw);
|
|
12312
14020
|
const deps = {
|
|
12313
14021
|
...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
|
|
@@ -12326,8 +14034,8 @@ async function inferPlatform(root, issues) {
|
|
|
12326
14034
|
return "cross-platform";
|
|
12327
14035
|
}
|
|
12328
14036
|
if ("react-native" in deps) {
|
|
12329
|
-
const hasAndroid = await exists5(
|
|
12330
|
-
const hasIos = await exists5(
|
|
14037
|
+
const hasAndroid = await exists5(path53.join(root, "android"));
|
|
14038
|
+
const hasIos = await exists5(path53.join(root, "ios"));
|
|
12331
14039
|
if (hasAndroid && hasIos) {
|
|
12332
14040
|
return "cross-platform";
|
|
12333
14041
|
}
|
|
@@ -12349,16 +14057,16 @@ function normalizePlatformInput(platform) {
|
|
|
12349
14057
|
}
|
|
12350
14058
|
|
|
12351
14059
|
// src/core/validators/uiDefinitionConsistency.ts
|
|
12352
|
-
import { readFile as
|
|
12353
|
-
import
|
|
14060
|
+
import { readFile as readFile39 } from "fs/promises";
|
|
14061
|
+
import path54 from "path";
|
|
12354
14062
|
import fg7 from "fast-glob";
|
|
12355
14063
|
import { parse as parseYaml9 } from "yaml";
|
|
12356
14064
|
init_utils();
|
|
12357
14065
|
async function validateUiDefinitionConsistency(root, config) {
|
|
12358
14066
|
const issues = [];
|
|
12359
14067
|
const configuredDir = config.uiux?.designTokensDir;
|
|
12360
|
-
const designDir = configuredDir ?
|
|
12361
|
-
const tokenPattern =
|
|
14068
|
+
const designDir = configuredDir ? path54.resolve(root, configuredDir) : path54.join(root, config.paths.contractsDir, "design");
|
|
14069
|
+
const tokenPattern = path54.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
|
|
12362
14070
|
const tokenFiles = await fg7(tokenPattern, {
|
|
12363
14071
|
absolute: true,
|
|
12364
14072
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -12366,7 +14074,7 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12366
14074
|
const resolvedTokens = /* @__PURE__ */ new Map();
|
|
12367
14075
|
for (const tokenFile of tokenFiles) {
|
|
12368
14076
|
try {
|
|
12369
|
-
const content = await
|
|
14077
|
+
const content = await readFile39(tokenFile, "utf-8");
|
|
12370
14078
|
const result = parseDesignToken(content);
|
|
12371
14079
|
for (const [key, val] of result.resolved) {
|
|
12372
14080
|
resolvedTokens.set(key, val);
|
|
@@ -12374,13 +14082,13 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12374
14082
|
} catch {
|
|
12375
14083
|
}
|
|
12376
14084
|
}
|
|
12377
|
-
const uiContractDir =
|
|
12378
|
-
const uiPattern =
|
|
14085
|
+
const uiContractDir = path54.join(root, config.paths.contractsDir, "ui");
|
|
14086
|
+
const uiPattern = path54.posix.join(uiContractDir.replace(/\\/g, "/"), "**/*.yaml");
|
|
12379
14087
|
const uiFiles = await fg7(uiPattern, { absolute: true });
|
|
12380
14088
|
const contractScreenIds = /* @__PURE__ */ new Set();
|
|
12381
14089
|
for (const uiFile of uiFiles) {
|
|
12382
14090
|
try {
|
|
12383
|
-
const content = await
|
|
14091
|
+
const content = await readFile39(uiFile, "utf-8");
|
|
12384
14092
|
const parsed = parseYaml9(content);
|
|
12385
14093
|
if (parsed && typeof parsed === "object") {
|
|
12386
14094
|
const screens = parsed.screens;
|
|
@@ -12396,15 +14104,15 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12396
14104
|
}
|
|
12397
14105
|
}
|
|
12398
14106
|
const mdPatterns = [
|
|
12399
|
-
|
|
12400
|
-
|
|
14107
|
+
path54.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
|
|
14108
|
+
path54.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
|
|
12401
14109
|
];
|
|
12402
14110
|
const mdFiles = await fg7(mdPatterns, { absolute: true });
|
|
12403
14111
|
const mockScreenIds = /* @__PURE__ */ new Set();
|
|
12404
14112
|
for (const mdFile of mdFiles) {
|
|
12405
14113
|
try {
|
|
12406
|
-
const content = await
|
|
12407
|
-
const rel =
|
|
14114
|
+
const content = await readFile39(mdFile, "utf-8");
|
|
14115
|
+
const rel = path54.relative(root, mdFile).replace(/\\/g, "/");
|
|
12408
14116
|
const htmlBlocks = collectHtmlMockBlocks(content);
|
|
12409
14117
|
if (resolvedTokens.size > 0) {
|
|
12410
14118
|
for (const htmlBlock of htmlBlocks) {
|
|
@@ -12464,8 +14172,8 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12464
14172
|
|
|
12465
14173
|
// src/core/validators/researchSummary.ts
|
|
12466
14174
|
init_utils();
|
|
12467
|
-
import { readFile as
|
|
12468
|
-
import
|
|
14175
|
+
import { readFile as readFile40 } from "fs/promises";
|
|
14176
|
+
import path55 from "path";
|
|
12469
14177
|
import fg8 from "fast-glob";
|
|
12470
14178
|
var RESEARCH_SUMMARY_HEADING_RE = /^#{1,3}\s+Research\s+Summary/im;
|
|
12471
14179
|
var SOURCE_ENTRY_RE = /^\s*-\s*id:\s*(\S+)/gm;
|
|
@@ -12473,17 +14181,17 @@ var REFLECTION_APPLY_RE = /action:\s*apply/i;
|
|
|
12473
14181
|
var FULL_DATE_RE = /^\s+published:\s*["']?(\d{4}-\d{2}-\d{2})["']?/m;
|
|
12474
14182
|
async function validateResearchSummary(root, config) {
|
|
12475
14183
|
const issues = [];
|
|
12476
|
-
const pattern =
|
|
14184
|
+
const pattern = path55.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
|
|
12477
14185
|
const files = await fg8(pattern, { absolute: true });
|
|
12478
14186
|
for (const filePath of files) {
|
|
12479
14187
|
let content;
|
|
12480
14188
|
try {
|
|
12481
|
-
content = await
|
|
14189
|
+
content = await readFile40(filePath, "utf-8");
|
|
12482
14190
|
} catch {
|
|
12483
14191
|
continue;
|
|
12484
14192
|
}
|
|
12485
14193
|
if (!RESEARCH_SUMMARY_HEADING_RE.test(content)) continue;
|
|
12486
|
-
const rel =
|
|
14194
|
+
const rel = path55.relative(root, filePath).replace(/\\/g, "/");
|
|
12487
14195
|
const section = extractResearchSummarySection(content);
|
|
12488
14196
|
if (!section) continue;
|
|
12489
14197
|
const sourceEntries = extractSourceEntries(section);
|
|
@@ -12715,8 +14423,8 @@ function resolveFreshnessReferenceNow() {
|
|
|
12715
14423
|
|
|
12716
14424
|
// src/core/validators/agentDefinition.ts
|
|
12717
14425
|
init_utils();
|
|
12718
|
-
import { readFile as
|
|
12719
|
-
import
|
|
14426
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
14427
|
+
import path56 from "path";
|
|
12720
14428
|
import { parse as parseYaml10 } from "yaml";
|
|
12721
14429
|
var REQUIRED_AGENT_SECTIONS = [
|
|
12722
14430
|
"## Mission",
|
|
@@ -12728,11 +14436,11 @@ var REQUIRED_AGENT_SECTIONS = [
|
|
|
12728
14436
|
];
|
|
12729
14437
|
async function validateAgentDefinition(root, _config) {
|
|
12730
14438
|
const issues = [];
|
|
12731
|
-
const steeringDir =
|
|
12732
|
-
const agentsDir =
|
|
12733
|
-
const catalogPath =
|
|
12734
|
-
const routingPath =
|
|
12735
|
-
const profilesPath =
|
|
14439
|
+
const steeringDir = path56.join(root, ".qfai", "assistant", "steering");
|
|
14440
|
+
const agentsDir = path56.join(root, ".qfai", "assistant", "agents");
|
|
14441
|
+
const catalogPath = path56.join(steeringDir, "agent-catalog.yml");
|
|
14442
|
+
const routingPath = path56.join(steeringDir, "agent-routing.yml");
|
|
14443
|
+
const profilesPath = path56.join(steeringDir, "review-profiles.yml");
|
|
12736
14444
|
if (!await exists5(agentsDir) && !await exists5(catalogPath)) {
|
|
12737
14445
|
return [];
|
|
12738
14446
|
}
|
|
@@ -12741,7 +14449,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
12741
14449
|
["agent-routing.yml", "QFAI-AGENT-002"],
|
|
12742
14450
|
["review-profiles.yml", "QFAI-AGENT-003"]
|
|
12743
14451
|
]) {
|
|
12744
|
-
const filePath =
|
|
14452
|
+
const filePath = path56.join(steeringDir, fileName);
|
|
12745
14453
|
if (!await exists5(filePath)) {
|
|
12746
14454
|
issues.push(
|
|
12747
14455
|
issue(
|
|
@@ -12766,7 +14474,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
12766
14474
|
catalog.filter((agent) => agent.kind === "reviewer").map((agent) => agent.id)
|
|
12767
14475
|
);
|
|
12768
14476
|
for (const agent of catalog) {
|
|
12769
|
-
const filePath =
|
|
14477
|
+
const filePath = path56.join(agentsDir, `${agent.id}.md`);
|
|
12770
14478
|
const rel = `.qfai/assistant/agents/${agent.id}.md`;
|
|
12771
14479
|
if (!await exists5(filePath)) {
|
|
12772
14480
|
issues.push(
|
|
@@ -12780,7 +14488,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
12780
14488
|
);
|
|
12781
14489
|
continue;
|
|
12782
14490
|
}
|
|
12783
|
-
const content = await
|
|
14491
|
+
const content = await readFile41(filePath, "utf-8");
|
|
12784
14492
|
for (const heading of REQUIRED_AGENT_SECTIONS) {
|
|
12785
14493
|
if (!content.includes(heading)) {
|
|
12786
14494
|
issues.push(
|
|
@@ -12801,7 +14509,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
12801
14509
|
}
|
|
12802
14510
|
async function readCatalog(catalogPath, issues) {
|
|
12803
14511
|
try {
|
|
12804
|
-
const parsed = parseYaml10(await
|
|
14512
|
+
const parsed = parseYaml10(await readFile41(catalogPath, "utf-8"));
|
|
12805
14513
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12806
14514
|
issues.push(
|
|
12807
14515
|
issue(
|
|
@@ -12875,7 +14583,7 @@ async function readCatalog(catalogPath, issues) {
|
|
|
12875
14583
|
}
|
|
12876
14584
|
async function validateRouting(routingPath, catalogIds, issues) {
|
|
12877
14585
|
try {
|
|
12878
|
-
const parsed = parseYaml10(await
|
|
14586
|
+
const parsed = parseYaml10(await readFile41(routingPath, "utf-8"));
|
|
12879
14587
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12880
14588
|
issues.push(
|
|
12881
14589
|
issue(
|
|
@@ -12991,7 +14699,7 @@ function validateAgentRefs(value, catalogIds, issues, skill, routeIndex, phaseIn
|
|
|
12991
14699
|
}
|
|
12992
14700
|
async function validateProfiles(profilesPath, reviewerIds, issues) {
|
|
12993
14701
|
try {
|
|
12994
|
-
const parsed = parseYaml10(await
|
|
14702
|
+
const parsed = parseYaml10(await readFile41(profilesPath, "utf-8"));
|
|
12995
14703
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
12996
14704
|
issues.push(
|
|
12997
14705
|
issue(
|
|
@@ -13070,8 +14778,8 @@ function validateReviewerRefs(value, reviewerIds, issues, profileName, field) {
|
|
|
13070
14778
|
}
|
|
13071
14779
|
|
|
13072
14780
|
// src/core/validators/tddList.ts
|
|
13073
|
-
import { readFile as
|
|
13074
|
-
import
|
|
14781
|
+
import { readFile as readFile42, stat as stat10 } from "fs/promises";
|
|
14782
|
+
import path57 from "path";
|
|
13075
14783
|
init_utils();
|
|
13076
14784
|
var REQUIRED_COLUMNS = [
|
|
13077
14785
|
"TDD-ID",
|
|
@@ -13086,7 +14794,7 @@ var REQUIRED_COLUMNS = [
|
|
|
13086
14794
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["todo", "red", "green", "refactor", "done", "exception"]);
|
|
13087
14795
|
var TEST_FILE_CHECK_STATUSES = /* @__PURE__ */ new Set(["green", "refactor", "done"]);
|
|
13088
14796
|
var TDD_ID_FORMAT = /^TDD-\d{4}$/;
|
|
13089
|
-
var TDD_LIST_REL_PATH =
|
|
14797
|
+
var TDD_LIST_REL_PATH = path57.join("tdd", "test-list.md");
|
|
13090
14798
|
async function validateTddList(root, config) {
|
|
13091
14799
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
13092
14800
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -13098,8 +14806,8 @@ async function validateTddList(root, config) {
|
|
|
13098
14806
|
return issues;
|
|
13099
14807
|
}
|
|
13100
14808
|
async function validateSpecTddList(root, specDir, specNumber) {
|
|
13101
|
-
const filePath =
|
|
13102
|
-
const relPath =
|
|
14809
|
+
const filePath = path57.join(specDir, TDD_LIST_REL_PATH);
|
|
14810
|
+
const relPath = path57.relative(root, filePath).replace(/\\/g, "/");
|
|
13103
14811
|
const issues = [];
|
|
13104
14812
|
if (!await exists5(filePath)) {
|
|
13105
14813
|
issues.push(
|
|
@@ -13286,9 +14994,9 @@ async function validateSpecTddList(root, specDir, specNumber) {
|
|
|
13286
14994
|
continue;
|
|
13287
14995
|
}
|
|
13288
14996
|
const normalized = testFile.replace(/\\/g, "/");
|
|
13289
|
-
const resolved =
|
|
13290
|
-
const relative =
|
|
13291
|
-
if (
|
|
14997
|
+
const resolved = path57.resolve(root, normalized);
|
|
14998
|
+
const relative = path57.relative(root, resolved);
|
|
14999
|
+
if (path57.isAbsolute(normalized) || path57.win32.isAbsolute(normalized) || relative === ".." || relative.startsWith(".." + path57.sep)) {
|
|
13292
15000
|
issues.push(
|
|
13293
15001
|
issue(
|
|
13294
15002
|
"TDDLIST_TEST_FILE_MISSING",
|
|
@@ -13302,7 +15010,7 @@ async function validateSpecTddList(root, specDir, specNumber) {
|
|
|
13302
15010
|
}
|
|
13303
15011
|
let isFile2 = false;
|
|
13304
15012
|
try {
|
|
13305
|
-
isFile2 = (await
|
|
15013
|
+
isFile2 = (await stat10(resolved)).isFile();
|
|
13306
15014
|
} catch {
|
|
13307
15015
|
}
|
|
13308
15016
|
if (!isFile2) {
|
|
@@ -13351,11 +15059,11 @@ async function validateSpecTddList(root, specDir, specNumber) {
|
|
|
13351
15059
|
}
|
|
13352
15060
|
async function collectTestCaseIds(specDir) {
|
|
13353
15061
|
const empty = { knownTcIds: /* @__PURE__ */ new Set(), unitComponentTcIds: /* @__PURE__ */ new Set() };
|
|
13354
|
-
const testCasesPath =
|
|
15062
|
+
const testCasesPath = path57.join(specDir, "06_Test-Cases.md");
|
|
13355
15063
|
if (!await exists5(testCasesPath)) return empty;
|
|
13356
15064
|
let content;
|
|
13357
15065
|
try {
|
|
13358
|
-
content = await
|
|
15066
|
+
content = await readFile42(testCasesPath, "utf-8");
|
|
13359
15067
|
} catch {
|
|
13360
15068
|
return empty;
|
|
13361
15069
|
}
|
|
@@ -13381,7 +15089,7 @@ async function collectTestCaseIds(specDir) {
|
|
|
13381
15089
|
}
|
|
13382
15090
|
|
|
13383
15091
|
// src/core/validators/navigationFlow.ts
|
|
13384
|
-
import { readFile as
|
|
15092
|
+
import { readFile as readFile43 } from "fs/promises";
|
|
13385
15093
|
init_utils();
|
|
13386
15094
|
var NODE_DEF_RE = /([A-Za-z_][\w-]*)\s*(?:\[.*?\]|\(.*?\)|\{.*?\})/g;
|
|
13387
15095
|
var EDGE_RE = /([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?\s*(?:--+>|==+>|-.->|~~>)\s*(?:\|"?([^"|]*)"?\|)?\s*([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?/g;
|
|
@@ -13661,7 +15369,7 @@ async function validateNavigationFlow(root, config) {
|
|
|
13661
15369
|
const specFiles = allFiles.filter((f) => /[\\/]spec-\d{4}[\\/]/.test(f));
|
|
13662
15370
|
const issues = [];
|
|
13663
15371
|
for (const file of specFiles) {
|
|
13664
|
-
const content = await
|
|
15372
|
+
const content = await readFile43(file, "utf-8");
|
|
13665
15373
|
const blocks = extractFencedCodeBlocks(content);
|
|
13666
15374
|
const mermaidBlocks = blocks.filter((b) => b.language === "mermaid");
|
|
13667
15375
|
const _flowchartBlocks = mermaidBlocks.filter((b) => hasFlowchartDeclaration(b.content));
|
|
@@ -13684,8 +15392,8 @@ async function validateNavigationFlow(root, config) {
|
|
|
13684
15392
|
|
|
13685
15393
|
// src/core/validators/renderCritique.ts
|
|
13686
15394
|
init_utils();
|
|
13687
|
-
import
|
|
13688
|
-
import { readFile as
|
|
15395
|
+
import path58 from "path";
|
|
15396
|
+
import { readFile as readFile44 } from "fs/promises";
|
|
13689
15397
|
import fg9 from "fast-glob";
|
|
13690
15398
|
var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
|
|
13691
15399
|
var SPEC_RE = /\b(01_spec|03_acceptance-criteria|spec-|\bspec\b)\b/i;
|
|
@@ -13709,11 +15417,11 @@ var FOUR_STATE_CHECK_RE = /\bfour_state_check\s*:/i;
|
|
|
13709
15417
|
var MAX_PRIMARY_STEPS_RE = /\bmax_primary_steps\s*:\s*(\d+)/i;
|
|
13710
15418
|
async function validateRenderCritique(root, config) {
|
|
13711
15419
|
const issues = [];
|
|
13712
|
-
const skillsDir =
|
|
13713
|
-
const evidenceDir =
|
|
13714
|
-
const skillPromptPattern =
|
|
15420
|
+
const skillsDir = path58.join(root, config.paths.skillsDir).replace(/\\/g, "/");
|
|
15421
|
+
const evidenceDir = path58.join(root, ".qfai", "evidence").replace(/\\/g, "/");
|
|
15422
|
+
const skillPromptPattern = path58.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
13715
15423
|
const skillFiles = await fg9(skillPromptPattern, { dot: true });
|
|
13716
|
-
const evidencePattern =
|
|
15424
|
+
const evidencePattern = path58.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
|
|
13717
15425
|
const evidenceFiles = await fg9(evidencePattern, { dot: true });
|
|
13718
15426
|
if (skillFiles.length === 0 && evidenceFiles.length === 0) return issues;
|
|
13719
15427
|
const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
|
|
@@ -13723,7 +15431,7 @@ async function validateRenderCritique(root, config) {
|
|
|
13723
15431
|
issues.push(
|
|
13724
15432
|
issue(
|
|
13725
15433
|
"QFAI-CRIT-001",
|
|
13726
|
-
`Skill prompt does not mention rendered/screenshot/HTML review: ${
|
|
15434
|
+
`Skill prompt does not mention rendered/screenshot/HTML review: ${path58.relative(root, sf)}`,
|
|
13727
15435
|
"error",
|
|
13728
15436
|
sf,
|
|
13729
15437
|
"renderCritique.codeOnly",
|
|
@@ -13740,7 +15448,7 @@ async function validateRenderCritique(root, config) {
|
|
|
13740
15448
|
issues.push(
|
|
13741
15449
|
issue(
|
|
13742
15450
|
"QFAI-CRIT-002",
|
|
13743
|
-
`Downstream skill prompt missing canonical spec/contract references: ${
|
|
15451
|
+
`Downstream skill prompt missing canonical spec/contract references: ${path58.relative(root, sf)}`,
|
|
13744
15452
|
"error",
|
|
13745
15453
|
sf,
|
|
13746
15454
|
"renderCritique.contractMissing",
|
|
@@ -13802,7 +15510,7 @@ async function validateRenderCritique(root, config) {
|
|
|
13802
15510
|
issues.push(
|
|
13803
15511
|
issue(
|
|
13804
15512
|
"QFAI-CRIT-005",
|
|
13805
|
-
`Read order missing required tokens (${missing.join(", ")}): ${
|
|
15513
|
+
`Read order missing required tokens (${missing.join(", ")}): ${path58.relative(root, sf)}`,
|
|
13806
15514
|
"error",
|
|
13807
15515
|
sf,
|
|
13808
15516
|
"renderCritique.readOrder",
|
|
@@ -13830,7 +15538,7 @@ async function validateRenderCritique(root, config) {
|
|
|
13830
15538
|
issues.push(
|
|
13831
15539
|
issue(
|
|
13832
15540
|
"QFAI-CRIT-006",
|
|
13833
|
-
`Critique evidence incomplete (missing: ${missing.join(", ")}): ${
|
|
15541
|
+
`Critique evidence incomplete (missing: ${missing.join(", ")}): ${path58.relative(root, ef)}`,
|
|
13834
15542
|
"error",
|
|
13835
15543
|
ef,
|
|
13836
15544
|
"renderCritique.incompleteEvidence",
|
|
@@ -13945,9 +15653,9 @@ async function collectContent(files) {
|
|
|
13945
15653
|
return contents.join("\n---\n");
|
|
13946
15654
|
}
|
|
13947
15655
|
async function collectRenderEvidenceViewports(root) {
|
|
13948
|
-
const prototypingJsonPath =
|
|
15656
|
+
const prototypingJsonPath = path58.join(root, ".qfai", "evidence", "prototyping.json");
|
|
13949
15657
|
try {
|
|
13950
|
-
const raw = await
|
|
15658
|
+
const raw = await readFile44(prototypingJsonPath, "utf-8");
|
|
13951
15659
|
const parsed = JSON.parse(raw);
|
|
13952
15660
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
13953
15661
|
return /* @__PURE__ */ new Set();
|
|
@@ -13987,8 +15695,8 @@ async function collectRenderEvidenceViewports(root) {
|
|
|
13987
15695
|
|
|
13988
15696
|
// src/core/validators/designFidelity.ts
|
|
13989
15697
|
init_utils();
|
|
13990
|
-
import { readFile as
|
|
13991
|
-
import
|
|
15698
|
+
import { readFile as readFile45 } from "fs/promises";
|
|
15699
|
+
import path59 from "path";
|
|
13992
15700
|
import fg10 from "fast-glob";
|
|
13993
15701
|
var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
|
|
13994
15702
|
var BASE_DIMENSIONS = ["hierarchy", "clarity", "accessibility", "responsive"];
|
|
@@ -14013,19 +15721,19 @@ async function validateDesignFidelity(root, config) {
|
|
|
14013
15721
|
const evidenceDirs = [".qfai/evidence", ".qfai/review"];
|
|
14014
15722
|
const allFiles = [];
|
|
14015
15723
|
for (const dir of evidenceDirs) {
|
|
14016
|
-
const pattern =
|
|
15724
|
+
const pattern = path59.posix.join(root.replace(/\\/g, "/"), dir, "**/*.md");
|
|
14017
15725
|
const files = await fg10(pattern, { absolute: true });
|
|
14018
15726
|
allFiles.push(...files);
|
|
14019
15727
|
}
|
|
14020
15728
|
for (const filePath of allFiles) {
|
|
14021
15729
|
let content;
|
|
14022
15730
|
try {
|
|
14023
|
-
content = await
|
|
15731
|
+
content = await readFile45(filePath, "utf-8");
|
|
14024
15732
|
} catch {
|
|
14025
15733
|
continue;
|
|
14026
15734
|
}
|
|
14027
15735
|
if (!SCORECARD_HEADING_RE.test(content)) continue;
|
|
14028
|
-
const rel =
|
|
15736
|
+
const rel = path59.relative(root, filePath).replace(/\\/g, "/");
|
|
14029
15737
|
const section = extractScorecardSection(content);
|
|
14030
15738
|
if (!section) continue;
|
|
14031
15739
|
const dimensions = parseDimensions(section);
|
|
@@ -14318,7 +16026,7 @@ function hasAntiPatternMention(section, code) {
|
|
|
14318
16026
|
|
|
14319
16027
|
// src/core/validators/discussionDesignHardening.ts
|
|
14320
16028
|
init_surfaceType();
|
|
14321
|
-
import
|
|
16029
|
+
import path60 from "path";
|
|
14322
16030
|
init_utils();
|
|
14323
16031
|
var REQUIRED_SIDECARS = [
|
|
14324
16032
|
"uiux/30_exploration_brief.md",
|
|
@@ -14333,14 +16041,14 @@ function canonicalIssue(code, message, severity, file, rule) {
|
|
|
14333
16041
|
return issue(code, message, severity, file, rule, void 0, "canonical");
|
|
14334
16042
|
}
|
|
14335
16043
|
async function validateDiscussionDesignHardening(root, config) {
|
|
14336
|
-
const discussionDir =
|
|
16044
|
+
const discussionDir = path60.join(root, config.paths.discussionDir);
|
|
14337
16045
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
14338
16046
|
if (!packRoot) return [];
|
|
14339
16047
|
const uiBearing = await isDiscussionUiBearingPack(packRoot);
|
|
14340
16048
|
if (!uiBearing) return [];
|
|
14341
16049
|
const issues = [];
|
|
14342
16050
|
for (const relPath of REQUIRED_SIDECARS) {
|
|
14343
|
-
const content = await readSafe(
|
|
16051
|
+
const content = await readSafe(path60.join(packRoot, relPath));
|
|
14344
16052
|
if (!content) {
|
|
14345
16053
|
issues.push(
|
|
14346
16054
|
canonicalIssue(
|
|
@@ -14382,7 +16090,7 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
14382
16090
|
"UIX-VAL-EVAL-CALIBRATION"
|
|
14383
16091
|
)
|
|
14384
16092
|
);
|
|
14385
|
-
const antiGoals = await readSafe(
|
|
16093
|
+
const antiGoals = await readSafe(path60.join(packRoot, "uiux/32_design_anti_goals.md"));
|
|
14386
16094
|
if (antiGoals && !/^- /m.test(antiGoals)) {
|
|
14387
16095
|
issues.push(
|
|
14388
16096
|
canonicalIssue(
|
|
@@ -14394,7 +16102,7 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
14394
16102
|
)
|
|
14395
16103
|
);
|
|
14396
16104
|
}
|
|
14397
|
-
const reviewBundle = await readSafe(
|
|
16105
|
+
const reviewBundle = await readSafe(path60.join(packRoot, "uiux/50_review_input_bundle.md"));
|
|
14398
16106
|
if (reviewBundle && !/best-of-history/i.test(reviewBundle)) {
|
|
14399
16107
|
issues.push(
|
|
14400
16108
|
canonicalIssue(
|
|
@@ -14409,7 +16117,7 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
14409
16117
|
return issues;
|
|
14410
16118
|
}
|
|
14411
16119
|
async function validateSection(packRoot, relPath, headings, code) {
|
|
14412
|
-
const content = await readSafe(
|
|
16120
|
+
const content = await readSafe(path60.join(packRoot, relPath));
|
|
14413
16121
|
if (!content) {
|
|
14414
16122
|
return [];
|
|
14415
16123
|
}
|
|
@@ -14432,12 +16140,12 @@ async function validateSection(packRoot, relPath, headings, code) {
|
|
|
14432
16140
|
|
|
14433
16141
|
// src/core/validators/designAudit.ts
|
|
14434
16142
|
init_surfaceType();
|
|
14435
|
-
import { readdir as
|
|
14436
|
-
import
|
|
16143
|
+
import { readdir as readdir10 } from "fs/promises";
|
|
16144
|
+
import path61 from "path";
|
|
14437
16145
|
init_utils();
|
|
14438
16146
|
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
14439
16147
|
function toPosixRelative2(root, targetPath) {
|
|
14440
|
-
return
|
|
16148
|
+
return path61.relative(root, targetPath).replace(/\\/g, "/");
|
|
14441
16149
|
}
|
|
14442
16150
|
function resolveAuditConfig(config) {
|
|
14443
16151
|
const audit = config.uiux?.audit;
|
|
@@ -14559,19 +16267,19 @@ var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|
|
|
|
14559
16267
|
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
14560
16268
|
const findings = [];
|
|
14561
16269
|
const configuredDir = cfg.uiux?.designTokensDir;
|
|
14562
|
-
const tokensDir = configuredDir ?
|
|
16270
|
+
const tokensDir = configuredDir ? path61.resolve(root, configuredDir) : path61.join(root, cfg.paths.contractsDir, "design");
|
|
14563
16271
|
let hasTokenFiles = false;
|
|
14564
16272
|
try {
|
|
14565
|
-
const entries = await
|
|
16273
|
+
const entries = await readdir10(tokensDir);
|
|
14566
16274
|
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
14567
16275
|
} catch {
|
|
14568
16276
|
return findings;
|
|
14569
16277
|
}
|
|
14570
16278
|
if (!hasTokenFiles) return findings;
|
|
14571
|
-
const contractsUiDir =
|
|
16279
|
+
const contractsUiDir = path61.join(root, cfg.paths.contractsDir, "ui");
|
|
14572
16280
|
let htmlFiles = [];
|
|
14573
16281
|
try {
|
|
14574
|
-
const entries = await
|
|
16282
|
+
const entries = await readdir10(contractsUiDir);
|
|
14575
16283
|
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
14576
16284
|
} catch {
|
|
14577
16285
|
return findings;
|
|
@@ -14579,7 +16287,7 @@ async function checkTokenDrift(root, auditConfig, cfg) {
|
|
|
14579
16287
|
let rawCount = 0;
|
|
14580
16288
|
const sampleLiterals = [];
|
|
14581
16289
|
for (const htmlFile of htmlFiles) {
|
|
14582
|
-
const content = await readSafe(
|
|
16290
|
+
const content = await readSafe(path61.join(contractsUiDir, htmlFile));
|
|
14583
16291
|
if (!content) continue;
|
|
14584
16292
|
const matches = content.match(RAW_COLOR_RE);
|
|
14585
16293
|
if (matches) {
|
|
@@ -14630,14 +16338,14 @@ function deduplicateFindings(issues, maxPerRule) {
|
|
|
14630
16338
|
async function validateDesignAudit(root, config) {
|
|
14631
16339
|
const auditConfig = resolveAuditConfig(config);
|
|
14632
16340
|
if (!auditConfig.enabled) return [];
|
|
14633
|
-
const discussionDir =
|
|
16341
|
+
const discussionDir = path61.join(root, config.paths.discussionDir);
|
|
14634
16342
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
14635
16343
|
if (!packRoot) return [];
|
|
14636
16344
|
const uiBearing = await isDiscussionUiBearingPack(packRoot);
|
|
14637
16345
|
if (!uiBearing) return [];
|
|
14638
|
-
const contractsPath =
|
|
16346
|
+
const contractsPath = path61.join(packRoot, "uiux", "40_screen_contracts.md");
|
|
14639
16347
|
const contractsContent = await readSafe(contractsPath);
|
|
14640
|
-
const selectedDirectionPath =
|
|
16348
|
+
const selectedDirectionPath = path61.join(
|
|
14641
16349
|
root,
|
|
14642
16350
|
config.paths.contractsDir,
|
|
14643
16351
|
"design",
|
|
@@ -14665,8 +16373,8 @@ async function validateDesignAudit(root, config) {
|
|
|
14665
16373
|
// src/core/validators/designSlop.ts
|
|
14666
16374
|
init_surfaceType();
|
|
14667
16375
|
import { existsSync as existsSync2 } from "fs";
|
|
14668
|
-
import { readFile as
|
|
14669
|
-
import
|
|
16376
|
+
import { readFile as readFile46 } from "fs/promises";
|
|
16377
|
+
import path62 from "path";
|
|
14670
16378
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
14671
16379
|
init_utils();
|
|
14672
16380
|
function isValidSlopPattern(rule) {
|
|
@@ -14675,7 +16383,7 @@ function isValidSlopPattern(rule) {
|
|
|
14675
16383
|
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";
|
|
14676
16384
|
}
|
|
14677
16385
|
async function loadSlopPatterns(jsonPath) {
|
|
14678
|
-
const raw = await
|
|
16386
|
+
const raw = await readFile46(jsonPath, "utf-8");
|
|
14679
16387
|
const parsed = JSON.parse(raw);
|
|
14680
16388
|
if (!Array.isArray(parsed)) return [];
|
|
14681
16389
|
return parsed.filter((r) => isValidSlopPattern(r));
|
|
@@ -14683,11 +16391,11 @@ async function loadSlopPatterns(jsonPath) {
|
|
|
14683
16391
|
function defaultPatternsPath() {
|
|
14684
16392
|
const base = import.meta.url;
|
|
14685
16393
|
const basePath = base.startsWith("file:") ? fileURLToPath3(base) : base;
|
|
14686
|
-
const baseDir =
|
|
16394
|
+
const baseDir = path62.dirname(basePath);
|
|
14687
16395
|
const candidates = [
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
16396
|
+
path62.join(baseDir, "designSlopPatterns.json"),
|
|
16397
|
+
path62.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
16398
|
+
path62.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
14691
16399
|
];
|
|
14692
16400
|
for (const c of candidates) {
|
|
14693
16401
|
if (existsSync2(c)) return c;
|
|
@@ -14698,7 +16406,7 @@ async function validateDesignSlop(root, config) {
|
|
|
14698
16406
|
const auditConfig = resolveAuditConfig(config);
|
|
14699
16407
|
if (!auditConfig.enabled) return [];
|
|
14700
16408
|
if (!auditConfig.slopDetection) return [];
|
|
14701
|
-
const discussionDir =
|
|
16409
|
+
const discussionDir = path62.join(root, config.paths.discussionDir);
|
|
14702
16410
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
14703
16411
|
if (!packRoot) return [];
|
|
14704
16412
|
const uiBearing = await isDiscussionUiBearingPack(packRoot);
|
|
@@ -14719,7 +16427,7 @@ async function validateDesignSlop(root, config) {
|
|
|
14719
16427
|
continue;
|
|
14720
16428
|
}
|
|
14721
16429
|
for (const scope of pattern.scopes) {
|
|
14722
|
-
const filePath =
|
|
16430
|
+
const filePath = path62.join(packRoot, scope);
|
|
14723
16431
|
const content = await readSafe(filePath);
|
|
14724
16432
|
if (!content) continue;
|
|
14725
16433
|
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
@@ -14742,8 +16450,8 @@ async function validateDesignSlop(root, config) {
|
|
|
14742
16450
|
|
|
14743
16451
|
// src/core/validators/designContractReadiness.ts
|
|
14744
16452
|
init_utils();
|
|
14745
|
-
import
|
|
14746
|
-
import { readFile as
|
|
16453
|
+
import path63 from "path";
|
|
16454
|
+
import { readFile as readFile47 } from "fs/promises";
|
|
14747
16455
|
import fg11 from "fast-glob";
|
|
14748
16456
|
import { parse as parseYaml11 } from "yaml";
|
|
14749
16457
|
var REQUIRED_DESIGN_FILES = [
|
|
@@ -14754,24 +16462,24 @@ var REQUIRED_DESIGN_FILES = [
|
|
|
14754
16462
|
"design-system.yaml"
|
|
14755
16463
|
];
|
|
14756
16464
|
function toPosixRelative3(root, targetPath) {
|
|
14757
|
-
return
|
|
16465
|
+
return path63.relative(root, targetPath).replace(/\\/g, "/");
|
|
14758
16466
|
}
|
|
14759
16467
|
async function validateDesignContractReadiness(root, config) {
|
|
14760
|
-
const uiPattern =
|
|
14761
|
-
|
|
16468
|
+
const uiPattern = path63.posix.join(
|
|
16469
|
+
path63.join(root, config.paths.contractsDir, "ui").replace(/\\/g, "/"),
|
|
14762
16470
|
"**/*.yaml"
|
|
14763
16471
|
);
|
|
14764
16472
|
const uiContracts = await fg11(uiPattern, { absolute: true });
|
|
14765
16473
|
if (uiContracts.length === 0) {
|
|
14766
16474
|
return [];
|
|
14767
16475
|
}
|
|
14768
|
-
const designDir =
|
|
16476
|
+
const designDir = path63.join(root, config.paths.contractsDir, "design");
|
|
14769
16477
|
const designDirRelative = toPosixRelative3(root, designDir);
|
|
14770
16478
|
const issues = [];
|
|
14771
16479
|
for (const fileName of REQUIRED_DESIGN_FILES) {
|
|
14772
|
-
const filePath =
|
|
16480
|
+
const filePath = path63.join(designDir, fileName);
|
|
14773
16481
|
try {
|
|
14774
|
-
await
|
|
16482
|
+
await readFile47(filePath, "utf-8");
|
|
14775
16483
|
} catch {
|
|
14776
16484
|
issues.push(
|
|
14777
16485
|
issue(
|
|
@@ -14795,7 +16503,7 @@ async function validateDesignContractReadiness(root, config) {
|
|
|
14795
16503
|
return issues;
|
|
14796
16504
|
}
|
|
14797
16505
|
async function validateExplorationBrief(root, config) {
|
|
14798
|
-
const filePath =
|
|
16506
|
+
const filePath = path63.join(root, config.paths.contractsDir, "design", "exploration-brief.yaml");
|
|
14799
16507
|
const parsed = await readYaml(filePath);
|
|
14800
16508
|
if (parsed.kind !== "ok") {
|
|
14801
16509
|
return parsed.kind === "invalid" ? [
|
|
@@ -14826,7 +16534,7 @@ async function validateExplorationBrief(root, config) {
|
|
|
14826
16534
|
);
|
|
14827
16535
|
}
|
|
14828
16536
|
async function validateEvaluationRubric(root, config) {
|
|
14829
|
-
const filePath =
|
|
16537
|
+
const filePath = path63.join(root, config.paths.contractsDir, "design", "evaluation-rubric.yaml");
|
|
14830
16538
|
const parsed = await readYaml(filePath);
|
|
14831
16539
|
if (parsed.kind !== "ok") {
|
|
14832
16540
|
return parsed.kind === "invalid" ? [
|
|
@@ -14857,7 +16565,7 @@ async function validateEvaluationRubric(root, config) {
|
|
|
14857
16565
|
return issues;
|
|
14858
16566
|
}
|
|
14859
16567
|
async function validateEvaluatorCalibration(root, config) {
|
|
14860
|
-
const filePath =
|
|
16568
|
+
const filePath = path63.join(
|
|
14861
16569
|
root,
|
|
14862
16570
|
config.paths.contractsDir,
|
|
14863
16571
|
"design",
|
|
@@ -14891,7 +16599,7 @@ async function validateEvaluatorCalibration(root, config) {
|
|
|
14891
16599
|
);
|
|
14892
16600
|
}
|
|
14893
16601
|
async function validateSelectedDirection(root, config) {
|
|
14894
|
-
const filePath =
|
|
16602
|
+
const filePath = path63.join(root, config.paths.contractsDir, "design", "selected-direction.yaml");
|
|
14895
16603
|
const parsed = await readYaml(filePath);
|
|
14896
16604
|
if (parsed.kind !== "ok") {
|
|
14897
16605
|
return parsed.kind === "invalid" ? [
|
|
@@ -14929,7 +16637,7 @@ async function validateSelectedDirection(root, config) {
|
|
|
14929
16637
|
return issues;
|
|
14930
16638
|
}
|
|
14931
16639
|
async function validateDesignSystem(root, config) {
|
|
14932
|
-
const filePath =
|
|
16640
|
+
const filePath = path63.join(root, config.paths.contractsDir, "design", "design-system.yaml");
|
|
14933
16641
|
const parsed = await readYaml(filePath);
|
|
14934
16642
|
if (parsed.kind !== "ok") {
|
|
14935
16643
|
return parsed.kind === "invalid" ? [
|
|
@@ -14987,14 +16695,14 @@ function isNonEmptyStringValue(value) {
|
|
|
14987
16695
|
}
|
|
14988
16696
|
async function readYaml(filePath) {
|
|
14989
16697
|
try {
|
|
14990
|
-
const parsed = parseYaml11(await
|
|
16698
|
+
const parsed = parseYaml11(await readFile47(filePath, "utf-8"));
|
|
14991
16699
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14992
16700
|
return { kind: "invalid" };
|
|
14993
16701
|
}
|
|
14994
16702
|
return { kind: "ok", value: parsed };
|
|
14995
16703
|
} catch {
|
|
14996
16704
|
try {
|
|
14997
|
-
await
|
|
16705
|
+
await readFile47(filePath, "utf-8");
|
|
14998
16706
|
return { kind: "invalid" };
|
|
14999
16707
|
} catch {
|
|
15000
16708
|
return { kind: "missing" };
|
|
@@ -15002,14 +16710,184 @@ async function readYaml(filePath) {
|
|
|
15002
16710
|
}
|
|
15003
16711
|
}
|
|
15004
16712
|
|
|
16713
|
+
// src/core/validators/evaluatorReviewHardFloor.ts
|
|
16714
|
+
import { readFile as readFile48, readdir as readdir11 } from "fs/promises";
|
|
16715
|
+
import path64 from "path";
|
|
16716
|
+
import { parse as parseYaml12 } from "yaml";
|
|
16717
|
+
init_utils();
|
|
16718
|
+
var ISSUE_CODE = "QFAI-PROT-AXIS-FLOOR-001";
|
|
16719
|
+
var RULE = "evaluatorReviewHardFloor.perAxisScore";
|
|
16720
|
+
function isRecord16(value) {
|
|
16721
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16722
|
+
}
|
|
16723
|
+
function toPosixRelative4(root, target) {
|
|
16724
|
+
return path64.relative(root, target).replace(/\\/g, "/");
|
|
16725
|
+
}
|
|
16726
|
+
function isEnoentError(error) {
|
|
16727
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
16728
|
+
}
|
|
16729
|
+
async function readYamlObject(filePath) {
|
|
16730
|
+
let raw;
|
|
16731
|
+
try {
|
|
16732
|
+
raw = await readFile48(filePath, "utf-8");
|
|
16733
|
+
} catch (error) {
|
|
16734
|
+
if (isEnoentError(error)) {
|
|
16735
|
+
return { kind: "missing" };
|
|
16736
|
+
}
|
|
16737
|
+
return { kind: "invalid" };
|
|
16738
|
+
}
|
|
16739
|
+
try {
|
|
16740
|
+
const parsed = parseYaml12(raw);
|
|
16741
|
+
return isRecord16(parsed) ? { kind: "ok", value: parsed } : { kind: "invalid" };
|
|
16742
|
+
} catch {
|
|
16743
|
+
return { kind: "invalid" };
|
|
16744
|
+
}
|
|
16745
|
+
}
|
|
16746
|
+
async function readJsonObject(filePath) {
|
|
16747
|
+
let raw;
|
|
16748
|
+
try {
|
|
16749
|
+
raw = await readFile48(filePath, "utf-8");
|
|
16750
|
+
} catch (error) {
|
|
16751
|
+
if (isEnoentError(error)) {
|
|
16752
|
+
return { kind: "missing" };
|
|
16753
|
+
}
|
|
16754
|
+
return { kind: "invalid" };
|
|
16755
|
+
}
|
|
16756
|
+
try {
|
|
16757
|
+
const parsed = JSON.parse(raw);
|
|
16758
|
+
return isRecord16(parsed) ? { kind: "ok", value: parsed } : { kind: "invalid" };
|
|
16759
|
+
} catch {
|
|
16760
|
+
return { kind: "invalid" };
|
|
16761
|
+
}
|
|
16762
|
+
}
|
|
16763
|
+
function loadHardFloors(rubric) {
|
|
16764
|
+
const map = /* @__PURE__ */ new Map();
|
|
16765
|
+
const entries = rubric.hard_floors;
|
|
16766
|
+
if (!Array.isArray(entries)) {
|
|
16767
|
+
return map;
|
|
16768
|
+
}
|
|
16769
|
+
for (const entry of entries) {
|
|
16770
|
+
if (!isRecord16(entry)) {
|
|
16771
|
+
continue;
|
|
16772
|
+
}
|
|
16773
|
+
const id = entry.id;
|
|
16774
|
+
const minScore = entry.min_score;
|
|
16775
|
+
if (typeof id !== "string" || id.trim().length === 0 || typeof minScore !== "number" || !Number.isFinite(minScore)) {
|
|
16776
|
+
continue;
|
|
16777
|
+
}
|
|
16778
|
+
map.set(id, minScore);
|
|
16779
|
+
}
|
|
16780
|
+
return map;
|
|
16781
|
+
}
|
|
16782
|
+
async function listCandidateReviewFiles(reviewsDirAbsolute) {
|
|
16783
|
+
try {
|
|
16784
|
+
const names = await readdir11(reviewsDirAbsolute);
|
|
16785
|
+
return names.filter((name) => name.endsWith(".json"));
|
|
16786
|
+
} catch (error) {
|
|
16787
|
+
if (isEnoentError(error)) {
|
|
16788
|
+
return null;
|
|
16789
|
+
}
|
|
16790
|
+
throw error;
|
|
16791
|
+
}
|
|
16792
|
+
}
|
|
16793
|
+
function extractCandidateId(fileName) {
|
|
16794
|
+
return fileName.replace(/\.json$/i, "");
|
|
16795
|
+
}
|
|
16796
|
+
function evaluatePerAxis(review, hardFloors) {
|
|
16797
|
+
const perAxis = review.perAxis;
|
|
16798
|
+
if (!Array.isArray(perAxis)) {
|
|
16799
|
+
return [];
|
|
16800
|
+
}
|
|
16801
|
+
const violations = [];
|
|
16802
|
+
for (const axis of perAxis) {
|
|
16803
|
+
if (!isRecord16(axis)) {
|
|
16804
|
+
continue;
|
|
16805
|
+
}
|
|
16806
|
+
const axisId = axis.axisId;
|
|
16807
|
+
const score = axis.score;
|
|
16808
|
+
if (typeof axisId !== "string" || axisId.trim().length === 0 || typeof score !== "number" || !Number.isFinite(score)) {
|
|
16809
|
+
continue;
|
|
16810
|
+
}
|
|
16811
|
+
const minScore = hardFloors.get(axisId);
|
|
16812
|
+
if (minScore === void 0) {
|
|
16813
|
+
continue;
|
|
16814
|
+
}
|
|
16815
|
+
if (score < minScore) {
|
|
16816
|
+
violations.push({ axisId, score, minScore });
|
|
16817
|
+
}
|
|
16818
|
+
}
|
|
16819
|
+
return violations;
|
|
16820
|
+
}
|
|
16821
|
+
async function checkRound(root, round, hardFloors) {
|
|
16822
|
+
const reviewsDirRelative = roundEvaluatorReviewsDir(round);
|
|
16823
|
+
const reviewsDirAbsolute = path64.join(root, reviewsDirRelative);
|
|
16824
|
+
const files = await listCandidateReviewFiles(reviewsDirAbsolute);
|
|
16825
|
+
if (files === null || files.length === 0) {
|
|
16826
|
+
return [];
|
|
16827
|
+
}
|
|
16828
|
+
const issues = [];
|
|
16829
|
+
for (const fileName of files) {
|
|
16830
|
+
const candidateId = extractCandidateId(fileName);
|
|
16831
|
+
const reviewPathAbsolute = path64.join(reviewsDirAbsolute, fileName);
|
|
16832
|
+
const result = await readJsonObject(reviewPathAbsolute);
|
|
16833
|
+
if (result.kind !== "ok") {
|
|
16834
|
+
continue;
|
|
16835
|
+
}
|
|
16836
|
+
const violations = evaluatePerAxis(result.value, hardFloors);
|
|
16837
|
+
const reviewPathRelative = toPosixRelative4(root, reviewPathAbsolute);
|
|
16838
|
+
for (const violation of violations) {
|
|
16839
|
+
issues.push(
|
|
16840
|
+
issue(
|
|
16841
|
+
ISSUE_CODE,
|
|
16842
|
+
`Candidate ${candidateId} axis ${violation.axisId} score ${violation.score} is below hard_floors min_score ${violation.minScore} in round ${round}.`,
|
|
16843
|
+
"error",
|
|
16844
|
+
reviewPathRelative,
|
|
16845
|
+
RULE,
|
|
16846
|
+
[
|
|
16847
|
+
round,
|
|
16848
|
+
candidateId,
|
|
16849
|
+
violation.axisId,
|
|
16850
|
+
String(violation.score),
|
|
16851
|
+
String(violation.minScore)
|
|
16852
|
+
],
|
|
16853
|
+
"canonical",
|
|
16854
|
+
`${reviewPathRelative} \u306E ${violation.axisId} \u30B9\u30B3\u30A2\u3092\u518D\u8A55\u4FA1\u3057\u3001hard_floors[].min_score (${violation.minScore}) \u4EE5\u4E0A\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
16855
|
+
)
|
|
16856
|
+
);
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
return issues;
|
|
16860
|
+
}
|
|
16861
|
+
async function validateEvaluatorReviewHardFloor(root, config) {
|
|
16862
|
+
const screens = await readUiContractScreenContracts(root, config.paths.contractsDir);
|
|
16863
|
+
if (screens.length === 0) {
|
|
16864
|
+
return [];
|
|
16865
|
+
}
|
|
16866
|
+
const rubricPath = path64.join(root, config.paths.contractsDir, "design", "evaluation-rubric.yaml");
|
|
16867
|
+
const rubric = await readYamlObject(rubricPath);
|
|
16868
|
+
if (rubric.kind !== "ok") {
|
|
16869
|
+
return [];
|
|
16870
|
+
}
|
|
16871
|
+
const hardFloors = loadHardFloors(rubric.value);
|
|
16872
|
+
if (hardFloors.size === 0) {
|
|
16873
|
+
return [];
|
|
16874
|
+
}
|
|
16875
|
+
const issues = [];
|
|
16876
|
+
for (const round of ABSORPTION_ROUNDS) {
|
|
16877
|
+
const roundIssues = await checkRound(root, round, hardFloors);
|
|
16878
|
+
issues.push(...roundIssues);
|
|
16879
|
+
}
|
|
16880
|
+
return issues;
|
|
16881
|
+
}
|
|
16882
|
+
|
|
15005
16883
|
// src/core/validators/uiEvidenceArtifacts.ts
|
|
15006
|
-
import
|
|
16884
|
+
import path65 from "path";
|
|
15007
16885
|
init_utils();
|
|
15008
16886
|
function resolveEvidenceRoot2(root, config) {
|
|
15009
|
-
return
|
|
16887
|
+
return path65.join(path65.dirname(resolvePath(root, config, "specsDir")), "evidence");
|
|
15010
16888
|
}
|
|
15011
|
-
function
|
|
15012
|
-
return
|
|
16889
|
+
function toPosixRelative5(root, targetPath) {
|
|
16890
|
+
return path65.relative(root, targetPath).replace(/\\/g, "/");
|
|
15013
16891
|
}
|
|
15014
16892
|
var SAFE_SCREEN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
15015
16893
|
async function validateUiEvidenceArtifacts(root, config) {
|
|
@@ -15019,8 +16897,8 @@ async function validateUiEvidenceArtifacts(root, config) {
|
|
|
15019
16897
|
return issues;
|
|
15020
16898
|
}
|
|
15021
16899
|
const evidenceRoot = resolveEvidenceRoot2(root, config);
|
|
15022
|
-
const screenshotRoot =
|
|
15023
|
-
const htmlRoot =
|
|
16900
|
+
const screenshotRoot = path65.join(evidenceRoot, "prototyping", "screenshots");
|
|
16901
|
+
const htmlRoot = path65.join(evidenceRoot, "prototyping", "html");
|
|
15024
16902
|
for (const screen of screens) {
|
|
15025
16903
|
if (!SAFE_SCREEN_ID_PATTERN.test(screen.screenId)) {
|
|
15026
16904
|
issues.push(
|
|
@@ -15037,11 +16915,11 @@ async function validateUiEvidenceArtifacts(root, config) {
|
|
|
15037
16915
|
);
|
|
15038
16916
|
continue;
|
|
15039
16917
|
}
|
|
15040
|
-
const screenshotPath =
|
|
15041
|
-
const htmlPath =
|
|
16918
|
+
const screenshotPath = path65.join(screenshotRoot, `${screen.screenId}.png`);
|
|
16919
|
+
const htmlPath = path65.join(htmlRoot, `${screen.screenId}.html`);
|
|
15042
16920
|
if (!await exists5(screenshotPath)) {
|
|
15043
|
-
const screenshotPattern =
|
|
15044
|
-
|
|
16921
|
+
const screenshotPattern = path65.posix.join(
|
|
16922
|
+
toPosixRelative5(root, screenshotRoot),
|
|
15045
16923
|
"<screen-id>.png"
|
|
15046
16924
|
);
|
|
15047
16925
|
issues.push(
|
|
@@ -15049,7 +16927,7 @@ async function validateUiEvidenceArtifacts(root, config) {
|
|
|
15049
16927
|
"QFAI-UIE-001",
|
|
15050
16928
|
`Missing screenshot evidence for declared screen "${screen.screenId}".`,
|
|
15051
16929
|
"error",
|
|
15052
|
-
|
|
16930
|
+
toPosixRelative5(root, screenshotPath),
|
|
15053
16931
|
"uiEvidenceArtifacts.screenshotRequired",
|
|
15054
16932
|
[screen.sourceRef],
|
|
15055
16933
|
"canonical",
|
|
@@ -15058,13 +16936,13 @@ async function validateUiEvidenceArtifacts(root, config) {
|
|
|
15058
16936
|
);
|
|
15059
16937
|
}
|
|
15060
16938
|
if (!await exists5(htmlPath)) {
|
|
15061
|
-
const htmlPattern =
|
|
16939
|
+
const htmlPattern = path65.posix.join(toPosixRelative5(root, htmlRoot), "<screen-id>.html");
|
|
15062
16940
|
issues.push(
|
|
15063
16941
|
issue(
|
|
15064
16942
|
"QFAI-UIE-002",
|
|
15065
16943
|
`Missing HTML snapshot evidence for declared screen "${screen.screenId}".`,
|
|
15066
16944
|
"error",
|
|
15067
|
-
|
|
16945
|
+
toPosixRelative5(root, htmlPath),
|
|
15068
16946
|
"uiEvidenceArtifacts.htmlRequired",
|
|
15069
16947
|
[screen.sourceRef],
|
|
15070
16948
|
"canonical",
|
|
@@ -15083,8 +16961,8 @@ async function isUiBearingSpec(root) {
|
|
|
15083
16961
|
}
|
|
15084
16962
|
|
|
15085
16963
|
// src/core/validators/uix/threeLayer.ts
|
|
15086
|
-
import { readdir as
|
|
15087
|
-
import
|
|
16964
|
+
import { readdir as readdir12 } from "fs/promises";
|
|
16965
|
+
import path66 from "path";
|
|
15088
16966
|
init_utils();
|
|
15089
16967
|
var EXPLORATION_SECTIONS = [
|
|
15090
16968
|
"product_intent",
|
|
@@ -15139,7 +17017,7 @@ async function validateThreeLayerModel(root, _config) {
|
|
|
15139
17017
|
if (!await isUiBearingSpec(root)) return [];
|
|
15140
17018
|
const issues = [];
|
|
15141
17019
|
for (const sidecar of CANONICAL_REQUIRED_SIDECAR_FILES) {
|
|
15142
|
-
const content = await readSafe(
|
|
17020
|
+
const content = await readSafe(path66.join(root, "uiux", sidecar));
|
|
15143
17021
|
if (!content) {
|
|
15144
17022
|
continue;
|
|
15145
17023
|
}
|
|
@@ -15181,7 +17059,7 @@ async function validateForbiddenLegacyFiles(root, _config) {
|
|
|
15181
17059
|
if (!await isUiBearingSpec(root)) return [];
|
|
15182
17060
|
let entries = [];
|
|
15183
17061
|
try {
|
|
15184
|
-
entries = await
|
|
17062
|
+
entries = await readdir12(path66.join(root, "uiux"));
|
|
15185
17063
|
} catch {
|
|
15186
17064
|
return [];
|
|
15187
17065
|
}
|
|
@@ -15197,11 +17075,11 @@ async function validateForbiddenLegacyFiles(root, _config) {
|
|
|
15197
17075
|
}
|
|
15198
17076
|
async function validateThreeLayerFamilyCompleteness(root, _config) {
|
|
15199
17077
|
if (!await isUiBearingSpec(root)) return [];
|
|
15200
|
-
const indexContent = await readSafe(
|
|
17078
|
+
const indexContent = await readSafe(path66.join(root, "uiux", "00_index.md"));
|
|
15201
17079
|
if (!indexContent) return [];
|
|
15202
17080
|
const issues = [];
|
|
15203
17081
|
for (const required of CANONICAL_REQUIRED_SIDECAR_FILES) {
|
|
15204
|
-
const content = await readSafe(
|
|
17082
|
+
const content = await readSafe(path66.join(root, "uiux", required));
|
|
15205
17083
|
if (!content) {
|
|
15206
17084
|
issues.push(
|
|
15207
17085
|
threeLayerIssue(
|
|
@@ -15218,7 +17096,7 @@ async function validateThreeLayerFamilyCompleteness(root, _config) {
|
|
|
15218
17096
|
}
|
|
15219
17097
|
|
|
15220
17098
|
// src/core/validators/uix/taste.ts
|
|
15221
|
-
import
|
|
17099
|
+
import path67 from "path";
|
|
15222
17100
|
init_utils();
|
|
15223
17101
|
var REQUIRED_SECTIONS = [
|
|
15224
17102
|
"visual_character",
|
|
@@ -15235,12 +17113,12 @@ var REQUIRED_SECTIONS = [
|
|
|
15235
17113
|
var REQUIRED_SECTION_COUNT = REQUIRED_SECTIONS.length;
|
|
15236
17114
|
|
|
15237
17115
|
// src/core/validators/uix/trendScan.ts
|
|
15238
|
-
import
|
|
17116
|
+
import path68 from "path";
|
|
15239
17117
|
init_utils();
|
|
15240
17118
|
|
|
15241
17119
|
// src/core/validators/uix/strategy.ts
|
|
15242
17120
|
init_surface();
|
|
15243
|
-
import
|
|
17121
|
+
import path69 from "path";
|
|
15244
17122
|
|
|
15245
17123
|
// src/core/domain/strategyDecision.ts
|
|
15246
17124
|
var CANONICAL_STRATEGY_DECISIONS = [
|
|
@@ -15257,9 +17135,9 @@ var CANONICAL_STRATEGY_DECISION_SET = new Set(CANONICAL_STRATEGY_DECISIONS);
|
|
|
15257
17135
|
init_utils();
|
|
15258
17136
|
|
|
15259
17137
|
// src/core/validators/uix/screenContract.ts
|
|
15260
|
-
import
|
|
17138
|
+
import path70 from "path";
|
|
15261
17139
|
init_utils();
|
|
15262
|
-
var
|
|
17140
|
+
var REQUIRED_FIELDS2 = [
|
|
15263
17141
|
"screen_id",
|
|
15264
17142
|
"route",
|
|
15265
17143
|
"purpose",
|
|
@@ -15388,7 +17266,7 @@ function assignNestedChild(block, key, value) {
|
|
|
15388
17266
|
}
|
|
15389
17267
|
async function validateScreenContractSchema(root, _config) {
|
|
15390
17268
|
if (!await isUiBearingSpec(root)) return [];
|
|
15391
|
-
const contractsPath =
|
|
17269
|
+
const contractsPath = path70.join(root, "uiux", "40_screen_contracts.md");
|
|
15392
17270
|
const content = await readSafe(contractsPath);
|
|
15393
17271
|
if (!content) return [];
|
|
15394
17272
|
const screens = parseScreenBlocks2(content);
|
|
@@ -15424,7 +17302,7 @@ async function validateScreenContractSchema(root, _config) {
|
|
|
15424
17302
|
);
|
|
15425
17303
|
continue;
|
|
15426
17304
|
}
|
|
15427
|
-
const missing =
|
|
17305
|
+
const missing = REQUIRED_FIELDS2.filter((f) => {
|
|
15428
17306
|
if (f === "primary_tasks") return screen.primaryTasks.length === 0;
|
|
15429
17307
|
if (f === "secondary_tasks") return screen.secondaryTasks.length === 0;
|
|
15430
17308
|
if (f === "required_states") return Object.keys(screen.requiredStates).length === 0;
|
|
@@ -15462,12 +17340,12 @@ async function validateScreenContractSchema(root, _config) {
|
|
|
15462
17340
|
|
|
15463
17341
|
// src/core/validators/uix/canonical.ts
|
|
15464
17342
|
init_utils();
|
|
15465
|
-
import
|
|
17343
|
+
import path75 from "path";
|
|
15466
17344
|
|
|
15467
17345
|
// src/core/validators/uix/classification.ts
|
|
15468
17346
|
init_surfaceType();
|
|
15469
17347
|
init_utils();
|
|
15470
|
-
import
|
|
17348
|
+
import path71 from "path";
|
|
15471
17349
|
var VALID_PRIMARY_SURFACES = [
|
|
15472
17350
|
"web",
|
|
15473
17351
|
"mobile",
|
|
@@ -15488,7 +17366,7 @@ function classificationIssue(code, message, severity, file, suggestedAction) {
|
|
|
15488
17366
|
};
|
|
15489
17367
|
}
|
|
15490
17368
|
async function validateClassification(root, _config) {
|
|
15491
|
-
const contextPath =
|
|
17369
|
+
const contextPath = path71.join(root, "01_Context.md");
|
|
15492
17370
|
const content = await readSafe(contextPath);
|
|
15493
17371
|
if (!content) {
|
|
15494
17372
|
return [];
|
|
@@ -15666,8 +17544,8 @@ async function validateClassification(root, _config) {
|
|
|
15666
17544
|
}
|
|
15667
17545
|
|
|
15668
17546
|
// src/core/validators/uix/foundation.ts
|
|
15669
|
-
import { readdir as
|
|
15670
|
-
import
|
|
17547
|
+
import { readdir as readdir13 } from "fs/promises";
|
|
17548
|
+
import path72 from "path";
|
|
15671
17549
|
function canonicalIssue2(code, message, severity, file, suggestedAction) {
|
|
15672
17550
|
return {
|
|
15673
17551
|
code,
|
|
@@ -15681,7 +17559,7 @@ function canonicalIssue2(code, message, severity, file, suggestedAction) {
|
|
|
15681
17559
|
async function validateSidecarMissing(root, _config) {
|
|
15682
17560
|
if (!await isUiBearingSpec(root)) return [];
|
|
15683
17561
|
try {
|
|
15684
|
-
await
|
|
17562
|
+
await readdir13(path72.join(root, "uiux"));
|
|
15685
17563
|
return [];
|
|
15686
17564
|
} catch (err) {
|
|
15687
17565
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
@@ -15700,7 +17578,7 @@ async function validateSidecarMissing(root, _config) {
|
|
|
15700
17578
|
}
|
|
15701
17579
|
|
|
15702
17580
|
// src/core/validators/uix/comparisonValidator.ts
|
|
15703
|
-
import
|
|
17581
|
+
import path73 from "path";
|
|
15704
17582
|
init_utils();
|
|
15705
17583
|
function canonicalIssue3(code, message, severity, file, suggestedAction) {
|
|
15706
17584
|
return {
|
|
@@ -15715,10 +17593,10 @@ function canonicalIssue3(code, message, severity, file, suggestedAction) {
|
|
|
15715
17593
|
async function validateExplorationArtifacts(root, _config) {
|
|
15716
17594
|
if (!await isUiBearingSpec(root)) return [];
|
|
15717
17595
|
const issues = [];
|
|
15718
|
-
const briefPath =
|
|
15719
|
-
const rubricPath =
|
|
15720
|
-
const calibrationPath =
|
|
15721
|
-
const reviewBundlePath =
|
|
17596
|
+
const briefPath = path73.join(root, "uiux", "30_exploration_brief.md");
|
|
17597
|
+
const rubricPath = path73.join(root, "uiux", "33_exploration_rubric.md");
|
|
17598
|
+
const calibrationPath = path73.join(root, "uiux", "34_evaluator_calibration.md");
|
|
17599
|
+
const reviewBundlePath = path73.join(root, "uiux", "50_review_input_bundle.md");
|
|
15722
17600
|
const briefContent = await readSafe(briefPath);
|
|
15723
17601
|
if (!briefContent) {
|
|
15724
17602
|
issues.push(
|
|
@@ -15818,7 +17696,7 @@ async function validateExplorationArtifacts(root, _config) {
|
|
|
15818
17696
|
}
|
|
15819
17697
|
|
|
15820
17698
|
// src/core/validators/uix/oqClosure.ts
|
|
15821
|
-
import
|
|
17699
|
+
import path74 from "path";
|
|
15822
17700
|
init_utils();
|
|
15823
17701
|
function canonicalIssue4(code, message, severity, file, suggestedAction) {
|
|
15824
17702
|
return {
|
|
@@ -15832,8 +17710,8 @@ function canonicalIssue4(code, message, severity, file, suggestedAction) {
|
|
|
15832
17710
|
}
|
|
15833
17711
|
async function validateOqClosure(root, _config) {
|
|
15834
17712
|
if (!await isUiBearingSpec(root)) return [];
|
|
15835
|
-
const rootOqPath =
|
|
15836
|
-
const uiuxOqPath =
|
|
17713
|
+
const rootOqPath = path74.join(root, "11_OQ-Register.md");
|
|
17714
|
+
const uiuxOqPath = path74.join(root, "uiux", "11_OQ-Register.md");
|
|
15837
17715
|
const rootContent = await readSafe(rootOqPath);
|
|
15838
17716
|
const content = rootContent || await readSafe(uiuxOqPath);
|
|
15839
17717
|
if (!content) return [];
|
|
@@ -15885,7 +17763,7 @@ async function validateOqClosure(root, _config) {
|
|
|
15885
17763
|
|
|
15886
17764
|
// src/core/validators/uix/canonical.ts
|
|
15887
17765
|
async function runCanonicalUixValidators(root, config) {
|
|
15888
|
-
const directSpec = await readSafe(
|
|
17766
|
+
const directSpec = await readSafe(path75.join(root, "01_Spec.md"));
|
|
15889
17767
|
if (!directSpec) {
|
|
15890
17768
|
return [];
|
|
15891
17769
|
}
|
|
@@ -15912,8 +17790,8 @@ async function runCanonicalUixValidators(root, config) {
|
|
|
15912
17790
|
|
|
15913
17791
|
// src/core/validators/traceabilityIntegrity.ts
|
|
15914
17792
|
import { execFileSync } from "child_process";
|
|
15915
|
-
import { readFile as
|
|
15916
|
-
import
|
|
17793
|
+
import { readFile as readFile49 } from "fs/promises";
|
|
17794
|
+
import path76 from "path";
|
|
15917
17795
|
init_utils();
|
|
15918
17796
|
var BR_AC_FILES = /* @__PURE__ */ new Set(["04_Business-Rules.md", "03_Acceptance-Criteria.md"]);
|
|
15919
17797
|
function normalizePath2(p) {
|
|
@@ -15997,10 +17875,10 @@ async function validateTraceabilityIntegrity(root, config) {
|
|
|
15997
17875
|
const specsRelDir = config.paths.specsDir;
|
|
15998
17876
|
const changedSpecIds = findChangedSpecDirs(changedFiles, specsRelDir);
|
|
15999
17877
|
for (const specId of changedSpecIds) {
|
|
16000
|
-
const ledgerPath =
|
|
17878
|
+
const ledgerPath = path76.join(specsDir, specId, "16_Traceability-ledger.md");
|
|
16001
17879
|
let ledgerContent;
|
|
16002
17880
|
try {
|
|
16003
|
-
ledgerContent = await
|
|
17881
|
+
ledgerContent = await readFile49(ledgerPath, "utf-8");
|
|
16004
17882
|
} catch {
|
|
16005
17883
|
issues.push(
|
|
16006
17884
|
issue(
|
|
@@ -16256,16 +18134,187 @@ function validatePrototypingSkillContent(content) {
|
|
|
16256
18134
|
};
|
|
16257
18135
|
}
|
|
16258
18136
|
|
|
18137
|
+
// src/core/validators/testTodoStubs.ts
|
|
18138
|
+
import { readFile as readFile50 } from "fs/promises";
|
|
18139
|
+
import path77 from "path";
|
|
18140
|
+
init_utils();
|
|
18141
|
+
var TEST_TODO_PATTERN = /\b(it|test|describe)\.todo\s*\(/g;
|
|
18142
|
+
async function validateTestTodoStubs(root, config) {
|
|
18143
|
+
if (!config.validation.testStrategy.forbidTestTodoStubs) {
|
|
18144
|
+
return [];
|
|
18145
|
+
}
|
|
18146
|
+
const globs = config.validation.traceability.testFileGlobs;
|
|
18147
|
+
if (globs.length === 0) {
|
|
18148
|
+
return [];
|
|
18149
|
+
}
|
|
18150
|
+
const excludeGlobs = Array.from(
|
|
18151
|
+
/* @__PURE__ */ new Set([
|
|
18152
|
+
...DEFAULT_TEST_FILE_EXCLUDE_GLOBS,
|
|
18153
|
+
...config.validation.traceability.testFileExcludeGlobs
|
|
18154
|
+
])
|
|
18155
|
+
);
|
|
18156
|
+
const { files } = await collectFilesByGlobs(root, {
|
|
18157
|
+
globs,
|
|
18158
|
+
ignore: excludeGlobs,
|
|
18159
|
+
limit: DEFAULT_GLOB_FILE_LIMIT
|
|
18160
|
+
});
|
|
18161
|
+
const issues = [];
|
|
18162
|
+
for (const absFile of files) {
|
|
18163
|
+
const relFile = path77.relative(root, absFile).replace(/\\/g, "/");
|
|
18164
|
+
let content;
|
|
18165
|
+
try {
|
|
18166
|
+
content = await readFile50(absFile, "utf-8");
|
|
18167
|
+
} catch {
|
|
18168
|
+
continue;
|
|
18169
|
+
}
|
|
18170
|
+
const lines = content.split(/\r?\n/);
|
|
18171
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
18172
|
+
const line = lines[i] ?? "";
|
|
18173
|
+
const lineNumber = i + 1;
|
|
18174
|
+
for (const match of line.matchAll(TEST_TODO_PATTERN)) {
|
|
18175
|
+
const matchedKind = match[1];
|
|
18176
|
+
const stubIssue = issue(
|
|
18177
|
+
"QFAI-TEST-001",
|
|
18178
|
+
`Test todo stub found: ${matchedKind}.todo at ${relFile}:${lineNumber}. Stubs are silent in vitest/jest and rot as missed work. Implement the body or delete the stub (spec-0017 REQ-0009).`,
|
|
18179
|
+
"error",
|
|
18180
|
+
relFile,
|
|
18181
|
+
"validation.testStrategy.forbidTestTodoStubs",
|
|
18182
|
+
[`${matchedKind}.todo`],
|
|
18183
|
+
"canonical",
|
|
18184
|
+
"Implement the test body, or delete the stub entirely. If you need to temporarily opt out of this check, set `validation.testStrategy.forbidTestTodoStubs: false` in qfai.config.yaml."
|
|
18185
|
+
);
|
|
18186
|
+
stubIssue.loc = { line: lineNumber };
|
|
18187
|
+
issues.push(stubIssue);
|
|
18188
|
+
}
|
|
18189
|
+
}
|
|
18190
|
+
}
|
|
18191
|
+
return issues;
|
|
18192
|
+
}
|
|
18193
|
+
|
|
16259
18194
|
// src/core/validate.ts
|
|
16260
18195
|
init_utils();
|
|
16261
18196
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
16262
18197
|
async function validateProject(root, configResult, options = {}) {
|
|
16263
18198
|
const resolved = configResult ?? await loadConfig(root);
|
|
16264
18199
|
const { config, issues: configIssues } = resolved;
|
|
16265
|
-
const
|
|
16266
|
-
const
|
|
18200
|
+
const profile = options.profile ?? "full";
|
|
18201
|
+
const findings = [
|
|
18202
|
+
...configIssues,
|
|
18203
|
+
...await runProfileValidators(root, config, profile, options.platform)
|
|
18204
|
+
];
|
|
18205
|
+
const { issues, waivers } = await applyWaivers(root, findings);
|
|
18206
|
+
const specsRoot = resolvePath(root, config, "specsDir");
|
|
18207
|
+
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
18208
|
+
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
18209
|
+
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
18210
|
+
root,
|
|
18211
|
+
config.validation.traceability.testFileGlobs,
|
|
18212
|
+
config.validation.traceability.testFileExcludeGlobs
|
|
18213
|
+
);
|
|
18214
|
+
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
18215
|
+
const toolVersion = await resolveToolVersion();
|
|
18216
|
+
return {
|
|
18217
|
+
toolVersion,
|
|
18218
|
+
profile,
|
|
18219
|
+
issues,
|
|
18220
|
+
counts: countIssues(issues),
|
|
18221
|
+
traceability: {
|
|
18222
|
+
sc: scCoverage,
|
|
18223
|
+
testFiles
|
|
18224
|
+
},
|
|
18225
|
+
waivers
|
|
18226
|
+
};
|
|
18227
|
+
}
|
|
18228
|
+
async function runProfileValidators(root, config, profile, platformOption) {
|
|
18229
|
+
switch (profile) {
|
|
18230
|
+
case "discussion":
|
|
18231
|
+
return runDiscussionValidators(root, config);
|
|
18232
|
+
case "sdd":
|
|
18233
|
+
return runSddValidators(root, config);
|
|
18234
|
+
case "prototyping":
|
|
18235
|
+
return runPrototypingValidators(root, config, platformOption);
|
|
18236
|
+
case "atdd":
|
|
18237
|
+
return runAtddValidators(root, config);
|
|
18238
|
+
case "tdd":
|
|
18239
|
+
return runTddValidators(root, config);
|
|
18240
|
+
case "verify":
|
|
18241
|
+
case "full":
|
|
18242
|
+
return runFullValidators(root, config, platformOption);
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
18245
|
+
async function runDiscussionValidators(root, config) {
|
|
18246
|
+
return [
|
|
18247
|
+
...await validateDiscussionMermaid(root),
|
|
18248
|
+
...await validateDiscussionPackReadiness(root, config),
|
|
18249
|
+
...await validateDiscussionVisuals(root),
|
|
18250
|
+
...await validateDiscussionDesignHardening(root, config),
|
|
18251
|
+
...await validateResearchSummary(root, config),
|
|
18252
|
+
...await runCanonicalUixValidators(root, config)
|
|
18253
|
+
];
|
|
18254
|
+
}
|
|
18255
|
+
async function runSddValidators(root, config, includeCodeReferences = false) {
|
|
18256
|
+
return [
|
|
18257
|
+
...await validateMermaidEnforcement(root),
|
|
18258
|
+
...await validateSpecPacks(root, config),
|
|
18259
|
+
...await validateStatusInSpecs(root, config),
|
|
18260
|
+
...await validateDensityHints(root, config),
|
|
18261
|
+
...await validateSpecSplitByCapability(root, config),
|
|
18262
|
+
...await validateLayeredTraceability(root, config),
|
|
18263
|
+
...await validateOrphanProhibition(root, config),
|
|
18264
|
+
...await validateLayerCoverage(root, config),
|
|
18265
|
+
...await validateContractReferences(root, config),
|
|
18266
|
+
...await validateTraceability(root, config, { includeCodeReferences }),
|
|
18267
|
+
...await validateDefinedIds(root, config),
|
|
18268
|
+
...await validateContracts(root, config),
|
|
18269
|
+
...await validateNavigationFlow(root, config)
|
|
18270
|
+
];
|
|
18271
|
+
}
|
|
18272
|
+
async function runPrototypingValidators(root, config, platformOption) {
|
|
18273
|
+
return [
|
|
18274
|
+
...await runUiuxValidators(root, config, platformOption),
|
|
18275
|
+
...await validatePrototypingEvidence(root, config),
|
|
18276
|
+
...await validateBreakthroughEvidence(root, config),
|
|
18277
|
+
...await validatePrototypingDesignSystem(root, config),
|
|
18278
|
+
...await validateUiEvidenceArtifacts(root, config),
|
|
18279
|
+
...await validateRenderCritique(root, config),
|
|
18280
|
+
...await validateDesignFidelity(root, config),
|
|
18281
|
+
...await validateDesignContractReadiness(root, config),
|
|
18282
|
+
...await validateEvaluatorReviewHardFloor(root, config),
|
|
18283
|
+
...await validateStateGate(root, config),
|
|
18284
|
+
...await validateCompletionCertificateIssues(root, config),
|
|
18285
|
+
...await validateConfigReferenceIntegrity(root, config),
|
|
18286
|
+
...await validatePrototypingArtifactRefIntegrity(root, config),
|
|
18287
|
+
...await validateSpecIdLinkage(root, config)
|
|
18288
|
+
];
|
|
18289
|
+
}
|
|
18290
|
+
async function runAtddValidators(root, config) {
|
|
18291
|
+
return [...await validateAtddCodeTraceability(root, config)];
|
|
18292
|
+
}
|
|
18293
|
+
async function runTddValidators(root, config, includeTraceability = true) {
|
|
18294
|
+
return [
|
|
18295
|
+
...await validateTddList(root, config),
|
|
18296
|
+
...await validateTestTodoStubs(root, config),
|
|
18297
|
+
...includeTraceability ? await validateTraceability(root, config, { includeCodeReferences: true }) : [],
|
|
18298
|
+
...await validateTraceabilityIntegrity(root, config)
|
|
18299
|
+
];
|
|
18300
|
+
}
|
|
18301
|
+
async function runFullValidators(root, config, platformOption) {
|
|
18302
|
+
return [
|
|
18303
|
+
...await validateRepositoryHygiene(root, config),
|
|
18304
|
+
...await validateSkillsIntegrity(root, config),
|
|
18305
|
+
...await validateAssistantAssets(root, config),
|
|
18306
|
+
...await runDiscussionValidators(root, config),
|
|
18307
|
+
...await runSddValidators(root, config, true),
|
|
18308
|
+
...await validateReviewArtifacts(root),
|
|
18309
|
+
...await runPrototypingValidators(root, config, platformOption),
|
|
18310
|
+
...await runAtddValidators(root, config),
|
|
18311
|
+
...await runTddValidators(root, config, false),
|
|
18312
|
+
...await validatePrototypingSkill(root, config)
|
|
18313
|
+
];
|
|
18314
|
+
}
|
|
18315
|
+
async function runUiuxValidators(root, config, platformOption) {
|
|
16267
18316
|
const uiuxStart = performance.now();
|
|
16268
|
-
const platformResult = await detectPlatform(root, config,
|
|
18317
|
+
const platformResult = await detectPlatform(root, config, platformOption);
|
|
16269
18318
|
const platform = platformResult.platform;
|
|
16270
18319
|
const uiuxValidators = [
|
|
16271
18320
|
() => validateDesignToken(root, config),
|
|
@@ -16291,68 +18340,13 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
16291
18340
|
rule: "uiux.performanceBudget"
|
|
16292
18341
|
});
|
|
16293
18342
|
}
|
|
18343
|
+
return uiuxIssues;
|
|
18344
|
+
}
|
|
18345
|
+
async function validatePrototypingSkill(root, config) {
|
|
16294
18346
|
const skillsDir = resolvePath(root, config, "skillsDir");
|
|
16295
|
-
const prototypingSkillPath =
|
|
18347
|
+
const prototypingSkillPath = path78.join(skillsDir, "qfai-prototyping", "SKILL.md");
|
|
16296
18348
|
const prototypingSkillContent = await readSafe(prototypingSkillPath);
|
|
16297
|
-
|
|
16298
|
-
const findings = [
|
|
16299
|
-
...configIssues,
|
|
16300
|
-
...await validateRepositoryHygiene(root, config),
|
|
16301
|
-
...await validateSkillsIntegrity(root, config),
|
|
16302
|
-
...await validateAssistantAssets(root, config),
|
|
16303
|
-
...await validateDiscussionMermaid(root),
|
|
16304
|
-
...await validateMermaidEnforcement(root),
|
|
16305
|
-
...await validateSpecPacks(root, config),
|
|
16306
|
-
...await validateDiscussionPackReadiness(root, config),
|
|
16307
|
-
...await validateDiscussionVisuals(root),
|
|
16308
|
-
...await validateStatusInSpecs(root, config),
|
|
16309
|
-
...await validateDensityHints(root, config),
|
|
16310
|
-
...await validateReviewArtifacts(root),
|
|
16311
|
-
...await validatePrototypingEvidence(root, config),
|
|
16312
|
-
...await validateBreakthroughEvidence(root, config),
|
|
16313
|
-
...await validatePrototypingDesignSystem(root, config),
|
|
16314
|
-
...await validateUiEvidenceArtifacts(root, config),
|
|
16315
|
-
...await validateSpecSplitByCapability(root, config),
|
|
16316
|
-
...await validateLayeredTraceability(root, config),
|
|
16317
|
-
...await validateOrphanProhibition(root, config),
|
|
16318
|
-
...await validateLayerCoverage(root, config),
|
|
16319
|
-
...atddCodeTraceabilityIssues,
|
|
16320
|
-
...await validateContractReferences(root, config),
|
|
16321
|
-
...await validateTraceability(root, config, phase),
|
|
16322
|
-
...await validateDefinedIds(root, config),
|
|
16323
|
-
...await validateContracts(root, config),
|
|
16324
|
-
...await validateTddList(root, config),
|
|
16325
|
-
...await validateDiscussionDesignHardening(root, config),
|
|
16326
|
-
...await validateNavigationFlow(root, config),
|
|
16327
|
-
...await validateRenderCritique(root, config),
|
|
16328
|
-
...await validateDesignFidelity(root, config),
|
|
16329
|
-
...await validateDesignContractReadiness(root, config),
|
|
16330
|
-
...await validateTraceabilityIntegrity(root, config),
|
|
16331
|
-
...prototypingSkillResult.issues,
|
|
16332
|
-
...uiuxIssues
|
|
16333
|
-
];
|
|
16334
|
-
const { issues, waivers } = await applyWaivers(root, findings);
|
|
16335
|
-
const specsRoot = resolvePath(root, config, "specsDir");
|
|
16336
|
-
const scenarioFiles = await collectScenarioFiles(specsRoot);
|
|
16337
|
-
const scIds = await collectScIdsFromScenarioFiles(scenarioFiles);
|
|
16338
|
-
const { refs: scTestRefs, scan: testFiles } = await collectScTestReferences(
|
|
16339
|
-
root,
|
|
16340
|
-
config.validation.traceability.testFileGlobs,
|
|
16341
|
-
config.validation.traceability.testFileExcludeGlobs
|
|
16342
|
-
);
|
|
16343
|
-
const scCoverage = buildScCoverage(scIds, scTestRefs);
|
|
16344
|
-
const toolVersion = await resolveToolVersion();
|
|
16345
|
-
return {
|
|
16346
|
-
toolVersion,
|
|
16347
|
-
phase,
|
|
16348
|
-
issues,
|
|
16349
|
-
counts: countIssues(issues),
|
|
16350
|
-
traceability: {
|
|
16351
|
-
sc: scCoverage,
|
|
16352
|
-
testFiles
|
|
16353
|
-
},
|
|
16354
|
-
waivers
|
|
16355
|
-
};
|
|
18349
|
+
return prototypingSkillContent.length > 0 ? validatePrototypingSkillContent(prototypingSkillContent).issues : [];
|
|
16356
18350
|
}
|
|
16357
18351
|
function countIssues(issues) {
|
|
16358
18352
|
return issues.reduce(
|
|
@@ -16368,11 +18362,11 @@ function countIssues(issues) {
|
|
|
16368
18362
|
}
|
|
16369
18363
|
|
|
16370
18364
|
// src/core/uiux/renderEvidence.ts
|
|
16371
|
-
import { readFile as
|
|
18365
|
+
import { readFile as readFile51 } from "fs/promises";
|
|
16372
18366
|
async function readRenderEvidenceBundle(filePath) {
|
|
16373
18367
|
let raw;
|
|
16374
18368
|
try {
|
|
16375
|
-
raw = await
|
|
18369
|
+
raw = await readFile51(filePath, "utf-8");
|
|
16376
18370
|
} catch {
|
|
16377
18371
|
return null;
|
|
16378
18372
|
}
|
|
@@ -16382,9 +18376,9 @@ async function readRenderEvidenceBundle(filePath) {
|
|
|
16382
18376
|
} catch {
|
|
16383
18377
|
return null;
|
|
16384
18378
|
}
|
|
16385
|
-
return
|
|
18379
|
+
return isRecord17(parsed) ? parsed : null;
|
|
16386
18380
|
}
|
|
16387
|
-
function
|
|
18381
|
+
function isRecord17(value) {
|
|
16388
18382
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16389
18383
|
}
|
|
16390
18384
|
function summarizeRenderEvidence(bundle) {
|
|
@@ -16392,7 +18386,7 @@ function summarizeRenderEvidence(bundle) {
|
|
|
16392
18386
|
let inlinePayloadViolation = false;
|
|
16393
18387
|
if (bundle?.screens) {
|
|
16394
18388
|
for (const screen of bundle.screens) {
|
|
16395
|
-
if (
|
|
18389
|
+
if (isRecord17(screen)) {
|
|
16396
18390
|
const s = screen;
|
|
16397
18391
|
if (s.status === "captured" || s.status === "skipped" || s.status === "failed") {
|
|
16398
18392
|
counts[s.status] += 1;
|
|
@@ -16417,7 +18411,7 @@ function summarizeRenderEvidence(bundle) {
|
|
|
16417
18411
|
}
|
|
16418
18412
|
|
|
16419
18413
|
// src/core/browserQa/index.ts
|
|
16420
|
-
import { readFile as
|
|
18414
|
+
import { readFile as readFile52 } from "fs/promises";
|
|
16421
18415
|
|
|
16422
18416
|
// src/core/browserQa/types.ts
|
|
16423
18417
|
var BROWSER_QA_PHASES = [
|
|
@@ -16980,7 +18974,7 @@ function summarizeBrowserQaResult(result) {
|
|
|
16980
18974
|
async function readBrowserQaBundle(filePath) {
|
|
16981
18975
|
let raw;
|
|
16982
18976
|
try {
|
|
16983
|
-
raw = await
|
|
18977
|
+
raw = await readFile52(filePath, "utf-8");
|
|
16984
18978
|
} catch {
|
|
16985
18979
|
return null;
|
|
16986
18980
|
}
|
|
@@ -16990,9 +18984,9 @@ async function readBrowserQaBundle(filePath) {
|
|
|
16990
18984
|
} catch {
|
|
16991
18985
|
return null;
|
|
16992
18986
|
}
|
|
16993
|
-
return
|
|
18987
|
+
return isRecord18(parsed) ? parsed : null;
|
|
16994
18988
|
}
|
|
16995
|
-
function
|
|
18989
|
+
function isRecord18(value) {
|
|
16996
18990
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
16997
18991
|
}
|
|
16998
18992
|
|
|
@@ -17001,15 +18995,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
17001
18995
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
17002
18996
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
17003
18997
|
async function createReportData(root, validation, configResult) {
|
|
17004
|
-
const resolvedRoot =
|
|
18998
|
+
const resolvedRoot = path79.resolve(root);
|
|
17005
18999
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
17006
19000
|
const config = resolved.config;
|
|
17007
19001
|
const configPath = resolved.configPath;
|
|
17008
19002
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
17009
19003
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
17010
|
-
const apiRoot =
|
|
17011
|
-
const uiRoot =
|
|
17012
|
-
const dbRoot =
|
|
19004
|
+
const apiRoot = path79.join(contractsRoot, "api");
|
|
19005
|
+
const uiRoot = path79.join(contractsRoot, "ui");
|
|
19006
|
+
const dbRoot = path79.join(contractsRoot, "db");
|
|
17013
19007
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
17014
19008
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
17015
19009
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -17806,6 +19800,28 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
17806
19800
|
);
|
|
17807
19801
|
lines.push("");
|
|
17808
19802
|
}
|
|
19803
|
+
if (data.prototyping.roundLifecycle) {
|
|
19804
|
+
const lifecycle = data.prototyping.roundLifecycle;
|
|
19805
|
+
lines.push("### prototyping.roundLifecycle");
|
|
19806
|
+
lines.push("");
|
|
19807
|
+
lines.push(`- schemaVersion: ${lifecycle.schemaVersion}`);
|
|
19808
|
+
lines.push(`- rounds: ${lifecycle.rounds}`);
|
|
19809
|
+
lines.push(`- round ids: ${lifecycle.roundIds.join(", ") || "(none)"}`);
|
|
19810
|
+
lines.push(`- candidates observed: ${lifecycle.candidatesObserved}`);
|
|
19811
|
+
lines.push(`- harvest artifacts: ${lifecycle.harvestArtifacts}`);
|
|
19812
|
+
lines.push(`- narrow decisions: ${lifecycle.narrowDecisions}`);
|
|
19813
|
+
lines.push(`- absorption plans: ${lifecycle.absorptionPlans}`);
|
|
19814
|
+
lines.push(`- reimplementations: ${lifecycle.reimplementations}`);
|
|
19815
|
+
lines.push(`- perfect rounds: ${lifecycle.perfectRounds}`);
|
|
19816
|
+
lines.push(`- polish cycles: ${lifecycle.polishCycles}`);
|
|
19817
|
+
lines.push(`- completed polish cycles: ${lifecycle.completedPolishCycles}`);
|
|
19818
|
+
lines.push(`- perfect polish cycles: ${lifecycle.perfectPolishCycles}`);
|
|
19819
|
+
if (Object.keys(lifecycle.polishKinds).length > 0) {
|
|
19820
|
+
const kinds = Object.entries(lifecycle.polishKinds).map(([kind, count]) => `${kind}=${count}`).join(", ");
|
|
19821
|
+
lines.push(`- polish kinds: ${kinds}`);
|
|
19822
|
+
}
|
|
19823
|
+
lines.push("");
|
|
19824
|
+
}
|
|
17809
19825
|
lines.push("### prototyping.mode");
|
|
17810
19826
|
lines.push("");
|
|
17811
19827
|
lines.push(`- requested: ${data.prototyping.mode.requested ?? "(none)"}`);
|
|
@@ -18162,7 +20178,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
18162
20178
|
};
|
|
18163
20179
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
18164
20180
|
for (const deltaFile of deltaFiles) {
|
|
18165
|
-
const text = await
|
|
20181
|
+
const text = await readFile53(deltaFile, "utf-8");
|
|
18166
20182
|
const parsed = parseDeltaV1(text);
|
|
18167
20183
|
for (const entry of parsed.entries) {
|
|
18168
20184
|
if (!entry.meta) {
|
|
@@ -18191,13 +20207,46 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
18191
20207
|
}
|
|
18192
20208
|
return summary;
|
|
18193
20209
|
}
|
|
20210
|
+
var PROTOTYPING_ROUND_DIR_NAMES = new Set(EXPLORATION_ROUNDS);
|
|
20211
|
+
async function scanPrototypingRoundsFilesystem(evidenceRoot) {
|
|
20212
|
+
const result = {
|
|
20213
|
+
observedRoundIds: [],
|
|
20214
|
+
harvestArtifacts: 0,
|
|
20215
|
+
narrowDecisions: 0,
|
|
20216
|
+
absorptionPlans: 0,
|
|
20217
|
+
reimplementations: 0
|
|
20218
|
+
};
|
|
20219
|
+
const roundsDir = path79.join(evidenceRoot, "prototyping", "rounds");
|
|
20220
|
+
const { readdir: readdir14 } = await import("fs/promises");
|
|
20221
|
+
let entries;
|
|
20222
|
+
try {
|
|
20223
|
+
entries = await readdir14(roundsDir);
|
|
20224
|
+
} catch {
|
|
20225
|
+
return result;
|
|
20226
|
+
}
|
|
20227
|
+
for (const name of entries.sort()) {
|
|
20228
|
+
if (!PROTOTYPING_ROUND_DIR_NAMES.has(name)) continue;
|
|
20229
|
+
let files;
|
|
20230
|
+
try {
|
|
20231
|
+
files = await readdir14(path79.join(roundsDir, name));
|
|
20232
|
+
} catch {
|
|
20233
|
+
continue;
|
|
20234
|
+
}
|
|
20235
|
+
result.observedRoundIds.push(name);
|
|
20236
|
+
if (files.includes("harvest.json")) result.harvestArtifacts += 1;
|
|
20237
|
+
if (files.includes("narrow-decision.json")) result.narrowDecisions += 1;
|
|
20238
|
+
if (files.includes("absorption-plan.json")) result.absorptionPlans += 1;
|
|
20239
|
+
if (files.includes("reimplementation.json")) result.reimplementations += 1;
|
|
20240
|
+
}
|
|
20241
|
+
return result;
|
|
20242
|
+
}
|
|
18194
20243
|
async function collectPrototypingSummary(root, config) {
|
|
18195
20244
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
18196
|
-
const evidenceRoot =
|
|
18197
|
-
const evidencePath =
|
|
20245
|
+
const evidenceRoot = path79.join(path79.dirname(specsRoot), "evidence");
|
|
20246
|
+
const evidencePath = path79.join(evidenceRoot, "prototyping.json");
|
|
18198
20247
|
let raw;
|
|
18199
20248
|
try {
|
|
18200
|
-
raw = await
|
|
20249
|
+
raw = await readFile53(evidencePath, "utf-8");
|
|
18201
20250
|
} catch {
|
|
18202
20251
|
return void 0;
|
|
18203
20252
|
}
|
|
@@ -18224,10 +20273,12 @@ async function collectPrototypingSummary(root, config) {
|
|
|
18224
20273
|
const warnings = [];
|
|
18225
20274
|
const obligations = derivePrototypingObligations({ surface: effectiveSurface, effectiveMode });
|
|
18226
20275
|
const fullHarness = asRecord2(record2.fullHarness);
|
|
20276
|
+
const rounds = Array.isArray(record2.rounds) ? record2.rounds : [];
|
|
20277
|
+
const polishCycles = Array.isArray(record2.polishCycles) ? record2.polishCycles : [];
|
|
18227
20278
|
const runtimeGate = asRecord2(record2.runtimeGate);
|
|
18228
20279
|
const uiFidelity = asRecord2(record2.uiFidelity);
|
|
18229
|
-
const renderBundle = await readRenderEvidenceBundle(
|
|
18230
|
-
const browserQaBundle = await readBrowserQaBundle(
|
|
20280
|
+
const renderBundle = await readRenderEvidenceBundle(path79.join(evidenceRoot, "render.json"));
|
|
20281
|
+
const browserQaBundle = await readBrowserQaBundle(path79.join(evidenceRoot, "browser-qa.json"));
|
|
18231
20282
|
const specs = Array.isArray(record2.specs) ? record2.specs : [];
|
|
18232
20283
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
18233
20284
|
const expectedSpecIds = specEntries.map((entry) => `spec-${entry.specNumber}`.toLowerCase());
|
|
@@ -18284,8 +20335,81 @@ async function collectPrototypingSummary(root, config) {
|
|
|
18284
20335
|
...classificationBlock?.secondary_surfaces?.length ? { secondarySurfaces: classificationBlock.secondary_surfaces } : {},
|
|
18285
20336
|
...classificationBlock?.classification_rationale ? { classificationRationale: classificationBlock.classification_rationale } : {}
|
|
18286
20337
|
};
|
|
20338
|
+
const fsRoundScan = await scanPrototypingRoundsFilesystem(evidenceRoot);
|
|
20339
|
+
const roundLifecycle = record2.schemaVersion === "2.0" || rounds.length > 0 || polishCycles.length > 0 || fsRoundScan.observedRoundIds.length > 0 ? (() => {
|
|
20340
|
+
const polishKinds = {};
|
|
20341
|
+
let candidatesObserved = 0;
|
|
20342
|
+
let perfectRounds = 0;
|
|
20343
|
+
let completedPolishCycles = 0;
|
|
20344
|
+
let perfectPolishCycles = 0;
|
|
20345
|
+
for (const item of rounds) {
|
|
20346
|
+
const round = asRecord2(item);
|
|
20347
|
+
if (!round) {
|
|
20348
|
+
continue;
|
|
20349
|
+
}
|
|
20350
|
+
const candidates = Array.isArray(round.candidates) ? round.candidates : [];
|
|
20351
|
+
candidatesObserved += candidates.length;
|
|
20352
|
+
if (round.allAxesPerfect100 === true) {
|
|
20353
|
+
perfectRounds += 1;
|
|
20354
|
+
}
|
|
20355
|
+
}
|
|
20356
|
+
for (const item of polishCycles) {
|
|
20357
|
+
const cycle = asRecord2(item);
|
|
20358
|
+
if (!cycle) {
|
|
20359
|
+
continue;
|
|
20360
|
+
}
|
|
20361
|
+
if (typeof cycle.kind === "string" && cycle.kind.length > 0) {
|
|
20362
|
+
polishKinds[cycle.kind] = (polishKinds[cycle.kind] ?? 0) + 1;
|
|
20363
|
+
if (cycle.kind === "completed") {
|
|
20364
|
+
completedPolishCycles += 1;
|
|
20365
|
+
}
|
|
20366
|
+
}
|
|
20367
|
+
if (cycle.allAxesPerfect100 === true) {
|
|
20368
|
+
perfectPolishCycles += 1;
|
|
20369
|
+
}
|
|
20370
|
+
}
|
|
20371
|
+
const indexAbsorptionRefs = rounds.map((item) => asRecord2(item)).filter(
|
|
20372
|
+
(round) => round && typeof round.absorptionPlanRef === "string" && round.absorptionPlanRef.length > 0
|
|
20373
|
+
).length;
|
|
20374
|
+
if (indexAbsorptionRefs !== fsRoundScan.absorptionPlans) {
|
|
20375
|
+
warnings.push(
|
|
20376
|
+
`prototyping.json index references ${indexAbsorptionRefs} absorption-plan(s) but filesystem has ${fsRoundScan.absorptionPlans}; counts use filesystem (canonical SoT).`
|
|
20377
|
+
);
|
|
20378
|
+
}
|
|
20379
|
+
const indexReimplRefs = rounds.map((item) => asRecord2(item)).filter(
|
|
20380
|
+
(round) => round && typeof round.reimplementationRef === "string" && round.reimplementationRef.length > 0
|
|
20381
|
+
).length;
|
|
20382
|
+
if (indexReimplRefs !== fsRoundScan.reimplementations) {
|
|
20383
|
+
warnings.push(
|
|
20384
|
+
`prototyping.json index references ${indexReimplRefs} reimplementation record(s) but filesystem has ${fsRoundScan.reimplementations}; counts use filesystem (canonical SoT).`
|
|
20385
|
+
);
|
|
20386
|
+
}
|
|
20387
|
+
const indexRoundIds = rounds.map((item) => asRecord2(item)).flatMap(
|
|
20388
|
+
(round) => round && typeof round.round === "string" && round.round.length > 0 ? [round.round] : []
|
|
20389
|
+
);
|
|
20390
|
+
const roundIdSet = /* @__PURE__ */ new Set([...indexRoundIds, ...fsRoundScan.observedRoundIds]);
|
|
20391
|
+
return {
|
|
20392
|
+
schemaVersion: "2.0",
|
|
20393
|
+
// Use the union size — Math.max underestimates when index and
|
|
20394
|
+
// filesystem describe disjoint round IDs (Copilot MAJOR review
|
|
20395
|
+
// on PR #201).
|
|
20396
|
+
rounds: roundIdSet.size,
|
|
20397
|
+
roundIds: Array.from(roundIdSet),
|
|
20398
|
+
candidatesObserved,
|
|
20399
|
+
perfectRounds,
|
|
20400
|
+
harvestArtifacts: fsRoundScan.harvestArtifacts,
|
|
20401
|
+
narrowDecisions: fsRoundScan.narrowDecisions,
|
|
20402
|
+
absorptionPlans: fsRoundScan.absorptionPlans,
|
|
20403
|
+
reimplementations: fsRoundScan.reimplementations,
|
|
20404
|
+
polishCycles: polishCycles.length,
|
|
20405
|
+
completedPolishCycles,
|
|
20406
|
+
perfectPolishCycles,
|
|
20407
|
+
polishKinds
|
|
20408
|
+
};
|
|
20409
|
+
})() : void 0;
|
|
18287
20410
|
return {
|
|
18288
20411
|
surfaceClassification,
|
|
20412
|
+
...roundLifecycle ? { roundLifecycle } : {},
|
|
18289
20413
|
mode: {
|
|
18290
20414
|
...resolvedMode.requested ? { requested: resolvedMode.requested } : {},
|
|
18291
20415
|
effective: effectiveMode,
|
|
@@ -18378,7 +20502,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
18378
20502
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
18379
20503
|
}
|
|
18380
20504
|
for (const file of specFiles) {
|
|
18381
|
-
const text = await
|
|
20505
|
+
const text = await readFile53(file, "utf-8");
|
|
18382
20506
|
const parsed = parseSpec(text, file);
|
|
18383
20507
|
const specKey = parsed.specId;
|
|
18384
20508
|
if (!specKey) {
|
|
@@ -18415,7 +20539,7 @@ async function collectIds(files) {
|
|
|
18415
20539
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
18416
20540
|
}
|
|
18417
20541
|
for (const file of files) {
|
|
18418
|
-
const text = await
|
|
20542
|
+
const text = await readFile53(file, "utf-8");
|
|
18419
20543
|
for (const prefix of ID_PREFIXES) {
|
|
18420
20544
|
const ids = extractIds(text, prefix);
|
|
18421
20545
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -18430,7 +20554,7 @@ async function collectIds(files) {
|
|
|
18430
20554
|
async function collectUpstreamIds(files) {
|
|
18431
20555
|
const ids = /* @__PURE__ */ new Set();
|
|
18432
20556
|
for (const file of files) {
|
|
18433
|
-
const text = await
|
|
20557
|
+
const text = await readFile53(file, "utf-8");
|
|
18434
20558
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
18435
20559
|
}
|
|
18436
20560
|
return ids;
|
|
@@ -18451,7 +20575,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
18451
20575
|
}
|
|
18452
20576
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
18453
20577
|
for (const file of targetFiles) {
|
|
18454
|
-
const text = await
|
|
20578
|
+
const text = await readFile53(file, "utf-8");
|
|
18455
20579
|
if (pattern.test(text)) {
|
|
18456
20580
|
return true;
|
|
18457
20581
|
}
|
|
@@ -18573,7 +20697,7 @@ function normalizeScSources(root, sources) {
|
|
|
18573
20697
|
async function countScenarios(scenarioFiles) {
|
|
18574
20698
|
let total = 0;
|
|
18575
20699
|
for (const file of scenarioFiles) {
|
|
18576
|
-
const text = await
|
|
20700
|
+
const text = await readFile53(file, "utf-8");
|
|
18577
20701
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
18578
20702
|
if (!document || errors.length > 0) {
|
|
18579
20703
|
continue;
|
|
@@ -18604,7 +20728,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
18604
20728
|
let totalScenarios = 0;
|
|
18605
20729
|
let e2eCount = 0;
|
|
18606
20730
|
for (const file of scenarioFiles) {
|
|
18607
|
-
const text = await
|
|
20731
|
+
const text = await readFile53(file, "utf-8");
|
|
18608
20732
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
18609
20733
|
if (!document || errors.length > 0) {
|
|
18610
20734
|
continue;
|
|
@@ -18692,10 +20816,10 @@ function buildHotspots(issues) {
|
|
|
18692
20816
|
async function collectTddCoverage(entries) {
|
|
18693
20817
|
const specs = [];
|
|
18694
20818
|
for (const entry of entries) {
|
|
18695
|
-
const testCasesPath =
|
|
20819
|
+
const testCasesPath = path79.join(entry.dir, "06_Test-Cases.md");
|
|
18696
20820
|
let tcContent;
|
|
18697
20821
|
try {
|
|
18698
|
-
tcContent = await
|
|
20822
|
+
tcContent = await readFile53(testCasesPath, "utf-8");
|
|
18699
20823
|
} catch {
|
|
18700
20824
|
continue;
|
|
18701
20825
|
}
|
|
@@ -18727,10 +20851,10 @@ async function collectTddCoverage(entries) {
|
|
|
18727
20851
|
});
|
|
18728
20852
|
continue;
|
|
18729
20853
|
}
|
|
18730
|
-
const tddListPath =
|
|
20854
|
+
const tddListPath = path79.join(entry.dir, "tdd", "test-list.md");
|
|
18731
20855
|
let tddContent;
|
|
18732
20856
|
try {
|
|
18733
|
-
tddContent = await
|
|
20857
|
+
tddContent = await readFile53(tddListPath, "utf-8");
|
|
18734
20858
|
} catch {
|
|
18735
20859
|
specs.push({
|
|
18736
20860
|
specNumber: entry.specNumber,
|
|
@@ -18881,16 +21005,16 @@ async function runRenderCapture(targets, outputDir, adapter, options) {
|
|
|
18881
21005
|
}
|
|
18882
21006
|
|
|
18883
21007
|
// src/core/evidence/bundleWriter.ts
|
|
18884
|
-
import { mkdir as
|
|
18885
|
-
import
|
|
21008
|
+
import { mkdir as mkdir6 } from "fs/promises";
|
|
21009
|
+
import path81 from "path";
|
|
18886
21010
|
|
|
18887
21011
|
// src/core/evidence/fsEvidenceWriter.ts
|
|
18888
|
-
import { writeFile as
|
|
18889
|
-
import
|
|
21012
|
+
import { writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
21013
|
+
import path80 from "path";
|
|
18890
21014
|
async function writeEvidenceFile(filePath, content) {
|
|
18891
21015
|
try {
|
|
18892
|
-
await
|
|
18893
|
-
await
|
|
21016
|
+
await mkdir5(path80.dirname(filePath), { recursive: true });
|
|
21017
|
+
await writeFile5(filePath, content);
|
|
18894
21018
|
return { path: filePath, written: true };
|
|
18895
21019
|
} catch (error) {
|
|
18896
21020
|
return {
|
|
@@ -18902,20 +21026,20 @@ async function writeEvidenceFile(filePath, content) {
|
|
|
18902
21026
|
}
|
|
18903
21027
|
|
|
18904
21028
|
// src/core/evidence/bundleWriter.ts
|
|
18905
|
-
function
|
|
18906
|
-
return
|
|
21029
|
+
function toPosixRelative6(root, targetPath) {
|
|
21030
|
+
return path81.relative(root, targetPath).replace(/\\/g, "/");
|
|
18907
21031
|
}
|
|
18908
21032
|
async function writeEvidenceBundles(input) {
|
|
18909
|
-
const evidenceRoot =
|
|
18910
|
-
const renderAssetRoot =
|
|
18911
|
-
await
|
|
18912
|
-
const prototypingPath =
|
|
18913
|
-
const renderPath =
|
|
18914
|
-
const browserQaPath =
|
|
18915
|
-
const browserQaSummaryPath =
|
|
18916
|
-
const browserQaFindingsPath =
|
|
18917
|
-
const browserQaRepairsPath =
|
|
18918
|
-
const breakthroughPath =
|
|
21033
|
+
const evidenceRoot = path81.join(input.root, ".qfai", "evidence");
|
|
21034
|
+
const renderAssetRoot = path81.join(evidenceRoot, "render");
|
|
21035
|
+
await mkdir6(renderAssetRoot, { recursive: true });
|
|
21036
|
+
const prototypingPath = path81.join(evidenceRoot, "prototyping.json");
|
|
21037
|
+
const renderPath = path81.join(evidenceRoot, "render.json");
|
|
21038
|
+
const browserQaPath = path81.join(evidenceRoot, "browser-qa.json");
|
|
21039
|
+
const browserQaSummaryPath = path81.join(evidenceRoot, "browserQa.summary.json");
|
|
21040
|
+
const browserQaFindingsPath = path81.join(evidenceRoot, "browserQa.findings.json");
|
|
21041
|
+
const browserQaRepairsPath = path81.join(evidenceRoot, "browserQa.repairs.json");
|
|
21042
|
+
const breakthroughPath = path81.join(evidenceRoot, "breakthrough.json");
|
|
18919
21043
|
const renderBundle = input.render === void 0 ? {
|
|
18920
21044
|
renderEvidence: {
|
|
18921
21045
|
status: "skipped",
|
|
@@ -18937,7 +21061,7 @@ async function writeEvidenceBundles(input) {
|
|
|
18937
21061
|
await Promise.all([
|
|
18938
21062
|
writeEvidenceFile(prototypingPath, JSON.stringify(input.prototyping, null, 2)),
|
|
18939
21063
|
writeEvidenceFile(
|
|
18940
|
-
|
|
21064
|
+
path81.join(evidenceRoot, "prototyping.md"),
|
|
18941
21065
|
buildPrototypingMarkdown(input.prototyping)
|
|
18942
21066
|
),
|
|
18943
21067
|
writeEvidenceFile(renderPath, JSON.stringify(renderBundle, null, 2)),
|
|
@@ -18954,15 +21078,15 @@ async function writeEvidenceBundles(input) {
|
|
|
18954
21078
|
writeEvidenceFile(breakthroughPath, JSON.stringify(input.prototyping.breakthrough, null, 2)),
|
|
18955
21079
|
...input.fullHarnessArtifacts ? [
|
|
18956
21080
|
writeEvidenceFile(
|
|
18957
|
-
|
|
21081
|
+
path81.join(evidenceRoot, "fullHarness.fakeUiDetection.json"),
|
|
18958
21082
|
JSON.stringify(input.fullHarnessArtifacts.fakeUiDetection, null, 2)
|
|
18959
21083
|
),
|
|
18960
21084
|
writeEvidenceFile(
|
|
18961
|
-
|
|
21085
|
+
path81.join(evidenceRoot, "fullHarness.handoff.json"),
|
|
18962
21086
|
JSON.stringify(input.fullHarnessArtifacts.handoff, null, 2)
|
|
18963
21087
|
),
|
|
18964
21088
|
writeEvidenceFile(
|
|
18965
|
-
|
|
21089
|
+
path81.join(evidenceRoot, "fullHarness.exit.json"),
|
|
18966
21090
|
JSON.stringify({ exit_reason: input.fullHarnessArtifacts.exitReason }, null, 2)
|
|
18967
21091
|
)
|
|
18968
21092
|
] : []
|
|
@@ -18976,8 +21100,8 @@ function buildRenderBundle(root, input) {
|
|
|
18976
21100
|
status: entry.status,
|
|
18977
21101
|
width: 1440,
|
|
18978
21102
|
height: 900,
|
|
18979
|
-
...entry.screenshot_path ? { imagePath:
|
|
18980
|
-
...entry.html_path ? { htmlPath:
|
|
21103
|
+
...entry.screenshot_path ? { imagePath: toPosixRelative6(root, entry.screenshot_path) } : {},
|
|
21104
|
+
...entry.html_path ? { htmlPath: toPosixRelative6(root, entry.html_path) } : {},
|
|
18981
21105
|
...entry.reason ? entry.status === "failed" ? { error: entry.reason } : { skippedReason: entry.reason } : {}
|
|
18982
21106
|
}));
|
|
18983
21107
|
const topStatus = screens.some((screen) => screen.status === "captured") ? "captured" : screens.some((screen) => screen.status === "failed") ? "failed" : "skipped";
|
|
@@ -19062,15 +21186,15 @@ function buildPrototypingMarkdown(bundle) {
|
|
|
19062
21186
|
}
|
|
19063
21187
|
|
|
19064
21188
|
// src/core/harness/history.ts
|
|
19065
|
-
import { readFile as
|
|
19066
|
-
import
|
|
21189
|
+
import { readFile as readFile54 } from "fs/promises";
|
|
21190
|
+
import path82 from "path";
|
|
19067
21191
|
var EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
|
|
19068
21192
|
async function loadHistory(root) {
|
|
19069
21193
|
try {
|
|
19070
|
-
const filePath =
|
|
19071
|
-
const content = await
|
|
21194
|
+
const filePath = path82.join(root, EVIDENCE_PATH);
|
|
21195
|
+
const content = await readFile54(filePath, "utf-8");
|
|
19072
21196
|
const parsed = JSON.parse(content);
|
|
19073
|
-
if (
|
|
21197
|
+
if (isRecord19(parsed) && isRecord19(parsed.fullHarness)) {
|
|
19074
21198
|
const fh = parsed.fullHarness;
|
|
19075
21199
|
if (Array.isArray(fh.iterations)) {
|
|
19076
21200
|
return {
|
|
@@ -19119,7 +21243,7 @@ function computeTerminationReason(history, iterationPolicy) {
|
|
|
19119
21243
|
return void 0;
|
|
19120
21244
|
}
|
|
19121
21245
|
const latestIteration = history.iterations[count - 1];
|
|
19122
|
-
if (latestIteration
|
|
21246
|
+
if (latestIteration && hasAllReviewerAxesPerfect100(latestIteration.reviewerScores)) {
|
|
19123
21247
|
return "converged";
|
|
19124
21248
|
}
|
|
19125
21249
|
if (count >= iterationPolicy.maxIterations) {
|
|
@@ -19131,6 +21255,7 @@ function buildScoreSnapshot(iteration) {
|
|
|
19131
21255
|
const axisScores = iteration.reviewerScores.flatMap(
|
|
19132
21256
|
(reviewer) => reviewer.scores.map((score) => score.score)
|
|
19133
21257
|
);
|
|
21258
|
+
const allReviewerAxesPerfect100 = hasAllReviewerAxesPerfect100(iteration.reviewerScores);
|
|
19134
21259
|
if (axisScores.length === 0) {
|
|
19135
21260
|
return {
|
|
19136
21261
|
iteration: iteration.iteration,
|
|
@@ -19138,7 +21263,7 @@ function buildScoreSnapshot(iteration) {
|
|
|
19138
21263
|
axisCount: 0,
|
|
19139
21264
|
minScore: null,
|
|
19140
21265
|
averageScore: null,
|
|
19141
|
-
|
|
21266
|
+
allReviewerAxesPerfect100: false,
|
|
19142
21267
|
commitSha: iteration.commitSha
|
|
19143
21268
|
};
|
|
19144
21269
|
}
|
|
@@ -19149,13 +21274,13 @@ function buildScoreSnapshot(iteration) {
|
|
|
19149
21274
|
axisCount: axisScores.length,
|
|
19150
21275
|
minScore: Math.min(...axisScores),
|
|
19151
21276
|
averageScore: total / axisScores.length,
|
|
19152
|
-
|
|
21277
|
+
allReviewerAxesPerfect100,
|
|
19153
21278
|
commitSha: iteration.commitSha
|
|
19154
21279
|
};
|
|
19155
21280
|
}
|
|
19156
21281
|
function compareSnapshots(left, right) {
|
|
19157
|
-
if (left.
|
|
19158
|
-
return left.
|
|
21282
|
+
if (left.allReviewerAxesPerfect100 !== right.allReviewerAxesPerfect100) {
|
|
21283
|
+
return left.allReviewerAxesPerfect100 ? 1 : -1;
|
|
19159
21284
|
}
|
|
19160
21285
|
const leftMin = left.minScore ?? -1;
|
|
19161
21286
|
const rightMin = right.minScore ?? -1;
|
|
@@ -19169,7 +21294,15 @@ function compareSnapshots(left, right) {
|
|
|
19169
21294
|
}
|
|
19170
21295
|
return left.iteration - right.iteration;
|
|
19171
21296
|
}
|
|
19172
|
-
function
|
|
21297
|
+
function hasAllReviewerAxesPerfect100(reviewerScores) {
|
|
21298
|
+
if (reviewerScores.length === 0) {
|
|
21299
|
+
return false;
|
|
21300
|
+
}
|
|
21301
|
+
return reviewerScores.every(
|
|
21302
|
+
(reviewer) => reviewer.scores.length > 0 && reviewer.scores.every((score) => score.score === 100)
|
|
21303
|
+
);
|
|
21304
|
+
}
|
|
21305
|
+
function isRecord19(value) {
|
|
19173
21306
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19174
21307
|
}
|
|
19175
21308
|
|
|
@@ -19204,16 +21337,16 @@ function validateReviewer(reviewer) {
|
|
|
19204
21337
|
}
|
|
19205
21338
|
|
|
19206
21339
|
// src/core/harness/gitRevision.ts
|
|
19207
|
-
import { readFile as
|
|
19208
|
-
import
|
|
21340
|
+
import { readFile as readFile55, stat as stat11 } from "fs/promises";
|
|
21341
|
+
import path83 from "path";
|
|
19209
21342
|
async function resolveGitDirs(root) {
|
|
19210
|
-
const dotGit =
|
|
19211
|
-
const info = await
|
|
21343
|
+
const dotGit = path83.join(root, ".git");
|
|
21344
|
+
const info = await stat11(dotGit);
|
|
19212
21345
|
const gitDir = info.isDirectory() ? dotGit : await resolveWorktreeGitDir(root, dotGit);
|
|
19213
21346
|
try {
|
|
19214
|
-
const commondirText = (await
|
|
21347
|
+
const commondirText = (await readFile55(path83.join(gitDir, "commondir"), "utf-8")).trim();
|
|
19215
21348
|
if (commondirText.length > 0) {
|
|
19216
|
-
const commonDir =
|
|
21349
|
+
const commonDir = path83.isAbsolute(commondirText) ? commondirText : path83.resolve(gitDir, commondirText);
|
|
19217
21350
|
return { gitDir, commonDir };
|
|
19218
21351
|
}
|
|
19219
21352
|
} catch {
|
|
@@ -19221,18 +21354,18 @@ async function resolveGitDirs(root) {
|
|
|
19221
21354
|
return { gitDir, commonDir: gitDir };
|
|
19222
21355
|
}
|
|
19223
21356
|
async function resolveWorktreeGitDir(root, dotGitFile) {
|
|
19224
|
-
const text = (await
|
|
21357
|
+
const text = (await readFile55(dotGitFile, "utf-8")).trim();
|
|
19225
21358
|
const match = /^gitdir:\s*(.+)$/m.exec(text);
|
|
19226
21359
|
if (!match?.[1]) {
|
|
19227
21360
|
throw new Error(`Unrecognized .git file format at ${dotGitFile}`);
|
|
19228
21361
|
}
|
|
19229
21362
|
const gitdirValue = match[1].trim();
|
|
19230
|
-
return
|
|
21363
|
+
return path83.isAbsolute(gitdirValue) ? gitdirValue : path83.resolve(root, gitdirValue);
|
|
19231
21364
|
}
|
|
19232
21365
|
async function lookupPackedRef(commonDir, refName) {
|
|
19233
21366
|
try {
|
|
19234
|
-
const packedPath =
|
|
19235
|
-
const content = await
|
|
21367
|
+
const packedPath = path83.join(commonDir, "packed-refs");
|
|
21368
|
+
const content = await readFile55(packedPath, "utf-8");
|
|
19236
21369
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
19237
21370
|
const line = rawLine.trim();
|
|
19238
21371
|
if (line.length === 0 || line.startsWith("#") || line.startsWith("^")) continue;
|
|
@@ -19248,7 +21381,7 @@ async function lookupPackedRef(commonDir, refName) {
|
|
|
19248
21381
|
}
|
|
19249
21382
|
async function readRefLoose(dir, refName) {
|
|
19250
21383
|
try {
|
|
19251
|
-
const sha = (await
|
|
21384
|
+
const sha = (await readFile55(path83.join(dir, refName), "utf-8")).trim();
|
|
19252
21385
|
return sha.length > 0 ? sha : null;
|
|
19253
21386
|
} catch {
|
|
19254
21387
|
return null;
|
|
@@ -19257,8 +21390,8 @@ async function readRefLoose(dir, refName) {
|
|
|
19257
21390
|
async function resolveCommitSha(root) {
|
|
19258
21391
|
try {
|
|
19259
21392
|
const { gitDir, commonDir } = await resolveGitDirs(root);
|
|
19260
|
-
const headPath =
|
|
19261
|
-
const headContent = (await
|
|
21393
|
+
const headPath = path83.join(gitDir, "HEAD");
|
|
21394
|
+
const headContent = (await readFile55(headPath, "utf-8")).trim();
|
|
19262
21395
|
if (headContent.startsWith("ref: ")) {
|
|
19263
21396
|
const refName = headContent.slice(5).trim();
|
|
19264
21397
|
const looseLocal = await readRefLoose(gitDir, refName);
|
|
@@ -19279,168 +21412,13 @@ async function resolveCommitSha(root) {
|
|
|
19279
21412
|
);
|
|
19280
21413
|
}
|
|
19281
21414
|
}
|
|
19282
|
-
|
|
19283
|
-
// src/core/evidence/playwrightRenderAdapter.ts
|
|
19284
|
-
import { mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
|
|
19285
|
-
import path75 from "path";
|
|
19286
|
-
function sanitizeName(value) {
|
|
19287
|
-
return value.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
19288
|
-
}
|
|
19289
|
-
async function loadPlaywright() {
|
|
19290
|
-
return import("playwright");
|
|
19291
|
-
}
|
|
19292
|
-
function createPlaywrightRenderAdapter(input) {
|
|
19293
|
-
async function _renderTarget(target) {
|
|
19294
|
-
if (!input.targetUrl) {
|
|
19295
|
-
throw new Error("target URL is required for Playwright render capture");
|
|
19296
|
-
}
|
|
19297
|
-
const playwright = await loadPlaywright();
|
|
19298
|
-
const browser = await playwright.chromium.launch();
|
|
19299
|
-
try {
|
|
19300
|
-
const page = await browser.newPage({
|
|
19301
|
-
viewport: {
|
|
19302
|
-
width: target.width ?? 1440,
|
|
19303
|
-
height: target.height ?? 900
|
|
19304
|
-
}
|
|
19305
|
-
});
|
|
19306
|
-
const resolvedUrl = new URL(target.route ?? "/", input.targetUrl).toString();
|
|
19307
|
-
await page.goto(resolvedUrl, { waitUntil: "networkidle" });
|
|
19308
|
-
const slug = sanitizeName(`${target.targetId}.${target.viewport}`);
|
|
19309
|
-
const screenshotPath = path75.join(process.cwd(), ".", slug);
|
|
19310
|
-
return {
|
|
19311
|
-
screenshotPath,
|
|
19312
|
-
htmlPath: await page.content()
|
|
19313
|
-
};
|
|
19314
|
-
} finally {
|
|
19315
|
-
await browser.close();
|
|
19316
|
-
}
|
|
19317
|
-
}
|
|
19318
|
-
return {
|
|
19319
|
-
async captureScreenshot(target, outputDir) {
|
|
19320
|
-
if (!input.targetUrl) {
|
|
19321
|
-
throw new Error("target URL is required for Playwright render capture");
|
|
19322
|
-
}
|
|
19323
|
-
const playwright = await loadPlaywright();
|
|
19324
|
-
const browser = await playwright.chromium.launch();
|
|
19325
|
-
try {
|
|
19326
|
-
await mkdir6(outputDir, { recursive: true });
|
|
19327
|
-
const page = await browser.newPage({
|
|
19328
|
-
viewport: {
|
|
19329
|
-
width: target.width ?? 1440,
|
|
19330
|
-
height: target.height ?? 900
|
|
19331
|
-
}
|
|
19332
|
-
});
|
|
19333
|
-
const resolvedUrl = new URL(target.route ?? "/", input.targetUrl).toString();
|
|
19334
|
-
await page.goto(resolvedUrl, { waitUntil: "networkidle" });
|
|
19335
|
-
const targetName = sanitizeName(`${target.targetId}.${target.viewport}`);
|
|
19336
|
-
const screenshotPath = path75.join(outputDir, `${targetName}.png`);
|
|
19337
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
19338
|
-
return screenshotPath;
|
|
19339
|
-
} finally {
|
|
19340
|
-
await browser.close();
|
|
19341
|
-
}
|
|
19342
|
-
},
|
|
19343
|
-
async captureHtml(target, outputDir) {
|
|
19344
|
-
if (!input.targetUrl) {
|
|
19345
|
-
throw new Error("target URL is required for Playwright render capture");
|
|
19346
|
-
}
|
|
19347
|
-
const playwright = await loadPlaywright();
|
|
19348
|
-
const browser = await playwright.chromium.launch();
|
|
19349
|
-
try {
|
|
19350
|
-
await mkdir6(outputDir, { recursive: true });
|
|
19351
|
-
const page = await browser.newPage({
|
|
19352
|
-
viewport: {
|
|
19353
|
-
width: target.width ?? 1440,
|
|
19354
|
-
height: target.height ?? 900
|
|
19355
|
-
}
|
|
19356
|
-
});
|
|
19357
|
-
const resolvedUrl = new URL(target.route ?? "/", input.targetUrl).toString();
|
|
19358
|
-
await page.goto(resolvedUrl, { waitUntil: "networkidle" });
|
|
19359
|
-
const targetName = sanitizeName(`${target.targetId}.${target.viewport}`);
|
|
19360
|
-
const htmlPath = path75.join(outputDir, `${targetName}.html`);
|
|
19361
|
-
await writeFile5(htmlPath, await page.content(), "utf-8");
|
|
19362
|
-
return htmlPath;
|
|
19363
|
-
} finally {
|
|
19364
|
-
await browser.close();
|
|
19365
|
-
}
|
|
19366
|
-
}
|
|
19367
|
-
};
|
|
19368
|
-
}
|
|
19369
|
-
|
|
19370
|
-
// src/core/providers/playwrightBrowserQaProvider.ts
|
|
19371
|
-
init_surfaceType();
|
|
19372
|
-
async function loadPlaywright2() {
|
|
19373
|
-
try {
|
|
19374
|
-
return await import("playwright");
|
|
19375
|
-
} catch {
|
|
19376
|
-
throw new Error("Playwright is not installed. Install it with: npm install playwright");
|
|
19377
|
-
}
|
|
19378
|
-
}
|
|
19379
|
-
async function withPage(input, task) {
|
|
19380
|
-
const playwright = await loadPlaywright2();
|
|
19381
|
-
const browser = await playwright.chromium.launch();
|
|
19382
|
-
try {
|
|
19383
|
-
const page = await browser.newPage();
|
|
19384
|
-
if (input.targetUrl) {
|
|
19385
|
-
await page.goto(input.targetUrl, { waitUntil: "networkidle" });
|
|
19386
|
-
} else if (input.htmlContent) {
|
|
19387
|
-
await page.setContent(input.htmlContent, { waitUntil: "load" });
|
|
19388
|
-
} else {
|
|
19389
|
-
throw new Error("Browser QA requires targetUrl or htmlContent");
|
|
19390
|
-
}
|
|
19391
|
-
return await task(page);
|
|
19392
|
-
} finally {
|
|
19393
|
-
await browser.close();
|
|
19394
|
-
}
|
|
19395
|
-
}
|
|
19396
|
-
function completed(phase) {
|
|
19397
|
-
return {
|
|
19398
|
-
phase,
|
|
19399
|
-
status: "executed",
|
|
19400
|
-
findings: [],
|
|
19401
|
-
repair_suggestions: [],
|
|
19402
|
-
evidence_refs: [`provider:playwright:${phase}`],
|
|
19403
|
-
checks_performed: [`playwright executed ${phase} checks`]
|
|
19404
|
-
};
|
|
19405
|
-
}
|
|
19406
|
-
function createPlaywrightBrowserQaProvider() {
|
|
19407
|
-
return {
|
|
19408
|
-
providerId: "playwright",
|
|
19409
|
-
canRun(surface) {
|
|
19410
|
-
return requiresVisualBrowserEvidence(surface);
|
|
19411
|
-
},
|
|
19412
|
-
async runSmoke(input) {
|
|
19413
|
-
return withPage(input, async (page) => {
|
|
19414
|
-
await page.title();
|
|
19415
|
-
return completed("smoke");
|
|
19416
|
-
});
|
|
19417
|
-
},
|
|
19418
|
-
async runInteraction(input) {
|
|
19419
|
-
return withPage(input, async (page) => {
|
|
19420
|
-
await page.locator("button, a, input, form").count();
|
|
19421
|
-
return completed("interaction");
|
|
19422
|
-
});
|
|
19423
|
-
},
|
|
19424
|
-
async runVisual(input) {
|
|
19425
|
-
return withPage(input, async (page) => {
|
|
19426
|
-
await page.viewportSize();
|
|
19427
|
-
return completed("visual");
|
|
19428
|
-
});
|
|
19429
|
-
},
|
|
19430
|
-
async runAccessibility(input) {
|
|
19431
|
-
return withPage(input, async (page) => {
|
|
19432
|
-
await page.locator("html").getAttribute("lang");
|
|
19433
|
-
return completed("accessibility");
|
|
19434
|
-
});
|
|
19435
|
-
}
|
|
19436
|
-
};
|
|
19437
|
-
}
|
|
19438
21415
|
export {
|
|
19439
21416
|
BROWSER_QA_PHASES,
|
|
19440
21417
|
DEFAULT_PROTOTYPING_MODE,
|
|
19441
21418
|
DISCUSSION_NON_UI_SURFACES,
|
|
19442
21419
|
DISCUSSION_UI_BEARING_SURFACES,
|
|
19443
21420
|
ID_PREFIXES,
|
|
21421
|
+
PROTOTYPING_MAX_CYCLES,
|
|
19444
21422
|
PROTOTYPING_MAX_ITERATIONS,
|
|
19445
21423
|
PROTOTYPING_MODES,
|
|
19446
21424
|
PROTOTYPING_SUPPORTED_SURFACES,
|
|
@@ -19449,8 +21427,6 @@ export {
|
|
|
19449
21427
|
appendIteration,
|
|
19450
21428
|
checkDecisionGuardrails,
|
|
19451
21429
|
computeTerminationReason,
|
|
19452
|
-
createPlaywrightBrowserQaProvider,
|
|
19453
|
-
createPlaywrightRenderAdapter,
|
|
19454
21430
|
createReportData,
|
|
19455
21431
|
defaultConfig,
|
|
19456
21432
|
derivePrototypingObligations,
|