phase2/negotiation_followup: $0 hold-reply never supersedes a dealer's real OTD; advisory uses the real best-competing OTD#42
Merged
Conversation
…r's real OTD; advisory uses the real best-competing OTD A no-fresh-number reply (hold / payment-only / come-onsite) extracted to otd_total=0 and — because SQLite MIN()/nulls-last rank 0 as the cheapest — superseded the dealer's real OTD on the board + latest-quote views (PIC-20260629r2-5). Separately, the negotiation advisory fed its "at or below best" tone the strict itemized give-up BATNA, so a quote $49 ABOVE a cheaper non-itemized competitor read "at or below best" (PIC-20260629r2-4). - persist.ts: write-normalize otd_total <= 0 -> null (a no-number reply is still recorded for provenance, but never persists a $0 quote). - migration 0006: heal pre-existing $0 rows -> NULL (data fix, no schema change; drizzle-kit generate stays no-op so db:check passes). - followupReads.ts: latest/competing reads skip otd_total <= 0; a new bestCompetingRealOtd (lowest real same-mode competing OTD, no itemization gate) drives the tone/display, while the strict itemized BATNA still governs the give_up_switch verdict. - negotiationProjection.ts: tone + batna_gap + best_competing_otd use the real baseline; MIN(NULLIF(otd_total,0)) heals the board roll-up. - negotiationAdvice.ts: the give_up_switch text cites the strict itemized gap; the tone wording cites the real gap. - giveUpProjection.ts: the board card's displayed gap mirrors the modal's real gap (verdict stays strict) so the two surfaces never disagree. Deterministic regressions across persist/projection/giveUp/advice. Parity restored: the latest-quote selector ignores $0/null and the tone uses the lowest real competing OTD, with the give_up_switch verdict still on the strict itemized BATNA.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two compounding negotiation-board correctness defects from live-e2e 2026-06-29-run2.
PIC-20260629r2-5 — a
$0hold-reply supersedes a dealer's real OTD. A "my $26,905 stands, I won't chase" reply re-extracted to adealer_quotesrow withotd_total=0; SQLiteMIN()/nulls-last rank0as the cheapest, so the boardbest_otd+ the latest-quote views showed$0where a real OTD was held.PIC-20260629r2-4 — "at or below best" advisory off by $49. The tone classifier was fed the strict itemized give-up BATNA, which dropped a cheaper bottom-line (non-itemized) competitor, so a quote $49 above the real best read "at or below best".
Fix
persist.ts— write-normalizeotd_total <= 0 → null. A no-number reply is still recorded (provenance) but never persists a$0quote.0006— heal pre-existing$0rows →NULL(data fix, no schema change;drizzle-kit generatestays no-op sodb:checkpasses).followupReads.ts— latest/competing reads skipotd_total <= 0; a newbestCompetingRealOtd(lowest real same-mode competing OTD, no itemization gate) drives the tone/display, while the strict itemized BATNA still governs thegive_up_switchverdict.negotiationProjection.ts— tone +batna_gap+best_competing_otduse the real baseline;MIN(NULLIF(otd_total,0))heals the board roll-up.negotiationAdvice.ts— thegive_up_switchtext cites the strict itemized gap; the tone wording cites the real gap.giveUpProjection.ts— the board card's displayed gap mirrors the modal's real gap (verdict stays strict) so the two surfaces never disagree.Verification
RUN_UI_FUNCTIONAL=1 bash scripts/green.sh→ GREEN;db:checkclean (drizzle-kit generate= "No schema changes"); migrator test applies0006cleanly.persist.test.ts($0→nullat write),negotiationProjection.test.ts(boardbest_otdignores$0; advisory uses the real competing OTD; non-itemized-competitor case;currentOtdignores a newer$0),giveUpProjection.test.ts(verdict strict, displayed gap real),negotiationAdvice.test.ts(give_up_switch cites the itemized gap, not the tone gap).