Skip to content

fix(shapes): close the reverse-coverage blind spot and the drift it hid#52

Merged
vdavez merged 3 commits into
mainfrom
fix/shape-coverage-blind-spot
Jul 20, 2026
Merged

fix(shapes): close the reverse-coverage blind spot and the drift it hid#52
vdavez merged 3 commits into
mainfrom
fix/shape-coverage-blind-spot

Conversation

@makegov-mark

@makegov-mark makegov-mark Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Follow-up to makegov/tango#2944, which fixed the contract's shape blind spot upstream. This re-vendors the corrected contract and fixes what the blind spot had been hiding.

The bug

The reverse shape-coverage gate skipped any resource whose contract shape tree was falsy:

shape = (r.get("runtime") or {}).get("shape")
if not shape:
    continue

Tango published "shape": null for entities, opportunities, notices, protests, itdashboard, events, and news. Five of those were a generator crash on tier-aware viewsets, not a real absence. So the gate reported full coverage while checking nothing for seven resources — including the two most heavily used.

Re-vendoring the fixed contract surfaced 69 real gaps (36 fields, 20 expands, 13 flattened expands) across entities, notices, protests, and itdashboard. All now closed by a regenerated overlay; the gate reports 0 gaps, 0 baselined.

Why it was worse than missing schema entries

ShapeParser validates client-side, so the SDK rejected 13 fields the API accepts, before ever issuing a request:

client.list_entities(shape="display_name")   # ShapeValidationError
# ...but GET /api/entities/?shape=display_name returns 200

Affected: Entity.display_name, entity_type, entity_structure, organization_structure, profit_structure, purpose_of_registration, country_of_incorporation, past_performance, registered, sba_business_types; Protest.decisions, resolved_agency, resolved_protester. Each confirmed accepted by the live API via raw HTTP. All 13 now validate.

registered and sba_business_types regressed in 1.3.0 — they validate under 1.1.1 but not 1.3.0. Overlay generation had narrowed Entity while nothing was watching, which is the clearest evidence the gate's blindness was load-bearing.

Making it non-repeatable

A resource declaring shape_supported: true with no shape tree is now a hard finding (contract_missing_shape), reported as an upstream contract defect with its own remediation text rather than being mixed in with SDK schema gaps. Contracts predating schema_version: 2 omit the key, where a null tree is genuinely ambiguous and skipping remains the only safe read.

A second, independent blind spot

RESOURCE_TO_METHOD never mapped protests, psc, mas_sins, departments, business_types, or assistance_listings, and mapped offices to None as "not yet implemented" — though list_offices exists. Seven resources with working SDK methods had never had their filter coverage checked.

Wiring them up found one real gap: protests.naics_code. The API has always supported it (15,027 protests unfiltered, 12 for 541519, 2 for 336411, 0 for a nonsense code). Now exposed on list_protests().

events and news stay None — genuinely absent from the SDK, now with a note that list_webhook_event_types is unrelated.

One false positive, worth reading

The checker also flagged mas_sins: search as a stale param — "exposes params the API does not accept (silent no-ops — deprecate or fix mapping)".

That was wrong. The API applies it: 330 results unfiltered, 0 for a nonsense term, 32 for office, 3 for cloud. The contract records filter_params: [] for mas_sins because Tango's generator only surfaces search params for filtersets declaring min_length_params or SearchFilter backends declaring search_fields, and mas_sins implements search another way.

Acting on the checker here would have deprecated a working parameter. Added to CONTRACT_OMITTED_PARAMS (the existing mechanism for exactly this), which now also warns when an entry stops being needed so the carve-out list can't rot. Dropped budget/accounts: search, which #2944's SearchFilter extraction now publishes — the first entry to be retired by that new warning.

Worth flagging upstream separately: mas_sins search is real but unpublished, so the contract still understates the API there.

Regenerated inputs

probe_shape_types.py re-run against the corrected contract, so the five newly-visible resources get live-sampled types rather than name heuristicsentities alone contributes 136 observed paths. This matters because 1.3.0's overlay explicitly promised types "resolved from live-API sampling rather than guessed", and these resources had no samples at all.

Testing

  • 363 unit tests pass, 124 integration (cassette) tests pass, 1 skipped.
  • mypy tango/ strict: clean.
  • Both gates green: shape coverage 0 gaps; filter/shape conformance exit 0, no errors.
  • New: tests/test_shape_coverage_gate.py (6 tests) and 2 list_protests filter tests. Gate tests verified non-vacuous — reverting the fix fails exactly the two that should.
  • ruff format --check: clean. ruff check reports 23 errors, all pre-existing on main in files this PR doesn't touch (verified by stashing).

One pre-existing failure, not from this PR: tests/production/test_production_smoke.py::test_search_filters fails with a live-API 504 Gateway Timeout. Reproduced on unmodified main in a clean worktree.

Risks

Additive. The overlay only widens what the SDK accepts — no field was removed, no signature changed except a new optional naics_code kwarg. The main judgment call is trusting the max-tier shape tree: a caller below the gating tier can now request a field their plan won't return. That matches the contract's documented semantics, and shape_tier_required is available for surfacing upgrade hints.

Follow-ups

  1. tangomas_sins search is real but unpublished; the generator's search extraction misses non-SearchFilter implementations.
  2. tango-mcp (#64) — unblocked by this. Its entity regression disappears once this releases, and its allowlists can finally be derived from the contract instead of hand-maintained.

🤖 Generated with Claude Code

vdavez and others added 3 commits July 20, 2026 06:54
The reverse shape-coverage gate skipped any resource whose contract shape
tree was falsy (`if not shape: continue`). Tango published "shape": null for
entities, opportunities, notices, protests, itdashboard, events, and news —
five of them because its generator crashed on tier-aware viewsets
(makegov/tango#2944). So the gate reported full coverage while checking
nothing for them.

Re-vendoring the fixed contract surfaced 69 real gaps (36 fields, 20 expands,
13 flattened expands) across entities, notices, protests, and itdashboard,
now closed by a regenerated overlay.

The user-visible damage was worse than missing schema entries. ShapeParser
validates client-side, so the SDK rejected 13 fields the API accepts before
ever issuing a request: list_entities(shape="display_name") raised
ShapeValidationError against an endpoint returning 200. All 13 now validate.
Note that `registered` and `sba_business_types` regressed in 1.3.0 — they
validated under 1.1.1 — so overlay generation had narrowed Entity while
nothing was watching.

The gate no longer skips silently: shape_supported=true with no tree is a
hard finding (contract_missing_shape), reported as an upstream contract
defect rather than an SDK one. Contracts predating schema_version 2 omit the
key, where null stays ambiguous and skipping remains correct.

Separately, seven resources with working SDK methods had never been
conformance-checked: RESOURCE_TO_METHOD omitted protests, psc, mas_sins,
departments, business_types, and assistance_listings, and mapped offices to
None as "not yet implemented" though list_offices exists. Wiring them up
found one real gap — protests.naics_code, which the API has always supported
(15,027 unfiltered vs 12 for 541519) — now added to list_protests.

It also produced one false positive worth guarding: the checker called
mas_sins `search` a stale no-op, but the API applies it (330 unfiltered vs 0
for nonsense). The contract records filter_params: [] there, so trusting the
checker would have deprecated a working parameter. Added to
CONTRACT_OMITTED_PARAMS, which now warns when an entry stops being needed —
and dropped budget/accounts `search`, which #2944 now publishes.

Regenerated observed_shape_types.json against the corrected contract so the
five newly-visible resources get live-sampled types instead of name
heuristics (entities alone contributes 136 observed paths).

Gate tests verified non-vacuous by reverting the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-vendors the contract now that makegov/tango#2948 merged, which taught the
generator to publish params a viewset reads straight from query_params.

Two consequences, both anticipated:

1. The self-cleaning warning added in this PR fired on its own carve-out —
   "mas_sins: CONTRACT_OMITTED_PARAMS entries no longer needed (the contract
   now publishes them): search". Removed it. The list is now empty, which is
   the goal state; the mechanism stays for the next case.

2. A new real gap surfaced: psc `has_awards` filters (2,526 codes -> 2,243)
   but list_psc did not expose it. It was invisible until #2948 published it.
   Added, sending False explicitly rather than dropping it, since the API
   distinguishes the two.

Note that #2948 also fixed a truthiness bug where has_awards=false filtered
identically to true. Production is still on 4.18.5 and staging is at 4.18.5e,
so until that deploys, has_awards=False returns awards-only results. Verified
live: 2,526 unfiltered, 2,243 for both True and False today.

Gates: conformance exit 0 with no errors, shape coverage 0 gaps. 366 unit
tests pass, 124 integration. mypy strict clean. The one failure,
test_production_smoke::test_search_filters, is a live-API 504 reproduced on
unmodified main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Said the tango fix was not yet in production. It is. My check queried a single
cache key (limit=1) that I had myself populated pre-deploy, and /api/psc/
caches responses for 30 days per query-param key — so the stale entry masked a
working deploy.

Verified across unprobed keys: has_awards=False returns 2,526 (correct) and
True returns 2,243. Only the keys probed before the deploy still serve the old
result.

Noted the caching caveat, which is a real user-facing consequence: any
?has_awards= key requested before the deploy keeps serving awards-only results
until its entry expires or is invalidated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vdavez
vdavez merged commit 9a4ba2c into main Jul 20, 2026
11 checks passed
@makegov-mark makegov-mark Bot mentioned this pull request Jul 20, 2026
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