Same-bucket path audit · re-run against the database
The published path audit re-checked column by column against capClone and clone1. Fifteen columns it lists as pending are already migrated. Three it never mentions hold bare filenames as JSON object keys — a shape no value-rewriting migration will touch.
The audit's structural reasoning holds. Its status column does not — most of what it marks pending is done, and its largest remaining exposure is absent from every table on the page.
| Claim on the audit page | Checked against | Result |
|---|---|---|
| 18 columns hold bare names, all Pending | capClone · clone1 | 13 of the 18 have zero bare rows in both databases. |
| 4 mixed columns, Pending per-row | capClone · clone1 | partly email_logs.attachments, document_manager.file_path and camp_inventory_items.images hold. warranties.file and shipping_estimates.bol_document are fully migrated. |
#4 — documents.file_name cannot fit a path | information_schema · capClone | holds varchar(256), longest value 217 chars. The overflow arithmetic is right. |
| #5 — clone1 already contains new-shape rows | capClone · clone1 | 0 rows match ^(img|doc)/ in all three columns named. See §05. |
#5 — presigned_url_links already exists | capClone | holds Table is present. |
| The JSON key inventory is complete | every json/jsonb column | Three columns hold bare names as object keys. See §02. |
The audit lists JSON values — .biomedFile, .images[], .warrantyFile. These three columns store the filename as the key of an object, with the finding as its value. A migration that walks values will pass straight over them.
| Table | Column | Rows | Shape |
|---|---|---|---|
| inventory_stickers | hipaa_rust_details | 125,108 | {"rust": {"3AXDMjpe_1758738949230.jpeg": false, …}} |
| capture_webhook_payloads | payload | 48,276 | {"rust": {"3JC2Vjpe_1781815344138.jpeg": false, …}} |
| inventory_stickers | capture_webhook_response | 6,845 | {"rust": {"FU6SD_1788281240437.jpeg": false}, "hipaa": {…}} |
Two things make inventory_stickers.hipaa_rust_details the sharpest of these. It is the largest single pocket of bare names left anywhere in the database. And it names the same objects as inventory_sticker_details.sticker_name — which holds 757,835 rows, all full paths. The sticker name has been migrated; the JSON keyed on it has not. They are already out of sync.
capture_webhook_payloads.payload is a raw provider payload, so freezing it may well be correct — but the plan neither says so nor lists it.
| Column | Bare rows | Note |
|---|---|---|
| equipment_models.images_old | 4,171 | Live table. Its siblings files and images are both listed and already migrated — this one looks simply overlooked. |
| equipment_models_snapshot_20260717.files · images | 5,028 · 4,122 | Snapshot table. |
| transactions_old.invoice_name · po_attachment | 608 · 90 | Legacy table. po_attachment has no counterpart in the audit at all. |
| transaction_equipment_details_old.equipment_details | 384 | Legacy twin of a column the audit does cover. |
| temp_tommy_data.image_name | 10 | Scratch table. |
The snapshot, legacy and scratch tables may be deliberately out of scope. The point is that the audit does not say so, and equipment_models.images_old is none of those things.
Five columns from the audit's own list genuinely still hold bare filenames. Both databases agree.
| Column | Non-empty | Bare · capClone | Bare · clone1 | Already path |
|---|---|---|---|---|
| email_logs.attachments | 7,205 | 3,948 | 3,949 | 3,257 |
| transactions.invoice_name | 1,053 | 1,053 | 1,055 | 0 |
| document_manager.file_path | 9,048 | 79 | 79 | 8,969 |
| budget_planning.attachments | 72 | 72 | 72 | 0 |
| camp_inventory_items.images | 274,204 | 20 | 20 | 274,184 |
documents.file_name (20,644 bare) and document_manager.name (8,902 bare) are display-name columns paired with a file_path sibling that carries the object path. They are supposed to hold the bare name, which is the audit's own verdict in its point #4. Listing them as migration targets overstates the work.
Fifteen columns the audit marks Pending have zero bare rows. Every value already carries a slash — in capClone and in clone1, the database the audit itself names.
| Column | Rows, all full path | Prefix the audit would have added |
|---|---|---|
| inventory_sticker_details.sticker_name | 757,835 | img/CAP-{id}/assets/ |
| copilot_launch_proposals.quote | 7,963 | doc/CAP-{id}/copilot/ |
| equipment_models.files | 5,043 | img/category/ |
| equipment_models.images | 4,171 | img/category/ |
| copilot_launch_proposals.warranty_document | 670 | doc/CAP-{id}/warranties/ |
| copilot_requests.attachments | 483 | doc/CAP-{id}/copilot/ |
| copilot_requests.quote | 110 | doc/CAP-{id}/copilot/ |
| equipment_categories.default_image | 105 | img/category/ |
| copilot_launch_proposals.service_agreement | 72 | doc/CAP-{id}/copilot/ |
| docusign_requests.attachments | 53 | doc/CAP-{id}/docusign/ |
| warranties.file | 16 | mixed — audit expected 14 bare |
| shipping_estimates.bol_document | 10 | mixed — audit expected 7 bare |
| bill_of_sale_requests.file_name | 8 | doc/CAP-{id}/bill-of-sales/ |
| feedbacks.attachment | 2 | doc/CAP-{id}/feedback/ |
| copilot_launch_proposals.equipment_condition_attachment | 1 | doc/CAP-{id}/copilot/ |
Re-prefixing any of these would produce doc/CAP-1234/copilot/doc/CAP-1234/copilot/file.pdf. The audit's own point #5 warns about exactly this — it just applies far more widely than the three columns it names.
Two of the five are database claims. One is exactly right; the other is crossed with a different fact.
documents.file_name is varchar(256) and its longest value is 217 characters. With a 54-character prefix that is 271, overflowing by 16. The verdict — leave file_name bare, use file_path — is sound, and it is why §03 excludes that column.
The claim: “documents.file_path holds 79 existing paths, document_manager.file_path 9, warranties.file 4, all under img/ or doc/.”
| Column | Rows matching ^(img|doc)/ | Total rows |
|---|---|---|
| documents.file_path | 0 | 20,652 |
| document_manager.file_path | 0 | 50,667 |
| warranties.file | 0 | 60 |
No row in any of the three is new-shape. The number 79 is real, but it belongs to a different fact: it is the count of bare names in document_manager.file_path, which §03 reproduces exactly. Two findings appear to have been crossed.
The stated risk is therefore absent today — but the recommended guard is still right, and for a larger reason than the one given: §04 shows fifteen columns that are already full paths and would be corrupted by an unguarded re-prefix.
These matched a bare-filename pattern but are not bucket objects. Their absence from the audit is correct.
| Column | Rows | Why it is not a target |
|---|---|---|
| equipment_models.candidate_image_links | 46,792 | External scrape results — eBay and CDN URLs with the source site's own filename. |
| email_logs.data | 30,614 | Email payload metadata; the match is incidental inside a token field. |
| family_barrier.parent_final | 18,700 | AI research output — prose in a reasoning field. |
| equipment_models.selected_image | 834 | External url on a Squarespace CDN. |
| copilot_requests.additional_details | 8 | Service-contract terms; incidental match. |
| pdf_extraction_cache.url · extracted_text | 25 | External source URL and extracted page text. |
Two generated passes over the live schema, then a sample of every hit to separate bucket objects from external URLs.
Scalar and array pass. Every text, varchar, character and text[] column in all BASE TABLE relations. Arrays were joined with array_to_string. Each value was split on commas; an element counted as bare when it contained no / and ended in a file extension:
e !~ '/' AND e ~* '\.(pdf|jpe?g|png|gif|webp|svg|heic|xlsx?|docx?|pptx?|csv|txt|zip|mp4|mov)$'
Requiring an extension is stricter than the migration's own ^[^/]+$ test, which keeps ordinary prose out of the results.
JSON pass. Every json and jsonb column, cast to text and matched against a quoted bare filename — "[^"/\\]{2,}\.(pdf|jpe?g|png|…)". Matching the serialised form is what surfaced the key-shaped columns in §02: a value-walking query would not have found them.
Exclusions. Views and *_bak* tables were skipped in both passes. Snapshot, _old and temp_ tables were scanned and are reported separately in §02 rather than mixed into the live findings.
Both databases. Every column in §03 and §04 was counted in capClone and again in clone1. They agree, so the §04 result is not an artifact of testing the wrong database.