The artifacts are the product and had no contract but this repository's source - #79
Merged
Merged
Conversation
…'s source site/data/*.json are what a downstream reader consumes and the pages are one rendering of them. Their shape was readable only out of render.py, and the three-state model -- the whole point of the measurement -- was invisible in the JSON: [222, 0, 0] means nothing without FIELD_STATE_ORDER, and nothing in the artifact said so. Adds JSON Schema (draft 2020-12) per artifact under site/data/schema/, a Frictionless data package at site/data/datapackage.json carrying each source's licence, endpoint, retrieval date, byte count, record count and SHA-256 from the reviewed record in sources.py, and artifact_schema_version in both artifacts with the bump rule stated beside the constant and in CHANGELOG.md. The schemas are declared rather than generated, because the payloads are not dataclasses. What holds them to the writer is validation in both directions against a real build: every object closes with additionalProperties false and requires every key it declares. The one case that leaves uncovered, an optional key nothing emits, is pinned by name to the two keys _field_json writes conditionally. Every existing number is unchanged. The rebuild from the acquired files moved exactly one leaf in each artifact.
This was referenced Sep 7, 2026
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 was wrong
site/data/*.jsonare the product. The pages are one rendering of them, and the sibling project, a researcher or a reporter had no contract for their shape except this repository's source.The sharpest version of that: the three-state model is the entire point of this measurement, and it is invisible in the JSON.
That is unreadable without
perimeter.artifacts.FIELD_STATE_ORDER, and nothing in the artifact said so. A consumer who guessed the order wrong would fold a recorded unknown into either presence or absence — the exact error this project exists to refuse — and nothing would tell them.What changed
site/data/schema/perimeters-coverage.schema.jsonanddins-coverage.schema.json— JSON Schema (draft 2020-12), written by the same run that writes the artifacts, so a schema can never describe a build that produced something else. They name every key, say which of the three counts is a recorded value, which is a published marker meaning the value could not be determined and which is an empty cell, and say why a share isnullrather than0over an empty denominator.site/data/datapackage.json— a Frictionless Data Package naming both resources with each source's licence, endpoint, layer, retrieval date, byte count, record count and SHA-256. Those come fromperimeter.sources, the single reviewed provenance record, so a descriptor cannot state a hash the artifacts do not — held there the waytests/test_provenance.pyholdsPROVENANCE.md. A fixture build writesisFixture: truewith every acquisition factnull: a fixture was never downloaded from anywhere, and a descriptor is the most quotable place a fixture could pass itself off as a measurement of CAL FIRE's files, because it is the file a machine reads rather than a person.artifact_schema_versionin both artifacts, with the bump rule stated beside the constant and inCHANGELOG.md, and a test holding the two together so neither can be edited alone.The footer of all three pages links the descriptor and both schemas.
Every existing number is unchanged
Rebuilt with
make siteagainst the acquired FRAP and DINS files (both present locally, both SHA-256-matchingsources.py).make diffagainstorigin/main:One leaf each, and nine footer lines per page. That is also independent confirmation that the committed
site/was byte-current withorigin/mainbefore this branch touched it.A deviation from the issue, stated plainly
#63 asks for schemas "generated from the dataclasses in
coverage.pyandartifacts.py". They are declared inschema_export.pyinstead, because the payloads are not dataclasses:perimeters_payloadanddins_payloadassemble dictionaries by hand, add derived keys (total,present_tenths_pct), rename others (outside_domainbecomesoutside_published_domain) and drop two conditionally. A generator overFieldCoveragewould describe the dataclass and not the artifact, which is the wrong document.What the issue actually asks for is that the schema "cannot drift from the writer", and that is delivered a different way: validation in both directions against a real build. Every object closes with
additionalProperties: falseand requires every key it declares, soThat leaves exactly one case uncovered — a key the schema marks optional that nothing emits, which would sit in the contract forever failing nothing. There are two optional keys, both written conditionally by
_field_json, andtest_every_optional_key_is_one_the_writer_can_omitpins the set to those two by name and proves each is present for some field of the published artifact and absent for another.How it was verified
Full suite 923 passed.
html-validate,tools/a11y.mjs(six rule sets, three pages, nothing undecided beyond the four declared rules) and the Chromium Playwright gate (axe plus reflow at 320px, 8 passed) all green against a fixture build with the new footer.Per ADR 0004 the contract is run against eight mutations it must refuse, including the case #63 names verbatim: a fixture artifact with
field_state_orderremoved fails, and the error names the path.Five negative controls, each committed first, baselined with
git hash-object, sabotaged, asserted changed, run with__pycache__cleared underPYTHONDONTWRITEBYTECODE=1, restored and re-hashed:_object:additionalProperties: False→True_resource: the descriptor's real SHA-256 →"sha256:0"_resource:None if is_fixture else source.record_count→ always the real countARTIFACT_SCHEMA_VERSION = 1→2_field_jsonemits an undeclaredmarker_confidencekeyAll five restored to their exact baseline blob hashes.
One control was botched on its first attempt and I am reporting it rather than only its second run. The fifth sabotage was applied through a shell argument containing a literal
\n, which produced aSyntaxErrorinstead of a valid mutation — the suite went red on a collection error, which would have read as a passing control if I had only looked at "did it go red". It was redone with a heredoc andast.parseasserting the sabotaged source still parses, so the red came from validation and not from a broken file. Worth recording: "the suite went red" is not the same measurement as "the guard fired".Also worth noting the fourth control's shape:
test_the_committed_artifact_validateswent red buttest_the_fixture_build_validatesstayed green, because the committed bytes and a fresh build disagree only when the writer moves without a rebuild. That is the pair working as intended rather than a flake.Scope
Part of #63. Not included, and not attempted: publishing either document to a registry or portal, which the issue puts out of scope.
Prepared with AI assistance; reviewed before submission.