fix(shapes): close the reverse-coverage blind spot and the drift it hid#52
Merged
Conversation
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>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Tango published
"shape": nullforentities,opportunities,notices,protests,itdashboard,events, andnews. 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
ShapeParservalidates client-side, so the SDK rejected 13 fields the API accepts, before ever issuing a request: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.registeredandsba_business_typesregressed in 1.3.0 — they validate under 1.1.1 but not 1.3.0. Overlay generation had narrowedEntitywhile nothing was watching, which is the clearest evidence the gate's blindness was load-bearing.Making it non-repeatable
A resource declaring
shape_supported: truewith 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 predatingschema_version: 2omit the key, where a null tree is genuinely ambiguous and skipping remains the only safe read.A second, independent blind spot
RESOURCE_TO_METHODnever mappedprotests,psc,mas_sins,departments,business_types, orassistance_listings, and mappedofficestoNoneas "not yet implemented" — thoughlist_officesexists. 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 for541519, 2 for336411, 0 for a nonsense code). Now exposed onlist_protests().eventsandnewsstayNone— genuinely absent from the SDK, now with a note thatlist_webhook_event_typesis unrelated.One false positive, worth reading
The checker also flagged
mas_sins: searchas 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 forcloud. The contract recordsfilter_params: []formas_sinsbecause Tango's generator only surfaces search params for filtersets declaringmin_length_paramsorSearchFilterbackends declaringsearch_fields, andmas_sinsimplements 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. Droppedbudget/accounts: search, which #2944'sSearchFilterextraction now publishes — the first entry to be retired by that new warning.Worth flagging upstream separately:
mas_sinssearch is real but unpublished, so the contract still understates the API there.Regenerated inputs
probe_shape_types.pyre-run against the corrected contract, so the five newly-visible resources get live-sampled types rather than name heuristics —entitiesalone 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
mypy tango/strict: clean.tests/test_shape_coverage_gate.py(6 tests) and 2list_protestsfilter tests. Gate tests verified non-vacuous — reverting the fix fails exactly the two that should.ruff format --check: clean.ruff checkreports 23 errors, all pre-existing onmainin 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_filtersfails with a live-API 504 Gateway Timeout. Reproduced on unmodifiedmainin 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_codekwarg. 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, andshape_tier_requiredis available for surfacing upgrade hints.Follow-ups
mas_sinssearch is real but unpublished; the generator's search extraction misses non-SearchFilterimplementations.entityregression disappears once this releases, and its allowlists can finally be derived from the contract instead of hand-maintained.🤖 Generated with Claude Code