Skip to content

T017: A lane control root that cannot be searched is read as not read, not as no trees: lane-state and lane-trees exit 10, the listing says UNREAD, and a sync keeps the lane's rows (opensoft/openRepoTools#291) (part of opensoft/openRepoTools#186) - #5

Merged
brettheap merged 1 commit into
mainfrom
fix/control-root-unsearchable-291
Nov 8, 2026

Conversation

@brettheap

@brettheap brettheap commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

What this is

This is the T017 continuation, in this leg (opensoft/openRepoTools-code), of the opensoft/openRepoTools branch fix/control-root-unsearchable-291 at 3e97ae514452, for opensoft/openRepoTools#291 (part of opensoft/openRepoTools#186). The original tree and branch stay as the archived original (lane 4's decision, opensoft/openRepoTools#186 comment 6548343086). One commit here, ca9b847f678e, carries it.

It carries that branch's own change against 50d05d4, with its merge of main 50d05d4 (457b73b) left out, for the code leg's paths: lanes-edit.sh (+157 −23), lanes-index (+5 −5, comments and one hold reason), lanes (+1 −1, one footer line) and the new tests/test_unsearchable_control_root_291.py (+442). Its one other path, docs/README-lanes.md (+25 −8), is the spec leg's, in the paired PR. lane-start and lane-handoff have no diff.

What was wrong and what changed

What was wrong

A lane's trees and its lifecycle snapshot sit under its control root. Where this account may not search a folder on the way to that root, the shell's -d, -e, -r and -L tests answer false exactly as they do for a name that is gone, so the lane read as one with no trees and no snapshot: worktrees --all left it out, an inventory sync deleted its rows, lane-trees and lane-state said 8, and lane-reconcile said no-state.

Found by the independent review of opensoft/openRepoTools#289 as its SHOULD-4. It is in the family of opensoft/openRepoTools#173's finding, a read that failed taken for an empty one (R22, Amendment 7(d)), and it is at main before opensoft/openRepoTools#289 too.

A lane's trees and its lifecycle snapshot are under its control root, <parent of its dir>/.lane-state/<lane>, or $PROJECTS_ROOT/.lane-state/<lane> for a lane whose dir has no parent here. [ -d ], [ -e ], [ -r ] and [ -L ] are all false for a path under a directory this account may not search: stat fails there with EACCES exactly as it fails with ENOENT for a name that is gone, and none of the shell's tests tells the two apart. So with a lane's .lane-state at mode 000 (the review's probe P5):

The rungs that place the root did the same one level up. A recorded dir whose parent could not be looked for fell to $PROJECTS_ROOT's root, a different place where nothing of the lane's is, and a $PROJECTS_ROOT that could not be looked for left the lane no root at all (8).

What changed

All in lanes-edit.sh unless said.

  • lookup_blocked, new, is called on a path the caller found not there. It walks up to the deepest name that is there. A directory this process can search has answered for the name below it, and a name that is not a directory has no name below it: the absence is established (1). A directory that cannot be searched has not answered (0), and nor has a link that cannot be followed (0), whose target is gone or sits under a directory that cannot be searched, which stat cannot tell apart. It tests -x, not -r, because a lookup needs search permission alone. LOOKUP_BLOCKED_WHY says where it stopped.
  • The rungs (lane_control_root): a rung is passed over only where its directory is established absent. A recorded dir whose parent could not be looked for keeps its own root, and so does a $PROJECTS_ROOT that could not be looked for.
  • The reads. lane_state_read and lane_trees_list answer 10, not 8, where the snapshot or trees could not be looked for. lane-state and lane-trees exit 10 and say that whether the record is there could not be read, and why ("could not be looked for, because the directory X cannot be searched", or "X is a symbolic link that cannot be followed"). They never say that it is there.
  • The listing (worktree_rows). A lane the listing named whose trees could not be looked for is its own UNREAD line, so lanes-index sync holds that lane's rows and removes none. It is UNREAD and not PARTIAL because in every one of these cases the listing named the lane and only its inventory is unread, which is what UNREAD has meant since Derived index: a worktrees table mirroring every lane's #97 inventory under inventory:<Workstation>, nudged by every inventory write, and lanes --worktrees to read it with or without --index openRepoTools#171. PARTIAL is for a lane the listing could not name, and the indexer refuses the whole read over it, so one lane's permissions would freeze every lane's rows. A lane with a tree record that could not be read is UNREAD beside that record's own row. A sync used to take the record's row, keyed by its id where its path would be, for a new tree, and remove the row the record had last given. lane-trees keeps exit 0 and the <unreadable> row, which the handoff reads per record (The handoff's tree inventory names a record it could not read whatever its schema, and a leg directory it cannot search is never read as absent (#246 follow-up, before the cutover) openRepoTools#255).
  • lane-reconcile pronounces indeterminate, with a reason that says that whether the snapshot, or the trees directory, is there could not be read, and why. It no longer says that a trees nobody could look for is there. A trees directory that is there and cannot be listed keeps its own wording (The lifecycle's remaining fail-open reads: an unreadable register becomes 'no holder', an unreadable sidecar becomes no tree, and two git reads become fabricated values openRepoTools#113, Findings deferred from #97: fetch-first reads, the live-swap takeover, a hand-written RESUMED, and the five of the 8c5958f round openRepoTools#157).
  • A write under a root nobody could look for fails there. set-lane-state and set-lane-tree cannot make their directory. Where the recorded dir's parent could not be looked for, they used to write under $PROJECTS_ROOT's root, where nothing of the lane's is: the lane's real records were hidden while the block lasted, and what was written stayed stranded when it lifted.
  • The wording. "There and could not be read" is no longer said of what nobody could look at: the lanes footer (+1 −1), lanes-index's hold reason (+5 −5, with its comments and the lane_generation docstring) and the second exit-10 header in lanes-edit.sh. The manual's matching text is the paired spec PR's.

Unchanged: an absence that was actually looked for is still 8, and a lane with no trees still holds nothing in a sync.

Brett Heap's word of 2026-10-21, logged at 15:07:36Z ("yes to all as recommended"), on the writer's four questions: lane-trees keeps exit 0 for an unreadable record (the row is the signal, opensoft/openRepoTools#255 and a test pin exit 0); the wording "is there and could not be read" is fixed on this branch, in lanes-index and the lanes footer (at 15:41:21Z he added lane_generation's docstring); a malformed record with no path: line goes to a separate v1.2.0 issue, opensoft/openRepoTools#302; and two deliberate behaviour changes stay, with two small tests: a write under a blocked rung fails and files nothing under $PROJECTS_ROOT's root, and a .lane-state link whose target is gone is exit 10.

Not in this PR

Old to new: the receipt

The original's own non-merge commits in opensoft/openRepoTools (50d05d4..3e97ae5), 11 in all, are carried by one commit in this leg, ca9b847f678e. The list is the commit's own body, and it equals the original's non-merge commits read from the chain's refs/root/ ref (checked).

a431854c42c4 Test that a lane control root nobody could search is never read as a lane with no trees, and that an inventory sync keeps that lane's rows (#291)
02571b4ade4f A lane control root that cannot be searched is read as not read, never as no trees, so an inventory sync keeps the lane's rows (#291)
af4d132af4c7 A lane whose inventory could not be read is no longer said to have one that is there, because a blocked lookup never saw whether it was (#291)
124b509eb6f0 Test that a write under a lane control root nobody could look for fails and is filed nowhere else, and that a .lane-state link whose target is gone is unread, never none (#291)
b0a631baa46c lane_generation's docstring no longer says that a snapshot it could not read is there, because a blocked lookup never saw whether it was (#291)
7db059f458a7 The README's rung section says that a rung is passed over only where its directory is established absent, and that a rung nobody could look for keeps its root, so its reads are 10 and its writes fail (#291)
468b20e6c411 lane-reconcile no longer says that a trees directory nobody could look for is there, and says instead that whether it is there could not be read, and why (#291)
3af5006f565f The lanes-index marker comment, the second exit-10 header in lanes-edit.sh and the README's unreadable verdict row no longer say that a record nobody could look for is there (#291)
a3ea2af1946a Only the #291 cases that set a mode skip as root, so the links that cannot be followed, which set none, are proved there too (#291)
82ac6b742392 lookup_blocked's comment names its limit: a denial that the mode bits do not show still reads as absent (#291)
3e97ae514452 The README's rung paragraph now names what each surface says where a rung could not be looked for, instead of calling every read 10 (#291)

The subjects are the original's, byte for byte; a bare (#N) in them names the root issue.

Resolutions

None. The patch applied without conflict, at offsets only: lanes +29 lines and lanes-edit.sh +97 to +158 lines, from the two commits below it in this chain.

The pairing

The manual's part of this change (docs/README-lanes.md, +25 −8) is the commit 0b6f2a0be00a on fix/control-root-unsearchable-291 in opensoft/openRepoTools-spec, its own PR, opensoft/openRepoTools-spec#8 (base fix/subfield-misreads-301). The code leg's checks read the manual through OPENREPOTOOLS_SPEC_ROOT: tests/test_repo_hygiene.py reads docs/README-lanes.md (call sites at lines 1373, 1404, 1423, 3139 and 3572 of the code leg's main; 1404 is the tuple of surfaces that a loop reads through doc()) and tests/test_test_roots.py pins which manual a run reads. The code leg's own CI (OPENREPOTOOLS_COMPOSED: '0', no spec leg beside the checkout) skips those checks, so they bind only in a composed run or with the variable set to a spec clone. A search of the manual lines this change touches for the strings those checks pin (the rung-count heading, the two settlement quotes, every lane on this workstation, the exit-code section's two markers and lane-end <lane> --retire) finds none: read, not run. So the two PRs are one change in two repositories, and they land together, as a pair. The code PR goes first, so that the manual never describes behaviour the code lacks: this PR first, then its spec PR, opensoft/openRepoTools-spec#8. The state that order passes through, the code chain's tip beside the spec leg's main, passed -k test_repo_hygiene in the independent review (run (e) under Tests: 223 passed, 14 skipped; that run read the chains' tips, not this commit alone). The other order's state, the spec tip beside the code leg's main, was not run. I found no check that turns either order red; that is a reading, not a run.

Tests

Run by the CODE writer, in a sandbox clone, with env -i (HOME, XDG_* and TMPDIR inside the sandbox), OPENREPOTOOLS_CODE_ROOT set and OPENREPOTOOLS_COMPOSED=0, as the leg's CI sets it. Each targeted run read this branch's own commit in its own worktree; that commit was 6ebbac8ff929 before the message amend, and the amend kept its tree, so the results stand for ca9b847f678e.

run exit result time
bash -n on lanes-edit.sh, lane and lanes, at every commit of the chain 0 all ok -
py_compile on every changed .py file and on lanes-index 0 all ok -
run.sh -k test_unsearchable_control_root_291 0 10 passed 74.51 s

shellcheck, over the whole chain (main to the tip): errors 0 to 0 and warnings 108 to 108 (lane 14 to 14, lanes 8 to 8); notes up by 3, 2 and 2, all SC1003 on the literal '\' patterns of opensoft/openRepoTools#301's commit.

The pair runs below are of the two chains' tips, so they cover this commit with every later commit of its chain; apart from the targeted run above, no run was made at this commit alone. They are lane 4's independent paired review, 2026-10-31 (the letters are the review report's own), with OPENREPOTOOLS_SPEC_ROOT set to the spec clone shown, env -i, TMPDIR=/tmp and a 54-character XDG_STATE_HOME.

run code + spec exit counts time
(a) -k test_repo_hygiene code tip 5f2995d43893 + spec tip 8104f2cf965c 0 223 passed, 14 skipped, 2944 deselected 11.1 s (33 s wall)
(b) -k test_repo_hygiene, baseline code main 2a057a6 + spec main 9f8b21a 0 223 passed, 14 skipped, 2772 deselected 9.2 s (10 s wall)
(c) -k lane_helpers code tip + spec tip 0 42 passed, 6 skipped; 0 FAIL in the shell suite (the wrapper asserts " passed, 0 failed"), with a 54-character state root 2188.6 s (36:28)
(e) -k test_repo_hygiene code tip 5f2995d43893 + spec main 9f8b21a 0 223 passed, 14 skipped 5.5 s

The pair runs read the chains' tips as they were before the message amend (code 5f2995d43893, spec 8104f2cf965c). The amend kept every tree (git diff from each old commit to its new one is empty), so the runs stand for the amended tips, code 2c15aa6613c3 and spec 430ea20784ac. No failure is new at the tips. Run (d), the manual-reading tests at each branch, was skipped under the review's brief, because none of the five branches' test files reads the manual.

The CODE writer's first run of -k lane_helpers at the chain's tip, with a 212-character run root, gave 5 FAILs in the shell suite (4870 ok, 5 FAIL, 3 skipped), because rc_tree_id in tests/test_lane_helpers.sh lacks the 180-character cap that tree_id_for in lanes-edit.sh has, so at that length the two names differ and the planted records are never read; the chain changes neither, and run (c) above, with the 54-character state root, gave 0 FAIL.

Not run: the suite at the leg's main for comparison, bash 3.2, macOS and the assembly's composed check; the submodule was not initialized, so 6 tests skip.

Test files this commit adds or changes:

  • tests/test_unsearchable_control_root_291.py, new (+442): 10 cases at the root head, on test_lanes_index's estate. Five layouts, one case each (the lane's .lane-state at mode 000; its control root readable and not searchable; a directory above the recorded dir's parent at 000, rung 2; a directory above $PROJECTS_ROOT at 000, rung 3; a .lane-state that is a link into a directory that cannot be searched), in each of which a sync writes 0 removed and keeps the rows, worktrees --all has one UNREAD line and no PARTIAL, lane-trees and lane-state exit 10, and lane-reconcile is indeterminate. Then: a write under a root nobody could look for fails and files nothing under $PROJECTS_ROOT's root; a .lane-state link whose target is gone is 10 and, once the target is back, 0 and 8; a trees link that cannot be followed, with the snapshot beside it read; a trees directory that cannot be searched, which passes on main; and a tree record that cannot be read. Only the cases that set a mode skip as root.

The original's own record, from the root repository, at the root heads and not repeated in this leg: red on main's code (the first seven cases 6 failed, 1 passed, the two ruled cases 2 failed, the reconcile-reason case 1 failed); 291 passed for test_unsearchable_control_root_291 or test_lifecycle_fail_open_113_114_115 or test_repo_hygiene at 82ac6b7; seven mutants of the fix, each killed, and the review's opposite one (the helper's -x test removed), which fails every case.

Release notes (v1.2.0)

Not established: whether lane 3 accepts this line, which it has not seen. No release-notes line was ever recorded for this PR; the line below was written from the PR's own text.

A lane's control root, or a directory above it, that this account cannot search is read as not read, not as no trees: lane-state and lane-trees exit 10 instead of 8 and say why, worktrees names the lane UNREAD so that lanes-index sync keeps its rows, lane-reconcile is indeterminate, and a write under such a root fails instead of landing under $PROJECTS_ROOT. (opensoft/openRepoTools#291)

The line is here, in the code PR of the pair, and only here: the paired spec PR, opensoft/openRepoTools-spec#8, repeats none.

Review

READY, lane 4's independent paired review of both chains, 2026-10-31. It found no MUST and no SHOULD against the commits, and three NITs. Report sha256: 4ac9e5441affc6aeac48e6d73419580f6786440dc0e5dd32722cc40b8070a682. The review read the chains before the message amend (tips 5f2995d43893 and 8104f2cf965c, the same trees as the amended tips).

The original's recorded review, from the root repository (no lane 3 review comment exists for it): an independent review of 124b509, by an Opus reviewer that wrote none of the code, is LAND, with no MUST. It probed 23 lookup cases and 12 rung cases, identical under bash 3.2.57 and 5.2.21, and read every caller (lane-handoff, lane-start, lane-worktrees, lanes-index, lanes). Its findings were all fixed: SHOULD-1, the manual's rung section (7db059f, then 3e97ae5, both manual-only, which are the paired spec PR's); NIT-1, lane_reconcile saying that a trees link nobody could follow is there (468b20e); NIT-2, the stale wording in a lanes-index comment, the second exit-10 header and the manual's unreadable row (3af5006); NIT-3, the file-wide root skip (a3ea2af); and NIT-4, lookup_blocked reading mode bits (82ac6b7, the comment). The fix round was checked by a second read. The root's CI run 39642710526 at 3e97ae5 was green on all four jobs.

Landing

It lands only on Brett Heap's word, PR by PR; nothing in this text is a landing.

Lane: openRepoTools-4
Claim: opensoft/openRepoTools#291 (comment 6313861277)

🤖 Generated with Claude Code

Summary by Sourcery

Preserve lane state and inventory data when control-root lookups are blocked, reporting inaccessible reads as indeterminate instead of treating them as empty.

New Features:

  • Distinguish inaccessible lane control roots and inventory paths from genuinely absent records across lane reads, listings, reconciliation, and synchronization.

Bug Fixes:

  • Prevent unreadable lanes from being reported as having no trees or snapshot, losing indexed rows, falling back to an unrelated control root, or writing stranded records elsewhere.
  • Preserve inventory rows and report unreadable lanes and records explicitly while retaining existing handling for confirmed absence and unreadable-but-existing directories.
  • Make writes under inaccessible control roots fail instead of writing to a fallback location.
  • Treat dangling or unresolvable control-root links as unreadable rather than absent.

Enhancements:

  • Add lookup diagnostics that identify the inaccessible directory or symbolic link responsible for an indeterminate read.
  • Report inaccessible lifecycle state and worktree inventories with exit status 10 and make reconciliation return an indeterminate verdict.

Tests:

  • Add coverage for inaccessible control-root layouts, blocked ancestor directories, unresolved symbolic links, protected inventory directories, unreadable records, synchronization row retention, and write safety.

… is read as not read instead of as no trees: lane-state and lane-trees exit 10 instead of 8, the listing says UNREAD, and an inventory sync keeps the lane's rows

T017 continuation, in opensoft/openRepoTools-code, of the
opensoft/openRepoTools branch fix/control-root-unsearchable-291 at
3e97ae51445291fc3efdd36da5709edee3103549 (issue
opensoft/openRepoTools#291). It carries that branch's own change
against 50d05d4, with its merge of main 50d05d4 (457b73b) left out, for
the code leg's paths: lanes-edit.sh, lanes, lanes-index and
tests/test_unsearchable_control_root_291.py. Its one other path,
docs/README-lanes.md (+25 -8), belongs to the spec leg and is not in
this commit. lookup_blocked, new, tells a lookup that a directory this
account cannot search refused from an established absence;
lane_control_root keeps a blocked rung's own root; lane-state and
lane-trees exit 10 instead of 8 there; worktree_rows names such a lane
UNREAD so that an inventory sync keeps its rows; and lane-reconcile is
indeterminate.

The subjects below are quoted from the originals; a bare #N in them is
opensoft/openRepoTools#N.

Old-to-new receipt (the original's own non-merge commits; 7db059f458a7
and 3e97ae514452 change docs/README-lanes.md alone, so their part is the
spec leg's):
a431854c42c4 Test that a lane control root nobody could search is never read as a lane with no trees, and that an inventory sync keeps that lane's rows (#291)
02571b4ade4f A lane control root that cannot be searched is read as not read, never as no trees, so an inventory sync keeps the lane's rows (#291)
af4d132af4c7 A lane whose inventory could not be read is no longer said to have one that is there, because a blocked lookup never saw whether it was (#291)
124b509eb6f0 Test that a write under a lane control root nobody could look for fails and is filed nowhere else, and that a .lane-state link whose target is gone is unread, never none (#291)
b0a631baa46c lane_generation's docstring no longer says that a snapshot it could not read is there, because a blocked lookup never saw whether it was (#291)
7db059f458a7 The README's rung section says that a rung is passed over only where its directory is established absent, and that a rung nobody could look for keeps its root, so its reads are 10 and its writes fail (#291)
468b20e6c411 lane-reconcile no longer says that a trees directory nobody could look for is there, and says instead that whether it is there could not be read, and why (#291)
3af5006f565f The lanes-index marker comment, the second exit-10 header in lanes-edit.sh and the README's unreadable verdict row no longer say that a record nobody could look for is there (#291)
a3ea2af1946a Only the #291 cases that set a mode skip as root, so the links that cannot be followed, which set none, are proved there too (#291)
82ac6b742392 lookup_blocked's comment names its limit: a denial that the mode bits do not show still reads as absent (#291)
3e97ae514452 The README's rung paragraph now names what each surface says where a rung could not be looked for, instead of calling every read 10 (#291)

Resolutions: none. The patch applied without conflict, at offsets
only: lanes +29 lines and lanes-edit.sh +97 to +158 lines, from the two
commits below it in this chain.

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

sourcery-ai Bot commented Nov 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

API

Read the lane-state and lane-trees exit-code and diagnostic changes together with the listing’s UNREAD behavior, since these public results distinguish an unreadable lookup from an established absence.

  • lanes-edit.sh L14194-14197, L18810-18816, L18940-18947 (+6 small edits)

Implementation

Review the lookup classification and control-root rung selection together: blocked ancestry must remain unknown rather than be treated as an absent rung.

  • lanes-edit.sh L545-549, L13586-13645, L13661-13671

Follow the unreadable snapshot and inventory results through reconciliation and row construction, including the wording that distinguishes a blocked lookup from an unreadable object that was found.

  • lanes-edit.sh L14557-14564, L14569-14574, L15173-15181 (+8 small edits)

Check the sync hold reason and generation-reader documentation alongside the new behavior so blocked reads retain indexed rows without claiming the object was found.

  • lanes-index L48-2067 (5 small edits)

Check the user-facing footer wording associated with the revised unreadability distinction.

  • lanes L409

Tests

Review the regression suite’s blocked-root, blocked-link, write-routing, inventory-sync, and neighboring unreadable-versus-absent cases against the intended exit codes and listing behavior.

  • tests/test_unsearchable_control_root_291.py L1-442

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

@sonarqubecloud

sonarqubecloud Bot commented Nov 7, 2026

Copy link
Copy Markdown

@brettheap
brettheap changed the base branch from fix/subfield-misreads-301 to main November 8, 2026 13:44
@brettheap
brettheap marked this pull request as ready for review November 8, 2026 13:45
@sourcery-ai

sourcery-ai Bot commented Nov 8, 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 6 days and 4 hours by commenting @sourcery-ai review. Upgrade to get a review now.

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