@mailwoman/bdc 8.6.0 → 9.0.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/sdk/build-bdc.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The `bdc.db` builder (2a Task 8) — ingests parsed FCC BDC availability rows (Task 6's
7
- * {@link BDCAvailabilityRow}) into Task 7's schema (`schema.ts`), producing a sealed layer database.
6
+ * The `bdc.db` builder — ingests parsed FCC BDC availability rows ({@link BDCAvailabilityRow})
7
+ * into the schema declared in `schema.ts`, producing a sealed layer database.
8
8
  *
9
9
  * Mirrors `mailwoman/gazetteer-pipeline/poi/build-poi.ts`'s shape closely: the same build-tuning
10
10
  * pragmas, the same single-pass `Map<number, number>` coverage aggregation taken during the load
@@ -66,11 +66,12 @@ import {
66
66
  writeLayerManifest,
67
67
  type LayerContractDatabase,
68
68
  } from "@mailwoman/core/layers"
69
+ import { tryParsingJSON } from "@mailwoman/core/objects"
69
70
  import { openBuiltDatabase, sealDatabase } from "@mailwoman/core/utils"
70
71
  import type { FilerDatabase } from "@mailwoman/filer"
71
72
  // `pickPrimaryFRN`/`readFRNFilingCandidates` are loaded via a LAZY `await import("@mailwoman/filer/sdk")`
72
73
  // inside `populateBDCProviderTable`, not a top-level runtime import — see that function's docstring
73
- // (review fix round 1, IMPORTANT-1). Only the TYPES are imported here; `import type` is fully erased, so
74
+ //. Only the TYPES are imported here; `import type` is fully erased, so
74
75
  // this line has zero runtime cost for every `@mailwoman/bdc` consumer that never populates providers.
75
76
  import type { FRN, ProviderListRow } from "@mailwoman/filer/sdk"
76
77
  import { shortCellToInt, type H3Cell } from "@mailwoman/spatial"
@@ -121,8 +122,8 @@ export interface BuildBDCOptions {
121
122
  */
122
123
  out: string
123
124
  /**
124
- * The FCC filing's `as_of_date` (e.g. from Task 5's `resolveLatestVintage`) — becomes the manifest's `sourceVintage`
125
- * AND `version` (BDC has no independent layer versioning yet, same deferral `build-poi.ts` makes for `release`).
125
+ * The FCC filing's `as_of_date` (e.g. from `resolveLatestVintage`) — becomes the manifest's `sourceVintage` AND
126
+ * `version` (BDC has no independent layer versioning yet, same deferral `build-poi.ts` makes for `release`).
126
127
  */
127
128
  asOfDate: string
128
129
  /**
@@ -144,24 +145,24 @@ export interface BuildBDCOptions {
144
145
  blockCentroids: (geoid: string) => { lat: number; lon: number } | undefined
145
146
  onProgress?: (message: string) => void
146
147
  /**
147
- * Provider-list rows (Task 3's {@link ProviderListRow}, `@mailwoman/filer/sdk`'s `parseProviderList`) — the test/CLI
148
- * seam for populating `bdc_provider` (2a decision 8 / 3a decision 6, Task 8). When ABSENT (the default),
149
- * `bdc_provider` stays empty and the rest of the build is byte-identical to the pre-Task-8 behavior: every code path
150
- * this option touches is gated behind `if (options.providers)`, so omitting it changes nothing. When present,
151
- * `buildBDCDatabase` groups rows by `providerID` and inserts one `bdc_provider` row per distinct provider — see
152
- * {@link BuildBDCOptions.filerDB} for how the primary FRN is picked when a provider carries more than one, and
153
- * `schema.ts`'s `BDCProviderTable` docstring for the full lossy-denormalization rationale (decision 6).
148
+ * Provider-list rows ({@link ProviderListRow}, `@mailwoman/filer/sdk`'s `parseProviderList`) — the test/CLI seam for
149
+ * populating `bdc_provider` (2a decision 8 / 3a decision 6). When ABSENT (the default), `bdc_provider` stays empty
150
+ * and the rest of the build is untouched: every code path this option touches is gated behind `if
151
+ * (options.providers)`, so omitting it changes nothing. When present, `buildBDCDatabase` groups rows by `providerID`
152
+ * and inserts one `bdc_provider` row per distinct provider — see {@link BuildBDCOptions.filerDB} for how the primary
153
+ * FRN is picked when a provider carries more than one, and `schema.ts`'s `BDCProviderTable` docstring for the full
154
+ * lossy-denormalization rationale (decision 6).
154
155
  */
155
156
  providers?: Iterable<ProviderListRow> | AsyncIterable<ProviderListRow>
156
157
  /**
157
158
  * Filer.db handle (`@mailwoman/filer`) used to resolve a multi-FRN provider's PRIMARY FRN via
158
159
  * `readFRNFilingCandidates` + `pickPrimaryFRN` (`@mailwoman/filer/sdk`, decision 6) — imported rather than
159
- * reimplemented, since Task 7's review found and fixed a temporal bug (the candidate query originally omitted the
160
- * `valid_to` half of the half-open predicate) that a fresh implementation here would reintroduce. Only actually
161
- * QUERIED for a `provider_id` whose rows carry more than one distinct `frn` — a single-FRN provider needs no lookup,
162
- * since its lone FRN is already primary by construction. Required whenever `providers` is given AND at least one
163
- * `provider_id` turns out to be multi-FRN; `buildBDCDatabase` throws a descriptive error naming the offending
164
- * `provider_id` if it's needed but missing, rather than silently picking an arbitrary FRN.
160
+ * reimplemented, because the candidate query needs BOTH halves of the half-open `valid_from`/`valid_to` predicate and
161
+ * a second implementation here would be a second place to drop the `valid_to` half. Only actually QUERIED for a
162
+ * `provider_id` whose rows carry more than one distinct `frn` — a single-FRN provider needs no lookup, since its lone
163
+ * FRN is already primary by construction. Required whenever `providers` is given AND at least one `provider_id` turns
164
+ * out to be multi-FRN; `buildBDCDatabase` throws a descriptive error naming the offending `provider_id` if it's
165
+ * needed but missing, rather than silently picking an arbitrary FRN.
165
166
  */
166
167
  filerDB?: DatabaseClient<FilerDatabase>
167
168
  /**
@@ -346,13 +347,9 @@ interface GeoJSONMultiPolygon {
346
347
  export function geometryCentroid(geometryJSON: string | null): { lat: number; lon: number } | undefined {
347
348
  if (!geometryJSON) return undefined
348
349
 
349
- let geometry: GeoJSONPolygon | GeoJSONMultiPolygon
350
+ const geometry = tryParsingJSON<GeoJSONPolygon | GeoJSONMultiPolygon>(geometryJSON)
350
351
 
351
- try {
352
- geometry = JSON.parse(geometryJSON)
353
- } catch {
354
- return undefined
355
- }
352
+ if (!geometry) return undefined
356
353
 
357
354
  const exteriorRings: number[][][] =
358
355
  geometry.type === "Polygon"
@@ -413,8 +410,8 @@ export function createTIGERBlockCentroidLookup(
413
410
  const PROVIDER_INSERT_BATCH_SIZE = 500
414
411
 
415
412
  /**
416
- * Groups `providers` by `providerID`. Task 3's `parseProviderList` yields one {@link ProviderListRow} PER LINE of the
417
- * source CSV, preserving cardinality (never folded, never last-wins — see that module's docstring) — so a `provider_id`
413
+ * Groups `providers` by `providerID`. `parseProviderList` yields one {@link ProviderListRow} PER LINE of the source
414
+ * CSV, preserving cardinality (never folded, never last-wins — see that module's docstring) — so a `provider_id`
418
415
  * appearing on N rows arrives here as N separate rows, exactly as decision 6 requires downstream.
419
416
  */
420
417
  async function groupProviderListRows(
@@ -436,7 +433,7 @@ async function groupProviderListRows(
436
433
  }
437
434
 
438
435
  /**
439
- * Populate `bdc_provider` from `options.providers` (2a decision 8 / 3a decision 6, Task 8) — see `schema.ts`'s
436
+ * Populate `bdc_provider` from `options.providers` (2a decision 8 / 3a decision 6) — see `schema.ts`'s
440
437
  * `BDCProviderTable` docstring for the full lossy-denormalization rationale. For each distinct `provider_id`:
441
438
  *
442
439
  * - Exactly one `frn` among its rows → that FRN is primary by construction; no `filerDB` query needed at all.
@@ -447,22 +444,22 @@ async function groupProviderListRows(
447
444
  * checks `candidates.length` first, mirroring `filerLookup`'s own `primary_frn: null` handling of the same case.
448
445
  * - `filerDB` is REQUIRED the instant a multi-FRN `provider_id` is encountered; its absence throws immediately, naming
449
446
  * the offending `provider_id`, rather than silently picking an arbitrary FRN.
450
- * - `holding_company` gets the IDENTICAL single-distinct-value shortcut `frn` gets (review fix round 1, IMPORTANT-3):
451
- * exactly one distinct non-null `holdingCompany` across a provider's rows means there's no conflict to resolve, so
452
- * it's populated directly, no rule needed. Two or more distinct values IS the real conflict decision 6 refuses to
453
- * paper over with last-wins — that case inserts NULL, and every value stays recoverable from `filer.db`. A `null`
454
- * `holdingCompany` on some rows doesn't count as a competing value (a row simply not stating it isn't a conflicting
455
- * assertion) — only distinct NON-NULL strings are compared.
447
+ * - `holding_company` gets the IDENTICAL single-distinct-value shortcut `frn` gets: exactly one distinct non-null
448
+ * `holdingCompany` across a provider's rows means there's no conflict to resolve, so it's populated directly, no rule
449
+ * needed. Two or more distinct values IS the real conflict decision 6 refuses to paper over with last-wins — that
450
+ * case inserts NULL, and every value stays recoverable from `filer.db`. A `null` `holdingCompany` on some rows
451
+ * doesn't count as a competing value (a row simply not stating it isn't a conflicting assertion) — only distinct
452
+ * NON-NULL strings are compared.
456
453
  *
457
454
  * `brand_name` is always inserted NULL — the provider list carries no brand-name column at all, so there is nothing to
458
455
  * populate it from, primary or otherwise (see the schema docstring).
459
456
  *
460
- * **Lazy `@mailwoman/filer/sdk` import (review fix round 1, IMPORTANT-1).** `readFRNFilingCandidates`/`pickPrimaryFRN`
461
- * are loaded via `await import("@mailwoman/filer/sdk")`, memoized in `filerSDK` below, rather than a top-level static
462
- * import — that barrel re-exports `cluster-filers.ts`, which pulls in `@mailwoman/match`/`record`/`registry`. A
463
- * top-level import regressed `@mailwoman/bdc`'s import time ~32% for EVERY consumer, including ones that never populate
464
- * providers at all; the dynamic import here only ever runs when a multi-FRN `provider_id` is actually encountered, so a
465
- * `providers`-less build (or one whose providers are all single-FRN) pays nothing.
457
+ * **Lazy `@mailwoman/filer/sdk` import.** `readFRNFilingCandidates`/`pickPrimaryFRN` are loaded via `await
458
+ * import("@mailwoman/filer/sdk")`, memoized in `filerSDK` below, rather than a top-level static import — that barrel
459
+ * re-exports `cluster-filers.ts`, which pulls in `@mailwoman/match`/`record`/`registry`. A top-level import regressed
460
+ * `@mailwoman/bdc`'s import time ~32% for EVERY consumer, including ones that never populate providers at all; the
461
+ * dynamic import here only ever runs when a multi-FRN `provider_id` is actually encountered, so a `providers`-less
462
+ * build (or one whose providers are all single-FRN) pays nothing.
466
463
  */
467
464
  async function populateBDCProviderTable(
468
465
  kdb: DatabaseClient<BDCDatabase>,
@@ -625,7 +622,7 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
625
622
  // all survive the staging dedup as distinct staged rows. In `includeLocationIDs` mode that's correct: every BSL
626
623
  // is a real, distinct row the caller asked to keep. In the default (NULL `location_id`) mode, when those BSLs
627
624
  // ALSO share identical speeds/flags, they'd otherwise materialize as byte-identical rows, inflating `result.rows`
628
- // and `layer_coverage.observed_rows` by the BSL count (found in review, ~100x at real scale) — `SELECT DISTINCT`
625
+ // and `layer_coverage.observed_rows` by the BSL count (~100x at real scale) — `SELECT DISTINCT`
629
626
  // over every column EXCEPT `location_id` collapses those byte-identical BSL duplicates down to one row.
630
627
  // IMPORTANT — this is NOT a guarantee of one row per (geoid, provider_id, technology_code) triple: BSLs at the
631
628
  // same triple with DIFFERING speeds/flags are NOT the same tuple, so `SELECT DISTINCT` does not merge them —
@@ -661,8 +658,8 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
661
658
  // Coverage cell MUST be derived as the res-9 cell's H3 hierarchy parent — NOT a second,
662
659
  // independent `latLngToCell(centroid, 6)` call. H3's cell hierarchy is not geometrically
663
660
  // exact: a point's directly-indexed res-6 cell and its res-9 cell's `cellToParent(…, 6)`
664
- // disagree for a real fraction of points (~6% empirically over CONUS, reviewer-verified
665
- // hexagon/pentagon boundary artifacts). Deriving both `h3_cell` and the coverage cell from
661
+ // disagree for a real fraction of points (~6% empirically over CONUS hexagon/pentagon
662
+ // boundary artifacts). Deriving both `h3_cell` and the coverage cell from
666
663
  // the SAME full res-9 index is what lets `filing-landscape.ts`'s reader reconstruct this
667
664
  // exact coverage cell from nothing but the stored `h3_cell` (its `res9ShortCellToRes6Parent`
668
665
  // applies `cellToParent` to the reconstructed res-9 cell) — builder and reader must derive
@@ -754,9 +751,9 @@ export async function buildBDCDatabase(options: BuildBDCOptions): Promise<BuildB
754
751
  createdAt: new Date().toISOString(),
755
752
  })
756
753
 
757
- // bdc_provider population (2a decision 8 / 3a decision 6, Task 8) — entirely additive and gated behind
758
- // `options.providers`: when absent, this block never runs and `bdc_provider` stays empty, exactly as before
759
- // Task 8 (see `BuildBDCOptions.providers`'s docstring the default-path byte-identical guarantee).
754
+ // bdc_provider population (2a decision 8 / 3a decision 6) — entirely additive and gated behind
755
+ // `options.providers`: when absent, this block never runs and `bdc_provider` stays empty (see
756
+ // `BuildBDCOptions.providers`'s docstring for the default-path guarantee).
760
757
  let providersPopulated = 0
761
758
 
762
759
  if (options.providers) {
package/sdk/common.ts CHANGED
@@ -170,8 +170,8 @@ export interface RawBDCFile {
170
170
  * 2-digit state or territory FIPS code.
171
171
  *
172
172
  * Loosely typed as `string` for now. The Nexus original was `AdminLevel1Code` (via `@isp.nexus/tiger`); this port
173
- * drops that dependency, same as `data-collection.ts`'s `FCCStateID`. Task 2c tightens this against
174
- * `@mailwoman/tiger` if a downstream dictionary needs the literal union.
173
+ * drops that dependency, same as `data-collection.ts`'s `FCCStateID`. Tighten it against `@mailwoman/tiger` if a
174
+ * downstream dictionary ever needs the literal union.
175
175
  *
176
176
  * Nullable in live data for rows not scoped to a specific state (e.g. Provider-category rows). Guarded in
177
177
  * {@linkcode parseRawBDCFile} — a null value parses to an empty `stateCode` string.
@@ -180,7 +180,7 @@ export interface RawBDCFile {
180
180
  /**
181
181
  * State or territory name.
182
182
  *
183
- * Loosely typed as `string` — the Nexus original was `StateName` (via `@isp.nexus/tiger`). Same task-2c deferral as
183
+ * Loosely typed as `string` — the Nexus original was `StateName` (via `@isp.nexus/tiger`). Same deferral as
184
184
  * `state_fips` above.
185
185
  */
186
186
  state_name: string
@@ -234,8 +234,8 @@ export interface BDCFile {
234
234
  /**
235
235
  * The state or territory FIPS code.
236
236
  *
237
- * Loosely typed as `string` — see {@linkcode RawBDCFile} for the task-2c deferral. Empty string when the raw
238
- * `state_fips` was `null`.
237
+ * Loosely typed as `string` — see {@linkcode RawBDCFile} for the deferral. Empty string when the raw `state_fips` was
238
+ * `null`.
239
239
  */
240
240
  stateCode: string
241
241
  /**
@@ -12,8 +12,8 @@ import type { BroadbandServicableLocationID } from "./location.ts"
12
12
  * Snake_case identifier for a US State or Territory.
13
13
  *
14
14
  * Loosely typed as `string` for now. The Nexus original derived this from `StateName` (via `@isp.nexus/tiger`); this
15
- * port drops that dependency since 2a doesn't consume the literal union anywhere. Task 2c tightens this against
16
- * `@mailwoman/tiger`'s `StateName` if a downstream dictionary needs it.
15
+ * port drops that dependency since nothing here consumes the literal union. Tighten it against `@mailwoman/tiger`'s
16
+ * `StateName` if a downstream dictionary ever needs it.
17
17
  *
18
18
  * @category FCC
19
19
  * @title FCC State Identifier
package/sdk/download.ts CHANGED
@@ -6,14 +6,14 @@
6
6
  *
7
7
  * Re-homed from Nexus's `sync/fcc/bdc/download-file.ts` (relicense-by-copy, no provenance headers),
8
8
  * trimmed hard: the Nexus original downloaded AND cached the `.zip`, extracted it, THEN wrote a Parquet
9
- * file with a row-count integrity check. All Parquet machinery is dropped here — 2a's `bdc.db` is
10
- * SQLite, not Parquet-backed (see Task 7+) — and the `.zip` itself isn't cached either; only the
9
+ * file with a row-count integrity check. All Parquet machinery is dropped here — `bdc.db` is
10
+ * SQLite, not Parquet-backed (`bdc/schema.ts`) — and the `.zip` itself isn't cached either; only the
11
11
  * extracted CSV is written to `destinationDir`, and its presence alone is the cache check.
12
12
  *
13
13
  * The zip-extraction library also changes: the Nexus original's `extractSingleFileZip` used `adm-zip`
14
14
  * (a repo-wide Nexus dependency). No unzip dependency exists anywhere in this repo — every workspace
15
- * `package.json` was checked, `tiger/` and `osm/` included — so `yauzl-promise` is added to `bdc` alone
16
- * (noted in this task's commit body per the brief).
15
+ * `package.json` was checked, `tiger/` and `osm/` included — so `yauzl-promise` is a `bdc`-only
16
+ * dependency.
17
17
  */
18
18
 
19
19
  import * as fs from "node:fs/promises"
@@ -3,7 +3,7 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `filing_landscape` reader (2a Task 9) — the FOUR PRE-REGISTERED ACCEPTANCE GATES this whole phase
6
+ * `filing_landscape` reader — the FOUR PRE-REGISTERED ACCEPTANCE GATES this whole phase
7
7
  * is judged by. See `filing-landscape.test.ts` for the gate tests; this module is only the reader.
8
8
  *
9
9
  * Coverage check (the meaning-of-zero rule): a queried block counts as SURVEYED only when its res-6
@@ -18,31 +18,24 @@
18
18
  * even for a cell with no filing rows of its own — a genuine "surveyed, zero providers here" result,
19
19
  * the meaning-of-zero rule's POSITIVE case (covered but empty is not the same as never surveyed).
20
20
  *
21
- * Res-9 res-6 parent reconstruction is deliberately NOT `@mailwoman/spatial`'s `expandH3Cell` — that
22
- * helper's left-shift reconstruction only round-trips a short cell that was shortened AT resolution
23
- * 15 (the address-id spine); fed a resolution-9 short cell it silently produces a full index
24
- * `cellToParent` rejects (`Cell arguments had incompatible resolutions`), verified empirically while
25
- * building this reader. The correct reconstruction for a short cell captured at a KNOWN resolution R
26
- * is a straight concatenation, not a shift: a full 64-bit H3 cell index is always
27
- * `"8" + <resolution nibble> + <52 bits of base-cell + digit path, trailing padding included>`, and
28
- * the 48-bit "short" form (`shortCellToInt`/`shortenH3Cell`) already carries exactly those low 52 bits
29
- * verbatim — so `"8" + R.toString(16) + shortHex.padStart(13, "0")` reassembles the identical full
30
- * index `latLngToCell` would have produced at resolution R. See {@link res9ShortCellToRes6Parent}.
21
+ * The res-6 parent is reconstructed from the STORED res-9 cell (`@mailwoman/spatial`'s `expandH3Cell`
22
+ * back to a full index, then `cellToParent`) rather than recomputed from the block centroid. See
23
+ * {@link res9ShortCellToRes6Parent}.
31
24
  *
32
- * This same formula is exactly what `build-bdc.ts` MUST use (and does, after fix round 1) to derive the
25
+ * This same formula is exactly what `build-bdc.ts` MUST use (and does) to derive the
33
26
  * coverage cell it writes at build time — H3's cell hierarchy is not geometrically exact, so a
34
27
  * `latLngToCell(centroid, 6)` computed independently of the stored res-9 cell disagrees with
35
28
  * `cellToParent(res9Cell, 6)` for a real fraction of points (verified ~6% over CONUS). Builder and
36
- * reader deriving the res-6 parent differently was a real bug (fix round 1): a genuinely-surveyed block
37
- * (real rows, real `layer_coverage` entry) could read back as `unknown_block_count` while its own rows
38
- * still populated `filings` a self-contradiction. `filings` is now scoped to units that PASS the
39
- * coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen again: a
29
+ * reader deriving the res-6 parent differently is a self-contradiction waiting to happen: a
30
+ * genuinely-surveyed block (real rows, real `layer_coverage` entry) reads back as
31
+ * `unknown_block_count` while its own rows still populate `filings`. `filings` is scoped to units that
32
+ * PASS the coverage check (see the `surveyedUnits` accumulator below) precisely so that can't happen: a
40
33
  * block excluded from `surveyed_block_count` never contributes to `filings` either.
41
34
  */
42
35
 
43
36
  import type { DatabaseClient } from "@mailwoman/core/kysley/client"
44
37
  import { readLayerCoverage, readLayerManifest, type LayerContractDatabase } from "@mailwoman/core/layers"
45
- import { shortCellToInt, type H3Cell } from "@mailwoman/spatial"
38
+ import { expandH3Cell, shortCellToInt, type H3Cell, type H3CellShort } from "@mailwoman/spatial"
46
39
  import { cellToParent } from "h3-js"
47
40
  import { sql, type Kysely } from "kysely"
48
41
 
@@ -155,13 +148,12 @@ function asContractDB(kdb: DatabaseClient<BDCDatabase>): Kysely<LayerContractDat
155
148
  }
156
149
 
157
150
  /**
158
- * Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why this
159
- * isn't `@mailwoman/spatial`'s `expandH3Cell`. Exported so tests can assert this agrees, cell-for-cell, with
160
- * `build-bdc.ts`'s own coverage-cell derivation (the two MUST share this exact formula — see that file's docstring).
151
+ * Reconstruct the res-6 ancestor of a res-9 short-cell int WITHOUT a centroid — see the module docstring for why the
152
+ * centroid is the wrong input. Exported so tests can assert this agrees, cell-for-cell, with `build-bdc.ts`'s own
153
+ * coverage-cell derivation (the two MUST share this derivation — see that file's docstring).
161
154
  */
162
155
  export function res9ShortCellToRes6Parent(h3CellShortInt: number): number {
163
- const shortHex = h3CellShortInt.toString(16).padStart(13, "0")
164
- const fullCell = `8${BDC_H3_RESOLUTION.toString(16)}${shortHex}` as H3Cell
156
+ const fullCell = expandH3Cell(h3CellShortInt.toString(16) as H3CellShort, BDC_H3_RESOLUTION)
165
157
  const parentCell = cellToParent(fullCell, BDC_COVERAGE_H3_RESOLUTION) as H3Cell
166
158
 
167
159
  return shortCellToInt(parentCell)
package/sdk/list-files.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * Re-homed from Nexus's `sync/fcc/bdc/list-files.ts` (relicense-by-copy, no provenance headers):
8
8
  * the `URLRoutePattern`-compiled route → a plain template literal (this port has no routing
9
9
  * dependency), the `$BCDClient`-bound `retrieveAvailabilityFiles()` → a plain function taking a
10
- * {@linkcode BDCClient}. Raw entries are parsed via Task 4's `parseRawBDCFile` and returned sorted
10
+ * {@linkcode BDCClient}. Raw entries are parsed via `parseRawBDCFile` and returned sorted
11
11
  * ascending by revision (`compareRevisionAsc`) rather than in API order.
12
12
  */
13
13
 
@@ -4,15 +4,14 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
6
  * `nearestInfrastructure` — a coverage-paired k-nearest read over the telecom-infrastructure POI
7
- * categories (`telecom_exchange`/`tower_comms`, poi-taxonomy task 1 + the `--source osm` extractor,
8
- * 2b tasks 1–2) that a caller can join against ANY layer's own `layer_coverage` survey-completeness
9
- * table (2b task 4, decision 7). Typical caller: a BDC filing scorer that wants "what's the nearest
10
- * real infrastructure to this claimed Broadband Serviceable Location, and does OUR layer even have
11
- * survey evidence for that area" in one call.
7
+ * categories (`telecom_exchange`/`tower_comms` — `@mailwoman/poi-taxonomy` categories, populated by the
8
+ * `--source osm` extractor) that a caller can join against ANY layer's own `layer_coverage`
9
+ * survey-completeness table (decision 7). Typical caller: a BDC filing scorer that wants "what's the
10
+ * nearest real infrastructure to this claimed Broadband Serviceable Location, and does OUR layer even
11
+ * have survey evidence for that area" in one call.
12
12
  *
13
- * Two deviations from the task brief's literal sketch, both forced by what `poi-lookup.ts` and
14
- * `@mailwoman/core/layers` actually expose (documented per the brief's "implementer picks and
15
- * documents" instruction):
13
+ * Two shapes below are not the obvious ones, and both are forced by what `poi-lookup.ts` and
14
+ * `@mailwoman/core/layers` actually expose:
16
15
  *
17
16
  * - **`poiLookup` is an already-open {@link POILookup}, not a `POILookupOpts` this function
18
17
  * constructs itself.** `POILookup`'s constructor eagerly loads the poi-taxonomy category dictionary
@@ -22,14 +21,14 @@
22
21
  * `poi.db` ONCE and reuse the same `POILookup` — this wrapper takes that shape: the caller owns
23
22
  * `POILookup`'s open/dispose lifecycle (`using poiLookup = new POILookup(...)`), we just call
24
23
  * `.search()` on it.
25
- * - **`nearestInfrastructure` is `async`, not the brief's sync sketch.** `readLayerCoverage`
24
+ * - **`nearestInfrastructure` is `async`, not sync.** `readLayerCoverage`
26
25
  * (`@mailwoman/core/layers`) is `Promise`-returning — every layer-contract read in this codebase is
27
26
  * (`readLayerManifest`, `filingLandscape` itself) — so pairing each POI hit with its coverage cell
28
27
  * means awaiting one `readLayerCoverage` call per hit. A sync signature can't await that.
29
28
  *
30
29
  * {@link res9ShortCellToRes6Parent} (exported by `filing-landscape.ts`; see that file's docstring for
31
- * why the res-9 res-6 reconstruction is a straight concatenation, not `@mailwoman/spatial`'s
32
- * `expandH3Cell`) turns each hit's res-9 cell into the res-6 cell every layer in this repo aggregates
30
+ * why the res-6 parent is reconstructed from the STORED res-9 cell rather than recomputed from the
31
+ * centroid) turns each hit's res-9 cell into the res-6 cell every layer in this repo aggregates
33
32
  * coverage at (poi.db's own convention; `bdc.db`'s `BDC_COVERAGE_H3_RESOLUTION` matches it
34
33
  * deliberately — see `schema.ts`). `POI_H3_RESOLUTION` (`poi-lookup.ts`) is ALSO 9, so the two layers'
35
34
  * spines agree without this module hardcoding a resolution of its own.
@@ -3,13 +3,13 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `plausibilityCheck` (2b task 5, spec §3.2/§4) — the heart of the BDC plausibility vertical. Composes
7
- * `filingLandscape` (2a) + `nearestInfrastructure` (2b task 4) into ONE evidence bundle over a single
8
- * broadband-service claim, under the registry-backed doctrine's positive-evidence-only invariant (spec
9
- * §4): a BDC filing or a nearby infrastructure hit can RAISE confidence; their absence can only ever
10
- * read as "unknown" or "no supporting evidence found, coverage permitting" — NEVER as "implausible."
11
- * The four §7-2b acceptance gates land in Task 6 as their own test file; this module is designed for
12
- * them but doesn't assert them itself.
6
+ * `plausibilityCheck` (spec §3.2/§4) — the heart of the BDC plausibility vertical. Composes
7
+ * `filingLandscape` (`bdc/sdk/filing-landscape.ts`) + `nearestInfrastructure` into ONE evidence bundle
8
+ * over a single broadband-service claim, under the registry-backed doctrine's positive-evidence-only
9
+ * invariant (spec §4): a BDC filing or a nearby infrastructure hit can RAISE confidence; their absence
10
+ * can only ever read as "unknown" or "no supporting evidence found, coverage permitting" — NEVER as
11
+ * "implausible." The four §7-2b acceptance gates are asserted in `plausibility.test.ts`'s
12
+ * `describe("§7-2b gates")` block; this module is designed for them but doesn't assert them itself.
13
13
  *
14
14
  * **Claim resolution (decision 4).** A claim's spatial key resolves two INDEPENDENT ways:
15
15
  *
@@ -88,25 +88,26 @@
88
88
  * filing coverage alone — it degrades to filing-covered → `"low"`, filing-unknown/absent →
89
89
  * `"insufficient_survey_data"`. Rationale: spec §4 frames physical co-presence as the vertical's
90
90
  * distinguishing "falsifier that only fires positively" and reserves `"high"` for genuine two-channel
91
- * corroboration (Gate 2 in Task 6: "matching filing + nearby plant in covered cells… high"); a tech
91
+ * corroboration (§7-2b Gate 2: "matching filing + nearby plant in covered cells… high"); a tech
92
92
  * that structurally can never offer a second channel should not be able to claim the same top
93
93
  * confidence a fiber claim earns by actually having one available. This reading is more conservative
94
- * than the alternative (treating the missing axis as inert and reporting `"high"` off filing alone)
95
- * flagged here for review since Task 6's four gates don't exercise a no-physical-falsifier tech code.
94
+ * than the alternative (treating the missing axis as inert and reporting `"high"` off filing alone),
95
+ * and nothing pins it: none of the four §7-2b gates exercises a no-physical-falsifier tech code.
96
96
  *
97
- * **Ledger note (task 4 review; extended task 5 fix round 1, finding 2) — the per-layer coverage-resolution sanity
98
- * check.** Neither bdc.db's nor poi.db's `layer_manifest` records the COVERAGE-cell h3 resolution (6) that
99
- * `res9ShortCellToRes6Parent` hardcodes on both sides — only each layer's ROW-spine resolution (9,
100
- * `spineKeys.h3.resolution`) is ever recorded. A real fix needs a layer-contract schema addition (out
101
- * of scope for this task; the same follow-up task 4's report already ticketed). What IS practical and
97
+ * **The per-layer coverage-resolution sanity check.** Neither bdc.db's nor poi.db's `layer_manifest`
98
+ * records the COVERAGE-cell h3 resolution (6) that `res9ShortCellToRes6Parent` hardcodes on both sides
99
+ * — only each layer's ROW-spine resolution (9, `spineKeys.h3.resolution`) is ever recorded. Closing
100
+ * that gap properly needs an addition to the layer contract itself (`@mailwoman/core/layers`), which
101
+ * does not exist yet. What IS practical and
102
102
  * cheap: each manifest is a single-row table already read at most once per call here, so whenever a layer is
103
103
  * WIRED — `bdcDB`, `poi`, or both, checked independently — {@link assertLayerSpineResolution} compares that one
104
104
  * layer's recorded `spineKeys.h3.resolution` directly against the `BDC_H3_RESOLUTION` constant `pointCell` is
105
105
  * actually derived from, and throws on a mismatch, catching a layer built at a different spine resolution before it
106
106
  * silently mis-joins a coverage cell. This is TWO-SIDED, not gated on both layers being present together: a
107
107
  * poi-only call still checks poi's own recorded resolution, since `readLayerCoverage`'s poi-side join key (below)
108
- * is derived from `BDC_H3_RESOLUTION` regardless of whether `bdcDB` is wired at all (the original one-sided version
109
- * compared the two manifests to each other, so it silently skipped poi entirely whenever `bdcDB` was absent). It
108
+ * is derived from `BDC_H3_RESOLUTION` regardless of whether `bdcDB` is wired at all comparing each manifest
109
+ * against the constant, rather than the two manifests against each other, is what makes a single-layer call
110
+ * checkable at all. It
110
111
  * can NOT catch a layer whose row spine is 9 but whose COVERAGE cells were derived at some OTHER resolution than 6
111
112
  * — that gap needs the schema addition, not a runtime assertion.
112
113
  */
@@ -169,12 +170,12 @@ export type PlausibilityEvidence =
169
170
  | { type: "abstain"; reason: PlausibilityAbstainReason; layer?: string }
170
171
 
171
172
  /**
172
- * One evidence channel's survey-completeness state for THIS claim, WITH the reason a non-`"covered"` state applies
173
- * task 5 fix round 1 (review finding 1): `coverage_confidence` alone folds several genuinely different situations into
174
- * the same `"low"`/`"insufficient_survey_data"` verdict (a tech with no physical falsifier at all vs. a real poi survey
175
- * gap vs. a geoid-only claim with no coordinate to search from), and Task 6's gates need to attribute WHICH one
176
- * applies. `"not_applicable"` and `"no_coordinate"` are only ever produced for the physical axis; the filing axis only
177
- * ever reaches `"covered"`, `"layer_missing"`, or `"cell_unsurveyed"`.
173
+ * One evidence channel's survey-completeness state for THIS claim, WITH the reason a non-`"covered"` state applies.
174
+ * `coverage_confidence` alone folds several genuinely different situations into the same
175
+ * `"low"`/`"insufficient_survey_data"` verdict (a tech with no physical falsifier at all vs. a real poi survey gap vs.
176
+ * a geoid-only claim with no coordinate to search from); this axis state is what tells them apart. `"not_applicable"`
177
+ * and `"no_coordinate"` are only ever produced for the physical axis; the filing axis only ever reaches `"covered"`,
178
+ * `"layer_missing"`, or `"cell_unsurveyed"`.
178
179
  */
179
180
  export type PlausibilityCoverageAxisState =
180
181
  | "covered"
@@ -203,7 +204,7 @@ export type PlausibilityCoverageAxisState =
203
204
 
204
205
  /**
205
206
  * Per-axis attribution for {@link PlausibilityBundle.coverage_confidence} — see {@link PlausibilityCoverageAxisState}.
206
- * Added alongside `coverage_confidence` (kept as-is for compatibility) rather than replacing it.
207
+ * Reported alongside `coverage_confidence`, never in place of it: the coarse field stays the stable public surface.
207
208
  */
208
209
  export interface PlausibilityCoverageDetail {
209
210
  filing: PlausibilityCoverageAxisState
@@ -215,8 +216,8 @@ export interface PlausibilityBundle {
215
216
  evidence_found: PlausibilityEvidence[]
216
217
  coverage_confidence: "high" | "low" | "insufficient_survey_data"
217
218
  /**
218
- * Per-axis WHY behind `coverage_confidence` — task 5 fix round 1 (finding 1). ALWAYS present, mirroring
219
- * `block_resolution`'s always-present discipline.
219
+ * Per-axis WHY behind `coverage_confidence`. ALWAYS present, mirroring `block_resolution`'s always-present
220
+ * discipline.
220
221
  */
221
222
  coverage_detail: PlausibilityCoverageDetail
222
223
  /**
@@ -245,7 +246,7 @@ export interface GeocodeLike {
245
246
  }
246
247
 
247
248
  /**
248
- * The already-open infra layer this scorer composes against Task 4's `nearestInfrastructure`. The caller owns BOTH
249
+ * The already-open infra layer this scorer composes against {@link nearestInfrastructure}. The caller owns BOTH
249
250
  * handles' open/dispose lifecycle (mirrors `nearestInfrastructure`'s own `using poiLookup = new POILookup(...)`
250
251
  * precedent). `contractDB` is used two ways: passed straight through to `nearestInfrastructure` (per-hit coverage), and
251
252
  * read directly here (the whole-cell coverage check this module needs for `coverage_confidence`, independent of whether
@@ -263,10 +264,9 @@ export interface PlausibilityDeps {
263
264
  }
264
265
 
265
266
  /**
266
- * Tech → physical-plant category mapping (exported per the task brief). Fiber implies the three
267
- * infrastructure-extension categories a fiber network plausibly touches; the three fixed-wireless codes imply a comms
268
- * tower; every other code maps to `[]` — no physical falsifier is claimed for it (see
269
- * {@link physicalCategoriesForTechnology}).
267
+ * Tech → physical-plant category mapping. Fiber implies the three infrastructure-extension categories a fiber network
268
+ * plausibly touches; the three fixed-wireless codes imply a comms tower; every other code maps to `[]` — no physical
269
+ * falsifier is claimed for it (see {@link physicalCategoriesForTechnology}).
270
270
  */
271
271
  export const PLAUSIBILITY_TECH_PHYSICAL_CATEGORIES: Readonly<Record<number, readonly string[]>> = {
272
272
  [BroadbandTechnologyCode.OpticalCarrierFiber]: ["telecom_exchange", "telecom_cabinet", "data_center"],
@@ -359,18 +359,17 @@ function combineCoverage(
359
359
  }
360
360
 
361
361
  /**
362
- * See the module docstring's "ledger note" section. Throws when a WIRED layer's manifest disagrees with
362
+ * See the module docstring's coverage-resolution note. Throws when a WIRED layer's manifest disagrees with
363
363
  * `BDC_H3_RESOLUTION` — the single constant `plausibilityCheck` actually uses at runtime to derive both the
364
364
  * filing-lookup cell (bdc side, via `pointCell`) and the coverage-cell join key `readLayerCoverage` is read against
365
365
  * (poi side, via `res9ShortCellToRes6Parent(pointCell)`).
366
366
  *
367
- * Task 5 fix round 1 (review finding 2): checked independently PER LAYER, whenever THAT layer is wired, rather than
368
- * only when both `bdcDB` and `poi` are wired together. The original one-sided assertion compared the two manifests to
369
- * EACH OTHER, which meant a poi-only call (no `bdcDB`) never checked poi's recorded resolution at all — even though
370
- * `pointCell` (computed unconditionally from `BDC_H3_RESOLUTION`) still drives the poi coverage-cell read below.
371
- * Comparing each layer directly against the constant is also strictly stronger than the retired manifest-vs-manifest
372
- * check: it catches a layer built under a since-changed `BDC_H3_RESOLUTION` even when the OTHER layer is absent
373
- * entirely, not just a disagreement between two present layers.
367
+ * Checked independently PER LAYER, whenever THAT layer is wired not only when `bdcDB` and `poi` are wired together. A
368
+ * poi-only call still needs poi's own recorded resolution checked, because `pointCell` is computed unconditionally from
369
+ * `BDC_H3_RESOLUTION` and still drives the poi coverage-cell read below. Comparing each layer directly against the
370
+ * constant, rather than the two manifests against each other, is also strictly stronger: it catches a layer built under
371
+ * a since-changed `BDC_H3_RESOLUTION` even when the OTHER layer is absent entirely, not just a disagreement between two
372
+ * present layers.
374
373
  */
375
374
  async function assertLayerSpineResolution(
376
375
  layer: "bdc" | "poi",
@@ -427,9 +426,9 @@ export async function plausibilityCheck(claim: PlausibilityClaim, deps: Plausibi
427
426
  ? shortCellToInt(latLngToCell(point.coordinates[1], point.coordinates[0], BDC_H3_RESOLUTION) as H3Cell)
428
427
  : undefined
429
428
 
430
- // Ledger note (task 4 review; task 5 fix round 1 finding 2): cheap, one-time per-layer sanity check — see the
431
- // module docstring. Runs independently per WIRED layer, not only when both are present a poi-only call still
432
- // joins poi's coverage table against a BDC_H3_RESOLUTION-derived cell (below) and must not do so unchecked.
429
+ // Cheap, one-time per-layer sanity check — see the module docstring's coverage-resolution note. Runs
430
+ // independently per WIRED layer, not only when both are present: a poi-only call still joins poi's coverage
431
+ // table against a BDC_H3_RESOLUTION-derived cell (below) and must not do so unchecked.
433
432
  if (deps.bdcDB) {
434
433
  await assertLayerSpineResolution("bdc", asContractDB(deps.bdcDB), BDC_H3_RESOLUTION)
435
434
  }
@@ -482,9 +481,8 @@ export async function plausibilityCheck(claim: PlausibilityClaim, deps: Plausibi
482
481
  } else if (!point) {
483
482
  // Geoid-only claim, no coordinate resolvable — see the module docstring's claim-resolution note. A real
484
483
  // capability gap, not a missing-layer abstain: no evidence entry is fabricated, but the axis still
485
- // degrades honestly for coverage_confidence (and now names ITS OWN reason in `coverage_detail`, distinct
486
- // from `"layer_missing"`, rather than folding into the same generic "unknown" task 5 fix round 1
487
- // finding 1).
484
+ // degrades honestly for coverage_confidence, naming ITS OWN reason in `coverage_detail` rather than
485
+ // folding into the same generic "unknown" as `"layer_missing"`.
488
486
  physicalCoverage = "no_coordinate"
489
487
  } else {
490
488
  const hits = await nearestInfrastructure(deps.poi.lookup, deps.poi.contractDB, {