Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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[<filter name>]` 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.
Expand Down
21 changes: 21 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1123,3 +1123,24 @@ if (resp.agencyWarnings.length > 0) {
console.warn("resolved to:", resp.resolvedAgencies);
}
```

Each entry of `meta.resolved_filters[<filter name>]` 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<string, unknown>`, 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<string, ResolvedEntry[]>;
for (const entry of filters.awarding_agency ?? []) {
if (entry.matched_by === "fuzzy") {
console.warn(`${entry.token} loosely matched ${entry.resolved?.name}`);
}
}
```
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export interface PaginatedResponse<T> {
/**
* 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.
*/
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/client.meta-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Array<Record<string, unknown>>>).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({
Expand Down
Loading