From 2cefc0b63436989210bca3204e7750e87edba662 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 15:40:21 +0000 Subject: [PATCH 01/43] feat(reports): leftover-map cross share on grouping comparison strip (v2.53.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caption persisted leftover-map cross share x = 2R̂U/R² on grouping comparison leftover-pair buttons through formatLeftoverMapCrossShare (ADR 0296). Distinct aria-label Leftover map comparison cross share. Missing or non-finite x omits that badge and keeps s, e, R̂, d, and coverage notes. Do not invent x from R̂, U, and R. Finite negative share is shown, never clamped. UI-only; no new columns. --- AGENTS.md | 10 +- ARCHITECTURE.md | 7 +- ...2.53.0-leftover-map-compare-cross-share.md | 47 ++++ CHANGELOG.md | 46 ++++ CLAUDE.md | 2 +- .../0296-leftover-map-compare-cross-share.md | 200 ++++++++++++++++++ docs/product-technical-gap-baseline.md | 79 +++++++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 26 +++ frontend/src/App.tsx | 15 ++ frontend/src/components/LeftoverPairList.tsx | 3 +- frontend/src/i18n.test.ts | 14 ++ frontend/src/i18n.ts | 4 + frontend/src/leftoverMapCrossShare.test.ts | 14 +- frontend/src/leftoverMapCrossShare.ts | 3 + frontend/src/leftoverMapPlotLayout.ts | 5 +- pyproject.toml | 2 +- 17 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.d/2.53.0-leftover-map-compare-cross-share.md create mode 100644 docs/adr/0296-leftover-map-compare-cross-share.md diff --git a/AGENTS.md b/AGENTS.md index 5cd94d89f..a767bd321 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,7 +273,7 @@ stops startup instead of leaving a healthy-looking partial schema, and application code must not compensate for a missing table. Period leftover pairs (ADR 0017 / 0018 / 0048 / 0049 / 0119 / 0158 / 0162 / -0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295) are computed in `lineageweave/leftover_pairs.py` from the +0163 / 0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Distances are Euclidean on the two-dimensional Gabriel leftover map; missing cells stay out of the factorization. Closest and farthest post–criterion pairs @@ -434,6 +434,12 @@ compares leftover pairs can match the pair-row `U²/R²` badge; a missing or non `s` omits that leftover-map comparison unexplained leftover share badge and does not invent `s` from `U` and `R` or from leftover-map distance. The strip does not gain the leftover-map graphic. +ADR 0296 captions grouping comparison leftover-pair buttons with persisted leftover-map +cross share `x` through formatLeftoverMapCrossShare so a buyer who +compares leftover pairs can match the pair-row `2R̂U/R²` badge; a missing or non-finite +`x` omits that leftover-map comparison cross share badge and does not invent +`x` from `R̂`, `U`, and `R` or from leftover-map distance. The strip does not gain the +leftover-map graphic. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, `e + s + x = 1`. When `Y`, `E`, and `R` are finite, `Y − E = R`. When `R`, `R̂`, and `U` are @@ -462,6 +468,8 @@ Grouping comparison explained leftover share (ADR 0294) captions grouping compar buttons with persisted leftover-map explained leftover share `e`. Grouping comparison unexplained leftover share (ADR 0295) captions grouping comparison leftover-pair buttons with persisted leftover-map unexplained leftover share `s`. +Grouping comparison leftover-map cross share (ADR 0296) captions grouping comparison leftover-pair +buttons with persisted leftover-map cross share `x`. Item complete-case coverage (ADR 0282) captions the leftover-map graphic with how many scored criteria entered the map. Item complete-case coverage on the pair list (ADR 0285) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e2e6b7be8..d8a47c972 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -681,6 +681,8 @@ ADR 0294 captions grouping comparison leftover-pair buttons with persisted lefto explained leftover share `e` through formatLeftoverMapExplainedShare. ADR 0295 captions grouping comparison leftover-pair buttons with persisted leftover-map unexplained leftover share `s` through formatLeftoverMapUnexplainedShare. +ADR 0296 captions grouping comparison leftover-pair buttons with persisted leftover-map +cross share `x` through formatLeftoverMapCrossShare. Click a post marker or a pair button opens that post. Those ADRs are the normative mathematical, storage, and display contracts. Leftover-map axis share @@ -703,8 +705,9 @@ leftover-map complete-case coverage (ADR 0289), leftover-map item complete-case coverage (ADR 0290), leftover-map incomplete post coverage (ADR 0291), leftover-map incomplete item coverage (ADR 0292), leftover-map reconstruction `R̂` (ADR 0293), leftover-map explained leftover share `e` -(ADR 0294), and leftover-map unexplained leftover share `s` -(ADR 0295); +(ADR 0294), leftover-map unexplained leftover share `s` +(ADR 0295), and leftover-map cross share `x` +(ADR 0296); `POST .../rebuild` scores every grouping kind (post_admin). `make seed` folds A-100/B-200 Event Lineage fixtures (and the Riverbend calendar post) that already have constructed IRT cells into the same shared diff --git a/CHANGELOG.d/2.53.0-leftover-map-compare-cross-share.md b/CHANGELOG.d/2.53.0-leftover-map-compare-cross-share.md new file mode 100644 index 000000000..aa34afbbd --- /dev/null +++ b/CHANGELOG.d/2.53.0-leftover-map-compare-cross-share.md @@ -0,0 +1,47 @@ +## 2.53.0 — Leftover-map cross share on grouping comparison strip pair rows + +- Caption leftover-map cross share `x` on grouping comparison strip + leftover-pair buttons through formatLeftoverMapCrossShare (ADR 0296). + After `make seed`, closest and farthest leftover pairs sit above the + member list with the Gabriel biplot of already-named coordinates, + leftover-map axis share when finite, axis ticks that match + `ξ (x, y) ζ (x, y)` on the pair row, pair segments that match `d` on + the pair row, pair segments that match `R̂` on the pair row, pair + segments that match `R̂²/R²` on the pair row, pair segments that match + `U²/R²` on the pair row, pair segments that match `2R̂U/R²` on the pair + row, pair segments that match `U` on the pair row, pair segments that + match `R` on the pair row, pair segments that match `Y` on the pair row, + pair segments that match `E` on the pair row, pair segments that match + leftover-map rank on the pair row, a plot caption that matches + leftover-map complete-case coverage above the pair list, a plot caption + that matches leftover-map item complete-case coverage, a plot caption + that matches leftover-map incomplete post coverage, a plot caption that + matches leftover-map incomplete item coverage, a pair-list note that + matches leftover-map post complete-case coverage, a pair-list note that + matches leftover-map item complete-case coverage, a pair-list note that + matches leftover-map incomplete post coverage, a pair-list note that + matches leftover-map incomplete item coverage, a grouping comparison + note that matches leftover-map post complete-case coverage, a grouping + comparison note that matches leftover-map item complete-case coverage, + a grouping comparison note that matches leftover-map incomplete post + coverage, a grouping comparison note that matches leftover-map + incomplete item coverage, grouping comparison leftover-pair buttons + that match leftover-map reconstruction `R̂` on the pair row, grouping + comparison leftover-pair buttons that match leftover-map explained + leftover share `e` on the pair row, grouping comparison leftover-pair + buttons that match leftover-map unexplained leftover share `s` on the + pair row, and grouping comparison leftover-pair buttons that match + leftover-map cross share `x` on the pair row; click a post marker or a + pair button opens that post. A missing or non-finite `x` omits that + leftover-map comparison cross share badge and keeps leftover-map + unexplained leftover share `s` when finite, leftover-map explained + leftover share `e` when finite, leftover-map reconstruction `R̂` when + finite, leftover-map distance `d`, plus the strip coverage notes. Do + not invent `x` from `R̂`, `U`, and `R`, leftover-map distance, plotted + coordinates, leftover-map reconstruction, leftover residual, + leftover-map rank, leftover-map post coverage, leftover-map item + coverage, leftover-map incomplete post coverage, leftover-map + incomplete item coverage, or the count of unused axes. A finite + negative share is shown, never clamped. The strip does not gain the + leftover-map graphic. Never invent a leftover score. Never invent a + theta. No new columns. diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b2d3e75..c57a69e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,52 @@ All notable changes to this project are documented here. Format follows ### Added +- Period leftover pairs now caption grouping comparison leftover-pair + buttons with persisted leftover-map cross share `x` through + formatLeftoverMapCrossShare (ADR 0296 / v2.53.0). After `make seed`, + closest and farthest leftover pairs sit above the member list with the + Gabriel biplot of already-named coordinates, leftover-map axis share when + finite, axis ticks that match `ξ (x, y) ζ (x, y)` on the pair row, pair + segments that match `d` on the pair row, pair segments that match `R̂` on + the pair row, pair segments that match `R̂²/R²` on the pair row, pair + segments that match `U²/R²` on the pair row, pair segments that match + `2R̂U/R²` on the pair row, pair segments that match `U` on the pair row, + pair segments that match `R` on the pair row, pair segments that match + `Y` on the pair row, pair segments that match `E` on the pair row, pair + segments that match leftover-map rank on the pair row, a plot caption + that matches leftover-map complete-case coverage above the pair list, a + plot caption that matches leftover-map item complete-case coverage, a + plot caption that matches leftover-map incomplete post coverage, a plot + caption that matches leftover-map incomplete item coverage, a pair-list + note that matches leftover-map post complete-case coverage, a pair-list + note that matches leftover-map item complete-case coverage, a pair-list + note that matches leftover-map incomplete post coverage, a pair-list + note that matches leftover-map incomplete item coverage, a grouping + comparison note that matches leftover-map post complete-case coverage, + a grouping comparison note that matches leftover-map item + complete-case coverage, a grouping comparison note that matches + leftover-map incomplete post coverage, a grouping comparison note + that matches leftover-map incomplete item coverage, grouping comparison + leftover-pair buttons that match leftover-map reconstruction `R̂` on the + pair row, grouping comparison leftover-pair buttons that match + leftover-map explained leftover share `e` on the pair row, grouping + comparison leftover-pair buttons that match leftover-map unexplained + leftover share `s` on the pair row, and grouping comparison leftover-pair + buttons that match leftover-map cross share `x` on the pair row; click a + post marker or a pair button opens that post. A missing or non-finite + `x` omits that leftover-map comparison cross share badge and keeps + leftover-map unexplained leftover share `s` when finite, leftover-map + explained leftover share `e` when finite, leftover-map reconstruction + `R̂` when finite, leftover-map distance `d`, plus the strip coverage + notes. Do not invent `x` from `R̂`, `U`, and `R`, leftover-map distance, + plotted coordinates, leftover-map reconstruction, leftover residual, + leftover-map rank, leftover-map post coverage, leftover-map item + coverage, leftover-map incomplete post coverage, leftover-map + incomplete item coverage, or the count of unused axes. A finite + negative share is shown, never clamped. The strip does not gain the + leftover-map graphic. Never invent a leftover score or a theta. No new + columns. + - Period leftover pairs now caption grouping comparison leftover-pair buttons with persisted leftover-map unexplained leftover share `s` through formatLeftoverMapUnexplainedShare (ADR 0295 / v2.52.0). After `make seed`, diff --git a/CLAUDE.md b/CLAUDE.md index fecef469e..6360d511e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ cutoff. Global Ask optional `knowledge_cutoff` uses the same cover Create/start endpoint rules (ADR 0017 / 0021), tie-vs-miss similarity (ADR 0026), R&R catalog ids (ADR 0019 / 0027), leftover pairs -(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295), occupational construct catalog search +(ADR 0048–0164 / 0182 / 0185 / 0201 / 0233 / 0266 / 0267 / 0268 / 0269 / 0270 / 0271 / 0272 / 0273 / 0274 / 0275 / 0276 / 0277 / 0278 / 0279 / 0280 / 0281 / 0282 / 0283 / 0284 / 0285 / 0286 / 0287 / 0288 / 0289 / 0290 / 0291 / 0292 / 0293 / 0294 / 0295 / 0296), occupational construct catalog search (ADR 0257), the text-channel embedding swap and cosine clamp (ADR 0190), per-edge channel-score persistence (ADR 0195), token-backed status notices (ADR 0220), diff --git a/docs/adr/0296-leftover-map-compare-cross-share.md b/docs/adr/0296-leftover-map-compare-cross-share.md new file mode 100644 index 000000000..275543772 --- /dev/null +++ b/docs/adr/0296-leftover-map-compare-cross-share.md @@ -0,0 +1,200 @@ +# ADR 0296 — Name leftover-map cross share on grouping comparison strip pair rows + +**Decision status:** Accepted +**Date:** 2026-08-31 + +Amends leftover pairs on the grouping comparison strip +([ADR 0149](0149-leftover-pairs-on-comparison-strip.md)), leftover-map +cross share persistence +([ADR 0185](0185-leftover-map-cross-share.md)), leftover-map cross share on +pair segments +([ADR 0275](0275-leftover-map-segment-cross-share.md)), leftover-map +unexplained leftover share on grouping comparison strip pair rows +([ADR 0295](0295-leftover-map-compare-unexplained-share.md)), leftover-map +explained leftover share on grouping comparison strip pair rows +([ADR 0294](0294-leftover-map-compare-explained-share.md)), leftover-map +reconstruction on grouping comparison strip pair rows +([ADR 0293](0293-leftover-map-compare-reconstruction.md)), leftover-map +incomplete item coverage on the grouping comparison strip +([ADR 0292](0292-leftover-map-compare-incomplete-item.md)), leftover-map +incomplete post coverage on the grouping comparison strip +([ADR 0291](0291-leftover-map-compare-incomplete-post.md)), leftover-map item +complete-case coverage on the grouping comparison strip +([ADR 0290](0290-leftover-map-compare-item-coverage.md)), leftover-map +complete-case coverage on the grouping comparison strip +([ADR 0289](0289-leftover-map-compare-coverage.md)), leftover-map +unexplained leftover share persistence +([ADR 0233](0233-leftover-map-unexplained-share.md)), leftover-map unexplained +leftover share on pair segments +([ADR 0274](0274-leftover-map-segment-unexplained-share.md)), leftover-map +explained leftover share persistence +([ADR 0266](0266-leftover-map-explained-share.md)), leftover-map explained +leftover share on pair segments +([ADR 0273](0273-leftover-map-segment-explained-share.md)), leftover-map +reconstruction persistence ([ADR 0201](0201-leftover-map-reconstruction.md)), +and leftover-map reconstruction on pair segments +([ADR 0272](0272-leftover-map-segment-reconstruction.md)). Independent of leftover-map +post complete-case coverage fail-closed on the pair list +([ADR 0288](0288-leftover-map-list-post-coverage-helper.md)), leftover-map +incomplete item coverage on the pair list +([ADR 0287](0287-leftover-map-list-incomplete-item.md)), leftover-map incomplete +post coverage on the pair list +([ADR 0286](0286-leftover-map-list-incomplete-post.md)), leftover-map item +complete-case coverage on the pair list +([ADR 0285](0285-leftover-map-list-item-coverage.md)), leftover-map incomplete +item coverage on the graphic display +([ADR 0284](0284-leftover-map-plot-incomplete-item.md)), leftover-map incomplete +post coverage on the graphic display +([ADR 0283](0283-leftover-map-plot-incomplete.md)), leftover-map item +complete-case coverage on the graphic display +([ADR 0282](0282-leftover-map-plot-item-coverage.md)), leftover-map complete-case +coverage on the graphic display +([ADR 0281](0281-leftover-map-plot-coverage.md)), leftover-map rank on pair +segments ([ADR 0280](0280-leftover-map-segment-rank.md)), leftover expected on pair +segments ([ADR 0279](0279-leftover-map-segment-expected.md)), leftover observed on +pair segments ([ADR 0278](0278-leftover-map-segment-observed.md)), leftover +residual on pair segments ([ADR 0277](0277-leftover-map-segment-residual.md)), +leftover-map unexplained leftover on pair segments +([ADR 0276](0276-leftover-map-segment-unexplained-leftover.md)), leftover-map +distance on pair segments ([ADR 0271](0271-leftover-map-segment-distance.md)), +leftover-map coordinate ticks ([ADR 0270](0270-leftover-map-coordinate-ticks.md)), +leftover-map axis share on the graphic display +([ADR 0269](0269-leftover-map-axis-share-plot.md)), leftover residual +disclosure ([ADR 0162](0162-leftover-residual-disclosure.md)), leftover +observed `Y` / expected `E` ([ADR 0163](0163-leftover-observed-expected.md)), +leftover-map unexplained leftover persistence +([ADR 0182](0182-leftover-map-unexplained.md)), leftover-map rank +persistence ([ADR 0164](0164-leftover-map-rank.md)), and leftover-map axis +share persistence ([ADR 0148](0148-leftover-map-axis-share.md)). + +## Context + +ADR 0185 already persists leftover-map cross share +`x = 2 R̂ U / R²` of raw residual after two-axis Gabriel reconstruction. +ADR 0049 already names that share on the period-report pair list. ADR 0275 +already names it on leftover-map graphic-display pair segments. ADR 0295 +already captions persisted leftover-map unexplained leftover share `s` on +grouping comparison leftover-pair buttons. The strip pair button still does +not name `x`, so a buyer who compares leftover pairs can treat leftover-map +unexplained leftover share `s`, leftover-map explained leftover share `e`, +leftover-map reconstruction `R̂`, or leftover-map distance `d` as the leftover +identity remainder even after the pair list and graphic already name +`2R̂U/R²`. When `R`, `R̂`, `U`, `x`, `s`, and `e` are finite, +`e + s + x = 1`; hiding `x` on the strip lets that identity vanish without +a next action. + +This increment captions persisted leftover-map cross share on each grouping +comparison leftover-pair button through formatLeftoverMapCrossShare. It does +not add columns. It does not recompute `x` from `R̂`, `U`, and `R`, leftover-map +distance, plotted coordinates, leftover-map reconstruction, leftover residual, +leftover-map explained leftover share, or leftover-map unexplained leftover +share. It does not persist leftover-map inner product, cosine, or length as +separate columns. Do not invent a leftover score. Do not invent a theta. + +The dashboard stack already used neighbouring leftover facts under other +numbers. This protected increment uses **0296** so it does not collide with +leftover-map unexplained leftover share on grouping comparison strip pair +rows (0295), leftover-map explained leftover share on grouping comparison +strip pair rows (0294), leftover-map reconstruction on grouping comparison +strip pair rows (0293), leftover-map incomplete item coverage on the grouping +comparison strip (0292), leftover-map incomplete post coverage on the grouping +comparison strip (0291), leftover-map item complete-case coverage on the +grouping comparison strip (0290), leftover-map complete-case coverage on the +grouping comparison strip (0289), leftover-map cross share on pair segments +(0275), leftover-map cross share persistence (0185), leftover-map unexplained +leftover share on pair segments (0274), leftover-map unexplained leftover +share persistence (0233), leftover-map explained leftover share on pair +segments (0273), leftover-map explained leftover share persistence (0266), +leftover-map reconstruction on pair segments (0272), leftover-map +reconstruction persistence (0201), or the dashboard stacks. + +## Decision + +On the grouping comparison strip, caption each leftover pair button with +the same persisted leftover-map cross share formatter as the pair-row +`2R̂U/R²` badge, next to leftover-map unexplained leftover share `s`, +leftover-map explained leftover share `e`, leftover-map reconstruction `R̂`, +and leftover-map distance `d`. Use the distinct accessible name +`Leftover map comparison cross share` so the strip badge is not the graphic +caption (`leftover-map cross share {label}`). A missing or non-finite `x` +omits that leftover-map comparison cross share badge and keeps leftover-map +unexplained leftover share `s` when finite, leftover-map explained leftover +share `e` when finite, leftover-map reconstruction `R̂` when finite, +leftover-map distance `d`, the strip leftover-map post coverage note, +leftover-map item coverage note, leftover-map incomplete post note, +leftover-map incomplete item note, leftover pairs, and any leftover-map +captions on the pair list and graphic. Rank-0 origin cells still name +`2R̂U/R² 0.00` when that persisted share is finite. A finite negative share +is shown, never clamped. Do not invent `x` from `R̂`, `U`, and `R`, leftover-map +distance, plotted coordinates, leftover-map reconstruction, leftover residual, +leftover-map explained leftover share, leftover-map unexplained leftover +share, leftover-map rank, leftover-map post coverage, leftover-map item +coverage, leftover-map incomplete post coverage, leftover-map incomplete +item coverage, or the count of unused axes. Do not add the leftover-map +graphic to the strip. Click a leftover pair on the strip to open that post. + +Do not add SQL. Do not edit shipped migrations. Do not persist inner +product, cosine, or length as separate columns. Do not invent a leftover +score. Do not invent a theta. + +## Consequences + +After `make seed`, the grouping comparison strip names persisted leftover-map +post complete-case coverage, persisted leftover-map item complete-case +coverage, persisted leftover-map incomplete post coverage, and persisted +leftover-map incomplete item coverage on each grouping row when +leftoverMapCoverageCounts / leftoverMapItemCoverageCounts / +leftoverMapIncompletePostCount / leftoverMapIncompleteItemCount return usable +integers, then names leftover pairs with leftover-map distance `d`, persisted +leftover-map reconstruction `R̂` when finite, persisted leftover-map +explained leftover share `e` when finite, persisted leftover-map unexplained +leftover share `s` when finite, and persisted leftover-map cross share `x` +when finite. Closest and farthest leftover pairs still sit above the member +list with the leftover-map graphic display; click a post marker or a pair +button opens that post. +Hidden posts stay hidden. When `R`, `R̂`, `U`, `x`, `s`, and `e` are +finite, `e + s + x = 1`. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover-map +complete-case coverage persistence, leftover-map axis share persistence, +leftover pairs on the grouping comparison strip, leftover-map complete-case +coverage on the grouping comparison strip, leftover-map item complete-case +coverage on the grouping comparison strip, leftover-map incomplete post +coverage on the grouping comparison strip, leftover-map incomplete item +coverage on the grouping comparison strip, leftover-map reconstruction on +grouping comparison strip pair rows, leftover-map explained leftover share +on grouping comparison strip pair rows, leftover-map unexplained leftover +share on grouping comparison strip pair rows, leftover-map inner product, +leftover-map cosine, leftover-map length, leftover-map graphic display, +leftover-map cross share on pair segments, leftover-map cross share +persistence, leftover-map unexplained leftover share on pair segments, +leftover-map unexplained leftover share persistence, leftover-map explained +leftover share on pair segments, leftover-map explained leftover share +persistence, leftover-map reconstruction on pair segments, leftover-map +reconstruction persistence, leftover-map item complete-case coverage on the +graphic display, leftover-map item complete-case coverage on the pair list, +leftover-map incomplete post coverage on the graphic display, leftover-map +incomplete post coverage on the pair list, leftover-map incomplete item +coverage on the graphic display, leftover-map incomplete item coverage on +the pair list, and leftover-map post complete-case coverage fail-closed on +the pair list. + +## 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 +(LSIRM interaction `−γ‖ξ_j − ζ_i‖` after main effects +`α_j − β_i`; typically `p = 2` for the interaction map. Cross share of the +leftover cell is `x = 2 R̂ U / R²` after two-axis Gabriel reconstruction. +Grouping comparison leftover-map cross share names that persisted remainder +on the strip pair row only when formatLeftoverMapCrossShare returns a usable +badge. When finite, `e + s + x = 1`.) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 507437366..21642a618 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,84 @@ # Product & Technical Gap Baseline +> Exact-head loop overlay: 2026-08-31 00:48 KST. Protected `main` is +> `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash +> of leftover-map coordinates (v2.24.0 / #782). Revert #808 +> (`revert-pr782-unauthorized` @ `1af3e53e`) restores `main` toward +> `fc13acaa` (v2.23.0) and still needs independent APPROVE. GitHub +> writes work (comment/close/create-PR/push). Token still has empty +> `X-OAuth-Scopes`; repo permission is ADMIN. Open leftover stack +> still lacks independent APPROVE. Do not self-approve. +> #782 leftover-map coordinates through pair-segment distance +> (v2.24.0–v2.28.0 / ADR 0267–0271) is `9bdd3e4d`. #802 explained +> leftover share on pair segments (v2.30.0 / ADR 0273, includes v2.29 +> reconstruction) is `79ec22f0`. #803 unexplained leftover share on pair +> segments (v2.31.0 / ADR 0274) is `089a1571`. #804 leftover-map cross +> share on pair segments (v2.32.0 / ADR 0275) is `c513002b`. #805 leftover-map +> unexplained leftover `U` on pair segments (v2.33.0 / ADR 0276) is +> `e3fdfd74`. #806 leftover residual `R` on pair segments (v2.34.0 / +> ADR 0277) is `33f6c3dd`. #809 leftover observed `Y` on pair segments +> (v2.35.0 / ADR 0278) is `b334b00e`. #810 leftover expected `E` on pair +> segments (v2.36.0 / ADR 0279) is `6e37757a`. #811 leftover-map rank on +> pair segments (v2.37.0 / ADR 0280) is `e626a1d0`. #812 leftover-map +> complete-case coverage on the graphic (v2.38.0 / ADR 0281) is `64964cb6`. +> #813 leftover-map item complete-case coverage on the graphic (v2.39.0 / +> ADR 0282) is `de2a8a8b`. #814 leftover-map incomplete post coverage on +> the graphic (v2.40.0 / ADR 0283) is `77292872`. #815 leftover-map +> incomplete item coverage on the graphic (v2.41.0 / ADR 0284) is +> `63092de`. #816 leftover-map item complete-case coverage on the pair +> list (v2.42.0 / ADR 0285) is `1e3d13e`. #817 leftover-map incomplete +> post coverage on the pair list (v2.43.0 / ADR 0286) is `ef30930`. +> #818 leftover-map incomplete item coverage on the pair list +> (v2.44.0 / ADR 0287) is `aa32089`. #819 leftover-map post complete-case +> coverage fail-closed on the pair list (v2.45.0 / ADR 0288) is `a2c96551`. +> #821 leftover-map complete-case coverage on the grouping comparison +> strip (v2.46.0 / ADR 0289) is `11a78553`. #822 leftover-map item +> complete-case coverage on the grouping comparison strip (v2.47.0 / +> ADR 0290) is `6726353a`. #823 leftover-map singular values on leftover-axis +> badges (v2.47.0 / ADR 0290, plot stack) is `eb172c40`. #824 leftover-map +> incomplete post coverage on the grouping comparison strip (v2.48.0 / +> ADR 0291) is `8b2ed95`. #825 leftover-map incomplete item coverage on +> the grouping comparison strip (v2.49.0 / ADR 0292) is `bca6baa`. #826 leftover-map +> reconstruction `R̂` on grouping comparison leftover-pair buttons +> (v2.50.0 / ADR 0293) is `a4bf239`. #827 leftover-map explained leftover +> share `e` on grouping comparison leftover-pair buttons (v2.51.0 / +> ADR 0294) is `ead81bd`. #829 leftover-map unexplained leftover share +> `s` on grouping comparison leftover-pair buttons (v2.52.0 / ADR 0295) +> is `4d82c28`. Leave +> #782/#802/#803/#804/#805/#806/#808/#809/#810/#811/#812/#813/#814/#815/#816/#817/#818/#819/#821/#822/#823/#824/#825/#826/#827/#829 open for +> independent review. Do not squash-merge stacked leftover PRs onto an +> unprotected leftover base. Do not merge #808 without independent APPROVE. +> Strix on #782 failed closed at `Run Strix (quick)`; do not weaken +> fail-closed on `Vulnerabilities [1-9]`. Org Strix `ModelBehaviorError` +> classifier is already on ContextualWisdomLab/.github. Copilot review is +> not independent APPROVE. Issues #79 and #87 stay open. #96 is already +> closed. Only collaborator is `seonghobae`; no independent reviewer can +> be requested from this token. +> +> Next buyer increment on this cycle: leftover-map cross share +> `x` on grouping comparison leftover-pair buttons (ADR 0296 / v2.53.0) +> delivered locally on `feat/leftover-map-compare-cross-share-v2530`. +> Caption each strip leftover-pair button through +> formatLeftoverMapCrossShare with distinct aria-label +> `Leftover map comparison cross share` next to leftover-map unexplained +> leftover share `s`, leftover-map explained leftover share `e`, leftover-map +> reconstruction `R̂`, and leftover-map distance `d`. UI-only; no new +> columns. Missing or non-finite `x` omits that leftover-map comparison +> cross share badge independently of `s`, `e`, `R̂`, `d`, and coverage +> notes. Do not invent `x` from `R̂`, `U`, and `R`, leftover-map distance, +> plotted coordinates, leftover-map reconstruction, leftover residual, +> leftover-map explained leftover share, leftover-map unexplained leftover +> share, leftover-map rank, leftover-map post coverage, leftover-map item +> coverage, leftover-map incomplete post coverage, leftover-map incomplete +> item coverage, or the count of unused axes. A finite negative share is +> shown, never clamped. Do not caption the leftover-map graphic on the +> strip. Do not invent leftover scores. Stack onto leftover branch +> `feat/leftover-map-compare-unexplained-share-v2520` / #829; leave the PR +> open for independent review. Do not squash-merge onto the leftover +> base. Do not persist leftover-map inner product, cosine, or length as +> separate columns. + + > Exact-head loop overlay: 2026-08-31 00:42 KST. Protected `main` is > `cb187cadee5fb6c46d8a944815ccc154a1e028d1` after unauthorized squash > of leftover-map coordinates (v2.24.0 / #782). Revert #808 diff --git a/frontend/package.json b/frontend/package.json index 10d0e822c..ff2d6a69f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.52.0", + "version": "2.53.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 0649589c4..5b43c2657 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -935,6 +935,7 @@ describe("App, authenticated", () => { leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, + leftover_map_cross_share: 0.12, }, ], leftover_map_coverage: { @@ -4381,6 +4382,21 @@ describe("App, authenticated", () => { "Leftover map comparison unexplained leftover share", ), ).toHaveLength(1); + expect( + within(screen.getByLabelText("Grouping comparison")).queryByLabelText( + /leftover-map cross share/, + ), + ).not.toBeInTheDocument(); + expect( + within(screen.getByLabelText("Grouping comparison")).getByLabelText( + "Leftover map comparison cross share", + ), + ).toHaveTextContent("2R̂U/R² 0.12"); + expect( + within(screen.getByLabelText("Grouping comparison")).getAllByLabelText( + "Leftover map comparison cross share", + ), + ).toHaveLength(1); expect( screen.getByRole("button", { name: /open leftover closest pair from comparison: specification revision requested/i, @@ -4401,6 +4417,11 @@ describe("App, authenticated", () => { name: /open leftover closest pair from comparison: specification revision requested/i, }), ).not.toHaveTextContent("U²/R²"); + expect( + screen.getByRole("button", { + name: /open leftover closest pair from comparison: specification revision requested/i, + }), + ).not.toHaveTextContent("2R̂U/R²"); expect( screen.getByRole("button", { name: "Compare Business unit (PU): Demo Report High, mean θ 0.81" }), ).toHaveTextContent("mean θ 0.81"); @@ -4435,6 +4456,11 @@ describe("App, authenticated", () => { name: /open leftover closest pair from comparison: public post/i, }), ).toHaveTextContent("U²/R² 0.02"); + expect( + screen.getByRole("button", { + name: /open leftover closest pair from comparison: public post/i, + }), + ).toHaveTextContent("2R̂U/R² 0.12"); await waitFor(() => expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/api/reports/thread_group/2026-W02"), diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 68e4f1a6a..02afbab9c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -149,6 +149,10 @@ import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, } from "./leftoverMapUnexplainedShare"; +import { + formatLeftoverMapCrossShare, + LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL, +} from "./leftoverMapCrossShare"; import "./App.css"; const AdminPanel = lazy(() => import("./components/AdminPanel").then((module) => ({ default: module.AdminPanel }))); @@ -4080,6 +4084,9 @@ function ReportsPanel({ const unexplainedShare = formatLeftoverMapUnexplainedShare( pair.leftover_map_unexplained_share, ); + const crossShare = formatLeftoverMapCrossShare( + pair.leftover_map_cross_share, + ); return (
  • ) : null} + {crossShare ? ( + + {crossShare} + + ) : null}
  • ); diff --git a/frontend/src/components/LeftoverPairList.tsx b/frontend/src/components/LeftoverPairList.tsx index aa6e6ee85..195767c48 100644 --- a/frontend/src/components/LeftoverPairList.tsx +++ b/frontend/src/components/LeftoverPairList.tsx @@ -92,7 +92,8 @@ export type LeftoverPairListProps = { * buttons, not this pair list. ADR 0294 names persisted leftover-map explained leftover * share on grouping comparison leftover-pair buttons, not this pair list. ADR 0295 names * persisted leftover-map unexplained leftover share on grouping comparison leftover-pair - * buttons, not this pair list. Every badge still + * buttons, not this pair list. ADR 0296 names persisted leftover-map cross share on + * grouping comparison leftover-pair buttons, not this pair list. Every badge still * renders together before opening the named post. */ export function LeftoverPairList({ diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index 41bc10a18..8ecc22dce 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -84,6 +84,7 @@ describe("i18n", () => { "Leftover map comparison reconstruction", "Leftover map comparison explained leftover share", "Leftover map comparison unexplained leftover share", + "Leftover map comparison cross share", "Leftover-map graphic item coverage", "Leftover map item coverage", "Leftover map incomplete posts", @@ -463,6 +464,19 @@ describe("i18n", () => { }, ); + it.each([ + ["ko", "잔여 지도 비교 교차 점유율"], + ["zh", "残差地图比较交叉份额"], + ["ja", "残差マップの比較交差割合"], + ["vi", "Phần giao so sánh bản đồ phần dư"], + ] as const)( + "formats leftover map comparison cross share label in %s", + (locale, expected) => { + setLocale(locale); + expect(t("Leftover map comparison cross share")).toBe(expected); + }, + ); + it.each([ ["ko", "잔여 지도 그림 기준 포함 범위"], ["zh", "残差图图形准则覆盖范围"], diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 6c08da157..6bf6b50ae 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -275,6 +275,7 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison reconstruction": "잔여 지도 비교 재구성", "Leftover map comparison explained leftover share": "잔여 지도 비교 설명 잔여 점유율", "Leftover map comparison unexplained leftover share": "잔여 지도 비교 미설명 잔여 점유율", + "Leftover map comparison cross share": "잔여 지도 비교 교차 점유율", "Leftover map item coverage": "잔여 지도 기준 포함 범위", "Leftover map incomplete posts": "잔여 지도 불완전 글", "Leftover map incomplete items": "잔여 지도 불완전 기준", @@ -888,6 +889,7 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison reconstruction": "残差地图比较重建", "Leftover map comparison explained leftover share": "残差地图比较已解释残差份额", "Leftover map comparison unexplained leftover share": "残差地图比较未解释残差份额", + "Leftover map comparison cross share": "残差地图比较交叉份额", "Leftover map item coverage": "残差地图准则覆盖范围", "Leftover map incomplete posts": "残差地图不完整帖文", "Leftover map incomplete items": "残差地图不完整准则", @@ -1522,6 +1524,7 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison reconstruction": "残差マップの比較再構成", "Leftover map comparison explained leftover share": "残差マップの比較説明済み残差割合", "Leftover map comparison unexplained leftover share": "残差マップの比較未説明残差割合", + "Leftover map comparison cross share": "残差マップの比較交差割合", "Leftover map item coverage": "残差マップの基準対象範囲", "Leftover map incomplete posts": "残差マップの不完全投稿", "Leftover map incomplete items": "残差マップの不完全基準", @@ -2135,6 +2138,7 @@ const TRANSLATIONS: Partial>> = { "Leftover map comparison reconstruction": "Tái dựng so sánh bản đồ phần dư", "Leftover map comparison explained leftover share": "Phần dư giải thích so sánh bản đồ phần dư", "Leftover map comparison unexplained leftover share": "Phần dư chưa giải thích so sánh bản đồ phần dư", + "Leftover map comparison cross share": "Phần giao so sánh bản đồ phần dư", "Leftover map item coverage": "Phạm vi tiêu chí bản đồ phần dư", "Leftover map incomplete posts": "Bài không đầy đủ trên bản đồ phần dư", "Leftover map incomplete items": "Tiêu chí không đầy đủ trên bản đồ phần dư", diff --git a/frontend/src/leftoverMapCrossShare.test.ts b/frontend/src/leftoverMapCrossShare.test.ts index 350e202ed..3a224bc86 100644 --- a/frontend/src/leftoverMapCrossShare.test.ts +++ b/frontend/src/leftoverMapCrossShare.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; +import { + formatLeftoverMapCrossShare, + LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL, +} from "./leftoverMapCrossShare"; describe("formatLeftoverMapCrossShare", () => { it("names leftover-map cross share without inventing a leftover score", () => { @@ -15,4 +18,13 @@ describe("formatLeftoverMapCrossShare", () => { expect(formatLeftoverMapCrossShare(Number.POSITIVE_INFINITY)).toBeNull(); expect(formatLeftoverMapCrossShare(Number.NEGATIVE_INFINITY)).toBeNull(); }); + + it("keeps the grouping comparison cross share label distinct from the graphic cross share label", () => { + expect(LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL).toBe( + "Leftover map comparison cross share", + ); + expect(LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL).not.toBe( + "leftover-map cross share {label}", + ); + }); }); diff --git a/frontend/src/leftoverMapCrossShare.ts b/frontend/src/leftoverMapCrossShare.ts index a113ee9dc..39367be53 100644 --- a/frontend/src/leftoverMapCrossShare.ts +++ b/frontend/src/leftoverMapCrossShare.ts @@ -3,6 +3,9 @@ export const LEFTOVER_MAP_CROSS_SHARE_ACTION = "Two leftover-map axes leave identity remainder {value} of raw residual after IRT main effects. Open this post to read {criterion}."; +export const LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL = + "Leftover map comparison cross share"; + export function formatLeftoverMapCrossShare( value: number | null | undefined, ): string | null { diff --git a/frontend/src/leftoverMapPlotLayout.ts b/frontend/src/leftoverMapPlotLayout.ts index a0858de36..598545847 100644 --- a/frontend/src/leftoverMapPlotLayout.ts +++ b/frontend/src/leftoverMapPlotLayout.ts @@ -22,8 +22,9 @@ * (ADR 0291), grouping comparison incomplete item coverage * (ADR 0292), grouping comparison reconstruction * (ADR 0293), grouping comparison explained leftover share - * (ADR 0294), and grouping comparison unexplained leftover share - * (ADR 0295) caption the pair list or the grouping comparison strip, + * (ADR 0294), grouping comparison unexplained leftover share + * (ADR 0295), and grouping comparison leftover-map cross share + * (ADR 0296) caption the pair list or the grouping comparison strip, * not this graphic layout. */ diff --git a/pyproject.toml b/pyproject.toml index 8fc35c845..38d8858ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lineageweave" -version = "2.52.0" +version = "2.53.0" description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication." readme = "README.md" license = { text = "MIT" } From 2c2c17070946ae881cbc7f5378a9380677e82fa6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:18:58 +0900 Subject: [PATCH 02/43] chore(ci): stage bounded #831 cross-share source repair --- .../automation-831-cross-share-repair.yml | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/automation-831-cross-share-repair.yml diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml new file mode 100644 index 000000000..f23725b67 --- /dev/null +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -0,0 +1,170 @@ +name: automation 831 cross-share repair + +on: + push: + branches: + - feat/leftover-map-compare-cross-share-v2530 + +permissions: + contents: write + +concurrency: + group: automation-831-cross-share-repair + cancel-in-progress: false + +jobs: + repair: + runs-on: ubuntu-latest + steps: + - name: Checkout exact branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + ref: feat/leftover-map-compare-cross-share-v2530 + fetch-depth: 2 + + - name: Verify expected predecessor + shell: bash + run: | + set -euo pipefail + expected=856464d69bdf989c6e47bd1f6e3126fc839ce229 + actual="$(git rev-parse HEAD^)" + test "$actual" = "$expected" || { + echo "branch moved before repair: expected parent $expected, got $actual" >&2 + exit 1 + } + + - name: Apply minimal causal source repair + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + import re + + path = Path("frontend/src/App.tsx") + source = path.read_text(encoding="utf-8") + + import_needle = '''import { + formatLeftoverMapUnexplainedShare, + LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, + } from "./leftoverMapUnexplainedShare"; + import { + leftoverMapCompareAxisShare,''' + import_replacement = '''import { + formatLeftoverMapUnexplainedShare, + LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, + } from "./leftoverMapUnexplainedShare"; + import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; + import { + leftoverMapCompareAxisShare,''' + if source.count(import_needle) != 1: + raise SystemExit(f"unexpected cross-share import anchor count: {source.count(import_needle)}") + source = source.replace(import_needle, import_replacement, 1) + + declaration_pattern = re.compile( + r'( const unexplainedShare = formatLeftoverMapUnexplainedShare\(\n' + r' pair\.leftover_map_unexplained_share,\n' + r' \);\n)' + r'( const pairAccessibleName = )' + ) + source, count = declaration_pattern.subn( + r'\1 const crossShare = formatLeftoverMapCrossShare(\n' + r' pair.leftover_map_cross_share,\n' + r' );\n\2', + source, + count=1, + ) + if count != 1: + raise SystemExit(f"unexpected cross-share declaration anchor count: {count}") + + name_needle = ''' }${ + unexplainedShare + ? ` · ${t(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL)} ${unexplainedShare}` + : "" + }`;''' + name_replacement = ''' }${ + unexplainedShare + ? ` · ${t(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL)} ${unexplainedShare}` + : "" + }${crossShare ? ` · ${crossShare}` : ""}`;''' + if source.count(name_needle) != 1: + raise SystemExit(f"unexpected accessible-name anchor count: {source.count(name_needle)}") + source = source.replace(name_needle, name_replacement, 1) + + badge_needle = ''' {unexplainedShare ? ( + + ) : null} + ''' + badge_replacement = ''' {unexplainedShare ? ( + + ) : null} + {crossShare ? ( + + ) : null} + ''' + if source.count(badge_needle) != 1: + raise SystemExit(f"unexpected visual-badge anchor count: {source.count(badge_needle)}") + source = source.replace(badge_needle, badge_replacement, 1) + + if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in source: + raise SystemExit("forbidden inline/static comparison cross-share label authority remains") + path.write_text(source, encoding="utf-8") + PY + git diff --check + + - name: Run executable cross-share regression + shell: bash + run: | + set -euo pipefail + python - <<'PY' + import runpy + ns = runpy.run_path("tests/test_grouping_comparison_cross_share_contract.py") + ns["test_grouping_comparison_projects_persisted_cross_share_into_one_accessible_action"]() + ns["test_cross_share_formatter_remains_fail_closed_and_unclamped"]() + PY + + - name: Set up Node + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 + with: + node-version: "24" + + - name: Enable Corepack + run: corepack enable + + - name: Install frontend dependencies + working-directory: frontend + run: pnpm install --frozen-lockfile + + - name: Lint frontend + working-directory: frontend + run: pnpm run lint + + - name: Test frontend + working-directory: frontend + run: pnpm run test + + - name: Build frontend + working-directory: frontend + run: pnpm run build + + - name: Build Storybook + working-directory: frontend + run: pnpm run build-storybook + + - name: Commit repair and remove temporary workflow + shell: bash + run: | + set -euo pipefail + git rm .github/workflows/automation-831-cross-share-repair.yml + git add frontend/src/App.tsx + git diff --cached --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "fix(reports): project persisted comparison cross share" + git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 From 9a958e7bf18e78f98c1627ad51e1997c27a25b60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:22:23 +0900 Subject: [PATCH 03/43] chore(ci): extend #831 repair through persisted transport --- .../automation-831-cross-share-repair.yml | 208 ++++++++++++++++-- 1 file changed, 187 insertions(+), 21 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index f23725b67..a3e6b4d75 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,31 +9,164 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair + group: automation-831-cross-share-repair-v2 cancel-in-progress: false jobs: repair: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc4999588649806c4678c61a1 + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + LINEAGEWEAVE_TEST_POSTGRES_ADMIN_DSN: postgresql://postgres:postgres@localhost:5432/postgres steps: - - name: Checkout exact branch + - name: Checkout exact trigger commit uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: - ref: feat/leftover-map-compare-cross-share-v2530 - fetch-depth: 2 + ref: ${{ github.sha }} + fetch-depth: 4 - - name: Verify expected predecessor + - name: Verify expected serialized predecessor shell: bash run: | set -euo pipefail - expected=856464d69bdf989c6e47bd1f6e3126fc839ce229 - actual="$(git rev-parse HEAD^)" - test "$actual" = "$expected" || { - echo "branch moved before repair: expected parent $expected, got $actual" >&2 + test "$(git rev-parse HEAD^)" = "2c2c17070946ae881cbc7f5378a9380677e82fa6" || { + echo "unexpected workflow-update parent" >&2 exit 1 } + git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD + test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" + test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" - - name: Apply minimal causal source repair + - name: Add backend transport RED + shell: bash + run: | + set -euo pipefail + cat > backend/tests/test_period_comparison_cross_share.py <<'PY' + """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), + ), + start=1, + ) + ] + if "from report_leftover_map_coverage" in query: + return [] + if "from report_leftover_map_axis" in query: + return [] + raise AssertionError(f"unexpected query: {query}") + + + def test_period_comparison_transports_persisted_cross_share(monkeypatch) -> None: + """Finite signed, zero, and null x values must survive the persisted pair boundary.""" + + 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 + pairs = payload[0]["leftover_pairs"] + assert [pair["leftover_map_cross_share"] for pair in pairs] == [0.12, 0.0, -0.25, None] + PY + git add backend/tests/test_period_comparison_cross_share.py + git diff --cached --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "test(reports): require comparison cross-share transport" + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Select pinned Rust toolchain + run: | + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + + - name: Install committed universal lock + run: uv sync --frozen --extra dev --extra backend + + - name: Prove backend transport RED + shell: bash + run: | + set -euo pipefail + if uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py; then + echo "expected the transport regression to fail before the source repair" >&2 + exit 1 + fi + + - name: Apply minimum causal source repairs shell: bash run: | set -euo pipefail @@ -41,8 +174,8 @@ jobs: from pathlib import Path import re - path = Path("frontend/src/App.tsx") - source = path.read_text(encoding="utf-8") + app_path = Path("frontend/src/App.tsx") + source = app_path.read_text(encoding="utf-8") import_needle = '''import { formatLeftoverMapUnexplainedShare, @@ -111,24 +244,59 @@ jobs: if source.count(badge_needle) != 1: raise SystemExit(f"unexpected visual-badge anchor count: {source.count(badge_needle)}") source = source.replace(badge_needle, badge_replacement, 1) - if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in source: raise SystemExit("forbidden inline/static comparison cross-share label authority remains") - path.write_text(source, encoding="utf-8") + app_path.write_text(source, encoding="utf-8") + + backend_path = Path("backend/app/report_ingestion.py") + backend = backend_path.read_text(encoding="utf-8") + query_needle = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, + p.post_title, p.visibility_code, p.corporate_entity_id,''' + query_replacement = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, + lp.leftover_map_cross_share, + p.post_title, p.visibility_code, p.corporate_entity_id,''' + if backend.count(query_needle) != 1: + raise SystemExit(f"unexpected backend select anchor count: {backend.count(query_needle)}") + backend = backend.replace(query_needle, query_replacement, 1) + + payload_needle = ''' "leftover_map_unexplained_share": ( + None + if pair["leftover_map_unexplained_share"] is None + else float(pair["leftover_map_unexplained_share"]) + ), + "visibility_code": pair["visibility_code"],''' + payload_replacement = ''' "leftover_map_unexplained_share": ( + None + if pair["leftover_map_unexplained_share"] is None + else float(pair["leftover_map_unexplained_share"]) + ), + "leftover_map_cross_share": ( + None + if pair["leftover_map_cross_share"] is None + else float(pair["leftover_map_cross_share"]) + ), + "visibility_code": pair["visibility_code"],''' + if backend.count(payload_needle) != 1: + raise SystemExit(f"unexpected backend payload anchor count: {backend.count(payload_needle)}") + backend_path.write_text(backend.replace(payload_needle, payload_replacement, 1), encoding="utf-8") PY git diff --check - - name: Run executable cross-share regression + - name: Prove causal GREEN contracts shell: bash run: | set -euo pipefail - python - <<'PY' + uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py + uv run --frozen python - <<'PY' import runpy ns = runpy.run_path("tests/test_grouping_comparison_cross_share_contract.py") ns["test_grouping_comparison_projects_persisted_cross_share_into_one_accessible_action"]() ns["test_cross_share_formatter_remains_fail_closed_and_unclamped"]() PY + - name: Run full PostgreSQL-backed suite + run: uv run --frozen python -m pytest -q + - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 with: @@ -157,14 +325,12 @@ jobs: working-directory: frontend run: pnpm run build-storybook - - name: Commit repair and remove temporary workflow + - name: Commit GREEN repair and remove temporary workflow shell: bash run: | set -euo pipefail git rm .github/workflows/automation-831-cross-share-repair.yml - git add frontend/src/App.tsx + git add frontend/src/App.tsx backend/app/report_ingestion.py git diff --cached --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "fix(reports): project persisted comparison cross share" + git commit -m "fix(reports): transport and project comparison cross share" git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 From 856cbdf451b21377cbe1b2b918dede8e4e4af58b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:23:12 +0900 Subject: [PATCH 04/43] fix(ci): correct bounded #831 repair harness --- .../automation-831-cross-share-repair.yml | 31 ++++--------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index a3e6b4d75..06f9a8473 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,39 +9,25 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v2 + group: automation-831-cross-share-repair-v3 cancel-in-progress: false jobs: repair: runs-on: ubuntu-latest - services: - postgres: - image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc4999588649806c4678c61a1 - env: - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U postgres" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - LINEAGEWEAVE_TEST_POSTGRES_ADMIN_DSN: postgresql://postgres:postgres@localhost:5432/postgres steps: - name: Checkout exact trigger commit uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: ref: ${{ github.sha }} - fetch-depth: 4 + fetch-depth: 5 - name: Verify expected serialized predecessor shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "2c2c17070946ae881cbc7f5378a9380677e82fa6" || { - echo "unexpected workflow-update parent" >&2 + test "$(git rev-parse HEAD^)" = "9a958e7bf18e78f98c1627ad51e1997c27a25b60" || { + echo "unexpected workflow-repair parent" >&2 exit 1 } git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD @@ -176,7 +162,6 @@ jobs: app_path = Path("frontend/src/App.tsx") source = app_path.read_text(encoding="utf-8") - import_needle = '''import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, @@ -193,7 +178,6 @@ jobs: if source.count(import_needle) != 1: raise SystemExit(f"unexpected cross-share import anchor count: {source.count(import_needle)}") source = source.replace(import_needle, import_replacement, 1) - declaration_pattern = re.compile( r'( const unexplainedShare = formatLeftoverMapUnexplainedShare\(\n' r' pair\.leftover_map_unexplained_share,\n' @@ -209,7 +193,6 @@ jobs: ) if count != 1: raise SystemExit(f"unexpected cross-share declaration anchor count: {count}") - name_needle = ''' }${ unexplainedShare ? ` · ${t(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL)} ${unexplainedShare}` @@ -223,7 +206,6 @@ jobs: if source.count(name_needle) != 1: raise SystemExit(f"unexpected accessible-name anchor count: {source.count(name_needle)}") source = source.replace(name_needle, name_replacement, 1) - badge_needle = ''' {unexplainedShare ? ( ) : null} ''' - if source.count(badge_needle) != 1: - raise SystemExit(f"unexpected visual-badge anchor count: {source.count(badge_needle)}") - source = source.replace(badge_needle, badge_replacement, 1) - if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in source: - raise SystemExit("forbidden inline/static comparison cross-share label authority remains") - app_path.write_text(source, encoding="utf-8") + if app.count(badge_anchor) != 1: + raise SystemExit(f"badge anchor count={app.count(badge_anchor)}") + app = app.replace(badge_anchor, badge_value, 1) + if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in app: + raise SystemExit("forbidden static comparison cross-share label authority") + app_path.write_text(app, encoding="utf-8") backend_path = Path("backend/app/report_ingestion.py") backend = backend_path.read_text(encoding="utf-8") - query_needle = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, + query_anchor = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, p.post_title, p.visibility_code, p.corporate_entity_id,''' - query_replacement = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, + query_value = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, lp.leftover_map_cross_share, p.post_title, p.visibility_code, p.corporate_entity_id,''' - if backend.count(query_needle) != 1: - raise SystemExit(f"unexpected backend select anchor count: {backend.count(query_needle)}") - backend = backend.replace(query_needle, query_replacement, 1) - payload_needle = ''' "leftover_map_unexplained_share": ( + if backend.count(query_anchor) != 1: + raise SystemExit(f"backend select anchor count={backend.count(query_anchor)}") + backend = backend.replace(query_anchor, query_value, 1) + payload_anchor = ''' "leftover_map_unexplained_share": ( None if pair["leftover_map_unexplained_share"] is None else float(pair["leftover_map_unexplained_share"]) ), "visibility_code": pair["visibility_code"],''' - payload_replacement = ''' "leftover_map_unexplained_share": ( + payload_value = ''' "leftover_map_unexplained_share": ( None if pair["leftover_map_unexplained_share"] is None else float(pair["leftover_map_unexplained_share"]) @@ -257,9 +236,9 @@ jobs: else float(pair["leftover_map_cross_share"]) ), "visibility_code": pair["visibility_code"],''' - if backend.count(payload_needle) != 1: - raise SystemExit(f"unexpected backend payload anchor count: {backend.count(payload_needle)}") - backend_path.write_text(backend.replace(payload_needle, payload_replacement, 1), encoding="utf-8") + if backend.count(payload_anchor) != 1: + raise SystemExit(f"backend payload anchor count={backend.count(payload_anchor)}") + backend_path.write_text(backend.replace(payload_anchor, payload_value, 1), encoding="utf-8") PY git diff --check @@ -275,38 +254,22 @@ jobs: ns["test_cross_share_formatter_remains_fail_closed_and_unclamped"]() PY - - name: Run bounded backend regressions - run: uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py tests/test_leftover_pairs.py - - name: Set up Node uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 with: node-version: "24" - - name: Enable Corepack - run: corepack enable - - - name: Install frontend dependencies - working-directory: frontend - run: pnpm install --frozen-lockfile - - - name: Lint frontend - working-directory: frontend - run: pnpm run lint - - - name: Test frontend - working-directory: frontend - run: pnpm run test - - - name: Build frontend + - name: Validate frontend exact candidate working-directory: frontend - run: pnpm run build - - - name: Build Storybook - working-directory: frontend - run: pnpm run build-storybook - - - name: Commit GREEN repair and remove temporary workflow + run: | + corepack enable + pnpm install --frozen-lockfile + pnpm run lint + pnpm run test + pnpm run build + pnpm run build-storybook + + - name: Commit GREEN repair, remove temporary workflow, and push non-force shell: bash run: | set -euo pipefail From 652bba70bfd997b63618381754ce58bcb0f7ba39 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:30:05 +0900 Subject: [PATCH 06/43] fix(ci): repair #831 badge surgery and validate candidate --- .../automation-831-cross-share-repair.yml | 117 ++++++++---------- 1 file changed, 50 insertions(+), 67 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index c6b7e7251..3ffe043c3 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,7 +9,7 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v4 + group: automation-831-cross-share-repair-v5 cancel-in-progress: false jobs: @@ -20,18 +20,18 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: ref: ${{ github.sha }} - fetch-depth: 6 + fetch-depth: 7 - - name: Verify expected predecessor and unchanged product sources + - name: Verify serialized predecessor and unchanged product sources shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "856cbdf451b21377cbe1b2b918dede8e4e4af58b" + test "$(git rev-parse HEAD^)" = "b7ebc9fa4ef464f8e8c36766a7477b70ee618be5" git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" - - name: Add backend transport RED commit locally + - name: Apply persisted transport and comparison projection repair shell: bash run: | set -euo pipefail @@ -98,42 +98,7 @@ jobs: 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] PY - git add backend/tests/test_period_comparison_cross_share.py - git diff --cached --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "test(reports): require comparison cross-share transport" - - - name: Set up Python and uv - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Set up locked dependency manager - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - with: - version: "0.11.28" - enable-cache: false - - name: Select pinned Rust toolchain and install lock - run: | - rustup toolchain install 1.97.1 --profile minimal - rustup default 1.97.1 - uv sync --frozen --extra dev --extra backend - - - name: Prove backend transport RED - shell: bash - run: | - set -euo pipefail - if uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py; then - echo "expected RED before source repair" >&2 - exit 1 - fi - - - name: Apply minimum causal source repairs - shell: bash - run: | - set -euo pipefail python - <<'PY' from pathlib import Path import re @@ -177,34 +142,32 @@ jobs: accessible = re.compile( r'(?P\$\{\n\s+unexplainedShare\n\s+\? ` · \$\{t\(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL\)\} \$\{unexplainedShare\}`\n\s+: ""\n\s+\})(?P`;)' ) - app, count = accessible.subn( - r'\g${crossShare ? ` · ${crossShare}` : ""}\g', - app, - count=1, - ) + app, count = accessible.subn(r'\g${crossShare ? ` · ${crossShare}` : ""}\g', app, count=1) if count != 1: raise SystemExit(f"accessible-name anchor count={count}") - badge_anchor = ''' {unexplainedShare ? ( - - ) : null} - ''' - badge_value = ''' {unexplainedShare ? ( - - ) : null} - {crossShare ? ( - - ) : null} - ''' - if app.count(badge_anchor) != 1: - raise SystemExit(f"badge anchor count={app.count(badge_anchor)}") - app = app.replace(badge_anchor, badge_value, 1) + pair_start = app.index("const pairAccessibleName =") + pair_prefix, pair_tail = app[:pair_start], app[pair_start:] + badge = re.compile( + r'(?P\s+\{unexplainedShare \? \(\n' + r'\s+\n' + r'\s+\) : null\}\n)' + r'(?P\s+)' + ) + pair_tail, count = badge.subn( + r'\g {crossShare ? (\n' + r' \n' + r' ) : null}\n\g', + pair_tail, + count=1, + ) + if count != 1: + raise SystemExit(f"badge anchor count={count}") + app = pair_prefix + pair_tail if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in app: raise SystemExit("forbidden static comparison cross-share label authority") app_path.write_text(app, encoding="utf-8") @@ -219,6 +182,7 @@ jobs: if backend.count(query_anchor) != 1: raise SystemExit(f"backend select anchor count={backend.count(query_anchor)}") backend = backend.replace(query_anchor, query_value, 1) + payload_anchor = ''' "leftover_map_unexplained_share": ( None if pair["leftover_map_unexplained_share"] is None @@ -242,6 +206,23 @@ jobs: PY git diff --check + - name: Set up Python and locked dependencies + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Install locked Python/Rust environment + run: | + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + uv sync --frozen --extra dev --extra backend + - name: Prove causal GREEN contracts shell: bash run: | @@ -259,7 +240,7 @@ jobs: with: node-version: "24" - - name: Validate frontend exact candidate + - name: Validate frontend candidate working-directory: frontend run: | corepack enable @@ -274,7 +255,9 @@ jobs: run: | set -euo pipefail git rm .github/workflows/automation-831-cross-share-repair.yml - git add frontend/src/App.tsx backend/app/report_ingestion.py + git add frontend/src/App.tsx backend/app/report_ingestion.py backend/tests/test_period_comparison_cross_share.py git diff --cached --check + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git commit -m "fix(reports): transport and project comparison cross share" git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 From 2fe724b6d9d501b4db13dfb50eb2367bf4d0525a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:31:49 +0900 Subject: [PATCH 07/43] fix(ci): make #831 backend surgery exact and bounded --- .../automation-831-cross-share-repair.yml | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index 3ffe043c3..742493c8c 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,7 +9,7 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v5 + group: automation-831-cross-share-repair-v6 cancel-in-progress: false jobs: @@ -20,13 +20,13 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: ref: ${{ github.sha }} - fetch-depth: 7 + fetch-depth: 8 - name: Verify serialized predecessor and unchanged product sources shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "b7ebc9fa4ef464f8e8c36766a7477b70ee618be5" + test "$(git rev-parse HEAD^)" = "652bba70bfd997b63618381754ce58bcb0f7ba39" git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" @@ -105,7 +105,6 @@ jobs: app_path = Path("frontend/src/App.tsx") app = app_path.read_text(encoding="utf-8") - import_anchor = '''import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, @@ -132,9 +131,7 @@ jobs: app, count = declaration.subn( r'\1 const crossShare = formatLeftoverMapCrossShare(\n' r' pair.leftover_map_cross_share,\n' - r' );\n\2', - app, - count=1, + r' );\n\2', app, count=1 ) if count != 1: raise SystemExit(f"declaration anchor count={count}") @@ -149,21 +146,11 @@ jobs: pair_start = app.index("const pairAccessibleName =") pair_prefix, pair_tail = app[:pair_start], app[pair_start:] badge = re.compile( - r'(?P\s+\{unexplainedShare \? \(\n' - r'\s+\n' - r'\s+\) : null\}\n)' - r'(?P\s+)' + r'(?P\s+\{unexplainedShare \? \(\n\s+\n\s+\) : null\}\n)(?P\s+)' ) pair_tail, count = badge.subn( - r'\g {crossShare ? (\n' - r' \n' - r' ) : null}\n\g', - pair_tail, - count=1, + r'\g {crossShare ? (\n \n ) : null}\n\g', + pair_tail, count=1 ) if count != 1: raise SystemExit(f"badge anchor count={count}") @@ -174,39 +161,34 @@ jobs: backend_path = Path("backend/app/report_ingestion.py") backend = backend_path.read_text(encoding="utf-8") - query_anchor = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, - p.post_title, p.visibility_code, p.corporate_entity_id,''' - query_value = ''' lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share, - lp.leftover_map_cross_share, - p.post_title, p.visibility_code, p.corporate_entity_id,''' + query_anchor = "lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,\n" if backend.count(query_anchor) != 1: raise SystemExit(f"backend select anchor count={backend.count(query_anchor)}") - backend = backend.replace(query_anchor, query_value, 1) + backend = backend.replace( + query_anchor, + query_anchor + " lp.leftover_map_cross_share,\n", + 1, + ) payload_anchor = ''' "leftover_map_unexplained_share": ( None if pair["leftover_map_unexplained_share"] is None else float(pair["leftover_map_unexplained_share"]) ), - "visibility_code": pair["visibility_code"],''' - payload_value = ''' "leftover_map_unexplained_share": ( - None - if pair["leftover_map_unexplained_share"] is None - else float(pair["leftover_map_unexplained_share"]) - ), - "leftover_map_cross_share": ( +''' + if backend.count(payload_anchor) != 1: + raise SystemExit(f"backend payload anchor count={backend.count(payload_anchor)}") + payload_value = payload_anchor + ''' "leftover_map_cross_share": ( None if pair["leftover_map_cross_share"] is None else float(pair["leftover_map_cross_share"]) ), - "visibility_code": pair["visibility_code"],''' - if backend.count(payload_anchor) != 1: - raise SystemExit(f"backend payload anchor count={backend.count(payload_anchor)}") +''' backend_path.write_text(backend.replace(payload_anchor, payload_value, 1), encoding="utf-8") PY git diff --check - - name: Set up Python and locked dependencies + - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 with: python-version: "3.12" From d2250234699fd3b87453fa8b66e5b201bd988efb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:33:51 +0900 Subject: [PATCH 08/43] chore(repair): stage bounded #831 source surgery --- scripts/automation_831_cross_share_repair.py | 187 +++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 scripts/automation_831_cross_share_repair.py diff --git a/scripts/automation_831_cross_share_repair.py b/scripts/automation_831_cross_share_repair.py new file mode 100644 index 000000000..0a5808bb9 --- /dev/null +++ b/scripts/automation_831_cross_share_repair.py @@ -0,0 +1,187 @@ +"""One-shot #831 source repair; removed by its owning workflow after GREEN.""" + +from __future__ import annotations + +from pathlib import Path +import re + + +def require_once(text: str, needle: str, label: str) -> None: + count = text.count(needle) + if count != 1: + raise SystemExit(f"{label} count={count}") + + +def write_backend_regression() -> None: + path = Path("backend/tests/test_period_comparison_cross_share.py") + path.write_text( + '''"""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)), + 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: + """Finite signed, zero, and null x values survive the persisted pair boundary.""" + + 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] +''', + encoding="utf-8", + ) + + +def repair_frontend() -> None: + path = Path("frontend/src/App.tsx") + app = path.read_text(encoding="utf-8") + + import_anchor = '''import { + formatLeftoverMapUnexplainedShare, + LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, +} from "./leftoverMapUnexplainedShare"; +import { + leftoverMapCompareAxisShare,''' + import_value = '''import { + formatLeftoverMapUnexplainedShare, + LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, +} from "./leftoverMapUnexplainedShare"; +import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; +import { + leftoverMapCompareAxisShare,''' + require_once(app, import_anchor, "frontend import anchor") + app = app.replace(import_anchor, import_value, 1) + + declaration = re.compile( + r'(\s+const unexplainedShare = formatLeftoverMapUnexplainedShare\(\n' + r'\s+pair\.leftover_map_unexplained_share,\n' + r'\s+\);\n)' + r'(\s+const pairAccessibleName = )' + ) + app, count = declaration.subn( + r'\1 const crossShare = formatLeftoverMapCrossShare(\n' + r' pair.leftover_map_cross_share,\n' + r' );\n\2', + app, + count=1, + ) + if count != 1: + raise SystemExit(f"frontend declaration anchor count={count}") + + accessible = re.compile( + r'(?P\$\{\n\s+unexplainedShare\n\s+\? ` · \$\{t\(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL\)\} \$\{unexplainedShare\}`\n\s+: ""\n\s+\})(?P`;)' + ) + app, count = accessible.subn( + r'\g${crossShare ? ` · ${crossShare}` : ""}\g', app, count=1 + ) + if count != 1: + raise SystemExit(f"frontend accessible-name anchor count={count}") + + pair_start = app.index("const pairAccessibleName =") + prefix, tail = app[:pair_start], app[pair_start:] + badge = re.compile( + r'(?P\s+\{unexplainedShare \? \(\n' + r'\s+\n' + r'\s+\) : null\}\n)' + r'(?P\s+)' + ) + tail, count = badge.subn( + r'\g {crossShare ? (\n' + r' \n' + r' ) : null}\n\g', + tail, + count=1, + ) + if count != 1: + raise SystemExit(f"frontend badge anchor count={count}") + app = prefix + tail + if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in app: + raise SystemExit("forbidden static comparison cross-share label authority") + path.write_text(app, encoding="utf-8") + + +def repair_backend() -> None: + path = Path("backend/app/report_ingestion.py") + backend = path.read_text(encoding="utf-8") + + query_anchor = "lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,\n" + require_once(backend, query_anchor, "backend select anchor") + backend = backend.replace( + query_anchor, + query_anchor + " lp.leftover_map_cross_share,\n", + 1, + ) + + payload_anchor = ''' "leftover_map_unexplained_share": ( + None + if pair["leftover_map_unexplained_share"] is None + else float(pair["leftover_map_unexplained_share"]) + ), +''' + require_once(backend, payload_anchor, "backend payload anchor") + payload_value = payload_anchor + ''' "leftover_map_cross_share": ( + None + if pair["leftover_map_cross_share"] is None + else float(pair["leftover_map_cross_share"]) + ), +''' + path.write_text(backend.replace(payload_anchor, payload_value, 1), encoding="utf-8") + + +if __name__ == "__main__": + write_backend_regression() + repair_frontend() + repair_backend() From 9e629f062e2ecad61984ea6672483081592c6019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:34:05 +0900 Subject: [PATCH 09/43] fix(ci): execute bounded #831 repair from one-shot script --- .../automation-831-cross-share-repair.yml | 189 ++---------------- 1 file changed, 15 insertions(+), 174 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index 742493c8c..d106e3ec4 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,7 +9,7 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v6 + group: automation-831-cross-share-repair-v7 cancel-in-progress: false jobs: @@ -17,208 +17,49 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout exact trigger commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: ref: ${{ github.sha }} - fetch-depth: 8 + fetch-depth: 9 - - name: Verify serialized predecessor and unchanged product sources + - name: Verify serialized predecessor shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "652bba70bfd997b63618381754ce58bcb0f7ba39" + test "$(git rev-parse HEAD^)" = "d2250234699fd3b87453fa8b66e5b201bd988efb" git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" - - name: Apply persisted transport and comparison projection repair - shell: bash + - name: Apply bounded source repair run: | - set -euo pipefail - cat > backend/tests/test_period_comparison_cross_share.py <<'PY' - """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)), - 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: - """Finite signed, zero, and null x values survive the persisted pair boundary.""" - - 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] - PY - - python - <<'PY' - from pathlib import Path - import re - - app_path = Path("frontend/src/App.tsx") - app = app_path.read_text(encoding="utf-8") - import_anchor = '''import { - formatLeftoverMapUnexplainedShare, - LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, - } from "./leftoverMapUnexplainedShare"; - import { - leftoverMapCompareAxisShare,''' - import_value = '''import { - formatLeftoverMapUnexplainedShare, - LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, - } from "./leftoverMapUnexplainedShare"; - import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; - import { - leftoverMapCompareAxisShare,''' - if app.count(import_anchor) != 1: - raise SystemExit(f"import anchor count={app.count(import_anchor)}") - app = app.replace(import_anchor, import_value, 1) - - declaration = re.compile( - r'(\s+const unexplainedShare = formatLeftoverMapUnexplainedShare\(\n' - r'\s+pair\.leftover_map_unexplained_share,\n' - r'\s+\);\n)' - r'(\s+const pairAccessibleName = )' - ) - app, count = declaration.subn( - r'\1 const crossShare = formatLeftoverMapCrossShare(\n' - r' pair.leftover_map_cross_share,\n' - r' );\n\2', app, count=1 - ) - if count != 1: - raise SystemExit(f"declaration anchor count={count}") - - accessible = re.compile( - r'(?P\$\{\n\s+unexplainedShare\n\s+\? ` · \$\{t\(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL\)\} \$\{unexplainedShare\}`\n\s+: ""\n\s+\})(?P`;)' - ) - app, count = accessible.subn(r'\g${crossShare ? ` · ${crossShare}` : ""}\g', app, count=1) - if count != 1: - raise SystemExit(f"accessible-name anchor count={count}") - - pair_start = app.index("const pairAccessibleName =") - pair_prefix, pair_tail = app[:pair_start], app[pair_start:] - badge = re.compile( - r'(?P\s+\{unexplainedShare \? \(\n\s+\n\s+\) : null\}\n)(?P\s+)' - ) - pair_tail, count = badge.subn( - r'\g {crossShare ? (\n \n ) : null}\n\g', - pair_tail, count=1 - ) - if count != 1: - raise SystemExit(f"badge anchor count={count}") - app = pair_prefix + pair_tail - if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in app: - raise SystemExit("forbidden static comparison cross-share label authority") - app_path.write_text(app, encoding="utf-8") - - backend_path = Path("backend/app/report_ingestion.py") - backend = backend_path.read_text(encoding="utf-8") - query_anchor = "lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,\n" - if backend.count(query_anchor) != 1: - raise SystemExit(f"backend select anchor count={backend.count(query_anchor)}") - backend = backend.replace( - query_anchor, - query_anchor + " lp.leftover_map_cross_share,\n", - 1, - ) - - payload_anchor = ''' "leftover_map_unexplained_share": ( - None - if pair["leftover_map_unexplained_share"] is None - else float(pair["leftover_map_unexplained_share"]) - ), -''' - if backend.count(payload_anchor) != 1: - raise SystemExit(f"backend payload anchor count={backend.count(payload_anchor)}") - payload_value = payload_anchor + ''' "leftover_map_cross_share": ( - None - if pair["leftover_map_cross_share"] is None - else float(pair["leftover_map_cross_share"]) - ), -''' - backend_path.write_text(backend.replace(payload_anchor, payload_value, 1), encoding="utf-8") - PY + python scripts/automation_831_cross_share_repair.py git diff --check - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 with: python-version: "3.12" - name: Set up uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 with: version: "0.11.28" enable-cache: false - - name: Install locked Python/Rust environment + - name: Install locked Python and Rust environment run: | rustup toolchain install 1.97.1 --profile minimal rustup default 1.97.1 uv sync --frozen --extra dev --extra backend - - name: Prove causal GREEN contracts - shell: bash + - name: Prove backend and source contracts GREEN run: | - set -euo pipefail uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py - uv run --frozen python - <<'PY' - import runpy - ns = runpy.run_path("tests/test_grouping_comparison_cross_share_contract.py") - ns["test_grouping_comparison_projects_persisted_cross_share_into_one_accessible_action"]() - ns["test_cross_share_formatter_remains_fail_closed_and_unclamped"]() - PY + uv run --frozen python -c 'import runpy; n=runpy.run_path("tests/test_grouping_comparison_cross_share_contract.py"); n["test_grouping_comparison_projects_persisted_cross_share_into_one_accessible_action"](); n["test_cross_share_formatter_remains_fail_closed_and_unclamped"]()' - name: Set up Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 with: node-version: "24" @@ -232,11 +73,11 @@ jobs: pnpm run build pnpm run build-storybook - - name: Commit GREEN repair, remove temporary workflow, and push non-force + - name: Commit GREEN and remove one-shot repair assets shell: bash run: | set -euo pipefail - git rm .github/workflows/automation-831-cross-share-repair.yml + git rm .github/workflows/automation-831-cross-share-repair.yml scripts/automation_831_cross_share_repair.py git add frontend/src/App.tsx backend/app/report_ingestion.py backend/tests/test_period_comparison_cross_share.py git diff --cached --check git config user.name "github-actions[bot]" From 6bcf1d79b4de58c27b47306f72f9664afb134f72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:35:16 +0900 Subject: [PATCH 10/43] fix(repair): scope #831 backend surgery to comparison read model --- scripts/automation_831_cross_share_repair.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/automation_831_cross_share_repair.py b/scripts/automation_831_cross_share_repair.py index 0a5808bb9..be6383324 100644 --- a/scripts/automation_831_cross_share_repair.py +++ b/scripts/automation_831_cross_share_repair.py @@ -156,10 +156,14 @@ def repair_frontend() -> None: def repair_backend() -> None: path = Path("backend/app/report_ingestion.py") backend = path.read_text(encoding="utf-8") + function_start = backend.index("async def fetch_period_comparison(") + next_function = backend.find("\nasync def ", function_start + 1) + function_end = len(backend) if next_function == -1 else next_function + prefix, function, suffix = backend[:function_start], backend[function_start:function_end], backend[function_end:] query_anchor = "lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,\n" - require_once(backend, query_anchor, "backend select anchor") - backend = backend.replace( + require_once(function, query_anchor, "comparison backend select anchor") + function = function.replace( query_anchor, query_anchor + " lp.leftover_map_cross_share,\n", 1, @@ -171,14 +175,15 @@ def repair_backend() -> None: else float(pair["leftover_map_unexplained_share"]) ), ''' - require_once(backend, payload_anchor, "backend payload anchor") + require_once(function, payload_anchor, "comparison backend payload anchor") payload_value = payload_anchor + ''' "leftover_map_cross_share": ( None if pair["leftover_map_cross_share"] is None else float(pair["leftover_map_cross_share"]) ), ''' - path.write_text(backend.replace(payload_anchor, payload_value, 1), encoding="utf-8") + function = function.replace(payload_anchor, payload_value, 1) + path.write_text(prefix + function + suffix, encoding="utf-8") if __name__ == "__main__": From 662bb9e87cde99eb5ab567a93fdb451201a37a90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:35:34 +0900 Subject: [PATCH 11/43] fix(ci): rerun #831 repair on scoped backend surgery --- .github/workflows/automation-831-cross-share-repair.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index d106e3ec4..9c21da56a 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,7 +9,7 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v7 + group: automation-831-cross-share-repair-v8 cancel-in-progress: false jobs: @@ -20,13 +20,13 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: ref: ${{ github.sha }} - fetch-depth: 9 + fetch-depth: 10 - name: Verify serialized predecessor shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "d2250234699fd3b87453fa8b66e5b201bd988efb" + test "$(git rev-parse HEAD^)" = "6bcf1d79b4de58c27b47306f72f9664afb134f72" git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" From fe30a82c3a13dcd0e8c1baea3ded690c3cd0bff9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:37:17 +0900 Subject: [PATCH 12/43] fix(ci): validate #831 against exact unchanged product blobs --- .../workflows/automation-831-cross-share-repair.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml index 9c21da56a..203fe0063 100644 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ b/.github/workflows/automation-831-cross-share-repair.yml @@ -9,7 +9,7 @@ permissions: contents: write concurrency: - group: automation-831-cross-share-repair-v8 + group: automation-831-cross-share-repair-v9 cancel-in-progress: false jobs: @@ -20,16 +20,15 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: ref: ${{ github.sha }} - fetch-depth: 10 + fetch-depth: 2 - - name: Verify serialized predecessor + - name: Verify serialized predecessor and product blobs shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "6bcf1d79b4de58c27b47306f72f9664afb134f72" - git merge-base --is-ancestor 856464d69bdf989c6e47bd1f6e3126fc839ce229 HEAD - test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:frontend/src/App.tsx)" - test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "$(git rev-parse 856464d69bdf989c6e47bd1f6e3126fc839ce229:backend/app/report_ingestion.py)" + test "$(git rev-parse HEAD^)" = "662bb9e87cde99eb5ab567a93fdb451201a37a90" + test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "e959ea7515286f976b9bade9247c17ae604f2608" + test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "b4bfb3187b8b85afa7f944a82e5e1664728b3c90" - name: Apply bounded source repair run: | From 5e2dbaf5fa0d9d3fe640d70e4e91e917c0087679 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:39:36 +0000 Subject: [PATCH 13/43] fix(reports): transport and project comparison cross share --- .../automation-831-cross-share-repair.yml | 85 -------- backend/app/report_ingestion.py | 6 + .../test_period_comparison_cross_share.py | 61 ++++++ frontend/src/App.tsx | 11 +- scripts/automation_831_cross_share_repair.py | 192 ------------------ 5 files changed, 77 insertions(+), 278 deletions(-) delete mode 100644 .github/workflows/automation-831-cross-share-repair.yml create mode 100644 backend/tests/test_period_comparison_cross_share.py delete mode 100644 scripts/automation_831_cross_share_repair.py diff --git a/.github/workflows/automation-831-cross-share-repair.yml b/.github/workflows/automation-831-cross-share-repair.yml deleted file mode 100644 index 203fe0063..000000000 --- a/.github/workflows/automation-831-cross-share-repair.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: automation 831 cross-share repair - -on: - push: - branches: - - feat/leftover-map-compare-cross-share-v2530 - -permissions: - contents: write - -concurrency: - group: automation-831-cross-share-repair-v9 - cancel-in-progress: false - -jobs: - repair: - runs-on: ubuntu-latest - steps: - - name: Checkout exact trigger commit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - - - name: Verify serialized predecessor and product blobs - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "662bb9e87cde99eb5ab567a93fdb451201a37a90" - test "$(git rev-parse HEAD:frontend/src/App.tsx)" = "e959ea7515286f976b9bade9247c17ae604f2608" - test "$(git rev-parse HEAD:backend/app/report_ingestion.py)" = "b4bfb3187b8b85afa7f944a82e5e1664728b3c90" - - - name: Apply bounded source repair - run: | - python scripts/automation_831_cross_share_repair.py - git diff --check - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 - with: - python-version: "3.12" - - - name: Set up uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 - with: - version: "0.11.28" - enable-cache: false - - - name: Install locked Python and Rust environment - run: | - rustup toolchain install 1.97.1 --profile minimal - rustup default 1.97.1 - uv sync --frozen --extra dev --extra backend - - - name: Prove backend and source contracts GREEN - run: | - uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py - uv run --frozen python -c 'import runpy; n=runpy.run_path("tests/test_grouping_comparison_cross_share_contract.py"); n["test_grouping_comparison_projects_persisted_cross_share_into_one_accessible_action"](); n["test_cross_share_formatter_remains_fail_closed_and_unclamped"]()' - - - name: Set up Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 - with: - node-version: "24" - - - name: Validate frontend candidate - working-directory: frontend - run: | - corepack enable - pnpm install --frozen-lockfile - pnpm run lint - pnpm run test - pnpm run build - pnpm run build-storybook - - - name: Commit GREEN and remove one-shot repair assets - shell: bash - run: | - set -euo pipefail - git rm .github/workflows/automation-831-cross-share-repair.yml scripts/automation_831_cross_share_repair.py - git add frontend/src/App.tsx backend/app/report_ingestion.py backend/tests/test_period_comparison_cross_share.py - git diff --cached --check - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "fix(reports): transport and project comparison cross share" - git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index b4bfb3187..048e786e2 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -1053,6 +1053,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 @@ -1143,6 +1144,11 @@ async def fetch_period_comparison( if pair["leftover_map_unexplained_share"] is None else float(pair["leftover_map_unexplained_share"]) ), + "leftover_map_cross_share": ( + None + if pair["leftover_map_cross_share"] is None + else float(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"]), diff --git a/backend/tests/test_period_comparison_cross_share.py b/backend/tests/test_period_comparison_cross_share.py new file mode 100644 index 000000000..371f070e8 --- /dev/null +++ b/backend/tests/test_period_comparison_cross_share.py @@ -0,0 +1,61 @@ +"""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)), + 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: + """Finite signed, zero, and null x values survive the persisted pair boundary.""" + + 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] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e959ea751..90e99e380 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -155,6 +155,7 @@ import { formatLeftoverMapUnexplainedShare, LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, } from "./leftoverMapUnexplainedShare"; +import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; import { leftoverMapCompareAxisShare, leftoverMapCompareAxisSingular, @@ -4116,6 +4117,9 @@ function ReportsPanel({ const unexplainedShare = formatLeftoverMapUnexplainedShare( pair.leftover_map_unexplained_share, ); + const crossShare = formatLeftoverMapCrossShare( + pair.leftover_map_cross_share, + ); const pairAccessibleName = `Open leftover ${pair.pair_kind} pair from comparison: ${pair.post_title} · ${criterion}${ reconstruction ? ` · ${t(LEFTOVER_MAP_COMPARE_RECONSTRUCTION_LABEL)} ${reconstruction}` @@ -4128,7 +4132,7 @@ function ReportsPanel({ unexplainedShare ? ` · ${t(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL)} ${unexplainedShare}` : "" - }`; + }${crossShare ? ` · ${crossShare}` : ""}`; return (
  • ) : null} + {crossShare ? ( + + ) : null}
  • ); diff --git a/scripts/automation_831_cross_share_repair.py b/scripts/automation_831_cross_share_repair.py deleted file mode 100644 index be6383324..000000000 --- a/scripts/automation_831_cross_share_repair.py +++ /dev/null @@ -1,192 +0,0 @@ -"""One-shot #831 source repair; removed by its owning workflow after GREEN.""" - -from __future__ import annotations - -from pathlib import Path -import re - - -def require_once(text: str, needle: str, label: str) -> None: - count = text.count(needle) - if count != 1: - raise SystemExit(f"{label} count={count}") - - -def write_backend_regression() -> None: - path = Path("backend/tests/test_period_comparison_cross_share.py") - path.write_text( - '''"""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)), - 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: - """Finite signed, zero, and null x values survive the persisted pair boundary.""" - - 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] -''', - encoding="utf-8", - ) - - -def repair_frontend() -> None: - path = Path("frontend/src/App.tsx") - app = path.read_text(encoding="utf-8") - - import_anchor = '''import { - formatLeftoverMapUnexplainedShare, - LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, -} from "./leftoverMapUnexplainedShare"; -import { - leftoverMapCompareAxisShare,''' - import_value = '''import { - formatLeftoverMapUnexplainedShare, - LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL, -} from "./leftoverMapUnexplainedShare"; -import { formatLeftoverMapCrossShare } from "./leftoverMapCrossShare"; -import { - leftoverMapCompareAxisShare,''' - require_once(app, import_anchor, "frontend import anchor") - app = app.replace(import_anchor, import_value, 1) - - declaration = re.compile( - r'(\s+const unexplainedShare = formatLeftoverMapUnexplainedShare\(\n' - r'\s+pair\.leftover_map_unexplained_share,\n' - r'\s+\);\n)' - r'(\s+const pairAccessibleName = )' - ) - app, count = declaration.subn( - r'\1 const crossShare = formatLeftoverMapCrossShare(\n' - r' pair.leftover_map_cross_share,\n' - r' );\n\2', - app, - count=1, - ) - if count != 1: - raise SystemExit(f"frontend declaration anchor count={count}") - - accessible = re.compile( - r'(?P\$\{\n\s+unexplainedShare\n\s+\? ` · \$\{t\(LEFTOVER_MAP_COMPARE_UNEXPLAINED_SHARE_LABEL\)\} \$\{unexplainedShare\}`\n\s+: ""\n\s+\})(?P`;)' - ) - app, count = accessible.subn( - r'\g${crossShare ? ` · ${crossShare}` : ""}\g', app, count=1 - ) - if count != 1: - raise SystemExit(f"frontend accessible-name anchor count={count}") - - pair_start = app.index("const pairAccessibleName =") - prefix, tail = app[:pair_start], app[pair_start:] - badge = re.compile( - r'(?P\s+\{unexplainedShare \? \(\n' - r'\s+\n' - r'\s+\) : null\}\n)' - r'(?P\s+)' - ) - tail, count = badge.subn( - r'\g {crossShare ? (\n' - r' \n' - r' ) : null}\n\g', - tail, - count=1, - ) - if count != 1: - raise SystemExit(f"frontend badge anchor count={count}") - app = prefix + tail - if "LEFTOVER_MAP_COMPARE_CROSS_SHARE_LABEL" in app: - raise SystemExit("forbidden static comparison cross-share label authority") - path.write_text(app, encoding="utf-8") - - -def repair_backend() -> None: - path = Path("backend/app/report_ingestion.py") - backend = path.read_text(encoding="utf-8") - function_start = backend.index("async def fetch_period_comparison(") - next_function = backend.find("\nasync def ", function_start + 1) - function_end = len(backend) if next_function == -1 else next_function - prefix, function, suffix = backend[:function_start], backend[function_start:function_end], backend[function_end:] - - query_anchor = "lp.leftover_map_reconstruction, lp.leftover_map_unexplained_share,\n" - require_once(function, query_anchor, "comparison backend select anchor") - function = function.replace( - query_anchor, - query_anchor + " lp.leftover_map_cross_share,\n", - 1, - ) - - payload_anchor = ''' "leftover_map_unexplained_share": ( - None - if pair["leftover_map_unexplained_share"] is None - else float(pair["leftover_map_unexplained_share"]) - ), -''' - require_once(function, payload_anchor, "comparison backend payload anchor") - payload_value = payload_anchor + ''' "leftover_map_cross_share": ( - None - if pair["leftover_map_cross_share"] is None - else float(pair["leftover_map_cross_share"]) - ), -''' - function = function.replace(payload_anchor, payload_value, 1) - path.write_text(prefix + function + suffix, encoding="utf-8") - - -if __name__ == "__main__": - write_backend_regression() - repair_frontend() - repair_backend() From ee88c921b17632523db7d413291c262abc909f2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:04:00 +0900 Subject: [PATCH 14/43] test(reports): reject nonfinite comparison cross share --- .../test_period_comparison_cross_share.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/tests/test_period_comparison_cross_share.py b/backend/tests/test_period_comparison_cross_share.py index 371f070e8..0d9635fc8 100644 --- a/backend/tests/test_period_comparison_cross_share.py +++ b/backend/tests/test_period_comparison_cross_share.py @@ -39,7 +39,15 @@ async def fetch(self, query: str, *_args: Any) -> list[dict[str, Any]]: "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", 0.12), + ("farthest", 0.0), + ("closest", -0.25), + ("farthest", None), + ("closest", float("nan")), + ("farthest", float("inf")), + ("closest", float("-inf")), + ), start=1, ) ] @@ -49,7 +57,7 @@ async def fetch(self, query: str, *_args: Any) -> list[dict[str, Any]]: def test_period_comparison_transports_persisted_cross_share(monkeypatch) -> None: - """Finite signed, zero, and null x values survive the persisted pair boundary.""" + """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" @@ -58,4 +66,12 @@ async def _label(_conn: Any, _kind: str, _key: str) -> str: 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] + assert [pair["leftover_map_cross_share"] for pair in payload[0]["leftover_pairs"]] == [ + 0.12, + 0.0, + -0.25, + None, + None, + None, + None, + ] From 071fe46d7decacbc058c43f3b0c53cf590d5f84c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:07:01 +0900 Subject: [PATCH 15/43] chore(automation): stage #831 nonfinite repair --- ...ation-831-nonfinite-cross-share-repair.yml | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/automation-831-nonfinite-cross-share-repair.yml diff --git a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml new file mode 100644 index 000000000..ae16c0aff --- /dev/null +++ b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml @@ -0,0 +1,134 @@ +name: Temporary #831 nonfinite cross-share repair + +on: + push: + branches: [feat/leftover-map-compare-cross-share-v2530] + paths: + - .github/workflows/automation-831-nonfinite-cross-share-repair.yml + +permissions: + contents: write + +jobs: + repair: + if: github.event.head_commit.message == 'chore(automation): stage #831 nonfinite repair' + runs-on: ubuntu-latest + steps: + - name: Checkout exact staging head + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 + with: + fetch-depth: 2 + persist-credentials: true + + - name: Verify single-writer staging ancestry + shell: bash + run: | + set -euo pipefail + test "$(git rev-parse HEAD^)" = "ee88c921b17632523db7d413291c262abc909f2a" + test "$(git diff --name-only HEAD^ HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Set up locked dependency manager + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.11.28" + enable-cache: false + + - name: Select pinned Rust toolchain + shell: bash + run: | + set -euo pipefail + rustup toolchain install 1.97.1 --profile minimal + rustup default 1.97.1 + + - name: Install backend dependencies + shell: bash + run: uv sync --frozen --extra dev --extra backend + + - name: Prove realistic RED + shell: bash + run: | + set -euo pipefail + set +e + uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py > /tmp/red.log 2>&1 + rc=$? + set -e + cat /tmp/red.log + test "$rc" -ne 0 + grep -F 'test_period_comparison_transports_persisted_cross_share' /tmp/red.log + grep -F 'At index 4 diff' /tmp/red.log + + - name: Apply minimum causal repair and rendered regression + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + source = Path("backend/app/report_ingestion.py") + text = source.read_text() + old_import = "import asyncio\nimport re\n" + assert text.count(old_import) == 1 + text = text.replace(old_import, "import asyncio\nimport math\nimport re\n", 1) + + anchor = '_SOURCE_CONTEXT_PRESENT_SQL = source_context_present_sql("p")\n\n' + assert text.count(anchor) == 1 + helper = '''_SOURCE_CONTEXT_PRESENT_SQL = source_context_present_sql("p")\n\n\ndef _finite_float_or_none(value: Any) -> float | None:\n """Return a strict-JSON finite float for an optional persisted numeric."""\n\n if value is None:\n return None\n result = float(value)\n return result if math.isfinite(result) else None\n\n''' + text = text.replace(anchor, helper, 1) + + old_cross = ''' "leftover_map_cross_share": (\n None\n if pair["leftover_map_cross_share"] is None\n else float(pair["leftover_map_cross_share"])\n ),\n''' + new_cross = ''' "leftover_map_cross_share": _finite_float_or_none(\n pair["leftover_map_cross_share"]\n ),\n''' + assert text.count(old_cross) == 1 + text = text.replace(old_cross, new_cross, 1) + source.write_text(text) + + app_test = Path("frontend/src/App.test.tsx") + app_text = app_test.read_text() + needle = ' expect(unexplainedSharePair).toHaveTextContent("U²/R² 0.02");\n' + assert app_text.count(needle) == 1 + rendered_regression = needle + ''' const negativeCrossSharePair = within(\n screen.getByLabelText("Grouping comparison"),\n ).getByRole("button", {\n name: /open leftover farthest pair from comparison: .*2R̂U\\/R² -0\\.24/i,\n });\n expect(negativeCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(negativeCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' + app_text = app_text.replace(needle, rendered_regression, 1) + app_test.write_text(app_text) + PY + + - name: Verify backend GREEN + shell: bash + run: uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py + + - name: Set up Node + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 + with: + node-version: "24" + + - name: Enable Corepack and install frontend + shell: bash + run: | + set -euo pipefail + corepack enable + cd frontend + pnpm install --frozen-lockfile + + - name: Verify rendered accessibility GREEN + working-directory: frontend + shell: bash + run: | + set -euo pipefail + pnpm exec vitest run src/App.test.tsx + pnpm run lint + + - name: Publish workflow-free causal child + shell: bash + run: | + set -euo pipefail + rm .github/workflows/automation-831-nonfinite-cross-share-repair.yml + git diff --check + git status --short + git config user.name "LineageWeave hourly maintainer" + git config user.email "8172694+seonghobae@users.noreply.github.com" + git add backend/app/report_ingestion.py backend/tests/test_period_comparison_cross_share.py frontend/src/App.test.tsx .github/workflows/automation-831-nonfinite-cross-share-repair.yml + git commit -m "fix(reports): normalize nonfinite comparison cross share" + git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 From e6c17e23ce36b11c1a0c2c019bdb9cdb12cd8413 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:07:58 +0900 Subject: [PATCH 16/43] fix(automation): run #831 nonfinite repair --- .../automation-831-nonfinite-cross-share-repair.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml index ae16c0aff..6c97ef05e 100644 --- a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml +++ b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml @@ -11,21 +11,21 @@ permissions: jobs: repair: - if: github.event.head_commit.message == 'chore(automation): stage #831 nonfinite repair' runs-on: ubuntu-latest steps: - name: Checkout exact staging head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: - fetch-depth: 2 + fetch-depth: 3 persist-credentials: true - name: Verify single-writer staging ancestry shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "ee88c921b17632523db7d413291c262abc909f2a" - test "$(git diff --name-only HEAD^ HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" + test "$(git rev-parse HEAD^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" + test "$(git rev-parse HEAD^^)" = "ee88c921b17632523db7d413291c262abc909f2a" + test "$(git diff --name-only ee88c921b17632523db7d413291c262abc909f2a HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 From 3362aee52f8939d254dc83e99e6a9a830a56e3ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:50:58 +0900 Subject: [PATCH 17/43] fix(automation): stabilize #831 rendered cross-share repair --- .../automation-831-nonfinite-cross-share-repair.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml index 6c97ef05e..21fc17554 100644 --- a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml +++ b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml @@ -16,15 +16,16 @@ jobs: - name: Checkout exact staging head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: - fetch-depth: 3 + fetch-depth: 4 persist-credentials: true - name: Verify single-writer staging ancestry shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" - test "$(git rev-parse HEAD^^)" = "ee88c921b17632523db7d413291c262abc909f2a" + test "$(git rev-parse HEAD^)" = "e6c17e23ce36b11c1a0c2c019bdb9cdb12cd8413" + test "$(git rev-parse HEAD^^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" + test "$(git rev-parse HEAD^^^)" = "ee88c921b17632523db7d413291c262abc909f2a" test "$(git diff --name-only ee88c921b17632523db7d413291c262abc909f2a HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" - name: Set up Python @@ -90,7 +91,7 @@ jobs: app_text = app_test.read_text() needle = ' expect(unexplainedSharePair).toHaveTextContent("U²/R² 0.02");\n' assert app_text.count(needle) == 1 - rendered_regression = needle + ''' const negativeCrossSharePair = within(\n screen.getByLabelText("Grouping comparison"),\n ).getByRole("button", {\n name: /open leftover farthest pair from comparison: .*2R̂U\\/R² -0\\.24/i,\n });\n expect(negativeCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(negativeCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' + rendered_regression = needle + ''' const comparisonCrossSharePair = screen.getByRole("button", {\n name: /open leftover farthest pair from comparison: specification revision requested.*2R̂U\\/R² -0\\.24/i,\n });\n expect(comparisonCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(comparisonCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' app_text = app_text.replace(needle, rendered_regression, 1) app_test.write_text(app_text) PY From fab41f59b9e2faca9663f8e3d55f130f25054aa6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:57:49 +0900 Subject: [PATCH 18/43] fix(automation): target #831 grouping fixture --- ...ation-831-nonfinite-cross-share-repair.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml index 21fc17554..917b7cbf4 100644 --- a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml +++ b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml @@ -16,16 +16,17 @@ jobs: - name: Checkout exact staging head uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 with: - fetch-depth: 4 + fetch-depth: 5 persist-credentials: true - name: Verify single-writer staging ancestry shell: bash run: | set -euo pipefail - test "$(git rev-parse HEAD^)" = "e6c17e23ce36b11c1a0c2c019bdb9cdb12cd8413" - test "$(git rev-parse HEAD^^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" - test "$(git rev-parse HEAD^^^)" = "ee88c921b17632523db7d413291c262abc909f2a" + test "$(git rev-parse HEAD^)" = "3362aee52f8939d254dc83e99e6a9a830a56e3ea" + test "$(git rev-parse HEAD^^)" = "e6c17e23ce36b11c1a0c2c019bdb9cdb12cd8413" + test "$(git rev-parse HEAD^^^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" + test "$(git rev-parse HEAD^^^^)" = "ee88c921b17632523db7d413291c262abc909f2a" test "$(git diff --name-only ee88c921b17632523db7d413291c262abc909f2a HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" - name: Set up Python @@ -89,9 +90,17 @@ jobs: app_test = Path("frontend/src/App.test.tsx") app_text = app_test.read_text() + fixture_anchor = ''' leftover_map_explained_share: 0.76,\n leftover_map_unexplained_share: 0.02,\n },\n ],\n leftover_map_coverage: {\n map_post_count: 2,\n''' + assert app_text.count(fixture_anchor) == 1 + app_text = app_text.replace( + fixture_anchor, + ''' leftover_map_explained_share: 0.76,\n leftover_map_unexplained_share: 0.02,\n leftover_map_cross_share: -0.24,\n },\n ],\n leftover_map_coverage: {\n map_post_count: 2,\n''', + 1, + ) + needle = ' expect(unexplainedSharePair).toHaveTextContent("U²/R² 0.02");\n' assert app_text.count(needle) == 1 - rendered_regression = needle + ''' const comparisonCrossSharePair = screen.getByRole("button", {\n name: /open leftover farthest pair from comparison: specification revision requested.*2R̂U\\/R² -0\\.24/i,\n });\n expect(comparisonCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(comparisonCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' + rendered_regression = needle + ''' const comparisonCrossSharePair = screen.getByRole("button", {\n name: /open leftover closest pair from comparison: public post.*2R̂U\\/R² -0\\.24/i,\n });\n expect(comparisonCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(comparisonCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' app_text = app_text.replace(needle, rendered_regression, 1) app_test.write_text(app_text) PY From 6d324e74b8dc692ab62140f584f8b436874011ba Mon Sep 17 00:00:00 2001 From: LineageWeave hourly maintainer <8172694+seonghobae@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:00:16 +0000 Subject: [PATCH 19/43] fix(reports): normalize nonfinite comparison cross share --- ...ation-831-nonfinite-cross-share-repair.yml | 144 ------------------ backend/app/report_ingestion.py | 16 +- frontend/src/App.test.tsx | 9 ++ 3 files changed, 21 insertions(+), 148 deletions(-) delete mode 100644 .github/workflows/automation-831-nonfinite-cross-share-repair.yml diff --git a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml b/.github/workflows/automation-831-nonfinite-cross-share-repair.yml deleted file mode 100644 index 917b7cbf4..000000000 --- a/.github/workflows/automation-831-nonfinite-cross-share-repair.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Temporary #831 nonfinite cross-share repair - -on: - push: - branches: [feat/leftover-map-compare-cross-share-v2530] - paths: - - .github/workflows/automation-831-nonfinite-cross-share-repair.yml - -permissions: - contents: write - -jobs: - repair: - runs-on: ubuntu-latest - steps: - - name: Checkout exact staging head - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout@v7 - with: - fetch-depth: 5 - persist-credentials: true - - - name: Verify single-writer staging ancestry - shell: bash - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "3362aee52f8939d254dc83e99e6a9a830a56e3ea" - test "$(git rev-parse HEAD^^)" = "e6c17e23ce36b11c1a0c2c019bdb9cdb12cd8413" - test "$(git rev-parse HEAD^^^)" = "071fe46d7decacbc058c43f3b0c53cf590d5f84c" - test "$(git rev-parse HEAD^^^^)" = "ee88c921b17632523db7d413291c262abc909f2a" - test "$(git diff --name-only ee88c921b17632523db7d413291c262abc909f2a HEAD)" = ".github/workflows/automation-831-nonfinite-cross-share-repair.yml" - - - name: Set up Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Set up locked dependency manager - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - with: - version: "0.11.28" - enable-cache: false - - - name: Select pinned Rust toolchain - shell: bash - run: | - set -euo pipefail - rustup toolchain install 1.97.1 --profile minimal - rustup default 1.97.1 - - - name: Install backend dependencies - shell: bash - run: uv sync --frozen --extra dev --extra backend - - - name: Prove realistic RED - shell: bash - run: | - set -euo pipefail - set +e - uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py > /tmp/red.log 2>&1 - rc=$? - set -e - cat /tmp/red.log - test "$rc" -ne 0 - grep -F 'test_period_comparison_transports_persisted_cross_share' /tmp/red.log - grep -F 'At index 4 diff' /tmp/red.log - - - name: Apply minimum causal repair and rendered regression - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - source = Path("backend/app/report_ingestion.py") - text = source.read_text() - old_import = "import asyncio\nimport re\n" - assert text.count(old_import) == 1 - text = text.replace(old_import, "import asyncio\nimport math\nimport re\n", 1) - - anchor = '_SOURCE_CONTEXT_PRESENT_SQL = source_context_present_sql("p")\n\n' - assert text.count(anchor) == 1 - helper = '''_SOURCE_CONTEXT_PRESENT_SQL = source_context_present_sql("p")\n\n\ndef _finite_float_or_none(value: Any) -> float | None:\n """Return a strict-JSON finite float for an optional persisted numeric."""\n\n if value is None:\n return None\n result = float(value)\n return result if math.isfinite(result) else None\n\n''' - text = text.replace(anchor, helper, 1) - - old_cross = ''' "leftover_map_cross_share": (\n None\n if pair["leftover_map_cross_share"] is None\n else float(pair["leftover_map_cross_share"])\n ),\n''' - new_cross = ''' "leftover_map_cross_share": _finite_float_or_none(\n pair["leftover_map_cross_share"]\n ),\n''' - assert text.count(old_cross) == 1 - text = text.replace(old_cross, new_cross, 1) - source.write_text(text) - - app_test = Path("frontend/src/App.test.tsx") - app_text = app_test.read_text() - fixture_anchor = ''' leftover_map_explained_share: 0.76,\n leftover_map_unexplained_share: 0.02,\n },\n ],\n leftover_map_coverage: {\n map_post_count: 2,\n''' - assert app_text.count(fixture_anchor) == 1 - app_text = app_text.replace( - fixture_anchor, - ''' leftover_map_explained_share: 0.76,\n leftover_map_unexplained_share: 0.02,\n leftover_map_cross_share: -0.24,\n },\n ],\n leftover_map_coverage: {\n map_post_count: 2,\n''', - 1, - ) - - needle = ' expect(unexplainedSharePair).toHaveTextContent("U²/R² 0.02");\n' - assert app_text.count(needle) == 1 - rendered_regression = needle + ''' const comparisonCrossSharePair = screen.getByRole("button", {\n name: /open leftover closest pair from comparison: public post.*2R̂U\\/R² -0\\.24/i,\n });\n expect(comparisonCrossSharePair).toHaveTextContent("2R̂U/R² -0.24");\n expect(within(comparisonCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute(\n "aria-hidden",\n "true",\n );\n''' - app_text = app_text.replace(needle, rendered_regression, 1) - app_test.write_text(app_text) - PY - - - name: Verify backend GREEN - shell: bash - run: uv run --frozen python -m pytest -q backend/tests/test_period_comparison_cross_share.py - - - name: Set up Node - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # actions/setup-node@v5 - with: - node-version: "24" - - - name: Enable Corepack and install frontend - shell: bash - run: | - set -euo pipefail - corepack enable - cd frontend - pnpm install --frozen-lockfile - - - name: Verify rendered accessibility GREEN - working-directory: frontend - shell: bash - run: | - set -euo pipefail - pnpm exec vitest run src/App.test.tsx - pnpm run lint - - - name: Publish workflow-free causal child - shell: bash - run: | - set -euo pipefail - rm .github/workflows/automation-831-nonfinite-cross-share-repair.yml - git diff --check - git status --short - git config user.name "LineageWeave hourly maintainer" - git config user.email "8172694+seonghobae@users.noreply.github.com" - git add backend/app/report_ingestion.py backend/tests/test_period_comparison_cross_share.py frontend/src/App.test.tsx .github/workflows/automation-831-nonfinite-cross-share-repair.yml - git commit -m "fix(reports): normalize nonfinite comparison cross share" - git push origin HEAD:feat/leftover-map-compare-cross-share-v2530 diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index 048e786e2..68ab84da4 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import math import re from collections import defaultdict from datetime import datetime, timezone @@ -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) @@ -1144,10 +1154,8 @@ async def fetch_period_comparison( if pair["leftover_map_unexplained_share"] is None else float(pair["leftover_map_unexplained_share"]) ), - "leftover_map_cross_share": ( - None - if pair["leftover_map_cross_share"] is None - else float(pair["leftover_map_cross_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"]), diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index fb9e4befd..239a5a221 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -929,6 +929,7 @@ describe("App, authenticated", () => { leftover_map_reconstruction: 0.248, leftover_map_explained_share: 0.76, leftover_map_unexplained_share: 0.02, + leftover_map_cross_share: -0.24, }, ], leftover_map_coverage: { @@ -4404,6 +4405,14 @@ describe("App, authenticated", () => { name: /open leftover closest pair from comparison: public post.*leftover map comparison unexplained leftover share U²\/R² 0\.02/i, }); expect(unexplainedSharePair).toHaveTextContent("U²/R² 0.02"); + const comparisonCrossSharePair = screen.getByRole("button", { + name: /open leftover closest pair from comparison: public post.*2R̂U\/R² -0\.24/i, + }); + expect(comparisonCrossSharePair).toHaveTextContent("2R̂U/R² -0.24"); + expect(within(comparisonCrossSharePair).getByText("2R̂U/R² -0.24")).toHaveAttribute( + "aria-hidden", + "true", + ); await waitFor(() => expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/api/reports/thread_group/2026-W02"), From 8d7d8fe17de30ef08121d9f38fb1e7279e64354a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 10:45:33 +0900 Subject: [PATCH 20/43] test(reports): keep unexplained-share fixture compatible with cross-share projection --- backend/tests/test_report_ingestion_unexplained_share.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/tests/test_report_ingestion_unexplained_share.py b/backend/tests/test_report_ingestion_unexplained_share.py index 0c6f6dff3..d17a50458 100644 --- a/backend/tests/test_report_ingestion_unexplained_share.py +++ b/backend/tests/test_report_ingestion_unexplained_share.py @@ -27,6 +27,7 @@ async def fetch(self, query: str, *_args: object) -> list[dict[str, object]]: "leftover_distance": 0.5, "leftover_residual": 0.4, "leftover_map_reconstruction": Decimal("0.25"), + "leftover_map_cross_share": None, "visibility_code": "public", "corporate_entity_id": "00000000-0000-0000-0000-000000000002", "has_real_source_context": False, From 9cc136c7eeb2d8639f6770b102e758491b149f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:49:30 +0900 Subject: [PATCH 21/43] test(report): reproduce non-finite primary cross-share leak --- ..._report_ingestion_cross_share_nonfinite.py | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 backend/tests/test_report_ingestion_cross_share_nonfinite.py diff --git a/backend/tests/test_report_ingestion_cross_share_nonfinite.py b/backend/tests/test_report_ingestion_cross_share_nonfinite.py new file mode 100644 index 000000000..ce01dbb90 --- /dev/null +++ b/backend/tests/test_report_ingestion_cross_share_nonfinite.py @@ -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) From b02e7d5cb4e856c49dca9e7fe74c5bb217796df7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 12:35:15 +0900 Subject: [PATCH 22/43] fix(report): normalize persisted cross-share at primary read boundary --- backend/app/report_ingestion.py | 6 +- ...test_period_report_cross_share_postgres.py | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_period_report_cross_share_postgres.py diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index 68ab84da4..9a3e2f59c 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -821,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 diff --git a/backend/tests/test_period_report_cross_share_postgres.py b/backend/tests/test_period_report_cross_share_postgres.py new file mode 100644 index 000000000..ff9980580 --- /dev/null +++ b/backend/tests/test_period_report_cross_share_postgres.py @@ -0,0 +1,81 @@ +"""PostgreSQL regression for primary-report cross-share JSON safety.""" + +from __future__ import annotations + +import asyncio +import json +import math + +import asyncpg +import psycopg2 + +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 + + +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) From d581a73c0be5a4906d9e6b19578f5dff4498d4c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 13:45:49 +0900 Subject: [PATCH 23/43] test(reports): decouple PostgreSQL cross-share fixture from Keycloak --- .../tests/test_period_report_cross_share_postgres.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/tests/test_period_report_cross_share_postgres.py b/backend/tests/test_period_report_cross_share_postgres.py index ff9980580..389cbc2a3 100644 --- a/backend/tests/test_period_report_cross_share_postgres.py +++ b/backend/tests/test_period_report_cross_share_postgres.py @@ -5,9 +5,12 @@ 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 @@ -16,6 +19,13 @@ 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: From 44c7ed4c4afb704151cb3700af6cb31e177bffd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:59:28 +0900 Subject: [PATCH 24/43] test(e2e): reuse real OIDC login for demo admin --- frontend/e2e/support/auth.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/frontend/e2e/support/auth.ts b/frontend/e2e/support/auth.ts index 5f3740ade..241eb054d 100644 --- a/frontend/e2e/support/auth.ts +++ b/frontend/e2e/support/auth.ts @@ -1,27 +1,26 @@ import type { Page } from "@playwright/test"; -/** - * Synthetic demo credentials seeded by `make seed` -- never a real account. - * See `backend/tests/test_api.py`'s `_fetch_demo_analyst_token` for the - * same login this drives through the real Keycloak realm. - */ -const DEMO_USERNAME = "demo.analyst"; +/** Synthetic demo identities seeded by the local product stack; never real accounts. */ const DEMO_PASSWORD = "lineageweave-demo-only"; +type DemoUsername = "demo.analyst" | "demo.admin"; -/** - * Logs in through the real Keycloak-hosted login form (OIDC redirect, - * not a token injected into storage) so the e2e suite exercises the same - * authorization-code flow a reader actually goes through. - * - * Next action: call this once per test before interacting with any - * authenticated destination. - */ -export async function loginAsDemoAnalyst(page: Page): Promise { +/** Exercise the real Keycloak authorization-code form instead of injecting a token. */ +async function loginAsDemoUser(page: Page, username: DemoUsername): Promise { await page.goto("/"); await page.getByRole("button", { name: "Log in" }).click(); await page.waitForURL(/\/realms\/lineageweave-demo\/protocol\/openid-connect\/auth/); - await page.getByLabel("Username or email").fill(DEMO_USERNAME); + await page.getByLabel("Username or email").fill(username); await page.getByLabel("Password", { exact: true }).fill(DEMO_PASSWORD); await page.getByRole("button", { name: "Sign In" }).click(); await page.waitForURL((url) => !url.pathname.includes("/realms/")); } + +/** Log in with the ABAC-narrowed synthetic analyst identity. */ +export async function loginAsDemoAnalyst(page: Page): Promise { + await loginAsDemoUser(page, "demo.analyst"); +} + +/** Log in with the synthetic product-admin identity used by advanced report tools. */ +export async function loginAsDemoAdmin(page: Page): Promise { + await loginAsDemoUser(page, "demo.admin"); +} From 6f3b1a186379f2dca8d30084758b52ebb1ebf392 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 15:59:55 +0900 Subject: [PATCH 25/43] test(e2e): cover grouping comparison cross-share in browser --- .../grouping-comparison-cross-share.spec.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 frontend/e2e/grouping-comparison-cross-share.spec.ts diff --git a/frontend/e2e/grouping-comparison-cross-share.spec.ts b/frontend/e2e/grouping-comparison-cross-share.spec.ts new file mode 100644 index 000000000..55d1a67c8 --- /dev/null +++ b/frontend/e2e/grouping-comparison-cross-share.spec.ts @@ -0,0 +1,80 @@ +import { devices, expect, test, type Page } from "@playwright/test"; +import { loginAsDemoAdmin } from "./support/auth.ts"; + +const CROSS_SHARE = "2R̂U/R² -0.24"; +const CROSS_SHARE_NAME = /2R̂U\/R² -0\.24/; + +async function openGroupingComparison(page: Page) { + await loginAsDemoAdmin(page); + await page.locator(".language-switcher select").selectOption("en"); + await page.getByRole("button", { name: "게시판" }).click(); + + const advancedTools = page.locator("details.advanced-review-tools"); + await expect(advancedTools).toBeVisible(); + if (!(await advancedTools.getAttribute("open"))) { + await advancedTools.locator("summary").click(); + } + + const comparison = page.getByLabel("Grouping comparison"); + await expect(comparison).toBeVisible(); + return comparison; +} + +async function expectNoHorizontalOverflow(page: Page) { + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= Math.ceil(window.innerWidth) + 1, + ), + ).toBe(true); +} + +test("keeps persisted cross-share actionable in the rendered accessibility tree", async ({ page }) => { + const comparison = await openGroupingComparison(page); + const pair = comparison.getByRole("button", { name: CROSS_SHARE_NAME }).first(); + + await expect(pair).toBeVisible(); + await expect(pair).toContainText(CROSS_SHARE); + await expect(pair.getByText(CROSS_SHARE, { exact: true })).toHaveAttribute("aria-hidden", "true"); + + // Pointer hit-testing is part of acceptance, but a trial click avoids changing + // the report state before keyboard and responsive checks run on the same node. + await pair.hover(); + await pair.click({ trial: true }); + + await pair.focus(); + await expect(pair).toBeFocused(); + await page.keyboard.press("Shift+Tab"); + await page.keyboard.press("Tab"); + await expect(pair).toBeFocused(); + + await page.setViewportSize({ width: 390, height: 844 }); + await expect(pair).toBeVisible(); + await expectNoHorizontalOverflow(page); + + // Exercise every locale the exact product head exposes. When the canonical + // translation-ledger owner adds ES/DE/FR, this loop covers them without a + // LineageWeave-local locale fork. + const localeSelect = page.locator(".language-switcher select"); + const locales = await localeSelect.locator("option").evaluateAll((options) => + options.map((option) => (option as HTMLOptionElement).value), + ); + expect(locales).toEqual(expect.arrayContaining(["en", "ko", "zh", "ja", "vi"])); + for (const locale of locales) { + await localeSelect.selectOption(locale); + await expect(comparison.getByRole("button", { name: CROSS_SHARE_NAME }).first()).toBeVisible(); + await expectNoHorizontalOverflow(page); + } +}); + +test.describe("touch interaction", () => { + test.use({ ...devices["Pixel 7"] }); + + test("keeps the cross-share pair tappable on a phone viewport", async ({ page }) => { + const comparison = await openGroupingComparison(page); + const pair = comparison.getByRole("button", { name: CROSS_SHARE_NAME }).first(); + + await expect(pair).toBeVisible(); + await pair.tap({ trial: true }); + await expectNoHorizontalOverflow(page); + }); +}); From 6315b2adde0664194c1366a80332d2c0fb011299 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:00:36 +0900 Subject: [PATCH 26/43] ci(e2e): run authenticated grouping comparison browser gate --- .github/workflows/tests.yml | 51 ++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9a73249f6..8612c2501 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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: @@ -100,3 +100,52 @@ 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: docker compose --env-file "$HOME/.env" logs --no-color --tail=400 + + - name: Tear down product stack + if: always() + run: docker compose --env-file "$HOME/.env" down -v --remove-orphans From 579703fdf39ff162c6f5cdd3b47a8050c1879c47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:03:51 +0900 Subject: [PATCH 27/43] fix(e2e): keep browser assertions out of Node DOM types --- frontend/e2e/grouping-comparison-cross-share.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/e2e/grouping-comparison-cross-share.spec.ts b/frontend/e2e/grouping-comparison-cross-share.spec.ts index 55d1a67c8..b1d8fa8d8 100644 --- a/frontend/e2e/grouping-comparison-cross-share.spec.ts +++ b/frontend/e2e/grouping-comparison-cross-share.spec.ts @@ -23,7 +23,7 @@ async function openGroupingComparison(page: Page) { async function expectNoHorizontalOverflow(page: Page) { expect( await page.evaluate( - () => document.documentElement.scrollWidth <= Math.ceil(window.innerWidth) + 1, + "document.documentElement.scrollWidth <= Math.ceil(window.innerWidth) + 1", ), ).toBe(true); } @@ -55,9 +55,9 @@ test("keeps persisted cross-share actionable in the rendered accessibility tree" // translation-ledger owner adds ES/DE/FR, this loop covers them without a // LineageWeave-local locale fork. const localeSelect = page.locator(".language-switcher select"); - const locales = await localeSelect.locator("option").evaluateAll((options) => - options.map((option) => (option as HTMLOptionElement).value), - ); + const locales = (await localeSelect + .locator("option") + .evaluateAll("options => options.map(option => option.value)")) as string[]; expect(locales).toEqual(expect.arrayContaining(["en", "ko", "zh", "ja", "vi"])); for (const locale of locales) { await localeSelect.selectOption(locale); From ac50985c681bb3184af53803e460ef918d601076 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:04:10 +0900 Subject: [PATCH 28/43] fix(ci): make browser cleanup safe before stack startup --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8612c2501..02e9defee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -144,8 +144,12 @@ jobs: - name: Capture product stack logs on browser failure if: failure() - run: docker compose --env-file "$HOME/.env" logs --no-color --tail=400 + run: | + touch "$HOME/.env" + docker compose --env-file "$HOME/.env" logs --no-color --tail=400 - name: Tear down product stack if: always() - run: docker compose --env-file "$HOME/.env" down -v --remove-orphans + run: | + touch "$HOME/.env" + docker compose --env-file "$HOME/.env" down -v --remove-orphans From 9d16d113bfcef9bad57dc1ffb473f97c9f7f3b23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 16:59:47 +0900 Subject: [PATCH 29/43] test(compose): require credential-free default profile --- ..._compose_optional_orchestrator_contract.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_compose_optional_orchestrator_contract.py diff --git a/tests/test_compose_optional_orchestrator_contract.py b/tests/test_compose_optional_orchestrator_contract.py new file mode 100644 index 000000000..3c6ba0e4c --- /dev/null +++ b/tests/test_compose_optional_orchestrator_contract.py @@ -0,0 +1,28 @@ +from pathlib import Path + + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_COMPOSE = (_REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8") +_ENV_EXAMPLE = (_REPO_ROOT / ".env.example").read_text(encoding="utf-8") + + +def _service_block(name: str, next_name: str) -> str: + """Return one top-level Compose service block for contract assertions.""" + start = _COMPOSE.index(f" {name}:\n") + end = _COMPOSE.index(f" {next_name}:\n", start) + return _COMPOSE[start:end] + + +def test_default_compose_keeps_llm_runtime_optional() -> None: + """Match the documented clean-checkout default with fail-closed LLM absence.""" + assert "`docker compose up` succeeds from a clean checkout" in _ENV_EXAMPLE + orchestrator = _service_block("orchestrator", "backend") + backend = _service_block("backend", "mcp") + mcp = _service_block("mcp", "frontend") + + assert 'profiles: ["llm"]' in orchestrator + assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in backend + assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in backend + assert "condition: service_healthy\n orchestrator:" not in backend + assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in mcp + assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in mcp From 1537af5db89b0c50f693efb356bc1e5064f9d964 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:00:56 +0900 Subject: [PATCH 30/43] fix(compose): keep orchestrator opt-in and fail closed --- docker-compose.yml | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d0a2422aa..170300315 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -98,8 +98,11 @@ services: condition: service_healthy orchestrator: - # Consume the paper-grounded orchestration service from main; inference - # remains behind its authenticated OpenAI-compatible boundary. + # LLM/vision is an optional product capability. The default profile must + # remain runnable without provider credentials, matching .env.example; + # enabling this profile consumes the canonical contextual-orchestrator + # boundary instead of creating a LineageWeave-local provider path. + profiles: ["llm"] build: context: ./docker/contextual-orchestrator dockerfile: Dockerfile @@ -129,11 +132,6 @@ services: interval: 5s timeout: 3s retries: 10 - # Warm-up window: failures inside start_period do not consume the retry - # budget, so a booting orchestrator that becomes healthy within 50s is - # never counted against retries. A dead service trips the gate at - # ~100s (start_period + 10 x 5s), matching the previous retries: 20 - # budget exactly; the win is boot tolerance, not faster dead detection. start_period: 50s backend: @@ -142,16 +140,10 @@ services: dockerfile: backend/Dockerfile environment: DATABASE_URL: postgresql://${POSTGRES_USER:-lineageweave}:${POSTGRES_PASSWORD:-lineageweave_dev_only}@postgres:5432/${POSTGRES_DB:-lineageweave} - # Internal DNS name for JWKS fetches (always reachable from inside the - # compose network); KEYCLOAK_ISSUER is the *external*, host-published - # URL a browser/token actually carries -- see backend/app/config.py. KEYCLOAK_BASE_URL: http://keycloak:8080 KEYCLOAK_ISSUER: http://localhost:${KEYCLOAK_PORT:-18080}/realms/lineageweave-demo KEYCLOAK_REALM: lineageweave-demo KEYCLOAK_CLIENT_ID: lineageweave-frontend - # Production may set these to the real Keyverse OIDC provider. Empty - # values keep this stack on its explicit local Keycloak development mode; - # no Keyverse-shaped identity service is created by Compose. KEYVERSE_ISSUER: ${KEYVERSE_ISSUER:-} KEYVERSE_CLIENT_ID: ${KEYVERSE_CLIENT_ID:-} KEYVERSE_AUDIENCE: ${KEYVERSE_AUDIENCE:-} @@ -168,11 +160,10 @@ services: OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-lineageweave} OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-} # Empty by default: every LLM/vision channel stays the Null client - # (dropped, not faked). Set these to a running contextual-orchestrator - # to turn the channels on. Provider credentials use LLM_GATEWAY_API_URL / - # LLM_GATEWAY_API_KEY in the orchestrator's private env file. - ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-http://orchestrator:8000} - ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-${CONTEXTUAL_ORCHESTRATOR_TOKEN:-lineageweave-orchestrator-dev-only}} + # (dropped, not faked). Opt into the llm Compose profile and point these + # at its contextual-orchestrator service to turn the channels on. + ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-} + ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-} SEARXNG_BASE_URL: http://searxng:8080 TEPP_TRANSPORT_URL: ${TEPP_TRANSPORT_URL:-} TEPP_API_KEY: ${TEPP_API_KEY:-} @@ -180,8 +171,6 @@ services: NARUON_CALENDAR_BASE_URL: ${NARUON_CALENDAR_BASE_URL:-} NARUON_CALENDAR_SERVICE_TOKEN: ${NARUON_CALENDAR_SERVICE_TOKEN:-} RANKWEAVE_DISABLED: ${RANKWEAVE_DISABLED:-} - # Process HMAC for ontology source-window continuation. Empty keeps the - # truncated-without-cursor contract. Never reuse an OIDC or orchestrator secret. ONTOLOGY_SOURCE_CURSOR_SECRET: ${ONTOLOGY_SOURCE_CURSOR_SECRET:-} ports: - "${BACKEND_PORT:-18420}:8000" @@ -190,8 +179,6 @@ services: condition: service_healthy database_migration: condition: service_completed_successfully - orchestrator: - condition: service_healthy keycloak: condition: service_started valkey: @@ -222,17 +209,13 @@ services: OIDC_JWKS_URI: ${OIDC_JWKS_URI:-} OIDC_CLOCK_SKEW_SECONDS: ${OIDC_CLOCK_SKEW_SECONDS:-5} VALKEY_URL: redis://valkey:6379/0 - ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-http://orchestrator:8000} - ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-${CONTEXTUAL_ORCHESTRATOR_TOKEN:-lineageweave-orchestrator-dev-only}} - # Local Keycloak mints this exact fixed audience. Production Keyverse - # deployments configure both values together outside this demo stack. + ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-} + ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-} MCP_RESOURCE_URL: http://localhost:18001/mcp MCP_AUDIENCE: http://localhost:18001/mcp MCP_ALLOWED_HOSTS: localhost:*,127.0.0.1:*,mcp:8001 MCP_ALLOWED_ORIGINS: ${MCP_ALLOWED_ORIGINS:-} MCP_MAX_REQUEST_BYTES: ${MCP_MAX_REQUEST_BYTES:-65536} - # No guessed quota: operators must supply values justified by the k6 - # capacity artifact for their deployment before enabling this profile. MCP_RATE_LIMIT_REQUESTS: ${MCP_RATE_LIMIT_REQUESTS:-} MCP_RATE_LIMIT_WINDOW_SECONDS: ${MCP_RATE_LIMIT_WINDOW_SECONDS:-} ports: @@ -242,8 +225,6 @@ services: condition: service_healthy database_migration: condition: service_completed_successfully - orchestrator: - condition: service_healthy keycloak: condition: service_started valkey: From 17db1ad0ae13761b34f820cf29ab55eda4992fbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:01:18 +0900 Subject: [PATCH 31/43] docs(compose): expose explicit local llm profile --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b6764b72f..260216627 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: up down logs smoke seed ps load-http load-mcp +.PHONY: up up-llm down logs smoke seed ps load-http load-mcp # Keep provider credentials outside the repository. Compose interpolation must # read the same home env file as the orchestrator container's env_file. @@ -7,6 +7,12 @@ COMPOSE := docker compose --env-file "$$HOME/.env" up: $(COMPOSE) up -d +# Opt in to the pinned contextual-orchestrator service. Provider credentials +# remain external to the repository; absent credentials fail that optional +# service closed instead of making the default product profile unbootable. +up-llm: + $(COMPOSE) --profile llm up -d + down: $(COMPOSE) down From 65bbb915bff986cb163594ed51f0dcf4e453d2ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:01:35 +0900 Subject: [PATCH 32/43] docs(compose): document optional llm profile --- .env.example | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 7282c5a2e..1dc0c1654 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,10 @@ # default baked into docker-compose.yml (see ${VAR:-default} references) -- # `docker compose up` succeeds from a clean checkout with no .env file at # all for the default profile. The optional MCP profile requires measured -# quota inputs below. Other defaults are throwaway local-dev-only credentials, not -# production secrets; see docs/adr/0001-demo-identity-and-data-boundary.md. +# quota inputs below; the optional `llm` profile requires a configured +# contextual-orchestrator provider. Other defaults are throwaway local-dev-only +# credentials, not production secrets; see +# docs/adr/0001-demo-identity-and-data-boundary.md. # Host ports deliberately avoid each service's own default (5432, 6379, # 8080) -- a dev machine commonly already runs its own Postgres/Redis/local @@ -36,7 +38,9 @@ MCP_RATE_LIMIT_WINDOW_SECONDS= # Optional. Empty = every LLM/vision channel is unavailable (Null client, # dropped and renormalized -- never a placeholder score). Point these at a -# running contextual-orchestrator to turn the channels on. +# running contextual-orchestrator to turn the channels on. For the repository's +# pinned local orchestrator, configure these for that service and use +# `make up-llm`; plain `make up` keeps the LLM runtime out of the default graph. ORCHESTRATOR_BASE_URL= ORCHESTRATOR_API_KEY= From f0b0e3ac6ce09ceaa7a4d845ac24371d135560f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:02:09 +0900 Subject: [PATCH 33/43] test(compose): pin optional dependency graph --- tests/test_compose_optional_orchestrator_contract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_compose_optional_orchestrator_contract.py b/tests/test_compose_optional_orchestrator_contract.py index 3c6ba0e4c..d336a8129 100644 --- a/tests/test_compose_optional_orchestrator_contract.py +++ b/tests/test_compose_optional_orchestrator_contract.py @@ -13,6 +13,11 @@ def _service_block(name: str, next_name: str) -> str: return _COMPOSE[start:end] +def _depends_on_block(service: str) -> str: + """Return only dependency declarations, excluding environment references.""" + return service.split(" depends_on:\n", maxsplit=1)[1] + + def test_default_compose_keeps_llm_runtime_optional() -> None: """Match the documented clean-checkout default with fail-closed LLM absence.""" assert "`docker compose up` succeeds from a clean checkout" in _ENV_EXAMPLE @@ -23,6 +28,7 @@ def test_default_compose_keeps_llm_runtime_optional() -> None: assert 'profiles: ["llm"]' in orchestrator assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in backend assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in backend - assert "condition: service_healthy\n orchestrator:" not in backend + assert " orchestrator:\n" not in _depends_on_block(backend) assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in mcp assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in mcp + assert " orchestrator:\n" not in _depends_on_block(mcp) From 81ad1e9e388df6256bb28c062f09fff8a6dd901b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:03:17 +0900 Subject: [PATCH 34/43] docs(e2e): align browser stack with optional llm profile --- frontend/playwright.config.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index a3fb286f5..d69dc2e36 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -2,10 +2,12 @@ import { defineConfig, devices } from "@playwright/test"; /** * Runs against the already-running docker-compose stack (`make up`), not a - * dev-server Playwright starts itself -- the app needs Postgres, Keycloak, - * Valkey, and the orchestrator alongside it, which `webServer` can't provide. - * Point `LINEAGEWEAVE_E2E_BASE_URL` at a different origin if the compose - * port mapping changes. + * dev-server Playwright starts itself -- authenticated product paths need + * Postgres, Keycloak, Valkey, backend, and frontend together. LLM/vision is a + * separate optional Compose profile; browser contracts that do not invoke it + * must not require provider credentials just to boot. Point + * `LINEAGEWEAVE_E2E_BASE_URL` at a different origin if the compose port mapping + * changes. */ export default defineConfig({ testDir: "./e2e", From 00cde1565cfaec0f7a63b0c7c7e74c7ecfb1923e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 17:53:48 +0900 Subject: [PATCH 35/43] fix(ci): seed browser fixture with locked dev dependency --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02e9defee..6ae9f3c05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -122,10 +122,12 @@ jobs: - name: Seed synthetic authenticated product data run: | + # The seed utility intentionally keeps psycopg2 in the dev extra. Install + # it only in this disposable seed container, not the backend runtime image. 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 \ + backend uv run --frozen --extra backend --extra dev 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 \ From 1bcdcc5427ca5d1083840947309a2ec0d46c57fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 20:10:10 +0900 Subject: [PATCH 36/43] fix(compose): pin Keycloak issuer to the public URL Keycloak 26 hostname v2 derives the iss claim from the request host. With a bare KC_HOSTNAME=localhost, the in-network call the seed makes to keycloak:8080 mints iss=http://localhost:8080/... while the backend validates KEYCLOAK_ISSUER=http://localhost:18080/... -- so every seeded post-content warm-up 401s and the authenticated browser gate cannot start. Pin KC_HOSTNAME to the full public URL so one issuer serves browser and in-network callers alike, and lock the invariant with a compose contract test. --- docker-compose.yml | 6 +++++- .../test_compose_optional_orchestrator_contract.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 170300315..da5c1c415 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -86,7 +86,11 @@ services: KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak KC_DB_USERNAME: ${POSTGRES_USER:-lineageweave} KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-lineageweave_dev_only} - KC_HOSTNAME: localhost + # Pin the full public URL, not a bare host. Keycloak 26's hostname v2 + # derives the ``iss`` claim from the request host, so a bare "localhost" + # makes an in-network call to keycloak:8080 mint an issuer the backend + # rejects. A full URL here keeps one issuer for every caller. + KC_HOSTNAME: http://localhost:${KEYCLOAK_PORT:-18080} KC_HOSTNAME_STRICT: "false" KC_HTTP_ENABLED: "true" KC_HEALTH_ENABLED: "true" diff --git a/tests/test_compose_optional_orchestrator_contract.py b/tests/test_compose_optional_orchestrator_contract.py index d336a8129..39ab540d5 100644 --- a/tests/test_compose_optional_orchestrator_contract.py +++ b/tests/test_compose_optional_orchestrator_contract.py @@ -32,3 +32,17 @@ def test_default_compose_keeps_llm_runtime_optional() -> None: assert "ORCHESTRATOR_BASE_URL: ${ORCHESTRATOR_BASE_URL:-}" in mcp assert "ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY:-}" in mcp assert " orchestrator:\n" not in _depends_on_block(mcp) + + +def test_keycloak_pins_issuer_to_public_url() -> None: + """Keycloak's ``iss`` must not depend on which host calls it. + + Keycloak 26 hostname v2 derives the issuer from the request host. A bare + ``localhost`` therefore makes the in-network ``keycloak:8080`` call that + seeds demo content mint an issuer the backend (``KEYCLOAK_ISSUER`` on the + public URL) rejects with 401. A full public URL keeps one issuer for every + caller, in-network or browser. + """ + keycloak = _service_block("keycloak", "orchestrator") + assert "KC_HOSTNAME: http://localhost:${KEYCLOAK_PORT:-18080}" in keycloak + assert "KC_HOSTNAME: localhost\n" not in keycloak From bddc24397875357bcd1e3123f618ccf8153ffc54 Mon Sep 17 00:00:00 2001 From: Seong Ho Bae Date: Fri, 11 Sep 2026 20:12:53 +0900 Subject: [PATCH 37/43] docs(config): describe the pinned issuer, not host reflection The keycloak_issuer comment still described hostname-strict=false reflecting the request Host into the iss claim. Compose now pins KC_HOSTNAME to the public URL, so the issuer no longer varies by caller. --- backend/app/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 0fea9a591..c3568909d 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -26,12 +26,12 @@ class Settings: keycloak_base_url: str keycloak_realm: str keycloak_client_id: str - # The issuer string real tokens actually carry -- whatever hostname the - # browser/client used to log in (Keycloak's hostname-strict=false mode - # reflects the request's Host header into the `iss` claim). This is - # deliberately a *separate* setting from keycloak_base_url: inside - # docker-compose the two differ (internal DNS name vs. the - # host-published port a browser actually hits). + # The issuer string real tokens actually carry -- the identity provider's + # configured public URL, which Compose pins with KC_HOSTNAME so every + # caller (browser or in-network service) sees one issuer. It stays a + # *separate* setting from keycloak_base_url because the two differ inside + # docker-compose: the issuer is the host-published URL a browser reaches, + # while keycloak_base_url is the internal DNS name used only for JWKS. keycloak_issuer: str # Production may use the organization's Keyverse OIDC issuer. The # keycloak fields above remain the explicit local-development fallback. From b4c8ea23ce3486254b0e52c2132c6ddc1c78ce00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:55:28 +0900 Subject: [PATCH 38/43] ci(e2e): retain Playwright failure evidence --- .github/workflows/tests.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6ae9f3c05..939f3407c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -144,6 +144,15 @@ jobs: working-directory: frontend run: pnpm exec playwright test grouping-comparison-cross-share.spec.ts --project=chromium + - name: Retain Playwright failure evidence + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4 + with: + name: playwright-grouping-comparison-${{ github.run_id }}-${{ github.run_attempt }} + path: frontend/test-results + if-no-files-found: warn + retention-days: 7 + - name: Capture product stack logs on browser failure if: failure() run: | From 4991912a822ef6db5cd6c063af8df1d84dd274e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 23:06:19 +0900 Subject: [PATCH 39/43] ci(e2e): persist browser assertion output --- .github/workflows/tests.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 939f3407c..bfa1bf0cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,15 +142,20 @@ jobs: - name: Run authenticated grouping-comparison browser acceptance working-directory: frontend - run: pnpm exec playwright test grouping-comparison-cross-share.spec.ts --project=chromium + run: | + set -o pipefail + pnpm exec playwright test grouping-comparison-cross-share.spec.ts --project=chromium \ + 2>&1 | tee playwright-grouping-comparison.log - name: Retain Playwright failure evidence if: failure() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4 with: name: playwright-grouping-comparison-${{ github.run_id }}-${{ github.run_attempt }} - path: frontend/test-results - if-no-files-found: warn + path: | + frontend/playwright-grouping-comparison.log + frontend/test-results + if-no-files-found: error retention-days: 7 - name: Capture product stack logs on browser failure From 41e54f9e1fdf27c0ad557a304671b2e036a1586b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 23:20:57 +0900 Subject: [PATCH 40/43] test(e2e): keep Pixel 7 override context-scoped --- frontend/e2e/grouping-comparison-cross-share.spec.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/e2e/grouping-comparison-cross-share.spec.ts b/frontend/e2e/grouping-comparison-cross-share.spec.ts index b1d8fa8d8..88daf6a02 100644 --- a/frontend/e2e/grouping-comparison-cross-share.spec.ts +++ b/frontend/e2e/grouping-comparison-cross-share.spec.ts @@ -3,6 +3,7 @@ import { loginAsDemoAdmin } from "./support/auth.ts"; const CROSS_SHARE = "2R̂U/R² -0.24"; const CROSS_SHARE_NAME = /2R̂U\/R² -0\.24/; +const PIXEL_7 = devices["Pixel 7"]; async function openGroupingComparison(page: Page) { await loginAsDemoAdmin(page); @@ -67,7 +68,16 @@ test("keeps persisted cross-share actionable in the rendered accessibility tree" }); test.describe("touch interaction", () => { - test.use({ ...devices["Pixel 7"] }); + // `defaultBrowserType` is worker-scoped, so a describe-local override must + // apply only Pixel 7 browser-context options or Playwright aborts collection. + test.use({ + userAgent: PIXEL_7.userAgent, + viewport: PIXEL_7.viewport, + screen: PIXEL_7.screen, + deviceScaleFactor: PIXEL_7.deviceScaleFactor, + isMobile: PIXEL_7.isMobile, + hasTouch: PIXEL_7.hasTouch, + }); test("keeps the cross-share pair tappable on a phone viewport", async ({ page }) => { const comparison = await openGroupingComparison(page); From f5621a674b39fc468bd709489a762ee304e96517 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 23:23:31 +0900 Subject: [PATCH 41/43] test(e2e): keep Pixel 7 override type-safe --- frontend/e2e/grouping-comparison-cross-share.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/e2e/grouping-comparison-cross-share.spec.ts b/frontend/e2e/grouping-comparison-cross-share.spec.ts index 88daf6a02..15e6cc0c1 100644 --- a/frontend/e2e/grouping-comparison-cross-share.spec.ts +++ b/frontend/e2e/grouping-comparison-cross-share.spec.ts @@ -73,7 +73,6 @@ test.describe("touch interaction", () => { test.use({ userAgent: PIXEL_7.userAgent, viewport: PIXEL_7.viewport, - screen: PIXEL_7.screen, deviceScaleFactor: PIXEL_7.deviceScaleFactor, isMobile: PIXEL_7.isMobile, hasTouch: PIXEL_7.hasTouch, From f5b181d1a6f6d426afcf58aa3d183b7e197a64bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 23:23:58 +0900 Subject: [PATCH 42/43] ci(e2e): scope browser failure evidence to browser RED --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bfa1bf0cb..bef53fa02 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -141,6 +141,7 @@ jobs: -c "update report_leftover_pair set leftover_map_cross_share = -0.24;" - name: Run authenticated grouping-comparison browser acceptance + id: browser_acceptance working-directory: frontend run: | set -o pipefail @@ -148,7 +149,7 @@ jobs: 2>&1 | tee playwright-grouping-comparison.log - name: Retain Playwright failure evidence - if: failure() + if: failure() && steps.browser_acceptance.outcome == 'failure' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4 with: name: playwright-grouping-comparison-${{ github.run_id }}-${{ github.run_attempt }} @@ -159,7 +160,7 @@ jobs: retention-days: 7 - name: Capture product stack logs on browser failure - if: failure() + if: failure() && steps.browser_acceptance.outcome == 'failure' run: | touch "$HOME/.env" docker compose --env-file "$HOME/.env" logs --no-color --tail=400 From b013b8b7bc717c543b0d2ca8458c60dcc1f2144e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 23:47:37 +0900 Subject: [PATCH 43/43] fix(ui): keep workspace navigation reachable on phones --- .../grouping-comparison-cross-share.spec.ts | 6 ++-- frontend/src/components/WorkspaceNav.css | 33 +++++++++++++++++++ frontend/src/components/WorkspaceNav.tsx | 6 +++- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/WorkspaceNav.css diff --git a/frontend/e2e/grouping-comparison-cross-share.spec.ts b/frontend/e2e/grouping-comparison-cross-share.spec.ts index 15e6cc0c1..4618d56d6 100644 --- a/frontend/e2e/grouping-comparison-cross-share.spec.ts +++ b/frontend/e2e/grouping-comparison-cross-share.spec.ts @@ -56,9 +56,9 @@ test("keeps persisted cross-share actionable in the rendered accessibility tree" // translation-ledger owner adds ES/DE/FR, this loop covers them without a // LineageWeave-local locale fork. const localeSelect = page.locator(".language-switcher select"); - const locales = (await localeSelect - .locator("option") - .evaluateAll("options => options.map(option => option.value)")) as string[]; + const locales = await localeSelect.locator("option").evaluateAll((options) => + options.map((option) => option.getAttribute("value") ?? ""), + ); expect(locales).toEqual(expect.arrayContaining(["en", "ko", "zh", "ja", "vi"])); for (const locale of locales) { await localeSelect.selectOption(locale); diff --git a/frontend/src/components/WorkspaceNav.css b/frontend/src/components/WorkspaceNav.css new file mode 100644 index 000000000..d5a1ba651 --- /dev/null +++ b/frontend/src/components/WorkspaceNav.css @@ -0,0 +1,33 @@ +/* The legacy shell stylesheet hides the workspace GNB below 768px for a drawer + that is not implemented. Keep the same semantic navigation and product tools + reachable on phones instead of rendering inaccessible controls off-screen. */ +@media (max-width: 768px) { + .workspace-gnb.workspace-gnb-responsive { + display: flex; + flex-wrap: wrap; + align-items: stretch; + height: auto; + min-height: var(--gnb-height); + padding: 0.5rem 1rem; + gap: 0.25rem 0.5rem; + } + + .workspace-gnb-responsive .workspace-gnb-item { + flex: 1 1 calc(50% - 0.25rem); + justify-content: center; + min-height: var(--size-control-min); + height: auto; + padding: 0.5rem 0.75rem; + } + + .workspace-gnb-responsive .workspace-gnb-tools { + flex: 1 0 100%; + width: 100%; + margin-left: 0; + } + + .workspace-gnb-responsive .language-switcher, + .workspace-gnb-responsive .language-switcher select { + width: 100%; + } +} diff --git a/frontend/src/components/WorkspaceNav.tsx b/frontend/src/components/WorkspaceNav.tsx index 933bde9f5..f793b9cac 100644 --- a/frontend/src/components/WorkspaceNav.tsx +++ b/frontend/src/components/WorkspaceNav.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from "react"; import { ANALYST_GNB_ITEMS, type AnalystGnbId } from "../gnbChrome"; import { t } from "../i18n"; +import "./WorkspaceNav.css"; export type WorkspaceDestination = AnalystGnbId | "admin"; @@ -12,7 +13,10 @@ export type WorkspaceNavProps = { export function WorkspaceNav({ destination, onChange, tools }: WorkspaceNavProps) { return ( -