Skip to content

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

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

feat(sled): state, local and education procurement#6
vdavez merged 3 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. Hold until the API ships them.

Siblings: makegov/tango-python#62, makegov/tango-node#18.

What's here

sled.go + sled_test.go, and five presets in shapes.go:

Method Route
ListSledOpportunities / IterateSledOpportunities 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 / IterateSledForecasts GET /api/sled/forecasts/
GetSledForecast GET /api/sled/forecasts/{forecast_id}/

Three options structs name every one of the API's 27 solicitation filters and 13 forecast filters.

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: boolPtr(false), which is the complement of open and therefore has to reach the server as the only liveness param. A subtest pins both halves — that no liveness param is sent when the caller asks for none, and that active=false is sent rather than dropped.

*bool, not bool, for the three boolean filters. Active, HasDocuments and SourceDeclared are pointers so false reaches the server instead of vanishing into Go's zero value — the same reason setIfNotNilBool exists in internal.go. Getting this wrong would silently make "closed solicitations" and "records with no documents" unqueryable.

ShapeSledRevisionsMinimal omits changes. The per-field before/after requires a Small plan; naming it in a suggested shape would 403 every Free caller on a field they never asked to gate. changed_fields is in the shape and available at every plan. A test asserts the split rather than trusting the constant to stay right.

ChangeSeenAfter has no Before twin, deliberately. The API exposes only the lower bound on that field. The struct comment says so, rather than leaving a reader to assume the pair was forgotten.

Scope note

This SDK has no dibbs, exclusions or sbir resources, so unlike tango-python and tango-node there was no vendored contract to refresh and no conformance or shape-coverage gate to satisfy — SLED lands as a plain resource file following the protests.go shape.

Testing

  • go test ./... and go test -race ./... — pass. sled_test.go adds 13 tests (one table-driven with 8 subtests).
  • go vet ./... — clean. gofmt -s -l . — clean.
  • golangci-lint run could not run here: the checked-in config fails to load on the locally installed version (can't unmarshal config by viper … 'output.formats' expected a map, got 'slice'). That is a pre-existing config/version mismatch unrelated to this change, and CI's pinned version should be unaffected — but it means lint is unverified locally, so worth a look at the CI result.

Docs

New State & Local (SLED) 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 both iterators and five `Shape*` presets. Three options structs name every one of the API's 27 solicitation filters and 13 forecast filters. Parity with the Python and Node SLED PRs.

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

Leaving both `Status` and `Active` unset returns open solicitations only. That default is the API's, and `ListSledOpportunities` deliberately does not synthesize `status=open` — doing so would make `Active: boolPtr(false)` unreachable, since `active=false` 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 `CategoryCode` is the escape hatch.

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

`Active`, `HasDocuments` and `SourceDeclared` are `*bool` rather than `bool`, so `false` reaches the server as a filter value instead of vanishing into the zero value.

`ShapeSledRevisionsMinimal` omits `changes` on purpose: the per-field before/after needs a Small plan, so naming it in a suggested shape would 403 a Free caller.

`go test ./...` and `go test -race ./...` pass; `go vet` and `gofmt -l` clean. `golangci-lint` could not run here — the checked-in config fails to load on the locally installed version (`output.formats expected a map, got slice`), which is unrelated to this change.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vdavez and others added 2 commits September 10, 2026 09:40
`attachments(extracted_text)` serves a SLED document body on a Small plan or above from API 4.25.1. This SDK returns `Record`, so the leaf needs no schema change — what it needed was saying so, in the three places a caller looks.

The leaf must be named: no `Shape*` preset includes it and `attachments(*)` does not carry it, because the API resolves the body only for a caller who asked. A preset naming it would make every detail fetch pay for a document nobody wanted to read, so `TestSledShapesDoNotNameThePaidDocumentBody` pins both presets against it.

The key is absent rather than null whenever 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, because 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`revive` requires an exported declaration's doc comment to begin with its own name, and this one said `ListSledOpportunitiesRevisionOptions` where the type is `ListSledOpportunityRevisionsOptions` — singular opportunity, plural revisions.

CI caught it because `golangci-lint` could not run locally: the checked-in config fails to load on the version installed here, which is the gap noted when this branch was opened. Swept every exported declaration in the file for the same class of mismatch; this was the only one.

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 0b62b9d into main Sep 10, 2026
5 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