Method · Changelog · 30 Aug

Every change, in the words it was made with

This is the repository’s own history, newest first, each entry the message its change was committed with and nothing added after the fact. The messages say what changed, why, and what they found wrong on the way, including in earlier entries. A figure quoted in one is the figure on the day it was written. A day to a page.

746 changes over 22 days since Monday 10 August 2026 · as held in the repository at 10 Sept 2026, 09:03 pm AWST, when this build was deployed. The defects that reached the database have their own register on the data-quality page.

Sunday 30 August 2026 · 6 changes

The tree carries every migration the database ever applied

04:03 pm AWST · 7158512

The drift check was broken in the way that matters most: silently. It numbered files by the database's apply order, but sixteen migrations from the mid-August build were applied without ever being filed, so the positions disagreed and a --write renumbered two hundred files out from under every comment that cites them.

Reconciled, both directions:

- The sixteen unfiled migrations are backfilled beside the work each belongs with (0137b..0204b, the letter convention 0177b already used), content taken verbatim from the database's own record. That includes the first variant of a check applied twice, and the probe-targets function whose create was never filed even though its drop was. - The one applied-but-unrecorded change is now recorded: the probe-targets drop ran ad hoc, so its file content was re-applied as a proper migration row (drop if exists — a no-op against the schema, a real row in the history). - The check itself now matches BY SLUG, not position: the tree's numbering is its own logical order, the database's timestamps are the replay order of record, and neither is rewritten to flatter the other. Content compares after normalising what the storage never kept — statement separators, comment preambles an older apply path stripped, string-literal line-wrapping — so real SQL drift still fails. - Eleven build-era files whose rows hold older in-place-edited variants are named in a KNOWN_VARIANCES list with the reason, verified against the live schema rather than waved through. Nothing joins that list without the same verification.

241 applied, 241 filed, check green. Every future unfiled migration fails CI the day it happens instead of years later.

The yard learns its branches, its live position, and its floor plan

03:52 pm AWST · 7e03b08

Four things Taj asked for after opening the Lexus group account, and the two data faults underneath them.

The faults first. Every yard panel read live_snapshot, which refreshes twice a day and does not carry dealer_name — blind on day one of a watch, blind to branches forever. And the statement engine read the raw days_old field, which Lexus's platform never publishes; the advertised_days() helper has known the createdAt shape since 0104, so a yard with 40 dateable cars reported zero dated stock. 0214 gives the yard a live engine (dealer_live_position_scoped, scoped like the statement by sources AND dealer_names) and teaches the statement to ask the helper; 0215 rounds the live reads so every past-90 comparison is between whole days and the page agrees with itself.

On top of that: - /yard grows the All/branch toggle, live header stats (including total advertised value), a live standing panel, a branches-side-by-side table, and a branch-scoped makes list. Single dealers see no change beyond the floor plan panel. - The floor plan: a dealer's own bailment rate and cost basis (dealer_floorplan_pref, scope-keyed so groups are one facility), multiplied against observed asks — assumptions printed inside the headline, undated stock a named row, POA stock named as carrying nothing. Supersedes StandingMoney's "never multiplied by a floorplan rate" only where the dealer supplies the rate themselves. - Back from the statement is now always "Your yard", branch toggles replace history instead of filling it, month links keep the branch, and the branch survives the trip between /yard and the statement. - ShelfCurve no longer draws a flat 0% line for a yard with no dated stock; a group's past-month PDF button no longer points at a 404.

Adversarially reviewed (12 agents, 7 confirmed findings, all fixed): the boundary disagreement, the undated floor-plan gap, step attributes that blocked 8.25%, drill-in links that erased history, and the copy that credited POA cars with asks they never published.

A temporary password opens nothing until it is replaced

12:58 pm AWST · 7255362

An account issued a temporary password carries must_change_password in app_metadata — which only the service role can write, so the holder cannot wave away their own obligation. Until it clears, the yard is out of reach.

Enforced in two places, because one is not enough. proxy.ts redirects every /yard page load to /set-password so the person is walked into choosing a password. But a Next server action is a globally addressable POST that never passes through the proxy, so the obligation also lives at the authorisation layer: currentMembership() fails closed while the flag is set, and every dealer-scoped read and action already routes through it. Path-matching is the convenience; the membership gate is the boundary.

The password itself still goes browser->auth-server directly and never touches us. The browser stamps user_metadata.pw_rotated in the same call that sets it; the finish route lifts the obligation only when that stamp is present, and consumes it in the same write so a later re-issued temporary password demands a fresh rotation rather than trusting an old one. A forced user whose finish failed once can resubmit the same new password and still complete; typing the temporary password back is refused.

Verified by two rounds of adversarial review: no bypass via off-path server actions, no redirect loop, no strand for a cooperative user.

The issuer learns to preview a group, without issuing one

11:59 am AWST · db07126

A dryRun with a groupId renders the whole group or one branch through the same document every dealer gets, and returns the bytes. It refuses to write an archive row: group issuance is a separate concern this route does not yet own, so the preview path stays a preview.

A group holds its branches, and the statement toggles between them

11:55 am AWST · 3e4401b

Lexus of Perth is one account with two rooftops — Osborne Park and Melville (Myaree) — sharing a single crawl source but reported as distinct dealers. The statement engine learns to be scoped by a set of sources AND a set of dealer_names at once, so both group shapes fit: one site with many branch names, or many sites under one banner.

- 0213 adds dealer_group / dealer_group_branch, a group_id on membership, and dealer_monthly_statement_scoped(source_ids[], dealer_names[], month). The single-source function becomes a thin wrapper over it, so every existing dealer is untouched (westside still reads 540). dealer_group_statement resolves a branch to its scope, or the whole group when no branch is named. - Auth resolves a group membership to its branches; the statement page and PDF grow an All / per-branch toggle that re-scopes both the numbers and the document. Verified: group 40, Osborne 21, Melville 19.

The rotation feeds freshness and discovery evenly, and the lapse keeps pace

09:16 am AWST · 4044420

Root-cause pass on why health has emailed red every ~5 hours since Aug 25. Every pg_cron job is 100% green (2,216 runs, 5 days) and CI is green; the only failure is integrity check 17, and its residue decomposed into four causes:

1. The daily lapse raced the continuous 14-day check — marketplace cars cross the line all day and sat stale until 12:35. lapse-unwatchable now runs six-hourly (schedule change, 2-second function). 2. Detail-rotation starvation: never-seen URLs went ahead of every stale re-read, so a source whose sitemap enumeration outruns its inventory starves its own freshness — pattersoncheney lists 3,780 URLs for 591 cars, the ~3,200 non-inventory pages parse to nothing, stay "never seen" forever, and claimed the whole budget twice a day while its stalest prices went 15 days unread. tonywhitegroup (8,700 cars) hit the same wall by size. The rotation now interleaves one unseen with one stale, halving the budget between discovery and freshness whatever the mix. 3. Probe-cycle latency: yesterday's sighting-stamps fix is working (847 -> 685 in a day) and drains the remainder as queues cycle — vonbibra's pages verified parsing fine, its stale set is queue position, not rot. 4. A redundant probe-budget change was written and reverted: store.ts already scales the check budget at candidates/4 up to 400 (the carsguide lesson, learned before this session knew it).