SPEC_ANSWERHUB_DISPLAY.md

AnswerHub display logic — back to the old application's hierarchy

What this covers: how an answer is presented — in a listing, in search, and on a question page — plus the two data faults that presentation uncovered. Drafted from a /grill-me session. Lives in the apps-siao-ai repo, on top of the shipped frontend restyle (SPEC_ANSWERHUB_FRONTEND.md) and the SEO/corpus work (SPEC_ANSWERHUB_SEO.md, tickets 01–10, done).

For what is built or deployed, see ClaudeDocs/HANDOFF.md — status deliberately does not live here.

"The old application" throughout means ~/Lwopan/learning-platform, the pre-migration app whose page structure SPEC_ANSWERHUB_FRONTEND.md already committed AnswerHub to preserving.

Problem Statement

AnswerHub is an answer bank whose answers are invisible until you click.

A visitor searches a question, gets a list of rows carrying a title, a description, an author and three engagement counters, and has to open a page to find out what the answer is. The old application did the opposite: the answer was the headline, rendered large and coloured, and the question shrank to a single supporting line beneath it. That hierarchy is the product. The current listing is the shape of a forum post, and it was arrived at by original design at a point where SPEC_ANSWERHUB_FRONTEND.md had already specified "the same page structure the old app had — a visual system change, not an information-architecture redesign". The owner has asked for the old application's hierarchy repeatedly; the surfaces still do not have it.

The same habit produced a second, worse fault. The question page presents three verification states, one of which — DISPUTED, with a dedicated two-readings-side-by-side layout and a written rationale for why it asserts neither — has never rendered a single row, because the stored state is DISPUTED on zero of 12,780 submissions. Layout was built for a state the data does not contain.

Re-deriving each submission's state from the answers actually stored shows the column is wrong on 4,522 rows (35%):

Stored → derived Rows What it means
UNVERIFIEDDISPUTED 2,191 Real disagreements filed as "nobody checked"
UNVERIFIEDVERIFIED 1,549 Agreement never credited
VERIFIEDUNVERIFIED 708 Claims two sources agree with no model answer at all
VERIFIEDDISPUTED 74 Stored state and the agreement function contradict each other

The 708 are the serious ones: pages currently telling a reader two independent sources agreed, when only one answer exists.

A third fault blocks the hierarchy itself. The old application's card takes its headline answer by parsing the submission's files JSON. files is [] on all 12,780 rows — copying that code would render an empty headline everywhere. And the question text a Q line needs is not stored: the importer truncates the question to 80 characters into title (3,473 rows, 27%, are truncated) and keeps nothing else. Where a reading passage is shared by several sub-questions, their truncations are byte-identical — one passage accounts for 37 rows whose titles cannot be told apart. This reads as duplicate content and is in fact 37 distinct questions. The full text still exists in the corpus source files on disk; only the import discarded it.

Solution

Rebuild AnswerHub's display logic around the old application's information hierarchy, keeping the current design tokens. In every listing the answer is the headline and the question is a supporting line; on a question page the highest-confidence answer sits in an isolated "猜你想找這個" block above everything else, and the frame carries a light confidence tint. Search results order answered questions ahead of unanswered ones. The homepage searches in place — results expand below the hero rather than navigating away — and its latest-answers strip becomes a right-to-left marquee carrying one question line and its source per item.

Route every one of those surfaces through one shared display seam, a pure function that turns a submission into the model both the card and the question page render. Today the homepage, search and question page each decide independently what to show, which is why the same answer looks like a different kind of thing depending on where it is found.

Derive confidence at read time from the stored source answer and model answer, using the existing agreement module, rather than trusting the verification column. This is what lets the interface be correct before the data is: the 708 false cross-verification claims stop being made the moment the seam ships, without waiting on a migration.

In parallel, fix the data: rewrite the verification column from the same derivation, and recover the full question text from the corpus source files — which are still on disk, so no re-crawl is needed.

User Stories

Finding an answer

  1. As a visitor, I want a search result to show me the answer itself, so that I do not have to open a page to find out what I searched for.
  2. As a visitor, I want the answer rendered as the largest, most prominent thing on the row, so that I can scan a list of results without reading any of them closely.
  3. As a visitor, I want the question shown as a single supporting line beneath the answer, so that I can confirm the row is the question I meant.
  4. As a visitor, I want questions whose answer is known to appear ahead of questions with no answer, so that the top of my results is usable.
  5. As a visitor, I want a question with no answer to still appear in results, marked as unanswered, so that I learn the question exists here and is simply not solved yet.
  6. As a visitor, I want an unanswered row to keep the same shape as every other row, so that a list does not fragment into several card designs.
  7. As a visitor searching from the homepage, I want results to appear below the hero without navigating away, so that I can refine my query without losing the page.
  8. As a visitor, I want the homepage to show a first page of results with a way to see the rest, so that an in-place expansion does not become an unbounded scroll.
  9. As a visitor, I want two sub-questions of the same reading passage to be distinguishable in a result list. Withdrawn. Written on the belief that 113 groups of look-alike rows were passages with distinct sub-questions. At least 40 of them, 83 rows, are the same question imported more than once with different trailing furniture, and some of those disagree about the answer. A listing that told them apart would be presenting copies as distinct content. Recorded with the measurements in .scratch/answerhub-display/issues/12, which is corpus work, not display work.
  10. As a visitor, I want a result row to carry its language, author, likes, comments and views, so that I can judge a row on more than its answer.

Judging an answer

  1. As a visitor, I want a small badge on each result telling me how far the answer has been checked, so that I can weigh a result before opening it.
  2. As a visitor, I want the badge to reflect the answers actually stored for that question, so that a page never claims a confidence its data does not support.
  3. As a visitor, I want a question page's frame to carry a light tint matching its confidence, so that the page's trustworthiness is legible before I read a word.
  4. As a visitor, I want a question page to lead with a single isolated "猜你想找這個" block holding the highest-confidence answer, so that the page answers me immediately.
  5. As a visitor, I want a question whose two sources disagree to show me its answer and say plainly that they disagree, so that I get what I came for and know exactly how much to lean on it.
  6. As a visitor, I want a question with no second opinion to say that plainly, so that "unchecked" is never dressed up as "verified".
  7. As a visitor, I want the source's answer and the model's independent answer shown for comparison when both exist, so that I can judge a disagreement myself.
  8. As a visitor, I want the most similar other questions listed below the answer, so that I have somewhere to go when this question was not quite mine.
  9. As an owner, I want no page to claim cross-verification without a second answer behind it, so that the product's core claim stays honest.

The homepage

  1. As a visitor, I want the homepage to lead with the product name and a search box, so that its purpose is obvious in one screen.
  2. As a visitor, I want a marquee of recent questions scrolling right-to-left, so that the homepage shows the bank is alive.
  3. As a visitor, I want each marquee item to carry one question line and its source, so that the strip is readable while it moves.
  4. As a visitor, I want the marquee to pause when I point at it and its items to be clickable, so that it is not decoration I cannot use.
  5. As a visitor using a keyboard or a screen reader, I want the recent-questions content reachable without interacting with the marquee, so that motion is never the only path to it.
  6. As a search engine, I want the homepage's recent questions present as static, crawlable links, so that a motion component does not cost the site its index coverage.

Authors and moderators

  1. As an author, I want my own drafts and pending submissions to show their review state in listings, so that I can tell what has been published.
  2. As a moderator, I want hidden and archived submissions marked in the listings I see, so that moderation state is visible where I work.
  3. As a visitor, I want review-state badges absent from public listings, so that a badge reading "published" on every row is not sold to me as information.

Data correctness

  1. As an owner, I want the stored verification state rewritten to match what the agreement function derives, so that exports, structured data and the admin surface agree with what pages display.
  2. As an owner, I want the 708 rows claiming verification without a model answer corrected first, so that the most damaging class of error is the first to go.
  3. As an owner, I want the full question text recovered from the corpus source files, so that a Q line can carry the real question rather than an 80-character truncation.
  4. As an owner, I want questions sharing a reading passage to become individually identifiable once their text is recovered, so that 113 groups of look-alike rows resolve without hand-editing.
  5. As an owner, I want the recovery to run from files already on disk, so that no source site has to be crawled again.

Implementation Decisions

The display seam

A new pure module turns one submission into the model that every surface renders. The card and the question page become renderers with no display logic of their own. It is unit-tested and imports nothing from React.

The shape, which encodes the decisions more precisely than prose (derived during the grilling session from the two corpora's actual contents):

type Headline =
  | { kind: "answer"; text: string }
  | { kind: "unanswered" };     // no answer in any source

interface AnswerDisplay {
  headline: Headline;
  questionLine: string;         // full question when recovered, else the title
  confidence: "VERIFIED" | "DISPUTED" | "UNVERIFIED";
  options: { text: string; correct: boolean }[] | null;
}

/** A listing renders no choices, so it does not carry them. */
type AnswerRowDisplay = Omit<AnswerDisplay, "options">;

Showing an answer and asserting one are different claims

Visually a disputed question leads with its answer. In the structured data it emits both readings as suggestedAnswer and no acceptedAnswer, exactly as before. Showing a reader an answer wrapped in a warning they can see is a weaker claim than telling a search engine this is the answer, and only the second is a promise the corpus cannot keep. The two were conflated in the draft, which is why "asserts neither" read as one decision instead of two.

Confidence is a tint on the frame, never a colour on the answer

The first build coloured the answer text per state — green, amber, grey — which put three meanings on the one element the eye is supposed to land on and stopped the answer's colour meaning "this is the answer". The old application had it the other way round: one colour for the answer always, the card behind it carrying the tier. Only VERIFIED and DISPUTED tint; UNVERIFIED is seven rows in ten and a tint that common is a background, not a signal.

Confidence is derived, not read

The existing agreement module's verdict function is called at read time with the stored source answer and model answer. The verification column is not consulted by any display surface. Consequences accepted:

The search seam

Search currently builds the same where/orderBy twice — once in the search API route, once in the search page. This is prefactored into one pure query builder before ordering changes, so answered-before-unanswered is decided once.

Ordering is: answered ahead of unanswered, then the existing recency order. Confidence does not enter the ordering — an unverified answer is still an answer, and burying DISPUTED would hide exactly the rows worth reading.

Do not copy the old application's answer parsing

The old application's card derives its headline by parsing the submission's files JSON for question, options and correctAnswer. That field is [] on every row in this database. The hierarchy is copied; the data access is not. The headline comes from the published answer column, and choices come from the dedicated options column (populated on 7,296 rows).

Card contents

Answer headline, then the question line, then a metadata row carrying language, author, likes, comments and views. Review-state badges render only for an author viewing their own work and on the moderation surface.

Listings are also one column, not a grid. This is not styling: the card was rewritten first and dropped back into a three-up grid, where a text-xl answer sits in a 300px column, wraps onto two lines and reads smaller than the question under it. The hierarchy was in the markup and absent from the page. The old application used a single narrow column for exactly this reason. One shared list component, so three listings cannot drift apart again — which is how they got there.

The description is shown only when it says something

Never, for an imported question. Measured across all 12,780 rows: 6,324 hold the explanation verbatim, 5,703 hold the title the page's own h1 is already showing, and 753 hold the published answer. Not one carries something of its own — the importer writes explanation || answer || title into it. Moving the answer to the top of the question page put two identical paragraphs in plain sight. It is rendered when it differs from all three, which is what a manual submission does.

Question page

An isolated highest-confidence block above all other content; the page frame carries a light confidence tint. Below it: the working, then the source answer and model answer side by side where both exist, then the most similar questions. There is no paginated "other answers" list — outside one cross-source pair, no question in this corpus has more than the two candidate answers already shown.

Homepage

The hero and its search box stay. Search submits in place and expands results below the hero, first page plus a link to the full search page; the search page keeps its own URL for indexing and sharing. The latest answers strip becomes a right-to-left marquee whose items carry one question line and a source site, pause on hover, and are links. There is no second, separate static list, though the draft called for one: rendering the same items twice means a screen reader reading the strip twice and the keyboard tabbing through it twice. Instead the first track holds the real links — ordinary markup a crawler indexes, a screen reader reads and the keyboard tabs through — and the animation is a transform applied to it. A second track, aria-hidden and untabbable, exists only so the loop has no visible seam. Motion is a presentation of the content, never the only path to it.

It stops on hover and on focus-within, because a link that keeps moving under a keyboard user is a link they cannot press. The repo's global reduced-motion rule already switches the animation off; the container scrolls so items past the edge stay reachable once it has.

Data track, running in parallel

  1. Rewrite verification from the same derivation the interface uses, the 708 no-model-answer rows first.
  2. Add a column for the full question text, backfill it from the corpus source files already on disk, and only then switch the question line to it. The 80-character title stays as-is for slugs and page titles, which are already bounded by an earlier ticket and must not churn.

Those three steps are three changes, not one. Landing the column and the queries that read it together left the application unable to start against any database that had not been pushed — the homepage answered 500 with The column main.Submission.question does not exist. Expand, migrate, then switch the reads: the column and the backfill script can land safely because nothing reads them yet.

Scope of surfaces

Search, question detail, homepage, taxonomy index pages, upload, profile and admin. Account pages are not touched — identity moved to accounts.siao.ai.

Existing structural guard

A structural test requires any file reading a content model to consult the visibility seam. New modules and surfaces must satisfy it rather than be added to its exemption list.

Testing Decisions

A good test here asserts what a reader sees and never how it was computed. Rendering a submission and asserting "the answer is the headline" is a test; asserting that a particular helper was called is not.

Out of Scope

Further Notes

How the visual work was checked, after it was not. The first build of the card was verified by grepping the served HTML for the strings it was supposed to contain. They were all there, and the page was still wrong: the rewritten card had been dropped back into a three-up grid, where the answer wrapped and read smaller than the question beneath it. Markup presence is not hierarchy. Everything visual here was afterwards checked by opening it in a browser, which is also how the duplicated description paragraph and the mis-centred column were found — none of them would have failed a test that was looking for text.

Two errors can be stacked. The homepage's 500 after the column landed was reported as the missing column; it was actually a dev server holding a Prisma client generated before the schema changed. The missing column was the next error. The cause was named from grepping a log for a word rather than reading the error, which is the same mistake as the paragraph above in a different medium.

The two faults in this spec share one cause with the display problem they were found behind: layout and rationale were written for states the data did not hold. The DISPUTED panel carried a paragraph explaining why it asserts neither reading, and rendered zero rows. The card carried a comment justifying the omission of the old application's verification badge, and omitted the answer instead. Deriving confidence at read time is the structural fix — a state that cannot be computed from stored columns cannot be given a layout.

Corpus facts measured during the grilling session, recorded so they are not re-measured: 12,780 submissions across two source sites (happyread 7,298, netholiday 5,482); 6,539 (51%) with no published answer; 7,296 with options; 3,862 with both a source answer and a model answer; exactly one question appearing in both sources; 113 title collision groups, all artefacts of truncation rather than duplicates.