Diff two revisions¶
Available in: Python · Node · CLI · Browser
When: a resubmission lands and you need to know what actually changed between Rev A and Rev B — not a line diff, but a KEY-aware, type-aware delta.
# what this shows: laterite.diff(a, b) — a KEY-aware, type-aware revision diff between two AGS4 texts.
from pathlib import Path
import laterite
# Two revisions of the same submission, differing in one PROJ cell (PROJ_NAME).
baseline = Path("examples/sample_site.ags").read_text()
revision = baseline.replace(
"laterite demo site (synthetic starter - replace me)",
"laterite demo site (Rev B)",
)
assert revision != baseline
# diff() returns a RevisionDelta dict: per-group row/heading deltas + counts.
# Rows are matched by the group's dictionary KEY headings, and cells are compared
# through the *typed* value, so only a genuine quantity change registers.
delta = laterite.diff(baseline, revision)
# Pull out the PROJ group and its single changed row.
proj = next(g for g in delta["groups"] if g["code"] == "PROJ")
changed = [row for row in proj["rows"] if row["kind"] == "changed"]
print("totals:", delta["total_added"], delta["total_removed"], delta["total_changed"])
print("PROJ key headings:", proj["key_headings"])
print("changed row key:", changed[0]["key"])
print("changed cell:", changed[0]["cells"][0])
# A 'changed' PROJ row, keyed on PROJ_ID, carrying a heading/type/a/b cell.
assert delta["total_changed"] == 1
assert proj["key_headings"] == ["PROJ_ID"]
assert proj["keyed"] is True
row = changed[0]
assert row["kind"] == "changed"
assert row["key"] == ["LAT-DEMO"] # the PROJ_ID value
cell = row["cells"][0]
assert cell["heading"] == "PROJ_NAME"
assert cell["type"] == "X"
assert cell["a"] == "laterite demo site (synthetic starter - replace me)"
assert cell["b"] == "laterite demo site (Rev B)"
totals: 0 0 1
PROJ key headings: ['PROJ_ID']
changed row key: ['LAT-DEMO']
changed cell: {'heading': 'PROJ_NAME', 'type': 'X', 'a': 'laterite demo site (synthetic starter - replace me)', 'b': 'laterite demo site (Rev B)'}
laterite.diff(a, b) compares two AGS4 texts and returns a RevisionDelta — a
per-group breakdown plus the total_added / total_removed / total_changed
counts. It is not a text diff: rows are matched on each group's dictionary
KEY headings (here PROJ_ID), so a row that moved or was reordered still
lines up with its counterpart. The single edit above registers as one changed
row keyed on ["LAT-DEMO"], carrying a cell that names the heading, its AGS
type, and the a/b values.
Because cells are compared through the born-typed
value, only a genuine quantity change registers — 1.50 vs 1.5 on a 2DP
column is the same number and produces no delta, where a naive line diff
would flag it.
Walk the structure to drive a review: each group in delta["groups"] carries
code, key_headings, keyed (whether the group has KEYs to match on), and a
rows list. Each row has a kind (added / removed / changed), its key,
and — for a changed row — a cells list of {heading, type, a, b}:
for group in delta["groups"]:
for row in group["rows"]:
if row["kind"] == "changed":
for cell in row["cells"]:
print(group["code"], row["key"], cell["heading"], cell["a"], "→", cell["b"])
Gotcha: matching is only as precise as the KEYs. A group with no KEY headings
(keyed is False) falls back to positional comparison, so a genuine row
insertion there can read as a cascade of changed rows rather than one added.
Check keyed before trusting row identity on KEY-less groups.
// what this shows: diff(a, b) — a KEY-aware, type-aware revision diff between two AGS4 revisions.
import { diff } from "laterite";
import { readFileSync } from "node:fs";
import assert from "node:assert/strict";
// Two revisions of the same submission, differing in one PROJ cell (PROJ_NAME).
// A bare string is a PATH in Node, so pass bytes for an in-memory revision.
const baseline = readFileSync("examples/sample_site.ags");
const revision = Buffer.from(
baseline
.toString("utf8")
.replace(
"laterite demo site (synthetic starter - replace me)",
"laterite demo site (Rev B)",
),
);
assert.ok(!revision.equals(baseline));
// diff() returns a RevisionDelta: per-group row/heading deltas + counts. Rows are
// matched by the group's dictionary KEY headings, and cells are compared through
// the typed value — so only a genuine quantity change registers. The shape is
// byte-identical to Python / wasm / `lat-check --diff`.
const delta = diff(baseline, revision);
const proj = delta.groups.find((g) => g.code === "PROJ");
const changed = proj.rows.filter((r) => r.kind === "changed");
console.log(
"totals:",
delta.total_added,
delta.total_removed,
delta.total_changed,
);
console.log("PROJ key headings:", proj.key_headings);
console.log("changed row key:", changed[0].key);
console.log("changed cell:", changed[0].cells[0]);
assert.equal(delta.total_changed, 1);
assert.deepEqual(proj.key_headings, ["PROJ_ID"]);
assert.equal(proj.keyed, true);
assert.deepEqual(changed[0].key, ["LAT-DEMO"]); // the PROJ_ID value
const cell = changed[0].cells[0];
assert.equal(cell.heading, "PROJ_NAME");
assert.equal(cell.type, "X");
assert.equal(cell.b, "laterite demo site (Rev B)");
totals: 0 0 1
PROJ key headings: [ 'PROJ_ID' ]
changed row key: [ 'LAT-DEMO' ]
changed cell: {
heading: 'PROJ_NAME',
type: 'X',
a: 'laterite demo site (synthetic starter - replace me)',
b: 'laterite demo site (Rev B)'
}
diff(a, b) runs the same laterite-ags4-diff engine and returns the
byte-identical RevisionDelta — the snake_case field names (total_changed,
key_headings, keyed) match Python one-for-one, so the same walk drives a
review in either language. One surface note: a bare string is a path in
Node (diff("a.ags", "b.ags") compares two files), so pass a Buffer /
Uint8Array — as above — when the revision only exists in memory. It's
synchronous; no DuckDB peer needed.
lat diff <file> <other> compares two revisions on disk and prints
the per-group delta, exiting 0:
The CLI reports the summary shape — per group, +added −removed ~changed
— off the same KEY-aware, type-aware engine, so the counts agree with every
other surface. When you need the cell-level a/b detail shown in the
Python/Node tabs, reach for diff() in a script.
Open the web app's revision-diff tool and drop in
Rev A and Rev B. The same KEY-aware, type-aware engine runs compiled to
WebAssembly, entirely client-side — you get the per-group added/removed/changed
breakdown with each changed cell's a/b values, and neither file leaves your
machine.
Every door runs the same diff leaf and reports the same delta, so a 1.50 vs
1.5 non-change stays a non-change everywhere. Reach for lat diff in a
resubmission gate, diff() in a pipeline when you need the cell-level detail, or
the browser tool for a quick visual compare.
See also: Certify a file · Born typed