diff --git a/CHANGELOG.md b/CHANGELOG.md index a46223352..9d0005ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project are documented here. Format follows ### Added +- ADRs 0150-0153 define the accepted boundaries for Korean relative-time + retrieval, multi-thread Event Lineage answers, persisted image-evidence + citations, and the focused evidence popup. Their implementations remain + active-PR evidence until protected merge. - ADR 0157 and its exact-head inventory choose the existing lowercase public ontology namespace as canonical and define the compatibility, publication, and migration evidence required by issue #372 without rewriting identifiers. diff --git a/docs/adr/0150-korean-relative-time-retrieval.md b/docs/adr/0150-korean-relative-time-retrieval.md new file mode 100644 index 000000000..c79933e02 --- /dev/null +++ b/docs/adr/0150-korean-relative-time-retrieval.md @@ -0,0 +1,62 @@ +# ADR 0150: Global Ask resolves Korean relative-time expressions + +- Status: Accepted +- Date: 2026-08-22 +- Related: [0047](0047-global-ask-semantic-retrieval.md), [0090](0090-global-ask-lineage-timeline-expansion.md) + +## Context + +A question like "어제 무슨 일이 있었나요?" ("what happened yesterday?") names a +time window the reader already has in mind. Before this decision, +`gather_global_chat_sources`'s keyword retrieval (ADR 0047) had no way to +use that window: "어제" only ever became a literal search token against +post titles and bodies, indistinguishable from any other two-character +term. A fresh, unrelated post that happened to rank highest on unrelated +keyword overlap could outrank the post the reader actually meant. + +## Decision + +`lineageweave.temporal_expressions.resolve_korean_relative_time` is a pure +date-arithmetic function (no database or network access) that resolves a +question's first Korean relative-time expression into an inclusive +`(start_date, end_date)` window. It covers 오늘/어제/그제(그저께)/그끄제(그끄저께), +작년/재작년/올해/내년, 작년·재작년 이맘때(쯤) (a ±5-day fuzz window around the +anniversary date, since "-쯤" means "approximately"), 지난/이번/다음 주/달, and +the general "N일/주/개월/년 전" pattern. "언젠가" ("someday") resolves to no +bound -- the reader has explicitly declined to name one, which is the same +retrieval behavior as finding no expression at all. + +`gather_global_chat_sources` applies the resolved window as an additional +`created_at` bound on its final ABAC-filtered candidate query, additive to +the existing keyword-match ranking -- it narrows the already-ranked +candidate set, it does not replace ranking with a date filter. Matched +temporal literals are excluded from keyword-term extraction +(`TEMPORAL_STOPWORDS`) so a resolved expression does not also become a +near-meaningless literal search term. + +## Considered alternatives + +- Send the raw question to an LLM to extract a date range: rejected for the + same reason ADR 0047's keyword step avoids ungrounded LLM inference at + the retrieval boundary -- a hallucinated date range would silently + narrow (or widen) the candidate set with no way for the reader to verify + it, and every extra provider round-trip is retrieval latency the reader + pays before seeing an answer. +- Resolve to a single anchor day for the generalized "N주/N개월 전" pattern + (mirroring "N일 전"): rejected -- "2 weeks ago" means that whole week to + a reader, not one arbitrary day inside it, so `_week_range`/`_month_range` + are used instead, matching how the named "지난주"/"지난달" patterns already + behave. + +## Consequences + +- Global Ask can now answer time-scoped questions without the relative-time + term itself acting as retrieval noise. +- The resolver is locale-specific (Korean only); a question in another + supported UI locale (ADR on i18n scope, `frontend/src/i18n.ts`) that + names a relative time in that language still falls back to keyword-only + retrieval. Extending to additional locales is a follow-up, not required + by this decision. +- `today` is always passed explicitly by the caller (server-local date); + the resolver itself never reads the wall clock, keeping it a pure, + trivially unit-testable function. diff --git a/docs/adr/0151-ask-multi-lineage-graph.md b/docs/adr/0151-ask-multi-lineage-graph.md new file mode 100644 index 000000000..22c9a14f4 --- /dev/null +++ b/docs/adr/0151-ask-multi-lineage-graph.md @@ -0,0 +1,61 @@ +# ADR 0151: Global Ask renders every cited thread as its own branch graph + +- Status: Accepted +- Date: 2026-08-22 +- Related: [0090](0090-global-ask-lineage-timeline-expansion.md), [0064](0064-lineage-evidence-and-tree-assembly.md) + +## Context + +ADR 0090 expands only the single top-ranked match through its direct +`post_lineage_edge` neighbors, so an answer could speak to at most one +connected timeline. A Global Ask answer frequently cites posts from more +than one unrelated reconstruct thread -- two separate customer complaints +that happen to share a keyword, for example -- and before this decision +the reader had no way to see how (or whether) those threads relate to each +other; the answer's text evidence facts named a lineage relationship in +prose only for the single expanded anchor. + +Separately, `LineageDag`/`layoutLineageDag` (the post-detail popup's Event +Lineage visualization) already renders one `LineageGraph` payload as N +independent branch-tree `
`s, one per reconstruct thread (bucketed +by `LineageGraphNode.group`, `lineageLayout.ts`'s `layoutLineageDag`). +Each thread's own tree is laid out with git-log-style branch/merge +semantics (`is_root`, `is_branch_point`) already -- there was no missing +git-branch-style layout to build, only missing graph *data* for Global +Ask to feed that existing component. + +## Decision + +`lineage_graphs_for_posts` (`backend/app/lineage_ingestion.py`) merges +every cited post's full reconstructed thread into one `LineageGraph` +payload: it calls the existing, ABAC-checked `visible_lineage_graph` once +per cited post id and deduplicates nodes/edges shared across citations. +`POST /api/ask` returns this as a new `lineage_graph` response field. +`AskAgentPanel` renders `` under the answer whenever that +field carries nodes -- reusing the post-detail popup's exact rendering +component, so citing posts from two unrelated threads produces two +independent branch-tree figures with no new frontend layout code. + +## Considered alternatives + +- Build a new, Ask-specific multi-graph component: rejected -- + `LineageDag` already does exactly this (grouped, branch-aware, per-thread + figures); a second implementation would only risk drifting from the + post-detail popup's established visual language and accessibility + behavior for the same underlying data shape. +- Bound the merged graph to the single top-cited post's thread, matching + ADR 0090's scope: rejected -- that reintroduces the original gap this + decision addresses (a multi-thread answer showing only one thread). + +## Consequences + +- An Ask answer's lineage evidence is now visually traceable per cited + thread, not summarized as prose for one anchor post only. +- `lineage_graphs_for_posts` issues one `visible_lineage_graph` call per + cited post (each a bounded `source_post` scan plus a full + `post_lineage_edge` table read); acceptable at the current citation cap + (`_POST_CHAT_SOURCE_LIMIT` = 8) and the existing `visible_lineage_graph` + precedent for the post-detail popup, revisit with a single batched query + if the citation cap grows materially. +- The response payload grows by one field (`lineage_graph`); existing + consumers that ignore unknown fields are unaffected. diff --git a/docs/adr/0152-ask-image-citation.md b/docs/adr/0152-ask-image-citation.md new file mode 100644 index 000000000..ac9e456bd --- /dev/null +++ b/docs/adr/0152-ask-image-citation.md @@ -0,0 +1,63 @@ +# ADR 0152: Global Ask cites persisted image evidence, never raw bytes + +- Status: Accepted +- Date: 2026-08-22 +- Related: [0066](0066-position-preserving-image-content.md), [0039](0039-global-ask-agent-source-boundary.md) + +## Context + +An Ask answer citing a post whose evidence actually came from an embedded +picture (a screenshot, a diagram) read as an unmarked text claim -- the +reader had no way to tell the citation was image-sourced rather than +drawn from the post's written body. Separately, no code path in this +repository ever sends embedded image bytes to a client: `post_content_image` +persists each image's OCR text, caption, and tags (ADR 0066), and +`GET /api/posts/{id}/content` already returns only that description, never +the image itself. `lineageweave.image_content`'s normalization step +likewise replaces every embedded image with a bracketed text placeholder +before any LLM or API response is built. + +## Decision + +"Cite images" is satisfied inside that existing, deliberate boundary +rather than by adding a new image-serving mechanism. `cited_post_images` +(`backend/app/post_chat_ingestion.py`) reads `post_content_image`/ +`post_content_image_tag` for the already-cited post ids and returns their +persisted `mime_type`, `caption`, `extracted_text`, and `tags` -- the same +fields `GET /api/posts/{id}/content` renders, scoped to citations. +`POST /api/ask` returns this as a new `cited_post_images` field; the +Ask answer view renders an explicit "Image evidence" line per cited post +carrying one. + +No additional ABAC check runs inside `cited_post_images`: `cited_post_ids` +only ever contains ids drawn from `gather_global_chat_sources`'s +already-authorized source set -- the same trust boundary +`cited_post_evidence`/`cited_post_summaries` (`lineageweave.post_chat`) +already rely on without re-checking visibility per call. + +## Considered alternatives + +- Add an endpoint that serves the original image bytes for a citation: + rejected -- this would be the first place in the codebase raw embedded + image bytes ever leave the server, reopening a boundary ADR 0066 and + `lineageweave.image_content`'s normalization step deliberately closed. + Nothing about "citing" an image requires the pixels themselves; the + persisted description is the citable claim. +- Fold image evidence into the existing `cited_post_evidence` fact list + (reusing its `kind`/`text` shape): rejected -- an image's caption and + its OCR text are two independently useful strings (a diagram's caption + says what it's a diagram *of*; its OCR says what text appears *in* it), + which the flat `{kind, text}` shape cannot carry without concatenating + them into one opaque string. + +## Consequences + +- A reader can now tell when an Ask citation's evidence came from a + picture rather than the post's written body, without any new image + storage or serving surface. +- The response payload grows by one field (`cited_post_images`); existing + consumers that ignore unknown fields are unaffected. +- Region-level citation (pointing at a specific area of a larger image, + as `post_content_image_region` already supports for the post-detail + popup) is not surfaced here -- a future enhancement, not required by + this decision. diff --git a/docs/adr/0153-ask-evidence-layer-popup.md b/docs/adr/0153-ask-evidence-layer-popup.md new file mode 100644 index 000000000..cf89521dd --- /dev/null +++ b/docs/adr/0153-ask-evidence-layer-popup.md @@ -0,0 +1,62 @@ +# ADR 0153: Ask citations open a focused evidence Layer Popup + +- Status: Accepted +- Date: 2026-08-22 +- Related: [0152](0152-ask-image-citation.md), [0151](0151-ask-multi-lineage-graph.md) + +## Context + +Reading one Ask citation's evidence meant either scanning the inline fact +list rendered under every citation at once, or leaving the answer +entirely to open the full post detail popup (`PostDetailPopup`) -- there +was no focused way to inspect a single citation's evidence without either +losing the answer or wading through unrelated citations' facts on screen +at the same time. + +## Decision + +`AskEvidenceLayerPopup` (`frontend/src/components/`) is a new, focused +modal opened by a "View evidence" button on each citation. It shows only +that one cited post's text evidence facts (ADR 0047's evidence chips) and +image evidence (ADR 0152) without navigating away from the answer or +displaying any other citation's evidence. It reuses the app's existing +`.popup-backdrop`/`.popup-panel` visual language (`PostDetailPopup`'s own +classes) rather than introducing a new modal style. + +Its dialog semantics are stricter than `PostDetailPopup`'s: `role="dialog"`, +`aria-modal="true"`, `aria-labelledby` naming the cited post's title, +Escape-to-close, backdrop-click-to-close, and initial focus moved onto the +panel on mount. `PostDetailPopup` has none of these today; this decision +does not retrofit them there -- a focused follow-up, not silently expanded +scope of this change. + +`chatEvidenceKindLabel` (previously a private `App.tsx` helper) moved to +`frontend/src/evidenceKindLabels.ts` so both `App.tsx` and the new +component read from one label map instead of maintaining two copies that +could drift. + +## Considered alternatives + +- Extend `PostDetailPopup` itself with an "evidence-only" display mode: + rejected -- that component already fetches and renders a large surface + (summary, 5W1H, Keymen, counterparties, Event Lineage, evaluation); a + mode flag threading through all of that to suppress everything except + evidence is a larger, riskier change than a small, independent + component with its own narrow props. +- Reuse the post-scoped chat's existing `EvidencePanel` (a non-modal, + `role="complementary"` sliding panel that shows a cited post's full + body): rejected -- it fetches and renders the entire post body, not + scoped facts/images, and its non-modal layout assumes the post-scoped + chat's own screen real estate, which Global Ask's answer view does not + have. + +## Consequences + +- A reader can inspect one citation's evidence in a focused layer without + losing their place in the answer. +- The Layer Popup pattern (a small, dialog-semantic overlay scoped to one + piece of evidence) is now precedent for future evidence surfaces that + don't warrant a full post detail popup. +- `PostDetailPopup`'s missing dialog semantics (no `role="dialog"`, no + Escape-to-close) remain an open accessibility gap, tracked here as a + known follow-up rather than fixed by this decision. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 0d78064f8..1c0852266 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -169,6 +169,17 @@ evidence across heads. | #421 | Playwright harness for Ask Agent capabilities | | #422 | ADRs for Ask Agent temporal / lineage / evidence goal | +ADRs 0150-0153 (recorded by #422) define the accepted boundaries before this +stack's implementation becomes protected-main evidence: #415 implements +Korean relative-time retrieval under ADR 0150; #418 implements scoped +multi-thread Event Lineage answers under ADR 0151; #419 implements persisted +image-evidence citations under ADR 0152; #420 implements a focused citation +evidence popup under ADR 0153; #421 adds the Playwright harness intended to +verify the combined flow. These capabilities remain active-PR evidence until +their exact heads pass all protected gates and merge; the combined browser +scenario is not release evidence until it runs successfully against one +merged release candidate. + ### 3.4 Scientific measurement recovery (must remain true-parameter tests) | PR | Intent |