Build AGS4 from frames¶
Available in: Python · Node · Browser
What / when: you have your data as per-group tables (one per group, columns named
for the AGS headings) and want an AGS4 file back — optionally with the
UNIT / TYPE boilerplate derived for you and TRAN stamped from your own
transmission details.
import laterite
import polars as pl
# Build valid AGS4 from your own per-group frames — columns are the AGS headings.
proj = pl.DataFrame({"PROJ_ID": ["LAT-DEMO"], "PROJ_NAME": ["Demo site"]})
loca = pl.DataFrame({"LOCA_ID": ["BH01", "BH02"], "LOCA_GL": [12.50, 13.75]})
res = laterite.build_ags4({"PROJ": proj, "LOCA": loca}) # default mode="autofix"
groups = laterite.read(data=res.bytes).groups
print("groups:", groups)
print("findings:", len(res.findings))
# You get back exactly the groups you supplied. AGS4 also mandates the metadata
# catalogs (TRAN/UNIT/TYPE), which your frames don't carry — so those are
# REPORTED rather than invented:
assert set(groups) == {"PROJ", "LOCA"}
assert {f["rule"] for f in res.findings} >= {
"AGS Format Rule 14", # TRAN
"AGS Format Rule 15", # UNIT
"AGS Format Rule 17", # TYPE
}
# Ask for them and UNIT and TYPE are derived from your columns. TRAN is not
# derivable — only you know who sent what to whom — so you state it, and a build
# that doesn't reports the gap instead of inventing a placeholder that would
# satisfy the rule while asserting a transmission that never happened. Opt-in
# either way, so nothing appears in your file that you didn't ask for.
full = laterite.build_ags4(
{"PROJ": proj, "LOCA": loca},
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
build_ags4 takes a {code: frame} mapping — each frame's columns are the AGS
headings for that group — and returns a BuildResult containing exactly the
groups you supplied. AGS4 also mandates the metadata catalogs, which your frames
don't carry, so they are reported, not invented: the three findings are Rules
14 (TRAN), 15 (UNIT) and 17 (TYPE). The result carries the file three ways:
res.text— the AGS4 as astr(in memory)res.bytes— the byte-faithful encoding (whatread(data=…)consumes above)res.save("out.ags")— persist to disk
To get the catalogs: pass synthesise_metadata=True. UNIT and TYPE are
derived from your columns and ABBR from the standard table when PA codes are
used. PROJ, DICT and TRAN are never synthesised: a project identity, a
schema extension and a record of transmission are authorial facts. Inventing a
DICT parent would turn a loud Rule 18 error into a silent false statement Rule
10's relational checks then trust; inventing a TRAN would satisfy Rule 14
while asserting a transmission that never happened.
To get a TRAN: state it — tran=TranStamp(issue=…, date=…, producer=…,
recipient=…, status=…). All five are required together because all five are
REQUIRED headings; the dataclass enforces that at your call site rather than
letting a half-stamp become a Rule 10b finding. Pass nothing and Rule 14
reports the gap instead.
Gotcha: synthesis is independent of mode, and only mode="autofix" (the
default) honours it. mode="report" emits unmodified and hands you the findings;
mode="strict" is a hard gate that rejects the build outright if the output
violates any error-severity rule — so a data-only build under strict raises
rather than emitting. The emitter also writes only the headings you supply,
never inventing data columns, so a sparse frame builds clean rather than padding
out the full dictionary.
// what this shows: buildAgs4(entries) — valid AGS4 from your own per-group row data (headings as keys).
import { buildAgs4, read } from "laterite";
import assert from "node:assert/strict";
// Each group's rows are plain objects whose KEYS are the AGS headings.
const proj = [{ PROJ_ID: "LAT-DEMO", PROJ_NAME: "Demo site" }];
const loca = [
{ LOCA_ID: "BH01", LOCA_GL: 12.5 },
{ LOCA_ID: "BH02", LOCA_GL: 13.75 },
];
// A Map (or Array of [code, rows]) — group order is preserved, so put PROJ first.
const res = buildAgs4(
new Map([
["PROJ", proj],
["LOCA", loca],
]),
); // default mode "autofix"
const groups = read(res.bytes).groups;
console.log("groups:", groups);
console.log("findings:", res.findings.length);
// You get back exactly the groups you supplied. AGS4 also mandates the metadata
// catalogs (TRAN/UNIT/TYPE), which your rows don't carry — so those are REPORTED
// rather than invented:
assert.deepEqual(groups, ["PROJ", "LOCA"]);
const rules = new Set(res.findings.map((f) => f.rule));
for (const r of [
"AGS Format Rule 14",
"AGS Format Rule 15",
"AGS Format Rule 17",
]) {
assert.ok(rules.has(r), `expected ${r}`);
}
// Ask for them and UNIT and TYPE are derived from your columns. TRAN is not
// derivable — only you know who sent what to whom — so you state it, and a
// build that doesn't reports the gap instead of inventing a placeholder that
// would satisfy the rule while asserting a transmission that never happened.
// Opt-in either way, so nothing appears in your file that you didn't ask for.
const full = buildAgs4(
new Map([
["PROJ", proj],
["LOCA", loca],
]),
{
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);
buildAgs4 takes a Map (or array) of [code, rows] entries — each row a
plain object whose keys are the AGS headings — or an arrow-js Table per
group. Group order is preserved, so put PROJ first. It returns the same
BuildResult as Python: res.bytes / res.text carry the document and
res.findings is the residual the mode couldn't clear. Pass
{ synthesiseMetadata: true } to derive UNIT/TYPE (and ABBR for PA
codes) and tran: { issue, date, producer, recipient, status } to stamp a
TRAN; without them those gaps are reported as Rules 14/15/17. No DuckDB
peer needed — emit is pure.
Open the web app's Export pane: assemble or
paste your per-group data and export an AGS4 file. The same emitter runs
compiled to WebAssembly, and nothing is uploaded to build it. Direct wasm
callers take synthesise_metadata on build_ags4 / build_ags4_ipc — the
browser twin of the Python and Node flags.
Every door runs the same emitter over the same dictionary, and metadata synthesis
is opt-in on all of them — synthesise_metadata= in Python, { synthesiseMetadata }
in Node, synthesise_metadata in the browser build. Without it you get exactly the
groups you supplied, plus findings naming the catalogs you didn't.
See also: Build from a typed graph · Produce AGS4