Skip to content

Commit 08087a0

Browse files
authored
Merge pull request #29 from gabelul/feat/persona-interaction-evidence
feat: ground persona reactions in the whole scrolled page and whether it works
2 parents 6dc7835 + ffd9b18 commit 08087a0

4 files changed

Lines changed: 34 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Agents use `pixelslop-tools` (bin/pixelslop-tools.cjs) for all state operations.
150150
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.
151151

152152
- **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).
153+
- **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.
153154
- **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.
154155
- **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.
155156
- **The internal evaluator count is now 8** (6 pillars + design-director + persona). `evaluator.test.js` pins it.

dist/agents/internal/pixelslop-eval-persona.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ Read dist/skill/resources/visual-eval.md # What the viewports mean and wh
3737

3838
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.
3939

40-
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.
40+
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.
41+
42+
**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.
4143

4244
3. **React first — the five-second read.** Before you touch a single measured number, say what actually happens when this page loads for you:
4345
- 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
4648
- Do you trust it? Would you keep going, or bounce?
4749
Write this in the persona's voice. It should read like a person describing their experience, not a QA log.
4850

49-
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).
51+
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:
52+
53+
**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."
54+
55+
**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):
56+
- `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.
57+
- `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).
58+
- `viewports.mobile.touchTargets` — are the tap targets big enough for a thumb? Casey's problem, not the design-critic's.
59+
- `hoverStates` — do interactive elements give any feedback on hover?
60+
61+
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).
5062

5163
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.
5264

dist/skill/resources/evidence-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ Present when `confidence.scrollData` is true. Contains fold-by-fold page scroll
500500
| `scroll.ratio` | number | Page height / viewport height. Values above 8 suggest very long pages. |
501501
| `scroll.stickyElements` | array | Sticky/fixed elements that persist across scroll positions. Empty = no persistent navigation. |
502502

503-
Other scroll sub-fields (`foldScreenshots`, `lazyImages`, `belowFoldTypography`, `belowFoldColors`) are collector internals not yet promoted to evaluator inputs.
503+
`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.
504504

505505
---
506506

tests/vision-personas.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ describe('the persona evaluator is vision-first', () => {
4747
'output must carry the fields the Persona Insights anchors need');
4848
assert.ok(/never a score|no.*score|\/20/i.test(agent), 'must never produce a /20 score');
4949
});
50+
51+
it('reads the whole scrolled page, not just the hero', () => {
52+
assert.ok(/foldScreenshots/.test(agent), 'must read scroll.foldScreenshots for the below-fold view');
53+
assert.ok(/don.?t stop at the fold|below the first screen|whole.*scrolled page/i.test(agent),
54+
'must react to the page beyond the first screen');
55+
});
56+
57+
it('grounds reactions in behavioral evidence, not just how the still looks', () => {
58+
// The point of the enrichment: "does the flow work", from evidence the collector already captured.
59+
assert.ok(/interactivePromises/.test(agent), 'must consult click→verify results');
60+
assert.ok(/focusPass/.test(agent), 'must consult keyboard focus evidence');
61+
assert.ok(/touchTargets/.test(agent), 'must consult touch-target evidence');
62+
assert.ok(/whether it works|whether the flow work|did the.*work|behavioral/i.test(agent),
63+
'must frame it as whether the page actually works');
64+
// Null pass = didn't run; the agent must not invent it.
65+
assert.ok(/null means the pass didn.?t run|don.?t invent/i.test(agent),
66+
'a missing interaction pass must not be fabricated');
67+
});
5068
});
5169

5270
describe('the orchestrator selects page-relevant personas', () => {

0 commit comments

Comments
 (0)