3. Add currency through a reviewed model change
The application currently treats every amount as EUR by convention. A new requirement makes that convention explicit: every expense must state its currency. The fictional policy still permits EUR only. Supporting USD would also require deciding how category caps, approval limits and conversions work; a currency label alone would not solve that problem.
This chapter uses an additive proposal, previews its impact, applies it with a linked-project conformance check, then updates the generated and developer-owned parts explicitly. Continue from chapter 2.
Inspect the proposed model
The supplied currency.orm.json adds a Currency value type and this fact:
The fact requires exactly one currency for every expense. A value constraint
allows EUR. New requirements carry user-supplied, confirmed, alethic
provenance from the synthetic exercise brief. Existing constraints and their
provenance are retained byte-for-byte as model values.
For your own project, select gnd-model-evolve explicitly and review the proposal before applying it:
Add an explicit currency requirement to expense-demo/model.orm.json. Preserve the current EUR limits and every unchanged constraint's provenance. Every expense must name one currency; support EUR only until I approve a conversion policy. Link expense-project for downstream conformance. Prepare and preview the complete additive proposal; explain data migration and generated-code impact before publication.
The checkpoint helper below prepares that specific proposal. It binds the exact
model bytes, complete candidate, three additive operations and a declared roster
of generated-project and extension files. It uses versioned evolution support
contracts; application code continues to use only the stable gnd.api surface.
The helper does not apply or accept a change.
python expense-demo/prepare_currency.py --model expense-demo/model.orm.json --candidate expense-demo/currency.orm.json --project expense-project --output expense-demo/currency.proposal.json
Preview the impact without changing the model
gnd evolve preview expense-demo/model.orm.json expense-demo/currency.proposal.json --workspace . --records expense-demo/evolution-records --out expense-demo/currency.preview.json
The preview contains the candidate validation, whole-model verbalization, exact
digests and complete blast-radius entries. Read expense-demo/currency.preview.json.
For this bounded tutorial there are no renamed or deleted elements, so the
protocol's breaking flag is false. An additive mandatory requirement can still
make old populations inadequate or invalid; that flag does not promise data
compatibility.
import hashlib
import json
from pathlib import Path
preview = json.loads(Path("expense-demo/currency.preview.json").read_text())
assert preview["breaking"] is False
assert len(preview["proposal"]["operations"]) == 3
assert all(operation["kind"] == "add" for operation in preview["proposal"]["operations"])
assert preview["before_model"]["sha256"] == hashlib.sha256(Path("expense-demo/model.orm.json").read_bytes()).hexdigest()
assert preview["candidate_model"]["sha256"] == hashlib.sha256(Path("expense-demo/currency.orm.json").read_bytes()).hexdigest()
for entry in preview["blast_radius"]["entries"]:
print(entry["change"], entry["reference"])
The listed impact includes currency roles, mandatory/uniqueness/value constraints, provenance and verbalization. The linked project roster intentionally excludes interpreter caches and approval output files; it includes the manifest, derived files, application modules, declared architecture, model and seed checkpoint. GND does not discover additional dependent applications for you.
Apply the reviewed proposal
In this exercise you have inspected a supplied additive change. Run the explicit apply command when ready to publish it into your local tutorial model:
gnd evolve apply expense-demo/model.orm.json expense-demo/currency.proposal.json --preview expense-demo/currency.preview.json --workspace . --records expense-demo/evolution-records
Expected exit: 13, published with drift. The model was changed and the
transaction record completed, but the linked generated project still describes
the previous model. This is a recorded downstream finding, not an automatic
regeneration or an uncommitted proposal. Keep the record; a later clean result
will not rewrite what this check observed.
import json
from pathlib import Path
record = json.loads(Path("expense-demo/evolution-records/expense-currency/record.json").read_text())
assert record["published_with_drift"] is True
assert record["conformance_exit_code"] == 13
assert len(record["linked_conformance"]) == 1
assert Path("expense-demo/model.orm.json").read_bytes() == Path("expense-demo/currency.orm.json").read_bytes()
model = json.loads(Path("expense-demo/model.orm.json").read_text())
assert any(fact["id"] == "expense_has_currency" for fact in model["fact_types"])
print("Currency model published; its linked project needs an explicit update.")
Learn what missing coverage means
First recheck the old seed, which makes no completeness claim for currency:
Exit 2 means the evidence is inadequate for acceptance. The missing currency
relation is vacuous, not a demonstrated pass. Treating it as clean would hide
an unfinished migration.
The next checkpoint claims currency coverage but assigns EUR only to expense 103. This gives GND enough information to identify expenses 101 and 102 as missing required currency facts:
from gnd.api import check
report = check("expense-demo/model.orm.json", "expense-demo/currency-missing.population.json", unredacted=True)
assert report["exit_code"] == 1
mandatory = [row for row in report["verdicts"] if row["constraint_type"] == "mandatory" and row["verdict"] == "fail"]
assert len(mandatory) == 1
assert "expense_has_currency" in mandatory[0]["constraint_ref"]
print(mandatory[0]["message"])
complete = check("expense-demo/model.orm.json", "expense-demo/currency.population.json")
assert complete["exit_code"] == 0
print("All three expenses explicitly carry EUR; the complete checkpoint passes.")
The complete population is supplied so you can reproduce the result without an agent or a database migration. In a real application, reconstruct the currency from trustworthy source data; do not label unknown amounts EUR just to make a check pass.
Update generated code and extension behavior separately
Explicitly select gnd-codegen if working with an agent. The local command below refreshes the derived files. The extension modules are retained:
Now migrate this tutorial's seed checkpoint and replace the report function with the supplied currency-aware version. Update the architecture declaration to record its additional consumed fact type:
from pathlib import Path
import shutil
source = Path("expense-demo")
project = Path("expense-project")
for origin, target in [
("model.orm.json", "model.orm.json"),
("currency.population.json", "seed.population.json"),
("currency_reporting.py", "cli/reporting.py"),
("currency.architecture.contract.yaml", "architecture.contract.yaml"),
]:
shutil.copy2(source / origin, project / target)
assert (project / "cli/expense_cli.py").read_bytes() == (source / "extension.py").read_bytes()
print("Derived files regenerated; currency reporting and population migration applied explicitly.")
The revised report exposes totals_by_currency, rather than leaving EUR in a
field-name convention. Its input gate ensures missing currency cannot silently
fall back to EUR. Run the application and conformance again:
(cd expense-project && python -m cli.expense_cli report --model model.orm.json --population seed.population.json)
import json
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-m", "cli.expense_cli", "report", "--model", "model.orm.json",
"--population", "seed.population.json"],
cwd="expense-project", capture_output=True, text=True, check=True,
)
assert json.loads(result.stdout) == {
"approved": 1, "submitted": 3, "totals_by_currency": {"EUR": 2260},
}
print("The evolved application reports three expenses totaling 2260 EUR.")
This migrated seed intentionally starts from chapter 1's checkpoint. Chapter 2's approval outputs remain old evidence; they have not been silently migrated. Their currency requirements must be handled before reuse under the changed model.
Exercise and next steps
Exercise: Remove expense 101's currency from a copy of the migrated seed,
retain its coverage declaration and run report. Verify that no total is
printed. Then change its currency to USD: the allowed-value rule should fail.
Explain which policies would need review before USD could become accepted.
You now have a working local reporting and approval application, a reproducible refusal path and a completed model evolution record. To apply the pattern to your project, identify the facts your application produces, model the rules that must hold, and place the check before publishing its result.
Read evolution and recovery for model change and brownfield workflows, or data and transitions for operation checks that audit how a state changes. The agent skill guide explains explicit handoffs between these workflows.