Ten parallel reviews across backendApi, capExpertApp, dbMigrations and the live clone1 database, each finding put to an adversarial verifier before it was allowed into this report.
Every fix below is in the working tree, unstaged and uncommitted. The API typechecks, the layering gate passes, both roles boot — the real test for the DI changes, and one of them caught a missing StorageModule import mid-way — and the front end builds with only its 11 pre-existing errors in an unreferenced component.
when predicate on the leaf's own object. Measured against 2,456 real rows: 6,438 → 104 values rewritten, and only the two keys that are genuinely files (quote 15, attachments 89, the latter now correctly routed to ezrfp/attachments). Round-trip up→down is byte-identical.camp_inventory_items.images added as an ordered array rebuild; listDown() now carries the skipJson guard its list() counterpart had, so a reversal no longer re-splits the JSON blobs. Both statements EXPLAIN cleanly against the live schema.warranties.file to text — it takes a comma-joined list of complete paths and was still varchar(255). The entity was updated to match.objectPathFor. The document-manager reclassifier now refuses outright to move an object that is already in the new trees — a new isReorganisedPath guard, since that path deleted and rewrote it into a legacy folder.StorageModule); the shipping-quote logo is filed by the settings save and its target corrected from account-scoped to global, so it can leave temp/ at all.noteFiled moved out of the copy's try so a Redis fault can no longer discard a copy that succeeded, and both Redis reads swallow. A durable record can no longer be lost to a file operation.remiServiceHistory now uses the spreadsheet validator that exists; the caller-less ezEstimatorQuote branch and its union member are gone.assets/QR; UploadModule.QrCode and its img/qr destination remain unreached. A deliberate move, not a bug — worth doing with step 5.Step 2 is essentially done — the shared path rule exists in both repos and is called nearly everywhere. Step 1 is half done in a way that is worse than not started: the column widening ran, the backfill did not, and the two backfill migrations are sitting on disk executable and untracked, so the next db:migrate anyone runs will apply them by accident. Step 4's machinery is built but has no rollback lever. Steps 5 and 6 do not exist yet.
The 46 confirmed findings are not 46 separate problems. They collapse into seven root causes, and one of them — a complete path being thrown away and rebuilt as a legacy path — accounts for fourteen of them on its own.
Widen ran and is applied. The backfill has not run — 709,801 sticker names, 627,763 inventory photo elements and 7,799 proposal quotes are still bare names.
Built in both repos. 17 hand-built locations remain in 15 front-end files and about 5 in the API, against a gate that says confirm zero. No tests cover the rule in either repo.
Cloud Function, outside all three repos — nothing here can confirm it. Step 4 is already live in code, which inverts the plan's required order if the function has not been deployed.
Staging, filing and the Redis commit path all work. But there is no shape flag anywhere, so the plan's rollback — "unset the shape flag and redeploy" — has nothing to unset.
presigned_url_links is created and mapped, and nothing writes a row to it. There is no copier and no scheduled job.
No legacy-read logging, no app-version report. Correctly last.
Separately, all six code deltas your plan's "where the code stands" table lists are closed — there are now zero references to useOldBucket, is_new_bucket, new_bucket_source_path or liveBucketName anywhere. The seventh row of that table is the backfill, which is step 1 above.
I told you earlier these were commented out. They are not. require() on both returns up and down as functions, and sequelize-cli globs the migrations directory from disk, not from git — so the next db:migrate in dbMigrations applies both to clone1, including the audit-trail damage below.
All three artifacts — both migrations and database/helpers/json-file-paths.js — are untracked, so this state exists only on this machine. Commit d6a2180, whose message reads "add database migrations to store complete file paths and update JSON-encoded references", contains only the widen migration.
ezrfp-quote/ prefix rewrites the copilot audit trailIn 101444, the key paths equipments[].changes[].new and .old are addressed by position, and the walker never reads the sibling key field that says what the change was. Measured on clone1: 6,228 values would be prefixed, of which 8 are actually quote files.
A status becomes ezrfp-quote/Approved, a manufacturer becomes ezrfp-quote/Stryker, a zip code becomes ezrfp-quote/90210. Free-text comments are additionally split on their commas and trimmed, which down() cannot undo. 60 attachment names are misfiled into the wrong folder — they belong in ezrfp/attachments.
copilot_parent_requests.action_logs entry — it is a frozen audit record, the same reasoning that already excludes email_logs.data — or teach the walker a sibling predicate so only key = 'quote' is rewritten.camp_inventory_items.images holds 627,763 bare-name elements across 312,152 rows — more elements than any other column in the schema. 101443 never mentions the table; 101444 covers only its additional_details JSON column. Every one of those photos would be left behind by a backfill that reports success.
varchar[] of inventory photos, is not. Order matters here more than anywhere: the first element is the preview shown on hundreds of thousands of records.The upload returns where the object really went. The caller discards that return value, keeps only the bare file name, and reconstructs a path from a legacy BucketFolder. The object is in the new tree; the recorded path points into the old one; the read 404s. This is the exact failure your plan names as the original cause of the whole project — "the upload step already works out the complete location and hands it back, the rest of the system just discards it."
doc/capture/export/ and mail a link to capture/export/. Every export over the size limit sends a dead link today.document_manager.file_path as a path the object never occupied, and its commit can never match.commitUploads, then re-prefixes pa-documents/ at three separate sites.img/temp/ and the record points nowhere.Your plan lists this among the seven ways to lose something permanently. Five sites still do it, and one of them physically moves the object backwards out of the new tree.
fileImports prefixes a legacy folder onto names already in the new tree — biomed, asset-registry, service-contract, warranty and service-history flows, five at once.ezrfp-warranty/ onto a proposal value that already holds a complete path.Your plan is explicit that there is no safety net: "each of the 24 places that hands out an upload link has to hand the id back on save". These do not, so their objects stay under img/temp/ or doc/temp/ and the row records an address holding nothing.
fileName where it should commit filePath.temp/ even if it were filed.Your plan states the trade plainly: losing someone's record to protect a file copy is the wrong way round. Two paths still get it wrong.
RedisService.get is not fail-open — so a Redis fault after the row is written throws back into the caller.noteFiled runs inside fileStagedObject's try, so a Redis fault after a successful copy loses the record of where the file went — the copy happened, nothing knows it.Exports, signed PDFs, QR codes and mail attachments skip staging by design — your plan says so. But they also skip the shared rule: they pass no upload target, so generatedPathFor silently writes them into the old structure. When the account is unknown it does the same thing rather than failing, which is the "silent default" the plan warns about.
generatedPathFor falls back to the old structure instead of refusing. Four of the eight server-generated kinds name destinations no call site can reach.uploadFileFromPath and save() have no route to the shared rule at all; 19 of 24 uploadBufferFile sites pass no target.assets/QR; UploadModule.QrCode and its img/qr destination are dead code.Five columns became varchar[]. An empty array is truthy in JavaScript where an empty string was not, so a guard that used to mean "no warranty" now means "has one".
string → string[] change.warranties.file is varchar(255) and was never widened, yet is fed 'ezrfp-warranty/' plus a complete path, joined per document — an overflow, not a mis-record.noImplicitAny: false in the import role's tsconfig hides it, so the build is green and the endpoint dies on the first request.
StorageService methods removed in the split — live crash on POST /remi-service-history.| Column | Type today | Rows / elements | Still bare | In backfill? |
|---|---|---|---|---|
| camp_inventory_items.images | varchar[] | 627,763 | 627,763 | No — gap |
| inventory_sticker_details.sticker_name | varchar(256) NOT NULL | 709,801 | 709,801 | Yes |
| copilot_launch_proposals.quote | text | 8,271 | 7,799 | Yes |
| equipment_models.files | text | 116,047 | 5,030 | Yes |
| equipment_models.images | text | 116,047 | 4,496 | Yes · JSON skipped |
| copilot_launch_proposals.warranty_document | varchar[] | 647 | 647 | Yes |
| copilot_requests.attachments | varchar[] | 662 | 662 | Yes |
| equipment_categories.default_image | varchar(512) | 3,532 | 105 | Yes |
| warranties.file | varchar(255) | 41 | 14 | Yes — not widened |
| documents.file_path | varchar(256) NOT NULL | 20,269 | 0 | Excluded — already paths |
| transactions.invoice_name | varchar(255) | 1,055 | 1,026 | Excluded — two folders |
Migrations are applied through 20260902101442-widen-file-path-columns.js. The tracking table is migrations, not SequelizeMeta. Exactly one row anywhere still points at a staged object — a single equipment_models.images value under img/temp/assets/ — so the stranding is real but has barely been exercised.
Your plan's fourth open question, "measure the real storage", is still open, and two columns the backfill prefixes were never widened. That is the same gap seen from both ends.
101443 and 101444 so an accidental db:migrate cannot fire them, and get all three files into git so they are reviewable. Nothing is run against the database.action_logs prefix and add the missing column.Restrict the JSON walker to the quote key, and add camp_inventory_items.images to the column backfill with order preserved.attachment.service.ts:1027, which moves objects backwards out of the new tree.thread.ts:103.One live crash, two lines.The shape flag, the step-5 copier and the step-6 logging are new construction rather than repair, and belong after this list rather than inside it.