---
name: evolution-quote-description-scramble
description: Quote description scramble root-cause — kit-indent helpers mix id-key detection with DOM-position writes; audit doc + open fixes
metadata: 
  node_type: memory
  type: project
  originSessionId: 64b4d6d7-154a-47cc-b81b-60f8034af36f
  modified: 2026-07-29T10:05:31.806Z
---

Otter Fencing quote 77588 (tenant `108-1481181613`) description scramble, 2026-07-28.
Living doc: `evolution/audits/QUOTE_DESCRIPTION_SCRAMBLE_77588.md`.

Corruption rule (proven from the repair backup `quoteitems_bak77588_20260729` +
`date_updated`): **source = every kit child row; target = the row immediately below it;
indent stripped on the top-level targets.** It is a DUPLICATION (source keeps its text)
and the scope is NON-CONTIGUOUS — so the positional `itemdescription[]`↔`quoteitemid[]`
binding in `quoteditsave.php` is a real weakness but NOT this cause. Two saves did
different things: 10:41:20 hit kit children only, 10:44:20 hit top-level rows only.

Mechanism: `global.js` `addChildIndentation`/`removeChildIndentation` **detect** a child
by id key (`parentid<cnt>`) but **write** by DOM position (`parentTable.rows[cnt]`) in the
`$('#desc'+cnt).length == 0` fallback. Precondition is the `newRows` vs `rowCount`
divergence in `addLineItem` — after ONE add, 38/38 rows have `domPos != descId`.

**Do NOT just delete that fallback.** Only two JS callers exist: quotedit.js and
purchaseadd.js (the many `getParentIDs()` hits in .inc/.php are a different server-side
function of the same name). purchaseadd's server rows have NO `desc<N>` id at all
(`purchaseadd.inc:779`), pass the whole `<table>` incl. thead, and call `hideRows()` with
`rowIndex` — so the fallback is that page's ONLY path and is correct there. The
`$('#desc'+cnt).length > 0` test is an implicit PAGE SNIFF, not a safety check; when a
hole appears in quotedit's id sequence it silently switches to purchaseadd's addressing
mid-loop. Fix = split the addressing schemes, not delete the branch.

Hole producer (Shane): deleting a tab WITH its items is the only thing in the app that
hard-DELETEs `quoteitems` rows; everything else soft-deletes via `status=0`.
The old "move it into DOMContentLoaded" fix removed a racer but left the divergence,
and `expandingText.js:18-22` still runs the duplicate init at parse time.

NOT yet reproduced: a clean add keeps `desc<N>` contiguous so the fallback never fires.
Need a HOLE in the id sequence (row removed from DOM, not soft-deleted) — see §6 of the doc.

**Why:** the obvious suspect (positional POST binding) is the wrong answer here; chasing
it wastes time. The non-contiguous, duplicating signature is the discriminator.

**How to apply:** fixes F1–F7 in the doc are proposed, NOT applied — get approval first
per [[feedback-summary-before-changes]]. Open: prod row 632936 still corrupt; dev quote
18947 has leftover probe rows to delete.

Related: [[evolution-multiselect-audit]], [[browser-screenshot-tooling]] (harnesses live in
`/config/workspace/browser-tools/`: repro-shift/repro-trace/repro-race.mjs).
