Skip to content

fix(catalog): preserve CSV round-trip names - #1150

Merged
khaira777 merged 5 commits into
mainfrom
codex/catalog-csv-roundtrip
Oct 15, 2026
Merged

khaira777 merged 5 commits into
mainfrom
codex/catalog-csv-roundtrip

Conversation

@khaira777

@khaira777 khaira777 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Intent

PR 7 - Make catalog CSV escaping reversible (Fixes #1075)
Branch: codex/catalog-csv-roundtrip
Title: fix(catalog): preserve CSV round-trip names

Locked product decisions:

  • Preserve ambiguous apostrophes in older CSV files. New exports must be reversible; decode older values only when an existing record proves the original value.

Requirements:

  • Keep spreadsheet formula protection.
  • Add a catalog export column flo_csv_encoding with value quote-v1 to category, product and add-on exports.
  • For this encoding, double an original leading apostrophe before passing the value through the existing CSV serializer. Formula-triggering strings continue receiving its protective apostrophe.
  • On marked imports, remove exactly the encoding-added apostrophe. Do not apply this decoding to unmarked files.
  • On old product/add-on rows with stable IDs, decode only when comparison with that exact existing record proves the original value.
  • Preserve ambiguous old names, including category-only rows without proof of identity.
  • Keep existing numeric parsing and import/export API bodies.
  • Reject unsupported nonempty encoding markers as import validation errors.
  • Do not rename existing ambiguous records, remove duplicate categories, or weaken other CSV exporters.
  • Document the new marker and the deliberate legacy ambiguity.

Acceptance: repeated new export/import preserves names beginning with =, +, -, @, whitespace before those characters, and genuine apostrophes. Quoted commas/newlines still parse correctly. Existing IDs, categories and add-on associations remain stable.

What Changed

  • Added flo_csv_encoding=quote-v1 to category, product, and add-on CSV exports (main/lib/csv.ts, main/routes/menu-csv.ts), doubling original leading apostrophes before applying spreadsheet formula neutralization so marked exports round-trip losslessly across formula-triggering prefixes, leading whitespace before formula triggers, and literal leading apostrophes.
  • Updated category, product, and add-on CSV importers to decode quote-v1 rows, reject unsupported non-empty flo_csv_encoding markers as per-row validation errors, and decode unmarked legacy product/add-on fields only when a stable ID matches an existing record whose stored value proves the leading apostrophe came from formula neutralization.
  • Added round-trip and legacy-ambiguity regression coverage in tests/issue-248-menu-csv.test.ts and tests/menu-csv-formula-export.test.ts, and documented the quote-v1 contract in docs/reference/api.md, docs/reference/daily-sales-export.md, and docs/reference/product-invariants.md.

Risk Assessment

✅ Low: The change cleanly implements reversible quote-v1 catalog CSV encoding, legacy ID-proven decoding, and unsupported marker validation in accordance with all specified requirements and product invariants.

Testing

Ran the focused catalog, reports, and daily-sales CSV integration test suites (npm run test:issue-248-csv, npm run test:reports-export, npm run test:daily-sales-export), built the backend (npm run build), and stood up an isolated live Flo Local API server (dist/main/server.js) with a fresh SQLite database to drive all six scenarios over HTTP; captured the exported CSV payloads, import responses, and persisted database rows in ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json (no screenshot or visual artifact was captured because this change only modifies backend CSV serialization/import handlers and documentation with no UI surface changes). All scenarios and checks passed.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Merchant exports categories, products, and add-ons CSVs and receives flo_csv_encoding=quote-v1 headers/rows with doubled leading apostrophes and formula neutralization on =, +, -, @, and whitespace-pr… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant runs three consecutive export/import cycles on categories, products, and add-ons and preserves all names, SKUs, descriptions, tags, quoted commas/newlines, IDs, and product-to-add-on-group as… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports unmarked legacy product and add-on CSV rows carrying stable IDs and the importer strips leading apostrophes only where comparison with the existing stored record proves formula neutra… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports unmarked legacy CSV rows for category-only records, unproven new/unknown-ID records, and existing apostrophe-leading records, and ambiguous leading apostrophes are preserved without r… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports category, product, and add-on CSV rows with unsupported non-empty flo_csv_encoding markers (quote-v2, unsupported-v9, legacy-v0) and receives per-row validation errors while valid sib… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant exports daily sales items CSV and other non-catalog CSV exporters continue using toCsvRow directly without adding flo_csv_encoding or doubling leading apostrophes ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Evidence: Live catalog CSV round-trip, legacy ambiguity, and validation evidence
{
  "server": {
    "baseUrl": "http://127.0.0.1:60616",
    "health": {
      "status": "ok",
      "db": "ok",
      "service": "Flo Local API",
      "version": "2.4.7",
      "timestamp": "2026-10-15T00:32:26.385Z"
    }
  },
  "cycleSnapshots": [
    {
      "cycle": 1,
      "exports": {
        "categories": "name,description,color,icon,sort_order,flo_csv_encoding\n'=Equals Category,'-Minus description,blue,☕,1,quote-v1\n\"'+Plus, Category\",\"'@At description,\nwith newline\",green,🍔,2,quote-v1\n'-Minus Category,'=Formula desc,pink,🍰,3,quote-v1\n'@At Category,'+Plus desc,amber,🎁,4,quote-v1\n' =Space Formula Category,'\t+Tab Formula Desc,blue,☕,5,quote-v1\n''Chef's Category,\"''Genuine quote, \"\"quoted\"\"\r\ndesc\",green,🍔,6,quote-v1\n''+Quoted Formula Category,'''Double Quoted Desc,pink,🍰,7,quote-v1",
        "products": "id,sku,name,category,price,description,cost,tax_category,tax_behavior,cashback_percent,tags,is_active,flo_csv_encoding\nprod-eq,'=SKU-EQ,'=Combo Deal,'=Equals Category,11,\"'+Rich, \"\"bold\"\"\nflavor\",4,,country_default,,\"'=tag-eq,featured\",yes,quote-v1\nprod-plus,'+SKU-PLUS,\"'+Special Curry, Large\",\"'+Plus, Category\",14.5,\"'-Spiced\r\ncurry\",5,,country_default,,'+tag-plus,yes,quote-v1\nprod-minus,'-SKU-MINUS,'-Discount Tea,'-Minus Category,6,'@Tea note,2,,country_default,,'-tag-minus,yes,quote-v1\nprod-at,'@SKU-AT,'@House Special,'@At Category,18,'=House note,7,,country_default,,'@tag-at,yes,quote-v1\nprod-ws-tab,'\t@SKU-TAB,'\t+Tab Formula Espresso,' =Space Formula Category,9.5,\"'\n=Newline Formula Desc\",3,,country_default,,veg,yes,quote-v1\nprod-ws-space,' +SKU-SPACE,' =Space Formula Roast,' =Space Formula Category,9,' -Space desc,3,,country_default,,veg,yes,quote-v1\nprod-quote,''SKU-QUOTE,\"''Chef's Signature, \"\"Hot\"\"\nBurger\",''Chef's Category,21,''Authentic recipe,8,,country_default,,''chef-tag,yes,quote-v1\nprod-qws,'' =SKU-QWS,'' =Literal Space Formula,''+Quoted Formula Category,17,'''@Double Quote At,6,,country_default,,veg,yes,quote-v1\nprod-qformula,''+SKU-QF,''+Literal Escaped Plus,''+Quoted Formula Category,16,'''Double Apostrophe Note,6,,country_default,,''+literal-tag,yes,quote-v1",
        "addons": "group_name,addon_name,price,group_required,group_min_select,group_max_select,track_inventory,stock_quantity,low_stock_threshold,flo_csv_encoding\n' =Space Group,'\t+Tab Addon,1.5,no,0,1,no,0,0,quote-v1\n''+Quoted Formula Group,'''Double Apostrophe Addon,2.5,no,0,1,no,0,0,quote-v1\n''Chef's Addons,''Artisan Cheese,4,no,0,1,no,0,0,quote-v1\n'-Minus Group,'@House Drizzle,3,no,0,1,no,0,0,quote-v1\n'=Formula Group,\"'+Extra Spice, Hot\",2,no,0,1,no,0,0,quote-v1"
      },
      "imports": {
        "categories": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 7,
          "failed": 0,
          "errors": []
        },
        "products": {
          "created": 0,
          "updated": 9,
          "reactivated": 0,
          "skipped": 0,
          "failed": 0,
          "errors": []
        },
        "addons": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 5,
          "failed": 0,
          "groups_created": 0,
          "groups_updated": 0,
          "addons_created": 0,
          "groups_reactivated": 0,
          "addons_reactivated": 0,
          "errors": []
        }
      }
    },
    {
      "cycle": 2,
      "imports": {
        "categories": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 7,
          "failed": 0,
          "errors": []
        },
        "products": {
          "created": 0,
          "updated": 9,
          "reactivated": 0,
          "skipped": 0,
          "failed": 0,
          "errors": []
        },
        "addons": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 5,
          "failed": 0,
          "groups_created": 0,
          "groups_updated": 0,
          "addons_created": 0,
          "groups_reactivated": 0,
          "addons_reactivated": 0,
          "errors": []
        }
      }
    },
    {
      "cycle": 3,
      "imports": {
        "categories": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 7,
          "failed": 0,
          "errors": []
        },
        "products": {
          "created": 0,
          "updated": 9,
          "reactivated": 0,
          "skipped": 0,
          "failed": 0,
          "errors": []
        },
        "addons": {
          "created": 0,
          "updated": 0,
          "reactivated": 0,
          "skipped": 5,
          "failed": 0,
          "groups_created": 0,
          "groups_updated": 0,
          "addons_created": 0,
          "groups_reactivated": 0,
          "addons_reactivated": 0,
          "errors": []
        }
      }
    }
  ],
  "postRoundTripDb": {
    "categories": [
      {
        "id": "cat-eq",
        "name": "=Equals Category",
        "description": "-Minus description"
      },
      {
        "id": "cat-plus",
        "name": "+Plus, Category",
        "description": "@At description,\nwith newline"
      },
      {
        "id": "cat-minus",
        "name": "-Minus Category",
        "description": "=Formula desc"
      },
      {
        "id": "cat-at",
        "name": "@At Category",
        "description": "+Plus desc"
      },
      {
        "id": "cat-ws",
        "name": " =Space Formula Category",
        "description": "\t+Tab Formula Desc"
      },
      {
        "id": "cat-quote",
        "name": "'Chef's Category",
        "description": "'Genuine quote, \"quoted\"\r\ndesc"
      },
      {
        "id": "cat-qformula",
        "name": "'+Quoted Formula Category",
        "description": "''Double Quoted Desc"
      }
    ],
    "products": [
      {
        "id": "prod-at",
        "sku": "@SKU-AT",
        "name": "@House Special",
        "category_id": "cat-at",
        "price": 18,
        "description": "=House note",
        "tags": "[\"@tag-at\"]"
      },
      {
        "id": "prod-eq",
        "sku": "=SKU-EQ",
        "name": "=Combo Deal",
        "category_id": "cat-eq",
        "price": 11,
        "description": "+Rich, \"bold\"\nflavor",
        "tags": "[\"=tag-eq\",\"featured\"]"
      },
      {
        "id": "prod-minus",
        "sku": "-SKU-MINUS",
        "name": "-Discount Tea",
        "category_id": "cat-minus",
        "price": 6,
        "description": "@Tea note",
        "tags": "[\"-tag-minus\"]"
      },
      {
        "id": "prod-plus",
        "sku": "+SKU-PLUS",
        "name": "+Special Curry, Large",
        "category_id": "cat-plus",
        "price": 14.5,
        "description": "-Spiced\r\ncurry",
        "tags": "[\"+tag-plus\"]"
      },
      {
        "id": "prod-qformula",
        "sku": "'+SKU-QF",
        "name": "'+Literal Escaped Plus",
        "category_id": "cat-qformula",
        "price": 16,
        "description": "''Double Apostrophe Note",
        "tags": "[\"'+literal-tag\"]"
      },
      {
        "id": "prod-quote",
        "sku": "'SKU-QUOTE",
        "name": "'Chef's Signature, \"Hot\"\nBurger",
        "category_id": "cat-quote",
        "price": 21,
        "description": "'Authentic recipe",
        "tags": "[\"'chef-tag\"]"
      },
      {
        "id": "prod-qws",
        "sku": "' =SKU-QWS",
        "name": "' =Literal Space Formula",
        "category_id": "cat-qformula",
        "price": 17,
        "description": "''@Double Quote At",
        "tags": "[\"veg\"]"
      },
      {
        "id": "prod-ws-space",
        "sku": " +SKU-SPACE",
        "name": " =Space Formula Roast",
        "category_id": "cat-ws",
        "price": 9,
        "description": " -Space desc",
        "tags": "[\"veg\"]"
      },
      {
        "id": "prod-ws-tab",
        "sku": "\t@SKU-TAB",
        "name": "\t+Tab Formula Espresso",
        "category_id": "cat-ws",
        "price": 9.5,
        "description": "\n=Newline Formula Desc",
        "tags": "[\"veg\"]"
      }
    ],
    "addonGroups": [
      {
        "id": "ag-eq",
        "name": "=Formula Group"
      },
      {
        "id": "ag-minus",
        "name": "-Minus Group"
      },
      {
        "id": "ag-qf",
        "name": "'+Quoted Formula Group"
      },
      {
        "id": "ag-quote",
        "name": "'Chef's Addons"
      },
      {
        "id": "ag-ws",
        "name": " =Space Group"
      }
    ],
    "addons": [
      {
        "id": "ad-at",
        "addon_group_id": "ag-minus",
        "name": "@House Drizzle",
        "price": 3
      },
      {
        "id": "ad-plus",
        "addon_group_id": "ag-eq",
        "name": "+Extra Spice, Hot",
        "price": 2
      },
      {
        "id": "ad-qf",
        "addon_group_id": "ag-qf",
        "name": "''Double Apostrophe Addon",
        "price": 2.5
      },
      {
        "id": "ad-quote",
        "addon_group_id": "ag-quote",
        "name": "'Artisan Cheese",
        "price": 4
      },
      {
        "id": "ad-ws",
        "addon_group_id": "ag-ws",
        "name": "\t+Tab Addon",
        "price": 1.5
      }
    ],
    "links": [
      {
        "product_id": "prod-plus",
        "addon_group_id": "ag-eq"
      },
      {
        "product_id": "prod-quote",
        "addon_group_id": "ag-quote"
      }
    ]
  },
  "legacyImports": {
    "productsResponse": {
      "created": 1,
      "updated": 2,
      "reactivated": 0,
      "skipped": 0,
      "failed": 0,
      "errors": []
    },
    "categoriesResponse": {
      "created": 1,
      "updated": 0,
      "reactivated": 0,
      "skipped": 0,
      "failed": 0,
      "errors": []
    },
    "addonsResponse": {
      "created": 2,
      "updated": 1,
      "reactivated": 0,
      "skipped": 1,
      "failed": 0,
      "groups_created": 1,
      "groups_updated": 1,
      "addons_created": 1,
      "groups_reactivated": 0,
      "addons_reactivated": 0,
      "errors": []
    },
    "dbState": {
      "provenPlusProduct": {
        "id": "prod-plus",
        "sku": "+SKU-PLUS",
        "name": "+Special Curry, Large",
        "price": 19.5,
        "description": "-Spiced\r\ncurry",
        "category_id": "cat-plus",
        "tags": "[\"+tag-plus\"]"
      },
      "ambiguousExistingProduct": {
        "id": "prod-qformula",
        "sku": "'+SKU-QF",
        "name": "'+Literal Escaped Plus",
        "price": 22,
        "description": "''Double Apostrophe Note",
        "tags": "[\"'+literal-tag\"]"
      },
      "unprovenNewProduct": {
        "id": "wzzoel",
        "name": "'+Unproven Legacy New",
        "description": "'-Unproven Desc"
      },
      "unprovenCategory": {
        "id": "cd225136-46a7-4a4e-bc9a-7482720a04d0",
        "name": "'=Equals Category",
        "description": "'-Minus description"
      },
      "originalCategory": {
        "id": "cat-eq",
        "name": "=Equals Category",
        "description": "-Minus description"
      },
      "provenAddonGroup": {
        "id": "ag-minus",
        "name": "-Minus Group",
        "is_required": 1
      },
      "unprovenAddonGroup": {
        "id": "4fb74f1c-cd63-4fd8-8e15-0ee5a9f46275",
        "name": "'+Unproven Addon Group"
      },
      "unprovenAddon": {
        "id": "9a898f3c-d522-47b0-8b88-dee2a6b2efda",
        "name": "'@Unproven Addon"
      }
    }
  },
  "unsupportedEncodingRes": {
    "categories": {
      "created": 1,
      "updated": 0,
      "reactivated": 0,
      "skipped": 0,
      "failed": 1,
      "errors": [
        "Row 2: unsupported flo_csv_encoding \"quote-v2\""
      ]
    },
    "products": {
      "created": 0,
      "updated": 0,
      "reactivated": 0,
      "skipped": 0,
      "failed": 1,
      "errors": [
        "Row 2: unsupported flo_csv_encoding \"unsupported-v9\""
      ]
    },
    "addons": {
      "created": 0,
      "updated": 0,
      "reactivated": 0,
      "skipped": 0,
      "failed": 1,
      "groups_created": 0,
      "groups_updated": 0,
      "addons_created": 0,
      "groups_reactivated": 0,
      "addons_reactivated": 0,
      "errors": [
        "Row 2: unsupported flo_csv_encoding \"legacy-v0\""
      ]
    }
  },
  "dailySalesItemsCsv": "product_id,product_name,product_sku,quantity,gross_item_sales,item_discounts,net_item_sales,tax_amount\nprod-quote,\"'Chef's Signature, \"\"Hot\"\"\nBurger\",'SKU-QUOTE,1,21,0,21,0\nprod-eq,'=Combo Deal,'=SKU-EQ,1,11,0,11,0\n"
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Merchant exports categories, products, and add-ons CSVs and receives flo_csv_encoding=quote-v1 headers/rows with doubled leading apostrophes and formula neutralization on =, +, -, @, and whitespace-pr… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant runs three consecutive export/import cycles on categories, products, and add-ons and preserves all names, SKUs, descriptions, tags, quoted commas/newlines, IDs, and product-to-add-on-group as… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports unmarked legacy product and add-on CSV rows carrying stable IDs and the importer strips leading apostrophes only where comparison with the existing stored record proves formula neutra… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports unmarked legacy CSV rows for category-only records, unproven new/unknown-ID records, and existing apostrophe-leading records, and ambiguous leading apostrophes are preserved without r… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant imports category, product, and add-on CSV rows with unsupported non-empty flo_csv_encoding markers (quote-v2, unsupported-v9, legacy-v0) and receives per-row validation errors while valid sib… ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
Merchant exports daily sales items CSV and other non-catalog CSV exporters continue using toCsvRow directly without adding flo_csv_encoding or doubling leading apostrophes ✅ pass live ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
  • npm run test:issue-248-csv
  • npm run test:reports-export
  • npm run test:daily-sales-export
  • npm run build
  • Live HTTP verification against compiled dist/main/server.js exercising /api/auth/login, /api/menu-csv/export/{categories,products,addons}, /api/menu-csv/import/{categories,products,addons}, and /api/reports/daily-sales/export with evidence written to ~/.no-mistakes/evidence/01M4YF3731WE3TCQHWA6M6NJVA/catalog-csv-roundtrip-live-evidence.json
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: FreeOpenSourcePOS/FloCafe/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bc8e7cc7-f0e3-4f99-afbf-8c9546e2a31d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@khaira777

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Adds reversible CSV encoding for catalog exports to prevent formula injection.

Fix tag whitespace preservation before merging so marked product exports round-trip without changing stored tags.

Findings

  1. P1 Tag whitespace disappears on import ▶

Summary

Adds reversible catalog CSV encoding with flo_csv_encoding=quote-v1.

  • Catalog CSV exports preserve genuine apostrophes and formula-leading text.
  • Catalog CSV imports decode marked rows and keep ambiguous legacy names intact.

Reviews (4) · Last reviewed commit: "Merge branch 'main' into codex/catalog-c..." · Reviewed by Greptile

…th `oxfmt` to satisfy the changed-file formatting check in `Lint & Build Validation (Linux)` (which also caused `Required Checks Gate` to fail). ### Invariant - Every file changed by the PR must pass `oxfmt --check` (via `scripts/ci/check-changed-format.cjs`). - Checked all changed root files (`main/lib/csv.ts`, `main/routes/menu-csv.ts`, `tests/issue-248-menu-csv.test.ts`, `tests/menu-csv-formula-export.test.ts`); only `tests/menu-csv-formula-export.test.ts` had formatting violations. ### Verification - `npx oxfmt --check -- main/lib/csv.ts main/routes/menu-csv.ts tests/issue-248-menu-csv.test.ts tests/menu-csv-formula-export.test.ts`: passed - `npm run lint`: passed (0 errors) - `npm run build`: passed - `npm run docs:check` & `git diff --check`: passed - `npx ts-node --transpile-only tests/menu-csv-formula-export.test.ts`: passed (21/21) - `npx ts-node --transpile-only tests/issue-248-menu-csv.test.ts`: passed (284/284)
@khaira777 khaira777 changed the title fix(main): preserve catalog CSV round-trip names fix(catalog): preserve CSV round-trip names Oct 15, 2026
@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

1 similar comment
@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread main/routes/menu-csv.ts
decodeQuoteV1Field restores the safety prefix (tab/space/newline/CR)
before the per-tag trim stripped it again, silently changing stored
tags like '\t=1+1' to '=1+1'. Keep tags verbatim in quote-v1 mode so
the encoding round-trips reversibly; legacy imports keep trimming.
@khaira777

Copy link
Copy Markdown
Contributor Author

Greptile P1 addressed in cfa4483: in quote-v1 mode the importer no longer trims per-tag whitespace after decodeQuoteV1Field restores it, so stored tags like \t=1+1 round-trip byte-for-byte. Legacy (unmarked) imports keep the existing trim behavior. Added a round-trip regression to tests/menu-csv-formula-export.test.ts (36/36) covering tab, space, newline, and CR triggers; tests/issue-248-menu-csv.test.ts still passes 284/284. Marking the thread resolved.

@khaira777
khaira777 merged commit c65859f into main Oct 15, 2026
23 checks passed
@khaira777
khaira777 deleted the codex/catalog-csv-roundtrip branch October 15, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(catalog): unescape formula-neutralized quotes during menu CSV import

1 participant