Skip to content

Assets Case Matrix ​

One row per behavioral case for the assets use cases built so far, named in ASSETS_USE_CASES.md §7 and specified stage by stage in ASSETS_STAGE_SPECS.md. Each case names the test that proves it, quoted verbatim as a spec file path, then the separator "›", then the exact test title, exactly like CATALOG_CASE_MATRIX.md. A case with no proving test answers — and is listed under Gaps, with the stage that will prove it.

Stages A2b, A2c and A4a are proven (no migration, baseline revision 104): AS1, the upload intent with its presigned S3 POST, AS2, the completion that moves the asset to processing and queues its job without calling the storage, AS3, the read of one asset with its rendition addresses, and the library of A4a: AS4, the keyset listing, AS5, the rename, and AS6, the archival and restoration through the definer functions, over HTTP against PostgreSQL 18. The storage is the real S3 signer with fixed test credentials (signing is local, nothing reaches the network); a ready or failed asset is set in SQL, because the media worker that produces them is stage A3. Stage A5a is proven too (no migration): AS12 and AS13, the delivery and attachability contracts, which Catalog calls since A5b (migration 0095, baseline revision 105). The media worker stages are proven too: AS7, processing an upload into a ready asset (A3a, A3b-1, A3b-2), AS8, the retry classes, dead letters and the operator requeue (A3c, migration 0097), AS9, the expiry of abandoned uploads and the usage reconciliation (A3a), and AS10, the purge (A4b, migration 0098). AS11 has no stage yet (no route in v1, ASSETS_USE_CASES.md §7.2), so it has no row.


Totals ​

Use cases12
Per-case scenario rows75
Per-case scenario rows proven75
Cross-tenant and concurrency invariants6 (6 proven)
Gaps0

Uploads ​

AS1 — Request an upload ​

POST /assets/uploads with Idempotency-Key; needs organization-wide assets.media.manage and the assets.media capability. 8 statements, a replay too.

CaseExpected answerProving test
A public JPEG of the declared size201, Cache-Control: no-store, a pending asset, a POST policy bound to the exact in/<token> key, the declared content type and content-length-range 1..byteSize, valid 600 s; the usage row counts one more asset and reserves the bytes; one assets.asset.upload_requested audit with media type, size and access class as after attributestest/e2e/assets-uploads.e2e-spec.ts › issues an upload for a public image with a presigned POST bound to its exact key, content type and declared size, a pending asset and a reserved quota, and records the request once
The policy, the signature and the credentialnever in a log line or an audit attributetest/e2e/assets-uploads.e2e-spec.ts › issues an upload for a public image with a presigned POST bound to its exact key, content type and declared size, a pending asset and a reserved quota, and records the request once
The same key and body again, after the asset was renamed201, Idempotent-Replayed: true, the asset as it is now, the same upload and key, a fresh policy; nothing written or auditedtest/e2e/assets-uploads.e2e-spec.ts › replays the same Idempotency-Key and body with the asset as it is now and a fresh upload policy, and refuses the same key with another body
The same key with another body409 · idempotency.key_reused, nothing signedtest/e2e/assets-uploads.e2e-spec.ts › replays the same Idempotency-Key and body with the asset as it is now and a fresh upload policy, and refuses the same key with another body; src/modules/assets/application/uploads/request-upload.use-case.spec.ts › refuses the same key with another request as reused and signs nothing
A replay one minute before the upload expiresa policy of 60 s, never outliving the uploadsrc/modules/assets/application/uploads/request-upload.use-case.spec.ts › replays an issued upload with the asset as it is now and a fresh policy that never outlives the upload, recording nothing
A replay of a received upload, or of an issued one already expired201 with upload: null and the asset as it is nowsrc/modules/assets/application/uploads/request-upload.use-case.spec.ts › replays an upload %s until %s with no policy
SVG, HEIC, an unknown type, more than 10 MiB, private, sensitive422 · assets.media_type_unsupported, assets.file_too_large, assets.access_class_unavailable; no row, no audittest/e2e/assets-uploads.e2e-spec.ts › refuses an svg, a heic and an unknown media type, a file over 10 MiB and a private or sensitive access class before writing anything
A declared size of 1.5, 0, -1 or NaN reaching the use caserefused before any transaction, answered 400 · foundation.validation_failed like the request schemasrc/modules/assets/application/uploads/request-upload.use-case.spec.ts › refuses a declared size of %s as invalid before opening a transaction; src/modules/assets/presentation/failures/to-public-assets-failure.spec.ts › answers a declared size that is not a positive whole number exactly as the request schema does
The plan's active asset limit, then its storage limit, reached422 · assets.asset_limit_reached, 422 · assets.storage_limit_reached; nothing writtentest/e2e/assets-uploads.e2e-spec.ts › refuses a new upload once the plan's asset or storage limit is reached, and lets two concurrent intents at the limit admit exactly one
A 21st open upload422 · assets.too_many_open_uploadstest/e2e/assets-uploads.e2e-spec.ts › caps the open uploads of an organization at 20
The storage group not configured, for the owner and for a cashier503 · assets.storage_unavailable, retryable, before authorizing: no reservation, no usage change, no audittest/e2e/assets-uploads.e2e-spec.ts › answers 503 when the storage is not configured, before authorizing and without reserving anything; src/modules/assets/application/uploads/request-upload.use-case.spec.ts › answers storage unavailable before authorizing or reserving when the signing group is absent
The signer fails after the commit503 · assets.storage_unavailable; the reservation stays and the same key replays with a fresh policysrc/modules/assets/infrastructure/storage/s3-upload-policy-signer.spec.ts › surfaces a signing failure as a storage failure that keeps the original error as its cause; src/modules/assets/presentation/failures/to-public-assets-failure.spec.ts › answers every assets refusal of the application with its public code and never the cause
The 31st intent of one client within a minute429test/e2e/assets-uploads.e2e-spec.ts › answers 429 to the 31st upload intent of one client within a minute
A cashier, a waiter, a manager of one business unit; then the owner after the plan lost assets.media403 · assets.action_forbidden three times, then 403 · platform.capability_not_entitled; each refusal audited as denied with its reason; the asset already uploaded stays readabletest/e2e/assets-access.e2e-spec.ts › refuses a cashier, a waiter and a manager of one business unit the plain denial and records it, and a lapsed plan the capability refusal
A refused memberthe denial audit is committed and nothing is reservedsrc/modules/assets/application/uploads/request-upload.use-case.spec.ts › commits the audited denial and reserves nothing when the member lacks the permission
Statement budget8 for an intent and 8 for its replaytest/e2e/statement-budget.e2e-spec.ts › requests an upload in 8 statements

AS2 — Complete an upload ​

POST /assets/uploads/{uploadId}/completion, no body; needs organization-wide assets.media.manage, never the plan. 8 statements.

CaseExpected answerProving test
An issued upload, then the same completion again202 with the asset processing; one process_upload job; the repeat answers the same body and writes nothing; no audittest/e2e/assets-uploads.e2e-spec.ts › completes an issued upload into processing, queues one processing job and answers repeats without writing
An expired upload, a rejected upload422 · assets.upload_expired, 422 · assets.upload_rejected; no job, the asset stays pendingtest/e2e/assets-uploads.e2e-spec.ts › refuses completing an expired or rejected upload, and an upload of another organization like an unknown id
The storage while completingnever calledsrc/modules/assets/application/uploads/complete-upload.use-case.spec.ts › never calls the object storage while completing an upload
A member without assets.media.manage403 · assets.action_forbidden before the upload is touchedsrc/modules/assets/application/uploads/complete-upload.use-case.spec.ts › refuses a member without assets.media.manage before touching the upload
Intent, read, completion, pollpending, then processing in the completion and in the poll, under a new tagtest/e2e/assets-uploads.e2e-spec.ts › takes a photo from intent to processing with the fake storage adapter in one journey
Statement budget8test/e2e/statement-budget.e2e-spec.ts › completes an upload in 8 statements

Library ​

AS3 — Read one asset ​

GET /assets/{assetId}; needs organization-wide assets.media.view, never the plan. Strong content tag, private, no-cache. 7 statements.

CaseExpected answerProving test
A ready public asset200, the master's width, height and size, the four a/<token>/p1/<name>.webp addresses under the public base URL, never m/ or in/; a strong tag; 304 with no body to that tag; a rename changes the tagtest/e2e/assets-library.e2e-spec.ts › reads a ready public asset with its rendition urls, never the master, under a strong tag and private no-cache, and 304 to that tag
A pending, a processing and a failed assetno address, no dimensions, the failure code of the failed onetest/e2e/assets-library.e2e-spec.ts › reads a pending, processing or failed asset with no urls and its failure code
Addresses only for ready and publicfour links for a ready public asset; none for pending, processing, failed, private or without a public base URLsrc/modules/assets/application/assets/asset-view.spec.ts › links the four public renditions of profile 1 of a ready public asset and never the master; src/modules/assets/application/assets/asset-view.spec.ts › links no rendition while the public base url is not configured
A member without assets.media.view; an unknown id403 · assets.action_forbidden before any read, not audited; 404 · assets.not_foundsrc/modules/assets/application/assets/read-asset.use-case.spec.ts › refuses a member without assets.media.view before reading; src/modules/assets/application/assets/read-asset.use-case.spec.ts › answers an unknown asset as not found
Statement budget7test/e2e/statement-budget.e2e-spec.ts › reads an asset in 7 statements

AS4 — List the media library ​

GET /assets?status=&archived=&after=&before=&limit=; needs organization-wide assets.media.view, never the plan. Keyset (created_at DESC, id DESC) on the partial indexes of A1a-1, signed cursors bound to the organization and the filters, private, no-cache, no ETag. 7 statements.

CaseExpected answerProving test
Three assets, one page at a timenewest first; after the first page reaches the next, before it comes back; hasNextPage and hasPreviousPage followtest/e2e/assets-library.e2e-spec.ts › lists the library newest first with keyset cursors, archived assets only when asked, and only the caller organization's assets
An archived assetabsent from the default listing; the only item of archived=true, with its archivedAttest/e2e/assets-library.e2e-spec.ts › lists the library newest first with keyset cursors, archived assets only when asked, and only the caller organization's assets
status=readyonly the ready asset, with only its thumbnail addresstest/e2e/assets-library.e2e-spec.ts › lists the library newest first with keyset cursors, archived assets only when asked, and only the caller organization's assets; src/modules/assets/presentation/dtos/asset-response.spec.ts › answers the asset as read with only its thumbnail rendition, and passes the strict list item schema
Five assets two at a time: forward two pages, then back twice with beforethe same pages in the same newest-first order; hasPreviousPage and hasNextPage per page; the page reached back carries the cursors it had going forward; an empty library answers both cursors nulltest/e2e/assets-library-paging.e2e-spec.ts › pages forward two pages and back with before, newest first, encoding the cursors of each page; src/modules/assets/infrastructure/assets/asset.repository.spec.ts › reads a page before a cursor position in ascending order inside and outside the variant join, and answers it newest first
A cursor of status=pending sent with status=ready or no status, one of the active listing sent with archived=true and back, one sent by another organization400 · collection.cursor_incompatible; the same filters continuetest/e2e/assets-library-paging.e2e-spec.ts › refuses a cursor replayed under another status or archived filter, or by another organization, as incompatible
Three ready assets with their original and four renditions each, two at a timeeach listed once; two, then onetest/e2e/assets-library-paging.e2e-spec.ts › lists a ready asset with its original and four renditions once, and pages three of them two at a time; src/modules/assets/infrastructure/assets/asset.repository.spec.ts › joins only the original variant, so a ready asset with its renditions is one row
A cursor sent with other filters, or by another organization400 · collection.cursor_incompatibletest/e2e/assets-library.e2e-spec.ts › lists the library newest first with keyset cursors, archived assets only when asked, and only the caller organization's assets
An unknown status, archived=yes, a limit of 0 or 101, both cursors, offset400 before any transactionsrc/modules/assets/presentation/dtos/asset-library.schemas.spec.ts › refuses an unknown status, a loose boolean, a limit out of range, both cursors and an unknown parameter; src/modules/assets/presentation/dtos/asset-library.schemas.spec.ts › lists the active assets of every status, 50 at a time, when nothing is sent
One pageone statement filtered by the caller organization and the archived state, one row more than the limit, never OFFSETsrc/modules/assets/infrastructure/assets/asset.repository.spec.ts › lists one page of the caller organization newest first, fetching one row more than the limit and never with OFFSET; src/modules/assets/infrastructure/assets/asset.repository.spec.ts › lists only archived assets when asked, and continues after a cursor position
The use casethe caller organization and the filters reach the repository; one transactionsrc/modules/assets/application/assets/list-assets.use-case.spec.ts › lists one page of the caller organization after checking assets.media.view, in one transaction and one read
A member without assets.media.view403 · assets.action_forbidden before listingsrc/modules/assets/application/assets/list-assets.use-case.spec.ts › refuses a member without assets.media.view before listing
Statement budget7test/e2e/statement-budget.e2e-spec.ts › lists the media library in 7 statements

AS5 — Rename an asset ​

PATCH /assets/{assetId} { displayName } with an optional If-Match; needs organization-wide assets.media.manage, never the plan. 10 statements, 7 when the name is unchanged.

CaseExpected answerProving test
A rename with the current tag200, the trimmed name, a new strong ETag equal to the next read's, private, no-store; one assets.asset.renamed audit with the name as after attributetest/e2e/assets-library.e2e-spec.ts › renames an asset with If-Match, records it once, and answers an identical rename without writing; src/modules/assets/application/assets/rename-asset.use-case.spec.ts › renames the locked asset, records the name before and after once, and answers the asset read again
A stale tag412 · foundation.precondition_failed, checked under the lock; nothing writtentest/e2e/assets-library.e2e-spec.ts › renames an asset with If-Match, records it once, and answers an identical rename without writing; src/modules/assets/application/assets/rename-asset.use-case.spec.ts › checks If-Match against the asset as locked and refuses a stale one before writing; src/modules/assets/presentation/failures/to-public-assets-failure.spec.ts › answers every assets refusal of the application with its public code and never the cause
If-Match absent; the current strong tag alone, in a list or *; a stale tag, its weak form, the bare tagno precondition; passes; refusedsrc/modules/assets/presentation/dtos/asset-entity-tag.spec.ts › builds no precondition when If-Match is absent; src/modules/assets/presentation/dtos/asset-entity-tag.spec.ts › passes the strong tag of the asset as GET /assets/{assetId} answers it, alone or in a list, and the * wildcard; src/modules/assets/presentation/dtos/asset-entity-tag.spec.ts › refuses a stale tag, the weak form of the current tag and a header that holds no well-formed tag
A malformed If-Match (not-a-tag, an unterminated quote, ,, W/)412 · foundation.precondition_failed as in every module (malformed entries are dropped, so nothing matches), never 500; nothing writtentest/e2e/assets-library-paging.e2e-spec.ts › refuses a malformed If-Match on a rename as a failed precondition, never a server error, and writes nothing
The current name again200, the same tag, nothing written or recordedtest/e2e/assets-library.e2e-spec.ts › renames an asset with If-Match, records it once, and answers an identical rename without writing; src/modules/assets/application/assets/rename-asset.use-case.spec.ts › answers an identical rename with the locked asset, writing and recording nothing
A blank, missing, null, NUL-bearing or 256-character name, another field400src/modules/assets/presentation/dtos/asset-library.schemas.spec.ts › refuses a missing, null, blank, too long or NUL-bearing name and any other field
A member without assets.media.manage403 · assets.action_forbidden before the locksrc/modules/assets/application/assets/rename-asset.use-case.spec.ts › refuses a member without assets.media.manage before locking the asset
Statement budget10 when it writes, 7 when unchangedtest/e2e/statement-budget.e2e-spec.ts › renames an asset in 10 statements, and in 7 when the name is unchanged

AS6 — Archive and restore an asset ​

POST /assets/{assetId}/archival and POST /assets/{assetId}/restoration, no body; needs organization-wide assets.media.manage, never the plan; through assets.archive_asset and assets.restore_asset (A1b-3). 9 statements, 8 for a repeat.

CaseExpected answerProving test
A ready asset attached to an item, on a lapsed plan200 with archivedAt; one active asset fewer; the read still answers its rendition addresses and the item keeps the image; one assets.asset.archived audittest/e2e/assets-library.e2e-spec.ts › archives and restores an asset on a lapsed plan, keeps an archived attached image deliverable, and refuses restoring after 30 days
Archiving it again; restoring it, then again200 each time; the repeats write and record nothing; the restoration counts it again, one assets.asset.restored audittest/e2e/assets-library.e2e-spec.ts › archives and restores an asset on a lapsed plan, keeps an archived attached image deliverable, and refuses restoring after 30 days; src/modules/assets/application/assets/change-asset-archival.use-case.spec.ts › answers archiving an archived asset and restoring an active one with the asset, recording nothing
Restoring 30 days and 1 minute after archival422 · assets.restoration_expired; it stays archivedtest/e2e/assets-library.e2e-spec.ts › archives and restores an asset on a lapsed plan, keeps an archived attached image deliverable, and refuses restoring after 30 days
Restoring at the plan's active asset limit422 · assets.asset_limit_reached; it stays archived, the count unchangedtest/e2e/assets-library.e2e-spec.ts › refuses restoring an asset when the asset limit is reached
Restoring a failed asset; archiving or restoring a purging one422 · assets.transition_invalid; nothing recordedtest/e2e/assets-archival.e2e-spec.ts › refuses restoring a failed asset and archiving or restoring a purging one as an invalid transition, writing nothing; database/tests/contracts/assets-lifecycle-contracts.ts › refuses archiving or restoring a purging asset and never lets its status leave purging
A refused transition in the use caseit reaches the failure mapping; the transaction rolls back with no auditsrc/modules/assets/application/assets/change-asset-archival.use-case.spec.ts › lets a refused transition reach the failure mapping, rolling back and recording nothing
A member without assets.media.manage403 · assets.action_forbidden before the definer functionsrc/modules/assets/application/assets/change-asset-archival.use-case.spec.ts › refuses a member without assets.media.manage before calling the definer function
Statement budget9, 8 for a repeat, on both routestest/e2e/statement-budget.e2e-spec.ts › archives and restores an asset in 9 statements, and in 8 for a repeat

Worker cases ​

Stages A3a to A3c and A4b: the media-worker process, no HTTP route.

AS7 — Process an upload ​

CaseExpected answerProving test
A completed uploadmaster in the private bucket, four immutable WebP renditions in the public one, the asset ready with sizes, checksums and stored bytes in one transaction, then the staging object removedtest/integration/assets-processing.integration-spec.ts › processes a completed upload into a ready asset whose master, renditions, sizes, checksums and stored bytes are recorded in one transaction, and removes the staging object; src/modules/assets/infrastructure/processing/process-upload.handler.spec.ts › writes the master to the private bucket and four immutable webp renditions to the public bucket, and answers ready with every variant
Content checksmagic bytes, declared type and decoded format must agree; pixel limits per formatsrc/modules/assets/application/processing/verify-and-master.spec.ts › requires the magic bytes, the declared type and the decoded format to agree; src/modules/assets/application/processing/verify-and-master.spec.ts › refuses an avif over 12 megapixels before decoding it, accepts one at exactly 12 megapixels and keeps 40 megapixels for jpeg and png
A bad uploadtruncated, mismatched, oversized, polyglot or wrongly sized fails for good, each reservation released once and each staging object removedtest/integration/assets-processing.integration-spec.ts › fails a truncated, a mismatched, an oversized, a polyglot and a wrongly sized upload, releasing each reservation once and removing each staging object

AS8 — Retry, fail and dead-letter processing ​

CaseExpected answerProving test
Retry classesa storage outage retries with capped backoff and no attempt cap; the fifth processing error dead-letterssrc/modules/assets/infrastructure/processing/process-upload.handler.spec.ts › retries a storage outage with capped backoff and no attempt cap, and dead-letters only the fifth processing error; src/modules/assets/application/processing/failure-classifier.spec.ts › backs storage retries off from 5 seconds by the total attempts, with a jitter of 20 percent, capped below 15 minutes
A staging object not there yeta storage error; the asset stays processingtest/integration/assets-processing.integration-spec.ts › retries an upload whose staging object has not arrived yet as a storage error and keeps the asset processing
Operator requeue (OD21)only through the operator function, only for a dead letter of the last day, with its reservation restoreddatabase/tests/contracts/assets-requeue-contracts.ts › requeues a dead-lettered processing job of the last day only through the operator function, restoring its reservation; database/tests/contracts/assets-dead-letter-contracts.ts › counts a reclaimed expired lease as a processing attempt and dead-letters the job at the fifth

AS9 — Expire abandoned uploads ​

CaseExpected answerProving test
An abandoned uploadexpired under the advisory lock, its staging object deleted, a missing object counted as donetest/integration/assets-processing.integration-spec.ts › expires an abandoned upload and deletes its staging object from the storage; src/modules/assets/infrastructure/worker/expire-uploads.job.spec.ts › expires uploads under an advisory lock and deletes their staging objects, treating a missing object as done
A staging delete that keeps failing; the nightly reconciliationthree attempts, then counted undeleted; reconciliation at 03:00 UTCsrc/modules/assets/application/processing/expire-abandoned-uploads.use-case.spec.ts › leaves an orphan staging object when every delete fails after the expiry committed, and counts it undeleted; src/modules/assets/infrastructure/worker/reconcile-usage.job.spec.ts › schedules the usage reconciliation nightly at 03:00 UTC

AS10 — Purge an asset ​

CaseExpected answerProving test
An asset archived more than 30 days agoenqueued by the hourly sweep, objects deleted, four public URLs purged from the CDN, usage exacttest/integration/assets-purge.integration-spec.ts › enqueues an asset archived more than 30 days ago, deletes its objects, purges its four public urls and leaves the usage counters exact; src/modules/assets/infrastructure/worker/enqueue-purge-candidates.job.spec.ts › sweeps every hour, enqueuing batches of 500 until a batch comes back short
A referenced asset; a failed onethe referenced asset and its objects kept and retried a month later; a failed one purged after 7 days without the CDNtest/integration/assets-purge.integration-spec.ts › answers referenced for an asset a catalog item uses, keeps it and its objects, and retries it only a month later; test/integration/assets-purge.integration-spec.ts › purges an asset that failed more than 7 days ago without calling the cdn
CDN unconfigured or URLs not derivablenothing deleted; retried, or dead-lettered after 14 days or when the URLs cannot be builtsrc/modules/assets/application/processing/purge-asset.handler.spec.ts › deletes nothing and retries later while the cdn purge is unconfigured for an asset that was ever public; src/modules/assets/application/processing/purge-asset.handler.spec.ts › dead-letters the job without deleting anything when the public urls cannot be derived

Contracts ​

Stage A5a (no migration): the two published contracts another module calls inside its own tenant transaction; Catalog consumes both since A5b (CATALOG_CASE_MATRIX.md, Images).

AS12 — Describe delivery ​

CaseExpected answerProving test
Up to 2,000 ids, ready public and other assetsone statement; imageRef and URLs only for a ready public asset, the others keep their status with null; a foreign id is absentsrc/modules/assets/infrastructure/delivery/asset-delivery.repository.integration-spec.ts › describes up to 2000 assets in one statement, with urls only for ready public assets; src/modules/assets/application/delivery/asset-delivery.spec.ts › gives an image reference and urls only to a ready public asset
No public base URL; more than 2,000 ids; an empty requestimageRef kept, no URLs or template; refused before any SQL; an empty map with no statementsrc/modules/assets/application/delivery/asset-delivery.spec.ts › keeps the image reference but gives no urls or template when no public base url is configured; src/modules/assets/application/delivery/asset-delivery.spec.ts › refuses more than 2000 ids and answers an empty request, both without reading the database
Template and fingerprintthe tile template matches the tile URL; the fingerprint changes with the base URL and the profile listsrc/modules/assets/contracts/asset-delivery.contract.spec.ts › builds the tile url of an image reference with the template it publishes; src/modules/assets/contracts/asset-delivery.contract.spec.ts › changes the fingerprint when the public base url changes; src/modules/assets/application/delivery/asset-delivery.spec.ts › keeps the fingerprint for the same base url and profile list and changes it with the profile list
What a consumer seesno key, bucket, provider or variant idsrc/modules/assets/contracts/asset-delivery.contract.spec.ts › exposes no storage key, bucket, provider or variant id to a consumer

AS13 — Check attachability ​

CaseExpected answerProving test
Not ready, archived, purging, private or foreign assetsone statement FOR SHARE in id order, one refusal per asset with its reason, the lock held until commitsrc/modules/assets/infrastructure/usage/asset-usage.repository.integration-spec.ts › refuses an asset that is not ready, archived, purging, private or of another organization, each with its reason, and holds a share lock until commit; src/modules/assets/application/usage/asset-usage.spec.ts › names the first refusal that applies, in the order not found, purging, archived, not ready, wrong media kind, wrong access class
One refused id among several; a missing id repeatedthe whole request refused naming every refused id, each distinct id oncesrc/modules/assets/application/usage/asset-usage.spec.ts › refuses the whole request with every refused id when one of several ids is attachable; src/modules/assets/application/usage/asset-usage.spec.ts › reports one refusal per distinct requested id when a missing id is repeated
Repeated attachable ids; an empty requesteach id once at its first position, in request order; no statementsrc/modules/assets/application/usage/asset-usage.spec.ts › answers the attachable assets in the order of the requested ids, each id once at its first position; src/modules/assets/application/usage/asset-usage.spec.ts › answers an empty request without reading the database
The share lock needs a column update grantthe runtime locks FOR SHARE through its UPDATE (display_name) grantdatabase/tests/contracts/assets-lifecycle-contracts.ts › lets the runtime lock assets for share, which needs its column update grant on display_name
Wiringthe contract module exports exactly the two contracts, bound to the working servicessrc/modules/assets/contracts/assets-contracts.module.spec.ts › exports exactly the usage and delivery contracts to an importing module; src/modules/assets/contracts/assets-contracts.module.spec.ts › resolves both contracts to the working services, wired to their repositories and the configured base url

Cross-tenant and concurrency invariants ​

InvariantExpected answerProving test
Two intents of one organization at its asset limit raceexactly one 201 and one 422 · assets.asset_limit_reached; the counter ends at the limittest/e2e/assets-uploads.e2e-spec.ts › refuses a new upload once the plan's asset or storage limit is reached, and lets two concurrent intents at the limit admit exactly one
An asset of another organizationthe same 404 · assets.not_found problem as an unknown id, no tagtest/e2e/assets-library.e2e-spec.ts › answers an asset of another organization exactly like an unknown id
An upload of another organizationthe same 404 · assets.not_found problem as an unknown idtest/e2e/assets-uploads.e2e-spec.ts › refuses completing an expired or rejected upload, and an upload of another organization like an unknown id
An asset of another organization renamed, archived or restored404 · assets.not_found, nothing writtentest/e2e/assets-library.e2e-spec.ts › renames an asset with If-Match, records it once, and answers an identical rename without writing; test/e2e/assets-library.e2e-spec.ts › archives and restores an asset on a lapsed plan, keeps an archived attached image deliverable, and refuses restoring after 30 days
The library of another organizationnever listed; its cursor refusedtest/e2e/assets-library.e2e-spec.ts › lists the library newest first with keyset cursors, archived assets only when asked, and only the caller organization's assets
Every assets refusalits public code; no SQL, constraint or storage detail in the answersrc/modules/assets/presentation/failures/to-public-assets-failure.spec.ts › answers every assets refusal of the application with its public code and never the cause

Gaps ​

None for the stages built. AS11 (regenerate renditions for a new profile) joins this page with the stage that builds it; none is planned until profile 2 is designed.

Application Foundation in progress. Tracked in issue #13.