qfai 1.7.6 → 1.7.7
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 +10 -4
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/20_eval_axis_usability.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/21_eval_axis_consistency.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/22_eval_axis_accessibility.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/23_eval_axis_delight.md +23 -1
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/30_comparison.md +20 -5
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/40_contracts.md +36 -15
- package/assets/uix-rev/comparison-review.md +13 -0
- package/assets/uix-rev/contracts-review.md +18 -0
- package/assets/uix-rev/migration-review.md +17 -0
- package/assets/uix-rev/oq-review.md +13 -0
- package/assets/uix-rev/scoring-review.md +14 -0
- package/assets/uix-rev/strategy-review.md +22 -0
- package/dist/cli/index.cjs +1809 -1146
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +1789 -1126
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +995 -384
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.mjs +963 -357
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -455,6 +455,15 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
455
455
|
);
|
|
456
456
|
}
|
|
457
457
|
}
|
|
458
|
+
if (raw.phase1ReleaseDate !== void 0) {
|
|
459
|
+
if (typeof raw.phase1ReleaseDate === "string" && !isNaN(new Date(raw.phase1ReleaseDate).getTime())) {
|
|
460
|
+
result.phase1ReleaseDate = raw.phase1ReleaseDate;
|
|
461
|
+
} else {
|
|
462
|
+
issues.push(
|
|
463
|
+
configIssue(configPath, "uiux.phase1ReleaseDate \u306F\u6709\u52B9\u306A\u65E5\u4ED8\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
458
467
|
if (raw.warning_as_error_override !== void 0) {
|
|
459
468
|
if (Array.isArray(raw.warning_as_error_override) && raw.warning_as_error_override.every((v) => typeof v === "string")) {
|
|
460
469
|
result.warning_as_error_override = raw.warning_as_error_override;
|
|
@@ -479,6 +488,12 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
479
488
|
result.audit = audit;
|
|
480
489
|
}
|
|
481
490
|
}
|
|
491
|
+
if (raw.migration !== void 0) {
|
|
492
|
+
const migration = normalizeUiuxMigration(raw.migration, configPath, issues);
|
|
493
|
+
if (migration) {
|
|
494
|
+
result.migration = migration;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
482
497
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
483
498
|
}
|
|
484
499
|
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
@@ -538,6 +553,23 @@ function normalizeUiuxAudit(raw, configPath, issues) {
|
|
|
538
553
|
}
|
|
539
554
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
540
555
|
}
|
|
556
|
+
function normalizeUiuxMigration(raw, configPath, issues) {
|
|
557
|
+
if (!isRecord(raw)) {
|
|
558
|
+
issues.push(configIssue(configPath, "uiux.migration \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
const result = {};
|
|
562
|
+
if (raw.strict !== void 0) {
|
|
563
|
+
if (typeof raw.strict === "boolean") {
|
|
564
|
+
result.strict = raw.strict;
|
|
565
|
+
} else {
|
|
566
|
+
issues.push(
|
|
567
|
+
configIssue(configPath, "uiux.migration.strict \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
572
|
+
}
|
|
541
573
|
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
542
574
|
if (!isRecord(raw)) {
|
|
543
575
|
issues.push(
|
|
@@ -3595,18 +3627,125 @@ function resolveObligationsWithOptIn(mode, optIn) {
|
|
|
3595
3627
|
return [.../* @__PURE__ */ new Set([...base, ...validOptIns])];
|
|
3596
3628
|
}
|
|
3597
3629
|
|
|
3630
|
+
// src/core/prototyping/discussionReader.ts
|
|
3631
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3632
|
+
import path12 from "path";
|
|
3633
|
+
import { parse as parseYaml3 } from "yaml";
|
|
3634
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["low-cost", "standard", "full-harness"]);
|
|
3635
|
+
var SIDECAR_FILENAME = "prototyping.yaml";
|
|
3636
|
+
async function readDiscussionRecommendation(discussionPackDir) {
|
|
3637
|
+
const filePath = path12.join(discussionPackDir, SIDECAR_FILENAME);
|
|
3638
|
+
let raw;
|
|
3639
|
+
try {
|
|
3640
|
+
raw = await readFile10(filePath, "utf-8");
|
|
3641
|
+
} catch {
|
|
3642
|
+
return { recommended_mode: null, rationale: null };
|
|
3643
|
+
}
|
|
3644
|
+
let doc;
|
|
3645
|
+
try {
|
|
3646
|
+
doc = parseYaml3(raw);
|
|
3647
|
+
} catch {
|
|
3648
|
+
return { recommended_mode: null, rationale: null };
|
|
3649
|
+
}
|
|
3650
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
3651
|
+
return { recommended_mode: null, rationale: null };
|
|
3652
|
+
}
|
|
3653
|
+
const record2 = doc;
|
|
3654
|
+
const prototyping = record2.prototyping;
|
|
3655
|
+
if (!prototyping || typeof prototyping !== "object" || Array.isArray(prototyping)) {
|
|
3656
|
+
return { recommended_mode: null, rationale: null };
|
|
3657
|
+
}
|
|
3658
|
+
const section = prototyping;
|
|
3659
|
+
const rawMode = typeof section.recommended_mode === "string" ? section.recommended_mode.trim() : null;
|
|
3660
|
+
const rationale = typeof section.rationale === "string" ? section.rationale.trim() : null;
|
|
3661
|
+
const recommended_mode = rawMode && VALID_MODES.has(rawMode) ? rawMode : null;
|
|
3662
|
+
return { recommended_mode, rationale };
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
// src/core/prototyping/precedenceResolver.ts
|
|
3666
|
+
var VALID_MODES2 = /* @__PURE__ */ new Set([
|
|
3667
|
+
"default",
|
|
3668
|
+
"standard",
|
|
3669
|
+
"low-cost",
|
|
3670
|
+
"full-harness"
|
|
3671
|
+
]);
|
|
3672
|
+
function isPrototypingMode(value) {
|
|
3673
|
+
return VALID_MODES2.has(value);
|
|
3674
|
+
}
|
|
3675
|
+
var SYSTEM_DEFAULT_MODE = "standard";
|
|
3676
|
+
function resolvePrecedence(input) {
|
|
3677
|
+
const { cliMode, discussion } = input;
|
|
3678
|
+
const rawRecommended = discussion?.recommended_mode ?? null;
|
|
3679
|
+
const recommended_mode = rawRecommended && isPrototypingMode(rawRecommended) ? rawRecommended : null;
|
|
3680
|
+
if (cliMode) {
|
|
3681
|
+
return {
|
|
3682
|
+
effective_mode: cliMode,
|
|
3683
|
+
mode_source: "cli-override",
|
|
3684
|
+
recommended_mode,
|
|
3685
|
+
rationale: "CLI override"
|
|
3686
|
+
};
|
|
3687
|
+
}
|
|
3688
|
+
if (recommended_mode) {
|
|
3689
|
+
return {
|
|
3690
|
+
effective_mode: recommended_mode,
|
|
3691
|
+
mode_source: "discussion-recommendation",
|
|
3692
|
+
recommended_mode,
|
|
3693
|
+
rationale: discussion?.rationale ?? "discussion recommendation"
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
return {
|
|
3697
|
+
effective_mode: SYSTEM_DEFAULT_MODE,
|
|
3698
|
+
mode_source: "default",
|
|
3699
|
+
recommended_mode: null,
|
|
3700
|
+
rationale: "system default"
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/core/prototyping/modeLogger.ts
|
|
3705
|
+
var VALID_MODE_SOURCES = /* @__PURE__ */ new Set([
|
|
3706
|
+
"cli-override",
|
|
3707
|
+
"discussion-recommendation",
|
|
3708
|
+
"default"
|
|
3709
|
+
]);
|
|
3710
|
+
var EVIDENCE_EXPECTATIONS = {
|
|
3711
|
+
"low-cost": "L1/L2",
|
|
3712
|
+
standard: "L2/L3",
|
|
3713
|
+
"full-harness": "L3/L4/L5"
|
|
3714
|
+
};
|
|
3715
|
+
function formatModeLog(resolution) {
|
|
3716
|
+
return {
|
|
3717
|
+
mode_source: resolution.mode_source,
|
|
3718
|
+
recommended_mode: resolution.recommended_mode,
|
|
3719
|
+
effective_mode: resolution.effective_mode,
|
|
3720
|
+
rationale: resolution.rationale,
|
|
3721
|
+
evidence_expectations: EVIDENCE_EXPECTATIONS[resolution.effective_mode] ?? "L2/L3"
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
// src/core/prototyping/surfaceAdapter.ts
|
|
3726
|
+
var VISUAL_SURFACES = /* @__PURE__ */ new Set(["web", "mobile", "desktop"]);
|
|
3727
|
+
function adaptSurfaceEvidence(input) {
|
|
3728
|
+
const { surfaceType, effectiveMode } = input;
|
|
3729
|
+
const isVisual = VISUAL_SURFACES.has(surfaceType);
|
|
3730
|
+
const isLowCost = effectiveMode === "low-cost";
|
|
3731
|
+
const static_evidence = "collected";
|
|
3732
|
+
const runtime_evidence = isLowCost ? "n/a" : "collected";
|
|
3733
|
+
const visual_review = isVisual && !isLowCost ? "collected" : "n/a";
|
|
3734
|
+
return { visual_review, static_evidence, runtime_evidence };
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3598
3737
|
// src/core/report.ts
|
|
3599
|
-
import { readFile as
|
|
3600
|
-
import
|
|
3738
|
+
import { readFile as readFile43 } from "fs/promises";
|
|
3739
|
+
import path57 from "path";
|
|
3601
3740
|
|
|
3602
3741
|
// src/core/contractIndex.ts
|
|
3603
|
-
import { readFile as
|
|
3604
|
-
import
|
|
3742
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3743
|
+
import path13 from "path";
|
|
3605
3744
|
async function buildContractIndex(root, config) {
|
|
3606
3745
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
3607
|
-
const uiRoot =
|
|
3608
|
-
const apiRoot =
|
|
3609
|
-
const dbRoot =
|
|
3746
|
+
const uiRoot = path13.join(contractsRoot, "ui");
|
|
3747
|
+
const apiRoot = path13.join(contractsRoot, "api");
|
|
3748
|
+
const dbRoot = path13.join(contractsRoot, "db");
|
|
3610
3749
|
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
3611
3750
|
collectUiContractFiles(uiRoot),
|
|
3612
3751
|
collectThemaContractFiles(),
|
|
@@ -3626,7 +3765,7 @@ async function buildContractIndex(root, config) {
|
|
|
3626
3765
|
}
|
|
3627
3766
|
async function indexContractFiles(files, index) {
|
|
3628
3767
|
for (const file of files) {
|
|
3629
|
-
const text = await
|
|
3768
|
+
const text = await readFile11(file, "utf-8");
|
|
3630
3769
|
extractDeclaredContractIds(text).forEach((id) => {
|
|
3631
3770
|
record(index, id, file);
|
|
3632
3771
|
});
|
|
@@ -3896,15 +4035,15 @@ function resolveParentTcId(tcRef) {
|
|
|
3896
4035
|
}
|
|
3897
4036
|
|
|
3898
4037
|
// src/core/paths.ts
|
|
3899
|
-
import
|
|
4038
|
+
import path14 from "path";
|
|
3900
4039
|
function toRelativePath(root, target) {
|
|
3901
4040
|
if (!target) {
|
|
3902
4041
|
return target;
|
|
3903
4042
|
}
|
|
3904
|
-
if (!
|
|
4043
|
+
if (!path14.isAbsolute(target)) {
|
|
3905
4044
|
return toPosixPath2(target);
|
|
3906
4045
|
}
|
|
3907
|
-
const relative =
|
|
4046
|
+
const relative = path14.relative(root, target);
|
|
3908
4047
|
if (!relative) {
|
|
3909
4048
|
return ".";
|
|
3910
4049
|
}
|
|
@@ -4075,7 +4214,7 @@ function parseSpec(md, file) {
|
|
|
4075
4214
|
}
|
|
4076
4215
|
|
|
4077
4216
|
// src/core/deltaV1.ts
|
|
4078
|
-
import { parse as
|
|
4217
|
+
import { parse as parseYaml4 } from "yaml";
|
|
4079
4218
|
var REQUIRED_DELTA_META_KEYS = [
|
|
4080
4219
|
"id",
|
|
4081
4220
|
"date",
|
|
@@ -4230,7 +4369,7 @@ function parseYamlMeta(block) {
|
|
|
4230
4369
|
return { value: null, error: null };
|
|
4231
4370
|
}
|
|
4232
4371
|
try {
|
|
4233
|
-
const parsed =
|
|
4372
|
+
const parsed = parseYaml4(sanitizeMetaYaml(block));
|
|
4234
4373
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4235
4374
|
return {
|
|
4236
4375
|
value: null,
|
|
@@ -4264,7 +4403,7 @@ function parseVerificationPlan(body) {
|
|
|
4264
4403
|
return { planHeadingLine: planHeading.line, parseError: null, items: [] };
|
|
4265
4404
|
}
|
|
4266
4405
|
try {
|
|
4267
|
-
const parsed =
|
|
4406
|
+
const parsed = parseYaml4(yamlSource);
|
|
4268
4407
|
if (!Array.isArray(parsed)) {
|
|
4269
4408
|
return {
|
|
4270
4409
|
planHeadingLine: planHeading.line,
|
|
@@ -4351,16 +4490,16 @@ function asTagArray(value) {
|
|
|
4351
4490
|
}
|
|
4352
4491
|
|
|
4353
4492
|
// src/core/version.ts
|
|
4354
|
-
import { readFile as
|
|
4355
|
-
import
|
|
4493
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4494
|
+
import path15 from "path";
|
|
4356
4495
|
import { fileURLToPath } from "url";
|
|
4357
4496
|
async function resolveToolVersion() {
|
|
4358
|
-
if ("1.7.
|
|
4359
|
-
return "1.7.
|
|
4497
|
+
if ("1.7.7".length > 0) {
|
|
4498
|
+
return "1.7.7";
|
|
4360
4499
|
}
|
|
4361
4500
|
try {
|
|
4362
4501
|
const packagePath = resolvePackageJsonPath();
|
|
4363
|
-
const raw = await
|
|
4502
|
+
const raw = await readFile12(packagePath, "utf-8");
|
|
4364
4503
|
const parsed = JSON.parse(raw);
|
|
4365
4504
|
const version = typeof parsed.version === "string" ? parsed.version : "";
|
|
4366
4505
|
return version.length > 0 ? version : "unknown";
|
|
@@ -4371,13 +4510,13 @@ async function resolveToolVersion() {
|
|
|
4371
4510
|
function resolvePackageJsonPath() {
|
|
4372
4511
|
const base = import.meta.url;
|
|
4373
4512
|
const basePath = base.startsWith("file:") ? fileURLToPath(base) : base;
|
|
4374
|
-
return
|
|
4513
|
+
return path15.resolve(path15.dirname(basePath), "../../package.json");
|
|
4375
4514
|
}
|
|
4376
4515
|
|
|
4377
4516
|
// src/core/waivers.ts
|
|
4378
|
-
import { access as access6, readFile as
|
|
4379
|
-
import
|
|
4380
|
-
import { parse as
|
|
4517
|
+
import { access as access6, readFile as readFile13 } from "fs/promises";
|
|
4518
|
+
import path16 from "path";
|
|
4519
|
+
import { parse as parseYaml5 } from "yaml";
|
|
4381
4520
|
|
|
4382
4521
|
// src/core/regex.ts
|
|
4383
4522
|
function escapeRegExp(value) {
|
|
@@ -4385,12 +4524,12 @@ function escapeRegExp(value) {
|
|
|
4385
4524
|
}
|
|
4386
4525
|
|
|
4387
4526
|
// src/core/waivers.ts
|
|
4388
|
-
var WAIVER_FILE =
|
|
4389
|
-
var UNSUPPORTED_WAIVER_FILE =
|
|
4527
|
+
var WAIVER_FILE = path16.join(".qfai", "waivers.yml");
|
|
4528
|
+
var UNSUPPORTED_WAIVER_FILE = path16.join(".qfai", "waivers.yaml");
|
|
4390
4529
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4391
4530
|
var RULE_ID_RE = /^[A-Z]+-\d{3}$/;
|
|
4392
4531
|
async function applyWaivers(root, findings) {
|
|
4393
|
-
const resolvedRoot =
|
|
4532
|
+
const resolvedRoot = path16.resolve(root);
|
|
4394
4533
|
const ruleSeverityIndex = buildRuleSeverityIndex(findings);
|
|
4395
4534
|
const loaded = await loadWaivers(resolvedRoot, ruleSeverityIndex);
|
|
4396
4535
|
const applied = applyWaiversToFindings(resolvedRoot, findings, loaded.applicableWaivers);
|
|
@@ -4403,8 +4542,8 @@ async function applyWaivers(root, findings) {
|
|
|
4403
4542
|
};
|
|
4404
4543
|
}
|
|
4405
4544
|
async function loadWaivers(root, ruleSeverityIndex) {
|
|
4406
|
-
const waiverPath =
|
|
4407
|
-
const unsupportedPath =
|
|
4545
|
+
const waiverPath = path16.join(root, WAIVER_FILE);
|
|
4546
|
+
const unsupportedPath = path16.join(root, UNSUPPORTED_WAIVER_FILE);
|
|
4408
4547
|
const validationIssues = [];
|
|
4409
4548
|
if (await exists6(unsupportedPath)) {
|
|
4410
4549
|
validationIssues.push(
|
|
@@ -4428,7 +4567,7 @@ async function loadWaivers(root, ruleSeverityIndex) {
|
|
|
4428
4567
|
}
|
|
4429
4568
|
let rawText;
|
|
4430
4569
|
try {
|
|
4431
|
-
rawText = await
|
|
4570
|
+
rawText = await readFile13(waiverPath, "utf-8");
|
|
4432
4571
|
} catch (error) {
|
|
4433
4572
|
validationIssues.push(
|
|
4434
4573
|
issue(
|
|
@@ -4449,7 +4588,7 @@ async function loadWaivers(root, ruleSeverityIndex) {
|
|
|
4449
4588
|
}
|
|
4450
4589
|
let parsed;
|
|
4451
4590
|
try {
|
|
4452
|
-
parsed =
|
|
4591
|
+
parsed = parseYaml5(rawText);
|
|
4453
4592
|
} catch (error) {
|
|
4454
4593
|
validationIssues.push(
|
|
4455
4594
|
issue(
|
|
@@ -5121,8 +5260,8 @@ var STATIC_RULE_SEVERITY = {
|
|
|
5121
5260
|
};
|
|
5122
5261
|
|
|
5123
5262
|
// src/core/validators/contracts.ts
|
|
5124
|
-
import { readFile as
|
|
5125
|
-
import
|
|
5263
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
5264
|
+
import path17 from "path";
|
|
5126
5265
|
var SQL_DANGEROUS_PATTERNS = [
|
|
5127
5266
|
{ pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
|
|
5128
5267
|
{ pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
|
|
@@ -5135,9 +5274,9 @@ var SQL_DANGEROUS_PATTERNS = [
|
|
|
5135
5274
|
async function validateContracts(root, config) {
|
|
5136
5275
|
const issues = [];
|
|
5137
5276
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
5138
|
-
const uiRoot =
|
|
5139
|
-
const apiRoot =
|
|
5140
|
-
const dbRoot =
|
|
5277
|
+
const uiRoot = path17.join(contractsRoot, "ui");
|
|
5278
|
+
const apiRoot = path17.join(contractsRoot, "api");
|
|
5279
|
+
const dbRoot = path17.join(contractsRoot, "db");
|
|
5141
5280
|
const [uiFiles, apiFiles, dbFiles] = await Promise.all([
|
|
5142
5281
|
collectUiContractFiles(uiRoot),
|
|
5143
5282
|
collectApiContractFiles(apiRoot),
|
|
@@ -5191,7 +5330,7 @@ async function validateContracts(root, config) {
|
|
|
5191
5330
|
}
|
|
5192
5331
|
async function validateContractFile(file, kind) {
|
|
5193
5332
|
const issues = [];
|
|
5194
|
-
const text = await
|
|
5333
|
+
const text = await readFile14(file, "utf-8");
|
|
5195
5334
|
const declaredIds = extractDeclaredContractIds(text);
|
|
5196
5335
|
issues.push(...validateDeclaredContractIds(declaredIds, file, kind));
|
|
5197
5336
|
if (kind === "DB") {
|
|
@@ -5312,23 +5451,23 @@ function formatError5(error) {
|
|
|
5312
5451
|
}
|
|
5313
5452
|
|
|
5314
5453
|
// src/core/validators/discussMermaid.ts
|
|
5315
|
-
import { readFile as
|
|
5316
|
-
import
|
|
5454
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
5455
|
+
import path18 from "path";
|
|
5317
5456
|
var DISCUSSION_PACK_FLOW_FILE = "03_Story-Workshop.md";
|
|
5318
5457
|
var MERMAID_START_RE = /^\s*(`{3,}|~{3,})\s*mermaid\b/i;
|
|
5319
5458
|
var FLOW_OR_SEQUENCE_RE = /\b(?:sequenceDiagram|flowchart)\b/;
|
|
5320
5459
|
async function validateDiscussionMermaid(root) {
|
|
5321
|
-
const discussionRootDir =
|
|
5460
|
+
const discussionRootDir = path18.join(root, ".qfai", "discussion");
|
|
5322
5461
|
const discussionPackMarkdownFiles = await collectFiles(discussionRootDir, {
|
|
5323
5462
|
extensions: [".md"]
|
|
5324
5463
|
});
|
|
5325
5464
|
const discussionPackFiles = [];
|
|
5326
5465
|
for (const file of discussionPackMarkdownFiles) {
|
|
5327
|
-
const fileName =
|
|
5466
|
+
const fileName = path18.basename(file);
|
|
5328
5467
|
if (fileName !== DISCUSSION_PACK_FLOW_FILE) {
|
|
5329
5468
|
continue;
|
|
5330
5469
|
}
|
|
5331
|
-
const discussionDirName =
|
|
5470
|
+
const discussionDirName = path18.basename(path18.dirname(file));
|
|
5332
5471
|
const nameValidation = validatePackName("discussion", discussionDirName);
|
|
5333
5472
|
if (nameValidation.isCanonical) {
|
|
5334
5473
|
discussionPackFiles.push({ file, kind: "current" });
|
|
@@ -5358,7 +5497,7 @@ async function validateDiscussionMermaid(root) {
|
|
|
5358
5497
|
);
|
|
5359
5498
|
}
|
|
5360
5499
|
for (const { file } of discussionPackFiles) {
|
|
5361
|
-
const text = await
|
|
5500
|
+
const text = await readFile15(file, "utf-8");
|
|
5362
5501
|
if (containsMermaidFlowDiagram(text)) {
|
|
5363
5502
|
continue;
|
|
5364
5503
|
}
|
|
@@ -5408,17 +5547,17 @@ function containsMermaidFlowDiagram(text) {
|
|
|
5408
5547
|
}
|
|
5409
5548
|
|
|
5410
5549
|
// src/core/validators/assistantAssets.ts
|
|
5411
|
-
import { access as access7, readFile as
|
|
5412
|
-
import
|
|
5550
|
+
import { access as access7, readFile as readFile16 } from "fs/promises";
|
|
5551
|
+
import path19 from "path";
|
|
5413
5552
|
var DRIFT_PROTOCOL_MARKER = "[DRIFT-PROTOCOL:MANDATORY]";
|
|
5414
5553
|
var REVIEWER_GATE_HEADING_PATTERN = /^###\s+Reviewer Gate\b.*$/im;
|
|
5415
5554
|
var ANY_MARKDOWN_HEADING_PATTERN = /^\s*#{1,6}\s+/m;
|
|
5416
5555
|
async function validateAssistantAssets(root, config) {
|
|
5417
5556
|
const skillsDir = resolvePath(root, config, "skillsDir");
|
|
5418
|
-
const assistantDir =
|
|
5419
|
-
const skillsLocalDir =
|
|
5420
|
-
const driftProtocolPath =
|
|
5421
|
-
const testLayersPath =
|
|
5557
|
+
const assistantDir = path19.dirname(skillsDir);
|
|
5558
|
+
const skillsLocalDir = path19.join(path19.dirname(skillsDir), `${path19.basename(skillsDir)}.local`);
|
|
5559
|
+
const driftProtocolPath = path19.join(assistantDir, "instructions", "drift-protocol.md");
|
|
5560
|
+
const testLayersPath = path19.join(assistantDir, "steering", "test-layers.md");
|
|
5422
5561
|
const issues = [];
|
|
5423
5562
|
if (!await exists7(driftProtocolPath)) {
|
|
5424
5563
|
issues.push(
|
|
@@ -5444,7 +5583,7 @@ async function validateAssistantAssets(root, config) {
|
|
|
5444
5583
|
}
|
|
5445
5584
|
const skillFiles = await collectSkillFiles([skillsDir, skillsLocalDir]);
|
|
5446
5585
|
for (const skillFile of skillFiles) {
|
|
5447
|
-
const content = await
|
|
5586
|
+
const content = await readFile16(skillFile, "utf-8");
|
|
5448
5587
|
if (!content.includes(DRIFT_PROTOCOL_MARKER)) {
|
|
5449
5588
|
issues.push(
|
|
5450
5589
|
issue(
|
|
@@ -5486,7 +5625,7 @@ async function validateAssistantAssets(root, config) {
|
|
|
5486
5625
|
}
|
|
5487
5626
|
async function collectSkillFiles(dirs) {
|
|
5488
5627
|
const files = await Promise.all(dirs.map((dir) => collectFiles(dir)));
|
|
5489
|
-
return files.flat().filter((filePath) =>
|
|
5628
|
+
return files.flat().filter((filePath) => path19.basename(filePath) === "SKILL.md").sort((a, b) => a.localeCompare(b));
|
|
5490
5629
|
}
|
|
5491
5630
|
function extractReviewerGateSection(content) {
|
|
5492
5631
|
const headingMatch = REVIEWER_GATE_HEADING_PATTERN.exec(content);
|
|
@@ -5527,20 +5666,20 @@ async function exists7(target) {
|
|
|
5527
5666
|
}
|
|
5528
5667
|
|
|
5529
5668
|
// src/core/skillsIntegrity.ts
|
|
5530
|
-
import { readFile as
|
|
5531
|
-
import
|
|
5669
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
5670
|
+
import path21 from "path";
|
|
5532
5671
|
|
|
5533
5672
|
// src/shared/assets.ts
|
|
5534
5673
|
import { existsSync } from "fs";
|
|
5535
|
-
import
|
|
5674
|
+
import path20 from "path";
|
|
5536
5675
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5537
5676
|
function getInitAssetsDir() {
|
|
5538
5677
|
const base = import.meta.url;
|
|
5539
5678
|
const basePath = base.startsWith("file:") ? fileURLToPath2(base) : base;
|
|
5540
|
-
const baseDir =
|
|
5679
|
+
const baseDir = path20.dirname(basePath);
|
|
5541
5680
|
const candidates = [
|
|
5542
|
-
|
|
5543
|
-
|
|
5681
|
+
path20.resolve(baseDir, "../../../assets/init"),
|
|
5682
|
+
path20.resolve(baseDir, "../../assets/init")
|
|
5544
5683
|
];
|
|
5545
5684
|
for (const candidate of candidates) {
|
|
5546
5685
|
if (existsSync(candidate)) {
|
|
@@ -5559,10 +5698,10 @@ function getInitAssetsDir() {
|
|
|
5559
5698
|
// src/core/skillsIntegrity.ts
|
|
5560
5699
|
async function diffProjectSkillsAgainstInitAssets(root, config) {
|
|
5561
5700
|
const skillsDirConfig = config.paths.skillsDir;
|
|
5562
|
-
const skillsDir =
|
|
5701
|
+
const skillsDir = path21.isAbsolute(skillsDirConfig) ? skillsDirConfig : path21.resolve(root, skillsDirConfig);
|
|
5563
5702
|
let templateDir;
|
|
5564
5703
|
try {
|
|
5565
|
-
const rel =
|
|
5704
|
+
const rel = path21.isAbsolute(skillsDirConfig) ? path21.relative(root, skillsDirConfig) : skillsDirConfig;
|
|
5566
5705
|
const normalized = rel.replace(/^[\\/]+/, "");
|
|
5567
5706
|
if (normalized.length === 0 || normalized.startsWith("..")) {
|
|
5568
5707
|
return {
|
|
@@ -5574,7 +5713,7 @@ async function diffProjectSkillsAgainstInitAssets(root, config) {
|
|
|
5574
5713
|
changed: []
|
|
5575
5714
|
};
|
|
5576
5715
|
}
|
|
5577
|
-
templateDir =
|
|
5716
|
+
templateDir = path21.join(getInitAssetsDir(), normalized);
|
|
5578
5717
|
} catch {
|
|
5579
5718
|
return {
|
|
5580
5719
|
status: "skipped_missing_assets",
|
|
@@ -5627,8 +5766,8 @@ async function diffProjectSkillsAgainstInitAssets(root, config) {
|
|
|
5627
5766
|
}
|
|
5628
5767
|
try {
|
|
5629
5768
|
const [a, b] = await Promise.all([
|
|
5630
|
-
|
|
5631
|
-
|
|
5769
|
+
readFile17(templateAbs, "utf-8"),
|
|
5770
|
+
readFile17(projectAbs, "utf-8")
|
|
5632
5771
|
]);
|
|
5633
5772
|
if (normalizeNewlines(a) !== normalizeNewlines(b)) {
|
|
5634
5773
|
changed.push(rel);
|
|
@@ -5651,7 +5790,7 @@ function normalizeNewlines(text) {
|
|
|
5651
5790
|
return text.replace(/\r\n/g, "\n");
|
|
5652
5791
|
}
|
|
5653
5792
|
function toRel(base, abs) {
|
|
5654
|
-
const rel =
|
|
5793
|
+
const rel = path21.relative(base, abs);
|
|
5655
5794
|
return rel.replace(/[\\/]+/g, "/");
|
|
5656
5795
|
}
|
|
5657
5796
|
function intersectKeys(a, b) {
|
|
@@ -5696,8 +5835,8 @@ async function validateSkillsIntegrity(root, config) {
|
|
|
5696
5835
|
}
|
|
5697
5836
|
|
|
5698
5837
|
// src/core/validators/ids.ts
|
|
5699
|
-
import { readFile as
|
|
5700
|
-
import
|
|
5838
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
5839
|
+
import path22 from "path";
|
|
5701
5840
|
var SC_TAG_RE2 = /^SC-\d{4}-\d{4}$/;
|
|
5702
5841
|
var AC_ID_RE2 = /\bAC-[A-Za-z0-9_-]+\b/g;
|
|
5703
5842
|
var CASE_ID_RE = /\b(?:CASE|TC)-[A-Za-z0-9_-]+\b/g;
|
|
@@ -5813,7 +5952,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
5813
5952
|
}
|
|
5814
5953
|
async function readSafe5(file) {
|
|
5815
5954
|
try {
|
|
5816
|
-
return await
|
|
5955
|
+
return await readFile18(file, "utf-8");
|
|
5817
5956
|
} catch {
|
|
5818
5957
|
return "";
|
|
5819
5958
|
}
|
|
@@ -5844,23 +5983,23 @@ function recordId(out, id, file) {
|
|
|
5844
5983
|
}
|
|
5845
5984
|
function formatFileList(files, root) {
|
|
5846
5985
|
return files.map((file) => {
|
|
5847
|
-
const relative =
|
|
5986
|
+
const relative = path22.relative(root, file);
|
|
5848
5987
|
return relative.length > 0 ? relative : file;
|
|
5849
5988
|
}).join(", ");
|
|
5850
5989
|
}
|
|
5851
5990
|
|
|
5852
5991
|
// src/core/validators/reviewArtifacts.ts
|
|
5853
|
-
import { readFile as
|
|
5854
|
-
import
|
|
5992
|
+
import { readFile as readFile19, readdir as readdir4, stat as stat3 } from "fs/promises";
|
|
5993
|
+
import path23 from "path";
|
|
5855
5994
|
var REVIEW_PACK_DIR_RE = /^review-(\d{17})$/i;
|
|
5856
5995
|
var REVIEWER_FILE_RE = /^R\d+_.+\.md$/i;
|
|
5857
5996
|
var ALLOWED_TARGET_KINDS = /* @__PURE__ */ new Set(["spec", "discussion"]);
|
|
5858
5997
|
var ALLOWED_ROSTER_STATUS = /* @__PURE__ */ new Set(["PASS", "FAIL", "NA"]);
|
|
5859
5998
|
var ALLOWED_OVERALL_STATUS = /* @__PURE__ */ new Set(["PASS", "FAIL"]);
|
|
5860
5999
|
async function validateReviewArtifacts(root) {
|
|
5861
|
-
const reviewRoot =
|
|
6000
|
+
const reviewRoot = path23.join(root, ".qfai", "review");
|
|
5862
6001
|
const issues = [];
|
|
5863
|
-
const gitignorePath =
|
|
6002
|
+
const gitignorePath = path23.join(reviewRoot, ".gitignore");
|
|
5864
6003
|
if (!await isFile(gitignorePath)) {
|
|
5865
6004
|
issues.push(
|
|
5866
6005
|
issue(
|
|
@@ -5898,8 +6037,8 @@ async function validateReviewArtifacts(root) {
|
|
|
5898
6037
|
}
|
|
5899
6038
|
async function validateReviewPack(reviewPackDir) {
|
|
5900
6039
|
const issues = [];
|
|
5901
|
-
const reviewRequestPath =
|
|
5902
|
-
const summaryPath =
|
|
6040
|
+
const reviewRequestPath = path23.join(reviewPackDir, "review_request.md");
|
|
6041
|
+
const summaryPath = path23.join(reviewPackDir, "summary.json");
|
|
5903
6042
|
if (!await isFile(reviewRequestPath)) {
|
|
5904
6043
|
issues.push(
|
|
5905
6044
|
issue(
|
|
@@ -5942,7 +6081,7 @@ async function validateReviewPack(reviewPackDir) {
|
|
|
5942
6081
|
async function validateSummarySchema(summaryPath) {
|
|
5943
6082
|
let parsed;
|
|
5944
6083
|
try {
|
|
5945
|
-
parsed = JSON.parse(await
|
|
6084
|
+
parsed = JSON.parse(await readFile19(summaryPath, "utf-8"));
|
|
5946
6085
|
} catch (error) {
|
|
5947
6086
|
return [
|
|
5948
6087
|
issue(
|
|
@@ -6031,7 +6170,7 @@ async function listReviewPackDirs(reviewRoot) {
|
|
|
6031
6170
|
} catch {
|
|
6032
6171
|
return [];
|
|
6033
6172
|
}
|
|
6034
|
-
return entries.map((name) =>
|
|
6173
|
+
return entries.map((name) => path23.join(reviewRoot, name));
|
|
6035
6174
|
}
|
|
6036
6175
|
async function listReviewerFiles(reviewPackDir) {
|
|
6037
6176
|
try {
|
|
@@ -6079,8 +6218,8 @@ function formatError6(error) {
|
|
|
6079
6218
|
}
|
|
6080
6219
|
|
|
6081
6220
|
// src/core/validators/specPack.ts
|
|
6082
|
-
import { readFile as
|
|
6083
|
-
import
|
|
6221
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
6222
|
+
import path24 from "path";
|
|
6084
6223
|
var LEDGER_REQUIRED_COLUMNS = [
|
|
6085
6224
|
"trace_id",
|
|
6086
6225
|
"obj_id",
|
|
@@ -6498,7 +6637,7 @@ function validateLayeredNamespace(entry, filePath, ids, prefix) {
|
|
|
6498
6637
|
"specPack.layered.namespace",
|
|
6499
6638
|
mismatched,
|
|
6500
6639
|
"compatibility",
|
|
6501
|
-
`${
|
|
6640
|
+
`${path24.basename(filePath)} \u306E ${prefix} ID \u3092 spec-${entry.specNumber} \u306B\u5408\u308F\u305B\u3066\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
6502
6641
|
)
|
|
6503
6642
|
];
|
|
6504
6643
|
}
|
|
@@ -6506,7 +6645,7 @@ async function collectExistingLayeredDeltaFiles(entry) {
|
|
|
6506
6645
|
const candidates = Array.from(new Set(entry.deltaCandidates));
|
|
6507
6646
|
const existing = [];
|
|
6508
6647
|
for (const candidate of candidates) {
|
|
6509
|
-
if (!/(?:^|_)delta\.md$/i.test(
|
|
6648
|
+
if (!/(?:^|_)delta\.md$/i.test(path24.basename(candidate))) {
|
|
6510
6649
|
continue;
|
|
6511
6650
|
}
|
|
6512
6651
|
if (await fileExists(candidate)) {
|
|
@@ -6517,7 +6656,7 @@ async function collectExistingLayeredDeltaFiles(entry) {
|
|
|
6517
6656
|
}
|
|
6518
6657
|
async function fileExists(target) {
|
|
6519
6658
|
try {
|
|
6520
|
-
await
|
|
6659
|
+
await readFile20(target, "utf-8");
|
|
6521
6660
|
return true;
|
|
6522
6661
|
} catch {
|
|
6523
6662
|
return false;
|
|
@@ -6678,7 +6817,7 @@ async function validateTraceabilityLedger(entry, contractIds) {
|
|
|
6678
6817
|
const issues = [];
|
|
6679
6818
|
let ledgerText;
|
|
6680
6819
|
try {
|
|
6681
|
-
ledgerText = await
|
|
6820
|
+
ledgerText = await readFile20(entry.traceabilityLedgerPath, "utf-8");
|
|
6682
6821
|
} catch {
|
|
6683
6822
|
return issues;
|
|
6684
6823
|
}
|
|
@@ -7076,10 +7215,10 @@ async function loadExistingRequiredTexts(entry, missingFiles) {
|
|
|
7076
7215
|
}
|
|
7077
7216
|
async function loadLayerPolicy(root, config) {
|
|
7078
7217
|
const skillsDir = resolvePath(root, config, "skillsDir");
|
|
7079
|
-
const assistantRoot =
|
|
7080
|
-
const policyPath =
|
|
7218
|
+
const assistantRoot = path24.dirname(skillsDir);
|
|
7219
|
+
const policyPath = path24.join(assistantRoot, "steering", "test-layers.md");
|
|
7081
7220
|
try {
|
|
7082
|
-
const policyText = await
|
|
7221
|
+
const policyText = await readFile20(policyPath, "utf-8");
|
|
7083
7222
|
return {
|
|
7084
7223
|
tags: resolveAllowedLayerTagsFromPolicy(policyText),
|
|
7085
7224
|
issues: []
|
|
@@ -7187,15 +7326,15 @@ function extractMarkdownSection(text, heading) {
|
|
|
7187
7326
|
}
|
|
7188
7327
|
async function readSafe6(filePath) {
|
|
7189
7328
|
try {
|
|
7190
|
-
return await
|
|
7329
|
+
return await readFile20(filePath, "utf-8");
|
|
7191
7330
|
} catch {
|
|
7192
7331
|
return "";
|
|
7193
7332
|
}
|
|
7194
7333
|
}
|
|
7195
7334
|
|
|
7196
7335
|
// src/core/validators/traceability.ts
|
|
7197
|
-
import { access as access8, readFile as
|
|
7198
|
-
import
|
|
7336
|
+
import { access as access8, readFile as readFile21 } from "fs/promises";
|
|
7337
|
+
import path25 from "path";
|
|
7199
7338
|
var SC_TAG_RE3 = /^SC-\d{4}-\d{4}$/;
|
|
7200
7339
|
var SC_TAG_RE_GLOBAL = /\bSC-\d{4}-\d{4}\b/g;
|
|
7201
7340
|
var SPEC_TAG_RE = /^SPEC-\d{4}$/;
|
|
@@ -7212,8 +7351,8 @@ async function validateTraceability(root, config, phase) {
|
|
|
7212
7351
|
if (layeredEntries.length === 0) {
|
|
7213
7352
|
return issues;
|
|
7214
7353
|
}
|
|
7215
|
-
const policiesDir = layeredEntries[0]?.sharedDir ??
|
|
7216
|
-
const capabilitiesPath =
|
|
7354
|
+
const policiesDir = layeredEntries[0]?.sharedDir ?? path25.join(specsRoot, "_policies");
|
|
7355
|
+
const capabilitiesPath = path25.join(policiesDir, "03_Capabilities.md");
|
|
7217
7356
|
const capabilitiesExists = await exists8(capabilitiesPath);
|
|
7218
7357
|
const capabilitiesText = await readSafe7(capabilitiesPath);
|
|
7219
7358
|
const capIds = new Set(extractIds(capabilitiesText, "CAP"));
|
|
@@ -7294,7 +7433,7 @@ async function validatePoliciesLayerDownRef(policiesDir) {
|
|
|
7294
7433
|
"04_Business-flow.md"
|
|
7295
7434
|
];
|
|
7296
7435
|
for (const fileName of targets) {
|
|
7297
|
-
const targetPath =
|
|
7436
|
+
const targetPath = path25.join(policiesDir, fileName);
|
|
7298
7437
|
const text = await readSafe7(targetPath);
|
|
7299
7438
|
if (text.length === 0) {
|
|
7300
7439
|
continue;
|
|
@@ -7753,7 +7892,7 @@ function cloneGlobal3(re) {
|
|
|
7753
7892
|
}
|
|
7754
7893
|
async function readSafe7(target) {
|
|
7755
7894
|
try {
|
|
7756
|
-
return await
|
|
7895
|
+
return await readFile21(target, "utf-8");
|
|
7757
7896
|
} catch {
|
|
7758
7897
|
return "";
|
|
7759
7898
|
}
|
|
@@ -7769,7 +7908,7 @@ async function exists8(target) {
|
|
|
7769
7908
|
|
|
7770
7909
|
// src/core/validators/atddCodeTraceability.ts
|
|
7771
7910
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
7772
|
-
import
|
|
7911
|
+
import path26 from "path";
|
|
7773
7912
|
async function validateAtddCodeTraceability(root, config) {
|
|
7774
7913
|
const result = await evaluateAtddCodeTraceability(root, config);
|
|
7775
7914
|
const issues = [];
|
|
@@ -7852,7 +7991,7 @@ async function validateAtddCodeTraceability(root, config) {
|
|
|
7852
7991
|
"QFAI-ATDD-901",
|
|
7853
7992
|
`atdd-traceability report \u306E\u51FA\u529B\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError7(error)}`,
|
|
7854
7993
|
"warning",
|
|
7855
|
-
|
|
7994
|
+
path26.join(resolvePath(root, config, "outDir"), "atdd-traceability"),
|
|
7856
7995
|
"atddCodeTraceability.report"
|
|
7857
7996
|
)
|
|
7858
7997
|
);
|
|
@@ -7918,7 +8057,7 @@ function buildUnknownIssues(unknown) {
|
|
|
7918
8057
|
});
|
|
7919
8058
|
}
|
|
7920
8059
|
async function writeAtddTraceabilityReport(root, config, result) {
|
|
7921
|
-
const outputDir =
|
|
8060
|
+
const outputDir = path26.join(resolvePath(root, config, "outDir"), "atdd-traceability");
|
|
7922
8061
|
await mkdir2(outputDir, { recursive: true });
|
|
7923
8062
|
const summary = {
|
|
7924
8063
|
missing: {
|
|
@@ -7942,12 +8081,12 @@ async function writeAtddTraceabilityReport(root, config, result) {
|
|
|
7942
8081
|
}
|
|
7943
8082
|
};
|
|
7944
8083
|
await writeFile2(
|
|
7945
|
-
|
|
8084
|
+
path26.join(outputDir, "summary.json"),
|
|
7946
8085
|
`${JSON.stringify(summary, null, 2)}
|
|
7947
8086
|
`,
|
|
7948
8087
|
"utf-8"
|
|
7949
8088
|
);
|
|
7950
|
-
await writeFile2(
|
|
8089
|
+
await writeFile2(path26.join(outputDir, "summary.md"), buildSummaryMarkdown(summary), "utf-8");
|
|
7951
8090
|
}
|
|
7952
8091
|
function buildSummaryMarkdown(summary) {
|
|
7953
8092
|
const lines = [];
|
|
@@ -8012,8 +8151,8 @@ function formatError7(error) {
|
|
|
8012
8151
|
}
|
|
8013
8152
|
|
|
8014
8153
|
// src/core/validators/discussionPack.ts
|
|
8015
|
-
import { readFile as
|
|
8016
|
-
import
|
|
8154
|
+
import { readFile as readFile22, stat as stat4 } from "fs/promises";
|
|
8155
|
+
import path27 from "path";
|
|
8017
8156
|
async function validateDiscussionPackReadiness(root, config) {
|
|
8018
8157
|
const discussionRoot = resolvePath(root, config, "discussionDir");
|
|
8019
8158
|
const readiness = await inspectLatestDiscussionPack(discussionRoot);
|
|
@@ -8105,7 +8244,7 @@ async function validateDiscussionPackReadiness(root, config) {
|
|
|
8105
8244
|
);
|
|
8106
8245
|
}
|
|
8107
8246
|
if (readiness.blockingOqIds.length > 0) {
|
|
8108
|
-
const oqPath =
|
|
8247
|
+
const oqPath = path27.join(readiness.latestPackDir, "11_OQ-Register.md");
|
|
8109
8248
|
issues.push(
|
|
8110
8249
|
issue(
|
|
8111
8250
|
"QFAI-DPACK-004",
|
|
@@ -8120,7 +8259,7 @@ async function validateDiscussionPackReadiness(root, config) {
|
|
|
8120
8259
|
);
|
|
8121
8260
|
}
|
|
8122
8261
|
if (readiness.deferredWithoutDetails.length > 0) {
|
|
8123
|
-
const deferredPath =
|
|
8262
|
+
const deferredPath = path27.join(readiness.latestPackDir, "13_Deferred.md");
|
|
8124
8263
|
issues.push(
|
|
8125
8264
|
issue(
|
|
8126
8265
|
"QFAI-DPACK-007",
|
|
@@ -8134,7 +8273,7 @@ async function validateDiscussionPackReadiness(root, config) {
|
|
|
8134
8273
|
)
|
|
8135
8274
|
);
|
|
8136
8275
|
}
|
|
8137
|
-
const storyWorkshopPath =
|
|
8276
|
+
const storyWorkshopPath = path27.join(readiness.latestPackDir, "03_Story-Workshop.md");
|
|
8138
8277
|
const storyWorkshopText = await readSafe8(storyWorkshopPath);
|
|
8139
8278
|
if (storyWorkshopText !== null && !containsMermaidBlock(storyWorkshopText)) {
|
|
8140
8279
|
issues.push(
|
|
@@ -8161,28 +8300,28 @@ async function readSafe8(filePath) {
|
|
|
8161
8300
|
if (!stats.isFile()) {
|
|
8162
8301
|
return null;
|
|
8163
8302
|
}
|
|
8164
|
-
return await
|
|
8303
|
+
return await readFile22(filePath, "utf-8");
|
|
8165
8304
|
} catch {
|
|
8166
8305
|
return null;
|
|
8167
8306
|
}
|
|
8168
8307
|
}
|
|
8169
8308
|
|
|
8170
8309
|
// src/core/validators/discussionVisuals.ts
|
|
8171
|
-
import { readFile as
|
|
8172
|
-
import
|
|
8310
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
8311
|
+
import path28 from "path";
|
|
8173
8312
|
var MERMAID_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*mermaid\b/im;
|
|
8174
8313
|
var SCREEN_MOCK_HEADING_RE = /^\s*#{1,6}\s*screen mock(?:\s*[-\u2014]+\s*fallback)?\s*\(html\+css\)\s*$/im;
|
|
8175
8314
|
var HTML_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*html\b/im;
|
|
8176
8315
|
var CSS_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*css\b/im;
|
|
8177
8316
|
var UI_HINT_RE = /\b(?:ui|ux|screen|screens|page|pages|layout|wireframe|mock|form|button|frontend|navigation)\b|画面|モック|遷移|フォーム|ボタン/i;
|
|
8178
8317
|
async function validateDiscussionVisuals(root) {
|
|
8179
|
-
const discussionRoot =
|
|
8318
|
+
const discussionRoot = path28.join(root, ".qfai", "discussion");
|
|
8180
8319
|
const latestPackDir = await findLatestDiscussionPackDir(discussionRoot);
|
|
8181
8320
|
if (!latestPackDir) {
|
|
8182
8321
|
return [];
|
|
8183
8322
|
}
|
|
8184
8323
|
const issues = [];
|
|
8185
|
-
const inceptionPath =
|
|
8324
|
+
const inceptionPath = path28.join(latestPackDir, "02_Inception-Deck.md");
|
|
8186
8325
|
const inceptionText = await readSafe9(inceptionPath);
|
|
8187
8326
|
if (inceptionText !== null && !MERMAID_FENCE_RE.test(inceptionText)) {
|
|
8188
8327
|
issues.push(
|
|
@@ -8198,7 +8337,7 @@ async function validateDiscussionVisuals(root) {
|
|
|
8198
8337
|
)
|
|
8199
8338
|
);
|
|
8200
8339
|
}
|
|
8201
|
-
const storyWorkshopPath =
|
|
8340
|
+
const storyWorkshopPath = path28.join(latestPackDir, "03_Story-Workshop.md");
|
|
8202
8341
|
const storyWorkshopText = await readSafe9(storyWorkshopPath);
|
|
8203
8342
|
const storyWorkshopPlainText = storyWorkshopText === null ? null : stripFencedCodeBlocks(storyWorkshopText);
|
|
8204
8343
|
if (storyWorkshopText === null || storyWorkshopPlainText === null) {
|
|
@@ -8262,14 +8401,14 @@ function escapeRegExp2(value) {
|
|
|
8262
8401
|
}
|
|
8263
8402
|
async function readSafe9(filePath) {
|
|
8264
8403
|
try {
|
|
8265
|
-
return await
|
|
8404
|
+
return await readFile23(filePath, "utf-8");
|
|
8266
8405
|
} catch {
|
|
8267
8406
|
return null;
|
|
8268
8407
|
}
|
|
8269
8408
|
}
|
|
8270
8409
|
|
|
8271
8410
|
// src/core/validators/densityHints.ts
|
|
8272
|
-
import { readFile as
|
|
8411
|
+
import { readFile as readFile24 } from "fs/promises";
|
|
8273
8412
|
var BR_ID_RE = /\bBR-[A-Za-z0-9-]+\b/g;
|
|
8274
8413
|
var SCENARIO_RE = /^\s*Scenario(?:\s+Outline)?\s*:/gim;
|
|
8275
8414
|
var TC_OR_CASE_RE = /\b(?:TC|CASE)-[A-Za-z0-9-]+\b/g;
|
|
@@ -8368,7 +8507,7 @@ function isSeparatorRow(line) {
|
|
|
8368
8507
|
}
|
|
8369
8508
|
async function readSafe10(filePath) {
|
|
8370
8509
|
try {
|
|
8371
|
-
return await
|
|
8510
|
+
return await readFile24(filePath, "utf-8");
|
|
8372
8511
|
} catch {
|
|
8373
8512
|
return "";
|
|
8374
8513
|
}
|
|
@@ -8376,11 +8515,11 @@ async function readSafe10(filePath) {
|
|
|
8376
8515
|
|
|
8377
8516
|
// src/core/validators/importLite.ts
|
|
8378
8517
|
import { readdir as readdir5 } from "fs/promises";
|
|
8379
|
-
import
|
|
8518
|
+
import path29 from "path";
|
|
8380
8519
|
|
|
8381
8520
|
// src/core/validators/layerCoverage.ts
|
|
8382
8521
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
8383
|
-
import
|
|
8522
|
+
import path30 from "path";
|
|
8384
8523
|
var ID_PATTERNS = {
|
|
8385
8524
|
us: /^US-\d{4}(?:-\d{4})?$/,
|
|
8386
8525
|
ac: /^AC-\d{4}(?:-\d{4})?$/,
|
|
@@ -8401,7 +8540,7 @@ async function validateLayerCoverage(root, config) {
|
|
|
8401
8540
|
if (layeredEntries.length === 0) {
|
|
8402
8541
|
return issues;
|
|
8403
8542
|
}
|
|
8404
|
-
const coverageRoot =
|
|
8543
|
+
const coverageRoot = path30.join(resolvePath(root, config, "outDir"), "specs-coverage");
|
|
8405
8544
|
for (const entry of layeredEntries) {
|
|
8406
8545
|
if (isV1421LayeredEntry(entry)) {
|
|
8407
8546
|
const coverageResult = await validateV1421Coverage(entry);
|
|
@@ -8436,7 +8575,7 @@ async function validateLayerCoverage(root, config) {
|
|
|
8436
8575
|
}
|
|
8437
8576
|
async function validatePlanArtifacts(specsRoot, entries) {
|
|
8438
8577
|
const issues = [];
|
|
8439
|
-
const deprecatedPlanPath =
|
|
8578
|
+
const deprecatedPlanPath = path30.join(specsRoot, "plan.md");
|
|
8440
8579
|
if (await exists5(deprecatedPlanPath)) {
|
|
8441
8580
|
issues.push(
|
|
8442
8581
|
issue(
|
|
@@ -8452,7 +8591,7 @@ async function validatePlanArtifacts(specsRoot, entries) {
|
|
|
8452
8591
|
);
|
|
8453
8592
|
}
|
|
8454
8593
|
for (const entry of entries) {
|
|
8455
|
-
const planFileName =
|
|
8594
|
+
const planFileName = path30.basename(entry.planPath).toLowerCase();
|
|
8456
8595
|
if (planFileName !== "10_plan.md") {
|
|
8457
8596
|
continue;
|
|
8458
8597
|
}
|
|
@@ -8834,7 +8973,7 @@ function buildSignalRows(prefix, counts, target) {
|
|
|
8834
8973
|
}
|
|
8835
8974
|
async function writeCoverageReport(coverageRoot, specNumber, snapshot) {
|
|
8836
8975
|
await mkdir3(coverageRoot, { recursive: true });
|
|
8837
|
-
const reportPath =
|
|
8976
|
+
const reportPath = path30.join(coverageRoot, `spec-${specNumber}.md`);
|
|
8838
8977
|
const lines = [];
|
|
8839
8978
|
lines.push(`# Spec Coverage (spec-${specNumber})`);
|
|
8840
8979
|
lines.push("");
|
|
@@ -8879,7 +9018,7 @@ function isV1421LayeredEntry(entry) {
|
|
|
8879
9018
|
if (entry.layeredStyle === "v1421") {
|
|
8880
9019
|
return true;
|
|
8881
9020
|
}
|
|
8882
|
-
return
|
|
9021
|
+
return path30.extname(entry.examplesPath).toLowerCase() === ".md";
|
|
8883
9022
|
}
|
|
8884
9023
|
function extractHeadings(text) {
|
|
8885
9024
|
const headings = [];
|
|
@@ -9071,7 +9210,7 @@ async function validateExToTcCoverage(examplesPath, testCasesPath) {
|
|
|
9071
9210
|
|
|
9072
9211
|
// src/core/validators/layeredTraceability.ts
|
|
9073
9212
|
import { readdir as readdir6 } from "fs/promises";
|
|
9074
|
-
import
|
|
9213
|
+
import path31 from "path";
|
|
9075
9214
|
var POLICIES_FILES = [
|
|
9076
9215
|
"01_Objective.md",
|
|
9077
9216
|
"02_Initiative.md",
|
|
@@ -9120,7 +9259,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
9120
9259
|
}
|
|
9121
9260
|
const issues = [];
|
|
9122
9261
|
if (layeredV1417Entries.length > 0) {
|
|
9123
|
-
const policiesDir = layeredV1417Entries[0]?.sharedDir ??
|
|
9262
|
+
const policiesDir = layeredV1417Entries[0]?.sharedDir ?? path31.join(specsRoot, "_policies");
|
|
9124
9263
|
issues.push(...await validatePoliciesDownstreamReferences(policiesDir));
|
|
9125
9264
|
for (const entry of layeredV1417Entries) {
|
|
9126
9265
|
issues.push(...await validateSpecRootParent(entry));
|
|
@@ -9161,7 +9300,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
9161
9300
|
}
|
|
9162
9301
|
}
|
|
9163
9302
|
if (layeredV1421Entries.length > 0) {
|
|
9164
|
-
const policiesDir = layeredV1421Entries[0]?.sharedDir ??
|
|
9303
|
+
const policiesDir = layeredV1421Entries[0]?.sharedDir ?? path31.join(specsRoot, "_policies");
|
|
9165
9304
|
issues.push(...await validatePoliciesScopeForV1421(policiesDir));
|
|
9166
9305
|
for (const entry of layeredV1421Entries) {
|
|
9167
9306
|
issues.push(...await validateDownstreamRefsForV1421(entry));
|
|
@@ -9172,7 +9311,7 @@ async function validateLayeredTraceability(root, config) {
|
|
|
9172
9311
|
async function validatePoliciesDownstreamReferences(policiesDir) {
|
|
9173
9312
|
const issues = [];
|
|
9174
9313
|
for (const fileName of POLICIES_FILES) {
|
|
9175
|
-
const filePath =
|
|
9314
|
+
const filePath = path31.join(policiesDir, fileName);
|
|
9176
9315
|
const text = await readSafe(filePath);
|
|
9177
9316
|
if (text.trim().length === 0) {
|
|
9178
9317
|
continue;
|
|
@@ -9245,7 +9384,7 @@ async function validateDownstreamRefsForV1421(entry) {
|
|
|
9245
9384
|
issues.push(
|
|
9246
9385
|
issue(
|
|
9247
9386
|
"TRACE_DOWNSTREAM_REF",
|
|
9248
|
-
`${
|
|
9387
|
+
`${path31.basename(check.filePath)} \u3067\u4E0B\u4F4D\u30EC\u30A4\u30E4\u30FC\u53C2\u7167\u306F\u7981\u6B62\u3067\u3059: ${refs.join(", ")}`,
|
|
9249
9388
|
"error",
|
|
9250
9389
|
check.filePath,
|
|
9251
9390
|
"layeredTraceability.downstream",
|
|
@@ -9275,13 +9414,13 @@ function resolveLayerFromId(id) {
|
|
|
9275
9414
|
async function collectMarkdownFiles(policiesDir) {
|
|
9276
9415
|
try {
|
|
9277
9416
|
const entries = await readdir6(policiesDir, { withFileTypes: true });
|
|
9278
|
-
return entries.filter((entry) => entry.isFile() &&
|
|
9417
|
+
return entries.filter((entry) => entry.isFile() && path31.extname(entry.name).toLowerCase() === ".md").map((entry) => path31.join(policiesDir, entry.name)).sort((left, right) => left.localeCompare(right));
|
|
9279
9418
|
} catch {
|
|
9280
9419
|
return [];
|
|
9281
9420
|
}
|
|
9282
9421
|
}
|
|
9283
9422
|
async function validateSpecRootParent(entry) {
|
|
9284
|
-
const specPath =
|
|
9423
|
+
const specPath = path31.join(entry.dir, "01_Spec.md");
|
|
9285
9424
|
const text = await readSafe(specPath);
|
|
9286
9425
|
if (text.trim().length > 0 && /\bCAP-\d{4}\b/.test(text)) {
|
|
9287
9426
|
return [];
|
|
@@ -9382,10 +9521,10 @@ async function validateForbiddenRefs(filePath, pattern, messagePrefix) {
|
|
|
9382
9521
|
|
|
9383
9522
|
// src/core/validators/legacyStatusDir.ts
|
|
9384
9523
|
import { readdir as readdir7, stat as stat5 } from "fs/promises";
|
|
9385
|
-
import
|
|
9524
|
+
import path32 from "path";
|
|
9386
9525
|
var BASELINE_STATUS_ARTIFACTS = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
|
|
9387
9526
|
async function validateLegacyStatusDir(root) {
|
|
9388
|
-
const statusDir =
|
|
9527
|
+
const statusDir = path32.join(root, ".qfai", "status");
|
|
9389
9528
|
if (!await isDirectory(statusDir)) {
|
|
9390
9529
|
return [];
|
|
9391
9530
|
}
|
|
@@ -9437,15 +9576,15 @@ async function isDirectory(target) {
|
|
|
9437
9576
|
}
|
|
9438
9577
|
|
|
9439
9578
|
// src/core/validators/mermaidEnforcement.ts
|
|
9440
|
-
import { readFile as
|
|
9441
|
-
import
|
|
9579
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
9580
|
+
import path33 from "path";
|
|
9442
9581
|
var TARGETS = [
|
|
9443
9582
|
{ segments: [".qfai", "specs"], extensions: [".md", ".feature"] },
|
|
9444
9583
|
{ segments: [".qfai", "discussion"], extensions: [".md"] }
|
|
9445
9584
|
];
|
|
9446
9585
|
var BUSINESS_FLOW_RELATIVE_CANDIDATES = [
|
|
9447
|
-
|
|
9448
|
-
|
|
9586
|
+
path33.join(".qfai", "specs", "_policies", "04_Business-Flow.md"),
|
|
9587
|
+
path33.join(".qfai", "specs", "_policies", "04_Business-flow.md")
|
|
9449
9588
|
];
|
|
9450
9589
|
var MERMAID_DIRECTIVE_RE = /^\s*(?:sequenceDiagram|flowchart|erDiagram|classDiagram|stateDiagram(?:-v2)?|journey|gantt|graph\s+(?:TB|BT|RL|LR|TD))\b/i;
|
|
9451
9590
|
var FLOW_OR_SEQUENCE_RE2 = /\b(?:sequenceDiagram|flowchart)\b/i;
|
|
@@ -9455,13 +9594,13 @@ async function validateMermaidEnforcement(root) {
|
|
|
9455
9594
|
const issues = [];
|
|
9456
9595
|
const scanResults = /* @__PURE__ */ new Map();
|
|
9457
9596
|
for (const filePath of targetFiles) {
|
|
9458
|
-
const text = await
|
|
9597
|
+
const text = await readFile25(filePath, "utf-8");
|
|
9459
9598
|
const result = scanMermaidUsage(filePath, text);
|
|
9460
9599
|
scanResults.set(filePath, result);
|
|
9461
9600
|
issues.push(...result.issues);
|
|
9462
9601
|
}
|
|
9463
9602
|
if (businessFlowPath) {
|
|
9464
|
-
const businessFlowScan = scanResults.get(businessFlowPath) ?? scanMermaidUsage(businessFlowPath, await
|
|
9603
|
+
const businessFlowScan = scanResults.get(businessFlowPath) ?? scanMermaidUsage(businessFlowPath, await readFile25(businessFlowPath, "utf-8"));
|
|
9465
9604
|
if (businessFlowScan.mermaidFenceCount === 0) {
|
|
9466
9605
|
issues.push(
|
|
9467
9606
|
issue(
|
|
@@ -9489,7 +9628,7 @@ async function validateMermaidEnforcement(root) {
|
|
|
9489
9628
|
)
|
|
9490
9629
|
);
|
|
9491
9630
|
}
|
|
9492
|
-
if (
|
|
9631
|
+
if (path33.basename(businessFlowPath) === "04_Business-flow.md") {
|
|
9493
9632
|
issues.push(
|
|
9494
9633
|
issue(
|
|
9495
9634
|
"QFAI-MMD-005",
|
|
@@ -9510,14 +9649,14 @@ async function validateMermaidEnforcement(root) {
|
|
|
9510
9649
|
async function collectTargetFiles(root) {
|
|
9511
9650
|
const files = [];
|
|
9512
9651
|
for (const target of TARGETS) {
|
|
9513
|
-
const targetDir =
|
|
9652
|
+
const targetDir = path33.join(root, ...target.segments);
|
|
9514
9653
|
files.push(
|
|
9515
9654
|
...await collectFiles(targetDir, {
|
|
9516
9655
|
extensions: [...target.extensions]
|
|
9517
9656
|
})
|
|
9518
9657
|
);
|
|
9519
9658
|
}
|
|
9520
|
-
return Array.from(new Set(files)).filter((filePath) =>
|
|
9659
|
+
return Array.from(new Set(files)).filter((filePath) => path33.basename(filePath).toLowerCase() !== "readme.md").sort((a, b) => a.localeCompare(b));
|
|
9521
9660
|
}
|
|
9522
9661
|
function scanMermaidUsage(filePath, text) {
|
|
9523
9662
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
@@ -9607,13 +9746,13 @@ function parseFenceStart(line) {
|
|
|
9607
9746
|
};
|
|
9608
9747
|
}
|
|
9609
9748
|
async function collectDeprecatedBusinessFlowFeatureWarnings(root) {
|
|
9610
|
-
const policiesDir =
|
|
9749
|
+
const policiesDir = path33.join(root, ".qfai", "specs", "_policies");
|
|
9611
9750
|
const featureFiles = await collectFiles(policiesDir, {
|
|
9612
9751
|
extensions: [".feature"]
|
|
9613
9752
|
});
|
|
9614
9753
|
const issues = [];
|
|
9615
9754
|
for (const filePath of featureFiles) {
|
|
9616
|
-
if (!/business-flow/i.test(
|
|
9755
|
+
if (!/business-flow/i.test(path33.basename(filePath))) {
|
|
9617
9756
|
continue;
|
|
9618
9757
|
}
|
|
9619
9758
|
issues.push(
|
|
@@ -9633,7 +9772,7 @@ async function collectDeprecatedBusinessFlowFeatureWarnings(root) {
|
|
|
9633
9772
|
}
|
|
9634
9773
|
async function resolveBusinessFlowPath(root) {
|
|
9635
9774
|
for (const relativePath of BUSINESS_FLOW_RELATIVE_CANDIDATES) {
|
|
9636
|
-
const target =
|
|
9775
|
+
const target = path33.join(root, relativePath);
|
|
9637
9776
|
if (await exists5(target)) {
|
|
9638
9777
|
return target;
|
|
9639
9778
|
}
|
|
@@ -9771,7 +9910,7 @@ function normalizeHeaderKey(column) {
|
|
|
9771
9910
|
}
|
|
9772
9911
|
|
|
9773
9912
|
// src/core/validators/orphanProhibition.ts
|
|
9774
|
-
import
|
|
9913
|
+
import path34 from "path";
|
|
9775
9914
|
async function validateOrphanProhibition(root, config) {
|
|
9776
9915
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
9777
9916
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -9782,9 +9921,9 @@ async function validateOrphanProhibition(root, config) {
|
|
|
9782
9921
|
return [];
|
|
9783
9922
|
}
|
|
9784
9923
|
const issues = [];
|
|
9785
|
-
const policiesDir = layeredEntries[0]?.sharedDir ??
|
|
9924
|
+
const policiesDir = layeredEntries[0]?.sharedDir ?? path34.join(specsRoot, "_policies");
|
|
9786
9925
|
const capIds = new Set(
|
|
9787
|
-
uniqueMatches(await readSafe(
|
|
9926
|
+
uniqueMatches(await readSafe(path34.join(policiesDir, "03_Capabilities.md")), /\bCAP-\d{4}\b/g)
|
|
9788
9927
|
);
|
|
9789
9928
|
for (const entry of layeredEntries) {
|
|
9790
9929
|
const usItems = collectMarkdownItems(await readSafe(entry.userStoriesPath), "US");
|
|
@@ -9934,8 +10073,8 @@ function validateExParentExists(filePath, exItems, acIds, brIds) {
|
|
|
9934
10073
|
}
|
|
9935
10074
|
|
|
9936
10075
|
// src/core/validators/prototypingEvidence.ts
|
|
9937
|
-
import { access as access9, readFile as
|
|
9938
|
-
import
|
|
10076
|
+
import { access as access9, readFile as readFile26 } from "fs/promises";
|
|
10077
|
+
import path35 from "path";
|
|
9939
10078
|
var EVIDENCE_MARKDOWN_FILE = "prototyping.md";
|
|
9940
10079
|
var EVIDENCE_JSON_FILE = "prototyping.json";
|
|
9941
10080
|
async function validatePrototypingEvidence(root, config) {
|
|
@@ -9944,10 +10083,10 @@ async function validatePrototypingEvidence(root, config) {
|
|
|
9944
10083
|
if (specEntries.length === 0) {
|
|
9945
10084
|
return [];
|
|
9946
10085
|
}
|
|
9947
|
-
const qfaiRoot =
|
|
9948
|
-
const evidenceRoot =
|
|
9949
|
-
const evidenceMarkdownPath =
|
|
9950
|
-
const evidenceJsonPath =
|
|
10086
|
+
const qfaiRoot = path35.dirname(specsRoot);
|
|
10087
|
+
const evidenceRoot = path35.join(qfaiRoot, "evidence");
|
|
10088
|
+
const evidenceMarkdownPath = path35.join(evidenceRoot, EVIDENCE_MARKDOWN_FILE);
|
|
10089
|
+
const evidenceJsonPath = path35.join(evidenceRoot, EVIDENCE_JSON_FILE);
|
|
9951
10090
|
const [markdownRaw, jsonRaw] = await Promise.all([
|
|
9952
10091
|
readSafe11(evidenceMarkdownPath),
|
|
9953
10092
|
readSafe11(evidenceJsonPath)
|
|
@@ -10124,7 +10263,7 @@ function extractSpecRefs(rows) {
|
|
|
10124
10263
|
}
|
|
10125
10264
|
async function readSafe11(filePath) {
|
|
10126
10265
|
try {
|
|
10127
|
-
return await
|
|
10266
|
+
return await readFile26(filePath, "utf-8");
|
|
10128
10267
|
} catch {
|
|
10129
10268
|
return null;
|
|
10130
10269
|
}
|
|
@@ -10280,7 +10419,7 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
|
|
|
10280
10419
|
for (const screen of uiFidelity.screens) {
|
|
10281
10420
|
const contractFiles = contractIndex.idToFiles.get(screen.uiContractId);
|
|
10282
10421
|
const contractFile = contractFiles ? Array.from(contractFiles).sort((left, right) => left.localeCompare(right))[0] : void 0;
|
|
10283
|
-
const contractRefFile = contractFile ? toPosixPath3(
|
|
10422
|
+
const contractRefFile = contractFile ? toPosixPath3(path35.relative(root, contractFile)) : void 0;
|
|
10284
10423
|
if (!contractFiles || contractFiles.size === 0) {
|
|
10285
10424
|
mismatches.push({
|
|
10286
10425
|
contractId: screen.uiContractId,
|
|
@@ -11089,7 +11228,7 @@ async function collectMissingRenderArtifacts(root, render) {
|
|
|
11089
11228
|
{ label: "htmlPath", target: render.htmlPath }
|
|
11090
11229
|
];
|
|
11091
11230
|
for (const candidate of candidates) {
|
|
11092
|
-
const resolved =
|
|
11231
|
+
const resolved = path35.isAbsolute(candidate.target) ? candidate.target : path35.resolve(root, candidate.target);
|
|
11093
11232
|
try {
|
|
11094
11233
|
await access9(resolved);
|
|
11095
11234
|
} catch {
|
|
@@ -11112,12 +11251,12 @@ function formatError9(error) {
|
|
|
11112
11251
|
}
|
|
11113
11252
|
|
|
11114
11253
|
// src/core/validators/requireIndex.ts
|
|
11115
|
-
import { readFile as
|
|
11116
|
-
import
|
|
11254
|
+
import { readFile as readFile27 } from "fs/promises";
|
|
11255
|
+
import path36 from "path";
|
|
11117
11256
|
|
|
11118
11257
|
// src/core/validators/repositoryHygiene.ts
|
|
11119
11258
|
import { readdir as readdir8, stat as stat6 } from "fs/promises";
|
|
11120
|
-
import
|
|
11259
|
+
import path37 from "path";
|
|
11121
11260
|
var LEGACY_DIR_RULES = [
|
|
11122
11261
|
{ legacy: "discussions", canonical: "discussion" },
|
|
11123
11262
|
{ legacy: "discuss", canonical: "discussion" },
|
|
@@ -11128,11 +11267,11 @@ var LEGACY_DIR_RULES = [
|
|
|
11128
11267
|
];
|
|
11129
11268
|
var SUSPICIOUS_TEMPLATE_NAME_RE = /^(?:_?templates?|_?sample(?:s)?|sample-template)$/i;
|
|
11130
11269
|
async function validateRepositoryHygiene(root, config) {
|
|
11131
|
-
const qfaiRoot =
|
|
11270
|
+
const qfaiRoot = path37.join(root, ".qfai");
|
|
11132
11271
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
11133
11272
|
const issues = [];
|
|
11134
11273
|
for (const rule of LEGACY_DIR_RULES) {
|
|
11135
|
-
const legacyPath =
|
|
11274
|
+
const legacyPath = path37.join(qfaiRoot, rule.legacy);
|
|
11136
11275
|
if (!await isDirectory2(legacyPath)) {
|
|
11137
11276
|
continue;
|
|
11138
11277
|
}
|
|
@@ -11187,10 +11326,10 @@ async function collectSuspiciousTemplatePaths(root) {
|
|
|
11187
11326
|
continue;
|
|
11188
11327
|
}
|
|
11189
11328
|
for (const entry of entries) {
|
|
11190
|
-
const absolute =
|
|
11329
|
+
const absolute = path37.join(current, entry.name);
|
|
11191
11330
|
if (entry.isDirectory()) {
|
|
11192
11331
|
if (SUSPICIOUS_TEMPLATE_NAME_RE.test(entry.name)) {
|
|
11193
|
-
matches.push(toPosix(
|
|
11332
|
+
matches.push(toPosix(path37.relative(root, absolute)));
|
|
11194
11333
|
}
|
|
11195
11334
|
queue.push(absolute);
|
|
11196
11335
|
continue;
|
|
@@ -11198,9 +11337,9 @@ async function collectSuspiciousTemplatePaths(root) {
|
|
|
11198
11337
|
if (!entry.isFile()) {
|
|
11199
11338
|
continue;
|
|
11200
11339
|
}
|
|
11201
|
-
const baseName =
|
|
11340
|
+
const baseName = path37.parse(entry.name).name;
|
|
11202
11341
|
if (SUSPICIOUS_TEMPLATE_NAME_RE.test(baseName)) {
|
|
11203
|
-
matches.push(toPosix(
|
|
11342
|
+
matches.push(toPosix(path37.relative(root, absolute)));
|
|
11204
11343
|
}
|
|
11205
11344
|
}
|
|
11206
11345
|
}
|
|
@@ -11218,7 +11357,7 @@ function toPosix(value) {
|
|
|
11218
11357
|
}
|
|
11219
11358
|
|
|
11220
11359
|
// src/core/validators/specSplitByCapability.ts
|
|
11221
|
-
import
|
|
11360
|
+
import path38 from "path";
|
|
11222
11361
|
var CAP_ID_RE2 = /\bCAP-\d{4}\b/g;
|
|
11223
11362
|
async function validateSpecSplitByCapability(root, config) {
|
|
11224
11363
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
@@ -11229,8 +11368,8 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
11229
11368
|
if (layeredEntries.length === 0) {
|
|
11230
11369
|
return [];
|
|
11231
11370
|
}
|
|
11232
|
-
const policiesDir = layeredEntries[0]?.sharedDir ??
|
|
11233
|
-
const capabilitiesPath =
|
|
11371
|
+
const policiesDir = layeredEntries[0]?.sharedDir ?? path38.join(specsRoot, "_policies");
|
|
11372
|
+
const capabilitiesPath = path38.join(policiesDir, "03_Capabilities.md");
|
|
11234
11373
|
const capabilityText = await readSafe(capabilitiesPath);
|
|
11235
11374
|
const issues = [];
|
|
11236
11375
|
if (!await exists5(capabilitiesPath)) {
|
|
@@ -11270,7 +11409,7 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
11270
11409
|
);
|
|
11271
11410
|
}
|
|
11272
11411
|
const actualSpecIds = new Set(
|
|
11273
|
-
layeredEntries.map((entry) =>
|
|
11412
|
+
layeredEntries.map((entry) => path38.basename(entry.dir).toLowerCase())
|
|
11274
11413
|
);
|
|
11275
11414
|
const expectedSpecIds = capIds.map((_, index) => `spec-${to4(index + 1)}`);
|
|
11276
11415
|
const missingSpecIds = expectedSpecIds.filter((specId) => !actualSpecIds.has(specId));
|
|
@@ -11307,11 +11446,11 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
11307
11446
|
continue;
|
|
11308
11447
|
}
|
|
11309
11448
|
const specId = `spec-${to4(index + 1)}`;
|
|
11310
|
-
const entry = layeredEntries.find((value) =>
|
|
11449
|
+
const entry = layeredEntries.find((value) => path38.basename(value.dir).toLowerCase() === specId);
|
|
11311
11450
|
if (!entry) {
|
|
11312
11451
|
continue;
|
|
11313
11452
|
}
|
|
11314
|
-
const specFilePath =
|
|
11453
|
+
const specFilePath = path38.join(entry.dir, "01_Spec.md");
|
|
11315
11454
|
const specText = await readSafe(specFilePath);
|
|
11316
11455
|
if (specText.trim().length === 0 || !specText.includes(capId)) {
|
|
11317
11456
|
issues.push(
|
|
@@ -11330,8 +11469,8 @@ async function validateSpecSplitByCapability(root, config) {
|
|
|
11330
11469
|
}
|
|
11331
11470
|
|
|
11332
11471
|
// src/core/validators/statusInSpecs.ts
|
|
11333
|
-
import { readFile as
|
|
11334
|
-
import
|
|
11472
|
+
import { readFile as readFile28 } from "fs/promises";
|
|
11473
|
+
import path39 from "path";
|
|
11335
11474
|
var STRONG_PATTERNS = [
|
|
11336
11475
|
{ label: "release_candidate:", pattern: /\brelease_candidate\s*:/i },
|
|
11337
11476
|
{ label: "status:", pattern: /^\s*(?:-\s*)?status\s*:/im },
|
|
@@ -11388,23 +11527,23 @@ function hasMatch(text, pattern) {
|
|
|
11388
11527
|
}
|
|
11389
11528
|
async function readSafe12(filePath) {
|
|
11390
11529
|
try {
|
|
11391
|
-
return await
|
|
11530
|
+
return await readFile28(filePath, "utf-8");
|
|
11392
11531
|
} catch {
|
|
11393
11532
|
return "";
|
|
11394
11533
|
}
|
|
11395
11534
|
}
|
|
11396
11535
|
function isOpenQuestionsFile(filePath) {
|
|
11397
|
-
return /open-questions\.md$/i.test(
|
|
11536
|
+
return /open-questions\.md$/i.test(path39.basename(filePath));
|
|
11398
11537
|
}
|
|
11399
11538
|
|
|
11400
11539
|
// src/core/validators/designToken.ts
|
|
11401
|
-
import { readFile as
|
|
11402
|
-
import
|
|
11540
|
+
import { readFile as readFile29 } from "fs/promises";
|
|
11541
|
+
import path40 from "path";
|
|
11403
11542
|
import fg2 from "fast-glob";
|
|
11404
|
-
import { parse as
|
|
11543
|
+
import { parse as parseYaml7 } from "yaml";
|
|
11405
11544
|
|
|
11406
11545
|
// src/core/parse/designToken.ts
|
|
11407
|
-
import { parse as
|
|
11546
|
+
import { parse as parseYaml6 } from "yaml";
|
|
11408
11547
|
var REF_PATTERN = /\{([^}]+)\}/g;
|
|
11409
11548
|
var MAX_RESOLVE_DEPTH = 10;
|
|
11410
11549
|
function parseDesignToken(yamlContent) {
|
|
@@ -11417,7 +11556,7 @@ function parseDesignToken(yamlContent) {
|
|
|
11417
11556
|
};
|
|
11418
11557
|
let parsed;
|
|
11419
11558
|
try {
|
|
11420
|
-
parsed =
|
|
11559
|
+
parsed = parseYaml6(yamlContent);
|
|
11421
11560
|
} catch (error) {
|
|
11422
11561
|
const msg = error instanceof Error ? error.message : String(error);
|
|
11423
11562
|
result.errors.push({ message: `YAML parse error: ${msg}` });
|
|
@@ -11446,7 +11585,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
11446
11585
|
}
|
|
11447
11586
|
function flattenTokens(obj, prefix, target, errors) {
|
|
11448
11587
|
for (const [key, value] of Object.entries(obj)) {
|
|
11449
|
-
const
|
|
11588
|
+
const path58 = `${prefix}.${key}`;
|
|
11450
11589
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11451
11590
|
const record2 = value;
|
|
11452
11591
|
if ("$value" in record2) {
|
|
@@ -11462,9 +11601,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
11462
11601
|
if (typeof record2.platform === "string") {
|
|
11463
11602
|
token.platform = record2.platform;
|
|
11464
11603
|
}
|
|
11465
|
-
target.set(
|
|
11604
|
+
target.set(path58, token);
|
|
11466
11605
|
} else {
|
|
11467
|
-
flattenTokens(record2,
|
|
11606
|
+
flattenTokens(record2, path58, target, errors);
|
|
11468
11607
|
}
|
|
11469
11608
|
}
|
|
11470
11609
|
}
|
|
@@ -11474,44 +11613,44 @@ function resolveAllReferences(result) {
|
|
|
11474
11613
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
11475
11614
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
11476
11615
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
11477
|
-
for (const [
|
|
11478
|
-
resolveTokenRef(
|
|
11616
|
+
for (const [path58] of allTokens) {
|
|
11617
|
+
resolveTokenRef(path58, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
11479
11618
|
}
|
|
11480
11619
|
}
|
|
11481
|
-
function resolveTokenRef(
|
|
11482
|
-
if (result.resolved.has(
|
|
11483
|
-
return result.resolved.get(
|
|
11620
|
+
function resolveTokenRef(path58, allTokens, visited, depth, result) {
|
|
11621
|
+
if (result.resolved.has(path58)) {
|
|
11622
|
+
return result.resolved.get(path58);
|
|
11484
11623
|
}
|
|
11485
11624
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
11486
11625
|
result.errors.push({
|
|
11487
|
-
message: `Max reference depth exceeded at: ${
|
|
11488
|
-
path:
|
|
11626
|
+
message: `Max reference depth exceeded at: ${path58}`,
|
|
11627
|
+
path: path58
|
|
11489
11628
|
});
|
|
11490
11629
|
return void 0;
|
|
11491
11630
|
}
|
|
11492
|
-
if (visited.has(
|
|
11631
|
+
if (visited.has(path58)) {
|
|
11493
11632
|
result.errors.push({
|
|
11494
|
-
message: `Circular reference detected: ${
|
|
11495
|
-
path:
|
|
11633
|
+
message: `Circular reference detected: ${path58}`,
|
|
11634
|
+
path: path58
|
|
11496
11635
|
});
|
|
11497
11636
|
return void 0;
|
|
11498
11637
|
}
|
|
11499
|
-
const token = allTokens.get(
|
|
11638
|
+
const token = allTokens.get(path58);
|
|
11500
11639
|
if (!token) {
|
|
11501
11640
|
return void 0;
|
|
11502
11641
|
}
|
|
11503
11642
|
if (typeof token.$value !== "string") {
|
|
11504
11643
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11505
|
-
result.resolved.set(
|
|
11644
|
+
result.resolved.set(path58, rawValue2);
|
|
11506
11645
|
return rawValue2;
|
|
11507
11646
|
}
|
|
11508
11647
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11509
11648
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11510
11649
|
if (refs.length === 0) {
|
|
11511
|
-
result.resolved.set(
|
|
11650
|
+
result.resolved.set(path58, rawValue);
|
|
11512
11651
|
return rawValue;
|
|
11513
11652
|
}
|
|
11514
|
-
visited.add(
|
|
11653
|
+
visited.add(path58);
|
|
11515
11654
|
let resolved = rawValue;
|
|
11516
11655
|
for (const ref of refs) {
|
|
11517
11656
|
const refPath = ref[1];
|
|
@@ -11519,8 +11658,8 @@ function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
|
11519
11658
|
const refToken = allTokens.get(refPath);
|
|
11520
11659
|
if (!refToken) {
|
|
11521
11660
|
result.errors.push({
|
|
11522
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11523
|
-
path:
|
|
11661
|
+
message: `Unresolved token reference: {${refPath}} at ${path58}`,
|
|
11662
|
+
path: path58
|
|
11524
11663
|
});
|
|
11525
11664
|
continue;
|
|
11526
11665
|
}
|
|
@@ -11529,7 +11668,7 @@ function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
|
11529
11668
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11530
11669
|
}
|
|
11531
11670
|
}
|
|
11532
|
-
result.resolved.set(
|
|
11671
|
+
result.resolved.set(path58, resolved);
|
|
11533
11672
|
return resolved;
|
|
11534
11673
|
}
|
|
11535
11674
|
function stringifyTokenValue(value) {
|
|
@@ -11584,8 +11723,8 @@ var VALID_TYPES = [
|
|
|
11584
11723
|
var VALID_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
|
|
11585
11724
|
async function validateDesignToken(root, config) {
|
|
11586
11725
|
const configuredDir = config.uiux?.designTokensDir;
|
|
11587
|
-
const designDir = configuredDir ?
|
|
11588
|
-
const pattern =
|
|
11726
|
+
const designDir = configuredDir ? path40.resolve(root, configuredDir) : path40.join(root, config.paths.contractsDir, "design");
|
|
11727
|
+
const pattern = path40.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
|
|
11589
11728
|
const files = await fg2(pattern, {
|
|
11590
11729
|
absolute: true,
|
|
11591
11730
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -11595,11 +11734,11 @@ async function validateDesignToken(root, config) {
|
|
|
11595
11734
|
}
|
|
11596
11735
|
const issues = [];
|
|
11597
11736
|
for (const filePath of files) {
|
|
11598
|
-
const rel =
|
|
11737
|
+
const rel = path40.relative(root, filePath).replace(/\\/g, "/");
|
|
11599
11738
|
let hasRootObjectError = false;
|
|
11600
11739
|
let content;
|
|
11601
11740
|
try {
|
|
11602
|
-
content = await
|
|
11741
|
+
content = await readFile29(filePath, "utf-8");
|
|
11603
11742
|
} catch {
|
|
11604
11743
|
issues.push(
|
|
11605
11744
|
issue(
|
|
@@ -11613,7 +11752,7 @@ async function validateDesignToken(root, config) {
|
|
|
11613
11752
|
continue;
|
|
11614
11753
|
}
|
|
11615
11754
|
try {
|
|
11616
|
-
const parsed =
|
|
11755
|
+
const parsed = parseYaml7(content);
|
|
11617
11756
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11618
11757
|
hasRootObjectError = true;
|
|
11619
11758
|
issues.push(
|
|
@@ -11762,8 +11901,8 @@ function normalizePlatform(value) {
|
|
|
11762
11901
|
}
|
|
11763
11902
|
|
|
11764
11903
|
// src/core/validators/htmlMock.ts
|
|
11765
|
-
import { readFile as
|
|
11766
|
-
import
|
|
11904
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
11905
|
+
import path41 from "path";
|
|
11767
11906
|
import fg3 from "fast-glob";
|
|
11768
11907
|
|
|
11769
11908
|
// src/core/uiux/contrastRatio.ts
|
|
@@ -12143,19 +12282,19 @@ async function validateHtmlMock(root, platform, config) {
|
|
|
12143
12282
|
const startTime = performance.now();
|
|
12144
12283
|
const budget = config.uiux?.htmlMockTimeout ?? 2e3;
|
|
12145
12284
|
const patterns = [
|
|
12146
|
-
|
|
12147
|
-
|
|
12285
|
+
path41.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
|
|
12286
|
+
path41.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
|
|
12148
12287
|
];
|
|
12149
12288
|
const files = await fg3(patterns, { absolute: true });
|
|
12150
12289
|
const mockBlocks = [];
|
|
12151
12290
|
for (const filePath of files) {
|
|
12152
12291
|
let content;
|
|
12153
12292
|
try {
|
|
12154
|
-
content = await
|
|
12293
|
+
content = await readFile30(filePath, "utf-8");
|
|
12155
12294
|
} catch {
|
|
12156
12295
|
continue;
|
|
12157
12296
|
}
|
|
12158
|
-
const rel =
|
|
12297
|
+
const rel = path41.relative(root, filePath).replace(/\\/g, "/");
|
|
12159
12298
|
for (const block of collectHtmlMockBlocks(content)) {
|
|
12160
12299
|
mockBlocks.push({ file: rel, html: block.html, rawBlock: block.rawBlock });
|
|
12161
12300
|
}
|
|
@@ -12321,8 +12460,8 @@ async function validateHtmlMock(root, platform, config) {
|
|
|
12321
12460
|
}
|
|
12322
12461
|
|
|
12323
12462
|
// src/core/validators/mermaidScreenFlow.ts
|
|
12324
|
-
import { readFile as
|
|
12325
|
-
import
|
|
12463
|
+
import { readFile as readFile31 } from "fs/promises";
|
|
12464
|
+
import path42 from "path";
|
|
12326
12465
|
import fg4 from "fast-glob";
|
|
12327
12466
|
|
|
12328
12467
|
// src/core/validators/mermaidUtils.ts
|
|
@@ -12373,18 +12512,18 @@ var FLOWCHART_RE = /^\s*flowchart\s+(TD|LR|TB|RL|BT)\b/;
|
|
|
12373
12512
|
async function validateMermaidScreenFlow(root, config) {
|
|
12374
12513
|
const issues = [];
|
|
12375
12514
|
const patterns = [
|
|
12376
|
-
|
|
12377
|
-
|
|
12515
|
+
path42.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
|
|
12516
|
+
path42.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
|
|
12378
12517
|
];
|
|
12379
12518
|
const files = await fg4(patterns, { absolute: true });
|
|
12380
12519
|
for (const filePath of files) {
|
|
12381
12520
|
let content;
|
|
12382
12521
|
try {
|
|
12383
|
-
content = await
|
|
12522
|
+
content = await readFile31(filePath, "utf-8");
|
|
12384
12523
|
} catch {
|
|
12385
12524
|
continue;
|
|
12386
12525
|
}
|
|
12387
|
-
const rel =
|
|
12526
|
+
const rel = path42.relative(root, filePath).replace(/\\/g, "/");
|
|
12388
12527
|
const blocks = extractFencedCodeBlocks(content);
|
|
12389
12528
|
for (const block of blocks) {
|
|
12390
12529
|
if (block.language !== "mermaid") continue;
|
|
@@ -12482,10 +12621,10 @@ function parseTransitions(content) {
|
|
|
12482
12621
|
}
|
|
12483
12622
|
|
|
12484
12623
|
// src/core/validators/bpApDb.ts
|
|
12485
|
-
import { readFile as
|
|
12486
|
-
import
|
|
12624
|
+
import { readFile as readFile32 } from "fs/promises";
|
|
12625
|
+
import path43 from "path";
|
|
12487
12626
|
import fg5 from "fast-glob";
|
|
12488
|
-
import { parse as
|
|
12627
|
+
import { parse as parseYaml8 } from "yaml";
|
|
12489
12628
|
var BP_ID_RE = /^BP-\d{4}$/;
|
|
12490
12629
|
var AP_ID_RE = /^AP-\d{4}$/;
|
|
12491
12630
|
var VALID_SEVERITIES = ["critical", "major", "minor"];
|
|
@@ -12520,9 +12659,9 @@ var AP_REQUIRED_FIELDS = [
|
|
|
12520
12659
|
];
|
|
12521
12660
|
async function validateBpApDb(root, config) {
|
|
12522
12661
|
const issues = [];
|
|
12523
|
-
const designDir =
|
|
12524
|
-
const bpPattern =
|
|
12525
|
-
const apPattern =
|
|
12662
|
+
const designDir = path43.join(root, config.paths.contractsDir, "design");
|
|
12663
|
+
const bpPattern = path43.posix.join(designDir.replace(/\\/g, "/"), "best-practices*.yaml");
|
|
12664
|
+
const apPattern = path43.posix.join(designDir.replace(/\\/g, "/"), "anti-patterns*.yaml");
|
|
12526
12665
|
const globOptions = {
|
|
12527
12666
|
absolute: true,
|
|
12528
12667
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -12535,14 +12674,14 @@ async function validateBpApDb(root, config) {
|
|
|
12535
12674
|
const seenBpIds = /* @__PURE__ */ new Set();
|
|
12536
12675
|
const seenApIds = /* @__PURE__ */ new Set();
|
|
12537
12676
|
for (const filePath of bpFiles) {
|
|
12538
|
-
const rel =
|
|
12677
|
+
const rel = path43.relative(root, filePath).replace(/\\/g, "/");
|
|
12539
12678
|
const entries = await parseRuleFile(filePath, rel, issues);
|
|
12540
12679
|
for (const entry of entries) {
|
|
12541
12680
|
validateBpEntry(entry, rel, seenBpIds, issues);
|
|
12542
12681
|
}
|
|
12543
12682
|
}
|
|
12544
12683
|
for (const filePath of apFiles) {
|
|
12545
|
-
const rel =
|
|
12684
|
+
const rel = path43.relative(root, filePath).replace(/\\/g, "/");
|
|
12546
12685
|
const entries = await parseRuleFile(filePath, rel, issues);
|
|
12547
12686
|
for (const entry of entries) {
|
|
12548
12687
|
validateApEntry(entry, rel, seenApIds, issues);
|
|
@@ -12553,7 +12692,7 @@ async function validateBpApDb(root, config) {
|
|
|
12553
12692
|
async function parseRuleFile(filePath, rel, issues) {
|
|
12554
12693
|
let content;
|
|
12555
12694
|
try {
|
|
12556
|
-
content = await
|
|
12695
|
+
content = await readFile32(filePath, "utf-8");
|
|
12557
12696
|
} catch {
|
|
12558
12697
|
issues.push(
|
|
12559
12698
|
issue("QFAI-BPAP-001", `BP/AP file unreadable: ${rel}`, "error", rel, "bpApDb.readFile")
|
|
@@ -12562,7 +12701,7 @@ async function parseRuleFile(filePath, rel, issues) {
|
|
|
12562
12701
|
}
|
|
12563
12702
|
let parsed;
|
|
12564
12703
|
try {
|
|
12565
|
-
parsed =
|
|
12704
|
+
parsed = parseYaml8(content);
|
|
12566
12705
|
} catch (error) {
|
|
12567
12706
|
issues.push(
|
|
12568
12707
|
issue(
|
|
@@ -12717,8 +12856,8 @@ function toSafeString(value) {
|
|
|
12717
12856
|
}
|
|
12718
12857
|
|
|
12719
12858
|
// src/core/validators/platformDetection.ts
|
|
12720
|
-
import { readFile as
|
|
12721
|
-
import
|
|
12859
|
+
import { readFile as readFile33 } from "fs/promises";
|
|
12860
|
+
import path44 from "path";
|
|
12722
12861
|
var KNOWN_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
|
|
12723
12862
|
async function detectPlatform(root, config, cliPlatform) {
|
|
12724
12863
|
const issues = [];
|
|
@@ -12761,13 +12900,13 @@ async function detectPlatform(root, config, cliPlatform) {
|
|
|
12761
12900
|
return { platform: "web", source: "fallback", issues };
|
|
12762
12901
|
}
|
|
12763
12902
|
async function inferPlatform(root, issues) {
|
|
12764
|
-
if (await exists5(
|
|
12765
|
-
const hasAndroid = await exists5(
|
|
12766
|
-
const hasIos = await exists5(
|
|
12767
|
-
const hasWeb = await exists5(
|
|
12768
|
-
const hasWindows = await exists5(
|
|
12769
|
-
const hasMacos = await exists5(
|
|
12770
|
-
const hasLinux = await exists5(
|
|
12903
|
+
if (await exists5(path44.join(root, "pubspec.yaml"))) {
|
|
12904
|
+
const hasAndroid = await exists5(path44.join(root, "android"));
|
|
12905
|
+
const hasIos = await exists5(path44.join(root, "ios"));
|
|
12906
|
+
const hasWeb = await exists5(path44.join(root, "web"));
|
|
12907
|
+
const hasWindows = await exists5(path44.join(root, "windows"));
|
|
12908
|
+
const hasMacos = await exists5(path44.join(root, "macos"));
|
|
12909
|
+
const hasLinux = await exists5(path44.join(root, "linux"));
|
|
12771
12910
|
const mobileTargets = [hasAndroid, hasIos].filter(Boolean).length;
|
|
12772
12911
|
const desktopTargets = [hasWeb, hasWindows, hasMacos, hasLinux].filter(Boolean).length;
|
|
12773
12912
|
if (mobileTargets + desktopTargets > 1) {
|
|
@@ -12787,10 +12926,10 @@ async function inferPlatform(root, issues) {
|
|
|
12787
12926
|
}
|
|
12788
12927
|
return null;
|
|
12789
12928
|
}
|
|
12790
|
-
const pkgJsonPath =
|
|
12929
|
+
const pkgJsonPath = path44.join(root, "package.json");
|
|
12791
12930
|
if (await exists5(pkgJsonPath)) {
|
|
12792
12931
|
try {
|
|
12793
|
-
const raw = await
|
|
12932
|
+
const raw = await readFile33(pkgJsonPath, "utf-8");
|
|
12794
12933
|
const pkg = JSON.parse(raw);
|
|
12795
12934
|
const deps = {
|
|
12796
12935
|
...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
|
|
@@ -12809,8 +12948,8 @@ async function inferPlatform(root, issues) {
|
|
|
12809
12948
|
return "cross-platform";
|
|
12810
12949
|
}
|
|
12811
12950
|
if ("react-native" in deps) {
|
|
12812
|
-
const hasAndroid = await exists5(
|
|
12813
|
-
const hasIos = await exists5(
|
|
12951
|
+
const hasAndroid = await exists5(path44.join(root, "android"));
|
|
12952
|
+
const hasIos = await exists5(path44.join(root, "ios"));
|
|
12814
12953
|
if (hasAndroid && hasIos) {
|
|
12815
12954
|
return "cross-platform";
|
|
12816
12955
|
}
|
|
@@ -12832,15 +12971,15 @@ function normalizePlatformInput(platform) {
|
|
|
12832
12971
|
}
|
|
12833
12972
|
|
|
12834
12973
|
// src/core/validators/uiDefinitionConsistency.ts
|
|
12835
|
-
import { readFile as
|
|
12836
|
-
import
|
|
12974
|
+
import { readFile as readFile34 } from "fs/promises";
|
|
12975
|
+
import path45 from "path";
|
|
12837
12976
|
import fg6 from "fast-glob";
|
|
12838
|
-
import { parse as
|
|
12977
|
+
import { parse as parseYaml9 } from "yaml";
|
|
12839
12978
|
async function validateUiDefinitionConsistency(root, config) {
|
|
12840
12979
|
const issues = [];
|
|
12841
12980
|
const configuredDir = config.uiux?.designTokensDir;
|
|
12842
|
-
const designDir = configuredDir ?
|
|
12843
|
-
const tokenPattern =
|
|
12981
|
+
const designDir = configuredDir ? path45.resolve(root, configuredDir) : path45.join(root, config.paths.contractsDir, "design");
|
|
12982
|
+
const tokenPattern = path45.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
|
|
12844
12983
|
const tokenFiles = await fg6(tokenPattern, {
|
|
12845
12984
|
absolute: true,
|
|
12846
12985
|
ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
|
|
@@ -12848,7 +12987,7 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12848
12987
|
const resolvedTokens = /* @__PURE__ */ new Map();
|
|
12849
12988
|
for (const tokenFile of tokenFiles) {
|
|
12850
12989
|
try {
|
|
12851
|
-
const content = await
|
|
12990
|
+
const content = await readFile34(tokenFile, "utf-8");
|
|
12852
12991
|
const result = parseDesignToken(content);
|
|
12853
12992
|
for (const [key, val] of result.resolved) {
|
|
12854
12993
|
resolvedTokens.set(key, val);
|
|
@@ -12856,14 +12995,14 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12856
12995
|
} catch {
|
|
12857
12996
|
}
|
|
12858
12997
|
}
|
|
12859
|
-
const uiContractDir =
|
|
12860
|
-
const uiPattern =
|
|
12998
|
+
const uiContractDir = path45.join(root, config.paths.contractsDir, "ui");
|
|
12999
|
+
const uiPattern = path45.posix.join(uiContractDir.replace(/\\/g, "/"), "**/*.yaml");
|
|
12861
13000
|
const uiFiles = await fg6(uiPattern, { absolute: true });
|
|
12862
13001
|
const contractScreenIds = /* @__PURE__ */ new Set();
|
|
12863
13002
|
for (const uiFile of uiFiles) {
|
|
12864
13003
|
try {
|
|
12865
|
-
const content = await
|
|
12866
|
-
const parsed =
|
|
13004
|
+
const content = await readFile34(uiFile, "utf-8");
|
|
13005
|
+
const parsed = parseYaml9(content);
|
|
12867
13006
|
if (parsed && typeof parsed === "object") {
|
|
12868
13007
|
const screens = parsed.screens;
|
|
12869
13008
|
if (Array.isArray(screens)) {
|
|
@@ -12878,15 +13017,15 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12878
13017
|
}
|
|
12879
13018
|
}
|
|
12880
13019
|
const mdPatterns = [
|
|
12881
|
-
|
|
12882
|
-
|
|
13020
|
+
path45.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
|
|
13021
|
+
path45.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
|
|
12883
13022
|
];
|
|
12884
13023
|
const mdFiles = await fg6(mdPatterns, { absolute: true });
|
|
12885
13024
|
const mockScreenIds = /* @__PURE__ */ new Set();
|
|
12886
13025
|
for (const mdFile of mdFiles) {
|
|
12887
13026
|
try {
|
|
12888
|
-
const content = await
|
|
12889
|
-
const rel =
|
|
13027
|
+
const content = await readFile34(mdFile, "utf-8");
|
|
13028
|
+
const rel = path45.relative(root, mdFile).replace(/\\/g, "/");
|
|
12890
13029
|
const htmlBlocks = collectHtmlMockBlocks(content);
|
|
12891
13030
|
if (resolvedTokens.size > 0) {
|
|
12892
13031
|
for (const htmlBlock of htmlBlocks) {
|
|
@@ -12943,8 +13082,8 @@ async function validateUiDefinitionConsistency(root, config) {
|
|
|
12943
13082
|
}
|
|
12944
13083
|
|
|
12945
13084
|
// src/core/validators/researchSummary.ts
|
|
12946
|
-
import { readFile as
|
|
12947
|
-
import
|
|
13085
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
13086
|
+
import path46 from "path";
|
|
12948
13087
|
import fg7 from "fast-glob";
|
|
12949
13088
|
var RESEARCH_SUMMARY_HEADING_RE = /^#{1,3}\s+Research\s+Summary/im;
|
|
12950
13089
|
var SOURCE_ENTRY_RE = /^\s*-\s*id:\s*(\S+)/gm;
|
|
@@ -12952,17 +13091,17 @@ var REFLECTION_APPLY_RE = /action:\s*apply/i;
|
|
|
12952
13091
|
var FULL_DATE_RE = /^\s+published:\s*["']?(\d{4}-\d{2}-\d{2})["']?/m;
|
|
12953
13092
|
async function validateResearchSummary(root, config) {
|
|
12954
13093
|
const issues = [];
|
|
12955
|
-
const pattern =
|
|
13094
|
+
const pattern = path46.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
|
|
12956
13095
|
const files = await fg7(pattern, { absolute: true });
|
|
12957
13096
|
for (const filePath of files) {
|
|
12958
13097
|
let content;
|
|
12959
13098
|
try {
|
|
12960
|
-
content = await
|
|
13099
|
+
content = await readFile35(filePath, "utf-8");
|
|
12961
13100
|
} catch {
|
|
12962
13101
|
continue;
|
|
12963
13102
|
}
|
|
12964
13103
|
if (!RESEARCH_SUMMARY_HEADING_RE.test(content)) continue;
|
|
12965
|
-
const rel =
|
|
13104
|
+
const rel = path46.relative(root, filePath).replace(/\\/g, "/");
|
|
12966
13105
|
const section = extractResearchSummarySection(content);
|
|
12967
13106
|
if (!section) continue;
|
|
12968
13107
|
const sourceEntries = extractSourceEntries(section);
|
|
@@ -13193,9 +13332,9 @@ function resolveFreshnessReferenceNow() {
|
|
|
13193
13332
|
}
|
|
13194
13333
|
|
|
13195
13334
|
// src/core/validators/agentDefinition.ts
|
|
13196
|
-
import { readFile as
|
|
13197
|
-
import
|
|
13198
|
-
import { parse as
|
|
13335
|
+
import { readFile as readFile36 } from "fs/promises";
|
|
13336
|
+
import path47 from "path";
|
|
13337
|
+
import { parse as parseYaml10 } from "yaml";
|
|
13199
13338
|
var REQUIRED_AGENTS = [
|
|
13200
13339
|
"uiux-expert.md",
|
|
13201
13340
|
"design-expert.md",
|
|
@@ -13214,13 +13353,13 @@ var REQUIRED_SECTIONS = [
|
|
|
13214
13353
|
var REQUIRED_PHASES = ["discussion", "SDD", "prototyping", "ATDD"];
|
|
13215
13354
|
async function validateAgentDefinition(root, _config) {
|
|
13216
13355
|
const issues = [];
|
|
13217
|
-
const agentsDir =
|
|
13356
|
+
const agentsDir = path47.join(root, ".qfai", "assistant", "agents");
|
|
13218
13357
|
if (!await exists5(agentsDir)) {
|
|
13219
13358
|
return [];
|
|
13220
13359
|
}
|
|
13221
13360
|
let anyAgentExists = false;
|
|
13222
13361
|
for (const agentFile of REQUIRED_AGENTS) {
|
|
13223
|
-
if (await exists5(
|
|
13362
|
+
if (await exists5(path47.join(agentsDir, agentFile))) {
|
|
13224
13363
|
anyAgentExists = true;
|
|
13225
13364
|
break;
|
|
13226
13365
|
}
|
|
@@ -13229,7 +13368,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
13229
13368
|
return [];
|
|
13230
13369
|
}
|
|
13231
13370
|
for (const agentFile of REQUIRED_AGENTS) {
|
|
13232
|
-
const filePath =
|
|
13371
|
+
const filePath = path47.join(agentsDir, agentFile);
|
|
13233
13372
|
const rel = `.qfai/assistant/agents/${agentFile}`;
|
|
13234
13373
|
if (!await exists5(filePath)) {
|
|
13235
13374
|
issues.push(
|
|
@@ -13245,7 +13384,7 @@ async function validateAgentDefinition(root, _config) {
|
|
|
13245
13384
|
}
|
|
13246
13385
|
let content;
|
|
13247
13386
|
try {
|
|
13248
|
-
content = await
|
|
13387
|
+
content = await readFile36(filePath, "utf-8");
|
|
13249
13388
|
} catch {
|
|
13250
13389
|
issues.push(
|
|
13251
13390
|
issue(
|
|
@@ -13351,11 +13490,11 @@ async function validateAgentDefinition(root, _config) {
|
|
|
13351
13490
|
}
|
|
13352
13491
|
}
|
|
13353
13492
|
}
|
|
13354
|
-
const rosterPath =
|
|
13493
|
+
const rosterPath = path47.join(root, ".qfai", "assistant", "steering", "review-roster.yml");
|
|
13355
13494
|
if (await exists5(rosterPath)) {
|
|
13356
13495
|
try {
|
|
13357
|
-
const rosterContent = await
|
|
13358
|
-
const roster =
|
|
13496
|
+
const rosterContent = await readFile36(rosterPath, "utf-8");
|
|
13497
|
+
const roster = parseYaml10(rosterContent);
|
|
13359
13498
|
if (roster && typeof roster === "object") {
|
|
13360
13499
|
const rosterObj = roster;
|
|
13361
13500
|
const entries = Array.isArray(rosterObj.roster) ? rosterObj.roster : [];
|
|
@@ -13399,8 +13538,8 @@ function extractPhaseBody(phaseSection, phaseName) {
|
|
|
13399
13538
|
}
|
|
13400
13539
|
|
|
13401
13540
|
// src/core/validators/tddList.ts
|
|
13402
|
-
import { readFile as
|
|
13403
|
-
import
|
|
13541
|
+
import { readFile as readFile37, stat as stat7 } from "fs/promises";
|
|
13542
|
+
import path48 from "path";
|
|
13404
13543
|
var REQUIRED_COLUMNS = [
|
|
13405
13544
|
"TDD-ID",
|
|
13406
13545
|
"TC-Refs",
|
|
@@ -13414,7 +13553,7 @@ var REQUIRED_COLUMNS = [
|
|
|
13414
13553
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["todo", "red", "green", "refactor", "done", "exception"]);
|
|
13415
13554
|
var TEST_FILE_CHECK_STATUSES = /* @__PURE__ */ new Set(["green", "refactor", "done"]);
|
|
13416
13555
|
var TDD_ID_FORMAT = /^TDD-\d{4}$/;
|
|
13417
|
-
var TDD_LIST_REL_PATH =
|
|
13556
|
+
var TDD_LIST_REL_PATH = path48.join("tdd", "test-list.md");
|
|
13418
13557
|
async function validateTddList(root, config) {
|
|
13419
13558
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
13420
13559
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -13426,8 +13565,8 @@ async function validateTddList(root, config) {
|
|
|
13426
13565
|
return issues;
|
|
13427
13566
|
}
|
|
13428
13567
|
async function validateSpecTddList(root, specDir, specNumber) {
|
|
13429
|
-
const filePath =
|
|
13430
|
-
const relPath =
|
|
13568
|
+
const filePath = path48.join(specDir, TDD_LIST_REL_PATH);
|
|
13569
|
+
const relPath = path48.relative(root, filePath).replace(/\\/g, "/");
|
|
13431
13570
|
const issues = [];
|
|
13432
13571
|
if (!await exists5(filePath)) {
|
|
13433
13572
|
issues.push(
|
|
@@ -13614,9 +13753,9 @@ async function validateSpecTddList(root, specDir, specNumber) {
|
|
|
13614
13753
|
continue;
|
|
13615
13754
|
}
|
|
13616
13755
|
const normalized = testFile.replace(/\\/g, "/");
|
|
13617
|
-
const resolved =
|
|
13618
|
-
const relative =
|
|
13619
|
-
if (
|
|
13756
|
+
const resolved = path48.resolve(root, normalized);
|
|
13757
|
+
const relative = path48.relative(root, resolved);
|
|
13758
|
+
if (path48.isAbsolute(normalized) || path48.win32.isAbsolute(normalized) || relative === ".." || relative.startsWith(".." + path48.sep)) {
|
|
13620
13759
|
issues.push(
|
|
13621
13760
|
issue(
|
|
13622
13761
|
"TDDLIST_TEST_FILE_MISSING",
|
|
@@ -13679,11 +13818,11 @@ async function validateSpecTddList(root, specDir, specNumber) {
|
|
|
13679
13818
|
}
|
|
13680
13819
|
async function collectTestCaseIds(specDir) {
|
|
13681
13820
|
const empty = { knownTcIds: /* @__PURE__ */ new Set(), unitComponentTcIds: /* @__PURE__ */ new Set() };
|
|
13682
|
-
const testCasesPath =
|
|
13821
|
+
const testCasesPath = path48.join(specDir, "06_Test-Cases.md");
|
|
13683
13822
|
if (!await exists5(testCasesPath)) return empty;
|
|
13684
13823
|
let content;
|
|
13685
13824
|
try {
|
|
13686
|
-
content = await
|
|
13825
|
+
content = await readFile37(testCasesPath, "utf-8");
|
|
13687
13826
|
} catch {
|
|
13688
13827
|
return empty;
|
|
13689
13828
|
}
|
|
@@ -13709,8 +13848,8 @@ async function collectTestCaseIds(specDir) {
|
|
|
13709
13848
|
}
|
|
13710
13849
|
|
|
13711
13850
|
// src/core/validators/ddpValidation.ts
|
|
13712
|
-
import { readFile as
|
|
13713
|
-
import
|
|
13851
|
+
import { readFile as readFile38 } from "fs/promises";
|
|
13852
|
+
import path49 from "path";
|
|
13714
13853
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13715
13854
|
import fg8 from "fast-glob";
|
|
13716
13855
|
var DDP_HEADING_RE = /^#{1,3}\s+Design\s+Direction\s+Pack/im;
|
|
@@ -13728,9 +13867,9 @@ var _bannedPatterns = null;
|
|
|
13728
13867
|
async function loadBannedPatterns() {
|
|
13729
13868
|
if (_bannedPatterns !== null) return _bannedPatterns;
|
|
13730
13869
|
try {
|
|
13731
|
-
const thisDir =
|
|
13732
|
-
const filePath =
|
|
13733
|
-
const content = await
|
|
13870
|
+
const thisDir = path49.dirname(fileURLToPath3(import.meta.url));
|
|
13871
|
+
const filePath = path49.join(thisDir, "ddpBannedPatterns.txt");
|
|
13872
|
+
const content = await readFile38(filePath, "utf-8");
|
|
13734
13873
|
_bannedPatterns = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
13735
13874
|
} catch {
|
|
13736
13875
|
_bannedPatterns = [];
|
|
@@ -13739,15 +13878,15 @@ async function loadBannedPatterns() {
|
|
|
13739
13878
|
}
|
|
13740
13879
|
async function validateDdpFields(root, config) {
|
|
13741
13880
|
const issues = [];
|
|
13742
|
-
const pattern =
|
|
13881
|
+
const pattern = path49.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
|
|
13743
13882
|
const files = await fg8(pattern, { absolute: true });
|
|
13744
13883
|
let ddpFound = false;
|
|
13745
13884
|
let isUiBearing2 = false;
|
|
13746
13885
|
for (const filePath of files) {
|
|
13747
|
-
const basename =
|
|
13886
|
+
const basename = path49.basename(filePath);
|
|
13748
13887
|
if (basename === "03_Story-Workshop.md") {
|
|
13749
13888
|
try {
|
|
13750
|
-
const storyContent = await
|
|
13889
|
+
const storyContent = await readFile38(filePath, "utf-8");
|
|
13751
13890
|
if (UI_BEARING_KEYWORDS_RE.test(storyContent)) {
|
|
13752
13891
|
isUiBearing2 = true;
|
|
13753
13892
|
}
|
|
@@ -13758,13 +13897,13 @@ async function validateDdpFields(root, config) {
|
|
|
13758
13897
|
for (const filePath of files) {
|
|
13759
13898
|
let content;
|
|
13760
13899
|
try {
|
|
13761
|
-
content = await
|
|
13900
|
+
content = await readFile38(filePath, "utf-8");
|
|
13762
13901
|
} catch {
|
|
13763
13902
|
continue;
|
|
13764
13903
|
}
|
|
13765
13904
|
if (!DDP_HEADING_RE.test(content)) continue;
|
|
13766
13905
|
ddpFound = true;
|
|
13767
|
-
const rel =
|
|
13906
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
13768
13907
|
const section = extractDdpSection(content);
|
|
13769
13908
|
if (!section) continue;
|
|
13770
13909
|
for (const field of DDP_REQUIRED_FIELDS) {
|
|
@@ -13957,7 +14096,7 @@ var ANTI_PATTERN_CHECKS = [
|
|
|
13957
14096
|
];
|
|
13958
14097
|
async function validateResearchTraceability(root, config, issues) {
|
|
13959
14098
|
const requireResearchSummary = config.uiux?.requireResearchSummary === true;
|
|
13960
|
-
const discussionPattern =
|
|
14099
|
+
const discussionPattern = path49.posix.join(
|
|
13961
14100
|
root.replace(/\\/g, "/"),
|
|
13962
14101
|
config.paths.discussionDir,
|
|
13963
14102
|
"**/*.md"
|
|
@@ -13966,7 +14105,7 @@ async function validateResearchTraceability(root, config, issues) {
|
|
|
13966
14105
|
let hasResearchSummary = false;
|
|
13967
14106
|
for (const filePath of discussionFiles) {
|
|
13968
14107
|
try {
|
|
13969
|
-
const content = await
|
|
14108
|
+
const content = await readFile38(filePath, "utf-8");
|
|
13970
14109
|
if (/research_summary\s*:/m.test(content)) {
|
|
13971
14110
|
hasResearchSummary = true;
|
|
13972
14111
|
break;
|
|
@@ -13985,7 +14124,7 @@ async function validateResearchTraceability(root, config, issues) {
|
|
|
13985
14124
|
)
|
|
13986
14125
|
);
|
|
13987
14126
|
}
|
|
13988
|
-
const contractPattern =
|
|
14127
|
+
const contractPattern = path49.posix.join(
|
|
13989
14128
|
root.replace(/\\/g, "/"),
|
|
13990
14129
|
config.paths.contractsDir,
|
|
13991
14130
|
"design",
|
|
@@ -13995,11 +14134,11 @@ async function validateResearchTraceability(root, config, issues) {
|
|
|
13995
14134
|
for (const filePath of contractFiles) {
|
|
13996
14135
|
let content;
|
|
13997
14136
|
try {
|
|
13998
|
-
content = await
|
|
14137
|
+
content = await readFile38(filePath, "utf-8");
|
|
13999
14138
|
} catch {
|
|
14000
14139
|
continue;
|
|
14001
14140
|
}
|
|
14002
|
-
const rel =
|
|
14141
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
14003
14142
|
const ruleBlocks = content.split(/(?=^-\s+(?:rule|id)\s*:)/m).filter((b) => b.trim());
|
|
14004
14143
|
for (const block of ruleBlocks) {
|
|
14005
14144
|
const idMatch = /(?:^|\n)\s*(?:-\s+)?id\s*:\s*(\S+)/m.exec(block);
|
|
@@ -14022,11 +14161,11 @@ async function validateStoryWorkshopTemplates(root, config, issues, discussionFi
|
|
|
14022
14161
|
for (const filePath of discussionFiles) {
|
|
14023
14162
|
let content;
|
|
14024
14163
|
try {
|
|
14025
|
-
content = await
|
|
14164
|
+
content = await readFile38(filePath, "utf-8");
|
|
14026
14165
|
} catch {
|
|
14027
14166
|
continue;
|
|
14028
14167
|
}
|
|
14029
|
-
const rel =
|
|
14168
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
14030
14169
|
if (/screen_type\s*:\s*list/im.test(content)) {
|
|
14031
14170
|
for (const field of LIST_TEMPLATE_REQUIRED_FIELDS) {
|
|
14032
14171
|
if (!hasNonEmptyField(content, field)) {
|
|
@@ -14126,7 +14265,7 @@ function validateQualityProfile(config, issues) {
|
|
|
14126
14265
|
}
|
|
14127
14266
|
}
|
|
14128
14267
|
async function validateOptionComparison(root, config, issues) {
|
|
14129
|
-
const contractPattern =
|
|
14268
|
+
const contractPattern = path49.posix.join(
|
|
14130
14269
|
root.replace(/\\/g, "/"),
|
|
14131
14270
|
config.paths.contractsDir,
|
|
14132
14271
|
"design",
|
|
@@ -14136,11 +14275,11 @@ async function validateOptionComparison(root, config, issues) {
|
|
|
14136
14275
|
for (const filePath of contractFiles) {
|
|
14137
14276
|
let content;
|
|
14138
14277
|
try {
|
|
14139
|
-
content = await
|
|
14278
|
+
content = await readFile38(filePath, "utf-8");
|
|
14140
14279
|
} catch {
|
|
14141
14280
|
continue;
|
|
14142
14281
|
}
|
|
14143
|
-
const rel =
|
|
14282
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
14144
14283
|
const optionMatches = content.match(/(?:^|\n)\s*-\s+option\s*:/gm) ?? [];
|
|
14145
14284
|
const numberedOptions = content.match(/(?:^|\n)\s*option_\d+\s*:/gm) ?? [];
|
|
14146
14285
|
const totalOptions = optionMatches.length + numberedOptions.length;
|
|
@@ -14175,7 +14314,7 @@ async function validateOptionComparison(root, config, issues) {
|
|
|
14175
14314
|
}
|
|
14176
14315
|
async function validateCompetitiveRefs(root, config, issues) {
|
|
14177
14316
|
const minRefs = config.uiux?.competitive_refs_min ?? 3;
|
|
14178
|
-
const contractPattern =
|
|
14317
|
+
const contractPattern = path49.posix.join(
|
|
14179
14318
|
root.replace(/\\/g, "/"),
|
|
14180
14319
|
config.paths.contractsDir,
|
|
14181
14320
|
"design",
|
|
@@ -14185,11 +14324,11 @@ async function validateCompetitiveRefs(root, config, issues) {
|
|
|
14185
14324
|
for (const filePath of contractFiles) {
|
|
14186
14325
|
let content;
|
|
14187
14326
|
try {
|
|
14188
|
-
content = await
|
|
14327
|
+
content = await readFile38(filePath, "utf-8");
|
|
14189
14328
|
} catch {
|
|
14190
14329
|
continue;
|
|
14191
14330
|
}
|
|
14192
|
-
const rel =
|
|
14331
|
+
const rel = path49.relative(root, filePath).replace(/\\/g, "/");
|
|
14193
14332
|
const refsBlock = extractNestedBlock(content, "competitive_refs");
|
|
14194
14333
|
if (refsBlock !== null) {
|
|
14195
14334
|
const refItems = refsBlock.split(/\n/).filter((l) => /^\s*-\s/.test(l));
|
|
@@ -14339,7 +14478,7 @@ function collectListItems(section, field) {
|
|
|
14339
14478
|
}
|
|
14340
14479
|
|
|
14341
14480
|
// src/core/validators/navigationFlow.ts
|
|
14342
|
-
import { readFile as
|
|
14481
|
+
import { readFile as readFile39 } from "fs/promises";
|
|
14343
14482
|
var NODE_DEF_RE = /([A-Za-z_][\w-]*)\s*(?:\[.*?\]|\(.*?\)|\{.*?\})/g;
|
|
14344
14483
|
var EDGE_RE = /([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?\s*(?:--+>|==+>|-.->|~~>)\s*(?:\|"?([^"|]*)"?\|)?\s*([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?/g;
|
|
14345
14484
|
var SUBGRAPH_RE = /^\s*subgraph\s+([\w-]+)/gm;
|
|
@@ -14618,7 +14757,7 @@ async function validateNavigationFlow(root, config) {
|
|
|
14618
14757
|
const specFiles = allFiles.filter((f) => /[\\/]spec-\d{4}[\\/]/.test(f));
|
|
14619
14758
|
const issues = [];
|
|
14620
14759
|
for (const file of specFiles) {
|
|
14621
|
-
const content = await
|
|
14760
|
+
const content = await readFile39(file, "utf-8");
|
|
14622
14761
|
const blocks = extractFencedCodeBlocks(content);
|
|
14623
14762
|
const mermaidBlocks = blocks.filter((b) => b.language === "mermaid");
|
|
14624
14763
|
const _flowchartBlocks = mermaidBlocks.filter((b) => hasFlowchartDeclaration(b.content));
|
|
@@ -14640,8 +14779,8 @@ async function validateNavigationFlow(root, config) {
|
|
|
14640
14779
|
}
|
|
14641
14780
|
|
|
14642
14781
|
// src/core/validators/renderCritique.ts
|
|
14643
|
-
import
|
|
14644
|
-
import { readFile as
|
|
14782
|
+
import path50 from "path";
|
|
14783
|
+
import { readFile as readFile40 } from "fs/promises";
|
|
14645
14784
|
import fg9 from "fast-glob";
|
|
14646
14785
|
var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
|
|
14647
14786
|
var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
|
|
@@ -14660,8 +14799,8 @@ var FOUR_STATE_CHECK_RE = /\bfour_state_check\s*:/i;
|
|
|
14660
14799
|
var MAX_PRIMARY_STEPS_RE = /\bmax_primary_steps\s*:\s*(\d+)/i;
|
|
14661
14800
|
async function validateRenderCritique(root, config) {
|
|
14662
14801
|
const issues = [];
|
|
14663
|
-
const discussionDir =
|
|
14664
|
-
const discussionFiles = await fg9(
|
|
14802
|
+
const discussionDir = path50.join(root, config.paths.discussionDir).replace(/\\/g, "/");
|
|
14803
|
+
const discussionFiles = await fg9(path50.posix.join(discussionDir, "**/*.md"), { absolute: true });
|
|
14665
14804
|
let hasDdp = false;
|
|
14666
14805
|
for (const df of discussionFiles) {
|
|
14667
14806
|
const content = await readSafe(df);
|
|
@@ -14671,12 +14810,12 @@ async function validateRenderCritique(root, config) {
|
|
|
14671
14810
|
}
|
|
14672
14811
|
}
|
|
14673
14812
|
if (!hasDdp) return issues;
|
|
14674
|
-
const skillsDir =
|
|
14675
|
-
const evidenceDir =
|
|
14813
|
+
const skillsDir = path50.join(root, config.paths.skillsDir).replace(/\\/g, "/");
|
|
14814
|
+
const evidenceDir = path50.join(root, ".qfai", "evidence").replace(/\\/g, "/");
|
|
14676
14815
|
const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
|
|
14677
|
-
const skillPromptPattern =
|
|
14816
|
+
const skillPromptPattern = path50.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
14678
14817
|
const skillFiles = await fg9(skillPromptPattern, { dot: true });
|
|
14679
|
-
const evidencePattern =
|
|
14818
|
+
const evidencePattern = path50.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
|
|
14680
14819
|
const evidenceFiles = await fg9(evidencePattern, { dot: true });
|
|
14681
14820
|
for (const sf of skillFiles) {
|
|
14682
14821
|
const content = await readSafe(sf);
|
|
@@ -14684,7 +14823,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14684
14823
|
issues.push(
|
|
14685
14824
|
issue(
|
|
14686
14825
|
"QFAI-CRIT-001",
|
|
14687
|
-
`Skill prompt does not mention rendered/screenshot/HTML review: ${
|
|
14826
|
+
`Skill prompt does not mention rendered/screenshot/HTML review: ${path50.relative(root, sf)}`,
|
|
14688
14827
|
"error",
|
|
14689
14828
|
sf,
|
|
14690
14829
|
"renderCritique.codeOnly",
|
|
@@ -14701,7 +14840,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14701
14840
|
issues.push(
|
|
14702
14841
|
issue(
|
|
14703
14842
|
"QFAI-CRIT-002",
|
|
14704
|
-
`Downstream skill prompt missing DDP reference: ${
|
|
14843
|
+
`Downstream skill prompt missing DDP reference: ${path50.relative(root, sf)}`,
|
|
14705
14844
|
"error",
|
|
14706
14845
|
sf,
|
|
14707
14846
|
"renderCritique.ddpMissing",
|
|
@@ -14747,7 +14886,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14747
14886
|
issues.push(
|
|
14748
14887
|
issue(
|
|
14749
14888
|
"QFAI-CRIT-005",
|
|
14750
|
-
`Read order not specified (DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow): ${
|
|
14889
|
+
`Read order not specified (DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow): ${path50.relative(root, sf)}`,
|
|
14751
14890
|
"error",
|
|
14752
14891
|
sf,
|
|
14753
14892
|
"renderCritique.readOrder",
|
|
@@ -14774,7 +14913,7 @@ async function validateRenderCritique(root, config) {
|
|
|
14774
14913
|
issues.push(
|
|
14775
14914
|
issue(
|
|
14776
14915
|
"QFAI-CRIT-006",
|
|
14777
|
-
`Critique evidence incomplete (missing: ${missing.join(", ")}): ${
|
|
14916
|
+
`Critique evidence incomplete (missing: ${missing.join(", ")}): ${path50.relative(root, ef)}`,
|
|
14778
14917
|
"error",
|
|
14779
14918
|
ef,
|
|
14780
14919
|
"renderCritique.incompleteEvidence",
|
|
@@ -14889,9 +15028,9 @@ async function collectContent(files) {
|
|
|
14889
15028
|
return contents.join("\n---\n");
|
|
14890
15029
|
}
|
|
14891
15030
|
async function collectRenderEvidenceViewports(root) {
|
|
14892
|
-
const prototypingJsonPath =
|
|
15031
|
+
const prototypingJsonPath = path50.join(root, ".qfai", "evidence", "prototyping.json");
|
|
14893
15032
|
try {
|
|
14894
|
-
const raw = await
|
|
15033
|
+
const raw = await readFile40(prototypingJsonPath, "utf-8");
|
|
14895
15034
|
const parsed = JSON.parse(raw);
|
|
14896
15035
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14897
15036
|
return /* @__PURE__ */ new Set();
|
|
@@ -14930,8 +15069,8 @@ async function collectRenderEvidenceViewports(root) {
|
|
|
14930
15069
|
}
|
|
14931
15070
|
|
|
14932
15071
|
// src/core/validators/designFidelity.ts
|
|
14933
|
-
import { readFile as
|
|
14934
|
-
import
|
|
15072
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
15073
|
+
import path51 from "path";
|
|
14935
15074
|
import fg10 from "fast-glob";
|
|
14936
15075
|
var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
|
|
14937
15076
|
var BASE_DIMENSIONS = ["hierarchy", "clarity", "accessibility", "responsive"];
|
|
@@ -14956,19 +15095,19 @@ async function validateDesignFidelity(root, config) {
|
|
|
14956
15095
|
const evidenceDirs = [".qfai/evidence", ".qfai/review"];
|
|
14957
15096
|
const allFiles = [];
|
|
14958
15097
|
for (const dir of evidenceDirs) {
|
|
14959
|
-
const pattern =
|
|
15098
|
+
const pattern = path51.posix.join(root.replace(/\\/g, "/"), dir, "**/*.md");
|
|
14960
15099
|
const files = await fg10(pattern, { absolute: true });
|
|
14961
15100
|
allFiles.push(...files);
|
|
14962
15101
|
}
|
|
14963
15102
|
for (const filePath of allFiles) {
|
|
14964
15103
|
let content;
|
|
14965
15104
|
try {
|
|
14966
|
-
content = await
|
|
15105
|
+
content = await readFile41(filePath, "utf-8");
|
|
14967
15106
|
} catch {
|
|
14968
15107
|
continue;
|
|
14969
15108
|
}
|
|
14970
15109
|
if (!SCORECARD_HEADING_RE.test(content)) continue;
|
|
14971
|
-
const rel =
|
|
15110
|
+
const rel = path51.relative(root, filePath).replace(/\\/g, "/");
|
|
14972
15111
|
const section = extractScorecardSection(content);
|
|
14973
15112
|
if (!section) continue;
|
|
14974
15113
|
const dimensions = parseDimensions(section);
|
|
@@ -15260,7 +15399,7 @@ function hasAntiPatternMention(section, code) {
|
|
|
15260
15399
|
}
|
|
15261
15400
|
|
|
15262
15401
|
// src/core/validators/discussionDesignHardening.ts
|
|
15263
|
-
import
|
|
15402
|
+
import path52 from "path";
|
|
15264
15403
|
var HTML_TAG_RE = /<(?:style|div|section|span|button|input|form|header|footer|nav|main|aside)\b/i;
|
|
15265
15404
|
var MERMAID_SCREEN_FLOW_RE = /```mermaid[\s\S]*?(?:stateDiagram|flowchart|graph)[\s\S]*?(?:Screen|Page|View|Dashboard|Login|Settings|Home)\b/i;
|
|
15266
15405
|
var DDS_HEADING = "## Design Direction Summary";
|
|
@@ -15275,10 +15414,17 @@ var DDS_SUBSECTIONS = [
|
|
|
15275
15414
|
var REQUIRED_STATES = ["empty", "loading", "error", "populated"];
|
|
15276
15415
|
var COMPETITIVE_REF_FIELDS = ["adopted_points", "rejected_points", "local_translation"];
|
|
15277
15416
|
var PLACEHOLDER_RE = /^(?:tbd|todo|n\/a|na|xxx|\?\?\?|placeholder)$/i;
|
|
15417
|
+
var SURFACE_TYPE_RE = /\|\s*Surface Type\s*\|\s*(\S+)\s*\|/i;
|
|
15278
15418
|
async function isUiBearing(packRoot) {
|
|
15279
|
-
const storyPath =
|
|
15419
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15280
15420
|
const content = await readSafe(storyPath);
|
|
15281
15421
|
if (!content) return false;
|
|
15422
|
+
const surfaceMatch = SURFACE_TYPE_RE.exec(content);
|
|
15423
|
+
if (surfaceMatch?.[1]) {
|
|
15424
|
+
const surface = surfaceMatch[1].toLowerCase();
|
|
15425
|
+
if (surface === "non-ui") return false;
|
|
15426
|
+
if (["web-ui", "mobile-ui", "desktop-ui", "mixed"].includes(surface)) return true;
|
|
15427
|
+
}
|
|
15282
15428
|
if (HTML_TAG_RE.test(content)) return true;
|
|
15283
15429
|
if (MERMAID_SCREEN_FLOW_RE.test(content)) return true;
|
|
15284
15430
|
return false;
|
|
@@ -15313,7 +15459,7 @@ function extractOptionNames(optionSection) {
|
|
|
15313
15459
|
}
|
|
15314
15460
|
async function validateDdsPresence(packRoot) {
|
|
15315
15461
|
const issues = [];
|
|
15316
|
-
const storyPath =
|
|
15462
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15317
15463
|
const content = await readSafe(storyPath);
|
|
15318
15464
|
const relPath = "03_Story-Workshop.md";
|
|
15319
15465
|
if (!content || !content.includes(DDS_HEADING)) {
|
|
@@ -15347,7 +15493,7 @@ async function validateDdsPresence(packRoot) {
|
|
|
15347
15493
|
}
|
|
15348
15494
|
async function validateOptionComparison2(packRoot) {
|
|
15349
15495
|
const issues = [];
|
|
15350
|
-
const storyPath =
|
|
15496
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15351
15497
|
const content = await readSafe(storyPath);
|
|
15352
15498
|
if (!content) return issues;
|
|
15353
15499
|
const dds = extractDdsSection(content);
|
|
@@ -15371,7 +15517,7 @@ async function validateOptionComparison2(packRoot) {
|
|
|
15371
15517
|
}
|
|
15372
15518
|
async function validateAnchorScreen(packRoot) {
|
|
15373
15519
|
const issues = [];
|
|
15374
|
-
const storyPath =
|
|
15520
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15375
15521
|
const content = await readSafe(storyPath);
|
|
15376
15522
|
if (!content) return issues;
|
|
15377
15523
|
const dds = extractDdsSection(content);
|
|
@@ -15413,7 +15559,7 @@ async function validateAnchorScreen(packRoot) {
|
|
|
15413
15559
|
}
|
|
15414
15560
|
async function validateCompetitiveRefs2(packRoot) {
|
|
15415
15561
|
const issues = [];
|
|
15416
|
-
const sourcesPath =
|
|
15562
|
+
const sourcesPath = path52.join(packRoot, "04_Sources.md");
|
|
15417
15563
|
const content = await readSafe(sourcesPath);
|
|
15418
15564
|
if (!content) return issues;
|
|
15419
15565
|
const registryHeadingRe = /^##\s+Competitive Reference Registry\b.*$/m;
|
|
@@ -15470,7 +15616,7 @@ function fieldGuidance(field) {
|
|
|
15470
15616
|
}
|
|
15471
15617
|
async function validateCtaHierarchy(packRoot) {
|
|
15472
15618
|
const issues = [];
|
|
15473
|
-
const storyPath =
|
|
15619
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15474
15620
|
const content = await readSafe(storyPath);
|
|
15475
15621
|
if (!content) return issues;
|
|
15476
15622
|
const dds = extractDdsSection(content);
|
|
@@ -15508,7 +15654,7 @@ async function validateCtaHierarchy(packRoot) {
|
|
|
15508
15654
|
}
|
|
15509
15655
|
async function validateStateCoverage(packRoot) {
|
|
15510
15656
|
const issues = [];
|
|
15511
|
-
const storyPath =
|
|
15657
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15512
15658
|
const content = await readSafe(storyPath);
|
|
15513
15659
|
if (!content) return issues;
|
|
15514
15660
|
const dds = extractDdsSection(content);
|
|
@@ -15533,7 +15679,7 @@ async function validateStateCoverage(packRoot) {
|
|
|
15533
15679
|
}
|
|
15534
15680
|
async function validateDesignAntiGoals(packRoot) {
|
|
15535
15681
|
const issues = [];
|
|
15536
|
-
const storyPath =
|
|
15682
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15537
15683
|
const content = await readSafe(storyPath);
|
|
15538
15684
|
if (!content) return issues;
|
|
15539
15685
|
const dds = extractDdsSection(content);
|
|
@@ -15570,7 +15716,7 @@ async function validateDesignAntiGoals(packRoot) {
|
|
|
15570
15716
|
return issues;
|
|
15571
15717
|
}
|
|
15572
15718
|
async function validateDiscussionDesignHardening(root, config) {
|
|
15573
|
-
const discussionDir =
|
|
15719
|
+
const discussionDir = path52.join(root, config.paths.discussionDir);
|
|
15574
15720
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15575
15721
|
if (!packRoot) return [];
|
|
15576
15722
|
const uiBearing = await isUiBearing(packRoot);
|
|
@@ -15588,7 +15734,7 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
15588
15734
|
|
|
15589
15735
|
// src/core/validators/designAudit.ts
|
|
15590
15736
|
import { readdir as readdir9 } from "fs/promises";
|
|
15591
|
-
import
|
|
15737
|
+
import path53 from "path";
|
|
15592
15738
|
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
15593
15739
|
function resolveAuditConfig(config) {
|
|
15594
15740
|
const audit = config.uiux?.audit;
|
|
@@ -15670,7 +15816,7 @@ var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|
|
|
|
15670
15816
|
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
15671
15817
|
const findings = [];
|
|
15672
15818
|
const configuredDir = cfg.uiux?.designTokensDir;
|
|
15673
|
-
const tokensDir = configuredDir ?
|
|
15819
|
+
const tokensDir = configuredDir ? path53.resolve(root, configuredDir) : path53.join(root, cfg.paths.contractsDir, "design");
|
|
15674
15820
|
let hasTokenFiles = false;
|
|
15675
15821
|
try {
|
|
15676
15822
|
const entries = await readdir9(tokensDir);
|
|
@@ -15679,7 +15825,7 @@ async function checkTokenDrift(root, auditConfig, cfg) {
|
|
|
15679
15825
|
return findings;
|
|
15680
15826
|
}
|
|
15681
15827
|
if (!hasTokenFiles) return findings;
|
|
15682
|
-
const contractsUiDir =
|
|
15828
|
+
const contractsUiDir = path53.join(root, cfg.paths.contractsDir, "ui");
|
|
15683
15829
|
let htmlFiles = [];
|
|
15684
15830
|
try {
|
|
15685
15831
|
const entries = await readdir9(contractsUiDir);
|
|
@@ -15690,7 +15836,7 @@ async function checkTokenDrift(root, auditConfig, cfg) {
|
|
|
15690
15836
|
let rawCount = 0;
|
|
15691
15837
|
const sampleLiterals = [];
|
|
15692
15838
|
for (const htmlFile of htmlFiles) {
|
|
15693
|
-
const content = await readSafe(
|
|
15839
|
+
const content = await readSafe(path53.join(contractsUiDir, htmlFile));
|
|
15694
15840
|
if (!content) continue;
|
|
15695
15841
|
const matches = content.match(RAW_COLOR_RE);
|
|
15696
15842
|
if (matches) {
|
|
@@ -15741,12 +15887,12 @@ function deduplicateFindings(issues, maxPerRule) {
|
|
|
15741
15887
|
async function validateDesignAudit(root, config) {
|
|
15742
15888
|
const auditConfig = resolveAuditConfig(config);
|
|
15743
15889
|
if (!auditConfig.enabled) return [];
|
|
15744
|
-
const discussionDir =
|
|
15890
|
+
const discussionDir = path53.join(root, config.paths.discussionDir);
|
|
15745
15891
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15746
15892
|
if (!packRoot) return [];
|
|
15747
15893
|
const uiBearing = await isUiBearing(packRoot);
|
|
15748
15894
|
if (!uiBearing) return [];
|
|
15749
|
-
const storyPath =
|
|
15895
|
+
const storyPath = path53.join(packRoot, "03_Story-Workshop.md");
|
|
15750
15896
|
const content = await readSafe(storyPath);
|
|
15751
15897
|
if (!content) return [];
|
|
15752
15898
|
const findings = [];
|
|
@@ -15758,8 +15904,8 @@ async function validateDesignAudit(root, config) {
|
|
|
15758
15904
|
|
|
15759
15905
|
// src/core/validators/designSlop.ts
|
|
15760
15906
|
import { existsSync as existsSync2 } from "fs";
|
|
15761
|
-
import { readFile as
|
|
15762
|
-
import
|
|
15907
|
+
import { readFile as readFile42 } from "fs/promises";
|
|
15908
|
+
import path54 from "path";
|
|
15763
15909
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
15764
15910
|
function isValidSlopPattern(rule) {
|
|
15765
15911
|
if (typeof rule !== "object" || rule === null) return false;
|
|
@@ -15767,7 +15913,7 @@ function isValidSlopPattern(rule) {
|
|
|
15767
15913
|
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";
|
|
15768
15914
|
}
|
|
15769
15915
|
async function loadSlopPatterns(jsonPath) {
|
|
15770
|
-
const raw = await
|
|
15916
|
+
const raw = await readFile42(jsonPath, "utf-8");
|
|
15771
15917
|
const parsed = JSON.parse(raw);
|
|
15772
15918
|
if (!Array.isArray(parsed)) return [];
|
|
15773
15919
|
return parsed.filter((r) => isValidSlopPattern(r));
|
|
@@ -15775,11 +15921,11 @@ async function loadSlopPatterns(jsonPath) {
|
|
|
15775
15921
|
function defaultPatternsPath() {
|
|
15776
15922
|
const base = import.meta.url;
|
|
15777
15923
|
const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
|
|
15778
|
-
const baseDir =
|
|
15924
|
+
const baseDir = path54.dirname(basePath);
|
|
15779
15925
|
const candidates = [
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15926
|
+
path54.join(baseDir, "designSlopPatterns.json"),
|
|
15927
|
+
path54.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
15928
|
+
path54.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
15783
15929
|
];
|
|
15784
15930
|
for (const c of candidates) {
|
|
15785
15931
|
if (existsSync2(c)) return c;
|
|
@@ -15790,7 +15936,7 @@ async function validateDesignSlop(root, config) {
|
|
|
15790
15936
|
const auditConfig = resolveAuditConfig(config);
|
|
15791
15937
|
if (!auditConfig.enabled) return [];
|
|
15792
15938
|
if (!auditConfig.slopDetection) return [];
|
|
15793
|
-
const discussionDir =
|
|
15939
|
+
const discussionDir = path54.join(root, config.paths.discussionDir);
|
|
15794
15940
|
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15795
15941
|
if (!packRoot) return [];
|
|
15796
15942
|
const uiBearing = await isUiBearing(packRoot);
|
|
@@ -15811,7 +15957,7 @@ async function validateDesignSlop(root, config) {
|
|
|
15811
15957
|
continue;
|
|
15812
15958
|
}
|
|
15813
15959
|
for (const scope of pattern.scopes) {
|
|
15814
|
-
const filePath =
|
|
15960
|
+
const filePath = path54.join(packRoot, scope);
|
|
15815
15961
|
const content = await readSafe(filePath);
|
|
15816
15962
|
if (!content) continue;
|
|
15817
15963
|
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
@@ -15832,6 +15978,460 @@ async function validateDesignSlop(root, config) {
|
|
|
15832
15978
|
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
15833
15979
|
}
|
|
15834
15980
|
|
|
15981
|
+
// src/core/validators/uixValidators.ts
|
|
15982
|
+
import { readdir as readdir11 } from "fs/promises";
|
|
15983
|
+
import path56 from "path";
|
|
15984
|
+
|
|
15985
|
+
// src/core/validators/uixDetection.ts
|
|
15986
|
+
import { readdir as readdir10 } from "fs/promises";
|
|
15987
|
+
import path55 from "path";
|
|
15988
|
+
var UI_BEARING_SURFACES = /* @__PURE__ */ new Set(["web-ui", "mobile-ui", "desktop-ui", "mixed"]);
|
|
15989
|
+
var NON_UI_SURFACES = /* @__PURE__ */ new Set(["non-ui"]);
|
|
15990
|
+
function stripCodeBlocks(content) {
|
|
15991
|
+
let stripped = content.replace(/```[\s\S]*?```/g, "");
|
|
15992
|
+
stripped = stripped.replace(/`[^`]+`/g, "");
|
|
15993
|
+
return stripped;
|
|
15994
|
+
}
|
|
15995
|
+
var HTML_TAG_RE2 = /<(?:style|div|section|span|button|input|form|header|footer|nav|main|aside)\b/i;
|
|
15996
|
+
var MERMAID_SCREEN_FLOW_RE2 = /```mermaid[\s\S]*?(?:stateDiagram)[\s\S]*?(?:Screen|Page|View|Dashboard|Login|Settings|Home)\b/i;
|
|
15997
|
+
var SCREEN_CONTRACT_YAML_RE = /screens:\s*\n\s*-\s*route:/;
|
|
15998
|
+
async function isUiBearingSpec(root) {
|
|
15999
|
+
const specContent = await readSafe(path55.join(root, "01_Spec.md"));
|
|
16000
|
+
const surfaceMatch = /^\s*-\s*surface:\s*(\S+)/im.exec(specContent);
|
|
16001
|
+
if (surfaceMatch?.[1]) {
|
|
16002
|
+
const surface = surfaceMatch[1].toLowerCase();
|
|
16003
|
+
if (UI_BEARING_SURFACES.has(surface)) return true;
|
|
16004
|
+
if (NON_UI_SURFACES.has(surface)) return false;
|
|
16005
|
+
}
|
|
16006
|
+
try {
|
|
16007
|
+
await readdir10(path55.join(root, "uiux"));
|
|
16008
|
+
return true;
|
|
16009
|
+
} catch {
|
|
16010
|
+
}
|
|
16011
|
+
const contractsContent = await readSafe(path55.join(root, "uiux", "40_contracts.md"));
|
|
16012
|
+
if (contractsContent && SCREEN_CONTRACT_YAML_RE.test(contractsContent)) return true;
|
|
16013
|
+
const storyContent = await readSafe(path55.join(root, "03_Story-Workshop.md"));
|
|
16014
|
+
if (storyContent) {
|
|
16015
|
+
const stripped = stripCodeBlocks(storyContent);
|
|
16016
|
+
if (HTML_TAG_RE2.test(stripped)) return true;
|
|
16017
|
+
if (MERMAID_SCREEN_FLOW_RE2.test(storyContent)) return true;
|
|
16018
|
+
}
|
|
16019
|
+
return false;
|
|
16020
|
+
}
|
|
16021
|
+
|
|
16022
|
+
// src/core/validators/uixValidators.ts
|
|
16023
|
+
function uixIssue(code, message, severity, file, suggestedAction) {
|
|
16024
|
+
return {
|
|
16025
|
+
code,
|
|
16026
|
+
severity,
|
|
16027
|
+
category: "compatibility",
|
|
16028
|
+
message,
|
|
16029
|
+
file,
|
|
16030
|
+
suggested_action: suggestedAction
|
|
16031
|
+
};
|
|
16032
|
+
}
|
|
16033
|
+
function parseSimpleYaml(content) {
|
|
16034
|
+
const result = {};
|
|
16035
|
+
const store = (rawKey, value) => {
|
|
16036
|
+
const key = rawKey.trim().toLowerCase().replace(/\s+/g, "_");
|
|
16037
|
+
if (key && value) result[key] = value;
|
|
16038
|
+
};
|
|
16039
|
+
for (const line of content.split("\n")) {
|
|
16040
|
+
const yamlMatch = /^\s*(\w[\w_]*):\s*(.*)$/.exec(line);
|
|
16041
|
+
if (yamlMatch?.[1] !== void 0 && yamlMatch[2] !== void 0) {
|
|
16042
|
+
store(yamlMatch[1], yamlMatch[2].trim());
|
|
16043
|
+
continue;
|
|
16044
|
+
}
|
|
16045
|
+
const bulletMatch = /^\s*-\s+([\w][\w\s]*?):\s+(.+)$/.exec(line);
|
|
16046
|
+
if (bulletMatch?.[1] !== void 0 && bulletMatch[2] !== void 0) {
|
|
16047
|
+
store(bulletMatch[1], bulletMatch[2].trim());
|
|
16048
|
+
continue;
|
|
16049
|
+
}
|
|
16050
|
+
const tableMatch = /^\s*\|\s*(\w[\w\s_]*?)\s*\|\s*(.+?)\s*\|\s*$/.exec(line);
|
|
16051
|
+
if (tableMatch?.[1] !== void 0 && tableMatch[2] !== void 0) {
|
|
16052
|
+
if (!/^[-:]+$/.test(tableMatch[2].trim())) {
|
|
16053
|
+
store(tableMatch[1], tableMatch[2].trim());
|
|
16054
|
+
}
|
|
16055
|
+
}
|
|
16056
|
+
}
|
|
16057
|
+
return result;
|
|
16058
|
+
}
|
|
16059
|
+
async function validateSidecarMissing(root, _config) {
|
|
16060
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16061
|
+
try {
|
|
16062
|
+
await readdir11(path56.join(root, "uiux"));
|
|
16063
|
+
return [];
|
|
16064
|
+
} catch {
|
|
16065
|
+
return [
|
|
16066
|
+
uixIssue(
|
|
16067
|
+
"UIX-VAL-SIDECAR-MISSING",
|
|
16068
|
+
"UI-bearing spec detected but uiux/ sidecar directory is missing.",
|
|
16069
|
+
"error",
|
|
16070
|
+
"uiux/",
|
|
16071
|
+
"Create the uiux/ directory and populate it with the sidecar template files."
|
|
16072
|
+
)
|
|
16073
|
+
];
|
|
16074
|
+
}
|
|
16075
|
+
}
|
|
16076
|
+
var STRATEGY_REQUIRED_FIELDS_LEGACY = [
|
|
16077
|
+
"selection_required",
|
|
16078
|
+
"candidate_options",
|
|
16079
|
+
"chosen_option",
|
|
16080
|
+
"verification_expectations",
|
|
16081
|
+
"none_as_legitimate_outcome"
|
|
16082
|
+
];
|
|
16083
|
+
var STRATEGY_REQUIRED_FIELDS_CURRENT = ["surface_type", "approach", "rationale"];
|
|
16084
|
+
var STRATEGY_MIN_LENGTH_FIELDS = ["rationale", "approach"];
|
|
16085
|
+
var STRATEGY_MIN_LENGTH = 20;
|
|
16086
|
+
async function validateStrategyCompleteness(root, _config) {
|
|
16087
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16088
|
+
const strategyPath = path56.join(root, "uiux", "10_strategy.md");
|
|
16089
|
+
const content = await readSafe(strategyPath);
|
|
16090
|
+
if (!content) return [];
|
|
16091
|
+
const parsed = parseSimpleYaml(content);
|
|
16092
|
+
const issues = [];
|
|
16093
|
+
const relPath = "uiux/10_strategy.md";
|
|
16094
|
+
const isCurrentFormat = parsed["surface_type"] !== void 0;
|
|
16095
|
+
const requiredFields = isCurrentFormat ? STRATEGY_REQUIRED_FIELDS_CURRENT : STRATEGY_REQUIRED_FIELDS_LEGACY;
|
|
16096
|
+
for (const field of requiredFields) {
|
|
16097
|
+
const value = parsed[field];
|
|
16098
|
+
if (value === void 0 || value === "") {
|
|
16099
|
+
issues.push(
|
|
16100
|
+
uixIssue(
|
|
16101
|
+
"UIX-VAL-STRATEGY-INCOMPLETE",
|
|
16102
|
+
`Strategy field '${field}' is missing or empty in 10_strategy.md.`,
|
|
16103
|
+
"error",
|
|
16104
|
+
relPath,
|
|
16105
|
+
`Add the '${field}' field to uiux/10_strategy.md with a valid value.`
|
|
16106
|
+
)
|
|
16107
|
+
);
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
for (const field of STRATEGY_MIN_LENGTH_FIELDS) {
|
|
16111
|
+
const value = parsed[field];
|
|
16112
|
+
if (value === void 0 || value.length < STRATEGY_MIN_LENGTH) {
|
|
16113
|
+
issues.push(
|
|
16114
|
+
uixIssue(
|
|
16115
|
+
"UIX-VAL-STRATEGY-INCOMPLETE",
|
|
16116
|
+
`Strategy field '${field}' must be at least ${STRATEGY_MIN_LENGTH} characters (current: ${value?.length ?? 0}).`,
|
|
16117
|
+
"error",
|
|
16118
|
+
relPath,
|
|
16119
|
+
`Expand the '${field}' field in uiux/10_strategy.md to at least ${STRATEGY_MIN_LENGTH} characters.`
|
|
16120
|
+
)
|
|
16121
|
+
);
|
|
16122
|
+
}
|
|
16123
|
+
}
|
|
16124
|
+
return issues;
|
|
16125
|
+
}
|
|
16126
|
+
async function validateScoringAxes(root, _config) {
|
|
16127
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16128
|
+
const singlePath = path56.join(root, "uiux", "20_eval_axes.md");
|
|
16129
|
+
let content = await readSafe(singlePath);
|
|
16130
|
+
let relPath = "uiux/20_eval_axes.md";
|
|
16131
|
+
if (!content) {
|
|
16132
|
+
const splitFiles = [
|
|
16133
|
+
"20_eval_axis_usability.md",
|
|
16134
|
+
"21_eval_axis_consistency.md",
|
|
16135
|
+
"22_eval_axis_accessibility.md",
|
|
16136
|
+
"23_eval_axis_delight.md"
|
|
16137
|
+
];
|
|
16138
|
+
const parts = [];
|
|
16139
|
+
for (const f of splitFiles) {
|
|
16140
|
+
const c = await readSafe(path56.join(root, "uiux", f));
|
|
16141
|
+
if (c) parts.push(c);
|
|
16142
|
+
}
|
|
16143
|
+
if (parts.length > 0) {
|
|
16144
|
+
content = parts.join("\n");
|
|
16145
|
+
relPath = "uiux/20_eval_axis_*.md";
|
|
16146
|
+
}
|
|
16147
|
+
}
|
|
16148
|
+
if (!content) return [];
|
|
16149
|
+
const issues = [];
|
|
16150
|
+
const lines = content.split("\n");
|
|
16151
|
+
let inTrendSection = false;
|
|
16152
|
+
for (const line of lines) {
|
|
16153
|
+
if (/trend[_-]derived/i.test(line)) {
|
|
16154
|
+
inTrendSection = true;
|
|
16155
|
+
continue;
|
|
16156
|
+
}
|
|
16157
|
+
if (inTrendSection && /^#+\s/.test(line)) {
|
|
16158
|
+
inTrendSection = false;
|
|
16159
|
+
continue;
|
|
16160
|
+
}
|
|
16161
|
+
const isBulletRow = /^\s*-\s/.test(line);
|
|
16162
|
+
const isTableRow2 = /^\s*\|/.test(line) && !/^\s*\|[\s-:|]+\|[\s-:|]*$/.test(line);
|
|
16163
|
+
if (inTrendSection && (isBulletRow || isTableRow2)) {
|
|
16164
|
+
if (!/source_translation/i.test(line)) {
|
|
16165
|
+
issues.push(
|
|
16166
|
+
uixIssue(
|
|
16167
|
+
"UIX-VAL-SCORING-AXIS-INCOMPLETE",
|
|
16168
|
+
"Trend-derived scoring axis row is missing source_translation field.",
|
|
16169
|
+
"error",
|
|
16170
|
+
relPath,
|
|
16171
|
+
"Add source_translation to each trend-derived evaluation axis row."
|
|
16172
|
+
)
|
|
16173
|
+
);
|
|
16174
|
+
}
|
|
16175
|
+
}
|
|
16176
|
+
}
|
|
16177
|
+
return issues;
|
|
16178
|
+
}
|
|
16179
|
+
var AGGREGATE_REQUIRED_FIELDS = ["weights", "normalization", "threshold"];
|
|
16180
|
+
async function validateAggregateScoringRules(root, _config) {
|
|
16181
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16182
|
+
const aggregatePath = path56.join(root, "uiux", "21_aggregate_scoring.md");
|
|
16183
|
+
let content = await readSafe(aggregatePath);
|
|
16184
|
+
let relPath = "uiux/21_aggregate_scoring.md";
|
|
16185
|
+
if (!content) {
|
|
16186
|
+
const delightContent = await readSafe(path56.join(root, "uiux", "23_eval_axis_delight.md"));
|
|
16187
|
+
if (delightContent) {
|
|
16188
|
+
const lines = delightContent.split("\n");
|
|
16189
|
+
let inSection = false;
|
|
16190
|
+
const sectionLines = [];
|
|
16191
|
+
for (const line of lines) {
|
|
16192
|
+
if (/^#+\s*Aggregate\s+Scoring/i.test(line)) {
|
|
16193
|
+
inSection = true;
|
|
16194
|
+
continue;
|
|
16195
|
+
}
|
|
16196
|
+
if (inSection && /^#+\s/.test(line)) break;
|
|
16197
|
+
if (inSection) sectionLines.push(line);
|
|
16198
|
+
}
|
|
16199
|
+
if (sectionLines.length > 0) {
|
|
16200
|
+
content = sectionLines.join("\n");
|
|
16201
|
+
relPath = "uiux/23_eval_axis_delight.md";
|
|
16202
|
+
}
|
|
16203
|
+
}
|
|
16204
|
+
}
|
|
16205
|
+
if (!content) return [];
|
|
16206
|
+
const parsed = parseSimpleYaml(content);
|
|
16207
|
+
const issues = [];
|
|
16208
|
+
for (const field of AGGREGATE_REQUIRED_FIELDS) {
|
|
16209
|
+
const value = parsed[field] ?? parsed[`${field}s`];
|
|
16210
|
+
if (value === void 0 || value === "") {
|
|
16211
|
+
issues.push(
|
|
16212
|
+
uixIssue(
|
|
16213
|
+
"UIX-VAL-AGGREGATE-SCORING-INCOMPLETE",
|
|
16214
|
+
`Aggregate scoring field '${field}' is missing in ${relPath}.`,
|
|
16215
|
+
"error",
|
|
16216
|
+
relPath,
|
|
16217
|
+
`Add the '${field}' field to ${relPath}.`
|
|
16218
|
+
)
|
|
16219
|
+
);
|
|
16220
|
+
}
|
|
16221
|
+
}
|
|
16222
|
+
return issues;
|
|
16223
|
+
}
|
|
16224
|
+
async function validateOptionComparison3(root, _config) {
|
|
16225
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16226
|
+
const issues = [];
|
|
16227
|
+
const compPath = path56.join(root, "uiux", "30_comparison.md");
|
|
16228
|
+
const compContent = await readSafe(compPath);
|
|
16229
|
+
if (compContent) {
|
|
16230
|
+
const headingOptions = compContent.match(/^##\s+Option\b/gim);
|
|
16231
|
+
const tableOptions = compContent.match(/\bOption\s+[A-Z]\b/gim);
|
|
16232
|
+
const uniqueTableOptions = tableOptions ? new Set(tableOptions.map((m) => m.trim().toUpperCase())).size : 0;
|
|
16233
|
+
const optionCount = Math.max(headingOptions?.length ?? 0, uniqueTableOptions);
|
|
16234
|
+
if (optionCount < 2) {
|
|
16235
|
+
issues.push(
|
|
16236
|
+
uixIssue(
|
|
16237
|
+
"UIX-VAL-COMPARISON-INSUFFICIENT",
|
|
16238
|
+
"30_comparison.md must contain at least 2 options for meaningful comparison.",
|
|
16239
|
+
"error",
|
|
16240
|
+
"uiux/30_comparison.md",
|
|
16241
|
+
"Add at least 2 '## Option' sections to uiux/30_comparison.md."
|
|
16242
|
+
)
|
|
16243
|
+
);
|
|
16244
|
+
}
|
|
16245
|
+
}
|
|
16246
|
+
const anchorPath = path56.join(root, "uiux", "31_anchor.md");
|
|
16247
|
+
const anchorContent = await readSafe(anchorPath);
|
|
16248
|
+
if (anchorContent) {
|
|
16249
|
+
if (!/selected_anchor\s*:/i.test(anchorContent) && !/chosen\s*:/i.test(anchorContent) && !/source\s+option\s*:/i.test(anchorContent)) {
|
|
16250
|
+
issues.push(
|
|
16251
|
+
uixIssue(
|
|
16252
|
+
"UIX-VAL-ANCHOR-MISSING",
|
|
16253
|
+
"31_anchor.md is missing a selected anchor declaration.",
|
|
16254
|
+
"error",
|
|
16255
|
+
"uiux/31_anchor.md",
|
|
16256
|
+
"Add a 'selected_anchor:' field to uiux/31_anchor.md."
|
|
16257
|
+
)
|
|
16258
|
+
);
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
return issues;
|
|
16262
|
+
}
|
|
16263
|
+
var SCREEN_CONTRACT_REQUIRED_FIELDS = [
|
|
16264
|
+
"route",
|
|
16265
|
+
"actor",
|
|
16266
|
+
"purpose",
|
|
16267
|
+
"primary_tasks",
|
|
16268
|
+
"required_states",
|
|
16269
|
+
"transitions",
|
|
16270
|
+
"observable_outcomes"
|
|
16271
|
+
];
|
|
16272
|
+
async function validateScreenContracts(root, _config) {
|
|
16273
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16274
|
+
const contractsPath = path56.join(root, "uiux", "40_contracts.md");
|
|
16275
|
+
const content = await readSafe(contractsPath);
|
|
16276
|
+
if (!content) return [];
|
|
16277
|
+
const parsed = parseSimpleYaml(content);
|
|
16278
|
+
const issues = [];
|
|
16279
|
+
const relPath = "uiux/40_contracts.md";
|
|
16280
|
+
const sectionHeadings = {
|
|
16281
|
+
primary_tasks: /^#{3,4}\s+Primary\s+Tasks/im,
|
|
16282
|
+
required_states: /^#{3,4}\s+Required\s+States/im,
|
|
16283
|
+
transitions: /^#{3,4}\s+Transitions/im,
|
|
16284
|
+
observable_outcomes: /^#{3,4}\s+Observable\s+Outcomes/im
|
|
16285
|
+
};
|
|
16286
|
+
for (const field of SCREEN_CONTRACT_REQUIRED_FIELDS) {
|
|
16287
|
+
const value = parsed[field];
|
|
16288
|
+
const hasValue = value !== void 0 && value !== "";
|
|
16289
|
+
const headingPattern = sectionHeadings[field];
|
|
16290
|
+
const hasHeading = headingPattern ? headingPattern.test(content) : false;
|
|
16291
|
+
if (!hasValue && !hasHeading) {
|
|
16292
|
+
issues.push(
|
|
16293
|
+
uixIssue(
|
|
16294
|
+
"UIX-VAL-SCREEN-CONTRACT-INCOMPLETE",
|
|
16295
|
+
`Screen contract field '${field}' is missing in 40_contracts.md.`,
|
|
16296
|
+
"error",
|
|
16297
|
+
relPath,
|
|
16298
|
+
`Add the '${field}' field to screen contracts in uiux/40_contracts.md.`
|
|
16299
|
+
)
|
|
16300
|
+
);
|
|
16301
|
+
}
|
|
16302
|
+
}
|
|
16303
|
+
return issues;
|
|
16304
|
+
}
|
|
16305
|
+
async function validateOqClosure(root, _config) {
|
|
16306
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16307
|
+
const rootOqPath = path56.join(root, "11_OQ-Register.md");
|
|
16308
|
+
const uiuxOqPath = path56.join(root, "uiux", "11_OQ-Register.md");
|
|
16309
|
+
const rootContent = await readSafe(rootOqPath);
|
|
16310
|
+
const content = rootContent || await readSafe(uiuxOqPath);
|
|
16311
|
+
if (!content) return [];
|
|
16312
|
+
const issues = [];
|
|
16313
|
+
const relPath = rootContent ? "11_OQ-Register.md" : "uiux/11_OQ-Register.md";
|
|
16314
|
+
const oqBlocks = content.split(/(?=^##\s+OQ-\d{4})/m);
|
|
16315
|
+
for (const block of oqBlocks) {
|
|
16316
|
+
const idMatch = /^##\s+(OQ-\d{4})/m.exec(block);
|
|
16317
|
+
if (!idMatch?.[1]) continue;
|
|
16318
|
+
const oqId = idMatch[1];
|
|
16319
|
+
const isOpen = /status\s*:\s*open/i.test(block);
|
|
16320
|
+
const isCritical = /severity\s*:\s*(?:critical|blocking)/i.test(block);
|
|
16321
|
+
if (isOpen && isCritical) {
|
|
16322
|
+
issues.push(
|
|
16323
|
+
uixIssue(
|
|
16324
|
+
"UIX-VAL-OQ-OPEN-CRITICAL",
|
|
16325
|
+
`Open critical OQ found: ${oqId}. Must be resolved before proceeding.`,
|
|
16326
|
+
"error",
|
|
16327
|
+
relPath,
|
|
16328
|
+
`Resolve or downgrade ${oqId} in uiux/11_OQ-Register.md before validation can pass.`
|
|
16329
|
+
)
|
|
16330
|
+
);
|
|
16331
|
+
}
|
|
16332
|
+
}
|
|
16333
|
+
const tableRowRegex = /^\s*\|\s*(OQ-\d{4})\s*\|/;
|
|
16334
|
+
for (const line of content.split("\n")) {
|
|
16335
|
+
const rowMatch = tableRowRegex.exec(line);
|
|
16336
|
+
if (!rowMatch?.[1]) continue;
|
|
16337
|
+
const oqId = rowMatch[1];
|
|
16338
|
+
const cols = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
16339
|
+
const disposition = cols[3]?.toLowerCase();
|
|
16340
|
+
if (disposition === "open") {
|
|
16341
|
+
const fullRow = line.toLowerCase();
|
|
16342
|
+
if (/critical|blocking/i.test(fullRow)) {
|
|
16343
|
+
issues.push(
|
|
16344
|
+
uixIssue(
|
|
16345
|
+
"UIX-VAL-OQ-OPEN-CRITICAL",
|
|
16346
|
+
`Open critical OQ found: ${oqId}. Must be resolved before proceeding.`,
|
|
16347
|
+
"error",
|
|
16348
|
+
relPath,
|
|
16349
|
+
`Resolve or downgrade ${oqId} in ${relPath} before validation can pass.`
|
|
16350
|
+
)
|
|
16351
|
+
);
|
|
16352
|
+
}
|
|
16353
|
+
}
|
|
16354
|
+
}
|
|
16355
|
+
return issues;
|
|
16356
|
+
}
|
|
16357
|
+
var CURRENT_SIDECAR_VERSION = "1.0.0";
|
|
16358
|
+
async function validateMigration(root, config) {
|
|
16359
|
+
if (!await isUiBearingSpec(root)) return [];
|
|
16360
|
+
const strict = config.uiux?.migration?.strict === true;
|
|
16361
|
+
const issues = [];
|
|
16362
|
+
try {
|
|
16363
|
+
await readdir11(path56.join(root, "uiux"));
|
|
16364
|
+
} catch {
|
|
16365
|
+
issues.push(
|
|
16366
|
+
uixIssue(
|
|
16367
|
+
"UIX-VAL-MIGRATION-SIDECAR-MISSING",
|
|
16368
|
+
"UI-bearing spec detected but uiux/ sidecar directory is missing. Migration required.",
|
|
16369
|
+
strict ? "error" : "warning",
|
|
16370
|
+
"uiux/",
|
|
16371
|
+
"Run the UIX sidecar migration to create the uiux/ directory structure."
|
|
16372
|
+
)
|
|
16373
|
+
);
|
|
16374
|
+
return issues;
|
|
16375
|
+
}
|
|
16376
|
+
const versionPath = path56.join(root, "uiux", ".sidecar-version");
|
|
16377
|
+
const versionContent = await readSafe(versionPath);
|
|
16378
|
+
if (versionContent) {
|
|
16379
|
+
const version = versionContent.trim();
|
|
16380
|
+
if (version && version !== CURRENT_SIDECAR_VERSION) {
|
|
16381
|
+
issues.push(
|
|
16382
|
+
uixIssue(
|
|
16383
|
+
"UIX-VAL-MIGRATION-STALE-VERSION",
|
|
16384
|
+
`Sidecar template version '${version}' is outdated (current: ${CURRENT_SIDECAR_VERSION}).`,
|
|
16385
|
+
"warning",
|
|
16386
|
+
"uiux/.sidecar-version",
|
|
16387
|
+
`Upgrade sidecar template from ${version} to ${CURRENT_SIDECAR_VERSION}. Run the migration tool for upgrade steps.`
|
|
16388
|
+
)
|
|
16389
|
+
);
|
|
16390
|
+
}
|
|
16391
|
+
}
|
|
16392
|
+
return issues;
|
|
16393
|
+
}
|
|
16394
|
+
function applyPhase1Ratchet(issues, releaseDate, now = /* @__PURE__ */ new Date()) {
|
|
16395
|
+
const phase1EndMs = releaseDate.getTime() + 30 * 24 * 60 * 60 * 1e3;
|
|
16396
|
+
if (now.getTime() > phase1EndMs) return issues;
|
|
16397
|
+
return issues.map((iss) => {
|
|
16398
|
+
if (iss.code.startsWith("UIX-VAL-") && iss.severity === "error") {
|
|
16399
|
+
return { ...iss, severity: "warning" };
|
|
16400
|
+
}
|
|
16401
|
+
return iss;
|
|
16402
|
+
});
|
|
16403
|
+
}
|
|
16404
|
+
async function runAllUixValidators(root, config) {
|
|
16405
|
+
let effectiveRoot = root;
|
|
16406
|
+
const directSpec = await readSafe(path56.join(root, "01_Spec.md"));
|
|
16407
|
+
if (!directSpec) {
|
|
16408
|
+
const discussionDir = path56.join(root, config.paths.discussionDir);
|
|
16409
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
16410
|
+
if (!packRoot) return [];
|
|
16411
|
+
effectiveRoot = packRoot;
|
|
16412
|
+
}
|
|
16413
|
+
const validators = [
|
|
16414
|
+
validateSidecarMissing,
|
|
16415
|
+
validateStrategyCompleteness,
|
|
16416
|
+
validateScoringAxes,
|
|
16417
|
+
validateAggregateScoringRules,
|
|
16418
|
+
validateOptionComparison3,
|
|
16419
|
+
validateScreenContracts,
|
|
16420
|
+
validateOqClosure,
|
|
16421
|
+
validateMigration
|
|
16422
|
+
];
|
|
16423
|
+
const results = await Promise.all(validators.map((v) => v(effectiveRoot, config)));
|
|
16424
|
+
let issues = results.flat();
|
|
16425
|
+
const relDateStr = config.uiux?.phase1ReleaseDate;
|
|
16426
|
+
if (relDateStr) {
|
|
16427
|
+
const releaseDate = new Date(relDateStr);
|
|
16428
|
+
if (!isNaN(releaseDate.getTime())) {
|
|
16429
|
+
issues = applyPhase1Ratchet(issues, releaseDate);
|
|
16430
|
+
}
|
|
16431
|
+
}
|
|
16432
|
+
return issues;
|
|
16433
|
+
}
|
|
16434
|
+
|
|
15835
16435
|
// src/core/validate.ts
|
|
15836
16436
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
15837
16437
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -15851,7 +16451,8 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
15851
16451
|
() => validateResearchSummary(root, config),
|
|
15852
16452
|
() => validateAgentDefinition(root, config),
|
|
15853
16453
|
() => validateDesignAudit(root, config),
|
|
15854
|
-
() => validateDesignSlop(root, config)
|
|
16454
|
+
() => validateDesignSlop(root, config),
|
|
16455
|
+
() => runAllUixValidators(root, config)
|
|
15855
16456
|
];
|
|
15856
16457
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
15857
16458
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -15938,15 +16539,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
15938
16539
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
15939
16540
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
15940
16541
|
async function createReportData(root, validation, configResult) {
|
|
15941
|
-
const resolvedRoot =
|
|
16542
|
+
const resolvedRoot = path57.resolve(root);
|
|
15942
16543
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
15943
16544
|
const config = resolved.config;
|
|
15944
16545
|
const configPath = resolved.configPath;
|
|
15945
16546
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
15946
16547
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
15947
|
-
const apiRoot =
|
|
15948
|
-
const uiRoot =
|
|
15949
|
-
const dbRoot =
|
|
16548
|
+
const apiRoot = path57.join(contractsRoot, "api");
|
|
16549
|
+
const uiRoot = path57.join(contractsRoot, "ui");
|
|
16550
|
+
const dbRoot = path57.join(contractsRoot, "db");
|
|
15950
16551
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
15951
16552
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
15952
16553
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -16883,7 +17484,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
16883
17484
|
};
|
|
16884
17485
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
16885
17486
|
for (const deltaFile of deltaFiles) {
|
|
16886
|
-
const text = await
|
|
17487
|
+
const text = await readFile43(deltaFile, "utf-8");
|
|
16887
17488
|
const parsed = parseDeltaV1(text);
|
|
16888
17489
|
for (const entry of parsed.entries) {
|
|
16889
17490
|
if (!entry.meta) {
|
|
@@ -16920,7 +17521,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
16920
17521
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
16921
17522
|
}
|
|
16922
17523
|
for (const file of specFiles) {
|
|
16923
|
-
const text = await
|
|
17524
|
+
const text = await readFile43(file, "utf-8");
|
|
16924
17525
|
const parsed = parseSpec(text, file);
|
|
16925
17526
|
const specKey = parsed.specId;
|
|
16926
17527
|
if (!specKey) {
|
|
@@ -16957,7 +17558,7 @@ async function collectIds(files) {
|
|
|
16957
17558
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
16958
17559
|
}
|
|
16959
17560
|
for (const file of files) {
|
|
16960
|
-
const text = await
|
|
17561
|
+
const text = await readFile43(file, "utf-8");
|
|
16961
17562
|
for (const prefix of ID_PREFIXES) {
|
|
16962
17563
|
const ids = extractIds(text, prefix);
|
|
16963
17564
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -16972,7 +17573,7 @@ async function collectIds(files) {
|
|
|
16972
17573
|
async function collectUpstreamIds(files) {
|
|
16973
17574
|
const ids = /* @__PURE__ */ new Set();
|
|
16974
17575
|
for (const file of files) {
|
|
16975
|
-
const text = await
|
|
17576
|
+
const text = await readFile43(file, "utf-8");
|
|
16976
17577
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
16977
17578
|
}
|
|
16978
17579
|
return ids;
|
|
@@ -16993,7 +17594,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
16993
17594
|
}
|
|
16994
17595
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
16995
17596
|
for (const file of targetFiles) {
|
|
16996
|
-
const text = await
|
|
17597
|
+
const text = await readFile43(file, "utf-8");
|
|
16997
17598
|
if (pattern.test(text)) {
|
|
16998
17599
|
return true;
|
|
16999
17600
|
}
|
|
@@ -17112,7 +17713,7 @@ function normalizeScSources(root, sources) {
|
|
|
17112
17713
|
async function countScenarios(scenarioFiles) {
|
|
17113
17714
|
let total = 0;
|
|
17114
17715
|
for (const file of scenarioFiles) {
|
|
17115
|
-
const text = await
|
|
17716
|
+
const text = await readFile43(file, "utf-8");
|
|
17116
17717
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
17117
17718
|
if (!document || errors.length > 0) {
|
|
17118
17719
|
continue;
|
|
@@ -17143,7 +17744,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
17143
17744
|
let totalScenarios = 0;
|
|
17144
17745
|
let e2eCount = 0;
|
|
17145
17746
|
for (const file of scenarioFiles) {
|
|
17146
|
-
const text = await
|
|
17747
|
+
const text = await readFile43(file, "utf-8");
|
|
17147
17748
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
17148
17749
|
if (!document || errors.length > 0) {
|
|
17149
17750
|
continue;
|
|
@@ -17231,10 +17832,10 @@ function buildHotspots(issues) {
|
|
|
17231
17832
|
async function collectTddCoverage(entries) {
|
|
17232
17833
|
const specs = [];
|
|
17233
17834
|
for (const entry of entries) {
|
|
17234
|
-
const testCasesPath =
|
|
17835
|
+
const testCasesPath = path57.join(entry.dir, "06_Test-Cases.md");
|
|
17235
17836
|
let tcContent;
|
|
17236
17837
|
try {
|
|
17237
|
-
tcContent = await
|
|
17838
|
+
tcContent = await readFile43(testCasesPath, "utf-8");
|
|
17238
17839
|
} catch {
|
|
17239
17840
|
continue;
|
|
17240
17841
|
}
|
|
@@ -17266,10 +17867,10 @@ async function collectTddCoverage(entries) {
|
|
|
17266
17867
|
});
|
|
17267
17868
|
continue;
|
|
17268
17869
|
}
|
|
17269
|
-
const tddListPath =
|
|
17870
|
+
const tddListPath = path57.join(entry.dir, "tdd", "test-list.md");
|
|
17270
17871
|
let tddContent;
|
|
17271
17872
|
try {
|
|
17272
|
-
tddContent = await
|
|
17873
|
+
tddContent = await readFile43(tddListPath, "utf-8");
|
|
17273
17874
|
} catch {
|
|
17274
17875
|
specs.push({
|
|
17275
17876
|
specNumber: entry.specNumber,
|
|
@@ -17354,6 +17955,8 @@ export {
|
|
|
17354
17955
|
ID_PREFIXES,
|
|
17355
17956
|
RUNTIME_HEAVY_CHECKS,
|
|
17356
17957
|
STATIC_OBLIGATIONS,
|
|
17958
|
+
VALID_MODE_SOURCES,
|
|
17959
|
+
adaptSurfaceEvidence,
|
|
17357
17960
|
autogenerateUiFidelity,
|
|
17358
17961
|
buildUiFidelityScreens,
|
|
17359
17962
|
checkDecisionGuardrails,
|
|
@@ -17379,6 +17982,7 @@ export {
|
|
|
17379
17982
|
filterDecisionGuardrailsByKeyword,
|
|
17380
17983
|
findConfigRoot,
|
|
17381
17984
|
formatGuardrailsForLlm,
|
|
17985
|
+
formatModeLog,
|
|
17382
17986
|
formatReportJson,
|
|
17383
17987
|
formatReportMarkdown,
|
|
17384
17988
|
getConfigPath,
|
|
@@ -17386,9 +17990,11 @@ export {
|
|
|
17386
17990
|
loadConfig,
|
|
17387
17991
|
loadDecisionGuardrails,
|
|
17388
17992
|
normalizeDecisionGuardrails,
|
|
17993
|
+
readDiscussionRecommendation,
|
|
17389
17994
|
resolveObligations,
|
|
17390
17995
|
resolveObligationsWithOptIn,
|
|
17391
17996
|
resolvePath,
|
|
17997
|
+
resolvePrecedence,
|
|
17392
17998
|
resolveToolVersion,
|
|
17393
17999
|
runMockPaths,
|
|
17394
18000
|
runSddPreflight,
|