Build AGS4 from a typed graph¶
Available in: Python · Node · Browser
Construct a PROJ typed-class tree, attach its children, and hand the whole
graph to the emitter — use this when your data is already a graph in memory
(objects, not tables) rather than {code: frame} mappings.
import laterite
from laterite import build_ags4
from laterite.groups import LOCA, PROJ
# A typed PROJ graph — children attach via .append or the constructor kwarg.
p = PROJ(proj_id="LAT-DEMO", proj_name="Built from a typed graph")
p.locas.append(LOCA(loca_id="BH01", loca_gl=12.50)) # attach via .append …
PROJ(proj_id="P2", locas=[LOCA(loca_id="BH02", loca_gl=13.75)]) # … or the ctor kwarg
# #214: the graph is walked depth-first, and the door emits only the headings
# you set (like the frames door) — so a sparse graph stays sparse.
res = build_ags4(p)
print("groups:", laterite.read(data=res.bytes).groups)
print("findings:", len(res.findings))
# The root-metadata groups (TRAN/UNIT/TYPE/ABBR/DICT) have no parent, so they are
# not part of a PROJ-rooted graph and cannot be reached by the walk. They are
# reported, not invented:
assert set(laterite.read(data=res.bytes).groups) == {"PROJ", "LOCA"}
assert res.findings
# `synthesise_metadata=True` derives the ones that CAN be derived — UNIT and TYPE
# from your data, ABBR when PA codes are used. PROJ, DICT and TRAN are never
# invented: a project identity, a schema extension and a record of transmission
# are yours to state. A guessed DICT parent would quietly mislead the relational
# checks, and a placeholder TRAN would satisfy the rule while asserting a
# transmission that never happened.
full = build_ags4(
p,
synthesise_metadata=True,
tran=laterite.TranStamp(
issue="1",
date="2026-07-30",
producer="Demo Producer",
recipient="Demo Recipient",
status="Final",
),
)
assert {"PROJ", "LOCA", "TRAN", "UNIT", "TYPE"}.issubset(
laterite.read(data=full.bytes).groups
)
assert not full.findings # a valid file, in one call, because you asked
# The managed child collection is append-only — reassigning it raises:
try:
p.locas = [LOCA(loca_id="BH99")]
raise AssertionError("expected AttributeError")
except AttributeError:
pass
build_ags4(PROJ(...)) walks the graph depth-first (#214) and emits exactly
the groups you built. You handed it a PROJ with one LOCA child; you got
those two back, plus three findings naming the mandatory metadata catalogs your
graph doesn't carry — Rules 14 (TRAN), 15 (UNIT) and 17 (TYPE). Pass
synthesise_metadata=True to derive UNIT and TYPE (ABBR too, when PA
pick-list codes are used). TRAN is not derivable — state it with the five
tran_* arguments — so a sparse graph builds clean in one call once you supply
the transmission it represents.
Children attach two ways, shown above: .locas.append(LOCA(...)) after the
fact, or the locas=[...] constructor kwarg up front. Either way the typed-graph
door emits only the headings you set — nothing is invented in your data
columns — which is why a graph carrying just loca_id and loca_gl is enough.
The managed child collection is append-only: reassigning p.locas = [...]
raises AttributeError rather than silently dropping the rows you built up.
Mutate it through .append (and the list's own methods), never by rebinding.
// what this shows: build a typed PROJ graph (objects, not tables) and emit valid AGS4 from it.
import { buildAgs4, read, PROJ, LOCA } from "laterite";
import assert from "node:assert/strict";
// A typed PROJ graph — children attach via the `locas` array.
const p = new PROJ({
PROJ_ID: "LAT-DEMO",
PROJ_NAME: "Built from a typed graph",
});
p.locas.push(new LOCA({ LOCA_ID: "BH01", LOCA_GL: 12.5 })); // attach via push …
new PROJ({
PROJ_ID: "P2",
locas: [new LOCA({ LOCA_ID: "BH02", LOCA_GL: 13.75 })],
}); // … or the ctor field
// buildAgs4 walks the graph depth-first (#214) and emits only the headings you
// set — so a sparse graph stays sparse.
const res = buildAgs4(p);
const groups = read(res.bytes).groups;
console.log("groups:", groups);
console.log("findings:", res.findings.length);
// The root-metadata groups (TRAN/UNIT/TYPE/ABBR/DICT) have no parent, so they
// are not part of a PROJ-rooted graph. They are reported, not invented:
assert.deepEqual(groups, ["PROJ", "LOCA"]);
assert.ok(res.findings.length > 0);
// `synthesiseMetadata` derives the ones that CAN be derived. PROJ, DICT and
// TRAN are never invented: a project identity, a schema extension and a record
// of transmission are yours to state. A guessed DICT parent would quietly
// mislead the relational checks, and a placeholder TRAN would satisfy the rule
// while asserting a transmission that never happened.
const full = buildAgs4(p, {
synthesiseMetadata: true,
tran: {
issue: "1",
date: "2026-07-30",
producer: "Demo Producer",
recipient: "Demo Recipient",
status: "Final",
},
});
const fullGroups = read(full.bytes).groups;
assert.ok(
["PROJ", "LOCA", "TRAN", "UNIT", "TYPE"].every((c) => fullGroups.includes(c)),
);
assert.equal(full.findings.length, 0); // valid in one call, because you asked
The same typed classes are named exports — import { PROJ, LOCA } from
"laterite" — with uppercase heading fields (PROJ_ID, LOCA_GL) and a
child array per parent (p.locas). Build the tree with the constructor's
locas: [...] field or push onto it, then hand the root to buildAgs4; it
walks the graph depth-first and returns the same BuildResult the frames door
does, with { synthesiseMetadata: true } deriving the catalogs the same way.
(Node's locas is a plain array — the append-only guard is a Python-only
nicety.)
Open the web app's Export pane: whether you
assemble data as a graph or as per-group tables, the same WebAssembly emitter
builds the file client-side — nothing uploaded. Direct wasm callers take
synthesise_metadata on build_ags4 / build_ags4_ipc to derive the
UNIT/TYPE catalogs, plus the five tran_* arguments to stamp a TRAN.
Tip
build_ags4 / buildAgs4 returns a BuildResult whichever door you use.
Inspect res.text / res.bytes in memory, check res.findings for any
caveats autofix couldn't resolve, or res.save("out.ags") to persist a
byte-faithful AGS4 file.
See also: Build from frames · Produce AGS4.