Repository navigation
Field limits: max_length, min_length, min and max (kinds-limits, D90) - #33
Merged
Merged
Conversation
…d the install gate (D90) A FieldDef may carry limits: max_length and min_length (Unicode code points, not bytes) on text and longtext, min and max (inclusive, exact) on number, money and decimal. A text field with no max_length has the default of its kind (2,048 and 100,000) and a declared one may not exceed the cap (10,000 and 1,000,000); the four numbers are named constants in limits.rs and are proposals for the product owner. The D41 gate refuses limits that cannot hold (0, above the cap, min above max, wrong kind, a bound that does not fit the decimal's precision and scale). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…_violated (D90) create, update, each row of bulk_create (and so imports and the CLI), the effects of actions, the pre-flight of a governed update and the public submit all hold a written value to the limits of its field. The refusal is a 400 with the new code limit_violated and names the model, the field, the limit and what was written. A value equal to the stored one is not re-checked on update, so lowering a limit locks no existing record. /api/meta (FieldInfo) carries max_length, min_length, min and max. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ts to them (D90) FieldM takes max_length, min_length, min and max (decimal bounds are strings, a TOML float is refused) and reads them into the field, so obop modules check, the install gate and the upgrade diff see them. A changed limit is the listed, never breaking, change kind field_limits, and a set_default value of a migration step must satisfy the limits of its field. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
maxLength, minLength, minimum and maximum on the input schemas of the write commands (decimals as x-obop-minimum and x-obop-maximum, because a decimal is a string), none on record schemas and none on personal-data fields in MCP, where an agent sees a mask token. A sentence is added to the field description only for a limit the author declared. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… follow (D90) The field-limit keys widen what the manifest parser accepts, so by the version rule of D88 Decision 14 this is the first patch number of the 0.2 line. A package that declares a limit writes requires_core = "^0.2.1"; the 23 bundled packages keep ^0.2. Changes every pin of the literal 0.2.0 that D88 Decision 14 lists: the core-version tests, the committed openapi.json (regenerated, with the maxLength keywords), the website snapshots, the e2e assertions and the examples in DEPLOY.md and spec/cli.md. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…(D90) REST create, update, bulk_create and CSV import over a real router; the default length on every built-in model; /api/meta, the OpenAPI document and the MCP tool schemas; installing a package whose limit cannot hold; and obop modules check through the real binary on good and bad packages. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…the server's refusal (D90)
A code-point counter ("8 of 10 characters") from four fifths of a limit,
red above it; a text or number outside a limit is refused before it is
sent, naming the field and the limit; number inputs take min and max; a
value the person did not change is not held to a limit, as on the server;
the server's own limit_violated sentence is shown on the field when the
browser did not know the limit. The native maxlength attribute is not set:
it counts UTF-16 units and silently cuts a paste. The six machine-drafted
catalogues carry the new strings.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Five Playwright tests over the real built app: the counter near a limit and the refusal before sending, 2,001 characters and 2,000 emoji against a limit of 2,000, a number and an exact decimal outside their range, the server's refusal when the browser did not know the limit, and an import that names the row and the limit. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…NS.md, list K43 to K46 D90 is the decision record for ask 11.3 of the platform team: the keys, how a length counts, the proposed default length and cap for the product owner, where limits are checked, the refusal, the rule for a value stored above a lowered limit, the gate, the surfaces, the version rule (0.2.1) and what is not delivered. MODULES.md gets the keys, the gate messages and the kinds table; CONVENTIONS.md the new spellings; ISSUES.md K43 to K46; the decision range becomes D1 to D90 where it is stated. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…d reword the default rationale (D90 review F6, F7, F20) Breach::requirement and message_without_value say what a field takes and nothing about the value written. The min_length advice names the cap and reports a min_length above it directly. The comments on the two defaults now give the real rationale (round numbers with a margin over measured seed data) and mark the URL, GitHub and mail figures as unofficial analogies. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ent or from an effect (D90 review F0, F1, F7, F8, F10) An effect's wrapper and the nested set_state wrapper keep the code limit_violated first (errors::wrap), so the wrapped answer classifies as that code with its hint on every surface. A refusal of a limit no longer says how long or how large the value was when the caller is an agent (its mask tokens are restored before the handler) or when an action effect wrote it (it may be a copy of a field the caller cannot read). Tests: effects (create, adjust, nested set_state), agent token, $self.salary, the governed pre-flight both ways, a decimal stored above a lowered bound in any spelling, and the public submit of a number and a decimal. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…they move, a float set_default is refused (D90 review F2, F3, F19) limits_text now shows the effective maximum of a text or longtext field (a dropped max_length is the default of its kind) and the plan says whether the limits tighten, loosen or both, comparing what is held and not how it is spelled. A float set_default into a number or money field is refused: a write refuses it too, and it skipped the bounds. The doc comment of min and max now says a decimal bound is a string or a TOML integer with at most scale fractional digits. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…f (D90 review F9) No change of behaviour: the delivery is answered with counts when an item was accepted, refused when all were rejected, the run records partial or failed, and the event id stays taken. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…lid while over (D90 review F14) A status region (always present for a field with a limit, empty far from it) says that the text is close to the limit and that it is over, in words that do not change within a band, so nothing is announced per key. The control carries aria-invalid while the text is over and about to be sent; a value left as stored is savable (Decision 6) and is not marked. The six catalogues carry the two new strings (machine-drafted). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…, F5, F6, F7, F8, F9, F10, F12, F14, F15, F17, F18, F19, F20, F21, F22, F23) Record in DECISIONS.md what the fix round built and what it learned: - Reversibility: lowering a number also makes check_installed_module and Manifest::field_def refuse an installed package at every start (F4). - Decision 1 and 7: a decimal bound is a string or a TOML integer with at most the scale; the registry's integer-on-decimal arm stays (F3, F23). - Decision 3: the reasons for the four numbers, without overstated sources (F6). Decision 4: inbound connector records are checked, effect maps only when the action runs (F5, F9). Decision 5: the code survives an effect, the count is withheld from agents and effects, bulk rows are named without the code (F0, F7, F8). Decision 6 and 8: decimals by value, the plan shows the effective limits, the counter is announced (F2, F10, F14). Decision 9: OR-35 wording of D89, a fourth visible difference (F17). - Rollback points to D89 Decision 15 (F18); the register row and the paragraph on counts say what is and is not registered (F21). - Lead decisions of the review are marked as such. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…F3, F7, F11, F16, F23) A decimal bound is a string or a whole number with at most the scale (not "exactly"), a bulk row is named without the code, an agent and an effect get no count, the limits of an effect's target are checked when the action runs, MODULES.md line 21 no longer names a version, and the second example of "Which build is running" reads 0.2.1. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…code map (D90 review F4, F5, F9, F13, F21, F22) K43 gets the start-up hazard of a lowered cap, K44 the inbound connector records that are checked, the schemas that list output-only fields as input, the effect maps and the hidden field, K45 the module builder. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…output-only fields (D90 review F13) The MCP tool connection__create offers result (maxLength 100000) and last_error (2048) as inputs, as does openapi.json; the module.install and user_account.add figures are the reviewers' and were not reproduced here. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…(D90 closure check, F19) The float refusal and the limits check of a set_default value now run for an upload and an upgrade only. parse_installed threads its flag into validate_migrations and default_matches, so a package installed on the previous core with a float default in a number or money field still loads, upgrades and rolls back instead of landing in the failed list. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…nt or from an effect (D90 closure check) A bounded decimal target, a decimal the HR group alone reads and a grant action that copies it join the rig; the agent variant of the governed state move sends a masked token as the title. Both mutations of the withholding (kinds.rs and crud.rs for the decimal, guards.rs for the pre-flight) now fail a named test. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…until it is changed The unchanged prop at RecordPanel.tsx:574 and views/InlineEdit.tsx:50 had no test: setting either to false left every spec green. One Playwright case now opens a stored 8-character value under a limit lowered to 5 (the counter says 8 of 5), checks the control is not aria-invalid in the panel and in a list cell, that one more character makes it invalid, and that going back clears it. Both mutations fail it. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…and what the schemas of the virtual commands state (closure check of PR 33) The public submit and the connector push are not refused with a 400 and limit_violated: the public submit drops the code and the contact form and shop checkout show a generic error, a connector item is rejected by itself. MODULES.md, the D90 register row, Decision 5 and K45 now say so, and a bulk call answers a 400 only in all_or_nothing mode. A decimal bound is a string or a whole number (MODULES.md types table). The text no longer says the refusal 'does not echo the value'. K44 (b) and Decision 8 now state what a real run showed: only the MCP schemas of the virtual commands (module__install 40 of 45 inputs, user_account__add 5 of 12) carry a limit nothing enforces; OpenAPI lists limits only for stored models. Decision 1, Decision 4 (ingress) and K44 (d) are labelled lead decisions, and Decision 7 and the Consequences say that installed manifests are not re-refused. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
… closure check of PR 33) K44 (b) and Decision 8: only connection.last_error is an output-only limit that is enforced (a 2,049-character last_error is refused on create); connection.result is dropped on create and refused on update by the connectors guard, so its 100,000 in openapi.json is enforced by nothing. Decision 8 carries its lead-decision marker. K44 (a) and Decision 4: a connection is created and updated through the generic create and update, so its limits apply; only the runtime's partial updates, the consents and the sync runs go straight to the store. Decision 9: a float set_default into a number or money field, which 0.2.0 accepted, is now a fifth visible difference (installed packages are exempt). Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
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.
What
Field limits (ask 11.3 of the platform team, DAT-060 / MTG-N4), recorded as decision D90 (PROPOSED):
max_lengthandmin_lengthontextandlongtext, counted in Unicode code points, not bytes.minandmax(inclusive, exact) onnumber,moneyanddecimal(a decimal bound is a string or a whole number with at most the field's scale; a TOML float is refused).limit_violated(a new error code, origin D90) oncreateandupdate(REST, MCP and the CLI) and on the effects of actions. The message names the model, the field and the limit. To a person who typed the value it also says what was written, for examplelim_note.headline must be at most 2000 characters; this has 2001; to an agent, and from an action effect, it says only what the field takes (... must be at most 2000 characters), because the count would reveal the length of a personal value behind a mask token or of a field the caller cannot read. The code is kept when an action effect meets a limit. The other writers refuse in their own way, and not with that code: a bulk row (and soobop importand the Import dialog) is named without it (row N: ...); abulk_createcall in the defaultall_or_nothingmode answers a plain 400 that names the first five rows, whilepartialmode anddry_runanswer a success that lists every refused row; a record that a connector pushes in is rejected as that one item (the others of the delivery are kept); a public submit of a[model.public]form is rejected as a whole with the text of a direct write and no code, and the two pages that call it (the contact form and the shop checkout) show the visitor only "Something went wrong. Please try again."./api/meta, the OpenAPI document (input schemas only), the MCP tool schemas,obop modules checkand the back office form (a code-point counter that a screen reader is told about once per band, bounds, the server's refusal on the field).FOR THE PRODUCT OWNER BEFORE THE MERGE: the proposed default and cap
These four numbers are proposals, not rulings. They are named constants in one place,
crates/kernel/obop-registry/src/limits.rs; nothing else repeats them.max_lengtha field may declare)textlongtextWhy (the first wording of this paragraph claimed more of its sources than they say; this one is corrected): the numbers are round numbers with a margin over what was measured. The longest
textvalue in the seed data is 140 characters and the longestlongtextvalue 2,040, so 2,048 and 100,000 leave about fifteen and about fifty times. 2,048 is also the conservative ceiling for a URL, the longest ordinary one-line value: sitemaps.org asks for fewer than 2,048 characters and old Internet Explorer stopped near it (modern browsers accept far more, so it is a conservative ceiling and not a limit of today's browsers), and it is this project's own limit for an OAuth redirect URI (obop-oauthredirect.rs). 100,000 is about fifty printed pages. GitHub's 65,536 for an issue body and about 102,400 bytes for a clipped HTML mail are unofficial third-party figures that were not checked against current documentation; they are analogies, not sources. The caps are about five and ten times the defaults. The HTTP request body limit (2 MiB) still applies first.Cheapest to change before the first release that ships the keys. Lowering a number later does more than make some writes fail:
check_installed_moduleandManifest::field_defrun the install gate for every installed package at every start, so an installed package that declares amax_lengthabove a lowered cap (or amin_lengthabove a lowered default with nomax_length) is refused at start, lands in thefailedlist and registers none of its models (the hazard D89 Decision 15 describes for a rollback; reproduced in review). Raising a number breaks nothing. D90 Reversibility and K43 say so; the field gate is not relaxed for an installed package (only the checks of aset_defaultmigration step are, see the second round below).The default is a behaviour change for every existing package and built-in module: a write of more than 2,048 characters into a
textfield, or more than 100,000 into alongtextfield, that used to succeed is refused from this release on. That includes an action effect that copies a longer value into a shorter field (the install gate checks the kind of an effect's values, not their limits, so it is refused when the action runs), and a record that a connector pushes in: that item is rejected, the others of the delivery are kept (202 with counts; 422 when all were rejected), and a floatset_defaultmigration step into a number or money field (value = 5.0) that 0.2.0 accepted is refused byobop modules check, an upload, a preview and an upgrade to such a package (an installed package is exempt, D90 Decision 7). Stored values are never re-checked (below), so no existing record is locked. These are the visible differences that OR-35 h does not name (that part covers "the text or the status of a refusal", not a refusal where there was none); D90 Decision 9 lists them as visible differences and as a point for the owner, and does not claim the amendment covers them.Longest stored value (measured, seed data only). A fresh instance with the demo data, the demo shop and blog and all 23 bundled packages installed (94 readable models, 477 text values): longest
longtextvalue 2,040 characters (connector_provider.config_fields, 100,000 allowed); longesttextvalue 140 characters (connector_provider.legal_name, 2,048 allowed). That says nothing about a real instance; there is no tool for that yet (K46), so the owner has to measure a real database before the merge.Not registered as questions. D90 raises two owner points (the four numbers; whether a patch release may add a refusal where there was none). Neither is an entry of the question register in DECISIONS.md section 8 and the question counts do not include them; K43 tracks both. (Reviewed: the first wording said D90 "adds no open question".)
The contract (D90 in docs/DECISIONS.md has the full record)
str::chars.å,ä,öand an emoji count 1 each; 2,000 Swedish letters and 2,000 emoji pass a limit of 2,000, 2,001 are refused. A combining accent or a family emoji counts as many code points as it has. Nothing is trimmed or normalised. The browser counts the same way with a surrogate-pair aware loop (codePointsinweb/apps/backoffice/src/limits.ts).requiredandmin_length.requiredis unchanged (present and non-null;""still passes it).min_length = 1refuses""but accepts" ", because a length is what was sent and nothing is trimmed. So for the platform's "a post of spaces is stored today",min_length = 1gives them the empty-string half and not the whitespace half; a blank-text rule is not delivered (K45).min_lengthdoes not make a field required either: a missing or null value is not checked.update, a value equal to the stored one is not checked against the limits again (a decimal is equal by value: a stored100.50and a resent100.5are the same), so a record holding 2,500 characters in a field that now allows 2,000 can still have its other fields edited and a form that sends the whole record back does not fail on a field nobody touched. The check before a governed state move compares with the stored record the same way. A different value is checked even if shorter than before. Every other check runs as always. The back office does the same. Existing rows are never scanned.max_length0,min_length0,max_lengthabove the cap,min_lengthabovemax_length(or above the default when there is nomax_length; the refusal names the cap and where more room is),max_length/min_lengthon any kind but text and longtext,min/maxon any kind but number, money and decimal, a bound of the wrong shape (for a decimal: a float, more fractional digits than the scale, beyond the precision),minabovemax, a limit on a computed or numbered field, an unparseable bound. Aset_defaultof a number or money field is a TOML integer; a float, and a value outside the limits of its field, is refused when a package is installed, uploaded, previewed or upgraded to, and not when an installed package is read again (it still loads at start, upgrades and rolls back as the previous core read it). All 23 bundled packages and the built-in modules install unchanged (obop modules check packages/*.toml: 23 ok; none declares a limit)./api/meta:max_lengthon every written text field (default included),min_length,min,max. OpenAPI:maxLength,minLength,minimum,maximumon input schemas only (decimals asx-obop-minimum/x-obop-maximum), none on record schemas. MCP: the same keywords, none for personal-data fields (an agent sees a mask token). A sentence is added to the field description only for a limit the author declared. The upgrade plan shows the limits a field is held to, the default included, and which way they move (limits tighten from max_length 5000 to max_length 2048 (default); values already stored are not re-checked). The generated create, update and bulk_create tool descriptions (OR-35 k) and the OpenAPI operation summaries are not changed.role="status"region that says "Close to the limit of 10 characters" / "Over the limit of 10 characters" once when the band changes; the control isaria-invalidwhile a changed value is over; client-side refusal before sending; number inputs getmin/max; the server's own sentence is shown on the field when the browser did not know the limit. Deliberate deviation: the native HTMLmaxlengthattribute is not set (it counts UTF-16 units and silently cuts a paste), so the control can hold more than the limit and the form says so. Storybook: none exists for the back office.First fix round: the review of 5034695 (24 confirmed findings, 6 refuted), head f755757
Five code commits and four document commits on top of 5034695; nothing rebased, nothing force-pushed. Where the lead decided and the owner did not, D90 says "lead decision".
errors::wrapkeepslimit_violatedfirst through the effect wrapper and the nestedset_statewrapper;create,adjustand nestedset_stateare classifiedlimit_violatedwith their hint. Bulk rows stay without the code, and the wording says soguards.rsare caughtnoneActor.agent,check_effect_fields); a person and a public visitor keep itaria-invalidwhile a changed value is overset_defaultinto number/moneymin_lengthadviceSecond fix round: the closure check of f755757 (34 of 36 closed, 2 partial, 14 new items, none high), head 174fc07
Three code or test commits and one document commit on top of f755757 (13810a5, 2c4bb9e, c86e7e7 and 174fc07); nothing rebased, nothing force-pushed. Each claim was checked in the code before it was acted on.
set_defaultinto a number or money field stopped an already installed package from loading (manifest.rs:parse_installedreachedvalidate_migrationsanddefault_matches; three findings said it)default_matchesandvalidate_migrationstakeinstalled: a float is refused, and aset_defaultvalue is held to the limits of its field, on install, upload, preview and upgrade, and not when the stored text of an installed package is read (start, the stored side of an upgrade, a rollback). One thing beyond what was asked: the same flag also skips the limits check of theset_defaultvalue for an installed manifest, not only the float, because the previous core had no limits and the same start-up hazard applies; D90 Decision 7 says so in one sentence and the field gate (limits::problems) is not relaxedthis is(a_decimal_breach_states_no_value_to_an_agent_or_from_an_effect); the agent variant of the state-move pre-flight test, with an exact stringunchangedprop atRecordPanel.tsx:574andInlineEdit.tsx:50was untestedaria-invalidin the panel or in a list cell, one more character makes it invalid, going back clears itlimit_violated"public.rs:951,errors::plain) and both callers showerr.generic(the contact form,obop-sitecontent.rs:755-762, and the shop checkout,checkout.rs:1261-1263); a connector item is rejected per item (ingress.rs:159). MODULES.md (:91, :304), the D90 register row, Decision 5 and K45 now say it in their own clauses, and a bulk call answers a 400 only inall_or_nothingmodescalefractional digitsplatform.adminagent token: MCP only, for the kernel's virtual commands (module__installlists 45 inputs and 40 carry amaxLength;user_account__add12 and 5), and the virtual handlers run no check. OpenAPI lists limits only for stored models (/api/module/installhas a generic body). Of the two connection fields it lists, onlylast_error(2,048) is enforced (a 2,049-character one is refused on create with 400limit_violated);connection.result(100,000, on/api/connection/createand/update) is another output-only field whose stated limit nothing enforces, because the connectors guard drops it on create and refuses it on update (third round, below). K44 (b), Decision 8 and this body are reworded;last_erroris no longer an example of an unenforced limit and "not reproduced" is gone (the reviewers saw it; the dev-agent of the first round lackedplatform.admin)Third fix round: the second closure check of 174fc07 (all code and tests closed; four low document items), head d85f832
One document commit (d85f832) on top of 174fc07; no code changed. Each item was checked in the code and, where it is about behaviour, in a real run (built binary, scratch SQLite database,
--dev-auth).connection.resultis not enforcedlast_errorof 2,049 characters answers 400limit_violatedonPOST /api/connection/create(2,048 is accepted); aresultof 100,001 characters is accepted on create and not stored, becausenormaliseincrates/modules/connectors/src/guards.rsremoves it on create (:384) and refuses it on update (:377). K44 (b) and Decision 8 citelast_erroras the enforced example and nameresult(100,000 inopenapi.json) as an output-only field whose stated limit nothing enforces; row 6 above says the sameset_defaultwas a visible difference nobody listedcreateandupdate(its limits apply). Only the connectors runtime's partial updates go straight to the store (last_sync_atandlast_error, the latter cut to 200 characters,runtime.rs:394-402;state,runtime.rs:428andhandlers.rs:248;oauth_tokens,runtime.rs:1167; credentials at a revocation,runtime.rs:1296), and the consents and the sync runs. K44 (a) and Decision 4 say exactly that. A search of D90, MODULES.md, ISSUES.md K43 to K46, CLAUDE.md and this body found no other sentence that calls connections unchecked orconnection.resultenforcedGates for this round (documents only):
cargo fmt --checkclean; the openapi drift testthe_committed_openapi_document_is_currentpasses andvp run generate:typesleaves no diff;vp checkandvp buildpass forweb/apps/site(it assembles MODULES.md),deploy/build-site.shassembles the site and Playwrightsite.spec.ts(28 tests, among them the manifest reference rendered from MODULES.md) passes. The full cargo,vpand Playwright runs below are those of 174fc07; the code is the same on the new head.Visible differences (for the release notes)
2a. A public form that breaks a limit is rejected as a whole, and the contact form and the shop checkout show the visitor only "Something went wrong. Please try again.", not the field or the limit (K45). Only a limit the form's own checks do not already catch gets there (the contact form checks the name at 120 and the message at 5,000 characters itself).
/api/metagainsmax_lengthon every written text field; OpenAPI input schemas of stored models and MCP tool schemas gain the keywords (the MCP schemas of the kernel's virtual commands state a limit on inputs that nothing enforces:module__install40 of 45,user_account__add5 of 12; OpenAPI does not, K44 (b)); a declared limit adds a sentence to the field description (OR-35 j).limit_violated(400) and new upgrade-diff change kindfield_limits(never breaking). These are new values of existing fields; no part of OR-35 names them, which is the record's reading (the wording of D89 Decision 15), and the owner may rule otherwise.max_lengthhas the default of its kind.set_defaultmigration step that puts a float into anumberormoneyfield (value = 5.0) was accepted on 0.2.0 and is refused byobop modules check, upload, preview and an upgrade to a package that carries it; a package that is already installed is exempt and loads, upgrades and rolls back as before (D90 Decision 9 (e), Decision 7).Version rule (D88 Decision 14, OR-35 h/j/k)
CORE_VERSIONis now 0.2.1 (the workspace version). This is the first commit that widens what the manifest parser accepts, because D88 and D89 are not built. A package that declares a limit writesrequires_core = "^0.2.1"; the 23 bundled packages keep^0.2. Testcore_version_requirement_kinds_limits(number written in the test; 0.2.1 andCORE_VERSIONsatisfy it, 0.2.0 does not).v0.2.1-r1by the form of D88 Decision 14; tags are made by hand and the lead decides.LIMITS_CORE_PATCHincrates/kernel/obop-pkg/src/limits_tests.rs, the^0.2.1examples and the pins of the bump commit change with it.0.2.0) to the first of its four commits that lands. This is not one of the four, so that edit is not made (D90 Decision 9 says so). Two leftovers of the bump found in review are fixed: MODULES.md line 21 no longer names a number, and the second example in DEPLOY.md "Which build is running" reads 0.2.1. On 0.2.0 a package withmax_lengthfails at parse time whateverrequires_coresays (same hazard as D88/D89).Tests
New in total: 67 Rust tests (registry 15, commands 27 including the public submit, pkg 11, mcp 3, binary over the real router 8, CLI through the real binary 2, connectors module 1; 3 of them are new in the second round), 24 back office component tests (
vp test), 6 Playwright tests (the first also asserts the status text andaria-invalid; the sixth is new in the second round). The three stores are covered: the commands tests run on memory, SQLite and PostgreSQL (OBOP_TEST_POSTGRES_URL).Run on head 174fc07 (
cargo,vp, Playwright; real output in the hand-back):cargo fmt --check: clean.cargo clippy --workspace --all-targets -- -D warnings: clean.cargo test --workspace --no-fail-fast: 69 suites, 2,148 passed, 0 failed, 6 ignored, both without and withOBOP_TEST_POSTGRES_URL(a scratch databaseobop_limits_fix3on the shared container, dropped afterwards; the database namedpostgreswas not used). That includesthe_committed_openapi_document_is_current.vp checkpasses for the 7 packages and for the e2e package (with its typecheck);vp test: ui 167, client 128 (10 integration tests skipped without a server), trip-core 26, backoffice 304, all passed;vp buildpasses for backoffice (alsodist-admin), platform, site, client and trip-core;vp run i18n:checkinweb/packages/ui: catalogues ok.--dev-auth, a test packagerr), see D90 Decision 5:NAME_4for a 37-character name; writing the token into a field ofmax_length = 10over REST and over MCP answersrr_note.headline must be at most 10 characters, 400limit_violatedwith its hint, no count (the review hadthis has 39). The same text from an agent that typed 11 characters. A person (dev-sales) writing 11 characters is told...; this has 11; anumberoutside itsmaxlikewise:must be at most 100; this is 101to the person andmust be at most 100to the agent.$self.salary(73,250, a field dev-sales cannot read, grouphr.manager) into a number field withmax = 100: dev-sales and dev-admin both get 400limit_violatedwith its hint on the wire (REST and MCP),rr_src.finish: effect 1 (create) failed, nothing was changed: rr_target.cap must be at most 100, without 73250; the state did not move and no target row exists (the review had the value in the sentence and nocode).bulk_createwith row 2 over the limit: a plain 4001 of 2 row(s) are invalid, so nothing was created. row 2: ...(with the count for a person, without for the agent), nolimit_violatedcode. Anupdatethat changes a value to a too-long one:... this has 16to a person.Mutation checks (each guard broken in turn; the tests that should notice were run; all restored)
First round, 19, all caught: count bytes instead of code points;
maxexclusive;minexclusive; no default length; gate does not refuse above the cap; decimalminexclusive; decimalmaxexclusive; gate not run bycheck_basics; update re-checks an unchanged value; update never checks limits; achangeeffect not checked; acreateeffect not checked;bulk_createrows not checked; public submit not checked;limit_violatednot a 400; MCP keywords for personal data; a migrationset_defaultnot held to limits; the browser counts UTF-16 units; the browser re-checks an unchanged text.First fix round, 21 plus the end to end ones, all caught (the test that failed is in the commit messages and the hand-back): M1 the wrapper keeps no code; M2 the effect wrapper site is plain; M3 the nested
set_statewrapper is plain; M4 an agent is told the count; M5 an effect states the value; M6 the pre-flight is given its own values as the stored ones; M7 the pre-flight has no stored record; M8 an unchanged decimal is not skipped; M9 the public decimal branch removed; M10 the public number branch removed; M11 the cap advice removed; M12 the refusal without a value says the value; M13 the plan ignores the default; M14 the plan never says which way; M15 a float default accepted; M16 the plan compares the spelling; M17 connector items unchecked; M18aria-invalideven when unchanged; M19 no status role; M20 never invalid while over; M21 the count announced at every key, and two end to end: E1aria-invalidnever set while over failslimits.spec.ts:102, E2 no announcement near the limit failslimits.spec.ts:92.Second round, 9 more, all caught: N1 the float arm for an installed manifest removed (fails
an_installed_manifest_is_read_as_the_previous_core_read_itanda_float_set_default_is_refused_on_install_and_upgrade_but_not_in_an_installed_package); N2limits_holdalso run for an installed manifest (fails the first); N3 a float always allowed, so an install refuses nothing (fails both); N4 theinstalledflag not passed tovalidate_migrations(fails both); N5 a decimal breach always states the value,kinds.rs:170(failsa_decimal_breach_states_no_value_to_an_agent_or_from_an_effect); N6 the pre-flight and every direct write state the value to an agent,crud.rs:185(failsa_state_move_with_an_over_limit_field_is_refused_before_the_action_runs); N7 an effect states the value,crud.rs:206(fails the decimal test); and, in the Playwright spec, N8unchanged={false}inRecordPanel.tsx:574(fails at the panel assertion,limits.spec.ts:252) and N9 the same inInlineEdit.tsx:50(fails at the cell assertion,limits.spec.ts:264).Not done / not verified
connection.resultin the OpenAPI document and/api/metastates one on the output-only text fields of the virtual models; the connectors runtime's partial updates of a connection, its consents and its sync runs go straight to the store; a caller learns the limit of a field they cannot read), K45 (no date bounds, nopattern, no blank-text rule, no limits in the Customize module builder; server-rendered forms do not carrymaxlength; a refused public submit shows a generic error), K46 (no scan or report of stored values above a limit).limit_violatedthrough an effect but no other table code; withholding the count from agents and effects (a public visitor and a person keep it); no install-time check of effect maps; the schemas of the virtual commands left as they are (Decision 8 now says so in place); the ingress left as it is; the disclosure of a hidden field's limit accepted; a whole-number decimal bound accepted; installed manifests not re-refused for aset_defaultstep (second round); the registry's integer-on-decimal arm kept (it cannot be reached from a package, but an in-code caller ofFieldDef::with_limitsreaches it and a test covers it).rustpass (7m56s),webpass (42s),e2epass (6m23s),lockfilespass,allpass (run 40934424213). On 174fc07, the head of the previous round:rustpass (8m16s),webpass (46s),e2epass (5m02s),lockfilespass,allpass.🤖 Generated with Claude Code