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
This commit is contained in:
Dan Milne
2026-08-13 21:18:19 +10:00
co-authored by Claude Fable 5
parent e0cd0f0d7a
commit 6dd57e84f1
27 changed files with 2010 additions and 383 deletions
+118 -10
View File
@@ -1,6 +1,11 @@
# Picopackage
A command line tool for installing and managing [Picopackages](https://picopackage.org).
A command line tool for installing and updating [picopackages](https://picopackage.org):
single files that carry their own provenance, so you can install one into your
project, edit it, and still pull upstream fixes later.
For code that's too small to be a gem but copied often enough that pasting it
between projects hurts.
## Installation
@@ -10,18 +15,121 @@ gem install picopackage
## Usage
`picopackage install <url|filepath>`
```bash
ppkg install https://gist.github.com/you/abc123 # fetch into .
ppkg install https://example.com/retry.rb lib/ # fetch into lib/
ppkg update lib/retry.rb # re-fetch from its recorded url
ppkg package lib/mine.rb --url https://… # turn a local file into a picopackage
ppkg verify lib/retry.rb # check it against its checksum
ppkg scan . # list picopackages in a tree
ppkg inspect lib/retry.rb # print its metadata
```
## What a picopackage looks like
An ordinary source file with a commented YAML block at the end:
```ruby
def retry_with_backoff(attempts: 3)
# …
end
# @PICOPACKAGE_START
# ---
# url: https://gist.github.com/you/abc123
# filename: retry.rb
# payload_timestamp: '2026-08-12T14:06:38Z'
# payload_checksum: sha256:7b856b1b…
# base_checksum: sha256:7b856b1b…
# @PICOPACKAGE_END
```
Everything above the block is the **payload**. The block is regenerated on every
write. Metadata keys the tool doesn't recognise are preserved as-is, so optional
fields survive a tool that predates them.
## Upstream doesn't need a block
A plain file with no metadata at all is a valid picopackage source. You can
install someone's gist, blog snippet or raw file without them adopting anything
— everything the block would have said is derived from the fetch.
When upstream *does* carry a block, it's opting in to extra guarantees, and only
half of it is upstream's to assert:
| | Fields | Who decides |
| --- | --- | --- |
| **Claims** | `url`, `filename`, `payload_version`, `licence` | upstream — adopted as given |
| **Records** | `payload_checksum`, `base_checksum` | your machine — always recomputed |
A claim is something only the author knows: the canonical URL that outlives the
mirror you fetched from, the version, the licence. A record describes one copy on
one disk, so upstream can't speak for yours. In particular an upstream
`payload_checksum` is never trusted — a checksum stored beside the content it
hashes proves nothing, and an author who edits without re-running the tool ships
a stale one. Adopting it would install a file that failed its own verification
and reported as locally modified before you touched it.
## Updating a file you've edited
The point of a picopackage is that it's *your* file — so editing it has to be
allowed, and updates have to cope with that. `ppkg update` does a three-way
merge against the version you originally installed:
- **Unchanged locally** → fast-forwarded to upstream.
- **Edited locally, upstream unchanged** → nothing happens. No merge is
attempted; upstream is still the version you branched from.
- **Edited locally, no overlap with upstream's changes** → merged, keeping both.
- **Edited locally, overlapping changes** → refused. Your file is left exactly
as it is and a copy with conflict markers is written to
`<file>.picopackage-merge` for you to work from.
- `--force` discards local changes and takes upstream wholesale.
Two rules decide every case:
1. **The checksum decides, never the timestamp.** An mtime says nothing about
content — a file re-uploaded unchanged gets a fresh one.
2. **Merges run on the payload alone.** The metadata block is regenerated
afterwards, so it never conflicts.
The merge needs the common ancestor, which is kept in a content-addressed cache
under `$XDG_CACHE_HOME/picopackage` (override with `PICOPACKAGE_CACHE`). A file
installed on another machine, or one whose cache has been cleared, has no
ancestor available — `ppkg` says so and refuses rather than guessing.
`git merge-file` is used when available, `diff3` otherwise.
## Sources
| Source | Handling |
| --- | --- |
| GitHub Gist | Resolved through the Gist API |
| Opengist | Resolved via its `.json` endpoint |
| Any URL | Fetched directly |
| Local path | Read from disk |
Responses are capped at 1 MB and time out after 10s. Redirects are followed up
to 5 times and only to `http`/`https`.
## What it deliberately doesn't do
- **No dependencies.** Each file stands alone. There is no solver, no version
ranges, no transitive graph.
- **No registry.** The file's `url` is its identity; no central index exists.
- **No author verification.** A checksum stored beside the content it hashes
proves nothing about who wrote it — its job is detecting *local* modification.
Signing (SSH signatures, TOFU-pinned — see notes.md) is designed, not
implemented. Until then, the security model is that a picopackage is one
file and you can read it.
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
`bin/setup` installs dependencies, `rake test` runs the tests, `rake` runs tests
and [Standard](https://github.com/standardrb/standard). `bin/console` gives you a
prompt.
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
To release: update `lib/picopackage/version.rb`, then `bundle exec rake release`.
## Contributing
## Licence
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/picop.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
MIT.