- 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
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.
Packagewraps a file's content and splits it intoPayload(the code, normalized torstrip + "\n\n") andMetadata(a Struct parsed from the YAML block).Metadata#extraround-trips unknown keys so old tools can't corrupt packages written by newer ones. Key distinction:modified?(payload differs from its recordedpayload_checksum— a human edited it) vsdiverged?(payload differs frombase_checksum, the upstream ancestor — true after a merge even whenmodified?is false);diverged?is what decides whether a fast-forward is safe. - provider.rb — resolves a URL to a fetch strategy.
Provider.forwalksPROVIDERS(FileProvider, GithubGistProvider, OpenGistProvider, DefaultProvider);handles_url?returns true/false/:maybe, and:maybeproviders 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; deliberatelyNet::HTTP, neverURI.open) and theResolver, which contains all install/update decision logic and returns aResultwith 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-fileordiff3; 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)
- The checksum decides, never the timestamp. mtime is not evidence about content; equal payload checksums settle the content question regardless of timestamps.
- Merges run on the payload alone; the metadata block is derived, never merged. It is regenerated on every write —
Package#generate_packageis the single choke point wherepayload_checksumis computed, so a stale checksum can never survive into a written file. - Conflicts are non-destructive. The user's file is never touched; conflict markers go to a sibling
<file>.picopackage-merge. - 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.