Markdown for LLMs
Change impact and dossiers
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# 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](/corpus/#how-this-topic-marks-confidence).
## A lab case: the residential fee moves from 100 to 120
### 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:
```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);
}
```
### Which answer change we expect
The bank holds five fee scenarios, lowered to case files:
```text
$ 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`:
```text
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 "3. Differences" of the rendered dossier, verbatim (lines
18–27 of `CHANGE-DOSSIER.en.md`):
```text
$ 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
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
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.
### 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:
```text
$ 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:
```text
$ 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:
```text
$ 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:
```text
$ 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:
```text
$ 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:
```text
$ 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:
```text
$ 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.
## 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
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:
```text
$ 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:
```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
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:
```text
$ 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
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:
```text
$ 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:
```text
$ 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)**
## What to read next
- [Command-line reference](/cli/) for the evaluator and world commands the
producers build on.
- [Schema catalog](/protocols/schemas/) for the change-set and dossier
formats.