Skip to content

superseded_by follows a chain of TAKEOVERs to its last link, so one lane holds the object (#74) - #204

Merged
brettheap merged 3 commits into
mainfrom
fix/superseded-by-takeover-chain-74
Oct 18, 2026
Merged

brettheap merged 3 commits into
mainfrom
fix/superseded-by-takeover-chain-74

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

Issue #30's dead-lane exception (PR #61) lets a TAKEOVER displace an earlier TAKEOVER: A claims, B takes over from A, and C takes over from B once B is dead. superseded_by, and its inline copy in lane_objects, asked only "does some other lane's last line here read TAKEOVER". So B's stale TAKEOVER was read as proof that C was displaced too.

Evidence: I ran lanes-edit.sh at f87fd5a against a throwaway workspace (LANES_NO_GIT=1, LANES_NO_GITHUB=1, a temp HOME). The logs were the issue's shape: repoZ-5 dead with TAKEOVER … ← lane:repoZ-0, then repoZ-6 with TAKEOVER … ← lane:repoZ-5.

who opensoft/repoZ#40              HOLDS=0
  super.   lane repoZ-5  TAKEOVER … superseded by TAKEOVER (lane:repoZ-6)
  super.   lane repoZ-6  TAKEOVER … superseded by TAKEOVER (lane:repoZ-5)
  state    FREE — no lane's last line on it is open
who --lane repoZ-6
  TAKEOVER  opensoft/repoZ#40 … ← lane:repoZ-5  (taken over by lane:repoZ-5 … — release it to close this line)

The same bug has three consequences at f87fd5a, each reproduced by the suite's new fixtures:

What changed

Every link that displaces a TAKEOVER names the lane it displaced as ← lane:<x>. claim --force writes that payload for every hold that is not a CLAIMED, and a comment URL is only ever the payload for a CLAIMED taken over. The rule now depends on the lane's own last line:

  • That line is a TAKEOVER. It is displaced only by another lane's standing TAKEOVER that names this lane, and not by one this lane's own TAKEOVER names back. So B is displaced by C and C by nothing: one lane holds the object, the chain's last link.
  • That line is any other open verb. It is displaced by any standing TAKEOVER, as before. The taker named is one no other standing TAKEOVER names, i.e. the lane that holds the object now, so A's line says C, not B.
  • Two takers neither naming the other are both holders, which matches the README again.
  • Two TAKEOVERs naming each other are both holders as well. No file order says which came last, so the conflict is shown instead of reported as FREE.

lane_objects applies the same rule, so who --lane and lane-end agree with who. The stale-CLAIMED path that pays a comment URL needs nothing: a URL only ever displaces a CLAIMED, never a TAKEOVER, so it is never a link a chain is followed by. R18 is unchanged: a taker that has released no longer displaces anyone. The single-hop shapes the suite already covers give the same answers as before.

Functions and regions edited:

  • lanes-edit.sh: lane_objects (other-lane block and END, lines 3422-3466)
  • lanes-edit.sh: superseded_by (its header comment and awk, lines 3495-3557)
  • tests/test_lane_helpers.sh: seed rows (repoChain-1..4, after repoL-6's row)
  • tests/test_lane_helpers.sh: seeded object logs (repoChain-1/2/3, after repoL-5's log)
  • tests/test_lane_helpers.sh: the #30 dead-lane takeover block (repoL-6's pinned assertions, and the new repoChain cases after it)
  • docs/README-lanes.md: "State is per lane" (the superseded_by reports zero holders for a chain of two dead-lane TAKEOVERs on one object #74 paragraph replaced by the rule, and "Known, deliberate")
  • docs/README-lanes.md: the index schema table, holds (a VIEW) row

Tests

  • repoL-6's pinned assertions are flipped. The block that pinned ZERO holders as the known superseded_by reports zero holders for a chain of two dead-lane TAKEOVERs on one object #74 gap now expects exactly one HOLDS, repoL-6, with repoL-5 superseded by repoL-6. It also checks that who --lane repoL-6 does not say "taken over by".
  • Three-link chain, made through the tool. repoChain-1 claims #1, repoChain-2 takes it over from repoChain-1, and repoChain-3 from repoChain-2, all seeded and dead. Then LANES_LANE=repoChain-4 claim opensoft/repoChain#1 --no-github --force must write TAKEOVER … ← lane:repoChain-3. who must report exactly one HOLDS, repoChain-4, and each earlier link must be superseded by the link that names it.
  • The race. On #2, two TAKEOVERs name repoChain-1. Expected: two HOLDS, state HELD, and claim --force refused with exit 2 ("is held by 2 lanes"), writing nothing.
  • Mutual naming. On #3, two TAKEOVERs name each other. Expected: two HOLDS.

Local proof that they fail before and pass after. I did not run the full suite locally, per AGENTS.md; CI's tests job is the record. I ran a scratch mini-suite instead. It extracts the suite's own text by line range: the helpers ok/bad/is/has/hasnt/run, the new seed rows, the seeded logs, and the whole block from repoL-6's claim to the last new assertion. It runs that text in a temp workspace under env -i with LANES_NO_GIT=1 LANES_NO_GITHUB=1:

f87fd5a lanes-edit.sh:  10 passed, 15 failed
this branch:            25 passed, 0 failed

A separate scratch harness drove who and who --lane on nine shapes. Holder counts, before → after:

shape before after
chain of 2 0 1
chain of 3 0 1
chain of 2 with a case-folded payload 0 1
R18 chain whose last link released 1 1
single hop 1 1
race 0 2
a TAKEOVER naming its own lane 0 2
mutual naming 0 2
real claim --force over a dead TAKEOVER 0 1

bash -n passes on both shell files.

Known remainders (not fixed here)

  • The lanes-index holds view still applies the one-hop rule. Evaluating ddl("sqlite", "") from lanes-index on an in-memory store returns no row for the two-link chain and none for the race, where who now names the last link and both racers. Fixing it means changing the view's SQL to read ref/payload. Existing SQLite stores also need a DROP VIEW first, because CREATE VIEW IF NOT EXISTS keeps the old definition. That is a separate change to the indexer. The README's holds row now says so, and test_the_holds_view_answers_as_who_does (single-hop) is unaffected.
  • A lane renamed between links (Amendment 16(e)) keeps its old name in the next link's ← lane:<old> payload. That chain shows two holders (a visible conflict) and does not resolve.
  • A ring of three or more TAKEOVERs each naming the next still displaces all of them. Only the two-lane mutual case is handled.

Closes #74

🤖 Generated with Claude Code

Summary by Sourcery

Make takeover resolution follow explicit lane links to the final holder while retaining ambiguous takeover conflicts as visible multi-holder states.

Bug Fixes:

  • Resolve takeover chains to their final link so holder detection, forced claims, and lane-specific guidance identify the actual current holder.
  • Preserve visible conflicts for competing takeovers that do not establish a clear displacement relationship.

Enhancements:

  • Apply consistent takeover-chain semantics across who, who --lane, and lane-end through the shared object-resolution logic.
  • Document the updated takeover displacement rules and clarify that the indexed holds view still uses the previous one-hop behavior.

Documentation:

  • Update lane state documentation to describe final-link takeover resolution, conflict cases, and the remaining holds view limitation.

Tests:

  • Add coverage for multi-link takeover chains, competing takers, mutual takeover naming, forced claims, and holder-specific output.

…ane holds the object (#74)

Issue #30's dead-lane exception (PR #61) lets a TAKEOVER displace an earlier
TAKEOVER: A claims, B takes over from A, C takes over from B once B is dead.
superseded_by and lane_objects both asked only "does some other lane's last
line here read TAKEOVER", so B's stale TAKEOVER was read as proof that C was
displaced too. `who` reported zero holders of an object C held, `who --lane C`
told C to release it, and `claim` granted the next lane a plain CLAIMED over
it, as if the object were free.

Every link that displaces a TAKEOVER names the lane it displaced as
`← lane:<x>`: `claim --force` writes that payload for every hold that is not
a CLAIMED. So a lane whose own last line is a TAKEOVER is now displaced only
by another lane's standing TAKEOVER that names it, and not by one its own
TAKEOVER names back. Any other open verb is displaced by any standing
TAKEOVER, as before, and the taker named is the one no other standing
TAKEOVER names, which is the lane that holds the object now. lane_objects
applies the same rule, so `who --lane` and lane-end agree with `who`.

The same one-hop test made two takers of one stale claim displace each
other, so `who` called the object FREE. The README's "Known, deliberate"
paragraph has always said both are holders, and with this change both are
again.

The suite's pinned ZERO for repoL-6 is now exactly one holder. New repoChain
fixtures cover a three-link chain whose third link `claim --force` makes, the
race (two holders, and --force refused), and two TAKEOVERs naming each other.
README-lanes.md describes the rule. It also notes that the lanes-index `holds`
view still applies the one-hop rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes takeover-chain state derivation by making superseded_by and lane_objects follow explicit ← lane:<x> links, ensuring the last takeover is the sole holder while preserving visible conflicts for ambiguous races; it adds focused fixtures and updates documentation, while explicitly leaving the SQLite holds view unchanged.

Sequence diagram for force-claim takeover-chain resolution

sequenceDiagram
    actor User
    participant Claim as claim --force
    participant Logs as Lane logs
    participant Who as who

    User->>Claim: claim object
    Claim->>Logs: read each lane's last line
    Claim->>Logs: write TAKEOVER ← lane:repoChain-3
    User->>Who: inspect object
    Who->>Logs: follow TAKEOVER links
    Logs-->>Who: repoChain-4 is the sole holder
    Who-->>User: HELD by repoChain-4
Loading

Flow diagram for takeover-chain holder resolution

flowchart LR
    A["A: CLAIMED"] -->|TAKEOVER ← lane:A| B["B: TAKEOVER"]
    B -->|TAKEOVER ← lane:B| C["C: TAKEOVER"]
    B -. superseded by named link .-> X["B is displaced"]
    C --> H["C is the sole holder"]
    A -. stale earlier line .-> S["A and B are superseded"]
Loading

Flow diagram for ambiguous takeover conflicts

flowchart LR
    R["Stale claim"] --> A["TAKEOVER ← lane:A"]
    R --> B["TAKEOVER ← lane:A"]
    A --> H1["A is a holder"]
    B --> H2["B is a holder"]
    H1 --> C["who reports HELD by 2 lanes"]
    H2 --> C
Loading

File-Level Changes

Change Details Files
Make takeover supersession follow explicit lane-to-lane links so chains resolve to their final holder.
  • Record ← lane:<x> payloads for takeover links and distinguish them from comment URLs.
  • Update superseded_by to displace a takeover only when a standing takeover names that lane; preserve ordinary open-verb behavior and represent races as conflicts.
  • Apply the same chain-aware logic in lane_objects so who, who --lane, and lane-end agree.
  • Ensure claim --force creates a takeover naming the current last link rather than a plain claim over a held chain.
lanes-edit.sh
Expand regression coverage for takeover chains and ambiguous takeover conflicts.
  • Add dead-lane fixtures for a three-link chain, two independent takers, and mutually naming takers.
  • Verify the final chain link is the sole holder, earlier links are superseded by the links naming them, and who --lane does not misdirect the holder.
  • Verify races remain held by two lanes and claim --force refuses without writing.
tests/test_lane_helpers.sh
Document the revised takeover resolution rules and identify the remaining index-view discrepancy.
  • Describe chain resolution, naming requirements, and conflict cases in the lane-state documentation.
  • Clarify that the SQLite holds view still uses the old one-hop rule and is outside this change.
docs/README-lanes.md

Assessment against linked issues

Issue Objective Addressed Explanation
#74 Make who report the final lane in a chain of TAKEOVERs as the sole holder, rather than treating an already-superseded TAKEOVER as evidence that the final lane is also superseded. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…tests this PR on today's main

Main moved 15 commits past this branch's base f87fd5a, through the suite's machinery and pin (.github/workflows/tests.yml, contracts/openreposhape-pin.yaml, tests/conftest.py, tests/test_repo_hygiene.py, upstream/openRepoShape), after this PR's last green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tests this PR on today's main

Main moved 4 commits past this branch's base 2cb14a9, through the suite's machinery and pin (), after this PR's last green run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@brettheap
brettheap marked this pull request as ready for review October 18, 2026 15:53
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@brettheap
brettheap merged commit 0a49697 into main Oct 18, 2026
5 checks passed
brettheap added a commit that referenced this pull request Oct 18, 2026
…so a migrated PAUSED row stops showing NO LOG (#89) (#206)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137733527) was green on head cd3ced0 with main 586dea2 in its tree. Main 0a49697 merged with head cd3ced0 is tree f88c2f1, which queue run 39137981522 (workflow_dispatch of tests on 3f5f4b9) tested whole and green.

Independent review (lane 5, Opus, of 00b75e6; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed the unpickable (row) lanes as built (2026-10-18).

Closes #89

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…or binding log instead of guessing a branch (#60) (#208)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137741203) was green on head 3edaf0a with main 586dea2 in its tree. Main be82c20 merged with head 3edaf0a is tree cfd2ee6, which queue run 39137988005 (workflow_dispatch of tests on 9db4863) tested whole and green.

Independent review (lane 5, Opus, of c252a3c; the merges of main add nothing to the PR's diff): LAND.

Closes #60

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…s why refuse it before writing, and the deferred lane-start command keeps --agent (#85, #72 items 5, 6) (#218)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137747959) was green on head bfc5ddd with main 586dea2 in its tree. Main fca60f5 merged with head bfc5ddd is tree b188309, which queue run 39137992777 (workflow_dispatch of tests on 586df3b) tested whole and green.

Independent review (lane 5, Opus, of 99f3853; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed refusing CR/LF at the door (2026-10-18).

Part of #85. Part of #72 (items 5-6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…ane-handoff renames the window only once the handoff can no longer be refused (#80 slices B and C) (#220)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137756635) was green on head 0b531e5 with main 586dea2 in its tree. Main 01eff2c merged with head 0b531e5 is tree a388312, which queue run 39137998354 (workflow_dispatch of tests on f335793) tested whole and green.

Independent review (lane 5, Opus, of 9f6fd2d; the merges of main add nothing to the PR's diff): LAND. Brett Heap confirmed the rename trade as built (2026-10-18).

Part of #80 (slices B and C).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 18, 2026
…ession ids, so the dead-lane --force check and duplicate-holder fail closed (#67) (#221)

Landed on Brett Heap's word "land #204 #206 #208 #218 #220 #221 when green" (2026-10-18), by lane openRepoTools-5. Required Linux CI (guard-launch-mode, tests, tests-no-submodule; run 39137764839) was green on head 14696c7 with main 586dea2 in its tree. Main be0743b merged with head 14696c7 is tree 0052eff, which queue run 39138003629 (workflow_dispatch of tests on 147cbf6) tested whole and green.

Independent review (lane 5, Opus, of fbd2bb1; the merges of main add nothing to the PR's diff): LAND.

Closes #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

superseded_by reports zero holders for a chain of two dead-lane TAKEOVERs on one object

1 participant