@mailwoman/corpus 10.6.0 → 10.7.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 (223) hide show
  1. package/data/PROVENANCE.md +24 -0
  2. package/data/address-source-register.json +18948 -8757
  3. package/data/corpus-source-inputs.json +4756 -0
  4. package/data/license-decisions.json +7260 -0
  5. package/data/licenses/whosonfirst-licenses.md +3 -2
  6. package/data/reviewed-ve-postcode-tuples.json +5 -72
  7. package/data/source-additions.json +2450 -0
  8. package/data/source-resolutions.json +87 -0
  9. package/data/training-manifests/v0.7.10-gb-county.effective.json +2335 -0
  10. package/data/training-manifests/v0.7.10-gb-county.json +1132 -0
  11. package/data/training-manifests/v0.7.2-address-systems.effective.json +3 -2
  12. package/data/training-manifests/v0.7.7-no-ppd.json +550 -0
  13. package/data/training-manifests/v0.7.8-rights-clean.json +538 -0
  14. package/data/training-manifests/v0.7.9-country-gap.json +1132 -0
  15. package/data/us-situs-sources.json +128 -0
  16. package/lib/adapters/cordis/adapter.ts +1 -1
  17. package/lib/adapters/libpostal-dictionary.ts +23 -3
  18. package/lib/adapters/osm/adapter.ts +42 -11
  19. package/lib/adapters.ts +14 -0
  20. package/lib/az/adapters/opendata/adapter.ts +389 -0
  21. package/lib/bs/adapters/gbpa/adapter.ts +248 -0
  22. package/lib/build/types.ts +16 -0
  23. package/lib/build.ts +21 -0
  24. package/lib/ee/adapters/ads/adapter.ts +18 -0
  25. package/lib/fo/adapters/adressur/adapter.ts +242 -0
  26. package/lib/gb/adapters/os-open/adapter.ts +12 -0
  27. package/lib/gb/county.ts +59 -0
  28. package/lib/gb/os-open/open/names.ts +41 -0
  29. package/lib/gu/adapters/bsp/adapter.ts +434 -0
  30. package/lib/im/adapters/land-registry/adapter.ts +340 -0
  31. package/lib/international/recipes/locale.ts +164 -19
  32. package/lib/li/adapters/llv/adapter.ts +246 -0
  33. package/lib/recipes/bare/postcode.ts +10 -34
  34. package/lib/recipes/country-balanced.ts +95 -78
  35. package/lib/recipes/intersection.ts +22 -30
  36. package/lib/recipes/po/box/cedex/recipe.ts +8 -5
  37. package/lib/recipes/po/box/cedex/sources.ts +2 -23
  38. package/lib/recipes/scaffold.ts +28 -3
  39. package/lib/recipes/sources.ts +112 -1
  40. package/lib/recipes/street/affix.ts +17 -29
  41. package/lib/recipes/sub/venue/context.ts +33 -9
  42. package/lib/recipes/sub/venue/sources.ts +54 -13
  43. package/lib/recipes/sub/venue.ts +68 -23
  44. package/lib/recipes/unit.ts +20 -7
  45. package/lib/registers.ts +33 -0
  46. package/lib/source-register/corpus-inputs.ts +528 -0
  47. package/lib/source-register/effective-manifest.ts +37 -5
  48. package/lib/source-register/types.ts +20 -0
  49. package/lib/south-asia/recipes/register.ts +1 -1
  50. package/lib/tools/overlay/manifest.ts +109 -1
  51. package/lib/tools/source-register/build.ts +47 -1
  52. package/lib/tools/variety-sample.ts +633 -0
  53. package/lib/us/adapters/usgov/nad/adapter.ts +39 -10
  54. package/lib/us/recipes/skeleton-sources.ts +156 -0
  55. package/lib/us/tools/nad-skeletons.ts +395 -0
  56. package/lib/uy/adapters/ide/adapter.ts +275 -0
  57. package/lib/ve/recipes/reviewed-postcode-tail.ts +29 -6
  58. package/out/adapters/cordis/adapter.d.ts +1 -1
  59. package/out/adapters/cordis/adapter.js +1 -1
  60. package/out/adapters/libpostal-dictionary.d.ts +9 -0
  61. package/out/adapters/libpostal-dictionary.d.ts.map +1 -1
  62. package/out/adapters/libpostal-dictionary.js +19 -3
  63. package/out/adapters/libpostal-dictionary.js.map +1 -1
  64. package/out/adapters/osm/adapter.d.ts +12 -1
  65. package/out/adapters/osm/adapter.d.ts.map +1 -1
  66. package/out/adapters/osm/adapter.js +30 -13
  67. package/out/adapters/osm/adapter.js.map +1 -1
  68. package/out/adapters.d.ts.map +1 -1
  69. package/out/adapters.js +14 -0
  70. package/out/adapters.js.map +1 -1
  71. package/out/az/adapters/opendata/adapter.d.ts +88 -0
  72. package/out/az/adapters/opendata/adapter.d.ts.map +1 -0
  73. package/out/az/adapters/opendata/adapter.js +282 -0
  74. package/out/az/adapters/opendata/adapter.js.map +1 -0
  75. package/out/bs/adapters/gbpa/adapter.d.ts +64 -0
  76. package/out/bs/adapters/gbpa/adapter.d.ts.map +1 -0
  77. package/out/bs/adapters/gbpa/adapter.js +187 -0
  78. package/out/bs/adapters/gbpa/adapter.js.map +1 -0
  79. package/out/build/types.d.ts +15 -0
  80. package/out/build/types.d.ts.map +1 -1
  81. package/out/build/types.js.map +1 -1
  82. package/out/build.d.ts.map +1 -1
  83. package/out/build.js +17 -0
  84. package/out/build.js.map +1 -1
  85. package/out/ee/adapters/ads/adapter.d.ts +6 -0
  86. package/out/ee/adapters/ads/adapter.d.ts.map +1 -1
  87. package/out/ee/adapters/ads/adapter.js +16 -0
  88. package/out/ee/adapters/ads/adapter.js.map +1 -1
  89. package/out/fo/adapters/adressur/adapter.d.ts +63 -0
  90. package/out/fo/adapters/adressur/adapter.d.ts.map +1 -0
  91. package/out/fo/adapters/adressur/adapter.js +177 -0
  92. package/out/fo/adapters/adressur/adapter.js.map +1 -0
  93. package/out/gb/adapters/os-open/adapter.d.ts +8 -0
  94. package/out/gb/adapters/os-open/adapter.d.ts.map +1 -1
  95. package/out/gb/adapters/os-open/adapter.js +11 -0
  96. package/out/gb/adapters/os-open/adapter.js.map +1 -1
  97. package/out/gb/county.d.ts +30 -0
  98. package/out/gb/county.d.ts.map +1 -0
  99. package/out/gb/county.js +51 -0
  100. package/out/gb/county.js.map +1 -0
  101. package/out/gb/os-open/open/names.d.ts +17 -0
  102. package/out/gb/os-open/open/names.d.ts.map +1 -1
  103. package/out/gb/os-open/open/names.js +19 -0
  104. package/out/gb/os-open/open/names.js.map +1 -1
  105. package/out/gu/adapters/bsp/adapter.d.ts +83 -0
  106. package/out/gu/adapters/bsp/adapter.d.ts.map +1 -0
  107. package/out/gu/adapters/bsp/adapter.js +346 -0
  108. package/out/gu/adapters/bsp/adapter.js.map +1 -0
  109. package/out/im/adapters/land-registry/adapter.d.ts +68 -0
  110. package/out/im/adapters/land-registry/adapter.d.ts.map +1 -0
  111. package/out/im/adapters/land-registry/adapter.js +261 -0
  112. package/out/im/adapters/land-registry/adapter.js.map +1 -0
  113. package/out/international/recipes/locale.d.ts +24 -4
  114. package/out/international/recipes/locale.d.ts.map +1 -1
  115. package/out/international/recipes/locale.js +107 -15
  116. package/out/international/recipes/locale.js.map +1 -1
  117. package/out/li/adapters/llv/adapter.d.ts +69 -0
  118. package/out/li/adapters/llv/adapter.d.ts.map +1 -0
  119. package/out/li/adapters/llv/adapter.js +181 -0
  120. package/out/li/adapters/llv/adapter.js.map +1 -0
  121. package/out/recipes/bare/postcode.d.ts.map +1 -1
  122. package/out/recipes/bare/postcode.js +9 -30
  123. package/out/recipes/bare/postcode.js.map +1 -1
  124. package/out/recipes/country-balanced.d.ts.map +1 -1
  125. package/out/recipes/country-balanced.js +82 -73
  126. package/out/recipes/country-balanced.js.map +1 -1
  127. package/out/recipes/intersection.d.ts +3 -2
  128. package/out/recipes/intersection.d.ts.map +1 -1
  129. package/out/recipes/intersection.js +20 -26
  130. package/out/recipes/intersection.js.map +1 -1
  131. package/out/recipes/po/box/cedex/recipe.d.ts.map +1 -1
  132. package/out/recipes/po/box/cedex/recipe.js +5 -4
  133. package/out/recipes/po/box/cedex/recipe.js.map +1 -1
  134. package/out/recipes/po/box/cedex/sources.d.ts +2 -45
  135. package/out/recipes/po/box/cedex/sources.d.ts.map +1 -1
  136. package/out/recipes/po/box/cedex/sources.js +0 -20
  137. package/out/recipes/po/box/cedex/sources.js.map +1 -1
  138. package/out/recipes/scaffold.d.ts +15 -2
  139. package/out/recipes/scaffold.d.ts.map +1 -1
  140. package/out/recipes/scaffold.js +21 -1
  141. package/out/recipes/scaffold.js.map +1 -1
  142. package/out/recipes/sources.d.ts.map +1 -1
  143. package/out/recipes/sources.js +112 -1
  144. package/out/recipes/sources.js.map +1 -1
  145. package/out/recipes/street/affix.d.ts.map +1 -1
  146. package/out/recipes/street/affix.js +8 -20
  147. package/out/recipes/street/affix.js.map +1 -1
  148. package/out/recipes/sub/venue/context.d.ts +14 -3
  149. package/out/recipes/sub/venue/context.d.ts.map +1 -1
  150. package/out/recipes/sub/venue/context.js +29 -9
  151. package/out/recipes/sub/venue/context.js.map +1 -1
  152. package/out/recipes/sub/venue/sources.d.ts +41 -8
  153. package/out/recipes/sub/venue/sources.d.ts.map +1 -1
  154. package/out/recipes/sub/venue/sources.js +26 -7
  155. package/out/recipes/sub/venue/sources.js.map +1 -1
  156. package/out/recipes/sub/venue.d.ts.map +1 -1
  157. package/out/recipes/sub/venue.js +35 -14
  158. package/out/recipes/sub/venue.js.map +1 -1
  159. package/out/recipes/unit.d.ts +4 -0
  160. package/out/recipes/unit.d.ts.map +1 -1
  161. package/out/recipes/unit.js +7 -6
  162. package/out/recipes/unit.js.map +1 -1
  163. package/out/registers.d.ts +33 -0
  164. package/out/registers.d.ts.map +1 -1
  165. package/out/registers.js +33 -0
  166. package/out/registers.js.map +1 -1
  167. package/out/source-register/corpus-inputs.d.ts +240 -0
  168. package/out/source-register/corpus-inputs.d.ts.map +1 -0
  169. package/out/source-register/corpus-inputs.js +275 -0
  170. package/out/source-register/corpus-inputs.js.map +1 -0
  171. package/out/source-register/effective-manifest.d.ts +2 -0
  172. package/out/source-register/effective-manifest.d.ts.map +1 -1
  173. package/out/source-register/effective-manifest.js +33 -4
  174. package/out/source-register/effective-manifest.js.map +1 -1
  175. package/out/source-register/types.d.ts +21 -1
  176. package/out/source-register/types.d.ts.map +1 -1
  177. package/out/source-register/types.js +20 -0
  178. package/out/source-register/types.js.map +1 -1
  179. package/out/south-asia/recipes/register.js +1 -1
  180. package/out/south-asia/recipes/register.js.map +1 -1
  181. package/out/tools/overlay/manifest.d.ts +7 -0
  182. package/out/tools/overlay/manifest.d.ts.map +1 -1
  183. package/out/tools/overlay/manifest.js +72 -1
  184. package/out/tools/overlay/manifest.js.map +1 -1
  185. package/out/tools/source-register/build.d.ts +9 -0
  186. package/out/tools/source-register/build.d.ts.map +1 -1
  187. package/out/tools/source-register/build.js +25 -3
  188. package/out/tools/source-register/build.js.map +1 -1
  189. package/out/tools/variety-sample.d.ts +196 -0
  190. package/out/tools/variety-sample.d.ts.map +1 -0
  191. package/out/tools/variety-sample.js +417 -0
  192. package/out/tools/variety-sample.js.map +1 -0
  193. package/out/us/adapters/usgov/nad/adapter.d.ts +80 -0
  194. package/out/us/adapters/usgov/nad/adapter.d.ts.map +1 -1
  195. package/out/us/adapters/usgov/nad/adapter.js +27 -8
  196. package/out/us/adapters/usgov/nad/adapter.js.map +1 -1
  197. package/out/us/recipes/skeleton-sources.d.ts +92 -0
  198. package/out/us/recipes/skeleton-sources.d.ts.map +1 -0
  199. package/out/us/recipes/skeleton-sources.js +121 -0
  200. package/out/us/recipes/skeleton-sources.js.map +1 -0
  201. package/out/us/tools/nad-skeletons.d.ts +120 -0
  202. package/out/us/tools/nad-skeletons.d.ts.map +1 -0
  203. package/out/us/tools/nad-skeletons.js +262 -0
  204. package/out/us/tools/nad-skeletons.js.map +1 -0
  205. package/out/uy/adapters/ide/adapter.d.ts +64 -0
  206. package/out/uy/adapters/ide/adapter.d.ts.map +1 -0
  207. package/out/uy/adapters/ide/adapter.js +206 -0
  208. package/out/uy/adapters/ide/adapter.js.map +1 -0
  209. package/out/ve/recipes/reviewed-postcode-tail.d.ts +6 -2
  210. package/out/ve/recipes/reviewed-postcode-tail.d.ts.map +1 -1
  211. package/out/ve/recipes/reviewed-postcode-tail.js +16 -5
  212. package/out/ve/recipes/reviewed-postcode-tail.js.map +1 -1
  213. package/package.json +14 -14
  214. package/lib/recipes/unit/sources.ts +0 -44
  215. package/lib/tools/fetch/ppd.ts +0 -121
  216. package/out/recipes/unit/sources.d.ts +0 -28
  217. package/out/recipes/unit/sources.d.ts.map +0 -1
  218. package/out/recipes/unit/sources.js +0 -30
  219. package/out/recipes/unit/sources.js.map +0 -1
  220. package/out/tools/fetch/ppd.d.ts +0 -15
  221. package/out/tools/fetch/ppd.d.ts.map +0 -1
  222. package/out/tools/fetch/ppd.js +0 -86
  223. package/out/tools/fetch/ppd.js.map +0 -1
@@ -9,13 +9,14 @@
9
9
 
10
10
  import { dataRootPath } from "@mailwoman/core/data-root"
11
11
  import { readLocalBuffer, readLocalJSONFile } from "@mailwoman/core/fs/readers"
12
- import { statPath } from "@mailwoman/core/fs/readers/stat"
12
+ import { pathExists, statPath } from "@mailwoman/core/fs/readers/stat"
13
13
  import { writeLocalJSONFile } from "@mailwoman/core/fs/writers"
14
14
  import { sha256Hex } from "@mailwoman/core/hash"
15
15
  import { stringifyJSON } from "@mailwoman/core/json"
16
16
  import { basename, dirname, PathBuilder, type PathBuilderLike } from "path-ts"
17
17
 
18
18
  import { escapeSQLString, openDuckDB } from "#parquet/duckdb"
19
+ import { openSourceRightsAdmission, type SourceRightsRecord } from "#source-register/corpus-inputs"
19
20
  import type { SplitName } from "#utils/split"
20
21
 
21
22
  interface ParquetFileDescriptor {
@@ -53,6 +54,10 @@ interface BaseManifest {
53
54
  total_rows: number
54
55
  [PRE_RENAME_FILES_KEY]?: ManifestFile[]
55
56
  [PRE_RENAME_ROWS_PER_FILE_KEY]?: unknown
57
+ /**
58
+ * The source-rights record a build or assembly wrote when it checked its sources.
59
+ */
60
+ source_rights?: SourceRightsRecord
56
61
  }
57
62
 
58
63
  /**
@@ -251,6 +256,106 @@ export interface OverlayManifestOptions {
251
256
  * Each source in this list must appear in the base, and its rows leave the split counts.
252
257
  */
253
258
  replaceSources?: readonly string[]
259
+ /**
260
+ * Why a local experiment admits sources whose register rows lack an elected
261
+ * decision permitting `transform` and `train`.
262
+ *
263
+ * Without it the assembly refuses them, and with it the manifest records the corpus as not shippable.
264
+ */
265
+ unshippableReason?: string | null
266
+ }
267
+
268
+ /**
269
+ * Reads the distinct `source` values of base files whose manifest entries name no source.
270
+ *
271
+ * Every file must be present under the local data root.
272
+ * When one is absent, or a row's `source` is null, `sources` is null and `unreadable` names the cause.
273
+ *
274
+ * The caller therefore cannot mistake a partial read for the whole base.
275
+ */
276
+ async function readBaseFileSources(
277
+ files: readonly ManifestFile[]
278
+ ): Promise<{ sources: string[]; unreadable: null } | { sources: null; unreadable: string }> {
279
+ const paths = files.map((file) => localManifestFilePath(file.path))
280
+ const absent: string[] = []
281
+
282
+ for (const path of paths) {
283
+ if (!(await pathExists(path))) {
284
+ absent.push(path)
285
+ }
286
+ }
287
+
288
+ if (absent.length) {
289
+ return { sources: null, unreadable: `${absent.length} of them are absent locally (first: ${absent[0]})` }
290
+ }
291
+
292
+ using db = await openDuckDB()
293
+
294
+ const list = paths.map((path) => `'${escapeSQLString(path)}'`).join(", ")
295
+ const result = await db.runAndReadAll(`SELECT DISTINCT source FROM read_parquet([${list}])`)
296
+ const values = result.getRowObjects().map((row) => row.source as string | null)
297
+
298
+ if (values.some((value) => value == null)) {
299
+ return { sources: null, unreadable: "a row among them carries a null source" }
300
+ }
301
+
302
+ return { sources: values as string[], unreadable: null }
303
+ }
304
+
305
+ /**
306
+ * Checks every corpus source the assembled manifest will hold, before the manifest or plan is written.
307
+ *
308
+ * The base manifest's own record covers a base file that records no source, when the base has a record.
309
+ * A base without a record is checked by reading its files' `source` column.
310
+ *
311
+ * When a file cannot be read, the check refuses the base rather than reading the absence as clear.
312
+ */
313
+ async function checkOverlaySources(
314
+ args: OverlayManifestOptions,
315
+ base: BaseManifest,
316
+ kept: readonly ManifestFile[]
317
+ ): Promise<SourceRightsRecord> {
318
+ const rights = await openSourceRightsAdmission({
319
+ context: `overlay assembly ${args.version}`,
320
+ unshippableReason: args.unshippableReason ?? null,
321
+ })
322
+
323
+ const named = new Set<string>(args.files.map((file) => file.source))
324
+ const unnamed = kept.filter((file) => file.source === undefined)
325
+
326
+ for (const file of kept) {
327
+ if (file.source !== undefined) {
328
+ named.add(file.source)
329
+ }
330
+ }
331
+
332
+ if (unnamed.length && base.source_rights) {
333
+ for (const source of base.source_rights.sources_checked) {
334
+ named.add(source)
335
+ }
336
+ }
337
+
338
+ // A base without a record is read instead: its files' own `source` column is what the loader groups rows by.
339
+ const read = unnamed.length && !base.source_rights ? await readBaseFileSources(unnamed) : null
340
+
341
+ for (const source of read?.sources ?? []) {
342
+ named.add(source)
343
+ }
344
+
345
+ rights.admitAll(named)
346
+
347
+ if (unnamed.length && !base.source_rights && read?.unreadable) {
348
+ rights.admitUnverified(
349
+ `${unnamed.length} file(s) of base ${args.base}, which name no source while the base manifest carries no ` +
350
+ `\`source_rights\` record, and ${read.unreadable}`
351
+ )
352
+ }
353
+
354
+ if (base.source_rights && !base.source_rights.shippable) {
355
+ rights.admitUnverified(`base ${args.base}, whose own record reads not shippable`)
356
+ }
357
+
358
+ return rights.record()
254
359
  }
255
360
 
256
361
  /**
@@ -357,6 +462,8 @@ export async function assembleOverlayManifest(args: OverlayManifestOptions): Pro
357
462
  .filter((s) => !dropped.includes(s))
358
463
  .map((s) => ({ ...s, path: rerootBaseFilePath(s.path, args.base) }))
359
464
 
465
+ const sourceRights = await checkOverlaySources(args, base, kept)
466
+
360
467
  const added: ParquetFileDescriptor[] = []
361
468
 
362
469
  const newDir = PathBuilder.from(args.newDir)
@@ -416,6 +523,7 @@ export async function assembleOverlayManifest(args: OverlayManifestOptions): Pro
416
523
  licenses_cover:
417
524
  `no license set is measured here. The base build's MANIFEST.json covers the rows it aligned, and ` +
418
525
  `these ${added.length} added file(s) carry their own: ${[...new Set(args.files.map((file) => file.source))].toSorted().join(", ")}.`,
526
+ source_rights: sourceRights,
419
527
  }
420
528
 
421
529
  const out = newDir("MANIFEST.json")
@@ -89,7 +89,9 @@ const RESEARCH_PASS_BY_ORIGIN: Readonly<Record<string, ResearchPass>> = {
89
89
  "2026-09-30_rights_review": ResearchPass.RightsReview,
90
90
  "2026-10-01_rights_review": ResearchPass.RightsReview,
91
91
  "2026-10-02_rights_review": ResearchPass.RightsReview,
92
+ "2026-10-17_rights_review": ResearchPass.RightsReview,
92
93
  "2026-10-03_decoder_exposure": ResearchPass.DecoderExposureSurvey,
94
+ "2026-10-17_corpus_input_mapping": ResearchPass.CorpusInputMapping,
93
95
  }
94
96
 
95
97
  const GEOMETRY_BY_NAME: Readonly<Record<string, SourceGeometry>> = {
@@ -169,6 +171,12 @@ const LICENSE_DECISIONS: ReadonlyArray<readonly [statement: string, licenseID: s
169
171
  "unchecked-access-licensed-or-query",
170
172
  "An access label: licensed, or answerable per query. It states no terms.",
171
173
  ],
174
+ [
175
+ "CORPUS INPUT, TERMS UNREAD",
176
+ "unchecked-corpus-input",
177
+ "A corpus source reads this publication, and the row exists so that its terms show as unread rather than absent. " +
178
+ "No reviewer has recorded a reading of them.",
179
+ ],
172
180
  ]
173
181
 
174
182
  /**
@@ -252,6 +260,15 @@ export interface BuildSourceRegisterOptions {
252
260
  * A path that does not exist is read as no resolutions recorded.
253
261
  */
254
262
  resolutionsPath?: PathBuilderLike
263
+ /**
264
+ * Source rows a later pass added in the research CSV's own columns, read after the CSV's rows.
265
+ *
266
+ * `source-additions.json` holds the publications a corpus source reads that no research pass recorded.
267
+ * They sit in this repository because the corpus-source table that names them does,
268
+ * and they are read as CSV rows so their ids and license scopes come from the same derivation.
269
+ * A path that does not exist is read as no additions.
270
+ */
271
+ additionsPath?: PathBuilderLike
255
272
  version: string
256
273
  /**
257
274
  * ISO 8601 calendar date the research pass was taken, `yyyy-MM-DD`.
@@ -435,8 +452,36 @@ function scopedLicenseID(
435
452
  return licenseID
436
453
  }
437
454
 
455
+ /**
456
+ * The shape of `source-additions.json`: rows in the research CSV's column names.
457
+ */
458
+ interface SourceAdditionsFile {
459
+ rows?: ReadonlyArray<Readonly<Record<string, string>>>
460
+ }
461
+
462
+ /**
463
+ * The research CSV's rows followed by the recorded additions.
464
+ *
465
+ * An absent additions file contributes zero rows.
466
+ * A file that exists and cannot be parsed raises, because reading it as empty
467
+ * would drop rows without saying so.
468
+ */
469
+ async function* researchRows(
470
+ sourcesPath: PathBuilderLike,
471
+ additionsPath: PathBuilderLike | undefined
472
+ ): AsyncGenerator<Readonly<Record<string, string | undefined>>> {
473
+ yield* readCSVRecords(sourcesPath)
474
+
475
+ if (!additionsPath || !(await pathExists(additionsPath))) return
476
+
477
+ const file = await readLocalJSONFile<SourceAdditionsFile>(additionsPath)
478
+
479
+ yield* file.rows ?? []
480
+ }
481
+
438
482
  async function readSources(
439
483
  sourcesPath: PathBuilderLike,
484
+ additionsPath: PathBuilderLike | undefined,
440
485
  licenseByStatement: ReadonlyMap<string, string>,
441
486
  fired: Set<string>,
442
487
  scopes: Map<string, LicenseScope>
@@ -446,7 +491,7 @@ async function readSources(
446
491
  let discoveryRailRowsDropped = 0
447
492
  let row = 1
448
493
 
449
- for await (const record of readCSVRecords(sourcesPath)) {
494
+ for await (const record of researchRows(sourcesPath, additionsPath)) {
450
495
  row++
451
496
 
452
497
  const origin = required(record["origin"], "origin", row)
@@ -733,6 +778,7 @@ export async function buildSourceRegister(options: BuildSourceRegisterOptions):
733
778
 
734
779
  const { sources: derived, discoveryRailRowsDropped } = await readSources(
735
780
  options.sourcesPath,
781
+ options.additionsPath,
736
782
  licenseByStatement,
737
783
  fired,
738
784
  scopes