Skip to content

fix(reconcile): stop writing absolute archive paths into tracked files - #53

Merged
ProtocolWarden merged 1 commit into
mainfrom
claude/portable-archive-pointer
Aug 3, 2026
Merged

fix(reconcile): stop writing absolute archive paths into tracked files#53
ProtocolWarden merged 1 commit into
mainfrom
claude/portable-archive-pointer

Conversation

@ProtocolWarden

Copy link
Copy Markdown
Owner

prune embedded the archive's absolute local path in the source repo's .console/log.md. On a Windows run that produced:

_Archived completed history → `C:\Users\<name>\Documents\GitHub\PrivateManifest\...`_

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_ref renders <private-manifest>/archive/console/<repo>/<file>. 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. Three resolution branches — relative to a known root, sliced at the archive/console anchor, bare filename — and none can return an absolute path.

_ensure_pointer now 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 ## Archived sections. _ensure_pointer recognises 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 Archived whose body lacks the prefix gets a second one appended rather than being recognised. Out of scope here, but real.

Verification

  • 15 tests — including "never emits an absolute path" over both Windows and POSIX shapes, I2 (private repo name never appears), annotated-pointer preservation, and two end-to-end prunes
  • Suite 473 → 486 passed; the 16 failures are unchanged and identical by name — no regressions
  • ruff clean on all three changed files; Custodian audit 0 findings
  • No absolute paths remain anywhere in this repo's tracked .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

`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
ProtocolWarden force-pushed the claude/portable-archive-pointer branch from 4de94f4 to 64e0871 Compare August 3, 2026 20:52
@ProtocolWarden
ProtocolWarden merged commit 97fe8be into main Aug 3, 2026
5 of 7 checks passed
@ProtocolWarden
ProtocolWarden deleted the claude/portable-archive-pointer branch August 3, 2026 20:55
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>
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