Skip to content

Merge two deliveries

Available in: Python · Node · CLI · Browser

When: two deliveries of one project need to become one file — a phased site investigation, or a resubmission that revises some boreholes and adds others — and you want the engine to reconcile them, not a manual copy-paste.

Merging is KEY-aware, not a file concatenation. Rows are matched on each group's dictionary KEY headings, so a re-sorted borehole list still merges onto its prior self. Files merge in argument order — a later file wins a KEY conflict — and the result is a union: a row present in only one file is kept (silence is not deletion). Every surface reports the same audit: the per-row revisions a later file made, plus any warnings.

lat merge <files…> --out <merged.ags> reconciles two or more deliveries on disk. The optional --tran-issue / --tran-date stamp the merged file's own transmission record (it genuinely is a new transmission):

lat merge examples/sample_site.ags examples/phase2.ags --out examples/merged.ags \
    --tran-issue 3 --tran-date 2024-01-15
merged 2 files → examples/merged.ags (6998 bytes)
  warning [parent_revised_check_children]: PROJ was revised; verify child group(s) ["LOCA"] are still consistent (merge performs no cross-group consistency check)
  1 row revision(s):
    PROJ ["LAT-DEMO"]: changed ["PROJ_NAME"] (from file[1])

The summary names each revision — here the second delivery changed PROJ_NAME, matched on the PROJ_ID KEY LAT-DEMO — and flags that a revised parent (PROJ) has child groups worth re-checking. When two files declare a column with different AGS types, merge errors (exit 6) rather than guess; --on-type-clash promote or --on-type-clash widen settles it (see When the two files disagree on a type).

laterite.merge(*sources, …) takes two or more of anything read accepts (paths, text, bytes, Ags4File handles) and returns a MergeResult — the merged bytes plus the warnings and per-row revisions audit:

import laterite

res = laterite.merge(
    "phase1.ags", "phase2.ags",
    tran=TranStamp(
        issue="3", date="2024-01-15", producer="Us",
        recipient="Client", status="Merged",
    ),
)
for rev in res.revisions:
    print(rev["group"], rev["key"], "changed", rev["changed"])
res.save("merged.ags")

An AGS-type clash between two files raises MergeConflictError by default; on_type_clash="promote" or "widen" settles it (see below). res.text decodes the merged bytes, and res.save(path) writes them.

merge(sources, opts) runs the same leaf and returns the same { bytes, warnings, revisions, text }:

import { merge } from "laterite";

const res = merge(["phase1.ags", "phase2.ags"], {
  tran: {
    issue: "3", date: "2024-01-15", producer: "Us",
    recipient: "Client", status: "Merged",
  },
});
for (const rev of res.revisions) {
  console.log(rev.group, rev.key, "changed", rev.changed);
}

A bare string is a path in Node, so pass a Buffer / Uint8Array when a delivery only exists in memory. A type clash throws MergeConflictError; { onTypeClash: "promote" } or "widen" settles it (see below).

Open the web app's Tools → Merge tool, keep the file you already loaded as the base, and drop in the incoming delivery. The same reconciliation runs compiled to WebAssembly, entirely client-side: you get the per-row revision audit, choose how to settle a type clash, and download the merged .ags — neither file leaves your machine.

When the two files disagree on a type

One delivery types LOCA_GL as 2DP, the next types it 5DP. Merge will not guess: by default it errors (exit 6 / MergeConflictError). You choose how to settle it.

mode what the merged column becomes your values
error (default) merge refuses
widen X (free text) kept byte-for-byte
promote the greatest precision2DP + 5DP5DP coarser values zero-padded: 10.0010.00000

widen is lossless on the bytes but throws the type away, and X is the least informative answer available. promote keeps the column numeric.

lat merge phase1.ags phase2.ags --out merged.ags --on-type-clash promote
res = laterite.merge("phase1.ags", "phase2.ags", on_type_clash="promote")
const res = merge(["phase1.ags", "phase2.ags"], { onTypeClash: "promote" });

promote never rounds and never demotes. It only ever appends zeros, so no digit you wrote is ever changed, and taking the maximum precision is the only direction that cannot destroy data — which also makes the result independent of argument order (unlike a KEY conflict, where the later file deliberately wins). A value it cannot pad losslessly is kept verbatim and warned about, never rounded.

It is deliberately limited to nDP. Significant figures (3SF) and scientific notation (2SCI) fall back to widen, because decimal places are a formatting convention but significant figures are a claim about measured precision — padding 3SF to 5SF would assert two digits the instrument never resolved.

Why promote matters downstream

_content_hash fingerprints a row's values, canonicalised through the declared type — so 10.00 hashes as a number under 2DP but as a string under X. A widened column therefore stops matching its own typed source, while a promoted one still dedups against it.

A conflicting UNIT is fatal in every mode

TYPE has a universal absorber (X); UNIT has none. There is no supertype of metres and millimetres, and merge will never convert — so if two files declare different UNITs for one heading, merge refuses in every mode, promote included. Reconcile the UNIT row at source.

One caveat everywhere: identity is KEY-based, so correcting a KEY value (a LOCA_ID typo BH1BH01) reads as a different row, not an edit — both persist. Fix KEY typos in the source before merging.

See also: Diff two revisions · Born typed