Files
picopackage/CLAUDE.md
T
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

46 lines
4.3 KiB
Markdown

# 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
```bash
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.