Change impact and dossiers
When a package changes, reviewers need one question answered with evidence: which case outcomes changed, and which only changed their justification. A change dossier is the prepared answer to that question: a checked JSON record plus a readable rendering for reviewers.
Environment: the dossier tools are Python scripts in a source
checkout (I), run from the repository root; the lab case reads the
fixture docs/corpus/lab/fixtures/dossier-mini/ and writes only to
/tmp/lab-work/. Marks follow the
topic legend.
A lab case: the residential fee moves from 100 to 120
Section titled “A lab case: the residential fee moves from 100 to 120”What changed in the model
Section titled “What changed in the model”One number in one rule of the lab fees package labparcels.fees: the
residential fee moves from 100 to 120. This is the after side:
rule FeeResidential strict { for p: Parcel; for k: ParcelKind; when labparcels.registry::registered(p) and labparcels.registry::kind_of(p, k) and k == labparcels.iface::residential; then fee_due(p, 120);}Which answer change we expect
Section titled “Which answer change we expect”The bank holds five fee scenarios, lowered to case files:
$ ls docs/corpus/lab/fixtures/dossier-mini/bank01-fee-residential-ok--001.json02-fee-commercial-ok--001.json03-fee-garden-ok--001.json04-fee-unregistered--001.json05-fee-wrong-amount--001.jsonOnly the first scenario depends on the residential amount. It asks
whether the fee due on residential parcel p1 is 100, and expects
TRUE_ONLY:
evaluate truth(fee_due(entity_ref("urn:lab:parcel:p1"), 100));expect truth_status == TRUE_ONLY;(Excerpt from parcels-fees/tests/01-fee-residential-ok.lawtest.)
So exactly one of five observations should move, and the other four
(commercial, garden, unregistered, wrong amount) should stay as they
were.
What the report showed
Section titled “What the report showed”Section “3. Differences” of the rendered dossier, verbatim (lines
18–27 of CHANGE-DOSSIER.en.md):
$ sed -n '18,27p' docs/corpus/lab/fixtures/dossier-mini/CHANGE-DOSSIER.en.mdVerdict `SEMANTIC-CHANGE` · rule structure: 0 added, 1 changed, 0 removed · 5 scenarios executed, 1 observations changed, 0 inputs rejected.Expected: **0** · outside declared scope: **0** · expected but missing: **0** · changed symbols without an executing scenario: **0**.
### Outcome changes (1)
**Applicability and positions (1)**
**🗓️ 2026-01-15**- **01-fee-residential-ok--001** (`docs/corpus/lab/fixtures/parcels-fees/tests/01-fee-residential-ok.lawtest:5`): **applies → undetermined**.One rule changed, one outcome moved from established to undetermined, and there are no proof-only changes. All five scenarios ran, and no input was rejected.
Why the answer became undetermined, not false
Section titled “Why the answer became undetermined, not false”The residential scenario asks for the old amount. After the change
the package derives fee_due(p1, 120); nothing derives
fee_due(p1, 100) any more, and nothing states that it is false. So
the question about 100 gets no answer on the after side
(TRUE_ONLY → NEITHER, rendered as “applies → undetermined”). To
confirm the new rate, a scenario should ask for 120.
Expected or observed: declaring intent
Section titled “Expected or observed: declaring intent”Read the Expected: 0 above as detection without declared intent:
the fixture change-set carries no intent block, so the one moved
row is classified observed, not expected (intentDeclared: false in the dossier JSON). For a pre-agreed change, declare the
delta instead — the same rebuild with this intent block added to a
copy of the change-set:
"intent": { "declaredBy": "lab author: the rate move is meant to silence the old-amount scenario", "declaredOn": "2026-10-03", "expectedChanges": [ {"scenario": "01-fee-residential-ok--001", "before": "TRUE_ONLY", "after": "NEITHER"} ], "protectedGoals": [], "protectedScope": []}renders Expected: **1** and classifies the row expected
(verified by rebuilding the copied change-set with
build_dossier.py --out-dir). (I)
What it justifies: a reviewer who sees Expected: 1 and
outside declared scope: 0 can accept the change as exactly the
agreed one; any other moved row would show up outside the declared
scope.
Commands that build the report
Section titled “Commands that build the report”Each step below requires a source checkout (I). Some tool messages are in Russian; an English reading follows each one.
Step 1. Lower the scenarios again into a scratch bank; the fixture stays untouched:
$ python3 corpus/tools/change-dossier/lower_scenarios.py docs/corpus/lab/fixtures/parcels-fees/tests --program docs/corpus/lab/fixtures/dossier-mini/before-root.lawir.json --imports docs/corpus/lab/fixtures/dossier-mini/imports-context.json --output /tmp/lab-work/dossier-rerun/bank --manifest /tmp/lab-work/dossier-rerun/bank-manifest.jsonlower_scenarios: 5 дел из 5 файлов → /tmp/lab-work/dossier-rerun/bankThe message reads: “5 cases from 5 files”.
Step 2. Compare the fresh bank with the saved one before using it downstream, so that a lowering error fails here and not later:
$ diff -r /tmp/lab-work/dossier-rerun/bank docs/corpus/lab/fixtures/dossier-mini/bank && echo "bank trees identical" && cmp /tmp/lab-work/dossier-rerun/bank-manifest.json docs/corpus/lab/fixtures/dossier-mini/bank-manifest.json && echo "manifest bytes identical"bank trees identicalmanifest bytes identicalStep 3. Run both programs, each over its own closed world of the shared vocabulary plus the registry:
$ python3 corpus/tools/change-dossier/impact_worlds.py --before-root docs/corpus/lab/fixtures/dossier-mini/before-root.lawir.json --before-world docs/corpus/lab/fixtures/dossier-mini/world-labparcels.iface.lawir.json docs/corpus/lab/fixtures/dossier-mini/world-labparcels.registry.lawir.json --after-root docs/corpus/lab/fixtures/dossier-mini/after-root.lawir.json --after-world docs/corpus/lab/fixtures/dossier-mini/world-labparcels.iface.lawir.json docs/corpus/lab/fixtures/dossier-mini/world-labparcels.registry.lawir.json --scenarios docs/corpus/lab/fixtures/dossier-mini/bank --out /tmp/lab-work/dossier-rerun/impact.json --health /tmp/lab-work/dossier-rerun/health.jsonimpact_worlds: SEMANTIC-CHANGE, изменилось 1 из 5; здоровы на обеих сторонах 5, пусты на обеих 0The message reads: “SEMANTIC-CHANGE, 1 of 5 changed; healthy on both sides 5, empty on both 0”. One verdict, one changed row, every row healthy on both sides:
$ python3 -c "import json; r=json.load(open('/tmp/lab-work/dossier-rerun/impact.json')); print('verdict:',r['summary']['verdict'],'| changed:',r['summary']['changedScenarios'],'of',r['summary']['scenarios']); print('changed rows:',[x['name'] for x in r['scenarios'] if x.get('changed')]); h=json.load(open('/tmp/lab-work/dossier-rerun/health.json')); print('healthy both sides:',h['summary']['healthyBothSides'],'of',h['summary']['scenarios'])"verdict: SEMANTIC-CHANGE | changed: 1 of 5changed rows: ['01-fee-residential-ok--001']healthy both sides: 5 of 5Step 4. Confirm the rerun bytes match the committed producer outputs:
$ cmp /tmp/lab-work/dossier-rerun/impact.json docs/corpus/lab/fixtures/dossier-mini/impact.json && echo "impact bytes identical" && cmp /tmp/lab-work/dossier-rerun/health.json docs/corpus/lab/fixtures/dossier-mini/bank-health.json && echo "health bytes identical"impact bytes identicalhealth bytes identicalStep 5. Read the change set: the unit, the world, and the three producer artifacts the dossier assembles from:
$ python3 -c "import json; c=json.load(open('docs/corpus/lab/fixtures/dossier-mini/change-set.json')); u=c['units'][0]; print('unit:',u['package'],'|',u['before']['label'],'=>',u['after']['label']); print('world root:',c['world']['root'],'| closure:',[p['name']+'@'+p['version'] for p in c['world']['closure']]); [print('producer:',p['tool'],p['role'],p['path']) for p in c['producers']]"unit: labparcels.fees | residential fee 100 (frozen edition) => residential fee 120 (draft)world root: labparcels.fees | closure: ['labparcels.iface@0.1.0', 'labparcels.registry@0.1.0']producer: impact root docs/corpus/lab/fixtures/dossier-mini/impact.jsonproducer: scenarios root docs/corpus/lab/fixtures/dossier-mini/bank-manifest.jsonproducer: bank-health root docs/corpus/lab/fixtures/dossier-mini/bank-health.jsonStep 6. Rebuild the dossier from the same bytes and compare hashes; the committed dossier passes:
$ python3 corpus/tools/change-dossier/build_dossier.py docs/corpus/lab/fixtures/dossier-mini/change-set.json --checkchange-dossier: --check OK — ядро sha256:02e6fb3a5e1fa86be8f617ecae99e9770e8a43857309d8e8bd3e05eb56f74f9e, пакет sha256:9c0f5a8076ed7c8fd569eca2e48ae9098e78330253339f5b014abd741ad8f1ffThe message reads: “--check OK — core sha256:…, package
sha256:…”, the two hashes of the rebuilt dossier.
The two artifacts
Section titled “The two artifacts”A dossier run starts from a change set (law.change-set/0.1): state pairs,
the world each side runs in, the introduction regime, the declared intent,
and the producer artifacts that already computed each part. It ends with a
dossier (law.change-dossier/0.1) plus its human-readable print. The checked
JSON dossier is the record; the rendered pages present that record. (I)
The assembler executes nothing
Section titled “The assembler executes nothing”The builder reads the producer bytes, verifies each producer hash against
the change set declaration, and lays the already-computed results out by
report section. A section without a producer prints as missing, never as
silent. Rebuilding from the same bytes must yield the same hashes, and the
--check flag verifies exactly that. Observed usage:
$ python3 corpus/tools/change-dossier/build_dossier.py --helpusage: build_dossier.py [-h] [--out-dir OUT_DIR] [--check] [--pin] [--commit COMMIT] [--reproduction {reproduced,not-attempted,failed}] [--reproduction-note REPRODUCTION_NOTE] [--binary BINARY] [--source-base-url SOURCE_BASE_URL] [--include-ru] [--strict] change_setTypical forms, quoted from the same help text:
python3 corpus/tools/change-dossier/build_dossier.py <change-set.json> [--out-dir DIR] [--commit <hash>] [--reproduction reproduced|not-attempted|failed --reproduction-note "…" [--binary "…"]]python3 corpus/tools/change-dossier/build_dossier.py <change-set.json> --check # пересборка в память и байтовая сверкаpython3 corpus/tools/change-dossier/build_dossier.py <change-set.json> --pin # проставить sha256 продюсерам (явная правка входа)The two Russian comments read: --check rebuilds in memory and
compares bytes; --pin writes the sha256 of each producer into the
change set (an explicit edit of the input).
Use --pin when producer bytes are final and their hashes must be recorded
in the change set input itself. (I)
The impact producer: before and after worlds
Section titled “The impact producer: before and after worlds”The impact report runs both programs, each over its own closed world: a root plus the compiled world on each side, evaluated against a directory of JSON cases. It emits two files: the impact report and a bank-health file of the same shape. An outcome counts as unchanged only when the row is healthy on both sides. Observed usage:
$ python3 corpus/tools/change-dossier/impact_worlds.py --helpusage: impact_worlds.py [-h] --before-root BEFORE_ROOT [--before-world [BEFORE_WORLD ...]] --after-root AFTER_ROOT [--after-world [AFTER_WORLD ...]] --scenarios SCENARIOS --out OUT --health HEALTHThe tool help records a cautionary case: a report that ran the root without its world failed most cases identically on both sides, which is not an observation of any kind. Always pass the world inputs. (I)
Review presentation and the acceptance check
Section titled “Review presentation and the acceptance check”The pull-request producer takes two prepared checkouts plus the world builder both sides share, then routes the scenario lowering, the impact run, the assembler, and the checker. Observed usage:
$ python3 corpus/tools/change-dossier/pr_dossier.py --helpusage: pr_dossier.py [-h] --base BASE --head HEAD --lawc LAWC --package-tool PACKAGE_TOOL --out OUT [--package PACKAGE] [--run-url RUN_URL] [--source-base-url SOURCE_BASE_URL] [--include-ru]The review comment separates changed outcomes from proof-only changes,
groups changed symbols by topic, and folds wide tables and full symbol ids
under technical details. English is the default; --include-ru adds the
folded Russian section. Optional display labels map scenario ids to short
bilingual titles without changing any test or legal result. (I)
The acceptance-bundle check admits a release dossier only when the exact journal-outcome comparison and the structural compiled-form comparison refer to the same pinned edition worlds and the same historical dossier. It rebuilds the export from the source journal and recomputes both reports before accepting anything. Observed usage:
$ python3 corpus/tools/change-dossier/verify_serve_c4_bundle.py --helpusage: verify_serve_c4_bundle.py [-h] --comparison COMPARISON --world-semdiff WORLD_SEMDIFF --expect-count EXPECT_COUNT --expect-changed EXPECT_CHANGED --expect-structural-package EXPECT_STRUCTURAL_PACKAGE --out OUTThe expectation flags pin the case count, the changed count, and the package
under comparison, so a one-row probe can never pass as full-bank acceptance.
For example, the documented release run expects 101 cases with 0 changed for
the package kz.corpus.ogpovts, while the bonus-malus bank run expects 101
cases with 26 changed for kz.corpus.bonus_malus. (I)
What to read next
Section titled “What to read next”- Command-line reference for the evaluator and world commands the producers build on.
- Schema catalog for the change-set and dossier formats.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.