fix(reconcile): stop writing absolute archive paths into tracked files - #53
Merged
Merged
Conversation
`prune` embedded the archive's ABSOLUTE local path in the source repo's `.console/log.md`. On a Windows run that put a `C:\Users\<name>\...` path into a PUBLIC repo -- an operator's home directory and real name -- and such a pointer is unfollowable on any other host or in CI. Not platform-specific: this repo's own log carried the same artifact from an earlier Linux run (`/home/dev/...`), which this commit also clears. `portable_archive_ref` renders `<private-manifest>/archive/console/<repo>/<file>` instead. It lives in privacy.py, which already owns the layout constant, and the placeholder is what boundary rule I2 requires anyway -- the private repo is resolved at runtime, never written down. The archive lives in a DIFFERENT repo from the source, so a repo-relative path is impossible; the placeholder is the only honest portable form. Separators are normalised before the anchor search. CI caught why that matters: the first version parsed with the host's `Path`, so on Linux a Windows path is a single opaque component, the anchor never matched, and the fallback handed back the whole absolute path -- reinstating the exact leak. The function's promise is that no input yields an absolute output, and it now holds for either platform's shape on either platform. `_ensure_pointer` heals a legacy absolute pointer in place rather than skipping it: the leak should not persist merely because a pointer already exists. One already in the portable form is left alone, annotations included, so a hand-corrected pointer survives a re-run. The end-to-end test earned its keep twice. Its first draft built the legacy fixture with an ASCII `->`, passed the whole unit suite, and produced TWO `## Archived` sections -- `_ensure_pointer` recognises a pointer by `_POINTER_PREFIX`, which carries a Unicode arrow. The fixture now builds from the constant. That near-miss also exposed a latent wart left unfixed here: a section headed `Archived` whose body lacks the prefix gets a second one appended rather than recognised. 15 tests. Suite 473 -> 486 passed; the 16 failures are unchanged and identical. No narrative log entry: this repo's log sits at exactly 400/400, so adding one would trip RC1, and Custodian ADR 0001 argues rationale belongs in the commit message rather than duplicated into the log. The log change here is the pointer heal itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
force-pushed
the
claude/portable-archive-pointer
branch
from
August 3, 2026 20:52
4de94f4 to
64e0871
Compare
This was referenced Aug 3, 2026
ProtocolWarden
added a commit
that referenced
this pull request
Aug 4, 2026
…d one `_ensure_pointer` recognised the pointer by heading AND `_POINTER_PREFIX`, so a section already headed `Archived` whose body lacked the prefix matched neither branch: the loop fell through and appended a SECOND `## Archived` beside it. #53 flagged this and left it. It is not hypothetical — that PR nearly shipped it. Its first end-to-end fixture wrote the legacy pointer with an ASCII `->` instead of the prefix's Unicode `→`, produced two `## Archived` sections, and passed the whole unit suite. Two ways in: an operator writes the heading by hand, or an older run's wording drifts from the constant. Matching on heading alone and branching on the body inside. A section under the pointer heading with no pointer line is now repaired rather than ignored. Repaired, not replaced. Replacing wholesale would discard whatever prose is already under that heading, and this module goes out of its way elsewhere to preserve an operator's annotations on a portable pointer — so the pointer line is inserted under the heading and the remaining body kept verbatim. That also keeps the invariant the caller actually needs: after a prune, exactly one `## Archived` section, carrying a correct portable ref. 6 tests. Each fails against the pre-fix predicate — checked by reverting it, which is worth stating because two of them did NOT fail on the first draft: the pre-fix code also preserves the prose and is also stable on re-run, just at two sections rather than one. Both now assert the section count, which is the thing that actually distinguishes the behaviours. Suite 488 -> 494 passed; the 16 failures are unchanged and identical, and `test_committed.py` / `test_signing.py` still fail to import for lack of `cryptography` in this environment. No narrative log entry, following #53: this repo's log is at exactly 400/400, so adding one would trip RC1 and force a prune to land a commit about pruning. Custodian ADR 0001 argues rationale belongs in the commit message instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
added a commit
that referenced
this pull request
Aug 4, 2026
…d one (#55) `_ensure_pointer` recognised the pointer by heading AND `_POINTER_PREFIX`, so a section already headed `Archived` whose body lacked the prefix matched neither branch: the loop fell through and appended a SECOND `## Archived` beside it. #53 flagged this and left it. It is not hypothetical — that PR nearly shipped it. Its first end-to-end fixture wrote the legacy pointer with an ASCII `->` instead of the prefix's Unicode `→`, produced two `## Archived` sections, and passed the whole unit suite. Two ways in: an operator writes the heading by hand, or an older run's wording drifts from the constant. Matching on heading alone and branching on the body inside. A section under the pointer heading with no pointer line is now repaired rather than ignored. Repaired, not replaced. Replacing wholesale would discard whatever prose is already under that heading, and this module goes out of its way elsewhere to preserve an operator's annotations on a portable pointer — so the pointer line is inserted under the heading and the remaining body kept verbatim. That also keeps the invariant the caller actually needs: after a prune, exactly one `## Archived` section, carrying a correct portable ref. 6 tests. Each fails against the pre-fix predicate — checked by reverting it, which is worth stating because two of them did NOT fail on the first draft: the pre-fix code also preserves the prose and is also stable on re-run, just at two sections rather than one. Both now assert the section count, which is the thing that actually distinguishes the behaviours. Suite 488 -> 494 passed; the 16 failures are unchanged and identical, and `test_committed.py` / `test_signing.py` still fail to import for lack of `cryptography` in this environment. No narrative log entry, following #53: this repo's log is at exactly 400/400, so adding one would trip RC1 and force a prune to land a commit about pruning. Custodian ADR 0001 argues rationale belongs in the commit message instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
added a commit
that referenced
this pull request
Aug 4, 2026
…#57) Closes #54. `prune --dry-run` printed N moves and `--apply` archived N plus every unclaimed section past `recent_n`. The extra sections were computed inside `_apply_plan_locked` and never reached `plan.moves`, which is all `format_plan` renders — so nothing reported them, before or after. The operator approved one thing and got another. What makes it bite rather than merely surprise: those sections are claimed by no item AND cleared by no gate. The DOC GAP gate only inspects *done* items, so anything still `partial` is never examined — and `owned_done` excludes it too, so its sections are never claimed and land in the age-swept bucket. Incomplete, undocumented work is therefore the MOST likely thing to be archived by age, which inverts what the gate is for. Destination is the private manifest, so on a public repo the content also changes repos on its way out. Three changes, no policy change: - `build_plan` records the age-swept sections as `PlannedTrim`, and `format_plan` prints them under their own heading, named for what they are. A separate type from `PlannedMove` on purpose: a move was released by the gate, a trim was swept by a number, and conflating them in the thing an operator approves is how this stayed invisible. - `PrunePlan` carries the `recent_n` it was built with, and `apply_plan` defaults to it. The two defaulted independently, so planning with `--recent 20` sweeps to 10 at apply time — a second, quieter divergence. An explicit `recent_n=` still overrides. - `is_noop` deliberately keeps ignoring `trims`: age-sweeping is a side effect of an otherwise-legitimate prune, never a reason for one. Counting trims there would start pruning repos that are today correctly left alone. There is a test pinning that. Not addressed here, because they are policy and belong to the spec owner: whether `recent_n` should sweep unclaimed sections at all, and whether the reconcile and trim operations should be separable. #54 lists both; this change only makes the current behaviour honest. 5 tests. Each of the three changes was reverted in turn to confirm a named test fails — the end-to-end one, which diffs the dry-run text against the headings that actually land in the archive, catches two of them on its own. Suite 493 -> 498 passed; the 17 failures are unchanged and identical. No narrative log entry, following #53 and #55: this repo's log is at 400/400, so adding one would trip RC1 and force a prune to land a commit about pruning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pruneembedded the archive's absolute local path in the source repo's.console/log.md. On a Windows run that produced:An operator's home directory and real name, written into a public repo — and unfollowable on any other host or in CI.
Not platform-specific. This repo's own log carried the same artifact from an earlier Linux run (
/home/dev/...at line 391). This commit clears it.Caught reviewing Custodian#71, where the same output was one merge away from landing in a public repo. That one was corrected by hand; this fixes the generator.
The fix
portable_archive_refrenders<private-manifest>/archive/console/<repo>/<file>. It lives inprivacy.py, which already owns the layout constant, and the placeholder is what boundary rule I2 requires anyway — the private repo is resolved at runtime, never written down.The archive lives in a different repo from the source, so a repo-relative path is impossible; the placeholder is the only honest portable form. Three resolution branches — relative to a known root, sliced at the
archive/consoleanchor, bare filename — and none can return an absolute path._ensure_pointernow also heals a legacy absolute pointer in place rather than skipping it. The leak shouldn't persist just because a pointer already exists. A pointer already in the portable form is left alone — annotations included — so a hand-corrected one survives a re-run.The end-to-end test earned its keep
Its first draft built the legacy fixture with an ASCII
->, passed the entire unit suite, and produced two## Archivedsections._ensure_pointerrecognises a pointer by_POINTER_PREFIX, which carries a Unicode arrow — so the fixture never matched. It now builds from the constant rather than retyping it.That near-miss also exposed a latent wart left unfixed: a section headed
Archivedwhose body lacks the prefix gets a second one appended rather than being recognised. Out of scope here, but real.Verification
ruffclean on all three changed files; Custodian audit 0 findings.console/Note on the missing log entry
This repo's log sits at exactly 400/400, so any entry trips RC1. Custodian ADR 0001 argues rationale belongs in commit messages rather than duplicated into the log; this follows that. The log change here is the pointer heal.
🤖 Generated with Claude Code