laterite 0.5.0 → 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 -103
- package/dist/index.d.ts +604 -103
- 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.cjs
CHANGED
|
@@ -189,6 +189,7 @@ __export(index_exports, {
|
|
|
189
189
|
SHBT: () => SHBT,
|
|
190
190
|
STND: () => STND,
|
|
191
191
|
SUCT: () => SUCT,
|
|
192
|
+
StaleCertError: () => StaleCertError,
|
|
192
193
|
TNPC: () => TNPC,
|
|
193
194
|
TRAN: () => TRAN,
|
|
194
195
|
TREG: () => TREG,
|
|
@@ -208,19 +209,24 @@ __export(index_exports, {
|
|
|
208
209
|
WSTG: () => WSTG,
|
|
209
210
|
agsTypes: () => ags_types_exports,
|
|
210
211
|
buildAgs4: () => buildAgs4,
|
|
212
|
+
diff: () => diff2,
|
|
211
213
|
fix: () => fix,
|
|
214
|
+
fromExcel: () => fromExcel,
|
|
212
215
|
listRules: () => listRules2,
|
|
213
216
|
read: () => read,
|
|
214
217
|
registry: () => registry_exports,
|
|
218
|
+
toExcel: () => toExcel,
|
|
215
219
|
transport: () => transport_exports,
|
|
216
220
|
validate: () => validate,
|
|
217
221
|
version: () => import_native.version
|
|
218
222
|
});
|
|
219
223
|
module.exports = __toCommonJS(index_exports);
|
|
224
|
+
var import_node_fs4 = require("fs");
|
|
220
225
|
var import_apache_arrow3 = require("apache-arrow");
|
|
221
226
|
|
|
222
227
|
// ts/ags4-file.ts
|
|
223
228
|
var import_node_fs = require("fs");
|
|
229
|
+
var import_node_path = require("path");
|
|
224
230
|
var import_apache_arrow2 = require("apache-arrow");
|
|
225
231
|
|
|
226
232
|
// ts/duckdb.ts
|
|
@@ -260,6 +266,12 @@ var BadDictError = class extends Ags4Error {
|
|
|
260
266
|
this.name = "BadDictError";
|
|
261
267
|
}
|
|
262
268
|
};
|
|
269
|
+
var StaleCertError = class extends Ags4Error {
|
|
270
|
+
constructor(message, exitCode = 4) {
|
|
271
|
+
super(message, exitCode);
|
|
272
|
+
this.name = "StaleCertError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
263
275
|
function makeError(kind, exitCode, message) {
|
|
264
276
|
switch (kind) {
|
|
265
277
|
case "not_found":
|
|
@@ -295,6 +307,10 @@ function fromNativeError(e) {
|
|
|
295
307
|
function quoteId(name) {
|
|
296
308
|
return `"${name.replace(/"/g, '""')}"`;
|
|
297
309
|
}
|
|
310
|
+
function stripSynthKeys(table) {
|
|
311
|
+
const dataCols = table.schema.fields.filter((f) => !f.name.startsWith("_")).map((f) => f.name);
|
|
312
|
+
return dataCols.length === table.numCols ? table : table.select(dataCols);
|
|
313
|
+
}
|
|
298
314
|
var DUCK_PACKAGE = "@duckdb/node-api";
|
|
299
315
|
var duckMod = null;
|
|
300
316
|
function loadDuck() {
|
|
@@ -334,11 +350,19 @@ var DuckEngine = class _DuckEngine {
|
|
|
334
350
|
* rows appended from the born-typed arrow-js Table). Idempotent per code. */
|
|
335
351
|
async register(code, meta, table) {
|
|
336
352
|
if (this.registered.has(code)) return;
|
|
337
|
-
const
|
|
353
|
+
const keyCols = table.schema.fields.some((f) => f.name === "_id") ? ["_id", "_parent_id"] : [];
|
|
354
|
+
const cols = [
|
|
355
|
+
...keyCols.map((k) => `${quoteId(k)} VARCHAR`),
|
|
356
|
+
...meta.headings.map((h, i) => `${quoteId(h)} ${meta.sqlTypes[i]}`)
|
|
357
|
+
].join(", ");
|
|
338
358
|
await this.#con.run(`CREATE TABLE ${quoteId(code)} (${cols})`);
|
|
339
359
|
const appender = await this.#con.createAppender(code);
|
|
360
|
+
const keyVectors = keyCols.map((k) => table.getChild(k));
|
|
340
361
|
const vectors = meta.headings.map((h) => table.getChild(h));
|
|
341
362
|
for (let r = 0; r < table.numRows; r++) {
|
|
363
|
+
for (let c = 0; c < keyCols.length; c++) {
|
|
364
|
+
this.#appendCell(appender, "VARCHAR", keyVectors[c]?.get(r));
|
|
365
|
+
}
|
|
342
366
|
for (let c = 0; c < meta.headings.length; c++) {
|
|
343
367
|
this.#appendCell(appender, meta.sqlTypes[c] ?? "VARCHAR", vectors[c]?.get(r));
|
|
344
368
|
}
|
|
@@ -408,6 +432,80 @@ function toMicros(value) {
|
|
|
408
432
|
return BigInt(Math.round(ms * 1e3));
|
|
409
433
|
}
|
|
410
434
|
|
|
435
|
+
// ts/native.ts
|
|
436
|
+
var import_native = require("#native");
|
|
437
|
+
|
|
438
|
+
// ts/report.ts
|
|
439
|
+
var Report = class _Report {
|
|
440
|
+
#r;
|
|
441
|
+
constructor(r) {
|
|
442
|
+
this.#r = r;
|
|
443
|
+
}
|
|
444
|
+
/** Synthesise a clean report from a fresh certificate — the engine-skipped
|
|
445
|
+
* outcome of `Ags4File.validate()` on an `index=`-certified file. `resolution`
|
|
446
|
+
* is the sentinel `"certified"` (the engine never emits it), `count` is 0, and
|
|
447
|
+
* the edition is the cert's. Mirrors laterite-py's `Report.from_cert`. */
|
|
448
|
+
static fromCert(cert, label) {
|
|
449
|
+
return new _Report({
|
|
450
|
+
ok: true,
|
|
451
|
+
exitCode: 0,
|
|
452
|
+
file: label,
|
|
453
|
+
dictVersion: cert.edition,
|
|
454
|
+
resolution: "certified",
|
|
455
|
+
count: 0,
|
|
456
|
+
findings: [],
|
|
457
|
+
json: JSON.stringify({ file: label, findings: {} }),
|
|
458
|
+
ndjson: ""
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
get file() {
|
|
462
|
+
return this.#r.file;
|
|
463
|
+
}
|
|
464
|
+
get dictVersion() {
|
|
465
|
+
return this.#r.dictVersion;
|
|
466
|
+
}
|
|
467
|
+
get resolution() {
|
|
468
|
+
return this.#r.resolution;
|
|
469
|
+
}
|
|
470
|
+
get count() {
|
|
471
|
+
return this.#r.count;
|
|
472
|
+
}
|
|
473
|
+
/** `true` iff there are zero findings (distinct from the native `ok`, which
|
|
474
|
+
* only means "validatable"). */
|
|
475
|
+
get isValid() {
|
|
476
|
+
return this.#r.count === 0;
|
|
477
|
+
}
|
|
478
|
+
get exitCode() {
|
|
479
|
+
return this.#r.exitCode;
|
|
480
|
+
}
|
|
481
|
+
/** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
|
|
482
|
+
get findings() {
|
|
483
|
+
return this.#r.findings;
|
|
484
|
+
}
|
|
485
|
+
/** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
|
|
486
|
+
* grouping (mirrors `Report.by_rule`). */
|
|
487
|
+
byRule() {
|
|
488
|
+
const out = {};
|
|
489
|
+
for (const f of this.#r.findings) {
|
|
490
|
+
const { rule, ...rest } = f;
|
|
491
|
+
(out[rule] ??= []).push(rest);
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
/** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
|
|
496
|
+
toJson() {
|
|
497
|
+
return this.#r.json;
|
|
498
|
+
}
|
|
499
|
+
/** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
|
|
500
|
+
toNdjson() {
|
|
501
|
+
return this.#r.ndjson;
|
|
502
|
+
}
|
|
503
|
+
toString() {
|
|
504
|
+
const v = this.isValid ? "valid" : `${this.count} finding(s)`;
|
|
505
|
+
return `<Report ${JSON.stringify(this.file)} ${v} dict=${this.dictVersion}>`;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
411
509
|
// ts/subset.ts
|
|
412
510
|
var AgsSubset = class _AgsSubset {
|
|
413
511
|
#parent;
|
|
@@ -416,7 +514,16 @@ var AgsSubset = class _AgsSubset {
|
|
|
416
514
|
this.#parent = parent;
|
|
417
515
|
this.#filters = filters;
|
|
418
516
|
}
|
|
419
|
-
/**
|
|
517
|
+
/**
|
|
518
|
+
* Narrow further by another entity's id (e.g. add a SAMP filter on top of a
|
|
519
|
+
* LOCA one). Returns a fresh subset — filters accumulate, they never mutate.
|
|
520
|
+
*
|
|
521
|
+
* @param group - The group code whose `_ID` key to filter on (`"SAMP"` becomes
|
|
522
|
+
* the `SAMP_ID` key column).
|
|
523
|
+
* @param values - The ids to keep; a row survives when its `<group>_ID` is one
|
|
524
|
+
* of these.
|
|
525
|
+
* @returns A new `AgsSubset` carrying this filter plus all the existing ones.
|
|
526
|
+
*/
|
|
420
527
|
at(group, values) {
|
|
421
528
|
return new _AgsSubset(this.#parent, [...this.#filters, [`${group}_ID`, [...values]]]);
|
|
422
529
|
}
|
|
@@ -438,17 +545,38 @@ var AgsSubset = class _AgsSubset {
|
|
|
438
545
|
};
|
|
439
546
|
|
|
440
547
|
// ts/ags4-file.ts
|
|
441
|
-
var Ags4File = class {
|
|
548
|
+
var Ags4File = class _Ags4File {
|
|
442
549
|
#reading;
|
|
443
|
-
// One arrow-js Table per group, decoded once
|
|
550
|
+
// One RAW keyed arrow-js Table per group, decoded once (with _id/_parent_id
|
|
551
|
+
// for known groups). The relational `#register` reads these.
|
|
444
552
|
#tables = /* @__PURE__ */ new Map();
|
|
553
|
+
// The key-stripped frame VIEW per group (a zero-copy projection of the raw
|
|
554
|
+
// Table), memoised so `table(code)` keeps returning the same instance.
|
|
555
|
+
#stripped = /* @__PURE__ */ new Map();
|
|
445
556
|
// The DuckDB engine — created lazily on first sql()/at()/connection.
|
|
446
557
|
#engine = null;
|
|
447
558
|
// Memoised AGS4 re-emit (the `text`/`bytes` getters; the emit is O(size)).
|
|
448
559
|
#text;
|
|
449
560
|
#bytes;
|
|
450
|
-
|
|
561
|
+
// The retained read source (for faithful chained re-runs) + the last verb
|
|
562
|
+
// outcomes: `#report` from `.validate()`, `#fixReport` on a `.fix()` result.
|
|
563
|
+
#src;
|
|
564
|
+
#report;
|
|
565
|
+
#fixReport;
|
|
566
|
+
// The `.ags.idx` certificate carried from `read(..., { index })`, and the check
|
|
567
|
+
// profile of the last `.validate()` engine run — so a following `.certify()`
|
|
568
|
+
// stamps what was actually checked (#294 Batch E / #14).
|
|
569
|
+
#cert;
|
|
570
|
+
#lastCheckFiles = false;
|
|
571
|
+
#lastForced = false;
|
|
572
|
+
constructor(reading, src) {
|
|
451
573
|
this.#reading = reading;
|
|
574
|
+
this.#src = src;
|
|
575
|
+
}
|
|
576
|
+
/** @internal — `read(..., { index })` attaches a freshness-checked certificate
|
|
577
|
+
* so a later errors-only `.validate()` can skip the rule engine. */
|
|
578
|
+
_attachCert(cert) {
|
|
579
|
+
this.#cert = cert;
|
|
452
580
|
}
|
|
453
581
|
// --- metadata (no Arrow decode) ------------------------------------------
|
|
454
582
|
/** Group codes in file order. */
|
|
@@ -464,12 +592,15 @@ var Ags4File = class {
|
|
|
464
592
|
if (m === null) throw new Error(`group ${JSON.stringify(code)} not in file`);
|
|
465
593
|
return m;
|
|
466
594
|
}
|
|
595
|
+
/** The HEADING-row codes of `code`, in file order. Throws if `code` isn't in the file. */
|
|
467
596
|
headings(code) {
|
|
468
597
|
return this.#meta(code).headings;
|
|
469
598
|
}
|
|
599
|
+
/** The UNIT row of `code`, one per heading. Throws if `code` isn't in the file. */
|
|
470
600
|
units(code) {
|
|
471
601
|
return this.#meta(code).units;
|
|
472
602
|
}
|
|
603
|
+
/** The TYPE row of `code` (the AGS data types), one per heading. Throws if `code` isn't in the file. */
|
|
473
604
|
types(code) {
|
|
474
605
|
return this.#meta(code).types;
|
|
475
606
|
}
|
|
@@ -481,15 +612,15 @@ var Ags4File = class {
|
|
|
481
612
|
lineNumbers(code) {
|
|
482
613
|
return this.#meta(code).lineNumbers;
|
|
483
614
|
}
|
|
615
|
+
/** Whether `code` is one of the file's groups — the cheap membership check `headings`/`table` would otherwise throw on. */
|
|
484
616
|
has(code) {
|
|
485
617
|
return this.#reading.meta(code) !== null;
|
|
486
618
|
}
|
|
487
619
|
// --- born-typed data (Arrow-direct) --------------------------------------
|
|
488
|
-
/**
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
|
|
492
|
-
table(code) {
|
|
620
|
+
/** The raw keyed Table straight from IPC — a KNOWN group carries the two
|
|
621
|
+
* content-addressed key columns `_id`/`_parent_id` first (#303). Cached; both
|
|
622
|
+
* the frame accessor and the relational `#register` read it. */
|
|
623
|
+
#rawTable(code) {
|
|
493
624
|
const cached = this.#tables.get(code);
|
|
494
625
|
if (cached !== void 0) return cached;
|
|
495
626
|
const ipc = this.#reading.tableIpc(code);
|
|
@@ -498,6 +629,24 @@ var Ags4File = class {
|
|
|
498
629
|
this.#tables.set(code, table);
|
|
499
630
|
return table;
|
|
500
631
|
}
|
|
632
|
+
/** One group as a born-typed arrow-js `Table` (a 2DP heading is Float64, an ID
|
|
633
|
+
* Utf8, a DT a Timestamp) — the SAME typing the Python/wasm hosts produce,
|
|
634
|
+
* byte-identical by construction (one shared `build_record_batch`). Cached per
|
|
635
|
+
* group. Throws if `code` isn't in the file.
|
|
636
|
+
*
|
|
637
|
+
* By default the synthetic `_id`/`_parent_id` key columns are **dropped**; pass
|
|
638
|
+
* `{ keys: true }` to include them (the relational `sql()`/`at()` layer always
|
|
639
|
+
* carries them regardless — that's what makes cross-group joins work). */
|
|
640
|
+
table(code, opts) {
|
|
641
|
+
const raw = this.#rawTable(code);
|
|
642
|
+
if (opts?.keys) return raw;
|
|
643
|
+
let stripped = this.#stripped.get(code);
|
|
644
|
+
if (stripped === void 0) {
|
|
645
|
+
stripped = stripSynthKeys(raw);
|
|
646
|
+
this.#stripped.set(code, stripped);
|
|
647
|
+
}
|
|
648
|
+
return stripped;
|
|
649
|
+
}
|
|
501
650
|
// --- emit / save ---------------------------------------------------------
|
|
502
651
|
/** Spec-correct AGS4 as text — byte-faithful to the source DATA values
|
|
503
652
|
* (re-emitted native-side from the retained parse). Memoised. */
|
|
@@ -508,19 +657,165 @@ var Ags4File = class {
|
|
|
508
657
|
get bytes() {
|
|
509
658
|
return this.#bytes ??= Buffer.from(this.text, "utf8");
|
|
510
659
|
}
|
|
511
|
-
/** Write the AGS4 to `path` (UTF-8)
|
|
660
|
+
/** Write the AGS4 to `path` (UTF-8) — the inverse of `read`. The bytes are
|
|
661
|
+
* byte-faithful to the source DATA values, re-emitted from the retained parse.
|
|
662
|
+
*
|
|
663
|
+
* @param path Filesystem path to write the UTF-8 AGS4 to.
|
|
664
|
+
* @returns The same `path`, for chaining. */
|
|
512
665
|
save(path) {
|
|
513
666
|
(0, import_node_fs.writeFileSync)(path, this.bytes);
|
|
514
667
|
return path;
|
|
515
668
|
}
|
|
669
|
+
// --- fluent verbs (validate / fix / diff) --------------------------------
|
|
670
|
+
/** Resolve this handle to the `(source, opts)` the free fns take: the retained
|
|
671
|
+
* read source (so line numbers match the original), or the re-emit for a
|
|
672
|
+
* synthesised handle. `text` is not re-encoded; a path/bytes source carries its
|
|
673
|
+
* read `encoding`. */
|
|
674
|
+
#freeSource() {
|
|
675
|
+
const s = this.#src;
|
|
676
|
+
if (s?.path !== void 0) return [s.path, { encoding: s.encoding }];
|
|
677
|
+
if (s?.text !== void 0) return [void 0, { text: s.text }];
|
|
678
|
+
if (s?.data !== void 0) return [s.data, { encoding: s.encoding }];
|
|
679
|
+
return [this.bytes, {}];
|
|
680
|
+
}
|
|
681
|
+
/** The last `.validate()` outcome (`undefined` until validated). */
|
|
682
|
+
get report() {
|
|
683
|
+
return this.#report;
|
|
684
|
+
}
|
|
685
|
+
/** The `FixResult` on a handle returned by `.fix()` (`undefined` otherwise). */
|
|
686
|
+
get fixReport() {
|
|
687
|
+
return this.#fixReport;
|
|
688
|
+
}
|
|
689
|
+
/** Validate this file against the AGS4 rules and return `this` (chainable —
|
|
690
|
+
* `read(p).validate().sql(...)`); the outcome lands on `.report`. Same engine as
|
|
691
|
+
* the free `validate()`, run on the source this handle was read from so line
|
|
692
|
+
* numbers match the original. Errors + WARNINGs by default (`warnings`); `fyi`
|
|
693
|
+
* adds the low-signal tier. `encoding` defaults to the one this handle was read
|
|
694
|
+
* with. Mirrors `laterite.Ags4File.validate()`. */
|
|
695
|
+
validate(opts = {}) {
|
|
696
|
+
const warnings = opts.warnings ?? true;
|
|
697
|
+
const fyi = opts.fyi ?? false;
|
|
698
|
+
const checkFiles = opts.checkFiles ?? false;
|
|
699
|
+
if (this.#cert !== void 0 && !warnings && !fyi && this.#cert.matchesNativeValidator() && this.#cert.profileCovers(checkFiles, opts.dictVersion)) {
|
|
700
|
+
this.#report = Report.fromCert(this.#cert, this.#srcLabel());
|
|
701
|
+
return this;
|
|
702
|
+
}
|
|
703
|
+
this.#lastCheckFiles = checkFiles;
|
|
704
|
+
this.#lastForced = opts.dictVersion !== void 0;
|
|
705
|
+
const [source, base] = this.#freeSource();
|
|
706
|
+
this.#report = validate(source, {
|
|
707
|
+
...base,
|
|
708
|
+
...opts,
|
|
709
|
+
encoding: opts.encoding ?? base.encoding
|
|
710
|
+
});
|
|
711
|
+
return this;
|
|
712
|
+
}
|
|
713
|
+
/** Mechanically repair this file and return a NEW, repaired `Ags4File` — the
|
|
714
|
+
* fluent transform, so `read(p).fix().validate().save(out)` reads as one chain.
|
|
715
|
+
* The `FixResult` (what was applied + residual findings) rides on the returned
|
|
716
|
+
* handle's `.fixReport`. Safe fixes always apply; `risky` adds the intent-
|
|
717
|
+
* guessing set. `encoding` defaults to this handle's read encoding. Non-
|
|
718
|
+
* destructive — the source on disk is untouched. Mirrors
|
|
719
|
+
* `laterite.Ags4File.fix()`. */
|
|
720
|
+
fix(opts = {}) {
|
|
721
|
+
const [source, base] = this.#freeSource();
|
|
722
|
+
const result = fix(source, { ...base, ...opts, encoding: opts.encoding ?? base.encoding });
|
|
723
|
+
const repaired = new _Ags4File((0, import_native.parseArrow)(void 0, void 0, result.bytes, void 0), {
|
|
724
|
+
data: result.bytes
|
|
725
|
+
});
|
|
726
|
+
repaired.#fixReport = result;
|
|
727
|
+
return repaired;
|
|
728
|
+
}
|
|
729
|
+
/** Compare this file (the baseline) against `other` (the revision) — the
|
|
730
|
+
* `RevisionDelta` the free `diff()` returns. `other` is a path, bytes, or another
|
|
731
|
+
* `Ags4File`. `encoding` defaults to this handle's read encoding. Mirrors
|
|
732
|
+
* `laterite.Ags4File.diff()`. */
|
|
733
|
+
diff(other, opts = {}) {
|
|
734
|
+
return diff2(this, other, { ...opts, encoding: opts.encoding ?? this.#src?.encoding });
|
|
735
|
+
}
|
|
736
|
+
// --- certificate (.ags.idx) ----------------------------------------------
|
|
737
|
+
/** The ORIGINAL source bytes — what a certificate indexes + fingerprints, NOT
|
|
738
|
+
* the spec-correct re-emit (which can differ from a non-canonically-formatted
|
|
739
|
+
* on-disk file). A path is re-read, raw bytes returned as-is, text UTF-8-encoded;
|
|
740
|
+
* a synthesised handle falls back to the re-emit. Mirrors `_source_bytes`. */
|
|
741
|
+
#sourceBytes() {
|
|
742
|
+
const s = this.#src;
|
|
743
|
+
if (s?.path !== void 0) return (0, import_node_fs.readFileSync)(s.path);
|
|
744
|
+
if (s?.data !== void 0) return s.data;
|
|
745
|
+
if (s?.text !== void 0) return Buffer.from(s.text, "utf8");
|
|
746
|
+
return this.bytes;
|
|
747
|
+
}
|
|
748
|
+
/** The `Report.fromCert` label for a certified verdict: the path, or a bytes/
|
|
749
|
+
* text sentinel. */
|
|
750
|
+
#srcLabel() {
|
|
751
|
+
const s = this.#src;
|
|
752
|
+
if (s?.path !== void 0) return s.path;
|
|
753
|
+
if (s?.data !== void 0) return "<bytes>";
|
|
754
|
+
return "<text>";
|
|
755
|
+
}
|
|
756
|
+
/** Mint this file's `.ags.idx` validity certificate — a clean-validation proof
|
|
757
|
+
* plus a byte-offset index — and write it beside the file. REQUIRES a prior
|
|
758
|
+
* clean `.validate()`: certify *vouches for* a passed validation, it does not run
|
|
759
|
+
* one. `path` is the certificate's OUTPUT location (default `<source>.idx`), not
|
|
760
|
+
* a file to certify — it refuses to overwrite the source or any existing
|
|
761
|
+
* non-certificate file. Returns the written path. The cert is byte-identical to
|
|
762
|
+
* Python's / `lat-check --emit-index`'s, so `lat-check` reads it, and a later
|
|
763
|
+
* `read(f, { index })` consumes it to skip re-validation. Mirrors
|
|
764
|
+
* `laterite.Ags4File.certify()`. */
|
|
765
|
+
certify(path) {
|
|
766
|
+
if (this.#report === void 0) {
|
|
767
|
+
throw new Ags4Error(
|
|
768
|
+
"call .validate() before .certify() \u2014 certify records a passed validation, it does not run one"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (!this.#report.isValid) {
|
|
772
|
+
throw new Ags4Error(
|
|
773
|
+
`cannot certify a file with ${this.#report.count} finding(s); fix them and re-validate clean first`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const srcPath = this.#src?.path;
|
|
777
|
+
const out = path ?? (srcPath !== void 0 ? `${srcPath}.idx` : void 0);
|
|
778
|
+
if (out === void 0) {
|
|
779
|
+
throw new Ags4Error(
|
|
780
|
+
"no source path to derive the .ags.idx location from; pass certify(path) for a handle read from text/bytes"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (srcPath !== void 0 && (0, import_node_path.resolve)(out) === (0, import_node_path.resolve)(srcPath)) {
|
|
784
|
+
throw new Ags4Error(
|
|
785
|
+
`certify(path) is the .ags.idx OUTPUT location, not the file to certify \u2014 refusing to overwrite the source ${out}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if ((0, import_node_fs.existsSync)(out) && (0, import_node_fs.statSync)(out).size > 0) {
|
|
789
|
+
const head = (0, import_node_fs.readFileSync)(out).subarray(0, 64).toString("utf8").trimStart();
|
|
790
|
+
if (!head.startsWith("{")) {
|
|
791
|
+
throw new Ags4Error(
|
|
792
|
+
`refusing to overwrite ${out}: it is not a laterite certificate (certify writes or replaces an .ags.idx)`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const cert = import_native.Sidecar.assemble(
|
|
797
|
+
this.#sourceBytes(),
|
|
798
|
+
this.#report.dictVersion,
|
|
799
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
void 0,
|
|
801
|
+
void 0,
|
|
802
|
+
void 0,
|
|
803
|
+
this.#lastCheckFiles,
|
|
804
|
+
this.#lastForced
|
|
805
|
+
);
|
|
806
|
+
(0, import_node_fs.writeFileSync)(out, cert.toJson());
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
516
809
|
// --- optional DuckDB engine (sql / at / connection) ----------------------
|
|
517
810
|
async #getEngine() {
|
|
518
811
|
if (this.#engine === null) this.#engine = await DuckEngine.create();
|
|
519
812
|
return this.#engine;
|
|
520
813
|
}
|
|
521
|
-
/** Load one group into the engine on demand (CTAS from its born-typed Table).
|
|
814
|
+
/** Load one group into the engine on demand (CTAS from its born-typed Table).
|
|
815
|
+
* Uses the RAW keyed Table so the relational layer carries `_id`/`_parent_id`
|
|
816
|
+
* (the accessor strips them; the engine keeps them for joins). */
|
|
522
817
|
async #register(code, engine) {
|
|
523
|
-
await engine.register(code, this.#meta(code), this
|
|
818
|
+
await engine.register(code, this.#meta(code), this.#rawTable(code));
|
|
524
819
|
}
|
|
525
820
|
async #registerAll(engine) {
|
|
526
821
|
for (const code of this.groups) await this.#register(code, engine);
|
|
@@ -534,7 +829,11 @@ var Ags4File = class {
|
|
|
534
829
|
* returns a view whose `table(code)` yields only the rows whose `{group}_ID`
|
|
535
830
|
* is in `values`. Chain to narrow (`.at("SAMP", […])`); `sub.groups` is the
|
|
536
831
|
* related groups, `sub.frames()` pulls them all. Groups carrying none of the
|
|
537
|
-
* keys pass through. For any other predicate, use `sql("… WHERE …")`.
|
|
832
|
+
* keys pass through. For any other predicate, use `sql("… WHERE …")`.
|
|
833
|
+
*
|
|
834
|
+
* @param group The parent group whose `{group}_ID` key drives the filter.
|
|
835
|
+
* @param values The id values to keep (empty matches nothing).
|
|
836
|
+
* @returns An `AgsSubset` view; further `.at()` calls accumulate filters. */
|
|
538
837
|
at(group, values) {
|
|
539
838
|
return new AgsSubset(this, [[`${group}_ID`, [...values]]]);
|
|
540
839
|
}
|
|
@@ -565,7 +864,9 @@ var Ags4File = class {
|
|
|
565
864
|
}
|
|
566
865
|
}
|
|
567
866
|
const where = clauses.length > 0 ? clauses.join(" AND ") : "TRUE";
|
|
568
|
-
const
|
|
867
|
+
const keyed = this.#rawTable(code).schema.fields.some((f) => f.name === "_id");
|
|
868
|
+
const select = keyed ? "* EXCLUDE (_id, _parent_id)" : "*";
|
|
869
|
+
const sql = `SELECT ${select} FROM ${quoteId(code)} WHERE ${where}`;
|
|
569
870
|
return opts.arrow ? engine.queryArrow(sql, params) : engine.query(sql, params);
|
|
570
871
|
}
|
|
571
872
|
// --- lifecycle -----------------------------------------------------------
|
|
@@ -578,9 +879,11 @@ var Ags4File = class {
|
|
|
578
879
|
this.#engine = null;
|
|
579
880
|
}
|
|
580
881
|
}
|
|
882
|
+
/** `using f = read(…)` disposal hook — delegates to `close()`. */
|
|
581
883
|
[Symbol.dispose]() {
|
|
582
884
|
this.close();
|
|
583
885
|
}
|
|
886
|
+
/** A compact one-line summary — group count and `TRAN_AGS` — for logs and the REPL. */
|
|
584
887
|
toString() {
|
|
585
888
|
return `<Ags4File groups=${this.groups.length} tranAgs=${JSON.stringify(this.tranAgs)}>`;
|
|
586
889
|
}
|
|
@@ -589,13 +892,15 @@ var Ags4File = class {
|
|
|
589
892
|
// ts/build-result.ts
|
|
590
893
|
var import_node_fs2 = require("fs");
|
|
591
894
|
var BuildResult = class {
|
|
592
|
-
constructor(bytes, findings, fixesApplied) {
|
|
895
|
+
constructor(bytes, findings, applied, fixesApplied) {
|
|
593
896
|
this.bytes = bytes;
|
|
594
897
|
this.findings = findings;
|
|
898
|
+
this.applied = applied;
|
|
595
899
|
this.fixesApplied = fixesApplied;
|
|
596
900
|
}
|
|
597
901
|
bytes;
|
|
598
902
|
findings;
|
|
903
|
+
applied;
|
|
599
904
|
fixesApplied;
|
|
600
905
|
/** The AGS4 document decoded as text. */
|
|
601
906
|
get text() {
|
|
@@ -642,9 +947,6 @@ var FixResult = class {
|
|
|
642
947
|
}
|
|
643
948
|
};
|
|
644
949
|
|
|
645
|
-
// ts/native.ts
|
|
646
|
-
var import_native = require("#native");
|
|
647
|
-
|
|
648
950
|
// ts/registry.ts
|
|
649
951
|
var registry_exports = {};
|
|
650
952
|
__export(registry_exports, {
|
|
@@ -652,6 +954,7 @@ __export(registry_exports, {
|
|
|
652
954
|
GroupDescriptor: () => GroupDescriptor,
|
|
653
955
|
ancestorChain: () => ancestorChain,
|
|
654
956
|
childGroups: () => childGroups,
|
|
957
|
+
dictionary: () => dictionary,
|
|
655
958
|
get: () => get,
|
|
656
959
|
inheritedKeyNames: () => inheritedKeyNames,
|
|
657
960
|
isKeyStatus: () => isKeyStatus
|
|
@@ -26423,9 +26726,13 @@ var GroupDescriptor = class {
|
|
|
26423
26726
|
get view() {
|
|
26424
26727
|
return `v_${this.code.toLowerCase()}`;
|
|
26425
26728
|
}
|
|
26729
|
+
/** This group's KEY headings (status part-matched via {@link isKeyStatus}), in
|
|
26730
|
+
* declaration order. */
|
|
26426
26731
|
get keyHeadings() {
|
|
26427
26732
|
return this.headings.filter((h) => isKeyStatus(h.status));
|
|
26428
26733
|
}
|
|
26734
|
+
/** This group's non-KEY headings — the complement of {@link keyHeadings} — in
|
|
26735
|
+
* declaration order. */
|
|
26429
26736
|
get nonKeyHeadings() {
|
|
26430
26737
|
return this.headings.filter((h) => !isKeyStatus(h.status));
|
|
26431
26738
|
}
|
|
@@ -26436,6 +26743,9 @@ var GROUPS = Object.freeze(
|
|
|
26436
26743
|
function get(code) {
|
|
26437
26744
|
return GROUPS[code];
|
|
26438
26745
|
}
|
|
26746
|
+
function dictionary(edition) {
|
|
26747
|
+
return JSON.parse((0, import_native.registryDictionaryJson)(edition));
|
|
26748
|
+
}
|
|
26439
26749
|
function childGroups(parentCode) {
|
|
26440
26750
|
return Object.values(GROUPS).filter((g) => g.parent === parentCode).sort((a, b) => a.code.localeCompare(b.code));
|
|
26441
26751
|
}
|
|
@@ -26452,67 +26762,21 @@ function ancestorChain(code) {
|
|
|
26452
26762
|
return chain;
|
|
26453
26763
|
}
|
|
26454
26764
|
function inheritedKeyNames(code) {
|
|
26765
|
+
const g = GROUPS[code];
|
|
26766
|
+
if (g === void 0) {
|
|
26767
|
+
throw new Ags4Error(`unknown group code: ${JSON.stringify(code)}`);
|
|
26768
|
+
}
|
|
26455
26769
|
const names = /* @__PURE__ */ new Set();
|
|
26456
|
-
|
|
26457
|
-
|
|
26770
|
+
if (g.parent === null) return names;
|
|
26771
|
+
const parent = GROUPS[g.parent];
|
|
26772
|
+
if (parent === void 0) return names;
|
|
26773
|
+
const parentKeys = new Set(parent.keyHeadings.map((h) => h.name));
|
|
26774
|
+
for (const h of g.keyHeadings) {
|
|
26775
|
+
if (parentKeys.has(h.name)) names.add(h.name);
|
|
26458
26776
|
}
|
|
26459
26777
|
return names;
|
|
26460
26778
|
}
|
|
26461
26779
|
|
|
26462
|
-
// ts/report.ts
|
|
26463
|
-
var Report = class {
|
|
26464
|
-
#r;
|
|
26465
|
-
constructor(r) {
|
|
26466
|
-
this.#r = r;
|
|
26467
|
-
}
|
|
26468
|
-
get file() {
|
|
26469
|
-
return this.#r.file;
|
|
26470
|
-
}
|
|
26471
|
-
get dictVersion() {
|
|
26472
|
-
return this.#r.dictVersion;
|
|
26473
|
-
}
|
|
26474
|
-
get resolution() {
|
|
26475
|
-
return this.#r.resolution;
|
|
26476
|
-
}
|
|
26477
|
-
get count() {
|
|
26478
|
-
return this.#r.count;
|
|
26479
|
-
}
|
|
26480
|
-
/** `true` iff there are zero findings (distinct from the native `ok`, which
|
|
26481
|
-
* only means "validatable"). */
|
|
26482
|
-
get isValid() {
|
|
26483
|
-
return this.#r.count === 0;
|
|
26484
|
-
}
|
|
26485
|
-
get exitCode() {
|
|
26486
|
-
return this.#r.exitCode;
|
|
26487
|
-
}
|
|
26488
|
-
/** All findings, in `lat-check` order: `{rule, line?, group, desc, severity?}`. */
|
|
26489
|
-
get findings() {
|
|
26490
|
-
return this.#r.findings;
|
|
26491
|
-
}
|
|
26492
|
-
/** `{ "AGS Format Rule N": [{line?, group, desc, …}] }` — the spec-rule
|
|
26493
|
-
* grouping (mirrors `Report.by_rule`). */
|
|
26494
|
-
byRule() {
|
|
26495
|
-
const out = {};
|
|
26496
|
-
for (const f of this.#r.findings) {
|
|
26497
|
-
const { rule, ...rest } = f;
|
|
26498
|
-
(out[rule] ??= []).push(rest);
|
|
26499
|
-
}
|
|
26500
|
-
return out;
|
|
26501
|
-
}
|
|
26502
|
-
/** `{file, findings:{…}}` pretty-JSON — byte-identical to `lat-check --json`. */
|
|
26503
|
-
toJson() {
|
|
26504
|
-
return this.#r.json;
|
|
26505
|
-
}
|
|
26506
|
-
/** One flat `{rule, …}` per line — byte-identical to `lat-check --ndjson`. */
|
|
26507
|
-
toNdjson() {
|
|
26508
|
-
return this.#r.ndjson;
|
|
26509
|
-
}
|
|
26510
|
-
toString() {
|
|
26511
|
-
const v = this.isValid ? "valid" : `${this.count} finding(s)`;
|
|
26512
|
-
return `<Report ${JSON.stringify(this.file)} ${v} dict=${this.dictVersion}>`;
|
|
26513
|
-
}
|
|
26514
|
-
};
|
|
26515
|
-
|
|
26516
26780
|
// ts/ags-group.ts
|
|
26517
26781
|
var AgsGroup = class {
|
|
26518
26782
|
};
|
|
@@ -31446,11 +31710,28 @@ function unlock(src, dest, password) {
|
|
|
31446
31710
|
function read(source, opts = {}) {
|
|
31447
31711
|
const path = typeof source === "string" ? source : void 0;
|
|
31448
31712
|
const data = typeof source === "string" || source == null ? void 0 : source;
|
|
31713
|
+
let handle;
|
|
31449
31714
|
try {
|
|
31450
|
-
|
|
31715
|
+
handle = new Ags4File((0, import_native.parseArrow)(path, opts.text, data, opts.encoding), {
|
|
31716
|
+
path,
|
|
31717
|
+
text: opts.text,
|
|
31718
|
+
data,
|
|
31719
|
+
encoding: opts.encoding
|
|
31720
|
+
});
|
|
31451
31721
|
} catch (e) {
|
|
31452
31722
|
throw fromNativeError(e);
|
|
31453
31723
|
}
|
|
31724
|
+
if (opts.index !== void 0) {
|
|
31725
|
+
const cert = import_native.Sidecar.fromJson((0, import_node_fs4.readFileSync)(opts.index));
|
|
31726
|
+
const srcBytes = data ?? (path !== void 0 ? (0, import_node_fs4.readFileSync)(path) : Buffer.from(opts.text ?? "", "utf8"));
|
|
31727
|
+
if (!cert.isFreshFor(srcBytes)) {
|
|
31728
|
+
throw new StaleCertError(
|
|
31729
|
+
`certificate ${opts.index} is stale for this file \u2014 its size / SHA-256 differ; rebuild it with read(...).validate().certify()`
|
|
31730
|
+
);
|
|
31731
|
+
}
|
|
31732
|
+
handle._attachCert(cert);
|
|
31733
|
+
}
|
|
31734
|
+
return handle;
|
|
31454
31735
|
}
|
|
31455
31736
|
function validate(source, opts = {}) {
|
|
31456
31737
|
const path = typeof source === "string" ? source : void 0;
|
|
@@ -31500,20 +31781,50 @@ function walkTree(root) {
|
|
|
31500
31781
|
}
|
|
31501
31782
|
};
|
|
31502
31783
|
visit(root);
|
|
31784
|
+
for (const [code, rows] of buckets) {
|
|
31785
|
+
const desc = get(code);
|
|
31786
|
+
for (const h of desc.headings) {
|
|
31787
|
+
if (isKeyStatus(h.status)) continue;
|
|
31788
|
+
if (rows.every((r) => r[h.name] == null)) {
|
|
31789
|
+
for (const r of rows) delete r[h.name];
|
|
31790
|
+
}
|
|
31791
|
+
}
|
|
31792
|
+
}
|
|
31503
31793
|
return [...buckets];
|
|
31504
31794
|
}
|
|
31795
|
+
function checkMeta(meta, name, columns) {
|
|
31796
|
+
if (meta === void 0) return;
|
|
31797
|
+
for (const [code, hmap] of Object.entries(meta)) {
|
|
31798
|
+
const known = columns.get(code);
|
|
31799
|
+
if (known === void 0) {
|
|
31800
|
+
throw new Ags4Error(`buildAgs4 ${name}: unknown group ${JSON.stringify(code)}`);
|
|
31801
|
+
}
|
|
31802
|
+
for (const heading of Object.keys(hmap)) {
|
|
31803
|
+
if (!known.has(heading)) {
|
|
31804
|
+
throw new Ags4Error(
|
|
31805
|
+
`buildAgs4 ${name}: group ${JSON.stringify(code)} has no heading ${JSON.stringify(heading)}`
|
|
31806
|
+
);
|
|
31807
|
+
}
|
|
31808
|
+
}
|
|
31809
|
+
}
|
|
31810
|
+
}
|
|
31505
31811
|
function buildAgs4(groups, opts = {}) {
|
|
31506
31812
|
const items = groups instanceof AgsGroup ? walkTree(groups) : groups instanceof Map ? [...groups] : groups;
|
|
31507
|
-
const ipcGroups =
|
|
31508
|
-
|
|
31509
|
-
|
|
31510
|
-
|
|
31511
|
-
|
|
31813
|
+
const ipcGroups = [];
|
|
31814
|
+
const columns = /* @__PURE__ */ new Map();
|
|
31815
|
+
for (const [code, data] of items) {
|
|
31816
|
+
const table = stripSynthKeys(Array.isArray(data) ? rowsToTable(data) : data);
|
|
31817
|
+
columns.set(code, new Set(table.schema.fields.map((f) => f.name)));
|
|
31818
|
+
ipcGroups.push({ code, ipc: Buffer.from((0, import_apache_arrow3.tableToIPC)(table, "stream")) });
|
|
31819
|
+
}
|
|
31820
|
+
checkMeta(opts.units, "units", columns);
|
|
31821
|
+
checkMeta(opts.types, "types", columns);
|
|
31822
|
+
const res = (0, import_native.emitAgs4FromIpc)(ipcGroups, opts.dictVersion, opts.mode, opts.units, opts.types);
|
|
31512
31823
|
const byRule = JSON.parse(res.findingsJson);
|
|
31513
31824
|
const findings = Object.entries(byRule).flatMap(
|
|
31514
31825
|
([rule, list]) => list.map((f) => ({ rule, ...f }))
|
|
31515
31826
|
);
|
|
31516
|
-
return new BuildResult(res.bytes, findings, res.fixesApplied);
|
|
31827
|
+
return new BuildResult(res.bytes, findings, res.applied, res.fixesApplied);
|
|
31517
31828
|
}
|
|
31518
31829
|
function listRules2() {
|
|
31519
31830
|
return JSON.parse((0, import_native.listRules)()).rules;
|
|
@@ -31525,6 +31836,33 @@ function fix(source, opts = {}) {
|
|
|
31525
31836
|
if (!r.ok) throw makeError(r.errorKind ?? "", r.exitCode, r.error ?? "unknown error");
|
|
31526
31837
|
return new FixResult(r.fixed, r.residual, r.applied, r.dictVersion);
|
|
31527
31838
|
}
|
|
31839
|
+
function diffBytes(x) {
|
|
31840
|
+
if (x instanceof Ags4File) return x.bytes;
|
|
31841
|
+
if (typeof x !== "string") return x;
|
|
31842
|
+
try {
|
|
31843
|
+
return (0, import_node_fs4.readFileSync)(x);
|
|
31844
|
+
} catch (e) {
|
|
31845
|
+
if (e && typeof e === "object" && e.code === "ENOENT") {
|
|
31846
|
+
throw new FileNotFoundError(`No such file or directory: ${x}`, 3);
|
|
31847
|
+
}
|
|
31848
|
+
throw e;
|
|
31849
|
+
}
|
|
31850
|
+
}
|
|
31851
|
+
function diff2(a, b, opts = {}) {
|
|
31852
|
+
const aBytes = diffBytes(a);
|
|
31853
|
+
const bBytes = diffBytes(b);
|
|
31854
|
+
try {
|
|
31855
|
+
return JSON.parse((0, import_native.diff)(aBytes, bBytes, opts.dictVersion, opts.encoding));
|
|
31856
|
+
} catch (e) {
|
|
31857
|
+
throw fromNativeError(e);
|
|
31858
|
+
}
|
|
31859
|
+
}
|
|
31860
|
+
function toExcel(agsPath, xlsxPath, opts = {}) {
|
|
31861
|
+
return (0, import_native.ags4ToExcel)(agsPath, xlsxPath, opts.groups);
|
|
31862
|
+
}
|
|
31863
|
+
function fromExcel(xlsxPath, agsPath, opts = {}) {
|
|
31864
|
+
return (0, import_native.excelToAgs4)(xlsxPath, agsPath, opts.formatNumericColumns);
|
|
31865
|
+
}
|
|
31528
31866
|
// Annotate the CommonJS export names for ESM import in node:
|
|
31529
31867
|
0 && (module.exports = {
|
|
31530
31868
|
AAVT,
|
|
@@ -31696,6 +32034,7 @@ function fix(source, opts = {}) {
|
|
|
31696
32034
|
SHBT,
|
|
31697
32035
|
STND,
|
|
31698
32036
|
SUCT,
|
|
32037
|
+
StaleCertError,
|
|
31699
32038
|
TNPC,
|
|
31700
32039
|
TRAN,
|
|
31701
32040
|
TREG,
|
|
@@ -31715,10 +32054,13 @@ function fix(source, opts = {}) {
|
|
|
31715
32054
|
WSTG,
|
|
31716
32055
|
agsTypes,
|
|
31717
32056
|
buildAgs4,
|
|
32057
|
+
diff,
|
|
31718
32058
|
fix,
|
|
32059
|
+
fromExcel,
|
|
31719
32060
|
listRules,
|
|
31720
32061
|
read,
|
|
31721
32062
|
registry,
|
|
32063
|
+
toExcel,
|
|
31722
32064
|
transport,
|
|
31723
32065
|
validate,
|
|
31724
32066
|
version
|