Skip to content

lanes-index follow-ups of #166 and #173: export reads one snapshot, history --index says its closing stderr count, a foreign source is refused before the lock, a damaged table reads as unusable, an unread lane stays NOT READ in the index, and the worktrees footer names the workstation - #219

Merged
brettheap merged 8 commits into
mainfrom
fix/lanes-index-166-173
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Lane openRepoTools-4's follow-ups to the derived index, under the TAKEOVER lines on #166 and #173 (lane openRepoTools-3's claims, taken with its consent). Brett Heap lifted the closeout direction for these deferred issues.

Refs #173 (the finding, and items 2, 3 and 4 of the fourth-review comment). Item 1 of that comment is skipped, so the issue stays open.
Refs #166 (items 1, 3 and 4). Item 2 is skipped on its open question, so the issue stays open.

Items

Line numbers are on this branch's head.

Issue / item Status Where Proving test (tests/test_lanes_index.py)
#173 finding: a first sync that cannot read a lane publishes an empty answer, and rows held over read as fresh Fixed, option 1 (persist the unread state). The issue's own second test, that the --index read says the rows are held, can only pass under option 1. lanes-index UNREAD_BRANCH, is_unread_marker, unread_key, unread_record, unread_line (1847–1881), collect_inventory (1934–1947), run_inventory (2080–2125), cmd_status (2366–2373), _export_worktrees (2506–2509), module docstring (41–52) test_a_first_sync_that_cannot_read_a_lane_says_not_read_in_the_index (new); test_an_inventory_that_cannot_be_read_keeps_its_rows (extended); test_a_tree_with_no_path_is_a_tree_and_not_an_unread_marker (new, review round); test_a_tree_whose_branch_reads_like_a_marker_is_a_tree (new, delta check)
#173 comment item 1: a partial lane listing can delete indexed rows Skipped lanes-edit.sh worktree_rows (~13154) is in lane openRepoTools-3's unpublished feat/triad-lane-tooling region (~13000–13350) none
#173 comment item 2: inventory source validated only after the lock Fixed. The register's --source gets the same check, since it had the same defect. lanes-index cmd_sync (2189–2202); this_workstation is asked once a process (1781–1795) test_a_source_is_refused_before_the_lock_is_asked_for; test_an_inventory_sync_asks_for_the_workstation_once (review round)
#173 comment item 3: SqliteStore.rows leaks sqlite3.Error Fixed lanes-index SqliteStore.rows (1131–1143) test_a_damaged_table_under_valid_provenance_is_unusable_not_a_traceback
#173 comment item 4: the footer's remedy reads only this workstation Fixed lanes --worktrees footer (368–373) test_the_index_read_of_the_worktrees_is_the_source_read (extended)
#166 item 1: export reads one snapshot Fixed lanes-index SqliteStore.read_image (1145–1167), PostgresStore._query_many (1328–1342), provenance/rows and their query helpers (1386–1428), PostgresStore.read_image (1430–1441), cmd_export (2428–2440) test_an_export_is_one_commits_image_when_a_sync_lands_mid_read[wal-60.0] and [delete-5.0], test_a_postgres_export_is_one_repeatable_read_transaction
#166 item 2: holds keyed by canonical repository Skipped. Its open question is unsettled: should a line under a legacy repository spelling count toward the canonical object's holds? who ignores such lines today. The fix also needs lanes/repos.tsv indexed, as schema 2 or an additive schema-1 change. none none
#166 item 3: history <lane> --index drops LOG_AWK's closing count on stderr Fixed lanes-edit.sh lane_log_events index branch (3157–3164) test_the_index_read_is_byte_identical_to_the_source_read (extended: stderr is compared for every read, the read: line removed)
#166 item 4: the fake psql hard-codes the marker Fixed tests/test_lanes_index.py FAKE_PSQL test_the_fake_psql_decodes_echo_as_psql_does

The manual changed where it states these behaviours: docs/README-lanes.md 3435–3445 (stderr parity, the flag's two own stderr lines, and the one-snapshot export) and 3567–3601, 3618–3621 (the key forms, the unread marker, how an indexer from before #173 reads it and what two indexers on one store do, and the footer). All of it is before the ~3830 boundary.

Neighbours

  • lanes: only the footer at 368–373. It stays clear of lane openRepoTools-5's ~520 and the other lane-4 writer's ~566.
  • lanes-edit.sh: only lane_log_events at 3157–3164. It stays clear of register_text (~3202), archive_text (~3238–3260), row_of_lane (~4568), lane 3's ~13000–13350 and ~16940–16960.

Design notes

Tests

Local, under flock /tmp/openrepotools-pytest.lock. The first 20-minute wait timed out, so this PR opened with CI as the record. The run then got the lock again at 19:04Z.

  • Red, against main. The new and extended tests ran against a git archive of main f87fd5a (OPENREPOTOOLS_CODE_ROOT, LANES_EDIT pointed into it). 9 failed, 9 of 9, each at its intended assertion:
    • the export mixed two commits' rows, for both [wal-60.0] and [delete-5.0]
    • the stderr comparison
    • BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; not in the export's script
    • the footer naming the workstation
    • a refused source left a mark
    • Traceback in status
    • no inventory NOT READ … kept from the last sync beside the held rows
    • no worktree is inventoried after a first sync
  • Item 4's red. Main's FAKE_PSQL prints the marker for all four of the new test's \echo lines. The four existing Postgres tests pass on main with the decoding fake, so the indexer's real \echo '\036LANES-INDEX-NEXT' decodes to the marker its parser reads.
  • Green, on this branch. The whole tests/test_lanes_index.py at f00ee58 gave 78 passed and 1 failed. The failure was my own stderr comparison: history --index adds a no-fetch note by design (the history arm sets LANES_NO_FETCH=1 where the index answered). ab853b1 sets that note aside on both sides, as the read: line is. After that, the 9 tests gave 9 passed.

Review round, 5bf29d9 (findings 1–4 of the review at ab853b1). Both runs were local, under flock /tmp/openrepotools-pytest.lock, which was taken at 22:11Z on 10-17.

Delta check, 0ac41f7 (nits 1–5 of the delta check at 5bf29d9). This ran local, under the lock, taken at 04:11Z on 10-18, with every LANES_* and TMUX* unset.

  • Red at 5bf29d9: 1 failed, 1 of 1. test_a_tree_whose_branch_reads_like_a_marker_is_a_tree exported a hand-made empty-path tree with branch unreadable inventory (forged by hand) as inventory NOT READ: forged by hand.
  • Green at 0ac41f7: tests/test_lanes_index.py and tests/test_repo_hygiene.py gave 234 passed.
  • CI at 0ac41f7: run 39096694157 succeeded. Linux tests gave 1548 passed and 19 skipped; tests-no-submodule and guard-launch-mode passed.

Doc fix, c255e18 (the checker's DELTA OK on 0ac41f7, with one doc correction). The manual now says an older and a newer indexer on one store rewrite it only where a lane has two trees with the same path, and says what a store from the unlanded heads keeps. This commit is documentation only.

  • Local at c255e18: tests/test_lanes_index.py and tests/test_repo_hygiene.py gave 234 passed, under the lock, with every LANES_* and TMUX* unset.
  • CI at c255e18: run 39113347016.

CI. Linux tests on head ab853b1: success, run 38452710904. Linux tests gave 1426 passed and 9 skipped. tests-no-submodule, tests-windows, parse-macos and guard-launch-mode passed; tests-macos waits for the ready label. Run 38450237715 on f00ee58 failed on the one stderr comparison that ab853b1 corrects (1 failed, 1205 passed in tests-no-submodule).

🤖 Generated with Claude Code

Summary by Sourcery

Harden lane index reads and synchronization so exported data is consistent, unread inventories remain explicit, and source behavior stays aligned with indexed reads.

Bug Fixes:

  • Ensure inventory index reads preserve unread-lane state, retain previously indexed rows, and distinguish unread markers from valid empty-path or marker-like worktrees.
  • Make exports use a single consistent database snapshot and keep indexed history stderr identical to source reads, including closing unread-line counts.
  • Reject foreign or invalid inventory sources before acquiring the lock and report damaged index tables as unusable instead of raising tracebacks.

Enhancements:

  • Make worktree guidance identify the workstation whose rows are being reconciled.
  • Improve the PostgreSQL test fake to decode echo commands using psql-compatible escaping.

Documentation:

  • Document snapshot-consistent exports, stderr parity, unread inventory markers, compatibility behavior, row keying, and workstation-specific reconciliation guidance.

Tests:

  • Add coverage for snapshot consistency, unread inventory persistence, source validation, damaged tables, workstation footer guidance, duplicate-path worktrees, marker handling, PostgreSQL transactions, and psql echo decoding.

Brett Heap and others added 3 commits October 13, 2026 18:51
…index.py: one export snapshot, the history --index stderr count, a fake psql that decodes \echo, a source refused before the lock, a damaged table read as unusable, the footer, an unread lane in the index

#166 item 1: an export with a sync landed after its first table, in WAL
mode and in the rollback journal, is the old commit's image or the new
one and never a mix; a Postgres export is one REPEATABLE READ READ ONLY
psql call. Item 3: the byte-identical read test compares stderr too, the
`read:` line aside, so `history repoA-1 --index` must say the count that
closes its log's nine unreadable lines. Item 4: FAKE_PSQL decodes each
`\echo` as psql's lexer does instead of printing the marker for any
argument, and a test holds it to psql's escapes.

#173: a foreign or nameless inventory source and another register are
refused with exit 2 and no rerun mark while another sync holds the lock;
a `worktrees` table dropped under valid provenance is `rows: unread` in
`status` and `why`/exit 3 in `export --worktrees`, never a traceback; the
`lanes --index --worktrees` footer names the workstation to run
`lane-reconcile` on; a first sync that cannot read a lane leaves the
index read byte-identical to the source's NOT READ, and a later one says
the rows it held are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… history <lane> --index says the count closing a log's unreadable lines (#166 items 1 and 3)

Item 1: `cmd_export` read the provenance and five tables as separate
statements, each its own snapshot on SQLite and its own psql session on
Postgres, so a sync committing between two reads made an image of two
commits under a `meta` naming the first. `read_image` reads them in one:
a SQLite read transaction (the SHARED lock in the rollback journal, a
snapshot in WAL) and one psql call inside BEGIN ISOLATION LEVEL
REPEATABLE READ READ ONLY ... COMMIT, through `_query_many(snapshot=True)`.

Item 3: in `lane_log_events`' index branch the filter kept only the
`unreadable: <log>:` lines of the export's `warn`; it now keeps the
export's own count line that follows them, which is LOG_AWK's END.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… table as unusable, and keeps an unread lane's NOT READ in the index; the worktrees footer names the workstation (#173)

The finding (option 1, which the issue's own tests ask for): a lane held
because its inventory could not be read gets one `worktrees` marker row,
no path and branch `unreadable inventory (<why>)`, which `export
--worktrees` writes back as the source's `UNREAD` line. A first sync no
longer commits an empty answer for it, and rows held over from an earlier
sync are said to be kept. The marker leaves with the first sync that
reads the lane; `status` counts it apart from the worktrees.

Copilot's fourth review of #171: `cmd_sync` validates the source before
it asks for the lock, so behind a held lock a foreign or nameless
inventory, or another register, is refused (exit 2) instead of leaving a
mark the holder syncs as this workstation's; `SqliteStore.rows` raises
StoreUnusable for a sqlite3.Error; and the `lanes --worktrees` footer
says to run `lane-reconcile` on the workstation the rows name.

Not here: the partial-listing item in `lanes-edit.sh` `worktree_rows`,
which is lane openRepoTools-3's region.

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

sourcery-ai Bot commented Oct 13, 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 23 hours and 29 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR completes selected follow-ups for the derived lanes index: exports now read a coherent snapshot and preserve source stderr, unread inventories are persisted and surfaced without being mistaken for empty results, invalid sources and damaged tables fail safely, and workstation-specific reconciliation guidance and PostgreSQL test emulation are corrected. The proposed changes intentionally leave the partial-listing deletion issue and canonical-repository holds question unresolved.

Sequence diagram for a coherent lanes-index export snapshot

sequenceDiagram
    participant User
    participant Export as lanes-index export
    participant Store as SQLiteStore or PostgresStore
    participant DB as Index database

    User->>Export: cmd_export
    Export->>Store: read_image
    Store->>DB: Begin one read snapshot
    Store->>DB: Read provenance and tables
    DB-->>Store: Consistent image
    Store-->>Export: provenance and rows
    Export-->>User: Export result from one commit image
Loading

Sequence diagram for persisting an unread inventory marker

sequenceDiagram
    participant Sync as lanes-index sync
    participant Inventory as Inventory source
    participant Index as Derived index
    participant Read as lanes-index --index

    Sync->>Inventory: Read lane inventory
    alt Inventory cannot be read
        Inventory-->>Sync: Read failure
        Sync->>Index: unread_record and unread_line
        Index-->>Read: inventory NOT READ marker
        Read-->>Read: Preserve earlier worktree rows
    else Inventory is readable
        Inventory-->>Sync: Worktree rows
        Sync->>Index: Store current rows
        Index-->>Read: Worktree listing
    end
Loading

Sequence diagram for rejecting an invalid source before locking

sequenceDiagram
    participant User
    participant Sync as lanes-index sync
    participant Source as Inventory source
    participant Lock as Index lock
    participant Index as Derived index

    User->>Sync: cmd_sync --source
    Sync->>Source: Validate source
    alt Source invalid
        Source-->>Sync: Invalid provenance
        Sync-->>User: Refuse source
    else Source valid
        Source-->>Sync: Valid
        Sync->>Lock: Acquire lock
        Lock-->>Sync: Lock granted
        Sync->>Index: Sync inventory
    end
Loading

Sequence diagram for preserving history index stderr parity

sequenceDiagram
    participant User
    participant History as lanes-edit.sh history
    participant Index as Derived index
    participant Output as stdout and stderr

    User->>History: history lane --index
    History->>Index: Read indexed log events
    Index-->>History: Events and log warnings
    History->>History: lane_log_events
    History->>Output: Write events to stdout
    History->>Output: Write warnings and closing unreadable count to stderr
Loading

File-Level Changes

Change Details Files
Make index exports consistent, atomic, and byte-compatible with source reads.
  • Read SQLite exports in one snapshot and PostgreSQL exports in one repeatable-read transaction.
  • Preserve per-log warning output and its closing unreadable-line count for indexed history reads.
  • Add coverage for mixed-commit prevention and PostgreSQL transaction boundaries.
lanes-index
lanes-edit.sh
tests/test_lanes_index.py
docs/README-lanes.md
Persist unread inventory state so failed reads are distinguishable from empty inventories.
  • Store an unread marker row with the failure reason while retaining previously indexed worktree rows.
  • Render first-sync and retained-row unread states consistently in source and index reads.
  • Count unread lanes separately in status and clear the marker after a successful sync.
lanes-index
tests/test_lanes_index.py
docs/README-lanes.md
Harden source validation and damaged-index handling.
  • Reject foreign or invalid sync sources before acquiring the index lock.
  • Translate database table-read errors into an unusable-store result instead of a traceback.
  • Ensure status and worktree export report the damaged index through their existing unread/wiped paths.
lanes-index
tests/test_lanes_index.py
Clarify worktree reconciliation scope and make the test PostgreSQL shim follow psql echo semantics.
  • Update the worktrees footer to direct reconciliation to the workstation named by the rows.
  • Decode quoted, escaped, octal, and hexadecimal psql echo arguments instead of hard-coding the protocol marker.
  • Document the updated snapshot, unread-marker, stderr, and workstation behaviors.
lanes
tests/test_lanes_index.py
docs/README-lanes.md

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

Brett Heap and others added 2 commits October 13, 2026 19:08
…ad: line is: both are the --index flag's own lines

`history --index` sets LANES_NO_FETCH=1 where the index answered
(lanes-edit.sh's `history` arm: the read is the index's, so no fetch goes
in front of it), and the note that prints is the flag's by design, as the
`lanes` wrapper's own filter of the same note says. The comparison now
drops both lines on both sides and holds the rest - LOG_AWK's warnings and
the count that closes them - to one byte stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, key it apart from every tree, keep two trees of one path, and ask for the workstation once a sync (#219 review)

Finding 1: a current-schema sidecar with `tree:` and no `path:` is a TREE
line with an EMPTY path, and the marker was read from the empty path
alone - exported as `UNREAD`, counted by `status` as a lane not read. A
marker is now a row with no path AND a branch beginning `unreadable
inventory (`, which no git ref can be (`is_unread_marker`, in
`run_inventory`, `cmd_status` and `_export_worktrees`), and its key is
`<lane>:` and a US (`unread_key`), so holding a lane no longer writes the
marker over a stored empty-path tree keyed `<lane>:`. Two trees of one
lane with one path - two empty paths, say - shared one key since #171
and the second replaced the first in `collect_inventory`; a later one is
now keyed with its place among them, after a US.

Finding 4: `this_workstation` is asked once a process, so the refusal
before the lock and the sync behind it make one `lanes-edit.sh
workstation` call, not two.

Findings 2 and 3, the manual: the stderr of `--index` carries the flag's
own lines - the `read:` line and `history`'s no-fetch note - and an
indexer from before #173 sharing a Postgres store reads a marker as a
TREE with an empty path, the schema staying 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tory's keys are spelled, when a same-path key moves, and what two indexers on one store do (#219 delta check)

Nit 1: `is_unread_marker` is `record_id == unread_key(lane)`. A hand-made
sidecar with no `path:` and a branch that reads `unreadable inventory (…)`
stays a tree, exported as its TREE line and counted as a worktree; the
predicate's three callers already read `record_id` and `lane` (`status`
now reads `lane` for it).

Nit 2: a same-path tree's `<US><n>` holds while those trees keep their
sorted order; a change that reorders them moves both keys and that sync
upserts both - said in `collect_inventory` and the manual, not "stable".

Nit 3: the key forms - `<lane>:<path>`, `<lane>:<path><US><n>`,
`<lane>:<US>unread` - in the module docstring, the `worktrees` table's
comment, `collect_inventory`'s docstring and the manual.

Nit 4: the manual says an older and a newer indexer syncing one store
rewrite it on every sync, each converging to its own view, with no
duplicates and no growth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here a lane has two trees with the same path; and what a store from #219's unlanded heads keeps (#219 delta check)

The checker's four alternating syncs with no same-path pair - an
unreadable lane included - wrote nothing after the first; the rewrite on
every sync is the same-path pair's alone, which the older indexer keys as
one. Each still converges to its own view, with no duplicates and no
growth. A store written by f00ee58 or ab853b1 keeps their `<lane>:` marker
as a kept row while that lane stays unreadable, and it clears at the
first sync that reads the lane.

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

Copy link
Copy Markdown

@brettheap
brettheap merged commit a7f09bd into main Oct 19, 2026
5 checks passed
@brettheap
brettheap deleted the fix/lanes-index-166-173 branch October 19, 2026 11:47
brettheap pushed a commit that referenced this pull request Oct 19, 2026
Main now holds #246 (T018), which lane 3's consent wanted #252 based on, and #219's lanes-index reader. Fetched by URL; the merge is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap pushed a commit that referenced this pull request Oct 19, 2026
…ecord with no path: is a malformed tree, never read and never the marker (#115, #219)

Merged with main, CI failed two tests of #219 (lanes-index's reader)
in tests/test_lanes_index.py. Both plant a current-schema sidecar with
no `path:` and assert its branch appears in `lanes-edit.sh worktrees`.
Since #115 such a record is not whole: the listing reads no field of it
and names its row `malformed sidecar`, by the path it records or, with
none, by its file. The rows were still trees, counted and never NOT READ,
so #219's reader and its marker key are unchanged; only the branch text
the tests looked for is deliberately no longer read.

- test_a_tree_with_no_path_is_a_tree_and_not_an_unread_marker: the
  path-less record is now asserted as a malformed row named by its file.
  Its second record keeps the path of the lane's whole tree (it lacks
  only `upstream:`), so two trees of one lane with one path stay two
  rows, the clause the source can no longer reach with an empty path.
- test_a_tree_whose_branch_reads_like_a_marker_is_a_tree: the forged
  branch is carried by a whole record (it gains a `path:`), so the
  branch is read and still makes no marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brettheap added a commit that referenced this pull request Oct 19, 2026
…nce closes with its operation, and a lane sidecar is valid only when whole (#252)

* A lane sidecar of the current schema is read and replaced only when it is whole (#115)

`lane_sidecar_schema_ok` asked one question, so a file whose first line is
`schema: 1` and that held nothing else passed it: the snapshot's reader printed
empty fields for it, the inventory's reader printed a row with an empty id and
empty counts (which `lane_reconcile` skipped, or read as `dirty 0, unpushed 0`),
and every writer renamed its own file over it.

The gate keeps its name and its call sites, and now also asks a record of the
current schema for every key its writer writes (not empty), the shapes the
readers decide with (`state` one of the four words, every `generation` a
number, a tree's `dirty` a number and `unpushed` a number or `unknown`, its
`path` absolute) and the id a tree record's file name gives it. Both writers
have written exactly these keys since #97 landed. The kind of record comes from
where it is filed; `LSS_WHY` says what failed.

A malformed record takes the unknown schema's path on both sides:
- every writer refuses it, exit 1, nothing written (`set-lane-state`,
  `set-lane-tree` for the snapshot and the tree record, and the lifecycle
  follow-up of a STARTED/RESUMED/ENDED/RETIRED line);
- `lane_state_read` answers 10, `lane-state` exits 10 and says which of the two
  10s it is, and `lane-reconcile` reads the state as `MALFORMED`, verdict
  `indeterminate`;
- `lane-trees` carries a tree record as a row named by its file, every field
  empty, schema `<malformed>`; `lane-reconcile` reports it `malformed-sidecar`,
  counted toward recovery; `worktrees` lists it as `malformed sidecar`.

`lane_sidecar_put` asks the same of its temporary file before the rename, so a
record cut short as it was written is removed rather than renamed into place,
and `set-lane-tree` refuses a `--dirty` or `--unpushed` that is not a count with
64, so no writer files a record its own readers refuse.

No lock line moves: the four `if ! lane_sidecar_schema_ok` lines, every
`acquire_lock`/`lock_try`/`release_lock` and the lock blocks are byte-identical;
only the refusal messages beside them change.

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

* set-lane-tree's fence refuses a poll filed under an operation the lane has finalized (#114)

`SWAPPING -> SWAPPED` keeps the generation and the operation by design, so the
fence's two comparisons accepted a poll from operation O after O's own
finalizer had run, and the delayed reading replaced the inventory the commit
point closed. Every other transition advances the generation, so `SWAPPED` is
the one state in which a matching pair names a finished operation: a fenced
`set-lane-tree` now reads the lane's state with the pair, under the same mutex,
and is refused with 7 where it is `SWAPPED`.

#114 also offered "require SWAPPING". That would refuse a write the suite makes
on purpose (repoRC-5, section 11(c): a lane RUNNING after a resume records under
its current generation and operation), so the narrower rule is the one taken.
An observation filed under no transition is still an unfenced `set-lane-tree`.
#114's second item, the partial observation, has been a 64 since 8.12.

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

* lane-reconcile keeps the status of every read it rests on, and a failed one is never an empty one (#113)

- The holder ids: `lane_session_ids_read` unions the published row's and this
  checkout's session ids and answers 1 where either copy could not be read (an
  empty published register, or a register file that is there and cannot be
  opened). That is `HOLDER unknown` and `indeterminate`, never an empty set that
  `live_holder` answers 8 for and the verdict table reads as a crash.
- `branch.<b>.remote` in `lane_tree_now` gets the `case` its `merge` sibling
  has: a read that failed is 3 (`lane-tree-now` 1, `set-lane-tree` refuses),
  never the fabricated local upstream `./<merge>`.
- `git worktree list --porcelain` in the lane's checkout keeps its status. A
  failure makes the verdict `indeterminate` and names it; a recorded directory
  that is NO repository registers nothing and stays an absence
  (`lane_no_repository`, `lane-worktrees`' `no_repository`, #170 G5).
- `lane-trees`' 10 for an inventory directory that cannot be listed, which this
  read still swallowed after 8.12 and #157 item 4e, and a lane worktree root
  that cannot be listed or searched, make the verdict `indeterminate` too.

#113's second item, the unreadable sidecar, has been its own row since 8.12.
Per-record failures stay rows counted toward recovery and leave the verdict as
it was, which is the line T018 (#246) draws for an unreadable leg.

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

* Test #113, #114 and #115 in the lane-start sandbox, every case red at 87a5e9d

tests/test_lifecycle_fail_open_113_114_115.py runs the real lane-start and
lanes-edit.sh in the sandbox of tests/test_lane_start_claude_current.py, as
tests/test_deferred_97_findings_157.py does. A read that failed is injected
by a `git` on PATH that passes every call through to the real one except the
one FAKE_GIT_FAIL names; the permission fixtures skip under root.

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

* Document #113, #114 and #115 in the manual and in the change's design and tasks

docs/README-lanes.md: the verdict table's MALFORMED and failed-read rows, the
fence's SWAPPED refusal, the whole-record rule and the counts' shapes, the
remote read, `malformed-sidecar`, and the reads whose failure is
`indeterminate`. design.md: decisions 26-28. tasks.md section 7: where each
remaining item was taken.

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

* lane-reconcile reads the holder ids through main's session_ids_checked (#67), not a copy of it (#113)

#67 landed on main with `session_ids_checked`: the published-union-local ids,
0 with them, 8 for none and 1 where either copy of the register could not be
read, on a `register_text_checked` that reports a failed `git show` or `cat`
itself rather than inferring it from an empty render. That is the read
#113 item 1 needs, so `lane_reconcile` asks it and `lane_session_ids_read`
goes: one implementation. An 8 goes on to `live_holder` as before; a 1 is
`HOLDER unknown` and `indeterminate`, as it was. The comments that named two
callers of the checked read now name three.

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

* lane-reconcile names a failed registrations read in the sweep's words, so lane-end still relays #170 G5's reason (#113)

The sweep asks lane-reconcile before its own reads and keeps the first
refusal. With #113 item 4 that first refusal is now the reconciliation's,
so a lane checkout whose .git is mode 000, or whose .git file points
nowhere, refused (2) as before but named "the worktrees git registers in
<dir> could not be listed" where test_lane_helpers.sh's #170 G5 cases
(and lane-end's relay of them) require "the worktree registrations of
<dir> could not be read". CI run 39141918371 failed those three cases on
33515f8. The reconciliation now says it in lane-worktrees' words, with the
exit status after it.

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

* A snapshot under a directory named trees is read as a snapshot, and set-lane-tree refuses a blank field with 64 (#115, review r1 of #252)

lane_sidecar_schema_ok told a snapshot from a tree record by where it is
filed, and asked */trees/*.yaml first. A case pattern's * matches /, so
under a control root with any directory named trees above it every
snapshot was asked a tree's keys and refused: lane-start wrote none,
set-lane-state exited 1, and lane-reconcile said no-state, the verdict a
launcher goes past. The snapshot's arm now comes first; no tree file is
named lane-state.yaml, since a tree id is c<cksum>-...

set-lane-tree refuses a --branch, --head, --upstream, --checkout or
--writer that is only blanks with 64, before the lock, as it refuses a
count that is not one. It used to fail at the rename with exit 1.

lane_no_repository asks git in the C locale, as lane-worktrees' git_env
does, so a translated git cannot turn an absence into a failed read.

worktree_rows' note no longer says a row's path always begins with /: a
row of a record that is not whole keeps the path the record names.

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

* Test a control root under trees, a blank set-lane-tree field and the C-locale read, each red at 36520b9 (#113, #115)

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

* Document the malformed snapshot's 10, the lifecycle 64s, the kept path of a malformed row, and why a record's lane: is not compared (#115)

The manual's main exit table names the malformed snapshot under 10 and
the lifecycle arms' 64s. A <malformed> lane-trees row keeps the path the
record names, which may be relative, and not every field empty. Decision
28 says which arm the validator asks first and why a record's lane: is
not compared: rename-lane leaves it naming the old lane until the next
write.

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

* #219's two path-less sidecar cases read #115's whole-record rule: a record with no path: is a malformed tree, never read and never the marker (#115, #219)

Merged with main, CI failed two tests of #219 (lanes-index's reader)
in tests/test_lanes_index.py. Both plant a current-schema sidecar with
no `path:` and assert its branch appears in `lanes-edit.sh worktrees`.
Since #115 such a record is not whole: the listing reads no field of it
and names its row `malformed sidecar`, by the path it records or, with
none, by its file. The rows were still trees, counted and never NOT READ,
so #219's reader and its marker key are unchanged; only the branch text
the tests looked for is deliberately no longer read.

- test_a_tree_with_no_path_is_a_tree_and_not_an_unread_marker: the
  path-less record is now asserted as a malformed row named by its file.
  Its second record keeps the path of the lane's whole tree (it lacks
  only `upstream:`), so two trees of one lane with one path stay two
  rows, the clause the source can no longer reach with an empty path.
- test_a_tree_whose_branch_reads_like_a_marker_is_a_tree: the forged
  branch is carried by a whole record (it gains a `path:`), so the
  branch is read and still makes no marker.

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

---------

Co-authored-by: Brett Heap <service@opensoft.dev>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant