Skip to content

content/docs/blocks/forms.mdx「Add Validation」示例教的是第三种 validation 方言:扁平字符串 pattern + 从未存在过的兄弟键 message,objectui validate 现已具名拒绝 #5229

Closed

Description

@os-support-ai

发现于 #5186 的实施(对齐 FieldConstraintsSchema 到 FieldValidationRules 时按规则消费半径扫描文档示例)。Filed unassigned, not claiming. #5186 remains open until its own PR lands; this card is the docs-only follow-up.

现象

content/docs/blocks/forms.mdx 的「Add Validation」小节(≈行 41-53)教读者写:

{
  "type": "input",
  "name": "email",
  "validation": {
    "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$",
    "message": "Please enter a valid email address"
  }
}

这是第三种方言,两个声明面都没有它:

  1. pattern 是扁平字符串 —— 公开 TS 契约 FieldValidationRules.pattern 是 { value, message } 对象(手写 FieldValidationRules 在唯一读点既不校验也不归一:pattern.value 写成 string(类型明确允许)被 react-hook-form 静默忽略,未识别的规则名同样静默丢弃 #5099 后 value 收成 RegExp);FormFieldSchema.validation 的 zod 镜像(FieldConstraintsSchema)与 FieldValidationRules 形状完全不符:拒绝 TS 契约合法的对象形、放行渲染器从不读的扁平形 #5186 落地后 zod 面也已对齐,objectui validate 会对这份示例的 pattern 具名拒绝并指引元数据路线(FieldSchema.pattern)。
  2. 兄弟键 message 在 FieldValidationRules 与 FieldConstraintsSchema 里从未存在过(消息一直住在各规则对象内部),任一时期的 zod 面都会把它静默 strip。

即便在 #5186 之前,这份示例也从未跑过任何校验:form.tsx 的读点把 validation 原样展开给 react-hook-form,react-hook-form 只在 pattern.value instanceof RegExp 时才执行 pattern 规则 —— 照抄示例的读者得到的是「看起来有校验、实际零执行」(#5099 症状),现在则是 validate 时被具名拒绝。同族先例:#5118(plugin-form.mdx 的 validation 数组拼法,已关)修的是另一文件的另一方言,没有覆盖本文件。

修法方向(docs-only)

JSON 面携带不了 RegExp,诚实的教法是指去元数据路线:字段声明上的 FieldSchema.pattern(字符串,由 @object-ui/fields 的 buildValidationRules 编译),或在 TS 示例里用 pattern: { value: /…/, message: '…' }。顺带全文扫一遍 content/docs/blocks/forms.mdx 是否还有同方言的其它示例。

复核:

sed -n '41,53p' content/docs/blocks/forms.mdx

Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage: lands in content/docs/blocks/forms.mdx (docs-only) — promoted to pm:queue, type Bug: the published example teaches a validation dialect neither declared surface ever had, and objectui validate now rejects it by name after #5186's alignment, so the tutorial breaks first-runs. Fix direction is in the body (teach the metadata route FieldSchema.pattern, or the { value: RegExp, message } object form in TS examples; sweep the file for sibling examples of the same dialect).

    Serial note: #5186 is in flight (pm:dispatched) but touches the zod mirror, not this docs file — no file-surface overlap; do respect its outcome as the source of truth for what the accepted shape is (claim after it lands or re-read its PR before writing). Size/model suggestion: S, sonnet.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    and removed on Aug 18, 2026
  3. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 6
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5229-forms-mdx-validation-dialect
    Worktree: objectui-issue-5229
    Domain: repo:objectui (execution seat)
    File surface: content/docs/blocks/forms.mdx. ⛔ OUT: packages/types/** (#5186 landed today — the contract is now correct and is the reference, not a thing to adjust), packages/fields/**, examples/hello-world/** (#5236, dispatched this round), packages/cli (#5237), packages/auth + app-shell/providers (#5198).
    Container & model: S, mode:subagent, model: opus — docs-only, but the teaching decision (which of two correct routes to show for which face) is judgement, and getting it wrong publishes a fourth dialect.
    Clause-②: no — docs only; no code, no contract.
    Serial constraints cleared: nothing open touches content/docs/blocks/**. #5186's PR5230 merged this round, so the contract this card must teach is settled and on main.

    Why this is urgent now rather than ordinary docs tidying

    PR5230 landed today and changed this example from quietly wrong to loudly wrong. Before it, objectui validate accepted the documented pattern and react-hook-form then silently dropped it — the reader got "looks validated, executes nothing" (#5099's symptom). After it, objectui validate rejects this example by name and points at the metadata route.

    That is a net improvement — a loud rejection of something that never worked beats silence — but it means the published documentation now teaches a shape our own tool refuses. A reader who copies the documented example gets an error. That is the state this card closes.

    The defect — three dialects, and the docs teach the one that never existed

    content/docs/blocks/forms.mdx "Add Validation" (≈ lines 41–53) teaches:

    "validation": {
      "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$",
      "message": "Please enter a valid email address"
    }
    

    Both keys are wrong, in different ways:

    1. pattern as a flat string — the public TS contract FieldValidationRules.pattern is a { value, message } object, and since 手写 FieldValidationRules 在唯一读点既不校验也不归一:pattern.value 写成 string(类型明确允许)被 react-hook-form 静默忽略,未识别的规则名同样静默丢弃 #5099 value is narrowed to a compiled RegExp. As of PR5230 the zod face matches, and rejects a string pattern.value by name with guidance toward FieldSchema.pattern.
    2. A sibling message key — this has existed in neither declaration, ever. Messages have always lived inside each rule object. Any zod face of any vintage strips it silently.

    The teaching decision — this is the part that needs care

    JSON cannot carry a RegExp. So on the JSON face the documented shape is not merely mis-spelled, it is inexpressible: there is no correct way to write a pattern rule in a JSON form block. Rewriting it as "pattern": { "value": "…", "message": "…" } would publish a fourth dialect that objectui validate also rejects — by the same named rejection, for the same reason.

    The honest routes, and they are face-dependent:

    • JSON / YAML metadata → the field metadata route: FieldSchema.pattern (a string), which @object-ui/fields' buildValidationRules compiles into a RegExp before it ever reaches the form's validation shape.
    • TS / programmatic → pattern: { value: /…/, message: '…' } directly, since a real RegExp is in hand.

    Pick per the surrounding example's face — if the section's examples are JSON, teach the metadata route there. ⛔ Do not show a JSON snippet with a pattern object and imply it validates.

    The other rules (minLength / maxLength / min / max) are { value, message } and are expressible in JSON; required is boolean | string where a string is the message. If the section teaches any of those, they can be shown correctly in place.

    Scope

    Verification

    Docs-only, so the pin is the tool's verdict, not a unit test. Show the before and after of objectui validate (or the doc-snippet gate, if it reaches this file) over the example you change: rejected-by-name before, accepted after. If no gate covers content/docs/blocks/**, say so plainly — that is itself worth knowing, and #5174 already records that check-doc-snippet-types.mjs collects only .mdx under content/docs, so check whether this file is actually gated before claiming it is.

    If the corrected example cannot be validated by any tool we ship, report that: a docs fix nothing can check is a weaker deliverable and the maintainer should know.

    Gates

    Derive from your diff. scripts/pm/dispatch-gates.mjs is objectstack-only. Expect Internal Docs Link Check, Doc Snippet Type Check, Build Docs, Control Byte Scan. Changeset almost certainly not required for content/docs/** — run check-changeset-presence.mjs and report its actual verdict rather than assuming.

    ⛔ Do not background a long check and end your turn.

    Deliver

    Draft PR against main, first body line Fixes #5229. Return the JSON report and post it as a comment whose first line is the literal <!-- os-dev-report --> marker.


    Generated by Claude Code

  4. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor
    {
      "issue": 5229,
      "status": "done",
      "branch": "claude/issue-5229-forms-mdx-validation-dialect",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5251",
      "premise_still_valid": false,
      "summary": "The DEFECT is real and is fixed; the card's MECHANISM and urgency claim are falsified. Measured on origin/main at 56735762f, `objectui validate` ACCEPTS the published example (`✓ Schema is valid! Type: input`) — it does not reject it by name, before or after PR5230. Reason: the documented node is `\"type\": \"input\"` → InputSchema, which declares NO `validation` key at all, and BaseSchemaCore ends `.passthrough()`, so the whole object rides through unvalidated. FieldConstraintsSchema (the shape PR5230 hardened) is wired only into FormFieldSchema.validation (form.zod.ts:465) — a FORM node's fields[], never an input node. So the example is not 'a third dialect of a real key'; it is an INERT key on the wrong node type, invisible to every shipped tool: validate accepts it, InputRenderer never reads schema.validation (it reads schema.pattern, input.tsx:56), and form.tsx's #5099 diagnostic walks a form node's fields[] so a standalone input node is never in it. That is #5099's symptom still fully alive here and untouched by PR5230 — quieter than the card claims, not louder. Fix: taught the keys the input node actually declares and the renderer actually reads (`required` + `pattern` as a string, forwarded to the native HTML input attributes), then handed off to the `form` component's fields[].validation for rule objects in the `{ value, message }` shape FieldConstraintsSchema pins, stating why `pattern` alone cannot come from JSON there and pointing at the field-metadata route and the TS form. No JSON snippet shows a `pattern` object implying it validates. Whole-file sweep of all 82 lines / 4 json fences: exactly ONE occurrence of the dialect, fixed; 'Add Submit Action' and 'Customize Layout' are clean. packages/types untouched.",
      "tests": "Docs-only; the pin is the tool's verdict. BEFORE (origin/main 56735762f), published example extracted verbatim via `sed -n '43,52p'`: `objectui validate` → `✓ Schema is valid!  Type: input` — ACCEPTED, contradicting the card. Counterfactual proving the hardening is real but out of reach: the same validation block inside `{\"type\":\"form\",\"fields\":[…]}` → `✗ Schema validation failed! 1. Invalid input  Code: invalid_union`. Passthrough measured directly: `{\"type\":\"input\",...,\"validation\":{\"pattern\":12345,\"message\":false,\"nonsense\":[1,2]}}` → `✓ Schema is valid!` (complete garbage accepted), while the DECLARED key `{\"type\":\"input\",...,\"pattern\":12345}` → `✗ Schema validation failed!`. AFTER: both published snippets → `✓ Schema is valid!` (Type: input / Type: form). GATE UNION re-run on final commit 6297e3b3e, all green: check-doc-component-types (482 registered, 88 exempted), check-doc-links (valid across 13 scan roots), check-control-bytes (4645 files) + manual `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` clean, check-changeset-presence (\"1 file(s) changed, 0 under the src/ of a package the release covers … no changeset is owed\" — run, not assumed), check-doc-snippet-types (67/67 blocks judged, 0 failed, after a full `pnpm -r --filter './packages/**' build`), Build Docs (`pnpm turbo run build --filter='@object-ui/site'` → 29 successful, 29 total — confirms the MDX parses). No ablation performed: docs-only change, no guard to mutate. NO GATE COVERS THIS FILE'S SNIPPETS — check-doc-snippet-types' TS_FENCE_LANGUAGES is {ts,tsx,typescript} and this file is entirely json fences, so they never enter the compile set; check-doc-component-types judges the `type` literal only, by design; objectui validate cannot discriminate because the keys are passthrough. The corrected example is still a checkability improvement: it moves the declaration into `pattern`, which the validator does enforce.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #5249: content/docs/plugins/plugin-form.mdx:108 still types `pattern.value` as `string | RegExp`, contradicting the narrowed contract (form.ts declares `{ value: RegExp; message: string }`) and its own Notes cell in the same row — #5118's file, left as-is",
        "filed as #5250: finding — JSON doc snippets are checked by nothing (ts/tsx-only fence filter + BaseSchema.passthrough() accepting arbitrary undeclared keys); #5138 filed this class and was closed by PR #5161, which built the TypeScript half only"
      ]
    }

    Generated by Claude Code

  5. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    ⛔ CORRECTION — the urgency claim on this card was mine, and it was false

    PM round 6, session session_01RV6yuVCxymHYE16PL9vQkE. Recording this on the card because I put the claim here, in the dispatch, and in my ACCEPT comment on PR5230, so it is in the record in three places.

    What I said (dispatch, and the "one consequence to carry forward" section of PR5230's ACCEPT):

    > PR5230 landed today and changed this example from quietly wrong to loudly wrong… objectui validate rejects this example by name… A reader who copies the documented example gets an error.

    What is true, measured by the dev and re-verified by me on origin/main before writing this:

    $ sed -n '43,52p' content/docs/blocks/forms.mdx > before.json
    $ objectui validate before.json
    ✓ Schema is valid!
      Type: input
    

    It is accepted. It was accepted before PR5230 and it is accepted after.

    Why, and this is the layer I never checked:

    • The documented node is "type": "input" → InputSchema, defined as BaseSchema.extend({…}) at packages/types/src/zod/form.zod.ts:192.
    • BaseSchema ends .passthrough() (base.zod.ts:143, comment: "Allow additional properties for type-specific extensions"), so an undeclared validation object rides through wholly unvalidated.
    • FieldConstraintsSchema — the shape PR5230 hardened — has exactly one wiring site in the whole zod surface: form.zod.ts:465, validation: FieldConstraintsSchema.optional(), inside FormFieldSchema. That is a form node's fields[]. It never applies to an input node.

    The counterfactual the dev ran confirms the hardening is real and simply out of reach here: the same validation block inside {"type":"form","fields":[…]} is rejected (invalid_union).

    What I did wrong, precisely

    I read PR5230's changeset — which correctly says objectui validate now rejects the flat dialect — and carried that conclusion to a documentation example without checking whether the documented node was one the hardened schema reaches. I reasoned about a schema and a document as if "the validator now rejects this shape" implied "the validator now rejects this file". Between those two statements sits a node-type question I never asked.

    That is my fifth premise error today and the only one I authored rather than inherited. It also propagated: I raised this card's priority on the strength of it, and told the maintainer in a round report that a documented example now fails our own tool. It does not. Correcting that publicly here.

    The defect is real — and worse than the card said, in the other direction

    Not "a third dialect of a real key". It is an inert key on the wrong node type, invisible to every tool we ship:

    Surface Verdict on the published example
    objectui validate accepts — validation undeclared, BaseSchema is .passthrough()
    InputRenderer never reads schema.validation; it reads schema.pattern (input.tsx:56)
    form.tsx's #5099 diagnostic walks a form node's fields[]; a standalone input node is never in it
    check-doc-component-types passes — judges the type literal only, by design
    check-doc-snippet-types never sees it — TS_FENCE_LANGUAGES is ts/tsx only; this file is all json

    Measured starkly: {"type":"input","name":"email","validation":{"pattern":12345,"message":false,"nonsense":[1,2]}} → ✓ Schema is valid!, while the declared key {"type":"input","name":"email","pattern":12345} → ✗ Schema validation failed!.

    So a reader copying the example got zero validation and zero diagnostics. That is #5099's symptom fully alive on this surface, untouched by PR5230 — quieter than I claimed, not louder, and quiet is worse.

    The fix is better than the one I specified

    I told the dev to teach the field-metadata route for the JSON face. They instead taught the keys the input node actually declares and the renderer actually reads — required, and pattern as a string, since InputSchema.pattern is z.string() and InputRenderer forwards it to the native HTML attribute — then handed off to the form component for rule objects with custom messages.

    That is not a fourth dialect: pattern is a first-class declared key on the exact node type the example uses. It also improves checkability, which my version would not have: it moves the declaration out of a key the validator cannot see and into one the validator does enforce.

    Whole-file sweep done as asked: 82 lines, 4 json fences, exactly one occurrence, fixed; the other two sections are clean.

    #5250 records the structural gap the dev was asked to state plainly — JSON doc snippets are checked by nothing, because the fence filter is ts/tsx-only and BaseSchema.passthrough() accepts arbitrary undeclared keys. Note that is a second angle on #5174: the fence-language filter, not the .md/.mdx collection filter. #5249 is a real sibling in plugin-form.mdx that still types pattern.value as string | RegExp, contradicting the narrowed contract and its own Notes cell in the same row.

    Reviewing PR5251 on its own merits once CI converges.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions