Skip to content

Commit 3da5a55

Browse files
davidercruzclaude
andcommitted
feat(persona-quiz): branching decision tree + sub-personas, reveal fidelity
The v2 quiz branched only twice per path, so ~91% of answers couldn't change the revealed persona and the questionnaire read as repetitive. Each domain is now a depth-3 decision tree (a branch in ~every phase) resolving to one of 8 sub-personas (32 total); paths drop from ~15 templated questions to 8 high-signal ones. The reveal now tracks the user's answers, seeds tags from the persona's keyTags, raises tagConfidenceFloor to 2, and backfills with domain-aware tags instead of generic javascript/webdev. The graph + archetypes are emitted and validated by generatePersonaQuiz.mjs (no dead pointers, all reachable, >=4 branch points/path, no repeated concept on a path, tags constrained to the system vocabulary), with a jest gate over the committed artifact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ebacafe commit 3da5a55

8 files changed

Lines changed: 2378 additions & 3168 deletions

File tree

packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz/FunnelPersonaQuiz.spec.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ const parameters: FunnelStepPersonaQuiz['parameters'] = {
168168
name: 'Frontend Dev',
169169
headline: 'TypeScript frontend dev',
170170
description: 'Heavy TS + React feed coming up.',
171-
keyTags: ['react', 'typescript', 'tailwind'],
171+
// `design-systems` isn't produced by any answer below — it can only end
172+
// up in the final tags if the archetype's keyTags seed the list.
173+
keyTags: ['design-systems', 'react', 'typescript', 'tailwind'],
172174
},
173175
{
174176
id: 'backend_dev',
@@ -260,6 +262,26 @@ describe('FunnelPersonaQuiz', () => {
260262
});
261263
});
262264

265+
it('seeds the final tag list with the resolved archetype keyTags', async () => {
266+
const { onTransition } = renderStep();
267+
fireEvent.click(await screen.findByText('Frontend'));
268+
fireEvent.click(await screen.findByText('Yes'));
269+
expect(
270+
await screen.findByText('TypeScript frontend dev'),
271+
).toBeInTheDocument();
272+
fireEvent.click(screen.getByText('Looks good'));
273+
await waitFor(() => {
274+
expect(onTransition).toHaveBeenCalledWith(
275+
expect.objectContaining({
276+
type: FunnelStepTransitionType.Complete,
277+
details: expect.objectContaining({
278+
tags: expect.arrayContaining(['design-systems']),
279+
}),
280+
}),
281+
);
282+
});
283+
});
284+
263285
it('falls back to a tag-based headline when no archetype is resolved', async () => {
264286
renderStep({
265287
parameters: {

packages/shared/src/features/onboarding/steps/FunnelPersonaQuiz/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,21 @@ function FunnelPersonaQuizComponent({
140140
archetypes.find((a) => a.id === terminalQuestion?.archetypeId) ?? null;
141141
setRevealArchetype(archetype);
142142

143+
// Backfill with tags relevant to the chosen domain (the opener answer)
144+
// rather than a generic list, falling back to the generic one.
145+
const domainAnswer = committedAnswers[0]?.optionId;
146+
const fallback =
147+
(domainAnswer && selection.fallbackTagsByDomain?.[domainAnswer]) ||
148+
selection.fallbackTags ||
149+
[];
150+
151+
// Seed the persona's canonical tags first so the reveal headline and the
152+
// followed tags stay coherent, then the user's strongest answer-derived
153+
// tags, then domain backfill. Cap at the target.
143154
const merged = dedupePreserveOrder([
155+
...(archetype?.keyTags ?? []).slice(0, 4),
144156
...accumulatedTags,
145-
...(selection.fallbackTags ?? []),
157+
...fallback,
146158
]).slice(0, selection.targetTotalTags);
147159

148160
// Most tags are already followed (we stream them incrementally above);
@@ -163,6 +175,7 @@ function FunnelPersonaQuizComponent({
163175
accumulatedTags,
164176
selection.targetTotalTags,
165177
selection.fallbackTags,
178+
selection.fallbackTagsByDomain,
166179
enrichmentComplete,
167180
followTags,
168181
refetchPreview,

packages/shared/src/features/onboarding/types/funnel.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,14 @@ export interface FunnelStepPersonaQuizParameters {
418418
maxQuestions: number;
419419
targetTotalTags: number;
420420
tagConfidenceFloor: number;
421+
/** Generic backfill used when no domain-specific list applies. */
421422
fallbackTags?: string[];
423+
/**
424+
* Domain-specific backfill keyed by the Q1 (opener) option id. Preferred
425+
* over `fallbackTags` so a sparse result is topped up with tags relevant
426+
* to the user's chosen domain rather than generic ones.
427+
*/
428+
fallbackTagsByDomain?: Record<string, string[]>;
422429
};
423430
/**
424431
* Finite set of persona archetypes the quiz can resolve to. The user's path

0 commit comments

Comments
 (0)