feat(ai): enable vision self-check loop on composite designs (Rank 3)

Toggle VALIDATION_ENABLED from false to true so the post-generation
vision LLM validation loop runs. The loop itself was fully built in
design-validation.ts long ago — only ai-runtime-config:109 was holding
it at runtime.

Add VALIDATION_NODE_COUNT_THRESHOLD=30 size-gate so atomic single-tool
outputs (one badge, one chart) skip the +30-90s vision round-trip.
Composite multi-section briefs (full-page mockups, dashboards) easily
clear the threshold and benefit from the screenshot -> vision LLM ->
safe-fix -> re-screenshot rounds.

Pre-validation heuristics (ms-cheap tree walks) still run regardless
of size.

Plumbing was already done before this commit:
- design-screenshot.ts captureRegion() shipped in Phase 1.5
- design-validation.ts MAX_VALIDATION_ROUNDS=3 loop fully implemented
- validate.ts has 4 vision provider paths (Anthropic Agent SDK,
  Codex CLI, OpenCode SDK, Gemini CLI)
This commit only flips the flag and adds the size-gate heuristic.

Tests: 4223/4223 pass; format:check + tsc clean.

Predicted KPI lift: M3 composite +5-10pp (speculative). Vision
catches what schema-coerce + role-hint can't — mis-positioned sibling
sections, missing component spacing, color-contrast issues. Gating by
node count keeps user-perceived latency contained to designs that
actually need it.

Out-of-scope for this commit (followups if needed):
- ab-corpus glm/minimax/deepseek client image_url part injection
  (lets the eval harness exercise vision for KPI verification)
- builtin Zig agent-native runtime image part support (only matters
  if the embedded provider becomes the default)
This commit is contained in:
Fini 2026-05-08 08:30:00 +08:00
parent f7f9226599
commit 3e289ada3c
2 changed files with 42 additions and 1 deletions

View file

@ -106,7 +106,17 @@ export const DESIGN_STREAM_TIMEOUTS = {
} as const;
/** When false, skips the vision LLM validation loop (pre-validation heuristics still run) */
export const VALIDATION_ENABLED = false;
export const VALIDATION_ENABLED = true;
/**
* Minimum total node count in the active page to trigger the vision LLM
* validation loop. Briefs that produce fewer nodes than this threshold
* skip vision (saves +30-90s + vision API tokens) — pre-validation
* heuristics still run regardless. Composite designs (multi-section
* dashboards, full-page mockups) easily clear this; atomic single-tool
* outputs (one badge, one chart) stay fast.
*/
export const VALIDATION_NODE_COUNT_THRESHOLD = 30;
export const VALIDATION_TIMEOUT_MS = 180_000;
export const MAX_VALIDATION_ROUNDS = 3;

View file

@ -9,6 +9,7 @@
import { DEFAULT_FRAME_ID, useDocumentStore } from '@/stores/document-store';
import {
VALIDATION_ENABLED,
VALIDATION_NODE_COUNT_THRESHOLD,
VALIDATION_TIMEOUT_MS,
MAX_VALIDATION_ROUNDS,
VALIDATION_QUALITY_THRESHOLD,
@ -40,6 +41,21 @@ function getValidationSystemPrompt(): string {
// Node tree dump — simplified for LLM context
// ---------------------------------------------------------------------------
function countNodesInActivePage(): number {
const store = useDocumentStore.getState();
const root = store.getNodeById(DEFAULT_FRAME_ID);
if (!root) return 0;
let count = 0;
function walk(node: PenNode): void {
count++;
if ('children' in node && Array.isArray(node.children)) {
for (const child of node.children) walk(child);
}
}
walk(root);
return count;
}
function buildNodeTreeDump(rootId: string): string {
const store = useDocumentStore.getState();
const lines: string[] = [];
@ -276,6 +292,21 @@ export async function runPostGenerationValidation(options?: {
return { applied: totalApplied, skipped: false };
}
// Skip vision loop on small designs — vision validation only earns its
// ~30-90s latency on composite multi-section briefs. Single-component
// outputs (one badge, one chart) are not worth the round-trip.
const nodeCount = countNodesInActivePage();
if (nodeCount < VALIDATION_NODE_COUNT_THRESHOLD) {
clearVisualReference();
emit(
'done',
preFixCount > 0
? `[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 };
}
for (let round = 1; round <= MAX_VALIDATION_ROUNDS; round++) {
const isFirstRound = round === 1;