Same-bucket running order · validation against clone1

Where CapExpert stores a path, and where it stores only a name

Every file-bearing column in the clone1 database, classified as full path or bare filename, with the JSON keys that hold them and the code that has to change. Checked against the published running order.

Database clone1 @ localhost · PostgreSQL 18.4 Scanned 169 tables · 2,913 columns · 4.77M rows Repos backendApi · capExpertApp · mobileApi · mobileApp Date 2026-09-01

01 Verdict on the plan

The structural claims hold. The counting claims are low in three places, and one of them — the mobile service versions — is low enough to change the shape of Step 2.

Legacy prefixes
73
Plan says 70. That is backendApi alone; mobileApi adds three more.
Top-level names
46
Plan says 43, for the same reason.
Upload kinds
45
Exactly as stated, and the Record type does fail the build.
Ledger columns
17
Exactly as stated, with both unique guards as described.
Mobile API versions
7
Plan says three. v6, v9, v10, v11, v12, v13, v14 all build paths.
Columns to change
41
Across 28 live tables, plus 47 JSON keys inside them.
Claim in the running orderChecked againstResult
Bucket is capexpert-prod-bucket, single bucketspreadsheet_runs.input_gcs_uriholds Live rows carry gs://capexpert-prod-bucket/…
70 flat prefixes across 43 distinct top-level namesBucketFolder enums, all reposlow 70/43 is backendApi only. Union is 73/46.
Migration table has 17 columns20260901134627-create-presigned-url-links.jsholds Exactly 17, names and meanings match.
Two unique guards, not on account + filenamesame migrationholds …_destination_uq on source_path; …_staged_uq on temp file_path.
45 upload kinds, each naming exactly one destinationenum/index.ts · upload-targets.tsholds 45 enum members, 45 target entries.
~170 website locationscapExpertAppholds 171 GCS_BUCKET_URL sites in 67 files.
~40 API hand-built locationsbackendApilow 57 concatenation sites in 31 files; 137 BucketFolder references in 56 files.
~70 mobile app locationsmobileApphigh 47 sites (38 URL + 29 enum, overlapping) across 17 files.
43 hand-built public URLs across 21 filesbackendApiclose 47 across 22 files, excluding 3 config files that define the base.
Apply to all three mobile service versionsmobileApiwrong Seven version trees touch BucketFolder. Inventory alone has six.
Records must store complete file pathsclone1not yet 24 of 41 file columns hold bare names only. See §03.
Open question: longest filename, longest pathclone1answered 217 chars (documents.file_name) and 191 chars (documents.file_path).

02 Five things the plan does not account for

1 · Three legacy prefixes live only in mobileApi. shipping-estimate-documents, site-images and support/diagnostics are in mobileApi/src/shared/utils/enum/index.ts and absent from backendApi's BucketFolder. The first two are in production data — 101 objects under shipping-estimate-documents/, and site-images/logo.png is hardcoded in four backendApi files including every notification and email footer. Neither has an UploadModule destination, so nothing in the 45-item list files them.

2 · mobileApi has seven versioned trees, not three. inventory.service.ts exists at v9, v10, v11, v12, v13 and v14, each with ~19 BucketFolder references; audit-view spans v10–v13; market-place v10–v11; misc v6. The shared read rule has to land in 26 files, and old app builds pin the old versions, so none can be skipped.

3 · {SCOPE} can collide on the destination guard. presigned_url_links_destination_uq is unique on source_path for every non-deleted row, including rows still holding img/{SCOPE}/assets/<name>. Before the shape flip the staged guard does not apply, so two accounts uploading the same filename unsaved both write img/{SCOPE}/assets/<name> and the second insert fails. Most names carry a millisecond suffix, but category images (Autoclaves.jpg) and QR names (000123.png) do not.

4 · documents.file_name will not fit a path. It is varchar(256) and already holds a 217-character name. The longest account-shaped prefix is 54 characters (doc/CAP-12345678/document-manager/pre-approved-pricing/), so converting that column overflows by 16. Its sibling documents.file_path already holds paths and has 28 characters of headroom — tight, but it fits. See §07.

5 · clone1 already contains new-shape rows. documents.file_path has 79, document_manager.file_path 9, warranties.file 4, all under img/ or doc/, and the presigned_url_links table is already created. Any backfill written against this clone must treat "already contains a slash" and "already starts with img/ or doc/" as two separate skips, or it will re-prefix them.


03 Columns storing a bare filename

These are the Step-1 targets: the record holds only the name, and the reader has to know the folder. Counts are full-table over non-null values that look like a file. Position is ordinal_position.

bare name only both forms in one column already a complete path not our bucket
Multi-value columns hold several names in one cell; the separator is shown. Copying one of these means splitting, re-prefixing each element, and rejoining in the same order.
TableColumn#TypeFile valuesMulti-valueReal value from clone1
High volume — these three are the migration
inventory_sticker_detailssticker_name3varchar(256)709,806single00022jpe_1778195211181.jpeg
camp_inventory_itemsimages51varchar[]627,785array · 576,654 rows have >1{0009Xjpe_1761753757126.jpeg,YDBV0jpe_1761753757125.jpeg,ZO7C1jpe_1761753757126.jpeg,XACQPjpe_1761753757126.jpeg,8VTKYjpe_1761753757126.jpeg}
documentsfile_name2varchar(256)20,369single000004.png
Comma-separated lists in a single text column
copilot_launch_proposalsquote26text7,951comma · 982 rows20109874293_New_York_Sporpd_1717614868585.pdf,20109874292_New_York_Sporpd_1717614870782.pdf,20109874291_New_York_Sporpd_1717614872335.pdf
equipment_modelsfiles8varchar(255)5,031comma · 37 rows1670236294016_07003MD88Z9HII22.pdf,1670236294477_P4IG17B3NPYGM6G5.pdf
equipment_modelsimages7text4,137comma · 3,308 rows00OPPjpe_1759228802460.jpeg,S6H4Njpe_1759228802460.jpeg,MVBVLjpe_1759228802460.jpeg
copilot_launch_proposalswarranty_document16varchar(255)738comma · 9 rowsCADTest1pd_1788264925410.pdf,BOL_Documentpd_1788264928689.pdf
copilot_requestsattachments24text470comma · 92 rows0001433807_Power_ease_pd_1767797175378.pdf,Quote2_SOMA_AD01062608__Mdo_1767797182282.doc
copilot_requestsquote27text109comma · 1 rowCPASC_W9_2pd_1773087466801.pdf,Ortho_NY_ASC_RFP_Instructipd_1773087606222.pdf
copilot_launch_proposalsservice_agreement37varchar(255)73comma · 1 rowOrderCAPVEDOB96pd_1788264959381.pdf,CADTest1pd_1788264963012.pdf,BOL_Documentpd_1788264965073.pdf
budget_planningattachments11text72comma · 40 rows1670235719451_NB536REFJ7.jpg,IMG_7178scaledjp_1679498094263.jpg,IMG_7176scaledjp_1679498105238.jpg
Single-value bare names
document_managername3varchar(256)8,639single0000126993_400839528_Casspd_1770830529623.pdf
transactionsinvoice_name18varchar(255)1,026single1680301533907.pdf
equipment_categoriesdefault_image5varchar(255)107singledefault_Anesthesia_Equipment.jpg
docusign_requestsattachments12text47singlecer_1750418847569MAP6U.pdf
bill_of_sale_requestsfile_name5varchar(255)8singleBill_of_sale_1719219486320Z5RTY.pdf
feedbacksattachment6varchar(200)2singleCooper_Surgical_Cleartone_pd_1717612100050.pdf
copilot_launch_proposalsequipment_condition_attachment13varchar(255)1single2jp_1713209028801.jpg

On equipment_models.images. 4,862 rows carry a file value: 4,137 are the comma-separated CapExpert names above, and 725 are a JSON blob of scraped vendor URLs stuffed into the same text column. A backfill that assumes one shape will corrupt the other. Split on the leading { before touching it.


04 Columns holding both forms

These are the ones the plan's "don't modify columns already containing slashes" warning is really about. Each already carries a mixture, so the backfill has to be per-row, not per-column.

TableColumn#TypeBarePathBare examplePath example
email_logsattachments10text3,8083,187419972pd_1762787793393.pdfcer-documents/1jpe_1700159245303jpe_1726809815901.jpeg
document_managerfile_path6varchar(256)798,62390210_Surgery_Medical_Centpd_1731344159705.pdfbill-of-sales/Bill_of_sale_1719219486320Z5RTY.pdf
warrantiesfile35varchar(255)147CDL_USHV_Lafayette_Horizonpd_1774621762664.pdfdoc/temp/assets/warranty/4889pd_1788258781662.pdf
shipping_estimatesbol_document21text73Bill of Lading 1723044241771_1723044241771.pdfdocument-manager/shipping-documents/Bill of Lading 1765229943240_1765229943241.pdf

Note the whitespace. shipping_estimates.bol_document stores names containing spaces. Any backfill regex or path splitter that assumes no spaces in a key will drop these four rows.


05 Columns already storing a complete path

Step 1 must leave these alone. They are already the shape the plan asks for; the work here is Step 5's rewrite to the account layout, not a Step-1 backfill.

TableColumn#TypeRowsReal value from clone1
documentsfile_path3varchar(256)20,377attachments/onboarding/CapExpert_SCA_Facility_Maxls_1718895984902.xlsx
po_requestsattachment3varchar(255)144cer-documents/2341727jpe_1726568391940jpe_1728417342272.jpeg
shipping_estimatesfile_name6text70document-manager/shipping-documents/1742317022537_1742317022538.pdf
chat_messagestext2text51chat-message-documents/1010SalesSheetpd_1771260090875.pdf
usersprofile8varchar(150)29profile/1A4A1364jpe_1772729243492.jpeg
file_import_logsfile_name3varchar(255)13onboard-file/CAHFxls_1754039288235.xlsx
shipping_estimatesoriginal_filename19text8document-manager/shipping-documents/1745251206770_1745251206770.pdf
profileshospital_logo27varchar(255)7profile/CAPSlogopn_1720757692960.png
admin_queuefile_name4text5inventory/service-contract/Inventory_07-23-2025_01-25-49.xlsx
settingsattribute_value3text1site-images/logo.png
spreadsheet_runsinput_gcs_uri4varchar(500)40gs://capexpert-prod-bucket/denovo-projects/Cedarwood_FHC_Listxls_1777064533843.xlsx
spreadsheet_runsoutput_gcs_uri5varchar(500)29gs://capexpert-prod-bucket/spreadsheets/output/34195eb6-6af3-4c00-b913-a8ad38ba1e0f/Cedarwood_FHC_Listxls_1777064533843_standardized.xlsx

Two of these are not in the 45-item destination list. settings.attribute_value points at site-images/, and spreadsheet_runs writes absolute gs:// URIs under spreadsheets/output/ — a prefix that appears in no BucketFolder enum in any repo. Neither has a home in the account layout yet.


06 File references inside JSON

Every key path below resolves to a string holding a filename or a path. Notation: .key descends into an object, [] into an array. Counts are from a 3,000-row sample per column, so read them as proportions, not totals.

jsonb / json columns that need a Step-1 rewrite bare

TableColumn#TypeKey holding the filenReal value
biomedsextracted_data13jsonb.biomedFile2,305012020260844530001pd_1768919771163.pdf
copilot_launch_proposalsadditional_details49jsonb.parentQuote2,7481205TG_2436068_SPRING_pd_1pd_1779887679183.pdf
.specifications[].fileName1640031053500pd_1779825946369.pdf
usersagreement_details28jsonb.name1,064eula_aaron_hayes1780079378131.pdf
transaction_equipment_detailsequipment_details3jsonb.inventoryStickerData.inventoryStickerDetail[].stickerName96500OFGjpe_1781788180450.jpeg
.images[]82600OFGjpe_1781788180450.jpeg
.equipmentCategoryData.img215Autoclaves.jpg
.inventoryStickerData.inventoryStickerDetail[].sticker1251jpe_1701347472926.jpeg
.images661jpe_1701478449843.jpeg
.referenceImages520_12jp_1709196811360.jpg
.inventoryStickerData.inventoryStickerDetail[]431jpe_1699746515688.jpeg
.userManualPdf151670236966908_FCF1K3IWFOQF8UYU.pdf
.serviceContract5DEMO_SERVICE_CONTRACT_1pd_1710185201917.pdf
.childInventories[].inventoryStickerData.inventoryStickerDetail[].stickerName41jpe_1739390044463.jpeg
.warrantyFile3CSA_GE_Vivid_T9_R6__620219pd_1744212068328.pdf
.childInventories[].equipmentCategoryData.img2Stirrups.jpg
.tradeInventoryDetail.newImages[]2Electrosurgeryjp_1723044572403.jpg
.tradeInventoryDetail.images19900001jp_1694199213778.jpg
.tradeInventoryDetail.equipmentCategoryData.img1C_Arms.jpg
shipping_estimatesrequest_details2jsonb.items[].images[]2800730Xjpe_1752603773879.jpeg
po_requestsadditional_details15jsonb.comparisonView.fileName36comparison_view_1749043309118.pdf
copilot_parent_requestsaction_logs20jsonb[].equipments[].changes[].new27allenfootpn_1759337538557.png
.equipments[].changes[].old11111924_Star_Med_Ctr_OQpd_1733508636666.pdf,Star_Med_Ctr_Opts_GAdams_1pd_1733508676261.pdf
camp_inventory_itemsadditional_details23json.quote18420176pd_1754346820897.pdf
.copilotReq.attachments1Screenshot_20251210_1010pn_1765383080588.png

Keys that pair a bare name with its own path both

These already follow the pattern the plan wants — a fileName for display and a filePath for the location. They need no Step-1 change; they are the model for the rest.

TableColumn#TypeName keyPath keyReal path value
denovo_projectsadditional_details15jsonb.formData.cadDocuments[].fileName.formData.cadDocuments[].filePathdenovo-projects/23009_A151A151pd_1777565439300.pdf
.formData.w9Attachment[].fileName.formData.w9Attachment[].filePathdenovo-projects/CPASC_W9_2pd_1773161844217.pdf
.formData.optionalDocuments[].fileName.formData.optionalDocuments[].filePathdenovo-projects/Ortho_NY_ASC_RFP_Instructipd_1773161881404.pdf
.formData.purchasingFormulary[].fileName.formData.purchasingFormulary[].filePathpurchasing-formulary/Cedarwood_FHC_List_Sheetpd_1777064710141.pdf
.denovoTemplate.filePathdocument-manager/DNP-018F6BTE_denovo_template1777318758294.xlsx

JSON keys already holding a complete path path

TableColumn#TypeKeynReal value
camp_inventory_itemsadditional_details23json.cad.cadUrl2,679document-manager/cad/extraction/A161_Northpoint_ASCA161pd_1776372936871_enhanced.xlsx
.transferDetail.fileName1inventory/images/8_Lafayette_Surgery_Centerpd_1738797079136.pdf
copilot_requestsproposal_summary61jsonb.comparisonViewAttachment712ezrfp/attachments/comparison_view_1761676961963.pdf
transactionsadditional_documents24jsonb.poDocument mixed727 path / 9 barecer-documents/1015_Stretcher_Equi_Demojpe_1724265729018.jpeg
.poAttachments[].attachment460cer-documents/396891798735333pd_1734545674644.pdf
.shippingQuote34document-manager/shipping-documents/1743547691705_1743547691705.pdf
.cerDocument22cer-documents/Bill_of_Salepd_1723044118308.pdf
.billOfSale12bill-of-sales/Bill_of_sale_1719219486320Z5RTY.pdf
.bolDocument7document-manager/shipping-documents/Bill of Lading 1765229943240_1765229943241.pdf
transactionsseller_details6jsonb.primaryUser.profile142profile/Didage_White_on_Blue_NO_TAGjp_1708444032630.jpg
inventory_transferstransfer_details11json.fileName115document-manager/purchase-orders/1212_6th_copyjpe_1743678572968.jpeg
copilot_pre_ordersadditional_details11json.poAttachments[].attachment98document-manager/purchase-orders/041526Neptune_1776266774975.pdf
copilot_launch_proposalsadditional_details49jsonb.poAttachment82document-manager/purchase-orders/02242025_Quotepd_1742594309796.pdf
file_importsadditional_details11jsonb.derivedExcelPdf.filePath38inventory/asset-registry/203_UNV_AssetRegisterRexls_1773359218125_JSON_derived.pdf
accountsimages_attachment10jsonb.assetTag comma-sep17account-tags/asset-tags/IMG_3633jpe_1763315944357.jpeg,account-tags/asset-tags/IMG_3632jpe_1763315945588.jpeg
.biomedTag comma-sep14account-tags/biomed-tags/Screenshot_20251114_at_1pn_1763144878456.png
.logo11profile/CSA_Mainjp_1738712930274.jpg
.smallLogo11profile/CSA_Microjpe_1738712931081.jpeg
make_offersadditional_details16json.poAttachment15document-manager/purchase-orders/Boco_Med_Tech_Signed_523pd_1748027216213.pdf
buy_ordersbuyer_details3jsonb.primaryUser.profile5profile/Didage_White_on_Blue_NO_TAGjp_1708444032630.jpg
shipping_estimatesshipping_responses12jsonb[].fileName mixed61 path / 25 baredocument-manager/shipping-documents/1742317022537_1742317022538.pdf
shipping_estimatesquote_email_details13jsonb.attachments[] mixed50 path / 25 baredocument-manager/shipping-documents/1742317022537_1742317022538.pdf
transaction_equipment_detailsequipment_details3jsonb.additionalDetails.transferDetail.fileName1transfer-documents/Bill_of_Salepd_1723043698956.pdf

The audit snapshot: email_logs.data decide

email_logs.data (position 9, jsonb, 90,732 rows, 2.8 GB) is a frozen copy of whatever the mailer was handed. It carries at least 39 distinct file-bearing key paths, mirroring almost every column above — .copilotDetail.equipments[].attachments, .shortlistedProposals[].quote, .documentManager.filePath, .reqUser.logo, .transaction.poAttachment, .requestDetails.<id>.<id>[].text, and so on. Roughly half already hold paths because they snapshot a path column.

Recommendation: exclude it from Step 1. It is a log of what was sent, not a live reference; rewriting it would falsify the record, and its per-row key shape varies by email type. Read it through the migration-table fallback (bare name → real path) if a historical email ever has to be re-rendered.


07 Matched but out of scope

These columns contain file-looking strings and will show up in any naive scan. None of them reference an object in capexpert-prod-bucket.

TableColumn#TypeWhy it is excluded
External vendor URLs from scraping and enrichment
equipment_modelscandidate_image_links25jsonbScraped vendor image URLs. [].filename is the vendor's own name, not ours.
equipment_modelscandidate_file_links26jsonbScraped document URLs, including ftp://.
equipment_modelscandidate_links16jsonbScraped source URLs.
equipment_modelsimage_candidate_links21jsonbScraped source URLs.
equipment_modelsfile_candidate_links22jsonbScraped source URLs.
equipment_modelsselected_image40jsonb.metadata.filename, .filename, .title — the chosen scrape's original name, not a stored object.
equipment_modelseol_data · recalls · secondhand_market_pricing · additional_identifiers · review_data · alt_specification49 33 32 47 55 15jsonbEnrichment payloads carrying source links and titles.
pdf_extraction_cacheurltext5,848 third-party PDF URLs. No entity; not in the app's model layer.
shipping_companieslogo_url · public_url12 · 7varcharCarrier brand assets on carrier domains.
Extracted document text — the match is inside prose
pdf_extraction_cacheextracted_text · first_500_wordstextOCR output. The "path" is a sentence containing a slash.
equipment_modelsscraped_data27textPage text.
inventory_stickersextracted_data5jsonbPatent notices under .additionalDetails.stickers.* containing URLs.
email_logscontent4textRendered HTML email bodies.
email_templates · static_pages · promptscontent · content · prompt5 · 4 · 2textTemplates whose markup references site-images/; handled by the code change, not a backfill.
copilot_parent_requestscomment9textFree-text notes containing vendor links.
service_contracts · file_imports · file_import_queues · inventory_sticker_detailsextracted_data (+ variants)41 · 9 · 12 · 6jsonb / jsonb[]1–10 incidental matches each in extracted document text.
Regex false positives
usersemail4varcharTwo addresses whose local part ends in a file-like token.
old_passwordspassword3varcharHashes containing a matching substring.
security_tokensvalue5varcharToken body.
docusign_requestswebhook_response11jsonb[]ISO timestamps ending .7Z matched the extension pattern.
account_module_permissionskey1varcharPermission keys.
service_contractsquote_number8varcharQuote numbers with a dot.
Backup, snapshot and scratch tables — no Sequelize entity, no code reference
equipment_models_bak_pre_p1_20260809
equipment_models_snapshot_20260717
equipment_models_bak_pre_batch_20260624
equipment_models_bak_acct1216_20260625
files · images · selected_image · candidate_* · eol_data · scraped_datamixedFour frozen copies of equipment_models. ~20,000 bare filenames each. Copying them would multiply the Step-5 object count fourfold for no benefit.
camp_inventory_items_bak_pre_finalize_20260626images · additional_details · warranty_filevarchar[] · json · text583,120 bare image names — a frozen copy of the live array.
transactions_old · transaction_equipment_details_oldinvoice_name · po_attachment · additional_documents · seller_details · buyer_details · equipment_detailsmixedSuperseded tables.
family_barrierparent_finaljsonb24,434 bare names under .images_array[].filename plus 33,512 external URLs. No entity, no code reference — a matching experiment.
temp_ai_image_camp · temp_tommy_dataHTTPS/GS link columns · images_url · image_namevarcharPoint at capexpert-prod-tmp-20250808151049-65b6, a different bucket entirely.
import_mh_staging · plano_rerun · updated_extraction_records · _bak_unified_is_audited_20260804steps · steps_c · extracted_data (+ variants)mixedImport staging and one-off remediation tables.

Decide on the backups before Step 5, not during it. The four equipment_models copies and the camp_inventory_items copy together reference roughly 660,000 filenames. If the copier is driven from the migration table and the migration table is seeded from a column list, they are excluded automatically. If it is driven by scanning the bucket, they are irrelevant. If anyone seeds by scanning all columns, the object count roughly doubles.


08 Column width headroom

The plan lists this as an open question before Step 1. Measured against the longest account-shaped prefix each column can receive. capexpert_id is 11–12 characters across all 3,104 accounts, so CAP-12345678 in the plan is the correct worst case.

Longest prefixes in the new layout

TableColumnWidthLongest today+ prefixTotalVerdict
documentsfile_name25621754272overflow by 16 — leave it bare; the sibling file_path is the path column.
documentsfile_path25617354228fits 28 to spare — the tightest column that must hold a path.
copilot_launch_proposalswarranty_document25517354228fits 27 to spare.
copilot_launch_proposalsservice_agreement25517354228fits 27 to spare.
equipment_modelsfiles25512554180fits per element — but the column is comma-separated, so the joined length is what matters. Widen or convert to text.
document_managername · file_path25610754162fits
camp_inventory_itemsimages (element)25510430135fits including the thumb/ variant.
inventory_sticker_detailssticker_name25610424129fits
warrantiesfile2558854143fits
usersprofile1507629106fits — narrowest column in the set, still 44 to spare.
feedbacksattachment2004654101fits
profiles · file_import_logs · po_requests · bill_of_sale_requests · transactions · equipment_categories6 columns255≤51≤54≤102fits comfortably.
Text and array-of-text columns (copilot_*.quote, copilot_requests.attachments, email_logs.attachments, budget_planning.attachments) are unbounded and need no width change.

Net answer to the open question. One column overflows, and it is one that should stay bare. Every other column fits without a width change. The migration table's own varchar(512) path columns are more than adequate — the longest possible key is 54 + 217 = 271.


09 Code that has to change

Counted by the two things that break when a file moves: somewhere a folder is glued to a filename, and somewhere a public URL is glued to a folder.

What already exists on the branch

backendApi is on CAP-2031-same-bucket-migration with uncommitted work in place: upload-targets.ts (the 45-entry destination map, {SCOPE} substitution, temp/final prefix builders), presigned-url-links.entity.ts, and storage.service.ts carrying getPreSignedUrlForUpload, recordUpload, commitUploads, commitRow, markSkipped and a getPreSignedUrlForView with a fallback lookup. The shared rule exists. What remains is adoption.

Adopted so far
2
Call sites passing an UploadModule: chatbot and denovo-external-link.
Reading via shared rule
20
Files calling getPreSignedUrlForView.
Still hand-building
31
backendApi files concatenating a BucketFolder.

backendApi — 31 files concatenate a folder onto a name

SitesFileSitesFile
8src/shared/modules/document-manager/document-manager.service.ts1src/primary/modules/warranties/warranties.service.ts
6src/import/queue/attachment/capture.service.ts1src/primary/modules/transaction/transaction.service.ts
4src/shared/queue/mail/mail.service.ts1src/primary/modules/qrcodes/qrcodes.service.ts
3src/primary/modules/vendor-offers/task-queue/vendor-offer-transaction.service.ts1src/primary/modules/inventory/divestiture/inventory-divestiture.service.ts
3src/import/modules/thread.ts1src/primary/modules/ez-estimators/ez-estimators.service.ts
2src/shared/modules/storage/storage.service.ts1src/primary/modules/developers-tab/developers-tab.service.ts
2src/shared/common/utils/constant.ts1src/primary/modules/content/feedback/feedback.service.ts
2src/primary/modules/gcp-agent-tools/gcp-agent-tools.controller.ts1src/primary/modules/carts/carts.service.ts
2src/primary/modules/copilot/copilot-utilities/copilot-utilities.service.ts1src/import/queue/vertex-ai/vertex-ai.service.ts
2src/import/queue/copilot-quote/copilot-quote-extractor.service.ts1src/import/queue/mail/pa-mail.service.ts
2src/import/modules/qr-codes/qr-codes.service.ts1src/import/queue/imap/imap.service.ts
2src/import/modules/qr-codes/export-qr-pdf.service.ts1src/import/queue/attachment/helpers/capture-helper.ts
1src/shared/queue/mail/copilot/copilot-mail.service.ts1src/import/modules/thread-helper/marketplace-export.ts
1src/primary/queue/mail/marketplace/shipping-mail.service.ts1src/import/modules/thread-helper/divest-export.ts
1src/primary/queue/mail/marketplace/marketplace-mail.service.ts1src/import/modules/thread-helper/capture-export.ts
1src/import/modules/reports/report-generation.job.ts
57 concatenation sites. The wider surface — every file naming a BucketFolder at all — is 137 references across 56 files.

backendApi — 22 files build a public URL by hand

47 sites gluing gcp.publicUrl / GCP_PUBLIC_URL to gcp.mediaBucket and a key. The heaviest are import/queue/attachment/capture.service.ts (8), shared/queue/mail/mail.service.ts (5), shared/queue/mail/copilot/copilot-mail.service.ts (4), shared/common/utils/constant.ts (3) and import/modules/thread.ts (3). Three further files — shared/core/config/configuration.ts, shared/core/config/validation.ts, shared/modules/storage/storage-config.ts — define the base and are not hand-built call sites.

Four of these emit site-images/logo.png into notifications and email footers: static-pages.controller.ts, notification.service.ts, mail-common-code.service.ts, imap.service.ts. That prefix has no UploadModule destination yet.

capExpertApp — 171 sites in 67 files

Every one is environment.GCS_BUCKET_URL or sharedService.GCS_BUCKET_URL concatenated with an inline folder string; there is no BucketFolder enum on the front end, so the 397 folder literals across 60 files are loose strings. Concentrations: services/shared.service.ts (11), modules/inventory/add-edit/add-edit.component.ts (10), modules/copilot/bid-submit-collective/… (7), modules/manage-users/add-account/add-account.hydrate.ts (6), modules/copilot/bid-submit-tabbing/bid-submit.component.ts (6), common/modules/chat/chat-widget/chat-widget.component.ts (6).

Introduce the enum here first. With folder names as free-text literals in 60 files, there is nothing for the compiler to catch when a folder moves. A shared constant plus one resolver function is the change that makes the other 170 mechanical.

mobileApi — 26 files across 7 API versions

VersionFilesRefsNotes
v9inventory/v9/inventory.service.ts19Oldest live inventory surface.
v10inventory · audit-view (get-asset, get-location-stickers) · market-place (service, get-all-marketplace-items) · copilot (create-copilot-request, copilot-request-created)17 + 12 + 6 + 2Widest version.
v11inventory · audit-view ×2 · market-place ×219 + 12 + 6
v12inventory · audit-view ×219 + 12
v13inventory · audit-view ×219 + 12
v14inventory · inventory-locations/fetch-consumable-group-items19 + 1Current.
v6misc/v6/misc.service.ts · misc/v6/dto/misc.dto.ts15 + 11Where the upload link is issued; 3 calls to getPreSignedUrlForUpload.
sharedmodules/storage/storage.service.ts · shared/services/end-user/services/helper.service.ts · shared/utils/enum/index.ts3 + 1 + 42The enum here holds the three prefixes backendApi is missing.
198 BucketFolder references in total. The six inventory.service.ts copies are near-identical; a shared resolver in shared/ collapses ~112 of them.

mobileApp — 47 sites in 17 files

38 GCS_BUCKET_URL concatenations and 29 BucketFolderEnum references (overlapping), against a 26-entry enum in src/app/core/constants/index.constant.ts that is a strict subset of backendApi's. Heaviest: pages/authorized-container/audit/update/update.page.ts, pages/authorized-container/audit/list/list.page.ts, pages/authorized-container/marketplace/add/add.page.ts, pages/authorized-container/copilot/copilot-detail/copilot-detail.ts.

update.page.ts:1569 already does the right thing — `${environment.GCS_BUCKET_URL}${imgData.src}`, no folder glued on, because src already holds a path. That line is the target shape for the other 46.

dbMigrations

One migration exists: 20260901134627-create-presigned-url-links.js, already applied to clone1. The Step-1 backfill migrations — one per column in §03 and §06 — do not exist yet.


10 How this was produced

So the numbers can be re-derived rather than trusted.

Known limits. JSON key counts are sample-based. A file with no extension, or one whose extension is outside the 40 matched, would be missed. email_logs.data was sampled rather than fully walked because the table is 2.8 GB. Nothing was written to clone1.