← Corpus / augment-it / prompt
Response Reviewer — Structured-Output Extension (Packs-and-Bundles Foundation)
Extend the shipped response-store schema and the shipped Response Reviewer remote to support pack-shaped responses — structured candidates with confidence-band pills, a five-value outcome enum, and a sibling-payload + archival-markdown shape — without breaking the free-form text responses that today's prompt-runs produce. Zero packs ship in this work. Zero bundles ship. The deliverable is the surface that every pack will land on, exercised by a small fixture of mock pack responses so the renderer can be felt in all three theme modes before any real pack exists.
- Path
- prompts/Response-Reviewer-Structured-Output-Extension.md
- Authors
- Michael Staton
- Augmented with
- Claude Code on Claude Opus 4.7
- Tags
- Prompt · Augment-It · Packs-and-Bundles · Response-Reviewer · Response-Store · Structured-Output · Confidence-Pill · Theme-System
Response Reviewer — Structured-Output Extension
The work
The packs-and-bundles pattern from [[Packs-and-Bundles-Pattern]]
introduces a richer response shape than the response-store currently
stores or the Response Reviewer currently renders. Every pack
(starting with linkedin-pack in the next session) produces a typed
Candidate rather than a markdown blob. The renderer has to be ready
before the first pack arrives. This session is that readiness.
The blueprint is the spec; this prompt is the scoping directive.
Why this is its own session
Two reasons:
- No pack exists yet. Building the surface alongside the first pack would conflate surface-correctness (does the candidate card render right?) with pack-correctness (does LinkedIn’s scraper work?). Better to prove the surface against mock fixtures first.
- Backward compatibility is load-bearing. Every existing response
in
services/response-store/data/responses.jsonmust continue to render exactly as today. The migration is invisible. Doing it as a standalone session means the diff is small enough to verify visually.
What’s in scope
1. Schema extension in services/response-store/src/store.ts
The shipped ResponseRecord (response_id, run_id, prompt_id, row_id,
record_set_id, output_column, model, request_body, response_text,
edited_text, flag, accepted, created_at, reviewed_at, edited_at) gains
six new fields:
type Outcome = 'found' | 'not_found' | 'error' | 'skipped' | 'pending';
type Candidate = {
url: string;
display_name: string;
confidence: number; // 0-100
snippet?: string;
source_metadata?: Record<string, unknown>;
};
// Added to ResponseRecord:
outcome: Outcome;
structured: Candidate | null;
archival_markdown: string | null;
pack_id: string | null;
bundle_id: string | null;
pass: 1 | 2 | null;
The existing backfill discipline in load() (already used for
edited_text / edited_at) extends to coerce older records:
outcome→'found'ifresponse_textis non-empty, else'pending'structured,archival_markdown,pack_id,bundle_id,pass→null
No migration script. The load-time backfill is the migration.
2. Theme tokens in packages/theme/theme.css
Three new named tokens. Each defined in all three mode blocks (light, dark, vibrant) per [[Impose-Theme-Modes-System]]:
--color-confidence-low (red family)
--color-confidence-med (amber family)
--color-confidence-high (green family)
Pick reasonable initial hex values per mode; ergonomics are more important than perfection — iterate on the named-token tier once the pill is rendering somewhere visible.
3. Confidence-pill component in packages/shared-ui/
A small Svelte 5 component. Props:
{
confidence: number; // 0-100, required
tooltip?: string; // optional hover reasoning
}
Behavior:
- Render the numeric value (e.g.,
87) inside a small pill - Color band by range: 0-39 =
--color-confidence-low, 40-69 =--color-confidence-med, 70-100 =--color-confidence-high - Colors via the theme tokens — never hardcoded hex
- On hover, show the
tooltipif provided
Pick whatever pill shape feels native to the existing
packages/shared-ui/ conventions. If packages/shared-ui/ is sparse
today, this is its first real component — establish the shape (Svelte
5, exported via package entry, consumable from any remote).
4. Response Reviewer renderer extension in apps/response-reviewer/
The shipped renderer shows response_text as free-form prose with
edit/triage ergonomics. The extension branches on outcome:
outcome | Render |
|---|---|
found | Candidate card above the prose (see below); triage controls unchanged |
not_found | Informational thin row (“source ran, zero candidates”); no triage controls |
error | Thin row with error text + a disabled retry button (tooltip: “retry coming later”) |
skipped | Thin row showing the carried-forward URL, pre-marked good |
pending | Spinner row, no controls |
Plus the legacy case: if structured === null AND outcome === 'found'
(i.e., a pre-pack response from the existing prompt-runner), render
exactly as today — same prose box, same triage controls. No visual
change for non-pack responses. This is the backward-compat path.
The candidate card (when structured !== null)
Renders above the existing prose. Contents:
- Confidence pill (the new component) immediately before the URL
- URL — clickable, opens in new tab
- Display name next to the URL
- Snippet — collapsible by default, single click to expand
- Source badge — for v1, just displays the raw
pack_idas a small label. The per-pack render-config from the blueprint comes online with the first real pack; until then, the raw pack_id is the v1 affordance.
The prose (response_text) renders below the card with all existing
triage controls intact. The card adds context; it doesn’t replace the
triage surface.
5. Mock fixture for end-to-end smoke
A small set of pack-shaped mock responses written into a fixture file
(somewhere appropriate — services/response-store/data/fixtures/ if
the response-store has a fixtures convention, otherwise a sibling
JSON the response-reviewer can load on demand). Five mock records,
one per outcome:
found(high confidence, ~87) — green pill, LinkedIn-shapedfound(med confidence, ~55) — amber pillfound(low confidence, ~22) — red pillnot_found— informational thin rowerror— thin row with disabled retryskipped— thin row with pre-populated URLpending— spinner row
(Seven mock records, not five — three for the confidence-band
coverage on found, plus one each for the other outcomes.)
Mock pack_id values: linkedin-pack-mock, candid-pack-mock —
clearly fake so nobody mistakes them for real packs.
What’s explicitly out of scope
linkedin-packor any other pack. The first pack lands in the next session.- The bundle runtime — roster, orchestration, carry-forward. Two sessions after this one.
- The
profiles.dedup.scancapability. Lands with the first bundle. - Two-pass orchestration + entity-typed bundles.
- The retry button’s actual retry implementation. Wired in a later session.
- Per-pack render-config plumbing — for now the source badge displays
the raw
pack_id; the declarative render-config comes online with the first real pack. - The
source_metadataper-source display (format hints, etc.) — lands per-pack in the next session. - The promote-to-canonical write-back of
profiles.<source>clusters — extension of the existing promote handler, noted but lives in a later session. - New external dependencies. No charting libs, no design-system imports — the pill is a small Svelte component.
Constraints
- Svelte 5 with
$staterunes; no React, no Tailwind - Backward compatible — every existing response renders identically to today after the migration. No visual diff for non-pack responses.
- Theme tokens only for color — never hardcoded hex in the pill or card components
- Three-mode contract — every new token defined in all three mode blocks; the pill verified in all three modes during smoke
- No data-migration script — the load-time backfill is the migration
- Type-driven rendering —
outcomedrives the render branch; we do not key offstructured !== nullfirst. Outcome is the discriminator. Per the [[feedback_augment_it_dynamic_schema]] memory and the recent “all data continues” changelog entry, type-based dispatch beats presence-of-field dispatch. - No hardcoded field names for the candidate card display — the
card reads
structured.url,structured.display_name,structured.confidence, etc., but anything fromsource_metadatastays generically rendered until per-pack render-configs exist.
What “done” looks like
Manual smoke walkthrough:
- Open Response Reviewer against the current
services/response-store/data/responses.json(your actual data). Every response renders exactly as it did before this session. No visual diff. The migration is invisible. - Load the mock fixture. Response Reviewer now shows the seven mock responses alongside (or in place of) the real ones.
- The three
foundmocks render with candidate cards:- Green pill for the 87-confidence one
- Amber pill for the 55-confidence one
- Red pill for the 22-confidence one All three with URL, display name, collapsible snippet, source badge, prose below, triage controls intact.
- The
not_foundmock shows the informational thin row. No triage controls visible. - The
errormock shows the thin row with error text + a disabled retry button. Hovering the button shows the tooltip. - The
skippedmock shows the thin row with the pre-populated URL markedgood. - The
pendingmock shows the spinner row. - Toggle theme modes (light / dark / vibrant). The pill re-tints correctly in all three — colors visibly different per mode but semantically consistent (low always feels “red”, etc.).
- Refresh the page. Schema persists; all responses still render correctly.
If all nine steps work, the surface is real and the next session (linkedin-pack) has a foundation to land on.
Suggested implementation order
- Schema extension in
store.ts— types first, then the backfill inload(). Run the server, confirm existing responses round-trip through the load + save cycle with the new nullable fields. - Theme tokens in
theme.css— three tokens × three modes = nine color values. Pick initial hex; iterate later. - Confidence-pill component in
packages/shared-ui/. Verify it renders standalone (a tiny test page or storybook-equivalent if one exists; otherwise mount it in a corner of Response Reviewer temporarily). - Renderer branch on
outcomeinapps/response-reviewer/. Start with thefound+ candidate-card path, then add the non-foundoutcome renders. - Mock fixture. Wire a way to view it (a query param? a debug button?). The mechanism is throwaway — it’s for this session’s smoke, not a permanent feature.
- Walk through the nine smoke steps. Iterate on whatever feels off.
Open calls the implementer makes
These are intentionally not pre-decided — the implementer picks based on what fits the existing codebase:
- Exact path for the confidence-pill component within
packages/shared-ui/ - The exact shape of the source badge label (just the pack_id, or pack_id rendered prettier — fine either way for v1)
- How the mock fixture is loaded (query param, debug button, environment flag — pick what’s least intrusive)
- Whether the retry button’s tooltip shape uses an existing tooltip
primitive or a fresh
title=— match the codebase
If any of these turn out to deserve a permanent decision, lift the answer into the blueprint when this session lands.
After this session lands
- Update the changelog entry for the ship
- If anything in the blueprint turned out wrong during implementation, edit the blueprint first, then re-implement (per [[Spec-Kit-and-Context-V-Coexistence]] — context-v is the source of truth)
- Next session opens with the linkedin-pack prompt — first concrete pack landing on this surface
Related
- [[Packs-and-Bundles-Pattern]] — the blueprint this session operationalizes (the foundation layer)
- [[Entity-Profile-Augmentation-Workflow]] — the exploration with the resolved open questions
- [[Response-Reviewer-and-Response-Store]] — the shipped spec being extended (status: Shipped)
- [[Impose-Theme-Modes-System]] — the three-mode contract
- [[Run-Speckit-Specify-Response-Reviewer-Structured-Output]] — the deferred Spec-Kit version of this directive, kept for future reference