Skip to content

fix(api): the suggest route stops accepting a detail it never honoured - #468

Merged
JArmandoAnaya merged 1 commit into
mainfrom
fix/suggest-detail
Aug 9, 2026
Merged

fix(api): the suggest route stops accepting a detail it never honoured#468
JArmandoAnaya merged 1 commit into
mainfrom
fix/suggest-detail

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

POST /inference/suggest declared a detail field, validated it gt=0.0, le=1.0, and published it in openapi.json. Nothing read it.

Armando's decision on #463, 2026-08-09: "remove the detail parameter from POST /inference/suggest — request shape, validation, and its openapi.json entry. The ratified design (D3 on #424) specifies detail as a single server-side setting, never a per-request knob; the field is iteration residue in the contract. Removing it now is free (no release published); after a release it becomes a breaking change. The server-side setting stays as is."

cf. #424 (D3), cf. #464 (the sweep that found it).

The chain, and where it broke

Step Where Was
The field, declared and bounded server/models.py float | None, gt=0.0, le=1.0
The route defaults it and passes it on server/routes/inference.py detail=DEFAULT_DETAIL if body.detail is None else body.detail
suggest() accepts it and never reads it inference/suggestions.py the break
What actually reached polygon_from sam_provider.py self._detail, set at construction
…which providers._local() never passes so always DEFAULT_DETAIL

All four removals ship: the field and its validation, the route's argument, the route's now-dead DEFAULT_DETAIL import, and the dead parameter on suggest() with its import. ruff --select ARG — which is what found this — now reports nothing on suggestions.py.

The contract

before  properties: allowed_geometries, asset_id, connection_id, detail, negative, positive, project_id
after   properties: allowed_geometries, asset_id, connection_id,         negative, positive, project_id

openapi.json −13 lines, frontend/ui-core/src/generated/api.ts −2 (/** Detail */ detail?: number | null;). checks.ts regenerated and unchanged. Both regenerated through scripts/export_openapi.py and pnpm generate:client, never hand-edited.

What stays

The server-side setting is untouched, exactly as the decision says. masks.DEFAULT_DETAIL, polygon_from(..., detail=...), LocalSamProvider(detail=...) and the visionset.inference.__all__ export are all unchanged — the tolerance is still a fraction of the region's own size, still applied on every suggestion. Only the per-request knob goes.

No behaviour changes, and the reason is worth stating

SuggestRequest does not set extra="forbid" — it uses pydantic's default. So a client that still sends detail has it silently ignored, which is precisely what happened before this change, when the field was declared and then ignored one layer down. The removal is therefore invisible from outside: same status, same body, same polygon.

That is a fact about this route rather than the house convention, and it is the one thing here worth a second look — see "Found, not fixed".

Tests

Red-before-green does not apply, and it is worth saying why rather than implying it ran. The dispatch made it conditional on a test existing that asserts the field is accepted. There is none: git grep -nw detail -- tests/server/test_suggest.py tests/server/test_inference.py is empty. That absence is the reason the defect shipped — test_masks.py exercises polygon_from(detail=…) directly and proves the simplification responds to the setting, and nothing joined that to the wire field.

No test was flipped, because none asserted the old shape. No expected output moved: the sweep proved detail: 0.9 and detail: 0.01 already produced identical polygons, and tests/inference passes unchanged.

Mutation verification

Committed before the mutation. The anchor was asserted present exactly once before applying, and the replacement asserted after; reverted by git apply -R on the recorded patch with git status --porcelain empty afterwards.

Mutationdetail: float | None = Field(default=None, gt=0.0, le=1.0) added back to SuggestRequest, without regenerating the contract.

FAILED tests/server/test_openapi_contract.py::test_the_committed_openapi_matches_the_application

That is the named test. The shell drift gate (scripts/check.sh generatedexport_openapi.py + git diff --exit-code openapi.json) catches the same thing a second way, and is green here.

Frontend

No call site ever sent it, so nothing needed adapting — which is what the dispatch asked to be confirmed rather than assumed. SuggestInput in ui-core/src/data/inferenceQueries.ts declares projectId, assetId, connectionId, positive, negative, allowedGeometries and nothing else, and a word-boundary grep for detail across frontend/ui-core/src, frontend/annotator/src and frontend/app/src returns only unrelated prose and the schema-diff change.detail. The generated client compiles: pnpm -r build, test and lint are all green in the frontend stage below.

Docs

docs/inference.md described detail as something a caller sends — "Omit it and the server's default…". That paragraph is now false rather than merely stale, and it is the user-facing API reference, so it moved: same information about the tolerance, stated as the server's setting. One paragraph, no other prose touched. CLAUDE.md requires the relevant doc to move with a feature change, and the bounds neither named nor banned docs/; flagging the judgment here rather than making it silently.

Found, not fixed

#466, filedSuggestRequest, SuggestPoint, ConnectionCreate and ConnectionUpdate are the only request models in server/models.py that do not set extra="forbid"; the other six do, with no comment anywhere recording the four as a deliberate exception. It is what makes this removal free, and it is also why a misspelled field on a connection update answers 200 and changes nothing. Tightening it is a behaviour change on a route the dispatch bounded, and the dispatch said not to invent a stricter or looser handling for this one route — so it is recorded, not acted on.

Gate

Every stage run in this worktree, exit code recorded. Split by directory because of the ~10-minute command ceiling on this machine; the split is ls tests/ at run time, and tests/scripts is node --test, covered by the frontend stage. Another session held the box with its own pytest throughout; load average stayed at 2–4 on 20 cores and the browser ports are worktree-derived, so nothing was contended.

Stage Command Exit
pytest A pytest tests/{architecture,cli,examples,fixtures,formats,inference,jobs,mcp,packaging,test_versioning.py} 0 — 986 passed, 5 skipped
pytest B pytest tests/kernel 0 — 1378 passed, 3 skipped
pytest C pytest tests/server 0 — 628 passed
lint ruff check . 0 — all checks passed
format ruff format --check . 0 — 341 files already formatted
types mypy src/visionset 0 — no issues in 150 source files
imports lint-imports 0 — 4 contracts kept, 0 broken
frontend scripts/check.sh frontend 0 — annotator 904 passed, ui-core 806 passed, app build, node --test script gates
generated scripts/check.sh generated 0 — openapi drift, generated client drift, MCP tool reference drift all clean after regenerating
browser scripts/check.sh browser 0 — e2e 233 passed, cycle 1 passed (ports 17789/19837/21885)

The generated stage is the load-bearing one here: it regenerates openapi.json and the client and diffs them, so a green run is the proof that the two committed artifacts match the application rather than my hand.

Closes #463

`POST /inference/suggest` declared a `detail` field, validated it
gt=0.0/le=1.0, and published it in openapi.json. Nothing read it: the
route passed it to `suggest()`, which took the parameter and dropped it,
and the value reaching `polygon_from` was the adapter's construction-time
default. `detail: 0.9` and `detail: 0.01` produced the same polygon.

D3 on #424 specifies the simplification tolerance as a single server-side
setting rather than a per-request knob, so the field was iteration residue
in the contract. The setting itself is untouched.

Removed end to end: the request field and its validation, the argument the
route threaded, and the dead parameter on `suggest()`. openapi.json and the
generated TS client regenerated. No frontend call site ever sent it.

Closes #463
@JArmandoAnaya JArmandoAnaya added this to the 0.1.0 milestone Aug 9, 2026
@JArmandoAnaya JArmandoAnaya added bug Something isn't working api visionset.server — REST API and OpenAPI contract backend visionset Python distribution — kernel, server, cli, mcp, formats labels Aug 9, 2026
@JArmandoAnaya
JArmandoAnaya merged commit c69c1ce into main Aug 9, 2026
13 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the fix/suggest-detail branch August 9, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api visionset.server — REST API and OpenAPI contract backend visionset Python distribution — kernel, server, cli, mcp, formats bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The suggest route's detail field is accepted, published, and ignored

1 participant