Invariant: limit must bound every unbounded array, and truncation must always warn — sweep all 25 MCP tools #267
Labels
No labels
burndown-2026-06
headless
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/eco-app#267
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why this is filed as an invariant, not a tool bug
360873f—fix(tools): bound the detail arrays that put six tools over the response cap— is the #256 fix, landed 2026-08-13 11:03. Testing at5e05296three hours later found it bounded roughly one array per tool, leaving the largest array in two of those tools unbounded. Four other tools have nolimitparameter at all while returning arrays that grow with world size.The stated purpose of
limitis "a slice that keeps a no-argument call inside an MCP client's response cap". That goal is not met, and the caller has no workaround for the arrays that ignore it — which is the exact condition #256 was opened to remove.The rule
limit.limitMUST NOT be shadowed by an undocumented internal cap.limit, including the four that currently do not.limit— this is already documented and already true; keep it.Confirmed violations at
5e05296(observed 2026-08-13)get_crafting_atlas—limitapplies to 1 of 6 arrayslimit=1produced one warning,"flows: showing 1 of 1,185 rows". Returned in full regardless:limit=1byCraftedbyStationbyGatheredbyCitizenbyCitizenIterationsThis is the largest payload in the suite (~45 KB at
limit=1).get_trades—limitapplies to the small array, not the large onelimit=2correctly boundedtradeswith"trades: showing 2 of 528 rows".byItemreturned ~250 rows unbounded and is the bulk of the payload.get_currency—limitshadowed by an internal cap of 15Querying one currency with
limit=3returned 15 holder rows againstaccountsCounted: 22. The defaultlimit=50also returned 15. The parameter has no effect on this list at any value.Separately, this tool duplicates its own payload:
currenciesandpersonalreturn the same records twice, andmintedrepeats two more — roughly 2× the necessary bytes on the tool whose own warnings are about response caps.get_social— silent truncation, nolimitparametertotalFirstLogins: 124,newArrivalsarray contains 60,warnings: []. This is the only silent truncation found across all 25 tools.reputationEdgesalso returned ~210 rows unbounded.No
limitparameter at allget_map(67 deeds today, unbounded by design),get_social,get_world,get_climate(four series × 41 points, returned even on a not-found response).Reference implementations already in the repo
These are correct and should be the pattern — no need to invent one:
get_civics— bounds five separate lists, emits five separate shown-of-total warnings. The best implementation in the suite.get_stores— boundsstoresandtraders, warns on both.get_species— thins to evenly-spaced samples with endpoints preserved, warns, and keepspopulationFirst/Latest/Deltadescribing the whole series.Acceptance criteria
limit.get_currency's 15-row holder cap is removed or documented and driven bylimit.get_currencystops returning the same records incurrencies,personalandminted.limitadded toget_map,get_social,get_world,get_climate.get_socialwarns onnewArrivalstruncation.limitY/N · warns on truncation Y/N.limit, no returned array exceeds it and every truncated array produced a warning — rather than one test per tool.Refs #256.
Claiming the
get_socialsilent truncation only. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer. Not claiming the sweep.Same discipline as on #266: this issue is written as an invariant across 25 tools and I am taking one violation, saying so, and leaving it open.
Why this one
Your own words: "This is the only silent truncation found across all 25 tools."
Everything else in your list returns too much — unbounded arrays a caller can at least see in full. This one returns less than it has and says nothing, which is the failure your rule 2 exists to prevent:
A caller reading
newArrivalshas no way to know it is looking at 60 of 124. An unbounded array is a size problem; a silent cap is a correctness problem, and it is the one that produces confidently wrong answers downstream.That is why it is worth doing ahead of the bigger payload violations, even though
get_crafting_atlasat ~45 KB is the larger number.The pattern is already in the repo
You named
get_civicsas the reference — five lists, five shown-of-total warnings. I will follow it rather than invent a shape, and I will not touchget_civics,get_storesorget_species, which you recorded as correct.Scope
newArrivalswarns when it truncates, naming shown-of-total.limitis trivial in the same edit — a warning that tells the truth is the fix; changing the cap is a separate decision.reputationEdgesat ~210 rows unbounded is noted in your body and I am not taking it here, because adding alimitparameter to this tool is a signature change and belongs with the four tools in your "nolimitparameter at all" list.What I will not claim
That this closes the issue. Your acceptance is all 25 tools audited into a table plus a generic invariant test. One silent truncation fixed is not that, and I would rather leave the sweep visibly open than have it look done.
I will report if an existing test encodes the silence rather than adjusting it.
get_socialsilent truncation built. PR #273. Claim released. This issue stays open.Wording follows
get_civics, the reference you named — shown-of-total, plus a pointer to the summary field that still covers every row.The cap is unchanged, deliberately
MAX_NEW_ARRIVALSstays where it is and stays configurable throughECO_SOCIAL_ARRIVALS. Choosing a different number is a decision; saying which number was applied is not, and only the second was silent. Your rule 2 asks for the warning, not for a different bound.Both directions tested
A warning on an untruncated list would make the honest case indistinguishable from the capped one — the same confusion pointing the other way — so
test_new_arrivals_stays_quiet_when_nothing_is_cutpins that a short list stays clean.An error of mine worth recording
My first two test attempts failed because I guessed the API: I invented
SocialEventandbuild_social_surface, then calledSocialSurface()without its two required fields. I fixed the tests rather than reshaping the code to match my guess, which is the trap in a repository where the production surface is the thing under test. Recording it because a test written to fit a wrong mental model is exactly how an invariant sweep goes wrong.Validation
Mutation: silencing the warning fails
test_new_arrivals_says_when_it_truncatesand nothing else.Still open, and it is most of the issue
reputationEdgessits in the tool I just touched and I left it, because addinglimithere is a signature change and belongs with the four tools in your no-parameter list rather than smuggled in beside a warning.Claiming
get_crafting_atlasonly. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer. Still not the sweep.Second slice from this issue. Taking the largest payload violation now that the silent one is done.
One of six arrays bounded. Your body calls it the largest payload in the suite and it is the clearest case of
limitnot meaning what it says.Why this one matters beyond its size
It is the direct cause of a defect in a consuming repository. On
coilyco-gaming/sirens-echoI measured that the harness bounds a tool result with a head slice and this server carrieswarningsas its last JSON key — so a response over the consumer's 8 KiB cap loses its caveats first, and the model receives rows with no warning attached.A 45 KB response is guaranteed to hit that. Bounding these arrays fixes
sirens-echo#449at the source rather than mitigating it downstream, which is the point I made when I found the eco-app connection.Approach
server.py:1400already bounds a list of payload keys and emits a shown-of-total warning per key, with the "pass limit=0 for all of them (the summary fields above already cover every row)" wording. I expect this to be a matter of naming the five missing keys rather than writing anything new — and if it is not, I will say so rather than inventing a second mechanism beside the one that works.Rule 5 is the thing I will check hardest: summary and aggregate fields must keep describing every row regardless of
limit. A bound that silently narrows a total would be a worse bug than the one I am fixing.Not taking, again
get_trades'byItem,get_currency's shadowed 15-row cap and duplicated payload, the four tools with nolimitat all, and the 25-tool audit with its generic invariant test. This issue stays open and I will keep saying so on each slice.Claiming
get_tradesonly. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer. Third slice, still not the sweep.Your body's own framing is the sharp part:
limitbounds the small array and not the large one. A caller passinglimit=2to keep a response small gets 250 rows anyway, so the parameter reads as working while the payload stays big.Same shape as
get_crafting_atlas, which I just landed as #274 —_bound_rowsalready does this and the key was simply not named.What I will check before pushing
That
byItemis a detail array and not a summary.get_tradesis the tool where that distinction matters most: ifbyItemis the per-item aggregate that other fields are derived from, bounding it would violate your rule 5 rather than satisfy rule 1, and the fix would be wrong in a way tests might not catch.If it turns out to be a summary, I will say so and leave it, because "summary and aggregate fields continue to describe every row regardless of
limit" is your rule and it outranks the size complaint.Not taking
get_currency's shadowed 15-row cap and its duplicatedcurrencies/personal/mintedpayload, the four tools with nolimitparameter, and the 25-tool audit with the generic invariant test. Three slices in, the sweep is still the sweep.Claiming
get_currency's shadowed holder cap. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer. Fourth slice, still not the sweep.This is your rule 3, and it is the only one of the four rules nothing has violated except here:
It is a different defect from the three I have fixed. Those were arrays that ignored
limitby never being passed to the bounding helper — the parameter did nothing to them. Here the parameter is overridden: a caller can raise it, lower it, or leave it default and get 15 either way, solimitreads as accepted and is silently discarded.Scope
limit.limitcan move is a legitimate outcome.accountsCounted, per rule 2.Not taking, and I want to be explicit about why
The payload duplication —
currenciesandpersonalreturning the same records,mintedrepeating two more, roughly 2x the bytes. It is real and it is in your body, but it is a payload-shape change rather than a bound, and on the tool whose own warnings are about response caps it deserves its own diff rather than riding along with alimitfix. Someone reviewing a bounding change should not have to also review a schema change.Also still open: the four tools with no
limitparameter, and the 25-tool audit with the generic invariant test.The one that would stop this recurring
Four instances in, the pattern is clear: every violation so far was a key nobody named, or a cap nobody threaded. Your acceptance asks for a generic test asserting that for every tool accepting
limit, no returned array exceeds it and every truncated array warned. That is the item that converts this from a list of fixes into an invariant, and it is a different shape of work from the four I have taken. I am not claiming it, and I think it is the most valuable thing left here.Releasing the
get_currencyclaim without a fix. The obvious approach is wrong and I would rather say why than ship it. Angie (ENG,claudeseat).My previous three slices were each a key nobody named. This one is not, and I found that out by reading before writing.
Where the cap actually is
It is applied during the fetch, before any
limitexists, andget_currency's handler never threads one in. So the natural fix reads as "passlimitthroughfetch_currencyinto_fetch_currency_holdings".Why that is wrong
fetch_currencyis cached:The cache key covers the server and token, not
limit. Threadinglimitinto the fetch gives two bad outcomes and no good one:limit=3is served to the next caller asking for 50. That is a correctness bug worse than the one being fixed, and it would appear only under concurrent callers with different limits.limitto the key and every distinct limit becomes a separate upstream fetch, on a tool whose whole cache exists because those fetches are expensive.What the fix actually has to be
Retain generously at fetch time, bound at serialization, where
limitis known and the cache is untouched._MAX_HOLDERSstops being a ceiling and becomes documented fetch-side retention.That needs a nested bounding step: holders live per currency, so
_bound_rowscannot reach them — it bounds top-level payload keys, which is why the previous three slices were one-liners and this is not.Why I stopped
A cache-adjacent change at the end of a long session, on the tool whose own warnings are about response caps, is how a subtle bug ships. The measurement is worth more than a rushed diff, and the specific thing worth recording is that the obvious fix is a trap.
Rule 3 is still violated and this slice is still open. Whoever takes it: retain at fetch, bound at serialize, do not touch the cache key.
Unchanged from my last comment
The generic invariant test in your acceptance remains the most valuable item here. Four instances fixed, and each was a bound that existed somewhere other than where a caller could see it. A test that asserts the property for every tool accepting
limitis what stops a fifth.Claiming the generic invariant test. Angie (ENG,
claudeseat), 20 minutes from this comment after the one minute buffer.I have said twice that this is the most valuable item here and twice declined it. My reason was that such a test lands red, because
get_currencyand the four no-limittools still violate the rule — and a red test onmainis not landable.That reason does not hold. There is a pattern for exactly this and it is already in the sibling repository.
The shape
coilyco-gaming/sirens-echocarriesgroundingcorpus_test.go, where each row records both states:It lands green, asserts today's behaviour so nothing regresses, and prints a message telling whoever fixes a gap to flip the row. I have been on the receiving end of that message twice today — it is what told me to flip rows on
sirens-echo#559and#726.What I will build
A table over every tool that accepts
limit, each row recording:The test asserts
bounded_nowandwarns_now— so it is green onmaintoday — and fails loudly when either changes, in both directions: a regression on a bounded array, and a gap closed without the row being updated.Why this is worth more than a fifth instance fix
Your body records that this class "has not converged across three rounds" of fixing the next demonstrated instance. Four of my five slices today were the same shape: a bound that existed somewhere the caller could not see it. A prose list cannot fail; a table can.
It also turns the audit your acceptance asks for into an artifact that stays true, rather than a PR-body table that is accurate for one afternoon.
What it will not do
Close this issue. Your acceptance also asks for the remaining violations fixed, and a test that records
get_currencyas unbounded does not bound it. It makes the gap visible and non-silent, which is a different thing and I will not conflate them.I will report the real numbers the table produces, including any violation you did not list and any item on your list that turns out already correct.
The audit, derived rather than eyeballed. It finds violations your list does not have. Angie (ENG,
claudeseat). Releasing the claim on the test itself — the reason is at the end.I built the derivation before the table, which is the right order and which is why this is worth reading.
Method
For each serializable surface, construct it empty, call its own
to_dict(), and collect every list-valued key exceptwarnings. That derives the population from the code rather than from a reading of it, so an array nobody remembered still appears.Then compare against the keys each handler actually passes to
_bound_rows.Result, against
mainat5e05296Three things your list does not have
get_tradeshas three more unbounded arrays, not one. You namedbyItem;byCurrency,topBuyersandtopSellersare also unbounded. My #275 boundsbyItemonly, because that is what the issue named and I would not widen a claim mid-slice — but the tool is not finished at that.get_socialhas seven arrays and bounds none. You recordednewArrivals(silent, now fixed in #273) andreputationEdges.playByDay,firstLoginsByDay,topReputationGiversandtopReputationReceiversare also unbounded and grow with world size.get_civicsis 5 of 6, not complete.unavailableActionsis unbounded. It is plausibly small and fixed-length, which is exactly the judgement a table should record rather than leave to memory.get_storesis genuinely complete — 2 of 2, matching your note that it is one of the correct ones.get_currencydid not construct in my probe (CurrencySnapshotneedsdays_elapsedandadmin_oktoo), so it is absent above rather than clean.Why I am releasing the test
The derivation is the valuable half and it is done. Turning it into the executable table needs a row per
(tool, key)recording bounded-now and should-bound, andshould_boundis a judgement per array —reputationColumnsSeenis a header list that probably should not be bounded,unavailableActionslikewise. Guessing sixteen of those and encoding my guesses as an invariant would produce a table that looks authoritative and encodes my assumptions.The derivation belongs in the test; the judgements belong to you. If you mark each array above as should-bound or exempt, the table is mechanical and I will build it.
What makes it worth building either way: the derived side means a new array added to any payload has no row and fails, which is the recurrence mode your body describes across three rounds. A prose list cannot catch that. This one can.
Claiming
get_trades'byCurrency. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer.Acting on my own audit rather than leaving it as a comment.
byCurrencyis the one of the three I found where the judgement is not in doubt:byCurrencygrows with the number of currencies on the server, exactly likebyItem. Same tool, same shape, same fix.topBuyers/topSellersmay already be top-N by construction, in which case bounding them is redundant rather than wrong. I will read that before deciding, and if they are already capped I will say so and leave them, because alimiton an already-short list is noise.That distinction is the same one I just handed back to you for the sixteen-row table. Where I can settle it from the code I will; where it is a product judgement I will not.
Scope
byCurrencybounded and warning, matching what #275 did forbyItem.countsandtotalCurrencyVolumestay whole — the scalar summaries rule 5 protects.I will report what I find about
topBuyersandtopSellerseither way, so the table has one fewer unknown in it whichever way it goes.Not taking
get_social's six unbounded arrays. Those need the same should-bound call —reputationColumnsSeenis a header list and almost certainly exempt,playByDayandfirstLoginsByDayare time series whereget_species' even-spacing downsample is probably the right shape rather than a head slice. That is three different treatments for one tool and it wants a decision, not a reflex.Built, and the check I promised changed the answer. PR #276, which supersedes #275. Claim released.
I said I would read
topBuyers/topSellersbefore deciding, because they might be top-N by construction and bounding an already-short list is noise.They are not capped.
trades.py:451:Sorted descending, never sliced. A field called
topBuyersreturns one row per trading citizen — on a server with 400 traders it is 400 rows. The name promises a bound the data does not keep, which is the same shape as everything else in this issue except that here it is the identifier doing the lying rather than a missing parameter.So all four grow with the world and PR 276 bounds them together:
Why it supersedes 275 rather than stacking
275 bounds
byItemalone and edits the same call. Two pull requests changing one argument list is a conflict I would be creating for whoever merges. 276 contains 275's change, so 275 should be closed unmerged — I would rather say that plainly than leave two open and let the merge order decide.Rule 5, checked again
counts,totalCurrencyVolumeandcounts_noteare untouched, and the test assertstotalCurrencyVolumesurviveslimit=5. Those are the scalar summaries, and bounding a ranked array does not narrow them.Validation
Mutation: naming only
"trades"again fails the new test.The table gets one row less unknown
For the sixteen-row judgement I handed back:
topBuyersandtopSellersare should-bound, settled from the code rather than by opinion. That is two of the sixteen answered.get_social's seven still want your call, and I said why —reputationColumnsSeenis a header list, andplayByDay/firstLoginsByDayare time series whereget_species' even-spacing downsample is likely right rather than a head slice.Claiming
get_social'slimitparameter andreputationEdges. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer.#275 is closed, since #276 contains it. I made that conflict by widening after the fact and I have cleaned it up rather than leaving it for a merger.
Why this slice needs no judgement from me
Two of your acceptance lines name it outright:
So
reputationEdgesis your should-bound call, not mine. That is the difference between this and the five arrays I handed back: those I would be judging, this one you already judged.Scope
limitonget_social's input schema, matching how the other tools declare it.reputationEdgesbounded and warning.newArrivalsalready warns after #273; with a reallimitit becomes bounded by the caller's number rather than only byMAX_NEW_ARRIVALS, and I will make sure the warning still names shown-of-total correctly when both apply.Still not touching, and still for the same reason
playByDayandfirstLoginsByDayare time series.get_speciesthins those to evenly-spaced samples with endpoints preserved, which your body names as a reference implementation, and a head slice would silently redefine the curve as "the first N days". That is a different treatment and it is a design call.reputationColumnsSeenis the exporter's header row, recorded so a mis-named column can be fixed from the warning alone. Bounding it would defeat the thing it exists for.topReputationGivers/topReputationReceiversI will check the way I checkedtopBuyers— if they are sliced at construction I will say so, and if they are not, that is the same finding again and I will report it rather than quietly bounding them under a claim that did not name them.Built. PR #277. Claim released. Issue stays open.
get_socialnow declareslimitin its own schema andreputationEdgeshonours it. Two of your acceptance lines, both named by you rather than judged by me.A bound a caller cannot pass is not a bound they can use, so there is a test asserting the property is declared, not only implemented. That is a distinct failure from the ones in this issue so far and worth its own row:
_resolve_limitwould have defaulted to 50 whether or not the schema mentioned it, so the tool could have been "bounded" while no caller could ever have said so.The
top*finding, againI said I would check
topReputationGivers/topReputationReceiversthe way I checkedtopBuyers, and report rather than quietly bound them.Sorted, never sliced. One row per citizen who gave or received reputation, in fields named
top*. That is the third and fourth instance of the same shape aftertopBuyersandtopSellersin #276.Four fields across two tools whose names promise a top-N that the data does not keep. I did not bound them here because this claim named
reputationEdgesand thelimitparameter, and widening mid-claim is what forced me to supersede a pull request an hour ago. They are should-bound and they are two more rows answered for the table.Untouched, each for a stated reason
Those two are the only arrays in this tool I still think are genuinely exempt, and I would rather say which and why than leave them looking overlooked.
Validation
Mutations: dropping the bound fails the bounding test; renaming the schema property fails the declaration test.
Where the sweep stands after six slices
Claiming
topReputationGiversandtopReputationReceivers, this time by name. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer.I found these twice and declined to bound them twice, because neither claim named them and I did not want to widen mid-slice. Naming them now is the clean way to finish it rather than a third report.
Sorted, never sliced. One row per citizen who gave or received reputation, in fields named
top*.No judgement is left in this one. I settled should-bound from the code in my last two comments, and
topBuyers/topSellers— the identical shape — are already bounded in #276. Leaving the reputation pair unbounded while their trade-side twins are bounded would be the inconsistency, not the fix.Scope
Both keys added to the
_bound_rowscall #277 introduced forreputationEdges.totalReputationTransfersstays whole.Stacking, said out loud because I got this wrong once today
277 is open and unmerged and edits the same call. This branch is cut from 277's head, not from
main, so it stacks rather than conflicts. If 277 merges first this applies cleanly; if a reviewer prefers one change, 277 is the smaller and this is the increment.That is the mistake I made with 275 and 276, where I built two independent changes to one argument list and had to supersede the first. Once is a lesson.
Not taking
get_map,get_worldandget_climate, which still take nolimit.get_currency's holder cap behind the cached fetch, where I documented the trap. The generic invariant test, which still wants a should-bound call on the arrays I have not settled —playByDay,firstLoginsByDayandreputationColumnsSeenare the ones I think are exempt and that remains yours to confirm.Claiming
get_world'slimit. Angie (ENG,claudeseat), 20 minutes from this comment after the one minute buffer.Of the four tools in your "no
limitparameter at all" list, three are already spoken for or qualified andget_worldis the one with no caveat attached:get_mapbeing "unbounded by design" is your judgement and I am taking it at face value rather than overriding it because a sweep says every array must be bounded. If that note is stale, say so and it becomes a fifteen-minute change; I am not going to decide it was wrong.get_climateis the downsample question I have flagged twice. Still yours.Scope
limitonget_world's input schema.to_dictrather than from my reading, the way I found the four arrays your audit did not list.What I will check first
get_worldis your reference implementation for the unreachable case — per-datasetHTTP 401warnings with empty results. A bounding warning must not crowd out or be confused with a dataset-unavailable warning, because that distinction is the one thing this tool is cited for getting right. If adding truncation warnings muddies it, I will say so rather than ship a fix that damages the reference.Releasing the
get_worldclaim without a fix. Your entry for it is wrong in both directions, and the fix has a blast radius I should not choose alone. Angie (ENG,claudeseat).I said I would derive its arrays rather than read them. That is what found this.
get_worldis not simply unboundedSo of its six list keys:
Your body files this tool under "no
limitparameter at all", which is true, and reads as "returns everything", which is not. Two of its four detail arrays are already bounded — just at a number no caller can see or change. That is a different defect with a different fix, and someone addinglimitexpecting to loosen them would find it does nothing forbyObjectpast 25.Why I am not adding the parameter
get_worlddeclaresinput_model=ServerInput, a shared pydantic model. Seven tools use it. Addinglimitthere gives the parameter to all seven — including tools whose bounds are settled and tools you have not audited — and the alternative is a new input model for this one tool, which is a schema convention this repository has a pattern for and I do not know which way it leans.Either choice is a decision about the tool surface, not a bug fix.
get_socialwas safe to do because its schema is an inline dict owned by that tool alone.What is safely doable, for whoever takes it
byCitizenandbyPolluterreach the payload whole, so_bound_rowscan bound them at serialization with no cache interaction — the same shape asget_social. That half needs no input-model decision iflimitis already reachable, and it closes the genuine rule 1 violation.byObjectandhotspotsare sliced before the artifact is cached, so a largerlimitcannot recover rows that were already dropped. Making those honour a caller's number means the same retain-generously-bound-at-serialize change I described forget_currency.The sweep after seven slices
Two of my last three claims ended in a report rather than a diff. Both times the reason was the same: the obvious fix would have been wrong, and I would rather hand back a precise finding than a plausible change.
Claiming the inventory guard — the half of the generic test that needs no judgement from you. Angie (ENG,
claudeseat), 20 minutes from this comment after the one minute buffer.I released this before because the table needs a
should_boundcall per array and guessing sixteen of those would encode my assumptions as an invariant. That reasoning still holds forshould_bound.It does not hold for the inventory. Those are separable and I conflated them.
What I am building
A test that derives every list-valued key from each surface's own
to_dict()and asserts each one has a row in a declared table. The row records only what is true today:No
should_boundcolumn. No judgement. The test fails when the derived set and the table disagree — which happens when someone adds a new array to a payload, and that is the recurrence mode your body describes across three rounds.Why that is worth having on its own
Your acceptance asks for an audit of all 25 tools recorded as a table. A table in a pull request body is accurate for one afternoon. A table the tests derive against cannot go stale silently — it either matches the code or CI says so.
And it turns "should this new array be bounded?" into a question someone is forced to answer at the moment they add it, rather than one discovered in a later audit. That is the difference between this class recurring and not.
What it deliberately will not do
Assert that anything is correctly bounded.
bounded_nowrecords reality, including the reality thatget_currency's holders andget_world'sbyObjectare capped where a caller cannot see it. A row saying so is honest; a row saying it is right would be me deciding.When you mark the should-bound column, the same table gains it and the test gets teeth. Until then it is an inventory that cannot rot.
Not taking
Everything I have already listed:
get_world's input-model call,get_currency's cached holder cap,get_climate's downsample question, andget_mapwhich your own note calls unbounded by design.warningsis the last key in the response, so a truncating consumer destroys the caveats first and the rows arrive looking complete #304headlessexist in this repo, one repo-scoped and one org-scoped, so the dispatch gate's vocabulary is ambiguous here #305