Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions dist/agents/internal/pixelslop-eval-persona.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion dist/skill/resources/evidence-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
18 changes: 18 additions & 0 deletions tests/vision-personas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading