laterite 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Table } from 'apache-arrow';
2
- import { Reading, Finding, AppliedFix, ValidationReport, displayHint, PackStats, UnpackStats } from '#native';
3
- export { AppliedFix, Finding, GroupMeta, version } from '#native';
2
+ import { Finding, AppliedFix, ValidationReport, Sidecar, Reading, displayHint, PackStats, UnpackStats, ExcelStats } from '#native';
3
+ export { AppliedFix, ExcelStats, Finding, GroupMeta, Sidecar, version } from '#native';
4
4
 
5
5
  /** One query result row — JS-native values (`getRowObjectsJS`: Dates, numbers,
6
6
  * nulls; 64-bit ints stay `bigint`). */
@@ -12,25 +12,161 @@ interface QueryOptions {
12
12
  arrow?: boolean;
13
13
  }
14
14
 
15
+ /**
16
+ * The product of {@link fix} — the Node port of laterite-py's `FixResult`.
17
+ *
18
+ * `fix()` mechanically repairs an AGS4 document — always the *safe* set (CRLF /
19
+ * BOM / embedded-CR normalisation, short-row padding, numeric reformatting, the
20
+ * TRAN delimiter+concatenator rows), plus the intent-guessing *risky* set when
21
+ * asked — then re-validates the repaired bytes. This object is what that pass
22
+ * hands back: the fixed document, an account of what was changed, and an honest
23
+ * record of what it could not touch. Construction is internal; you receive one
24
+ * from `fix()`, you don't build it.
25
+ *
26
+ * The repair is non-destructive — nothing is written to disk by the fixer. The
27
+ * repaired document rides home on `.bytes`, decoded on demand via the `.text`
28
+ * getter (UTF-8), and persisted only when you choose to with `.save(path)`
29
+ * (which writes the bytes and returns the path). `applied` enumerates each fix
30
+ * that landed (`{kind, label, rule, line?, risk}` — serde snake_case, identical
31
+ * across Python / CLI / Node), with `.fixesApplied` as its count for a quick
32
+ * "did anything change?". Crucially, `findings` is *not* the input's problems —
33
+ * it is the residual after re-validation: the rule violations that survived the
34
+ * repair and still need a human. `dictVersion` records the AGS4 edition the fix
35
+ * resolved against. `toString()` gives a one-line summary (byte count, fixes
36
+ * applied, residual findings).
37
+ */
38
+ declare class FixResult {
39
+ readonly bytes: Buffer;
40
+ /** Findings that remain after the fixes — what could NOT be mechanically fixed. */
41
+ readonly findings: Finding[];
42
+ /** The fixes that were applied (`{kind, label, rule, line?, risk}`). */
43
+ readonly applied: AppliedFix[];
44
+ readonly dictVersion: string;
45
+ constructor(bytes: Buffer,
46
+ /** Findings that remain after the fixes — what could NOT be mechanically fixed. */
47
+ findings: Finding[],
48
+ /** The fixes that were applied (`{kind, label, rule, line?, risk}`). */
49
+ applied: AppliedFix[], dictVersion: string);
50
+ /** How many fixes were applied. */
51
+ get fixesApplied(): number;
52
+ /** The repaired AGS4 document decoded as text. */
53
+ get text(): string;
54
+ /** Save the repaired bytes to `path`; returns `path`. */
55
+ save(path: string): string;
56
+ toString(): string;
57
+ }
58
+
59
+ /** One finding without its `rule` key — the value shape `byRule()` groups. */
60
+ type RuleFinding = Omit<Finding, "rule">;
61
+ /**
62
+ * The verdict from {@link validate} — the Node port of laterite-py's `Report`.
63
+ *
64
+ * `validate()` runs the numbered-rule engine over an AGS4 source and hands back
65
+ * one of these: an immutable wrapper over the native `ValidationReport`. It is a
66
+ * pure *result* object (it does not carry the file's bytes — that is what the
67
+ * read/build outputs are for); what it carries is the answer to "is this file
68
+ * conformant, and if not, why".
69
+ *
70
+ * Start with the headline getters. `isValid` is the one most callers branch on —
71
+ * it is `true` iff there are zero findings, which is deliberately *distinct* from
72
+ * the native `ok` flag (`ok` only means the source was parseable enough to
73
+ * validate). `count` is the number of findings, and `exitCode` mirrors what the
74
+ * `lat-check` binary would return for the same file, so a CLI wrapper can pass it
75
+ * straight through. The provenance getters — `file`, `dictVersion` (the AGS
76
+ * edition the rules were drawn from), and `resolution` — say *what* was checked
77
+ * and *against which dictionary*.
78
+ *
79
+ * The findings themselves come three ways. `findings` is the flat array in
80
+ * `lat-check` order (each `{rule, line?, group, desc, severity?}`). `byRule()`
81
+ * regroups them into the spec-rule map `{ "AGS Format Rule N": [...] }` for
82
+ * rule-oriented reporting. For machine output, `toJson()` and `toNdjson()` return
83
+ * strings byte-identical to `lat-check --json` / `--ndjson` (minted native-side),
84
+ * and `toString()` gives a one-line human summary.
85
+ *
86
+ * @see {@link validate} — the verb that produces a `Report`.
87
+ */
88
+ declare class Report {
89
+ #private;
90
+ constructor(r: ValidationReport);
91
+ /** Synthesise a clean report from a fresh certificate — the engine-skipped
92
+ * outcome of `Ags4File.validate()` on an `index=`-certified file. `resolution`
93
+ * is the sentinel `"certified"` (the engine never emits it), `count` is 0, and
94
+ * the edition is the cert's. Mirrors laterite-py's `Report.from_cert`. */
95
+ static fromCert(cert: Sidecar, label: string): Report;
96
+ get file(): string;
97
+ get dictVersion(): string;
98
+ get resolution(): string;
99
+ get count(): number;
100
+ /** `true` iff there are zero findings (distinct from the native `ok`, which
101
+ * only means "validatable"). */
102
+ get isValid(): boolean;
103
+ get exitCode(): number;
104
+ /** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
105
+ get findings(): Finding[];
106
+ /** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
107
+ * grouping (mirrors `Report.by_rule`). */
108
+ byRule(): Record<string, RuleFinding[]>;
109
+ /** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
110
+ toJson(): string;
111
+ /** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
112
+ toNdjson(): string;
113
+ toString(): string;
114
+ }
115
+
15
116
  /** A `(keyColumn, values)` filter, e.g. `["LOCA_ID", ["BH01", "BH02"]]`. */
16
117
  type Filter = [key: string, values: unknown[]];
118
+ /**
119
+ * A key-filtered view over an `Ags4File`'s engine — the Node port of laterite-py's
120
+ * `_AgsSubset`, returned by `Ags4File.at()`. Filters ACCUMULATE by chaining
121
+ * (`at("LOCA", […]).at("SAMP", […])` keeps both): `table(code)` applies every
122
+ * filter whose key column is present in `code` (others ignored), so groups
123
+ * carrying none of the keys pass through unfiltered. Async, because the engine
124
+ * is promise-based (`@duckdb/node-api`).
125
+ */
17
126
  declare class AgsSubset {
18
127
  #private;
19
128
  constructor(parent: Ags4File, filters: Filter[]);
20
- /** Narrow further by another entity's id (e.g. add a SAMP filter). */
129
+ /**
130
+ * Narrow further by another entity's id (e.g. add a SAMP filter on top of a
131
+ * LOCA one). Returns a fresh subset — filters accumulate, they never mutate.
132
+ *
133
+ * @param group - The group code whose `_ID` key to filter on (`"SAMP"` becomes
134
+ * the `SAMP_ID` key column).
135
+ * @param values - The ids to keep; a row survives when its `<group>_ID` is one
136
+ * of these.
137
+ * @returns A new `AgsSubset` carrying this filter plus all the existing ones.
138
+ */
21
139
  at(group: string, values: Iterable<unknown>): AgsSubset;
22
140
  /** The related groups — those carrying at least one filter's key column. */
23
141
  get groups(): string[];
24
- /** `code` filtered by every applicable key (groups carrying none pass
25
- * through), as JS-native row objects (or an arrow-js `Table` with
26
- * `{ arrow: true }`). */
142
+ /**
143
+ * `code` filtered by every applicable key (groups carrying none of the keys
144
+ * pass through unfiltered), as JS-native row objects by default — or a
145
+ * born-typed arrow-js `Table` with `{ arrow: true }`.
146
+ *
147
+ * @param code - The group to read (its clean name).
148
+ * @param opts - Query options; `{ arrow: true }` switches the return to an
149
+ * arrow-js `Table`.
150
+ * @returns The filtered rows as `Row[]`, or a `Table` when arrow output is
151
+ * requested.
152
+ * @throws If `{ arrow: true }` is set but DuckDB's `arrow` community extension
153
+ * can't be installed/loaded (offline or air-gapped).
154
+ */
27
155
  table(code: string): Promise<Row[]>;
28
156
  table(code: string, opts: {
29
157
  arrow: true;
30
158
  }): Promise<Table>;
31
159
  table(code: string, opts?: QueryOptions): Promise<Row[] | Table>;
32
- /** `{group: rows}` for every related group, each filtered — a location's whole
33
- * related record set in one call (arrow-js Tables with `{ arrow: true }`). */
160
+ /**
161
+ * `{group: rows}` for every related group, each filtered a location's whole
162
+ * related record set in one call. Returns arrow-js `Table`s with
163
+ * `{ arrow: true }`, JS-native row objects otherwise.
164
+ *
165
+ * @param opts - Query options; `{ arrow: true }` makes each value a `Table`.
166
+ * @returns A map from group code to its filtered rows (or `Table`).
167
+ * @throws If `{ arrow: true }` is set but DuckDB's `arrow` community extension
168
+ * can't be installed/loaded (offline or air-gapped).
169
+ */
34
170
  frames(): Promise<Record<string, Row[]>>;
35
171
  frames(opts: {
36
172
  arrow: true;
@@ -38,39 +174,114 @@ declare class AgsSubset {
38
174
  frames(opts?: QueryOptions): Promise<Record<string, Row[] | Table>>;
39
175
  }
40
176
 
177
+ /** The source a handle was read from — retained so the chained `validate`/`fix`/
178
+ * `diff` verbs re-run against the TRUE bytes (matching original line numbers),
179
+ * not the byte-faithful `.bytes` re-emit. A synthesised handle has none. */
180
+ interface Ags4Source {
181
+ path?: string;
182
+ text?: string;
183
+ data?: Uint8Array;
184
+ encoding?: string;
185
+ }
186
+ /**
187
+ * A parsed AGS4 file — the Node port of laterite-py's `Ags4File`. The read
188
+ * surface is **Arrow-direct**: `table(code)` decodes the native typed Arrow IPC
189
+ * straight to an arrow-js Table (no DuckDB round-trip). Python routes reads
190
+ * through DuckDB only because its pandas path is otherwise pyarrow-bound; Node
191
+ * has no such reason, so the base read surface needs no engine at all.
192
+ *
193
+ * The OPTIONAL DuckDB layer — `sql()` / `at()` / `connection` — is lazy: the
194
+ * engine spins up only on first use, and `@duckdb/node-api` is an optional peer
195
+ * (absent → a helpful install error). These are async (the Neo client is
196
+ * promise-based); everything else stays sync.
197
+ */
41
198
  declare class Ags4File {
42
199
  #private;
43
- constructor(reading: Reading);
200
+ constructor(reading: Reading, src?: Ags4Source);
201
+ /** @internal — `read(..., { index })` attaches a freshness-checked certificate
202
+ * so a later errors-only `.validate()` can skip the rule engine. */
203
+ _attachCert(cert: Sidecar): void;
44
204
  /** Group codes in file order. */
45
205
  get groups(): string[];
46
206
  /** The file's `TRAN_AGS` edition string, if present. */
47
207
  get tranAgs(): string | null;
208
+ /** The HEADING-row codes of `code`, in file order. Throws if `code` isn't in the file. */
48
209
  headings(code: string): string[];
210
+ /** The UNIT row of `code`, one per heading. Throws if `code` isn't in the file. */
49
211
  units(code: string): string[];
212
+ /** The TYPE row of `code` (the AGS data types), one per heading. Throws if `code` isn't in the file. */
50
213
  types(code: string): string[];
51
214
  /** AGS TYPE → the DuckDB column type each heading lands as (for the P3 engine). */
52
215
  sqlTypes(code: string): string[];
53
216
  /** 1-indexed source line of each DATA row. */
54
217
  lineNumbers(code: string): number[];
218
+ /** Whether `code` is one of the file's groups — the cheap membership check `headings`/`table` would otherwise throw on. */
55
219
  has(code: string): boolean;
56
220
  /** One group as a born-typed arrow-js `Table` (a 2DP heading is Float64, an ID
57
221
  * Utf8, a DT a Timestamp) — the SAME typing the Python/wasm hosts produce,
58
222
  * byte-identical by construction (one shared `build_record_batch`). Cached per
59
- * group. Throws if `code` isn't in the file. */
60
- table(code: string): Table;
223
+ * group. Throws if `code` isn't in the file.
224
+ *
225
+ * By default the synthetic `_id`/`_parent_id` key columns are **dropped**; pass
226
+ * `{ keys: true }` to include them (the relational `sql()`/`at()` layer always
227
+ * carries them regardless — that's what makes cross-group joins work). */
228
+ table(code: string, opts?: {
229
+ keys?: boolean;
230
+ }): Table;
61
231
  /** Spec-correct AGS4 as text — byte-faithful to the source DATA values
62
232
  * (re-emitted native-side from the retained parse). Memoised. */
63
233
  get text(): string;
64
234
  /** `text` encoded UTF-8 — the bytes `save()` writes. Memoised. */
65
235
  get bytes(): Buffer;
66
- /** Write the AGS4 to `path` (UTF-8); returns `path`. The inverse of `read`. */
236
+ /** Write the AGS4 to `path` (UTF-8) the inverse of `read`. The bytes are
237
+ * byte-faithful to the source DATA values, re-emitted from the retained parse.
238
+ *
239
+ * @param path Filesystem path to write the UTF-8 AGS4 to.
240
+ * @returns The same `path`, for chaining. */
67
241
  save(path: string): string;
242
+ /** The last `.validate()` outcome (`undefined` until validated). */
243
+ get report(): Report | undefined;
244
+ /** The `FixResult` on a handle returned by `.fix()` (`undefined` otherwise). */
245
+ get fixReport(): FixResult | undefined;
246
+ /** Validate this file against the AGS4 rules and return `this` (chainable —
247
+ * `read(p).validate().sql(...)`); the outcome lands on `.report`. Same engine as
248
+ * the free `validate()`, run on the source this handle was read from so line
249
+ * numbers match the original. Errors + WARNINGs by default (`warnings`); `fyi`
250
+ * adds the low-signal tier. `encoding` defaults to the one this handle was read
251
+ * with. Mirrors `laterite.Ags4File.validate()`. */
252
+ validate(opts?: Omit<ValidateOptions, "text">): this;
253
+ /** Mechanically repair this file and return a NEW, repaired `Ags4File` — the
254
+ * fluent transform, so `read(p).fix().validate().save(out)` reads as one chain.
255
+ * The `FixResult` (what was applied + residual findings) rides on the returned
256
+ * handle's `.fixReport`. Safe fixes always apply; `risky` adds the intent-
257
+ * guessing set. `encoding` defaults to this handle's read encoding. Non-
258
+ * destructive — the source on disk is untouched. Mirrors
259
+ * `laterite.Ags4File.fix()`. */
260
+ fix(opts?: Omit<FixOptions, "text">): Ags4File;
261
+ /** Compare this file (the baseline) against `other` (the revision) — the
262
+ * `RevisionDelta` the free `diff()` returns. `other` is a path, bytes, or another
263
+ * `Ags4File`. `encoding` defaults to this handle's read encoding. Mirrors
264
+ * `laterite.Ags4File.diff()`. */
265
+ diff(other: DiffSource, opts?: DiffOptions): RevisionDelta;
266
+ /** Mint this file's `.ags.idx` validity certificate — a clean-validation proof
267
+ * plus a byte-offset index — and write it beside the file. REQUIRES a prior
268
+ * clean `.validate()`: certify *vouches for* a passed validation, it does not run
269
+ * one. `path` is the certificate's OUTPUT location (default `<source>.idx`), not
270
+ * a file to certify — it refuses to overwrite the source or any existing
271
+ * non-certificate file. Returns the written path. The cert is byte-identical to
272
+ * Python's / `lat-check --emit-index`'s, so `lat-check` reads it, and a later
273
+ * `read(f, { index })` consumes it to skip re-validation. Mirrors
274
+ * `laterite.Ags4File.certify()`. */
275
+ certify(path?: string): string;
68
276
  /** Run SQL over the file's groups by their clean names — e.g.
69
277
  * `await ags.sql("SELECT * FROM SAMP JOIN LOCA USING (LOCA_ID) WHERE …")`.
70
- * Returns JS-native row objects by default, or a born-typed arrow-js `Table`
71
- * with `{ arrow: true }` (loads the `arrow` community extension on first use).
72
- * Any group may be referenced, so this loads them all. Needs the optional
73
- * `@duckdb/node-api` peer. */
278
+ * Any group may be referenced, so this loads them all into the engine.
279
+ *
280
+ * @param query SQL referencing groups by their bare AGS code as table names.
281
+ * @param opts `{ arrow: true }` returns a born-typed arrow-js `Table` (loads the
282
+ * `arrow` community extension on first use) instead of JS-native rows.
283
+ * @returns JS-native row objects by default, or a `Table` when `arrow` is set.
284
+ * @throws If the optional `@duckdb/node-api` peer is absent. */
74
285
  sql(query: string): Promise<Row[]>;
75
286
  sql(query: string, opts: {
76
287
  arrow: true;
@@ -80,7 +291,11 @@ declare class Ags4File {
80
291
  * returns a view whose `table(code)` yields only the rows whose `{group}_ID`
81
292
  * is in `values`. Chain to narrow (`.at("SAMP", […])`); `sub.groups` is the
82
293
  * related groups, `sub.frames()` pulls them all. Groups carrying none of the
83
- * keys pass through. For any other predicate, use `sql("… WHERE …")`. */
294
+ * keys pass through. For any other predicate, use `sql("… WHERE …")`.
295
+ *
296
+ * @param group The parent group whose `{group}_ID` key drives the filter.
297
+ * @param values The id values to keep (empty matches nothing).
298
+ * @returns An `AgsSubset` view; further `.at()` calls accumulate filters. */
84
299
  at(group: string, values: Iterable<unknown>): AgsSubset;
85
300
  /** The raw `@duckdb/node-api` connection — every engine feature — seeded with
86
301
  * all of this file's groups under their clean names. */
@@ -91,7 +306,9 @@ declare class Ags4File {
91
306
  /** Drop the decoded-Table cache and close the DuckDB engine (if any).
92
307
  * `using f = read(…)` runs this automatically. */
93
308
  close(): void;
309
+ /** `using f = read(…)` disposal hook — delegates to `close()`. */
94
310
  [Symbol.dispose](): void;
311
+ /** A compact one-line summary — group count and `TRAN_AGS` — for logs and the REPL. */
95
312
  toString(): string;
96
313
  }
97
314
 
@@ -103,11 +320,38 @@ interface BuildFinding {
103
320
  desc?: string;
104
321
  [key: string]: unknown;
105
322
  }
323
+ /**
324
+ * The product of {@link buildAgs4} — the data→AGS4 door's return value, and the
325
+ * Node port of laterite-py's `BuildResult`. Where `read` hands you an *existing*
326
+ * file, `buildAgs4` *constructs* one from your data and then runs the output back
327
+ * through the validator; this object is what falls out of that round trip. It is
328
+ * a plain, inert carrier — no DuckDB, no native handle to hold open — so you can
329
+ * keep it, pass it around, or persist it at leisure.
330
+ *
331
+ * It carries three things. {@link BuildResult.bytes | `bytes`} is the AGS4
332
+ * document as the validator emitted it (UTF-8); reach for {@link BuildResult.text
333
+ * | `text`} when you want it decoded as a string, or {@link BuildResult.save |
334
+ * `save(path)`} to write the bytes straight to disk (it returns the path).
335
+ * {@link BuildResult.findings | `findings`} is the *residual* set of validator
336
+ * findings — what the build could **not** clear given the `mode` it ran under
337
+ * (e.g. `"autofix"` applies the safe fixes and leaves only what it can't touch,
338
+ * `"report"` records everything); each is a flat {@link BuildFinding} of `rule`
339
+ * plus whatever rich keys the validator set. {@link BuildResult.fixesApplied |
340
+ * `fixesApplied`} counts how many safe fixes were applied along the way, and
341
+ * {@link BuildResult.applied | `applied`} is the ledger of those fixes (each a
342
+ * `{kind, label, rule, line?, risk}` record, the same shape `fix()`'s
343
+ * `FixResult.applied` carries). A clean build is an empty `findings` array; a
344
+ * non-empty one tells you exactly what the emitted document still trips on.
345
+ */
106
346
  declare class BuildResult {
107
347
  readonly bytes: Buffer;
108
348
  readonly findings: BuildFinding[];
349
+ /** The safe fixes AutoFix applied (`{kind, label, rule, line?, risk}`); empty outside `"autofix"`. */
350
+ readonly applied: AppliedFix[];
109
351
  readonly fixesApplied: number;
110
- constructor(bytes: Buffer, findings: BuildFinding[], fixesApplied: number);
352
+ constructor(bytes: Buffer, findings: BuildFinding[],
353
+ /** The safe fixes AutoFix applied (`{kind, label, rule, line?, risk}`); empty outside `"autofix"`. */
354
+ applied: AppliedFix[], fixesApplied: number);
111
355
  /** The AGS4 document decoded as text. */
112
356
  get text(): string;
113
357
  /** Save the bytes to `path`; returns `path`. */
@@ -115,52 +359,6 @@ declare class BuildResult {
115
359
  toString(): string;
116
360
  }
117
361
 
118
- declare class FixResult {
119
- readonly bytes: Buffer;
120
- /** Findings that remain after the fixes — what could NOT be mechanically fixed. */
121
- readonly findings: Finding[];
122
- /** The fixes that were applied (`{kind, label, rule, line?, risk}`). */
123
- readonly applied: AppliedFix[];
124
- readonly dictVersion: string;
125
- constructor(bytes: Buffer,
126
- /** Findings that remain after the fixes — what could NOT be mechanically fixed. */
127
- findings: Finding[],
128
- /** The fixes that were applied (`{kind, label, rule, line?, risk}`). */
129
- applied: AppliedFix[], dictVersion: string);
130
- /** How many fixes were applied. */
131
- get fixesApplied(): number;
132
- /** The repaired AGS4 document decoded as text. */
133
- get text(): string;
134
- /** Save the repaired bytes to `path`; returns `path`. */
135
- save(path: string): string;
136
- toString(): string;
137
- }
138
-
139
- /** One finding without its `rule` key — the value shape `byRule()` groups. */
140
- type RuleFinding = Omit<Finding, "rule">;
141
- declare class Report {
142
- #private;
143
- constructor(r: ValidationReport);
144
- get file(): string;
145
- get dictVersion(): string;
146
- get resolution(): string;
147
- get count(): number;
148
- /** `true` iff there are zero findings (distinct from the native `ok`, which
149
- * only means "validatable"). */
150
- get isValid(): boolean;
151
- get exitCode(): number;
152
- /** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
153
- get findings(): Finding[];
154
- /** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
155
- * grouping (mirrors `Report.by_rule`). */
156
- byRule(): Record<string, RuleFinding[]>;
157
- /** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
158
- toJson(): string;
159
- /** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
160
- toNdjson(): string;
161
- toString(): string;
162
- }
163
-
164
362
  declare abstract class AgsGroup {
165
363
  }
166
364
 
@@ -4550,20 +4748,41 @@ declare class UnsupportedEditionError extends Ags4Error {
4550
4748
  declare class BadDictError extends Ags4Error {
4551
4749
  constructor(message: string, exitCode?: number);
4552
4750
  }
4751
+ /** A passed `index=` certificate (`.ags.idx`) does not match the file it was read
4752
+ * for — its size / SHA-256 differ, so its byte offsets and clean verdict are now
4753
+ * lies. Raised at `read` time (fail-fast): an explicit `index=` asserts "this cert
4754
+ * is for this file", so a mismatch is an error, never a silent fall-back. Rebuild
4755
+ * it (`read(p).validate().certify()`). (#294 Batch E / #14) */
4756
+ declare class StaleCertError extends Ags4Error {
4757
+ constructor(message: string, exitCode?: number);
4758
+ }
4553
4759
 
4554
4760
  /** Cross-system target categories — the lowercase labels the engine returns. */
4555
4761
  type CanonicalType = "string" | "integer" | "decimal" | "datetime" | "date" | "time" | "bool" | "enum";
4556
4762
  /** A parsed AGS value: number (integer/decimal), boolean (YN), string
4557
4763
  * (text/enum **and** the canonical datetime/date/time strings), or null. */
4558
4764
  type AgsValue = string | number | boolean | null;
4559
- /** AGS spec type code → canonical category. Throws for unknown codes (mirrors
4560
- * Python's `ValueError`). */
4765
+ /**
4766
+ * AGS spec type code → canonical category. Throws for unknown codes (mirrors
4767
+ * Python's `ValueError`), so the engine's permissive `null` never leaks into
4768
+ * caller code as a silent miss.
4769
+ *
4770
+ * @param agsType - An AGS4 spec type code (e.g. `"2DP"`, `"ID"`, `"YN"`, `"DT"`).
4771
+ * @returns The lowercase canonical category the engine maps that code to.
4772
+ * @throws {Ags4Error} If the code is not a recognised AGS type.
4773
+ */
4561
4774
  declare function canonicalType(agsType: string): CanonicalType;
4562
4775
 
4563
- /** Parse an AGS4-shaped raw value into its canonical JS value (empty /
4776
+ /**
4777
+ * Parse an AGS4-shaped raw value into its canonical JS value (empty /
4564
4778
  * unparseable → null). datetime/date/time come back as the canonical **string**
4565
4779
  * (engine shape; `new Date(s)` if you want a Date). Non-string input is
4566
- * stringified first (matches the Python wrapper). */
4780
+ * stringified first (matches the Python wrapper).
4781
+ *
4782
+ * @param raw - The raw cell value; non-string input is coerced via `String(...)`, and `null`/`undefined` short-circuit to `null`.
4783
+ * @param agsType - The AGS4 spec type code governing how `raw` is interpreted.
4784
+ * @returns The canonical value: a number (integer/decimal), boolean (YN), string (text/enum and the datetime/date/time strings), or `null`.
4785
+ */
4567
4786
  declare function parseValue(raw: unknown, agsType: string): AgsValue;
4568
4787
 
4569
4788
  type agsTypes_AgsValue = AgsValue;
@@ -4596,6 +4815,10 @@ type Heading = GeneratedHeading;
4596
4815
  * dictionary carries combined statuses like `KEY+REQUIRED`, so a bare
4597
4816
  * `status === "KEY"` would wrongly miss them (matches the Rust/Python check). */
4598
4817
  declare function isKeyStatus(status: string): boolean;
4818
+ /** A typed, immutable view onto one standard AGS group: its 4-letter `code`,
4819
+ * human `contents` description, `parent` code (or `null` for a root group), and
4820
+ * its ordered `headings`. Wraps a single entry from the generated dictionary so
4821
+ * the registry never hands out mutable raw rows. */
4599
4822
  declare class GroupDescriptor {
4600
4823
  readonly code: string;
4601
4824
  readonly contents: string;
@@ -4606,21 +4829,71 @@ declare class GroupDescriptor {
4606
4829
  get table(): string;
4607
4830
  /** DuckDB view name (`v_<code>`). */
4608
4831
  get view(): string;
4832
+ /** This group's KEY headings (status part-matched via {@link isKeyStatus}), in
4833
+ * declaration order. */
4609
4834
  get keyHeadings(): readonly Heading[];
4835
+ /** This group's non-KEY headings — the complement of {@link keyHeadings} — in
4836
+ * declaration order. */
4610
4837
  get nonKeyHeadings(): readonly Heading[];
4611
4838
  }
4612
4839
  /** Every standard AGS group, keyed by 4-letter code. */
4613
4840
  declare const GROUPS: Readonly<Record<string, GroupDescriptor>>;
4614
4841
  /** Single-group lookup; `undefined` for unknown codes. */
4615
4842
  declare function get(code: string): GroupDescriptor | undefined;
4843
+ /** One heading in a {@link DictionarySnapshot} group (`type` is the AGS data type). */
4844
+ interface DictHeading {
4845
+ name: string;
4846
+ status: string;
4847
+ type: string;
4848
+ unit?: string;
4849
+ description: string;
4850
+ }
4851
+ /** One group in a {@link DictionarySnapshot}. */
4852
+ interface DictGroup {
4853
+ code: string;
4854
+ contents: string;
4855
+ parent?: string;
4856
+ headings: DictHeading[];
4857
+ }
4858
+ /** The bundled standard dictionary for one edition. */
4859
+ interface DictionarySnapshot {
4860
+ ags_edition: string;
4861
+ groups: DictGroup[];
4862
+ }
4863
+ /**
4864
+ * The bundled STANDARD dictionary for one AGS `edition` — the per-edition view
4865
+ * of the official dictionary (canonical group + heading names, descriptions,
4866
+ * UNIT/TYPE, status). Where {@link GROUPS} is the *union* registry across all
4867
+ * editions (the default), this is a single edition's standard dictionary — the
4868
+ * same content the browser and `laterite.registry.dictionary()` render, from
4869
+ * one shared Rust builder (#294 F#6).
4870
+ *
4871
+ * @param edition `"4.0.3" | "4.0.4" | "4.1" | "4.1.1" | "4.2"`; omit (or
4872
+ * `"auto"`) for the fallback edition.
4873
+ * @throws {Error} if `edition` is not a recognised edition.
4874
+ */
4875
+ declare function dictionary(edition?: string): DictionarySnapshot;
4616
4876
  /** Every direct child group of `parentCode`, alphabetically. */
4617
4877
  declare function childGroups(parentCode: string): GroupDescriptor[];
4618
4878
  /** Parent chain from `code` to root: `[code, parent, …, root]`. Throws for an
4619
4879
  * unknown code (so root groups — `[code]` — are distinguishable). */
4620
4880
  declare function ancestorChain(code: string): string[];
4621
- /** KEY heading names a group inherits from its ancestors. */
4881
+ /** KEY heading names a group inherits from its **direct parent** — the
4882
+ * intersection of this group's KEY headings with its immediate parent's. This
4883
+ * matches the Rust/Python `inherited_key_names` (NOT the whole ancestor chain):
4884
+ * because AGS re-declares inherited keys at every level, the direct-parent
4885
+ * intersection already captures every key a group carries from above, so an
4886
+ * ancestor-chain union would only add ancestor keys the group doesn't have
4887
+ * (e.g. `PROJ_ID` on `SAMP`).
4888
+ *
4889
+ * @param code The group whose inherited KEY names to gather.
4890
+ * @returns The set of KEY heading names shared with the direct parent (empty for a root).
4891
+ * @throws {Ags4Error} If `code` isn't in the registry. */
4622
4892
  declare function inheritedKeyNames(code: string): Set<string>;
4623
4893
 
4894
+ type registry_DictGroup = DictGroup;
4895
+ type registry_DictHeading = DictHeading;
4896
+ type registry_DictionarySnapshot = DictionarySnapshot;
4624
4897
  declare const registry_GROUPS: typeof GROUPS;
4625
4898
  type registry_GroupDescriptor = GroupDescriptor;
4626
4899
  declare const registry_GroupDescriptor: typeof GroupDescriptor;
@@ -4628,21 +4901,56 @@ type registry_Heading = Heading;
4628
4901
  type registry_HeadingStatus = HeadingStatus;
4629
4902
  declare const registry_ancestorChain: typeof ancestorChain;
4630
4903
  declare const registry_childGroups: typeof childGroups;
4904
+ declare const registry_dictionary: typeof dictionary;
4631
4905
  declare const registry_get: typeof get;
4632
4906
  declare const registry_inheritedKeyNames: typeof inheritedKeyNames;
4633
4907
  declare const registry_isKeyStatus: typeof isKeyStatus;
4634
4908
  declare namespace registry {
4635
- export { registry_GROUPS as GROUPS, registry_GroupDescriptor as GroupDescriptor, type registry_Heading as Heading, type registry_HeadingStatus as HeadingStatus, registry_ancestorChain as ancestorChain, registry_childGroups as childGroups, registry_get as get, registry_inheritedKeyNames as inheritedKeyNames, registry_isKeyStatus as isKeyStatus };
4909
+ export { type registry_DictGroup as DictGroup, type registry_DictHeading as DictHeading, type registry_DictionarySnapshot as DictionarySnapshot, registry_GROUPS as GROUPS, registry_GroupDescriptor as GroupDescriptor, type registry_Heading as Heading, type registry_HeadingStatus as HeadingStatus, registry_ancestorChain as ancestorChain, registry_childGroups as childGroups, registry_dictionary as dictionary, registry_get as get, registry_inheritedKeyNames as inheritedKeyNames, registry_isKeyStatus as isKeyStatus };
4636
4910
  }
4637
4911
 
4638
- /** zstd-compress `src` → `dest`. `level` 1–22 (default 9). */
4912
+ /**
4913
+ * zstd-compress `src` → `dest` for transport — content-agnostic, so it works on
4914
+ * any file (an `.ags` transfer, an `.ags5db`, anything), not just AGS data.
4915
+ *
4916
+ * @param src - Path to the file to compress.
4917
+ * @param dest - Path the compressed output is written to.
4918
+ * @param level - zstd level 1–22 (default 9, the empirical sweet spot on AGS
4919
+ * data — higher levels buy minutes, not bytes).
4920
+ * @returns The output size, compression ratio vs source, and elapsed seconds.
4921
+ */
4639
4922
  declare function pack(src: string, dest: string, level?: number): PackStats;
4640
- /** zstd-decompress `src` → `dest`. */
4923
+ /**
4924
+ * zstd-decompress a `.zst` produced by {@link pack} back to its original bytes.
4925
+ *
4926
+ * @param src - Path to the compressed `.zst` file.
4927
+ * @param dest - Path the decompressed output is written to.
4928
+ * @returns The output size and elapsed seconds.
4929
+ */
4641
4930
  declare function unpack(src: string, dest: string): UnpackStats;
4642
- /** zstd-compress + age-passphrase-encrypt `src` → `dest`. */
4931
+ /**
4932
+ * zstd-compress, then age-passphrase-encrypt `src` → `dest`. Compress-then-
4933
+ * encrypt is load-bearing: zstd needs low-entropy input, and ciphertext is
4934
+ * random — so the order can't flip. The age envelope is interoperable with the
4935
+ * Python side (pyrage) and `lat-db lock`, all linking the same Rust `age` crate.
4936
+ *
4937
+ * @param src - Path to the file to compress and encrypt.
4938
+ * @param dest - Path the encrypted output is written to.
4939
+ * @param password - Passphrase for the age envelope (scrypt + ChaCha20-Poly1305).
4940
+ * @param level - zstd level 1–22 (default 9).
4941
+ * @returns The output size, compression ratio vs source, and elapsed seconds.
4942
+ */
4643
4943
  declare function lock(src: string, dest: string, password: string, level?: number): PackStats;
4644
- /** age-passphrase-decrypt + zstd-decompress `src` → `dest`. Throws on a wrong
4645
- * passphrase / non-passphrase envelope. */
4944
+ /**
4945
+ * age-passphrase-decrypt, then zstd-decompress a `.zst.age` produced by
4946
+ * {@link lock} back to its original bytes.
4947
+ *
4948
+ * @param src - Path to the encrypted `.zst.age` file.
4949
+ * @param dest - Path the recovered output is written to.
4950
+ * @param password - Passphrase the envelope was sealed with.
4951
+ * @returns The output size and elapsed seconds.
4952
+ * @throws If the passphrase is wrong or the input is not a passphrase envelope.
4953
+ */
4646
4954
  declare function unlock(src: string, dest: string, password: string): UnpackStats;
4647
4955
 
4648
4956
  declare const transport_PackStats: typeof PackStats;
@@ -4660,12 +4968,37 @@ interface ReadOptions {
4660
4968
  text?: string;
4661
4969
  /** Source encoding label (`"utf-8"` default, `"windows-1252"`, …). */
4662
4970
  encoding?: string;
4663
- }
4664
- /** Parse AGS4a file `source` path, raw `Uint8Array`/`Buffer` bytes, or
4665
- * in-memory `text` into an `Ags4File`. Pass bytes (not a string) for large
4666
- * inputs: V8 caps strings at ~512 MB, but a `Uint8Array` does not, so a web
4667
- * backend can hand a multi-hundred-MB upload straight in. Throws `NotAgs4Error`
4668
- * / `FileNotFoundError` / `UnsupportedEditionError` for un-parseable input. */
4971
+ /** Path to this file's `.ags.idx` certificate (minted by `Ags4File.certify()`).
4972
+ * Opt-in, no autodiscovery naming it asserts the cert is for THIS file. A
4973
+ * fresh cert is carried so a later errors-only `.validate()` skips the rule
4974
+ * engine; a size/SHA mismatch throws {@link StaleCertError}. (#294 Batch E / #14) */
4975
+ index?: string;
4976
+ }
4977
+ /**
4978
+ * Parse AGS4 into an `Ags4File`. Where the data→AGS4 door (`buildAgs4`)
4979
+ * *constructs* a file, `read` *loads* one that already exists — from a file
4980
+ * `source` path, raw `Uint8Array`/`Buffer` bytes, or in-memory `opts.text`.
4981
+ *
4982
+ * Prefer bytes over a string for large inputs: V8 caps a single string at
4983
+ * ~512 MB, but a `Uint8Array` does not, so a web backend can hand a
4984
+ * multi-hundred-MB upload straight in without first stringifying it. A string
4985
+ * `source` is the file path; non-string bytes are the raw content; an absent
4986
+ * `source` means the input came in as `opts.text`.
4987
+ *
4988
+ * The engine never returns a soft failure for un-parseable input — it throws a
4989
+ * typed error (the same exit-code identity as `lat-check`) so callers branch on
4990
+ * the class, not a brittle message match.
4991
+ *
4992
+ * @param source - File path (string), raw `Uint8Array`/`Buffer` bytes, or
4993
+ * omitted when the input is supplied via `opts.text`.
4994
+ * @param opts - Read options ({@link ReadOptions}): in-memory `text` and the
4995
+ * source `encoding` label (default `"utf-8"`).
4996
+ * @returns An {@link Ags4File} wrapping the parsed groups.
4997
+ * @throws {FileNotFoundError} The path could not be opened.
4998
+ * @throws {NotAgs4Error} The input has no GROUP rows / is not decodable as AGS4.
4999
+ * @throws {UnsupportedEditionError} A recognised but unsupported edition (e.g. AGS3).
5000
+ * @throws {Ags4Error} Any other native parse failure (the fallback mapping).
5001
+ */
4669
5002
  declare function read(source?: string | Uint8Array, opts?: ReadOptions): Ags4File;
4670
5003
  interface ValidateOptions extends ReadOptions {
4671
5004
  /** Force an edition (`"4.0.3"`…`"4.2"`); default auto-detects from `TRAN_AGS`. */
@@ -4678,9 +5011,29 @@ interface ValidateOptions extends ReadOptions {
4678
5011
  checkFiles?: boolean;
4679
5012
  }
4680
5013
  /** Validate AGS4 — a file `source` path, raw `Uint8Array`/`Buffer` bytes, or
4681
- * in-memory `text` — against the AGS4 rules. Throws for un-validatable input;
4682
- * rule *violations* come back in the `Report`. (Bytes avoid V8's ~512 MB string
4683
- * cap, the same as `read`.) */
5014
+ * in-memory `text` (via `opts.text`) — against the numbered AGS4 rules, returning
5015
+ * a `Report`. The crucial distinction: rule *violations* are data, not errors
5016
+ * they come back inside the `Report` (`.findings`, `.byRule()`, `.isValid`); only
5017
+ * *un-validatable* input (missing file, not AGS4, AGS3, bad dictionary) throws.
5018
+ * By default the report is the error-tier findings; opt warnings and FYIs in with
5019
+ * `opts.warnings` / `opts.fyi`. The edition auto-detects from `TRAN_AGS` unless
5020
+ * pinned with `opts.dictVersion`. Pass bytes (not a string) for large inputs:
5021
+ * V8 caps strings at ~512 MB but a `Uint8Array` does not, so a web backend can
5022
+ * hand a multi-hundred-MB upload straight in — the same byte door as `read`.
5023
+ * Mirrors `laterite.validate()` / the `lat-check` binary.
5024
+ *
5025
+ * @param source File path (string), or raw bytes (`Uint8Array`/`Buffer`); omit to
5026
+ * validate `opts.text` instead.
5027
+ * @param opts Validation knobs (`ValidateOptions`) — the dictionary-version pin
5028
+ * (`dictVersion`), severity gates, in-memory text/encoding, and the on-disk
5029
+ * Rule-20 toggle; see the interface for each field.
5030
+ * @returns A `Report` carrying the findings, the resolved `dictVersion`, the
5031
+ * finding `count`, `isValid`, and `lat-check`-faithful `toJson()` / `toNdjson()`.
5032
+ * @throws {FileNotFoundError} the path could not be opened.
5033
+ * @throws {NotAgs4Error} the input has no GROUP rows / is not decodable AGS4.
5034
+ * @throws {UnsupportedEditionError} a recognised-but-unsupported edition (AGS3).
5035
+ * @throws {BadDictError} an invalid `opts.dictVersion` / unimplemented dictionary.
5036
+ */
4684
5037
  declare function validate(source?: string | Uint8Array, opts?: ValidateOptions): Report;
4685
5038
  /** Row-oriented group data: an array of `{HEADING: value}` objects. */
4686
5039
  type GroupRows = Array<Record<string, unknown>>;
@@ -4691,14 +5044,42 @@ interface EmitOptions {
4691
5044
  dictVersion?: string;
4692
5045
  /** `"autofix"` (default) | `"report"` | `"strict"`. */
4693
5046
  mode?: "autofix" | "report" | "strict";
4694
- }
4695
- /** Build valid AGS4 from your own data the data→AGS4 door. Where `read` loads
4696
- * an *existing* file, `buildAgs4` *constructs* a new one (and autofixes +
4697
- * validates it). `groups` is either a **typed-graph root** (`new PROJ({…})`,
4698
- * walked depth-first) OR a Map/array mapping each AGS group code to an arrow-js
4699
- * `Table` or row objects whose **keys are the AGS headings** (`LOCA_ID`, …).
4700
- * UNIT/TYPE are filled from the chosen `dictVersion`; order is preserved (put `PROJ`
4701
- * first). Needs no DuckDB. Persist the result with `BuildResult.save`. */
5047
+ /** Per-heading UNIT overrides, keyed `{ code: { heading: unit } }` (#294 F#9)
5048
+ * — e.g. `{ LOCA: { LOCA_XTRA: "kPa" } }`. Name only the headings you want to
5049
+ * set; the rest fill from the dictionary. Throws on an unknown code/heading. */
5050
+ units?: Record<string, Record<string, string>>;
5051
+ /** Per-heading AGS data-TYPE overrides, same `{ code: { heading: type } }` shape. */
5052
+ types?: Record<string, Record<string, string>>;
5053
+ }
5054
+ /**
5055
+ * Build valid AGS4 from your own data — the data→AGS4 door. Where `read` loads
5056
+ * an *existing* file, `buildAgs4` *constructs* a new one: it lays the groups out
5057
+ * in order, fills UNIT/TYPE from the chosen `dictVersion`, then runs the output
5058
+ * through the validator (the `mode` knob on `opts` decides what happens to the
5059
+ * findings — `"autofix"` applies the safe fixes *and* synthesizes any missing
5060
+ * UNIT/TYPE/TRAN/ABBR metadata group so a data-only build is valid; `"report"` merely
5061
+ * records them). The returned `BuildResult` carries the bytes, the residual `findings`,
5062
+ * and a `fixesApplied` count; persist it with `BuildResult.save`. Needs no DuckDB.
5063
+ *
5064
+ * `groups` accepts two shapes. A **typed-graph root** (`new PROJ({…, locas:[new
5065
+ * LOCA({…})]})`) is walked depth-first via the registry's parent→child links,
5066
+ * only the headings you set becoming columns (entirely-unset ones are dropped,
5067
+ * except KEY). The walk covers only
5068
+ * PROJ's subtree (the root-metadata groups have no parent), but under `"autofix"`
5069
+ * the missing UNIT/TYPE/TRAN (and ABBR for PA codes) are synthesized, so a
5070
+ * typed-graph build is valid out of the box. Or pass a **Map / array of `[code,
5071
+ * data]`** entries where `data` is
5072
+ * an arrow-js `Table` or row objects whose **keys are the AGS headings**
5073
+ * (`LOCA_ID`, …). Either way group order is preserved, so put `PROJ` first.
5074
+ *
5075
+ * @param groups The data to emit — a typed-graph root (`new PROJ({…})`), or a
5076
+ * `Map`/array of `[groupCode, Table | rowObjects]` entries (headings as keys).
5077
+ * @param opts Emit options (`dictVersion`, `mode`); see {@link EmitOptions}.
5078
+ * @returns A {@link BuildResult} — `.bytes`/`.text` of the AGS4 document, the
5079
+ * `findings` it could not fix, the `fixesApplied` count, and `.save(path)`.
5080
+ * @throws {Ags4Error} If a typed-graph node is not a registered AGS group.
5081
+ * @throws If the native emitter rejects the input (e.g. an unknown `dictVersion`).
5082
+ */
4702
5083
  declare function buildAgs4(groups: AgsGroup | Map<string, GroupData> | Array<[string, GroupData]>, opts?: EmitOptions): BuildResult;
4703
5084
  /** One cited divergence observation on a rule (`{id, note}`). */
4704
5085
  interface RuleObservation {
@@ -4714,9 +5095,20 @@ interface RuleMeta {
4714
5095
  fixable: boolean;
4715
5096
  observations: RuleObservation[];
4716
5097
  }
4717
- /** The AGS4 rule catalogue — one entry per rule (title, severity, whether `fix`
4718
- * can repair it, cited `O-N` notes). Mirrors `laterite.list_rules()` /
4719
- * `lat-check --list-rules`; backed by the gated `rules_meta.json`. No input. */
5098
+ /**
5099
+ * The AGS4 rule catalogue — one `RuleMeta` per numbered rule, surfaced so a
5100
+ * caller can show *which* rules exist and how each behaves before (or instead
5101
+ * of) running `validate`/`fix` over a file. Each entry carries the rule id and
5102
+ * title, a one-line `checks` summary, its `severity`, whether `fix` can
5103
+ * mechanically repair it (`fixable`), and any cited `O-N` divergence
5104
+ * observations. Mirrors `laterite.list_rules()` / `lat-check --list-rules`;
5105
+ * backed by the gated `rules_meta.json` (the catalogue is static, so this takes
5106
+ * no input and reads no file).
5107
+ *
5108
+ * @returns The full rule catalogue as a `RuleMeta[]` — see `RuleMeta` for the
5109
+ * per-entry shape (`rule`, `title`, `checks`, `severity`, `fixable`,
5110
+ * `observations`).
5111
+ */
4720
5112
  declare function listRules(): RuleMeta[];
4721
5113
  interface FixOptions {
4722
5114
  /** Repair in-memory `text` instead of a file path. */
@@ -4728,11 +5120,122 @@ interface FixOptions {
4728
5120
  /** Also apply the intent-guessing (risky) fixes, not just the safe set. */
4729
5121
  risky?: boolean;
4730
5122
  }
4731
- /** Mechanically repair AGS4 — a file `source` path, raw `Uint8Array`/`Buffer`
4732
- * bytes, or in-memory `text`. Applies the safe fixes (plus the risky set when
4733
- * `risky`), re-validates, and returns a `FixResult` (`.bytes` / `.text` /
4734
- * `.save(path)`); `findings` are what could NOT be mechanically fixed. Mirrors
4735
- * `laterite.fix()` / `lat-check --fix`. Throws for un-fixable input. */
5123
+ /** Mechanically repair AGS4 — the headless twin of the browser's Fix engine.
5124
+ * `source` is a file path, raw `Uint8Array`/`Buffer` bytes, or (via `opts.text`)
5125
+ * in-memory text. The *safe* fixes CRLF / BOM / embedded-CR normalisation,
5126
+ * short-row padding, numeric reformatting, and the TRAN delimiter+concatenator
5127
+ * rows — are always applied; pass `risky` to also run the intent-guessing set
5128
+ * (duplicate-heading rename, `dd/mm` datetime canonicalisation, smart-quote→ASCII
5129
+ * typography). The repaired bytes are re-validated, so `FixResult.findings` is
5130
+ * what could NOT be mechanically fixed.
5131
+ *
5132
+ * Non-destructive: nothing is written here — the repaired bytes come back on the
5133
+ * result (`.bytes` / `.text` / `.save(path)`), already UTF-8 with no BOM, so
5134
+ * fixing a non-UTF-8 file also normalises its encoding. Mirrors `laterite.fix()`
5135
+ * / `lat-check --fix`.
5136
+ *
5137
+ * @param source - The AGS4 input: a filesystem path (`string`) or raw bytes
5138
+ * (`Uint8Array`/`Buffer`). Omit to repair `opts.text` instead.
5139
+ * @param opts - {@link FixOptions} — `text` source, `risky` fixes, `dictVersion`
5140
+ * override, and source `encoding`.
5141
+ * @returns A {@link FixResult} carrying the repaired `bytes` (and `.text` /
5142
+ * `.save`), the `applied` fixes (with `fixesApplied` count), the residual
5143
+ * `findings` left after re-validation, and the resolved `dictVersion`.
5144
+ * @throws {Ags4Error} (or a subclass — {@link FileNotFoundError},
5145
+ * {@link NotAgs4Error}, {@link UnsupportedEditionError}, {@link BadDictError})
5146
+ * for un-fixable input, carrying the matching `lat-check` exit code.
5147
+ */
4736
5148
  declare function fix(source?: string | Uint8Array, opts?: FixOptions): FixResult;
5149
+ /** One changed cell of a matched row (`kind === "changed"`). `type` is the AGS
5150
+ * data type; `a`/`b` are the raw values on each side (`null` if that side's row
5151
+ * is shorter than the heading list). Snake_case fields mirror the wire shape the
5152
+ * shared `laterite-ags4-diff` leaf serialises — identical to Python's dict. */
5153
+ interface CellDelta {
5154
+ heading: string;
5155
+ type: string;
5156
+ a: string | null;
5157
+ b: string | null;
5158
+ }
5159
+ /** One row's verdict: `added` (only in `b`), `removed` (only in `a`), or
5160
+ * `changed` (matched by KEY, ≥1 typed cell differs). `key` is the KEY values (or
5161
+ * whole-row tuple when unkeyed); `cells` is populated only for `changed`. */
5162
+ interface RowDelta {
5163
+ kind: "added" | "removed" | "changed";
5164
+ key: string[];
5165
+ line_a: number | null;
5166
+ line_b: number | null;
5167
+ cells: CellDelta[];
5168
+ }
5169
+ /** A group's deltas. `added`/`removed`/`changed` are true totals; `keyed` is
5170
+ * false when matched on the whole-row tuple (no dictionary KEY headings). */
5171
+ interface GroupDelta {
5172
+ code: string;
5173
+ added: number;
5174
+ removed: number;
5175
+ changed: number;
5176
+ headings_added: string[];
5177
+ headings_removed: string[];
5178
+ keyed: boolean;
5179
+ key_headings: string[];
5180
+ rows: RowDelta[];
5181
+ }
5182
+ /** The revision diff — the shape `diff()` returns (parsed from the shared
5183
+ * `laterite-ags4-diff` leaf's JSON; byte-identical to Python / wasm / `lat-check
5184
+ * --diff`). */
5185
+ interface RevisionDelta {
5186
+ groups: GroupDelta[];
5187
+ groups_added: string[];
5188
+ groups_removed: string[];
5189
+ total_added: number;
5190
+ total_removed: number;
5191
+ total_changed: number;
5192
+ }
5193
+ interface DiffOptions {
5194
+ /** Force the edition used to resolve each group's KEY headings (`"4.0.3"`…
5195
+ * `"4.2"`); default takes it from the revision's `TRAN_AGS`. */
5196
+ dictVersion?: string;
5197
+ /** Source encoding label for path / bytes inputs (default `"utf-8"`). */
5198
+ encoding?: string;
5199
+ }
5200
+ /** A diff input: a file path (`string`), raw bytes, or an already-read `Ags4File`. */
5201
+ type DiffSource = string | Uint8Array | Ags4File;
5202
+ /** Compare two AGS4 documents and return their **revision diff** — the Node port
5203
+ * of `laterite.diff()` and the browser's revision-diff tool, over the SAME shared
5204
+ * `laterite-ags4-diff` engine `lat-check --diff` uses.
5205
+ *
5206
+ * `a` (baseline) and `b` (revision) are each a path, raw `Uint8Array`/`Buffer`
5207
+ * bytes, or an already-read `Ags4File`. Two choices make the diff meaningful
5208
+ * rather than noisy: rows are matched by the group's dictionary **KEY** headings
5209
+ * (not line order — re-sorted boreholes still pair up), and cells are compared
5210
+ * through the **typed** value (a formatting-only edit like `"1.0"` → `"1.00"` is
5211
+ * not a diff). The KEY-heading edition is the revision's `TRAN_AGS` unless pinned
5212
+ * with `opts.dictVersion`.
5213
+ *
5214
+ * @param a - The baseline document (path / bytes / `Ags4File`).
5215
+ * @param b - The revision document, in any of the same forms.
5216
+ * @param opts - {@link DiffOptions} — the `dictVersion` pin and source `encoding`.
5217
+ * @returns A {@link RevisionDelta}: per-group row/heading deltas, `groups_added`/
5218
+ * `groups_removed`, and the `total_added`/`total_removed`/`total_changed` counts.
5219
+ * @throws {FileNotFoundError} a path input could not be opened.
5220
+ * @throws {NotAgs4Error} either side is not decodable AGS4.
5221
+ * @throws {BadDictError} an invalid `opts.dictVersion`.
5222
+ */
5223
+ declare function diff(a: DiffSource, b: DiffSource, opts?: DiffOptions): RevisionDelta;
5224
+ /**
5225
+ * Write an AGS4 file's groups to an `.xlsx` — one worksheet per group (the
5226
+ * Node analog of Python's `to_excel`). `groups` forces the worksheet order;
5227
+ * otherwise AGS4 source order is preserved. Returns the conversion stats.
5228
+ */
5229
+ declare function toExcel(agsPath: string, xlsxPath: string, opts?: {
5230
+ groups?: string[];
5231
+ }): ExcelStats;
5232
+ /**
5233
+ * Read an `.xlsx` back into an AGS4 file (the Node analog of `from_excel`).
5234
+ * `formatNumericColumns` (default true) re-applies AGS4 numeric formatting to
5235
+ * numeric-looking columns. Returns the conversion stats.
5236
+ */
5237
+ declare function fromExcel(xlsxPath: string, agsPath: string, opts?: {
5238
+ formatNumericColumns?: boolean;
5239
+ }): ExcelStats;
4737
5240
 
4738
- export { AAVT, ABBR, ACVT, AELO, AFLK, AIVT, ALOS, APSV, ARTW, ASDI, ASNS, AWAD, Ags4Error, Ags4File, AgsGroup, AgsSubset, type AgsValue, BKFL, BadDictError, type BuildFinding, BuildResult, CBRG, CBRP, CBRT, CDIA, CHIS, CHOC, CMPG, CMPT, CONG, CONS, CORE, CPDG, CPDT, CPTG, CPTM, CPTP, CPTT, CPTY, CPTZ, CTRC, CTRD, CTRG, CTRP, CTRS, type CanonicalType, DCPG, DCPT, DETL, DICT, DISC, DLOG, DMDG, DMDT, DMTG, DMTP, DMTT, DMTZ, DOBS, DPRB, DPRG, DREM, ECTN, ELRG, ERES, ESCG, ESCT, type EmitOptions, FGHG, FGHI, FGHS, FGHT, FILE, FLSH, FRAC, FRST, FileNotFoundError, type Filter, type FixOptions, FixResult, GCHM, GEOL, GRAG, GRAT, type GroupData, GroupDescriptor, type GroupRows, HDIA, HDPH, HORN, type Heading, type HeadingStatus, ICBR, IDEN, IFID, IPEN, IPID, IPRG, IPRT, IRDX, IRES, ISAG, ISAT, ISPT, ISTA, ISTG, ISTR, ISTS, ITCH, IVAN, LBSG, LBST, LDEN, LDYN, LFCN, LLIN, LLPL, LNMC, LOCA, LPDN, LPEN, LRES, LSLT, LSTG, LSTT, LSWL, LTCH, LUCT, LVAN, MCVG, MCVT, MOND, MONG, MONS, NotAgs4Error, PIPE, PLTG, PLTT, PMMC, PMMD, PMMG, PMTD, PMTG, PMTL, PMTP, PMTZ, PREM, PROJ, PTIM, PTST, PUMG, PUMT, type QueryOptions, RCAG, RCAT, RCCV, RDEN, RELD, RESC, RESD, RESG, RESP, RESS, RPLT, RSCH, RSHR, RTEN, RUCS, RWCO, type ReadOptions, Report, type Row, type RuleFinding, type RuleMeta, type RuleObservation, SAMP, SCDG, SCDT, SCPG, SCPP, SCPT, SHBG, SHBT, STND, SUCT, TNPC, TRAN, TREG, TREM, TRET, TRIG, TRIT, TYPE, UNIT, UnsupportedEditionError, type ValidateOptions, WADD, WETH, WGPG, WGPT, WINS, WSTD, WSTG, agsTypes, buildAgs4, fix, listRules, read, registry, transport, validate };
5241
+ export { AAVT, ABBR, ACVT, AELO, AFLK, AIVT, ALOS, APSV, ARTW, ASDI, ASNS, AWAD, Ags4Error, Ags4File, AgsGroup, AgsSubset, type AgsValue, BKFL, BadDictError, type BuildFinding, BuildResult, CBRG, CBRP, CBRT, CDIA, CHIS, CHOC, CMPG, CMPT, CONG, CONS, CORE, CPDG, CPDT, CPTG, CPTM, CPTP, CPTT, CPTY, CPTZ, CTRC, CTRD, CTRG, CTRP, CTRS, type CanonicalType, type CellDelta, DCPG, DCPT, DETL, DICT, DISC, DLOG, DMDG, DMDT, DMTG, DMTP, DMTT, DMTZ, DOBS, DPRB, DPRG, DREM, type DiffOptions, type DiffSource, ECTN, ELRG, ERES, ESCG, ESCT, type EmitOptions, FGHG, FGHI, FGHS, FGHT, FILE, FLSH, FRAC, FRST, FileNotFoundError, type Filter, type FixOptions, FixResult, GCHM, GEOL, GRAG, GRAT, type GroupData, type GroupDelta, GroupDescriptor, type GroupRows, HDIA, HDPH, HORN, type Heading, type HeadingStatus, ICBR, IDEN, IFID, IPEN, IPID, IPRG, IPRT, IRDX, IRES, ISAG, ISAT, ISPT, ISTA, ISTG, ISTR, ISTS, ITCH, IVAN, LBSG, LBST, LDEN, LDYN, LFCN, LLIN, LLPL, LNMC, LOCA, LPDN, LPEN, LRES, LSLT, LSTG, LSTT, LSWL, LTCH, LUCT, LVAN, MCVG, MCVT, MOND, MONG, MONS, NotAgs4Error, PIPE, PLTG, PLTT, PMMC, PMMD, PMMG, PMTD, PMTG, PMTL, PMTP, PMTZ, PREM, PROJ, PTIM, PTST, PUMG, PUMT, type QueryOptions, RCAG, RCAT, RCCV, RDEN, RELD, RESC, RESD, RESG, RESP, RESS, RPLT, RSCH, RSHR, RTEN, RUCS, RWCO, type ReadOptions, Report, type RevisionDelta, type Row, type RowDelta, type RuleFinding, type RuleMeta, type RuleObservation, SAMP, SCDG, SCDT, SCPG, SCPP, SCPT, SHBG, SHBT, STND, SUCT, StaleCertError, TNPC, TRAN, TREG, TREM, TRET, TRIG, TRIT, TYPE, UNIT, UnsupportedEditionError, type ValidateOptions, WADD, WETH, WGPG, WGPT, WINS, WSTD, WSTG, agsTypes, buildAgs4, diff, fix, fromExcel, listRules, read, registry, toExcel, transport, validate };