fix(git-sync): branch choreography + strict scoping + delete cap (Phase B hardening)

Fixes found by the live pull/push e2e:
- CRITICAL: driveCycle never checked out the 'docmost' branch before
  applyPullActions, so Docmost content was written straight onto 'main',
  clobbering local file edits before push could diff them. Now checkout
  'docmost' before pull (applyPullActions commits there then checks out main +
  merges) — mirrors the engine's pull main(). Round-trip now works both ways.
- add an unresolved-merge guard (SPEC §9): skip the cycle if the vault is
  mid-merge instead of failing on checkout.
- SAFETY: enabledSpaces() is now STRICT opt-in — only spaces with
  settings.gitSync.enabled===true; removed the all-spaces fallback that synced
  every space (incl. a 92-page one) the moment GIT_SYNC_ENABLED flipped.
- SAFETY: per-cycle delete cap (GIT_SYNC_MAX_DELETES_PER_CYCLE, default 5):
  dry-run the push, and if planned deletes exceed the cap, run the apply with
  deletePage neutralized — phantom absence-deletions from a non-convergent vault
  can't soft-delete real pages. Fails safe if the dry-run throws.
- fix manual trigger: TriggerGitSyncDto.spaceId needs @IsUUID or the global
  whitelist ValidationPipe strips it (arrived undefined -> vault 'undefined').

Live-verified on an isolated flagged space: push (vault file edit -> Docmost
content, stamped lastUpdatedSource='git-sync') and pull (Docmost rename -> vault
file + meta) both work; an unrelated 92-page space stayed untouched throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 15:37:09 +03:00
parent da612c600e
commit 44a0b08e52
5 changed files with 133 additions and 31 deletions

View File

@@ -362,6 +362,20 @@ export class EnvironmentService {
);
}
/**
* Defense-in-depth absolute cap on how many pages a single push cycle may
* soft-delete (default 5). A non-convergent / phantom-absence cycle whose push
* plan would delete more than this is forced to skip deletions that cycle (the
* orchestrator logs a WARNING). A non-positive or unparseable value falls back
* to the default 5 so the cap can never be silently disabled.
*/
getGitSyncMaxDeletesPerCycle(): number {
const parsed = parseInt(
this.configService.get<string>('GIT_SYNC_MAX_DELETES_PER_CYCLE', '5'),
);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 5;
}
/**
* The service user id git-sync writes are attributed to. Required when sync is
* enabled (validated in environment.validation.ts); optional otherwise.