Storage reorganisation · validation
Validation
The plan measured against the live databases and the three repositories, step by step. Every figure below was read back on 2026-09-08 — nothing is carried over from the design.
- Values verified
- 1,488,632Across 32 columns and JSON key paths
- Complete paths
- 1,488,59499.997% of the set
- Bare names left
- 38All pre-existing, all in one column
- Steps complete
- 2 of 6Two built but not run, two unbuilt
- Gates passed
- 0 of 3Gate 1 substantially met
The data half of the plan holds up. Every column and JSON key the design lists now stores a complete path, the conversion preserved list order, and the only bare names left predate the work and are present identically in production. Steps 1 and 2 are real.
The object half has not started. The copier is built but has never been run, and the two remaining steps are unbuilt. Nothing has moved in the bucket — which is exactly where the plan says Gate 1 should leave things.
Step by step
| Step | State | Evidence |
|---|---|---|
| Step 1 — store the full file path | complete | 9 columns retyped, 17 backfilled, 32 JSON key paths rewritten. 1,488,632 values re-counted; 38 bare, all pre-existing. |
| Step 2 — one rule finds every file | partial | upload-targets.ts is the single rule in the API; the web app mirrors it. The mobile leg carries none of this work. |
| Gate 1 | partial | Zero files moved and the rule is in place. Open: the mobile leg, and evidence the step-1 reversal has been run once. |
| Step 3 — thumbnails learn both shapes | deployed · running | Deployed as a Cloud Function on the finalize event; 78 of 78 tests pass. Also mirrors new-layout images back to their legacy folder. |
| Step 4 — new uploads change shape | partial | Staging works as designed. The flag does not exist — finalPrefixFor is unconditional, so the behaviour is already live. |
| Gate 2 | not started | The thumbnail and reclaim halves are both live. Open: the phone half, which needs the mobile leg of step 2. |
| Step 5 — copy the existing files | built · not run | CAP-680 built seed, run, write-back and rollback. Both ledger tables hold 0 rows. |
| Gate 3 | not started | Not performed. No account copied, no rollback exercised on live data. |
| Step 6 — keep and watch the old folders | not started | No legacy-read logging exists. Nothing writes to the legacy folders any more, which is the first half by construction. |
Read back out of the database
Every value in the columns and keys below, counted by whether it contains a slash. clone1 carries every migration; capClone is the production comparison.
| Column or key | Values | Complete path | Bare |
|---|---|---|---|
| inventory_sticker_details.sticker_name | 757,840 | 757,840 | 0 |
| camp_inventory_items.images | 678,643 | 678,605 | 38 |
| biomeds.extracted_data → biomedFile | 20,052 | 20,052 | 0 |
| copilot_launch_proposals.quote | 9,044 | 9,044 | 0 |
| equipment_models.images | 8,280 | 8,280 | 0 |
| equipment_models.files | 5,088 | 5,088 | 0 |
| copilot_launch_proposals.additional_details → parentQuote | 3,435 | 3,435 | 0 |
| users.agreement_details → name | 1,086 | 1,086 | 0 |
| transactions.additional_documents → sellerInvoice | 962 | 962 | 0 |
| transactions.additional_documents → buyerInvoice | 961 | 961 | 0 |
| transactions.additional_documents → poDocument | 755 | 755 | 0 |
| copilot_requests.attachments | 684 | 684 | 0 |
| copilot_launch_proposals.warranty_document | 681 | 681 | 0 |
| transactions.additional_documents → poAttachments[].attachment | 483 | 483 | 0 |
| copilot_requests.quote | 115 | 115 | 0 |
| equipment_categories.default_image | 105 | 105 | 0 |
| shipping_estimates.shipping_responses → [].fileName | 92 | 92 | 0 |
| copilot_launch_proposals.service_agreement | 75 | 75 | 0 |
| docusign_requests.attachments | 53 | 53 | 0 |
| po_requests.additional_details → comparisonView.fileName | 36 | 36 | 0 |
| transactions.additional_documents → shippingQuote | 35 | 35 | 0 |
| users.profile | 29 | 29 | 0 |
| transactions.additional_documents → cerDocument | 22 | 22 | 0 |
| warranties.file | 18 | 18 | 0 |
| transaction_equipment_details.equipment_details → userManualPdf | 15 | 15 | 0 |
| transactions.additional_documents → billOfSale | 12 | 12 | 0 |
| shipping_estimates.bol_document | 10 | 10 | 0 |
| bill_of_sale_requests.file_name | 8 | 8 | 0 |
| transactions.additional_documents → bolDocument | 7 | 7 | 0 |
| transaction_equipment_details.equipment_details → warrantyFile | 3 | 3 | 0 |
| feedbacks.attachment | 2 | 2 | 0 |
| copilot_launch_proposals.equipment_condition_attachment | 1 | 1 | 0 |
| Total | 1,488,632 | 1,488,594 | 38 |
All 38 are array entries in camp_inventory_items.images, which is derived from
inventory_sticker_details.sticker_name — and that column is fully converted, 757,840
of 757,840. So those entries point at stickers that no longer exist. The same 38 are present in
capClone, which puts them older than this work. Cleanup, not a blocker.
Both path shapes are live at once
An old file keeps its legacy flat path. A new save writes the new layout. A value with a slash is
used as it stands either way, which is what lets a half-migrated table read correctly throughout.
The split in clone1 today:
| Layout | clone1 | capClone |
|---|---|---|
New tree — img/… or doc/… | 26 | 0 |
| Legacy flat folder, complete path | 1,488,568 | 1,488,562 |
| Bare name | 38 | 38 |
| Total | 1,488,632 | 1,488,600 |
Those 26 values are new uploads that landed in the account layout, and they are the evidence
that step 4's behaviour is already live — the plan's flag would have kept them at zero until step 3
was deployed. capClone has none, so the new layout exists only in
clone1.
capClone is not a clean "before" picture either: it already carries the path
conversion, so every value in it is a complete path too. It differs only in that all of them are
still in the legacy folders.
Migration drift between the clones
Fifteen migrations from 2026-09-01 onward are applied in clone1. Four of them are not in capClone.
| Migration | clone1 | capClone |
|---|---|---|
20260901134627 — create presigned_url_links | applied | applied |
20260902101440 — PO paths on transactions | applied | not applied |
| 20260902101441 … 101448 — widen + backfill | applied | applied |
| 20260904102729 — invoice paths | applied | applied |
| 20260905084518 — recreate the ledger with sweep columns | applied | applied |
20260905110855 — file_migration queue and batch tables | applied | not applied |
20260905110946 — the file_migration_enabled switch | applied | not applied |
20260908064726 — ledger refs to ref_data jsonb | applied | not applied |
The copier cannot run against capClone as it stands: the two tables it works
through and the switch that starts it are not there. Its own PO-path migration is missing too, so
transactions.poDocument in production has not had the offline purchase-order prefix
applied that clone1 has.
Where the build departs from the plan
| The plan says | What is built | |
|---|---|---|
| Step 4 flips one setting | finalPrefixFor is unconditional; no setting gates the upload shape | deviation |
| Step 3 is deployed before step 4 | Step 3 is deployed, so the order holds — but by circumstance, not by the flag | order holds |
| Step 2 covers the mobile service | Neither mobile repository carries the storage work | not done |
| Step 5 copies thumbnail before picture | Built — copy_thumb is carried on every queue row | complete |
| Step 5 verifies, then updates the record | Built — write-back is a separate pass, gated on the copy having succeeded | complete |
| Every step reversible on its own | Built for steps 1 and 5; steps 2 and 4 reverse by deploy rather than by switch | partial |
PATH_COLUMNS — the codebase's own list of every column an upload path can reach —
holds 73 entries. The copier reaches 13. Of the 60 it does not, 32 hold legacy paths in
clone1 — two of those are excluded by decision, leaving 30 columns across
51,636 rows unaccounted for, including biomeds.extracted_data and
camp_inventory_items.additional_details, neither of which the plan mentions.
Enumerated in full on CAP-680 Gaps; the ordered punch
list of everything outstanding is on Remaining.