Skip to content

vcs: follow a rename to the end, and never name a path that is not there - #8

Merged
melbinjp merged 1 commit into
mainfrom
fix/follow-the-rename-chain
Aug 18, 2026
Merged

vcs: follow a rename to the end, and never name a path that is not there#8
melbinjp merged 1 commit into
mainfrom
fix/follow-the-rename-chain

Conversation

@melbinjp

Copy link
Copy Markdown
Owner

moved to X should mean X is there now. It did not.

Git.moved_to looked at the deleting commit and stopped, so a file renamed more than once
was reported at its FIRST hop - a path that may itself be gone. That is the worst thing this
function can do, because its entire purpose is to save the reader a search, and instead it
sends them somewhere empty.

Found on a real repository

open-gsd/gsd-core, docs/CONFIGURATION.md cites sdk/shared/model-catalog.json:

11918dcc  sdk/shared/model-catalog.json      ->  get-shit-done/bin/shared/model-catalog.json
463cffd8  get-shit-done/bin/shared/...json   ->  gsd-core/bin/shared/model-catalog.json

We reported the middle one. get-shit-done/ has zero files at that repository's HEAD, so
the advice pointed into a directory that no longer exists anywhere in the tree.

- but  moved to `get-shit-done/bin/shared/model-catalog.json` in 11918dcc ...
+ but  moved to `gsd-core/bin/shared/model-catalog.json` in 11918dcc ...

The invariant, now enforced

A destination is named only if it is tracked at HEAD. The chain is followed, and if it
dead-ends somewhere untracked the answer is None - so the finding reads "deleted ... and
never restored"
, which is true, rather than naming a phantom. Saying less beats saying wrong.

Bounded at ten hops, because a rename cycle is cheaper to survive than to prove impossible.
The worst real case in the corpus is two.

Verdicts do not move

This changes the advice, not the judgement. On the repository that surfaced it: 34 broken
before, 34 after.
The docstring already said this function "changes no verdict" and that
stays true.

Proof

Four tests, building their own two-commit history because a rename is paired only within one
commit and make_repo cannot produce two.

Confirmed failing before the fix, and for the right reasons rather than by luck:

before after
two hops returns the middle path returns the final path, asserted tracked
chain ending in a real deletion returns a phantom returns None
one hop correct correct
never renamed correct correct

The two that were already right still pass, which is the half that shows the fix did not
buy its result by breaking something else.

162 tests, ruff clean, docproof passes on itself.

moved_to looked at the deleting commit and stopped, so a file renamed twice was
reported at its first hop. Found on open-gsd/gsd-core, where
docs/CONFIGURATION.md cites sdk/shared/model-catalog.json and the chain is
sdk/ -> get-shit-done/ -> gsd-core/. We named the middle one, and get-shit-done/
has zero files at that repository's HEAD - so the advice pointed into a directory
that does not exist anywhere in the tree.

That is the worst thing this function can do: its whole purpose is to save the
reader a search, and it was sending them somewhere empty.

The invariant is now the one a reader assumes - a destination is named only if it
is tracked at HEAD. A chain that dead-ends untracked returns None, so the finding
says 'deleted and never restored', which is true. Saying less beats saying wrong.
Bounded at ten hops; the worst real case is two.

Verdicts do not move: 34 broken before and after on that repository.

Four tests with their own two-commit history, because a rename is paired only
within one commit. Confirmed failing beforehand for the right reasons - the
two-hop case returned the middle path and the dead-end case returned a phantom -
while the one-hop and never-renamed cases passed throughout.
@melbinjp
melbinjp merged commit ca4d6e3 into main Aug 18, 2026
20 checks passed
@melbinjp
melbinjp deleted the fix/follow-the-rename-chain branch August 18, 2026 13:58
melbinjp added a commit that referenced this pull request Aug 18, 2026
…wn (#9)

pyproject carried version = "0.1.2" while src/docproof/__init__.py carried
"0.1.0". Three releases had shipped and the README pinned v0.1.2, so anyone who
followed the README ran a tool whose every report header read

    docproof 0.1.0 - their-repo, N document(s)

and whose --version agreed. The packaging metadata was right; the number the tool
says about itself was two releases stale. That is a documented claim contradicted
by the repository, which is the exact thing this tool exists to find, inside this
tool.

Found while checking whether docproof was fit to offer into another project's CI,
after a gsd-core maintainer had been pointed at it. He would have seen 0.1.0.

Correcting the number would have left the second copy in place, so the second copy
is gone instead: hatchling reads the package attribute and there is one literal.
Bumped to 0.1.3, which is also the release that carries the rename-chain fix in #8.

tests/test_version_has_one_source.py asserts the SHAPE rather than the value. A
test that the two numbers are equal passes today and rots the moment somebody bumps
one of them, which is how this arose. It asserts there is no static version under
[project] at all, so the drift cannot be reintroduced by remembering to keep two
things in sync - the class of rule this repository exists because people forget.

Verified: full suite 167 passing, and `python -m build --wheel` produces
docproof-0.1.3-py3-none-any.whl, so the build really does read the attribute.
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