From 123e98180838c691f8b1d73617dad268605cf56f Mon Sep 17 00:00:00 2001 From: claude-stand Date: Thu, 2 Jul 2026 12:57:12 +0300 Subject: [PATCH] fix(git-sync): strip cosmetic ~ disambiguation suffix from ingested title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sibling pages that share a title collide on one vault filename, so the layout appends a cosmetic ` ~` suffix (engine disambiguate()). That suffix is a local filesystem artifact and must never become the page's real Docmost title, but on ingest the filename-derived title carried it back into the DB on some paths (observed: intermittent same-title collision left a page permanently titled "Title ~"). Strip it in renamePage() — the single choke point every git-sync title write funnels through — but only when the trailing token equals THIS page's own slugId, so a genuine user title that legitimately ends in ` ~token` is never corrupted (slugId is a random nanoid). Repro: create two pages with the same title; ~1 in 4 the second page's title is permanently polluted. After fix: 0/6. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../services/gitmost-datasource.service.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/server/src/integrations/git-sync/services/gitmost-datasource.service.ts b/apps/server/src/integrations/git-sync/services/gitmost-datasource.service.ts index 4dd707cf..afa77298 100644 --- a/apps/server/src/integrations/git-sync/services/gitmost-datasource.service.ts +++ b/apps/server/src/integrations/git-sync/services/gitmost-datasource.service.ts @@ -313,13 +313,29 @@ export class GitmostDataSourceService { if (!page) { throw new NotFoundException(`Page ${pageId} not found`); } + // Defensive de-pollution of the cosmetic ` ~` disambiguation suffix. + // When two sibling pages share a title, the vault layout appends ` ~` + // to the colliding file's stem (engine `disambiguate(name, slugId)` = exactly + // `${name} ~${slugId}`) so two pages never map to one `.md`. That suffix is a + // LOCAL filesystem artifact and must NEVER become the page's real Docmost + // title. A filename-derived title can carry it back in on ingest (observed: + // intermittent same-title collision left a page permanently titled + // "Title ~"). Strip it at this single choke point every git-sync + // title write funnels through — but ONLY when the trailing token equals THIS + // page's own slugId, so a genuine user title that legitimately ends in + // ` ~token` is never corrupted (slugId is a random nanoid; no real collision). + const suffix = ` ~${page.slugId}`; + const cleanTitle = + page.slugId && title.endsWith(suffix) + ? title.slice(0, -suffix.length) + : title; // PageService.update takes a User; the git-sync service user is the // responsible author. Only the id is read off it for lastUpdatedById. // `pageId` satisfies the UpdatePageDto type; PageService.update reads the // page id off `page`, not the DTO. Only `title` is applied here. await this.pageService.update( page, - { pageId, title }, + { pageId, title: cleanTitle }, { id: ctx.userId } as any, GIT_SYNC_PROVENANCE, );