Add design docs for Go rewrite.

ARCHITECTURE.md: tech stack (Go, YAML, cobra), repo layout, manifest
schema, content-addressable blob store, CLI commands, package structure.
PROJECT_PLAN.md: 8-step implementation sequence with parallelism notes.
PROGRESS.md: status tracker for resumable implementation.
CLAUDE.md: project guidance for Claude Code, references design docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 21:16:42 -07:00
parent db09939d38
commit b678ce572a
4 changed files with 408 additions and 0 deletions

217
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,217 @@
# ARCHITECTURE.md
Design document for sgard (Shimmering Clarity Gardener), a dotfiles manager.
## Overview
sgard manages dotfiles by checkpointing them into a portable repository and
restoring them to their original locations. The repository is a single
directory that can live anywhere — local disk, USB drive, NFS mount — making
it portable between machines.
## Tech Stack
**Language: Go** (`github.com/kisom/sgard`)
- Static binaries by default, no runtime dependencies on target machines.
- First-class gRPC and protobuf support for the future remote mode.
- Standard library covers all core needs: file I/O (`os`, `path/filepath`),
hashing (`crypto/sha256`), and cross-platform path handling.
- Trivial cross-compilation via `GOOS`/`GOARCH`.
**CLI framework: cobra**
**Manifest format: YAML** (via `gopkg.in/yaml.v3`)
- Human-readable and supports comments (unlike JSON).
- Natural syntax for lists of structured entries (unlike TOML's `[[array_of_tables]]`).
- File modes stored as quoted strings (`"0644"`) to avoid YAML's octal coercion.
## Repository Layout on Disk
A sgard repository is a single directory with this structure:
```
<repo>/
manifest.yaml # single manifest tracking all files
blobs/
a1/b2/a1b2c3d4... # content-addressable file storage
```
### Manifest Schema
```yaml
version: 1
created: "2026-03-23T12:00:00Z"
updated: "2026-03-23T14:30:00Z"
message: "pre-upgrade checkpoint" # optional
files:
- path: ~/.bashrc # original location (default restore target)
hash: a1b2c3d4e5f6... # SHA-256 of file contents
type: file # file | directory | link
mode: "0644" # permissions (quoted to avoid YAML coercion)
updated: "2026-03-23T14:30:00Z" # last checkpoint time for this file
- path: ~/.config/nvim
type: directory
mode: "0755"
updated: "2026-03-23T14:30:00Z"
# directories have no hash or blob — they're structural entries
- path: ~/.vimrc
type: link
target: ~/.config/nvim/init.vim # symlink target
updated: "2026-03-23T14:30:00Z"
# links have no hash or blob — just the target path
- path: ~/.ssh/config
hash: d4e5f6a1b2c3...
type: file
mode: "0600"
updated: "2026-03-23T14:30:00Z"
```
### Blob Store
Files are stored by their SHA-256 hash in a two-level directory structure:
```
blobs/<first 2 hex chars>/<next 2 hex chars>/<full 64-char hash>
```
Example: a file with hash `a1b2c3d4e5...` is stored at `blobs/a1/b2/a1b2c3d4e5...`
Properties:
- **Deduplication**: identical files across different paths share one blob.
- **Rename-safe**: moving a dotfile to a new path updates only the manifest.
- **Integrity**: the filename *is* the expected hash — corruption is trivially detectable.
- **Directories and symlinks** are manifest-only entries. No blobs are stored for them.
## CLI Commands
All commands operate on a repository directory (default: `~/.sgard`, override with `--repo`).
### Phase 1 — Local
| Command | Description |
|---|---|
| `sgard init [--repo <path>]` | Create a new repository |
| `sgard add <path>...` | Track files; copies them into the blob store and adds manifest entries |
| `sgard remove <path>...` | Untrack files; removes manifest entries (blobs cleaned up on next checkpoint) |
| `sgard checkpoint [-m <message>]` | Re-hash all tracked files, store any changed blobs, update manifest |
| `sgard restore [<path>...] [--force]` | Restore files from manifest to their original locations |
| `sgard status` | Compare current files against manifest: modified, missing, ok |
| `sgard verify` | Check all blobs against manifest hashes (integrity check) |
| `sgard list` | List all tracked files |
| `sgard diff [<path>]` | Show content diff between current file and stored blob |
**Workflow example:**
```sh
# Initialize a repo on a USB drive
sgard init --repo /mnt/usb/dotfiles
# Track some files
sgard add ~/.bashrc ~/.gitconfig ~/.ssh/config --repo /mnt/usb/dotfiles
# Checkpoint current state
sgard checkpoint -m "initial" --repo /mnt/usb/dotfiles
# On a new machine, restore
sgard restore --repo /mnt/usb/dotfiles
```
### Phase 2 — Remote (Future)
| Command | Description |
|---|---|
| `sgard push` | Push checkpoint to remote gRPC server |
| `sgard pull` | Pull checkpoint from remote gRPC server |
| `sgard serve` | Run the gRPC daemon |
## Go Package Structure
```
sgard/
cmd/
sgard/ # CLI entry point
main.go # cobra root command, --repo flag
init.go # sgard init
add.go # sgard add
remove.go # sgard remove
checkpoint.go # sgard checkpoint
restore.go # sgard restore
status.go # sgard status
verify.go # sgard verify
list.go # sgard list
diff.go # sgard diff
sgardd/ # gRPC server entry point (Phase 2)
garden/ # Core business logic
garden.go # Garden struct: orchestrates manifest + store + filesystem
garden_test.go
hasher.go # SHA-256 file hashing
diff.go # File diff generation
manifest/ # YAML manifest parsing
manifest.go # Manifest and Entry structs, Load/Save
manifest_test.go
store/ # Content-addressable blob storage
store.go # Store struct: Write/Read/Exists/Delete
store_test.go
proto/ # gRPC service definition (Phase 2)
sgard/v1/
sgard.proto
server/ # gRPC server implementation (Phase 2)
```
### Key Architectural Rule
**The `garden` package contains all logic. The `cmd` package is pure CLI wiring.**
The `Garden` struct is the central coordinator:
```go
type Garden struct {
manifest *manifest.Manifest
store *store.Store
root string // repository root directory
}
func (g *Garden) Add(paths []string) error
func (g *Garden) Remove(paths []string) error
func (g *Garden) Checkpoint(message string) error
func (g *Garden) Restore(paths []string, force bool) error
func (g *Garden) Status() ([]FileStatus, error)
func (g *Garden) Verify() ([]VerifyResult, error)
func (g *Garden) Diff(path string) (string, error)
```
This separation means the future gRPC server calls the same `Garden` methods
as the CLI — no logic duplication.
## Design Decisions
**Paths in manifest use `~` unexpanded.** The `garden` package expands `~` to
`$HOME` at runtime. This makes the manifest portable across machines with
different usernames.
**No history.** Phase 1 stores only the latest checkpoint. For versioning,
place the manifest under git. The `blobs/` directory should be gitignored —
blob durability (backup, replication) is deferred to a future phase.
**Per-file timestamps.** Each manifest entry records an `updated` timestamp
set at checkpoint time. On restore, if the manifest entry is newer than the
file on disk (by mtime), the restore proceeds without prompting. If the file
on disk is newer or the times match, sgard prompts for confirmation.
`--force` always skips the prompt.
**Atomic writes.** Checkpoint writes `manifest.yaml.tmp` then renames to
`manifest.yaml`. A crash cannot corrupt the manifest.
**Old C++/proto source files** are retained in the git history for reference
and will be removed as part of the Go rewrite.

54
CLAUDE.md Normal file
View File

@@ -0,0 +1,54 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Critical: Keep Project Docs Updated
Any change to the codebase MUST be reflected in these files:
- **ARCHITECTURE.md** — design decisions, data model, package structure
- **PROJECT_PLAN.md** — implementation steps; check off completed items
- **PROGRESS.md** — current status, change log; update after completing any step
If another agent or engineer picks this up later, these files are how they
resume. Keeping them accurate is not optional.
## Project
sgard (Shimmering Clarity Gardener) — a dotfiles manager.
Module: `github.com/kisom/sgard`. Author: K. Isom <kyle@imap.cc>.
## Build
```bash
go build ./cmd/sgard
```
Run tests:
```bash
go test ./...
```
## Dependencies
- `gopkg.in/yaml.v3` — manifest serialization
- `github.com/spf13/cobra` — CLI framework
## Package Structure
```
cmd/sgard/ CLI entry point (cobra commands, pure wiring)
garden/ Core business logic (Garden struct orchestrating everything)
manifest/ YAML manifest parsing (Manifest/Entry structs, Load/Save)
store/ Content-addressable blob storage (SHA-256 keyed)
```
Key rule: all logic lives in `garden/`. The `cmd/` layer only parses flags
and calls `Garden` methods. This enables the future gRPC server to reuse
the same logic with zero duplication.
## Legacy Files
Old C++ and proto source files may still be present. They are retained in
git history for reference and should be removed as part of the Go rewrite
(see PROJECT_PLAN.md Step 1).

38
PROGRESS.md Normal file
View File

@@ -0,0 +1,38 @@
# PROGRESS.md
Tracks implementation status. See PROJECT_PLAN.md for the full plan and
ARCHITECTURE.md for design details.
**If you are picking this up mid-implementation, read this file first.**
## Current Status
**Phase:** Pre-implementation — design complete, ready to begin Step 1.
**Last updated:** 2026-03-23
## Completed Steps
(none yet)
## In Progress
(none yet)
## Up Next
Step 1: Project Scaffolding — remove old C++ files, initialize Go module,
create directory structure, set up cobra root command.
## Known Issues / Decisions Deferred
- **Blob durability**: blobs are not stored in git. A strategy for backup or
replication is deferred to a future phase.
- **gRPC remote mode**: Phase 2. Package structure is designed to accommodate
it (garden core separates logic from CLI wiring).
## Change Log
| Date | Step | Summary |
|---|---|---|
| 2026-03-23 | — | Design phase complete. ARCHITECTURE.md and PROJECT_PLAN.md written. |

99
PROJECT_PLAN.md Normal file
View File

@@ -0,0 +1,99 @@
# PROJECT_PLAN.md
Implementation plan for sgard. See ARCHITECTURE.md for design details.
## Step 1: Project Scaffolding
Remove old C++ source files and set up the Go project.
- [ ] Remove old files: `sgard.cc`, `proto/`, `CMakeLists.txt`, `scripts/`, `.clang-format`, `.clang-tidy`, `.idea/`
- [ ] `go mod init github.com/kisom/sgard`
- [ ] Add dependencies: `gopkg.in/yaml.v3`, `github.com/spf13/cobra`
- [ ] Create directory structure: `cmd/sgard/`, `manifest/`, `store/`, `garden/`
- [ ] Set up `cmd/sgard/main.go` with cobra root command and `--repo` persistent flag
- [ ] Update CLAUDE.md to reflect Go project
- [ ] Verify: `go build ./...` compiles clean
## Step 2: Manifest Package
*Can be done in parallel with Step 3.*
- [ ] `manifest/manifest.go`: `Manifest` and `Entry` structs with YAML tags
- Entry types: `file`, `directory`, `link`
- Mode as string type to avoid YAML octal coercion
- Per-file `updated` timestamp
- [ ] `manifest/manifest.go`: `Load(path)` and `Save(path)` functions
- Save uses atomic write (write to `.tmp`, rename)
- [ ] `manifest/manifest_test.go`: round-trip marshal/unmarshal, atomic save, entry type validation
## Step 3: Store Package
*Can be done in parallel with Step 2.*
- [ ] `store/store.go`: `Store` struct with `root` path
- [ ] `store/store.go`: `Write(data) (hash, error)` — hash content, write to `blobs/XX/YY/<hash>`
- [ ] `store/store.go`: `Read(hash) ([]byte, error)` — read blob by hash
- [ ] `store/store.go`: `Exists(hash) bool` — check if blob exists
- [ ] `store/store.go`: `Delete(hash) error` — remove a blob
- [ ] `store/store_test.go`: write/read round-trip, integrity check, missing blob error
## Step 4: Garden Core — Init and Add
Depends on Steps 2 and 3.
- [ ] `garden/hasher.go`: `HashFile(path) (string, error)` — SHA-256 of a file
- [ ] `garden/garden.go`: `Garden` struct tying manifest + store + root path
- [ ] `garden/garden.go`: `Open(root) (*Garden, error)` — load existing repo
- [ ] `garden/garden.go`: `Init(root) (*Garden, error)` — create new repo (dirs + empty manifest)
- [ ] `garden/garden.go`: `Add(paths []string) error` — hash files, store blobs, add manifest entries
- [ ] `garden/garden_test.go`: init creates correct structure, add stores blob and updates manifest
- [ ] Wire up CLI: `cmd/sgard/init.go`, `cmd/sgard/add.go`
- [ ] Verify: `go build ./cmd/sgard && ./sgard init && ./sgard add ~/.bashrc`
## Step 5: Checkpoint and Status
Depends on Step 4.
- [ ] `garden/garden.go`: `Checkpoint(message string) error` — re-hash all tracked files, store changed blobs, update manifest timestamps
- [ ] `garden/garden.go`: `Status() ([]FileStatus, error)` — compare current hashes to manifest; report modified/missing/ok
- [ ] `garden/garden_test.go`: checkpoint detects changed files, status reports correctly
- [ ] Wire up CLI: `cmd/sgard/checkpoint.go`, `cmd/sgard/status.go`
## Step 6: Restore
Depends on Step 5.
- [ ] `garden/garden.go`: `Restore(paths []string, force bool) error`
- Restore all files if paths is empty, otherwise just the specified paths
- Timestamp comparison: skip prompt if manifest `updated` is newer than file mtime
- Prompt user if file on disk is newer or times match (unless `--force`)
- Create parent directories as needed
- Recreate symlinks for `link` type entries
- Set file permissions from manifest `mode`
- [ ] `garden/garden_test.go`: restore writes correct content, respects permissions, handles symlinks
- [ ] Wire up CLI: `cmd/sgard/restore.go`
## Step 7: Remaining Commands
*These can be done in parallel with each other.*
- [ ] `garden/garden.go`: `Remove(paths []string) error` — remove manifest entries
- [ ] `garden/garden.go`: `Verify() ([]VerifyResult, error)` — check blobs against manifest hashes
- [ ] `garden/garden.go`: `List() []Entry` — return all manifest entries
- [ ] `garden/diff.go`: `Diff(path string) (string, error)` — diff stored blob vs current file
- [ ] Wire up CLI: `cmd/sgard/remove.go`, `cmd/sgard/verify.go`, `cmd/sgard/list.go`, `cmd/sgard/diff.go`
- [ ] Tests for each
## Step 8: Polish
- [ ] Lint setup (golangci-lint config)
- [ ] End-to-end test: init → add → checkpoint → modify file → status → restore → verify
- [ ] Ensure `go vet ./...` and `go test ./...` pass clean
- [ ] Update CLAUDE.md, ARCHITECTURE.md, PROGRESS.md
## Future Steps (Not Phase 1)
- Blob durability (backup/replication strategy)
- gRPC remote mode (push/pull/serve)
- Proto definitions for wire format
- Shell completion via cobra