Skip to content

fix(config): stop disconnect destroying files the user wrote - #367

Open
fstubner wants to merge 1 commit into
mainfrom
fix/memory-file-data-loss
Open

fix(config): stop disconnect destroying files the user wrote#367
fstubner wants to merge 1 commit into
mainfrom
fix/memory-file-data-loss

Conversation

@fstubner

@fstubner fstubner commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Two ways disconnect destroyed user content. Both found by driving a real setup/disconnect round trip, not by reading the code — the splicing helpers were individually defensible and the round trip still ate the file.

1. A file that was only the author's own frontmatter was deleted

Removal asked "is what survives any YAML frontmatter?" and deleted the file when it was. That cannot tell xtctx's own Cursor-rule prelude from a metadata stub someone wrote themselves.

CLAUDE.md before:  ---\ntitle: My own notes\ncustom_field: true\n---\n
after disconnect:  (file does not exist)

Now it compares against the exact prelude the writer prepends for that path — the only string removal is entitled to take back. The writer and remover already promise in their own comments to agree byte-for-byte; this is what that promise required.

2. An orphaned begin marker ate everything to the next block's end

The pattern was begin[\s\S]*?end, which pairs an opening marker with the nearest following close whatever lies between. An end marker lost to a hand-edit, or a merge that kept half of one side, made removal swallow the user's content.

before:  TOP LINE THE USER WROTE
         <!-- xtctx:begin -->
         stale text whose end marker was deleted

         ## The user's own heading
         Content the user cares about.

after:   TOP LINE THE USER WROTE

A begin that meets another begin before it meets an end is no longer treated as an opening. It stays as inert text — which inspectManagedFile already surfaces, and inert text is recoverable where deleted content is not.

countManagedBlocks now counts by the same rule, so status can't report a block removal refuses to touch and send the user to a repair that can never change anything.

Verification

Both cases now round-trip byte-identical through a real setup/disconnect. Tests assert at that level, not at the helper level.

mutation result
restore the old regex pairing 2 tests fail
restore the loose frontmatter test 1 test fails

714 tests pass, typecheck and lint clean.

Deliberately unchanged

A file holding nothing but xtctx's own block is still removed rather than left as a stub. The --all global blast radius, duplicate-block self-healing, and comment preservation all behave as before — each verified still passing.

Two ways it did, both found by driving a real setup/disconnect round trip
rather than by reading the code, and both reproduced before and after.

A memory file containing nothing but the author's own YAML frontmatter was
DELETED. Removal asked "is what survives any frontmatter?" and deleted the
file when it was — a question that cannot tell xtctx's own Cursor-rule prelude
from a metadata stub someone wrote themselves. A `CLAUDE.md` holding
`---\ntitle: My own notes\n---` did not exist after `disconnect`. It now
compares against the exact prelude the writer prepends for that path, which is
the only string removal is entitled to take back; the writer and the remover
already promise in their own comments to agree byte-for-byte, and this is what
that promise required.

A file with one orphaned `begin` marker lost every line between it and the
next block's `end`. The pattern was `begin[\s\S]*?end`, which pairs an opening
marker with the nearest following close whatever lies between — so an end
marker lost to a hand-edit, or a merge that kept half of one side, made
removal swallow the user's content. Reproduced: a file with the author's
heading and body came back as a single line. A `begin` that meets another
`begin` before it meets an `end` is now not treated as an opening at all. It
stays in the file as inert text, which `inspectManagedFile` already surfaces,
and inert text is recoverable where deleted content is not.

`countManagedBlocks` counts by the same rule, so status cannot report a block
that removal refuses to touch — which would tell the user to run a repair that
can never change anything.

Both cases now round-trip byte-identical through setup and disconnect. The
regression tests assert through `setupProject`/`disconnectProject` rather than
through the splicing helpers, because that is the layer where the loss
happened: the helpers were individually defensible and the round trip still
ate the file. Verified by mutation — restoring either the old regex pairing or
the loose frontmatter test turns them red.

Kept: a file that held nothing but xtctx's own block is still removed rather
than left as a stub, and the `--all` global blast radius, duplicate-block
self-healing and comment preservation all still behave as before.
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