A refresh replaced both artifacts wholesale and nothing said what it moved - #71
Conversation
|
What was ruled out
The only tree difference between #68's passing branch and What the failure actually isThe checkout is not truncated. In the same job, on the same checkout, the new So the checkout has full history and That is this portfolio's dominant defect class pointed the other way: a state marker read What I did not doI did not touch that gate. Weakening or re-baselining a version-honesty check that landed The fix is not a looser assertion: it is to make the predicate ask what the message Prepared with AI assistance; reviewed before submission. |
|
This PR's red
Measured on a runner via a throwaway diagnostic branch (now deleted): It presented as flake because it is a race: whether it fired depended on whether PR #74 turns the capture off ( Once #74 is on Prepared with AI assistance; reviewed before submission. |
…moved The figures on these pages move only when the pinned retrievals are deliberately refreshed, and a refresh rewrites site/data/perimeters-coverage.json and dins-coverage.json in full. The only account of what changed was git diff over a large JSON document, which answers a different question: it reports lines, so a reordered list reads as hundreds of changes and one count that moved reads as two. python -m perimeter.diff OLD NEW walks every leaf of both documents and reports the value at its path with both sides. Exit 0 no change, 1 changes reported, 2 a refused removal or an unreadable input. --json writes the rows sorted by path, byte-identical on repeat, so a refresh can cite a comparison in PROVENANCE.md. make diff wraps it for a terminal, and records in the Makefile that make collapses every recipe failure to its own exit 2, so a script that needs the three codes calls the module. Three distinctions decide the design. A key the later artifact stops publishing is not a value moving: it is refused unless ALLOW_REMOVALS names it deliberate. A number becoming null is a change to absence and never a removal, because ADR-0010 writes a domain the layer stopped publishing as null rather than dropping it. And 1000 and 1000.0 are a type change, because every percentage here is a *_tenths_pct integer precisely so that no float decides an equality. make site-check now prints the leaf comparison before its byte-for-byte check decides. The comparison is the report and diff -r is still the gate: the report's exit status is discarded deliberately, so it can add detail and can never turn a red target green. Refusals per ADR-0004: a missing file, an empty file, an unparseable one and a JSON document whose top level is not an object are each refused rather than parsed into an empty document, because two empty documents compare equal and would report no change about two artifacts that were never read. Running the tests against the faults they exist to catch found a weak assertion in this change's own test file. With the emptiness check deleted, an empty file still failed as unparseable JSON and match='empty' was satisfied by the fixture filename empty.json. The fixture is renamed and the pattern now matches the reason. Closes #58
9ff21f1 to
9f86ddf
Compare
Closes #58.
What was wrong
A refresh of the pinned retrievals replaces
site/data/perimeters-coverage.jsonandsite/data/dins-coverage.jsonwholesale, and the only account of what changed wasgit diffover a large JSON document. That answers a different question: it reportslines, so a reordered list reads as hundreds of changes and a single count that moved
reads as two. The README promises the figures "move only when those retrievals are
deliberately refreshed", and a deliberate refresh should be able to say what it moved.
What changed
python -m perimeter.diff OLD NEWwalks every leaf of both documents and reports thevalue at its path (
/fields[3]/present) with both sides. Exit0no change,1changesreported,
2a refused removal or an unreadable input.--jsonwrites the rows sorted bypath, byte-identical on repeat, so a refresh can cite a comparison in
PROVENANCE.md.make diff OLD=… NEW=…wraps it, and the Makefile records that make collapses everyrecipe failure to its own exit 2, so a script needing the three codes must call the
module directly.
Three distinctions decide the design:
unless
ALLOW_REMOVALS=1names it deliberate.nullis a change to absence, never a removal. ADR-0010 writes adomain the layer stopped publishing as
nullrather than dropping it, so collapsing thetwo would lose the distinction the artifact exists to carry.
1000and1000.0are a type change. Every percentage here is a*_tenths_pctinteger precisely so that no float decides an equality; nothing converts, rounds or
tolerances a value.
make site-checknow prints the leaf comparison before its byte-for-byte check decides.The comparison is the report;
diff -ris still the gate. Its exit status is discardeddeliberately and that is commented in the recipe, so the reporting step can add detail and
can never turn a red target green.
Refusals, per ADR-0004: a missing file, an empty file, an unparseable one, and a JSON
document whose top level is not an object are each refused rather than parsed into an
empty document. Two empty documents compare equal, and "no change" about two artifacts
that were never read is exactly the failure this repository is organised against.
Done-when, checked
no change: 56241 leaves compared, none differ, none added, none removedduplicate_signalsexits 2 without--allow-removals, 1 with it, naming the keyrecordscount change reported with both values; JSON output byte-identical on repeat/records: 132522 -> 999; two runs compared equal in-testmake site-checkprints the leaf-level diffdiff -r(that target needsdata/raw/, so it is not exercisable in CI)How it was verified
822 passed(full suite less the two Node/browser a11y gates); coverage 99.27% total,95% on the new module, floor 90;
mypy --strictclean over 13 source files;ruff check/ruff format --checkclean.Negative controls, each with the sabotage asserted present in the file before the
result was read:
diff.pyremovalsalways empty…is_a_removal_not_a_change, both exit-code tests)The second control initially passed, and that was a real finding in this PR's own test.
With the emptiness check gone, the empty file still failed as unparseable JSON, and
pytest.raises(match="empty")was satisfied by the fixture's filename,empty.json. Theassertion was passing for the wrong reason. The fixture is renamed
nothing-here.jsonandthe pattern now matches the reason; the control then failed as it should. A control that
passes for the wrong reason is a control that is not there.
Out of scope, as the issue says
Diffing the pages (they are rendered from the artifacts) and automatic refreshes.
Prepared with AI assistance; reviewed before submission.