fix(git-sync): screen non-page files out of PUSH (CRITICAL — review)

Self-review of phase 3 caught a data-corruption regression: nativeMeta always
supplies the run's spaceId, so the planner's 'create-without-spaceId' skip — which
had doubled as the only filter for non-page files — went dead. An ADDED
.obsidian/*.json, attachment, or dotfile (committed to the vault, no .gitignore)
would then be classified as a CREATE: a junk Docmost page, plus a gitmost_id
frontmatter written INTO the file, corrupting it.

Fix: isPageFile(path) — a .md file with NO dot-segment anywhere — and filter the
diff to page files at the very top of computePushActions, BEFORE any
classification, so non-page A/M/D/R are ignored (design §Адопция). 2 unit tests
pin it (.obsidian/json, attachment, dotfile, dot-segment, .md dotfile all ignored;
real pages still created). 614 engine tests green.

Also: refreshed stale docmost:meta comments to gitmost_id (review SUGGESTION), and
documented the deferred adoption frontmatter-preservation gap (review WARNING) in
page-file.ts + the design doc (do NOT roll native onto a real vault with Obsidian
properties until phase 4 round-trips them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-24 05:14:36 +03:00
parent 04c99ff860
commit 3b00cd5021
4 changed files with 94 additions and 12 deletions

View File

@@ -318,3 +318,47 @@ describe('computePushActions — currentPageIds guard (cross-cycle move)', () =>
expect(actions.skipped).toEqual([]);
});
});
describe('computePushActions — page-file filter (non-page files ignored)', () => {
it('IGNORES added/modified/deleted non-page files (.obsidian, dotfiles, non-.md)', () => {
// A vault commits `.obsidian/*`, attachments, dotfiles (no .gitignore), so
// they show up in the diff — but they are NEVER Docmost pages. Even though a
// synthetic metaAt would hand back a spaceId (the vault's), none of these may
// become a CREATE/UPDATE/DELETE. This pins the data-corruption guard: an
// added `.obsidian/workspace.json` must NOT create a page nor get a gitmost_id.
const changes: DiffEntry[] = [
{ status: 'A', path: '.obsidian/workspace.json' },
{ status: 'M', path: '.obsidian/app.json' },
{ status: 'A', path: 'attachments/diagram.png' },
{ status: 'A', path: '.hidden.md' }, // dotfile, even with .md
{ status: 'A', path: 'Notes/.config/x.md' }, // dot-segment mid-path
{ status: 'D', path: '.obsidian/old.json' },
];
// Every path resolves to a spaceId-bearing meta (the vault's space) — proving
// the filter, not a missing spaceId, is what screens them out.
const metaAt = (path: string): DocmostMdMeta =>
({ version: 1, title: 'x', spaceId: 'sp-vault' }) as DocmostMdMeta;
const actions = computePushActions({ changes, metaAt });
expect(actions.creates).toEqual([]);
expect(actions.updates).toEqual([]);
expect(actions.deletes).toEqual([]);
expect(actions.renamesMoves).toEqual([]);
expect(actions.skipped).toEqual([]); // not even recorded as skipped — ignored
});
it('still processes a normal .md page alongside ignored non-page files', () => {
const changes: DiffEntry[] = [
{ status: 'A', path: '.obsidian/workspace.json' },
{ status: 'A', path: 'Real Page.md' },
{ status: 'A', path: 'Folder/Note.md' },
];
const metaAt = (path: string): DocmostMdMeta =>
({ version: 1, title: 'x', spaceId: 'sp-vault' }) as DocmostMdMeta;
const actions = computePushActions({ changes, metaAt });
// Only the two real .md pages become creates; the .obsidian file is ignored.
expect(actions.creates).toEqual([
{ path: 'Real Page.md' },
{ path: 'Folder/Note.md' },
]);
});
});