Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/answer-page-redesign-handover.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,48 @@ case, and it fails closed: no sections, no marks.

That is why the rail and drawer ship first, and the marks second.

### 1a. Corrected again, 2026-08-25, when the marks were built: sections are the wrong source

Everything above is accurate about `answerSections` and still wrong about the marks, and
the reason only shows up when you try to render one.

`answerSections` is a **second layer**. The generation contract calls it "Second-layer
structured support… distinct source-backed modules that improve scanability"
(`src/lib/rag/rag.ts:340`), and the composition instruction tells the model to write the
prose into `answer` and _then_ use `answerSections` for separate structured support
(`:4313`). A section's body is therefore not text the clinician reads in the prose, so
there is no sentence in the answer for a section's mark to attach to. A mark per section
is only buildable by rendering the sections themselves — which is a different product
decision (more content on the answer surface), not the design that was approved.

**The field that does anchor to the prose is `RagAnswer.supportedClaims`**
(`src/lib/types.ts:519`). `rag-claim-support.ts:1049` builds its top-level entries as
`splitClaims(answer.answer)` — literally the sentences of the displayed prose — and each
carries `supportingChunkIds` and a `supportStatus` of `direct | partial | unsupported`.
`answer-render-policy.ts` already reads it client-side, so it is on the client today with
no payload change.

So the marks are built from `supportedClaims`, and §3's requirement is met by a stricter
route than it asked for: attribution is per _sentence_ rather than per section, and it is
the pipeline's own recorded attribution rather than one the render layer derived. The
resolution rules live in `src/lib/answer-claim-marks.ts` and are exact — a sentence either
**is** a recorded claim (or exactly a run of consecutive all-`direct` ones) or it carries
no mark. Every ambiguity resolves to no mark:

- the display sanitizer rewrote the sentence → no mark;
- two recorded claims disagree about the same sentence → no mark;
- the claim cites a chunk the rail does not list → that citation is dropped, never
renumbered onto a neighbouring card;
- the word budget cut the sentence short → no mark;
- `supportStatus: "unsupported"` → no mark, and no worded tag either (see §12.2 below,
superseded by the owner in design review on 2026-08-25).

**Expect partial coverage, and do not tune it up.** A sentence the usefulness pass
rewrote, or one holding several claims at different support levels, renders unmarked with
the rail underneath still carrying every source. That is the designed degrade. Raising
coverage means changing what the generation contract emits, which is PR 3 — protected RAG
surface, owner flag, live eval canary.

---

## 2. What replaces what
Expand Down
7 changes: 6 additions & 1 deletion docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,11 @@
"preview": ".design-sync/previews/SafeBoldText.tsx",
"previewValid": true
},
"testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/display-text.test.ts"],
"testFiles": [
"tests/answer-content.test.ts",
"tests/design-sync-visual-exports.test.ts",
"tests/display-text.test.ts"
],
"baseline": {
"targetLayer": "v2",
"liveLayer": "v2",
Expand Down Expand Up @@ -1702,6 +1706,7 @@
},
"testFiles": [
"tests/accessible-table.dom.test.tsx",
"tests/answer-source-rail.dom.test.tsx",
"tests/caring-contacts-overlay-host.dom.test.tsx",
"tests/caring-contacts-overlay-trigger.dom.test.tsx",
"tests/design-sync-visual-exports.test.ts",
Expand Down
73 changes: 73 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,79 @@ summary::-webkit-details-marker {
/* Composer primitives are intentionally unlayered because inputs/buttons are
governed by the global element resets. Their exported TypeScript recipes are
class handles only, making this block the single owner of chrome and states. */
/* The numbered source mark in the answer prose, and the `+N` after a capped
cluster.

These values are ONE interdependent set, which is why they live in a class
rather than as scattered utilities. At the answer's 16px/1.66 prose they draw
a 12x12px box lifted 5px: it spans 5->17px above the baseline, clearing the
line above by ~2px without growing the line box, and the margins leave ~2.7px
between two marks so "2" and "3" in a cluster cannot touch. Re-measure in a
browser before changing any one of them.

The size is `em`, not a named step, because the mark has to scale with the
prose it annotates — the scale's floor (`--text-3xs`, 10px) is absolute and
would also overrun the line at this rise. Keeping it here rather than as a
`text-[…]` utility is the sanctioned form: `check:type-scale` guards the
utility vocabulary, and unlayered component CSS is how this repo expresses a
geometry that the scale cannot.

Never set line-height to 0. An inline-block with zero line-height has a
zero-height padding box, which collapsed the mark to a sliver the first time. */
/* The in-prose source mark's own metrics. Component metrics, not scale steps:
they are em-based and mutually dependent — the box has to clear the prose
without growing the line box — so they sit here rather than in @theme, where
they would generate a `leading-mark` utility with one consumer and a stepped
scale that reads as though the mark were a typographic role.

Declared on :root because the mark is drawn from three places that are not
nested: `.answer-source-mark` and `.answer-source-mark-overflow` below, and
the cluster spacing in answer-source-mark.tsx.
--answer-mark-leading the mark box takes its own line and nothing more
--answer-mark-gap-lead first mark of a cluster, closer to the word
--answer-mark-gap every later mark, and the "+N" overflow count
--answer-mark-gap-star the partial-support glyph against its digit */
:root {
--answer-mark-leading: 1;
--answer-mark-gap-lead: 0.28em;
--answer-mark-gap: 0.34em;
--answer-mark-gap-star: 0.03em;
}

.answer-source-mark {
position: relative;
top: -0.625em;
display: inline-block;
min-width: 1.5em;
padding: 0.15em 0.2em;
border-radius: var(--radius-xs);
border-width: 1px;
border-style: solid;
font-size: 0.5em;
line-height: var(--answer-mark-leading);
text-align: center;
vertical-align: baseline;
}

/* The partial-support glyph. One glyph reads at this size; a dotted underline
and a 1px bottom border were both tried under it and neither draws. */
.answer-source-mark-star {
margin-left: var(--answer-mark-gap-star);
font-size: 0.85em;
line-height: var(--answer-mark-leading);
}

/* A statement, not a control: the sources it counts all sit on the rail below
at full tap size. */
.answer-source-mark-overflow {
position: relative;
top: -0.625em;
margin-left: var(--answer-mark-gap);
font-size: 0.5em;
line-height: var(--answer-mark-leading);
vertical-align: baseline;
}

.chat-composer-shell-base {
display: flex;
align-items: center;
Expand Down
1 change: 1 addition & 0 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3801,6 +3801,7 @@ function ClinicalDashboardContent({
followUpSuggestionsDisabled={loading}
crossModeQueries={crossModeQueries}
onCrossModeSearch={handleCrossModeSearch}
onScopeDocument={handleScopeDocument}
/>
</>
) : null
Expand Down
Loading
Loading