Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions src/context_lifecycle/reconcile/prune.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,20 +340,57 @@ def _pointer_section(archive_file: Path, *, private_root: Path | None = None) ->
_ABSOLUTE_REF_RE = re.compile(r"`(?:[A-Za-z]:[\\/]|/)")


def _pointer_line(pointer: Section) -> str:
"""The `_Archived … → `<ref>`_` line on its own, without the heading."""
for line in pointer.body.splitlines(keepends=True):
if _POINTER_PREFIX in line:
return line
return ""


def _merge_pointer_into(sec: Section, pointer: Section) -> Section:
"""Insert the pointer line into an existing section, keeping its prose.

Used when a section already carries the pointer heading but no pointer
line. Replacing it wholesale would discard whatever the operator wrote
there, and this module elsewhere goes out of its way to preserve
annotations — so the pointer is inserted under the heading and the rest of
the body is kept verbatim.
"""
lines = sec.body.splitlines(keepends=True)
heading_line = lines[0] if lines else f"## {sec.heading}\n"
remainder = "".join(lines[1:]).strip("\n")
body = f"{heading_line}\n{_pointer_line(pointer)}"
if remainder:
body += f"\n{remainder}\n"
return Section(heading=sec.heading, body=body.rstrip("\n") + "\n\n")


def _ensure_pointer(sections: list[Section], pointer: Section) -> list[Section]:
"""Append the pointer section, or upgrade a legacy absolute one in place.
"""Append the pointer section, or repair an existing one in place.

Idempotent for any pointer already in the portable form — including one an
operator has annotated, which is left untouched. A pointer carrying an
ABSOLUTE path is replaced: older runs embedded the operator's home directory
(and often their name) into tracked files, and leaving that in place would
keep leaking it every time the file is read.

A section under the pointer heading that carries no pointer line at all —
hand-written, or an older run whose wording drifted from ``_POINTER_PREFIX``
— is repaired rather than ignored. Matching on heading *and* prefix meant
such a section was not recognised, so a second ``## Archived`` was appended
beside it and the file ended up with two. That very nearly shipped: a test
fixture written with an ASCII ``->`` instead of the prefix's ``→`` produced
exactly this and passed the unit suite.
"""
for i, sec in enumerate(sections):
if sec.heading == pointer.heading and _POINTER_PREFIX in sec.body:
if _ABSOLUTE_REF_RE.search(sec.body):
return sections[:i] + [pointer] + sections[i + 1 :]
return sections # already portable — leave it, annotations and all
if sec.heading != pointer.heading:
continue
if _POINTER_PREFIX not in sec.body:
return sections[:i] + [_merge_pointer_into(sec, pointer)] + sections[i + 1 :]
if _ABSOLUTE_REF_RE.search(sec.body):
return sections[:i] + [pointer] + sections[i + 1 :]
return sections # already portable — leave it, annotations and all
return sections + [pointer]


Expand Down
74 changes: 74 additions & 0 deletions tests/test_archive_pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,80 @@ def test_upgrades_a_legacy_absolute_pointer_in_place(self, tmp_path, legacy_raw)
assert PRIVATE_ROOT_PLACEHOLDER in out[1].body


class TestPointerHeadingWithoutPrefix:
"""A section under the pointer heading that carries no pointer line.

Matching on heading AND prefix meant such a section went unrecognised, so a
second ``## Archived`` was appended beside it. Reachable two ways: an
operator writes the heading by hand, or an older run's wording drifts from
``_POINTER_PREFIX`` — which is exactly how the ASCII-arrow fixture slipped
through the suite that shipped this module.
"""

def _pointer(self, tmp_path) -> Section:
root = tmp_path / "P"
return _pointer_section(
root / "archive" / "console" / "Custodian" / "log-2026-08-03.md",
private_root=root,
)

def test_does_not_append_a_second_section(self, tmp_path):
bare = Section(heading="Archived", body="## Archived\n\nSee the private manifest.\n\n")
out = _ensure_pointer([bare], self._pointer(tmp_path))
assert len(out) == 1
assert out[0].body.count("## Archived") == 1

def test_inserts_the_pointer_line(self, tmp_path):
bare = Section(heading="Archived", body="## Archived\n\nSee the private manifest.\n\n")
out = _ensure_pointer([bare], self._pointer(tmp_path))
assert _POINTER_PREFIX in out[0].body
assert PRIVATE_ROOT_PLACEHOLDER in out[0].body

def test_keeps_the_existing_prose_in_the_repaired_section(self, tmp_path):
"""Replacing wholesale would discard whatever the operator wrote.

Asserts the prose and the pointer land in the SAME single section — the
pre-fix code also kept the prose, just in a section sitting beside a
second ``## Archived`` heading, so a laxer assertion passes either way.
"""
bare = Section(heading="Archived", body="## Archived\n\nSee the private manifest.\n\n")
out = _ensure_pointer([bare], self._pointer(tmp_path))
assert len(out) == 1
assert "See the private manifest." in out[0].body
assert _POINTER_PREFIX in out[0].body

def test_ascii_arrow_variant_is_repaired_not_duplicated(self, tmp_path):
"""The near-miss made concrete: prefix wording drifts, heading does not."""
drifted = Section(
heading="Archived",
body="## Archived\n\n_Archived completed history -> `<private-manifest>/x.md`_\n\n",
)
out = _ensure_pointer([drifted], self._pointer(tmp_path))
assert len(out) == 1
assert out[0].body.count("## Archived") == 1
assert _POINTER_PREFIX in out[0].body

def test_result_is_idempotent(self, tmp_path):
"""A second prune over the repaired section must change nothing.

The length assertion is what makes this bite: the pre-fix code is also
stable on re-run, but stable at two sections rather than one.
"""
p = self._pointer(tmp_path)
bare = Section(heading="Archived", body="## Archived\n\nSee the private manifest.\n\n")
once = _ensure_pointer([bare], p)
assert len(once) == 1
assert _ensure_pointer(once, p) == once

def test_other_sections_are_untouched(self, tmp_path):
bare = Section(heading="Archived", body="## Archived\n\nnote\n\n")
before = Section(heading="Before", body="## Before\n\nb\n\n")
after = Section(heading="After", body="## After\n\na\n\n")
out = _ensure_pointer([before, bare, after], self._pointer(tmp_path))
assert len(out) == 3
assert out[0] is before and out[2] is after


class TestPruneEndToEnd:
"""Unit tests cover the pointer in isolation; this drives a real prune.

Expand Down
Loading