@mailwoman/corpus 5.9.0 → 5.10.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/out/src/adapter.d.ts +1 -1
- package/out/src/adapters/ban/adapter.d.ts +1 -1
- package/out/src/adapters/fcc-bdc/adapter.d.ts +1 -1
- package/out/src/adapters/geonames/adapter.d.ts +1 -1
- package/out/src/adapters/geonames-postal/adapter.d.ts +1 -1
- package/out/src/adapters/gnaf/adapter.d.ts +1 -1
- package/out/src/adapters/index.d.ts +19 -19
- package/out/src/adapters/openaddresses/adapter.d.ts +1 -1
- package/out/src/adapters/overture/adapter.d.ts +1 -1
- package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
- package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
- package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
- package/out/src/adapters/state-tx-notaries/adapter.d.ts +1 -1
- package/out/src/adapters/synth-po-box/adapter.d.ts +2 -2
- package/out/src/adapters/tiger/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-irs-bmf/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-nad/adapter.d.ts +2 -2
- package/out/src/adapters/usgov-nad/adapter.js +1 -1
- package/out/src/adapters/usgov-nppes/adapter.d.ts +1 -1
- package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts +1 -1
- package/out/src/adapters/wof-admin-jp/adapter.d.ts +1 -1
- package/out/src/adapters/wof-admin-json/adapter.d.ts +2 -2
- package/out/src/adapters/wof-postalcode-json/adapter.d.ts +2 -2
- package/out/src/align.d.ts +2 -2
- package/out/src/build.d.ts +4 -4
- package/out/src/index.d.ts +22 -21
- package/out/src/index.d.ts.map +1 -1
- package/out/src/index.js +1 -0
- package/out/src/index.js.map +1 -1
- package/out/src/parquet-wrapper/index.d.ts +3 -3
- package/out/src/parquet-wrapper/reader.d.ts +1 -1
- package/out/src/parquet-wrapper/writer.d.ts +4 -2
- package/out/src/parquet-wrapper/writer.d.ts.map +1 -1
- package/out/src/parquet-wrapper/writer.js +3 -1
- package/out/src/parquet-wrapper/writer.js.map +1 -1
- package/out/src/parquet.d.ts +3 -3
- package/out/src/runner.d.ts +2 -2
- package/out/src/shard-recipes/anchor-absorption.d.ts +1 -1
- package/out/src/shard-recipes/boundary-stress.d.ts +1 -1
- package/out/src/shard-recipes/country-balanced.d.ts +1 -1
- package/out/src/shard-recipes/cz-pcfirst-preposition.d.ts +1 -1
- package/out/src/shard-recipes/fr-admin-split.d.ts +1 -1
- package/out/src/shard-recipes/fr-bare-street.d.ts +1 -1
- package/out/src/shard-recipes/fr-order.d.ts +1 -1
- package/out/src/shard-recipes/german.d.ts +1 -1
- package/out/src/shard-recipes/house-venue.d.ts +1 -1
- package/out/src/shard-recipes/index.d.ts +2 -2
- package/out/src/shard-recipes/intersection.d.ts +1 -1
- package/out/src/shard-recipes/locale.d.ts +2 -2
- package/out/src/shard-recipes/nl-postcode.d.ts +1 -1
- package/out/src/shard-recipes/no-street-led.d.ts +1 -1
- package/out/src/shard-recipes/no-street.d.ts +1 -1
- package/out/src/shard-recipes/po-box-cedex.d.ts +1 -1
- package/out/src/shard-recipes/po-box.d.ts +1 -1
- package/out/src/shard-recipes/si-bare-village.d.ts +1 -1
- package/out/src/shard-recipes/street-affix.d.ts +1 -1
- package/out/src/shard-recipes/street-bare.d.ts +1 -1
- package/out/src/shard-recipes/street.d.ts +1 -1
- package/out/src/shard-recipes/unit.d.ts +1 -1
- package/out/src/split.d.ts +1 -1
- package/out/src/split.js +1 -1
- package/out/src/split.js.map +1 -1
- package/out/src/synthesize-boundary-stress.d.ts +1 -1
- package/out/src/synthesize-german.d.ts +1 -1
- package/out/src/synthesize-house-venue.d.ts +1 -1
- package/out/src/synthesize-intersection.d.ts +1 -1
- package/out/src/synthesize-no-street.d.ts +1 -1
- package/out/src/synthesize-po-box.d.ts +1 -1
- package/out/src/synthesize-street.d.ts +1 -1
- package/out/src/synthesize.d.ts +2 -2
- package/out/src/tools/align-shard.d.ts +8 -0
- package/out/src/tools/align-shard.d.ts.map +1 -0
- package/out/src/tools/align-shard.js +58 -0
- package/out/src/tools/align-shard.js.map +1 -0
- package/out/src/tools/audit.d.ts +28 -0
- package/out/src/tools/audit.d.ts.map +1 -0
- package/out/src/tools/audit.js +273 -0
- package/out/src/tools/audit.js.map +1 -0
- package/out/src/tools/corpus-stats.d.ts +44 -0
- package/out/src/tools/corpus-stats.d.ts.map +1 -0
- package/out/src/tools/corpus-stats.js +151 -0
- package/out/src/tools/corpus-stats.js.map +1 -0
- package/out/src/tools/fetch/ban.d.ts +31 -0
- package/out/src/tools/fetch/ban.d.ts.map +1 -0
- package/out/src/tools/fetch/ban.js +248 -0
- package/out/src/tools/fetch/ban.js.map +1 -0
- package/out/src/tools/fetch/download.d.ts +49 -0
- package/out/src/tools/fetch/download.d.ts.map +1 -0
- package/out/src/tools/fetch/download.js +81 -0
- package/out/src/tools/fetch/download.js.map +1 -0
- package/out/src/tools/fetch/hrsa.d.ts +18 -0
- package/out/src/tools/fetch/hrsa.d.ts.map +1 -0
- package/out/src/tools/fetch/hrsa.js +46 -0
- package/out/src/tools/fetch/hrsa.js.map +1 -0
- package/out/src/tools/fetch/imls-pls.d.ts +25 -0
- package/out/src/tools/fetch/imls-pls.d.ts.map +1 -0
- package/out/src/tools/fetch/imls-pls.js +120 -0
- package/out/src/tools/fetch/imls-pls.js.map +1 -0
- package/out/src/tools/fetch/index.d.ts +118 -0
- package/out/src/tools/fetch/index.d.ts.map +1 -0
- package/out/src/tools/fetch/index.js +117 -0
- package/out/src/tools/fetch/index.js.map +1 -0
- package/out/src/tools/fetch/nad.d.ts +56 -0
- package/out/src/tools/fetch/nad.d.ts.map +1 -0
- package/out/src/tools/fetch/nad.js +225 -0
- package/out/src/tools/fetch/nad.js.map +1 -0
- package/out/src/tools/fetch/nppes.d.ts +26 -0
- package/out/src/tools/fetch/nppes.d.ts.map +1 -0
- package/out/src/tools/fetch/nppes.js +136 -0
- package/out/src/tools/fetch/nppes.js.map +1 -0
- package/out/src/tools/fetch/openaddresses.d.ts +59 -0
- package/out/src/tools/fetch/openaddresses.d.ts.map +1 -0
- package/out/src/tools/fetch/openaddresses.js +307 -0
- package/out/src/tools/fetch/openaddresses.js.map +1 -0
- package/out/src/tools/fetch/state-hi-schools.d.ts +25 -0
- package/out/src/tools/fetch/state-hi-schools.d.ts.map +1 -0
- package/out/src/tools/fetch/state-hi-schools.js +171 -0
- package/out/src/tools/fetch/state-hi-schools.js.map +1 -0
- package/out/src/tools/fetch/state-sources.d.ts +22 -0
- package/out/src/tools/fetch/state-sources.d.ts.map +1 -0
- package/out/src/tools/fetch/state-sources.js +110 -0
- package/out/src/tools/fetch/state-sources.js.map +1 -0
- package/out/src/tools/fetch/tiger-full.d.ts +36 -0
- package/out/src/tools/fetch/tiger-full.d.ts.map +1 -0
- package/out/src/tools/fetch/tiger-full.js +239 -0
- package/out/src/tools/fetch/tiger-full.js.map +1 -0
- package/out/src/tools/golden-expand.d.ts +74 -0
- package/out/src/tools/golden-expand.d.ts.map +1 -0
- package/out/src/tools/golden-expand.js +394 -0
- package/out/src/tools/golden-expand.js.map +1 -0
- package/out/src/tools/golden-promote.d.ts +60 -0
- package/out/src/tools/golden-promote.d.ts.map +1 -0
- package/out/src/tools/golden-promote.js +223 -0
- package/out/src/tools/golden-promote.js.map +1 -0
- package/out/src/tools/index.d.ts +24 -0
- package/out/src/tools/index.d.ts.map +1 -0
- package/out/src/tools/index.js +24 -0
- package/out/src/tools/index.js.map +1 -0
- package/out/src/tools/ingest-csv.d.ts +45 -0
- package/out/src/tools/ingest-csv.d.ts.map +1 -0
- package/out/src/tools/ingest-csv.js +309 -0
- package/out/src/tools/ingest-csv.js.map +1 -0
- package/out/src/tools/jsonl-to-parquet.d.ts +49 -0
- package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -0
- package/out/src/tools/jsonl-to-parquet.js +157 -0
- package/out/src/tools/jsonl-to-parquet.js.map +1 -0
- package/out/src/tools/lint-shard-vocab.d.ts +66 -0
- package/out/src/tools/lint-shard-vocab.d.ts.map +1 -0
- package/out/src/tools/lint-shard-vocab.js +266 -0
- package/out/src/tools/lint-shard-vocab.js.map +1 -0
- package/out/src/tools/lint-shard.d.ts +73 -0
- package/out/src/tools/lint-shard.d.ts.map +1 -0
- package/out/src/tools/lint-shard.js +350 -0
- package/out/src/tools/lint-shard.js.map +1 -0
- package/out/src/tools/overlay-manifest.d.ts +35 -0
- package/out/src/tools/overlay-manifest.d.ts.map +1 -0
- package/out/src/tools/overlay-manifest.js +85 -0
- package/out/src/tools/overlay-manifest.js.map +1 -0
- package/out/src/tools/shard-kryptonite.d.ts +28 -0
- package/out/src/tools/shard-kryptonite.d.ts.map +1 -0
- package/out/src/tools/shard-kryptonite.js +101 -0
- package/out/src/tools/shard-kryptonite.js.map +1 -0
- package/out/src/tools/shard-translit.d.ts +42 -0
- package/out/src/tools/shard-translit.d.ts.map +1 -0
- package/out/src/tools/shard-translit.js +196 -0
- package/out/src/tools/shard-translit.js.map +1 -0
- package/package.json +63 -7
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Promote LLM-synthesized golden-set candidates into a versioned golden dir, with
|
|
7
|
+
* human-typed-likelihood filters + dedup. Companion to `golden-expand.ts`.
|
|
8
|
+
*
|
|
9
|
+
* ## What it does
|
|
10
|
+
*
|
|
11
|
+
* 1. Reads a candidates JSONL from `data/eval/golden/candidates/`
|
|
12
|
+
* 2. Reads the previous-version golden dir for forward-copy + dedup base
|
|
13
|
+
* 3. Applies filters that drop candidates unlikely to be human-typed:
|
|
14
|
+
*
|
|
15
|
+
* - Components-glued-without-commas (5+ components but <2 separators → freeform jumble)
|
|
16
|
+
* - Postcode-at-start with many other components (US/UK conventions put postcode last; FR puts it
|
|
17
|
+
* before locality only)
|
|
18
|
+
* - Suspicious-token signals (unmatched brackets, control chars, etc.)
|
|
19
|
+
* 4. Dedupes by normalized raw (case-insensitive, whitespace-collapsed)
|
|
20
|
+
* 5. Splits by country (US/FR/other) and writes `data/eval/golden/v<X.Y.Z>/{us,fr,other}.jsonl`
|
|
21
|
+
* 6. Forward-copies the prior version's adversarial.jsonl + README as-is
|
|
22
|
+
* 7. Writes MANIFEST.json with sha256 of each output file
|
|
23
|
+
*
|
|
24
|
+
* ## Usage
|
|
25
|
+
*
|
|
26
|
+
* ```sh
|
|
27
|
+
* mailwoman corpus golden promote \
|
|
28
|
+
* --input data/eval/golden/candidates/expand-20260518-162627.jsonl \
|
|
29
|
+
* --bump-to v0.1.1 \
|
|
30
|
+
* --prior v0.1.0
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { readJSONL, sha256File, writeJSONL } from "@mailwoman/core/utils";
|
|
36
|
+
// ── Filters ───────────────────────────────────────────────────────────────
|
|
37
|
+
function normalize(s) {
|
|
38
|
+
return s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Heuristic: an address with 5+ components but fewer than 2 separators (commas/newlines/dashes) is most likely glued
|
|
42
|
+
* together rather than human-typed.
|
|
43
|
+
*/
|
|
44
|
+
function isComponentsGlued(entry) {
|
|
45
|
+
const componentCount = Object.keys(entry.components).length;
|
|
46
|
+
if (componentCount < 5)
|
|
47
|
+
return false;
|
|
48
|
+
const separators = (entry.raw.match(/[,\n;]/g) ?? []).length;
|
|
49
|
+
return separators < 2;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Heuristic: in US/UK conventions, postcode goes at the END of the address. If postcode appears in the first third of a
|
|
53
|
+
* multi-component raw AND there are 4+ components, the LLM probably over-aggressively reordered. FR is exempt (postcode
|
|
54
|
+
* often precedes locality there).
|
|
55
|
+
*/
|
|
56
|
+
function isPostcodeBadlyLeading(entry) {
|
|
57
|
+
if (Object.keys(entry.components).length < 4)
|
|
58
|
+
return false;
|
|
59
|
+
if (entry.country === "FR" || entry.country === "France")
|
|
60
|
+
return false;
|
|
61
|
+
const postcode = entry.components.postcode;
|
|
62
|
+
if (!postcode)
|
|
63
|
+
return false;
|
|
64
|
+
const postcodeIdx = entry.raw.indexOf(postcode);
|
|
65
|
+
if (postcodeIdx === -1)
|
|
66
|
+
return false;
|
|
67
|
+
return postcodeIdx < entry.raw.length / 3;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Heuristic: catch-all for visually-bad outputs — unmatched brackets, control chars, suspicious punctuation that
|
|
71
|
+
* suggests the LLM emitted markup instead of an address.
|
|
72
|
+
*/
|
|
73
|
+
function isSuspicious(entry) {
|
|
74
|
+
const raw = entry.raw;
|
|
75
|
+
// eslint-disable-next-line no-control-regex
|
|
76
|
+
if (/[\x00-\x1f\x7f]/.test(raw))
|
|
77
|
+
return true;
|
|
78
|
+
const openBrackets = (raw.match(/[[({<]/g) ?? []).length;
|
|
79
|
+
const closeBrackets = (raw.match(/[\])}>]/g) ?? []).length;
|
|
80
|
+
if (openBrackets !== closeBrackets)
|
|
81
|
+
return true;
|
|
82
|
+
if (raw.split('"').length > 3)
|
|
83
|
+
return true;
|
|
84
|
+
// too many quote marks
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// ── IO ─────────────────────────────────────────────────────────────────────
|
|
88
|
+
/** Read a JSONL, tolerating a missing file (returns `[]`) — prior golden dirs may lack a bucket. */
|
|
89
|
+
function readJSONLIfPresent(path) {
|
|
90
|
+
if (!existsSync(path))
|
|
91
|
+
return [];
|
|
92
|
+
return readJSONL(path);
|
|
93
|
+
}
|
|
94
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
95
|
+
export async function promoteGolden(options, report) {
|
|
96
|
+
const prior = options.prior ?? "v0.1.0";
|
|
97
|
+
const goldenRoot = options.goldenRoot ?? "data/eval/golden";
|
|
98
|
+
const applyFilters = !options.noFilters;
|
|
99
|
+
const dryRun = options.dryRun ?? false;
|
|
100
|
+
report?.(`reading candidates: ${options.input}`);
|
|
101
|
+
const candidates = readJSONLIfPresent(options.input);
|
|
102
|
+
report?.(` ${candidates.length} candidates loaded`);
|
|
103
|
+
// Forward-copy base: existing entries from the prior golden version go forward verbatim,
|
|
104
|
+
// and we dedupe new candidates against them so v_new = v_old ∪ accepted_candidates.
|
|
105
|
+
const priorDir = join(goldenRoot, prior);
|
|
106
|
+
const priorEntries = [];
|
|
107
|
+
const seenNormalized = new Set();
|
|
108
|
+
if (existsSync(priorDir)) {
|
|
109
|
+
for (const f of readdirSync(priorDir).filter((n) => n.endsWith(".jsonl"))) {
|
|
110
|
+
const country = f.replace(".jsonl", "").toUpperCase();
|
|
111
|
+
const entries = readJSONLIfPresent(join(priorDir, f));
|
|
112
|
+
priorEntries.push({ country, entries });
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
seenNormalized.add(normalize(e.raw));
|
|
115
|
+
}
|
|
116
|
+
report?.(` prior ${country}: ${entries.length} entries (forward-copy base)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
report?.(` ⚠ prior dir ${priorDir} not found — starting fresh`);
|
|
121
|
+
}
|
|
122
|
+
// Filter pass
|
|
123
|
+
const stats = {
|
|
124
|
+
candidatesIn: candidates.length,
|
|
125
|
+
filteredOut: { glued: 0, postcodeLeading: 0, suspicious: 0, duplicate: 0, forwardDup: 0 },
|
|
126
|
+
kept: 0,
|
|
127
|
+
perCountry: {},
|
|
128
|
+
};
|
|
129
|
+
const accepted = [];
|
|
130
|
+
const seenInBatch = new Set();
|
|
131
|
+
for (const cand of candidates) {
|
|
132
|
+
const norm = normalize(cand.raw);
|
|
133
|
+
// Dedup pass 1: against prior versioned golden
|
|
134
|
+
if (seenNormalized.has(norm)) {
|
|
135
|
+
stats.filteredOut.forwardDup++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Dedup pass 2: against this batch
|
|
139
|
+
if (seenInBatch.has(norm)) {
|
|
140
|
+
stats.filteredOut.duplicate++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (applyFilters) {
|
|
144
|
+
if (isComponentsGlued(cand)) {
|
|
145
|
+
stats.filteredOut.glued++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (isPostcodeBadlyLeading(cand)) {
|
|
149
|
+
stats.filteredOut.postcodeLeading++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (isSuspicious(cand)) {
|
|
153
|
+
stats.filteredOut.suspicious++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
accepted.push(cand);
|
|
158
|
+
seenInBatch.add(norm);
|
|
159
|
+
stats.kept++;
|
|
160
|
+
const country = cand.country || "OTHER";
|
|
161
|
+
stats.perCountry[country] = (stats.perCountry[country] ?? 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
// Bucket per country, including forward-copied entries
|
|
164
|
+
const buckets = new Map();
|
|
165
|
+
for (const { country, entries } of priorEntries) {
|
|
166
|
+
// Existing files keyed by filename uppercase (us.jsonl → US, adversarial.jsonl → ADVERSARIAL)
|
|
167
|
+
buckets.set(country, [...entries]);
|
|
168
|
+
}
|
|
169
|
+
for (const cand of accepted) {
|
|
170
|
+
const key = (cand.country || "OTHER").toUpperCase();
|
|
171
|
+
if (!buckets.has(key)) {
|
|
172
|
+
buckets.set(key, []);
|
|
173
|
+
}
|
|
174
|
+
buckets.get(key).push(cand);
|
|
175
|
+
}
|
|
176
|
+
// Output
|
|
177
|
+
const outDir = join(goldenRoot, options.bumpTo);
|
|
178
|
+
report?.(`=== plan ===`);
|
|
179
|
+
report?.(`output dir: ${outDir}${dryRun ? " (dry-run)" : ""}`);
|
|
180
|
+
for (const [key, entries] of buckets) {
|
|
181
|
+
report?.(` ${key.toLowerCase()}.jsonl: ${entries.length} entries`);
|
|
182
|
+
}
|
|
183
|
+
report?.(`=== filter stats ===`);
|
|
184
|
+
report?.(`candidates in: ${stats.candidatesIn}`);
|
|
185
|
+
report?.(` filtered (glued): ${stats.filteredOut.glued}`);
|
|
186
|
+
report?.(` filtered (postcode-lead): ${stats.filteredOut.postcodeLeading}`);
|
|
187
|
+
report?.(` filtered (suspicious): ${stats.filteredOut.suspicious}`);
|
|
188
|
+
report?.(` filtered (dup-in-batch): ${stats.filteredOut.duplicate}`);
|
|
189
|
+
report?.(` filtered (dup-vs-prior): ${stats.filteredOut.forwardDup}`);
|
|
190
|
+
report?.(` kept: ${stats.kept}`);
|
|
191
|
+
report?.(`per-country (kept):`);
|
|
192
|
+
for (const [c, n] of Object.entries(stats.perCountry).sort((a, b) => b[1] - a[1])) {
|
|
193
|
+
report?.(` ${c}: ${n}`);
|
|
194
|
+
}
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
report?.(`(dry-run: no files written)`);
|
|
197
|
+
return stats;
|
|
198
|
+
}
|
|
199
|
+
mkdirSync(outDir, { recursive: true });
|
|
200
|
+
const manifest = {
|
|
201
|
+
promoted_at: new Date().toISOString(),
|
|
202
|
+
from: options.input,
|
|
203
|
+
from_sha256: await sha256File(options.input),
|
|
204
|
+
files: {},
|
|
205
|
+
};
|
|
206
|
+
for (const [key, entries] of buckets) {
|
|
207
|
+
const filename = `${key.toLowerCase()}.jsonl`;
|
|
208
|
+
const path = join(outDir, filename);
|
|
209
|
+
writeJSONL(path, entries);
|
|
210
|
+
manifest.files[filename] = { entries: entries.length, sha256: await sha256File(path) };
|
|
211
|
+
}
|
|
212
|
+
// Forward-copy non-.jsonl files (README.md, etc.) from prior
|
|
213
|
+
if (existsSync(priorDir)) {
|
|
214
|
+
for (const f of readdirSync(priorDir).filter((n) => !n.endsWith(".jsonl"))) {
|
|
215
|
+
copyFileSync(join(priorDir, f), join(outDir, f));
|
|
216
|
+
report?.(` forward-copied: ${f}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
writeFileSync(join(outDir, "MANIFEST.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
220
|
+
report?.(`✓ promoted to ${outDir}`);
|
|
221
|
+
return stats;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=golden-promote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"golden-promote.js","sourceRoot":"","sources":["../../../src/tools/golden-promote.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Corpus operator tools — the `run()`-style modules behind `mailwoman corpus …` commands. No argv,
|
|
7
|
+
* no `process.exit`: commands own parsing, rendering, and exit codes (see the 2026-07-09
|
|
8
|
+
* scripts→Pastel spec).
|
|
9
|
+
*/
|
|
10
|
+
export * from "./align-shard.ts";
|
|
11
|
+
export * from "./audit.ts";
|
|
12
|
+
export * from "./corpus-stats.ts";
|
|
13
|
+
export * from "./ingest-csv.ts";
|
|
14
|
+
export * from "./overlay-manifest.ts";
|
|
15
|
+
export * from "./fetch/download.ts";
|
|
16
|
+
export * from "./fetch/index.ts";
|
|
17
|
+
export * from "./golden-expand.ts";
|
|
18
|
+
export * from "./golden-promote.ts";
|
|
19
|
+
export * from "./jsonl-to-parquet.ts";
|
|
20
|
+
export * from "./lint-shard.ts";
|
|
21
|
+
export * from "./lint-shard-vocab.ts";
|
|
22
|
+
export * from "./shard-kryptonite.ts";
|
|
23
|
+
export * from "./shard-translit.ts";
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Corpus operator tools — the `run()`-style modules behind `mailwoman corpus …` commands. No argv,
|
|
7
|
+
* no `process.exit`: commands own parsing, rendering, and exit codes (see the 2026-07-09
|
|
8
|
+
* scripts→Pastel spec).
|
|
9
|
+
*/
|
|
10
|
+
export * from "./align-shard.js";
|
|
11
|
+
export * from "./audit.js";
|
|
12
|
+
export * from "./corpus-stats.js";
|
|
13
|
+
export * from "./ingest-csv.js";
|
|
14
|
+
export * from "./overlay-manifest.js";
|
|
15
|
+
export * from "./fetch/download.js";
|
|
16
|
+
export * from "./fetch/index.js";
|
|
17
|
+
export * from "./golden-expand.js";
|
|
18
|
+
export * from "./golden-promote.js";
|
|
19
|
+
export * from "./jsonl-to-parquet.js";
|
|
20
|
+
export * from "./lint-shard.js";
|
|
21
|
+
export * from "./lint-shard-vocab.js";
|
|
22
|
+
export * from "./shard-kryptonite.js";
|
|
23
|
+
export * from "./shard-translit.js";
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* CSV → SQLite ingestion utility. Ported from isp-nexus's `sdk/data/csv.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Reads a CSV file, infers column types from a sample of rows, creates a SQLite table, and imports
|
|
9
|
+
* the data. Handles quoted fields, NULL normalization, and duplicate column name disambiguation.
|
|
10
|
+
*
|
|
11
|
+
* ## Usage
|
|
12
|
+
*
|
|
13
|
+
* ```sh
|
|
14
|
+
* mailwoman corpus ingest-csv \
|
|
15
|
+
* --input /data/corpus/sources/usgov-nppes/npidata_pfile.csv \
|
|
16
|
+
* --table nppes_providers \
|
|
17
|
+
* --output /data/corpus/sources/usgov-nppes/nppes.db
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Options: --input <path> CSV file to ingest (required) --table <name> SQLite table name (default:
|
|
21
|
+
* derived from input filename) --output <path> SQLite database path (default: input dir /
|
|
22
|
+
* table.db) --sample <n> Rows to sample for type inference (default: 100) --separator <char>
|
|
23
|
+
* Field separator (default: ,) --skip <n> Lines to skip before header (default: 0) --no-header
|
|
24
|
+
* CSV has no header row — columns will be col_0, col_1, etc. --dry-run Infer schema and print
|
|
25
|
+
* CREATE TABLE, but don't import
|
|
26
|
+
*/
|
|
27
|
+
/** Flag-shaped options for {@linkcode ingestCSV} — `table`/`output` derive from `input` when omitted. */
|
|
28
|
+
export interface IngestCSVOptions {
|
|
29
|
+
input: string;
|
|
30
|
+
table?: string;
|
|
31
|
+
output?: string;
|
|
32
|
+
sample?: number;
|
|
33
|
+
separator?: string;
|
|
34
|
+
skip?: number;
|
|
35
|
+
noHeader?: boolean;
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Ingest a CSV into SQLite: infer column types from a sample, create the table, import the rows. Throws when `input` is
|
|
40
|
+
* missing. NOTE(phase1): progress narration still writes stderr directly — this predates the report-callback contract
|
|
41
|
+
* and the write sites are deep in the type-inference helpers; thread a report param if a caller ever needs to capture
|
|
42
|
+
* it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ingestCSV(options: IngestCSVOptions): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=ingest-csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-csv.d.ts","sourceRoot":"","sources":["../../../src/tools/ingest-csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAwWH,yGAAyG;AACzG,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBxE"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* CSV → SQLite ingestion utility. Ported from isp-nexus's `sdk/data/csv.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Reads a CSV file, infers column types from a sample of rows, creates a SQLite table, and imports
|
|
9
|
+
* the data. Handles quoted fields, NULL normalization, and duplicate column name disambiguation.
|
|
10
|
+
*
|
|
11
|
+
* ## Usage
|
|
12
|
+
*
|
|
13
|
+
* ```sh
|
|
14
|
+
* mailwoman corpus ingest-csv \
|
|
15
|
+
* --input /data/corpus/sources/usgov-nppes/npidata_pfile.csv \
|
|
16
|
+
* --table nppes_providers \
|
|
17
|
+
* --output /data/corpus/sources/usgov-nppes/nppes.db
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Options: --input <path> CSV file to ingest (required) --table <name> SQLite table name (default:
|
|
21
|
+
* derived from input filename) --output <path> SQLite database path (default: input dir /
|
|
22
|
+
* table.db) --sample <n> Rows to sample for type inference (default: 100) --separator <char>
|
|
23
|
+
* Field separator (default: ,) --skip <n> Lines to skip before header (default: 0) --no-header
|
|
24
|
+
* CSV has no header row — columns will be col_0, col_1, etc. --dry-run Infer schema and print
|
|
25
|
+
* CREATE TABLE, but don't import
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
29
|
+
import { TextSpliterator } from "spliterator";
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Core: quote-aware CSV field splitting
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const COMMA = 44;
|
|
34
|
+
const DOUBLE_QUOTE = 34;
|
|
35
|
+
// These stay hand-rolled on purpose — spliterator ≥ 3.2.0's CSVSpliterator does quote handling
|
|
36
|
+
// correctly now, but it can't express this tool's CLI contract: `--skip N` drops N preamble lines
|
|
37
|
+
// BEFORE the header (CSVSpliterator consumes row 1 as the header immediately, with no skip-first-N
|
|
38
|
+
// hook), `--no-header` auto-generates `col_0…col_N` names from the first data row's width, and the
|
|
39
|
+
// separator is any single caller-supplied byte. If a future edit removes those knobs, revisit;
|
|
40
|
+
// until then, don't "finish the job" by swapping in CSVSpliterator.
|
|
41
|
+
function splitCSVLine(line, separator = COMMA) {
|
|
42
|
+
const fields = [];
|
|
43
|
+
let start = 0;
|
|
44
|
+
let inQuotes = false;
|
|
45
|
+
for (let i = 0; i < line.length; i++) {
|
|
46
|
+
const ch = line.charCodeAt(i);
|
|
47
|
+
if (ch === DOUBLE_QUOTE) {
|
|
48
|
+
inQuotes = !inQuotes;
|
|
49
|
+
}
|
|
50
|
+
else if (ch === separator && !inQuotes) {
|
|
51
|
+
fields.push(line.slice(start, i));
|
|
52
|
+
start = i + 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fields.push(line.slice(start));
|
|
56
|
+
return fields;
|
|
57
|
+
}
|
|
58
|
+
function stripQuotes(field) {
|
|
59
|
+
const trimmed = field.trim();
|
|
60
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
61
|
+
return trimmed.slice(1, -1).replace(/""/g, '"');
|
|
62
|
+
}
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Column name normalization
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function normalizeColumnName(raw) {
|
|
69
|
+
return (raw
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/^"+|"+$/g, "")
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
74
|
+
.replace(/^_|_$/g, "") || "unnamed");
|
|
75
|
+
}
|
|
76
|
+
function dedupColumns(names) {
|
|
77
|
+
const seen = new Map();
|
|
78
|
+
return names.map((name) => {
|
|
79
|
+
const count = seen.get(name) ?? 0;
|
|
80
|
+
seen.set(name, count + 1);
|
|
81
|
+
return count === 0 ? name : `${name}_${count + 1}`;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function normalizeField(raw) {
|
|
85
|
+
let s = raw.trim();
|
|
86
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
87
|
+
s = s.slice(1, -1).replace(/""/g, '"');
|
|
88
|
+
}
|
|
89
|
+
// Normalize common null-like values
|
|
90
|
+
if (!s || s === "null" || s === "NULL" || s === "N/A" || s === "n/a" || s === "-" || s === "<UNAVAIL>") {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return s;
|
|
94
|
+
}
|
|
95
|
+
function inferColumnType(samples) {
|
|
96
|
+
let nullCount = 0;
|
|
97
|
+
let intCount = 0;
|
|
98
|
+
let realCount = 0;
|
|
99
|
+
let textCount = 0;
|
|
100
|
+
for (const s of samples) {
|
|
101
|
+
if (s === null) {
|
|
102
|
+
nullCount++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (/^-?\d+$/.test(s)) {
|
|
106
|
+
intCount++;
|
|
107
|
+
}
|
|
108
|
+
else if (/^-?\d+\.?\d+$/.test(s)) {
|
|
109
|
+
realCount++;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
textCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const total = samples.length;
|
|
116
|
+
const type = realCount / total >= 0.5 ? "REAL" : intCount / total >= 0.5 ? "INTEGER" : "TEXT";
|
|
117
|
+
return { name: "", type, nullable: nullCount / total >= 0.5 };
|
|
118
|
+
}
|
|
119
|
+
async function runIngest(opts) {
|
|
120
|
+
const sep = opts.separator.charCodeAt(0);
|
|
121
|
+
// --- Pass 1: read header + sample rows for type inference ---
|
|
122
|
+
process.stderr.write(`Reading ${opts.inputPath} for schema inference...\n`);
|
|
123
|
+
// CRLF-safe by construction: every field below flows through stripQuotes/normalizeField, both of
|
|
124
|
+
// which `.trim()`, so a trailing CR on the last column of a CRLF file is stripped. TextSpliterator's
|
|
125
|
+
// default skipEmpty matches readline row-for-row on files with a trailing newline (the common case);
|
|
126
|
+
// it drops interior blank lines that readline would have turned into all-null rows. The early `break`
|
|
127
|
+
// closes the file descriptor.
|
|
128
|
+
let headerLine = null;
|
|
129
|
+
const sampleRows = [];
|
|
130
|
+
let lineNum = 0;
|
|
131
|
+
for await (const line of TextSpliterator.fromAsync(opts.inputPath)) {
|
|
132
|
+
lineNum++;
|
|
133
|
+
// Skip lines before header
|
|
134
|
+
if (lineNum <= opts.skipLines)
|
|
135
|
+
continue;
|
|
136
|
+
if (!headerLine && opts.hasHeader) {
|
|
137
|
+
headerLine = line;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (sampleRows.length < opts.sampleSize) {
|
|
141
|
+
sampleRows.push(splitCSVLine(line, sep).map(stripQuotes));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!headerLine && opts.hasHeader) {
|
|
148
|
+
throw new Error("No header line found in CSV");
|
|
149
|
+
}
|
|
150
|
+
// --- Determine column names ---
|
|
151
|
+
let rawHeaders;
|
|
152
|
+
if (opts.hasHeader && headerLine) {
|
|
153
|
+
rawHeaders = splitCSVLine(headerLine, sep).map(stripQuotes);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Auto-generate column names: col_0, col_1, ...
|
|
157
|
+
const numCols = sampleRows[0]?.length ?? 0;
|
|
158
|
+
rawHeaders = Array.from({ length: numCols }, (_, i) => `col_${i}`);
|
|
159
|
+
}
|
|
160
|
+
const colNames = dedupColumns(rawHeaders.map(normalizeColumnName));
|
|
161
|
+
// --- Infer column types ---
|
|
162
|
+
const columns = colNames.map((name, i) => {
|
|
163
|
+
const samples = sampleRows.map((row) => {
|
|
164
|
+
const raw = row[i] ?? "";
|
|
165
|
+
return normalizeField(raw);
|
|
166
|
+
});
|
|
167
|
+
const info = inferColumnType(samples);
|
|
168
|
+
info.name = name;
|
|
169
|
+
return info;
|
|
170
|
+
});
|
|
171
|
+
// --- Generate SQL ---
|
|
172
|
+
const colDefs = columns.map((c) => `"${c.name}" ${c.type}`).join(",\n ");
|
|
173
|
+
// Raw DDL by design: the column set + types are INFERRED from the CSV at runtime (colDefs above),
|
|
174
|
+
// so a Kysely builder loop would just wrap the same dynamic strings with ceremony and no type safety.
|
|
175
|
+
const createTableSQL = `CREATE TABLE IF NOT EXISTS "${opts.tableName}" (\n ${colDefs}\n);`;
|
|
176
|
+
const tempCols = columns.map((c) => `"${c.name}"`).join(", ");
|
|
177
|
+
const insertSQL = `INSERT INTO "${opts.tableName}" (${tempCols})\nSELECT ${tempCols} FROM temp."${opts.tableName}_source";`;
|
|
178
|
+
process.stderr.write(`\nSchema inferred from ${sampleRows.length} sample rows:\n`);
|
|
179
|
+
process.stderr.write(`${createTableSQL}\n\n`);
|
|
180
|
+
if (opts.dryRun) {
|
|
181
|
+
process.stderr.write("--dry-run: stopping before import\n");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// --- Create database + import ---
|
|
185
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
186
|
+
mkdirSync(dirname(opts.outputPath), { recursive: true });
|
|
187
|
+
const db = new DatabaseSync(opts.outputPath);
|
|
188
|
+
db.exec("PRAGMA journal_mode = OFF"); // faster for bulk import
|
|
189
|
+
db.exec("PRAGMA synchronous = OFF");
|
|
190
|
+
db.exec(createTableSQL);
|
|
191
|
+
// Use the .import approach via a temp table, then INSERT INTO ... SELECT to handle
|
|
192
|
+
// NULL normalization and type coercion.
|
|
193
|
+
const csvBasename = basename(opts.inputPath);
|
|
194
|
+
const importSQL = [
|
|
195
|
+
`CREATE TEMP TABLE "${opts.tableName}_source" (${colDefs});`,
|
|
196
|
+
`.mode csv`,
|
|
197
|
+
`.separator "${opts.separator}"`,
|
|
198
|
+
`.import "${csvBasename}" --skip ${opts.skipLines + (opts.hasHeader ? 1 : 0)} --schema temp ${opts.tableName}_source`,
|
|
199
|
+
insertSQL,
|
|
200
|
+
`DROP TABLE temp."${opts.tableName}_source";`,
|
|
201
|
+
];
|
|
202
|
+
// better-sqlite3 doesn't support .import natively, so we use a different approach:
|
|
203
|
+
// Read the CSV line-by-line and INSERT in a transaction.
|
|
204
|
+
process.stderr.write(`Importing rows...\n`);
|
|
205
|
+
const insertStmt = db.prepare(`INSERT INTO "${opts.tableName}" (${tempCols}) VALUES (${columns.map(() => "?").join(", ")})`);
|
|
206
|
+
let imported = 0;
|
|
207
|
+
let headerSkipped = false;
|
|
208
|
+
// node:sqlite has no `db.transaction(fn)` wrapper; use raw BEGIN/COMMIT around the batch.
|
|
209
|
+
const doInsert = () => {
|
|
210
|
+
db.exec("BEGIN");
|
|
211
|
+
try {
|
|
212
|
+
for (const row of batch) {
|
|
213
|
+
insertStmt.run(...row);
|
|
214
|
+
}
|
|
215
|
+
db.exec("COMMIT");
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
db.exec("ROLLBACK");
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const batch = [];
|
|
223
|
+
const BATCH_SIZE = 10000;
|
|
224
|
+
for await (const line of TextSpliterator.fromAsync(opts.inputPath)) {
|
|
225
|
+
lineNum++;
|
|
226
|
+
if (lineNum <= opts.skipLines)
|
|
227
|
+
continue;
|
|
228
|
+
if (opts.hasHeader && !headerSkipped) {
|
|
229
|
+
headerSkipped = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const fields = splitCSVLine(line, sep).map(stripQuotes);
|
|
233
|
+
const values = fields.map((f, i) => {
|
|
234
|
+
const v = normalizeField(f);
|
|
235
|
+
if (v === null)
|
|
236
|
+
return null;
|
|
237
|
+
const col = columns[i];
|
|
238
|
+
if (col?.type === "INTEGER" && /^-?\d+$/.test(v))
|
|
239
|
+
return parseInt(v, 10);
|
|
240
|
+
if (col?.type === "REAL" && /^-?\d+\.?\d+$/.test(v))
|
|
241
|
+
return parseFloat(v);
|
|
242
|
+
return v;
|
|
243
|
+
});
|
|
244
|
+
// Pad or truncate to column count
|
|
245
|
+
while (values.length < columns.length) {
|
|
246
|
+
values.push(null);
|
|
247
|
+
}
|
|
248
|
+
values.length = columns.length;
|
|
249
|
+
batch.push(values);
|
|
250
|
+
if (batch.length >= BATCH_SIZE) {
|
|
251
|
+
doInsert();
|
|
252
|
+
imported += batch.length;
|
|
253
|
+
batch.length = 0;
|
|
254
|
+
if (imported % 100000 === 0) {
|
|
255
|
+
process.stderr.write(` ${(imported / 1_000_000).toFixed(1)}M rows...\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// Flush remaining
|
|
260
|
+
if (batch.length > 0) {
|
|
261
|
+
doInsert();
|
|
262
|
+
imported += batch.length;
|
|
263
|
+
}
|
|
264
|
+
process.stderr.write(` Imported ${imported.toLocaleString()} rows into "${opts.tableName}"\n`);
|
|
265
|
+
// Build a basic index on the first TEXT column (likely the primary key)
|
|
266
|
+
const firstTextCol = columns.find((c) => c.type === "TEXT");
|
|
267
|
+
if (firstTextCol) {
|
|
268
|
+
process.stderr.write(`Building index on "${firstTextCol.name}"...\n`);
|
|
269
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_${opts.tableName}_${firstTextCol.name} ON "${opts.tableName}"("${firstTextCol.name}");`);
|
|
270
|
+
}
|
|
271
|
+
db.close();
|
|
272
|
+
// Write MANIFEST
|
|
273
|
+
const fileSize = (await import("node:fs/promises")).stat;
|
|
274
|
+
const stat = await (await import("node:fs/promises")).stat(opts.outputPath);
|
|
275
|
+
const manifest = {
|
|
276
|
+
ingested_at: new Date().toISOString(),
|
|
277
|
+
source_csv: basename(opts.inputPath),
|
|
278
|
+
table_name: opts.tableName,
|
|
279
|
+
columns: columns.map((c) => ({ name: c.name, type: c.type, nullable: c.nullable })),
|
|
280
|
+
row_count: imported,
|
|
281
|
+
db_bytes: stat.size,
|
|
282
|
+
};
|
|
283
|
+
const manifestPath = opts.outputPath.replace(/\.db$/, ".manifest.json");
|
|
284
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
285
|
+
process.stderr.write(`Done. ${imported.toLocaleString()} rows → ${opts.outputPath} (${(stat.size / 1024 / 1024).toFixed(0)} MB)\n`);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Ingest a CSV into SQLite: infer column types from a sample, create the table, import the rows. Throws when `input` is
|
|
289
|
+
* missing. NOTE(phase1): progress narration still writes stderr directly — this predates the report-callback contract
|
|
290
|
+
* and the write sites are deep in the type-inference helpers; thread a report param if a caller ever needs to capture
|
|
291
|
+
* it.
|
|
292
|
+
*/
|
|
293
|
+
export async function ingestCSV(options) {
|
|
294
|
+
if (!existsSync(options.input)) {
|
|
295
|
+
throw new Error(`File not found: ${options.input}`);
|
|
296
|
+
}
|
|
297
|
+
const csvName = basename(options.input, extname(options.input));
|
|
298
|
+
await runIngest({
|
|
299
|
+
inputPath: options.input,
|
|
300
|
+
tableName: options.table ?? csvName.replace(/[^a-zA-Z0-9_]/g, "_"),
|
|
301
|
+
outputPath: options.output ?? join(dirname(options.input), csvName + ".db"),
|
|
302
|
+
sampleSize: options.sample ?? 100,
|
|
303
|
+
separator: options.separator ?? ",",
|
|
304
|
+
skipLines: options.skip ?? 0,
|
|
305
|
+
hasHeader: !options.noHeader,
|
|
306
|
+
dryRun: options.dryRun ?? false,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=ingest-csv.js.map
|