Files
Dan MilneandClaude Fable 5 6dd57e84f1 Version 0.3.0: resolver/merge/cache refactor, signing design notes
- Split the monolith into package/provider/fetch/merge/cache modules with
  the Resolver deciding install/adopt/update/merge/conflict outcomes
- Three-way merges via git merge-file/diff3 against a content-addressed
  merge-base cache; conflicts go to a .picopackage-merge sibling
- Tests for package, provider, resolver, merge, and cache
- notes.md: update UX and signing design — diff-by-default, SSH signature
  identity pinning (TOFU), key changes as a hard stop, exit-code contract
- Add CLAUDE.md; remove the pre-refactor exe/pppkg monolith and scratch files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiyJC73Mz8xZyCTvCEY8qn
2026-08-13 21:18:19 +10:00

4.3 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

A Ruby gem (picopackage, CLI ppkg) for installing and updating single-file packages: ordinary source files that carry their own provenance in a commented YAML block at the end (# @PICOPACKAGE_START# @PICOPACKAGE_END). See README.md for the format and notes.md for the resolution rules and open design questions.

Commands

bin/setup                        # install dependencies
rake                             # tests + StandardRB (the default task)
rake test                        # tests only
rake test N=/pattern/            # run tests whose names match a pattern
ruby -Itest test/test_merge.rb   # run a single test file
bundle exec standardrb           # lint (config in .standard.yml, not .rubocop.yml)
bundle exec standardrb --fix

Releasing: bump lib/picopackage/version.rb, then bundle exec rake release.

Architecture

Everything lives under lib/picopackage/; lib/picopackage.rb just requires the pieces and defines the shared error classes.

The flow of an install/update is: CLI → Fetch.fetch → Provider → Resolver → (Merge, Cache) → Package#save.

  • package.rb — the data model. Package wraps a file's content and splits it into Payload (the code, normalized to rstrip + "\n\n") and Metadata (a Struct parsed from the YAML block). Metadata#extra round-trips unknown keys so old tools can't corrupt packages written by newer ones. Key distinction: modified? (payload differs from its recorded payload_checksum — a human edited it) vs diverged? (payload differs from base_checksum, the upstream ancestor — true after a merge even when modified? is false); diverged? is what decides whether a fast-forward is safe.
  • provider.rb — resolves a URL to a fetch strategy. Provider.for walks PROVIDERS (FileProvider, GithubGistProvider, OpenGistProvider, DefaultProvider); handles_url? returns true/false/:maybe, and :maybe providers prove themselves by fetching (they fetch in their constructor). Upstream metadata claims (url, version, licence) are adopted with ||=; records (checksums) are always recomputed from the payload actually received.
  • fetch.rb — two things: the HTTP fetcher (Fetch: 1 MB cap, 10s timeout, ≤5 redirects to http/https only, ETag support; deliberately Net::HTTP, never URI.open) and the Resolver, which contains all install/update decision logic and returns a Result with a state (installed/adopted/current/updated/merged/conflict) — deciding what to do about a conflict is the CLI's job.
  • merge.rb — three-way merge shelled out to git merge-file or diff3; no Ruby reimplementation.
  • cache.rb — content-addressed store of upstream payloads under $XDG_CACHE_HOME/picopackage (override: PICOPACKAGE_CACHE), keyed by checksum. This is where merge bases come from; a cache miss means the merge is refused, not guessed.
  • cli.rb — argument parsing and reporting only; commands are install update package verify inspect scan.

Invariants (violating these is a bug, not a style choice)

  1. The checksum decides, never the timestamp. mtime is not evidence about content; equal payload checksums settle the content question regardless of timestamps.
  2. Merges run on the payload alone; the metadata block is derived, never merged. It is regenerated on every write — Package#generate_package is the single choke point where payload_checksum is computed, so a stale checksum can never survive into a written file.
  3. Conflicts are non-destructive. The user's file is never touched; conflict markers go to a sibling <file>.picopackage-merge.
  4. A bare local file whose payload differs from upstream is an unrelated file, not a modified package — refuse, don't merge.

Tests

Minitest, in test/test_*.rb. test/test_helper.rb provides PicopackageTest#with_dirs, which gives every test its own upstream dir, project dir, and cache root (the cache is global state; never share one across tests). Fake an upstream by writing a real file and fetching it through the real FileProvider (publish + upstream_package helpers) rather than stubbing. Fixtures in test/files/ are deliberately malformed or copied verbatim and are excluded from linting.