Skip to content

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.md in 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.

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.

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. Internal ORDER BY embedding*0.6 + fulltext*0.4 DESC, filters WHERE 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_items table — RLS reference_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 the reference_ingest RPC). Indexes: idx_reference_items_published_at (published_at DESC), plus the HNSW embedding index. The api.reference_items view (security_invoker=true) re-exposes all columns including embedding.

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 were proc_69_homepage / proc_70_homepage (HomePage→GetRecentSearches, priority 0.071, the generic browse SearchBar at components/browse/search-bar.tsx); definitions listed runItemSearch/runChunkSearch (lib/mcp/tools/search.ts) — the content_items MCP search path, not references. No reference_* 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 grep sweep confirms the only TS reference to either RPC name is a comment in app/api/ingest/url/route.ts; find app -path '*reference*' returns no route. The ccc search fallback daemon errored in this environment, but the grep evidence is definitive: zero TS callers, no app/reference/* route. gitnexus orientation: no existing symbols match — greenfield read surface over existing RPCs.

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).

  1. 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 (the body column is PullMD/Docling markdown and is the canonical body of record), the summary (when non-null), and metadata: primary_domain, primary_subtopic, and layer (each shown only when non-null), the source_url as an outbound link, and published_at formatted as a UK date (DD/MM/YYYY) when non-null.

  2. The page also surfaces a provenance affordance: an indication that the reference derives from a source document (source_document_id) and which ingestion_source produced it (rss_feed or url_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; the ingestion_source value 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.

  3. 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_url link, the body’s own markdown links, and standard navigation (e.g. a back/breadcrumb to /reference).

  4. 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.

  5. 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)).

  6. Unauthenticated access. /reference/[id] is an authenticated surface (it is not added to proxy.ts publicRoutes). An unauthenticated request redirects to /login exactly as other authenticated routes do. No reference content is rendered pre-auth.

  7. 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.

  1. 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 the referenceId already present on the card (the id returned by POST /api/ingest/url).

  2. 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.

  3. The “View reference” link is only rendered when a non-empty referenceId is 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)”
  1. /reference is an authenticated page (not in publicRoutes; 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, and published_at (DD/MM/YYYY when present).

  2. Default (no search term) list. With no active search query, the page shows references ordered by published_at descending (most recently published first), mirroring the existing published_at DESC index. References with a null published_at sort 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.

  3. 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_preview for the preview text. Search covers the reference corpus only and never blends in content_items or q_a_pairs results (BI-16 two-surface separation).

  4. 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 calls reference_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.

  5. Clearing search. Clearing the query (empty submit or a clear affordance) returns the page to the default published_at DESC list 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.

  6. Filters. The page offers a reduced, reference-appropriate filter set only: primary_domain, primary_subtopic, ingestion_source (rss_feed | url_import), and a published_at date 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.

  7. 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_source toggle reads in plain language (“URL import” / “RSS feed”), not raw enum values.

  8. 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.

  9. 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.

  10. 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.

  1. 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.

  2. 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.

  3. 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 raw embedding_score / fulltext_score columns are an implementation detail of ranking, not user-facing content.

  4. 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 existing reference_ingest RPC; this feature adds read surfaces exclusively.

  5. Shared RPC seam with ID-71. Both pages read through the same reference_search / reference_get_verbatim / reference_items substrate 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.

  6. 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_source indicator never rely on colour alone to convey meaning (WCAG 2.1 AA) — each carries a text label.

  7. Date and locale. All dates render UK-format (DD/MM/YYYY); all copy is UK English (“colour”, “organisation”, “summarise”). published_at is the original publication time (not ingest time) and is rendered as such; a null published_at is shown as an explicit “no publication date” treatment rather than a fabricated or epoch date.

  1. B-28 — Detail provenance depth. Phase A surfaces provenance via ingestion_source + “derives from a source document” (B-2), without loading the source_documents row. Is the plain ingestion_source signal 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_source only for v1; promote richer source-document provenance as a follow-on if needed.

  2. 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.

  3. B-30 — Browse list path (the one possible TECH/schema slice). The default published_at DESC list (B-12) is a plain corpus listing that reference_search cannot serve (it requires a query embedding and filters WHERE embedding IS NOT NULL). Two options: (a) a direct authenticated from('reference_items').select(<list cols>) read (RLS already permits it; no migration; simplest — recommended for v1), or (b) a new reference_list(p_limit, p_offset, filters…) RPC mirroring the published_at DESC index (only if pagination/filter pushdown is needed at scale). Confirm (a) for v1. Note: with (a), references whose embedding IS NULL appear 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.

  4. 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_items select. 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.