Skip to content
docs
Arxo ↗

Change impact and dossiers

For LLMs6 sections

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”

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:

Arxo Law
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);
}

The bank holds five fee scenarios, lowered to case files:

Output
$ ls docs/corpus/lab/fixtures/dossier-mini/bank
01-fee-residential-ok--001.json
02-fee-commercial-ok--001.json
03-fee-garden-ok--001.json
04-fee-unregistered--001.json
05-fee-wrong-amount--001.json

Only the first scenario depends on the residential amount. It asks whether the fee due on residential parcel p1 is 100, and expects TRUE_ONLY:

Output
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.

Section “3. Differences” of the rendered dossier, verbatim (lines 18–27 of CHANGE-DOSSIER.en.md):

Output
$ sed -n '18,27p' docs/corpus/lab/fixtures/dossier-mini/CHANGE-DOSSIER.en.md
Verdict `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.

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:

JSON
"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.

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:

Output
$ 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.json
lower_scenarios: 5 дел из 5 файлов → /tmp/lab-work/dossier-rerun/bank

The 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:

Output
$ 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 identical
manifest bytes identical

Step 3. Run both programs, each over its own closed world of the shared vocabulary plus the registry:

Output
$ 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.json
impact_worlds: SEMANTIC-CHANGE, изменилось 1 из 5; здоровы на обеих сторонах 5, пусты на обеих 0

The 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:

Output
$ 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 5
changed rows: ['01-fee-residential-ok--001']
healthy both sides: 5 of 5

Step 4. Confirm the rerun bytes match the committed producer outputs:

Output
$ 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 identical
health bytes identical

Step 5. Read the change set: the unit, the world, and the three producer artifacts the dossier assembles from:

Output
$ 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.json
producer: scenarios root docs/corpus/lab/fixtures/dossier-mini/bank-manifest.json
producer: bank-health root docs/corpus/lab/fixtures/dossier-mini/bank-health.json

Step 6. Rebuild the dossier from the same bytes and compare hashes; the committed dossier passes:

Output
$ python3 corpus/tools/change-dossier/build_dossier.py docs/corpus/lab/fixtures/dossier-mini/change-set.json --check
change-dossier: --check OK — ядро sha256:02e6fb3a5e1fa86be8f617ecae99e9770e8a43857309d8e8bd3e05eb56f74f9e, пакет sha256:9c0f5a8076ed7c8fd569eca2e48ae9098e78330253339f5b014abd741ad8f1ff

The message reads: “--check OK — core sha256:…, package sha256:…”, the two hashes of the rebuilt dossier.

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 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:

Output
$ python3 corpus/tools/change-dossier/build_dossier.py --help
usage: 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_set

Typical forms, quoted from the same help text:

Output
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:

Output
$ python3 corpus/tools/change-dossier/impact_worlds.py --help
usage: 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 HEALTH

The 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:

Output
$ python3 corpus/tools/change-dossier/pr_dossier.py --help
usage: 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:

Output
$ python3 corpus/tools/change-dossier/verify_serve_c4_bundle.py --help
usage: 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 OUT

The 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)

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.