@cliwant/mcp-sam-gov 0.3.0 → 1.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.
Files changed (228) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +48 -16
  3. package/README.ko.md +48 -16
  4. package/README.md +226 -64
  5. package/dist/attachments.d.ts +63 -0
  6. package/dist/attachments.d.ts.map +1 -0
  7. package/dist/attachments.js +576 -0
  8. package/dist/attachments.js.map +1 -0
  9. package/dist/bls.d.ts +267 -0
  10. package/dist/bls.d.ts.map +1 -0
  11. package/dist/bls.js +1549 -0
  12. package/dist/bls.js.map +1 -0
  13. package/dist/cache.d.ts +2 -0
  14. package/dist/cache.d.ts.map +1 -1
  15. package/dist/cache.js +4 -0
  16. package/dist/cache.js.map +1 -1
  17. package/dist/census.d.ts +182 -0
  18. package/dist/census.d.ts.map +1 -0
  19. package/dist/census.js +576 -0
  20. package/dist/census.js.map +1 -0
  21. package/dist/ckan.d.ts +141 -0
  22. package/dist/ckan.d.ts.map +1 -0
  23. package/dist/ckan.js +393 -0
  24. package/dist/ckan.js.map +1 -0
  25. package/dist/clinicaltrials.d.ts +180 -0
  26. package/dist/clinicaltrials.d.ts.map +1 -0
  27. package/dist/clinicaltrials.js +730 -0
  28. package/dist/clinicaltrials.js.map +1 -0
  29. package/dist/cms.d.ts +140 -0
  30. package/dist/cms.d.ts.map +1 -0
  31. package/dist/cms.js +482 -0
  32. package/dist/cms.js.map +1 -0
  33. package/dist/coerce.d.ts +32 -0
  34. package/dist/coerce.d.ts.map +1 -0
  35. package/dist/coerce.js +50 -0
  36. package/dist/coerce.js.map +1 -0
  37. package/dist/datagov-catalog.d.ts +84 -0
  38. package/dist/datagov-catalog.d.ts.map +1 -0
  39. package/dist/datagov-catalog.js +233 -0
  40. package/dist/datagov-catalog.js.map +1 -0
  41. package/dist/datagov.d.ts +146 -0
  42. package/dist/datagov.d.ts.map +1 -0
  43. package/dist/datagov.js +689 -0
  44. package/dist/datagov.js.map +1 -0
  45. package/dist/datagovKey.d.ts +36 -0
  46. package/dist/datagovKey.d.ts.map +1 -0
  47. package/dist/datagovKey.js +60 -0
  48. package/dist/datagovKey.js.map +1 -0
  49. package/dist/datasource.d.ts +313 -0
  50. package/dist/datasource.d.ts.map +1 -0
  51. package/dist/datasource.js +551 -0
  52. package/dist/datasource.js.map +1 -0
  53. package/dist/disclosure.d.ts +55 -0
  54. package/dist/disclosure.d.ts.map +1 -0
  55. package/dist/disclosure.js +57 -0
  56. package/dist/disclosure.js.map +1 -0
  57. package/dist/ecfr.d.ts +4 -2
  58. package/dist/ecfr.d.ts.map +1 -1
  59. package/dist/ecfr.js +74 -1
  60. package/dist/ecfr.js.map +1 -1
  61. package/dist/echo.d.ts +143 -0
  62. package/dist/echo.d.ts.map +1 -0
  63. package/dist/echo.js +424 -0
  64. package/dist/echo.js.map +1 -0
  65. package/dist/edgar.d.ts +377 -0
  66. package/dist/edgar.d.ts.map +1 -0
  67. package/dist/edgar.js +2418 -0
  68. package/dist/edgar.js.map +1 -0
  69. package/dist/errors.d.ts +26 -0
  70. package/dist/errors.d.ts.map +1 -1
  71. package/dist/errors.js +69 -1
  72. package/dist/errors.js.map +1 -1
  73. package/dist/fac.d.ts +180 -0
  74. package/dist/fac.d.ts.map +1 -0
  75. package/dist/fac.js +416 -0
  76. package/dist/fac.js.map +1 -0
  77. package/dist/far.d.ts +170 -0
  78. package/dist/far.d.ts.map +1 -0
  79. package/dist/far.js +804 -0
  80. package/dist/far.js.map +1 -0
  81. package/dist/fdic.d.ts +599 -0
  82. package/dist/fdic.d.ts.map +1 -0
  83. package/dist/fdic.js +1624 -0
  84. package/dist/fdic.js.map +1 -0
  85. package/dist/federal-register.d.ts +139 -2
  86. package/dist/federal-register.d.ts.map +1 -1
  87. package/dist/federal-register.js +419 -3
  88. package/dist/federal-register.js.map +1 -1
  89. package/dist/fema.d.ts +181 -0
  90. package/dist/fema.d.ts.map +1 -0
  91. package/dist/fema.js +436 -0
  92. package/dist/fema.js.map +1 -0
  93. package/dist/fpds.d.ts +108 -0
  94. package/dist/fpds.d.ts.map +1 -0
  95. package/dist/fpds.js +519 -0
  96. package/dist/fpds.js.map +1 -0
  97. package/dist/gao.d.ts +64 -0
  98. package/dist/gao.d.ts.map +1 -0
  99. package/dist/gao.js +640 -0
  100. package/dist/gao.js.map +1 -0
  101. package/dist/govinfo.d.ts +111 -0
  102. package/dist/govinfo.d.ts.map +1 -0
  103. package/dist/govinfo.js +422 -0
  104. package/dist/govinfo.js.map +1 -0
  105. package/dist/grants.d.ts +27 -4
  106. package/dist/grants.d.ts.map +1 -1
  107. package/dist/grants.js +111 -6
  108. package/dist/grants.js.map +1 -1
  109. package/dist/gsa-csv.d.ts +249 -0
  110. package/dist/gsa-csv.d.ts.map +1 -0
  111. package/dist/gsa-csv.js +784 -0
  112. package/dist/gsa-csv.js.map +1 -0
  113. package/dist/integrity.d.ts +212 -0
  114. package/dist/integrity.d.ts.map +1 -0
  115. package/dist/integrity.js +707 -0
  116. package/dist/integrity.js.map +1 -0
  117. package/dist/meta.d.ts +165 -0
  118. package/dist/meta.d.ts.map +1 -0
  119. package/dist/meta.js +162 -0
  120. package/dist/meta.js.map +1 -0
  121. package/dist/nih.d.ts +117 -0
  122. package/dist/nih.d.ts.map +1 -0
  123. package/dist/nih.js +291 -0
  124. package/dist/nih.js.map +1 -0
  125. package/dist/nppes.d.ts +157 -0
  126. package/dist/nppes.d.ts.map +1 -0
  127. package/dist/nppes.js +648 -0
  128. package/dist/nppes.js.map +1 -0
  129. package/dist/nsf.d.ts +176 -0
  130. package/dist/nsf.d.ts.map +1 -0
  131. package/dist/nsf.js +554 -0
  132. package/dist/nsf.js.map +1 -0
  133. package/dist/nvd.d.ts +176 -0
  134. package/dist/nvd.d.ts.map +1 -0
  135. package/dist/nvd.js +912 -0
  136. package/dist/nvd.js.map +1 -0
  137. package/dist/ofac.d.ts +205 -0
  138. package/dist/ofac.d.ts.map +1 -0
  139. package/dist/ofac.js +919 -0
  140. package/dist/ofac.js.map +1 -0
  141. package/dist/pricing.d.ts +110 -0
  142. package/dist/pricing.d.ts.map +1 -0
  143. package/dist/pricing.js +843 -0
  144. package/dist/pricing.js.map +1 -0
  145. package/dist/sam-gov/client.d.ts +60 -2
  146. package/dist/sam-gov/client.d.ts.map +1 -1
  147. package/dist/sam-gov/client.js +320 -54
  148. package/dist/sam-gov/client.js.map +1 -1
  149. package/dist/sam-gov/index.d.ts +1 -1
  150. package/dist/sam-gov/index.d.ts.map +1 -1
  151. package/dist/sam-gov/index.js +1 -1
  152. package/dist/sam-gov/index.js.map +1 -1
  153. package/dist/sam-gov/types.d.ts +24 -0
  154. package/dist/sam-gov/types.d.ts.map +1 -1
  155. package/dist/sba.d.ts +72 -0
  156. package/dist/sba.d.ts.map +1 -0
  157. package/dist/sba.js +281 -0
  158. package/dist/sba.js.map +1 -0
  159. package/dist/server.d.ts +14 -2
  160. package/dist/server.d.ts.map +1 -1
  161. package/dist/server.js +3889 -296
  162. package/dist/server.js.map +1 -1
  163. package/dist/snapshot.d.ts +98 -0
  164. package/dist/snapshot.d.ts.map +1 -0
  165. package/dist/snapshot.js +146 -0
  166. package/dist/snapshot.js.map +1 -0
  167. package/dist/socrata.d.ts +157 -0
  168. package/dist/socrata.d.ts.map +1 -0
  169. package/dist/socrata.js +448 -0
  170. package/dist/socrata.js.map +1 -0
  171. package/dist/treasury.d.ts +143 -0
  172. package/dist/treasury.d.ts.map +1 -0
  173. package/dist/treasury.js +436 -0
  174. package/dist/treasury.js.map +1 -0
  175. package/dist/usaspending.d.ts +260 -65
  176. package/dist/usaspending.d.ts.map +1 -1
  177. package/dist/usaspending.js +1629 -201
  178. package/dist/usaspending.js.map +1 -1
  179. package/dist/usitc.d.ts +142 -0
  180. package/dist/usitc.d.ts.map +1 -0
  181. package/dist/usitc.js +339 -0
  182. package/dist/usitc.js.map +1 -0
  183. package/package.json +24 -2
  184. package/src/attachments.ts +652 -0
  185. package/src/bls.ts +1943 -0
  186. package/src/cache.ts +5 -0
  187. package/src/census.ts +735 -0
  188. package/src/ckan.ts +495 -0
  189. package/src/clinicaltrials.ts +923 -0
  190. package/src/cms.ts +634 -0
  191. package/src/coerce.ts +47 -0
  192. package/src/datagov-catalog.ts +296 -0
  193. package/src/datagov.ts +907 -0
  194. package/src/datagovKey.ts +68 -0
  195. package/src/datasource.ts +721 -0
  196. package/src/disclosure.ts +61 -0
  197. package/src/ecfr.ts +98 -1
  198. package/src/echo.ts +496 -0
  199. package/src/edgar.ts +3014 -0
  200. package/src/errors.ts +84 -1
  201. package/src/fac.ts +529 -0
  202. package/src/far.ts +1007 -0
  203. package/src/fdic.ts +2052 -0
  204. package/src/federal-register.ts +512 -3
  205. package/src/fema.ts +541 -0
  206. package/src/fpds.ts +620 -0
  207. package/src/gao.ts +744 -0
  208. package/src/govinfo.ts +497 -0
  209. package/src/grants.ts +139 -7
  210. package/src/gsa-csv.ts +992 -0
  211. package/src/integrity.ts +928 -0
  212. package/src/meta.ts +292 -0
  213. package/src/nih.ts +375 -0
  214. package/src/nppes.ts +834 -0
  215. package/src/nsf.ts +706 -0
  216. package/src/nvd.ts +1124 -0
  217. package/src/ofac.ts +1166 -0
  218. package/src/pricing.ts +1075 -0
  219. package/src/sam-gov/client.ts +345 -63
  220. package/src/sam-gov/index.ts +5 -1
  221. package/src/sam-gov/types.ts +22 -0
  222. package/src/sba.ts +357 -0
  223. package/src/server.ts +4551 -328
  224. package/src/snapshot.ts +192 -0
  225. package/src/socrata.ts +532 -0
  226. package/src/treasury.ts +575 -0
  227. package/src/usaspending.ts +1946 -214
  228. package/src/usitc.ts +420 -0
package/src/sba.ts ADDED
@@ -0,0 +1,357 @@
1
+ /**
2
+ * SBA small-business size standards (keyless reference lookup).
3
+ *
4
+ * Why this exists
5
+ * ----------------
6
+ * "Is this firm actually SMALL for this NAICS?" is the gating question for
7
+ * every set-aside eligibility call and for vetting a teaming candidate
8
+ * (`usas_search_teaming_partners` surfaces award-derived candidates — this tool
9
+ * answers whether one of them clears the SBA size standard for the work).
10
+ * The answer is a per-NAICS threshold published by SBA: either an average
11
+ * annual RECEIPTS cap (most services) or an EMPLOYEE-count cap (most
12
+ * manufacturing/mining), with a small set of financial NAICS gated on ASSETS.
13
+ *
14
+ * Source — VERIFIED LIVE 2026-07-03
15
+ * ---------------------------------
16
+ * GET https://www.sba.gov/sites/default/files/data/naics.json
17
+ * → HTTP 200, application/json, a keyless ARRAY of ~997 entries, one per
18
+ * 6-digit NAICS (plus a handful of `<code>_a_Except` exception rows).
19
+ * Each entry:
20
+ * { id, description, sectorId, sectorDescription, subsectorId,
21
+ * subsectorDescription, revenueLimit, assetLimit, employeeCountLimit,
22
+ * parent, footnote }
23
+ * - revenueLimit = receipts-based standard in $ MILLIONS (34 ⇒ $34,000,000)
24
+ * - assetLimit = asset-based standard in $ MILLIONS (850 ⇒ $850,000,000)
25
+ * - employeeCountLimit = employee-based standard as a NUMBER OF EMPLOYEES
26
+ * A NAICS uses exactly ONE of these; the other two are null. (Verified: zero
27
+ * rows carry revenue+assets or revenue+employees together.)
28
+ *
29
+ * IMPORTANT — not a permanent constant (doc-07 caveat)
30
+ * ----------------------------------------------------
31
+ * SBA adjusts size standards periodically (inflation adjustments to the
32
+ * monetary caps, employee-based reviews). This dataset carries NO explicit
33
+ * effective-date field, so we surface the value AS PUBLISHED in the fetched
34
+ * file at retrieval time — with an `asOf` timestamp and an explicit _meta note
35
+ * that high-stakes eligibility must be re-verified at sba.gov. We never present
36
+ * the number as an immutable truth.
37
+ *
38
+ * Keyless. Fetched once and served from the shared 5-minute reference cache
39
+ * (the file is ~200 KB — we cache the parsed lookup, not one fetch per call).
40
+ */
41
+
42
+ import { fetchWithRetry, ToolErrorCarrier } from "./errors.js";
43
+ import {
44
+ throughPathChain,
45
+ CircuitBreaker,
46
+ type ResiliencePath,
47
+ type Provenance,
48
+ } from "./datasource.js";
49
+ import { snapshotPath, provenanceMeta } from "./snapshot.js";
50
+ import { memoize } from "./cache.js";
51
+ import { withMeta } from "./meta.js";
52
+
53
+ const SBA_NAICS_URL =
54
+ "https://www.sba.gov/sites/default/files/data/naics.json";
55
+
56
+ // ─── Resilience wiring (ADR-0045 pilot expansion — INERT by default) ───────
57
+ // The whole naics.json file (all ~997 size standards) is a single, queryless,
58
+ // slow-changing PUBLIC reference — the cleanest snapshot candidate: one
59
+ // pre-fetch serves EVERY per-NAICS lookup. The live host + a per-host circuit
60
+ // breaker keyed on the FIXED set {this host} (bounded — m3-regression),
61
+ // CONSULTED only by `throughPathChain` for a ≥2-path chain. When
62
+ // SAMGOV_SNAPSHOT_BASE_URL is unset the chain is single-path (live only), the
63
+ // breaker is a pure no-op, and the tool is BYTE-IDENTICAL to before this ADR.
64
+ const SBA_HOST = "www.sba.gov";
65
+ let sbaBreaker = new CircuitBreaker([SBA_HOST]);
66
+
67
+ /** Test-only: reset the resilience breaker between OFFLINE fixtures (mirrors
68
+ * treasury.ts's `_resetTreasuryBreakerForTests`). */
69
+ export function _resetSbaBreakerForTests(): void {
70
+ sbaBreaker = new CircuitBreaker([SBA_HOST]);
71
+ }
72
+
73
+ /** One raw entry in sba.gov/naics.json (only the fields we consume). */
74
+ type SbaNaicsEntry = {
75
+ id?: string;
76
+ description?: string;
77
+ sectorId?: string;
78
+ sectorDescription?: string;
79
+ subsectorId?: string;
80
+ subsectorDescription?: string;
81
+ /** Receipts-based standard, in $ MILLIONS (null when not receipts-based). */
82
+ revenueLimit?: number | null;
83
+ /** Asset-based standard, in $ MILLIONS (null unless a financial NAICS). */
84
+ assetLimit?: number | null;
85
+ /** Employee-based standard, a NUMBER OF EMPLOYEES (null when not emp-based). */
86
+ employeeCountLimit?: number | null;
87
+ parent?: number | string | null;
88
+ footnote?: string[] | null;
89
+ };
90
+
91
+ const MILLIONS = 1_000_000;
92
+
93
+ /**
94
+ * Fetch + parse the SBA naics.json into a Map keyed by 6-digit NAICS id.
95
+ * Cached (5-min TTL) under a single key so the ~200 KB file is pulled at most
96
+ * once per cache window regardless of how many NAICS the agent looks up.
97
+ *
98
+ * On upstream failure this THROWS a ToolErrorCarrier (retry-classified by
99
+ * fetchWithRetry) — it never resolves to an empty map, so a lookup can never be
100
+ * silently reported as "NAICS not found" during an outage.
101
+ */
102
+ async function loadSizeStandards(): Promise<{
103
+ byId: Map<string, SbaNaicsEntry>;
104
+ count: number;
105
+ provenance: Provenance;
106
+ }> {
107
+ return memoize("sba:size-standards", async () => {
108
+ // ★Route the fetch through the resilience path-chain. The LIVE path is
109
+ // byte-identical to the prior bare fetch — same URL, same init
110
+ // ({headers:{Accept}, signal}), same label — so with no snapshot configured
111
+ // the chain is SINGLE-ENTRY, `throughPathChain` fast-paths (no breaker
112
+ // consult), and behavior is BYTE-IDENTICAL to before this ADR. The snapshot
113
+ // (key `sba_size_standards`, the whole array) is added only when
114
+ // SAMGOV_SNAPSHOT_BASE_URL is configured (else snapshotPath returns null).
115
+ const livePath: ResiliencePath<unknown> = {
116
+ host: SBA_HOST,
117
+ provenance: { dataPath: "live" },
118
+ run: async () => {
119
+ const r = await fetchWithRetry(
120
+ SBA_NAICS_URL,
121
+ {
122
+ headers: { Accept: "application/json" },
123
+ signal: AbortSignal.timeout(15_000),
124
+ },
125
+ "sba:naics.json",
126
+ );
127
+ return (await r.json()) as unknown;
128
+ },
129
+ };
130
+ const snap = snapshotPath<unknown>("sba_size_standards");
131
+ const paths = snap ? [livePath, snap] : [livePath];
132
+ const { body: raw, provenance } = await throughPathChain<unknown>(
133
+ paths,
134
+ sbaBreaker,
135
+ );
136
+ if (!Array.isArray(raw)) {
137
+ // 200 with an unexpected shape ⇒ schema drift, not "no data".
138
+ throw new ToolErrorCarrier({
139
+ kind: "schema_drift",
140
+ message:
141
+ "sba.gov naics.json did not return a JSON array (schema drift — the size-standards dataset shape changed).",
142
+ retryable: false,
143
+ upstreamEndpoint: "sba:naics.json",
144
+ });
145
+ }
146
+ const byId = new Map<string, SbaNaicsEntry>();
147
+ for (const entry of raw as SbaNaicsEntry[]) {
148
+ const id = entry?.id;
149
+ // Index only clean 6-digit NAICS ids. The `<code>_a_Except` exception
150
+ // rows share a base code with their parent and would otherwise clobber
151
+ // the canonical entry; a caller looks up a plain 6-digit NAICS.
152
+ if (typeof id === "string" && /^\d{6}$/.test(id) && !byId.has(id)) {
153
+ byId.set(id, entry);
154
+ }
155
+ }
156
+ return { byId, count: byId.size, provenance };
157
+ });
158
+ }
159
+
160
+ export type SizeStandardResult = {
161
+ naics: string;
162
+ found: boolean;
163
+ description: string | null;
164
+ sector: string | null;
165
+ subsector: string | null;
166
+ standardType: "receipts" | "employees" | "assets" | "receipts+assets" | "unknown";
167
+ /** Receipts/assets → DOLLARS (revenueLimit*1e6); employees → the count. */
168
+ threshold: number | null;
169
+ unit: "USD annual receipts" | "employees" | "USD assets" | null;
170
+ /** Raw normalized values (null where the dataset has none). */
171
+ revenueLimitUSD: number | null;
172
+ employeeCountLimit: number | null;
173
+ assetLimitUSD: number | null;
174
+ footnote: string[] | null;
175
+ /** Retrieval timestamp — the value is "as published as of" this instant. */
176
+ asOf: string;
177
+ sourceUrl: string;
178
+ };
179
+
180
+ /** The doc-07 "not a permanent constant" caveat, surfaced in every _meta. */
181
+ const NOT_A_CONSTANT_CAVEAT =
182
+ "SBA size standards are adjusted periodically (e.g. SBA has proposed monetary increases) — " +
183
+ "treat this as the value published in the fetched dataset as of retrieval, not a permanent " +
184
+ "constant; the dataset does not carry an explicit effective-date field, so verify the current " +
185
+ "standard at sba.gov for high-stakes eligibility.";
186
+
187
+ /**
188
+ * Look up the SBA small-business size standard for one 6-digit NAICS.
189
+ *
190
+ * Returns `found:false` (never a fabricated standard) when the NAICS is not in
191
+ * the fetched dataset, with the disclosure carried in `_meta.notes`.
192
+ */
193
+ export async function sizeStandard(args: { naics: string }) {
194
+ const naics = (args.naics ?? "").trim();
195
+ // Validate shape at the tool boundary — a malformed NAICS is invalid_input,
196
+ // not a silent "not found". (The server also enforces this via Zod.)
197
+ if (!/^\d{6}$/.test(naics)) {
198
+ throw new ToolErrorCarrier({
199
+ kind: "invalid_input",
200
+ message: `naics must be a 6-digit NAICS code (got "${args.naics}").`,
201
+ retryable: false,
202
+ upstreamEndpoint: "sba:naics.json",
203
+ });
204
+ }
205
+
206
+ const { byId, count, provenance } = await loadSizeStandards();
207
+ const asOf = new Date().toISOString();
208
+ const entry = byId.get(naics);
209
+
210
+ if (!entry) {
211
+ const data: SizeStandardResult = {
212
+ naics,
213
+ found: false,
214
+ description: null,
215
+ sector: null,
216
+ subsector: null,
217
+ standardType: "unknown",
218
+ threshold: null,
219
+ unit: null,
220
+ revenueLimitUSD: null,
221
+ employeeCountLimit: null,
222
+ assetLimitUSD: null,
223
+ footnote: null,
224
+ asOf,
225
+ sourceUrl: SBA_NAICS_URL,
226
+ };
227
+ return withMeta(data, {
228
+ source: "sba.gov naics.json (keyless)",
229
+ keylessMode: true,
230
+ returned: 0,
231
+ // LEAD-12 fix (C78 adversarial review): a definitive not-found is a COMPLETE,
232
+ // non-truncated answer — the tool scanned the whole cached dataset and confirmed
233
+ // the NAICS is absent. totalAvailable is 0 (zero records match THIS query), NOT
234
+ // the ~978-row dataset size; sourcing `count` here made returned(0) < 978 →
235
+ // truncated:true → complete:false, which falsely told the agent to paginate for
236
+ // the "other 977 results" that do not exist. complete now derives to true. The
237
+ // dataset size stays in the note below (its correct home = prose disclosure).
238
+ totalAvailable: 0,
239
+ fieldsUnavailable: [
240
+ "description",
241
+ "sector",
242
+ "subsector",
243
+ "standardType",
244
+ "threshold",
245
+ "unit",
246
+ "revenueLimitUSD",
247
+ "employeeCountLimit",
248
+ "assetLimitUSD",
249
+ ],
250
+ notes: [
251
+ `NAICS ${naics} is not present in the SBA size-standards dataset (${count} 6-digit codes as of retrieval). No size standard was fabricated — confirm the code is a current 6-digit NAICS and check sba.gov.`,
252
+ NOT_A_CONSTANT_CAVEAT,
253
+ ],
254
+ // P5 provenance — threaded ONLY when NON-live ⇒ live stays byte-identical.
255
+ ...provenanceMeta(provenance),
256
+ });
257
+ }
258
+
259
+ // Normalize the raw millions figures to dollars. Exactly one of the three
260
+ // limits is set on any real row (verified live), so we classify by presence.
261
+ const revenueLimitUSD =
262
+ typeof entry.revenueLimit === "number"
263
+ ? entry.revenueLimit * MILLIONS
264
+ : null;
265
+ const assetLimitUSD =
266
+ typeof entry.assetLimit === "number" ? entry.assetLimit * MILLIONS : null;
267
+ const employeeCountLimit =
268
+ typeof entry.employeeCountLimit === "number"
269
+ ? entry.employeeCountLimit
270
+ : null;
271
+
272
+ let standardType: SizeStandardResult["standardType"];
273
+ let threshold: number | null;
274
+ let unit: SizeStandardResult["unit"];
275
+ const fieldsUnavailable: string[] = [];
276
+
277
+ if (revenueLimitUSD !== null) {
278
+ // Receipts-based. (A financial NAICS could in principle also carry an
279
+ // asset figure — none do today — in which case we flag it receipts+assets.)
280
+ standardType = assetLimitUSD !== null ? "receipts+assets" : "receipts";
281
+ threshold = revenueLimitUSD;
282
+ unit = "USD annual receipts";
283
+ } else if (employeeCountLimit !== null) {
284
+ standardType = "employees";
285
+ threshold = employeeCountLimit;
286
+ unit = "employees";
287
+ } else if (assetLimitUSD !== null) {
288
+ // Asset-only standard (financial institutions — e.g. Commercial Banking).
289
+ // Labeled "assets" (NOT "receipts+assets") — these rows carry NO receipts
290
+ // component, so the label must not imply one.
291
+ standardType = "assets";
292
+ threshold = assetLimitUSD;
293
+ unit = "USD assets";
294
+ } else {
295
+ // A row exists but publishes no numeric standard (rare/reserved).
296
+ standardType = "unknown";
297
+ threshold = null;
298
+ unit = null;
299
+ fieldsUnavailable.push("threshold", "unit", "standardType");
300
+ }
301
+
302
+ const data: SizeStandardResult = {
303
+ naics,
304
+ found: true,
305
+ description: entry.description ?? null,
306
+ sector: entry.sectorDescription ?? null,
307
+ subsector: entry.subsectorDescription ?? null,
308
+ standardType,
309
+ threshold,
310
+ unit,
311
+ revenueLimitUSD,
312
+ employeeCountLimit,
313
+ assetLimitUSD,
314
+ footnote: entry.footnote ?? null,
315
+ asOf,
316
+ sourceUrl: SBA_NAICS_URL,
317
+ };
318
+
319
+ // A human-readable restatement of the threshold, so the AI never mistakes the
320
+ // raw millions figure ("34") for dollars.
321
+ const thresholdNote =
322
+ unit === "USD annual receipts"
323
+ ? `Size standard: $${(threshold as number).toLocaleString("en-US")} in average annual receipts (SBA publishes this as $${entry.revenueLimit} million).`
324
+ : unit === "employees"
325
+ ? `Size standard: ${(threshold as number).toLocaleString("en-US")} employees.`
326
+ : unit === "USD assets"
327
+ ? `Size standard: $${(threshold as number).toLocaleString("en-US")} in assets (SBA publishes this as $${entry.assetLimit} million); this is a financial-institution asset-based standard.`
328
+ : `No numeric size standard is published for NAICS ${naics} in this dataset.`;
329
+
330
+ const notes = [thresholdNote, NOT_A_CONSTANT_CAVEAT];
331
+ if (entry.footnote && entry.footnote.length > 0) {
332
+ notes.push(
333
+ "This NAICS carries an SBA footnote qualifying the standard (see data.footnote).",
334
+ );
335
+ }
336
+
337
+ return withMeta(data, {
338
+ source: "sba.gov naics.json (keyless)",
339
+ keylessMode: true,
340
+ returned: 1,
341
+ // LEAD-12 fix (Codex C76 dogfood, C78 live-repro): a single-NAICS lookup has
342
+ // exactly ONE matching size standard — totalAvailable is 1, NOT the whole
343
+ // dataset row count (`count`, ~978). Using `count` made totalAvailable(978) >
344
+ // returned(1), which buildMeta reads as totalProvesTruncation → truncated:true
345
+ // → complete:false, MISLABELING a complete exact answer as a truncated 1-of-978
346
+ // result (the explicit complete:true above was silently overridden).
347
+ totalAvailable: 1,
348
+ complete: true,
349
+ fieldsUnavailable,
350
+ notes,
351
+ // P5 provenance — threaded ONLY when NON-live ⇒ live stays byte-identical.
352
+ // On a snapshot, buildMeta forces complete!==true (M1a) and qualifies the
353
+ // totalAvailable via totalIsEstimated (M1b) — the found standard is disclosed
354
+ // as an as-of figure, never a live-authoritative one.
355
+ ...provenanceMeta(provenance),
356
+ });
357
+ }