@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.
Files changed (103) hide show
  1. package/package.json +59 -27
  2. package/src/adapter.ts +153 -0
  3. package/src/adapters/ban/adapter.ts +189 -0
  4. package/src/adapters/ban/street-decompose.ts +98 -0
  5. package/src/adapters/fcc-bdc/adapter.ts +188 -0
  6. package/src/adapters/geonames/adapter.ts +185 -0
  7. package/src/adapters/geonames-postal/adapter.ts +111 -0
  8. package/src/adapters/gnaf/adapter.ts +139 -0
  9. package/src/adapters/gnaf/assemble.ts +200 -0
  10. package/src/adapters/index.ts +135 -0
  11. package/src/adapters/openaddresses/adapter.ts +237 -0
  12. package/src/adapters/overture/adapter.ts +149 -0
  13. package/src/adapters/state-hi-schools/adapter.ts +171 -0
  14. package/src/adapters/state-ia-contractors/adapter.ts +148 -0
  15. package/src/adapters/state-ny-notaries/adapter.ts +169 -0
  16. package/src/adapters/state-tx-notaries/adapter.ts +160 -0
  17. package/src/adapters/synth-po-box/adapter.ts +197 -0
  18. package/src/adapters/tiger/adapter.ts +232 -0
  19. package/src/adapters/tiger/street-decompose.ts +120 -0
  20. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
  21. package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
  22. package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
  23. package/src/adapters/usgov-nad/adapter.ts +338 -0
  24. package/src/adapters/usgov-nppes/adapter.ts +169 -0
  25. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
  26. package/src/adapters/wof-admin-jp/adapter.ts +176 -0
  27. package/src/adapters/wof-admin-json/adapter.ts +280 -0
  28. package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
  29. package/src/align.ts +317 -0
  30. package/src/build.ts +348 -0
  31. package/src/codex/us-fips-state.ts +109 -0
  32. package/src/format.ts +14 -0
  33. package/src/golden.ts +147 -0
  34. package/src/index.ts +28 -0
  35. package/src/license.ts +48 -0
  36. package/src/parquet-wrapper/index.ts +9 -0
  37. package/src/parquet-wrapper/reader.ts +67 -0
  38. package/src/parquet-wrapper/schema.ts +80 -0
  39. package/src/parquet-wrapper/writer.ts +102 -0
  40. package/src/parquet.ts +360 -0
  41. package/src/runner.ts +283 -0
  42. package/src/shard-recipes/anchor-absorption.ts +86 -0
  43. package/src/shard-recipes/boundary-stress.ts +87 -0
  44. package/src/shard-recipes/country-balanced.ts +471 -0
  45. package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
  46. package/src/shard-recipes/fr-admin-split.ts +212 -0
  47. package/src/shard-recipes/fr-bare-street.ts +85 -0
  48. package/src/shard-recipes/fr-fragment.ts +304 -0
  49. package/src/shard-recipes/fr-order.ts +283 -0
  50. package/src/shard-recipes/german.ts +206 -0
  51. package/src/shard-recipes/house-venue.ts +78 -0
  52. package/src/shard-recipes/index.ts +76 -0
  53. package/src/shard-recipes/intersection.ts +617 -0
  54. package/src/shard-recipes/locale.ts +415 -0
  55. package/src/shard-recipes/nl-postcode.ts +105 -0
  56. package/src/shard-recipes/no-fragment.ts +206 -0
  57. package/src/shard-recipes/no-street-led.ts +130 -0
  58. package/src/shard-recipes/no-street.ts +78 -0
  59. package/src/shard-recipes/po-box-cedex.ts +853 -0
  60. package/src/shard-recipes/po-box.ts +111 -0
  61. package/src/shard-recipes/scaffold.ts +187 -0
  62. package/src/shard-recipes/si-bare-village.ts +88 -0
  63. package/src/shard-recipes/street-affix.ts +549 -0
  64. package/src/shard-recipes/street-bare.ts +90 -0
  65. package/src/shard-recipes/street.ts +74 -0
  66. package/src/shard-recipes/unit.ts +315 -0
  67. package/src/split.ts +251 -0
  68. package/src/synthesize-anchor-absorption.ts +263 -0
  69. package/src/synthesize-boundary-stress.ts +504 -0
  70. package/src/synthesize-german.ts +227 -0
  71. package/src/synthesize-house-venue.ts +192 -0
  72. package/src/synthesize-intersection.ts +205 -0
  73. package/src/synthesize-no-street.ts +341 -0
  74. package/src/synthesize-po-box.ts +307 -0
  75. package/src/synthesize-street.ts +264 -0
  76. package/src/synthesize.ts +872 -0
  77. package/src/tokenize.ts +68 -0
  78. package/src/tools/align-shard.ts +67 -0
  79. package/src/tools/audit.ts +352 -0
  80. package/src/tools/corpus-stats.ts +183 -0
  81. package/src/tools/fetch/ban.ts +288 -0
  82. package/src/tools/fetch/download.ts +119 -0
  83. package/src/tools/fetch/hrsa.ts +65 -0
  84. package/src/tools/fetch/imls-pls.ts +162 -0
  85. package/src/tools/fetch/index.ts +121 -0
  86. package/src/tools/fetch/nad.ts +304 -0
  87. package/src/tools/fetch/nppes.ts +177 -0
  88. package/src/tools/fetch/openaddresses.ts +380 -0
  89. package/src/tools/fetch/state-hi-schools.ts +217 -0
  90. package/src/tools/fetch/state-sources.ts +142 -0
  91. package/src/tools/fetch/tiger-full.ts +318 -0
  92. package/src/tools/golden-expand.ts +524 -0
  93. package/src/tools/golden-promote.ts +304 -0
  94. package/src/tools/index.ts +24 -0
  95. package/src/tools/ingest-csv.ts +420 -0
  96. package/src/tools/jsonl-to-parquet.ts +200 -0
  97. package/src/tools/lint-shard-vocab.ts +357 -0
  98. package/src/tools/lint-shard.ts +502 -0
  99. package/src/tools/overlay-manifest.ts +141 -0
  100. package/src/tools/shard-kryptonite.ts +131 -0
  101. package/src/tools/shard-translit.ts +244 -0
  102. package/src/types.ts +213 -0
  103. 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
+ }