Repository navigation
spec: maxLength is authorable on every field type and validated as no more than a number — maxLength: 0 and maxLength: 12.5 parse cleanly #11566
Description
Activity
分诊(Routine 席,小时轮):入决策箱,
needs-user-decision+domain:spec,type Bug(校验缺口本体是缺陷,但两个修法都收窄接受集 = 契约变化,人工地板)。下游注记:#11431(engine,pm:blocked)的 Blocked-by 指向本卡,裁决落地即解锁它。标准四棱块:os-decision-facets
① 实际业务需求:
maxLength: 0已实测把 schema-drift 推到「计划一个没有服务器接受的 varchar(0) DDL、且标记为 destructive」——这是真实的错误放大链,不是理论洞。写侧校验器的十类型清单就是今天真实在读这个键的消费者。② 项目长远合理性:形状收紧(
z.number().int().min(1))与同文件precision的既有 house pattern 完全同构,是把已声明意图兑现;适用面收窄(哪些类型允许 maxLength)触 ADR-0078 declared=enforced,且三份清单(field.form / object.form / record-validator)互不一致——收敛到校验器的十类型清单是唯一有实测读者背书的答案。③ 防 AI 犯错:这是四轴里最强的一条:
maxLength现在「每个类型都可声明、什么形状都能过」,AI 作者在 boolean/lookup 上写 maxLength 得不到任何响声。publish 时响亮拒绝优于驱动侧逐个防御(#11431 已被迫在消费端防了一次)。④ 创业阶段不扩散需求:两个修法都是收窄不是扩张,成本一次性;不裁的成本是每个驱动/工具各自防御一遍。
推荐:形状收紧(int ≥1)+ 适用面收敛到 record-validator 的十类型清单,一次裁掉两问;实施走条款②契约复审档位,并同笔对齐 field.form/object.form 两份表单清单。 本分析看不见的:存量已发布 app 里是否有踩线声明(如小数/0/挂在非字符串类型上)——若维护者知道存在,裁决需附迁移/宽限条款。
Generated by Claude Code
Maintainer ruling recorded — tighten both halves
Provenance: maintainer, 2026-08-24, live PM chat, batch acceptance, verbatim: 「四维分析一致的,接手你的建议。」 The four-facet block above is aligned, so the recommendation is adopted.
Ruled: shape tightens to the house pattern (
z.number().int().min(1), per theprecisionprecedent one field below), and applicability converges to the write-time validator's ten-type list (text | textarea | email | url | phone | password | markdown | html | richtext | code) —maxLengthrefused on types that store no bounded string. The two authoring-form lists (field.form.ts,object.form.ts) are aligned to the same ten in the same stroke. Clause-②: yes (narrows the accept set) — contract-review tier applies at dispatch.State:
needs-user-decision→pm:queue(spec lane). Downstream: #11431'sBlocked-by: #11566unlocks when this closes; the engine seat should re-verify that card's surface on the post-landing ref per the unlock discipline.
Generated by Claude Code
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsClaim: PM loop round R5 wave 1
Session:session_01K93P8PbH7kVjsAXkqzH1zY
Branch:claude/issue-11566-maxlength-shape-applicability
Worktree:objectstack-issue-11566
Domain:domain:spec
File surface:packages/spec/src/data/field.zod.ts(base-schemamaxLengthat :856 + thelength/sizealias map at :738),packages/spec/src/data/field.form.ts,packages/spec/src/data/object.form.ts, new rejection pins + changeset (stop on breach; explain in the report). ⛔packages/spec/src/migrations/registry.tsis HELD by the #10724/#11169 family dispatch this wave — if this tightening's conventions force a touch there, stop and report rather than editing it.
Container & model:M,mode:subagent,model: claude-fable-5— dispatch-gates--tier packages/spec/src/data/field.zod.ts(this dispatch): "Clause ② SUSPECT surface —packages/spec/src/**— the contract surface … the normal landing zone of a clause-② card"; content reading is the verdict: the 2026-08-24 ruling narrows the accept set ⇒ fable-mandatory.
Clause-②: yes
Serial constraints cleared: same-file predecessor PR #11903 (deleteBehavior, the.overwrite()region) merged 2026-08-25 — same-day churn clause applies, work from current merged main. H17 hit named per protocol: hold #8345 declaresfield.zod.tsas aRestart-touchtrigger file — notification only, no rider work taken from it. In-flight overlap: none (lane had 0 dispatched cards at fire); lane sibling #11408 (editMode, may touchobject.form.ts) is deliberately held to wave 2 behind this card — cross-wave serial, not a fold (different defect shape, fails family gate ①). Cross-lane: #11739 (services,feat(spec,auth)) touches spec on the auth-posture surface, disjoint from this file set.
Generated by Claude Code
- added a commit that references this issue
on Aug 25, 2026 claude commented
on Aug 25, 2026 claudeboton Aug 25, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 11566, "status": "done", "branch": "claude/issue-11566-maxlength-shape-applicability", "pr": "https://github.com/objectstack-ai/objectstack/pull/11989", "premise_still_valid": true, "summary": "Ruled tightening implemented on post-#11903 main (base ac59744): maxLength is z.number().int().min(1); a new superRefine refuses it outside the write-time validator's ten types, exported as BOUNDED_STRING_FIELD_TYPES; both authoring-form maxLength rows converge on the same ten; 15 new pins assert specific zod issue code+path+message. Alias-map assumption measured false-but-safe: strictObject aliases are unknown-key rejection guidance only — length:/size: never flow a value into maxLength, so no bypass exists. Repo sweep found zero spec-parsed authors of now-rejected values; driver-sql's #11431 consumer-defense fixtures bypass FieldSchema.parse and are preserved verbatim.", "tests": "All at head e36b89a (re-affirmed after the lint census commit; initial readings at 9ca3a81, tree byte-identical for spec surfaces). spec field.test.ts: 'Tests 184 passed (184)'. Full spec suite: 'Test Files 424 passed (424)' / 'Tests 11288 passed (11288)'; spec typecheck exit 0 incl 'check:test-typecheck: OK'. Reverse verification from committed state (git restore --source=origin/main, mutation grep-confirmed: BOUNDED_STRING_FIELD_TYPES count 0; no dist in the loop — spec tests import ./field.zod source, so no rebuild leg applies; restore leg grep count 4 + git status clean vs HEAD): exactly the predicted 12 reds ('Failed Tests 12' = 3 shape + 9 applicability pins), direction red-on-revert as expected. Consumers: objectql 'Test Files 232 passed (232)' / 'Tests 4113 passed (4113)'; driver-sql 'Test Files 128 passed | 8 skipped (136)' / 'Tests 1981 passed | 114 skipped (2095)' (live PG/MySQL matrix skips, no DB); lint 'Test Files 81 passed (81)' / 'Tests 2294 passed (2294)' after the census re-derivation.", "gates": [ "check:generated --fix: proved exactly 3 stale (api-surface, export-origins, docs) and regenerated them; 'check:authorable-surface ... ✓', 'check:skill-refs ... ✓' (no skills/** touch)", "check-adr-0087-registration: '✓ this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'", "check-changeset-no-major: '✓ This diff introduces no major bump.'", "check-empty-changeset: '✓ No empty-frontmatter changeset introduced by this diff'", "check:i18n: 'check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).' (after building the CLI; first run refused loudly with nothing measured)", "check:cross-package-test-inputs: 'OK: 16 package(s) read outside themselves, all declared'", "check:where-matcher / check:query-options-erasure / check:engine-double-contract: 'baseline key set verified against ac59744: no files added.'", "check:spec-parsed-alias: 'ADR-0122 type-alias convention ... OK'", "check-nul-bytes: 'OK (scanned 6651 text file(s) ... no raw ASCII control bytes).'", "dispatch-gates re-derived from actual diff, derivation stamped 'at commit 9ca3a81' then re-run at e36b89a (37 matched families placed); declared narrowing: repo-wide pnpm lint and check:type-check-debt --re-measure left to CI — spec's own typecheck+test-typecheck accounting ran green locally", "CI at 9ca3a81: Test Core red on lint's RHS-rule corpus census (expected 47, got 44) — fixed in e36b89a by re-deriving the census (the pin's own anchor note places in-list literals outside the rule); full lint suite green locally at that head" ], "deviations": [ "ADR-0087 ledger entry deferred to #11950: registry.ts is HELD by the #10724/#11169 family dispatch per the card constraint; the changeset carries no **BREAKING** marker (matching the #9689 accept-set-narrowing precedent), so the disposition gate does not fire — confirmed by its own verdict line above. The convention did NOT mechanically force a registry touch, so I did not stop-and-block; the debt is tracked openly instead of landing silently. PM may re-judge this fork.", "record-validator NOT rewired to import BOUNDED_STRING_FIELD_TYPES (outside the claimed file surface; membership identical and pinned on both sides) — possible follow-up refactor, noted in PR body.", "field.form minLength row left at 3 types / object.form minLength at 9 (ruled scope covered maxLength only); the minLength defect pair is filed as #11949.", "Report fields are the union of the dispatch's machine-checkable list and the standard template (the two specs differ; conflict noted per protocol).", "Second commit e36b89a (lint census) landed after the PR opened, in response to the PM's CI notification — same card scope, no widening." ], "files_changed": [ ".changeset/maxlength-bounded-string-only.md", "content/docs/references/data/field.mdx", "packages/spec/api-surface/data.json", "packages/spec/export-origins/data.json", "packages/spec/src/data/field.form.ts", "packages/spec/src/data/field.test.ts", "packages/spec/src/data/field.zod.ts", "packages/spec/src/data/object.form.ts", "packages/lint/src/validate-predicate-path-refs.test.ts" ], "open_questions": [ { "question": "Should the #11950 ledger entry land as soon as the registry hold lifts (this wave) or ride the next spec seat round?", "options": [ "A: next round, PM-scheduled", "B: rider on the #10724/#11169 family PR" ], "recommendation": "A — a rider on the family PR widens their scope; the entry is inert until protocol major 18 per step18's own text, so there is no urgency beyond tracking." } ], "out_of_scope_findings": [ "filed as #11949: minLength carries the identical pre-#11566 defect pair (shape + base-schema applicability + stale form lists); lower bound (0 vs 1) is a judgment call", "filed as #11950: deferred ADR-0087 ledger entry for this narrowing (registry.ts held this wave)" ] }
Generated by Claude Code
os-support-ai commented
on Aug 25, 2026 CollaboratorMore actionsACCEPT + Contract review: PASS — PR #11989 at head
e36b89a(spec seat R5, reviewer of recordsession_01K93P8PbH7kVjsAXkqzH1zY).Checklist verdict, verified against GitHub and my own reads:
- Shape: draft, base
main, first lineFixes #11566(full ruled scope delivered). Changed files (9) = claim surface + the PM-directed patch-round addition (packages/lint/src/validate-predicate-path-refs.test.ts— deviation explained in the report, directed by this seat's CI-failure round). Changeset on the published package; nocontent/docs/releases/; no governed surface. - Ruling implemented literally:
z.number().int().min(1)+BOUNDED_STRING_FIELD_TYPESten-typesuperRefinerefusal + both form rows converged to the ten. 15 pins assert specific zod issuecode+path+message — real rejection tests, not baretoThrow. - CI red round-trip closed: the Test Core (3/6) failure at
9ca3a81(lint's Publish-time validation of predicate path references — a spec-delivered predicate naming a nonexistent path should fail at authoring, not evaluate to a guess at render (#6936 companion) #7010 corpus census, 47→44) was this PR's own doing (the==-chain →in-list respell removed 3 literal comparisons); the fix ate36b89are-derives the census per the pin's own anchor note (in-list literals are outside the RHS rule) with the derivation recorded in a comment — I read the hunk myself; it is a reasoned re-derivation, not a blind count bump. Full lint suite green locally (2294 passed). - Mechanism assumptions honestly measured: the alias-map hazard I dispatched was falsified-but-safe (strictObject aliases feed unknown-key guidance only; no value path exists) — reported, not silently absorbed. deleteBehavior's
.overwrite()machinery correctly identified as default-materialization-only;superRefineis the right house pattern and absence-stays-absence is pinned. - Consumer readings (required): objectql 232/4113 green; driver-sql 128 files green with the live-DB matrix skips declared; lint green post-census. The driver-sql: the string family ignores
maxLengthtoo — a declared 1024 becomes varchar(255) and refuses legitimate writes #11431 consumer-defense fixtures preserved verbatim per the dispatch constraint (they bypassFieldSchema.parseby design). - Reverse verification: disk-confirmed mutation → exactly the predicted 12 reds, restore leg proven; no dist in the loop (source-relative import), correctly stating why no rebuild leg applies.
- ADR-0087 fork accepted as reported: the changeset carries no
**BREAKING**marker, matching the FieldSchema acceptsdeleteBehavior: 'set_null'on amaster_detail, and the engine silently resolves it tocascade#9689 accept-set-narrowing precedent, so the disposition gate does not fire; the ledger-entry debt is tracked openly in spec: register the #11566 maxLength narrowing in the ADR-0087 migration ledger (deferred from the enforcement PR — registry.ts was held) #11950 instead of landing silently in the HELDmigrations/registry.ts. This is lane precedent, and the annotation-gap itself is already tracked (No gate requires a breaking-behavior changeset to carry the breaking annotation at all — the ADR-0087 disposition gate only fires once one already does #8607) — flagged to the maintainer in the round report, not re-ruled here. - Open question answered (PM): Option A — spec: register the #11566 maxLength narrowing in the ADR-0087 migration ledger (deferred from the enforcement PR — registry.ts was held) #11950's ledger entry lands as its own card next round, after the Retire the nine dead members of the plugin-manifest
contributesblock (events/menus/themes/translations/actions/drivers/fieldTypes/functions/commands) — tombstone route,kindssurvives #10724/contributes.kinds.globsis declared-but-unenforced: nothing reads it, and the file-type discovery it advertises globsfilePatternsoff a registrycontributes.kindsdoes not extend #11169 fold releases the registry hold; a rider would widen the family PR. The entry is inert until protocol major 18 per step18's text; tracking is the urgency, and it is tracked. - Out-of-scope findings verified filed: spec:
minLengthstill has the pre-#11566 defect pair — validated as no more than a number, authorable on every field type #11949 (minLength twin defect, unassigned, judgment call flagged) and spec: register the #11566 maxLength narrowing in the ADR-0087 migration ledger (deferred from the enforcement PR — registry.ts was held) #11950 — both exist, both back-linked. - Contract-review tier: this seat's serving model re-verified this shift (
get_session→claude-fable-5=CONTRACT_REVIEW_TIER); standing authorization on seat post [PM seat] domain:spec — ⏳ vacant #6017 (2026-08-24, 「你自己就是fabke,为什么还要等 review」).
Disposition: enqueue follows the standing rule — every check on the PR green at head
e36b89a; the seat flips ready + queues on the all-green reading and tracks to MERGED. Card stayspm:dispatcheduntil landing.
Generated by Claude Code
- Shape: draft, base
- added a commit that references this issue
on Aug 31, 2026 - added 3 commits that reference this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while implementing #11431. Filed unassigned. Cannot be fixed from the driver lane —
packages/specis a different seat.What
packages/spec/src/data/field.zod.tsdeclares:on the base field schema. Two consequences:
.number()alone accepts0,-5,12.5andInfinity. None of those is a character length, and each parses without complaint.maxLengthis legal onboolean,date,lookup,autonumber,formula— types where it describes nothing that is stored.Evidence that this is not theoretical
schema-drift.tsread a malformed bound as authoritative and planned DDL no server accepts —maxLength: 0took the narrowing arm (0 > 255is false) and asked forvarchar(0), at severityerror/ categorydestructive, i.e. as workos migrate apply --allow-destructiveshould go do. Fixed on the driver side in #11431 by refusing anything that is not a positive integer, but that is a consumer defending itself against a producer that should not have emitted the value (PD #12).The authoring forms already disagree about where the key applies, which suggests the base-schema placement was never deliberate:
field.form.tsshowsmaxLengthfortext/textarea/emailonlyobject.form.tsshows it fortext,textarea,email,url,phone,password,markdown,html,richtextNeither list matches the other, and neither matches the base schema's "every type".
The write-time validator (
packages/objectql/src/validation/record-validator.ts) enforcesmaxLengthfortext | textarea | email | url | phone | password | markdown | html | richtext | code— a third list, and the closest thing to a real answer for which types the key means something.Direction (for triage — not prescribing)
Two separable decisions:
z.number().int().min(1)is the house pattern, and the file already applies exactly it toprecisionone field below (z.number().int().min(0)) for the same stated reason — spec:Field.scaleaccepts meaningless declarations (2.5,-1) — now that scale is enforced, malformed declarations should be refused at authoring time #8321's "a non-integer or negative declaration has no defined meaning". Cheap, and strictly a tightening.maxLengthshould be refused on types that do not store a bounded string is the larger question, and touches ADR-0078 declared=enforced. The validator's ten-type list is the natural candidate for the allowed set.Generated by Claude Code