@mailwoman/mcp 8.3.0 → 8.5.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/cli.ts CHANGED
@@ -6,14 +6,36 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
33
+ * `mailwoman_filer_family` (3b task 9) follows the IDENTICAL discipline, reusing the same two guards — `familyRollup`
34
+ * has no optional-dep abstain shape either (it throws on a bad `familyID`/`nodeID` XOR or a pre-`filer_family`
35
+ * schema), so filer.db is required unconditionally here too. Its result — always `FamilyRollup[]`, never `null` or
36
+ * a bare object — is passed through untouched: this data is who-owns-whom, and a silent reshape here would be a
37
+ * fabricated relationship claim.
38
+ *
17
39
  * ```sh
18
40
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
41
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
@@ -24,8 +46,10 @@ import { existsSync } from "node:fs"
24
46
  import { DatabaseSync } from "node:sqlite"
25
47
  import { parseArgs } from "node:util"
26
48
 
49
+ import { filingLandscape, plausibilityCheck, type BDCDatabase } from "@mailwoman/bdc"
27
50
  import { DatabaseClient } from "@mailwoman/core/kysley/client"
28
51
  import { readLayerManifest, type LayerContractDatabase } from "@mailwoman/core/layers"
52
+ import { familyRollup, filerLookup, toFRN, type FRN } from "@mailwoman/filer/sdk"
29
53
  import { NeuralAddressClassifier } from "@mailwoman/neural"
30
54
  import { getPOICategory } from "@mailwoman/poi-taxonomy"
31
55
  import { createWOFResolver, type Resolver } from "@mailwoman/resolver"
@@ -39,6 +63,13 @@ import {
39
63
  wofShardPaths,
40
64
  } from "mailwoman/resolver-backend"
41
65
 
66
+ import {
67
+ assertBDCDatabaseExists,
68
+ assertFilerDatabaseExists,
69
+ openBDCDatabaseIfPresent,
70
+ openFilerDatabaseIfPresent,
71
+ openPlausibilityPOIDeps,
72
+ } from "./layer-guards.ts"
42
73
  import { createMCPServer } from "./server.ts"
43
74
  import type { MCPToolDeps } from "./tools.ts"
44
75
 
@@ -117,6 +148,18 @@ async function getPoiPipeline(dbPath: string | undefined): Promise<Pipeline> {
117
148
  return pipeline
118
149
  }
119
150
 
151
+ /**
152
+ * `plausibilityCheck`'s geocode dep — reuses the SAME shared classifier+resolver `deps.geocode` builds from (see the
153
+ * module header's laziness note), wired at this CLI/MCP layer per the 2b task 5 brief ("`deriveGeocodeRegister`/
154
+ * formatted register is the geocode dep's concern, wired at the CLI/MCP layer"). The real return type (`GeocodeResult`)
155
+ * is structurally assignable to `plausibility.ts`'s minimal `GeocodeLike` — no adapter needed.
156
+ */
157
+ async function resolveGeocode(address: string) {
158
+ const { classifier, resolver, shards } = await loadCore()
159
+
160
+ return geocodeAddress(address, { classifier, resolver, shards: shards.for })
161
+ }
162
+
120
163
  const deps: MCPToolDeps = {
121
164
  async parse(text, opts) {
122
165
  const pipeline = opts?.poi ? await getPoiPipeline(poiDatabasePath) : await getPlainPipeline()
@@ -125,9 +168,7 @@ const deps: MCPToolDeps = {
125
168
  },
126
169
 
127
170
  async geocode(text) {
128
- const { classifier, resolver, shards } = await loadCore()
129
-
130
- return geocodeAddress(text, { classifier, resolver, shards: shards.for })
171
+ return resolveGeocode(text)
131
172
  },
132
173
 
133
174
  async poiSearch(q) {
@@ -177,6 +218,80 @@ const deps: MCPToolDeps = {
177
218
 
178
219
  return { manifest, coverage }
179
220
  },
221
+
222
+ async bdcFilingLandscape(q) {
223
+ // Decision 6 (2b task 7): `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
224
+ // abstain shape exists for this tool), so a missing file becomes a friendly thrown Error naming the layer —
225
+ // never the raw `node:sqlite` "unable to open database file" message.
226
+ assertBDCDatabaseExists("mailwoman_bdc_filing_landscape", q.databasePath)
227
+
228
+ using db = new DatabaseClient<BDCDatabase>({ database: new DatabaseSync(q.databasePath, { readOnly: true }) })
229
+
230
+ return filingLandscape(db, { geoids: q.geoids, h3Cells: q.h3Cells })
231
+ },
232
+
233
+ async plausibilityCheck(q) {
234
+ const bdcDB = openBDCDatabaseIfPresent(q.bdcDatabasePath)
235
+ const poi = await openPlausibilityPOIDeps(q.poiDatabasePath)
236
+
237
+ try {
238
+ return await plausibilityCheck(
239
+ {
240
+ address: q.address,
241
+ point: q.point,
242
+ geoid: q.geoid,
243
+ technologyCode: q.technologyCode,
244
+ claimedDownloadMbps: q.claimedDownloadMbps,
245
+ },
246
+ { bdcDB, poi, geocode: resolveGeocode }
247
+ )
248
+ } finally {
249
+ bdcDB?.destroy()
250
+ poi?.contractDB.destroy()
251
+ }
252
+ },
253
+
254
+ async filerLookup(q) {
255
+ // Decision 6/gate 4 (3a task 7): filerLookup has no optional-dep abstain shape — it throws rather than
256
+ // answer unstamped — so filer.db is required unconditionally, same discipline as bdc.db is for
257
+ // mailwoman_bdc_filing_landscape.
258
+ assertFilerDatabaseExists("mailwoman_filer_lookup", q.databasePath)
259
+
260
+ using db = openFilerDatabaseIfPresent(q.databasePath)!
261
+
262
+ let frn: FRN | undefined
263
+
264
+ if (q.frn !== undefined) {
265
+ const parsed = toFRN(q.frn)
266
+
267
+ if (!parsed) {
268
+ throw new Error(`mailwoman_filer_lookup: "${q.frn}" is not a valid FRN`)
269
+ }
270
+
271
+ frn = parsed
272
+ }
273
+
274
+ return filerLookup(db, {
275
+ frn,
276
+ form499ID: q.form499ID,
277
+ bdcProviderID: q.bdcProviderID,
278
+ asOf: q.asOf,
279
+ })
280
+ },
281
+
282
+ async filerFamily(q) {
283
+ // Same discipline as mailwoman_filer_lookup (3a task 7) — familyRollup has no optional-dep abstain shape
284
+ // either, so filer.db is required unconditionally.
285
+ assertFilerDatabaseExists("mailwoman_filer_family", q.databasePath)
286
+
287
+ using db = openFilerDatabaseIfPresent(q.databasePath)!
288
+
289
+ return familyRollup(db, {
290
+ familyID: q.familyID,
291
+ nodeID: q.nodeID,
292
+ asOf: q.asOf,
293
+ })
294
+ },
180
295
  }
181
296
 
182
297
  const server = createMCPServer(deps)
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Decision 6 (2b task 7) layer-absent guards — pulled out of `cli.ts` into their own importable module (task 7
7
+ * fix round 1 review finding) so the branching itself has direct unit coverage (`layer-guards.test.ts`), not just
8
+ * "the tool handler passes an abstain through" (`tools.test.ts`'s stub-level dispatch tests). `cli.ts`
9
+ * top-level-`await`s a real stdio transport connection at import time, so IT can't be imported by vitest — these
10
+ * three functions have no such dependency (pure existence-check + open, or a thrown Error), so they live here and
11
+ * `cli.ts` just calls them.
12
+ *
13
+ * - `openBDCDatabaseIfPresent` / `openPlausibilityPOIDeps` — `mailwoman_plausibility_check`'s `bdcDB`/`poi` deps
14
+ * (`PlausibilityDeps`) are each OPTIONAL, so a missing/absent path degrades to `undefined`, which
15
+ * `plausibilityCheck` (`@mailwoman/bdc`) already turns into a typed abstain evidence entry
16
+ * (`{type:"abstain", reason:"requires_bdc_layer"|"requires_build_local_layer"}`) — never a raw sqlite throw.
17
+ * - `assertBDCDatabaseExists` — `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
18
+ * abstain shape exists for that tool), so a missing file becomes one friendly thrown `Error` naming the layer
19
+ * instead of the raw `node:sqlite` "unable to open database file" message.
20
+ * - `openFilerDatabaseIfPresent` / `assertFilerDatabaseExists` (3a task 7) — the SAME pairing, for filer.db.
21
+ * `mailwoman_filer_lookup` requires filer.db unconditionally (mirrors `mailwoman_bdc_filing_landscape`'s own
22
+ * "requires the layer" discipline — `filerLookup` itself has no optional-dep abstain shape either, since gate
23
+ * 4 makes it throw rather than answer unstamped), so `cli.ts` pairs `assertFilerDatabaseExists` (the friendly
24
+ * throw) with `openFilerDatabaseIfPresent` (the actual open) the same way `bdcFilingLandscape`'s handler does.
25
+ */
26
+
27
+ import { existsSync } from "node:fs"
28
+ import { DatabaseSync } from "node:sqlite"
29
+
30
+ import type { BDCDatabase, PlausibilityDeps } from "@mailwoman/bdc"
31
+ import { DatabaseClient } from "@mailwoman/core/kysley/client"
32
+ import type { LayerContractDatabase } from "@mailwoman/core/layers"
33
+ import type { FilerDatabase } from "@mailwoman/filer"
34
+
35
+ /**
36
+ * Open a bdc.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw.
37
+ * See the module header.
38
+ */
39
+ export function openBDCDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<BDCDatabase> | undefined {
40
+ if (!databasePath || !existsSync(databasePath)) return undefined
41
+
42
+ return new DatabaseClient<BDCDatabase>({ database: new DatabaseSync(databasePath, { readOnly: true }) })
43
+ }
44
+
45
+ /**
46
+ * Same graceful discipline as {@link openBDCDatabaseIfPresent}, for the poi.db side of `plausibilityCheck`'s deps —
47
+ * `undefined` here becomes the `{type:"abstain", reason:"requires_build_local_layer"}` entry `plausibilityCheck`
48
+ * already produces when a claimed technology's physical-plant categories can't be searched. `POILookup` is dynamically
49
+ * imported (matching `cli.ts`'s existing `resolver-wof-sqlite` laziness) since it's only ever needed when a caller
50
+ * actually wires a poi.db. `lookup` and `contractDB` share ONE `DatabaseSync` handle (the AGENTS.md "one connection,
51
+ * shared" convention) — a real poi.db's rows and its `layer_manifest`/`layer_coverage` tables live in the same file in
52
+ * production, so disposing `contractDB` (which closes the shared handle) is enough; `POILookup` never owns it
53
+ * (constructed with `{database}`, not `{databasePath}` — see `poi-lookup.ts`), so it never double-closes.
54
+ */
55
+ export async function openPlausibilityPOIDeps(databasePath: string | undefined): Promise<PlausibilityDeps["poi"]> {
56
+ if (!databasePath || !existsSync(databasePath)) return undefined
57
+
58
+ const { POILookup } = await import("@mailwoman/resolver-wof-sqlite/poi-lookup")
59
+ const database = new DatabaseSync(databasePath, { readOnly: true })
60
+
61
+ return {
62
+ lookup: new POILookup({ database }),
63
+ contractDB: new DatabaseClient<LayerContractDatabase>({ database }),
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_bdc_filing_landscape`'s guard
69
+ * (decision 6b). `toolName` is threaded through so the message matches whichever tool calls this (today: only
70
+ * `mailwoman_bdc_filing_landscape`).
71
+ */
72
+ export function assertBDCDatabaseExists(toolName: string, databasePath: string): void {
73
+ if (!existsSync(databasePath)) {
74
+ throw new Error(`${toolName}: bdc.db not found at "${databasePath}"`)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Open a filer.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw
80
+ * (3a task 7, mirroring {@link openBDCDatabaseIfPresent}). Used by `cli.ts`'s `mailwoman_filer_lookup` handler after
81
+ * {@link assertFilerDatabaseExists} has already confirmed the file is present.
82
+ */
83
+ export function openFilerDatabaseIfPresent(
84
+ databasePath: string | undefined
85
+ ): DatabaseClient<FilerDatabase> | undefined {
86
+ if (!databasePath || !existsSync(databasePath)) return undefined
87
+
88
+ return new DatabaseClient<FilerDatabase>({ database: new DatabaseSync(databasePath, { readOnly: true }) })
89
+ }
90
+
91
+ /**
92
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_filer_lookup`'s guard (3a
93
+ * task 7, mirroring {@link assertBDCDatabaseExists}). `filerLookup` itself has no optional-dep abstain shape (gate 4
94
+ * makes it throw rather than answer unstamped), so filer.db is required unconditionally, same as bdc.db is for
95
+ * `mailwoman_bdc_filing_landscape`.
96
+ */
97
+ export function assertFilerDatabaseExists(toolName: string, databasePath: string): void {
98
+ if (!existsSync(databasePath)) {
99
+ throw new Error(`${toolName}: filer.db not found at "${databasePath}"`)
100
+ }
101
+ }
package/out/cli.d.ts CHANGED
@@ -6,14 +6,36 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
33
+ * `mailwoman_filer_family` (3b task 9) follows the IDENTICAL discipline, reusing the same two guards — `familyRollup`
34
+ * has no optional-dep abstain shape either (it throws on a bad `familyID`/`nodeID` XOR or a pre-`filer_family`
35
+ * schema), so filer.db is required unconditionally here too. Its result — always `FamilyRollup[]`, never `null` or
36
+ * a bare object — is passed through untouched: this data is who-owns-whom, and a silent reshape here would be a
37
+ * fabricated relationship claim.
38
+ *
17
39
  * ```sh
18
40
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
41
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
package/out/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG"}
package/out/cli.js CHANGED
@@ -6,14 +6,36 @@
6
6
  *
7
7
  * `mailwoman-mcp` — boot the MCP server over stdio. Wires the real `MCPToolDeps` (`tools.ts`) from the mailwoman
8
8
  * library: `createRuntimePipeline` for parse/POI-intent, `geocode-core`'s `geocodeAddress` for geocode,
9
- * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, and `@mailwoman/core/layers` for the layer
10
- * manifest tool.
9
+ * `mailwoman/poi-overpass`'s `emitOverpassQL` for the export tool, `@mailwoman/core/layers` for the layer
10
+ * manifest tool, and `@mailwoman/bdc`'s `filingLandscape`/`plausibilityCheck` for the two BDC tools.
11
11
  *
12
12
  * Deps are LAZY: nothing here loads the neural weights or opens a gazetteer db at startup — an MCP client
13
13
  * connects, lists tools, and may never call one (or may call `mailwoman_overpass_export`/`mailwoman_layer_manifest`,
14
14
  * neither of which needs the classifier at all). The shared classifier+resolver are built once, on the FIRST call
15
15
  * to any tool that needs them, and cached for the process lifetime.
16
16
  *
17
+ * **Graceful layer-absent guards (2b task 7, decision 6).** Both BDC-backed tools treat a missing/unreadable
18
+ * database file as absence, never a raw `node:sqlite` throw ("unable to open database file"): `bdcFilingLandscape`
19
+ * requires bdc.db unconditionally, so a missing file becomes one friendly thrown `Error` naming the layer;
20
+ * `plausibilityCheck`'s `bdcDB`/`poi` deps are each OPTIONAL, so a missing/absent `bdc_database_path`/
21
+ * `poi_database_path` degrades to the SAME typed-abstain evidence entry (`{type:"abstain",
22
+ * reason:"requires_bdc_layer"|"requires_build_local_layer"}`) the scorer already produces for an omitted dep. The
23
+ * guards themselves (`assertBDCDatabaseExists`, `openBDCDatabaseIfPresent`, `openPlausibilityPOIDeps`) live in
24
+ * `./layer-guards.ts`, NOT here — they're pure, transport-independent logic with no need for the stdio connection
25
+ * this file opens at import time (which is exactly why THIS file can't be unit-tested directly; see
26
+ * `layer-guards.test.ts` for their branch coverage).
27
+ *
28
+ * `mailwoman_filer_lookup` (3a task 7) follows the SAME "requires the layer unconditionally" discipline as
29
+ * `mailwoman_bdc_filing_landscape` (`assertFilerDatabaseExists` + `openFilerDatabaseIfPresent`, mirroring
30
+ * `assertBDCDatabaseExists` + the BDC open) — `filerLookup` itself has no optional-dep abstain shape (gate 4 makes
31
+ * it throw rather than answer unstamped), so a missing filer.db becomes one friendly thrown Error naming the layer.
32
+ *
33
+ * `mailwoman_filer_family` (3b task 9) follows the IDENTICAL discipline, reusing the same two guards — `familyRollup`
34
+ * has no optional-dep abstain shape either (it throws on a bad `familyID`/`nodeID` XOR or a pre-`filer_family`
35
+ * schema), so filer.db is required unconditionally here too. Its result — always `FamilyRollup[]`, never `null` or
36
+ * a bare object — is passed through untouched: this data is who-owns-whom, and a silent reshape here would be a
37
+ * fabricated relationship claim.
38
+ *
17
39
  * ```sh
18
40
  * mailwoman-mcp # geocode/poi_search degrade gracefully with no poi.db wired
19
41
  * mailwoman-mcp --poi-db poi.db # mailwoman_poi_search additionally executes against poi.db
@@ -22,8 +44,10 @@
22
44
  import { existsSync } from "node:fs";
23
45
  import { DatabaseSync } from "node:sqlite";
24
46
  import { parseArgs } from "node:util";
47
+ import { filingLandscape, plausibilityCheck } from "@mailwoman/bdc";
25
48
  import { DatabaseClient } from "@mailwoman/core/kysley/client";
26
49
  import { readLayerManifest } from "@mailwoman/core/layers";
50
+ import { familyRollup, filerLookup, toFRN } from "@mailwoman/filer/sdk";
27
51
  import { NeuralAddressClassifier } from "@mailwoman/neural";
28
52
  import { getPOICategory } from "@mailwoman/poi-taxonomy";
29
53
  import { createWOFResolver } from "@mailwoman/resolver";
@@ -31,6 +55,7 @@ import { createRuntimePipeline } from "mailwoman";
31
55
  import { geocodeAddress, ShardProvider } from "mailwoman/geocode-core";
32
56
  import { emitOverpassQL } from "mailwoman/poi-overpass";
33
57
  import { createResolverBackend, mailwomanDataRoot, resolveCandidateDBPath, wofShardPaths, } from "mailwoman/resolver-backend";
58
+ import { assertBDCDatabaseExists, assertFilerDatabaseExists, openBDCDatabaseIfPresent, openFilerDatabaseIfPresent, openPlausibilityPOIDeps, } from "./layer-guards.js";
34
59
  import { createMCPServer } from "./server.js";
35
60
  const { values } = parseArgs({
36
61
  options: {
@@ -91,14 +116,23 @@ async function getPoiPipeline(dbPath) {
91
116
  poiPipelines.set(key, pipeline);
92
117
  return pipeline;
93
118
  }
119
+ /**
120
+ * `plausibilityCheck`'s geocode dep — reuses the SAME shared classifier+resolver `deps.geocode` builds from (see the
121
+ * module header's laziness note), wired at this CLI/MCP layer per the 2b task 5 brief ("`deriveGeocodeRegister`/
122
+ * formatted register is the geocode dep's concern, wired at the CLI/MCP layer"). The real return type (`GeocodeResult`)
123
+ * is structurally assignable to `plausibility.ts`'s minimal `GeocodeLike` — no adapter needed.
124
+ */
125
+ async function resolveGeocode(address) {
126
+ const { classifier, resolver, shards } = await loadCore();
127
+ return geocodeAddress(address, { classifier, resolver, shards: shards.for });
128
+ }
94
129
  const deps = {
95
130
  async parse(text, opts) {
96
131
  const pipeline = opts?.poi ? await getPoiPipeline(poiDatabasePath) : await getPlainPipeline();
97
132
  return pipeline(text);
98
133
  },
99
134
  async geocode(text) {
100
- const { classifier, resolver, shards } = await loadCore();
101
- return geocodeAddress(text, { classifier, resolver, shards: shards.for });
135
+ return resolveGeocode(text);
102
136
  },
103
137
  async poiSearch(q) {
104
138
  const pipeline = await getPoiPipeline(q.poiDatabasePath ?? poiDatabasePath);
@@ -135,6 +169,63 @@ const deps = {
135
169
  .executeTakeFirst();
136
170
  return { manifest, coverage };
137
171
  },
172
+ async bdcFilingLandscape(q) {
173
+ // Decision 6 (2b task 7): `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
174
+ // abstain shape exists for this tool), so a missing file becomes a friendly thrown Error naming the layer —
175
+ // never the raw `node:sqlite` "unable to open database file" message.
176
+ assertBDCDatabaseExists("mailwoman_bdc_filing_landscape", q.databasePath);
177
+ using db = new DatabaseClient({ database: new DatabaseSync(q.databasePath, { readOnly: true }) });
178
+ return filingLandscape(db, { geoids: q.geoids, h3Cells: q.h3Cells });
179
+ },
180
+ async plausibilityCheck(q) {
181
+ const bdcDB = openBDCDatabaseIfPresent(q.bdcDatabasePath);
182
+ const poi = await openPlausibilityPOIDeps(q.poiDatabasePath);
183
+ try {
184
+ return await plausibilityCheck({
185
+ address: q.address,
186
+ point: q.point,
187
+ geoid: q.geoid,
188
+ technologyCode: q.technologyCode,
189
+ claimedDownloadMbps: q.claimedDownloadMbps,
190
+ }, { bdcDB, poi, geocode: resolveGeocode });
191
+ }
192
+ finally {
193
+ bdcDB?.destroy();
194
+ poi?.contractDB.destroy();
195
+ }
196
+ },
197
+ async filerLookup(q) {
198
+ // Decision 6/gate 4 (3a task 7): filerLookup has no optional-dep abstain shape — it throws rather than
199
+ // answer unstamped — so filer.db is required unconditionally, same discipline as bdc.db is for
200
+ // mailwoman_bdc_filing_landscape.
201
+ assertFilerDatabaseExists("mailwoman_filer_lookup", q.databasePath);
202
+ using db = openFilerDatabaseIfPresent(q.databasePath);
203
+ let frn;
204
+ if (q.frn !== undefined) {
205
+ const parsed = toFRN(q.frn);
206
+ if (!parsed) {
207
+ throw new Error(`mailwoman_filer_lookup: "${q.frn}" is not a valid FRN`);
208
+ }
209
+ frn = parsed;
210
+ }
211
+ return filerLookup(db, {
212
+ frn,
213
+ form499ID: q.form499ID,
214
+ bdcProviderID: q.bdcProviderID,
215
+ asOf: q.asOf,
216
+ });
217
+ },
218
+ async filerFamily(q) {
219
+ // Same discipline as mailwoman_filer_lookup (3a task 7) — familyRollup has no optional-dep abstain shape
220
+ // either, so filer.db is required unconditionally.
221
+ assertFilerDatabaseExists("mailwoman_filer_family", q.databasePath);
222
+ using db = openFilerDatabaseIfPresent(q.databasePath);
223
+ return familyRollup(db, {
224
+ familyID: q.familyID,
225
+ nodeID: q.nodeID,
226
+ asOf: q.asOf,
227
+ });
228
+ },
138
229
  };
139
230
  const server = createMCPServer(deps);
140
231
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
package/out/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Decision 6 (2b task 7) layer-absent guards — pulled out of `cli.ts` into their own importable module (task 7
7
+ * fix round 1 review finding) so the branching itself has direct unit coverage (`layer-guards.test.ts`), not just
8
+ * "the tool handler passes an abstain through" (`tools.test.ts`'s stub-level dispatch tests). `cli.ts`
9
+ * top-level-`await`s a real stdio transport connection at import time, so IT can't be imported by vitest — these
10
+ * three functions have no such dependency (pure existence-check + open, or a thrown Error), so they live here and
11
+ * `cli.ts` just calls them.
12
+ *
13
+ * - `openBDCDatabaseIfPresent` / `openPlausibilityPOIDeps` — `mailwoman_plausibility_check`'s `bdcDB`/`poi` deps
14
+ * (`PlausibilityDeps`) are each OPTIONAL, so a missing/absent path degrades to `undefined`, which
15
+ * `plausibilityCheck` (`@mailwoman/bdc`) already turns into a typed abstain evidence entry
16
+ * (`{type:"abstain", reason:"requires_bdc_layer"|"requires_build_local_layer"}`) — never a raw sqlite throw.
17
+ * - `assertBDCDatabaseExists` — `mailwoman_bdc_filing_landscape` requires bdc.db unconditionally (no optional-dep
18
+ * abstain shape exists for that tool), so a missing file becomes one friendly thrown `Error` naming the layer
19
+ * instead of the raw `node:sqlite` "unable to open database file" message.
20
+ * - `openFilerDatabaseIfPresent` / `assertFilerDatabaseExists` (3a task 7) — the SAME pairing, for filer.db.
21
+ * `mailwoman_filer_lookup` requires filer.db unconditionally (mirrors `mailwoman_bdc_filing_landscape`'s own
22
+ * "requires the layer" discipline — `filerLookup` itself has no optional-dep abstain shape either, since gate
23
+ * 4 makes it throw rather than answer unstamped), so `cli.ts` pairs `assertFilerDatabaseExists` (the friendly
24
+ * throw) with `openFilerDatabaseIfPresent` (the actual open) the same way `bdcFilingLandscape`'s handler does.
25
+ */
26
+ import type { BDCDatabase, PlausibilityDeps } from "@mailwoman/bdc";
27
+ import { DatabaseClient } from "@mailwoman/core/kysley/client";
28
+ import type { FilerDatabase } from "@mailwoman/filer";
29
+ /**
30
+ * Open a bdc.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw.
31
+ * See the module header.
32
+ */
33
+ export declare function openBDCDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<BDCDatabase> | undefined;
34
+ /**
35
+ * Same graceful discipline as {@link openBDCDatabaseIfPresent}, for the poi.db side of `plausibilityCheck`'s deps —
36
+ * `undefined` here becomes the `{type:"abstain", reason:"requires_build_local_layer"}` entry `plausibilityCheck`
37
+ * already produces when a claimed technology's physical-plant categories can't be searched. `POILookup` is dynamically
38
+ * imported (matching `cli.ts`'s existing `resolver-wof-sqlite` laziness) since it's only ever needed when a caller
39
+ * actually wires a poi.db. `lookup` and `contractDB` share ONE `DatabaseSync` handle (the AGENTS.md "one connection,
40
+ * shared" convention) — a real poi.db's rows and its `layer_manifest`/`layer_coverage` tables live in the same file in
41
+ * production, so disposing `contractDB` (which closes the shared handle) is enough; `POILookup` never owns it
42
+ * (constructed with `{database}`, not `{databasePath}` — see `poi-lookup.ts`), so it never double-closes.
43
+ */
44
+ export declare function openPlausibilityPOIDeps(databasePath: string | undefined): Promise<PlausibilityDeps["poi"]>;
45
+ /**
46
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_bdc_filing_landscape`'s guard
47
+ * (decision 6b). `toolName` is threaded through so the message matches whichever tool calls this (today: only
48
+ * `mailwoman_bdc_filing_landscape`).
49
+ */
50
+ export declare function assertBDCDatabaseExists(toolName: string, databasePath: string): void;
51
+ /**
52
+ * Open a filer.db, or return `undefined` when `databasePath` is unset or the file is missing — NEVER a raw sqlite throw
53
+ * (3a task 7, mirroring {@link openBDCDatabaseIfPresent}). Used by `cli.ts`'s `mailwoman_filer_lookup` handler after
54
+ * {@link assertFilerDatabaseExists} has already confirmed the file is present.
55
+ */
56
+ export declare function openFilerDatabaseIfPresent(databasePath: string | undefined): DatabaseClient<FilerDatabase> | undefined;
57
+ /**
58
+ * Throws a friendly Error naming the layer when `databasePath` doesn't exist — `mailwoman_filer_lookup`'s guard (3a
59
+ * task 7, mirroring {@link assertBDCDatabaseExists}). `filerLookup` itself has no optional-dep abstain shape (gate 4
60
+ * makes it throw rather than answer unstamped), so filer.db is required unconditionally, same as bdc.db is for
61
+ * `mailwoman_bdc_filing_landscape`.
62
+ */
63
+ export declare function assertFilerDatabaseExists(toolName: string, databasePath: string): void;
64
+ //# sourceMappingURL=layer-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer-guards.d.ts","sourceRoot":"","sources":["../layer-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,CAAC,WAAW,CAAC,GAAG,SAAS,CAIlH;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAUhH;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAIpF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACzC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,cAAc,CAAC,aAAa,CAAC,GAAG,SAAS,CAI3C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAItF"}