Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
62 changes: 62 additions & 0 deletions docs/adr/0150-korean-relative-time-retrieval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADR 0150: Global Ask resolves Korean relative-time expressions
Comment thread
seonghobae marked this conversation as resolved.

- 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.
61 changes: 61 additions & 0 deletions docs/adr/0151-ask-multi-lineage-graph.md
Original file line number Diff line number Diff line change
@@ -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 `<figure>`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 `<LineageDag>` 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.
63 changes: 63 additions & 0 deletions docs/adr/0152-ask-image-citation.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions docs/adr/0153-ask-evidence-layer-popup.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
seonghobae marked this conversation as resolved.

### 3.4 Scientific measurement recovery (must remain true-parameter tests)

| PR | Intent |
Expand Down
Loading