ID-111 {111.2} — Reference-item read/browse/detail UI: PRODUCT
ID-111 — Reference-item read/browse/detail UI (PRODUCT)
Section titled “ID-111 — Reference-item read/browse/detail UI (PRODUCT)”Spec tier: PRODUCT + PLAN (the {111.4} decomposition follows this spec). No separate
TECH.md is required: the technical approach is low-risk and largely pre-decided by the
ratified RESEARCH-seed-s355.md (the {111.1} research artefact) — purpose-fit read UI over
two already-shipped read RPCs, no migration on the happy path, no novel subsystem. The
per-Subtask details briefs carry the implementation specifics the Executor needs. If the
{111.4} decomposition surfaces a genuine schema/RPC need (see B-30, the browse list-path
open question), that single Subtask escalates to a TECH slice; the rest does not.
Predecessor research:
RESEARCH-seed-s355.mdin this directory (S355, READ-ONLY). It is ratifiable-quality and is adopted as {111.1}; this PRODUCT.md does not restate its inventory — it converts the recommendation into numbered, testable behaviour invariants.
Figma: none provided. The surface deliberately echoes the existing content_items browse/detail look-and-feel (Warm Meridian, semantic tokens) rather than introducing new visual design — see B-26.
Summary
Section titled “Summary”Manual-URL imports (ID-110) and RSS feed articles now land in reference_items — a global,
workspace-less external-evidence layer — but there is no human web surface to read them.
This feature adds a purpose-fit reference reader: a detail page /reference/[id] (Phase A)
and a browse/list + search page /reference with lightweight filtering (Phase B), both over
the read RPCs ID-75 already shipped. It is not a clone of the content_items browse stack
and not an intelligence-workspace tab.
Problem
Section titled “Problem”After ID-110 closed, a manual URL ingest succeeds and lands a reference_items row, but the
ingestion success card surfaces only the title, summary, domain/subtopic badges, and a
copyable reference id — deliberately omitting any “view item” link, because no
reference-detail page exists to link to (ID-110 PRODUCT OQ-N; ID-110 TECH OQ-N resolution).
This is the explicit ID-110 follow-on that this Task discharges. It also blocks the bl-119
create→read round-trip E2E (e2e/tests/content-ingestion-url.spec.ts is describe.skip
pending this surface).
The data-access substrate already exists and is unused by web code:
reference_search(p_query text, p_query_embedding vector(1024), p_limit int DEFAULT 20)— ranked preview list:reference_id, title, summary_preview(LEFT 200 chars),body_preview(LEFT 200 chars),embedding_score+fulltext_score(separate raw numeric(5,4) columns — N9 RESOLVED-S236, the caller applies its own blend),source_url,published_at,primary_domain,primary_subtopic,layer,ingestion_source,source_document_id. InternalORDER BY embedding*0.6 + fulltext*0.4 DESC, filtersWHERE ri.embedding IS NOT NULL.STABLE SECURITY DEFINER,GRANT authenticated, service_role; REVOKE anon, PUBLIC.reference_get_verbatim(p_reference_id uuid)— full row minus embedding:id, title, body, summary, source_url, published_at, primary_domain, primary_subtopic, layer, source_document_id, ingestion_source, op_id, created_at, updated_at. Same grants.reference_itemstable — RLSreference_items_select FOR SELECT TO authenticated USING (true)(corpus-wide read for all authenticated; no app-side write policies — writes are pipeline-only or via thereference_ingestRPC). Indexes:idx_reference_items_published_at (published_at DESC), plus the HNSW embedding index. Theapi.reference_itemsview (security_invoker=true) re-exposes all columns includingembedding.
Code-intelligence orientation (verbatim, not paraphrased):
gitnexus_query({query: 'reference item search verbatim retrieval', repo: 'canonical'})returned no execution flow over the reference RPCs — top processes wereproc_69_homepage/proc_70_homepage(HomePage→GetRecentSearches, priority 0.071, the generic browseSearchBaratcomponents/browse/search-bar.tsx);definitionslistedrunItemSearch/runChunkSearch(lib/mcp/tools/search.ts) — the content_items MCP search path, not references. Noreference_*symbol appeared.gitnexus_context({name: 'reference_search', repo: 'canonical'})→{"error": "Symbol 'reference_search' not found"}.gitnexus_context({name: 'reference_get_verbatim', repo: 'canonical'})→{"error": "Symbol 'reference_get_verbatim' not found"}. (Expected: both are Postgres SECURITY DEFINER functions, not TS symbols; GitNexus indexes the TS/Python call graph.)- A
grepsweep confirms the only TS reference to either RPC name is a comment inapp/api/ingest/url/route.ts;find app -path '*reference*'returns no route. Theccc searchfallback daemon errored in this environment, but the grep evidence is definitive: zero TS callers, noapp/reference/*route.gitnexus orientation: no existing symbols match — greenfield read surface over existing RPCs.
Goals / Non-goals
Section titled “Goals / Non-goals”Goals. A reference-detail page; the success-card “view reference” link wired to it; a
reference browse/list page with a search box and lightweight filters; a reference-scoped
search API endpoint (because /api/search is content_items-scoped). UK English; Warm
Meridian semantic tokens; authenticated-only; WCAG 2.1 AA.
Non-goals (do NOT build). A workspace FK/junction for references (ID-75 BI-7 — global,
workspace-less, ratified). Any editor / governance-review / publication-lifecycle / QA /
starred / user-tags / read-marks / bulk-review / preset chrome (references carry none of
those columns). Content-type / platform / author / freshness / thumbnail / quality-flag
filters (references have no such columns). Entity-mention rendering (the re-point is an
ID-71-inherited follow-on, not wired). Any placement under /intelligence/[workspaceId].
Any change to the AI/MCP tool surface over these RPCs (that is ID-71’s — coordinate on
the shared RPC seam, do not author divergent contracts here).
Behavior
Section titled “Behavior”Detail page — /reference/[id] (Phase A)
Section titled “Detail page — /reference/[id] (Phase A)”-
Navigating to
/reference/<uuid>for an existing reference renders a read-only detail page showing, at minimum: the title (page heading), the body rendered as markdown (thebodycolumn is PullMD/Docling markdown and is the canonical body of record), the summary (when non-null), and metadata:primary_domain,primary_subtopic, andlayer(each shown only when non-null), thesource_urlas an outbound link, andpublished_atformatted as a UK date (DD/MM/YYYY) when non-null. -
The page also surfaces a provenance affordance: an indication that the reference derives from a source document (
source_document_id) and whichingestion_sourceproduced it (rss_feedorurl_import), phrased in plain user language (e.g. “Imported from URL” vs “From an RSS feed”). It does not require loading the full source_documents row for v1; theingestion_sourcevalue is sufficient signal.op_id,created_at,updated_at,source_document_id(raw uuid) are not surfaced as prominent UI — they are infrastructure, not user content. -
The detail page is read-only: it renders no edit, delete, governance-review, publication-status, layer-suggestion, starred, tag, or QA control. The only interactive affordances are the outbound
source_urllink, the body’s own markdown links, and standard navigation (e.g. a back/breadcrumb to/reference). -
The body markdown renders consistently with how content_items bodies render — GFM tables, headings, inline code, links, lists — reusing the established markdown rendering behaviour so the same markdown produces the same output on both surfaces. Raw pipe/markdown text is not shown to the user when the content is valid markdown.
-
Not found. Navigating to
/reference/<id>where no reference with that id exists (or the id is not a valid uuid) renders the standard application not-found surface (404), not a blank page, a spinner that never resolves, or a raw error. A reference whose row exists is always retrievable here regardless of which workspace the viewer belongs to — the corpus is global (reference_items_select USING (true)). -
Unauthenticated access.
/reference/[id]is an authenticated surface (it is not added toproxy.tspublicRoutes). An unauthenticated request redirects to/loginexactly as other authenticated routes do. No reference content is rendered pre-auth. -
Loading / error. While the row is being fetched the page shows a loading state (not a blank page). If the fetch fails for a reason other than not-found (RPC/transport error), the page shows a non-destructive error state with a retry affordance, and never silently renders an empty page as if the reference had no content.
Success-card wiring (Phase A)
Section titled “Success-card wiring (Phase A)”-
After a successful manual-URL ingest, the ingestion success card’s reference variant surfaces a “View reference” link (or button) navigating to
/reference/<referenceId>, in addition to the existing copyable reference id. The link uses thereferenceIdalready present on the card (theidreturned byPOST /api/ingest/url). -
The copyable reference id affordance is retained alongside the new link (the id remains useful for lookup/sharing); the change is additive — wiring a previously-omitted link, not replacing the copy control. The “Reference saved successfully” framing and the domain/subtopic badges are unchanged.
-
The “View reference” link is only rendered when a non-empty
referenceIdis present; if for any reason the success response carried no id, the card degrades to today’s copyable-id-only behaviour rather than rendering a link to/reference/(which would 404). Colour is never the sole signal that the link is actionable — it is a standard link/button affordance with a text label (WCAG 2.1 AA).
Browse / list page — /reference (Phase B)
Section titled “Browse / list page — /reference (Phase B)”-
/referenceis an authenticated page (not inpublicRoutes; unauthenticated →/login). It lists references with, per card/row: title (linking to/reference/<id>), a preview of the summary or body,primary_domain/primary_subtopic/layer(shown when present),ingestion_source, andpublished_at(DD/MM/YYYY when present). -
Default (no search term) list. With no active search query, the page shows references ordered by
published_atdescending (most recently published first), mirroring the existingpublished_at DESCindex. References with a nullpublished_atsort after those with a value (they are not silently dropped from the list). The default list does not require an embedding (it is a plain corpus listing) — see B-30 for the list-path decision. -
Search. Entering a free-text query and submitting runs a reference-scoped semantic search and replaces the list with ranked results: most relevant first. Search results show the same per-item fields as the default list, using the RPC’s
summary_preview/body_previewfor the preview text. Search covers the reference corpus only and never blends incontent_itemsorq_a_pairsresults (BI-16 two-surface separation). -
The reference search is served by a reference-scoped endpoint, distinct from
/api/search(which is content_items-scoped). The endpoint is authenticated (auth.success/authFailureResponse(auth)), embeds the query text with the same embedding helper the content search uses, and callsreference_search. It returns the RPC’s columns to the client; the client (not the SQL) decides display ordering/blend if it chooses to re-rank, but the default presentation follows the RPC’s returned order. -
Clearing search. Clearing the query (empty submit or a clear affordance) returns the page to the default
published_at DESClist of B-12. Search state is reflected in the URL so a searched view is shareable/back-navigable, consistent with the existing browse surface’s URL-driven filter convention. -
Filters. The page offers a reduced, reference-appropriate filter set only:
primary_domain,primary_subtopic,ingestion_source(rss_feed|url_import), and apublished_atdate range. No other filter dimension is offered (explicitly: no workspace, entity, content_type, platform, author, freshness, quality, owner, governance, starred, user-tags, or preset filters — references carry none of those columns). Applying a filter narrows the currently-shown set (default list or search results); filters compose (AND) with each other and with an active search query. -
Active filters are reflected in the page state/URL and are individually clearable; a “clear all” affordance resets to the unfiltered default list. Each filter control is labelled with text (not colour-only); the
ingestion_sourcetoggle reads in plain language (“URL import” / “RSS feed”), not raw enum values. -
Empty states. When the corpus has no references at all, the page shows an explicit empty state explaining there are no references yet (not a blank page or an infinite spinner). When a search or filter combination yields no matches, the page shows a distinct “no results for these filters/this search” empty state that makes clear the corpus is non-empty but nothing matched, with a way to clear filters/search.
-
Loading / pagination. The list shows a loading state on initial load and when changing search/filters (not a blank page). If the corpus is large enough to warrant it, the list loads incrementally (paginated/“load more” or infinite scroll consistent with the existing browse surface); a v1 that loads a bounded page and offers more is acceptable. Whatever the mechanism, the user can reach all matching references and the count shown never claims more or fewer than are reachable.
-
Error state. If the list or search fetch fails (transport/RPC error, not merely “empty”), the page shows a non-destructive error state with retry, distinct from the empty state, and never renders an empty list as though the corpus were genuinely empty.
Cross-cutting invariants
Section titled “Cross-cutting invariants”-
Authenticated, role-uniform, workspace-uniform. Every authenticated user sees the same reference corpus on both pages — there is no role-gating beyond authentication and no workspace scoping (ID-75 BI-7;
reference_items_select USING (true)). A reference visible to one authenticated user is visible to all. No admin/editor/reviewer/viewer distinction applies to reading references. -
References are a separate surface from content items. Reference pages live under
/reference(never/item,/browse,/library, or/intelligence/[workspaceId]). Content_items browse/detail surfaces are unchanged by this feature; no reference rows appear in/browse,/item/[id],/library, or the intelligence articles list, and no content_items appear under/reference. -
AI-invisible infrastructure. The embedding-backed semantic search and any AI-originated enrichment behind
summary/ classification are presented as ordinary platform behaviour — the reference reader does not advertise “AI search” or expose model internals, scores, or embeddings as user-facing chrome (per the AI-visibility policy). The rawembedding_score/fulltext_scorecolumns are an implementation detail of ranking, not user-facing content. -
Read-only, no write paths. Neither page exposes any affordance that writes to
reference_items(no edit, delete, re-classify, layer-change, tag, star). Writes remain pipeline-only or via the existingreference_ingestRPC; this feature adds read surfaces exclusively. -
Shared RPC seam with ID-71. Both pages read through the same
reference_search/reference_get_verbatim/reference_itemssubstrate that ID-71’s AI/MCP tool surface consumes. This feature MUST NOT alter those RPC signatures or semantics; if a list-path gap (B-30) needs a new read path, it is additive and coordinated with ID-71, never a breaking change to the shared seam. -
Visual consistency (Warm Meridian). Cards, lists, badges, empty/loading/error states, and the detail layout use Warm Meridian semantic design tokens only (never raw Tailwind colours), echoing the content_items browse/detail look-and-feel so the reference surface feels native. Domain/subtopic/layer badges and the
ingestion_sourceindicator never rely on colour alone to convey meaning (WCAG 2.1 AA) — each carries a text label. -
Date and locale. All dates render UK-format (DD/MM/YYYY); all copy is UK English (“colour”, “organisation”, “summarise”).
published_atis the original publication time (not ingest time) and is rendered as such; a nullpublished_atis shown as an explicit “no publication date” treatment rather than a fabricated or epoch date.
Open questions (ratification)
Section titled “Open questions (ratification)”-
B-28 — Detail provenance depth. Phase A surfaces provenance via
ingestion_source+ “derives from a source document” (B-2), without loading thesource_documentsrow. Is the plainingestion_sourcesignal sufficient for v1, or does Liam want the source document’s own metadata (e.g. original filename / fetched-at) surfaced on the reference detail page? Recommended default:ingestion_sourceonly for v1; promote richer source-document provenance as a follow-on if needed. -
B-29 — Detail page vs modal. The research leans “page or modal over
reference_get_verbatim”; this spec assumes a page at/reference/[id](needed anyway for the success-card link and the bl-119 round-trip to have a stable URL). Confirm a page (not a modal) is the intended Phase-A destination. Recommended default: page. -
B-30 — Browse list path (the one possible TECH/schema slice). The default
published_at DESClist (B-12) is a plain corpus listing thatreference_searchcannot serve (it requires a query embedding and filtersWHERE embedding IS NOT NULL). Two options: (a) a direct authenticatedfrom('reference_items').select(<list cols>)read (RLS already permits it; no migration; simplest — recommended for v1), or (b) a newreference_list(p_limit, p_offset, filters…)RPC mirroring thepublished_at DESCindex (only if pagination/filter pushdown is needed at scale). Confirm (a) for v1. Note: with (a), references whoseembedding IS NULLappear in the default list but are unreachable via search — acceptable (search inherently requires an embedding), and called out so it is not mistaken for a bug. -
B-31 — Filter application (client vs server). For the small v1 corpus, filters (B-16) may be applied client-side over a bounded fetched page, or pushed into the direct
reference_itemsselect. This is a {111.4}/implementation decision, not a behaviour change (the user-visible behaviour in B-16/B-17 holds either way). Flagged so the decomposition picks one explicitly and the Checker knows either is acceptable.