diff --git a/CLAUDE.md b/CLAUDE.md index 7dc24da..f9a4bd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,6 +150,7 @@ Agents use `pixelslop-tools` (bin/pixelslop-tools.cjs) for all state operations. The measured /20 is the objective backbone; the perceptual layer is how the page actually *reads to a human*. It never touches the score — it's judgment, grounded in what was seen. - **Personas are vision-first.** Each page-relevant persona is a spawned read-only vision agent (`dist/agents/internal/pixelslop-eval-persona.md`) that opens the screenshots and reacts as that human first — the five-second read, eye-path, trust, bounce — then grounds the reaction in measured evidence. Not all 8 run: the orchestrator picks the ~4 that matter for the page type via `browser analyze-page` → `suggestedPersonas`, plus project-specific personas. They spawn independently (blind to each other), with an inline fallback like the other evaluators. Returns `{ humanName, name, narrative, issues, priority, workedWell, reactedTo }` — `reactedTo` proves it opened a screenshot. This replaced the old inline "match triggers against measured findings" synthesis (a voice narrating the spreadsheet). +- **Personas ground in behavior, not just the still.** Beyond the three above-fold viewport shots, the persona evaluator reads `scroll.foldScreenshots` (the whole scrolled page) and the interaction evidence the collector already captured — `interactivePromises.results` (did the mobile menu open, did the anchor jump), `focusPass` (keyboard reachability + focus rings), `viewports.mobile.touchTargets`, `hoverStates`. So "would I bounce" is grounded in whether the flow *works*, not only how the hero looks. Each persona pulls the evidence its `designPriorities` care about; a null pass means it didn't run and must not be invented. All deterministic — it's evidence collected once, not the persona re-driving the browser. - **Project persona sourcing (in priority order).** The tailored persona comes from the best available signal: (1) an explicit audience/brand description in `.pixelslop.md` (setup) — always wins; (2) failing that, a **hero-inference fallback** in Step 6a — if the desktop hero pitches a specific, nameable audience ("The product development system for teams and agents"), infer one persona from it, tagged as an unconfirmed hypothesis; (3) failing that, the built-ins. The fallback self-gates: a bare search box, app shell, or generic splash produces nothing (verified against Linear/Stripe/Airbnb heroes — sharp on the first two, correctly silent on Airbnb's search box). Text about the audience always beats a guess from pixels, so the fallback only fires when there was no text to begin with. - **The Read co-headlines.** The design-director's verdict + the sharpest persona reactions lead the report: a `### The Read` section above the Scores, a `Reads as:` header line, a co-led scan summary (`Measured: X/20` **and** `Reads as: …`), and an HTML card next to the /20 (`{{PERCEPTUAL_READ}}`, from `scan.perceptualRead`). It's prose grounded in what was seen — **never a competing number.** A second "/10" would be judgment masquerading as measurement, the exact failure the measured/judgment split prevents. - **The internal evaluator count is now 8** (6 pillars + design-director + persona). `evaluator.test.js` pins it. diff --git a/dist/agents/internal/pixelslop-eval-persona.md b/dist/agents/internal/pixelslop-eval-persona.md index 48ed1c2..a2507b3 100644 --- a/dist/agents/internal/pixelslop-eval-persona.md +++ b/dist/agents/internal/pixelslop-eval-persona.md @@ -37,7 +37,9 @@ Read dist/skill/resources/visual-eval.md # What the viewports mean and wh 1. **Become the persona.** Read the persona JSON. Who are you, why are you on this page, what would make this a good or bad five seconds for you? Load `narrationStyle.voice` for tone and `sampleReactions` for cadence — you'll write *new* text in that voice, never copy the samples. -2. **Open the screenshots — this is the whole point.** The bundle has `viewports.desktop.screenshot`, `viewports.tablet.screenshot`, `viewports.mobile.screenshot` (plus scroll-fold shots if present). `Read` the PNGs for **your** viewports — `browserChecks.viewports` tells you which ones matter for this persona (Casey the rushed-mobile-user lives on `mobile`; the design-critic wants `desktop`). A screenshot you didn't open doesn't count. You do not get to react to a page you haven't seen. +2. **Open the screenshots — this is the whole point.** The bundle has `viewports.desktop.screenshot`, `viewports.tablet.screenshot`, `viewports.mobile.screenshot`. `Read` the PNGs for **your** viewports — `browserChecks.viewports` tells you which ones matter for this persona (Casey the rushed-mobile-user lives on `mobile`; the design-critic wants `desktop`). A screenshot you didn't open doesn't count. You do not get to react to a page you haven't seen. + + **And don't stop at the fold.** If `scroll.foldScreenshots` is present, `Read` those too — they show the page *below* the first screen, the way you'd actually experience it as you scroll. This matters for the reads that are really about scrolling: a rushed-mobile-user who'd "bounce before the CTA" has to see how far down that CTA truly is; a design-critic judging rhythm needs the whole page, not just the hero. Reacting to the above-fold shot alone is reacting to a page you only half-saw. 3. **React first — the five-second read.** Before you touch a single measured number, say what actually happens when this page loads for you: - What do you notice first? Where does your eye land, and is that where it should? @@ -46,7 +48,17 @@ Read dist/skill/resources/visual-eval.md # What the viewports mean and wh - Do you trust it? Would you keep going, or bounce? Write this in the persona's voice. It should read like a person describing their experience, not a QA log. -4. **Ground it second.** Now reach for the evidence bundle — `specialist findings`, `personaChecks` (`headingHierarchy`, `aboveFoldCta`, `imageOptimization`, `cognitiveDensity`, etc.) — and pin your reaction to specifics. "The CTA felt buried" becomes "the CTA felt buried — and `aboveFoldCta` confirms it's below the fold on mobile." The reaction leads; the measurement backs it up. Weight what counts as a real problem by this persona's `designPriorities` (a priority-4 pillar failing hurts far more than a priority-1 one). +4. **Ground it second — in how it looks AND whether it works.** Now reach for the evidence bundle and pin your reaction to specifics. Two kinds of evidence: + + **Static** — `specialist findings`, `personaChecks` (`headingHierarchy`, `aboveFoldCta`, `imageOptimization`, `cognitiveDensity`). "The CTA felt buried" becomes "the CTA felt buried — and `aboveFoldCta` confirms it's below the fold on mobile." + + **Behavioral** — the collector already drove the page, so you can ground "would I actually get this done" in whether it *works*, not just how the still looks. Pull the interaction evidence that matters to *this* persona (skip what doesn't; a field that's null means the pass didn't run — don't invent it): + - `interactivePromises.results` — did the things that must work, work? Each result's `passed` says whether the mobile menu opened, the anchor jumped, the accordion expanded. "The menu didn't open" is a hard bounce for a rushed-mobile-user, not a nitpick. + - `focusPass` — the keyboard-user's whole world: `withoutIndicator` / `missingIndicators` (can I see where I am?) and `nonSemanticClickables` (divs pretending to be buttons I can't reach). + - `viewports.mobile.touchTargets` — are the tap targets big enough for a thumb? Casey's problem, not the design-critic's. + - `hoverStates` — do interactive elements give any feedback on hover? + + The reaction leads; the evidence — visual and behavioral — backs it up. Weight what counts as a real problem by this persona's `designPriorities` (a priority-4 pillar failing hurts far more than a priority-1 one). 5. **Derive the count and priority.** Tally the issues that genuinely bothered *you* (not every measured finding — only what matters to this persona). Priority per the scoring.md rule: High = multiple issues in your priority-4 pillars; Medium = priority-2-3; Low = only minor priority-1 issues. If nothing bothered you and there's nothing notable to praise, say so — an empty persona gets skipped by the orchestrator, not padded. diff --git a/dist/skill/resources/evidence-schema.md b/dist/skill/resources/evidence-schema.md index 85b8d50..0bf5ba4 100644 --- a/dist/skill/resources/evidence-schema.md +++ b/dist/skill/resources/evidence-schema.md @@ -500,7 +500,7 @@ Present when `confidence.scrollData` is true. Contains fold-by-fold page scroll | `scroll.ratio` | number | Page height / viewport height. Values above 8 suggest very long pages. | | `scroll.stickyElements` | array | Sticky/fixed elements that persist across scroll positions. Empty = no persistent navigation. | -Other scroll sub-fields (`foldScreenshots`, `lazyImages`, `belowFoldTypography`, `belowFoldColors`) are collector internals — not yet promoted to evaluator inputs. +`foldScreenshots` is consumed by the **persona evaluator** — it reads the below-fold shots so a persona reacts to the whole scrolled page, not just the hero. The other scroll sub-fields (`lazyImages`, `belowFoldTypography`, `belowFoldColors`) remain collector internals, not yet promoted to evaluator inputs. --- diff --git a/tests/vision-personas.test.js b/tests/vision-personas.test.js index 421d37a..ae7ed60 100644 --- a/tests/vision-personas.test.js +++ b/tests/vision-personas.test.js @@ -47,6 +47,24 @@ describe('the persona evaluator is vision-first', () => { 'output must carry the fields the Persona Insights anchors need'); assert.ok(/never a score|no.*score|\/20/i.test(agent), 'must never produce a /20 score'); }); + + it('reads the whole scrolled page, not just the hero', () => { + assert.ok(/foldScreenshots/.test(agent), 'must read scroll.foldScreenshots for the below-fold view'); + assert.ok(/don.?t stop at the fold|below the first screen|whole.*scrolled page/i.test(agent), + 'must react to the page beyond the first screen'); + }); + + it('grounds reactions in behavioral evidence, not just how the still looks', () => { + // The point of the enrichment: "does the flow work", from evidence the collector already captured. + assert.ok(/interactivePromises/.test(agent), 'must consult click→verify results'); + assert.ok(/focusPass/.test(agent), 'must consult keyboard focus evidence'); + assert.ok(/touchTargets/.test(agent), 'must consult touch-target evidence'); + assert.ok(/whether it works|whether the flow work|did the.*work|behavioral/i.test(agent), + 'must frame it as whether the page actually works'); + // Null pass = didn't run; the agent must not invent it. + assert.ok(/null means the pass didn.?t run|don.?t invent/i.test(agent), + 'a missing interaction pass must not be fabricated'); + }); }); describe('the orchestrator selects page-relevant personas', () => {