Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2cefc0b
feat(reports): leftover-map cross share on grouping comparison strip …
seonghobae Aug 30, 2026
8a12062
chore(stack): converge #831 onto current #829
seonghobae Sep 5, 2026
856464d
test(red): reconstruct grouping comparison cross-share contract
seonghobae Sep 10, 2026
2c2c170
chore(ci): stage bounded #831 cross-share source repair
seonghobae Sep 10, 2026
9a958e7
chore(ci): extend #831 repair through persisted transport
seonghobae Sep 10, 2026
856cbdf
fix(ci): correct bounded #831 repair harness
seonghobae Sep 10, 2026
b7ebc9f
fix(ci): make #831 source surgery structural
seonghobae Sep 10, 2026
652bba7
fix(ci): repair #831 badge surgery and validate candidate
seonghobae Sep 10, 2026
2fe724b
fix(ci): make #831 backend surgery exact and bounded
seonghobae Sep 10, 2026
d225023
chore(repair): stage bounded #831 source surgery
seonghobae Sep 10, 2026
9e629f0
fix(ci): execute bounded #831 repair from one-shot script
seonghobae Sep 10, 2026
6bcf1d7
fix(repair): scope #831 backend surgery to comparison read model
seonghobae Sep 10, 2026
662bb9e
fix(ci): rerun #831 repair on scoped backend surgery
seonghobae Sep 10, 2026
fe30a82
fix(ci): validate #831 against exact unchanged product blobs
seonghobae Sep 10, 2026
5e2dbaf
fix(reports): transport and project comparison cross share
github-actions[bot] Sep 10, 2026
ee88c92
test(reports): reject nonfinite comparison cross share
seonghobae Sep 11, 2026
071fe46
chore(automation): stage #831 nonfinite repair
seonghobae Sep 11, 2026
e6c17e2
fix(automation): run #831 nonfinite repair
seonghobae Sep 11, 2026
3362aee
fix(automation): stabilize #831 rendered cross-share repair
seonghobae Sep 11, 2026
fab41f5
fix(automation): target #831 grouping fixture
seonghobae Sep 11, 2026
6d324e7
fix(reports): normalize nonfinite comparison cross share
seonghobae Sep 11, 2026
8d7d8fe
test(reports): keep unexplained-share fixture compatible with cross-s…
seonghobae Sep 11, 2026
9cc136c
test(report): reproduce non-finite primary cross-share leak
seonghobae Sep 11, 2026
b02e7d5
fix(report): normalize persisted cross-share at primary read boundary
seonghobae Sep 11, 2026
d581a73
test(reports): decouple PostgreSQL cross-share fixture from Keycloak
seonghobae Sep 11, 2026
9189a3e
Merge #1008: normalize non-finite primary cross-share
seonghobae Sep 11, 2026
44c7ed4
test(e2e): reuse real OIDC login for demo admin
seonghobae Sep 11, 2026
6f3b1a1
test(e2e): cover grouping comparison cross-share in browser
seonghobae Sep 11, 2026
6315b2a
ci(e2e): run authenticated grouping comparison browser gate
seonghobae Sep 11, 2026
579703f
fix(e2e): keep browser assertions out of Node DOM types
seonghobae Sep 11, 2026
ac50985
fix(ci): make browser cleanup safe before stack startup
seonghobae Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
run: uv run --frozen python -m pytest -q

frontend:
name: Frontend lint, test, build
name: Frontend lint, test, build, browser acceptance
if: github.event_name != 'pull_request' || (github.event.action != 'closed' && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -100,3 +100,56 @@ jobs:
- name: Build Storybook
working-directory: frontend
run: pnpm run build-storybook

- name: Install Playwright Chromium
working-directory: frontend
run: pnpm exec playwright install --with-deps chromium

- name: Start authenticated product stack
run: |
touch "$HOME/.env"
docker compose --env-file "$HOME/.env" up -d --build --wait
for attempt in $(seq 1 60); do
if curl --fail --silent --show-error \
http://localhost:18080/realms/master/.well-known/openid-configuration \
>/dev/null; then
exit 0
fi
sleep 2
done
echo "Keycloak did not become ready" >&2
exit 1

- name: Seed synthetic authenticated product data
run: |
docker compose --env-file "$HOME/.env" run --rm --no-deps \
-v "$PWD/scripts:/app/scripts:ro" \
-v "$PWD/migrations:/app/migrations:ro" \
backend python /app/scripts/seed_demo_data.py \
--postgres-dsn postgresql://lineageweave:lineageweave_dev_only@postgres:5432/lineageweave \
--keycloak-base-url http://keycloak:8080 \
--keycloak-admin-password admin_dev_only \
--valkey-url redis://valkey:6379/0 \
--backend-base-url http://backend:8000

- name: Pin signed cross-share browser fixture
run: |
docker compose --env-file "$HOME/.env" exec -T postgres \
psql -X -v ON_ERROR_STOP=1 -U lineageweave -d lineageweave \
-c "update report_leftover_pair set leftover_map_cross_share = -0.24;"

- name: Run authenticated grouping-comparison browser acceptance
working-directory: frontend
run: pnpm exec playwright test grouping-comparison-cross-share.spec.ts --project=chromium

- name: Capture product stack logs on browser failure
if: failure()
run: |
touch "$HOME/.env"
docker compose --env-file "$HOME/.env" logs --no-color --tail=400

- name: Tear down product stack
if: always()
run: |
touch "$HOME/.env"
docker compose --env-file "$HOME/.env" down -v --remove-orphans
20 changes: 16 additions & 4 deletions backend/app/report_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import asyncio
import math
import re
from collections import defaultdict
from datetime import datetime, timezone
Expand All @@ -28,6 +29,15 @@
_SOURCE_CONTEXT_PRESENT_SQL = source_context_present_sql("p")


def _finite_float_or_none(value: Any) -> float | None:
"""Return a strict-JSON finite float for an optional persisted numeric."""

if value is None:
return None
result = float(value)
return result if math.isfinite(result) else None


def parse_period_code(period_code: str) -> tuple[str, int, int]:
"""Return ``(kind, year, week_or_month)`` or raise ValueError."""
week = _WEEK_PERIOD.fullmatch(period_code)
Expand Down Expand Up @@ -811,10 +821,8 @@ async def fetch_period_reports(
if row["leftover_map_unexplained"] is None
else float(row["leftover_map_unexplained"])
),
"leftover_map_cross_share": (
None
if row["leftover_map_cross_share"] is None
else float(row["leftover_map_cross_share"])
"leftover_map_cross_share": _finite_float_or_none(
row["leftover_map_cross_share"]
),
"leftover_map_reconstruction": (
None
Expand Down Expand Up @@ -1053,6 +1061,7 @@ async def fetch_period_comparison(
select lp.grouping_kind, lp.grouping_key, lp.pair_kind, lp.post_id,
lp.criterion_code, lp.leftover_distance, lp.leftover_residual,
lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,
lp.leftover_map_cross_share,
p.post_title, p.visibility_code, p.corporate_entity_id,
({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context
from report_leftover_pair lp
Expand Down Expand Up @@ -1143,6 +1152,9 @@ async def fetch_period_comparison(
if pair["leftover_map_unexplained_share"] is None
else float(pair["leftover_map_unexplained_share"])
),
"leftover_map_cross_share": _finite_float_or_none(
pair["leftover_map_cross_share"]
),
"visibility_code": pair["visibility_code"],
"corporate_entity_id": str(pair["corporate_entity_id"]),
"has_real_source_context": bool(pair["has_real_source_context"]),
Expand Down
77 changes: 77 additions & 0 deletions backend/tests/test_period_comparison_cross_share.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Regression for persisted cross-share transport on the comparison read model."""

from __future__ import annotations

import asyncio
from typing import Any

from backend.app import report_ingestion


class _ComparisonConnection:
"""Minimal asyncpg-shaped fixture for one comparison grouping."""

def __init__(self) -> None:
self.leftover_query = ""

async def fetch(self, query: str, *_args: Any) -> list[dict[str, Any]]:
if "from report_period_score" in query:
return [{"grouping_kind": "process_unit", "grouping_key": "PU-1", "mean_theta": 0.25, "post_count": 4, "link_method": "fixture"}]
if "from report_member_score" in query:
return []
if "from report_leftover_pair" in query:
self.leftover_query = query
return [
{
"grouping_kind": "process_unit",
"grouping_key": "PU-1",
"pair_kind": pair_kind,
"post_id": f"post-{index}",
"criterion_code": "criterion-a",
"leftover_distance": 1.0,
"leftover_residual": -0.5,
"leftover_map_reconstruction": None,
"leftover_map_unexplained_share": None,
"leftover_map_cross_share": cross_share,
"post_title": f"Post {index}",
"visibility_code": "public",
"corporate_entity_id": f"entity-{index}",
"has_real_source_context": True,
}
for index, (pair_kind, cross_share) in enumerate(
(
("closest", 0.12),
("farthest", 0.0),
("closest", -0.25),
("farthest", None),
("closest", float("nan")),
("farthest", float("inf")),
("closest", float("-inf")),
),
start=1,
)
]
if "from report_leftover_map_coverage" in query or "from report_leftover_map_axis" in query:
return []
raise AssertionError(f"unexpected query: {query}")


def test_period_comparison_transports_persisted_cross_share(monkeypatch) -> None:
"""Preserve finite signed x values and normalize null or non-finite x to null."""

async def _label(_conn: Any, _kind: str, _key: str) -> str:
return "Process unit 1"

monkeypatch.setattr(report_ingestion, "resolve_grouping_label", _label)
connection = _ComparisonConnection()
payload = asyncio.run(report_ingestion.fetch_period_comparison(connection, "2026-W02"))
assert "lp.leftover_map_cross_share" in connection.leftover_query
assert [pair["leftover_map_cross_share"] for pair in payload[0]["leftover_pairs"]] == [
0.12,
0.0,
-0.25,
None,
None,
None,
None,
]
91 changes: 91 additions & 0 deletions backend/tests/test_period_report_cross_share_postgres.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""PostgreSQL regression for primary-report cross-share JSON safety."""

from __future__ import annotations

import asyncio
import json
import math
import uuid

import asyncpg
import jwt
import psycopg2
import pytest

from backend.app.report_ingestion import fetch_period_reports
from backend.tests.test_api import seeded_db as _seeded_db_fixture
from scripts.seed_demo_data import _seed_demo_period_report

seeded_db = _seeded_db_fixture


@pytest.fixture(scope="module")
def demo_analyst_token() -> str:
"""Supply only the subject claim needed by the PostgreSQL seed fixture."""

return jwt.encode({"sub": str(uuid.uuid4())}, key="", algorithm="none")


def test_fetch_period_reports_normalizes_persisted_nonfinite_cross_share(
seeded_db,
) -> None:
"""PostgreSQL non-finite numerics become null while finite signs survive."""
admin_conn = psycopg2.connect(seeded_db["dsn"])
admin_conn.autocommit = True
try:
with admin_conn.cursor() as cur:
cur.execute(
"insert into process_unit (corporate_entity_id, process_unit_code, process_unit_name) "
"select corporate_entity_id, 'TEST-PU-FINITE-REPORT', 'Finite report unit' "
"from source_post where post_id = %s returning process_unit_id",
(seeded_db["own_private_post_id"],),
)
process_unit_id = cur.fetchone()[0]
cur.execute(
"select author_account_id, corporate_entity_id from source_post where post_id = %s",
(seeded_db["own_private_post_id"],),
)
author_id, corp_id = cur.fetchone()
_seed_demo_period_report(cur, author_id, corp_id, process_unit_id)
cur.execute(
"select grouping_kind, grouping_key, post_id, pair_kind "
"from report_leftover_pair where period_code = '2026-W02' "
"order by grouping_kind, grouping_key, pair_kind, post_id"
)
pair_rows = cur.fetchall()
assert len(pair_rows) >= 7, pair_rows
injected = [0.12, 0.0, -0.25, None, "NaN", "Infinity", "-Infinity"]
for index, (kind, key, post_id, pair_kind) in enumerate(pair_rows):
cur.execute(
"update report_leftover_pair set leftover_map_cross_share = %s "
"where grouping_kind = %s and grouping_key = %s "
"and period_code = '2026-W02' and pair_kind = %s and post_id = %s",
(injected[index % len(injected)], kind, key, pair_kind, post_id),
)
finally:
admin_conn.close()

async def _read_reports():
conn = await asyncpg.connect(seeded_db["dsn"])
try:
return [
await fetch_period_reports(conn, kind, "2026-W02")
for kind in ("process_unit", "corporate_entity", "thread_group")
]
finally:
await conn.close()

payloads = asyncio.run(_read_reports())
shares = [
pair["leftover_map_cross_share"]
for payload in payloads
for report in payload
for pair in report["leftover_pairs"]
]
assert shares
assert all(share is None or math.isfinite(share) for share in shares)
assert 0.12 in shares
assert 0.0 in shares
assert -0.25 in shares
assert None in shares
json.dumps(payloads, allow_nan=False)
102 changes: 102 additions & 0 deletions backend/tests/test_report_ingestion_cross_share_nonfinite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Regression for primary report cross-share JSON safety."""

from __future__ import annotations

import asyncio
import json
from datetime import datetime, timezone
from decimal import Decimal

from backend.app import report_ingestion


class _PrimaryReportConnection:
"""Minimal asyncpg-compatible boundary for one persisted report pair."""

def __init__(self, cross_share: Decimal | None) -> None:
self.cross_share = cross_share

async def fetch(self, query: str, *_args: object) -> list[dict[str, object]]:
if "from report_period_score" in query:
return [
{
"grouping_kind": "thread_group",
"grouping_key": "synthetic-thread",
"period_code": "2026-W02",
"rubric_version": report_ingestion.RUBRIC_VERSION,
"selected_model": "rasch",
"mean_theta": 0.1,
"mean_theta_sd": 0.2,
"post_count": 2,
"item_count": 1,
"fit_loglik": -1.0,
"fit_converged": True,
"calibration_score": 0.9,
"computed_at": datetime(2026, 1, 5, tzinfo=timezone.utc),
"link_method": "synthetic",
"anchor_period_code": None,
"delta_mean_theta": None,
}
]
if "from report_member_score" in query:
return []
if "from report_item_information" in query:
return []
if "from report_leftover_pair" in query:
assert "lp.leftover_map_cross_share" in query
return [
{
"grouping_key": "synthetic-thread",
"pair_kind": "closest",
"post_id": "00000000-0000-0000-0000-000000000001",
"post_title": "Synthetic report post",
"criterion_code": "synthetic_criterion",
"leftover_distance": 0.5,
"leftover_residual": 0.4,
"observed_response": None,
"expected_response": None,
"leftover_map_rank": None,
"leftover_map_unexplained": None,
"leftover_map_cross_share": self.cross_share,
"leftover_map_reconstruction": None,
"leftover_map_unexplained_share": None,
"leftover_map_explained_share": None,
"leftover_map_person_axis_1": None,
"leftover_map_person_axis_2": None,
"leftover_map_item_axis_1": None,
"leftover_map_item_axis_2": None,
"visibility_code": "public",
"corporate_entity_id": "00000000-0000-0000-0000-000000000002",
"process_unit_id": None,
"has_real_source_context": False,
}
]
if "from report_leftover_map_axis" in query:
return []
if "from report_leftover_map_coverage" in query:
return []
raise AssertionError(f"unexpected report query: {query}")


def test_fetch_period_reports_normalizes_only_nonfinite_cross_share() -> None:
"""Finite signed values survive; non-finite persisted values become JSON-safe null."""
cases = (
(Decimal("0.42"), 0.42),
(Decimal("0"), 0.0),
(Decimal("-0.24"), -0.24),
(None, None),
(Decimal("NaN"), None),
(Decimal("Infinity"), None),
(Decimal("-Infinity"), None),
)
for persisted, expected in cases:
payload = asyncio.run(
report_ingestion.fetch_period_reports(
_PrimaryReportConnection(persisted), # type: ignore[arg-type]
"thread_group",
"2026-W02",
)
)
actual = payload[0]["leftover_pairs"][0]["leftover_map_cross_share"]
assert actual == expected
json.dumps(payload, allow_nan=False)
Loading
Loading