feat: start a pending lineage reconstruction (v0.89.0) - #167
Conversation
POST /api/analysis-runs/{id}/start runs ThreadWeave on the authorized
cutoff bag and persists run-scoped edges. The home detail starts that
row so a buyer can confirm the designed A-100 fork. TEPP start stays
422 — this path never invents a theta.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Stale comment
Do not merge #167 at
da73880.The start-reconstruction slice is the right next buyer action after the write-clock marks, but this head cannot land on current #74:
- It stacks on dirty #162 / #150, not current #74 (
69c035b).- It reuses ADR 0020 and migration 0020. #74 already shipped those numbers for retention purge (
0020-analysis-run-retention-purge). A second 0020 would overwrite the purge contract.Rebase onto #176 (
7e4a4b0, write clock + live-body popup on current #74) and take 0021 for the reconstruction start tables and ADR. KeepPOSTlineage-only. Do not invent a TEPP theta.Prefer that successor over this head. Do not merge before #74. This automation cannot merge.
Sent by Cursor Automation: Fix Issues
There was a problem hiding this comment.
Do not merge this head. Start reconstruction is the right buyer gap, but this tip repeats the #142 defects that #152 already closed on the older stack.
Blockers
- Running is written before
FOR UPDATE. Two concurrent starts both read Pending, both insert the same next status ordinal, and the loser UniqueViolates. ADR 0020’s sequence says lock first. After the lock, re-read status: replay Succeeded, 409 if not Pending, then append Running. UniqueViolationis not mapped to 409. Create already does this. Start lets a raced Running or reconstruction insert become a 500. Catchasyncpg.UniqueViolationErrorand tell the operator to open the run.- Start re-queries live
source_post. Create already hashed the authorized post ids into the snapshot digest, but those members are not persisted. A backdated insert, delete, or visibility change changes the tree after the request. Persistanalysis_source_snapshot_memberat create; start from those ids.
Also fix before landing
- Edge title joins are bare
source_postinner joins — apply the same ABAC predicate as visible posts, and mark titles rewritten after cutoff (ADR 0016). reconstruction_result_sha256is persisted and typed, but the UI only shows Code/Config. RenderResultwith the same audible prefix + hover-full-digest pattern.- The unit test named “product start path” never calls
start_pending_analysis_runorrecords_from_source_posts. Keep the HTTP A-100 test; add a CI-runnable mapper test so the fork is not Keycloak-gated.
TEPP 422, Succeeded replay, hidden 404, run-scoped edges, and synthetic fixtures are sound. Do not merge #142 or #152 as a substitute. Land the lock-first + snapshot-member fix on this cutoff-warning stack.
Sent by Cursor Automation: Fix Issues
|
|
||
| now = datetime.now(timezone.utc) | ||
| running_ordinal = _next_status_ordinal(current) | ||
| await _append_status(conn, analysis_run_id, running_ordinal, _RUNNING, now) |
There was a problem hiding this comment.
Running is appended here, and FOR UPDATE is below. Two concurrent starts both see Pending on the unlocked read, both compute the same next ordinal, and the loser UniqueViolates (analysis_run_status_event PK). Lock the run first, re-read status under that lock, replay Succeeded, 409 if not Pending, then append Running.
| if scope_kind_code == "analysis_scope_corporate_entity" and corporate_entity_id: | ||
| rows = await conn.fetch( | ||
| f"select {columns} " | ||
| "from source_post where corporate_entity_id = $1 " |
There was a problem hiding this comment.
This is a live scope walk, not the create-time bag. plan_analysis_run_capture already hashed these post ids into snapshot_sha256, then discarded them. Persist analysis_source_snapshot_member at create and start from those ids so a backdated insert or visibility change cannot rewrite the tree.
| child_post.post_title as child_post_title, | ||
| edge.fused_score | ||
| from analysis_run_lineage_edge edge | ||
| join source_post parent_post on parent_post.post_id = edge.parent_post_id |
There was a problem hiding this comment.
Anyone who can see the run gets live titles with no visibility_code / affiliation predicate. A deleted post also drops the edge (inner join) while the digest stays. Filter with the same ABAC rule as fetch_visible_scope_posts, and mark live_after_cutoff so a rewritten title is not treated as reconstructed evidence.
| {starting ? "Reconstructing the cutoff bag..." : "Start reconstruction"} | ||
| </button> | ||
| )} | ||
| {selected.reconstructed_edges && selected.reconstructed_edges.length > 0 && ( |
There was a problem hiding this comment.
After start, the operator sees titled edges but not the Result digest the API just persisted. Add reconstructionResultSha256 next to Code/Config (Result ${prefix}, full digest on title) so they can verify the tree they just started.
| account_id=account.user_account_id, | ||
| affiliated_entity_ids=list(account.corporate_entity_ids), | ||
| ) | ||
| except AnalysisRunStartError as exc: |
There was a problem hiding this comment.
This except only maps AnalysisRunStartError. A raced Running insert or analysis_run_reconstruction PK collision becomes an unhandled 500. Catch asyncpg.UniqueViolationError in start_pending_analysis_run and raise 409 with a next action: open the run.


Buyer gap
After #125 / ADR 0017 an operator could record a Pending lineage run, then could not start reconstruction. Seed still owned the only Succeeded Demo Corp tree.
What the operator sees
Stacked on #162 (
feat/analysis-run-cutoff-body-warning). Do not merge before #74 → #150 → #162.POST /api/analysis-runs/{id}/startruns ThreadWeave on the authorized cutoff bag (every registered scope kind).analysis_run_reconstruction+analysis_run_lineage_edge(migration 0020; 0019 is already role-catalog identity).make seed, request a lineage reconstruction, open the Pending Demo Corp row, start it: the designed A-100 fork appears (revised quote + delivery question under the pricing follow-up).Boundaries
tepp_client. This write does not invent a theta.Tests
tests/test_analysis_run_start.py— digest stability; designed A-100 fork; TEPP next-action error.tests/test_analysis_run_reconstruction_schema.py— 3NF names, Dockerfile copy, empty rollback.backend/tests/test_api.py— start recovers the fixture fork; replay; TEPP 422; hidden 404 (self-skips withoutmake up).frontend/src/App.test.tsx— Start reconstruction shows the titled A-100 edges.Next action: Request a lineage reconstruction, open the Pending Demo Corp row, start reconstruction, and confirm the designed A-100 fork.