feat(ai): show per-category breakdown in pre-check status line

Why: when the pre-validation pass auto-fixes issues, the chat panel
just says "Pre-checks: fixed 5 issues" — generic and uninformative.
The user can't tell whether 5 invisible-container fixes happened
(structural, mostly safe), 5 unexpected-rotation fixes (aesthetic,
worth reviewing), or 5 mixed-sibling-padding fixes (consistency, worth
reviewing). With 10 detector categories now (4 original + 6 aesthetic
added in 53435bf7 / 7aef1b14 / cd1e4325 / ad025c95), the per-category
visibility starts to matter.

What:
- runPreValidationFixesDetailed() returns { total, byCategory } where
  byCategory is a per-category count of APPLIED fixes (excludes the
  info-severity skips and the protected-status-bar skip).
- runPreValidationFixes() kept as a thin wrapper returning .total so
  no caller needs to change.
- design-validation.ts now uses the detailed result and formats the
  breakdown as e.g. "fixed 5 (3 text-effect, 2 unexpected-rotation)"
  in the chat panel — sorted by count descending so the dominant
  category surfaces first. Both the no-vision-validation path and the
  size-gated skip path show the breakdown when it exists.

Falls back to the legacy "fixed N issues" format when byCategory is
empty (defensive — should never happen if total > 0). 1080 / 1080 AI
tests still pass — the new return shape is additive and the wrapper
preserves the integer contract.
This commit is contained in:
Fini 2026-05-09 21:27:00 +08:00
parent e8a07e9b08
commit 8475797c7f
2 changed files with 42 additions and 9 deletions

View file

@ -21,19 +21,33 @@ import type { PenNode } from '@/types/pen';
* Detected issues that are skipped — `info` severity (detect-only) and
* protected status-bar removals — are not counted, so callers can rely on
* the return value as a faithful "did anything change" signal.
*
* Use {@link runPreValidationFixesDetailed} when the caller needs the
* per-category breakdown for surfacing in the UI.
*/
export function runPreValidationFixes(): number {
return runPreValidationFixesDetailed().total;
}
export interface PreValidationResult {
total: number;
/** Number of fixes APPLIED per detector category (not detected). */
byCategory: Record<string, number>;
}
export function runPreValidationFixesDetailed(): PreValidationResult {
const store = useDocumentStore.getState();
const root = store.getNodeById(DEFAULT_FRAME_ID);
if (!root) return 0;
if (!root) return { total: 0, byCategory: {} };
const issues = detectAllIssues(root, store.document);
return applyFixes(issues);
}
function applyFixes(issues: Issue[]): number {
function applyFixes(issues: Issue[]): PreValidationResult {
const store = useDocumentStore.getState();
let applied = 0;
const byCategory: Record<string, number> = {};
for (const issue of issues) {
// 'info' severity is detect-only — the detector emits the issue for
// debug visibility but is not confident enough that an auto-fix would
@ -51,16 +65,18 @@ function applyFixes(issues: Issue[]): number {
}
store.removeNode(issue.nodeId);
applied++;
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
console.log(`[Pre-validation] ${issue.nodeId}: removed (${issue.reason})`);
} else {
store.updateNode(issue.nodeId, {
[issue.property]: issue.suggestedValue,
} as Partial<PenNode>);
applied++;
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
console.log(
`[Pre-validation] ${issue.nodeId}: ${issue.property} → ${JSON.stringify(issue.suggestedValue)} (${issue.reason})`,
);
}
}
return applied;
return { total: applied, byCategory };
}

View file

@ -20,7 +20,15 @@ import type { PenNode } from '@/types/pen';
import type { AIProviderType } from '@/types/agent-settings';
import { getCurrentVisualReference, clearVisualReference } from './visual-ref-orchestrator';
import { resolveSkills } from '@zseven-w/pen-ai-skills';
import { runPreValidationFixes } from './design-pre-validation';
import { runPreValidationFixesDetailed } from './design-pre-validation';
/** Stringify per-category counts as "3 text-effect, 2 unexpected-rotation". */
function formatCategoryBreakdown(byCategory: Record<string, number>): string {
return Object.entries(byCategory)
.sort(([, a], [, b]) => b - a)
.map(([cat, n]) => `${n} ${cat}`)
.join(', ');
}
import { captureRootFrameScreenshot } from './design-screenshot';
import {
applyValidationFixes,
@ -287,11 +295,14 @@ export async function runPostGenerationValidation(options?: {
// Pre-validation: pure code checks (no LLM needed)
emit('streaming', '[pending] Running pre-checks...');
const preFixCount = runPreValidationFixes();
const preFix = runPreValidationFixesDetailed();
const preFixCount = preFix.total;
if (preFixCount > 0) {
totalApplied += preFixCount;
log[log.length - 1] =
`[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`;
const breakdown = formatCategoryBreakdown(preFix.byCategory);
log[log.length - 1] = breakdown
? `[done] Pre-checks: fixed ${preFixCount} (${breakdown})`
: `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`;
} else {
log[log.length - 1] = '[done] Pre-checks: OK';
}
@ -300,10 +311,13 @@ export async function runPostGenerationValidation(options?: {
// If LLM validation is disabled, stop after pre-checks
if (!VALIDATION_ENABLED) {
clearVisualReference();
const breakdown = formatCategoryBreakdown(preFix.byCategory);
emit(
'done',
preFixCount > 0
? `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`
? breakdown
? `[done] Pre-checks: fixed ${preFixCount} (${breakdown})`
: `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''}`
: '[done] Pre-checks complete',
);
return { applied: totalApplied, skipped: false };
@ -315,10 +329,13 @@ export async function runPostGenerationValidation(options?: {
const nodeCount = countNodesInActivePage();
if (nodeCount < VALIDATION_NODE_COUNT_THRESHOLD) {
clearVisualReference();
const breakdown = formatCategoryBreakdown(preFix.byCategory);
emit(
'done',
preFixCount > 0
? `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''} (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})`
? breakdown
? `[done] Pre-checks: fixed ${preFixCount} (${breakdown}) (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})`
: `[done] Pre-checks: fixed ${preFixCount} issue${preFixCount > 1 ? 's' : ''} (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})`
: `[done] Pre-checks complete (vision skipped: ${nodeCount} nodes < ${VALIDATION_NODE_COUNT_THRESHOLD})`,
);
return { applied: totalApplied, skipped: false };