Files
sgard/PROJECT_PLAN.md
Kyle Isom 153cc9c203 Add Step 12b: directory recursion and mirror command.
Add recurses directories. mirror up syncs filesystem -> manifest,
mirror down syncs manifest -> filesystem (exact restore with cleanup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 23:34:25 -07:00

8.2 KiB

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/, .trunk/
  • 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, confirm func) 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/remove.go: Remove(paths []string) error — remove manifest entries
  • garden/verify.go: Verify() ([]VerifyResult, error) — check blobs against manifest hashes
  • garden/list.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)
  • Clock abstraction: inject jonboulle/clockwork into Garden for deterministic timestamp tests
  • 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

Phase 2: gRPC Remote Sync

Step 9: Proto Definitions + Code Gen

  • Write proto/sgard/v1/sgard.proto — 5 RPCs (PushManifest, PushBlobs, PullManifest, PullBlobs, Prune), all messages
  • Add Makefile target for protoc code generation
  • Add grpc, protobuf, x/crypto deps to go.mod
  • Update flake.nix devShell with protoc tools
  • Verify: go build ./sgardpb compiles

Step 10: Garden Accessor Methods

Can be done in parallel with Step 11.

  • garden/garden.go: GetManifest(), BlobExists(), ReadBlob(), WriteBlob(), ReplaceManifest()
  • Tests for each accessor
  • Verify: go test ./garden/...

Step 11: Proto-Manifest Conversion

Can be done in parallel with Step 10.

  • server/convert.go: ManifestToProto, ProtoToManifest, entry helpers
  • server/convert_test.go: round-trip test
  • Verify: go test ./server/...

Step 12: Server Implementation (No Auth)

Depends on Steps 9, 10, 11.

  • server/server.go: Server struct with RWMutex, 4 RPC handlers
  • PushManifest: timestamp compare, compute missing blobs
  • PushBlobs: receive stream, write to store, replace manifest
  • PullManifest: return manifest
  • PullBlobs: stream requested blobs (64 KiB chunks)
  • server/server_test.go: in-process test with bufconn, push+pull between two repos

Step 12b: Directory Recursion and Mirror Command

  • garden/garden.go: Add recurses directories — walk all files/symlinks, add each as its own entry
  • garden/mirror.go: MirrorUp(paths []string) error — walk directory, add new files, remove entries for files gone from disk, re-hash changed
  • garden/mirror.go: MirrorDown(paths []string, force bool, confirm func(string) bool) error — restore all tracked files under path, delete anything not in manifest
  • garden/mirror_test.go: tests for recursive add, mirror up (detects new/removed), mirror down (cleans extras)
  • cmd/sgard/mirror.go: sgard mirror up <path>, sgard mirror down <path> [--force]
  • Update existing add tests for directory recursion

Step 13: Client Library (No Auth)

Depends on Step 12.

  • client/client.go: Client struct, Push(), Pull() methods
  • client/client_test.go: integration test against in-process server

Step 14: SSH Key Auth

  • server/auth.go: AuthInterceptor, parse authorized_keys, verify SSH signatures
  • client/auth.go: LoadSigner (ssh-agent or key file), PerRPCCredentials
  • server/auth_test.go: in-memory ed25519 key pair, reject unauthenticated
  • client/auth_test.go: metadata generation test

Step 15: CLI Wiring + Prune

Depends on Steps 13, 14.

  • garden/prune.go: Prune() (int, error) — collect referenced hashes from manifest, delete orphaned blobs, return count removed
  • garden/prune_test.go: add file, remove it, prune removes orphaned blob
  • server/server.go: add Prune RPC — server-side prune, returns count
  • proto/sgard/v1/sgard.proto: add rpc Prune(PruneRequest) returns (PruneResponse)
  • client/client.go: add Prune() method
  • cmd/sgard/prune.go: local prune; with --remote flag prunes remote instead
  • cmd/sgard/main.go: add --remote, --ssh-key persistent flags
  • cmd/sgard/push.go, cmd/sgard/pull.go
  • cmd/sgardd/main.go: flags, garden open, auth interceptor, gRPC serve
  • Verify: both binaries compile

Step 16: Polish + Release

  • Update ARCHITECTURE.md, README.md, CLAUDE.md, PROGRESS.md
  • Update flake.nix (add sgardd, protoc to devShell)
  • Update .goreleaser.yaml (add sgardd build)
  • E2e integration test: init two repos, push from one, pull into other
  • Verify: all tests pass, full push/pull cycle works

Future Steps (Not Phase 2)

  • Shell completion via cobra
  • TLS transport (optional --tls-cert/--tls-key on sgardd)
  • Multiple repo support on server