Files
zpan/spec
Jasper VanandClaude Opus 4.8 7b8c8c915e refactor(api)!: unify object upload + rework delete/trash lifecycle (#448) (#454)
Resolve #448 — one upload entry point and an AIP-164 soft delete.

Upload: POST /objects now returns size-decided upload instructions
{ sessionId, partSize, urls }; the server picks single PutObject (<=5 GiB)
vs 5 GiB-part multipart (>5 GiB) and rejects >5 TiB. The client PUTs each
slice, reads its ETag, then POSTs them to
POST /objects/{id}/uploads/{sid}/completions (returns the live object).
DELETE /objects/{id}/uploads/{sid} aborts and discards the draft.

Trash: matters.status drops 'trashed' (enum is {draft,active}); trash is
tracked by the existing trashedAt timestamp. DELETE /objects/{id} now
soft-deletes; the recycle bin lives under /trash/objects (list roots, get,
restorations, purge). Empty-trash is a frontend loop over roots.

BREAKING CHANGE:
- removes PUT /objects/{id}/status and POST /objects/{id}/uploads
- PUT .../uploads/{sid}/status -> POST .../uploads/{sid}/completions {parts}
- DELETE /objects/{id} flips hard-purge -> soft-delete; permanent purge
  moves to DELETE /trash/objects/{id}
- DELETE /trash removed; restore is POST /trash/objects/{id}/restorations
- matters.status enum loses 'trashed' (migration backfills to trashedAt)

The migration swaps the matters_active_name_uniq partial index to exclude
trashed rows (WHERE status='active' AND trashed_at IS NULL). The single-PUT
presign is header-free so the uniform slice uploader's raw PUT matches the
S3 signature. Go downloader client + agent reworked to the unified flow.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:21:43 -04:00
..

Product specs

Behaviour-first product specs in Gherkin .feature files. This directory is the source of truth for what ZPan does, independent of implementation. There is no Cucumber runner — the .feature files are documentation, and tests trace back to scenarios by id.

Convention

  • One .feature file per capability (storages.feature, site-invitations.feature, …).

  • Each scenario carries two tags: the id @<capability>/<slug> and the layer that proves it (@domain / @usecase / @web / @api / @e2e):

    @storages/create-records-activity @api
    Scenario: Creating a storage records an audit activity
      Given an authenticated admin
      When they create a storage
      Then a storage_create activity is recorded
    
  • The id never changes once written (rename = new id).

  • Verify each scenario at the cheapest layer that can prove it. In this repo most land at @api (the workerd + real-D1 *.integration.test.ts / *.cf-test.ts flows through app.fetch) or @web (jsdom + MSW). Reserve @e2e for the Playwright cross-stack journeys. Pure rules sit at @domain / @usecase.

Traceability

Each scenario's home test carries [spec: <id>] in its name:

it('records a storage_create activity [spec: storages/create-records-activity]', …)

pnpm lint:spec (wired into CI) enforces the link both ways: every scenario id must have a referencing test, and every [spec: id] breadcrumb must match a real scenario.

Status

Specs are authored capability-by-capability alongside the clean-architecture migration (see docs/clean-arch-migration.md). Migrated capabilities are specced and traced here; the rest land as their slices migrate.

Escalation

If a non-technical audience ever needs to run the Gherkin, wire playwright-bdd (compiles .feature → Playwright). These are real .feature files, so that step is drop-in — no rewrite.