@mailwoman/corpus 7.2.0 → 7.3.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/package.json +59 -27
- package/src/adapter.ts +153 -0
- package/src/adapters/ban/adapter.ts +189 -0
- package/src/adapters/ban/street-decompose.ts +98 -0
- package/src/adapters/fcc-bdc/adapter.ts +188 -0
- package/src/adapters/geonames/adapter.ts +185 -0
- package/src/adapters/geonames-postal/adapter.ts +111 -0
- package/src/adapters/gnaf/adapter.ts +139 -0
- package/src/adapters/gnaf/assemble.ts +200 -0
- package/src/adapters/index.ts +135 -0
- package/src/adapters/openaddresses/adapter.ts +237 -0
- package/src/adapters/overture/adapter.ts +149 -0
- package/src/adapters/state-hi-schools/adapter.ts +171 -0
- package/src/adapters/state-ia-contractors/adapter.ts +148 -0
- package/src/adapters/state-ny-notaries/adapter.ts +169 -0
- package/src/adapters/state-tx-notaries/adapter.ts +160 -0
- package/src/adapters/synth-po-box/adapter.ts +197 -0
- package/src/adapters/tiger/adapter.ts +232 -0
- package/src/adapters/tiger/street-decompose.ts +120 -0
- package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
- package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
- package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
- package/src/adapters/usgov-nad/adapter.ts +338 -0
- package/src/adapters/usgov-nppes/adapter.ts +169 -0
- package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
- package/src/adapters/wof-admin-jp/adapter.ts +176 -0
- package/src/adapters/wof-admin-json/adapter.ts +280 -0
- package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
- package/src/align.ts +317 -0
- package/src/build.ts +348 -0
- package/src/codex/us-fips-state.ts +109 -0
- package/src/format.ts +14 -0
- package/src/golden.ts +147 -0
- package/src/index.ts +28 -0
- package/src/license.ts +48 -0
- package/src/parquet-wrapper/index.ts +9 -0
- package/src/parquet-wrapper/reader.ts +67 -0
- package/src/parquet-wrapper/schema.ts +80 -0
- package/src/parquet-wrapper/writer.ts +102 -0
- package/src/parquet.ts +360 -0
- package/src/runner.ts +283 -0
- package/src/shard-recipes/anchor-absorption.ts +86 -0
- package/src/shard-recipes/boundary-stress.ts +87 -0
- package/src/shard-recipes/country-balanced.ts +471 -0
- package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
- package/src/shard-recipes/fr-admin-split.ts +212 -0
- package/src/shard-recipes/fr-bare-street.ts +85 -0
- package/src/shard-recipes/fr-fragment.ts +304 -0
- package/src/shard-recipes/fr-order.ts +283 -0
- package/src/shard-recipes/german.ts +206 -0
- package/src/shard-recipes/house-venue.ts +78 -0
- package/src/shard-recipes/index.ts +76 -0
- package/src/shard-recipes/intersection.ts +617 -0
- package/src/shard-recipes/locale.ts +415 -0
- package/src/shard-recipes/nl-postcode.ts +105 -0
- package/src/shard-recipes/no-fragment.ts +206 -0
- package/src/shard-recipes/no-street-led.ts +130 -0
- package/src/shard-recipes/no-street.ts +78 -0
- package/src/shard-recipes/po-box-cedex.ts +853 -0
- package/src/shard-recipes/po-box.ts +111 -0
- package/src/shard-recipes/scaffold.ts +187 -0
- package/src/shard-recipes/si-bare-village.ts +88 -0
- package/src/shard-recipes/street-affix.ts +549 -0
- package/src/shard-recipes/street-bare.ts +90 -0
- package/src/shard-recipes/street.ts +74 -0
- package/src/shard-recipes/unit.ts +315 -0
- package/src/split.ts +251 -0
- package/src/synthesize-anchor-absorption.ts +263 -0
- package/src/synthesize-boundary-stress.ts +504 -0
- package/src/synthesize-german.ts +227 -0
- package/src/synthesize-house-venue.ts +192 -0
- package/src/synthesize-intersection.ts +205 -0
- package/src/synthesize-no-street.ts +341 -0
- package/src/synthesize-po-box.ts +307 -0
- package/src/synthesize-street.ts +264 -0
- package/src/synthesize.ts +872 -0
- package/src/tokenize.ts +68 -0
- package/src/tools/align-shard.ts +67 -0
- package/src/tools/audit.ts +352 -0
- package/src/tools/corpus-stats.ts +183 -0
- package/src/tools/fetch/ban.ts +288 -0
- package/src/tools/fetch/download.ts +119 -0
- package/src/tools/fetch/hrsa.ts +65 -0
- package/src/tools/fetch/imls-pls.ts +162 -0
- package/src/tools/fetch/index.ts +121 -0
- package/src/tools/fetch/nad.ts +304 -0
- package/src/tools/fetch/nppes.ts +177 -0
- package/src/tools/fetch/openaddresses.ts +380 -0
- package/src/tools/fetch/state-hi-schools.ts +217 -0
- package/src/tools/fetch/state-sources.ts +142 -0
- package/src/tools/fetch/tiger-full.ts +318 -0
- package/src/tools/golden-expand.ts +524 -0
- package/src/tools/golden-promote.ts +304 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/ingest-csv.ts +420 -0
- package/src/tools/jsonl-to-parquet.ts +200 -0
- package/src/tools/lint-shard-vocab.ts +357 -0
- package/src/tools/lint-shard.ts +502 -0
- package/src/tools/overlay-manifest.ts +141 -0
- package/src/tools/shard-kryptonite.ts +131 -0
- package/src/tools/shard-translit.ts +244 -0
- package/src/types.ts +213 -0
- package/src/wof-json.ts +236 -0
package/src/runner.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Adapter runner — drives a `CorpusAdapter` to completion and writes intermediate JSONL + a
|
|
7
|
+
* per-shard manifest.
|
|
8
|
+
*
|
|
9
|
+
* Output layout under `outputDir`:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* <outputDir>/<adapter.id>/
|
|
13
|
+
* canonical.jsonl # one row per line, in emission order
|
|
14
|
+
* MANIFEST.json # adapter id, version, row count, sha256, license, started_at, ended_at
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The runner is responsible for everything an adapter is **not** responsible for:
|
|
18
|
+
*
|
|
19
|
+
* - Stamping `corpus_version` on every row (adapters must NOT set it).
|
|
20
|
+
* - Applying `canonicalDedupKey` and skipping duplicates.
|
|
21
|
+
* - Streaming sha256 over JSONL bytes so the manifest checksum doesn't require a re-read.
|
|
22
|
+
* - Honoring backpressure on the output write stream.
|
|
23
|
+
* - Counting + emitting periodic progress to an optional callback.
|
|
24
|
+
* - Honoring `signal` (delegates to adapter's iteration boundary).
|
|
25
|
+
*
|
|
26
|
+
* The runner does NOT perform alignment, tokenization, synthesis, or sharding into Parquet. Those
|
|
27
|
+
* steps run later, consuming the JSONL shards this writes.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { createWriteStream, type WriteStream } from "node:fs"
|
|
31
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
32
|
+
import { dirname, join } from "node:path"
|
|
33
|
+
|
|
34
|
+
import { canonicalDedupKey, streamingSha256, type AdapterRegistry, type StreamingHasher } from "./adapter.ts"
|
|
35
|
+
import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "./types.ts"
|
|
36
|
+
|
|
37
|
+
/** Snapshot of the runner's state, emitted on every progress tick. */
|
|
38
|
+
export interface RunnerProgress {
|
|
39
|
+
/** Adapter being driven. */
|
|
40
|
+
adapterID: string
|
|
41
|
+
|
|
42
|
+
/** Total rows the adapter has yielded (before dedup). */
|
|
43
|
+
yielded: number
|
|
44
|
+
|
|
45
|
+
/** Rows actually written to JSONL (after dedup). */
|
|
46
|
+
written: number
|
|
47
|
+
|
|
48
|
+
/** Bytes written to JSONL so far. */
|
|
49
|
+
bytes: number
|
|
50
|
+
|
|
51
|
+
/** Wall-clock milliseconds since the run started. */
|
|
52
|
+
elapsed_ms: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Per-invocation options for `runAdapter`. */
|
|
56
|
+
export interface RunAdapterOptions {
|
|
57
|
+
/** Adapter to drive. */
|
|
58
|
+
adapter: CorpusAdapter
|
|
59
|
+
|
|
60
|
+
/** Options handed to the adapter (input path, country filter, limit, signal). */
|
|
61
|
+
adapterOptions: AdapterOptions
|
|
62
|
+
|
|
63
|
+
/** Root output directory; the runner creates `<outputDir>/<adapter.id>/` under it. */
|
|
64
|
+
outputDir: string
|
|
65
|
+
|
|
66
|
+
/** Corpus version stamped onto every row. Locked together with the tokenizer version. */
|
|
67
|
+
corpusVersion: string
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Optional progress callback. Invoked every `progressEvery` rows yielded (default 1000) and once at the end of the
|
|
71
|
+
* run. Errors thrown from this callback abort the run.
|
|
72
|
+
*/
|
|
73
|
+
onProgress?: (snapshot: RunnerProgress) => void
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Yielded-row interval at which `onProgress` fires. Defaults to 1000. The terminal tick is always emitted regardless
|
|
77
|
+
* of this value.
|
|
78
|
+
*/
|
|
79
|
+
progressEvery?: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Return value of `runAdapter`: the same shape as `MANIFEST.json` on disk. */
|
|
83
|
+
export interface AdapterRunManifest {
|
|
84
|
+
adapter_id: string
|
|
85
|
+
corpus_version: string
|
|
86
|
+
default_license: string
|
|
87
|
+
description: string
|
|
88
|
+
yielded: number
|
|
89
|
+
written: number
|
|
90
|
+
deduped: number
|
|
91
|
+
bytes: number
|
|
92
|
+
sha256: string
|
|
93
|
+
jsonl_path: string
|
|
94
|
+
started_at: string
|
|
95
|
+
ended_at: string
|
|
96
|
+
elapsed_ms: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drive a single adapter to completion.
|
|
101
|
+
*
|
|
102
|
+
* Returns the manifest describing the run. Writes `canonical.jsonl` + `MANIFEST.json` under `outputDir/<adapter.id>/`.
|
|
103
|
+
* Throws if the output directory cannot be created, if a row arrives with a missing required field, or if the abort
|
|
104
|
+
* signal fires.
|
|
105
|
+
*/
|
|
106
|
+
export async function runAdapter(opts: RunAdapterOptions): Promise<AdapterRunManifest> {
|
|
107
|
+
const { adapter, adapterOptions, outputDir, corpusVersion } = opts
|
|
108
|
+
const progressEvery = opts.progressEvery ?? 1_000
|
|
109
|
+
|
|
110
|
+
const adapterDir = join(outputDir, adapter.id)
|
|
111
|
+
await mkdir(adapterDir, { recursive: true })
|
|
112
|
+
|
|
113
|
+
const jsonlPath = join(adapterDir, "canonical.jsonl")
|
|
114
|
+
const manifestPath = join(adapterDir, "MANIFEST.json")
|
|
115
|
+
|
|
116
|
+
const startedAt = new Date()
|
|
117
|
+
const t0 = performance.now()
|
|
118
|
+
|
|
119
|
+
const stream = createWriteStream(jsonlPath, { encoding: "utf8" })
|
|
120
|
+
const hasher: StreamingHasher = streamingSha256()
|
|
121
|
+
const seen = new Set<string>()
|
|
122
|
+
const DEDUP_MAX_SIZE = 10_000_000
|
|
123
|
+
let dedupExhausted = false
|
|
124
|
+
|
|
125
|
+
let yielded = 0
|
|
126
|
+
let written = 0
|
|
127
|
+
let bytes = 0
|
|
128
|
+
|
|
129
|
+
const emitProgress = (): void => {
|
|
130
|
+
opts.onProgress?.({
|
|
131
|
+
adapterID: adapter.id,
|
|
132
|
+
yielded,
|
|
133
|
+
written,
|
|
134
|
+
bytes,
|
|
135
|
+
elapsed_ms: performance.now() - t0,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
for await (const row of adapter.rows(adapterOptions)) {
|
|
141
|
+
if (adapterOptions.signal?.aborted) {
|
|
142
|
+
throw new DOMException("Adapter run aborted by signal", "AbortError")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
yielded++
|
|
146
|
+
assertEmittedRow(adapter, row)
|
|
147
|
+
|
|
148
|
+
const stamped: CanonicalRow = { ...row, corpus_version: corpusVersion }
|
|
149
|
+
const key = canonicalDedupKey(stamped)
|
|
150
|
+
|
|
151
|
+
if (!dedupExhausted) {
|
|
152
|
+
if (seen.has(key)) {
|
|
153
|
+
if (yielded % progressEvery === 0) {
|
|
154
|
+
emitProgress()
|
|
155
|
+
}
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (seen.size >= DEDUP_MAX_SIZE) {
|
|
160
|
+
dedupExhausted = true
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
` runner: dedup set full at ${DEDUP_MAX_SIZE.toLocaleString()} — skipping dedup for remaining rows\n`
|
|
163
|
+
)
|
|
164
|
+
} else {
|
|
165
|
+
seen.add(key)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const line = `${JSON.stringify(stamped)}\n`
|
|
170
|
+
hasher.update(line)
|
|
171
|
+
bytes += Buffer.byteLength(line, "utf8")
|
|
172
|
+
written++
|
|
173
|
+
|
|
174
|
+
if (!stream.write(line)) {
|
|
175
|
+
await once(stream, "drain")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (yielded % progressEvery === 0) {
|
|
179
|
+
emitProgress()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
stream.end()
|
|
184
|
+
await once(stream, "close")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const endedAt = new Date()
|
|
188
|
+
const elapsed_ms = performance.now() - t0
|
|
189
|
+
emitProgress()
|
|
190
|
+
|
|
191
|
+
const manifest: AdapterRunManifest = {
|
|
192
|
+
adapter_id: adapter.id,
|
|
193
|
+
corpus_version: corpusVersion,
|
|
194
|
+
default_license: adapter.defaultLicense,
|
|
195
|
+
description: adapter.description,
|
|
196
|
+
yielded,
|
|
197
|
+
written,
|
|
198
|
+
deduped: yielded - written,
|
|
199
|
+
bytes,
|
|
200
|
+
sha256: hasher.digest(),
|
|
201
|
+
jsonl_path: jsonlPath,
|
|
202
|
+
started_at: startedAt.toISOString(),
|
|
203
|
+
ended_at: endedAt.toISOString(),
|
|
204
|
+
elapsed_ms,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
|
208
|
+
|
|
209
|
+
return manifest
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Drive every adapter in a registry sequentially. Stops on the first failure (caller can filter the registry before
|
|
214
|
+
* calling if partial-failure is desired).
|
|
215
|
+
*
|
|
216
|
+
* Returns the manifests in registry insertion order.
|
|
217
|
+
*/
|
|
218
|
+
export async function runAllAdapters(
|
|
219
|
+
registry: AdapterRegistry,
|
|
220
|
+
common: Omit<RunAdapterOptions, "adapter"> & { adapterOptionsFor?: (a: CorpusAdapter) => AdapterOptions }
|
|
221
|
+
): Promise<AdapterRunManifest[]> {
|
|
222
|
+
const out: AdapterRunManifest[] = []
|
|
223
|
+
|
|
224
|
+
for (const adapter of registry.list()) {
|
|
225
|
+
const adapterOptions = common.adapterOptionsFor?.(adapter) ?? common.adapterOptions
|
|
226
|
+
out.push(
|
|
227
|
+
await runAdapter({
|
|
228
|
+
...common,
|
|
229
|
+
adapter,
|
|
230
|
+
adapterOptions,
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return out
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate an emitted row. Cheap; runs once per row. Catches adapter bugs early so the JSONL doesn't end up
|
|
240
|
+
* half-malformed.
|
|
241
|
+
*/
|
|
242
|
+
function assertEmittedRow(adapter: CorpusAdapter, row: CanonicalRow): void {
|
|
243
|
+
if (row.source !== adapter.id) {
|
|
244
|
+
throw new Error(`adapter ${adapter.id}: row.source must equal adapter.id (got ${JSON.stringify(row.source)})`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!row.source_id) {
|
|
248
|
+
throw new Error(`adapter ${adapter.id}: row.source_id is empty`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!row.raw) {
|
|
252
|
+
throw new Error(`adapter ${adapter.id}: row.raw is empty for source_id=${row.source_id}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!row.country) {
|
|
256
|
+
throw new Error(`adapter ${adapter.id}: row.country is empty for source_id=${row.source_id}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!row.license) {
|
|
260
|
+
throw new Error(`adapter ${adapter.id}: row.license is empty for source_id=${row.source_id}`)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Promise-ify a single event emission. Used to await `drain` / `close` on the write stream. */
|
|
265
|
+
function once(emitter: WriteStream, event: "drain" | "close"): Promise<void> {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const onEvent = (): void => {
|
|
268
|
+
emitter.off("error", onError)
|
|
269
|
+
resolve()
|
|
270
|
+
}
|
|
271
|
+
const onError = (err: Error): void => {
|
|
272
|
+
emitter.off(event, onEvent)
|
|
273
|
+
reject(err)
|
|
274
|
+
}
|
|
275
|
+
emitter.once(event, onEvent)
|
|
276
|
+
emitter.once("error", onError)
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Convenience: ensure the parent directory of `filePath` exists. */
|
|
281
|
+
export async function ensureParentDir(filePath: string): Promise<void> {
|
|
282
|
+
await mkdir(dirname(filePath), { recursive: true })
|
|
283
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `anchor-absorption` shard recipe (#220/#723, Probe A1) — self-generates `--count` rows from
|
|
7
|
+
* {@link synthesizeAnchorAbsorptionRow}'s 6-slice mix (CASE-H / CASE-P-us-rural / CASE-P-de /
|
|
8
|
+
* anchor-fp / locale-ambig / standard), aligns each to BIO, and emits a labeled JSONL. The
|
|
9
|
+
* LEADING 5-digit on CASE-H/anchor-fp/locale-ambig is sampled from the REAL US ZIPs in the
|
|
10
|
+
* postcode-anchor lookup, so the shaped-painted anchor fires on it exactly as inference does —
|
|
11
|
+
* teaching the model to OVERRIDE a present anchor from context. Ported from
|
|
12
|
+
* scripts/build-anchor-absorption-shard.mjs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs"
|
|
16
|
+
|
|
17
|
+
import { dataRootPath } from "@mailwoman/core/utils"
|
|
18
|
+
|
|
19
|
+
import { stableSourceID } from "../adapter.ts"
|
|
20
|
+
import { alignRow } from "../align.ts"
|
|
21
|
+
import { synthesizeAnchorAbsorptionRow } from "../synthesize-anchor-absorption.ts"
|
|
22
|
+
import { makeLcg, type ShardRecipe } from "./scaffold.ts"
|
|
23
|
+
|
|
24
|
+
// The leading-5-digit source: the real US ZIPs in the postcode anchor's pilot lookup. Resolved through
|
|
25
|
+
// the data-root helper (the lab default is `$MAILWOMAN_DATA_ROOT`), never re-hardcoded.
|
|
26
|
+
const ANCHOR_LOOKUP = dataRootPath("anchor", "pilot-anchor-lookup.json")
|
|
27
|
+
|
|
28
|
+
/** The real US ZIPs in the anchor lookup (entries whose value is a `[{ US: … }]` candidate list). */
|
|
29
|
+
function loadRealUsZips(path: string): string[] {
|
|
30
|
+
const d = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>
|
|
31
|
+
const zips: string[] = []
|
|
32
|
+
|
|
33
|
+
for (const [pc, v] of Object.entries(d)) {
|
|
34
|
+
if (Array.isArray(v) && v[0] && typeof v[0] === "object" && "US" in (v[0] as object) && /^\d{5}$/.test(pc)) {
|
|
35
|
+
zips.push(pc)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return zips
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const anchorAbsorptionRecipe: ShardRecipe = {
|
|
43
|
+
name: "anchor-absorption",
|
|
44
|
+
description: "Anchor-absorption counter-augmentation (#220/#723): 6-slice mix → synthesizeAnchorAbsorptionRow",
|
|
45
|
+
mode: "generate",
|
|
46
|
+
async run(opts, write) {
|
|
47
|
+
// Emit PRNG: the legacy build-anchor-absorption-shard.mjs seeded an LCG (lcg(opts.seed)).
|
|
48
|
+
const random = makeLcg(opts.seed)
|
|
49
|
+
const source = opts.sourceName ?? "synth-anchor-absorption"
|
|
50
|
+
const count = opts.count ?? 50000
|
|
51
|
+
const realZips = loadRealUsZips(ANCHOR_LOOKUP)
|
|
52
|
+
console.error(`Loaded ${realZips.length} real US ZIPs from the anchor lookup (the leading-5-digit source).`)
|
|
53
|
+
|
|
54
|
+
let written = 0
|
|
55
|
+
let quarantined = 0
|
|
56
|
+
const byTemplate: Record<string, number> = {}
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const synth = synthesizeAnchorAbsorptionRow({ random, realZips })
|
|
60
|
+
const country = synth.locale.split("-")[1] // "en-US" -> "US", "de-DE" -> "DE"
|
|
61
|
+
const canonical = {
|
|
62
|
+
raw: synth.raw,
|
|
63
|
+
components: synth.components,
|
|
64
|
+
country,
|
|
65
|
+
locale: synth.locale,
|
|
66
|
+
source,
|
|
67
|
+
source_id: stableSourceID(source, `${i}` as unknown as Parameters<typeof stableSourceID>[1]),
|
|
68
|
+
}
|
|
69
|
+
const aligned = alignRow(canonical as Parameters<typeof alignRow>[0])
|
|
70
|
+
|
|
71
|
+
if (aligned.kind !== "labeled") {
|
|
72
|
+
quarantined++
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
write(
|
|
76
|
+
JSON.stringify({ ...aligned.row, synth_method: "anchor-absorption", synth_template: synth.template }) + "\n"
|
|
77
|
+
)
|
|
78
|
+
written++
|
|
79
|
+
byTemplate[synth.template] = (byTemplate[synth.template] ?? 0) + 1
|
|
80
|
+
}
|
|
81
|
+
console.error(`\nwrote ${written} rows (${quarantined} quarantined)`)
|
|
82
|
+
console.error(" by slice:", JSON.stringify(byTemplate))
|
|
83
|
+
|
|
84
|
+
return { emitted: written, skipped: quarantined }
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `boundary-stress` shard recipe (#375) — the boundary-instability augmentation. Self-generates
|
|
7
|
+
* `--count` rows from {@link synthesizeBoundaryStressRow}'s weighted template mix (the v1.7.0,
|
|
8
|
+
* DeepSeek-tuned 2026-06-18 composition), aligns each to BIO, and emits a labeled JSONL. The
|
|
9
|
+
* lever for the taxonomy's #1 parser family (the boundary-wobble class). Ported from
|
|
10
|
+
* scripts/build-boundary-stress-shard.mjs.
|
|
11
|
+
*
|
|
12
|
+
* `synthesizeBoundaryStressRow` is NOT re-exported from the corpus index — imported directly here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { alignRow } from "../align.ts"
|
|
16
|
+
import { type BoundaryStressTemplate, synthesizeBoundaryStressRow } from "../synthesize-boundary-stress.ts"
|
|
17
|
+
import { makeMulberry32, type ShardRecipe, shardSourceID } from "./scaffold.ts"
|
|
18
|
+
|
|
19
|
+
// Revised composition (v1.7.0, DeepSeek-tuned 2026-06-18): `bare-locality` ~11% (recover the 84% locality
|
|
20
|
+
// drop on bare "City, STATE" rows WITHOUT becoming a locality-first majority), and
|
|
21
|
+
// house-number-before:after = 7:3 (FR's dominant order is number-BEFORE; 30% after breaks the order-bias
|
|
22
|
+
// shortcut without risking FR hn-before accuracy). The three original non-number shapes keep the bulk.
|
|
23
|
+
// Weights sum to 1.0. Key order is load-bearing — it drives the cumulative thresholds below.
|
|
24
|
+
const WEIGHTS: Record<BoundaryStressTemplate, number> = {
|
|
25
|
+
"street-eats-affix": 0.22,
|
|
26
|
+
"comma-less-city-state": 0.22,
|
|
27
|
+
"fr-prefix": 0.18,
|
|
28
|
+
"bare-locality": 0.11,
|
|
29
|
+
"house-number-before-street": 0.189,
|
|
30
|
+
"house-number-after-street": 0.081,
|
|
31
|
+
}
|
|
32
|
+
const CUM: Array<[BoundaryStressTemplate, number]> = (() => {
|
|
33
|
+
let acc = 0
|
|
34
|
+
|
|
35
|
+
return (Object.entries(WEIGHTS) as Array<[BoundaryStressTemplate, number]>).map(
|
|
36
|
+
([t, w]) => [t, (acc += w)] as [BoundaryStressTemplate, number]
|
|
37
|
+
)
|
|
38
|
+
})()
|
|
39
|
+
function pickTemplate(r: () => number): BoundaryStressTemplate {
|
|
40
|
+
const x = r()
|
|
41
|
+
|
|
42
|
+
for (const [t, c] of CUM) if (x <= c) return t
|
|
43
|
+
|
|
44
|
+
return CUM[CUM.length - 1]![0]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const boundaryStressRecipe: ShardRecipe = {
|
|
48
|
+
name: "boundary-stress",
|
|
49
|
+
description: "Boundary-instability rows (#375): weighted template mix → synthesizeBoundaryStressRow → aligned BIO",
|
|
50
|
+
mode: "generate",
|
|
51
|
+
async run(opts, write) {
|
|
52
|
+
// Emit PRNG: the legacy build-boundary-stress-shard.mjs seeded mulberry32(opts.seed).
|
|
53
|
+
const random = makeMulberry32(opts.seed)
|
|
54
|
+
const count = opts.count ?? 20000
|
|
55
|
+
let emitted = 0
|
|
56
|
+
let skipped = 0
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < count; i++) {
|
|
59
|
+
const row = synthesizeBoundaryStressRow(undefined, { random, forceTemplate: pickTemplate(random) })
|
|
60
|
+
const country = row.locale.split("-")[1] ?? "US"
|
|
61
|
+
const source_id = shardSourceID("synth-boundary-stress", { ...row.components, v: String(i) })
|
|
62
|
+
const canonical = {
|
|
63
|
+
raw: row.raw,
|
|
64
|
+
components: row.components,
|
|
65
|
+
country,
|
|
66
|
+
locale: row.locale,
|
|
67
|
+
source: "synth-boundary-stress",
|
|
68
|
+
source_id,
|
|
69
|
+
corpus_version: "0.6.0",
|
|
70
|
+
license: "Synthetic — boundary-stress; derived from public-domain locality/region tuples",
|
|
71
|
+
}
|
|
72
|
+
const aligned = alignRow(canonical as Parameters<typeof alignRow>[0])
|
|
73
|
+
|
|
74
|
+
if (aligned.kind !== "labeled") {
|
|
75
|
+
skipped++
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
// Match the base corpus parquet schema: flat synth_method / synth_base_id, not a nested `synth`.
|
|
79
|
+
write(
|
|
80
|
+
JSON.stringify({ ...aligned.row, synth_method: `boundary-stress:${row.template}`, synth_base_id: null }) + "\n"
|
|
81
|
+
)
|
|
82
|
+
emitted++
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { emitted, skipped }
|
|
86
|
+
},
|
|
87
|
+
}
|