Repository navigation
v3.2.0
Twelve tickets and twenty-seven review items, in four batches. Three of the
changes are breaking: two of them close a cost a public unauthenticated
endpoint was paying, and the third makes a ceiling bound what a request
receives rather than only what it may ask for.
Breaking
- A single request may now read at most 4,000 list rows across every list
field it names,MAX_LIST_ROWS_PER_REQUEST. Nothing bounded the list
fan-out before: 166 aliasedactivitiesfields carrying no track at all
took about 3.1 s on the real export, eleven times the worst legal track
shape and within 4.9x of the 15 s function limit, on a public endpoint
that needs no authentication. The budget is charged before the SQL runs
and counts rows materialised rather than rows scanned, so a refusal costs
nothing. A document that reads more than 4,000 rows, legal until now, is
refused with a readable sentence (CUI-0027). track(points: 0)is refused in both deployment modes, with the same
sentence. The static mode returned every stored point for0while the
API refused it -- the API's bound is what closed AU-001's denial-of-service
vector, so the static mode is what moves. Non-integer and beyond-Int32
values are likewise refused on both sides now; their two messages differ,
which the source records deliberately, because the API refuses them in
Intcoercion before any resolver sees them (CUI-0025).track()withpointsomitted returns at mostMAX_TRACK_POINTSrows.
track(id, 1200)was refused whiletrack(id)returned 1,200, so the
constant bounded what could be asked for and not what came back. No
currently exported track exceeds 600 rows, so today's data is unchanged --
verified point for point over 134,041 track rows across all 365
activities, and again against the static writer, which this change does
not touch. An export built with--pointsabove 1,000 would be capped
where it previously was not (CUI-0033).
Fixed
- A budget refusal no longer writes a traceback to the server log. Strawberry
logged theValueErrorthese budgets raised as an unexpected error, at
ERRORwithexc_info, which put nine frames of absolute source paths --
the repository layout and the interpreter'ssite-packagesdirectory among
them -- into the log for every refused request, on a public endpoint, under
no authentication and in a deployment's default logging configuration. The
refusal now carries the GraphQL error itself and is logged atINFO, below
the threshold an unconfigured deployment applies, so what the log gains per
refused request goes to zero rather than to one line. The client is told
exactly what it was told before (CUI-0029). - Every view routes a failed query through
readableError()instead of
renderingerror.message. Four views put the raw message on the page: a
serialised blob of up to about 1,250 characters carrying the whole GraphQL
document and its variables' values (CUI-0030).
Added
MAX_TRACK_POINTSis now held across both languages in both directions.
Changing the TypeScript copy already went red; changing the Python one and
regenerating the SDL went green with the two sides disagreeing. The
generatedschema.graphqlis the link that closes it (CUI-0034).SAMPLE_KEY_SEPARATOR's load-bearing invariant has tests. It could be set
to a decimal digit, or dropped entirely, with the whole suite still green
(CUI-0028).pytest-covis in thedevextra with its coverage configuration, rather
than being assumed present (CUI-0032).docs/deployment.mdsays how to see budget refusals in a deployment's log,
with a snippet that works as written -- they are below the default
threshold by design (S-076).
Documented
- The test inventory in
README.mdanddocs/architecture.mdreads as the
partial list it is, and points attests/as the source of truth rather
than repeating counts that drift (CUI-0031, CUI-0024). - Twenty-seven review items, almost all of them a sentence that explained why
correct code was correct and got the reason wrong: a wall clock bound to
the wrong document, a cost attributed to per-field dispatch that the
measurements put at 2% rather than half, a byte size quoted from a minimal
reproduction as though it were the real file, a fixture described as
captured from a real run that was built by hand (S-053 through S-060,
S-063 through S-082, W-027 through W-029). graphql-coreis declared third-party so its import sorts with the other
packages. Ruff read a baregraphqlimport as this repository's own module,
becauseapi/is on its source path and Vercel matches only
api/graphql.pyas a function (S-077).
Known
- The budget refusals are fixed; the bounds refusals beside them are not.
_pageand_track_pointsstill raise a bareValueError, so
{ activities(offset: -1) { id } }-- 33 bytes, eleven tokens, no
authentication -- still writes 2,016 bytes of traceback with the same nine
absolute paths. That is about five times cheaper to trigger than the
cheapest document that reaches a budget refusal, five aliasedactivities
fields atlimit: 1000: eleven tokens against fifty-seven, 33 bytes against
173 (CUI-0037). Corrected on 2026-09-17: this bullet first said seven tokens
and 140 times cheaper. Seven was a miscount, and 140 was 998 divided by it --
998 being the size of CUI-0027's worst-case query-cost document rather than
the cost of triggering a budget refusal, so the two were never like for like.
CUI-0037 carries the working; the conclusion it draws is unchanged. - Two mutants survive the whole suite behind 100% line coverage on
api/schema.py: raisingREFUSAL_LOG_LEVELtoWARNINGundoes what
CUI-0029 buys in an unconfigured deployment, and handingprocess_errors
the unfiltered list puts refusals back onERROR. No test names an
operation carrying a refusal and a fault at once, which is the only shape
that tells the two lists apart (CUI-0038, CUI-0039). - The Vercel entry's start-up failure path has no tests and is outside the
coverage source, so the code that explains a broken deploy is unguarded
(CUI-0035).npm auditruns in no gate; the fourteen advisories it reports
today are all in the build toolchain, and--omit=devreports none
(CUI-0036).