Skip to content

Commit c46f642

Browse files
committed
add ARCHITECTURE.md
1 parent 2791cf1 commit c46f642

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Architecture
2+
3+
A macOS CLI tool that exports Apple Notes to a Git repository as Markdown files, with optional Google Drive sync via rclone. Single-run binary — scheduling is handled externally via launchd.
4+
5+
## Package Map
6+
7+
```
8+
cmd/apple-notes-sync/main.go Cobra CLI entrypoint, dependency wiring
9+
internal/
10+
model/model.go Domain types: Note, Folder, Attachment, SyncResult
11+
shell/command.go CommandExecutor interface — the single mock seam for all subprocesses
12+
config/config.go Config loading via viper (CLI flags > ENV > YAML > defaults)
13+
logging/logger.go Zap logger factory (NewLogger)
14+
applescript/
15+
scripts/*.applescript AppleScript files embedded via go:embed
16+
parser.go Parses delimiter-separated osascript output into structs
17+
extractor.go NoteExtractor interface + AppleScriptExtractor implementation
18+
converter/converter.go MarkdownConverter interface — HTML to Markdown via html-to-markdown/v2
19+
filesystem/writer.go NoteWriter interface — writes .md files, saves attachments, cleans orphans
20+
gitops/git.go GitClient interface — init, add, commit, push via shell
21+
rclone/sync.go Syncer interface — rclone availability check + sync
22+
syncer/syncer.go Orchestrator — runs the full sync pipeline
23+
```
24+
25+
## Data Flow
26+
27+
```
28+
AppleScript (osascript)
29+
30+
31+
Parser (delimiter protocol → []model.Note with attachment metadata)
32+
33+
34+
ResolveAttachments (walks ~/Library/Group Containers/group.com.apple.notes/ to read attachment files)
35+
36+
37+
Filters (exclude folders, accounts, protected, shared)
38+
39+
40+
Converter (HTML → Markdown)
41+
42+
43+
Writer (Markdown files to disk + attachments to _attachments/)
44+
45+
46+
Git (add, commit, push)
47+
48+
49+
Rclone (sync to Google Drive)
50+
```
51+
52+
## Interfaces
53+
54+
| Interface | Package | Implementation | Purpose |
55+
|-----------|---------|----------------|---------|
56+
| `CommandExecutor` | `shell` | `OSCommandExecutor` | Runs subprocesses (osascript, git, rclone) |
57+
| `NoteExtractor` | `applescript` | `AppleScriptExtractor` | Extracts notes/folders, resolves attachments |
58+
| `MarkdownConverter` | `converter` | `HTMLToMDConverter` | Converts Apple Notes HTML to Markdown |
59+
| `NoteWriter` | `filesystem` | `FSNoteWriter` | Writes notes/attachments to disk, cleans orphans |
60+
| `GitClient` | `gitops` | `ShellGitClient` | Git operations via subprocess |
61+
| `Syncer` (rclone) | `rclone` | `RcloneSyncer` | Rclone availability check + sync |
62+
63+
## Key Design Decisions
64+
65+
### Delimiter protocol for AppleScript output
66+
AppleScript has poor JSON support. Notes are output as fields separated by `|||FIELD|||` with records separated by `|||NOTE|||`. Attachments within a note use `|||ATTACH|||` and `|||AFIELD|||` sub-delimiters. The parser (`parser.go`) handles all delimiter splitting and date parsing.
67+
68+
### CommandExecutor as single mock seam
69+
All external process calls (osascript, git, rclone) go through `shell.CommandExecutor`. Tests mock this one interface to control subprocess behavior. The syncer tests mock the higher-level interfaces (NoteExtractor, NoteWriter, etc.) instead.
70+
71+
### go:embed for AppleScript files
72+
Scripts live in `internal/applescript/scripts/` and are embedded into the binary via `//go:embed scripts`. No external file dependencies at runtime.
73+
74+
### Attachment resolution
75+
Attachments are extracted in two phases:
76+
1. **AppleScript** outputs attachment metadata (name + content identifier) per note
77+
2. **ResolveAttachments** walks `~/Library/Group Containers/group.com.apple.notes/` to build a filename→filepath index, then reads matching files. Content identifier is used for disambiguation when filenames collide.
78+
79+
### Real filesystem in writer tests
80+
`filesystem` tests use `t.TempDir()` for real filesystem operations rather than mocking the FS. This catches real path handling issues.
81+
82+
### Note file format
83+
Each `.md` file has: `# Title` heading at top, body content, then a `---` divider with a metadata table (ID, Created, Modified, Account, Shared) at the bottom. Configurable via `front_matter` setting.
84+
85+
## Configuration
86+
87+
Precedence: CLI flags > ENV vars (`ANS_` prefix) > YAML config (`~/.apple-notes-sync.yaml`) > defaults.
88+
89+
Key config: `repo_path` (required), `git.enabled/push`, `rclone.enabled`, `attachments.enabled/max_size_mb`, `filter.*`, `clean_orphans`, `dry_run`, `front_matter`.
90+
91+
See `configs/config.example.yaml` for full reference.
92+
93+
## Test Strategy
94+
95+
| Package | Approach |
96+
|---------|----------|
97+
| `shell` | Real subprocess calls (`echo`, `ls`) |
98+
| `config` | Temp YAML files via `t.TempDir()` |
99+
| `applescript/parser` | Pure functions, no mocks |
100+
| `applescript/extractor` | Mock `CommandExecutor` |
101+
| `converter` | Real html-to-markdown library |
102+
| `filesystem` | Real FS via `t.TempDir()` |
103+
| `gitops` | Mock `CommandExecutor` |
104+
| `rclone` | Mock `CommandExecutor` |
105+
| `syncer` | Mock all interfaces (extractor, converter, writer, git, rclone) |
106+
107+
Coverage target: ≥80%. CI runs on ubuntu-latest — all tests mock osascript so macOS is not required.
108+
109+
## Dependencies
110+
111+
- `spf13/cobra` — CLI framework
112+
- `spf13/viper` — Configuration loading
113+
- `go.uber.org/zap` — Structured logging
114+
- `JohannesKaufmann/html-to-markdown/v2` — HTML→Markdown conversion
115+
- `stretchr/testify` — Test assertions and mocks

0 commit comments

Comments
 (0)