From 0591a7001813d76ebabd3b1b6135b1923ca12289 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:30:24 +0900 Subject: [PATCH 1/2] fix: preserve lineage rebuild fallback transaction --- backend/app/lineage_ingestion.py | 7 +++--- tests/test_lineage_ingestion.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/backend/app/lineage_ingestion.py b/backend/app/lineage_ingestion.py index ec069bee5..ee0d3a09f 100644 --- a/backend/app/lineage_ingestion.py +++ b/backend/app/lineage_ingestion.py @@ -86,9 +86,10 @@ async def load_estimated_channel_weights( the fallback constants. """ try: - rows = await conn.fetch( - "select channel_code, weight_value from lineage_channel_weight" - ) + async with conn.transaction(): + rows = await conn.fetch( + "select channel_code, weight_value from lineage_channel_weight" + ) except asyncpg.UndefinedTableError: return None persisted = {row["channel_code"]: float(row["weight_value"]) for row in rows} diff --git a/tests/test_lineage_ingestion.py b/tests/test_lineage_ingestion.py index de8f289c6..2c53cbd43 100644 --- a/tests/test_lineage_ingestion.py +++ b/tests/test_lineage_ingestion.py @@ -5,7 +5,10 @@ import asyncio from datetime import datetime, timezone +import asyncpg + from backend.app.lineage_ingestion import ( + load_estimated_channel_weights, reconstruct_group_key, records_from_source_posts, visible_lineage_graph, @@ -14,6 +17,43 @@ from lineageweave.lineage_persistence import lineage_edge_specs +def test_missing_weight_table_rolls_back_before_fallback() -> None: + class _MissingTableConnection: + aborted = False + + class Savepoint: + def __init__(self, connection: _MissingTableConnection) -> None: + self.connection = connection + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, traceback) -> bool: + self.connection.aborted = False + return False + + def transaction(self): + return self.Savepoint(self) + + async def fetch(self, query: str): + if "lineage_channel_weight" in query: + self.aborted = True + raise asyncpg.UndefinedTableError("synthetic missing table") + if self.aborted: + raise asyncpg.InFailedSQLTransactionError("transaction is aborted") + return [] + + connection = _MissingTableConnection() + weights = asyncio.run( + load_estimated_channel_weights( + connection, {"temporal", "secondary_key", "text"} + ) + ) + asyncio.run(connection.fetch("select 1")) + + assert weights is None + + def test_records_use_persisted_thread_keys_not_process_unit_or_voc_type() -> None: rows = [ { From b895aa57d8b7c8b70f8830ecf32a44972199e6a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 00:32:10 +0900 Subject: [PATCH 2/2] docs: name fast-mlsirm cluster contract accurately --- docs/adr/0145-psychometric-channel-weight-estimation.md | 5 +++-- lineageweave/channel_weight_estimation.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0145-psychometric-channel-weight-estimation.md b/docs/adr/0145-psychometric-channel-weight-estimation.md index 269e21c4f..fbc53d8d4 100644 --- a/docs/adr/0145-psychometric-channel-weight-estimation.md +++ b/docs/adr/0145-psychometric-channel-weight-estimation.md @@ -32,7 +32,7 @@ exactly that information (McNeish & Wolf, 2020). Pairs are nested inside reconstruction groups (process unit / corporate entity / thread), so a single-level fit would commit the ecological/atomistic inference error the standing mandate calls out (Robinson, 1950); `fast-mlsirm`'s MLS2PLM -is a *multilevel* 2PL whose `factor_id` models exactly this nesting, and +is a *multilevel* 2PL whose `cluster_id` models exactly this nesting, and its `MLSIRMParams.alpha` field is the per-item log-discrimination (natural-scale discrimination `exp(alpha)` is positive by construction, so normalizing to sum 1 always yields valid convex weights). @@ -49,7 +49,8 @@ unconfigured, never a fabricated result); the same pattern applies here. weights by fitting `fast-mlsirm`'s MLS2PLM over observed channel scores: items = channels, respondents = candidate pairs sampled the same way `reconstruct` forms them (same grouping, same candidate - window), `factor_id` = the pair's reconstruction group (multilevel + window), `factor_id` assigns every channel item to the one relatedness + trait, and `cluster_id` = the pair's reconstruction group (multilevel nesting per Robinson, 1950). Estimated weights are the normalized natural-scale discriminations, `exp(alpha_j) / Σ exp(alpha_k)` (Birnbaum, 1968). diff --git a/lineageweave/channel_weight_estimation.py b/lineageweave/channel_weight_estimation.py index bbdf67623..78a30d986 100644 --- a/lineageweave/channel_weight_estimation.py +++ b/lineageweave/channel_weight_estimation.py @@ -65,7 +65,7 @@ def estimate_channel_weights( carry the same channel set -- a pair missing a channel is a caller bug, not missing data to impute. group_ids: the reconstruction-group index of each pair (same - length/order), used as MLS2PLM's multilevel ``factor_id``. + length/order), used as MLS2PLM's multilevel ``cluster_id``. Returns: The estimate, or ``None`` whenever a grounded estimate cannot be