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>
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
.featurefile 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.tsflows throughapp.fetch) or@web(jsdom + MSW). Reserve@e2efor 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.