Skip to content

A zero nobody has ruled on is not a measurement - #94

Merged
ChelseaKR merged 1 commit into
mainfrom
fix/dins-undeclared-zeros
Sep 13, 2026
Merged

ChelseaKR merged 1 commit into
mainfrom
fix/dins-undeclared-zeros

Conversation

@ChelseaKR

Copy link
Copy Markdown
Owner

Closes #83.

The defect

Five DINS fields are FieldSpec(..., numeric=True) with no declared vocabulary, so each carries Basis.NONE and publishes marker_basis: "none". So does a field nobody has ever opened. docs/MARKERS.md section 7 named it in its own last sentence — "the gate that reads this file has no way to tell a reviewed decision from an unexamined one" — and the registry, the artifacts and both pages had no word for the difference.

Field Recorded zeros Share of what it records Reading section 7 already carried
NOOFCARSONPROPERTY 55,831 99.4% a count of no cars
NOOUTBUILDINGSDAMAGED 59,628 96.3% a count of no damaged outbuildings
NOOUTBUILDINGSNOTDAMAGED 58,888 95.0% a count of no undamaged outbuildings
NUMBEROFUNITPERSTRUCTURE 58,411 94.3% contested
ASSESSEDIMPROVEDVALUE 6,613 5.2% contested

What this does

FieldSpec.zero_reading is a second axis, not more Basis members. Basis answers where a declared vocabulary came from; ZeroReading answers what a zero is, and a field can be in any combination of the two. Four words, because a reader acts differently on each:

Word What it says What a reader should do with a 0
measurement Somebody read the form and the distribution; a 0 is a finding Use it as a count of none
undecidable Somebody read them and the published documentation does not settle it. The zero is left present — declaring it a marker would move tens of thousands of records on a reading nothing supports Do not use it as a finding, and do not use it as an absence
marker Declared: 0 stands in for a value that was not recorded, so the zeros are counted as recorded-as-unknown and the field publishes none There is no 0 to read; it is already counted as an absence
unreviewed Nobody has ruled. A 0 would be published as a value and might be an absence Nothing — and the build fails if a field in this state publishes one

The readings are section 7's own, transcribed rather than invented. Three counts are measurement, the two contested fields are undecidable — the third answer #83 asks for by name — and YEARBUILT is marker. Section 7 now states the word beside each row and tests/test_schema.py holds document and registry to each other by an exact token, in both directions. UNREVIEWED is the default, because a field added tomorrow has not been reviewed.

Two numbers everywhere, derived at render time from the rows beside them. recorded_zero_review in each artifact; the sentence above each field table on dins.html and perimeters.html; each field's own reading on its own row.

Coverage, before and after

surface before after
DINS fields measured as numbers, carrying a reading 0 of 8 — the state was not representable 6 of 8 (LATITUDE, LONGITUDE named as the rest; both publish no zero)
of those publishing a zero 0 of 5 5 of 5
FRAP 0 of 1 0 of 1, published rather than silent; GIS_ACRES publishes no zero
artifact per-field zero count only zero count and the reading
page nothing the pair above each table, the word on each row

Schema and consumers

artifact_schema_version 1 → 2. recorded_zero_review is a new required top-level key and recorded_zero_reading a new optional per-field one; a consumer validating against version 1 — whose schema closes with additionalProperties: false — would reject the new artifact. The rule for a bump is stated in src/perimeter/artifacts.py and in the CHANGELOG so neither can be edited alone.

site/ is regenerated from the pinned 2026-08-07 retrieval, and make site-check reports it byte-identical to a fresh build from data/raw/ (1182 and 81203 leaves compared, none differing).

Negative controls

Three sabotages, each with the occurrence count and git hash-object recorded before and after, each restored to a byte-identical tree.

sabotage result
ZeroReading defaults to MEASUREMENT, so an unexamined field's zeros publish as reviewed fired — 59 tests red
withdraw NOOFCARSONPROPERTY's ruling, so it publishes 55,831 zeros declaring nobody ruled on them did not fire — all 534 tests in tests/test_schema.py green
count every examinable field as reviewed, so the block publishes 8 of 8 did not fire — all 1,158 tests green

Two of the three controls found a gate that could not fail, and both are fixed in this PR.

The second passed because every gate on the zero question reads the committed artifact, and the only thing that rebuilds it — make site-check — needs data/raw/ and cannot run in CI. So a ruling withdrawn in schema.py would have shipped green. spine() in tests/test_published_site_is_current.py already holds marker_basis between a fixture build and the committed copy; it now holds recorded_zero_reading beside it. Re-run after the fix: fired, dins-coverage.json shape mismatch.

The third passed because every gate on recorded_zero_review recomputes it from the same payload the block was written into — a summary checked against itself. zero_review now has two tests that read its own answer over a field set whose two numbers are known to differ, one of them specifically that a field with no zeros today still counts as a question. Re-run after the fix: fired, 2 tests red.

Verified

pytest 1160 passed, 25 skipped (browser/jsdom gates skip without make node-sync); ruff check, ruff format --check, mypy --strict src all clean, each exit code captured off the bare command rather than through a pipe. make site-check byte-identical.

Prepared with AI assistance; reviewed before submission.

Five DINS fields measured as numbers publish a recorded zero, and until now the
registry had no way to say whether anybody had read the inspection form for them.
A reviewed decision and an unexamined field both carried Basis.NONE and published
marker_basis: "none" -- which docs/MARKERS.md section 7 said in its own last
sentence: "the gate that reads this file has no way to tell a reviewed decision
from an unexamined one."

FieldSpec.zero_reading is a second axis, not more Basis members: Basis answers
where a declared vocabulary came from, this answers what a zero is, and a field
can be in any combination of the two. Four words -- measurement, undecidable,
marker, unreviewed -- with unreviewed the default, because a field added tomorrow
has not been reviewed.

The readings are the ones section 7 already recorded, transcribed into the
registry rather than invented. Every artifact and both pages now publish two
numbers derived from the rows they describe: fields measured as numbers, and how
many carry a reading, with the rest named. A field publishing a recorded zero
while declaring unreviewed fails the build.

artifact_schema_version 1 -> 2.

Closes #83.
@ChelseaKR
ChelseaKR merged commit 9221999 into main Sep 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Five DINS fields measured as numbers publish a zero nobody has ruled on, four of them above 94 percent

1 participant