Skip to content

feat: make Event Lineage directional and evidence-readable - #330

Merged
seonghobae merged 122 commits into
feat/analysis-run-name-evidence-lineagefrom
feat/lineage-dag-regression
Aug 21, 2026
Merged

feat: make Event Lineage directional and evidence-readable#330
seonghobae merged 122 commits into
feat/analysis-run-name-evidence-lineagefrom
feat/lineage-dag-regression

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Forward-only Event Lineage scope

This PR strengthens the reconstructed post/record lineage surface without converting it into a different product or deleting accepted behavior.

  • Renders node-clear parent→child arrowheads and correct source → target semantics.
  • Shows each record's ISO event date while explicitly stating that horizontal position is lineage depth, not elapsed time.
  • Preserves deterministic authored width in a named, keyboard-focusable horizontal region instead of shrinking deep graphs.
  • Provides redundant shape/text legend, current/root/branch state, and five-locale non-causal/non-authoritative disclosure.
  • Provides open-by-default exact-value evidence with relation, dates, and fused_score for keyboard, touch, print, and audit.
  • Keeps isolated roots interactive without an empty edge table and gives empty lineages an actionable five-locale next step.
  • Preserves click, Enter, and Space activation under SVG group semantics with component-local marker/caption IDs.
  • Fixes the event-date CSS specificity defect with a focused contract test; the 9px muted date rule now outranks the shared node-text selector without !important.

Product and design boundary

Forward stack repair completed

A two-parent merge commit was created without force-push or history rewrite. Protected main@ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7 is now the merge base, and conflict resolution used protected main as the complete tree baseline before reapplying exactly the twelve-file Event Lineage delta. GitHub compare reports behind 0 and a twelve-file product/documentation delta.

Exact current candidate

  • Protected base: main@ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7.
  • Exact current head: 1fad94ff4ec6007d3d3b732580ed68b586f56061.
  • Net changed files: 12, limited to Event Lineage implementation, tests, Storybook, i18n, ADR/changelog, and the forward-only stabilization plan.
  • Valid unresolved review threads: zero.
  • Exact-current-head hosted checks and an independent approval must regenerate; queued, absent, or predecessor evidence is non-passing.

Protected merge gate

Merge only after exact-head terminal checks, zero unresolved threads, and independent formal approval. Auto-merge is enabled, but no self-approval, force-push, protection bypass, or capability-reducing resolution is allowed.


Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Reviewed and corrected against exact current head f9df5ead44a2f2b1fd3b578bfd20f2fad15bb8aa.

Finding fixed:

  • layoutGroup recursively walked a cyclic component when that component was attached to a root (for example root → A → B → A). The malformed lineage graph could therefore overflow the browser call stack instead of rendering a bounded product view.
  • The shared layout walk now tracks nodes currently being visited and nodes already positioned, skips cyclic/repeated descendants, and still places every graph node.
  • Added a regression test for the root-attached cycle.

Verification on this exact head:

  • frontend full Vitest: 149 passed
  • frontend lint: passed
  • frontend TypeScript/Vite build: passed
  • CodeGraph sync/status: healthy and up to date
  • diff check: passed

Merge gate remains intentionally open: the head is current, required checks are queued, and no formal approval is present.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Exact head f9df5ea was revalidated against its current parent. Frontend lint passed, 149 tests passed, Vite build passed, and diff-check passed. Hosted checks and formal approval remain pending; no merge attempted.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Perform the forward-only stack repair on exact current head 6f4712d2b7c97ae75a5965d9cddd54bc02cb1bfe: ordinary-merge protected main@ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7 (or its newer exact successor if main advances before checkout) into feat/lineage-dag-regression. Do not rebase, force-push, drop commits, or shrink functionality. Preserve every #330 behavior and regression, especially parent→child geometry, exact evidence, five-locale copy, isolated/empty states, Figma/Storybook contracts, the new forward-only stabilization plan, and the TDD CSS-specificity fix/test. Resolve only real conflicts by retaining both current-main product/security/UI changes and the Event Lineage capability. Run focused DAG/CSS/i18n/layout tests, full frontend Vitest, lint, production build, Storybook build, documentation hygiene, and git diff --check; push a normal merge commit and publish the resulting exact parent/head plus receipts. Do not merge the PR or update #350.

Copy link
Copy Markdown
Contributor Author

Resolved the current CSS-specificity defect on exact head 6f4712d2b7c97ae75a5965d9cddd54bc02cb1bfe. A focused regression now requires .lineage-dag-node text.lineage-dag-node-date and rejects the lower-specificity standalone selector; the production rule uses that selector, so the intended 9px muted date styling outranks the shared .lineage-dag-node text rule without !important. Current-head Tests, SAST, and Security Scan are queued and remain non-passing until terminal success. The forward-only ordinary-main restack request is active; no capability was removed.

@seonghobae seonghobae changed the title feat: make buyer lineage DAG directional and evidence-readable feat: make Event Lineage directional and evidence-readable Aug 21, 2026
Forward-only conflict resolution uses protected main as the tree baseline and reapplies the exact twelve-file Event Lineage delta. This preserves current main product/security changes plus direction, dates, evidence, accessibility, Figma/Storybook, i18n, cycle safety, changelog, and stabilization-plan contracts without rewriting history or dropping capability.
@seonghobae
seonghobae changed the base branch from codex/normalize-source-indent-semantics to main August 21, 2026 05:55
seonghobae added a commit that referenced this pull request Aug 21, 2026
Forward-only conflict resolution preserves the authenticated mobile shell and UI/UX Guide v3 header/navigation work while inheriting PR #330's exact direction, date, evidence, accessibility, i18n, deterministic layout, Figma, Storybook, CSS-regression, and stabilization-plan contracts. No capability is removed and shared history is not rewritten.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 19 new potential issues.

Open in Devin Review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Old empty-state and edge-direction translations are now dead entries

This PR moves the DAG empty-state and edge-description copy into a new lineageDagI18n.ts module and renders the corrected source → target direction inline. The old main-catalog keys "No reconstructed lineage yet. Rebuild after seeding posts." and "{from} follows {to} ({score})" in frontend/src/i18n.ts (e.g. lines 283-284, 305) are no longer referenced by LineageDag.tsx. I confirmed no other component uses them (the "follows" match in App.tsx:2971 is an unrelated related-posts list). These are harmless dead entries, not a bug, but could be cleaned up.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

margin: 0 0 0.55rem;
padding: 0;
color: var(--color-text);
font-size: var(--lw-font-size-meta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Lineage legend, disclosure, and evidence table ignore their intended small font size

The legend, non-causal note, and evidence table request a meta font size through var(--lw-font-size-meta) (LineageDag.css), but that custom property is defined nowhere in the stylesheets, so each declaration is invalid and the text falls back to the surrounding font size instead of the smaller meta size.

Undefined custom property resolution

--lw-font-size-meta is referenced only in LineageDag.css, :99, and :120 and is never declared in frontend/src/index.css or frontend/src/styles/tokens.css (other tokens such as --color-accent-info-background and --radius-control are declared there). A font-size: var(--lw-font-size-meta) with no fallback resolves to the guaranteed-invalid value; because font-size is inherited, the affected elements (.lineage-dag-legend, .lineage-dag-boundary, .lineage-dag-evidence-table) render at the inherited ambient size rather than the intended compact meta size.

Prompt for agents
The stylesheet frontend/src/LineageDag.css references a design token var(--lw-font-size-meta) on lines 9, 99, and 120, but this custom property is not defined anywhere (not in frontend/src/index.css, frontend/src/styles/tokens.css, or App.css). As a result the legend, non-causal boundary note, and evidence table do not get the intended smaller meta font size and instead inherit the ambient size. Either define --lw-font-size-meta in the token files (matching the meta font size used elsewhere in the app, e.g. ~0.85rem which App.css uses for .lineage-dag-group figcaption), or reference an existing font-size token, or provide a fallback like var(--lw-font-size-meta, 0.85rem).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 51 to 68
const walk = (id: string, depth: number) => {
const kids = (children.get(id) ?? []).filter((childId) => byId.has(childId));
if (positions.has(id) || visiting.has(id)) return;
visiting.add(id);
const kids = (children.get(id) ?? []).filter(
(childId) => byId.has(childId) && !positions.has(childId) && !visiting.has(childId),
);
if (kids.length === 0) {
positions.set(id, { x: PAD + depth * COL_W, y: PAD + nextRow * ROW_H });
nextRow += 1;
visiting.delete(id);
return;
}
const startRow = nextRow;
for (const childId of kids) walk(childId, depth + 1);
const midRow = (startRow + nextRow - 1) / 2;
positions.set(id, { x: PAD + depth * COL_W, y: PAD + midRow * ROW_H });
visiting.delete(id);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Cycle guard in DAG layout prevents infinite recursion but changes multi-parent layout

The new visiting/positions guards in layoutGroup (lineageLayout.ts) correctly bound recursion for malformed cyclic components (the new test covers this). Note a behavioral side effect for legitimate multi-parent/merge DAGs: the kids filter now excludes children that are already positioned or currently on the recursion stack (lineageLayout.ts), so a node reachable from two parents is positioned only under the first parent encountered; the second parent then sees no remaining children and is placed as a leaf. Previously the shared child was re-positioned (last-writer-wins). Neither behavior crashes; this is purely an aesthetic layout difference for merge-shaped lineages, which are uncommon in the git-branch-style reconstruction. Not flagged as a bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +188 to +191
)}
</p>
<details className="lineage-dag-evidence" open>
<summary>{t("Evidence trail")}</summary>

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Evidence-table header 'When' reuses the 5W1H translation key

t("When") for the evidence-table column header (LineageDag.tsx) resolves to the same catalog entry used by the 5W1H panel (e.g. Korean 언제 at i18n.ts). The rendering is semantically acceptable, but note the shared key means any future retranslation of the 5W1H 'When' will also change this column header. Not a bug, but worth awareness if the two contexts ever need to diverge.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +76 to +84
.lineage-dag-canvas {
display: block;
max-width: none;
color: color-mix(in srgb, canvastext 55%, transparent);
}

.lineage-dag-arrow {
fill: currentColor;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Arrowhead color differs from edge stroke color

The edge path stroke is var(--border) (App.css .lineage-dag-edge), while the arrow marker fills with currentColor inherited from .lineage-dag-canvas { color: color-mix(in srgb, canvastext 55%, transparent) } (frontend/src/LineageDag.css:79,83). The arrowhead therefore renders in a muted mixed color that does not match the edge line color. Cosmetic only, but worth confirming the direction indicator is intended to be a different tone than the edge it terminates.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +24 to +34
function edgePath(from: Point, to: Point): string {
const angle = Math.atan2(to.y - from.y, to.x - from.x);
const offsetX = Math.cos(angle) * (NODE_RADIUS + EDGE_CLEARANCE);
const offsetY = Math.sin(angle) * (NODE_RADIUS + EDGE_CLEARANCE);
const startX = from.x + offsetX;
const startY = from.y + offsetY;
const endX = to.x - offsetX;
const endY = to.y - offsetY;
const midX = (startX + endX) / 2;
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`;
}

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Arrowhead orientation vs endpoint offset

edgePath (LineageDag.tsx) offsets both the start and end points along the straight-line angle between node centers, but the cubic bezier control points share the endpoint's y (midX, endY), so the curve always enters the target horizontally. With orient="auto" the arrowhead therefore renders horizontally rather than along the straight-line angle. Because the endpoint is placed a constant NODE_RADIUS + EDGE_CLEARANCE (11px) from the target center, the arrow still stops ~4px outside the node circle as intended, so this is cosmetic only and consistent with how the S-curve visually approaches the node. Verified not a functional bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +33 to +36
.lineage-dag-legend-branch {
border-color: var(--badge-actor-organization-text);
background: var(--badge-actor-organization-bg);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Legend branch swatch color only loosely matches actual branch node color

The legend branch swatch uses --badge-actor-organization-text/bg (a brown-orange palette, tokens.css:18-19) while the actual branch node circle is styled with literal orange in App.css:867-870. Both are in the orange family so the legend is not misleading, but they are not the same value, so a strict visual match isn't guaranteed across light/dark themes. Minor cosmetic inconsistency, not a correctness bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +191 to +200
<summary>{t("Evidence trail")}</summary>
<div className="lineage-dag-evidence-scroll">
<table className="lineage-dag-evidence-table">
<caption className="visually-hidden">{`${lineageLabel} — ${t("Evidence trail")}`}</caption>
<thead>
<tr>
<th scope="col">{relationLabel}</th>
<th scope="col">{whenLabel}</th>
<th scope="col">{evidenceLabel}</th>
</tr>

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: New user-facing strings have full locale coverage

The evidence table introduces t("Evidence trail"), t("Graph relation"), t("When"), and t("Evidence"). All four keys already exist in every non-English locale in i18n.ts (ko/zh/ja/vi), so these render translated rather than falling back to English. The hardcoded relation/date strings and the literal (fused_score) suffix are intentionally language-neutral. No missing-translation regression.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +45 to +63
const instanceId = useId().replaceAll(":", "");
const groups = layoutLineageDag(graph);
if (graph.nodes.length === 0) {
return <p className="lineage-empty">{t("No reconstructed lineage yet. Rebuild after seeding posts.")}</p>;
return (
<p className="lineage-empty">
{lineageDagText(
"No reconstructed lineage yet. Add eligible source records, then rebuild Event Lineage.",
)}
</p>
);
}

return (
<div className="lineage-dag" aria-label={t("Reconstructed lineage")}>
{groups.map((group) => {
{groups.map((group, groupIndex) => {
const byId = Object.fromEntries(group.nodes.map((node) => [node.id, node]));
const arrowMarkerId = `lineage-dag-arrow-${instanceId}-${groupIndex}`;
const captionId = `lineage-dag-caption-${instanceId}-${groupIndex}`;
const lineageLabel = tf("{group} lineage", { group: group.heading });

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Marker IDs kept component-local; verified no duplicate-ID or cross-group collision

arrowMarkerId and captionId are derived from useId() (LineageDag.tsx) combined with groupIndex, so multiple LineageDag instances on one page and multiple groups within one instance produce distinct SVG marker IDs. markerEnd={url(#arrowMarkerId)} is referenced only inside the same <svg> that defines the marker, so scoping is correct. No collision risk.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1 to +6
### Changed

- Made the buyer Event Lineage DAG explicitly directional with parent-to-child arrowheads whose paths stop outside node circles.
- Preserved authored graph width behind a keyboard-focusable horizontal scroll region instead of shrinking deep lineages.
- Added visible event dates, a redundant visual/text legend, and an accessible exact-value evidence table for lineage relations, dates, and fused scores.
- Added five-locale buyer copy stating that reconstructed continuation edges do not prove causality or authoritative fact.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Changelog version 2.12.7 is below the latest 2.13.1 fragment

This adds 2.12.7-lineage-dag-evidence.md while newer fragments (up to 2.13.1-*) already exist in CHANGELOG.d/. The PR description notes it stacks on codex/normalize-source-indent-semantics, so the lower version may be intentional to slot into an earlier release line, but reviewers should confirm the version fragment is correct for the intended release rather than an accidental regression.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

@opencode-agent Review exact current head 1fad94ff4ec6007d3d3b732580ed68b586f56061 against protected main@ef6f5a5ffcb467bd935dc1e53acc0029669b0bd7. The forward-only merge is complete: protected main is the merge base, behind is zero, and the net delta is exactly 12 Event Lineage files. Verify parent→child geometry, unique marker IDs, accessible interactive descendants, visible dates and depth-not-duration semantics, five-locale legend/truth/empty-state copy, exact fused_score table and mobile data labels, isolated/empty states, cycle-safe deterministic layout, CSS specificity regression, Figma frames, Storybook states, synthetic-only docs, and preservation of all current-main product/security changes. Run focused DAG/layout/CSS/i18n tests plus full frontend lint/Vitest/build/Storybook and documentation hygiene. Publish a durable exact-head receipt and formal verdict or explicit fail-visible evidence. Do not merge, rewrite history, or transfer predecessor evidence.

@seonghobae seonghobae added the enhancement New feature or request label Aug 21, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

Queue 2026-08-21T15:09 KST: exact head 1fad94ff4ec6007d3d3b732580ed68b586f56061 still needs an independent formal APPROVE on this SHA. Copilot/Devin comments are not independent approval. Do not self-approve, force-push, or merge around required Checks. This comment is a review request, not an approval.

# Conflicts:
#	frontend/src/App.tsx
#	lineageweave/post_summary.py
#	tests/test_post_summary.py
@seonghobae
seonghobae changed the base branch from main to feat/analysis-run-name-evidence-lineage August 21, 2026 06:46
@seonghobae

Copy link
Copy Markdown
Contributor Author

Current head 336730f is normally restacked on the current #258 provider-boundary stack. The merge conflict was resolved by preserving authenticated-only AdminPanel rendering and the provider-safe summary parser.

Verification: backend 768 passed, 16 skipped; frontend 153 passed; lint, Vite build, Storybook build, diff check, and raw-provider audit passed. The PR is now stacked and its required Checks are restarting; independent review remains pending.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment on lines +86 to +90
.lineage-dag-node text.lineage-dag-node-date {
font-size: 9px;
opacity: 0.72;
fill: var(--text);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Event-date style depends on App.css selector specificity

.lineage-dag-node text.lineage-dag-node-date (0,2,1) outranks .lineage-dag-node text in frontend/src/App.css:527 (0,1,1), so the 9px date wins without !important. Raising the App.css selector's specificity later would silently regress this.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread frontend/src/LineageDag.tsx Outdated

Copy link
Copy Markdown
Contributor Author

@opencode-agent Rapidly stabilize the current Event Lineage DAG without removing or weakening any completed behavior. Preserve parent→child geometry, visible dates, localized legend, non-causal disclosure, exact fused-score evidence, actionable empty state, keyboard interaction, responsive exact-value fallback, Figma frames 5:14/5:15, and Storybook states. First ordinary-restack on the current exact #319 head if the parent advanced; then resolve only valid current-head findings, update docs/product-technical-gap-baseline.md truthfully, and run focused DAG/layout/i18n/accessibility tests plus frontend lint/test/build/Storybook. Publish exact-head receipts and request a fresh independent review. Do not force-push, rewrite history, merge, or treat predecessor evidence as current. Keep auto-merge armed for terminal-success gates.

* fix: align buyer lineage shell with UI UX guide

* docs: track post-merge UI UX review gate

* feat: expose authorized account scope and global search

* fix: place locale control in header top menu

* docs: record central hourly merge loop

* fix: refocus buyer search after board load

* refactor: retire "Buyer" terminology across code and living docs

LineageWeave has no explicit buyer actor -- rename BuyerNav/BuyerDestination
to WorkspaceNav/WorkspaceDestination, .buyer-gnb* CSS to .workspace-gnb*,
the "Buyer navigation" i18n key, the "BUYER EVIDENCE" legend label, and
backend _buyer_evidence_kind/_buyer_evidence_text helpers. Replace prose
referring to "the buyer" with "the reader" in AGENTS.md, ARCHITECTURE.md,
docstrings, living docs, and test descriptions/idempotency keys.

Historical ADRs (0002-0118) and CHANGELOG.md/CHANGELOG.d entries keep their
original wording as a point-in-time record; ADR 0119 documents the rename.
Fixture/table content that uses "buyer" as ordinary sales-note prose is
left untouched.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: translate workspace navigation labels

* fix: make buyer search focus one-shot and localize lineage label

* docs: record authenticated UI UX gap evidence

* fix: use lineage evidence terminology

* fix: keep health probe public and settings route covered

* docs: record current health and lineage coverage gaps

* fix: brand the Keycloak login theme and put post-popup analysis first

- Add a custom Keycloak login theme (docker/keycloak/themes/lineageweave)
  extending keycloak.v2 with the app's Noto Sans / brand-blue tokens,
  white background instead of the stock dark polygon image, and a fixed
  !important-vs-!important override so the realm brand text is legible
  (the parent theme's #kc-header-wrapper white-on-dark color otherwise
  renders white-on-white once the background is swapped to white). Set
  displayName/loginTheme on the realm so "LINEAGEWEAVE-DEMO" no longer
  shows as the raw realm slug.
- Reorder the post-detail popup: the Korean summary and 5W1H now render
  immediately after the title/actions in a two-column grid
  (.popup-analysis-grid, single column below 768px), instead of after
  the full raw post body. The raw body and "Original source state" raw
  codes move below the analysis, next to Event Lineage/Keyman -- they
  are supporting evidence, not the first thing a reader needs to see.
- Fix the phone header (<=768px): .app-header-top-menu no longer clips
  the language/search/logout controls off the right edge of a 390px
  viewport. It now wraps onto additional rows instead, so logout stays
  reachable (SS3.2 requires a clear logout path on every page).

Found via a manual visual E2E walkthrough (login -> board -> post popup
-> workspace destinations -> 390px mobile) driven by Playwright against
the local docker compose stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: localize Event Lineage labels

* fix: complete lineage locale coverage

* fix: remove duplicate lineage locale keys

* test: reproduce phone header padding override

* fix: bound normalized body search indexes

* fix: preserve phone header padding

* docs(gap): track external lineage and calendar integration

* docs: record search and phone UI gap fixes

* docs: anchor merged gap baseline

* docs: record current runtime evidence

* docs: record source mapping boundary

* test: preserve global search focus behavior

* fix: preserve UI guide shell and exact Event Lineage evidence

* fix: preserve metric scripts and replay tenant settings

* fix: keep lineage boundary evidence accessible

* docs: refresh UI UX and runtime gap evidence

* docs: refresh stacked PR gate status

* fix: preserve lineage edge direction markers

* fix: bump minimum control size to the 44px touch-target floor

--size-control-min was 24px, well under the WCAG 2.5.5 / platform
minimum of 44x44px. It backed three icon-only, high-traffic controls
with no other size constraint: the mobile drawer hamburger trigger, the
main post-popup close button (.popup-close, previously sized only by
its font-size glyph with no box), and the mobile drawer close button.
Bump the token once and give the trigger/popup-close buttons the
flex centering needed so the larger hit area doesn't misalign the
glyph.

Found via the ui-ux-pro-max skill's touch-target checklist, cross-
checked against the live app.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* feat: add a skip-to-main-content link for keyboard users

The authenticated shell puts a sticky header top-menu (user info,
language, search, logout) and a five-item GNB before <main>, so a
keyboard user had to Tab through all of it on every page load with no
way to jump straight to content. Add a standard skip link: visually
hidden until focused, translated in all five locales, and explicitly
focuses #main-content on activation rather than relying on native
anchor-fragment focus (inconsistent across browsers, notably Safari,
and not exercised by jsdom in tests).

Found via the ui-ux-pro-max skill's Accessibility checklist (Skip
Links, priority 1). Also fixes a stacking bug caught while verifying
it live: the skip link initially shared --z-header with the sticky
header, so with equal z-index the header (later in DOM order) painted
over it -- focused but invisible. Added a dedicated --z-skip-link
token above every other layer.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* perf: lazy-load and async-decode embedded post images

Posts can carry a dozen-plus embedded base64 images (e.g. the HSWG
technical-diagram post seen during manual walkthrough). None had
loading="lazy" or decoding="async", so opening a popup forced the
browser to synchronously decode every embedded image up front instead
of deferring off-screen ones -- main-thread cost that scales with how
image-heavy a given post is.

Image scaling (max-width:100%/height:auto) and the Noto Sans web font
load (display=swap + preconnect) were already correct; checked both
while auditing this against the ui-ux-pro-max Performance checklist.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* docs: remove stale duplicate gap section reintroduced by a merge

A concurrent session independently resolved the same
feat/lineage-dag-regression <- fix/uiux-standard-guide-v3-postmerge merge
conflict and pushed it first (d0cef47). Their resolution for
docs/product-technical-gap-baseline.md kept both sides of the conflict
instead of dropping the stale one, reintroducing an old, generic
"## 2. LLM Extraction & Knowledge Graph Gaps" / "## 3. General
Architecture Gaps" section -- duplicating the "## 2" heading number and
sitting stale content (Entity Resolution/Searxng, Base64 Image
Omni-modal, DB Architecture, Zotero, PII masking, LLM Orchestration)
ahead of the detailed, current traceability table that already tracks
every one of those same topics with live-corpus evidence.

Verified equivalent (already-pushed tip matches everywhere else this
worktree's merge resolution touched: workspace-gnb naming, the
skip-link CSS, and the mobile lineage-dag-evidence rules are all
present), then reset this local branch to the pushed tip and removed
just the reintroduced stale section, restoring sequential ## numbering.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: enforce canonical database identifiers

* docs: record final local quality evidence

* docs: anchor final exact audit head

* fix: close mobile navigation and preserve search focus

* docs: refresh acceptance evidence after UI fix

* docs: record mobile navigation review fixes

* fix: use inline SVG icons instead of raw glyphs for menu/close buttons

The mobile drawer trigger ("☰"), drawer close ("×"), and main popup
close (PopupCloseButton, "&times;") all rendered raw font glyphs. A
glyph's shape, weight, and baseline vary by OS/browser font stack --
inconsistent with the design system's rendered chrome and a known
anti-pattern (SVG icons, no emoji/glyph icons is a Must-Have per
ui-ux-pro-max's Style Selection checklist). Added two small inline SVG
icons (MenuIcon, CloseIcon; no new dependency -- native SVG covers it)
using currentColor so they inherit each button's existing color token,
and swapped all three usages. aria-labels are unchanged so no
accessible-name regression.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* test: model mobile drawer navigation flow

* docs: anchor drawer acceptance evidence

* feat: resolve source bodies from verified MHTML artifacts

* fix: floor form-control font-size at 16px to stop iOS auto-zoom

The base font scales down at narrower breakpoints (16px -> 15px -> 14px
at 768px, SS2.1.2), and the existing "Base Form & Input Standards"
block (SS3.1.4) inherited that scale verbatim on every input/select/
textarea. Any focused control under 16px makes iOS Safari zoom the
whole viewport -- a well-known WebKit behavior unrelated to the body-
text density tuning that rule exists for. Floor it at
`max(16px, 1em)` so it still respects an explicitly larger font-size
where one is set, but never drops below 16px. The one place with its
own smaller explicit size (.language-switcher select at 0.82rem) gets
the same floor for the same reason -- select focus triggers the same
WebKit zoom.

Found via the ui-ux-pro-max Typography & Color checklist ("Base 16px").

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix: respect prefers-reduced-motion and use transform for skip-link

Nothing in the app respected prefers-reduced-motion: reduce -- every
transition/animation played at full motion regardless of the user's OS
setting, an explicit anti-pattern on the ui-ux-pro-max Animation
checklist. Add the standard global override (near-zero duration
instead of fully removing the transition, so state changes stay
perceptible without the motion itself).

Also switch the skip-link's reveal-on-focus from animating `top`
(layout-triggering) to `transform: translateY()` (compositor-only) --
the same category of fix as the checklist's "animating width/height"
anti-pattern, free to make while touching this rule.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* docs: refresh source mapping acceptance evidence

* fix: clear stale search focus on navigation

* fix: improve post popup action layout

* docs: record navigation focus acceptance

* fix: label chat and ticket inputs

* docs: anchor accessibility acceptance head

* feat: model operational vocabularies in ontology

* fix: preserve popup history navigation

* fix: name lineage DAG node kind for screen readers and tooltips

Root/branch/current status was only conveyed by stroke color and
border width. Add the same legend wording used visually to each
node's aria-label and title tooltip so it reaches screen reader
and colorblind users too.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* docs: refresh product gap evidence baseline

* fix: preserve search focus request boundaries

* fix: add send affordance to ask actions

* docs: anchor baseline to final UI head

* feat: add desktop site map utility

* fix: improve desktop evidence popup layout

* docs: record site map and popup gap evidence

* feat: turn Ask Agent into an evidence conversation

---------

Co-authored-by: Claude Sonnet 5 <[email protected]>
@seonghobae
seonghobae merged commit aaad6f1 into feat/analysis-run-name-evidence-lineage Aug 21, 2026
3 of 4 checks passed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Open in Devin Review

Comment thread frontend/src/App.tsx
Comment on lines +4883 to +4906
<WorkspaceNav destination={destination} onChange={changeDestination} />
{mobileMenuOpen ? (
<div className="mobile-drawer-backdrop" onClick={() => setMobileMenuOpen(false)}>
<aside
className="mobile-drawer"
onClick={(event) => event.stopPropagation()}
>
<button
type="button"
className="mobile-drawer-close"
aria-label={t("Close")}
onClick={() => setMobileMenuOpen(false)}
>
<CloseIcon />
</button>
<WorkspaceNav
id="mobile-workspace-navigation"
destination={destination}
onChange={changeDestination}
drawer
/>
</aside>
</div>
) : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Duplicate navigation landmarks share one accessible name

WorkspaceNav renders with aria-label "Workspace navigation" in the header, again inside the mobile drawer, and again inside SiteMapUtility. When the drawer or site map is open, two navigation landmarks carry the same name, which screen-reader landmark navigation cannot distinguish.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread frontend/src/App.tsx
Comment on lines 3751 to 3753
function closeSelectedPost() {
setSelectedPostId(null);
setOpenedAfterCutoff(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Asymmetric popup history: push on open, replace on close

selectPost opens a post with pushState (App.tsx:3724) but closeSelectedPost uses replaceState (App.tsx:3759) instead of navigating back. Opening then closing via the button rewrites the pushed entry rather than popping it, so the button-close and back-button-close paths yield different history stacks. Behavior stays correct in traced flows but the asymmetry can surprise later work on back navigation.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread lineageweave/ontology.py
return {}
fields = {"ontology_iri": str(subject)}
label = ONTOLOGY.value(subject, RDFS.label)
label = ONTOLOGY.value(subject, RDFS.label) or ONTOLOGY.value(subject, SKOS.prefLabel)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Ontology annotation now resolves operational codes

ontology_annotations and iri_for_lookup_code now resolve codes like public, voc, open to SKOS concepts, where they previously returned empty/None. Current callers pass only node/edge/actor codes, so no payload changes today, but any future caller spreading annotations for an operational lookup code will start emitting an ontology_iri/ontology_label.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant