Skip to content

Commit 3852e5b

Browse files
authored
Merge pull request #2 from git-stunts/m2-boomerang
feat(cas): M2 Boomerang — restore, CLI, integration tests
2 parents 2d415f7 + d7f8fcf commit 3852e5b

25 files changed

Lines changed: 2269 additions & 567 deletions

.github/workflows/ci.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 22
17+
cache: npm
18+
- run: npm ci
19+
- run: npm run lint
20+
21+
test-unit:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: 22
28+
cache: npm
29+
- run: npm ci
30+
- run: npm test
31+
32+
test-docker:
33+
runs-on: ubuntu-latest
34+
needs: [lint, test-unit]
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
runtime: [node, bun, deno]
39+
include:
40+
- runtime: node
41+
integration_cmd: npx vitest run test/integration
42+
- runtime: bun
43+
integration_cmd: bunx vitest run test/integration
44+
- runtime: deno
45+
integration_cmd: deno run -A npm:vitest run test/integration
46+
steps:
47+
- uses: actions/checkout@v4
48+
- name: Build test-${{ matrix.runtime }}
49+
run: docker compose build test-${{ matrix.runtime }}
50+
- name: Unit tests (${{ matrix.runtime }})
51+
run: docker compose run --rm test-${{ matrix.runtime }}
52+
- name: Integration tests (${{ matrix.runtime }})
53+
run: docker compose run --rm test-${{ matrix.runtime }} ${{ matrix.integration_cmd }}

CHANGELOG.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,48 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [Unreleased] — M2 Boomerang (v1.2.0)
99

1010
### Added
11-
- None.
11+
- `CasService.restore()` — reconstruct files from manifests with per-chunk SHA-256 integrity verification.
12+
- `ContentAddressableStore.restoreFile()` — facade method that restores and writes to disk.
13+
- `readTree()` on `GitPersistencePort` / `GitPersistenceAdapter` — parse Git trees via `ls-tree`.
14+
- `STREAM_ERROR` wrapping — stream failures during `store()` surface as `CasError('STREAM_ERROR')` with `{ chunksWritten }` metadata.
15+
- `MISSING_KEY` error code — `restore()` now fails fast when manifest is encrypted but no decryption key is provided.
16+
- CLI: `git cas store`, `git cas tree`, `git cas restore` subcommands via `bin/git-cas.js`.
17+
- Integration test suite (59 tests) running against real Git bare repos inside Docker.
18+
- `commander` dependency for CLI.
1219

1320
### Changed
21+
- `readBlob()` now normalises `Uint8Array` from plumbing into `Buffer` for codec/crypto compatibility.
22+
- `readTree()` uses `git ls-tree -z` (NUL-delimited output) for safe parsing of filenames with leading/trailing spaces.
23+
24+
### Fixed
25+
- Fuzz tests in stream-error suite now fail explicitly if `store()` does not throw.
26+
- ROADMAP: resolved inconsistent CLI signatures for `git cas tree` (`--slug` vs `--manifest`).
27+
28+
### Security
1429
- None.
1530

31+
## [1.1.0] — M1 Bedrock
32+
33+
### Added
34+
- `CryptoPort` interface and `NodeCryptoAdapter` — extracted all `node:crypto` usage from the domain layer.
35+
- `CasService.store()` — accepts `AsyncIterable<Buffer>` sources (renamed from `storeFile`).
36+
- Multi-stage Dockerfile (Node 22, Bun, Deno) with `docker-compose.yml` for per-runtime testing.
37+
- BATS parallel test runner (`test/platform/runtimes.bats`).
38+
- Devcontainer setup (`.devcontainer/`) with all three runtimes + BATS.
39+
- Encryption key validation (`INVALID_KEY_TYPE`, `INVALID_KEY_LENGTH` error codes).
40+
- Encryption round-trip unit tests (110 tests including fuzz).
41+
- Empty file (0-byte) edge case tests.
42+
- Error-path unit tests for constructors and core failures.
43+
- Deterministic test digest helper (`digestOf`).
44+
45+
### Changed
46+
- `CasService` domain layer has zero `node:*` imports — all platform dependencies injected via ports.
47+
- Constructor requires `crypto` and `codec` params (no defaults); facade supplies them.
48+
- Facade `storeFile()` now opens the file and delegates to `CasService.store()`.
49+
1650
### Fixed
1751
- None.
1852

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ CMD ["bunx", "vitest", "run", "test/unit"]
2121
# --- Deno ---
2222
FROM denoland/deno:latest AS deno
2323
USER root
24-
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
24+
RUN apt-get update && apt-get install -y git nodejs npm && rm -rf /var/lib/apt/lists/*
2525
WORKDIR /app
2626
COPY . .
2727
RUN deno install --allow-scripts

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ We use the object database.
2323
- **Optional AES-256-GCM encryption** store secrets without leaking plaintext into the ODB.
2424
- **Manifests** a tiny explicit index of chunks + metadata (JSON/CBOR).
2525
- **Tree output** generates standard Git trees so assets snap into commits cleanly.
26+
- **Full round-trip** store, tree, and restore — get your bytes back, verified.
2627

2728
**Use it for:** binary assets, build artifacts, model weights, data packs, secret bundles, weird experiments, etc.
2829

@@ -43,19 +44,32 @@ const manifest = await cas.storeFile({
4344
});
4445

4546
// Turn the manifest into a Git tree OID
46-
const treeOid = cas.createTree({ manifest });
47+
const treeOid = await cas.createTree({ manifest });
4748

48-
// Now you can point a ref/commit at that tree like a normal Git artifact.
49+
// Restore later — get your bytes back, integrity-verified
50+
await cas.restoreFile({ manifest, outputPath: './restored.png' });
4951
```
5052

5153
## CLI (git plugin)
5254

5355
`git-cas` installs as a Git subcommand:
5456

5557
```bash
56-
git cas store ./image.png --slug my-image
57-
git cas tree --slug my-image
58-
git cas restore <tree-oid> --out ./image.png
58+
# Store a file — prints manifest JSON
59+
git cas store ./image.png --slug my-image
60+
61+
# Store and get a tree OID directly
62+
git cas store ./image.png --slug my-image --tree
63+
64+
# Create a tree from an existing manifest
65+
git cas tree --manifest manifest.json
66+
67+
# Restore from a tree OID
68+
git cas restore <tree-oid> --out ./restored.png
69+
70+
# Encrypted round-trip (32-byte raw key file)
71+
git cas store ./secret.bin --slug vault --key-file ./my.key --tree
72+
git cas restore <tree-oid> --out ./decrypted.bin --key-file ./my.key
5973
```
6074

6175
## Why not Git LFS?

ROADMAP.md

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This roadmap is structured as:
99
3. **Contracts** — Return/throw semantics for all public methods
1010
4. **Version Plan** — Table mapping versions to milestones
1111
5. **Milestone Dependency Graph** — ASCII diagram
12-
6. **Milestones & Task Cards** — 7 milestones, 24 tasks (uniform task card template)
12+
6. **Milestones & Task Cards** — 7 milestones, 26 tasks (uniform task card template)
1313

1414
---
1515

@@ -42,6 +42,7 @@ Single registry of all error codes used across the codebase. Each code is a stri
4242
| `INVALID_KEY_TYPE` | Encryption key is not a Buffer. | Task 1.3 |
4343
| `INTEGRITY_ERROR` | Decryption auth-tag verification failed (wrong key, tampered ciphertext, or tampered tag), or chunk digest mismatch on restore. | Exists (decrypt); extended by Task 1.6, Task 2.1 |
4444
| `STREAM_ERROR` | Read stream failed during `storeFile`. Partial chunks may have been written to Git ODB (unreachable; handled by `git gc`). Meta includes `{ chunksWritten: <number> }`. | Task 2.4 |
45+
| `MISSING_KEY` | Encryption key required to restore encrypted content but none was provided. | Task 2.1 |
4546
| `TREE_PARSE_ERROR` | `git ls-tree` output could not be parsed into valid entries. | Task 2.2 |
4647
| `MANIFEST_NOT_FOUND` | No manifest entry (e.g. `manifest.json` / `manifest.cbor`) found in the Git tree. | Task 4.1 |
4748
| `GIT_ERROR` | Underlying Git plumbing command failed. Wraps the original error from the plumbing layer. | Task 2.2, Task 4.1 |
@@ -106,14 +107,29 @@ Return and throw semantics for every public method (current and planned).
106107
- **Algorithms:** `pbkdf2` (default), `scrypt` — both Node.js built-ins.
107108
- **Throws:** Standard Node.js crypto errors on invalid parameters.
108109

110+
### CLI: `git cas store <file> --slug <slug> [--key-file <path>]` *(planned — Task 2.5)*
111+
- **Output:** Prints manifest JSON to stdout. If `--tree` is passed, prints only the Git tree OID instead.
112+
- **Exit 0:** Store succeeded.
113+
- **Exit 1:** Store failed (error message to stderr).
114+
115+
### CLI: `git cas tree --manifest <path>` *(planned — Task 2.5)*
116+
- **Output:** Prints Git tree OID to stdout.
117+
- **Exit 0:** Tree created.
118+
- **Exit 1:** Invalid manifest or Git error (message to stderr).
119+
120+
### CLI: `git cas restore <tree-oid> --out <path> [--key-file <path>]` *(planned — Task 2.6)*
121+
- **Output:** Writes restored file to `--out` path.
122+
- **Exit 0:** Restore succeeded, prints bytes written to stdout.
123+
- **Exit 1:** Integrity error, missing manifest, or I/O error (message to stderr).
124+
109125
---
110126

111127
## 4) Version Plan
112128

113129
| Version | Milestone | Codename | Theme |
114130
|--------:|-----------|----------|-------|
115131
| v1.1.0 | M1 | Bedrock | Foundation hardening |
116-
| v1.2.0 | M2 | Boomerang| File retrieval round trip |
132+
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI |
117133
| v1.3.0 | M3 | Launchpad| CI/CD pipeline |
118134
| v1.4.0 | M4 | Compass | Lifecycle management |
119135
| v1.5.0 | M5 | Sonar | Observability |
@@ -152,13 +168,13 @@ M3 Launchpad (v1.3.0) M4 Compass (v1.4.0)
152168
| # | Codename | Theme | Version | Tasks | ~LoC | ~Hours |
153169
|---:|--------------|----------------------------|:-------:|------:|-------:|------:|
154170
| M1 | Bedrock | Foundation hardening | v1.1.0 | 7 | ~475 | ~6.5h |
155-
| M2 | Boomerang | File retrieval round trip | v1.2.0 | 4 | ~295 | ~9.5h |
171+
| M2 | Boomerang | File retrieval round trip + CLI | v1.2.0 | 6 | ~435 | ~14h |
156172
| M3 | Launchpad | CI/CD pipeline | v1.3.0 | 2 | ~110 | ~4h |
157173
| M4 | Compass | Lifecycle management | v1.4.0 | 3 | ~180 | ~5.5h |
158174
| M5 | Sonar | Observability | v1.5.0 | 2 | ~210 | ~5.5h |
159175
| M6 | Cartographer | Documentation | v1.6.0 | 3 | ~750 | ~10h |
160176
| M7 | Horizon | Advanced features | v2.0.0 | 3 | ~450 | ~17h |
161-
| | **Total** | | | **24**| **~2,470** | **~58h** |
177+
| | **Total** | | | **26**| **~2,610** | **~62.5h** |
162178

163179
---
164180

@@ -535,7 +551,7 @@ As a maintainer, I want error conditions covered by tests so regressions in vali
535551
---
536552

537553
# M2 — Boomerang (v1.2.0)
538-
**Theme:** Complete store→retrieve round trip.
554+
**Theme:** Complete store→retrieve round trip + CLI.
539555

540556
---
541557

@@ -761,6 +777,132 @@ As a developer, I want storeFile to fail safely on stream errors so partial stor
761777

762778
---
763779

780+
## Task 2.5: CLI scaffold + `store` and `tree` subcommands
781+
782+
**User Story**
783+
As a developer, I want `git cas store` and `git cas tree` commands so I can use CAS from the terminal without writing Node scripts.
784+
785+
**Requirements**
786+
- R1: Add `bin/git-cas.js` entry point (Git discovers `git-cas` on PATH for `git cas` subcommands).
787+
- R2: Add `"bin": { "git-cas": "./bin/git-cas.js" }` to `package.json`.
788+
- R3: Use a lightweight CLI framework (e.g., `commander`) for subcommand routing.
789+
- R4: `git cas store <file> --slug <slug> [--key-file <path>] [--tree]`:
790+
- Reads the file, calls `storeFile()`.
791+
- Prints manifest JSON to stdout by default.
792+
- If `--tree` is passed, also calls `createTree()` and prints tree OID.
793+
- `--key-file` reads a 32-byte raw key from a file for encryption.
794+
- R5: `git cas tree --manifest <path>`:
795+
- Reads a manifest JSON from file/stdin, calls `createTree()`.
796+
- Prints tree OID to stdout.
797+
- R6: Exit 0 on success, exit 1 on error with message to stderr.
798+
- R7: `--cwd` flag to set Git working directory (defaults to `.`).
799+
800+
**Acceptance Criteria**
801+
- AC1: `npx git-cas store ./test.txt --slug test` prints manifest JSON.
802+
- AC2: `npx git-cas store ./test.txt --slug test --tree` prints tree OID.
803+
- AC3: `npx git-cas tree --manifest manifest.json` prints tree OID.
804+
- AC4: Invalid arguments produce helpful usage message and exit 1.
805+
- AC5: `--key-file` with valid 32-byte file encrypts successfully.
806+
- AC6: `--key-file` with wrong-size file exits 1 with clear error.
807+
808+
**Scope**
809+
- In scope: CLI scaffold, store subcommand, tree subcommand, key-file reading.
810+
- Out of scope: `restore` subcommand (Task 2.6), shell completions, config files.
811+
812+
**Est. Complexity (LoC)**
813+
- Prod: ~80
814+
- Tests: ~30
815+
- Total: ~110
816+
817+
**Est. Human Working Hours**
818+
- ~3h
819+
820+
**Test Plan**
821+
- Golden path:
822+
- store a file via CLI → valid manifest JSON on stdout.
823+
- store with `--tree` → tree OID on stdout.
824+
- tree from manifest file → tree OID on stdout.
825+
- Failures:
826+
- missing file → exit 1 with error.
827+
- missing `--slug` → exit 1 with usage message.
828+
- bad key file → exit 1 with INVALID_KEY_LENGTH/TYPE error.
829+
- Edges:
830+
- 0-byte file store.
831+
- manifest piped via stdin (if supported).
832+
- Fuzz/stress:
833+
- None (thin wrapper over tested API).
834+
835+
**Definition of Done**
836+
- DoD1: `bin/git-cas.js` exists with store and tree subcommands.
837+
- DoD2: `package.json` declares bin entry.
838+
- DoD3: `npx git-cas --help` prints usage.
839+
- DoD4: Integration smoke test passes against real Git repo.
840+
841+
**Blocking**
842+
- Blocks: Task 2.6
843+
844+
**Blocked By**
845+
- Blocked by: None
846+
847+
---
848+
849+
## Task 2.6: CLI `restore` subcommand
850+
851+
**User Story**
852+
As a developer, I want `git cas restore <tree-oid> --out <path>` so I can retrieve stored assets from the terminal.
853+
854+
**Requirements**
855+
- R1: `git cas restore <tree-oid> --out <path> [--key-file <path>]`:
856+
- Reads the tree, extracts the manifest, restores the file to `--out`.
857+
- Prints bytes written to stdout on success.
858+
- `--key-file` supplies decryption key for encrypted assets.
859+
- R2: Exit 0 on success, exit 1 on error (INTEGRITY_ERROR, MANIFEST_NOT_FOUND, etc.) with message to stderr.
860+
- R3: Requires `restoreFile()` (Task 2.1) and `readManifest()` or equivalent tree-reading capability.
861+
862+
**Acceptance Criteria**
863+
- AC1: `npx git-cas restore <oid> --out ./restored.txt` writes correct file.
864+
- AC2: Encrypted asset with `--key-file` restores correctly.
865+
- AC3: Wrong key exits 1 with INTEGRITY_ERROR message.
866+
- AC4: Invalid tree OID exits 1 with clear error.
867+
868+
**Scope**
869+
- In scope: restore subcommand wired to restoreFile API.
870+
- Out of scope: Streaming output to stdout, partial restore, resume.
871+
872+
**Est. Complexity (LoC)**
873+
- Prod: ~30
874+
- Tests: ~20
875+
- Total: ~50
876+
877+
**Est. Human Working Hours**
878+
- ~1.5h
879+
880+
**Test Plan**
881+
- Golden path:
882+
- store → tree → restore → byte-compare original.
883+
- encrypted store → tree → restore with key → byte-compare.
884+
- Failures:
885+
- wrong key → exit 1 INTEGRITY_ERROR.
886+
- nonexistent tree OID → exit 1.
887+
- missing `--out` → exit 1 with usage.
888+
- Edges:
889+
- 0-byte file round-trip via CLI.
890+
- Fuzz/stress:
891+
- None (thin wrapper over tested API).
892+
893+
**Definition of Done**
894+
- DoD1: `restore` subcommand added to `bin/git-cas.js`.
895+
- DoD2: Full CLI round-trip (store → tree → restore) documented and tested.
896+
- DoD3: README CLI section is now accurate and deliverable.
897+
898+
**Blocking**
899+
- Blocks: None
900+
901+
**Blocked By**
902+
- Blocked by: Task 2.1, Task 2.5
903+
904+
---
905+
764906
# M3 — Launchpad (v1.3.0)
765907
**Theme:** Automated quality gates and release process.
766908

0 commit comments

Comments
 (0)