qfai 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/validators/designSlopPatterns.json +56 -0
- package/dist/cli/index.cjs +435 -82
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +424 -71
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +393 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.mjs +393 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -554,6 +554,69 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
554
554
|
result.renderEvidence = renderEvidence;
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
|
+
if (raw.audit !== void 0) {
|
|
558
|
+
const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
|
|
559
|
+
if (audit) {
|
|
560
|
+
result.audit = audit;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
564
|
+
}
|
|
565
|
+
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
566
|
+
if (!isRecord(raw)) {
|
|
567
|
+
issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
568
|
+
return void 0;
|
|
569
|
+
}
|
|
570
|
+
const result = {};
|
|
571
|
+
if (raw.enabled !== void 0) {
|
|
572
|
+
if (typeof raw.enabled === "boolean") {
|
|
573
|
+
result.enabled = raw.enabled;
|
|
574
|
+
} else {
|
|
575
|
+
issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (raw.slopDetection !== void 0) {
|
|
579
|
+
if (typeof raw.slopDetection === "boolean") {
|
|
580
|
+
result.slopDetection = raw.slopDetection;
|
|
581
|
+
} else {
|
|
582
|
+
issues.push(
|
|
583
|
+
configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (raw.maxPrimaryCtas !== void 0) {
|
|
588
|
+
if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
|
|
589
|
+
result.maxPrimaryCtas = raw.maxPrimaryCtas;
|
|
590
|
+
} else {
|
|
591
|
+
issues.push(
|
|
592
|
+
configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (raw.maxRawTokenLiteralWarnings !== void 0) {
|
|
597
|
+
if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
|
|
598
|
+
result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
|
|
599
|
+
} else {
|
|
600
|
+
issues.push(
|
|
601
|
+
configIssue(
|
|
602
|
+
configPath,
|
|
603
|
+
"uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (raw.maxDuplicateFindingsPerRule !== void 0) {
|
|
609
|
+
if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
|
|
610
|
+
result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
|
|
611
|
+
} else {
|
|
612
|
+
issues.push(
|
|
613
|
+
configIssue(
|
|
614
|
+
configPath,
|
|
615
|
+
"uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
557
620
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
558
621
|
}
|
|
559
622
|
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
@@ -3585,8 +3648,8 @@ function formatError4(error) {
|
|
|
3585
3648
|
}
|
|
3586
3649
|
|
|
3587
3650
|
// src/core/report.ts
|
|
3588
|
-
var
|
|
3589
|
-
var
|
|
3651
|
+
var import_promises52 = require("fs/promises");
|
|
3652
|
+
var import_node_path54 = __toESM(require("path"), 1);
|
|
3590
3653
|
|
|
3591
3654
|
// src/core/contractIndex.ts
|
|
3592
3655
|
var import_promises13 = require("fs/promises");
|
|
@@ -4344,8 +4407,8 @@ var import_promises14 = require("fs/promises");
|
|
|
4344
4407
|
var import_node_path14 = __toESM(require("path"), 1);
|
|
4345
4408
|
var import_node_url = require("url");
|
|
4346
4409
|
async function resolveToolVersion() {
|
|
4347
|
-
if ("1.7.
|
|
4348
|
-
return "1.7.
|
|
4410
|
+
if ("1.7.2".length > 0) {
|
|
4411
|
+
return "1.7.2";
|
|
4349
4412
|
}
|
|
4350
4413
|
try {
|
|
4351
4414
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -11435,7 +11498,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
11435
11498
|
}
|
|
11436
11499
|
function flattenTokens(obj, prefix, target, errors) {
|
|
11437
11500
|
for (const [key, value] of Object.entries(obj)) {
|
|
11438
|
-
const
|
|
11501
|
+
const path55 = `${prefix}.${key}`;
|
|
11439
11502
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11440
11503
|
const record2 = value;
|
|
11441
11504
|
if ("$value" in record2) {
|
|
@@ -11451,9 +11514,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
11451
11514
|
if (typeof record2.platform === "string") {
|
|
11452
11515
|
token.platform = record2.platform;
|
|
11453
11516
|
}
|
|
11454
|
-
target.set(
|
|
11517
|
+
target.set(path55, token);
|
|
11455
11518
|
} else {
|
|
11456
|
-
flattenTokens(record2,
|
|
11519
|
+
flattenTokens(record2, path55, target, errors);
|
|
11457
11520
|
}
|
|
11458
11521
|
}
|
|
11459
11522
|
}
|
|
@@ -11463,44 +11526,44 @@ function resolveAllReferences(result) {
|
|
|
11463
11526
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
11464
11527
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
11465
11528
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
11466
|
-
for (const [
|
|
11467
|
-
resolveTokenRef(
|
|
11529
|
+
for (const [path55] of allTokens) {
|
|
11530
|
+
resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
11468
11531
|
}
|
|
11469
11532
|
}
|
|
11470
|
-
function resolveTokenRef(
|
|
11471
|
-
if (result.resolved.has(
|
|
11472
|
-
return result.resolved.get(
|
|
11533
|
+
function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
11534
|
+
if (result.resolved.has(path55)) {
|
|
11535
|
+
return result.resolved.get(path55);
|
|
11473
11536
|
}
|
|
11474
11537
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
11475
11538
|
result.errors.push({
|
|
11476
|
-
message: `Max reference depth exceeded at: ${
|
|
11477
|
-
path:
|
|
11539
|
+
message: `Max reference depth exceeded at: ${path55}`,
|
|
11540
|
+
path: path55
|
|
11478
11541
|
});
|
|
11479
11542
|
return void 0;
|
|
11480
11543
|
}
|
|
11481
|
-
if (visited.has(
|
|
11544
|
+
if (visited.has(path55)) {
|
|
11482
11545
|
result.errors.push({
|
|
11483
|
-
message: `Circular reference detected: ${
|
|
11484
|
-
path:
|
|
11546
|
+
message: `Circular reference detected: ${path55}`,
|
|
11547
|
+
path: path55
|
|
11485
11548
|
});
|
|
11486
11549
|
return void 0;
|
|
11487
11550
|
}
|
|
11488
|
-
const token = allTokens.get(
|
|
11551
|
+
const token = allTokens.get(path55);
|
|
11489
11552
|
if (!token) {
|
|
11490
11553
|
return void 0;
|
|
11491
11554
|
}
|
|
11492
11555
|
if (typeof token.$value !== "string") {
|
|
11493
11556
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11494
|
-
result.resolved.set(
|
|
11557
|
+
result.resolved.set(path55, rawValue2);
|
|
11495
11558
|
return rawValue2;
|
|
11496
11559
|
}
|
|
11497
11560
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11498
11561
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11499
11562
|
if (refs.length === 0) {
|
|
11500
|
-
result.resolved.set(
|
|
11563
|
+
result.resolved.set(path55, rawValue);
|
|
11501
11564
|
return rawValue;
|
|
11502
11565
|
}
|
|
11503
|
-
visited.add(
|
|
11566
|
+
visited.add(path55);
|
|
11504
11567
|
let resolved = rawValue;
|
|
11505
11568
|
for (const ref of refs) {
|
|
11506
11569
|
const refPath = ref[1];
|
|
@@ -11508,8 +11571,8 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11508
11571
|
const refToken = allTokens.get(refPath);
|
|
11509
11572
|
if (!refToken) {
|
|
11510
11573
|
result.errors.push({
|
|
11511
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11512
|
-
path:
|
|
11574
|
+
message: `Unresolved token reference: {${refPath}} at ${path55}`,
|
|
11575
|
+
path: path55
|
|
11513
11576
|
});
|
|
11514
11577
|
continue;
|
|
11515
11578
|
}
|
|
@@ -11518,7 +11581,7 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11518
11581
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11519
11582
|
}
|
|
11520
11583
|
}
|
|
11521
|
-
result.resolved.set(
|
|
11584
|
+
result.resolved.set(path55, resolved);
|
|
11522
11585
|
return resolved;
|
|
11523
11586
|
}
|
|
11524
11587
|
function stringifyTokenValue(value) {
|
|
@@ -15575,6 +15638,252 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
15575
15638
|
return issues;
|
|
15576
15639
|
}
|
|
15577
15640
|
|
|
15641
|
+
// src/core/validators/designAudit.ts
|
|
15642
|
+
var import_promises50 = require("fs/promises");
|
|
15643
|
+
var import_node_path52 = __toESM(require("path"), 1);
|
|
15644
|
+
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
15645
|
+
function resolveAuditConfig(config) {
|
|
15646
|
+
const audit = config.uiux?.audit;
|
|
15647
|
+
const profile = config.uiux?.qualityProfile ?? "default";
|
|
15648
|
+
return {
|
|
15649
|
+
enabled: audit?.enabled ?? true,
|
|
15650
|
+
slopDetection: audit?.slopDetection ?? true,
|
|
15651
|
+
qualityProfile: profile,
|
|
15652
|
+
maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
|
|
15653
|
+
maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
|
|
15654
|
+
maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
|
|
15655
|
+
};
|
|
15656
|
+
}
|
|
15657
|
+
function mapSeverity(tier, profile, category) {
|
|
15658
|
+
if (tier === 1) return "error";
|
|
15659
|
+
if (tier === 2) return profile === "strict" ? "error" : "warning";
|
|
15660
|
+
if (profile === "default") {
|
|
15661
|
+
return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
|
|
15662
|
+
}
|
|
15663
|
+
return "warning";
|
|
15664
|
+
}
|
|
15665
|
+
function findingToIssue(finding, profile, rulePrefix = "audit") {
|
|
15666
|
+
const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
|
|
15667
|
+
return issue(
|
|
15668
|
+
finding.ruleId,
|
|
15669
|
+
finding.message,
|
|
15670
|
+
severity,
|
|
15671
|
+
finding.file,
|
|
15672
|
+
`${rulePrefix}.${finding.dimension}`,
|
|
15673
|
+
finding.evidence.length > 0 ? finding.evidence : void 0,
|
|
15674
|
+
"compatibility",
|
|
15675
|
+
finding.guidance
|
|
15676
|
+
);
|
|
15677
|
+
}
|
|
15678
|
+
function extractSection2(content, heading) {
|
|
15679
|
+
const idx = content.indexOf(heading);
|
|
15680
|
+
if (idx === -1) return null;
|
|
15681
|
+
const start = idx + heading.length;
|
|
15682
|
+
const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
|
|
15683
|
+
const rest = content.slice(start);
|
|
15684
|
+
const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
|
|
15685
|
+
const nextHeadingMatch = headingPattern.exec(rest);
|
|
15686
|
+
const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
15687
|
+
return sectionContent.trim() || null;
|
|
15688
|
+
}
|
|
15689
|
+
function checkCtaHierarchy(content, auditConfig, file) {
|
|
15690
|
+
const findings = [];
|
|
15691
|
+
const ctaSection = extractSection2(content, "### CTA Hierarchy");
|
|
15692
|
+
if (!ctaSection) return findings;
|
|
15693
|
+
const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
|
|
15694
|
+
const primaryCount = primaryLines.length;
|
|
15695
|
+
if (primaryCount === 0) {
|
|
15696
|
+
findings.push({
|
|
15697
|
+
ruleId: "QFAI-AUD-001",
|
|
15698
|
+
dimension: "visualHierarchy",
|
|
15699
|
+
severityTier: 1,
|
|
15700
|
+
message: "No primary CTA defined in CTA Hierarchy",
|
|
15701
|
+
why: "Every UI screen needs a clear primary action to guide users",
|
|
15702
|
+
evidence: [],
|
|
15703
|
+
guidance: "Define at least one primary CTA in the CTA Hierarchy section",
|
|
15704
|
+
file
|
|
15705
|
+
});
|
|
15706
|
+
}
|
|
15707
|
+
if (primaryCount > auditConfig.maxPrimaryCtas) {
|
|
15708
|
+
findings.push({
|
|
15709
|
+
ruleId: "QFAI-AUD-020",
|
|
15710
|
+
dimension: "visualHierarchy",
|
|
15711
|
+
severityTier: 2,
|
|
15712
|
+
message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
|
|
15713
|
+
why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
|
|
15714
|
+
evidence: primaryLines.map((l) => l.trim()),
|
|
15715
|
+
guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
|
|
15716
|
+
file
|
|
15717
|
+
});
|
|
15718
|
+
}
|
|
15719
|
+
return findings;
|
|
15720
|
+
}
|
|
15721
|
+
var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
|
|
15722
|
+
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
15723
|
+
const findings = [];
|
|
15724
|
+
const configuredDir = cfg.uiux?.designTokensDir;
|
|
15725
|
+
const tokensDir = configuredDir ? import_node_path52.default.resolve(root, configuredDir) : import_node_path52.default.join(root, cfg.paths.contractsDir, "design");
|
|
15726
|
+
let hasTokenFiles = false;
|
|
15727
|
+
try {
|
|
15728
|
+
const entries = await (0, import_promises50.readdir)(tokensDir);
|
|
15729
|
+
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
15730
|
+
} catch {
|
|
15731
|
+
return findings;
|
|
15732
|
+
}
|
|
15733
|
+
if (!hasTokenFiles) return findings;
|
|
15734
|
+
const contractsUiDir = import_node_path52.default.join(root, cfg.paths.contractsDir, "ui");
|
|
15735
|
+
let htmlFiles = [];
|
|
15736
|
+
try {
|
|
15737
|
+
const entries = await (0, import_promises50.readdir)(contractsUiDir);
|
|
15738
|
+
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
15739
|
+
} catch {
|
|
15740
|
+
return findings;
|
|
15741
|
+
}
|
|
15742
|
+
let rawCount = 0;
|
|
15743
|
+
const sampleLiterals = [];
|
|
15744
|
+
for (const htmlFile of htmlFiles) {
|
|
15745
|
+
const content = await readSafe(import_node_path52.default.join(contractsUiDir, htmlFile));
|
|
15746
|
+
if (!content) continue;
|
|
15747
|
+
const matches = content.match(RAW_COLOR_RE);
|
|
15748
|
+
if (matches) {
|
|
15749
|
+
rawCount += matches.length;
|
|
15750
|
+
for (const m of matches) {
|
|
15751
|
+
if (sampleLiterals.length < 10) {
|
|
15752
|
+
sampleLiterals.push(m.toLowerCase());
|
|
15753
|
+
}
|
|
15754
|
+
}
|
|
15755
|
+
}
|
|
15756
|
+
}
|
|
15757
|
+
if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
|
|
15758
|
+
findings.push({
|
|
15759
|
+
ruleId: "QFAI-AUD-004",
|
|
15760
|
+
dimension: "tokenDiscipline",
|
|
15761
|
+
severityTier: 1,
|
|
15762
|
+
message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
|
|
15763
|
+
why: "Raw color values bypass design tokens, causing visual inconsistency",
|
|
15764
|
+
evidence: sampleLiterals,
|
|
15765
|
+
guidance: "Replace raw color literals with design token references"
|
|
15766
|
+
});
|
|
15767
|
+
}
|
|
15768
|
+
return findings;
|
|
15769
|
+
}
|
|
15770
|
+
function deduplicateFindings(issues, maxPerRule) {
|
|
15771
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15772
|
+
const result = [];
|
|
15773
|
+
for (const iss of issues) {
|
|
15774
|
+
const count = counts.get(iss.code) ?? 0;
|
|
15775
|
+
if (count < maxPerRule) {
|
|
15776
|
+
result.push(iss);
|
|
15777
|
+
}
|
|
15778
|
+
counts.set(iss.code, count + 1);
|
|
15779
|
+
}
|
|
15780
|
+
for (const [code, count] of counts) {
|
|
15781
|
+
if (count > maxPerRule) {
|
|
15782
|
+
result.push({
|
|
15783
|
+
code,
|
|
15784
|
+
severity: "info",
|
|
15785
|
+
category: "compatibility",
|
|
15786
|
+
message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
|
|
15787
|
+
rule: `audit.dedup.${code}`
|
|
15788
|
+
});
|
|
15789
|
+
}
|
|
15790
|
+
}
|
|
15791
|
+
return result;
|
|
15792
|
+
}
|
|
15793
|
+
async function validateDesignAudit(root, config) {
|
|
15794
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15795
|
+
if (!auditConfig.enabled) return [];
|
|
15796
|
+
const discussionDir = import_node_path52.default.join(root, config.paths.discussionDir);
|
|
15797
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15798
|
+
if (!packRoot) return [];
|
|
15799
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15800
|
+
if (!uiBearing) return [];
|
|
15801
|
+
const storyPath = import_node_path52.default.join(packRoot, "03_Story-Workshop.md");
|
|
15802
|
+
const content = await readSafe(storyPath);
|
|
15803
|
+
if (!content) return [];
|
|
15804
|
+
const findings = [];
|
|
15805
|
+
findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
|
|
15806
|
+
findings.push(...await checkTokenDrift(root, auditConfig, config));
|
|
15807
|
+
const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
|
|
15808
|
+
return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
|
|
15809
|
+
}
|
|
15810
|
+
|
|
15811
|
+
// src/core/validators/designSlop.ts
|
|
15812
|
+
var import_node_fs2 = require("fs");
|
|
15813
|
+
var import_promises51 = require("fs/promises");
|
|
15814
|
+
var import_node_path53 = __toESM(require("path"), 1);
|
|
15815
|
+
var import_node_url4 = require("url");
|
|
15816
|
+
function isValidSlopPattern(rule) {
|
|
15817
|
+
if (typeof rule !== "object" || rule === null) return false;
|
|
15818
|
+
const r = rule;
|
|
15819
|
+
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";
|
|
15820
|
+
}
|
|
15821
|
+
async function loadSlopPatterns(jsonPath) {
|
|
15822
|
+
const raw = await (0, import_promises51.readFile)(jsonPath, "utf-8");
|
|
15823
|
+
const parsed = JSON.parse(raw);
|
|
15824
|
+
if (!Array.isArray(parsed)) return [];
|
|
15825
|
+
return parsed.filter((r) => isValidSlopPattern(r));
|
|
15826
|
+
}
|
|
15827
|
+
function defaultPatternsPath() {
|
|
15828
|
+
const base = __filename;
|
|
15829
|
+
const basePath = base.startsWith("file:") ? (0, import_node_url4.fileURLToPath)(base) : base;
|
|
15830
|
+
const baseDir = import_node_path53.default.dirname(basePath);
|
|
15831
|
+
const candidates = [
|
|
15832
|
+
import_node_path53.default.join(baseDir, "designSlopPatterns.json"),
|
|
15833
|
+
import_node_path53.default.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
15834
|
+
import_node_path53.default.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
15835
|
+
];
|
|
15836
|
+
for (const c of candidates) {
|
|
15837
|
+
if ((0, import_node_fs2.existsSync)(c)) return c;
|
|
15838
|
+
}
|
|
15839
|
+
return candidates[0];
|
|
15840
|
+
}
|
|
15841
|
+
async function validateDesignSlop(root, config) {
|
|
15842
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15843
|
+
if (!auditConfig.enabled) return [];
|
|
15844
|
+
if (!auditConfig.slopDetection) return [];
|
|
15845
|
+
const discussionDir = import_node_path53.default.join(root, config.paths.discussionDir);
|
|
15846
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15847
|
+
if (!packRoot) return [];
|
|
15848
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15849
|
+
if (!uiBearing) return [];
|
|
15850
|
+
let patterns;
|
|
15851
|
+
try {
|
|
15852
|
+
patterns = await loadSlopPatterns(defaultPatternsPath());
|
|
15853
|
+
} catch {
|
|
15854
|
+
return [];
|
|
15855
|
+
}
|
|
15856
|
+
const findings = [];
|
|
15857
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
15858
|
+
for (const pattern of patterns) {
|
|
15859
|
+
let regex;
|
|
15860
|
+
try {
|
|
15861
|
+
regex = new RegExp(pattern.match, "gi");
|
|
15862
|
+
} catch {
|
|
15863
|
+
continue;
|
|
15864
|
+
}
|
|
15865
|
+
for (const scope of pattern.scopes) {
|
|
15866
|
+
const filePath = import_node_path53.default.join(packRoot, scope);
|
|
15867
|
+
const content = await readSafe(filePath);
|
|
15868
|
+
if (!content) continue;
|
|
15869
|
+
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
15870
|
+
seenRules.add(pattern.id);
|
|
15871
|
+
findings.push({
|
|
15872
|
+
ruleId: pattern.id,
|
|
15873
|
+
dimension: pattern.category,
|
|
15874
|
+
severityTier: pattern.tier,
|
|
15875
|
+
message: pattern.message,
|
|
15876
|
+
why: `Slop pattern "${pattern.id}" matched in ${scope}`,
|
|
15877
|
+
evidence: [],
|
|
15878
|
+
guidance: pattern.guidance,
|
|
15879
|
+
file: scope
|
|
15880
|
+
});
|
|
15881
|
+
}
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15578
15887
|
// src/core/validate.ts
|
|
15579
15888
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
15580
15889
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -15592,7 +15901,9 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
15592
15901
|
() => validateBpApDb(root, config),
|
|
15593
15902
|
() => validateUiDefinitionConsistency(root, config),
|
|
15594
15903
|
() => validateResearchSummary(root, config),
|
|
15595
|
-
() => validateAgentDefinition(root, config)
|
|
15904
|
+
() => validateAgentDefinition(root, config),
|
|
15905
|
+
() => validateDesignAudit(root, config),
|
|
15906
|
+
() => validateDesignSlop(root, config)
|
|
15596
15907
|
];
|
|
15597
15908
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
15598
15909
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -15679,15 +15990,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
15679
15990
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
15680
15991
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
15681
15992
|
async function createReportData(root, validation, configResult) {
|
|
15682
|
-
const resolvedRoot =
|
|
15993
|
+
const resolvedRoot = import_node_path54.default.resolve(root);
|
|
15683
15994
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
15684
15995
|
const config = resolved.config;
|
|
15685
15996
|
const configPath = resolved.configPath;
|
|
15686
15997
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
15687
15998
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
15688
|
-
const apiRoot =
|
|
15689
|
-
const uiRoot =
|
|
15690
|
-
const dbRoot =
|
|
15999
|
+
const apiRoot = import_node_path54.default.join(contractsRoot, "api");
|
|
16000
|
+
const uiRoot = import_node_path54.default.join(contractsRoot, "ui");
|
|
16001
|
+
const dbRoot = import_node_path54.default.join(contractsRoot, "db");
|
|
15691
16002
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
15692
16003
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
15693
16004
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -16015,6 +16326,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16015
16326
|
lines.push("");
|
|
16016
16327
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
16017
16328
|
lines.push("- [Change Issues](#change-issues)");
|
|
16329
|
+
lines.push("- [Design Audit Findings](#design-audit-findings)");
|
|
16330
|
+
lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
|
|
16018
16331
|
lines.push("- [Change Type](#change-type)");
|
|
16019
16332
|
lines.push("- [Waivers](#waivers)");
|
|
16020
16333
|
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
@@ -16163,6 +16476,46 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16163
16476
|
lines.push("### Issues");
|
|
16164
16477
|
lines.push("");
|
|
16165
16478
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
16479
|
+
const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
|
|
16480
|
+
if (auditIssues.length > 0) {
|
|
16481
|
+
lines.push("## Design Audit Findings");
|
|
16482
|
+
lines.push("");
|
|
16483
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
16484
|
+
for (const iss of auditIssues) {
|
|
16485
|
+
const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
|
|
16486
|
+
const group = byDimension.get(dim) ?? [];
|
|
16487
|
+
group.push(iss);
|
|
16488
|
+
byDimension.set(dim, group);
|
|
16489
|
+
}
|
|
16490
|
+
for (const [dim, dimIssues] of byDimension) {
|
|
16491
|
+
lines.push(`### ${dim}`);
|
|
16492
|
+
lines.push("");
|
|
16493
|
+
for (const iss of dimIssues) {
|
|
16494
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16495
|
+
}
|
|
16496
|
+
lines.push("");
|
|
16497
|
+
}
|
|
16498
|
+
}
|
|
16499
|
+
const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
|
|
16500
|
+
if (slopIssues.length > 0) {
|
|
16501
|
+
lines.push("## Slop Guardrails Findings");
|
|
16502
|
+
lines.push("");
|
|
16503
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
16504
|
+
for (const iss of slopIssues) {
|
|
16505
|
+
const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
|
|
16506
|
+
const group = byCategory.get(cat) ?? [];
|
|
16507
|
+
group.push(iss);
|
|
16508
|
+
byCategory.set(cat, group);
|
|
16509
|
+
}
|
|
16510
|
+
for (const [cat, catIssues] of byCategory) {
|
|
16511
|
+
lines.push(`### ${cat}`);
|
|
16512
|
+
lines.push("");
|
|
16513
|
+
for (const iss of catIssues) {
|
|
16514
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16515
|
+
}
|
|
16516
|
+
lines.push("");
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
16166
16519
|
lines.push("## Change Type");
|
|
16167
16520
|
lines.push("");
|
|
16168
16521
|
lines.push("### Summary");
|
|
@@ -16582,7 +16935,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
16582
16935
|
};
|
|
16583
16936
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
16584
16937
|
for (const deltaFile of deltaFiles) {
|
|
16585
|
-
const text = await (0,
|
|
16938
|
+
const text = await (0, import_promises52.readFile)(deltaFile, "utf-8");
|
|
16586
16939
|
const parsed = parseDeltaV1(text);
|
|
16587
16940
|
for (const entry of parsed.entries) {
|
|
16588
16941
|
if (!entry.meta) {
|
|
@@ -16619,7 +16972,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
16619
16972
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
16620
16973
|
}
|
|
16621
16974
|
for (const file of specFiles) {
|
|
16622
|
-
const text = await (0,
|
|
16975
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16623
16976
|
const parsed = parseSpec(text, file);
|
|
16624
16977
|
const specKey = parsed.specId;
|
|
16625
16978
|
if (!specKey) {
|
|
@@ -16656,7 +17009,7 @@ async function collectIds(files) {
|
|
|
16656
17009
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
16657
17010
|
}
|
|
16658
17011
|
for (const file of files) {
|
|
16659
|
-
const text = await (0,
|
|
17012
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16660
17013
|
for (const prefix of ID_PREFIXES) {
|
|
16661
17014
|
const ids = extractIds(text, prefix);
|
|
16662
17015
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -16671,7 +17024,7 @@ async function collectIds(files) {
|
|
|
16671
17024
|
async function collectUpstreamIds(files) {
|
|
16672
17025
|
const ids = /* @__PURE__ */ new Set();
|
|
16673
17026
|
for (const file of files) {
|
|
16674
|
-
const text = await (0,
|
|
17027
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16675
17028
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
16676
17029
|
}
|
|
16677
17030
|
return ids;
|
|
@@ -16692,7 +17045,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
16692
17045
|
}
|
|
16693
17046
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
16694
17047
|
for (const file of targetFiles) {
|
|
16695
|
-
const text = await (0,
|
|
17048
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16696
17049
|
if (pattern.test(text)) {
|
|
16697
17050
|
return true;
|
|
16698
17051
|
}
|
|
@@ -16811,7 +17164,7 @@ function normalizeScSources(root, sources) {
|
|
|
16811
17164
|
async function countScenarios(scenarioFiles) {
|
|
16812
17165
|
let total = 0;
|
|
16813
17166
|
for (const file of scenarioFiles) {
|
|
16814
|
-
const text = await (0,
|
|
17167
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16815
17168
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16816
17169
|
if (!document || errors.length > 0) {
|
|
16817
17170
|
continue;
|
|
@@ -16842,7 +17195,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
16842
17195
|
let totalScenarios = 0;
|
|
16843
17196
|
let e2eCount = 0;
|
|
16844
17197
|
for (const file of scenarioFiles) {
|
|
16845
|
-
const text = await (0,
|
|
17198
|
+
const text = await (0, import_promises52.readFile)(file, "utf-8");
|
|
16846
17199
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16847
17200
|
if (!document || errors.length > 0) {
|
|
16848
17201
|
continue;
|
|
@@ -16930,10 +17283,10 @@ function buildHotspots(issues) {
|
|
|
16930
17283
|
async function collectTddCoverage(entries) {
|
|
16931
17284
|
const specs = [];
|
|
16932
17285
|
for (const entry of entries) {
|
|
16933
|
-
const testCasesPath =
|
|
17286
|
+
const testCasesPath = import_node_path54.default.join(entry.dir, "06_Test-Cases.md");
|
|
16934
17287
|
let tcContent;
|
|
16935
17288
|
try {
|
|
16936
|
-
tcContent = await (0,
|
|
17289
|
+
tcContent = await (0, import_promises52.readFile)(testCasesPath, "utf-8");
|
|
16937
17290
|
} catch {
|
|
16938
17291
|
continue;
|
|
16939
17292
|
}
|
|
@@ -16965,10 +17318,10 @@ async function collectTddCoverage(entries) {
|
|
|
16965
17318
|
});
|
|
16966
17319
|
continue;
|
|
16967
17320
|
}
|
|
16968
|
-
const tddListPath =
|
|
17321
|
+
const tddListPath = import_node_path54.default.join(entry.dir, "tdd", "test-list.md");
|
|
16969
17322
|
let tddContent;
|
|
16970
17323
|
try {
|
|
16971
|
-
tddContent = await (0,
|
|
17324
|
+
tddContent = await (0, import_promises52.readFile)(tddListPath, "utf-8");
|
|
16972
17325
|
} catch {
|
|
16973
17326
|
specs.push({
|
|
16974
17327
|
specNumber: entry.specNumber,
|