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.cjs +424 -82
- package/dist/index.d.mts +604 -101
- package/dist/index.d.ts +604 -101
- package/dist/index.mjs +441 -98
- package/index.d.ts +270 -143
- package/index.js +549 -269
- package/package.json +15 -19
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,12 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// ts/index.ts
|
|
8
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
8
9
|
import { tableFromArrays, tableToIPC } from "apache-arrow";
|
|
9
10
|
|
|
10
11
|
// ts/ags4-file.ts
|
|
11
|
-
import { writeFileSync } from "fs";
|
|
12
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
12
14
|
import { tableFromIPC as tableFromIPC2 } from "apache-arrow";
|
|
13
15
|
|
|
14
16
|
// ts/duckdb.ts
|
|
@@ -48,6 +50,12 @@ var BadDictError = class extends Ags4Error {
|
|
|
48
50
|
this.name = "BadDictError";
|
|
49
51
|
}
|
|
50
52
|
};
|
|
53
|
+
var StaleCertError = class extends Ags4Error {
|
|
54
|
+
constructor(message, exitCode = 4) {
|
|
55
|
+
super(message, exitCode);
|
|
56
|
+
this.name = "StaleCertError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
51
59
|
function makeError(kind, exitCode, message) {
|
|
52
60
|
switch (kind) {
|
|
53
61
|
case "not_found":
|
|
@@ -83,6 +91,10 @@ function fromNativeError(e) {
|
|
|
83
91
|
function quoteId(name) {
|
|
84
92
|
return `"${name.replace(/"/g, '""')}"`;
|
|
85
93
|
}
|
|
94
|
+
function stripSynthKeys(table) {
|
|
95
|
+
const dataCols = table.schema.fields.filter((f) => !f.name.startsWith("_")).map((f) => f.name);
|
|
96
|
+
return dataCols.length === table.numCols ? table : table.select(dataCols);
|
|
97
|
+
}
|
|
86
98
|
var DUCK_PACKAGE = "@duckdb/node-api";
|
|
87
99
|
var duckMod = null;
|
|
88
100
|
function loadDuck() {
|
|
@@ -122,11 +134,19 @@ var DuckEngine = class _DuckEngine {
|
|
|
122
134
|
* rows appended from the born-typed arrow-js Table). Idempotent per code. */
|
|
123
135
|
async register(code, meta, table) {
|
|
124
136
|
if (this.registered.has(code)) return;
|
|
125
|
-
const
|
|
137
|
+
const keyCols = table.schema.fields.some((f) => f.name === "_id") ? ["_id", "_parent_id"] : [];
|
|
138
|
+
const cols = [
|
|
139
|
+
...keyCols.map((k) => `${quoteId(k)} VARCHAR`),
|
|
140
|
+
...meta.headings.map((h, i) => `${quoteId(h)} ${meta.sqlTypes[i]}`)
|
|
141
|
+
].join(", ");
|
|
126
142
|
await this.#con.run(`CREATE TABLE ${quoteId(code)} (${cols})`);
|
|
127
143
|
const appender = await this.#con.createAppender(code);
|
|
144
|
+
const keyVectors = keyCols.map((k) => table.getChild(k));
|
|
128
145
|
const vectors = meta.headings.map((h) => table.getChild(h));
|
|
129
146
|
for (let r = 0; r < table.numRows; r++) {
|
|
147
|
+
for (let c = 0; c < keyCols.length; c++) {
|
|
148
|
+
this.#appendCell(appender, "VARCHAR", keyVectors[c]?.get(r));
|
|
149
|
+
}
|
|
130
150
|
for (let c = 0; c < meta.headings.length; c++) {
|
|
131
151
|
this.#appendCell(appender, meta.sqlTypes[c] ?? "VARCHAR", vectors[c]?.get(r));
|
|
132
152
|
}
|
|
@@ -196,6 +216,100 @@ function toMicros(value) {
|
|
|
196
216
|
return BigInt(Math.round(ms * 1e3));
|
|
197
217
|
}
|
|
198
218
|
|
|
219
|
+
// ts/native.ts
|
|
220
|
+
import {
|
|
221
|
+
version,
|
|
222
|
+
parseArrow,
|
|
223
|
+
runCheck,
|
|
224
|
+
fixFile,
|
|
225
|
+
listRules,
|
|
226
|
+
registryDictionaryJson,
|
|
227
|
+
diff,
|
|
228
|
+
emitAgs4FromIpc,
|
|
229
|
+
Reading,
|
|
230
|
+
Sidecar,
|
|
231
|
+
canonicalType,
|
|
232
|
+
displayHint,
|
|
233
|
+
parseValue,
|
|
234
|
+
transportPack,
|
|
235
|
+
transportUnpack,
|
|
236
|
+
transportLock,
|
|
237
|
+
transportUnlock,
|
|
238
|
+
ags4ToExcel,
|
|
239
|
+
excelToAgs4
|
|
240
|
+
} from "#native";
|
|
241
|
+
|
|
242
|
+
// ts/report.ts
|
|
243
|
+
var Report = class _Report {
|
|
244
|
+
#r;
|
|
245
|
+
constructor(r) {
|
|
246
|
+
this.#r = r;
|
|
247
|
+
}
|
|
248
|
+
/** Synthesise a clean report from a fresh certificate — the engine-skipped
|
|
249
|
+
* outcome of `Ags4File.validate()` on an `index=`-certified file. `resolution`
|
|
250
|
+
* is the sentinel `"certified"` (the engine never emits it), `count` is 0, and
|
|
251
|
+
* the edition is the cert's. Mirrors laterite-py's `Report.from_cert`. */
|
|
252
|
+
static fromCert(cert, label) {
|
|
253
|
+
return new _Report({
|
|
254
|
+
ok: true,
|
|
255
|
+
exitCode: 0,
|
|
256
|
+
file: label,
|
|
257
|
+
dictVersion: cert.edition,
|
|
258
|
+
resolution: "certified",
|
|
259
|
+
count: 0,
|
|
260
|
+
findings: [],
|
|
261
|
+
json: JSON.stringify({ file: label, findings: {} }),
|
|
262
|
+
ndjson: ""
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
get file() {
|
|
266
|
+
return this.#r.file;
|
|
267
|
+
}
|
|
268
|
+
get dictVersion() {
|
|
269
|
+
return this.#r.dictVersion;
|
|
270
|
+
}
|
|
271
|
+
get resolution() {
|
|
272
|
+
return this.#r.resolution;
|
|
273
|
+
}
|
|
274
|
+
get count() {
|
|
275
|
+
return this.#r.count;
|
|
276
|
+
}
|
|
277
|
+
/** `true` iff there are zero findings (distinct from the native `ok`, which
|
|
278
|
+
* only means "validatable"). */
|
|
279
|
+
get isValid() {
|
|
280
|
+
return this.#r.count === 0;
|
|
281
|
+
}
|
|
282
|
+
get exitCode() {
|
|
283
|
+
return this.#r.exitCode;
|
|
284
|
+
}
|
|
285
|
+
/** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
|
|
286
|
+
get findings() {
|
|
287
|
+
return this.#r.findings;
|
|
288
|
+
}
|
|
289
|
+
/** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
|
|
290
|
+
* grouping (mirrors `Report.by_rule`). */
|
|
291
|
+
byRule() {
|
|
292
|
+
const out = {};
|
|
293
|
+
for (const f of this.#r.findings) {
|
|
294
|
+
const { rule, ...rest } = f;
|
|
295
|
+
(out[rule] ??= []).push(rest);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
|
|
300
|
+
toJson() {
|
|
301
|
+
return this.#r.json;
|
|
302
|
+
}
|
|
303
|
+
/** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
|
|
304
|
+
toNdjson() {
|
|
305
|
+
return this.#r.ndjson;
|
|
306
|
+
}
|
|
307
|
+
toString() {
|
|
308
|
+
const v = this.isValid ? "valid" : `${this.count} finding(s)`;
|
|
309
|
+
return `<Report ${JSON.stringify(this.file)} ${v} dict=${this.dictVersion}>`;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
199
313
|
// ts/subset.ts
|
|
200
314
|
var AgsSubset = class _AgsSubset {
|
|
201
315
|
#parent;
|
|
@@ -204,7 +318,16 @@ var AgsSubset = class _AgsSubset {
|
|
|
204
318
|
this.#parent = parent;
|
|
205
319
|
this.#filters = filters;
|
|
206
320
|
}
|
|
207
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* Narrow further by another entity's id (e.g. add a SAMP filter on top of a
|
|
323
|
+
* LOCA one). Returns a fresh subset — filters accumulate, they never mutate.
|
|
324
|
+
*
|
|
325
|
+
* @param group - The group code whose `_ID` key to filter on (`"SAMP"` becomes
|
|
326
|
+
* the `SAMP_ID` key column).
|
|
327
|
+
* @param values - The ids to keep; a row survives when its `<group>_ID` is one
|
|
328
|
+
* of these.
|
|
329
|
+
* @returns A new `AgsSubset` carrying this filter plus all the existing ones.
|
|
330
|
+
*/
|
|
208
331
|
at(group, values) {
|
|
209
332
|
return new _AgsSubset(this.#parent, [...this.#filters, [`${group}_ID`, [...values]]]);
|
|
210
333
|
}
|
|
@@ -226,17 +349,38 @@ var AgsSubset = class _AgsSubset {
|
|
|
226
349
|
};
|
|
227
350
|
|
|
228
351
|
// ts/ags4-file.ts
|
|
229
|
-
var Ags4File = class {
|
|
352
|
+
var Ags4File = class _Ags4File {
|
|
230
353
|
#reading;
|
|
231
|
-
// One arrow-js Table per group, decoded once
|
|
354
|
+
// One RAW keyed arrow-js Table per group, decoded once (with _id/_parent_id
|
|
355
|
+
// for known groups). The relational `#register` reads these.
|
|
232
356
|
#tables = /* @__PURE__ */ new Map();
|
|
357
|
+
// The key-stripped frame VIEW per group (a zero-copy projection of the raw
|
|
358
|
+
// Table), memoised so `table(code)` keeps returning the same instance.
|
|
359
|
+
#stripped = /* @__PURE__ */ new Map();
|
|
233
360
|
// The DuckDB engine — created lazily on first sql()/at()/connection.
|
|
234
361
|
#engine = null;
|
|
235
362
|
// Memoised AGS4 re-emit (the `text`/`bytes` getters; the emit is O(size)).
|
|
236
363
|
#text;
|
|
237
364
|
#bytes;
|
|
238
|
-
|
|
365
|
+
// The retained read source (for faithful chained re-runs) + the last verb
|
|
366
|
+
// outcomes: `#report` from `.validate()`, `#fixReport` on a `.fix()` result.
|
|
367
|
+
#src;
|
|
368
|
+
#report;
|
|
369
|
+
#fixReport;
|
|
370
|
+
// The `.ags.idx` certificate carried from `read(..., { index })`, and the check
|
|
371
|
+
// profile of the last `.validate()` engine run — so a following `.certify()`
|
|
372
|
+
// stamps what was actually checked (#294 Batch E / #14).
|
|
373
|
+
#cert;
|
|
374
|
+
#lastCheckFiles = false;
|
|
375
|
+
#lastForced = false;
|
|
376
|
+
constructor(reading, src) {
|
|
239
377
|
this.#reading = reading;
|
|
378
|
+
this.#src = src;
|
|
379
|
+
}
|
|
380
|
+
/** @internal — `read(..., { index })` attaches a freshness-checked certificate
|
|
381
|
+
* so a later errors-only `.validate()` can skip the rule engine. */
|
|
382
|
+
_attachCert(cert) {
|
|
383
|
+
this.#cert = cert;
|
|
240
384
|
}
|
|
241
385
|
// --- metadata (no Arrow decode) ------------------------------------------
|
|
242
386
|
/** Group codes in file order. */
|
|
@@ -252,12 +396,15 @@ var Ags4File = class {
|
|
|
252
396
|
if (m === null) throw new Error(`group ${JSON.stringify(code)} not in file`);
|
|
253
397
|
return m;
|
|
254
398
|
}
|
|
399
|
+
/** The HEADING-row codes of `code`, in file order. Throws if `code` isn't in the file. */
|
|
255
400
|
headings(code) {
|
|
256
401
|
return this.#meta(code).headings;
|
|
257
402
|
}
|
|
403
|
+
/** The UNIT row of `code`, one per heading. Throws if `code` isn't in the file. */
|
|
258
404
|
units(code) {
|
|
259
405
|
return this.#meta(code).units;
|
|
260
406
|
}
|
|
407
|
+
/** The TYPE row of `code` (the AGS data types), one per heading. Throws if `code` isn't in the file. */
|
|
261
408
|
types(code) {
|
|
262
409
|
return this.#meta(code).types;
|
|
263
410
|
}
|
|
@@ -269,15 +416,15 @@ var Ags4File = class {
|
|
|
269
416
|
lineNumbers(code) {
|
|
270
417
|
return this.#meta(code).lineNumbers;
|
|
271
418
|
}
|
|
419
|
+
/** Whether `code` is one of the file's groups — the cheap membership check `headings`/`table` would otherwise throw on. */
|
|
272
420
|
has(code) {
|
|
273
421
|
return this.#reading.meta(code) !== null;
|
|
274
422
|
}
|
|
275
423
|
// --- born-typed data (Arrow-direct) --------------------------------------
|
|
276
|
-
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
280
|
-
table(code) {
|
|
424
|
+
/** The raw keyed Table straight from IPC — a KNOWN group carries the two
|
|
425
|
+
* content-addressed key columns `_id`/`_parent_id` first (#303). Cached; both
|
|
426
|
+
* the frame accessor and the relational `#register` read it. */
|
|
427
|
+
#rawTable(code) {
|
|
281
428
|
const cached = this.#tables.get(code);
|
|
282
429
|
if (cached !== void 0) return cached;
|
|
283
430
|
const ipc = this.#reading.tableIpc(code);
|
|
@@ -286,6 +433,24 @@ var Ags4File = class {
|
|
|
286
433
|
this.#tables.set(code, table);
|
|
287
434
|
return table;
|
|
288
435
|
}
|
|
436
|
+
/** One group as a born-typed arrow-js `Table` (a 2DP heading is Float64, an ID
|
|
437
|
+
* Utf8, a DT a Timestamp) — the SAME typing the Python/wasm hosts produce,
|
|
438
|
+
* byte-identical by construction (one shared `build_record_batch`). Cached per
|
|
439
|
+
* group. Throws if `code` isn't in the file.
|
|
440
|
+
*
|
|
441
|
+
* By default the synthetic `_id`/`_parent_id` key columns are **dropped**; pass
|
|
442
|
+
* `{ keys: true }` to include them (the relational `sql()`/`at()` layer always
|
|
443
|
+
* carries them regardless — that's what makes cross-group joins work). */
|
|
444
|
+
table(code, opts) {
|
|
445
|
+
const raw = this.#rawTable(code);
|
|
446
|
+
if (opts?.keys) return raw;
|
|
447
|
+
let stripped = this.#stripped.get(code);
|
|
448
|
+
if (stripped === void 0) {
|
|
449
|
+
stripped = stripSynthKeys(raw);
|
|
450
|
+
this.#stripped.set(code, stripped);
|
|
451
|
+
}
|
|
452
|
+
return stripped;
|
|
453
|
+
}
|
|
289
454
|
// --- emit / save ---------------------------------------------------------
|
|
290
455
|
/** Spec-correct AGS4 as text — byte-faithful to the source DATA values
|
|
291
456
|
* (re-emitted native-side from the retained parse). Memoised. */
|
|
@@ -296,19 +461,165 @@ var Ags4File = class {
|
|
|
296
461
|
get bytes() {
|
|
297
462
|
return this.#bytes ??= Buffer.from(this.text, "utf8");
|
|
298
463
|
}
|
|
299
|
-
/** Write the AGS4 to `path` (UTF-8)
|
|
464
|
+
/** Write the AGS4 to `path` (UTF-8) — the inverse of `read`. The bytes are
|
|
465
|
+
* byte-faithful to the source DATA values, re-emitted from the retained parse.
|
|
466
|
+
*
|
|
467
|
+
* @param path Filesystem path to write the UTF-8 AGS4 to.
|
|
468
|
+
* @returns The same `path`, for chaining. */
|
|
300
469
|
save(path) {
|
|
301
470
|
writeFileSync(path, this.bytes);
|
|
302
471
|
return path;
|
|
303
472
|
}
|
|
473
|
+
// --- fluent verbs (validate / fix / diff) --------------------------------
|
|
474
|
+
/** Resolve this handle to the `(source, opts)` the free fns take: the retained
|
|
475
|
+
* read source (so line numbers match the original), or the re-emit for a
|
|
476
|
+
* synthesised handle. `text` is not re-encoded; a path/bytes source carries its
|
|
477
|
+
* read `encoding`. */
|
|
478
|
+
#freeSource() {
|
|
479
|
+
const s = this.#src;
|
|
480
|
+
if (s?.path !== void 0) return [s.path, { encoding: s.encoding }];
|
|
481
|
+
if (s?.text !== void 0) return [void 0, { text: s.text }];
|
|
482
|
+
if (s?.data !== void 0) return [s.data, { encoding: s.encoding }];
|
|
483
|
+
return [this.bytes, {}];
|
|
484
|
+
}
|
|
485
|
+
/** The last `.validate()` outcome (`undefined` until validated). */
|
|
486
|
+
get report() {
|
|
487
|
+
return this.#report;
|
|
488
|
+
}
|
|
489
|
+
/** The `FixResult` on a handle returned by `.fix()` (`undefined` otherwise). */
|
|
490
|
+
get fixReport() {
|
|
491
|
+
return this.#fixReport;
|
|
492
|
+
}
|
|
493
|
+
/** Validate this file against the AGS4 rules and return `this` (chainable —
|
|
494
|
+
* `read(p).validate().sql(...)`); the outcome lands on `.report`. Same engine as
|
|
495
|
+
* the free `validate()`, run on the source this handle was read from so line
|
|
496
|
+
* numbers match the original. Errors + WARNINGs by default (`warnings`); `fyi`
|
|
497
|
+
* adds the low-signal tier. `encoding` defaults to the one this handle was read
|
|
498
|
+
* with. Mirrors `laterite.Ags4File.validate()`. */
|
|
499
|
+
validate(opts = {}) {
|
|
500
|
+
const warnings = opts.warnings ?? true;
|
|
501
|
+
const fyi = opts.fyi ?? false;
|
|
502
|
+
const checkFiles = opts.checkFiles ?? false;
|
|
503
|
+
if (this.#cert !== void 0 && !warnings && !fyi && this.#cert.matchesNativeValidator() && this.#cert.profileCovers(checkFiles, opts.dictVersion)) {
|
|
504
|
+
this.#report = Report.fromCert(this.#cert, this.#srcLabel());
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
this.#lastCheckFiles = checkFiles;
|
|
508
|
+
this.#lastForced = opts.dictVersion !== void 0;
|
|
509
|
+
const [source, base] = this.#freeSource();
|
|
510
|
+
this.#report = validate(source, {
|
|
511
|
+
...base,
|
|
512
|
+
...opts,
|
|
513
|
+
encoding: opts.encoding ?? base.encoding
|
|
514
|
+
});
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
/** Mechanically repair this file and return a NEW, repaired `Ags4File` — the
|
|
518
|
+
* fluent transform, so `read(p).fix().validate().save(out)` reads as one chain.
|
|
519
|
+
* The `FixResult` (what was applied + residual findings) rides on the returned
|
|
520
|
+
* handle's `.fixReport`. Safe fixes always apply; `risky` adds the intent-
|
|
521
|
+
* guessing set. `encoding` defaults to this handle's read encoding. Non-
|
|
522
|
+
* destructive — the source on disk is untouched. Mirrors
|
|
523
|
+
* `laterite.Ags4File.fix()`. */
|
|
524
|
+
fix(opts = {}) {
|
|
525
|
+
const [source, base] = this.#freeSource();
|
|
526
|
+
const result = fix(source, { ...base, ...opts, encoding: opts.encoding ?? base.encoding });
|
|
527
|
+
const repaired = new _Ags4File(parseArrow(void 0, void 0, result.bytes, void 0), {
|
|
528
|
+
data: result.bytes
|
|
529
|
+
});
|
|
530
|
+
repaired.#fixReport = result;
|
|
531
|
+
return repaired;
|
|
532
|
+
}
|
|
533
|
+
/** Compare this file (the baseline) against `other` (the revision) — the
|
|
534
|
+
* `RevisionDelta` the free `diff()` returns. `other` is a path, bytes, or another
|
|
535
|
+
* `Ags4File`. `encoding` defaults to this handle's read encoding. Mirrors
|
|
536
|
+
* `laterite.Ags4File.diff()`. */
|
|
537
|
+
diff(other, opts = {}) {
|
|
538
|
+
return diff2(this, other, { ...opts, encoding: opts.encoding ?? this.#src?.encoding });
|
|
539
|
+
}
|
|
540
|
+
// --- certificate (.ags.idx) ----------------------------------------------
|
|
541
|
+
/** The ORIGINAL source bytes — what a certificate indexes + fingerprints, NOT
|
|
542
|
+
* the spec-correct re-emit (which can differ from a non-canonically-formatted
|
|
543
|
+
* on-disk file). A path is re-read, raw bytes returned as-is, text UTF-8-encoded;
|
|
544
|
+
* a synthesised handle falls back to the re-emit. Mirrors `_source_bytes`. */
|
|
545
|
+
#sourceBytes() {
|
|
546
|
+
const s = this.#src;
|
|
547
|
+
if (s?.path !== void 0) return readFileSync(s.path);
|
|
548
|
+
if (s?.data !== void 0) return s.data;
|
|
549
|
+
if (s?.text !== void 0) return Buffer.from(s.text, "utf8");
|
|
550
|
+
return this.bytes;
|
|
551
|
+
}
|
|
552
|
+
/** The `Report.fromCert` label for a certified verdict: the path, or a bytes/
|
|
553
|
+
* text sentinel. */
|
|
554
|
+
#srcLabel() {
|
|
555
|
+
const s = this.#src;
|
|
556
|
+
if (s?.path !== void 0) return s.path;
|
|
557
|
+
if (s?.data !== void 0) return "<bytes>";
|
|
558
|
+
return "<text>";
|
|
559
|
+
}
|
|
560
|
+
/** Mint this file's `.ags.idx` validity certificate — a clean-validation proof
|
|
561
|
+
* plus a byte-offset index — and write it beside the file. REQUIRES a prior
|
|
562
|
+
* clean `.validate()`: certify *vouches for* a passed validation, it does not run
|
|
563
|
+
* one. `path` is the certificate's OUTPUT location (default `<source>.idx`), not
|
|
564
|
+
* a file to certify — it refuses to overwrite the source or any existing
|
|
565
|
+
* non-certificate file. Returns the written path. The cert is byte-identical to
|
|
566
|
+
* Python's / `lat-check --emit-index`'s, so `lat-check` reads it, and a later
|
|
567
|
+
* `read(f, { index })` consumes it to skip re-validation. Mirrors
|
|
568
|
+
* `laterite.Ags4File.certify()`. */
|
|
569
|
+
certify(path) {
|
|
570
|
+
if (this.#report === void 0) {
|
|
571
|
+
throw new Ags4Error(
|
|
572
|
+
"call .validate() before .certify() \u2014 certify records a passed validation, it does not run one"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (!this.#report.isValid) {
|
|
576
|
+
throw new Ags4Error(
|
|
577
|
+
`cannot certify a file with ${this.#report.count} finding(s); fix them and re-validate clean first`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const srcPath = this.#src?.path;
|
|
581
|
+
const out = path ?? (srcPath !== void 0 ? `${srcPath}.idx` : void 0);
|
|
582
|
+
if (out === void 0) {
|
|
583
|
+
throw new Ags4Error(
|
|
584
|
+
"no source path to derive the .ags.idx location from; pass certify(path) for a handle read from text/bytes"
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
if (srcPath !== void 0 && resolve(out) === resolve(srcPath)) {
|
|
588
|
+
throw new Ags4Error(
|
|
589
|
+
`certify(path) is the .ags.idx OUTPUT location, not the file to certify \u2014 refusing to overwrite the source ${out}`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
if (existsSync(out) && statSync(out).size > 0) {
|
|
593
|
+
const head = readFileSync(out).subarray(0, 64).toString("utf8").trimStart();
|
|
594
|
+
if (!head.startsWith("{")) {
|
|
595
|
+
throw new Ags4Error(
|
|
596
|
+
`refusing to overwrite ${out}: it is not a laterite certificate (certify writes or replaces an .ags.idx)`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const cert = Sidecar.assemble(
|
|
601
|
+
this.#sourceBytes(),
|
|
602
|
+
this.#report.dictVersion,
|
|
603
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
604
|
+
void 0,
|
|
605
|
+
void 0,
|
|
606
|
+
void 0,
|
|
607
|
+
this.#lastCheckFiles,
|
|
608
|
+
this.#lastForced
|
|
609
|
+
);
|
|
610
|
+
writeFileSync(out, cert.toJson());
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
304
613
|
// --- optional DuckDB engine (sql / at / connection) ----------------------
|
|
305
614
|
async #getEngine() {
|
|
306
615
|
if (this.#engine === null) this.#engine = await DuckEngine.create();
|
|
307
616
|
return this.#engine;
|
|
308
617
|
}
|
|
309
|
-
/** Load one group into the engine on demand (CTAS from its born-typed Table).
|
|
618
|
+
/** Load one group into the engine on demand (CTAS from its born-typed Table).
|
|
619
|
+
* Uses the RAW keyed Table so the relational layer carries `_id`/`_parent_id`
|
|
620
|
+
* (the accessor strips them; the engine keeps them for joins). */
|
|
310
621
|
async #register(code, engine) {
|
|
311
|
-
await engine.register(code, this.#meta(code), this
|
|
622
|
+
await engine.register(code, this.#meta(code), this.#rawTable(code));
|
|
312
623
|
}
|
|
313
624
|
async #registerAll(engine) {
|
|
314
625
|
for (const code of this.groups) await this.#register(code, engine);
|
|
@@ -322,7 +633,11 @@ var Ags4File = class {
|
|
|
322
633
|
* returns a view whose `table(code)` yields only the rows whose `{group}_ID`
|
|
323
634
|
* is in `values`. Chain to narrow (`.at("SAMP", […])`); `sub.groups` is the
|
|
324
635
|
* related groups, `sub.frames()` pulls them all. Groups carrying none of the
|
|
325
|
-
* keys pass through. For any other predicate, use `sql("… WHERE …")`.
|
|
636
|
+
* keys pass through. For any other predicate, use `sql("… WHERE …")`.
|
|
637
|
+
*
|
|
638
|
+
* @param group The parent group whose `{group}_ID` key drives the filter.
|
|
639
|
+
* @param values The id values to keep (empty matches nothing).
|
|
640
|
+
* @returns An `AgsSubset` view; further `.at()` calls accumulate filters. */
|
|
326
641
|
at(group, values) {
|
|
327
642
|
return new AgsSubset(this, [[`${group}_ID`, [...values]]]);
|
|
328
643
|
}
|
|
@@ -353,7 +668,9 @@ var Ags4File = class {
|
|
|
353
668
|
}
|
|
354
669
|
}
|
|
355
670
|
const where = clauses.length > 0 ? clauses.join(" AND ") : "TRUE";
|
|
356
|
-
const
|
|
671
|
+
const keyed = this.#rawTable(code).schema.fields.some((f) => f.name === "_id");
|
|
672
|
+
const select = keyed ? "* EXCLUDE (_id, _parent_id)" : "*";
|
|
673
|
+
const sql = `SELECT ${select} FROM ${quoteId(code)} WHERE ${where}`;
|
|
357
674
|
return opts.arrow ? engine.queryArrow(sql, params) : engine.query(sql, params);
|
|
358
675
|
}
|
|
359
676
|
// --- lifecycle -----------------------------------------------------------
|
|
@@ -366,9 +683,11 @@ var Ags4File = class {
|
|
|
366
683
|
this.#engine = null;
|
|
367
684
|
}
|
|
368
685
|
}
|
|
686
|
+
/** `using f = read(…)` disposal hook — delegates to `close()`. */
|
|
369
687
|
[Symbol.dispose]() {
|
|
370
688
|
this.close();
|
|
371
689
|
}
|
|
690
|
+
/** A compact one-line summary — group count and `TRAN_AGS` — for logs and the REPL. */
|
|
372
691
|
toString() {
|
|
373
692
|
return `<Ags4File groups=${this.groups.length} tranAgs=${JSON.stringify(this.tranAgs)}>`;
|
|
374
693
|
}
|
|
@@ -377,13 +696,15 @@ var Ags4File = class {
|
|
|
377
696
|
// ts/build-result.ts
|
|
378
697
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
379
698
|
var BuildResult = class {
|
|
380
|
-
constructor(bytes, findings, fixesApplied) {
|
|
699
|
+
constructor(bytes, findings, applied, fixesApplied) {
|
|
381
700
|
this.bytes = bytes;
|
|
382
701
|
this.findings = findings;
|
|
702
|
+
this.applied = applied;
|
|
383
703
|
this.fixesApplied = fixesApplied;
|
|
384
704
|
}
|
|
385
705
|
bytes;
|
|
386
706
|
findings;
|
|
707
|
+
applied;
|
|
387
708
|
fixesApplied;
|
|
388
709
|
/** The AGS4 document decoded as text. */
|
|
389
710
|
get text() {
|
|
@@ -430,24 +751,6 @@ var FixResult = class {
|
|
|
430
751
|
}
|
|
431
752
|
};
|
|
432
753
|
|
|
433
|
-
// ts/native.ts
|
|
434
|
-
import {
|
|
435
|
-
version,
|
|
436
|
-
parseArrow,
|
|
437
|
-
runCheck,
|
|
438
|
-
fixFile,
|
|
439
|
-
listRules,
|
|
440
|
-
emitAgs4FromIpc,
|
|
441
|
-
Reading,
|
|
442
|
-
canonicalType,
|
|
443
|
-
displayHint,
|
|
444
|
-
parseValue,
|
|
445
|
-
transportPack,
|
|
446
|
-
transportUnpack,
|
|
447
|
-
transportLock,
|
|
448
|
-
transportUnlock
|
|
449
|
-
} from "#native";
|
|
450
|
-
|
|
451
754
|
// ts/registry.ts
|
|
452
755
|
var registry_exports = {};
|
|
453
756
|
__export(registry_exports, {
|
|
@@ -455,6 +758,7 @@ __export(registry_exports, {
|
|
|
455
758
|
GroupDescriptor: () => GroupDescriptor,
|
|
456
759
|
ancestorChain: () => ancestorChain,
|
|
457
760
|
childGroups: () => childGroups,
|
|
761
|
+
dictionary: () => dictionary,
|
|
458
762
|
get: () => get,
|
|
459
763
|
inheritedKeyNames: () => inheritedKeyNames,
|
|
460
764
|
isKeyStatus: () => isKeyStatus
|
|
@@ -26226,9 +26530,13 @@ var GroupDescriptor = class {
|
|
|
26226
26530
|
get view() {
|
|
26227
26531
|
return `v_${this.code.toLowerCase()}`;
|
|
26228
26532
|
}
|
|
26533
|
+
/** This group's KEY headings (status part-matched via {@link isKeyStatus}), in
|
|
26534
|
+
* declaration order. */
|
|
26229
26535
|
get keyHeadings() {
|
|
26230
26536
|
return this.headings.filter((h) => isKeyStatus(h.status));
|
|
26231
26537
|
}
|
|
26538
|
+
/** This group's non-KEY headings — the complement of {@link keyHeadings} — in
|
|
26539
|
+
* declaration order. */
|
|
26232
26540
|
get nonKeyHeadings() {
|
|
26233
26541
|
return this.headings.filter((h) => !isKeyStatus(h.status));
|
|
26234
26542
|
}
|
|
@@ -26239,6 +26547,9 @@ var GROUPS = Object.freeze(
|
|
|
26239
26547
|
function get(code) {
|
|
26240
26548
|
return GROUPS[code];
|
|
26241
26549
|
}
|
|
26550
|
+
function dictionary(edition) {
|
|
26551
|
+
return JSON.parse(registryDictionaryJson(edition));
|
|
26552
|
+
}
|
|
26242
26553
|
function childGroups(parentCode) {
|
|
26243
26554
|
return Object.values(GROUPS).filter((g) => g.parent === parentCode).sort((a, b) => a.code.localeCompare(b.code));
|
|
26244
26555
|
}
|
|
@@ -26255,67 +26566,21 @@ function ancestorChain(code) {
|
|
|
26255
26566
|
return chain;
|
|
26256
26567
|
}
|
|
26257
26568
|
function inheritedKeyNames(code) {
|
|
26569
|
+
const g = GROUPS[code];
|
|
26570
|
+
if (g === void 0) {
|
|
26571
|
+
throw new Ags4Error(`unknown group code: ${JSON.stringify(code)}`);
|
|
26572
|
+
}
|
|
26258
26573
|
const names = /* @__PURE__ */ new Set();
|
|
26259
|
-
|
|
26260
|
-
|
|
26574
|
+
if (g.parent === null) return names;
|
|
26575
|
+
const parent = GROUPS[g.parent];
|
|
26576
|
+
if (parent === void 0) return names;
|
|
26577
|
+
const parentKeys = new Set(parent.keyHeadings.map((h) => h.name));
|
|
26578
|
+
for (const h of g.keyHeadings) {
|
|
26579
|
+
if (parentKeys.has(h.name)) names.add(h.name);
|
|
26261
26580
|
}
|
|
26262
26581
|
return names;
|
|
26263
26582
|
}
|
|
26264
26583
|
|
|
26265
|
-
// ts/report.ts
|
|
26266
|
-
var Report = class {
|
|
26267
|
-
#r;
|
|
26268
|
-
constructor(r) {
|
|
26269
|
-
this.#r = r;
|
|
26270
|
-
}
|
|
26271
|
-
get file() {
|
|
26272
|
-
return this.#r.file;
|
|
26273
|
-
}
|
|
26274
|
-
get dictVersion() {
|
|
26275
|
-
return this.#r.dictVersion;
|
|
26276
|
-
}
|
|
26277
|
-
get resolution() {
|
|
26278
|
-
return this.#r.resolution;
|
|
26279
|
-
}
|
|
26280
|
-
get count() {
|
|
26281
|
-
return this.#r.count;
|
|
26282
|
-
}
|
|
26283
|
-
/** `true` iff there are zero findings (distinct from the native `ok`, which
|
|
26284
|
-
* only means "validatable"). */
|
|
26285
|
-
get isValid() {
|
|
26286
|
-
return this.#r.count === 0;
|
|
26287
|
-
}
|
|
26288
|
-
get exitCode() {
|
|
26289
|
-
return this.#r.exitCode;
|
|
26290
|
-
}
|
|
26291
|
-
/** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
|
|
26292
|
-
get findings() {
|
|
26293
|
-
return this.#r.findings;
|
|
26294
|
-
}
|
|
26295
|
-
/** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
|
|
26296
|
-
* grouping (mirrors `Report.by_rule`). */
|
|
26297
|
-
byRule() {
|
|
26298
|
-
const out = {};
|
|
26299
|
-
for (const f of this.#r.findings) {
|
|
26300
|
-
const { rule, ...rest } = f;
|
|
26301
|
-
(out[rule] ??= []).push(rest);
|
|
26302
|
-
}
|
|
26303
|
-
return out;
|
|
26304
|
-
}
|
|
26305
|
-
/** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
|
|
26306
|
-
toJson() {
|
|
26307
|
-
return this.#r.json;
|
|
26308
|
-
}
|
|
26309
|
-
/** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
|
|
26310
|
-
toNdjson() {
|
|
26311
|
-
return this.#r.ndjson;
|
|
26312
|
-
}
|
|
26313
|
-
toString() {
|
|
26314
|
-
const v = this.isValid ? "valid" : `${this.count} finding(s)`;
|
|
26315
|
-
return `<Report ${JSON.stringify(this.file)} ${v} dict=${this.dictVersion}>`;
|
|
26316
|
-
}
|
|
26317
|
-
};
|
|
26318
|
-
|
|
26319
26584
|
// ts/ags-group.ts
|
|
26320
26585
|
var AgsGroup = class {
|
|
26321
26586
|
};
|
|
@@ -31249,11 +31514,28 @@ function unlock(src, dest, password) {
|
|
|
31249
31514
|
function read(source, opts = {}) {
|
|
31250
31515
|
const path = typeof source === "string" ? source : void 0;
|
|
31251
31516
|
const data = typeof source === "string" || source == null ? void 0 : source;
|
|
31517
|
+
let handle;
|
|
31252
31518
|
try {
|
|
31253
|
-
|
|
31519
|
+
handle = new Ags4File(parseArrow(path, opts.text, data, opts.encoding), {
|
|
31520
|
+
path,
|
|
31521
|
+
text: opts.text,
|
|
31522
|
+
data,
|
|
31523
|
+
encoding: opts.encoding
|
|
31524
|
+
});
|
|
31254
31525
|
} catch (e) {
|
|
31255
31526
|
throw fromNativeError(e);
|
|
31256
31527
|
}
|
|
31528
|
+
if (opts.index !== void 0) {
|
|
31529
|
+
const cert = Sidecar.fromJson(readFileSync2(opts.index));
|
|
31530
|
+
const srcBytes = data ?? (path !== void 0 ? readFileSync2(path) : Buffer.from(opts.text ?? "", "utf8"));
|
|
31531
|
+
if (!cert.isFreshFor(srcBytes)) {
|
|
31532
|
+
throw new StaleCertError(
|
|
31533
|
+
`certificate ${opts.index} is stale for this file \u2014 its size / SHA-256 differ; rebuild it with read(...).validate().certify()`
|
|
31534
|
+
);
|
|
31535
|
+
}
|
|
31536
|
+
handle._attachCert(cert);
|
|
31537
|
+
}
|
|
31538
|
+
return handle;
|
|
31257
31539
|
}
|
|
31258
31540
|
function validate(source, opts = {}) {
|
|
31259
31541
|
const path = typeof source === "string" ? source : void 0;
|
|
@@ -31303,20 +31585,50 @@ function walkTree(root) {
|
|
|
31303
31585
|
}
|
|
31304
31586
|
};
|
|
31305
31587
|
visit(root);
|
|
31588
|
+
for (const [code, rows] of buckets) {
|
|
31589
|
+
const desc = get(code);
|
|
31590
|
+
for (const h of desc.headings) {
|
|
31591
|
+
if (isKeyStatus(h.status)) continue;
|
|
31592
|
+
if (rows.every((r) => r[h.name] == null)) {
|
|
31593
|
+
for (const r of rows) delete r[h.name];
|
|
31594
|
+
}
|
|
31595
|
+
}
|
|
31596
|
+
}
|
|
31306
31597
|
return [...buckets];
|
|
31307
31598
|
}
|
|
31599
|
+
function checkMeta(meta, name, columns) {
|
|
31600
|
+
if (meta === void 0) return;
|
|
31601
|
+
for (const [code, hmap] of Object.entries(meta)) {
|
|
31602
|
+
const known = columns.get(code);
|
|
31603
|
+
if (known === void 0) {
|
|
31604
|
+
throw new Ags4Error(`buildAgs4 ${name}: unknown group ${JSON.stringify(code)}`);
|
|
31605
|
+
}
|
|
31606
|
+
for (const heading of Object.keys(hmap)) {
|
|
31607
|
+
if (!known.has(heading)) {
|
|
31608
|
+
throw new Ags4Error(
|
|
31609
|
+
`buildAgs4 ${name}: group ${JSON.stringify(code)} has no heading ${JSON.stringify(heading)}`
|
|
31610
|
+
);
|
|
31611
|
+
}
|
|
31612
|
+
}
|
|
31613
|
+
}
|
|
31614
|
+
}
|
|
31308
31615
|
function buildAgs4(groups, opts = {}) {
|
|
31309
31616
|
const items = groups instanceof AgsGroup ? walkTree(groups) : groups instanceof Map ? [...groups] : groups;
|
|
31310
|
-
const ipcGroups =
|
|
31311
|
-
|
|
31312
|
-
|
|
31313
|
-
|
|
31314
|
-
|
|
31617
|
+
const ipcGroups = [];
|
|
31618
|
+
const columns = /* @__PURE__ */ new Map();
|
|
31619
|
+
for (const [code, data] of items) {
|
|
31620
|
+
const table = stripSynthKeys(Array.isArray(data) ? rowsToTable(data) : data);
|
|
31621
|
+
columns.set(code, new Set(table.schema.fields.map((f) => f.name)));
|
|
31622
|
+
ipcGroups.push({ code, ipc: Buffer.from(tableToIPC(table, "stream")) });
|
|
31623
|
+
}
|
|
31624
|
+
checkMeta(opts.units, "units", columns);
|
|
31625
|
+
checkMeta(opts.types, "types", columns);
|
|
31626
|
+
const res = emitAgs4FromIpc(ipcGroups, opts.dictVersion, opts.mode, opts.units, opts.types);
|
|
31315
31627
|
const byRule = JSON.parse(res.findingsJson);
|
|
31316
31628
|
const findings = Object.entries(byRule).flatMap(
|
|
31317
31629
|
([rule, list]) => list.map((f) => ({ rule, ...f }))
|
|
31318
31630
|
);
|
|
31319
|
-
return new BuildResult(res.bytes, findings, res.fixesApplied);
|
|
31631
|
+
return new BuildResult(res.bytes, findings, res.applied, res.fixesApplied);
|
|
31320
31632
|
}
|
|
31321
31633
|
function listRules2() {
|
|
31322
31634
|
return JSON.parse(listRules()).rules;
|
|
@@ -31328,6 +31640,33 @@ function fix(source, opts = {}) {
|
|
|
31328
31640
|
if (!r.ok) throw makeError(r.errorKind ?? "", r.exitCode, r.error ?? "unknown error");
|
|
31329
31641
|
return new FixResult(r.fixed, r.residual, r.applied, r.dictVersion);
|
|
31330
31642
|
}
|
|
31643
|
+
function diffBytes(x) {
|
|
31644
|
+
if (x instanceof Ags4File) return x.bytes;
|
|
31645
|
+
if (typeof x !== "string") return x;
|
|
31646
|
+
try {
|
|
31647
|
+
return readFileSync2(x);
|
|
31648
|
+
} catch (e) {
|
|
31649
|
+
if (e && typeof e === "object" && e.code === "ENOENT") {
|
|
31650
|
+
throw new FileNotFoundError(`No such file or directory: ${x}`, 3);
|
|
31651
|
+
}
|
|
31652
|
+
throw e;
|
|
31653
|
+
}
|
|
31654
|
+
}
|
|
31655
|
+
function diff2(a, b, opts = {}) {
|
|
31656
|
+
const aBytes = diffBytes(a);
|
|
31657
|
+
const bBytes = diffBytes(b);
|
|
31658
|
+
try {
|
|
31659
|
+
return JSON.parse(diff(aBytes, bBytes, opts.dictVersion, opts.encoding));
|
|
31660
|
+
} catch (e) {
|
|
31661
|
+
throw fromNativeError(e);
|
|
31662
|
+
}
|
|
31663
|
+
}
|
|
31664
|
+
function toExcel(agsPath, xlsxPath, opts = {}) {
|
|
31665
|
+
return ags4ToExcel(agsPath, xlsxPath, opts.groups);
|
|
31666
|
+
}
|
|
31667
|
+
function fromExcel(xlsxPath, agsPath, opts = {}) {
|
|
31668
|
+
return excelToAgs4(xlsxPath, agsPath, opts.formatNumericColumns);
|
|
31669
|
+
}
|
|
31331
31670
|
export {
|
|
31332
31671
|
AAVT,
|
|
31333
31672
|
ABBR,
|
|
@@ -31498,6 +31837,7 @@ export {
|
|
|
31498
31837
|
SHBT,
|
|
31499
31838
|
STND,
|
|
31500
31839
|
SUCT,
|
|
31840
|
+
StaleCertError,
|
|
31501
31841
|
TNPC,
|
|
31502
31842
|
TRAN,
|
|
31503
31843
|
TREG,
|
|
@@ -31517,10 +31857,13 @@ export {
|
|
|
31517
31857
|
WSTG,
|
|
31518
31858
|
ags_types_exports as agsTypes,
|
|
31519
31859
|
buildAgs4,
|
|
31860
|
+
diff2 as diff,
|
|
31520
31861
|
fix,
|
|
31862
|
+
fromExcel,
|
|
31521
31863
|
listRules2 as listRules,
|
|
31522
31864
|
read,
|
|
31523
31865
|
registry_exports as registry,
|
|
31866
|
+
toExcel,
|
|
31524
31867
|
transport_exports as transport,
|
|
31525
31868
|
validate,
|
|
31526
31869
|
version
|