The record for the rebuild-window collision, which the previous commit's script skipped
08:58 pm AWST · b0be893
Its Python stopped at an OPERATIONS anchor the earlier deadlock rewrite
had already replaced, so the code shipped and the two doc notes did not.
This is the ops addendum and the DATA-QUALITY 53 coda.
A freshness read waits two seconds for a board being rebuilt, and says so
08:58 pm AWST · 081e18e
The first scheduled depth-boards run under the up-front lock succeeded:
1,166 seconds, seventeen locks taken at the door, only transient readers
waiting, nothing stuck. And the deep chain, whose crawl ran until 12:38,
put check-integrity inside that window. Its freshness check reads
board_status(), waited on aged_board until the authenticator's
eight-second lock_timeout cancelled it, and threw before filing --- so the
chain reported "a step failed" and left no row to say why. health.ts was
A locked board is, by definition, not stale. board_status() now carries a
two-second lock_timeout --- a function-level lock_timeout, unlike
statement_timeout, is consulted at each lock wait and so actually takes
effect, proved by reading the setting back inside a function body --- and
both suites treat the timeout as "rebuilding": check-integrity files its
verdict with a warning, health prints boards=rebuilding, and neither can
turn a rebuild into a red chain or a false ok again.
The six tables the previous commit said it changed, and did not
08:34 pm AWST · 0ceea75
ae4a23c's message claims seven tables gained a scrolling region; its
script stopped at the dealer page's first table, whose wrapper carries an
extra class the anchor did not allow for, and the chain committed anyway
with only the repricing panel's two changed. This is the other six ---
the dealer page's three and the model page's three --- and the record is
corrected here rather than rewritten there.
Seven more tables scroll in a named region a keyboard can reach
08:31 pm AWST · ae4a23c
The repricing panel's two, the dealer page's three and the model page's
with a visible focus ring and carries the table's name, and the ones
labelled by a string gain a caption stating what they count. That leaves
the tables on /coverage, /value, the yard and the reports.
Three more tables carry a caption and scroll in a named region
08:22 pm AWST · 3336b26
The home Most traded table, /moving's model table and /market's fuel
scrolling region that is a tab stop with a visible focus ring, as the
kit's table already models. Nothing visible changes.
A wide table is a region a keyboard can reach
08:20 pm AWST · bda5593
The kit's table scrolls inside a named region that is a tab stop with a
visible focus ring --- ink on yellow, yellow elsewhere --- so a keyboard
user can reach and scroll it. The other tables still owe this; the kit
models it.
The heat table's qualifiers are its caption
08:18 pm AWST · 1c41c04
Unit, n, date and basis are part of the table to a screen reader now, not
a paragraph above it, and the [u] shorthand is met before the first cell
that uses it (a11y-dense-ui 5.2). The kit's table models the rule the
other twenty-six tables still owe.
H1 is the state table that already exists
08:17 pm AWST · ea3b2e2
The stock cross-section eval-heatmaps asked for --- distinct cars live per
state on a stated day --- is the per-state table /market has carried since
market_state_board counted cars, dated by its board. Recording that it is
served rather than building it a second time under a new name.
The one heat form the evidence allows: a matrix of printed numbers
08:15 pm AWST · ddbe12f
eval-heatmaps refused the colour ramp and the map permanently --- over the
recorded state days a cell's watched listings moved 95.7% on average
against 8.5% in its median ask, so any colour on a per-state change is
eleven parts crawler and one part market --- and named the duration
cross-section as the panel that survives, because a duration is a property
of the car, not of the panel.
turn_matrix serves it: median days advertised at exit by state and
segment over a stated closed window, used cars only (demo medians run
three to five times longer and a blend describes neither), proved only, one
row per car, every cell with its count. A cell under the floor of thirty
comes back with its count and no median, so the table prints [u] and the
number rather than a blank: the reader learns the cell is thin, not that it
is empty. Over the last closed week 26 cells clear the floor, from 21 days
for NSW utes to 41 for QLD utes.
The kit's HeatTable carries the magnitude in the digits and the emphasis
in weight alone --- a row's extremes bold --- with the shorthand explained
above the table where a reader meets it first, rows and columns scoped,
the table named by its heading. Tested on the rendered markup, including
that nothing in it is coloured.
The dealer's age bands and exit dots draw from the kit
08:09 pm AWST · e7a74f3
Two more forms. Categorical columns place a continuous marker on the band
scale, because a 175-day median on a 0-to-180 axis lands inside the 180+
bar --- the page had learned that once and the kit now proves it. And a
dot strip: one mark per car along one axis, staggered so coincident days
stay visible, hollow where the car was discounted first, the median marked
--- which is also the strip the market wrap needs. Both are tested on the
numbers and on the rendered markup, and every band carries the readout
the interaction layer reads aloud.
The checks that run, and what each one proves
07:49 pm AWST · 870c4cc
Eleven scripts, one table: which invariant each holds, where it runs, and
that every lint carries a planted failure it must catch before it is
believed.
The listing's price step draws from the kit
07:47 pm AWST · 3b52fbd
The one chart CHARTS-PLAN rejected by name --- a line through every priced
observation on an axis that is our fetch schedule --- had already become a
step; it is the kit's step now, on a form that takes the real moments of
each reading rather than slot indices, because a price held between reads
is a fact about the price and the gaps between reads are a fact about us.
Every reading is a mark, the change sits at the first reading that saw
it, and the page says the read is live.
The yard's three comparisons draw from the kit
07:44 pm AWST · 36cd933
CutRates and PriceGap were the same chart twice --- two bars per band,
the yard beside the field, a floor of thirty cars a side, a round axis,
the count under each pair --- and ShelfCurve was two survival curves on
one scale with a marker at ninety days. They are two kit forms now,
pairedBars and curvePair, with the honesty rules in the geometry: a band
under the floor on either side is dropped AND NAMED with its counts, never
drawn thin; the axis top is the next round ten so the same measure reads on
the same rule across both charts; a gap on one side breaks only that
side's line; and the subject is identified by position and by the word
beside the swatch as well as by colour, so a greyscale reader still finds
it. Each form is tested on its numbers and on its rendered markup, and
every mark still carries the readout n the interaction layer reads aloud.
Nothing a reader sees changes except that the dropped bands are now named
under the chart instead of silently absent.
The definition of confirmed sold is a link, not a tooltip
07:38 pm AWST · f09c852
Five sentences sat in a title attribute on the header counter: hoverable
with a mouse and reachable by nothing else --- no touch screen, no
keyboard, and most screen readers skip it. The words now link to the
definition's home on /method, which gained the #sold anchor.
The a11y row says what is enforced and what is still open
07:37 pm AWST · f010e9c
Tables named and scoped, contrast computed from the tokens, chart names
checklist is listed as open rather than left as "the sweep".
One chart kit, two renderers, and the rules run instead of being intended
07:35 pm AWST · 0cc8b70
Section 5 said the same forms were drawn twice --- inline SVG on the site,
react-pdf on paper --- with colours retyped in each, and that the chart
rules were aspirations because nothing ran them. Both are addressed.
src/lib/chart-kit/: tokens.ts is the only place a chart colour is written
(the PDF's #ffd200 is gone; print yellow is BRAND's #e4e619); geometry.ts
computes every form as numbers with no renderer in it; provenance.ts makes
a chart without a headline, a formal title, a unit, an n or its reason, a
date or its reason, a basis or null, and a source a type error, and turns
the spec into the two title lines and the accessible description both
surfaces use; web.tsx and pdf.tsx draw the same geometry. The statement's
forms on both surfaces are on the kit, and the proof sheet renders on the
real renderer.
The rules are tests now. A null slot splits a series into runs, so no
segment can ever cross a gap. A running month ends at the last observed
day and only its last point is provisional --- it used to draw the
unelapsed month as a dashed flat line under a footer reading "nothing
projected or modelled". Small multiples share a scale and the flattened
panel is the finding. Ticks come from a scale function, so a zero-spread
cohort prints one tick, not three identical ones. And a hollow bar --- the
mark that says "this day absorbs days nobody looked" --- is decided from
the days the yard was actually crawled, which the statement payload now
names (crawled_days), never from two zeros in the values: that heuristic
hollowed a quiet day and drew the true backlog solid, bold, as the peak.
check-charts.ts fails CI on any role="img" chart with no name, proved on
a planted one; the whole suite runs in CI.
Two corrections travel with it. The Grand Prix "repair" from earlier today
was not one: the box's first pass on the new URL failed the same way, and
the run history shows it reaches that host about once in thirteen attempts
a day on either address, while a DNS census puts 510 of 999 dealer hosts on
the same Vercel infrastructure crawling normally. The cause was unknowable
because every connection-stage failure reached the crawler as the string
"TypeError: fetch failed" and the run rows kept the string; the crawler
records undici's cause now, code first, so the next failure names itself.
DATA-QUALITY 52 and the source's own notes say so.
The register is published as written, and the series can be cited
07:16 pm AWST · 49e207b
Two of section 8's deliverables. Every persona in the audit asked for
provenance, and the cheapest credibility in the program is publishing the
fifty-three defects we found rather than claiming accuracy.
/method/data-quality renders docs/DATA-QUALITY.md itself --- not a summary
of it --- through a renderer that parses exactly the six constructs the
document uses and treats every other line as a paragraph, which is the
honest failure: nothing is interpreted the author did not write, and React
escapes every string so no markup is ever injected. A planted document
with every construct and the real file both pass scripts/test-md-lite.ts;
the test asserts the page shows every entry the file holds, because a
register showing fewer defects than we wrote is the least true thing the
site could do. The document lives outside the uploaded directory, so it
travels into the build the way the sha does: written by the stamp step,
committed empty, discarded after a deploy. An unstamped build says so
instead of rendering an empty register.
/api/v1/series serves the daily rollups --- market, state, fuel --- as JSON
or CSV. Public, unlike /api/v1/value, because a series exists to be cited
and a door only the crawler's email opens is not that. Three things it says
rather than assumes: a day with no row is a gap and is listed as one, never
interpolated (2026-08-12 and 2026-08-23 today, 2026-09-03 once the next
day is written); a break carries the register's own words for the window
it falls in; and every column is named for its unit, with the columns it
DROPS named too --- daily_market_rollup.sold counts advertisements at 228%
of the car count and is not served. Read one scope at a time, keyed on the
day, because nine states share every day and a page boundary on a
non-unique key drops rows silently. Verified on the built app: 21 market
rows with both gaps and the 29 August break; a WA window with its gap; fuel
at exactly seven scopes by nineteen days; malformed input refused; CSV gap
rows present with empty cells and gap=1.
The status table catches up with the tables under it
07:05 pm AWST · 7ba8221
Section 15 said five section-4 rows were remaining that section 8 had
marked done on 3 September, said 4.6 was on a path to its own timeout when
0202 and 0229 had already moved every sold figure onto sold_car_fact (288
runs in 24h, none failed, mean 6s), and said section 2 still owed the
reports and the rollups when e547a55 closed both. A status table that
lags its own evidence is the least useful kind of record.
Open question 3 is recorded as answered by 0283, with the distinction the
question did not draw: a cross-car diff is PROVED not to be a price change,
a flap is a real reading of a page that oscillated, and the two populations
are near-disjoint. Proven-false events are excluded everywhere; flapping
stays in with the detector. Whether flapping should also go is left as
Taj's call, explicitly. Question 5 is resolved: f109df0 has been on main
since section 0 and the footer stamp names the live commit.
Two dead sources, one dead domain, and the deadlock underneath the boards
07:01 pm AWST · 4fdbcc3
Health named dealer:berwickldv. The class query --- enabled sources whose
last five runs all fetched nothing with a fetch error --- named a second,
dealer:grandprixautogroup, still inside the alarm's two-day window. Same
symptom on the box; two facts.
berwickldv.com.au is NXDOMAIN from 1.1.1.1 and 8.8.8.8; none of its 36
VINs appear at the seven Berwick siblings. Withdrawn with the evidence in
robots_notes, and its 36 live rows LAPSED, not delisted: the crawl's own
rule that a failed fetch proves nothing about a car is right and was left
alone, which is exactly why those rows would otherwise have shown as live
stock forever. A dead domain is evidence about the site (0208). recordLapse
is exported so the script writes the crawl's own two rows, not a copy.
Grand Prix rebuilt onto Vercel. Its index sat on an apex the box cannot
reach while it reads www fine and all 28 live rows already carry www; the
index moved to the URL the apex itself redirects to. One config row. The
box's next pass is the proof and DATA-QUALITY 52 says so.
partner was never a page reading two boards or another job --- both were
checked and neither exists. It is board_status(), called by every /market
and model-page render, walking all thirty-three boards alphabetically in
one transaction, while the job truncated two of its pairs in the reverse
order. The job now locks all seventeen boards it rebuilds first, in the
reader's order, so it never waits while holding; a guard recomputes what
the job's functions truncate and demands the lock list match, and was
proved to fire on a one-board list (sixteen missing).
And the day the deadlock cost: 2026-09-03 has no daily rollup, because the
rollup found a stale snapshot after the job that refreshes it died, and
correctly refused. It cannot be written after the fact --- a day's stock
columns can only describe that day's snapshot --- so it is a gap and stays
one. daily-rollup now refreshes the snapshot itself when it finds one over
90 minutes old, so one job's failure is no longer another job's lost day.
The one integrity failure is that rollup stamp, 46 hours old against 36; it
clears at 12:50 UTC when the first run of the new daily-rollup writes 4
September. Everything else passes, 33 checks.
The ticker's proved count is named for what it is
06:53 pm AWST · bcf09c3
`turn.delisted` in the model ticker carried the proved-only count --- the
seller's own SOLD text or a dead page --- and was printed directly under
the word "Confirmed". The copy was right and the name contradicted it.
Across all 1,439 model boards the two quantities differ by 6,106 cars,
19.6% unproved, so the name was one edit from making the copy wrong with
no line of it looking stale: anyone repointing `delisted` at a genuine
exit total would have been making the name honest and the heading false.
It is `turn.proved` now, end to end: the interface, both mapping branches
(the board's `proved` key, and the tape fallback, which is proved-only
because model_sold_tape filters exit_basis), and the three JSX readers.
Nothing rendered changes.
One related inconsistency from the same audit is closed with it. The page
body says "(most recent only)" when its figures came from the displayed
tape rather than every car; the page's metadata did not, so a search
snippet would have called a 60-row cap the model's total. Unreachable
today --- every board carries a full turn key --- and now impossible.
The deadlock that empties the boards, and how to get them back
08:14 am AWST · 2d8662b
Recovering the boards after 2026-09-03's failure meant learning this twice in
one hour, so it is written down rather than rediscovered.
The refresh functions truncate each board before refilling it, and pg_cron
sends a whole multi-statement command as ONE transaction, so every
AccessExclusiveLock is held until the entire job commits. A page holding one
board and wanting another closes the cycle. Twice on aged_board against
live_snapshot, once on model_chart_board against model_event_board, and once
more before that: six of depth-boards' last thirty-five runs have failed.
Recovery is not obvious and cost real time. There is no board-refresh script,
because cron is the mechanism; a plain call from an MCP session dies when the
connector drops the connection. A one-shot cron job runs detached and works,
provided it is unscheduled only after cron.job_run_details shows it finished,
because cron.unschedule kills a running job.
And retry first. refresh_model_chart_board deadlocked at 21 seconds and then
rebuilt cleanly in 28 on the next attempt, which also confirms it survives the
price-change patch it had not been run under. All 31 checks now pass.
A figure says which cars it counted
07:18 am AWST · e547a55
Three numbers described one population. The home page's sale_speed said
16,768 used and demo cars watched out of the market, market_by_fuel summed
to 16,584, and sold_car_fact --- the table every other sale surface reads
--- held 15,854. The first two were each built from their own copy of a
query on listing_current, and that view carries no exit basis, so both
counted soft 404s as sales; both also filtered `days is not null` BEFORE
taking one row per car, so 1,044 cars carried an age from an older
advertisement of themselves, which is the duration of a different listing
episode than the exit being described.
Both now read sold_car_fact. Verified from one snapshot: sum(sold) from
market_by_fuel() and sales from sale_speed() both returned 15,913, gap
exactly 0. The median survives at 37; p25 moves 14 to 13. What changes is
that the panel says what the 15,854 are --- "used and demo cars watched out
of the market, 85% of them proved sold, and those in 35 days" --- instead of
calling them sales.
Then the same defect, everywhere else it had spread:
- turn_by_model has never had an exit_basis filter, and the home page
called its output "confirmed sales". 2,301 of 15,529 (14.8%) unproved.
Aggregated that is mild, 37 days against 35; it does not distribute
evenly. Of 334 rows, 6 are majority unproved and 3 have no proof at all
--- Toyota Prado 8 exits 0 proved, Landcruiser 70 Series 18 and 0,
Mercedes-Benz GLC 4 and 0. A page-level share cannot fix that: 85%
proved overall is exactly what hides a row that is 0% proved. Both that
table and /moving's now carry a per-row proved count.
- generate_market_report_data has no filter either, and every archived
report headed a panel "Cars confirmed sold". For the week of 24 August
that named 10,723 cars over a population 2,530 of 10,213 unproved:
24.8%, worse than the live surfaces because a report's window closes
before later proof arrives. ReportView already had the honest wording
for the running week, gated on proved_total --- a key no stored payload
carried, so the 0279 correction reached the preview and left the archive
alone. The generator now emits the split, the four issued reports have
it backfilled, and the wording is fixed at all five sites including
shareText, which is the one artefact built to travel without footnotes.
- /market's fuel table announced itself to screen readers as "Models by
cars confirmed sold". Wrong about the rows, which are fuels, and wrong
about the population. That was mine, from b876481 yesterday, whose own
rule was that a table with a heading in reach gets aria-labelledby so
the name cannot drift; this table has one and was misclassified. It is
labelled by its heading now, and the page publishes a proved share for
the first time --- "proved" and "unproved" appeared nowhere on /market
while its metadata promised confirmed sales.
Two integrity checks, so neither can drift back. A tolerance check was
measured and refused: 855 of the 15,854 exit in a typical 24 hours, so a
band loose enough not to fire on a twelve-hour-old board is far too loose
to notice the population changing. The guard is definitional instead ---
both readings must come from one table --- and it was verified to fire by
pointing its predicates at a function with the old shape.
Also recorded, because both cost real time today:
- `SET statement_timeout` in a function's definition does nothing for that
function's own call. Same function, minutes apart: plain call cancelled
by 57014 with the SET in place; `set statement_timeout = '15min';` as a
separate statement first ran three minutes and finished. The timer is
armed when the top-level statement begins and nothing re-arms it. The
cron prefix is load-bearing, not decoration.
- Migrations must cite each other by NAME. The number is assigned by
sync-migrations from the order the database applied it, long after the
prose is written, and today three migrations guessed their own number
and got it wrong. Those three cannot be fixed --- the file is generated
from the stored statements and must stay content-exact --- so the
mapping is recorded instead. Five such references in .ts files were
editable and were corrected.