Skip to content

feat(sled): state, local and education procurement - #18

Merged
vdavez merged 2 commits into
mainfrom
feat/sled
Sep 10, 2026
Merged

feat(sled): state, local and education procurement#18
vdavez merged 2 commits into
mainfrom
feat/sled

Conversation

@makegov-mark

@makegov-mark makegov-mark Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

SDK support for Tango's new /api/sled/ namespace — state, local and education (SLED) procurement. Blocked: the endpoints are not live on the public API yet, and the vendored contract in this PR is ahead of what is deployed. Hold until the API ships them.

Python SDK equivalent: makegov/tango-python#62.

What's here

Method Route
listSledOpportunities() GET /api/sled/opportunities/
getSledOpportunity() GET /api/sled/opportunities/{opportunity_id}/
listSledOpportunityRevisions() GET /api/sled/opportunities/{opportunity_id}/revisions/
getSledCoverage() GET /api/sled/opportunities/coverage/
listSledForecasts() GET /api/sled/forecasts/
getSledForecast() GET /api/sled/forecasts/{forecast_id}/

Plus iterateSledOpportunities / iterateSledForecasts and their IterableListMethod entries, the SledOpportunity / SledOpportunityRevision / SledForecast interfaces and six nested payload types, explicit shape schemas for all nine models, and five ShapeConfig constants. All 27 solicitation filters and all 13 forecast filters are typed options.

Four things a reviewer should check

The SDK must not synthesize status=open. The API defaults a list request with no status and no active to open-only. Making that explicit client-side breaks active: false, which is the complement of open and therefore has to reach the server as the only liveness param. Two tests pin both halves.

SLED_REVISIONS_MINIMAL omits changes. The per-field before/after requires a Small plan; naming it in a default shape would 403 every Free caller on a field they never asked to gate. changed_fields is in the default and available at every plan.

Support filters are filters only. platform, native_id and external_id are typed options and appear in no schema — a test asserts the registry has no such fields, so a caller can't shape on something the API will never return.

The shape tests walk nested expands, not just root leaves. This SDK has no parser.validate(); the existing parity test only checks root-level field names. client.sled.test.ts adds a walker that follows each expand through nestedModel, so attachments(size_bytes,…) and revisions(…,changes) are actually checked against SledAttachment / SledRevision rather than passing because nobody looked.

The fourteen nested sub-resource routes

Refreshing the contract described agencies_contracts_awarding, agencies_contracts_funding, contracts_subawards, entities_contracts, entities_idvs, entities_lcats, entities_otas, entities_otidvs, entities_subawards, idvs_awards, idvs_idvs, idvs_lcats, otidvs_awards and vehicles_orders for the first time. Unlike tango-python, this SDK treats an unmapped resource as a hard error, so the conformance gate went red with fourteen of them.

All fourteen already have SDK methods, so they are now mapped in RESOURCE_TO_METHOD. That converts fourteen errors into the honest finding: most of their options interfaces accept filters through an [key: string]: unknown index signature rather than declaring them, which the gate now reports as warnings. Declaring those is its own piece of work.

Their shape-coverage gaps are baselined instead, matching tango-python — each reuses its parent resource's model rather than carrying one of its own, so there is no model to map. None is SLED, and none is a regression.

Testing

  • npx vitest run tests/393 passed across 46 files; tests/unit/client.sled.test.ts adds 27.
  • npm run typecheck — clean.
  • npm run lint — clean.
  • npm run check-conformance0 errors (was 14 after the contract refresh), 12 warnings.
  • npm run check-shape-coverage0 new drift, 14 baselined.

npm run format reformats the whole repo, so the diff here is hand-scoped: three files (scripts/check-filter-shape-conformance.ts, scripts/check-shape-coverage.ts, tests/unit/config.shapes.parity.test.ts) were already prettier-dirty at main and carry only the added lines, not a reformat.

Docs

New State & Local (SLED) — Beta section in docs/API_REFERENCE.md, and an addition to docs/WEBHOOKS.md troubleshooting: an exclusion or DIBBS solicitation reaching its date fires nothing, because open/closed is derived at query time — but alerts.sled_opportunity.match does fire when a SLED solicitation closes, since its liveness is a stored field the API recomputes every fifteen minutes.

🤖 Generated with Claude Code


Update: the paid document-body leaf

Pushed a second commit adding attachments(extracted_text) — a SLED document body, served on the Small plan and above, from Tango API 4.25.1.

Three rules are documented wherever a caller will look, because each one is otherwise a support ticket:

  • The leaf must be named. No default or suggested shape includes it, and attachments(*) does not carry it — the API resolves the body only for a caller who asked, so a default naming it would make every detail fetch pay for a document nobody wanted to read. A test pins the shapes against it.
  • The key is absent, not null, whenever the text is not being served: below Small (where it is withheld and named in meta.upgrade_hints), on a contested document, or where the text could not be resolved.
  • A contested document never returns text, at any plan — its stored bytes disagree with what the record advertised, so its extracted text is not reliably that record's content.

And the distinction worth stating plainly, because it reads backwards in both directions: searching document text is ungated (every plan, ids only, no fragment of the document) while reading a body is paid (Small and above, one record at a time). Buying the body does not change search.

Sequencing: the SLED surface itself is live on the production API. extracted_text needs 4.25.1, which is not deployed yet — so this PR is mergeable, but that one field will 400 until the API catches up. GET /api/version reports what a caller is talking to.

Six methods over Tango's new `/api/sled/` namespace — `listSledOpportunities`/`getSledOpportunity`, `listSledOpportunityRevisions`, `getSledCoverage`, `listSledForecasts`/`getSledForecast` — plus the two async iterators, three model interfaces with their six nested payload types, explicit shape schemas for all of them, and five `ShapeConfig` defaults. Parity with the Python SDK's own SLED PR.

Four behaviors are documented on the options interfaces because each one misleads a caller who assumes federal semantics.

Passing neither `status` nor `active` returns open solicitations only. That default is the API's, and the SDK deliberately does not synthesize a `status` param — doing so would make `active: false` unreachable, since it is the complement of open. A test pins that.

`status` is API-derived liveness, recomputed every fifteen minutes. The source portal's own word is served as `source_status`, frozen at last capture, and is not a liveness filter.

Category scheme tagging is partial, so `naics` matches only the small tagged share and `category_code` is the escape hatch.

`meta.attachment_count` can be lower than `attachments.length`, because an auto-generated portal cover sheet is listed and flagged but excluded from the count.

`SLED_REVISIONS_MINIMAL` omits `changes` on purpose: the per-field before/after needs a Small plan, so naming it by default would 403 a Free caller.

Refreshing the vendored contract described fourteen nested sub-resource routes for the first time, and an unmapped resource is a hard conformance error here rather than a warning. All fourteen have SDK methods, so they are now mapped; that turns fourteen errors into the honest finding, which is that most of their options interfaces accept filters through an index signature rather than declaring them. Their shape-coverage gaps are baselined, matching the Python SDK — each reuses its parent resource's model rather than carrying one of its own.

`docs/WEBHOOKS.md` troubleshooting gained the date-lapse rule and its one exception. An exclusion or DIBBS solicitation reaching its date fires nothing, because open/closed is derived at query time — but `alerts.sled_opportunity.match` does fire when a SLED solicitation closes, since its liveness is a stored field the API recomputes on a clock.

393 tests passing, tsc and eslint clean, conformance 0 errors, shape coverage 0 new drift.

Blocked until the SLED endpoints are live on the production API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`attachments(extracted_text)` — SLED document bodies on the Small plan and above. The leaf joins `SledAttachmentPayload` and the explicit shape schema, and re-vendoring the contract is what makes it validate rather than be rejected client-side before a request is issued.

Three properties are documented on the interface and in the API reference, because each is otherwise a support question.

The leaf must be named. Neither `ShapeConfig` default includes it and `attachments(*)` does not carry it, because the API only resolves the body for a caller who asked — a default naming it would make every detail fetch pay for a document nobody wanted to read. A test asserts neither default names it.

The key is absent rather than null when the text is not being served; below Small it is withheld and named in `meta.upgrade_hints`.

A contested document never returns text at any plan: its stored bytes disagree with what the record advertised.

Searching document text and reading it stay separate. `search` matches inside attachment text on every plan and returns no fragment of it; the body is a per-record read on Small and above.

396 tests passing, tsc and eslint clean, conformance 0 errors, shape coverage 0 new drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vdavez
vdavez marked this pull request as ready for review September 10, 2026 15:53
@vdavez
vdavez merged commit bf67f70 into main Sep 10, 2026
7 checks passed
@vdavez
vdavez deleted the feat/sled branch September 10, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant