The authoritative inventory of error codes is ERROR_RULES in src/visionset/server/errors.py, which maps every concrete kernel error to the status and code it becomes over HTTP. It currently holds 75 codes. The "full table" in docs/api.md lists 56 of them, so a reader treating that document as the inventory is missing 19.
The nineteen are ASSET_NOT_IN_BATCH, ASSET_NOT_WRITABLE, BACKGROUND_JOB_NOT_FOUND, BATCH_IMMUTABLE, DUPLICATE_CLASSIFICATION_TAG, EXPORT_SOURCE_UNREADABLE, TOKEN_NAME_TAKEN, TOKEN_NOT_FOUND, UNKNOWN_JOB_TYPE, and every inference code but one — INFERENCE_CONNECTION_INVALID, INFERENCE_CONNECTION_NAME_TAKEN, INFERENCE_CONNECTION_NOT_CHECKABLE, INFERENCE_CONNECTION_NOT_DOWNLOADABLE, INFERENCE_CONNECTION_NOT_FOUND, INFERENCE_CONNECTION_NOT_RUNNABLE, INFERENCE_CONNECTION_NOT_SET_UP, LOCAL_INFERENCE_UNAVAILABLE, UNSUPPORTED_PROMPT and WEIGHTS_DAMAGED. The exception is PROMPT_POINT_OUT_OF_BOUNDS, which is in the table only because the change that added it (#523, the suggest-prompt bounds refusal) put it there by hand — and finding that it was joining a table nineteen codes behind is what produced this issue.
Nothing gates the table. That is the whole explanation for the drift, and it is why syncing alone is not the fix: a table brought up to date today falls behind again the first time somebody adds a code without remembering this document exists.
The work is both halves
Sync the table to ERROR_RULES at HEAD. Every code, under its status, carrying the what-happened and what-to-do prose the error contract in that same document asks for. The three status rules already written there — 404 for a thing that is not present, 409 for a state that refuses a well-formed request, 422 for a payload that is wrong — decide where each one goes, and several of the nineteen also want a sentence rather than a bare listing, since a code whose remedy differs from its sibling's is exactly the case the document tells clients to branch on.
Gate it, so it cannot drift again. The invariant is that a new error code cannot ship without the document moving, and the gate has to fail in both directions: a code in ERROR_RULES and not in the table, and a code in the table that no longer exists in the code.
Two precedents worth reading before choosing a shape. tests/server/test_errors.py already does exactly this comparison one layer over — test_the_status_and_code_of_every_error holds ERROR_RULES to exact equality against a hand-maintained expected mapping, and a second test asserts each literal code is the screaming-snake spelling of its class name — so the new gate is that same construction with the markdown table as the other side. The frontend's frontend/ui-core/src/tokens.test.ts is the precedent for the document half specifically: it parses styles.css and holds it to two-way exact agreement with the tokens.ts mirror, under a pair of tests named for the two directions, and its own docstring makes the argument this issue is making — a mirror nobody checks is a second spelling waiting to drift.
Whether the gate parses the markdown table in place, or the table becomes a generated-and-committed artifact on the openapi.json footing where regeneration is the only legal way to change it, is the implementer's call. State which was chosen in the PR and why; the invariant is what matters, not the mechanism.
Two related records for context: the split that produced INFERENCE_CONNECTION_NOT_CHECKABLE (#475) is the reasoning for why sibling codes get separate rows rather than a shared one, and #523's PR body carries the delta as it stood when it was found.
The authoritative inventory of error codes is
ERROR_RULESinsrc/visionset/server/errors.py, which maps every concrete kernel error to the status and code it becomes over HTTP. It currently holds 75 codes. The "full table" indocs/api.mdlists 56 of them, so a reader treating that document as the inventory is missing 19.The nineteen are
ASSET_NOT_IN_BATCH,ASSET_NOT_WRITABLE,BACKGROUND_JOB_NOT_FOUND,BATCH_IMMUTABLE,DUPLICATE_CLASSIFICATION_TAG,EXPORT_SOURCE_UNREADABLE,TOKEN_NAME_TAKEN,TOKEN_NOT_FOUND,UNKNOWN_JOB_TYPE, and every inference code but one —INFERENCE_CONNECTION_INVALID,INFERENCE_CONNECTION_NAME_TAKEN,INFERENCE_CONNECTION_NOT_CHECKABLE,INFERENCE_CONNECTION_NOT_DOWNLOADABLE,INFERENCE_CONNECTION_NOT_FOUND,INFERENCE_CONNECTION_NOT_RUNNABLE,INFERENCE_CONNECTION_NOT_SET_UP,LOCAL_INFERENCE_UNAVAILABLE,UNSUPPORTED_PROMPTandWEIGHTS_DAMAGED. The exception isPROMPT_POINT_OUT_OF_BOUNDS, which is in the table only because the change that added it (#523, the suggest-prompt bounds refusal) put it there by hand — and finding that it was joining a table nineteen codes behind is what produced this issue.Nothing gates the table. That is the whole explanation for the drift, and it is why syncing alone is not the fix: a table brought up to date today falls behind again the first time somebody adds a code without remembering this document exists.
The work is both halves
Sync the table to
ERROR_RULESat HEAD. Every code, under its status, carrying the what-happened and what-to-do prose the error contract in that same document asks for. The three status rules already written there — 404 for a thing that is not present, 409 for a state that refuses a well-formed request, 422 for a payload that is wrong — decide where each one goes, and several of the nineteen also want a sentence rather than a bare listing, since a code whose remedy differs from its sibling's is exactly the case the document tells clients to branch on.Gate it, so it cannot drift again. The invariant is that a new error code cannot ship without the document moving, and the gate has to fail in both directions: a code in
ERROR_RULESand not in the table, and a code in the table that no longer exists in the code.Two precedents worth reading before choosing a shape.
tests/server/test_errors.pyalready does exactly this comparison one layer over —test_the_status_and_code_of_every_errorholdsERROR_RULESto exact equality against a hand-maintained expected mapping, and a second test asserts each literal code is the screaming-snake spelling of its class name — so the new gate is that same construction with the markdown table as the other side. The frontend'sfrontend/ui-core/src/tokens.test.tsis the precedent for the document half specifically: it parsesstyles.cssand holds it to two-way exact agreement with thetokens.tsmirror, under a pair of tests named for the two directions, and its own docstring makes the argument this issue is making — a mirror nobody checks is a second spelling waiting to drift.Whether the gate parses the markdown table in place, or the table becomes a generated-and-committed artifact on the
openapi.jsonfooting where regeneration is the only legal way to change it, is the implementer's call. State which was chosen in the PR and why; the invariant is what matters, not the mechanism.Two related records for context: the split that produced
INFERENCE_CONNECTION_NOT_CHECKABLE(#475) is the reasoning for why sibling codes get separate rows rather than a shared one, and #523's PR body carries the delta as it stood when it was found.