From d652b64f48befa03210d5e1828490eb8136b0934 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Tue, 6 Oct 2026 14:52:02 -0500 Subject: [PATCH] docs(pagination): document and test matched_by on agency-filter diagnostics Tango API 5.9.0 adds `matched_by` to each resolved entry of `meta.resolved_filters`, saying how the token matched: `key`, `code`, `name`, `alias` or `fuzzy`. `PaginatedResponse.meta` is passed through as the API sends it, so the field already reaches callers. This documents it on the `meta` property and in the API reference, and adds a test that it round-trips and leaves the three parsed agency views unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ docs/API_REFERENCE.md | 21 +++++++++++++++++++++ src/types.ts | 2 ++ tests/unit/client.meta-diagnostics.test.ts | 15 +++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9cb43..3891661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project follows [Semantic Versioning](https://semver.org/). ### Added +- **`matched_by` on agency-filter diagnostics** (Tango API 5.9.0; parity with tango-python). Each entry of `PaginatedResponse.meta.resolved_filters[]` that resolved now says how its token matched: `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match, worth checking against the resolved name). An entry that did not resolve has no `matched_by`. `meta` is passed through as the API sends it, so no code change was needed to receive the field; it is now documented on the interface and in `docs/API_REFERENCE.md` and pinned by a test. `agencyWarnings`, `unresolvedAgencyTokens` and `resolvedAgencies` are unchanged. + - **Boards-of-contract-appeals decisions** (Tango API 4.26.0). `listContractAppeals(options)` and `getContractAppeal(uuid, options)` over `/api/contract_appeals/`, with every filter the API accepts declared as a typed option on `ListContractAppealsOptions` (`search`, `board`, `docket`, `appellant`, `judge`, `decision_type`, the `decision_date_after` / `_before` pair, `listed`, `document_id`, `ordering`), the new `ContractAppealRecord` return type, and a registered `ContractAppeal` shape schema so the typed shape API resolves the resource's fields. These are Contract Disputes Act decisions from the CBCA (civilian) and the ASBCA (defense) — a dispute under an existing contract, not a challenge to an award. Bid protests remain the separate `listProtests()` resource, and the two do not overlap. diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index ddbb4e7..fe32342 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1123,3 +1123,24 @@ if (resp.agencyWarnings.length > 0) { console.warn("resolved to:", resp.resolvedAgencies); } ``` + +Each entry of `meta.resolved_filters[]` that resolved also says how its token matched, in `matched_by` (Tango API 5.9.0+): + +- `key` — an organization UUID. +- `code` — a 3-digit CGAC or 4-digit FPDS code. +- `name` — the organization's name, including a department's everyday name, a spelling variant or a rename. +- `alias` — an abbreviation or the organization's own alias. +- `fuzzy` — a looser text match, worth checking against the resolved `name`. + +An entry that did not resolve has no `matched_by`. +`meta` is typed `Record`, so narrow it before reading: + +```ts +type ResolvedEntry = { token: string; matched_by?: string; resolved: { name: string } | null }; +const filters = (resp.meta?.resolved_filters ?? {}) as Record; +for (const entry of filters.awarding_agency ?? []) { + if (entry.matched_by === "fuzzy") { + console.warn(`${entry.token} loosely matched ${entry.resolved?.name}`); + } +} +``` diff --git a/src/types.ts b/src/types.ts index fddc861..88cb482 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,8 @@ export interface PaginatedResponse { /** * Response-level metadata the API attached to this page, when present. * Currently carries agency-filter diagnostics: `resolved_filters` maps each agency filter to the organizations its `|`-separated tokens resolved to (or `null`), and `warnings` lists human-readable notes about tokens that were dropped or matched loosely. + * Each `resolved_filters` entry that resolved also carries `matched_by` (Tango API 5.9.0+), saying how the token matched: `"key"` (an organization UUID), `"code"` (a 3-digit CGAC or 4-digit FPDS code), `"name"` (the organization's name, including a department's everyday name, a spelling variant or a rename), `"alias"` (an abbreviation or the organization's own alias) or `"fuzzy"` (a looser text match, worth checking against the resolved `name`). + * An entry that did not resolve has no `matched_by`. * See `agencyWarnings`, `unresolvedAgencyTokens`, and `resolvedAgencies` for the parsed views. * Optional (like the other three diagnostics) so pre-existing code constructing a `PaginatedResponse` still compiles; responses built by the client always populate it. */ diff --git a/tests/unit/client.meta-diagnostics.test.ts b/tests/unit/client.meta-diagnostics.test.ts index e8d5014..22e6abc 100644 --- a/tests/unit/client.meta-diagnostics.test.ts +++ b/tests/unit/client.meta-diagnostics.test.ts @@ -49,6 +49,21 @@ describe("PaginatedResponse agency-filter diagnostics", () => { expect(res.meta).toEqual(meta); }); + it("matched_by reaches the caller and leaves the parsed views unchanged", async () => { + const entries = [ + { token: "HUD", matched_by: "alias", resolved: HUD }, + { token: "Housing and Urban Developmnt", matched_by: "fuzzy", resolved: HUD }, + { token: "HUDD", resolved: null }, + ]; + const res = await makeClient(emptyPage({ resolved_filters: { awarding_agency: entries } })).listContracts({ + awarding_agency: "HUD|Housing and Urban Developmnt|HUDD", + }); + const returned = (res.meta?.resolved_filters as Record>>).awarding_agency; + expect(returned.map((entry) => entry.matched_by)).toEqual(["alias", "fuzzy", undefined]); + expect(res.unresolvedAgencyTokens).toEqual({ awarding_agency: ["HUDD"] }); + expect(res.resolvedAgencies).toEqual({ awarding_agency: [HUD, HUD] }); + }); + it("dropped tokens are reported per filter", async () => { const res = await makeClient( emptyPage({