# BidBrain project status, part 10: v2.8, CompMatch (25 August 2026)

Moved unchanged from CLAUDE.md on 2026-09-18. The standing rules stay in CLAUDE.md, which lists every part of this log.

**v2.8 starts here, 2026-08-25 (after the v2.7.12 tag was cut, following v2.7.5 through v2.7.12 as a run of small same day patches).**
  - THE SUGGESTED BID MADE LIKE FOR LIKE, AND BACKFILLED FROM BIDBRAIN'S OWN DATABASE FIRST, 2026-08-25 (Mark, two follow ups on the same feature in one breath: "one thing im not sure we have is the % of cazana retail price taken in to account for the ai prediction... im always bidding 88% im lossing most of them and so having this info should improve the accuracy", then, once the retail backfill for this was already running, "as we bid on cars over the next month this list is gonna populate with more data... it would make sense to populate this from our database as much as possible", and separately "compare year, transmission, engine size, mileage so that the ai is comparing as similar cars as it can"). Explicitly given permission for this to ship as v2.8 ("this is a large release"), the standing "stay on v2.7.x patches" instruction from earlier the same day no longer applies from here.
    - THE % OF RETAIL IDEA, investigated first, real data confirms Mark's own hunch. `bid_retail_enrich.py` (new): `--mileage` visits each lost bid's own still viewable Motorway listing page (proven live, a sold car's own page stays up and readable indefinitely) and reads its real mileage; `--retail` then looks up Cazana for every lost bid that now has one, storing it on `lost_bids.mileage`/`cazana_retail` (both already migrated in). Both scoped to only the model groups with at least `--min-samples` (default 10) real comps, "start with the biggest groups" (Mark's own choice, offered as a real tradeoff via AskUserQuestion, against a full 931 row backfill that would simply take too long to be worth starting with). A live pilot (5 cars) showed sold-for/retail ratios genuinely clustering 73 to 81% for the same rough model, directly matching Mark's own "a 3 series winning bid was 90% of retail" framing. `db.bid_comps_with_retail()` combines both tables' own (name, amount, retail) triples where a real retail figure is on file (a strict subset of `bid_comps()`, purchases' own via `cazana_115 / 1.15`, lost bids' own `cazana_retail` directly), ready for a ratio based model once enough real data exists; not yet wired into the actual suggestion (superseded by the richer like for like model below before that step was reached, kept as the real, tested building block it is).
    - THE COMPARISON MADE LIKE FOR LIKE, the second, larger ask. `bidbrain/bid_suggest.py` reworked: `build_pool(comps_full)` groups `db.bid_comps_full()`'s own rows (name, amount, retail, mileage, year, transmission, engine, any of the last five may be `None`) by `model_key`; `suggest_bid_for_car(car, pool, min_samples=5, top_n=15)` scores every real comp for that make and model against the actual car being priced (`_similarity`, weighted: mileage heaviest since it is the single biggest real driver of what a specific car is worth, tapering off past 15,000 miles apart; year tapering off past 4 years; transmission a binary match; engine size, parsed as a plain X.X litres figure via `engine_litres()`, tapering off past 0.6L apart), only ever scoring a factor when BOTH the car and the comp have it, never guessed and never penalised for a historical gap. Narrows to the closest `top_n` once there is real ranking signal (at least `min_samples` comps score above zero), otherwise falls back to the WHOLE model group, the exact same figure the original coarse `build_stats`/`suggest_bid(make, model, stats)` pair produced, which this supersedes outright and replaces at the one production call site (removed rather than kept as unused dead code, per house style). `render._hf_gauge`'s own tooltip now says which kind of comparison a suggestion got ("closely matched on mileage, year, transmission or engine size" versus plain "same make and model"), via a new `car._suggested_bid_llk` flag threaded the same private, never persisted way as `_suggested_bid`/`_suggested_bid_n` (`daily_run._write_pages`, `bid_pool = bid_suggest.build_pool(db.bid_comps_full())` computed once per render, same cost profile as before).
    - POPULATING FROM BIDBRAIN'S OWN DATABASE FIRST, the third ask, and the piece that makes the like for like comparison actually work at scale over time rather than needing a live re-read for every historical row. `sightings` (already logging every car BidBrain reads off every platform each day, before any gate is applied, by far the widest net on file, much wider than `assessments` which only ever covers gate passers, checked live: only 2 of 931 lost bids and 4 of 107 purchases ever had a matching `assessments` row, "too sparse to use") gained `mileage`, `engine`, `transmission` columns (`year` was already there), written by `record_sightings()` from the very same `Car` fields already read for every single sighted car, no extra cost. New `db.latest_sighting_for_reg(reg, before_date=None)` looks up the most recent sighting on file for a reg, optionally only counting one on or before a given date (a car's mileage only ever goes up, so a sighting from after a deal was won or lost is never a safe stand in for its mileage at the time). `bid_retail_enrich.py --fields` (new phase, database only, no browser) fills in year/transmission/engine on any lost bid or purchase still missing them from the closest sighting on file (`db.set_lost_bid_fields`/`set_purchase_fields`, both `COALESCE`-based so a value already on file is never overwritten); the `--mileage` phase now checks a sighting first too, only falling back to a live Motorway read when nothing is on file. Checked live against the real database: only 5 of 758 target rows currently have a matching sighting at all, because `sightings` itself only goes back to 2026-08-19 (five days, since that is when Mark's own Mac started running this project) while the lost bid and purchase history reaches back months earlier, an honest, expected data coverage gap, not a bug, that closes on its own as fresh sightings accumulate day by day exactly as Mark's own framing anticipated ("as we bid on cars over the next month this list is gonna populate with more data").
    - A REAL GAP FOUND AND FIXED ALONG THE WAY: `purchases` never had a mileage column at all, despite `purchases_run.py` already reading a real one off every Motorway and Carwow purchase's own page (used once, in memory only, to run the existing Cazana retail estimate lookup, then thrown away). Added `purchases.mileage` and folded it into the existing `_PURCHASE_OWN_FIELDS` fill-once-never-overwrite set (`db.record_purchases`), so it is now persisted for free from the exact same read already happening, no extra page visit, available to `bid_comps_full()` alongside the year/transmission/engine columns for the purchases side of the comparison.
    - Verified: 429 tests pass (9 new: `engine_litres` reading a plain X.X figure and correctly finding nothing to guess at in a badge like "320d"; `build_pool`/`suggest_bid_for_car` finding the right group, respecting the `min_samples` floor, and returning `None` for a genuinely unseen model; a synthetic like for like case proving a car's own mileage correctly ranks it against the CLOSEST real comps rather than a far higher mileage half of the same group; a synthetic case with no rich data anywhere in the group proving the full comp set is used rather than an arbitrary `top_n` slice once there is nothing real to rank on; the gauge showing the right tooltip wording for a like for like suggestion). The real migration was run live against the production database (`db.init_db()`), confirmed the new columns exist and the existing 758 target rows and 679 lost bids with sale prices were untouched. The `--retail` Cazana backfill (already running when this second round of work started) continued unaffected throughout, proving the new schema and the in-flight batch coexist safely. Live `--render-only` afterward showed 209 of 458 real shortlisted cars carrying a suggestion, 127 of them already genuinely like for like even with the retail backfill still only around half done, since year alone (already tracked in `sightings` long before this change) was enough real ranking signal on its own for many of them.
    - COMPMATCH NAMED, AND ITS REAL AGE/TRANSMISSION COVERAGE GAP FOUND AND FIXED, shipped as v2.8.2, 2026-08-25, same day (Mark: "make sure the age and mileage are being considered as the ai bids seem a bit off" then "and if its manual or auto as auto will be worth more", and separately "give the ai suggested bid a name so i can come back to improve later", answered by naming the whole engine CompMatch in `bid_suggest.py`'s own module docstring). Checked the real live comp pool rather than guessed at a cause: of 1,038 real comps, only 5 carried a year and 0 carried a transmission (mileage was fine, 679, and retail 623), so `suggest_bid_for_car`'s own similarity ranking almost never had age or gearbox to compare on, and most suggestions were quietly using the unranked "same make and model" fallback despite the like for like mechanism working correctly whenever it actually had data. Root cause: the `--fields` backfill only ever tried a real sighting on file, and `sightings` itself only goes back to 2026-08-19, five days, hopelessly short against lost bid and purchase history reaching back months.
      - Fixed with two purely algorithmic derivations needing no lookup at all, checked live against the real regs and names first: `bid_suggest.year_from_reg(reg)` reads a real registration year straight off a current format UK plate's own age identifier (AA99AAA, the two digits are 01-50 for March to August of 2000+that figure, or 51-99 for September to February of 2000+(that figure minus 50)), proven against real examples in the data (PN68ZWX correctly reads 2018, AY66WFW correctly reads 2016, both matching known real world plate reading); checked live that 980 of 1,038 real regs actually match this format, only an older letter prefix/suffix style or a private plate does not, both correctly returning None rather than guessed. `bid_suggest.transmission_from_name(name)` finds a real automatic badge in the platform's own free text vehicle name (Auto, Automatic, DSG, Tiptronic, S Tronic, PDK, CVT, or Motorway's own "S-A" abbreviation for Semi-Automatic), checked live: 120 of 1,038 real names carry one, 0 ever spell out "Manual", so this only ever returns "Automatic" or None, never guessing Manual from an absence, the same convention already established by pricing.py's own `_gearbox_label` for a live car.
      - `bid_retail_enrich.py --fields`'s new `_derived_fields(r, before_date)` tries a real sighting first (the most precise, an actual read of that exact car), then falls back to these two derivations for whatever a sighting could not supply. Also widened to run over EVERY lost bid and purchase rather than just the "biggest groups" scope `--mileage`/`--retail` deliberately use: those two are real, time costly browser lookups, this one is a pure database join plus two regexes, free, so there was no reason to leave a smaller model group without its own comps' age and transmission too.
      - Verified live end to end: re ran `--fields` against the real database, year coverage rose from 5 to 981 of 1,038 real comps (94%), transmission from 0 to 160 (honestly still modest, matching the real 15% of names that ever carry a badge, never inflated by guessing). `daily_run.py --render-only` against the real 458 shortlisted cars afterward showed like for like coverage rise from 127 to 187 of the 209 suggested cards (about 89%, up from 61%), only 22 cards now genuinely falling back to the plain comparison (a car whose own reg is an older or private plate shape, or whose model group's comps still lack data). 15 new tests (`year_from_reg` against real March-August and September-February identifiers, a plate with a space, and three genuinely unguessable shapes; `transmission_from_name` against three real automatic badge styles and one genuinely unlabelled name). 444 tests pass.
    - RECOVERING REAL TRANSMISSION FROM A VEHICLE'S OWN PAGE, shipped as v2.8.8, 2026-08-25, same day, straight after the hard gate above (Mark, once shown the real 209-to-13 drop plainly: "these limited suggested bids look much closer, any way to increase the amount of cars you can suggest bids on"). Investigated live before building anything, the same discipline as every other CompMatch fix this session: does a lost bid or purchase's own still viewable Motorway vehicle page carry anything better than the name-badge guess or the thin sighting backfill already tried? Checked several real pages (a mix of known automatic and, deliberately sought out, manual examples): every one carries a real "Vehicle details" block with a literal "Transmission\nAutomatic" or "Transmission\nManual" line, plus "Engine size\nNNNN cc", genuine confirmed platform data, not a guess, the exact same shape already trusted for mileage since v2.8.2. Crucially this is the FIRST source in this whole project able to positively confirm "Manual" at all, `transmission_from_name` (v2.8.2) can only ever return "Automatic" or None by design.
      - `motorway.read_vehicle_mileage` replaced outright with `read_vehicle_details(page, listing_url)` (one visit, no extra cost over the old mileage-only version), returning `{"mileage", "transmission", "engine"}`; new pure parsers `_parse_transmission` (the literal "Transmission\nX" line) and `_parse_engine_cc` (the literal "Engine size\nNNNN cc" line, converted to the same "X.X" litres text `bid_suggest.engine_litres()` already parses everywhere else, e.g. 1461cc reads as "1.5"). `bid_retail_enrich.py`'s `run_mileage` (still the `--mileage` flag, now doing more than its name alone suggests) reworked to target BOTH lost bids missing mileage OR transmission, AND Motorway purchases missing transmission (mileage is already known for a purchase from the time it was bought, so purchases only need the transmission/engine half), one continuous session, the existing sighting-first shortcut kept for whichever of a row's own needs a sighting can actually answer, a live visit only for whatever is still missing after that.
      - A real full batch run, not just a small proof: 634 rows in scope (586 lost bids, 48 Motorway purchases, within the biggest model groups, matching every other enrichment pass's own scope), all 634 processed cleanly, watched live via a repeated `bid_comps_full()` transmission count check rather than the log file (which stays empty until the process exits, the same stdout buffering quirk already known from earlier valuation backfills, not a sign of anything wrong). Confirmed transmission coverage across the whole real comp history rose from 180 (160 Automatic, 20 Manual from the earlier pilot, 0 before that) to 814 of 1,038 (616 Manual, 185 Automatic, 13 Semi-automatic), the large majority now genuinely known rather than guessed or absent. `daily_run.py --render-only` against the real 458 shortlisted cars confirmed the real payoff: suggested cards rose from 13 back up to 119, over 9x, while the v2.8.7 hard gate stays completely untouched, no manual and automatic comp has ever been mixed at any point in this recovery.
      - 5 new tests (`_parse_transmission` against a real page's own text shape for both a real Automatic and a real Manual example, and against a page with neither; `_parse_engine_cc` converting a real cc figure to the expected litres text, and finding nothing on an unrelated page). 476 tests pass.
    - TRANSMISSION MADE A REAL HARD GATE, NEVER A SOFT FALLBACK, shipped as v2.8.7, 2026-08-25, same day, straight after the segmentation above (Mark, not satisfied the soft "fall back to the mixed pool when there is not enough of one transmission" version went far enough: "the suggested bid must hard gate respect the difference between manual and auto, it would be better to have less data than inc auto and manual together"). `suggest_bid_for_car` reworked: the transmission filter now runs FIRST, unconditionally, whenever the car being priced has a known transmission (virtually always true, a live car is read straight off the platform), narrowing `candidates` to ONLY comps that also have a known, MATCHING transmission, with NO `min_ratio_samples`-gated fallback to the mixed pool any more; `min_samples` (still 5) is now checked AFTER that filter, so a model with too little of exactly this car's own transmission returns None outright rather than ever quietly blending in the other kind. Only when the car's OWN transmission is genuinely unknown does the gate not apply at all, there being nothing to respect the difference against.
      - The real, honest consequence, checked live rather than assumed: this is a big swing, not a small one. Every one of the 484 real live cars in the current run has a known transmission (332 Manual, 128 Automatic, 24 Semi-automatic, none blank, confirmed by inspecting the real cache), so the gate now applies to essentially every car. But "Manual" is essentially NEVER a confirmed value anywhere in the historical comp pool yet (`transmission_from_name`, from v2.8.2, only ever returns "Automatic" or None, by design, it never guesses Manual from an absent badge; real sighting-sourced Manual values only exist for the last few days of history), so a Manual car, the majority of the fleet, currently has zero real Manual comps to gate against and gets no suggestion at all. `daily_run.py --render-only` against the real 458 shortlisted cars confirmed it: suggested cards dropped from 209 to 13. Told Mark this plainly rather than let the number speak for itself, this closes on its own as real sighting-sourced transmission data (captured going forward since v2.8.3) accumulates and starts feeding real Manual comps into lost bids and purchases, not something to soften without his own say so given how explicit the instruction was.
      - 7 tests reworked in the same block that covered the old soft fallback: a genuine automatic match still correctly isolates only the model's own automatic comps (untouched by manual ones sitting right next to them in the same synthetic pool); too few of the car's own transmission among the comps now asserts a straight `None`, not a fallback count; a new check proves the SAME thin pool works fine for a MANUAL car with plenty of its own comps, proving the gate is genuinely symmetric, not automatic-favouring; a new check proves a car with no known transmission at all (the one case the gate cannot apply to) still correctly uses the whole model group. 471 tests pass.
    - REAL PER MODEL TRANSMISSION SEGMENTATION, AND THE ROOT CAUSE OF THE BACKWARDS DATA FOUND, shipped as v2.8.6, 2026-08-25, same day, straight after the cap above (Mark, still not satisfied: "i still think you can get it better by really considering what % of cazana retail each make model and transmission each car achieves"). The v2.8.5 cap was a symptom fix, this is the root cause one. Checked live before building anything: pooling ALL comps across every model, automatic (78 comps) showed a LOWER median ratio (84.6% of retail) than everything else (545 comps, 93.3%), backwards from reality, and confounded, a global pool mixes WHICH MODELS happen to get badged "Auto" in their own name with the transmission effect itself, not a same-car comparison at all. Narrowed to the 12 real model groups with an actual same-model automatic-versus-other split (at least 3 comps each side, for example Mercedes A 16 auto vs 18 other, Kia Sportage 18 vs 62): 3 of 4 spot checked STILL showed automatic selling for LESS of its own retail, still backwards, even within one model. Root cause found by re running the same check after excluding any comp whose own ratio exceeds 105% of its own retail (the same implausible-data pattern the v2.8.5 cap exists for, about a third of comps for some models): 3 of 4 flipped to the expected, sensible direction, automatic selling for MORE. The bad data was not just inflating the final suggested figure (what the cap already caught), it was actively corrupting which direction the transmission signal itself pointed.
      - Two changes to `bid_suggest.suggest_bid_for_car`. (1) A new module constant `MAX_PLAUSIBLE_RATIO = 1.05`; the ratio list built for the % of retail calculation now excludes any comp whose own `amount / retail` exceeds it BEFORE the percentile is ever computed, not just clamped on the way out the door as v2.8.5 already did (both stay, the cap is still a real backstop for whatever slips past this earlier filter). (2) Real transmission segmentation: when the car being priced has a known transmission and at least `min_ratio_samples` (3) of ITS OWN model's own comps share that exact transmission, `candidates` is narrowed to just that same-transmission subset BEFORE the mileage/year/engine/grade similarity ranking runs, a hard filter, not a soft nudge folded into the blended `_similarity` score the way it worked before; falls back to the full model group exactly as before whenever there is not yet enough of this model's own transmission split, which is still most models, real transmission coverage across the whole comp pool is only about 15%. Deliberately never a CROSS-model adjustment (tried, rejected, that is exactly the confound that gave the backwards global signal in the first place), always the same model compared against itself. New `transmission_matched` key on the returned dict, available for future use, not yet threaded into the cockpit's own tooltip (the existing "closely matched on mileage, year, transmission or engine size" wording already covers it honestly enough for now).
      - Verified live end to end: re ran the exact same automatic-vs-other check inside `suggest_bid_for_car` itself (not just the raw db query) on Mercedes A, Kia Sportage and BMW 320D, all three now show automatic segmented from other correctly and sensibly. CE16DVT (the real Ford Kuga from the complaint above) now suggests £7,647, no longer needing the cap at all (`capped: False`), close to and slightly above BidBrain's own £7,084 max bid formula, a genuinely sensible figure rather than either the original nonsensical £10,807 or the merely-clamped £8,769. A full `daily_run.py --render-only` rebuild of the real cockpit showed capped cards drop from 69 to 19 of 209 (a 73% reduction), confirming the root cause fix, not just the backstop, is doing most of the real work now. 6 new tests (plausible comps correctly separated from a deliberately implausible batch, proving the suggestion lands on the plausible figure only, not distorted or merely capped after the fact; a real same-model transmission segmentation proving an automatic car is compared only against this model's own automatic comps once there are enough, flagged as like for like; too few of the model's own matching transmission correctly falling back to the whole group). 469 tests pass.
    - A SUGGESTION COULD SAY TO PAY MORE THAN A CAR WAS WORTH, FOUND LIVE AND CAPPED, shipped as v2.8.5, 2026-08-25, same day (Mark, on a real live car: "CE16DVT this is partically silly this ai suggested bid, manual is worth less than automatic, more mileage = low value, new the car means its worth more, the ai bids are way off atm"). Investigated the exact real car rather than guessed at a fix: CE16DVT, a 2016 Ford Kuga, 60,800 miles, Manual, Cazana retail £8,769, was suggesting £10,807 to win, genuinely above its own retail value, real nonsense for buying advice. Traced to the model's own real comp pool: of 25 real "Ford Kuga" comps carrying both a winning price and a retail figure, 14 (56%) showed a winning price ABOVE their own retail (ratios up to 1.40, 140% of retail), so the 70th percentile ratio CompMatch scales onto a car's own retail landed comfortably over 100%. Most likely stale or mismatched historical Cazana lookups on older purchases and lost bids rather than dealers genuinely overpaying at that scale, but the real cause did not matter for the fix, a suggestion that says pay more than a car is actually worth can never be sound advice regardless of why the underlying data looks that way, the same "play safe" discipline as the rest of this project.
      - `suggest_bid_for_car` now clamps its own final `suggested` figure to never exceed `car_retail` when it is known, on BOTH computation paths (the ratio based one, and the flat raw price fallback, a raw historical price could exceed a specific car's own current retail just as easily), returns a new `capped` flag in its own result dict. `daily_run.py` threads it through as `car._suggested_bid_capped`, the same private, never persisted convention as `_suggested_bid_llk`/`_suggested_bid_ratio`; `render._hf_gauge`'s own tooltip now says "capped at this car's own retail value, some of the real sales it is based on look high against their own retail" when it applies, plain and honest about why the figure was pulled down rather than silently changing it with no explanation.
      - Also checked the other two specific complaints directly against this exact car's own real comp data before assuming they needed a fix too: mileage WAS working correctly (the top ranked real comps clustered 57,000 to 88,000 miles against this car's own 60,800, not scattered); transmission genuinely had NOTHING to compare on for this specific model, 0 of 33 real Ford Kuga comps carry a transmission value at all (Kuga diesels of this era are almost always manual in the real world and essentially never carry an "Auto" badge in their own listing name, which `transmission_from_name`'s own design deliberately never guesses around), an honest data sparsity limitation for this one model rather than a bug, matching the 15% overall transmission coverage already known from v2.8.2/v2.8.3. Told Mark both findings plainly rather than claim a fix for something that was not actually broken.
      - Verified live end to end: re computed CE16DVT's own real suggestion directly against the live database both before and after the fix (£10,807, capped True false beforehand since the flag did not exist yet, to £8,769, capped True afterward, exactly its own real Cazana retail), then a full `daily_run.py --render-only` rebuild of the real cockpit confirmed the fix reached the actual page (grepped the real card, "AI suggests to win... £8,769... 13 similar sales") and found the cap is genuinely widespread, not a one off: 69 of 209 real suggested cards (about a third) were being capped before this fix, now all landing at a sensible, never-above-retail figure. 5 new tests (a synthetic case where every real comp's own ratio would suggest paying more than a car's own known retail, proving the cap lands exactly on that retail figure and is correctly flagged; the same cap proven on the flat raw price fallback path too, not just the ratio one; a genuinely under-retail suggestion proven untouched by the cap; the gauge's own new tooltip wording). 463 tests pass.
    - COMPMATCH LEARNING ACROSS BOTH DEALERSHIPS, shipped as v2.8.4, 2026-08-25, same day (Mark: "so i run a dealership called RightDrive and steven runs a dealership called Really easy car credit, we are both happy for CompMatch to learn from both our data can you wire this in"). This is a real cross business decision, not just a code change, both Macs run their own separate BidBrain install with their own local database, so the question was HOW two physically separate machines actually move data between them; asked rather than guessed, Mark chose the shared private GitHub repo both dealerships already use to collaborate on the code, over a shared cloud folder.
      - New top level `shared_comps/` folder, deliberately tracked in git (unlike everything under `data/`, which stays local per Mac, see `.gitignore`, golden rule 6, keep purchase and margin data local, applies to `data/`, this is a narrow, both sides agreed exception scoped to comp data alone). One JSON file per dealership, `shared_comps/<dealer_key>.json`. `bidbrain/comp_share.py`: `dealer_key(name)` slugifies a real dealer name into a filesystem safe key ("Right Drive" -> "right_drive"); `export_comps(dealer_name, out_dir, db_path)` writes ONLY the eight fields CompMatch itself ever compares on (name, amount, retail, mileage, year, transmission, engine, grade, the exact shape `db.bid_comps_full()` already returns), never a registration, a price breakdown, a margin, or anything else about either business, raises loudly on a blank dealer name rather than write a nameless file nobody could ever attribute; `load_shared_comps(exclude_dealer, comps_dir)` reads every OTHER dealership's own file (skips this Mac's own, its live database already covers that more freshly than a snapshot could), returns an empty list for a missing or empty folder so a fresh checkout with nothing shared yet works exactly as before.
      - `dealer_config.py` gained `DEALER_NAME` (Right Drive's own live value, "Right Drive"; `dealer_config.example.py`'s own copy defaults to `""`, meaning opted out, matching `load_shared_comps`'s own "nothing to exclude, nothing found" graceful behaviour). `daily_run.py`'s `bid_pool` construction and `bid_retail_enrich.py`'s `_target_keys` both now pool `db.bid_comps_full()` with `comp_share.load_shared_comps(exclude_dealer=dealer_config.DEALER_NAME)` before building the comp pool, so a model group thin on this Mac's own history but well covered by the other dealership's is both usable for a suggestion AND worth the real browser time `--mileage`/`--retail` spend enriching it.
      - New top level `share_comps.py` (`--export`, local only, never touches git; `--sync`, the real thing): fetches origin, then `git checkout origin/main -- shared_comps/` specifically (never a full pull or rebase, deliberately surgical, this repo is also the live application code on the same Mac, touching only this one path avoids any risk of pulling in an unrelated code change or fighting an unrelated local edit mid sync), re-exports this Mac's own fresh comps over it, and only commits and pushes if this Mac's own file actually changed. A push failure (someone else pushed since the fetch) prints a clear "run again to retry" rather than force anything.
      - Wired into the cockpit the same way every other on demand pass already is: a new "Sync CompMatch data" button in the Run modal's Other checks section, `serve.py`'s `_sync_compmatch` running `share_comps.py --sync` headless as its own subprocess (no headed browser needed at all, pure git and the local database), folded into the same shared `_any_proc_busy()`/`_stop_run` machinery as every other pass. Its own polling is deliberately NOT routed through the shared cockpit progress bar (`renderProgress`/`pollRun`/`ACTIVE_PHASES`), that mechanism is keyed off `run_progress.json`'s own phase, which this fast, few-second git operation never writes to, showing it there risked a stale phase from the last real run being displayed as if it described this sync; instead a small self contained `pollCompShare()` polls `/api/run-status`'s new `compshare_running` flag directly and just disables/relabels its own button.
      - Verified live end to end: a real `python3 share_comps.py --export` against the real database wrote a genuine 1,038 comp, 195KB file with no registration anywhere in it, confirmed by inspecting the JSON directly, matching golden rule 6's spirit exactly, only comp relevant figures ever leave this Mac. `daily_run.py --render-only` rebuilt the real cockpit with the new button present. Generated page JS checked with `node --check` (clean) the same way every other render.py change in this project has been verified. 458 tests pass (12 new: `dealer_key` against a real name, messier input, and nothing at all; a full export/load round trip against an isolated temp database and temp folder, proving the exported file's own field set is exactly the eight safe ones and nothing else, a blank dealer name is refused, and `load_shared_comps` both finds another dealership's file and correctly excludes this Mac's own).
      - Not yet proven: an actual two machine round trip (Steven's own Mac has not run this code yet, he would need to pull this release, set his own `DEALER_NAME` in his own `dealer_config.py`, and run `share_comps.py --sync` for the first real cross dealership merge to happen), and Mark's own real 1,038 comp export was committed alongside this release specifically so it is there waiting the moment Steven's own Mac does.
    - GRADE ADDED, AND FUTURE MATCHING SET UP TO USE THE REAL SCRAPE NOT A DERIVATION, shipped as v2.8.3, 2026-08-25, same day (Mark: "do the two free derivations, but from now on add the transmission and year, engine cc, grade, from the scrape for better matching"). Year, transmission and engine were already being captured straight off every daily read into `sightings` since v2.8 (the free derivations from v2.8.2 exist specifically to backfill the OLDER history that predates that), only condition grade was missing. `sightings` gained a `grade` column, `record_sightings()` now writes `c.grade` from the same `Car` object already read for every sighted car, no extra cost, matching the existing mileage/engine/transmission pattern exactly. `lost_bids` and `purchases` both gained their own `grade` column too, `bid_comps_full()` now returns it per comp, `set_lost_bid_fields`/`set_purchase_fields` both gained a `grade` parameter (same COALESCE, fill once never overwrite convention as the other three). No algorithmic fallback exists for grade the way `year_from_reg`/`transmission_from_name` cover the other two, a condition grade cannot be derived from a plate or a name, so `bid_retail_enrich.py --fields`'s `_derived_fields` only ever passes through whatever a real sighting has, honestly None otherwise; grade coverage across the real 1,038 comp history is 0 right now as a result, purely forward looking exactly as Mark's own "from now on" framing intended, closing in as fresh sightings accumulate.
      - `bid_suggest._similarity` gained a grade term, same tapering shape as year and engine size (`max(0, 1 - abs(diff) * 0.5)`, an exact match scores 1.0, one grade apart 0.5, two or more apart nothing), `suggest_bid_for_car`'s own `target` dict gained `grade` alongside the other four. 3 new tests (a synthetic grade 2 versus grade 5 ranking case mirroring the existing mileage ranking test, proving a car's own condition grade correctly ranks it against the closest real comps rather than a far rougher graded half of the same group). 446 tests pass. Verified live: `db.init_db()` migration applied cleanly against the real database, `bid_retail_enrich.py --fields` re run (992 of 1038 still correctly filled from the existing sighting/reg/name sources, grade genuinely 0 as expected), `daily_run.py --render-only` rebuilt the real cockpit with no error.
    - A REAL BUG FOUND ONCE THE FULL RETAIL BACKFILL ACTUALLY FINISHED (674 of 674 target lost bids processed, a couple of hours after it started): `lost_bids_missing_retail()` filtered on `cazana_retail IS NULL`, but a confirmed Cazana no match (the reg genuinely is not on Cazana, about 101 of the 674) also stores NULL there, indistinguishable from "never attempted", so a later re run of `--retail` would have kept re-querying the same already-settled no-match cars forever, exactly the mistake the daily `valuation_cache` was already careful to avoid ("a confirmed no match is cached too, not just a hit"). Fixed with a new `lost_bids.cazana_checked` flag (the same pattern already established for `Car.glass_checked`, "a Glass's pass has tried this car... survives re assessment"), set to 1 by `set_lost_bid_retail` on every outcome, match or not; `lost_bids_missing_retail()` now filters on `cazana_checked` instead. The 679 rows already carrying a real mileage were backfilled to `cazana_checked=1` directly against the production database (matching the batch that had genuinely just finished), confirmed `lost_bids_missing_retail()` correctly returns 0 afterward. 429 tests still pass (no regression, this is a pure efficiency fix for a future re run, not something the current single pass needed).
