# Build the "Source freshness and coverage" pattern: `SourceCoverage`

## Stack (edit this block to match your project)
- React 18+ with TypeScript: one function component, typed props, no `any`.
- Tailwind CSS, using your design system's tokens if you have them.
- Presentational only: data comes in through props, decisions go out through callbacks. No fetching or global state inside.
- If your codebase already has conventions (naming, shared components, tokens), follow them over this spec.

## What the pattern is for
State when sources were updated and how much of the corpus was actually searched.

Use it when: Retrieval products where staleness or thin coverage is the dominant failure mode.

## What to build
A compact coverage line attached to an AI answer that says how many sources were searched out of how many are available, and how fresh each one is, with an expandable list. When coverage is below a threshold or a key source is stale, it says so next to the answer and offers to refresh and search again. It renders under a retrieval answer.

### Props
- `sources`: Array<{ id: string; name: string; state: "searched" | "stale" | "not-searched" | "not-connected" | "syncing"; lastSyncedAt?: Date; matches?: number; reason?: string }>, from the index's retrieval metadata.
- `documentCount`: number, documents actually used in the answer.
- `threshold`: { minSources?: number; maxAgeDays?: number }, below which coverage is flagged.
- `warning?`: { title: string; detail: string }, why the gap matters here, e.g. "Renewal conversations usually happen on calls".
- `onRefresh?(sourceId: string)`: asks the parent to re-sync and re-run. The component never syncs itself.
- `onConnect?(sourceId: string)`: for sources that aren't connected.
- `expanded`: boolean and `onExpandedChange(v: boolean)`.

### States it must render
- Full coverage: a quiet line "Based on 4 of 4 sources, all synced today".
- Thin coverage: an amber notice by the answer, "Based on 2 of 4 sources", with the warning detail and "Refresh call notes and search again".
- Expanded list: each source with an icon and text state, e.g. "Help desk tickets: synced 2 hours ago, 3 matches", "Call notes: not searched, last synced 21 Aug, 41 days ago", "Email: not connected".
- Refreshing: the source row shows "Syncing 6 calls since 21 Aug" and the answer dims until the parent passes the new one.
- Updated: the count rises ("3 of 4 sources"), changed rows highlight briefly, and any remaining gap is still stated.

### Interactions
- The coverage line toggles the source list.
- Refresh calls `onRefresh(sourceId)`; Connect calls `onConnect(sourceId)`.
- Hovering a source highlights the parts of the answer drawn from it, when the parent provides that mapping.

### Edge cases
- A "none found" answer on thin coverage: the warning is required, since absence from two sources is not absence.
- Sync timestamps in the future or missing: show "Sync time unknown" rather than "just now".
- Many sources (more than 8): show the flagged ones first, then a count of the rest.
- Refresh finds nothing new: say "Refreshed, no new matches" so the person knows it ran.

## Must avoid
Presenting a confident answer built on two documents as if the whole corpus was consulted.

## Engineering constraint
Surface retrieval metadata already returned by the index; flag below-threshold coverage explicitly.

## Accessibility
- Source states are words, not only icons; each row's accessible name includes name, state and age.
- Announce when a refresh finishes and the new coverage count.

## House rules
- Real `<button>`/`<input>` elements, full keyboard use, visible focus; announce status changes with `aria-live="polite"`; respect `prefers-reduced-motion`.
- All user-facing text comes from props (or a `labels` prop) so it can be translated.
- One consistent accent marks anything the AI made or is doing; amber means "needs a person", red means risky, green means done. Never rely on colour alone.
- Fits a 360 px wide container without horizontal scrolling.

## Acceptance checks
- [ ] Every state above can be rendered by passing props, with no internal fake data.
- [ ] Every interaction and edge case above behaves as described.
- [ ] Nothing in "Must avoid" can happen with this component.

## Deliver
1. The component file with its exported prop types.
2. A short usage example that renders each state.
