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.mjs
CHANGED
|
@@ -473,6 +473,69 @@ function normalizeUiux(raw, configPath, issues) {
|
|
|
473
473
|
result.renderEvidence = renderEvidence;
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
+
if (raw.audit !== void 0) {
|
|
477
|
+
const audit = normalizeUiuxAudit(raw.audit, configPath, issues);
|
|
478
|
+
if (audit) {
|
|
479
|
+
result.audit = audit;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
483
|
+
}
|
|
484
|
+
function normalizeUiuxAudit(raw, configPath, issues) {
|
|
485
|
+
if (!isRecord(raw)) {
|
|
486
|
+
issues.push(configIssue(configPath, "uiux.audit \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
const result = {};
|
|
490
|
+
if (raw.enabled !== void 0) {
|
|
491
|
+
if (typeof raw.enabled === "boolean") {
|
|
492
|
+
result.enabled = raw.enabled;
|
|
493
|
+
} else {
|
|
494
|
+
issues.push(configIssue(configPath, "uiux.audit.enabled \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (raw.slopDetection !== void 0) {
|
|
498
|
+
if (typeof raw.slopDetection === "boolean") {
|
|
499
|
+
result.slopDetection = raw.slopDetection;
|
|
500
|
+
} else {
|
|
501
|
+
issues.push(
|
|
502
|
+
configIssue(configPath, "uiux.audit.slopDetection \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (raw.maxPrimaryCtas !== void 0) {
|
|
507
|
+
if (typeof raw.maxPrimaryCtas === "number" && Number.isFinite(raw.maxPrimaryCtas) && raw.maxPrimaryCtas >= 0) {
|
|
508
|
+
result.maxPrimaryCtas = raw.maxPrimaryCtas;
|
|
509
|
+
} else {
|
|
510
|
+
issues.push(
|
|
511
|
+
configIssue(configPath, "uiux.audit.maxPrimaryCtas \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (raw.maxRawTokenLiteralWarnings !== void 0) {
|
|
516
|
+
if (typeof raw.maxRawTokenLiteralWarnings === "number" && Number.isFinite(raw.maxRawTokenLiteralWarnings) && raw.maxRawTokenLiteralWarnings >= 0) {
|
|
517
|
+
result.maxRawTokenLiteralWarnings = raw.maxRawTokenLiteralWarnings;
|
|
518
|
+
} else {
|
|
519
|
+
issues.push(
|
|
520
|
+
configIssue(
|
|
521
|
+
configPath,
|
|
522
|
+
"uiux.audit.maxRawTokenLiteralWarnings \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
523
|
+
)
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (raw.maxDuplicateFindingsPerRule !== void 0) {
|
|
528
|
+
if (typeof raw.maxDuplicateFindingsPerRule === "number" && Number.isFinite(raw.maxDuplicateFindingsPerRule) && raw.maxDuplicateFindingsPerRule >= 0) {
|
|
529
|
+
result.maxDuplicateFindingsPerRule = raw.maxDuplicateFindingsPerRule;
|
|
530
|
+
} else {
|
|
531
|
+
issues.push(
|
|
532
|
+
configIssue(
|
|
533
|
+
configPath,
|
|
534
|
+
"uiux.audit.maxDuplicateFindingsPerRule \u306F0\u4EE5\u4E0A\u306E\u6570\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
476
539
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
477
540
|
}
|
|
478
541
|
function normalizeRenderEvidence(raw, configPath, issues) {
|
|
@@ -3504,8 +3567,8 @@ function formatError4(error) {
|
|
|
3504
3567
|
}
|
|
3505
3568
|
|
|
3506
3569
|
// src/core/report.ts
|
|
3507
|
-
import { readFile as
|
|
3508
|
-
import
|
|
3570
|
+
import { readFile as readFile42 } from "fs/promises";
|
|
3571
|
+
import path54 from "path";
|
|
3509
3572
|
|
|
3510
3573
|
// src/core/contractIndex.ts
|
|
3511
3574
|
import { readFile as readFile10 } from "fs/promises";
|
|
@@ -4263,8 +4326,8 @@ import { readFile as readFile11 } from "fs/promises";
|
|
|
4263
4326
|
import path14 from "path";
|
|
4264
4327
|
import { fileURLToPath } from "url";
|
|
4265
4328
|
async function resolveToolVersion() {
|
|
4266
|
-
if ("1.7.
|
|
4267
|
-
return "1.7.
|
|
4329
|
+
if ("1.7.2".length > 0) {
|
|
4330
|
+
return "1.7.2";
|
|
4268
4331
|
}
|
|
4269
4332
|
try {
|
|
4270
4333
|
const packagePath = resolvePackageJsonPath();
|
|
@@ -11354,7 +11417,7 @@ function collectLayer(layer, layerName, target, errors) {
|
|
|
11354
11417
|
}
|
|
11355
11418
|
function flattenTokens(obj, prefix, target, errors) {
|
|
11356
11419
|
for (const [key, value] of Object.entries(obj)) {
|
|
11357
|
-
const
|
|
11420
|
+
const path55 = `${prefix}.${key}`;
|
|
11358
11421
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
11359
11422
|
const record2 = value;
|
|
11360
11423
|
if ("$value" in record2) {
|
|
@@ -11370,9 +11433,9 @@ function flattenTokens(obj, prefix, target, errors) {
|
|
|
11370
11433
|
if (typeof record2.platform === "string") {
|
|
11371
11434
|
token.platform = record2.platform;
|
|
11372
11435
|
}
|
|
11373
|
-
target.set(
|
|
11436
|
+
target.set(path55, token);
|
|
11374
11437
|
} else {
|
|
11375
|
-
flattenTokens(record2,
|
|
11438
|
+
flattenTokens(record2, path55, target, errors);
|
|
11376
11439
|
}
|
|
11377
11440
|
}
|
|
11378
11441
|
}
|
|
@@ -11382,44 +11445,44 @@ function resolveAllReferences(result) {
|
|
|
11382
11445
|
for (const [key, val] of result.primitives) allTokens.set(key, val);
|
|
11383
11446
|
for (const [key, val] of result.semantics) allTokens.set(key, val);
|
|
11384
11447
|
for (const [key, val] of result.components) allTokens.set(key, val);
|
|
11385
|
-
for (const [
|
|
11386
|
-
resolveTokenRef(
|
|
11448
|
+
for (const [path55] of allTokens) {
|
|
11449
|
+
resolveTokenRef(path55, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
11387
11450
|
}
|
|
11388
11451
|
}
|
|
11389
|
-
function resolveTokenRef(
|
|
11390
|
-
if (result.resolved.has(
|
|
11391
|
-
return result.resolved.get(
|
|
11452
|
+
function resolveTokenRef(path55, allTokens, visited, depth, result) {
|
|
11453
|
+
if (result.resolved.has(path55)) {
|
|
11454
|
+
return result.resolved.get(path55);
|
|
11392
11455
|
}
|
|
11393
11456
|
if (depth > MAX_RESOLVE_DEPTH) {
|
|
11394
11457
|
result.errors.push({
|
|
11395
|
-
message: `Max reference depth exceeded at: ${
|
|
11396
|
-
path:
|
|
11458
|
+
message: `Max reference depth exceeded at: ${path55}`,
|
|
11459
|
+
path: path55
|
|
11397
11460
|
});
|
|
11398
11461
|
return void 0;
|
|
11399
11462
|
}
|
|
11400
|
-
if (visited.has(
|
|
11463
|
+
if (visited.has(path55)) {
|
|
11401
11464
|
result.errors.push({
|
|
11402
|
-
message: `Circular reference detected: ${
|
|
11403
|
-
path:
|
|
11465
|
+
message: `Circular reference detected: ${path55}`,
|
|
11466
|
+
path: path55
|
|
11404
11467
|
});
|
|
11405
11468
|
return void 0;
|
|
11406
11469
|
}
|
|
11407
|
-
const token = allTokens.get(
|
|
11470
|
+
const token = allTokens.get(path55);
|
|
11408
11471
|
if (!token) {
|
|
11409
11472
|
return void 0;
|
|
11410
11473
|
}
|
|
11411
11474
|
if (typeof token.$value !== "string") {
|
|
11412
11475
|
const rawValue2 = stringifyTokenValue(token.$value);
|
|
11413
|
-
result.resolved.set(
|
|
11476
|
+
result.resolved.set(path55, rawValue2);
|
|
11414
11477
|
return rawValue2;
|
|
11415
11478
|
}
|
|
11416
11479
|
const rawValue = stringifyTokenValue(token.$value);
|
|
11417
11480
|
const refs = [...rawValue.matchAll(REF_PATTERN)];
|
|
11418
11481
|
if (refs.length === 0) {
|
|
11419
|
-
result.resolved.set(
|
|
11482
|
+
result.resolved.set(path55, rawValue);
|
|
11420
11483
|
return rawValue;
|
|
11421
11484
|
}
|
|
11422
|
-
visited.add(
|
|
11485
|
+
visited.add(path55);
|
|
11423
11486
|
let resolved = rawValue;
|
|
11424
11487
|
for (const ref of refs) {
|
|
11425
11488
|
const refPath = ref[1];
|
|
@@ -11427,8 +11490,8 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11427
11490
|
const refToken = allTokens.get(refPath);
|
|
11428
11491
|
if (!refToken) {
|
|
11429
11492
|
result.errors.push({
|
|
11430
|
-
message: `Unresolved token reference: {${refPath}} at ${
|
|
11431
|
-
path:
|
|
11493
|
+
message: `Unresolved token reference: {${refPath}} at ${path55}`,
|
|
11494
|
+
path: path55
|
|
11432
11495
|
});
|
|
11433
11496
|
continue;
|
|
11434
11497
|
}
|
|
@@ -11437,7 +11500,7 @@ function resolveTokenRef(path53, allTokens, visited, depth, result) {
|
|
|
11437
11500
|
resolved = resolved.split(`{${refPath}}`).join(refValue);
|
|
11438
11501
|
}
|
|
11439
11502
|
}
|
|
11440
|
-
result.resolved.set(
|
|
11503
|
+
result.resolved.set(path55, resolved);
|
|
11441
11504
|
return resolved;
|
|
11442
11505
|
}
|
|
11443
11506
|
function stringifyTokenValue(value) {
|
|
@@ -15494,6 +15557,252 @@ async function validateDiscussionDesignHardening(root, config) {
|
|
|
15494
15557
|
return issues;
|
|
15495
15558
|
}
|
|
15496
15559
|
|
|
15560
|
+
// src/core/validators/designAudit.ts
|
|
15561
|
+
import { readdir as readdir9 } from "fs/promises";
|
|
15562
|
+
import path52 from "path";
|
|
15563
|
+
var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
|
|
15564
|
+
function resolveAuditConfig(config) {
|
|
15565
|
+
const audit = config.uiux?.audit;
|
|
15566
|
+
const profile = config.uiux?.qualityProfile ?? "default";
|
|
15567
|
+
return {
|
|
15568
|
+
enabled: audit?.enabled ?? true,
|
|
15569
|
+
slopDetection: audit?.slopDetection ?? true,
|
|
15570
|
+
qualityProfile: profile,
|
|
15571
|
+
maxPrimaryCtas: audit?.maxPrimaryCtas ?? 1,
|
|
15572
|
+
maxRawTokenLiteralWarnings: audit?.maxRawTokenLiteralWarnings ?? 5,
|
|
15573
|
+
maxDuplicateFindingsPerRule: audit?.maxDuplicateFindingsPerRule ?? 5
|
|
15574
|
+
};
|
|
15575
|
+
}
|
|
15576
|
+
function mapSeverity(tier, profile, category) {
|
|
15577
|
+
if (tier === 1) return "error";
|
|
15578
|
+
if (tier === 2) return profile === "strict" ? "error" : "warning";
|
|
15579
|
+
if (profile === "default") {
|
|
15580
|
+
return category && COSMETIC_CATEGORIES.includes(category) ? "info" : "warning";
|
|
15581
|
+
}
|
|
15582
|
+
return "warning";
|
|
15583
|
+
}
|
|
15584
|
+
function findingToIssue(finding, profile, rulePrefix = "audit") {
|
|
15585
|
+
const severity = mapSeverity(finding.severityTier, profile, finding.dimension);
|
|
15586
|
+
return issue(
|
|
15587
|
+
finding.ruleId,
|
|
15588
|
+
finding.message,
|
|
15589
|
+
severity,
|
|
15590
|
+
finding.file,
|
|
15591
|
+
`${rulePrefix}.${finding.dimension}`,
|
|
15592
|
+
finding.evidence.length > 0 ? finding.evidence : void 0,
|
|
15593
|
+
"compatibility",
|
|
15594
|
+
finding.guidance
|
|
15595
|
+
);
|
|
15596
|
+
}
|
|
15597
|
+
function extractSection2(content, heading) {
|
|
15598
|
+
const idx = content.indexOf(heading);
|
|
15599
|
+
if (idx === -1) return null;
|
|
15600
|
+
const start = idx + heading.length;
|
|
15601
|
+
const headingLevel = heading.match(/^#+/)?.[0]?.length ?? 3;
|
|
15602
|
+
const rest = content.slice(start);
|
|
15603
|
+
const headingPattern = new RegExp(`^#{1,${headingLevel}} `, "m");
|
|
15604
|
+
const nextHeadingMatch = headingPattern.exec(rest);
|
|
15605
|
+
const sectionContent = nextHeadingMatch ? rest.slice(0, nextHeadingMatch.index) : rest;
|
|
15606
|
+
return sectionContent.trim() || null;
|
|
15607
|
+
}
|
|
15608
|
+
function checkCtaHierarchy(content, auditConfig, file) {
|
|
15609
|
+
const findings = [];
|
|
15610
|
+
const ctaSection = extractSection2(content, "### CTA Hierarchy");
|
|
15611
|
+
if (!ctaSection) return findings;
|
|
15612
|
+
const primaryLines = ctaSection.match(/^-\s*Primary:/gm) || [];
|
|
15613
|
+
const primaryCount = primaryLines.length;
|
|
15614
|
+
if (primaryCount === 0) {
|
|
15615
|
+
findings.push({
|
|
15616
|
+
ruleId: "QFAI-AUD-001",
|
|
15617
|
+
dimension: "visualHierarchy",
|
|
15618
|
+
severityTier: 1,
|
|
15619
|
+
message: "No primary CTA defined in CTA Hierarchy",
|
|
15620
|
+
why: "Every UI screen needs a clear primary action to guide users",
|
|
15621
|
+
evidence: [],
|
|
15622
|
+
guidance: "Define at least one primary CTA in the CTA Hierarchy section",
|
|
15623
|
+
file
|
|
15624
|
+
});
|
|
15625
|
+
}
|
|
15626
|
+
if (primaryCount > auditConfig.maxPrimaryCtas) {
|
|
15627
|
+
findings.push({
|
|
15628
|
+
ruleId: "QFAI-AUD-020",
|
|
15629
|
+
dimension: "visualHierarchy",
|
|
15630
|
+
severityTier: 2,
|
|
15631
|
+
message: `Multiple primary CTAs detected (${primaryCount} > ${auditConfig.maxPrimaryCtas})`,
|
|
15632
|
+
why: "Multiple primary CTAs create decision paralysis and weaken visual hierarchy",
|
|
15633
|
+
evidence: primaryLines.map((l) => l.trim()),
|
|
15634
|
+
guidance: "Reduce to a single primary CTA per screen; demote others to secondary",
|
|
15635
|
+
file
|
|
15636
|
+
});
|
|
15637
|
+
}
|
|
15638
|
+
return findings;
|
|
15639
|
+
}
|
|
15640
|
+
var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)/g;
|
|
15641
|
+
async function checkTokenDrift(root, auditConfig, cfg) {
|
|
15642
|
+
const findings = [];
|
|
15643
|
+
const configuredDir = cfg.uiux?.designTokensDir;
|
|
15644
|
+
const tokensDir = configuredDir ? path52.resolve(root, configuredDir) : path52.join(root, cfg.paths.contractsDir, "design");
|
|
15645
|
+
let hasTokenFiles = false;
|
|
15646
|
+
try {
|
|
15647
|
+
const entries = await readdir9(tokensDir);
|
|
15648
|
+
hasTokenFiles = entries.some((e) => /\.ya?ml$/i.test(e));
|
|
15649
|
+
} catch {
|
|
15650
|
+
return findings;
|
|
15651
|
+
}
|
|
15652
|
+
if (!hasTokenFiles) return findings;
|
|
15653
|
+
const contractsUiDir = path52.join(root, cfg.paths.contractsDir, "ui");
|
|
15654
|
+
let htmlFiles = [];
|
|
15655
|
+
try {
|
|
15656
|
+
const entries = await readdir9(contractsUiDir);
|
|
15657
|
+
htmlFiles = entries.filter((e) => /\.html?$/i.test(e));
|
|
15658
|
+
} catch {
|
|
15659
|
+
return findings;
|
|
15660
|
+
}
|
|
15661
|
+
let rawCount = 0;
|
|
15662
|
+
const sampleLiterals = [];
|
|
15663
|
+
for (const htmlFile of htmlFiles) {
|
|
15664
|
+
const content = await readSafe(path52.join(contractsUiDir, htmlFile));
|
|
15665
|
+
if (!content) continue;
|
|
15666
|
+
const matches = content.match(RAW_COLOR_RE);
|
|
15667
|
+
if (matches) {
|
|
15668
|
+
rawCount += matches.length;
|
|
15669
|
+
for (const m of matches) {
|
|
15670
|
+
if (sampleLiterals.length < 10) {
|
|
15671
|
+
sampleLiterals.push(m.toLowerCase());
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
}
|
|
15675
|
+
}
|
|
15676
|
+
if (rawCount > auditConfig.maxRawTokenLiteralWarnings) {
|
|
15677
|
+
findings.push({
|
|
15678
|
+
ruleId: "QFAI-AUD-004",
|
|
15679
|
+
dimension: "tokenDiscipline",
|
|
15680
|
+
severityTier: 1,
|
|
15681
|
+
message: `Token drift: ${rawCount} raw color literal occurrences found (threshold: ${auditConfig.maxRawTokenLiteralWarnings})`,
|
|
15682
|
+
why: "Raw color values bypass design tokens, causing visual inconsistency",
|
|
15683
|
+
evidence: sampleLiterals,
|
|
15684
|
+
guidance: "Replace raw color literals with design token references"
|
|
15685
|
+
});
|
|
15686
|
+
}
|
|
15687
|
+
return findings;
|
|
15688
|
+
}
|
|
15689
|
+
function deduplicateFindings(issues, maxPerRule) {
|
|
15690
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15691
|
+
const result = [];
|
|
15692
|
+
for (const iss of issues) {
|
|
15693
|
+
const count = counts.get(iss.code) ?? 0;
|
|
15694
|
+
if (count < maxPerRule) {
|
|
15695
|
+
result.push(iss);
|
|
15696
|
+
}
|
|
15697
|
+
counts.set(iss.code, count + 1);
|
|
15698
|
+
}
|
|
15699
|
+
for (const [code, count] of counts) {
|
|
15700
|
+
if (count > maxPerRule) {
|
|
15701
|
+
result.push({
|
|
15702
|
+
code,
|
|
15703
|
+
severity: "info",
|
|
15704
|
+
category: "compatibility",
|
|
15705
|
+
message: `${count - maxPerRule} additional "${code}" finding(s) suppressed (max ${maxPerRule} per rule)`,
|
|
15706
|
+
rule: `audit.dedup.${code}`
|
|
15707
|
+
});
|
|
15708
|
+
}
|
|
15709
|
+
}
|
|
15710
|
+
return result;
|
|
15711
|
+
}
|
|
15712
|
+
async function validateDesignAudit(root, config) {
|
|
15713
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15714
|
+
if (!auditConfig.enabled) return [];
|
|
15715
|
+
const discussionDir = path52.join(root, config.paths.discussionDir);
|
|
15716
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15717
|
+
if (!packRoot) return [];
|
|
15718
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15719
|
+
if (!uiBearing) return [];
|
|
15720
|
+
const storyPath = path52.join(packRoot, "03_Story-Workshop.md");
|
|
15721
|
+
const content = await readSafe(storyPath);
|
|
15722
|
+
if (!content) return [];
|
|
15723
|
+
const findings = [];
|
|
15724
|
+
findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
|
|
15725
|
+
findings.push(...await checkTokenDrift(root, auditConfig, config));
|
|
15726
|
+
const issues = findings.map((f) => findingToIssue(f, auditConfig.qualityProfile));
|
|
15727
|
+
return deduplicateFindings(issues, auditConfig.maxDuplicateFindingsPerRule);
|
|
15728
|
+
}
|
|
15729
|
+
|
|
15730
|
+
// src/core/validators/designSlop.ts
|
|
15731
|
+
import { existsSync as existsSync2 } from "fs";
|
|
15732
|
+
import { readFile as readFile41 } from "fs/promises";
|
|
15733
|
+
import path53 from "path";
|
|
15734
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
15735
|
+
function isValidSlopPattern(rule) {
|
|
15736
|
+
if (typeof rule !== "object" || rule === null) return false;
|
|
15737
|
+
const r = rule;
|
|
15738
|
+
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";
|
|
15739
|
+
}
|
|
15740
|
+
async function loadSlopPatterns(jsonPath) {
|
|
15741
|
+
const raw = await readFile41(jsonPath, "utf-8");
|
|
15742
|
+
const parsed = JSON.parse(raw);
|
|
15743
|
+
if (!Array.isArray(parsed)) return [];
|
|
15744
|
+
return parsed.filter((r) => isValidSlopPattern(r));
|
|
15745
|
+
}
|
|
15746
|
+
function defaultPatternsPath() {
|
|
15747
|
+
const base = import.meta.url;
|
|
15748
|
+
const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
|
|
15749
|
+
const baseDir = path53.dirname(basePath);
|
|
15750
|
+
const candidates = [
|
|
15751
|
+
path53.join(baseDir, "designSlopPatterns.json"),
|
|
15752
|
+
path53.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
|
|
15753
|
+
path53.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
|
|
15754
|
+
];
|
|
15755
|
+
for (const c of candidates) {
|
|
15756
|
+
if (existsSync2(c)) return c;
|
|
15757
|
+
}
|
|
15758
|
+
return candidates[0];
|
|
15759
|
+
}
|
|
15760
|
+
async function validateDesignSlop(root, config) {
|
|
15761
|
+
const auditConfig = resolveAuditConfig(config);
|
|
15762
|
+
if (!auditConfig.enabled) return [];
|
|
15763
|
+
if (!auditConfig.slopDetection) return [];
|
|
15764
|
+
const discussionDir = path53.join(root, config.paths.discussionDir);
|
|
15765
|
+
const packRoot = await findLatestDiscussionPackDir(discussionDir);
|
|
15766
|
+
if (!packRoot) return [];
|
|
15767
|
+
const uiBearing = await isUiBearing(packRoot);
|
|
15768
|
+
if (!uiBearing) return [];
|
|
15769
|
+
let patterns;
|
|
15770
|
+
try {
|
|
15771
|
+
patterns = await loadSlopPatterns(defaultPatternsPath());
|
|
15772
|
+
} catch {
|
|
15773
|
+
return [];
|
|
15774
|
+
}
|
|
15775
|
+
const findings = [];
|
|
15776
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
15777
|
+
for (const pattern of patterns) {
|
|
15778
|
+
let regex;
|
|
15779
|
+
try {
|
|
15780
|
+
regex = new RegExp(pattern.match, "gi");
|
|
15781
|
+
} catch {
|
|
15782
|
+
continue;
|
|
15783
|
+
}
|
|
15784
|
+
for (const scope of pattern.scopes) {
|
|
15785
|
+
const filePath = path53.join(packRoot, scope);
|
|
15786
|
+
const content = await readSafe(filePath);
|
|
15787
|
+
if (!content) continue;
|
|
15788
|
+
if (regex.test(content) && !seenRules.has(pattern.id)) {
|
|
15789
|
+
seenRules.add(pattern.id);
|
|
15790
|
+
findings.push({
|
|
15791
|
+
ruleId: pattern.id,
|
|
15792
|
+
dimension: pattern.category,
|
|
15793
|
+
severityTier: pattern.tier,
|
|
15794
|
+
message: pattern.message,
|
|
15795
|
+
why: `Slop pattern "${pattern.id}" matched in ${scope}`,
|
|
15796
|
+
evidence: [],
|
|
15797
|
+
guidance: pattern.guidance,
|
|
15798
|
+
file: scope
|
|
15799
|
+
});
|
|
15800
|
+
}
|
|
15801
|
+
}
|
|
15802
|
+
}
|
|
15803
|
+
return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
|
|
15804
|
+
}
|
|
15805
|
+
|
|
15497
15806
|
// src/core/validate.ts
|
|
15498
15807
|
var UIUX_VALIDATION_BUDGET_MS = 2e3;
|
|
15499
15808
|
async function validateProject(root, configResult, options = {}) {
|
|
@@ -15511,7 +15820,9 @@ async function validateProject(root, configResult, options = {}) {
|
|
|
15511
15820
|
() => validateBpApDb(root, config),
|
|
15512
15821
|
() => validateUiDefinitionConsistency(root, config),
|
|
15513
15822
|
() => validateResearchSummary(root, config),
|
|
15514
|
-
() => validateAgentDefinition(root, config)
|
|
15823
|
+
() => validateAgentDefinition(root, config),
|
|
15824
|
+
() => validateDesignAudit(root, config),
|
|
15825
|
+
() => validateDesignSlop(root, config)
|
|
15515
15826
|
];
|
|
15516
15827
|
const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
|
|
15517
15828
|
const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
|
|
@@ -15598,15 +15909,15 @@ var REPORT_GUARDRAILS_MAX = 20;
|
|
|
15598
15909
|
var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
|
|
15599
15910
|
var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
|
|
15600
15911
|
async function createReportData(root, validation, configResult) {
|
|
15601
|
-
const resolvedRoot =
|
|
15912
|
+
const resolvedRoot = path54.resolve(root);
|
|
15602
15913
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
15603
15914
|
const config = resolved.config;
|
|
15604
15915
|
const configPath = resolved.configPath;
|
|
15605
15916
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
15606
15917
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
15607
|
-
const apiRoot =
|
|
15608
|
-
const uiRoot =
|
|
15609
|
-
const dbRoot =
|
|
15918
|
+
const apiRoot = path54.join(contractsRoot, "api");
|
|
15919
|
+
const uiRoot = path54.join(contractsRoot, "ui");
|
|
15920
|
+
const dbRoot = path54.join(contractsRoot, "db");
|
|
15610
15921
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
15611
15922
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
15612
15923
|
const specEntries = await collectSpecEntries(specsRoot);
|
|
@@ -15934,6 +16245,8 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
15934
16245
|
lines.push("");
|
|
15935
16246
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
15936
16247
|
lines.push("- [Change Issues](#change-issues)");
|
|
16248
|
+
lines.push("- [Design Audit Findings](#design-audit-findings)");
|
|
16249
|
+
lines.push("- [Slop Guardrails Findings](#slop-guardrails-findings)");
|
|
15937
16250
|
lines.push("- [Change Type](#change-type)");
|
|
15938
16251
|
lines.push("- [Waivers](#waivers)");
|
|
15939
16252
|
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
@@ -16082,6 +16395,46 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
16082
16395
|
lines.push("### Issues");
|
|
16083
16396
|
lines.push("");
|
|
16084
16397
|
lines.push(...formatIssueCards(issuesByCategory.change));
|
|
16398
|
+
const auditIssues = data.issues.filter((i) => /^QFAI-AUD-/.test(i.code));
|
|
16399
|
+
if (auditIssues.length > 0) {
|
|
16400
|
+
lines.push("## Design Audit Findings");
|
|
16401
|
+
lines.push("");
|
|
16402
|
+
const byDimension = /* @__PURE__ */ new Map();
|
|
16403
|
+
for (const iss of auditIssues) {
|
|
16404
|
+
const dim = iss.rule?.replace(/^audit\./, "").split(".")[0] ?? "unknown";
|
|
16405
|
+
const group = byDimension.get(dim) ?? [];
|
|
16406
|
+
group.push(iss);
|
|
16407
|
+
byDimension.set(dim, group);
|
|
16408
|
+
}
|
|
16409
|
+
for (const [dim, dimIssues] of byDimension) {
|
|
16410
|
+
lines.push(`### ${dim}`);
|
|
16411
|
+
lines.push("");
|
|
16412
|
+
for (const iss of dimIssues) {
|
|
16413
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16414
|
+
}
|
|
16415
|
+
lines.push("");
|
|
16416
|
+
}
|
|
16417
|
+
}
|
|
16418
|
+
const slopIssues = data.issues.filter((i) => /^SLP-/.test(i.code));
|
|
16419
|
+
if (slopIssues.length > 0) {
|
|
16420
|
+
lines.push("## Slop Guardrails Findings");
|
|
16421
|
+
lines.push("");
|
|
16422
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
16423
|
+
for (const iss of slopIssues) {
|
|
16424
|
+
const cat = iss.rule?.replace(/^slop\./, "").split(".")[0] ?? "unknown";
|
|
16425
|
+
const group = byCategory.get(cat) ?? [];
|
|
16426
|
+
group.push(iss);
|
|
16427
|
+
byCategory.set(cat, group);
|
|
16428
|
+
}
|
|
16429
|
+
for (const [cat, catIssues] of byCategory) {
|
|
16430
|
+
lines.push(`### ${cat}`);
|
|
16431
|
+
lines.push("");
|
|
16432
|
+
for (const iss of catIssues) {
|
|
16433
|
+
lines.push(`- **${iss.severity.toUpperCase()}** [${iss.code}] ${iss.message}`);
|
|
16434
|
+
}
|
|
16435
|
+
lines.push("");
|
|
16436
|
+
}
|
|
16437
|
+
}
|
|
16085
16438
|
lines.push("## Change Type");
|
|
16086
16439
|
lines.push("");
|
|
16087
16440
|
lines.push("### Summary");
|
|
@@ -16501,7 +16854,7 @@ async function collectChangeTypeSummary(specsRoot) {
|
|
|
16501
16854
|
};
|
|
16502
16855
|
const deltaFiles = await collectDeltaFiles(specsRoot);
|
|
16503
16856
|
for (const deltaFile of deltaFiles) {
|
|
16504
|
-
const text = await
|
|
16857
|
+
const text = await readFile42(deltaFile, "utf-8");
|
|
16505
16858
|
const parsed = parseDeltaV1(text);
|
|
16506
16859
|
for (const entry of parsed.entries) {
|
|
16507
16860
|
if (!entry.meta) {
|
|
@@ -16538,7 +16891,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
16538
16891
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
16539
16892
|
}
|
|
16540
16893
|
for (const file of specFiles) {
|
|
16541
|
-
const text = await
|
|
16894
|
+
const text = await readFile42(file, "utf-8");
|
|
16542
16895
|
const parsed = parseSpec(text, file);
|
|
16543
16896
|
const specKey = parsed.specId;
|
|
16544
16897
|
if (!specKey) {
|
|
@@ -16575,7 +16928,7 @@ async function collectIds(files) {
|
|
|
16575
16928
|
result[prefix] = /* @__PURE__ */ new Set();
|
|
16576
16929
|
}
|
|
16577
16930
|
for (const file of files) {
|
|
16578
|
-
const text = await
|
|
16931
|
+
const text = await readFile42(file, "utf-8");
|
|
16579
16932
|
for (const prefix of ID_PREFIXES) {
|
|
16580
16933
|
const ids = extractIds(text, prefix);
|
|
16581
16934
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -16590,7 +16943,7 @@ async function collectIds(files) {
|
|
|
16590
16943
|
async function collectUpstreamIds(files) {
|
|
16591
16944
|
const ids = /* @__PURE__ */ new Set();
|
|
16592
16945
|
for (const file of files) {
|
|
16593
|
-
const text = await
|
|
16946
|
+
const text = await readFile42(file, "utf-8");
|
|
16594
16947
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
16595
16948
|
}
|
|
16596
16949
|
return ids;
|
|
@@ -16611,7 +16964,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
16611
16964
|
}
|
|
16612
16965
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
16613
16966
|
for (const file of targetFiles) {
|
|
16614
|
-
const text = await
|
|
16967
|
+
const text = await readFile42(file, "utf-8");
|
|
16615
16968
|
if (pattern.test(text)) {
|
|
16616
16969
|
return true;
|
|
16617
16970
|
}
|
|
@@ -16730,7 +17083,7 @@ function normalizeScSources(root, sources) {
|
|
|
16730
17083
|
async function countScenarios(scenarioFiles) {
|
|
16731
17084
|
let total = 0;
|
|
16732
17085
|
for (const file of scenarioFiles) {
|
|
16733
|
-
const text = await
|
|
17086
|
+
const text = await readFile42(file, "utf-8");
|
|
16734
17087
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16735
17088
|
if (!document || errors.length > 0) {
|
|
16736
17089
|
continue;
|
|
@@ -16761,7 +17114,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
|
|
|
16761
17114
|
let totalScenarios = 0;
|
|
16762
17115
|
let e2eCount = 0;
|
|
16763
17116
|
for (const file of scenarioFiles) {
|
|
16764
|
-
const text = await
|
|
17117
|
+
const text = await readFile42(file, "utf-8");
|
|
16765
17118
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
16766
17119
|
if (!document || errors.length > 0) {
|
|
16767
17120
|
continue;
|
|
@@ -16849,10 +17202,10 @@ function buildHotspots(issues) {
|
|
|
16849
17202
|
async function collectTddCoverage(entries) {
|
|
16850
17203
|
const specs = [];
|
|
16851
17204
|
for (const entry of entries) {
|
|
16852
|
-
const testCasesPath =
|
|
17205
|
+
const testCasesPath = path54.join(entry.dir, "06_Test-Cases.md");
|
|
16853
17206
|
let tcContent;
|
|
16854
17207
|
try {
|
|
16855
|
-
tcContent = await
|
|
17208
|
+
tcContent = await readFile42(testCasesPath, "utf-8");
|
|
16856
17209
|
} catch {
|
|
16857
17210
|
continue;
|
|
16858
17211
|
}
|
|
@@ -16884,10 +17237,10 @@ async function collectTddCoverage(entries) {
|
|
|
16884
17237
|
});
|
|
16885
17238
|
continue;
|
|
16886
17239
|
}
|
|
16887
|
-
const tddListPath =
|
|
17240
|
+
const tddListPath = path54.join(entry.dir, "tdd", "test-list.md");
|
|
16888
17241
|
let tddContent;
|
|
16889
17242
|
try {
|
|
16890
|
-
tddContent = await
|
|
17243
|
+
tddContent = await readFile42(tddListPath, "utf-8");
|
|
16891
17244
|
} catch {
|
|
16892
17245
|
specs.push({
|
|
16893
17246
|
specNumber: entry.specNumber,
|