-
Notifications
You must be signed in to change notification settings - Fork 1
feat: persist leftover interaction-map coordinates (v2.12.7) #481
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
68eadf7
b20d70c
732f2b2
7ec9aee
7ff509e
94dc79a
d192e8f
205abd7
75da7cf
3294497
e83fc9d
ede936c
4aa82de
b37721a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| ## 2.12.10 — Leftover-map criterion nodes | ||
|
|
||
| - Pair-member leftover-map criterion nodes open the leftover-pair post | ||
| (closest preferred, then farthest). Non-pair criteria stay | ||
| non-interactive diamonds so the map does not invent a next action | ||
| (ADR 0126). | ||
| - Keyboard Enter/Space matches leftover-map person nodes. Click does | ||
| not land leftover focus on Post quality. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| ## 2.12.7 — Leftover interaction map | ||
|
|
||
| - Persist complete-case leftover-map coordinates ξ / ζ after IRT main | ||
| effects (Jeon et al., 2021; Gabriel, 1971) in | ||
| `report_leftover_map_person` and `report_leftover_map_item` (ADR 0121). | ||
| - Render a 2D leftover interaction map above the leftover pair list. | ||
| Click a post node to open that post. Closest and farthest pairs stay | ||
| highlighted. Hidden posts stay hidden. | ||
| - Rank-0 and rank-1 maps pad the unused axis with zero. Missing residual | ||
| cells never enter the factorization as zero. | ||
| - Replay `0103_tenant_settings.sql` and | ||
| `0172_report_leftover_interaction_map.sql` on existing volumes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -351,7 +351,7 @@ async def persist_period_report( | |
| period_code: str, | ||
| report: PeriodReport, | ||
| ) -> None: | ||
| """Replace the stored report, member scores, leftover pairs, and item bank.""" | ||
| """Replace the stored report, member scores, leftover pairs, leftover map, and item bank.""" | ||
| await conn.execute( | ||
| """ | ||
| delete from report_period_score | ||
|
|
@@ -460,6 +460,38 @@ async def persist_period_report( | |
| pair.expected_response, | ||
| pair.leftover_map_rank, | ||
| ) | ||
| for person in report.leftover_map_persons: | ||
| await conn.execute( | ||
| """ | ||
| insert into report_leftover_map_person ( | ||
| grouping_kind, grouping_key, period_code, rubric_version, | ||
| post_id, axis_one, axis_two | ||
| ) values ($1,$2,$3,$4,$5,$6,$7) | ||
| """, | ||
| grouping_kind, | ||
| grouping_key, | ||
| period_code, | ||
| RUBRIC_VERSION, | ||
| person.post_id, | ||
| person.axis_one, | ||
| person.axis_two, | ||
| ) | ||
| for item in report.leftover_map_items: | ||
| await conn.execute( | ||
| """ | ||
| insert into report_leftover_map_item ( | ||
| grouping_kind, grouping_key, period_code, rubric_version, | ||
| criterion_code, axis_one, axis_two | ||
| ) values ($1,$2,$3,$4,$5,$6,$7) | ||
| """, | ||
| grouping_kind, | ||
| grouping_key, | ||
| period_code, | ||
| RUBRIC_VERSION, | ||
| item.criterion_code, | ||
| item.axis_one, | ||
| item.axis_two, | ||
| ) | ||
|
Comment on lines
+463
to
+494
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Map-row FK integrity relies on insert order and cascade The new map tables have composite FKs to Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def _groups_from_rows( | ||
|
|
@@ -620,6 +652,32 @@ async def fetch_period_reports( | |
| period_code, | ||
| RUBRIC_VERSION, | ||
| ) | ||
| # Safe SQL: the source-context expression is an immutable schema fragment; report keys are bound. | ||
| leftover_map_persons = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli | ||
| f""" | ||
| select lp.grouping_key, lp.post_id, lp.axis_one, lp.axis_two, p.post_title, | ||
| p.visibility_code, p.corporate_entity_id, | ||
| ({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context | ||
| from report_leftover_map_person lp | ||
| join source_post p on p.post_id = lp.post_id | ||
| where lp.grouping_kind = $1 and lp.period_code = $2 and lp.rubric_version = $3 | ||
| order by lp.grouping_key, p.post_title | ||
| """, | ||
| grouping_kind, | ||
| period_code, | ||
| RUBRIC_VERSION, | ||
| ) | ||
| leftover_map_items = await conn.fetch( | ||
| """ | ||
| select grouping_key, criterion_code, axis_one, axis_two | ||
| from report_leftover_map_item | ||
| where grouping_kind = $1 and period_code = $2 and rubric_version = $3 | ||
| order by grouping_key, criterion_code | ||
| """, | ||
| grouping_kind, | ||
| period_code, | ||
| RUBRIC_VERSION, | ||
| ) | ||
| status_labels = await labels_for_codes( | ||
| conn, | ||
| [row["ticket_status_code"] for row in members if row["ticket_status_code"]], | ||
|
|
@@ -633,6 +691,12 @@ async def fetch_period_reports( | |
| leftover_by_group: dict[str, list[asyncpg.Record]] = defaultdict(list) | ||
| for row in leftover: | ||
| leftover_by_group[row["grouping_key"]].append(row) | ||
| leftover_persons_by_group: dict[str, list[asyncpg.Record]] = defaultdict(list) | ||
| for row in leftover_map_persons: | ||
| leftover_persons_by_group[row["grouping_key"]].append(row) | ||
| leftover_items_by_group: dict[str, list[asyncpg.Record]] = defaultdict(list) | ||
| for row in leftover_map_items: | ||
| leftover_items_by_group[row["grouping_key"]].append(row) | ||
| payload: list[dict[str, Any]] = [] | ||
| for header in headers: | ||
| grouping_key = header["grouping_key"] | ||
|
|
@@ -724,6 +788,26 @@ async def fetch_period_reports( | |
| } | ||
| for row in leftover_by_group.get(header["grouping_key"], []) | ||
| ], | ||
| "leftover_map_persons": [ | ||
| { | ||
| "post_id": str(row["post_id"]), | ||
| "post_title": row["post_title"], | ||
| "axis_one": float(row["axis_one"]), | ||
| "axis_two": float(row["axis_two"]), | ||
| "visibility_code": row["visibility_code"], | ||
| "corporate_entity_id": str(row["corporate_entity_id"]), | ||
| "has_real_source_context": bool(row["has_real_source_context"]), | ||
| } | ||
| for row in leftover_persons_by_group.get(header["grouping_key"], []) | ||
| ], | ||
| "leftover_map_items": [ | ||
| { | ||
| "criterion_code": str(row["criterion_code"]), | ||
| "axis_one": float(row["axis_one"]), | ||
| "axis_two": float(row["axis_two"]), | ||
| } | ||
| for row in leftover_items_by_group.get(header["grouping_key"], []) | ||
| ], | ||
| } | ||
| ) | ||
| return payload | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # ADR 0121 — Persist leftover interaction-map coordinates | ||
|
|
||
| **Decision status:** Accepted | ||
| **Date:** 2026-08-23 | ||
|
|
||
| ## Context | ||
|
|
||
| ADR 0048 persists the closest and farthest leftover post–criterion | ||
| pairs after IRT main effects. Those pairs are two cells on the Jeon | ||
| et al. (2021, eq. 3) leftover interaction map `−γ‖ξ_p − ζ_i‖`. The | ||
| Gabriel (1971) biplot that produces the pairs already computes person | ||
| positions `ξ` and item positions `ζ`, then discards them. A buyer | ||
| who sees only two named pairs cannot see *why* those cells sat | ||
| closest or farthest, or where the other complete-case posts and | ||
| criteria sit on the same leftover map. | ||
|
|
||
| `fast-mlsirm` still exposes no leftover-map API. LineageWeave must | ||
| not fork LSIRM, invent a second IRT fit, or treat a missing residual | ||
| cell as a zero residual. | ||
|
|
||
| ## Decision | ||
|
|
||
| After a real GRM/GPCM score, keep the complete-case Gabriel | ||
| coordinates that leftover pairs already use. Persist every complete- | ||
| case post as `report_leftover_map_person` (`axis_one`, `axis_two`) | ||
| and every complete-case criterion as `report_leftover_map_item`. | ||
| Pad unused axes with zero when residual rank is below two. Do not | ||
| invent a second component. Closest/farthest selection and persisted | ||
| distance use those same two buyer-visible axes; unpersisted higher | ||
| components never silently change a highlighted map pair. Incomplete | ||
| rows and columns stay out of the factorization. | ||
|
|
||
| Cascade the rows with `report_period_score`. A leftover-map post | ||
| must also be a `report_member_score` row. A leftover-map criterion | ||
| must be a `report_item_information` item on that same report. Do not | ||
| store a second theta. A rank-0 residual still emits origin | ||
| coordinates so `make seed` is not empty; those zeros are not a | ||
| fabricated interaction. | ||
|
|
||
| Closest and farthest pairs remain ADR 0048 / ADR 0049. The map sits | ||
| **above** that pair list on the period-report group. Clicking a | ||
| person node opens that post with the same handler as a leftover | ||
| pair. Pair-member criterion nodes open that leftover-pair post | ||
| ([ADR 0126](0126-leftover-map-criterion-node.md)). Hidden posts stay hidden: leftover-map persons join | ||
| `source_post` and use the same ABAC gate as members and leftover | ||
| pairs. Missing map rows render nothing. | ||
|
|
||
| The biplot remains in `lineageweave/leftover_pairs.py` so leftover | ||
| tests do not import `period_report` or `fast_mlsirm`. | ||
|
|
||
| ## Consequences | ||
|
|
||
| Rebuild and seed write leftover-map coordinates in the same | ||
| transaction as leftover pairs. `GET /api/reports/{grouping}/{period}` | ||
| returns `leftover_map_persons` (with post title) and | ||
| `leftover_map_items`. Migration | ||
| `0172_report_leftover_interaction_map.sql` upgrades volumes that | ||
| already applied `0001` / `0012`. `migrate.sh` also replays `0103` | ||
| (`tenant_settings`) so existing volumes pick up that table. | ||
|
|
||
| ## Related | ||
|
|
||
| Depends on [ADR 0048](0048-persist-lsirm-leftover-pairs.md), | ||
| [ADR 0049](0049-leftover-pair-report-ui.md), and | ||
| [ADR 0003](0003-fast-mlsirm-report-integration.md). | ||
|
|
||
| ## References | ||
|
|
||
| Gabriel, K. R. (1971). The biplot graphic display of matrices with | ||
| application to principal component analysis. *Biometrika, 58*(3), | ||
| 453–467. https://doi.org/10.1093/biomet/58.3.453 | ||
|
|
||
| Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping | ||
| unobserved item–respondent interactions: A latent space item response | ||
| model with interaction map. *Psychometrika, 86*(2), 378–403. | ||
| https://doi.org/10.1007/s11336-021-09762-5 |
Uh oh!
There was an error while loading. Please reload this page.