Complete the push action coverage (create/update/delete/move/rename/noop).
- push.ts classifyRenameMoves (pure): the file PATH is the source of truth for
tree position (§5) — new parent resolved from the enclosing folder's <dir>.md
page, not the stale meta.parentPageId. Emit move iff parent changed, rename iff
meta.title changed; a pure path-only rename is a NOOP (no Docmost call — the
path is local, identity is pageId)
- applyPushActions: move (move_page, reparent) THEN rename (rename_page); noop
records and calls nothing; per-page isolation + refs-only-on-success preserved
- resolveParentPageId reads <dir>.md meta via readFile (current) /
git.showFileAtRef(last-pushed) (prev), matching buildVaultLayout
- review fixes: prefetch wrapped in per-page try/catch so a tree-read throw
isolates one page (§12), not the batch; failures.kind attributes the op that
actually threw (rename-after-move -> "rename")
- tests (+13): classifier (move/rename/both/noop/to-root), apply (calls/no-calls,
ordering, isolation); 724 -> 737 green (x2 stable); corpus STABLE
Deferred (final increment): live main() daemon, FS-watcher/debounce (§7.1),
git-remote push (§7.2), pull-side bodyHash/updatedAt consumption, fractional-index
position, escalate-on-divergent-docmost.
- git.ts: fastForwardBranch(branch, toCommit) — advances ONLY on a true
fast-forward (merge-base --is-ancestor), refuses a non-ff without clobbering
divergent docmost history
- push.ts: after a CLEAN push (failures===0) advance both refs/docmost/last-pushed
AND fast-forward the docmost mirror, so the next pull sees no diff for pushed
pages (loop-guard, git-native); a partial push advances NEITHER (§12)
- push.ts: per-page error isolation (one bad page doesn't block the batch,
failures recorded); create requires a non-empty spaceId else skipped (§8 spirit)
- loop-guard.ts: bodyHash() (sha256) + per-page pushed:[{pageId,updatedAt?,bodyHash}]
record for the §10 self-write suppression (pull-side consumption deferred)
- test: markdown-roundtrip property tests get a 30s per-test timeout (deterministic
inputs via fixed seed; the only flakiness was wall-clock under parallel load,
which intermittently failed CI/docker)
- 709 -> 724 green (3x stable); build clean; corpus STABLE
Deferred (next/final increment): move/rename apply, pull-side loop-guard consumption,
FS-watcher/debounce (§7.1), git-remote push (§7.2), runnable live main(),
escalate-on-divergent-docmost.
First slice of the push direction (SPEC §6), mirroring pull: VaultGit primitives +
pure planner + thin injectable apply, exercised via fakes (no live destructive run).
- git.ts: diffNameStatus (--name-status -M -z, NUL-parsed, rename-aware),
revParse/readRef/updateRef (refs/docmost/last-pushed), showFileAtRef (recover a
deleted file's pre-image pageId)
- push.ts computePushActions (pure): A/M/D/R -> create/update/delete/renamesMoves;
delete only when pageId is recovered from the pre-image, else skipped (§8 guard —
no spurious Docmost delete)
- push.ts applyPushActions (fakes): update via importPageMarkdown (collab/Yjs path,
§2 — never a raw jsonb overwrite); create via createPage then write the assigned
pageId back into the file meta (body preserved); delete via deletePage (soft, §8);
renamesMoves deferred; advances last-pushed
- tests (+26): diffNameStatus A/M/D/rename, ref round-trip, showFileAtRef; pure
classification incl. §8 no-pageid skip; apply with fakes (collab-path update,
pageid write-back, soft-delete, deferred moves)
- 683 -> 709 green; build clean; corpus STABLE
Deferred (next increment): move/rename apply, loop-guard (§10), watcher/debounce,
remote push, live main wiring, empty-spaceId create guard, per-page error isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-preserving refactors (R-Collab-1, R-Pull-1, R-Pull-2) to unblock testing,
plus the integration tests they enable.
- collaboration: extract applyTransformToYdoc from onSynced; onSynced stays
synchronous (NO await between Yjs read and write — SPEC §2 atomicity preserved)
- pull: readExisting(deps) injectable IO; split main into pure computePullActions
(plan + suppression/mass-delete decisions) + thin applyPullActions(deps) (IO);
ordering and data-loss guards preserved bit-for-bit
- tests (+35): collaboration-apply (atomicity/null-abort/throw-no-partial),
read-existing, compute/apply-pull-actions (move-write-fail keeps old path),
git temp-repo 3-way non-FF merge
- transforms-extra property: constrain the generator to mutually-non-substring
words (the domain where the renumber property holds) -> deterministic; document
the inherited commentsToFootnotes substring-overlap comment-drop via it.fails
(off the sync path, SPEC §3; backport-fix lives in docmost-mcp)
- 695 -> 731 green; build clean; corpus STABLE
Address a code review of the git-hardening changes.
- single runRaw primitive: every git invocation funnels through it; run() is a
thin throw+trim wrapper; the two direct execFileAsync bypasses (commitRaw,
assertGitAvailable) removed; one unified error format
- `-c core.quotepath=false` is now the argv baseline for ALL commands (was only
listTrackedFiles) — removes the latent quoting asymmetry on ls-files -u /
diff --name-only; persisted LOCAL config (autocrlf/safecrlf/gpgsign/
attributesFile) kept as-is in ensureRepo
- preserve spawn-error message (ENOENT): use `||` not `??` (promisified execFile
sets stderr to "" on spawn failure)
- contextual error when pinning vault git config; module/vaultGitEnv docs corrected
- README: require a system git binary on PATH for local runs
- tests: --no-verify honored (failing pre-commit hook), vaultGitEnv pins,
core.attributesFile=/dev/null neutralization (593 green)
Address git-integration fragility (output is not parsed for control flow; we rely
on exit codes + plumbing — but porcelain BEHAVIOR is config-sensitive, and the
runtime image lacked git).
- listTrackedFiles: `git -c core.quotepath=false ls-files -z` + NUL split — fixes
Cyrillic/UTF-8 vault filenames being returned octal-escaped/quoted
- Dockerfile: install git (node:22-slim ships none; the daemon shells out at runtime)
- VaultGit env: LC_ALL=C/LANG=C, GIT_PAGER=cat, GIT_TERMINAL_PROMPT=0; keep
stripping GIT_DIR/GIT_WORK_TREE (cwd-isolation, §12)
- ensureRepo local config: core.autocrlf=false + core.safecrlf=false (protect §11
byte-stability from a global autocrlf=true), commit.gpgsign=false, and
core.attributesFile=/dev/null (neutralize a global clean/smudge filter that
would rewrite the stored blob); commit uses --no-verify (skip injected hooks)
- assertGitAvailable() preflight: clear error if the git binary is missing
- tests: Cyrillic listTrackedFiles, LF byte-preservation of the stored blob,
local-config neutralization incl. attributesFile (590+ green)
Turn the read-only mirror into a git-backed pull cycle. Read-only toward Docmost.
- git.ts (VaultGit): system-git wrapper, all ops cwd=vaultPath (vault is its own
repo under data/vault, never the source repo); ensureRepo/branches main+docmost,
commit with provenance (author/committer identity + Docmost-Sync-Source trailer,
§7.3), merge with conflict surfacing (no auto-resolve, §9), isMergeInProgress;
GIT_DIR/GIT_WORK_TREE stripped from env (§12 cwd isolation)
- stabilize.ts: normalize-on-write (one export->import->export fixpoint pass, §11)
- reconcile.ts: pure planReconciliation (add/update/move/delete by pageId) +
decideAbsenceDeletions gate
- pull.ts: write/commit on docmost -> merge into main; listSpaceTree completeness
signal suppresses absence-deletions on a partial fetch (§8); mass-delete guard;
merge-in-progress guard makes re-runs converge (§12); move old-path removal only
on successful write
- docmost-client: listSpaceTree({pages, complete}) without touching the 1:1-copied
enumerateSpacePages
- tests: reconcile planner + decideAbsenceDeletions, VaultGit incl. real temp-repo
merge conflict, listSpaceTree completeness (586 green)
Push to a git remote and the FS->Docmost direction are deferred to the next increment.
Close Задача №0 (SPEC §11) with the spec-sanctioned option (b): compare a
canonicalized ProseMirror form instead of raw bytes.
- canonicalize.ts: canonicalizeContent/docsCanonicallyEqual — strip node attrs.id,
drop null/undefined attrs, and drop attrs equal to their type's known non-null
schema default (KNOWN_DEFAULTS: link target/rel, comment.resolved, orderedList.start,
diagram/media align) so "absent" ≡ "default"; comment anchors + meaningful attrs kept
- roundtrip.ts: assert markdown byte-stability AND canonical stability; add --corpus
mode and mutually-exclusive-flag warning
- synthetic corpus (headings, marks, lists, table, callout, code w/ trailing \n,
diagrams, textStyle/mention) + canonicalize/corpus tests (558 green)
- known converter asymmetries (block image after paragraph; embed width/height
coercion) converge to a fixpoint after one export->import pass -> handled by
normalize-on-write at vault-write time; isolated under it.fails
- SPEC §11: record the resolution and normalize-on-write strategy
Add documentation of the monorepo layout, describing the `docmost-client` and `sync` packages and their responsibilities. Clarify that all Docmost access is performed via REST for reads, while writes use collab/Yjs, documenting the architectural decision and its rationale.
Set up the project structure per the new-project guide, adapted from the
Python skeleton to the Node/TS stack fixed in SPEC.md (reuses docmost-mcp).
Scaffold only — the sync engine is not implemented yet.
- src/settings.ts: single config layer on zod, schema keyed by real ENV
names; credentials and own-service address have no default (fail fast).
- src/config-errors.ts: loadSettingsOrExit — clear startup message naming
the missing/invalid env var instead of a raw stack trace; exit(1).
- src/index.ts: thin entry point that validates config and logs (stub).
- test/: vitest unit tests for settings parsing and config errors (10 tests).
- Makefile (install/env/build/test/run/dev/clean), strict tsconfig, vitest.
- Dockerfile (single-stage, no EXPOSE, prunes dev deps), docker-compose
(daemon, volume on /app/data, watchtower), ghcr CI with build needs test.
- .env.example, .gitignore/.dockerignore, AGENTS.md, README.md.
- Pinned deps (dotenv, zod) + committed package-lock.json.
Research Docmost source (docmost/docmost@main) to close the last four §12 items.
- auth: dedicated service user; API-key (EE/Cloud) vs login JWT (OSS, 90d default,
no refresh, session-bound) with "401 -> re-login"
- position: reuse `fractional-indexing-jittered` generateJitteredKeyBetween,
siblings via /sidebar-pages, compare as raw bytes (COLLATE "C")
- initial clone: canonical = sidebar-pages walk + /info via our converter;
spaces/export (turndown markdown, no meta/anchors) is bootstrap-only, not baseline
- attachments: v1 keeps them as links (out of scope), includeAttachments flag noted
- §16: add auth/JWT/API-key facts, /sidebar-pages, bulk export endpoints,
extra gotchas (collation, throttling, EE-only API keys)
- park "REST vs direct Postgres" as the sole remaining open question
Research Docmost source (docmost/docmost@main) to pin real REST endpoints
and close all five §12 TODOs.
- §6: replace MCP `list_pages` polling with the real "changes since T"
mechanism — `POST /api/pages/recent` (updatedAt DESC, cursor) + client cutoff
- §8: concrete trash/restore endpoints (per-space `trash`, `restore`),
auto-purge note and `permanentlyDelete` guard
- §10: add `lastUpdatedById` loop-guard signal
- §12: turn open questions into decisions (trash/restore, changes-since-T,
commit-attribution trailer, filename collisions, long-offline reconciliation);
add a new list of genuinely-open items
- §15/§16: add the confirmed Docmost REST map (auth, info, recent, create,
update, move, move-to-space, delete, trash, restore) with gotchas
- fix a nested-list markdown glitch in §12