Job → Framework → Format — the three-layer taxonomy¶
This is the source of truth. The whole Sonaloop product (core app, website IA, and the
tracker) aligns on the three orthogonal layers below. The machine-readable companion is
sonaloop/taxonomy.json, loaded via sonaloop.job_taxonomy;
keep the two in lock-step. Every repo and ticket tags itself with the layer (and id) it serves.
The doctor analogy¶
You walk into a doctor's office because your stomach hurts — that is the Job: why does my stomach hurt? The doctor runs a process — history → exam → tests → diagnose — that is the Framework. Along the way the doctor reaches for individual tests — a blood test, an X-ray — each of those is a Format.
You arrive with a Job; the doctor runs a Framework; reaches for Formats inside it.
The three are orthogonal axes, not competing lists: a Job runs through a Framework using Formats. The same Format (a council) shows up inside many Frameworks serving many Jobs.
The three layers¶
| Layer | One-liner | What it is |
|---|---|---|
| Job | What the user wants — the use case they buy. | The outcome a customer walks in for ("how is my positioning?", "what should I charge?"). The thing sold and the thing measured. The core has no first-class concept of it today — the host (Claude) improvises it from a free-text goal; this taxonomy gives Jobs stable ids. |
| Framework | The process the run follows end-to-end. | A constellation of steps (a DAG) taking a run from ambiguity to a buildable answer. Frameworks already exist in core as sonaloop/methodologies/*.json (keyed by key) and seed the plan engine. |
| Format | A single move inside a run. | One concrete research move at a plan step: a council, a prototype test, a head-to-head, a red-team. |
Mislabels this model fixes¶
- How-Might-We is a Format-shaped move, not a Job — though it is often sold as one. Here it is folded into the ideation Job.
- "JTBD" is a Job, whose core engine is the
lean_jtbdFramework of the same name. Keep the two layers distinct: the Job (jtbd_demand) runs through the Framework (lean_jtbd).
Frameworks (real methodology keys)¶
Each Framework id maps 1:1 to a methodology key shipped under sonaloop/methodologies/.
Framework id / key |
Name |
|---|---|
double_diamond |
Double Diamond |
double_diamond_deep |
Double Diamond (Deep) |
dschool_micro |
d.school Micro-Cycle |
lean_jtbd |
Lean / Jobs-to-be-Done |
Each Framework is documented in plain language (what it is, when to use it, its diverge→converge
stages) in docs/frameworks.md. The structured descriptions
({id, name, what, when, stages}) are exposed via sonaloop.job_taxonomy.framework_descriptions()
and the list_frameworks / describe_framework MCP tools, so the website "how it works" page and
the job presets draw on one source.
Formats¶
| Format id | Name | Status |
|---|---|---|
council |
Council | implemented |
prototype_test |
Prototype Test | implemented |
head_to_head |
Head-to-Head | implemented |
red_team |
Red-Team | implemented |
head_to_head ships as a Format on top of the artifacts/variant plumbing: it compares two (or more)
concrete options (captured A/B variants or plain text options) side-by-side and the server tallies a
reasoned, segmented preference (preference + margin + segment-splits). Drive it via
brief_head_to_head / record_head_to_head (MCP) — Job presets compose it.
red_team ships as a falsification Format on top of the council plumbing: it deliberately argues the
NEGATIVE case — why would this segment NOT adopt / NOT pay / churn? — to stress-test an idea instead of
flattering it. It re-frames a normal council toward disconfirmation and assigns each persona an explicit
adversarial lens (skeptic / blocker / switching-cost / status-quo / risk); the server groups their
concrete objections by theme into a structured case-against (how many personas raise each blocker +
worst severity), optionally beside the case-for when run with stance="both" (the same question in
both directions). Drive it via brief_red_team / record_red_team (MCP) — Job presets compose it.
The mapping table — Job → Framework(s) → Formats → coverage¶
Each named Job resolves to a concrete default Framework, a set of Formats, and the persona coverage it needs.
Job (id) |
Sells as | Default Framework | Frameworks | Formats | Coverage (min personas · axes) |
|---|---|---|---|---|---|
Positioning (positioning) |
Positioning | double_diamond |
double_diamond |
council, head_to_head, red_team |
4 · segment, buying-stage, current-alternative |
Pricing / Price-Sensitivity (pricing) |
Pricing | lean_jtbd |
lean_jtbd |
council, head_to_head, red_team |
4 · willingness-to-pay, budget-authority, current-alternative |
Demand / Jobs-to-be-Done (jtbd_demand) |
Demand/JTBD | lean_jtbd |
lean_jtbd |
council, prototype_test |
5 · segment, trigger-moment, current-alternative |
Ideation (How-Might-We) (ideation_hmw) |
Ideation (HMW) | dschool_micro |
dschool_micro, double_diamond_deep |
council, prototype_test, head_to_head |
4 · segment, expertise-level, extreme-user |
Continuous Discovery (continuous_discovery) |
Continuous discovery | dschool_micro |
dschool_micro |
council, prototype_test |
3 · segment, lifecycle-stage, recency-of-use |
Churn Reasons (churn_reasons) |
Churn reasons | lean_jtbd |
lean_jtbd |
council, red_team |
4 · churn-reason, tenure, current-alternative |
A/B Test (ab_test) |
A/B test | lean_jtbd |
lean_jtbd |
head_to_head, prototype_test, red_team |
4 · segment, current-alternative, buying-stage |
The coverage column is the persona spread a Job needs to be trustworthy: a minimum count
plus the axes the panel must span. The full notes (one per Job) live in taxonomy.json.
Job protocols — the run discipline a Job carries¶
Some Jobs are only trustworthy when run with a specific discipline — not just the right
Framework and Formats, but rules about ordering and commitment (what must be frozen before
what). Those Jobs carry a protocol block in taxonomy.json:
"protocol": {
"name": "…",
"summary": "…",
"steps": [{"id": "…", "rule": "…", "tooling": "…"}]
}
Each step is a rule (what the discipline demands) plus the tooling that makes it
checkable (the MCP tools / fields that carry it). The protocol rides verbatim into the Job's
preset (get_job_preset), so the host sees the discipline exactly where it plans the run. As
everywhere in core: the host authors all text; a protocol constrains structure and order —
the server validates and persists, it never generates.
A/B test (ab_test)¶
Which variant wins — and for whom?
Framework: lean_jtbd — the decide-fast loop. An A/B test arrives with the variants
already in hand, so the empathy-first front half of dschool_micro (Understand & Observe →
Define POV) would be ceremony; lean_jtbd's spine — state the bet, expose the thing, validate
at a hard waist, loop back if refuted — is the A/B discipline (hypothesis before exposure,
verdict at the gate). A run typically enters at solution_explore (the variants are the
solution options) and converges at validate.
The protocol (taxonomy.json → jobs[ab_test].protocol):
variants_up_front— all variants defined and FROZEN before any persona sees them; a changed variant is a new test. Tooling:add_artifact(captured variants) or text options.hypothesis_before_exposure— exactly ONE falsifiable hypothesis + success metric stamped before exposure (record_hypothesis); the recording carries its ref (variant_meta.hypothesis_id), so the verdict answers a pre-registered bet, never a post-hoc story.randomized_order— variant presentation order is randomized per persona (the position-bias guard).brief_head_to_headhands each participant a deterministicoption_order; the order actually shown is recorded back viavariant_meta.order_shown(validated: each entry must be a permutation of the option labels).forced_preference— every persona states a forced preference plus intensity (a per-option stance −2..2 and one choice with a reason). A genuine "torn" is an abstention and is counted as one — never silently dropped.segmented_verdict— the verdict is overall AND per-segment: winner, margin, abstentions per segment (head_to_head.result.segment_splits+services.segmented_verdict(session_id)), never one blended number.
The head_to_head Format persists all of this on the recorded session
(head_to_head.variant_meta = variant ids + per-persona order shown + hypothesis ref) and
stays backward-compatible: recordings made before the metadata existed still load, and
segmented_verdict derives per-segment margins from their stored tallies.
Pricing (pricing) — willingness-to-pay ladder¶
What should I charge, and where does price sensitivity break?
The pricing Job carries a van-Westendorp-style price-ladder protocol
(taxonomy.json → jobs[pricing].protocol):
ladder_up_front— a fixed ascending ladder of ≥ 2 price points (brief_price_ladder/record_price_ladder,price_points=[{label, amount}]or plain"$29/mo"strings); a changed ladder is a new run.anchor_band_reactions— each persona reacts to EACH rung with exactly one anchor band:too_cheap(suspiciously cheap) /bargain/getting_expensive/too_expensive, plus a short grounding quote. The vocabulary is closed — an off-vocabulary band is rejected, never coerced.grounded_in_profile— reactions quote the persona's budget, authority and constraints from their loadedagent_context; the host never invents a wallet.range_and_cliffs— the server derives, deterministically: acceptance per rung (share inbargain/getting_expensive), the acceptable-price range (lowest→highest rung with majority acceptance) and the cliff point (largest acceptance drop between adjacent rungs) — overall and per segment (price_ladder.result+services.price_ladder_analysis(session_id)).tier_head_to_head— tier/packaging comparisons reuse thehead_to_headFormat with price as variant metadata (variant_meta.variants={label: {id, price}}); the ladder finds the range, the head-to-head picks the tier.
Storage follows the head-to-head pattern: a recorded ladder IS a CouncilSession carrying a
price_ladder block — the ladder, the raw structured responses (persona, price point, band,
quote — queryable for analytics) and the derived result — plus a finding of kind
price_ladder. No new table.
Ideation (ideation_hmw) — structured HMW, end-to-end¶
Given this How-Might-We, what could we build?
The ideation Job carries a three-step reframe → diverge → converge protocol
(taxonomy.json → jobs[ideation_hmw].protocol), built entirely on existing record
primitives — no parallel store:
reframe— the host turns the raw problem into 3–5 HMW questions (record_hmw_reframe), persisted as the project's open-question records (stable ids — thehmw_refevery idea attaches to). A bare HMW question is not falsifiable, so it is deliberately not forced into the hypotheses table (that would pollute the eval scorecard's calibration math); whenever the host can state a checkable bet for an HMW it passespredictionand the question is also promoted to a real hypothesis (record_hypothesis, full falsifiability validation,derived_fromthe question).diverge— the council generates ideas (brief_councilwith the HMW questions as prompts); every idea is persisted as a first-class note of kindidea(record_ideas) carrying{persona_id, hmw_ref, cluster?}— an unattributed or unanchored idea is rejected. Ideas live in the project graph (sections/edges work on them) and are queryable vialist_ideas(project, hmw_ref|persona_id|cluster).converge— a forced ranking (record_ideation_summary, orderedshortlist=[{idea_id, rationale}], rank = position, rationale required per pick) is recorded as anideationblock on a CouncilSession: problem + HMW questions + the full idea pool + the ranked shortlist. The returnedcite_asref ({kind: 'council', id}) plugs straight intorecord_decision'sbased_on— the ideation output is decision-record compatible by construction.
Adding a Job — the repeatable recipe¶
Every new Job follows the same checklist (machine-checked by
job_taxonomy.lint_taxonomy() — run sonaloop taxonomy-lint, enforced for every Job by
tests/test_job_taxonomy.py):
- Definition question — the
user_questiona buyer walks in with, phrased from their side ("Which variant wins — and for whom?"). Plus stableid(lower_snake_case, never renamed),name, and a unique buyer-facingsells_aslabel. - Framework mapping —
frameworks(each id must be a taxonomy Framework whosemethodology_keyresolves to a real spec undersonaloop/methodologies/) and adefault_frameworkfrom that list. Justify the default in this doc. - Formats — the moves the Job composes (
formats⊆ the taxonomy Format ids), lead Format first. - Coverage minimum —
coverage.min_personas(≥ 1) + thepersona_axesthe panel must span, with a one-linenoteon why that spread makes the answer trustworthy. - Protocol block — when the Job sells a run discipline (ordering/commitment rules),
encode it as
protocol.steps[{id, rule, tooling}](see "Job protocols" above) and add the Job tojob_taxonomy.PROTOCOL_REQUIRED_JOBSso the lint enforces it. - Docs section — a row in the mapping table plus (if it has a protocol) a protocol
subsection here, mentioning the job as
(`<id>`)— the lint greps for it. - Website registry note — the website repo consumes this taxonomy (
sells_asis the nav label); a new Job lands there by inheriting the taxonomy entry — never hand-duplicate ids or labels in the website registry. Keyword signals forsharpen_question(job_presets._JOB_SIGNALS) must also cover the new Job (test-enforced).
Candidate Jobs (not yet scheduled)¶
| Candidate id | One-liner |
|---|---|
onboarding_friction |
Where do new users stall in the first session, and which friction is cheapest to remove? |
naming_tests |
Which name/label do the target segments understand, remember, and not mis-read? |
messaging_resonance |
Which message lands with which segment — and which falls flat or backfires? |
churn_deep_dive |
For ONE churn cohort: reconstruct the leaving journey and find the reversible moment. |
How to consume it¶
from sonaloop import job_taxonomy
job_taxonomy.jobs() # all Jobs (id, frameworks, formats, coverage)
job_taxonomy.get_job("positioning")
job_taxonomy.framework_keys() # {"double_diamond", "lean_jtbd", ...} — real methodology keys
job_taxonomy.format_ids() # {"council", "prototype_test", "head_to_head", "red_team"}
Downstream tickets reference this artifact directly:
- Website IA — Jobs are the products sold; the
sells_asfield is the navigation label. - Job presets + sharpen-the-question — shipped in
sonaloop/job_presets.py: one preset per Job (framework + formats + coverage, derived live from this taxonomy) plus the deterministicsharpen_questionhelper that turns a fuzzy goal into a well-formed study spec. MCP surface:list_job_presets/get_job_preset/sharpen_question/start_job_study. - Methodology surface — Frameworks here are the real
keys already insonaloop/methodologies/.
Naming + stable ids¶
- Job ids are lower_snake_case and stable (
positioning,pricing,jtbd_demand,ideation_hmw,continuous_discovery,churn_reasons,ab_test). Never renumber or rename — add new Jobs instead. - Framework ids equal the methodology
key(double_diamond, …). - Format ids are lower_snake_case (
council,prototype_test,head_to_head,red_team).
Any repo or ticket tags itself with layer + the relevant id (e.g. job:positioning,
format:red_team) so cross-repo work stays aligned.