fix(ui-ux): grow citation chip to a 24px touch target - #556
Merged
seonghobae merged 5 commits intoAug 24, 2026
Conversation
.citation-chip rendered ~19px tall on mobile (0.1rem block padding via --space-chip-block, no min-height), below the 24px minimum touch target. Add min-height: var(--size-control-min), matching the convention already used by .language-switcher select and .lineage-entity-picker select. display: inline-flex + align-items: center keep the button's text vertically centered in the taller box, since (unlike the label-based board-option rules) this is a bare <button>. Pins the fix with a regression test in tokens.test.ts that extracts the .citation-chip rule from App.css and asserts min-height/inline-flex are present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt
|
Warning Review limit reachedNext included review available in 53 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
seonghobae
enabled auto-merge
August 24, 2026 01:23
- The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing.
# Conflicts: # frontend/src/App.tsx
seonghobae
added a commit
that referenced
this pull request
Aug 24, 2026
…ADR 0200 point 5, no activation) (#586) * fix: detach provider parse error context (#393) * fix: detach provider parse error context * fix(frontend): restore authenticated admin boundary * fix(frontend): use safe OIDC return URL * fix: close provider transport error boundary * fix: hide raw TEPP transport failures * fix: restore exception chaining at HTTP transport/parse boundary ADR 0123 keeps the original exception as an in-process chained cause for operator logging while the buyer-facing message stays generic. The transport_error/contentless-raise idiom in _request, _decode_json, and configured_tepp_client's transport() dropped both __cause__ and __context__ on every failure mode (TLS errors, DNS failures, timeouts, malformed JSON), erasing the diagnostic trail an operator needs from server-side logs. Restore `raise ... from exc` at all three sites; the exposed message text is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5M79L945DMyMs3sg5yJ14 * docs: remove nonexistent provider-boundary ADR citations --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * feat: persist leftover-map axis share on period reports (v2.12.16) (#519) Gabriel inertia of residual SVD axes 1 and 2 (σ²/Σσ²) is a report-level 3NF slice next to leftover pairs (ADR 0148). Rank-0 residuals emit two zero-share axes. Do not invent a leftover score. Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat(ask): move Global Ask behind a durable Valkey job queue (#572) * feat(ask): move Global Ask behind a durable Valkey job queue A live Ask answer is a multi-minute orchestrator LLM round-trip under shared-gateway load (158 s measured); serving it inside one blocking HTTP request pinned connections and timed out every client. POST /api/ask now persists a global_ask_job row (migration 0165), wakes an in-process worker over the global_ask_request_stream Valkey stream (the post_content_queue idiom: durable row is truth, stream entry is a wake-up, queued rows republish after 60 s), and returns 202 with a job id. GET /api/ask/jobs/{id} is owner-scoped (404 hides existence) and returns the settled answer payload. The worker reloads the account's ABAC visibility at processing time so revocations between submit and processing are honored. The frontend submits then polls with an unchanged askAgent signature; e2e deadlines now match the async reality and the temporal e2e question targets the seeded 2026-01 window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * docs: docstring for the worker-side ABAC predicate The repo's docstring-coverage gate (tests/test_public_docstrings.py) covers nested defs too; the can_see predicate in compute_global_ask_answer was the one uncovered definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): bounded-concurrency worker, per-job deadline, orphan recovery The live e2e run surfaced two queue defects the unit tests could not: the consumer awaited each job inline, so one slow answer head-of-line blocked every question behind it (observed: a 17-minute job starving a queued one), and a hung orchestrator round-trip kept a job running indefinitely with no recovery for rows orphaned by a mid-job crash. The worker now dispatches jobs as concurrent tasks behind a 4-slot semaphore (the queued->running claim already makes duplicate wake-ups no-ops, so at-least-once semantics hold), every job runs under a 600 s asyncio.wait_for deadline that settles it as failed, and the recovery sweep re-queues running rows older than deadline+300 s — which a live worker can never legitimately produce. e2e deadlines are sized to the measured contended settle (480 s) plus setup slack for a cold, loaded host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): size the answer client timeout to the job deadline The orchestrator was observed completing an answer and hitting BrokenPipe writing it back: the chat client's 180 s default socket timeout hung up first, discarding an answer already paid for and failing the job. The Ask client now uses ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS (default 570 s) — just under the worker's 600 s job deadline, so the client ends a slow call before the reaper does and a generated answer has the whole deadline to arrive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * test(e2e): size the answer deadline to the 570 s client timeout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): restate the resolved relative-time window in the prompt Retrieval already scopes sources to the resolved window ('7개월 전' → 2026-01-01..2026-01-31), but the numbered sources carry no dates, so the model answered the temporal e2e question with 'no date information' and cited nothing (observed live: 4 in-window sources, zero citations). The worker now appends the resolved window to the question and states that every provided source falls inside it, grounding the answer in the evidence it was given. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * feat(seed): warm post-content ingestion for seeded posts Seeded posts are inserted straight into Postgres, but post-content extraction (units, embeddings, embedded images) only enqueues when a post's /content endpoint is first served — so a fresh stack had an empty image pipeline and image citation had nothing to cite until a human happened to open the right post. The seed now opens each seeded post once through the API with the demo reader account, replaying the exact production enqueue path instead of duplicating its SQL. Verified live: the seeded TIFF was extracted and vision-captioned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): anchor today's date when restating a resolved time window The first grounding clause named only the resolved window, and the model read that window as the reference point ('now') — re-subtracting the offset and answering that '7개월 전' meant mid-2025. The clause now states today's date and equates the relative expression to the window outright, then directs the answer at the provided in-window sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * test(e2e): ask questions the seeded corpus can answer decisively 'What happened between these events?' has no referent and 'Which project?' names nothing — a careful model correctly declines both and cites nothing, so the suite depended on a bluffing model to pass. The questions now target seeded content directly (the Westfield Power specification thread, the synthetic raster evidence post, Ada West's initial site visit), so a model answering from the sources must cite them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): settle a job on any failure, not a named few An exception outside the settlement handler's tuple (first exercised live when citation assembly ran against real image rows) killed the worker task silently and stranded the row running until orphan recovery re-queued it 15 minutes later. Settlement is fail-closed now: any exception settles the job failed; CancelledError still propagates so shutdown leaves the row for recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): single-literal SQL statements for the Semgrep asyncpg rule The asyncpg-sqli rule flags adjacent-literal SQL concatenation as non-literal string building even with fully parameterized values (two blocking findings on the recovery sweep). Every statement in the module is now one triple-quoted literal, so there is no concatenation for the rule to misread and nothing to suppress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): express recovery windows without make_interval The Semgrep asyncpg-sqli rule still flagged the recovery statements after the single-literal rewrite; the trigger is the make_interval(secs => $n) call syntax, which its matcher reads as non-literal string building. interval '1 second' * $n is the same parameterized arithmetic without the construct the rule misparses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): suppress the asyncpg-sqli false positive on recovery SQL Both recovery statements are fully parameterized; the rule misreads the literal-plus-constant-arguments shape regardless of literal style or interval syntax (verified across three rewrites). Suppressed with the same annotation the repo already carries in report_ingestion.py and customer_hint_ingestion.py for this exact rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): apply PR-572 review batch across queue, config, seed, e2e - Bound the wake-up stream (maxlen 1000, approximate) at both XADD sites and swallow a failed wake-up publish in enqueue: the committed row is truth and the sweep republishes within a minute, so the caller keeps its pollable job id instead of a 500. - Share the 600 s job deadline through config (GLOBAL_ASK_JOB_DEADLINE_SECONDS) and validate ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS as finite and strictly inside (0, deadline) so the client-hangs-up-first ordering cannot be misconfigured away. - Tighten orphan recovery to deadline+60 s so a crashed worker's job returns to the queue while a reader is still polling; raise the frontend poll ceiling to 15 min to cover queue wait plus deadline. - Resolve one Seoul reference date per job and use it for both retrieval and the grounded prompt (midnight-boundary consistency); gather_global_chat_sources accepts the pinned date. - Give only the Ask worker the long answer timeout; the synchronous per-post chat endpoint keeps the interactive client default. - Harden the worker loop: one failed consume/recovery round logs and retries instead of silently ending Ask consumption. - e2e computes the months-ago offset from the real clock so the temporal question keeps resolving onto the seeded 2026-01 window in any month. - Seed waits for backend /healthz before warming, and closes its psycopg2 connection explicitly. - Drop imports left dead by the Ask logic move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore: remove one-shot patch scripts left from PR #347 development (#575) add_translations.py, patch_api.py, patch_app_fetch.py, patch_app_order.py, and patch_main.py were single-use edit scripts that already applied their changes; nothing in the Makefile, CI, or runtime references them, and leaving dead top-level scripts invites someone to run them twice. Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix: bootstrap repo-root sys.path in the two operator scripts missing it (#570) backfill_post_keymen.py and requeue_failed_post_content.py were the only scripts/ entries without the repo-root sys.path insert their siblings use, so `python scripts/<name>.py` failed with ModuleNotFoundError: backend unless a caller manually exported PYTHONPATH=. first. Match the existing pattern instead of documenting the workaround. Found while re-running backfill_post_keymen.py to refresh a stale reason_no_live_client role row now that the local orchestrator/searxng services are reachable. * fix(ui-ux): size secondary details/summary toggles to --size-control-min (#560) * fix(ui-ux): size secondary details/summary toggles to --size-control-min Several disclosure toggles (advanced review tools, evidence-operation actions, semantic provenance, source-author hint context) rendered with a browser-default <summary> well under the project's 24px --size-control-min touch target, the same convention already used by .language-switcher select and .lineage-entity-picker select. - Add a shared App.css rule giving .advanced-review-tools, .semantic-provenance, .operator-action-tools, .keyman-source-context, and the new .hint-disclosure class an inline-flex summary with min-height: var(--size-control-min) and horizontal padding, so the hit target isn't text-width-only. - .operator-action-tools and .keyman-source-context previously had zero CSS rules at all -- not just a missing min-height. - The three bare <details> "Related posts"/"Hint only" expanders in CustomerMasterPanel had no className, so there was no selector able to reach them; give them className="hint-disclosure". Tests: frontend/src/styles/tokens.test.ts pins the new selector list and asserts the rule uses var(--size-control-min) plus horizontal padding, not a bespoke value. frontend/src/App.test.tsx adds a render test asserting the three previously-bare <details> carry the .hint-disclosure class. Both were confirmed to fail before this change and pass after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(ui-ux): keep disclosure markers on secondary summary toggles display: inline-flex replaced the summary list-item box, which stops browsers from rendering the built-in expand/collapse marker. Reach the --size-control-min touch target with min-height and padding instead so the native affordance stays visible (devin review thread). * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix(ui-ux): expose bare loading text as live regions (#558) * fix(a11y): expose bare loading text as live regions Async loading placeholders in App.tsx and FiveW1H.tsx rendered as plain <p> text with no role/aria-live, so assistive tech never announced the transition from "Loading..." to the resolved content. This included the app-root auth gate every session passes through, and the post-detail popup's loading paragraph. Add role="status" to every bare loading <p> (19 in App.tsx, 1 in FiveW1H.tsx), matching the role="status" pattern already used elsewhere in the same files (e.g. "Loading posts...", SourceResearchPanel). No behavior change beyond the attribute. Tests: add an RTL assertion that the auth-loading gate and the post-detail popup's loading state are exposed via getByRole("status") rather than only getByText, extending stubBackend with a deferPostOne option to deterministically observe the popup's pre-resolution state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix(ui-ux): grow citation chip to a 24px touch target (#556) * fix(ui-ux): grow citation chip to a 24px touch target .citation-chip rendered ~19px tall on mobile (0.1rem block padding via --space-chip-block, no min-height), below the 24px minimum touch target. Add min-height: var(--size-control-min), matching the convention already used by .language-switcher select and .lineage-entity-picker select. display: inline-flex + align-items: center keep the button's text vertically centered in the taller box, since (unlike the label-based board-option rules) this is a bare <button>. Pins the fix with a regression test in tokens.test.ts that extracts the .citation-chip rule from App.css and asserts min-height/inline-flex are present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix: shorten the orchestrator healthcheck's retry budget (#547) * fix: shorten the orchestrator healthcheck's retry budget 10 retries at a 5s interval matches every other service's healthcheck (postgres, valkey, searxng all use retries: 10). The orchestrator was the one outlier at 20 retries -- a 100s startup grace period for a liveness-only check (does no real work beyond confirming the process is up) versus 50s everywhere else, with no documented reason for the gap. * fix(compose): give the orchestrator healthcheck a 50s warm-up window retries: 10 alone halves cold-boot grace to ~50s, which could fail the backend depends_on gate on slow hosts. start_period: 50s keeps failures during warm-up outside the retry budget: a booting orchestrator still gets ~100s total while a genuinely dead one trips the gate in ~50s (devin review thread). * docs(compose): correct the healthcheck warm-up timing comment start_period: 50s + retries: 10 x 5s means a dead service trips at ~100s total (not ~50s): identical to the old retries: 20 budget. The actual win is that a booting orchestrator no longer consumes retry budget during warm-up (devin review thread). --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat: name leftover-map unexplained leftover on leftover pairs (v2.12.26) (#535) * feat: name leftover-map unexplained leftover on leftover pairs (v2.12.26) Persist unexplained leftover U = R − R̂ after two-axis Gabriel reconstruction so a leftover residual is not read as the leftover the map does not reconstruct. R̂ stays internal and is not persisted. Rank-0 maps store U = 0; fallback pairs omit U. After make seed, closest and farthest leftover pairs sit above the member list with U next to leftover-map distance d; click opens that post. * fix(frontend): remember OIDC return URL and drop unreachable AdminPanel render Login screen built returnUrl via raw string concatenation instead of returnUrlFromLocation()/rememberOidcReturnUrl(), leaving both imports unused (TS6192) and rendering an AdminPanel with accessToken: undefined (TS2322) on a branch guarded by !auth.isAuthenticated. --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * feat: bind corroborated SKOS org aliases to one catalog row (#480) * docs: restore non-identifying gap baseline * fix(frontend): restore authenticated admin boundary * feat: bind corroborated SKOS org aliases to one catalog row Expand corporate_entity candidates with search-verified alt/pref labels so synthetic AGP and Aurora Grid Power mentions reuse one row (ADR 0120). Ties and uncorroborated pairs stay unbound. No real organization names. * docs: correct live Strix failure count * fix: preserve raw organization ties before aliases * docs: clarify alias resolution order * fix: recheck organization evidence under lock * fix: require exact SKOS alias matches * fix: exclude full inferred ancestor path * docs: align ancestor exclusion contract * docs(adr): renumber SKOS alias-binding decision to avoid 0120 collision PR #490 already claims ADR 0120 for an unrelated decision (two-word database identifiers). Renumber this PR's ADR to 0158, which is currently free across origin/main and the open PRs checked, and update all cross-references (ARCHITECTURE.md, CHANGELOG.md, CHANGELOG.d, ADR 0008, ADR 0012, and the two backend docstrings). * docs: resolve SKOS ADR number collision * feat(estimation): queued llm pair judging via durable batch routing (ADR 0200 point 5, no activation) The llm channel joins the estimate without a single bulk synchronous provider call: 'submit' samples pairs exactly as the deterministic estimator does, takes a bounded deterministic stride subsample, submits ONE contextual-orchestrator batch routing job (one request per pair, caller-supplied custom_id=pair-<ordinal> -- the id round-trip landed upstream as contextual-orchestrator #832; #829's Valkey registry makes the job survive orchestrator restarts) and persists the run plus every pair's deterministic scores into migration 0201's ledger, never waiting. 'collect' polls once, maps returned scores to pairs by custom_id only (never result order), persists each llm score durably as it lands -- a killed collect loses nothing -- and fits the 4-channel expected-information estimate only over a complete run, persisting it as channel_set_with_llm with full provenance. A refused fit marks the run run_failed and writes no weights. The adjudication prompt and confidence parsing move to shared helpers (judge_prompt / parse_confidence) so the live client and the queued scorer ask the byte-identical question. sample_pair_scores returns pair labels again and the deterministic stride subsample returns for the bounded llm pass. Migration 0201 live-validated (replay-idempotent, judged-at/llm-score consistency check enforced, rollback round-trip). Activation remains zero: the loader's authorized anchor set is empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J * feat(operability): structured server diagnostics behind the generic 503 (#577) * feat(operability): structured server diagnostics behind the generic 503 Global Ask hid every failure behind a stable 503 (correct customer boundary) but the cause never reached structured telemetry, and the f-string leaked raw exception text to callers. Split the ask handler into three classified paths, all returning the same generic 503: - HttpClientError/OSError -> known provider/transport fault; warning event orchestrator_provider_unavailable with operation code, correlation id, exception class; message deliberately not logged. - KeyError/ValueError -> evidence-object contract break; error event orchestrator_internal_fault with stack trace attached. - broad Exception -> unexpected defect; same internal-fault diagnostic so a programming regression cannot degrade into an opaque availability incident. Chaining is preserved on every path. backend/app/operability.py documents the forbidden-field contract (no prompt text, model output, bearer tokens, provider keys, tenant PII, or post bodies in any record); alerting keys on event_type so pager load separates provider-down from our-bug (issue #361). Unit tests cover both event shapes, uniqueness of correlation ids, and the forbidden- field guarantee without needing a live stack. * feat(ontology): deterministic legacy-namespace migration tooling ADR 0157 keeps the lowercase ontology namespace canonical and demoted the repository-case spelling to deprecated compatibility status, but rows written before the decision can still carry legacy IRIs in post_project_mention.ontology_iri -- and RDF consumers treat the two spellings as different resources. scripts/migrate_legacy_namespace.py scans, prints every planned rewrite, refuses unrecognized namespaces (fail closed rather than bulk-mangle a third spelling), and only writes under --apply inside one transaction guarded by the exact old IRI so a concurrent edit aborts instead of double-applying. Provenance columns (extraction method, confidence, evidence) are never touched per ADR 0157's do-not-silently-rewrite rule. Dry run is the default. 8 unit tests cover canonicalize mapping, dry-run reporting, selective apply, fail-closed behavior, and the clean-database no-op. * fix(operability): redact exception messages from internal-fault tracebacks Python renders a traceback's final line as 'ExceptionType: message', so exc_info=exc violated the module's own forbidden-field contract: parsing exceptions from orchestrator responses can embed provider payload or prompt fragments, and those landed in the log. Re-emit through a _MessageRedacted carrier that keeps the original __traceback__ (the raise-site frames stay diagnosable) while its text is a fixed redaction notice; the real class name is retained in the structured exception_class field. Also unify the three /api/ask 503 detail strings into one generic message so callers cannot probe which internal classifier fired; the provider-vs-contract-vs-defect distinction lives only in server-side event_type (devin/coderabbit review threads on PR #577). Drop the docstring's nonexistent include_message option. --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat: add bounded ontology and provenance explorer (v2.13.0) (#349) * feat: add bounded ontology and provenance explorer GET /api/ontology/neighborhood returns a typed Post/Person/CorporateEntity/Team neighborhood with SKOS broader distinct from OWL subclass, truth-status vocabulary, knowledge-cutoff binding, and no hidden-count side channels. Buyers inspect it from the Keyman panel, not a second GNB destination (ADR 0119 / #341). * fix: keep ontology JSON-LD exports filtered * fix: validate ontology neighborhood request bounds * fix: escape ontology CSV formulas * fix: make ontology layout ordering deterministic * fix: make ontology explorer controls accessible * fix: translate ontology post node labels * fix: deduplicate ontology node evidence * fix: drop unlabeled ontology edges * fix: harden typed ontology neighborhood boundaries * fix: bound ontology fact loading by requested edge limit * fix: complete ontology explorer verification coverage * fix: hide unauthorized ontology parents * test: reproduce missing Storybook viewport addon * fix: declare Storybook viewport addon * fix: register Storybook viewport addon * fix: use Storybook 10 built-in viewport feature * fix: keep Storybook 10 essentials zero-config * fix: declare Storybook built-in viewport options * test: lock Storybook 10 built-in viewport contract * fix: bind narrow ontology story to mobile viewport * test: reproduce cutoff and uppercase focus defects * fix: apply ontology cutoff before SQL limit * test: require localized ontology stabilization copy * feat: localize ontology paging and provenance guidance * test: reproduce ontology paging and provenance UX defects * fix: stabilize ontology paging and provenance UX * test: reproduce bounded window and disconnected first page * fix: preserve proximity and source truncation truth * fix: expose bounded ontology source windows * fix: keep ontology loader test doubles compatible * test: reproduce duplicate ontology focus rule * test: reproduce endpoint N+1 and shallow evidence expansion * test: require a next action for hard ontology bounds * test: require localized hard-bound guidance * feat: localize hard ontology query-bound guidance * fix: validate provider chat response envelopes * fix: close remaining provider error leaks * fix: normalize summary enrichment failures * fix: normalize unexpected provider failures * fix: keep TEPP and worker errors provider-safe * fix: bound and batch ontology neighborhood evidence * fix: preserve ontology visibility query compatibility * fix: restore ontology explorer next actions * fix: honor ontology cutoff and canonical focus UUIDs * fix: read ontology visibility records safely * fix: preserve provider parse failures and truncation * fix: preserve ontology explorer session and parent visibility Clear a loaded neighborhood when the session token is removed, hide live refocus on static catalog snapshots, look up SKOS parent visibility independently of the child, and fail closed on dangling exact-value endpoints. OWL-Time is cited as a W3C Candidate Recommendation Draft. * fix: authorize final ontology endpoints * fix: preserve ontology evidence and stale diagnostics * fix: make tenant settings migration replayable * fix: harden ontology explorer paging and login return * docs: sanitize baseline and record ontology gates * fix: align provider changelog release * test: assert ontology fact query limit argument * feat: continue ontology neighborhoods with an opaque source cursor (v2.14.0) (#369) * feat: continue ontology neighborhoods with an opaque source cursor Add a versioned HMAC source-window cursor so authorized relations beyond the bounded SQL window can be paged with keyset continuation, not OFFSET. Tamper, scope, version, expiry, and snapshot drift fail closed. A missing process secret keeps truncated-without-cursor. The explorer accumulates pages without losing selected evidence (ADR 0124 / #363). * fix: retain paged ontology relations * fix: reuse expanded ontology window * fix: seal ontology continuation windows * docs: record ontology continuation safeguards * test: assert continuation cursor anchor * fix: deduplicate ontology source-window edges * fix: preserve ontology source cursor ordering * Fix ontology cursor after derived edge overflow * fix(ontology): remove unused subclass IRI constant * fix(frontend): preserve ontology pages on continuation failure * test(frontend): retain ontology selection after page failure * test(frontend): align rejected ontology fixture rows * fix: make ontology page retries effective * fix(security): replace custom source cursor cipher * fix(security): keep aggregate SQL immutable * test(security): preserve reviewed SQL contract * fix(ontology): keep paging safe and stable * fix: conceal ontology error details * fix: distinguish full ontology page from truncation * test: use one ontology ingestion import style * fix(ontology-neighborhood): trim by BFS distance, not key string assemble_ontology_neighborhood() trimmed nodes over maximum_nodes by sorting the "node_type_code:node_id" key lexicographically instead of by BFS/source-hop distance from the focus. Since "node_corporate_entity" sorts before "node_post", farther nodes of an alphabetically-earlier type survived while closer nodes of a later type were dropped, even though truncated stayed true and evidence_count was still correct for the wrong node set. Sort the trim candidates by the same `reached` distance already computed during BFS (and via source_hop_depth in the source-window path), using the node key only as a tiebreaker. Adds a regression test covering two node types straddling the maximum_nodes boundary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5M79L945DMyMs3sg5yJ14 * fix: allocate migration 0175 for ontology truth * fix: restore ontology snapshot on reset * fix: resolve silent ADR-0119 number collision from main merge main independently claimed ADR 0119 for the leftover-map two-axis distance change while this PR's own ADR 0119 (ontology/provenance explorer) merged in cleanly as a separate file, since git diffs by path and both files kept distinct names. Renumber this PR's ADR to 0168 (next free number after main's 0167) and update every cross-reference (CHANGELOG, AGENTS.md, ARCHITECTURE.md, the 0175 migration comment, ontology_neighborhood.py/test/ingestion docstrings, ADR 0124's predecessor link, and the ADR/storybook README index). Main's own ADR 0119 file and its references are untouched. * fix: document the source-window page sort key closure tests/test_public_docstrings.py's repository-wide public-docstring contract walks every non-underscore-prefixed def, including nested closures (matching flush/transport/stale_fallback elsewhere in this codebase). source_page_sort_key was missing its docstring; add one describing the cursor-key-first, hop/property/node-tuple-fallback ordering it implements. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix: strip Keycloak OIDC callback params from the post share link (#576) * fix: strip Keycloak OIDC callback params from the post share link PostDetailPopup built its permanent link from window.location.href verbatim, so sharing right after (or during) a Keycloak sign-in redirect copied the raw code/state/session_state/iss query params along with the post link instead of a clean permalink. Add stripOidcCallbackParams (oidcReturnUrl.ts) and use it before appending ?post=<id>, with unit tests covering both the strip and the no-op case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUcvhvimNVGqjFuD9f1fUa * fix(oidc): strip callback params from restored return URLs too stripOidcCallbackParams only cleaned share links, but restoreOidcReturnUrl falls back to returnUrlFromLocation() on the post-redirect location -- which still carries code/state/session_state/iss. Strip them at build time so no consumer of this module re-mints a URL with a one-time authorization code in it; regression test covers the redirect-shaped search string (devin review thread). --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix(ui-ux): give Event Lineage DAG node marks a 24x24px hit target (#554) * fix(ui-ux): give Event Lineage DAG node marks a 24x24px hit target The visible node mark in LineageDag.tsx is a 7px-radius (14px) SVG circle with no separate hit area, well under the WCAG 2.2 SC 2.5.8 AA minimum of 24x24 CSS px and this codebase's own --size-control-min token (styles/tokens.css). Add a transparent, pointer-events:all circle (r=12, matching --size-control-min at this DAG's ~1 user-unit-per-px scale) as the first child inside the existing role="button" <g>, ahead of the visible mark, so it enlarges the click/tap area without changing appearance. ROW_H is a 52px row pitch (lineageLayout.ts), so a 24px-diameter hit circle leaves 28px of clearance between adjacent rows -- no overlap. Adds LineageDag.test.tsx asserting the hit circle is present, sized >=24px diameter, transparent, and painted before the visible mark, plus a click-still-works regression check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(ui-ux): keep the DAG hit circle invisible under the author CSS fill rule The .lineage-dag-node circle rule paints every child circle with --surface-muted plus a border stroke, and author CSS overrides the fill="transparent" presentation attribute, so the enlarged hit target rendered as a second opaque disc. Scope an explicit transparent/no-stroke override to .lineage-dag-hit (devin review thread). * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. * fix(ui-ux): keep focus/hover/current rings off the DAG hit target The aria-current ring rule (0,2,1) outranked the hit-circle override (0,2,0) and painted a large hollow ring around the current node's mark. Exclude .lineage-dag-hit from the focus/hover/current selectors via :not(), which never matches the hit circle at all, and lift the override to (0,3,0) so future circle-scoped emphasis rules lose by specificity rather than source order (devin review thread). * fix(ui-ux): exclude the DAG hit target from focus/hover/current rings Equal-specificity source-order was still letting the aria-current ring paint over the hit circle. :not(.lineage-dag-hit) on the emphasis selectors never matches the hit target, closing the ring regression for good. * fix(ui-ux): pin the DAG svg to its user-unit width width="100%" plus a viewBox let the container scale the coordinate system, shrinking the effective 24px hit target below WCAG 2.5.8 on wide containers. Render at the layout's own user-unit width (height already behaves this way) so one SVG unit stays ~1 CSS px and the hit radius keeps its intended size; horizontal overflow is handled by the panel's existing scroll container. * test(red): require scrollable lineage DAG viewport * test(red): require mobile lineage scroll guidance * fix(ui-ux): keep lineage DAG reachable in narrow viewports * fix(ui-ux): style keyboard-scrollable lineage viewport * test(storybook): cover mobile lineage scrolling * docs(storybook): record mobile lineage scroll state --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix(review): connection-free fit in collect; errored judgments stay unjudged Devin findings on #586: (1) _collect held its database connection open across the model fit -- the exact idle-reap failure class #571 fixed -- now every phase (run lookup, ledger update, fit, persist) opens its own short-lived connection and the fit runs with none. (2) An empty or non-numeric batch answer parsed as a confident 0.0; the new parse_confidence_or_none keeps the live client's semantics while the queued scorer omits unparseable answers so an errored request stays unjudged (pure helper judgment_updates_from_results, tested). Also: collect gains --run-id for stale earlier runs, and the rebase onto the updated ADR branch adopts main's chat_completion_content safe extractor inside the shared-helper judge(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com>
seonghobae
added a commit
that referenced
this pull request
Aug 24, 2026
… two active lines (#574) * docs(adr): propose ADR 0200 reconciling channel-weight measurement across lines The two active lines carry ADR 0145 with opposite decisions (estimation active + constants deleted vs. rejection + constants retained), on divergent lineage_channel_weight schemas. ADR 0200 keeps the operator directive (no hand-picked weight anywhere), upgrades the estimator to expected-information weighting over the fitted latent distribution (answering the theta-conditionality critique), adds an anchor-honesty label plus a TEPP criterion-validity gate (answering the criterion-validity critique, amending ADR 0003 explicitly), merges the two schemas as a union, and moves llm pair scoring to the durable Valkey queue idiom per the operator's no-bulk-synchronous-LLM directive and issue #289. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J * feat(estimation): expected-information channel weights (ADR 0200 point 2, no activation) Replaces main's unconditional-refusal estimation stub with the full estimator ADR 0200 specifies: MLS2PLM over dichotomized channel scores with multilevel cluster intercepts, weights = normalized EXPECTED item information over the fitted latent distribution, computed on the fitted person parameters with the package's own predict_proba -- answering the theta-conditionality critique in main's ADR 0145 rejection instead of ignoring it. Non-converged fits are rejected outright (convergence_status must be 'converged'). Method code: mls2plm_expected_information. NOTHING ACTIVATES: the product loader's authorized anchor set remains empty, so persisted-weight activation still fails closed exactly as main's ADR 0145 requires -- this stage lands the estimator and its parameter-recovery evidence only. The demo generative design's follow probabilities are re-declared (0.80/0.72/0.66) so the fixture estimate preserves the designed A-100 demo fork under expected-information weighting; the estimate over that design converges and recovers the declared ordering. Supersedes main's test_unanchored_channel_scores_never_run_a_fit by design: running the fit is now permitted, activation is not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J * feat(estimation): schema union + provenance persistence (ADR 0200 point 4, no activation) Migration 0200 unions the two lines' lineage_channel_weight schemas: primary key (channel_set_code, channel_code) -- one persisted set per active-channel combination -- carrying main's full per-run provenance contract and integrity constraints. Validated live against BOTH predecessor shapes (main's 0135 provenance table and the customer-master line's 0135+0136 set table), replay-idempotent per the ADR 0166 window, with a rollback that restores main's single-set shape. Pre-provenance rows are deleted, not backfilled with invented provenance -- the loader refuses them either way and re-estimation is the operator's next action. The loader becomes set-aware (exact active-channel match of exactly one set) while keeping every provenance-integrity gate; a pre-0200 schema is probed via the catalog (never a failing statement that would abort the caller's transaction) and read as the single implicit deterministic set. ACTIVATION IS UNCHANGED: the authorized anchor set stays empty, so every loaded vector is still refused. The operator script becomes the full estimator front-end: fetch on one short-lived connection, no connection held while fitting, persist with full provenance (fresh run uuid, installed estimator version, honest anchor_method_code=unanchored_internal_structure, reproducible source-snapshot digest over the ordered sampled rows, knowledge cutoff = max sampled created_at) on a fresh connection. Its report names activation as blocked until an anchor is authorized. The llm channel is deliberately absent -- bulk synchronous provider calls are banned; llm scoring arrives with the queued worker (ADR 0200 point 5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J * fix(estimation): raise mmle max_iter so the CPU fallback actually converges fast-mlsirm's FitConfig default (max_iter=1000) is tuned against its GPU/f32 kernel; the f64 CPU fallback -- the only path available on any CI runner, since none expose a GPU -- needs materially more EM iterations to reach the same optimum at full precision (observed up to ~1850 on this module's own recovery fixture). Confirmed locally by forcing rust_device="cpu": both channel-weight recovery tests reliably hit max_iter_reached at the old default and reliably converge at 3000. * feat(estimation): queued llm pair judging via durable batch routing (ADR 0200 point 5, no activation) (#586) * fix: detach provider parse error context (#393) * fix: detach provider parse error context * fix(frontend): restore authenticated admin boundary * fix(frontend): use safe OIDC return URL * fix: close provider transport error boundary * fix: hide raw TEPP transport failures * fix: restore exception chaining at HTTP transport/parse boundary ADR 0123 keeps the original exception as an in-process chained cause for operator logging while the buyer-facing message stays generic. The transport_error/contentless-raise idiom in _request, _decode_json, and configured_tepp_client's transport() dropped both __cause__ and __context__ on every failure mode (TLS errors, DNS failures, timeouts, malformed JSON), erasing the diagnostic trail an operator needs from server-side logs. Restore `raise ... from exc` at all three sites; the exposed message text is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5M79L945DMyMs3sg5yJ14 * docs: remove nonexistent provider-boundary ADR citations --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * feat: persist leftover-map axis share on period reports (v2.12.16) (#519) Gabriel inertia of residual SVD axes 1 and 2 (σ²/Σσ²) is a report-level 3NF slice next to leftover pairs (ADR 0148). Rank-0 residuals emit two zero-share axes. Do not invent a leftover score. Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat(ask): move Global Ask behind a durable Valkey job queue (#572) * feat(ask): move Global Ask behind a durable Valkey job queue A live Ask answer is a multi-minute orchestrator LLM round-trip under shared-gateway load (158 s measured); serving it inside one blocking HTTP request pinned connections and timed out every client. POST /api/ask now persists a global_ask_job row (migration 0165), wakes an in-process worker over the global_ask_request_stream Valkey stream (the post_content_queue idiom: durable row is truth, stream entry is a wake-up, queued rows republish after 60 s), and returns 202 with a job id. GET /api/ask/jobs/{id} is owner-scoped (404 hides existence) and returns the settled answer payload. The worker reloads the account's ABAC visibility at processing time so revocations between submit and processing are honored. The frontend submits then polls with an unchanged askAgent signature; e2e deadlines now match the async reality and the temporal e2e question targets the seeded 2026-01 window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * docs: docstring for the worker-side ABAC predicate The repo's docstring-coverage gate (tests/test_public_docstrings.py) covers nested defs too; the can_see predicate in compute_global_ask_answer was the one uncovered definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): bounded-concurrency worker, per-job deadline, orphan recovery The live e2e run surfaced two queue defects the unit tests could not: the consumer awaited each job inline, so one slow answer head-of-line blocked every question behind it (observed: a 17-minute job starving a queued one), and a hung orchestrator round-trip kept a job running indefinitely with no recovery for rows orphaned by a mid-job crash. The worker now dispatches jobs as concurrent tasks behind a 4-slot semaphore (the queued->running claim already makes duplicate wake-ups no-ops, so at-least-once semantics hold), every job runs under a 600 s asyncio.wait_for deadline that settles it as failed, and the recovery sweep re-queues running rows older than deadline+300 s — which a live worker can never legitimately produce. e2e deadlines are sized to the measured contended settle (480 s) plus setup slack for a cold, loaded host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): size the answer client timeout to the job deadline The orchestrator was observed completing an answer and hitting BrokenPipe writing it back: the chat client's 180 s default socket timeout hung up first, discarding an answer already paid for and failing the job. The Ask client now uses ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS (default 570 s) — just under the worker's 600 s job deadline, so the client ends a slow call before the reaper does and a generated answer has the whole deadline to arrive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * test(e2e): size the answer deadline to the 570 s client timeout Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): restate the resolved relative-time window in the prompt Retrieval already scopes sources to the resolved window ('7개월 전' → 2026-01-01..2026-01-31), but the numbered sources carry no dates, so the model answered the temporal e2e question with 'no date information' and cited nothing (observed live: 4 in-window sources, zero citations). The worker now appends the resolved window to the question and states that every provided source falls inside it, grounding the answer in the evidence it was given. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * feat(seed): warm post-content ingestion for seeded posts Seeded posts are inserted straight into Postgres, but post-content extraction (units, embeddings, embedded images) only enqueues when a post's /content endpoint is first served — so a fresh stack had an empty image pipeline and image citation had nothing to cite until a human happened to open the right post. The seed now opens each seeded post once through the API with the demo reader account, replaying the exact production enqueue path instead of duplicating its SQL. Verified live: the seeded TIFF was extracted and vision-captioned. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): anchor today's date when restating a resolved time window The first grounding clause named only the resolved window, and the model read that window as the reference point ('now') — re-subtracting the offset and answering that '7개월 전' meant mid-2025. The clause now states today's date and equates the relative expression to the window outright, then directs the answer at the provided in-window sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * test(e2e): ask questions the seeded corpus can answer decisively 'What happened between these events?' has no referent and 'Which project?' names nothing — a careful model correctly declines both and cites nothing, so the suite depended on a bluffing model to pass. The questions now target seeded content directly (the Westfield Power specification thread, the synthetic raster evidence post, Ada West's initial site visit), so a model answering from the sources must cite them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): settle a job on any failure, not a named few An exception outside the settlement handler's tuple (first exercised live when citation assembly ran against real image rows) killed the worker task silently and stranded the row running until orphan recovery re-queued it 15 minutes later. Settlement is fail-closed now: any exception settles the job failed; CancelledError still propagates so shutdown leaves the row for recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): single-literal SQL statements for the Semgrep asyncpg rule The asyncpg-sqli rule flags adjacent-literal SQL concatenation as non-literal string building even with fully parameterized values (two blocking findings on the recovery sweep). Every statement in the module is now one triple-quoted literal, so there is no concatenation for the rule to misread and nothing to suppress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): express recovery windows without make_interval The Semgrep asyncpg-sqli rule still flagged the recovery statements after the single-literal rewrite; the trigger is the make_interval(secs => $n) call syntax, which its matcher reads as non-literal string building. interval '1 second' * $n is the same parameterized arithmetic without the construct the rule misparses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * chore(ask): suppress the asyncpg-sqli false positive on recovery SQL Both recovery statements are fully parameterized; the rule misreads the literal-plus-constant-arguments shape regardless of literal style or interval syntax (verified across three rewrites). Suppressed with the same annotation the repo already carries in report_ingestion.py and customer_hint_ingestion.py for this exact rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho * fix(ask): apply PR-572 review batch across queue, config, seed, e2e - Bound the wake-up stream (maxlen 1000, approximate) at both XADD sites and swallow a failed wake-up publish in enqueue: the committed row is truth and the sweep republishes within a minute, so the caller keeps its pollable job id instead of a 500. - Share the 600 s job deadline through config (GLOBAL_ASK_JOB_DEADLINE_SECONDS) and validate ORCHESTRATOR_ANSWER_TIMEOUT_SECONDS as finite and strictly inside (0, deadline) so the client-hangs-up-first ordering cannot be misconfigured away. - Tighten orphan recovery to deadline+60 s so a crashed worker's job returns to the queue while a reader is still polling; raise the frontend poll ceiling to 15 min to cover queue wait plus deadline. - Resolve one Seoul reference date per job and use it for both retrieval and the grounded prompt (midnight-boundary consistency); gather_global_chat_sources accepts the pinned date. - Give only the Ask worker the long answer timeout; the synchronous per-post chat endpoint keeps the interactive client default. - Harden the worker loop: one failed consume/recovery round logs and retries instead of silently ending Ask consumption. - e2e computes the months-ago offset from the real clock so the temporal question keeps resolving onto the seeded 2026-01 window in any month. - Seed waits for backend /healthz before warming, and closes its psycopg2 connection explicitly. - Drop imports left dead by the Ask logic move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * chore: remove one-shot patch scripts left from PR #347 development (#575) add_translations.py, patch_api.py, patch_app_fetch.py, patch_app_order.py, and patch_main.py were single-use edit scripts that already applied their changes; nothing in the Makefile, CI, or runtime references them, and leaving dead top-level scripts invites someone to run them twice. Claude-Session: https://claude.ai/code/session_01VENX71RtEntaUq6nkAWZho Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix: bootstrap repo-root sys.path in the two operator scripts missing it (#570) backfill_post_keymen.py and requeue_failed_post_content.py were the only scripts/ entries without the repo-root sys.path insert their siblings use, so `python scripts/<name>.py` failed with ModuleNotFoundError: backend unless a caller manually exported PYTHONPATH=. first. Match the existing pattern instead of documenting the workaround. Found while re-running backfill_post_keymen.py to refresh a stale reason_no_live_client role row now that the local orchestrator/searxng services are reachable. * fix(ui-ux): size secondary details/summary toggles to --size-control-min (#560) * fix(ui-ux): size secondary details/summary toggles to --size-control-min Several disclosure toggles (advanced review tools, evidence-operation actions, semantic provenance, source-author hint context) rendered with a browser-default <summary> well under the project's 24px --size-control-min touch target, the same convention already used by .language-switcher select and .lineage-entity-picker select. - Add a shared App.css rule giving .advanced-review-tools, .semantic-provenance, .operator-action-tools, .keyman-source-context, and the new .hint-disclosure class an inline-flex summary with min-height: var(--size-control-min) and horizontal padding, so the hit target isn't text-width-only. - .operator-action-tools and .keyman-source-context previously had zero CSS rules at all -- not just a missing min-height. - The three bare <details> "Related posts"/"Hint only" expanders in CustomerMasterPanel had no className, so there was no selector able to reach them; give them className="hint-disclosure". Tests: frontend/src/styles/tokens.test.ts pins the new selector list and asserts the rule uses var(--size-control-min) plus horizontal padding, not a bespoke value. frontend/src/App.test.tsx adds a render test asserting the three previously-bare <details> carry the .hint-disclosure class. Both were confirmed to fail before this change and pass after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(ui-ux): keep disclosure markers on secondary summary toggles display: inline-flex replaced the summary list-item box, which stops browsers from rendering the built-in expand/collapse marker. Reach the --size-control-min touch target with min-height and padding instead so the native affordance stays visible (devin review thread). * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix(ui-ux): expose bare loading text as live regions (#558) * fix(a11y): expose bare loading text as live regions Async loading placeholders in App.tsx and FiveW1H.tsx rendered as plain <p> text with no role/aria-live, so assistive tech never announced the transition from "Loading..." to the resolved content. This included the app-root auth gate every session passes through, and the post-detail popup's loading paragraph. Add role="status" to every bare loading <p> (19 in App.tsx, 1 in FiveW1H.tsx), matching the role="status" pattern already used elsewhere in the same files (e.g. "Loading posts...", SourceResearchPanel). No behavior change beyond the attribute. Tests: add an RTL assertion that the auth-loading gate and the post-detail popup's loading state are exposed via getByRole("status") rather than only getByText, extending stubBackend with a deferPostOne option to deterministically observe the popup's pre-resolution state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix(ui-ux): grow citation chip to a 24px touch target (#556) * fix(ui-ux): grow citation chip to a 24px touch target .citation-chip rendered ~19px tall on mobile (0.1rem block padding via --space-chip-block, no min-height), below the 24px minimum touch target. Add min-height: var(--size-control-min), matching the convention already used by .language-switcher select and .lineage-entity-picker select. display: inline-flex + align-items: center keep the button's text vertically centered in the taller box, since (unlike the label-based board-option rules) this is a bare <button>. Pins the fix with a regression test in tokens.test.ts that extracts the .citation-chip rule from App.css and asserts min-height/inline-flex are present. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * fix: shorten the orchestrator healthcheck's retry budget (#547) * fix: shorten the orchestrator healthcheck's retry budget 10 retries at a 5s interval matches every other service's healthcheck (postgres, valkey, searxng all use retries: 10). The orchestrator was the one outlier at 20 retries -- a 100s startup grace period for a liveness-only check (does no real work beyond confirming the process is up) versus 50s everywhere else, with no documented reason for the gap. * fix(compose): give the orchestrator healthcheck a 50s warm-up window retries: 10 alone halves cold-boot grace to ~50s, which could fail the backend depends_on gate on slow hosts. start_period: 50s keeps failures during warm-up outside the retry budget: a booting orchestrator still gets ~100s total while a genuinely dead one trips the gate in ~50s (devin review thread). * docs(compose): correct the healthcheck warm-up timing comment start_period: 50s + retries: 10 x 5s means a dead service trips at ~100s total (not ~50s): identical to the old retries: 20 budget. The actual win is that a booting orchestrator no longer consumes retry budget during warm-up (devin review thread). --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat: name leftover-map unexplained leftover on leftover pairs (v2.12.26) (#535) * feat: name leftover-map unexplained leftover on leftover pairs (v2.12.26) Persist unexplained leftover U = R − R̂ after two-axis Gabriel reconstruction so a leftover residual is not read as the leftover the map does not reconstruct. R̂ stays internal and is not persisted. Rank-0 maps store U = 0; fallback pairs omit U. After make seed, closest and farthest leftover pairs sit above the member list with U next to leftover-map distance d; click opens that post. * fix(frontend): remember OIDC return URL and drop unreachable AdminPanel render Login screen built returnUrl via raw string concatenation instead of returnUrlFromLocation()/rememberOidcReturnUrl(), leaving both imports unused (TS6192) and rendering an AdminPanel with accessToken: undefined (TS2322) on a branch guarded by !auth.isAuthenticated. --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> * feat: bind corroborated SKOS org aliases to one catalog row (#480) * docs: restore non-identifying gap baseline * fix(frontend): restore authenticated admin boundary * feat: bind corroborated SKOS org aliases to one catalog row Expand corporate_entity candidates with search-verified alt/pref labels so synthetic AGP and Aurora Grid Power mentions reuse one row (ADR 0120). Ties and uncorroborated pairs stay unbound. No real organization names. * docs: correct live Strix failure count * fix: preserve raw organization ties before aliases * docs: clarify alias resolution order * fix: recheck organization evidence under lock * fix: require exact SKOS alias matches * fix: exclude full inferred ancestor path * docs: align ancestor exclusion contract * docs(adr): renumber SKOS alias-binding decision to avoid 0120 collision PR #490 already claims ADR 0120 for an unrelated decision (two-word database identifiers). Renumber this PR's ADR to 0158, which is currently free across origin/main and the open PRs checked, and update all cross-references (ARCHITECTURE.md, CHANGELOG.md, CHANGELOG.d, ADR 0008, ADR 0012, and the two backend docstrings). * docs: resolve SKOS ADR number collision * feat(estimation): queued llm pair judging via durable batch routing (ADR 0200 point 5, no activation) The llm channel joins the estimate without a single bulk synchronous provider call: 'submit' samples pairs exactly as the deterministic estimator does, takes a bounded deterministic stride subsample, submits ONE contextual-orchestrator batch routing job (one request per pair, caller-supplied custom_id=pair-<ordinal> -- the id round-trip landed upstream as contextual-orchestrator #832; #829's Valkey registry makes the job survive orchestrator restarts) and persists the run plus every pair's deterministic scores into migration 0201's ledger, never waiting. 'collect' polls once, maps returned scores to pairs by custom_id only (never result order), persists each llm score durably as it lands -- a killed collect loses nothing -- and fits the 4-channel expected-information estimate only over a complete run, persisting it as channel_set_with_llm with full provenance. A refused fit marks the run run_failed and writes no weights. The adjudication prompt and confidence parsing move to shared helpers (judge_prompt / parse_confidence) so the live client and the queued scorer ask the byte-identical question. sample_pair_scores returns pair labels again and the deterministic stride subsample returns for the bounded llm pass. Migration 0201 live-validated (replay-idempotent, judged-at/llm-score consistency check enforced, rollback round-trip). Activation remains zero: the loader's authorized anchor set is empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J * feat(operability): structured server diagnostics behind the generic 503 (#577) * feat(operability): structured server diagnostics behind the generic 503 Global Ask hid every failure behind a stable 503 (correct customer boundary) but the cause never reached structured telemetry, and the f-string leaked raw exception text to callers. Split the ask handler into three classified paths, all returning the same generic 503: - HttpClientError/OSError -> known provider/transport fault; warning event orchestrator_provider_unavailable with operation code, correlation id, exception class; message deliberately not logged. - KeyError/ValueError -> evidence-object contract break; error event orchestrator_internal_fault with stack trace attached. - broad Exception -> unexpected defect; same internal-fault diagnostic so a programming regression cannot degrade into an opaque availability incident. Chaining is preserved on every path. backend/app/operability.py documents the forbidden-field contract (no prompt text, model output, bearer tokens, provider keys, tenant PII, or post bodies in any record); alerting keys on event_type so pager load separates provider-down from our-bug (issue #361). Unit tests cover both event shapes, uniqueness of correlation ids, and the forbidden- field guarantee without needing a live stack. * feat(ontology): deterministic legacy-namespace migration tooling ADR 0157 keeps the lowercase ontology namespace canonical and demoted the repository-case spelling to deprecated compatibility status, but rows written before the decision can still carry legacy IRIs in post_project_mention.ontology_iri -- and RDF consumers treat the two spellings as different resources. scripts/migrate_legacy_namespace.py scans, prints every planned rewrite, refuses unrecognized namespaces (fail closed rather than bulk-mangle a third spelling), and only writes under --apply inside one transaction guarded by the exact old IRI so a concurrent edit aborts instead of double-applying. Provenance columns (extraction method, confidence, evidence) are never touched per ADR 0157's do-not-silently-rewrite rule. Dry run is the default. 8 unit tests cover canonicalize mapping, dry-run reporting, selective apply, fail-closed behavior, and the clean-database no-op. * fix(operability): redact exception messages from internal-fault tracebacks Python renders a traceback's final line as 'ExceptionType: message', so exc_info=exc violated the module's own forbidden-field contract: parsing exceptions from orchestrator responses can embed provider payload or prompt fragments, and those landed in the log. Re-emit through a _MessageRedacted carrier that keeps the original __traceback__ (the raise-site frames stay diagnosable) while its text is a fixed redaction notice; the real class name is retained in the structured exception_class field. Also unify the three /api/ask 503 detail strings into one generic message so callers cannot probe which internal classifier fired; the provider-vs-contract-vs-defect distinction lives only in server-side event_type (devin/coderabbit review threads on PR #577). Drop the docstring's nonexistent include_message option. --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * feat: add bounded ontology and provenance explorer (v2.13.0) (#349) * feat: add bounded ontology and provenance explorer GET /api/ontology/neighborhood returns a typed Post/Person/CorporateEntity/Team neighborhood with SKOS broader distinct from OWL subclass, truth-status vocabulary, knowledge-cutoff binding, and no hidden-count side channels. Buyers inspect it from the Keyman panel, not a second GNB destination (ADR 0119 / #341). * fix: keep ontology JSON-LD exports filtered * fix: validate ontology neighborhood request bounds * fix: escape ontology CSV formulas * fix: make ontology layout ordering deterministic * fix: make ontology explorer controls accessible * fix: translate ontology post node labels * fix: deduplicate ontology node evidence * fix: drop unlabeled ontology edges * fix: harden typed ontology neighborhood boundaries * fix: bound ontology fact loading by requested edge limit * fix: complete ontology explorer verification coverage * fix: hide unauthorized ontology parents * test: reproduce missing Storybook viewport addon * fix: declare Storybook viewport addon * fix: register Storybook viewport addon * fix: use Storybook 10 built-in viewport feature * fix: keep Storybook 10 essentials zero-config * fix: declare Storybook built-in viewport options * test: lock Storybook 10 built-in viewport contract * fix: bind narrow ontology story to mobile viewport * test: reproduce cutoff and uppercase focus defects * fix: apply ontology cutoff before SQL limit * test: require localized ontology stabilization copy * feat: localize ontology paging and provenance guidance * test: reproduce ontology paging and provenance UX defects * fix: stabilize ontology paging and provenance UX * test: reproduce bounded window and disconnected first page * fix: preserve proximity and source truncation truth * fix: expose bounded ontology source windows * fix: keep ontology loader test doubles compatible * test: reproduce duplicate ontology focus rule * test: reproduce endpoint N+1 and shallow evidence expansion * test: require a next action for hard ontology bounds * test: require localized hard-bound guidance * feat: localize hard ontology query-bound guidance * fix: validate provider chat response envelopes * fix: close remaining provider error leaks * fix: normalize summary enrichment failures * fix: normalize unexpected provider failures * fix: keep TEPP and worker errors provider-safe * fix: bound and batch ontology neighborhood evidence * fix: preserve ontology visibility query compatibility * fix: restore ontology explorer next actions * fix: honor ontology cutoff and canonical focus UUIDs * fix: read ontology visibility records safely * fix: preserve provider parse failures and truncation * fix: preserve ontology explorer session and parent visibility Clear a loaded neighborhood when the session token is removed, hide live refocus on static catalog snapshots, look up SKOS parent visibility independently of the child, and fail closed on dangling exact-value endpoints. OWL-Time is cited as a W3C Candidate Recommendation Draft. * fix: authorize final ontology endpoints * fix: preserve ontology evidence and stale diagnostics * fix: make tenant settings migration replayable * fix: harden ontology explorer paging and login return * docs: sanitize baseline and record ontology gates * fix: align provider changelog release * test: assert ontology fact query limit argument * feat: continue ontology neighborhoods with an opaque source cursor (v2.14.0) (#369) * feat: continue ontology neighborhoods with an opaque source cursor Add a versioned HMAC source-window cursor so authorized relations beyond the bounded SQL window can be paged with keyset continuation, not OFFSET. Tamper, scope, version, expiry, and snapshot drift fail closed. A missing process secret keeps truncated-without-cursor. The explorer accumulates pages without losing selected evidence (ADR 0124 / #363). * fix: retain paged ontology relations * fix: reuse expanded ontology window * fix: seal ontology continuation windows * docs: record ontology continuation safeguards * test: assert continuation cursor anchor * fix: deduplicate ontology source-window edges * fix: preserve ontology source cursor ordering * Fix ontology cursor after derived edge overflow * fix(ontology): remove unused subclass IRI constant * fix(frontend): preserve ontology pages on continuation failure * test(frontend): retain ontology selection after page failure * test(frontend): align rejected ontology fixture rows * fix: make ontology page retries effective * fix(security): replace custom source cursor cipher * fix(security): keep aggregate SQL immutable * test(security): preserve reviewed SQL contract * fix(ontology): keep paging safe and stable * fix: conceal ontology error details * fix: distinguish full ontology page from truncation * test: use one ontology ingestion import style * fix(ontology-neighborhood): trim by BFS distance, not key string assemble_ontology_neighborhood() trimmed nodes over maximum_nodes by sorting the "node_type_code:node_id" key lexicographically instead of by BFS/source-hop distance from the focus. Since "node_corporate_entity" sorts before "node_post", farther nodes of an alphabetically-earlier type survived while closer nodes of a later type were dropped, even though truncated stayed true and evidence_count was still correct for the wrong node set. Sort the trim candidates by the same `reached` distance already computed during BFS (and via source_hop_depth in the source-window path), using the node key only as a tiebreaker. Adds a regression test covering two node types straddling the maximum_nodes boundary. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5M79L945DMyMs3sg5yJ14 * fix: allocate migration 0175 for ontology truth * fix: restore ontology snapshot on reset * fix: resolve silent ADR-0119 number collision from main merge main independently claimed ADR 0119 for the leftover-map two-axis distance change while this PR's own ADR 0119 (ontology/provenance explorer) merged in cleanly as a separate file, since git diffs by path and both files kept distinct names. Renumber this PR's ADR to 0168 (next free number after main's 0167) and update every cross-reference (CHANGELOG, AGENTS.md, ARCHITECTURE.md, the 0175 migration comment, ontology_neighborhood.py/test/ingestion docstrings, ADR 0124's predecessor link, and the ADR/storybook README index). Main's own ADR 0119 file and its references are untouched. * fix: document the source-window page sort key closure tests/test_public_docstrings.py's repository-wide public-docstring contract walks every non-underscore-prefixed def, including nested closures (matching flush/transport/stale_fallback elsewhere in this codebase). source_page_sort_key was missing its docstring; add one describing the cursor-key-first, hop/property/node-tuple-fallback ordering it implements. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix: strip Keycloak OIDC callback params from the post share link (#576) * fix: strip Keycloak OIDC callback params from the post share link PostDetailPopup built its permanent link from window.location.href verbatim, so sharing right after (or during) a Keycloak sign-in redirect copied the raw code/state/session_state/iss query params along with the post link instead of a clean permalink. Add stripOidcCallbackParams (oidcReturnUrl.ts) and use it before appending ?post=<id>, with unit tests covering both the strip and the no-op case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUcvhvimNVGqjFuD9f1fUa * fix(oidc): strip callback params from restored return URLs too stripOidcCallbackParams only cleaned share links, but restoreOidcReturnUrl falls back to returnUrlFromLocation() on the post-redirect location -- which still carries code/state/session_state/iss. Strip them at build time so no consumer of this module re-mints a URL with a one-time authorization code in it; regression test covers the redirect-shaped search string (devin review thread). --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix(ui-ux): give Event Lineage DAG node marks a 24x24px hit target (#554) * fix(ui-ux): give Event Lineage DAG node marks a 24x24px hit target The visible node mark in LineageDag.tsx is a 7px-radius (14px) SVG circle with no separate hit area, well under the WCAG 2.2 SC 2.5.8 AA minimum of 24x24 CSS px and this codebase's own --size-control-min token (styles/tokens.css). Add a transparent, pointer-events:all circle (r=12, matching --size-control-min at this DAG's ~1 user-unit-per-px scale) as the first child inside the existing role="button" <g>, ahead of the visible mark, so it enlarges the click/tap area without changing appearance. ROW_H is a 52px row pitch (lineageLayout.ts), so a 24px-diameter hit circle leaves 28px of clearance between adjacent rows -- no overlap. Adds LineageDag.test.tsx asserting the hit circle is present, sized >=24px diameter, transparent, and painted before the visible mark, plus a click-still-works regression check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt * fix(ui-ux): keep the DAG hit circle invisible under the author CSS fill rule The .lineage-dag-node circle rule paints every child circle with --surface-muted plus a border stroke, and author CSS overrides the fill="transparent" presentation attribute, so the enlarged hit target rendered as a second opaque disc. Scope an explicit transparent/no-stroke override to .lineage-dag-hit (devin review thread). * fix(frontend): wire ADR 0109 return-url capture and narrow admin token - The unauthenticated Log in handler now calls returnUrlFromLocation() then rememberOidcReturnUrl() before signinRedirect, per ADR 0109, so a shared /?post= link still opens that post after enterprise SSO. The previously unused oidcReturnUrl import is now load-bearing. - AdminPanel renders only when accessToken is a string; the OIDC access token is string | undefined before isAuthenticated narrowing. * fix(ui-ux): keep focus/hover/current rings off the DAG hit target The aria-current ring rule (0,2,1) outranked the hit-circle override (0,2,0) and painted a large hollow ring around the current node's mark. Exclude .lineage-dag-hit from the focus/hover/current selectors via :not(), which never matches the hit circle at all, and lift the override to (0,3,0) so future circle-scoped emphasis rules lose by specificity rather than source order (devin review thread). * fix(ui-ux): exclude the DAG hit target from focus/hover/current rings Equal-specificity source-order was still letting the aria-current ring paint over the hit circle. :not(.lineage-dag-hit) on the emphasis selectors never matches the hit target, closing the ring regression for good. * fix(ui-ux): pin the DAG svg to its user-unit width width="100%" plus a viewBox let the container scale the coordinate system, shrinking the effective 24px hit target below WCAG 2.5.8 on wide containers. Render at the layout's own user-unit width (height already behaves this way) so one SVG unit stays ~1 CSS px and the hit radius keeps its intended size; horizontal overflow is handled by the panel's existing scroll container. * test(red): require scrollable lineage DAG viewport * test(red): require mobile lineage scroll guidance * fix(ui-ux): keep lineage DAG reachable in narrow viewports * fix(ui-ux): style keyboard-scrollable lineage viewport * test(storybook): cover mobile lineage scrolling * docs(storybook): record mobile lineage scroll state --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> * fix(review): connection-free fit in collect; errored judgments stay unjudged Devin findings on #586: (1) _collect held its database connection open across the model fit -- the exact idle-reap failure class #571 fixed -- now every phase (run lookup, ledger update, fit, persist) opens its own short-lived connection and the fit runs with none. (2) An empty or non-numeric batch answer parsed as a confident 0.0; the new parse_confidence_or_none keeps the live client's semantics while the queued scorer omits unparseable answers so an errored request stays unjudged (pure helper judgment_updates_from_results, tested). Also: collect gains --run-id for stale earlier runs, and the rebase onto the updated ADR branch adopts main's chat_completion_content safe extractor inside the shared-helper judge(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015HgzNGsCxqiTaT4YuJEb5J --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> --------- Co-authored-by: seonghobae <seonghobae@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gap
Dimension: touch_interaction
Severity: medium
.citation-chip(the Ask/Chat evidence citation button rendered byChatCitations/CitationChip.tsx) renders ~19px tall on mobile, belowthe 24px minimum touch target.
Evidence (
frontend/src/App.css):No
min-height/min-width, and--space-chip-blockis0.1rem(
frontend/src/styles/tokens.css:83) — unlike.language-switcher selectand
.lineage-entity-picker select, which already addmin-height: var(--size-control-min).Fix
Added to
.citation-chip:min-heightmirrors the existing convention (11 other rules inApp.cssalready usevar(--size-control-min), e.g..language-switcher select,.lineage-entity-picker select).display: inline-flex; align-items: center;is needed because.citation-chipis a bare<button>, so its textwould otherwise sit at the top of the taller box once
min-heightapplies.No hardcoded sizes/colors — reused the existing
--size-control-mintoken.Did not bump
--space-chip-gap(mentioned as optional in the gap report,not required to close the core gap — YAGNI).
Test
Added a regression test in
frontend/src/styles/tokens.test.ts(extendsthe file's existing pattern of reading
App.cssas text and asserting onCSS rule content, since jsdom doesn't apply real layout/CSS in this repo's
Vitest config) that extracts the
.citation-chiprule and assertsmin-height: var(--size-control-min),display: inline-flex, andalign-items: centerare present. Confirmed red before the fix / greenafter.
Checks (run from
frontend/)corepack pnpm exec vitest run src/styles/tokens.test.ts src/components/CitationChip.test.tsx→ 5 passed (2 files)corepack pnpm exec oxlint src/App.css src/styles/tokens.test.ts src/components/CitationChip.tsx src/components/CitationChip.test.tsx→ clean, no outputcorepack pnpm exec tsc -b→ 2 pre-existing errors insrc/App.tsx(unused import at line 104,AdminPanel accessTokentype mismatch at line 4623), unrelated to this change and unrelated toApp.css/tokens.test.ts. Confirmed viagit diffthatApp.tsxis untouched by this PR, and these errors reproduce identically against a pristineorigin/maincheckout.🤖 Generated with Claude Code
https://claude.ai/code/session_011jWJzKUd82yy97esEBfJbt