Skip to content

update-shape.py apply rewrites a pinned copy, contracts/shape-pin.yaml or the manifest THROUGH a symlink the root keeps at that path (Rollback.write opens it r+b), so the standard's bytes land at the link's end, outside the root if that is where it points, with exit 0, and --branch reports a commit holding a file that commit does not contain #286

Closed

Description

@brettheap

(lane openRepoShape-2, 2026-10-11T23:54:40Z)

Found by this lane's adversarial review of PR #269 (fix/update-shape-add-dangling-symlink at 7bceeca, the fix for #239), its candidate follow-up 4; the #239 implementer noted it first and left it out ("not an absence decision"). #239 and #269 cover --add, a file the root does not have yet. This issue is about the files apply REWRITES. Pre-existing on main c3f4987 and the same at 7bceeca: #269 neither causes it nor changes it. Not claimed.

What is wrong. A root can keep a pinned copy as a symlink, committed, for example AGENTS-shape.md pointing at a shared file elsewhere on disk. check reads through the link and reports the row as upstream-changed when the standard edits it. apply then rewrites the file AT THE LINK'S END and keeps the link:

  • With the validators green it exits 0.
  • Under --branch it prints committed on <branch>: AGENTS-shape.md, contracts/shape-pin.yaml, project.yaml, but git sees the link unchanged, so the commit holds only the pin and the manifest.
  • The pin row now records the digest of bytes that exist only at the link's end, outside the repository.

contracts/shape-pin.yaml kept as a link is the same: the pin is rewritten at the link's end and left out of the commit, while the commit moves project.yaml's shape pin and the copies.

PR #269's new refusal says "this command never writes through a link". That is true of --add at that head and not of these rewrites.

# $U: a checkout of openRepoShape. gh is a PATH shim that logs its argv and exits 97 (never called). Every repository is local; no network.
git clone -q "$U" "$R/shape"
python3 "$R/shape/scaffold-project.py" --org testorg --project Atlas --elected-by 'Test Human' --elected-on 2026-09-02 \
        --local-remote-dir "$R/remotes" --work-dir "$R/work"
git clone -q --recurse-submodules "$R/remotes/Atlas.git" "$C"
mkdir "$R/outside" && mv "$C/AGENTS-shape.md" "$R/outside/" && ln -s "$R/outside/AGENTS-shape.md" "$C/AGENTS-shape.md"
git -C "$C" add AGENTS-shape.md && git -C "$C" commit -q -m 'keep AGENTS-shape.md as a link' -- AGENTS-shape.md
sha256sum < "$R/outside/AGENTS-shape.md"     # 8694c502cdfd...
# the standard edits that copy
printf '<!-- an upstream edit -->\n' >> "$R/shape/templates/assembly-root/AGENTS-shape.md"
git -C "$R/shape" commit -q -am 'edit AGENTS-shape.md'
python3 "$U/update-shape.py" check --root "$C" --upstream "$R/shape"
#   exit 1   upstream-changed   AGENTS-shape.md
python3 "$U/update-shape.py" apply --root "$C" --upstream "$R/shape" --yes --branch shape/update
#   exit 0     copied   AGENTS-shape.md
#              re-pinned contracts/shape-pin.yaml and project.yaml c3f4987190ca -> f339273c6f73
#   pins ok / manifest ok: Atlas (atlas), 3 legs
#     committed on shape/update: AGENTS-shape.md, contracts/shape-pin.yaml, project.yaml
sha256sum < "$R/outside/AGENTS-shape.md"     # 9e88c8e59159...: the file outside the root now ends `<!-- an upstream edit -->`
test -L "$C/AGENTS-shape.md" && echo still a link
git -C "$C" show --stat --format=%s HEAD     # contracts/shape-pin.yaml | 6 +++---   project.yaml | 4 ++--   (no AGENTS-shape.md)
the root keeps the run result on c3f4987 (and at #269's 7bceeca)
AGENTS-shape.md as a committed link to a file outside the root apply --branch, validators green exit 0; the outside file rewritten; the commit omits AGENTS-shape.md though the run says it committed it; the pin row is the new digest
the same apply, validators red exit 2, ROLLED BACK: the original bytes are written back THROUGH the link, and the outside file is as it was
contracts/shape-pin.yaml as a committed link to a file outside the root apply --branch, validators green exit 0; the outside pin rewritten; the commit holds project.yaml and the copy, not the pin, though the run says it committed it
a DANGLING link at a pinned copy check, apply copy-missing, refused update-conflict before any write: already right

Why.

  • _classify_row reads the root's bytes through the link: target_path.is_file() at update-shape.py:482 on main, then read_bytes() at :494.
  • _write_copies (:2222) and _repin (:2249, :2252) write through Rollback.write (:1771-1793), which opens the path r+b (:1789) and so follows the link.
  • commit_on_branch stages the path names. The link itself is unchanged, so git commits nothing for it, while _land prints the paths apply meant to commit (:2355).

The README's Shape: line already has the posture this lacks: ReadmeShapeLine reads a linked README as README_SYMLINK (:1019) and never writes through it (#152, tests/test_update_shape.py's test_a_symlinked_readme_is_never_written_through).

What it should do. A path apply would rewrite is refused BEFORE ANY WRITE when it is a symlink, or has one above it: a pinned copy, contracts/shape-pin.yaml, the manifest. Use #269's symlink_on_the_way walk, with the Windows junction case its review asks for.

How to test. In tests/test_update_shape_add_symlink.py's fixture, plus one upstream commit that edits templates/assembly-root/AGENTS-shape.md, with @WINDOWS_SKIP as its link tests use:

  • (a) AGENTS-shape.md a committed link to a file outside the root, with the same bytes: check reports the new verdict, and apply and apply --branch exit 2 by name. The outside file, the tree (snapshot/assert_untouched) and the branches are untouched.
  • (b) contracts/shape-pin.yaml, then project.yaml, as such a link: the same.
  • (c) A directory link above a pinned copy (scripts to a copy of itself outside the root): the same. With today's code this fixture goes red, because the validators resolve their own location through the link, and rolls back; the refusal must come first.
  • (d) The controls: a dangling link at a pinned copy stays copy-missing, and a root reached through a link still updates.

Related: #239 and PR #269 (--add's half), #152 (the README's link posture), #178 and #183 (the adopt and scaffold materializer writing through a root-kept link).

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 5b7b5f3d-dce7-452d-a33d-ab8537d91db3@Eagle, 2026-10-12T10:59:47Z, for #286

    Logged in lanes/log/openRepoShape-2.md at 110e7ac9ffbc29038d5b5a284f80ecc4629a53e2.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#286:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#286"`, kept where the row or its body names `#286`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `286` as a whole token:
       none
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions