Appearance
Media illustrations & coverage (reference)
Issue #1244. Part of the media-curation workbench (#1238).
An illustration wires the best photography to the guided flow as a teaching aid: it says "this media explains this factor/attribute value" (role = illustrates). This is distinct from:
- a gallery attachment (
media_links) — decoration on an entity, and - a classification tag (
media_tags) — what the image depicts.
The payoff (the "goldmine"): illustrate factors and attributes so the guided flow, compare, and factor explainers can teach with real photography, not just decorate.
The association
Table media_illustrations (migration 0063):
| Column | Notes |
|---|---|
target_type | factor_option or attribute_value — what is illustrated. |
target_ref | a factor_options.id, or ${attributeId}:${value} for an attribute value. |
media_asset_id XOR shot_group_id | the source: a single image, or a shot-group scrubber (#1240 — e.g. a folding value shown open→closed). A CHECK enforces exactly one. |
role | the semantic role; v1 is illustrates (extensible). |
is_canonical | the canonical illustration for the target value — at most one per (target_type, target_ref), app-enforced. |
Domain: $lib/server/admin/illustrations.ts — addIllustration (validates the target + source exist, refuses duplicates), removeIllustration, setCanonicalIllustration (clears any prior canonical), illustrationsForTarget, and canonicalIllustration.
Canonical illustration (AC-02)
Per factor/attribute value, an admin sets one canonical illustration — a single image or a shot-group scrubber. setCanonicalIllustration(db, id) makes a row canonical and clears any prior one, so the target always resolves to a single teaching image/sequence.
Coverage report (AC-03)
illustrationCoverage(db) (illustration-coverage.ts) enumerates the whole target universe — every factor option and every attribute enum value — and reports, per value, whether it has an illustration (and a canonical one). Values with no illustration are gaps, listed first: the curation worklist, and the priority signal for description/illustration runs ("illustrate the uncovered values first"). Bounded — the catalog value tables are small and the illustration set is grouped once.
The admin surface is /admin/illustrations (Content & media): a coverage summary (factors X/Y, attributes X/Y) and gap-first lists with the canonical thumbnail per covered value. Since #1482 it is a report only — it carries no assignment controls; each row deep-links (illustrationSlotHref, $lib/admin/illustration-links.ts) to the owning entity editor's illustration slot.
Single curation path (#1482 → #1520)
Curation lives inside the entity's one Media widget (MediaManager) — the factor editor (per option, Options tab) and the attribute editor (per enum value, Values tab) mount a single always-visible widget that owns BOTH the gallery (educates the buyer during selection — guided flow, compare, heroes) and the results-page role (shows the recommended system in the buyer's setting — the recommendation page's contextual slots and the #1282 ranking floor). The former separate IllustrationSlot panel is retired (#1520).
In the widget: the gallery image carrying the role is badged results page; any other attached image offers one-click Use on results page; the role section's Manage view lists the value's illustrations (show-this-one / remove), the depicts→illustrates candidates (#1279), and the widget's own library picker in illustrate mode. Deep links (?illustrate=<targetRef>, via illustrationSlotHref) still land expanded — the coverage report's rows keep working.
Mutations ride the one media manage endpoint (/admin/media/manage, actions illustrate / illustration-canonical / illustration-clear, plus the illustration-slot read) — edit_data guarded, audited, wired through the #1244 seams above; no schema change. Read halves live in $lib/server/admin/illustration-slot.ts (illustrationSummaries, illustrationSlotProps).
Divergence audit (#1482 AC-04). illustrationDivergence(db) (illustration-coverage.ts) reports how many canonicals equal the same value's primary gallery image (flagged link, else first by position) — rendered as a summary line on /admin/illustrations. If canonicals almost always match the primary, a follow-up can derive the canonical from the gallery primary by default. Audit at 2026-07-15: the local catalog carried 0 canonical illustrations, so the report starts at zero — the line becomes meaningful as curation lands.
Public query (AC-04)
canonicalIllustration(db, targetType, targetRef) resolves a value's canonical illustration to a renderable:
ts
{ kind: 'image' | 'scrubber', coverAssetId: string | null, shotGroupId: string | null }For a scrubber, coverAssetId is the shot group's cover frame (the default image); shotGroupId lets a surface render the full open→closed sequence. This is the query the public rendering surfaces consume — the guided flow, compare, and factor explainers (#410 / #413 / #359). Public rendering of the scrubber itself rides those issues; this req provides the association + the query.
Contextual illustration ranking (#1282)
The recommendation page's thesis is "here's the {System} we recommend in the setting you described" — so a slot shows the media that best matches that value × the recommended system × the buyer's scene, not a static canonical shot. The match/rank engine + query lives in src/lib/server/illustration-ranking.ts (the public display rides #410); the scorer is pure and unit-tested.
The query. rankIllustrationsForRecommendation(db, { systemId, optionIds }, weights?) returns { hero, answers } — one RankedIllustration for the system hero and one per buyer answer. A buyer answer id is a factor_option tag ref (#1279), so candidacy for a slot = assets carrying that value tag (curated media_tags ∪ accepted media_visual_tags), and overlap is scored from the candidate's curated tags against the scene (the recommended system + the other answers). Only publishable (cleared-rights) assets surface (#1115). It is exposed on the recommendation API as illustrationRanking for #410 to render.
attribute_valueoverlap has no answer bridge yet (factor_option_attributescarries no value), so answer slots arefactor_optiontoday; the engine already handles theattribute_valuetag type, so it flows through once a bridge exists.
Fallback ladder (never a broken/empty slot; the chosen tier is on the result, so why this image is inspectable):
| Tier | Meaning |
|---|---|
exact | value + same system + shared context |
system | value + same system |
context | value + shared context (any system) |
canonical | the value's canonicalIllustration (#1244) |
education | no image — the value's education copy stands alone |
A bare value-only tag never out-ranks the curated canonical.
System-vs-context weighting is an explicit, tunable knob (IllustrationWeights / DEFAULT_ILLUSTRATION_WEIGHTS = { system: 3, context: 2, provenanceHuman: 0.5 }), not a hardcoded guess: by default a same-system match outranks a single loose context match, but a generic system-only shot loses to a candidate that nails ≥2 context signals (the coastal-buyer decision). Tie-breaks, in order: human-curated > AI → gallery order → recency → id.
Related
- Shot-group model — the scrubber primitive.
- Media AI runs — coverage gaps drive run priority.
- Media tagging — the classification tags illustrations are distinct from.
- Recommendation API — carries
illustrationRanking(#1282).