Follows #852. Nothing has been in GridFS since #783 (epic #413), but the persisted names, the API JSON, the route params and the Angular model all still say gridFsId. That misleading name is what made #849 look safe and took every Art Studio image to a 404.
PR #484 restored production with PropertyNameCaseInsensitive = true. That shim is a hack and this ticket removes it. Goal: zero gridFsId in the Art Studio path.
objectKey is not a new coinage — ArtAssetGenerationParams already persists objectKey. This aligns the rest of Art Studio with it.
Two structural facts that shape the work
ArtAssetArtifactRef is persisted two different ways.ArtAsset.Artifacts is an EF JSON-string column (SpikerDbContext.cs:845) where [BsonElement] is inert and [JsonPropertyName] is live. ArtAsset.QuarantinedArtifacts is a native BSON array where the reverse is true. Both attributes must move in lockstep, and the migration needs two mechanisms — $rename works for the native array, but is impossible for the JSON string, which must be parsed and rewritten per document.
Every write already emits canonical lowercase. All mutations go through EF with default options (primary writer ArtPipeStageOrchestrator.cs:553); there is not one raw-driver write to art-assets. Documents self-heal PascalCase → lowercase on any save, so the migration must handle both casings, not just the PascalCase visible today.
De-risking: there are no Mongo indexes, filters, projections or raw "gridFsId" query literals anywhere in the repo.
Production data (read live)
8 documents, 92 artifact objects, artifacts a JSON string on all 8, all PascalCase today. 69 also persist Url/ThumbnailUrl/DisplayUrl — all null. Those are [BsonIgnore] transient presigned URLs; [BsonIgnore] does not apply to a JSON column, so they are being written. Harmless today, but a save after minting would put a real signed URL in the DB. quarantinedArtifacts all empty; sourceImageGridFsId all null; art-variant-upload-jobs empty.
Delete LegacyTolerantArtifactJson; one shared explicit JsonSerializerOptions for read and write so they can never diverge again
Routes: {gridFsId} → {objectKey} on the artifact, gallery and quarantine downloads; {sourceGridFsId} → {sourceObjectKey} on the variant POST
Worker contracts (cross RabbitMQ — drain queues in the window): RequestArtAssetStageCommand, ProcessUploadedArtVariantCommand
Fix GetArtVariantJobStatusQuery.SourceArtifactKey, which #849 already left serializing as sourceArtifactKey while Angular declares sourceArtifactGridFsId — dormant, but fix rather than perpetuate
Migration — mongodb-scripts/2026-07-migrate-art-assets-object-key.js, modelled on 2026-07-migrate-userbookmarks-userid.js. Handles both casings, drops the transient URL fields, $renames the native fields, DRY_RUN defaults true, residual assertions expect 0.
Angular — model rename, plus a key guard in the four URL builders. No call site guards the key today, which is why a missing value became /artifacts//download instead of a visible error.
Tests — ArtStudioStorageKeyNamingTests exists to fail if someone "finishes the job"; it gets inverted, not deleted. Legacy fixtures must use literal JSON, never JsonSerializer.Serialize(currentModel) — that round-trip blind spot is how #849 shipped green.
Cutover (agreed: hard cutover, no dual-emit)
mongodumpart-assets
Drain ArtStudio RabbitMQ queues
Maintenance window: run migration → deploy backend → deploy Angular
Hard refresh, verify images
Rollback: restore the dump, redeploy previous images.
Acceptance
Every artifact has a non-empty objectKey; no gridFsId anywhere in the API response; every thumbnail 200; zero /artifacts//download or /artifacts/undefined/download requests; cards render real thumbnails rather than the chair fallback.
Follows #852. Nothing has been in GridFS since #783 (epic #413), but the persisted names, the API JSON, the route params and the Angular model all still say `gridFsId`. That misleading name is what made #849 look safe and took every Art Studio image to a 404.
PR #484 restored production with `PropertyNameCaseInsensitive = true`. **That shim is a hack and this ticket removes it.** Goal: zero `gridFsId` in the Art Studio path.
`objectKey` is not a new coinage — `ArtAssetGenerationParams` already persists `objectKey`. This aligns the rest of Art Studio with it.
## Two structural facts that shape the work
1. **`ArtAssetArtifactRef` is persisted two different ways.** `ArtAsset.Artifacts` is an EF JSON-string column (`SpikerDbContext.cs:845`) where `[BsonElement]` is **inert** and `[JsonPropertyName]` is live. `ArtAsset.QuarantinedArtifacts` is a **native BSON array** where the reverse is true. Both attributes must move in lockstep, and the migration needs two mechanisms — `$rename` works for the native array, but is impossible for the JSON string, which must be parsed and rewritten per document.
2. **Every write already emits canonical lowercase.** All mutations go through EF with default options (primary writer `ArtPipeStageOrchestrator.cs:553`); there is not one raw-driver write to `art-assets`. Documents self-heal PascalCase → lowercase on any save, so **the migration must handle both casings**, not just the PascalCase visible today.
De-risking: there are **no** Mongo indexes, filters, projections or raw `"gridFsId"` query literals anywhere in the repo.
## Production data (read live)
8 documents, 92 artifact objects, `artifacts` a JSON string on all 8, all PascalCase today. 69 also persist `Url`/`ThumbnailUrl`/`DisplayUrl` — **all null**. Those are `[BsonIgnore]` transient presigned URLs; `[BsonIgnore]` does not apply to a JSON column, so they are being written. Harmless today, but a save after minting would put a real signed URL in the DB. `quarantinedArtifacts` all empty; `sourceImageGridFsId` all null; `art-variant-upload-jobs` empty.
## Scope
**Backend**
- `ArtAssetArtifactRef.ObjectKey` → `objectKey` (both attributes); `SourceArtifactKey` → `sourceArtifactKey`; `ArtAsset.SourceImageKey` → `sourceImageKey`
- Explicit `[JsonPropertyName]` on **every** property of `ArtAssetArtifactRef` — only two are pinned today, so the rest serialize under CLR names by accident
- `[JsonIgnore]` on `Url`/`ThumbnailUrl`/`DisplayUrl`
- `[JsonRequired]` on `ObjectKey` so a missing key **throws** instead of silently defaulting to `""` — the guarantee #852 lacked
- `ArtVariantUploadJob`: `sourceArtifactGridFsId` → `sourceArtifactKey`, `stagedGridFsId` → `stagedObjectKey`
- Delete `LegacyTolerantArtifactJson`; one shared explicit `JsonSerializerOptions` for read **and** write so they can never diverge again
- Routes: `{gridFsId}` → `{objectKey}` on the artifact, gallery and quarantine downloads; `{sourceGridFsId}` → `{sourceObjectKey}` on the variant POST
- Worker contracts (**cross RabbitMQ** — drain queues in the window): `RequestArtAssetStageCommand`, `ProcessUploadedArtVariantCommand`
- Fix `GetArtVariantJobStatusQuery.SourceArtifactKey`, which #849 already left serializing as `sourceArtifactKey` while Angular declares `sourceArtifactGridFsId` — dormant, but fix rather than perpetuate
- Fix the broken XML `cref`s #849 left behind
**Migration** — `mongodb-scripts/2026-07-migrate-art-assets-object-key.js`, modelled on `2026-07-migrate-userbookmarks-userid.js`. Handles both casings, drops the transient URL fields, `$renames` the native fields, `DRY_RUN` defaults true, residual assertions expect 0.
**Angular** — model rename, plus a **key guard** in the four URL builders. No call site guards the key today, which is why a missing value became `/artifacts//download` instead of a visible error.
**Tests** — `ArtStudioStorageKeyNamingTests` exists to fail if someone "finishes the job"; it gets **inverted**, not deleted. Legacy fixtures must use **literal JSON**, never `JsonSerializer.Serialize(currentModel)` — that round-trip blind spot is how #849 shipped green.
## Cutover (agreed: hard cutover, no dual-emit)
1. `mongodump` `art-assets`
2. Drain ArtStudio RabbitMQ queues
3. Maintenance window: run migration → deploy backend → deploy Angular
4. Hard refresh, verify images
Rollback: restore the dump, redeploy previous images.
## Acceptance
Every artifact has a non-empty `objectKey`; no `gridFsId` anywhere in the API response; every thumbnail 200; zero `/artifacts//download` or `/artifacts/undefined/download` requests; cards render real thumbnails rather than the `chair` fallback.
Sibling fields intact. A second run reports 0 changes — idempotent. Scratch DB dropped afterwards.
The proof that images will not break
The migrated documents were fed through the new model and converter options — the exact mapping path production uses:
all 92 artifacts bind with a non-empty objectKey, correct kind, and url/thumbnailUrl/displayUrl all null (the transient presigned URLs are no longer persisted).
That is as close to "images work" as is possible before deploying. The remaining check is the live one in the acceptance criteria, which runs after the cutover.
Also fixed along the way
[BsonIgnore] now genuinely means "never persisted" on the JSON-column path. System.Text.Json knew nothing about it, so the transient presigned URLs documented as never persisted were being written into every document. Null today, but a save after minting would have stored an expiring signed URL. Scoped to the storage options — API responses still carry them.
The Angular side had no guard at all on the storage key. That is why #852 produced /artifacts//download rather than a visible error. One guard at four builders now covers all twelve call sites.
ArtVariantJobStatusDto — a contract #849 had already broken (backend serialized sourceArtifactKey, Angular declared sourceArtifactGridFsId). Dormant; now aligned.
tools/BackfillPhotoStackDerivatives referenced the property #849 removed and only compiled because it is not in the solution.
Verification summary
dotnet build SpikerSoft.UnitTests.slnf
0 errors
ArtStudio/ArtPipe backend tests
1055 green
nx test feature-art-studio
432 green
nx test spikersoft
3428 green
nx build spikersoft
bundle complete
lint:fix + lint:styles:fix
clean
Cutover order (unchanged)
mongodump art-assets
Drain / confirm-empty the ArtStudio RabbitMQ queues — the worker contracts renamed, and a pre-#853 message will not bind (there is a test asserting exactly that, so the requirement can't be forgotten)
Run the migration with DRY_RUN = false
Deploy backend, then Angular
Hard refresh, then the live verification
Migration must precede the backend deploy: ObjectKey is [JsonRequired], so an un-migrated document fails loudly. That is intended.
## Work complete — both PRs ready, held as WIP pending the cutover
- backend **#485** `fix/artstudio-object-key-rename-853`
- angular **#579** `fix/artstudio-object-key-rename-853`
The `#852` shim (`PropertyNameCaseInsensitive`) is **deleted**.
## Migration rehearsed against real production data
`mongodump` of `art-assets` restored into a scratch DB, migration run for real there:
```
documents scanned : 8
documents needing a rewrite : 8
transient URL fields dropped : 207 (69 artifacts x 3)
unrecognised keys : none
every residual assertion : 0
```
Then compared prod vs migrated, artifact by artifact:
```
artifacts compared : 92
key mismatches : 0
empty objectKeys : 0
```
Sibling fields intact. A second run reports **0 changes** — idempotent. Scratch DB dropped afterwards.
## The proof that images will not break
The migrated documents were fed through the **new** model and converter options — the exact mapping path production uses:
> all 92 artifacts bind with a non-empty `objectKey`, correct `kind`, and `url`/`thumbnailUrl`/`displayUrl` all null (the transient presigned URLs are no longer persisted).
That is as close to "images work" as is possible before deploying. The remaining check is the live one in the acceptance criteria, which runs after the cutover.
## Also fixed along the way
- `[BsonIgnore]` now genuinely means "never persisted" on the JSON-column path. System.Text.Json knew nothing about it, so the transient presigned URLs documented as *never persisted* were being written into every document. Null today, but a save after minting would have stored an expiring signed URL. Scoped to the storage options — API responses still carry them.
- The Angular side had **no guard at all** on the storage key. That is why #852 produced `/artifacts//download` rather than a visible error. One guard at four builders now covers all twelve call sites.
- `ArtVariantJobStatusDto` — a contract #849 had already broken (backend serialized `sourceArtifactKey`, Angular declared `sourceArtifactGridFsId`). Dormant; now aligned.
- `tools/BackfillPhotoStackDerivatives` referenced the property #849 removed and only compiled because it is not in the solution.
## Verification summary
| | |
|---|---|
| `dotnet build SpikerSoft.UnitTests.slnf` | 0 errors |
| ArtStudio/ArtPipe backend tests | 1055 green |
| `nx test feature-art-studio` | 432 green |
| `nx test spikersoft` | 3428 green |
| `nx build spikersoft` | bundle complete |
| lint:fix + lint:styles:fix | clean |
## Cutover order (unchanged)
1. `mongodump art-assets`
2. Drain / confirm-empty the ArtStudio RabbitMQ queues — the worker contracts renamed, and a pre-#853 message will **not** bind (there is a test asserting exactly that, so the requirement can't be forgotten)
3. Run the migration with `DRY_RUN = false`
4. Deploy backend, then Angular
5. Hard refresh, then the live verification
Migration must precede the backend deploy: `ObjectKey` is `[JsonRequired]`, so an un-migrated document fails loudly. That is intended.
Resolved in spikersoft-backend PR #485 (8071ff66) + spikersoft-angular PR #579 (d1de0dca), both merged 2026-07-26 and shipped in v2026.07.26b / v2026.07.26a.
Correcting the record: these were previously noted as "held WIP". They are not — they merged, and releases have continued uninterrupted through v2026.07.29a. Since ObjectKey is [JsonRequired], an un-migrated document would hard-fail every Art Studio read, and no follow-up incident was filed, so the cutover evidently completed.
Verified item by item against origin/master:
Both attributes pinned to objectKey (ArtAsset.cs:447-450) with [JsonRequired] (:449); sourceArtifactKey (:540); sourceImageKey (:60); explicit [JsonPropertyName] on every persisted property of ArtAssetArtifactRef.
ArtVariantUploadJob renamed (:33-38); one shared serializer-options instance; all four routes renamed (ArtStudioController.cs:478,515,699,1436); both worker contracts renamed; GetArtVariantJobStatusQuery.SourceArtifactKey aligned.
The #852 shim is gone — SpikerDbContext.cs:884-891 now uses the single shared ArtifactColumnJson (:33-39) for read and write, deliberately case-sensitive. LegacyTolerantArtifactJson no longer exists.
Angular: objectKey on the model (art-studio.models.ts:103), requireObjectKey guard (art-studio.service.ts:680-685) called from all four builders (:689,709,793,886), zero gridFsId left in the Art Studio lib.
One implementation improvement over the plan worth recording: instead of [JsonIgnore] on the transient URLs, it used a storage-scoped TypeInfoResolver honouring [BsonIgnore] (SpikerDbContext.cs:33-39,60). [JsonIgnore] would have stripped those fields from API responses too — this was the right call, and both halves are test-pinned.
Small residuals, none affecting acceptance: no legacy-bind test for ProcessUploadedArtVariantCommand (one exists for RequestArtAssetStageCommand at ArtStudioStorageKeyNamingTests.cs:202-215); stale route shapes in doc comments at GetArtAssetManifestQuery.cs:17-18 and S3ArtAssetFileStore.cs:17. The ~101 remaining gridFsId hits under Art Studio paths are local variables, log templates, test names and intentional legacy fixtures — zero in Mongos/ArtStudio/* or Contracts.Workers/ArtStudio/*.
The live acceptance items ("every thumbnail 200", "zero /artifacts//download") are post-deploy checks not verifiable from a git tree.
Closing.
Resolved in spikersoft-backend PR #485 (`8071ff66`) + spikersoft-angular PR #579 (`d1de0dca`), both merged 2026-07-26 and shipped in `v2026.07.26b` / `v2026.07.26a`.
**Correcting the record:** these were previously noted as "held WIP". They are not — they merged, and releases have continued uninterrupted through `v2026.07.29a`. Since `ObjectKey` is `[JsonRequired]`, an un-migrated document would hard-fail every Art Studio read, and no follow-up incident was filed, so the cutover evidently completed.
Verified item by item against `origin/master`:
- Both attributes pinned to `objectKey` (`ArtAsset.cs:447-450`) with `[JsonRequired]` (`:449`); `sourceArtifactKey` (`:540`); `sourceImageKey` (`:60`); explicit `[JsonPropertyName]` on every persisted property of `ArtAssetArtifactRef`.
- `ArtVariantUploadJob` renamed (`:33-38`); one shared serializer-options instance; all four routes renamed (`ArtStudioController.cs:478,515,699,1436`); both worker contracts renamed; `GetArtVariantJobStatusQuery.SourceArtifactKey` aligned.
- **The #852 shim is gone** — `SpikerDbContext.cs:884-891` now uses the single shared `ArtifactColumnJson` (`:33-39`) for read and write, deliberately case-sensitive. `LegacyTolerantArtifactJson` no longer exists.
- Migration `mongodb-scripts/2026-07-migrate-art-assets-object-key.js`: `DRY_RUN = true` default (`:36`), both-casing handling (`:87`), transient-URL dropping (`:88-91`), `$rename` on native fields (`:126-129`, `:195-200`), zero-residual assertions.
- Angular: `objectKey` on the model (`art-studio.models.ts:103`), `requireObjectKey` guard (`art-studio.service.ts:680-685`) called from all four builders (`:689,709,793,886`), zero `gridFsId` left in the Art Studio lib.
One implementation improvement over the plan worth recording: instead of `[JsonIgnore]` on the transient URLs, it used a storage-scoped `TypeInfoResolver` honouring `[BsonIgnore]` (`SpikerDbContext.cs:33-39,60`). `[JsonIgnore]` would have stripped those fields from API responses too — this was the right call, and both halves are test-pinned.
Small residuals, none affecting acceptance: no legacy-bind test for `ProcessUploadedArtVariantCommand` (one exists for `RequestArtAssetStageCommand` at `ArtStudioStorageKeyNamingTests.cs:202-215`); stale route shapes in doc comments at `GetArtAssetManifestQuery.cs:17-18` and `S3ArtAssetFileStore.cs:17`. The ~101 remaining `gridFsId` hits under Art Studio paths are local variables, log templates, test names and intentional legacy fixtures — zero in `Mongos/ArtStudio/*` or `Contracts.Workers/ArtStudio/*`.
The live acceptance items ("every thumbnail 200", "zero `/artifacts//download`") are post-deploy checks not verifiable from a git tree.
Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follows #852. Nothing has been in GridFS since #783 (epic #413), but the persisted names, the API JSON, the route params and the Angular model all still say
gridFsId. That misleading name is what made #849 look safe and took every Art Studio image to a 404.PR #484 restored production with
PropertyNameCaseInsensitive = true. That shim is a hack and this ticket removes it. Goal: zerogridFsIdin the Art Studio path.objectKeyis not a new coinage —ArtAssetGenerationParamsalready persistsobjectKey. This aligns the rest of Art Studio with it.Two structural facts that shape the work
ArtAssetArtifactRefis persisted two different ways.ArtAsset.Artifactsis an EF JSON-string column (SpikerDbContext.cs:845) where[BsonElement]is inert and[JsonPropertyName]is live.ArtAsset.QuarantinedArtifactsis a native BSON array where the reverse is true. Both attributes must move in lockstep, and the migration needs two mechanisms —$renameworks for the native array, but is impossible for the JSON string, which must be parsed and rewritten per document.ArtPipeStageOrchestrator.cs:553); there is not one raw-driver write toart-assets. Documents self-heal PascalCase → lowercase on any save, so the migration must handle both casings, not just the PascalCase visible today.De-risking: there are no Mongo indexes, filters, projections or raw
"gridFsId"query literals anywhere in the repo.Production data (read live)
8 documents, 92 artifact objects,
artifactsa JSON string on all 8, all PascalCase today. 69 also persistUrl/ThumbnailUrl/DisplayUrl— all null. Those are[BsonIgnore]transient presigned URLs;[BsonIgnore]does not apply to a JSON column, so they are being written. Harmless today, but a save after minting would put a real signed URL in the DB.quarantinedArtifactsall empty;sourceImageGridFsIdall null;art-variant-upload-jobsempty.Scope
Backend
ArtAssetArtifactRef.ObjectKey→objectKey(both attributes);SourceArtifactKey→sourceArtifactKey;ArtAsset.SourceImageKey→sourceImageKey[JsonPropertyName]on every property ofArtAssetArtifactRef— only two are pinned today, so the rest serialize under CLR names by accident[JsonIgnore]onUrl/ThumbnailUrl/DisplayUrl[JsonRequired]onObjectKeyso a missing key throws instead of silently defaulting to""— the guarantee #852 lackedArtVariantUploadJob:sourceArtifactGridFsId→sourceArtifactKey,stagedGridFsId→stagedObjectKeyLegacyTolerantArtifactJson; one shared explicitJsonSerializerOptionsfor read and write so they can never diverge again{gridFsId}→{objectKey}on the artifact, gallery and quarantine downloads;{sourceGridFsId}→{sourceObjectKey}on the variant POSTRequestArtAssetStageCommand,ProcessUploadedArtVariantCommandGetArtVariantJobStatusQuery.SourceArtifactKey, which #849 already left serializing assourceArtifactKeywhile Angular declaressourceArtifactGridFsId— dormant, but fix rather than perpetuatecrefs #849 left behindMigration —
mongodb-scripts/2026-07-migrate-art-assets-object-key.js, modelled on2026-07-migrate-userbookmarks-userid.js. Handles both casings, drops the transient URL fields,$renamesthe native fields,DRY_RUNdefaults true, residual assertions expect 0.Angular — model rename, plus a key guard in the four URL builders. No call site guards the key today, which is why a missing value became
/artifacts//downloadinstead of a visible error.Tests —
ArtStudioStorageKeyNamingTestsexists to fail if someone "finishes the job"; it gets inverted, not deleted. Legacy fixtures must use literal JSON, neverJsonSerializer.Serialize(currentModel)— that round-trip blind spot is how #849 shipped green.Cutover (agreed: hard cutover, no dual-emit)
mongodumpart-assetsRollback: restore the dump, redeploy previous images.
Acceptance
Every artifact has a non-empty
objectKey; nogridFsIdanywhere in the API response; every thumbnail 200; zero/artifacts//downloador/artifacts/undefined/downloadrequests; cards render real thumbnails rather than thechairfallback.Work complete — both PRs ready, held as WIP pending the cutover
fix/artstudio-object-key-rename-853fix/artstudio-object-key-rename-853The
#852shim (PropertyNameCaseInsensitive) is deleted.Migration rehearsed against real production data
mongodumpofart-assetsrestored into a scratch DB, migration run for real there:Then compared prod vs migrated, artifact by artifact:
Sibling fields intact. A second run reports 0 changes — idempotent. Scratch DB dropped afterwards.
The proof that images will not break
The migrated documents were fed through the new model and converter options — the exact mapping path production uses:
That is as close to "images work" as is possible before deploying. The remaining check is the live one in the acceptance criteria, which runs after the cutover.
Also fixed along the way
[BsonIgnore]now genuinely means "never persisted" on the JSON-column path. System.Text.Json knew nothing about it, so the transient presigned URLs documented as never persisted were being written into every document. Null today, but a save after minting would have stored an expiring signed URL. Scoped to the storage options — API responses still carry them./artifacts//downloadrather than a visible error. One guard at four builders now covers all twelve call sites.ArtVariantJobStatusDto— a contract #849 had already broken (backend serializedsourceArtifactKey, Angular declaredsourceArtifactGridFsId). Dormant; now aligned.tools/BackfillPhotoStackDerivativesreferenced the property #849 removed and only compiled because it is not in the solution.Verification summary
dotnet build SpikerSoft.UnitTests.slnfnx test feature-art-studionx test spikersoftnx build spikersoftCutover order (unchanged)
mongodump art-assetsDRY_RUN = falseMigration must precede the backend deploy:
ObjectKeyis[JsonRequired], so an un-migrated document fails loudly. That is intended.Resolved in spikersoft-backend PR #485 (
8071ff66) + spikersoft-angular PR #579 (d1de0dca), both merged 2026-07-26 and shipped inv2026.07.26b/v2026.07.26a.Correcting the record: these were previously noted as "held WIP". They are not — they merged, and releases have continued uninterrupted through
v2026.07.29a. SinceObjectKeyis[JsonRequired], an un-migrated document would hard-fail every Art Studio read, and no follow-up incident was filed, so the cutover evidently completed.Verified item by item against
origin/master:objectKey(ArtAsset.cs:447-450) with[JsonRequired](:449);sourceArtifactKey(:540);sourceImageKey(:60); explicit[JsonPropertyName]on every persisted property ofArtAssetArtifactRef.ArtVariantUploadJobrenamed (:33-38); one shared serializer-options instance; all four routes renamed (ArtStudioController.cs:478,515,699,1436); both worker contracts renamed;GetArtVariantJobStatusQuery.SourceArtifactKeyaligned.SpikerDbContext.cs:884-891now uses the single sharedArtifactColumnJson(:33-39) for read and write, deliberately case-sensitive.LegacyTolerantArtifactJsonno longer exists.mongodb-scripts/2026-07-migrate-art-assets-object-key.js:DRY_RUN = truedefault (:36), both-casing handling (:87), transient-URL dropping (:88-91),$renameon native fields (:126-129,:195-200), zero-residual assertions.objectKeyon the model (art-studio.models.ts:103),requireObjectKeyguard (art-studio.service.ts:680-685) called from all four builders (:689,709,793,886), zerogridFsIdleft in the Art Studio lib.One implementation improvement over the plan worth recording: instead of
[JsonIgnore]on the transient URLs, it used a storage-scopedTypeInfoResolverhonouring[BsonIgnore](SpikerDbContext.cs:33-39,60).[JsonIgnore]would have stripped those fields from API responses too — this was the right call, and both halves are test-pinned.Small residuals, none affecting acceptance: no legacy-bind test for
ProcessUploadedArtVariantCommand(one exists forRequestArtAssetStageCommandatArtStudioStorageKeyNamingTests.cs:202-215); stale route shapes in doc comments atGetArtAssetManifestQuery.cs:17-18andS3ArtAssetFileStore.cs:17. The ~101 remaininggridFsIdhits under Art Studio paths are local variables, log templates, test names and intentional legacy fixtures — zero inMongos/ArtStudio/*orContracts.Workers/ArtStudio/*.The live acceptance items ("every thumbnail 200", "zero
/artifacts//download") are post-deploy checks not verifiable from a git tree.Closing.