Skip to content

feat(api): package revisions — immutable issuance snapshots (ADR-015 D5) - #168

Merged
thewrz merged 3 commits into
mainfrom
feat/issue-96
Jun 12, 2026
Merged

feat(api): package revisions — immutable issuance snapshots (ADR-015 D5)#168
thewrz merged 3 commits into
mainfrom
feat/issue-96

Conversation

@thewrz

@thewrz thewrz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Why

"50% DD" / "100% CD" / "Addendum 2" must be reproducible point-in-time records of exactly what was issued — liability-grade chain of custody at the issuance boundary (ADR-015 D5). Until now a package was only a live view over the project TOC: any later paragraph edit silently changed what "the issued set" looked like.

What

  • Migration 021: package_revisions (UNIQUE (package_id, label)) + package_revision_specs (frozen SpecTree JSONB per member section), exactly per the ADR-015 D5 schema. Reversible; up → down → up verified.
  • POST /packages/:id/revisions { label } freezes every member section's full tree inside one transaction; GET /revisions/:id returns the frozen trees in membership order, Zod-validated against SpecTreeSchema on read.
  • Acceptance criteria covered by integration tests at the API boundary: post-issuance paragraph edits do not alter the stored tree; duplicate label → 409; every snapshot tree round-trips SpecTreeSchema.parse.

Design decisions

  • Snapshot trees are validated at write too, not just read. Issuance fails (422) rather than freezing a snapshot that can't round-trip — a frozen record that fails its own schema would defeat the purpose. Read-side validation (per the issue) remains as a tamper/drift guard and surfaces as 500 (integrity failure, not client error).
  • REPEATABLE READ for the snapshot transaction. All member trees come from a single consistent DB snapshot, so a concurrent edit mid-issuance can't produce a torn revision.
  • package_revision_specs.spec_id keeps the FK default (NO ACTION), exactly as ADR-015 D5 writes it: checked at end of statement, so project-delete cascades (specs + packages + revisions in one statement) stay safe, while ad-hoc deletion of a spec that appears in an issued revision is blocked — custody survives TOC churn.
  • Issuing an empty package is allowed (specCount: 0). The revision record is still a meaningful issuance event; rejecting would be policy, not data integrity.
  • POST returns a summary (specCount), not the trees — trees can be large; GET /revisions/:id is the read surface.
  • lifecycle_state='issued' hook deliberately absent — deferred to the document-concurrency work (ADR-018), per the issue.
  • buildNodeTree is now exported from queries/specs.ts for intra-db-module reuse (not on the barrel) instead of duplicating the row→forest assembly.

Testing

  • Unit tests pass (pnpm test — 733 passed)
  • Integration tests pass (pnpm test:integration — 264 passed, isolated PG 16)
  • Migration reversible: pnpm migratepnpm migrate:downpnpm migrate verified
  • pnpm lint green (eslint + tsc + prettier); redocly lint openapi.yaml valid (pre-existing warnings only)
  • Manual verification: issue revision → mutate paragraph via SQL → GET still returns pre-edit tree
  • CI green

🤖 Co-authored by Claude (Fable 5). Closes #96.

Summary by CodeRabbit

  • New Features
    • Package revisions enable creating immutable snapshots of packages with unique labels.
    • New API endpoints allow issuing revisions and retrieving frozen member snapshots.
    • Revisions preserve all package member states at the time of issuance.
    • Each revision label is unique within its package.

thewrz and others added 3 commits June 12, 2026 08:34
Immutable issuance snapshots: each revision freezes every member
section's SpecTree as JSONB. UNIQUE (package_id, label); snapshot rows
cascade with their revision; spec_id FK stays NO ACTION per ADR-015 D5
so custody blocks ad-hoc spec deletion but project-delete cascades
remain safe. Reversible (paired up/down, verified both directions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /packages/:id/revisions freezes every member section's SpecTree
as JSONB inside one REPEATABLE READ transaction (consistent
point-in-time view across all members); GET /revisions/:id returns the
frozen trees in membership order, Zod-validated against SpecTreeSchema
on read. Trees are also validated at write — issuance fails (422)
rather than freezing a snapshot that cannot round-trip.

buildNodeTree is exported from queries/specs.ts for intra-db-module
reuse (not on the barrel). Duplicate label → 409 via the
UNIQUE (package_id, label) constraint; unknown package → 404.
lifecycle_state hook deliberately deferred (ADR-018).

Closes #96

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /packages/{id}/revisions and GET /revisions/{id}, with
RevisionSummary / RevisionSpecEntry / RevisionWithTrees component
schemas (frozen trees reference the existing SpecTree schema).
Validated with redocly lint (pre-existing warnings only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 447201c0-1e4d-4fde-8a7b-b45048f01570

📥 Commits

Reviewing files that changed from the base of the PR and between 0e31f37 and 648b414.

📒 Files selected for processing (11)
  • docs/superpowers/plans/2026-06-12-package-revisions.md
  • openapi.yaml
  • src/api/revisions.integration.test.ts
  • src/api/revisions.ts
  • src/api/router.ts
  • src/ast/index.ts
  • src/ast/schemas.ts
  • src/db/index.ts
  • src/db/migrations/021_create_package_revisions.ts
  • src/db/queries/revisions.ts
  • src/db/queries/specs.ts

📝 Walkthrough

Walkthrough

This PR implements immutable package revision snapshots per ADR-015 D5. It adds a database migration for package_revisions and package_revision_specs tables, core query logic that locks packages and snapshots member spec trees in a REPEATABLE READ transaction with Zod validation, API handlers mapping domain errors to HTTP responses, and comprehensive integration tests covering issuance, retrieval, immutability, uniqueness, and cascade deletion.

Changes

Package Revisions — Immutable Issuance Snapshots

Layer / File(s) Summary
Migration, schema contracts, and public API surface
src/db/migrations/021_create_package_revisions.ts, src/db/queries/revisions.ts, openapi.yaml, src/ast/schemas.ts, src/ast/index.ts, src/db/index.ts
Migration 021 creates package_revisions and package_revision_specs tables with cascade delete and (package_id, label) uniqueness. New type contracts RevisionSummary, RevisionSpecEntry, and RevisionWithTrees defined and re-exported across db and AST modules. OpenAPI paths and schemas added. CreateRevisionBodySchema validates non-empty label.
Paragraph tree extraction contract
src/db/queries/specs.ts
ParagraphTreeRow interface and buildNodeTree function exported to enable revisions module to snapshot and validate spec member trees. Internal ParaRow type replaced.
Core revision query implementation
src/db/queries/revisions.ts
createPackageRevision locks package FOR UPDATE, inserts revision header, snapshots all member specs' paragraph trees in membership order via snapshotMemberTrees, validates each tree with Zod SpecTreeSchema before persist, and commits within REPEATABLE READ transaction or rolls back on failure. getPackageRevision fetches revision and snapshots, validates each stored tree on read to detect tamper/drift, returns null if absent. SnapshotValidationError wraps Zod failures.
API handlers and routing
src/api/revisions.ts, src/api/router.ts
createRevisionHandler validates :id param, calls createPackageRevision, returns 201 with metadata, maps PackageNotFoundError404, SnapshotValidationError422, PostgreSQL unique-violation 23505409. getRevisionHandler returns 404 if absent, 200 with frozen trees, logs and returns 500 on error while masking validation details. Routes registered at POST /packages/:id/revisions and GET /revisions/:id.
Integration tests and documentation
src/api/revisions.integration.test.ts, docs/superpowers/plans/2026-06-12-package-revisions.md
Spins up ephemeral Express server, seeds database with specs and packages, validates POST /revisions returns 201 with correct metadata, enforces per-package label uniqueness (409 on duplicate, 201 on other package), rejects unknown packages (404) and empty labels (422). GET /revisions/:id validates specs returned in position order, round-trip against SpecTreeSchema, and immutability by mutating paragraphs post-issuance and confirming stored tree unchanged. Cascade test verifies revision/snapshot deletion on package removal while preserving underlying specs. Planning doc outlines implementation scope.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

style-fidelity:1

🐇 A snapshot in time, locked and true,
Frozen trees of spec in the queue,
Label each package, let none collide,
Validation both ways, with nowhere to hide,
Immutable records—the audit survives! 📋✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding immutable package revision snapshots via API, database migration, and validation logic.
Linked Issues check ✅ Passed All coding requirements from issue #96 are met: migration 021 with package_revisions/package_revision_specs tables, POST /packages/:id/revisions with label validation, GET /revisions/:id with Zod validation, UNIQUE constraint, snapshot immutability, and round-trip validation.
Out of Scope Changes check ✅ Passed All changes are in-scope. Exports of buildNodeTree and ParagraphTreeRow support the revision snapshot logic; all additions align with ADR-015 D5 requirements and avoid lifecycle_state hooks (deferred per scope).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-96

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api): package revisions — immutable issuance snapshots

1 participant