Files
outline-sync/SYNC_PRD.md
2026-03-07 20:54:59 +01:00

32 KiB

PRD: Outline ↔ Obsidian Sync

Version: 0.3 Date: 2026-03-04 Status: Revised — Git + Frontmatter Architecture


1. Problem Statement

The user maintains a knowledge base in Outline (web) and wants to edit it locally in Obsidian. Changes must flow in both directions. The existing export/import scripts provide one-way, full-replacement operations — insufficient for safe bidirectional sync where both sides accumulate independent changes between syncs.

This is not a shared git repository. Git is used purely as a local sync engine — for its change detection, three-way merge, and conflict resolution primitives. There is no git remote, no git push to a server, no collaboration workflow.


2. Goals

# Goal
G1 Changes made in Outline are visible in Obsidian after sync
G2 Changes made in Obsidian are pushed to Outline without destroying document history
G3 Conflicts are detected and surfaced — never silently overwritten
G4 Safe to run unattended via Ofelia cron
G5 Outline document URLs remain stable across syncs (no delete+recreate)
G6 Setup is self-contained in a dedicated folder with clear structure

3. Non-Goals

# Non-Goal Reason
NG1 Real-time sync Async workflow; polling is sufficient
NG2 Wikilink ↔ Outline link translation Lossy and fragile
NG3 Syncing attachments/images Separate concern
NG4 Obsidian-only content (Daily Notes, templates) No Outline equivalent
NG5 Multi-user conflict resolution Single-user use case
NG6 A shared/remote git repository Git is local tooling only

4. Folder Setup

4.1 Directory Structure

A dedicated folder is created for the Obsidian vault and sync tooling. It is separate from outline-tools/ (which holds export/import scripts).

/home/crabix/docker_authentik/
├── outline-tools/          ← existing: export + import scripts (unchanged)
└── outline-vault/          ← NEW: Obsidian vault + git sync engine
    ├── .git/               ← local git repo (never pushed to any remote)
    ├── .gitignore
    ├── .gitattributes
    ├── sync.sh             ← main sync entrypoint (bash wrapper)
    ├── outline_sync.py     ← core sync logic
    ├── settings.json       ← API token + config (gitignored)
    ├── _sync_log.md        ← human-readable sync log (readable in Obsidian)
    ├── .obsidian/          ← Obsidian config (gitignored)
    ├── Bewerbungen/        ← Outline collection → top-level folder
    │   ├── CV.md
    │   └── Tipico/         ← nested document → subfolder
    │       ├── Tipico.md
    │       └── Pitch Tipico.md
    └── Projekte/
        └── ...

Obsidian is pointed at /home/crabix/docker_authentik/outline-vault/ as its vault root.

4.2 Git Initialization

Git is initialized locally with two branches. There is no remote.

cd /home/crabix/docker_authentik/outline-vault
git init
git checkout -b outline    # branch: tracks last known Outline state
git checkout -b main       # branch: Obsidian edits live here

Branch semantics:

Branch Purpose
outline Snapshot of Outline at last successful sync. Committed by sync script only. Never edited by hand.
main Working branch. Obsidian edits land here. Obsidian Git plugin auto-commits.

The outline branch tip is the common ancestor for all merges. It replaces any custom state file — no sync_state.json needed.

4.3 .gitignore

# Obsidian internals
.obsidian/

# Sync config (contains API token)
settings.json

# Conflict sidecars (resolved manually, not tracked)
*.conflict.md

# OS
.DS_Store
Thumbs.db

4.4 .gitattributes

# Treat all markdown as text, normalize line endings
*.md text eol=lf

# Tell git to always use union merge on the sync log
# (append-only, never conflicts)
_sync_log.md merge=union

4.5 settings.json (not committed)

{
  "source": {
    "url": "http://outline:3000",
    "token": "ol_api_YOUR_TOKEN_HERE"
  },
  "sync": {
    "vault_dir": "/home/crabix/docker_authentik/outline-vault",
    "enable_deletions": false,
    "auto_push": false
  }
}

5. Frontmatter as the ID Layer

Every synced file carries its Outline identity in YAML frontmatter. This is the bridge between git paths and Outline document IDs.

---
outline_id: abc-123
outline_collection_id: col-456
outline_parent_id: def-789        # omitted if root document
outline_updated_at: 2026-03-03T10:30:00Z
---

# CV

Actual document content here...

Rules:

  • Frontmatter is stripped before sending content to Outline API
  • Frontmatter is written back after each successful API call with updated outline_updated_at
  • Frontmatter travels with the file on rename/move — the Outline ID is never lost
  • New files created in Obsidian have no frontmatter until first push
  • The sync script identifies new files by the absence of outline_id in frontmatter

6. Architecture: Outline as a "Dumb Remote"

Git handles change detection, merging, and conflict marking. The sync script is purely an API translation layer.

git pull equivalent:
  Export Outline → temp dir → convert to frontmatter format
  → git commit to `outline` branch
  → git merge outline into main

git push equivalent:
  git diff outline..main → list of changed files
  → for each file: call appropriate Outline API endpoint
  → write updated frontmatter back
  → advance outline branch tip

6.1 Change Detection Matrix

git diff outline..main --name-status --find-renames produces all cases:

Git status Outline API call
M modified documents.update(id, title, text)
A added (with outline_id in frontmatter) Already synced — skip
A added (no outline_id) documents.create(collectionId, title, text) → write frontmatter
D deleted documents.delete(id) — only if enable_deletions: true
R renamed/moved documents.update(id, title) + documents.move(id, newParentId) if folder changed

6.2 New Outline API Calls Required

Existing scripts only use documents.create. Sync additionally requires:

Endpoint Used for
documents.update Core push operation — preserves history
documents.move Reparent when file moved between folders
documents.delete Deletion (flag-gated)
documents.list Detect remote changes during pull
collections.list Detect new/deleted collections during pull
collections.create Create collection for new top-level folder
collections.delete Delete collection (flag-gated)

7. Sync Commands

sync.sh init

First-time setup. Creates vault from current Outline state.

./sync.sh init

Steps:

  1. Run existing outline_export_fixed.py into temp dir
  2. git init + create outline and main branches
  3. Convert export metadata headers → YAML frontmatter in each .md file
  4. Commit to outline branch: git commit -m "sync: initial import"
  5. Checkout main, merge outline
  6. Write .gitignore, .gitattributes

sync.sh pull

Pull latest Outline state into Obsidian.

./sync.sh pull [--auto]

Steps:

  1. Export Outline → temp dir
  2. Convert to frontmatter format
  3. Checkout outline branch, apply changes, commit: git commit -m "sync: outline@TIMESTAMP"
  4. Checkout main
  5. git merge outline
  6. If conflict: write *.conflict.md sidecars, append to _sync_log.md, exit non-zero
  7. If clean: append success to _sync_log.md

sync.sh push

Push local Obsidian changes to Outline.

./sync.sh push [--dry-run]

Steps:

  1. Check for unresolved merge conflicts — abort if any
  2. git diff outline..main --name-status --find-renames
  3. For each changed file: call Outline API (topological order for new docs)
  4. Write updated frontmatter, git add the file
  5. On full success: git checkout outline && git merge main
  6. On partial failure: log errors, do not advance outline branch — next push retries failed files

sync.sh (bidirectional)

Pull, then push if pull was clean.

./sync.sh [--dry-run] [--auto]

sync.sh status

Show pending local changes not yet in Outline.

./sync.sh status

Output: git diff outline..main --name-status formatted as human-readable table.

sync.sh resolve FILE --accept [local|remote]

Resolve a merge conflict.

./sync.sh resolve Bewerbungen/CV.md --accept local
./sync.sh resolve Bewerbungen/CV.md --accept remote

8. Conflict Handling

A conflict occurs when git merge outline produces conflict markers (same document edited on both sides since last sync).

Bewerbungen/CV.md           ← contains git conflict markers, NOT pushed
Bewerbungen/CV.conflict.md  ← human-readable: what changed locally vs remotely

_sync_log.md always reflects current sync state (readable in Obsidian):

## 2026-03-04 08:00
- pull: 3 updated, 1 CONFLICT (Bewerbungen/CV.md) — push blocked
- push: blocked pending conflict resolution

9. Scheduled Sync via Ofelia

[job-exec "outline-pull"]
schedule  = @every 1h
container = outline-sync
command   = /work/sync.sh pull --auto

# Push is manual by default. Enable only after P6 (conflict detection) is complete.
# [job-exec "outline-push"]
# schedule  = @daily
# container = outline-sync
# command   = /work/sync.sh push --auto

--auto flag: suppresses interactive prompts, writes to _sync_log.md, exits non-zero on conflict so Ofelia can detect failures.


10. Implementation Phases + User Stories


Phase 1 — sync init

Scope: Create vault from current Outline state. Git initialized. Frontmatter injected.

User Stories

US-1.1 — As a user, I want to run a single command that sets up the vault from scratch, so that I don't need to manually configure git or copy files.

  • Given: outline-vault/ does not exist (or is empty)
  • When: ./sync.sh init is run
  • Then: The vault folder is created, git is initialized with outline and main branches, all Outline documents are exported as .md files with frontmatter, and main is checked out

US-1.2 — As a user, I want every exported document to have frontmatter with its Outline ID, so that future syncs can identify documents without an external map.

  • Given: A document exists in Outline with id abc-123
  • When: sync init completes
  • Then: The corresponding .md file contains outline_id: abc-123 in its YAML frontmatter

US-1.3 — As a user, I want the vault folder structure to mirror Outline's collection/document hierarchy, so that navigation in Obsidian feels natural.

  • Given: Outline has collection "Bewerbungen" with nested document "Tipico > Pitch"
  • When: sync init completes
  • Then: Files exist at Bewerbungen/Tipico/Tipico.md and Bewerbungen/Tipico/Pitch.md

US-1.4 — As a user, I want settings.json to be gitignored automatically, so that my API token is never accidentally committed.

  • Given: sync init runs
  • Then: .gitignore is created containing settings.json and *.conflict.md

Phase 2 — sync pull

Scope: Export Outline → commit to outline branch → merge into main.

User Stories

US-2.1 — As a user, I want to run sync pull to get the latest Outline changes into Obsidian, so that edits made on the web are reflected locally.

  • Given: A document was updated in Outline since last sync
  • When: ./sync.sh pull is run
  • Then: The corresponding .md file is updated in the vault with the new content and updated outline_updated_at frontmatter

US-2.2 — As a user, I want new Outline documents to appear as new files after pull, so that I don't miss newly created content.

  • Given: A new document was created in Outline since last sync
  • When: ./sync.sh pull is run
  • Then: A new .md file exists with correct frontmatter, in the correct folder

US-2.3 — As a user, I want deleted Outline documents to be removed locally after pull, so that my vault stays in sync with Outline.

  • Given: A document was deleted in Outline since last sync
  • When: ./sync.sh pull is run
  • Then: The corresponding local .md file is removed

US-2.4 — As a user, I want to be informed if a pull produces a conflict (same document edited both locally and in Outline), so that I can resolve it manually.

  • Given: CV.md was edited locally AND in Outline since last sync
  • When: ./sync.sh pull is run
  • Then: A CV.conflict.md sidecar is created, _sync_log.md records the conflict, the command exits non-zero, and CV.md is left untouched

US-2.5 — As a user running pull in --auto mode (cron), I want the sync log to be updated even on failure, so that I can check _sync_log.md in Obsidian to understand what happened.

  • Given: ./sync.sh pull --auto runs via Ofelia and encounters a conflict
  • Then: _sync_log.md is updated with timestamp and conflict details, exit code is non-zero

Phase 3 — sync status

Scope: Show pending local changes not yet pushed to Outline.

User Stories

US-3.1 — As a user, I want to see which local files have changed since last sync, so that I know what will be pushed before running push.

  • Given: CV.md was edited locally and not yet pushed
  • When: ./sync.sh status is run
  • Then: Output shows M Bewerbungen/CV.md (modified)

US-3.2 — As a user, I want status to also show new files that will be created in Outline on push, so that I can review before committing.

  • Given: NewNote.md was created in Obsidian with no frontmatter
  • When: ./sync.sh status is run
  • Then: Output shows A Bewerbungen/NewNote.md (new → will create in Outline)

US-3.3 — As a user, I want status to exit cleanly when there are no pending changes, so that cron jobs don't produce false alarms.

  • Given: No local changes since last sync
  • When: ./sync.sh status is run
  • Then: Output is "Nothing to push. Outline is up to date." and exit code is 0

Phase 4 — sync push (modify + rename)

Scope: Push modified and renamed/moved files to Outline. No new doc creation yet.

User Stories

US-4.1 — As a user, I want editing a file in Obsidian and running push to update the document in Outline, so that my local edits appear on the web.

  • Given: CV.md was edited in Obsidian (has outline_id in frontmatter)
  • When: ./sync.sh push is run
  • Then: Outline document abc-123 is updated with new content, document history is preserved (not recreated), outline_updated_at in frontmatter is updated

US-4.2 — As a user, I want renaming a file in Obsidian to update the document title in Outline, so that both sides stay consistent.

  • Given: CV.md is renamed to Lebenslauf.md in Obsidian (frontmatter outline_id intact)
  • When: ./sync.sh push is run
  • Then: Outline document title is updated to "Lebenslauf", document URL is preserved

US-4.3 — As a user, I want moving a file to a different folder in Obsidian to reparent the document in Outline, so that the hierarchy stays consistent.

  • Given: Bewerbungen/CV.md is moved to Projekte/CV.md in Obsidian
  • When: ./sync.sh push is run
  • Then: In Outline, the document is moved to the "Projekte" collection with the new parent

US-4.4 — As a user, I want push to be atomic per-document — a failure on one file should not block others.

  • Given: Push is run with 5 changed files, one API call fails
  • When: ./sync.sh push completes
  • Then: 4 documents are updated in Outline, 1 failure is logged, the outline branch is NOT advanced (failed file will retry on next push)

US-4.5 — As a user, I want --dry-run to show me exactly what API calls would be made without touching Outline.

  • Given: 3 files modified locally
  • When: ./sync.sh push --dry-run
  • Then: Output lists each file and the API call that would be made, no changes to Outline or git

Phase 5 — sync push (new documents)

Scope: Create new Outline documents from new Obsidian files.

User Stories

US-5.1 — As a user, I want creating a new .md file in an existing collection folder to create a new document in Outline on push.

  • Given: Bewerbungen/NewApplication.md created in Obsidian, no frontmatter
  • When: ./sync.sh push is run
  • Then: Document created in Outline under "Bewerbungen" collection, frontmatter written back with new outline_id

US-5.2 — As a user, I want creating a file in a subfolder to correctly set the parent document in Outline, preserving hierarchy.

  • Given: Bewerbungen/Tipico/NewDoc.md created, Tipico.md has outline_id: def-789
  • When: ./sync.sh push is run
  • Then: Outline document created with parentDocumentId: def-789

US-5.3 — As a user, I want creating a new top-level folder to create a new Outline collection on push.

  • Given: NewCollection/FirstDoc.md created in Obsidian
  • When: ./sync.sh push is run
  • Then: New collection "NewCollection" is created in Outline, FirstDoc is created inside it

US-5.4 — As a user, I want parent documents to always be created before their children, even if I created them in reverse order.

  • Given: Bewerbungen/Tipico/NewChild.md created, but Bewerbungen/Tipico/Tipico.md is also new
  • When: ./sync.sh push is run
  • Then: Tipico.md is created first, then NewChild.md with correct parentDocumentId

Phase 6 — Conflict Detection

Scope: Block push when unresolved conflicts exist. Generate conflict sidecars.

User Stories

US-6.1 — As a user, I want push to be blocked if there are unresolved merge conflicts, so that I never push broken content.

  • Given: CV.md contains git conflict markers from a previous pull
  • When: ./sync.sh push is run
  • Then: Push aborts with "Resolve conflicts before pushing", lists conflicted files, exits non-zero

US-6.2 — As a user, I want a human-readable conflict sidecar file, so that I can understand what changed on each side.

  • Given: CV.md conflicts during pull
  • When: Pull completes
  • Then: CV.conflict.md exists containing: last synced content, local changes, Outline changes, and instructions for resolution

US-6.3 — As a user, I want non-conflicted files to be pushed successfully even when other files have conflicts.

  • Given: CV.md has conflict, Projekte/Roadmap.md is cleanly modified
  • When: ./sync.sh push is run (after resolving or acknowledging the conflict situation)
  • Then: Only Roadmap.md is pushed; CV.md is skipped with a warning

Phase 7 — sync resolve

Scope: Command to resolve conflicts without needing raw git knowledge.

User Stories

US-7.1 — As a user, I want to resolve a conflict by accepting the local version with a single command.

  • Given: CV.md is in conflict
  • When: ./sync.sh resolve Bewerbungen/CV.md --accept local
  • Then: Conflict markers removed, local content kept, file staged, CV.conflict.md deleted

US-7.2 — As a user, I want to resolve a conflict by accepting the remote (Outline) version with a single command.

  • Given: CV.md is in conflict
  • When: ./sync.sh resolve Bewerbungen/CV.md --accept remote
  • Then: Conflict markers removed, Outline's content kept, file staged, CV.conflict.md deleted

US-7.3 — As a user, I want sync status to clearly show which files are in conflict, so that I know what needs resolving.

  • Given: CV.md and Notes.md are conflicted
  • When: ./sync.sh status
  • Then: Output shows both files marked as CONFLICT with instructions to run sync resolve

Phase 8 — Ofelia + _sync_log.md

Scope: Unattended scheduled pull. Human-readable log in vault.

User Stories

US-8.1 — As a user, I want hourly automatic pulls from Outline so that Obsidian stays current without manual intervention.

  • Given: Ofelia runs sync.sh pull --auto every hour
  • When: New documents were created in Outline during the hour
  • Then: New .md files appear in vault, _sync_log.md records the event

US-8.2 — As a user, I want to check _sync_log.md in Obsidian to see the history of syncs, so that I can diagnose issues without accessing a terminal.

  • Given: Multiple sync runs have occurred
  • When: Opening _sync_log.md in Obsidian
  • Then: A chronological log of pull/push operations, counts of changes, and any errors or conflicts is visible

US-8.3 — As a user, I want failed syncs to be visible in Ofelia's output so that I can be alerted.

  • Given: A pull encounters a conflict
  • When: Ofelia's job completes
  • Then: Exit code is non-zero, Ofelia marks the job as failed

Phase 9 — Deletions (flag-gated)

Scope: Handle deleted files/documents. Off by default.

User Stories

US-9.1 — As a user, I want to opt into deletion sync so that documents I delete locally are also removed from Outline.

  • Given: enable_deletions: true in settings.json, OldNote.md deleted in Obsidian
  • When: ./sync.sh push
  • Then: Outline document is deleted, frontmatter entry removed

US-9.2 — As a user, I want deletions to be off by default so that accidental file deletion never cascades to Outline without explicit opt-in.

  • Given: enable_deletions: false (default), OldNote.md deleted in Obsidian
  • When: ./sync.sh push
  • Then: Deletion is logged as a warning, Outline document is untouched

US-9.3 — As a user, I want --dry-run to show pending deletions so that I can review before enabling.

  • Given: enable_deletions: true, 2 files deleted locally
  • When: ./sync.sh push --dry-run
  • Then: Output shows 2 documents marked for deletion, no Outline changes made

11. Automated Testing

All tests run inside Docker (same python:3.11-slim + domnet network as existing scripts). A dedicated test collection _sync_test_TIMESTAMP is created in Outline at test start and deleted at teardown.

Test Runner

./sync_tests.sh              # run all tests
./sync_tests.sh --phase 1    # run tests for specific phase
./sync_tests.sh --keep       # don't delete test collection on failure (debug)
./sync_tests.sh --verbose    # show full API request/response

11.1 Phase 1 Tests — Init

TEST-1.1  init creates vault directory if not exists
TEST-1.2  init creates git repo with outline and main branches
TEST-1.3  init exports all Outline collections as top-level folders
TEST-1.4  init injects frontmatter into every .md file
TEST-1.5  frontmatter contains: outline_id, outline_collection_id, outline_updated_at
TEST-1.6  outline_id in frontmatter matches actual Outline document ID (API verified)
TEST-1.7  nested documents are in correct subfolder matching hierarchy
TEST-1.8  settings.json is listed in .gitignore
TEST-1.9  .obsidian/ is listed in .gitignore
TEST-1.10 outline branch and main branch are at same commit after init
TEST-1.11 re-running init on existing vault aborts with clear error message

11.2 Phase 2 Tests — Pull (Outline → Local)

Read direction:

TEST-2.1  pull: modified Outline document updates local .md content
TEST-2.2  pull: modified Outline document updates outline_updated_at in frontmatter
TEST-2.3  pull: outline_id in frontmatter unchanged after content update
TEST-2.4  pull: new Outline document appears as new .md file in correct folder
TEST-2.5  pull: new nested Outline document appears in correct subfolder
TEST-2.6  pull: new Outline collection appears as new top-level folder
TEST-2.7  pull: deleted Outline document removes local .md file
TEST-2.8  pull: deleted Outline collection removes local folder
TEST-2.9  pull: unchanged documents are not modified (mtime unchanged)
TEST-2.10 pull: git commits to outline branch, not main
TEST-2.11 pull: outline branch advances, main branch merges cleanly
TEST-2.12 pull: _sync_log.md updated with counts and timestamp

Conflict detection:

TEST-2.13 pull: same document edited locally + in Outline → conflict detected
TEST-2.14 pull: conflict produces *.conflict.md sidecar file
TEST-2.15 pull: conflict sidecar contains local diff and remote diff sections
TEST-2.16 pull: conflicted file is left untouched (not overwritten)
TEST-2.17 pull: non-conflicted files merge successfully despite other conflicts
TEST-2.18 pull: --auto mode exits non-zero on conflict
TEST-2.19 pull: _sync_log.md records conflict with filename and timestamp

11.3 Phase 3 Tests — Status

TEST-3.1  status: shows M for locally modified file
TEST-3.2  status: shows A for new local file without outline_id
TEST-3.3  status: shows D for locally deleted file (even if deletions off)
TEST-3.4  status: shows R for renamed/moved file
TEST-3.5  status: shows CONFLICT for files with unresolved merge conflicts
TEST-3.6  status: shows nothing when vault is clean
TEST-3.7  status: exit code 0 in all cases (informational only)
TEST-3.8  status: does not modify any files or git state

11.4 Phase 4 Tests — Push Modified + Renamed

Update direction:

TEST-4.1  push: modified local file calls documents.update (not create)
TEST-4.2  push: Outline document content matches local file content after push (strip frontmatter)
TEST-4.3  push: documents.update preserves document history (version count increases)
TEST-4.4  push: outline_updated_at in frontmatter updated to API response timestamp
TEST-4.5  push: outline_id unchanged after update
TEST-4.6  push: Outline document URL unchanged after update (ID preserved)
TEST-4.7  push: renamed file → Outline document title updated
TEST-4.8  push: renamed file → Outline document ID unchanged
TEST-4.9  push: file moved to different folder → documents.move called
TEST-4.10 push: moved file → document appears under new parent in Outline
TEST-4.11 push: moved file → document removed from old parent in Outline
TEST-4.12 push: file moved to different top-level folder → document moved to different collection
TEST-4.13 push: API failure on one file → other files still pushed
TEST-4.14 push: failed file → outline branch NOT advanced
TEST-4.15 push: failed file → retried on next push
TEST-4.16 push: --dry-run → no Outline changes, no git state changes
TEST-4.17 push: --dry-run → lists all API calls that would be made
TEST-4.18 push: frontmatter stripped from content sent to Outline API
TEST-4.19 push: Outline document does NOT contain frontmatter YAML in its body

11.5 Phase 5 Tests — Push New Documents

TEST-5.1  push: new file in existing collection folder → documents.create called
TEST-5.2  push: new document appears in correct Outline collection
TEST-5.3  push: outline_id written back to frontmatter after create
TEST-5.4  push: outline_collection_id written back to frontmatter after create
TEST-5.5  push: new file in subfolder → parentDocumentId set correctly
TEST-5.6  push: parent document outline_id used as parentDocumentId
TEST-5.7  push: new file in new top-level folder → collection created first
TEST-5.8  push: new file in new top-level folder → document created after collection
TEST-5.9  push: two new files parent+child → parent created before child (topological order)
TEST-5.10 push: three-level new hierarchy → correct creation order preserved
TEST-5.11 push: new file with no frontmatter → document still created (title from filename)
TEST-5.12 push: created document is published (not draft)

11.6 Phase 6 Tests — Conflict Detection

TEST-6.1  push blocked when any file contains git conflict markers
TEST-6.2  push blocked lists all conflicted files
TEST-6.3  push blocked exits non-zero
TEST-6.4  non-conflicted files can still be pushed despite other files in conflict
TEST-6.5  conflict sidecar .conflict.md contains "LOCAL" and "REMOTE" sections
TEST-6.6  conflict sidecar contains instructions for resolution commands
TEST-6.7  outline branch not advanced when push is blocked by conflicts

11.7 Phase 7 Tests — Resolve

TEST-7.1  resolve --accept local → file contains local content
TEST-7.2  resolve --accept local → no conflict markers remain
TEST-7.3  resolve --accept local → .conflict.md sidecar deleted
TEST-7.4  resolve --accept local → file is staged (git add)
TEST-7.5  resolve --accept remote → file contains Outline content
TEST-7.6  resolve --accept remote → no conflict markers remain
TEST-7.7  resolve --accept remote → .conflict.md sidecar deleted
TEST-7.8  resolve --accept remote → file is staged (git add)
TEST-7.9  resolve on non-conflicted file → error message, no changes
TEST-7.10 after resolve, push succeeds for that file

11.8 Phase 8 Tests — Ofelia / Auto mode

TEST-8.1  --auto flag: no interactive prompts, completes without stdin
TEST-8.2  --auto flag: _sync_log.md updated on success
TEST-8.3  --auto flag: _sync_log.md updated on conflict
TEST-8.4  --auto flag: exit code 0 on clean pull
TEST-8.5  --auto flag: exit code non-zero on conflict
TEST-8.6  _sync_log.md uses union merge strategy (no conflicts on the log itself)
TEST-8.7  _sync_log.md entries are append-only (history preserved)

11.9 Phase 9 Tests — Deletions

TEST-9.1  enable_deletions false: locally deleted file → no Outline change
TEST-9.2  enable_deletions false: warning logged in _sync_log.md
TEST-9.3  enable_deletions true: locally deleted file → documents.delete called
TEST-9.4  enable_deletions true: Outline document is gone after push
TEST-9.5  enable_deletions true: deleted folder → collection deleted after all docs deleted
TEST-9.6  enable_deletions true: --dry-run lists pending deletions without executing
TEST-9.7  Outline-side deletion during pull: local file removed (regardless of enable_deletions)
TEST-9.8  Outline-side deletion during pull: deletion logged in _sync_log.md

11.10 Full Round-Trip Tests

These run the complete cycle and verify end-to-end consistency.

TEST-RT.1  Create in Outline → pull → verify local file content + frontmatter
TEST-RT.2  Create in Obsidian → push → verify Outline document content + ID in frontmatter
TEST-RT.3  Edit in Outline → pull → edit locally → push → verify Outline has latest content
TEST-RT.4  Edit locally → push → edit in Outline → pull → verify local has Outline content
TEST-RT.5  Edit same doc in Outline AND locally → pull → verify conflict detected → resolve local → push → verify Outline updated
TEST-RT.6  Edit same doc in Outline AND locally → pull → verify conflict detected → resolve remote → push → verify Outline unchanged, local matches Outline
TEST-RT.7  Rename locally → push → pull (clean) → verify no duplicate documents
TEST-RT.8  Move locally → push → pull (clean) → verify hierarchy correct on both sides
TEST-RT.9  Delete locally (deletions on) → push → verify gone from Outline
TEST-RT.10 Delete in Outline → pull → verify gone locally
TEST-RT.11 Create parent+child locally → push → verify parent-child relationship in Outline
TEST-RT.12 Full CRUD cycle: create → edit → rename → move → delete, verify Outline matches at each step

12. What Is NOT Written from Scratch

Problem Solution
Change detection git diff
Three-way merge git merge
Conflict markers git native
History + rollback git log / git reset
Local auto-commit Obsidian Git plugin
API retry + rate limiting Reuse OutlineImporter._api_request() from outline_import.py
Docker network execution Reuse pattern from import_to_outline.sh
Export logic Reuse outline_export_fixed.py unchanged

New code only: outline_sync.py (~500 lines) + sync.sh wrapper (~100 lines).


13. Risks

Risk Mitigation
git merge fails in unattended cron Pull conflicts block push only; clean files still merged; logged to _sync_log.md
Frontmatter stripped incorrectly before push Unit test the strip/restore; verify Outline body does not contain YAML
outline_updated_at clock skew Use API response timestamp, not local clock; normalize to UTC
New file in unknown folder (not an Outline collection) Warn and skip; require top-level folder to match existing collection or be brand new
Outline deletes document between pull and push documents.update returns 404 → log as conflict, skip
Obsidian Git plugin commits during sync Sync script checks for clean working tree before starting; aborts if dirty
outline branch diverges from main after long conflict sync status shows divergence; sync pull always catches up