feat: persist leftover-map axis share on period reports (v2.12.16) - #519
Conversation
|
Exact head |
|
Warning Review limit reachedNext included review available in 54 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 ignored due to path filters (1)
📒 Files selected for processing (26)
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
left a comment
There was a problem hiding this comment.
requesting copilot
|
Clarification: review |
|
Integration repair at |
|
Closed the remaining reader-facing locale gap at exact head Validation: |
* feat: persist leftover observed Y and expected E (v2.12.20) After seed, leftover closest/farthest pairs sit above the member list with observed Y and expected E[Y|θ, item] next to leftover-map distance d. Click opens that post. Residual stays R = Y − E (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0170). Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), and two-axis distance (#522). Issues #79 and #87 stay open. * Repair inherited login build boundary * Move leftover evidence contract to ADR 0177 * fix: show leftover Y and E after stacking onto the workspace branch Merge #490 without force-push. Keep ADR 0049 next-action copy and name observed Y and expected E on leftover pair buttons. Replay migration 0177 on existing volumes. * fix(leftover-observed-expected): wire missing migration and translations Two genuine gaps in this PR's own diff, unrelated to any merge conflict: 1. backend/tests/test_api.py and tests/test_schema.py never applied migration 0177_report_leftover_observed_expected.sql in their test DB fixtures, even though report_ingestion.py already inserts into the observed_response/expected_response columns it adds. Every period-report test that touches leftover pairs failed with UndefinedColumnError. 2. frontend/src/i18n.ts never got the ko/zh/ja/vi translations for the two leftover-map-rank action strings that frontend/src/leftoverMapRank.ts (added downstream, in the PR that bases on this one) references — t()/tf() fall back to the raw English key for every locale, failing the i18n test suite in the whole downstream chain. Verified: 1035 Python tests pass, 362 frontend tests pass, build/lint clean.
* feat: persist leftover observed Y and expected E (v2.12.20) After seed, leftover closest/farthest pairs sit above the member list with observed Y and expected E[Y|θ, item] next to leftover-map distance d. Click opens that post. Residual stays R = Y − E (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0170). Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), and two-axis distance (#522). Issues #79 and #87 stay open. * Repair inherited login build boundary * Move leftover evidence contract to ADR 0177 * feat: name leftover-map rank on leftover pairs (v2.12.21) After seed, leftover closest/farthest pairs sit above the member list with leftover-map rank next to leftover-map distance d. Click opens that post. Rank is the count of Gabriel singular values above the floor (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0171). Rank 0 names no leftover structure. Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), two-axis distance (#522), and observed Y / expected E (#527). Issues #79 and #87 stay open. * Move leftover-map rank contract to ADR 0172 ADR 0171 is already used by analysis-run status same clock (#524). Keep leftover-map rank independent: ADR 0172 and migration 0172. Shipped 0001 / 0012 stay untouched. * fix: restore frontend build on leftover-map PR Drop unused OIDC import and unauthenticated AdminPanel that fail tsc (TS6192 / TS2322). AdminPanel stays authenticated-only. * fix: show leftover Y and E after stacking onto the workspace branch Merge #490 without force-push. Keep ADR 0049 next-action copy and name observed Y and expected E on leftover pair buttons. Replay migration 0177 on existing volumes. * fix: keep leftover-map rank after stacking onto leftover Y/E Merge stacked #527 without force-push. Persist leftover_map_rank with observed Y and expected E, and name rank on leftover pair buttons.
) * feat: persist leftover observed Y and expected E (v2.12.20) After seed, leftover closest/farthest pairs sit above the member list with observed Y and expected E[Y|θ, item] next to leftover-map distance d. Click opens that post. Residual stays R = Y − E (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0170). Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), and two-axis distance (#522). Issues #79 and #87 stay open. * Repair inherited login build boundary * Move leftover evidence contract to ADR 0177 * feat: name leftover-map rank on leftover pairs (v2.12.21) After seed, leftover closest/farthest pairs sit above the member list with leftover-map rank next to leftover-map distance d. Click opens that post. Rank is the count of Gabriel singular values above the floor (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0171). Rank 0 names no leftover structure. Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), two-axis distance (#522), and observed Y / expected E (#527). Issues #79 and #87 stay open. * Move leftover-map rank contract to ADR 0172 ADR 0171 is already used by analysis-run status same clock (#524). Keep leftover-map rank independent: ADR 0172 and migration 0172. Shipped 0001 / 0012 stay untouched. * feat: name leftover residual on period-report pair rows (v2.12.22) After seed, leftover closest/farthest pairs sit above the member list with signed leftover residual R next to leftover-map distance d. Click opens that post. Residual is R = Y − E[Y|θ, item] (Jeon et al., 2021 eq. 3; Gabriel 1971; ADR 0178). A non-finite residual is an em dash. Never invent a leftover score or theta. Independent of leftover persist-map (#481), criterion landing (#485), complete-case coverage (#518), leftover-map axis share (#519), comparison-strip leftover pairs (#521), two-axis distance (#522), observed Y / expected E (#527), and leftover-map rank (#529). Issues #79 and #87 stay open. * fix: restore frontend build on leftover residual disclosure Drop unused OIDC import and unauthenticated AdminPanel that fail tsc (TS6192 / TS2322). Login matches the workspace branch: AdminPanel is authenticated-only. * fix: restore frontend build on leftover-map PR Drop unused OIDC import and unauthenticated AdminPanel that fail tsc (TS6192 / TS2322). AdminPanel stays authenticated-only. * fix: show leftover Y and E after stacking onto the workspace branch Merge #490 without force-push. Keep ADR 0049 next-action copy and name observed Y and expected E on leftover pair buttons. Replay migration 0177 on existing volumes. * fix: keep leftover-map rank after stacking onto leftover Y/E Merge stacked #527 without force-push. Persist leftover_map_rank with observed Y and expected E, and name rank on leftover pair buttons.
| ) | ||
| closest = min(candidates, key=lambda row: (row[0], row[1], row[2])) | ||
| farthest = max(candidates, key=lambda row: (row[0], row[1], row[2])) | ||
| return ( | ||
| pairs = ( | ||
| _pair_from_candidate(PAIR_KIND_CLOSEST, closest, leftover_map_rank), | ||
| _pair_from_candidate(PAIR_KIND_FARTHEST, farthest, leftover_map_rank), | ||
| ) | ||
| return pairs, axes |
There was a problem hiding this comment.
📝 Info: Rank-0 pair fallback keeps non-zero axis shares
In leftover_map_from_residual, axis shares are computed from the singular values (leftover_pairs.py:115) but the pair leftover_map_rank is reset to 0 in the no-candidate fallback (leftover_pairs.py:136-137). If a complete-case rectangle yields non-empty singulars yet all map distances are non-finite, pairs report rank 0 while axes report non-zero share. Only reachable via the monkeypatched non-finite coordinates in test_nonfinite_map_distance_falls_back_to_centered_residual; real SVD coordinates are finite, so this does not occur in practice.
(Refers to this code)
Was this helpful? React with 👍 or 👎 to provide feedback.
| for axis in report.leftover_map_axes: | ||
| await conn.execute( | ||
| """ | ||
| insert into report_leftover_map_axis ( | ||
| grouping_kind, grouping_key, period_code, rubric_version, | ||
| axis_index, leftover_singular_value, leftover_share | ||
| ) values ($1,$2,$3,$4,$5,$6,$7) | ||
| """, | ||
| grouping_kind, | ||
| grouping_key, | ||
| period_code, | ||
| RUBRIC_VERSION, | ||
| axis.axis_index, | ||
| axis.leftover_singular_value, | ||
| axis.leftover_share, | ||
| ) |
There was a problem hiding this comment.
📝 Info: Axis-share replace relies on cascade delete
Neither persist_period_report nor the seed's _persist_seed_period_report explicitly deletes report_leftover_map_axis rows before re-inserting; replacement depends on ON DELETE CASCADE from the leading report_period_score delete (migration 0169 FK). This mirrors the existing report_leftover_pair pattern and is correct as long as that period-score delete always precedes the inserts.
Was this helpful? React with 👍 or 👎 to provide feedback.
| values = np.asarray(singular, dtype=np.float64).reshape(-1) | ||
| kept = values[np.isfinite(values) & (values > _LEFTOVER_SINGULAR_FLOOR)] | ||
| total = float(np.sum(kept * kept)) if kept.size else 0.0 | ||
| axes: list[LeftoverMapAxis] = [] | ||
| for index in (1, 2): | ||
| value = float(kept[index - 1]) if kept.size >= index else 0.0 | ||
| if not np.isfinite(value) or value < 0.0: | ||
| value = 0.0 | ||
| share = (value * value / total) if total > 0.0 else 0.0 | ||
| axes.append( | ||
| LeftoverMapAxis( | ||
| axis_index=index, | ||
| leftover_singular_value=value, | ||
| leftover_share=max(share, 0.0), | ||
| ) | ||
| ) | ||
| return tuple(axes) |
There was a problem hiding this comment.
📝 Info: Axis-share denominator excludes below-floor singulars
leftover_map_axes_from_singular builds the inertia denominator from singular values above the floor only (leftover_pairs.py:214-216), so axis 1 and 2 shares are relative to surviving inertia. For rank>2 residuals the two shares deliberately sum to less than 1, and the frontend badges will not total 100%. Consistent with ADR 0148's definition.
Was this helpful? React with 👍 or 👎 to provide feedback.
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.
31c150c to
5a4a41b
Compare
…ource-detail-state-filter Resolves fresh conflicts in tests/test_migration_replay.py (additive test concatenation) and frontend/src/i18n.ts (additive locale key concatenation across ko/zh/ja/vi).
…are) # Conflicts: # frontend/package.json # frontend/src/i18n.ts # pyproject.toml # uv.lock
…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>
… 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>
Summary
Persist leftover-map axis share (Gabriel inertia of residual SVD axes 1 and 2,
σ_k² / Σ_j σ_j²) as report-level 3NF next to leftover pairs (ADR 0148). Rank-0 residuals emit two zero-share axes so seed can name leftover-map structure without inventing a leftover score. Missing cells stay out of the factorization.After
make seed, leftover-axis badges sit with the leftover pairs. The caption tells the reader to open a leftover pair to read the post–criterion cell. Axis share is aggregate and non-identifying: ABAC that hides individual leftover pairs does not hide the report-level share, and reports with no visible members remain absent.This is independent of leftover-map coordinates (#481), leftover-criterion landing (#485), and leftover residual disclosure (#502).
The current head also preserves the inherited OIDC return-path/build repair and localizes every new axis badge, accessible label, and scientific caption through the existing five-locale boundary.
Reader next action
Open a leftover pair. The shares do not invent a leftover score.
Schema
report_leftover_map_axisis created only by idempotent migration0169_report_leftover_map_axis.sqland reaches fresh and existing Compose volumes through the migration service replay window. It is deliberately not duplicated in shipped migration0001. Axes 1 and 2 only. Cascade withreport_period_score.leftover_singular_value >= 0;leftover_shareis in[0, 1].Merge contract
Independent exact-head APPROVE is required. This identity will not self-approve or bypass protected checks. Do not merge until that approval exists on this exact head.
Test plan
sh -n, andgit diff --check