Skip to content

fix(spec): minLength is a positive integer, authorable only on bounded-string field types; register the deferred #11566 maxLength ADR-0087 entry - #12191

Merged
os-litant merged 6 commits into
mainfrom
claude/issue-11949-minlength-defect-pair
Aug 25, 2026
Merged

fix(spec): minLength is a positive integer, authorable only on bounded-string field types; register the deferred #11566 maxLength ADR-0087 entry#12191
os-litant merged 6 commits into
mainfrom
claude/issue-11949-minlength-defect-pair

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#11949
Fixes#11950

Family fold (declared in the #11949 claim): one branch, one PR, one commit per member. Draft by dispatch contract — Clause-② yes on the chain head, so the PR stays draft for the needs:contract-review chain; no AI seat flips it ready.

Member 1 — #11949: minLength joins the bounded-string tightening (commit 1)

Maintainer ruling (2026-08-25, decision-inbox batch 8, verbatim 「同意」 on recommendation B): shape z.number().int().min(1); the lower bound is 1minLength: 0 is refused loudly, "no minimum" is expressed by omitting the key; applicability restricted to BOUNDED_STRING_FIELD_TYPES; both authoring forms' rows aligned.

  • packages/spec/src/data/field.zod.ts — shape tightened; applicability superRefine (twin of maxLength's 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 check, reading the same exported set); set doc comment extended.
  • packages/spec/src/data/field.form.tsminLength row respelled from a 3-type == chain to the in-list matching the maxLength row beside it.
  • packages/spec/src/data/object.form.tsminLength row aligned to the same set (was 9 types, code missing).
  • packages/spec/src/data/field.test.ts — refusal envelopes asserting path + code + message for 0 / -5 / 12.5 (too_small / too_small / invalid_type at [minLength]) and for ten wrong-type representatives (custom issue naming the legal set and the offending type); byte-identical round-trip (minLength: 2 in → 2 out) on every bounded-string type; minLength: 1 lower-bound pin; absence stays absent (no default materializes).
  • packages/lint/src/validate-predicate-path-refs.test.ts — RHS-rule corpus census re-derived 44 → 41 (3 fewer == literal comparisons after the in-list respell; the exact fix(spec): maxLength is a positive integer, authorable only on bounded-string field types #11989 precedent, which took it 47 → 44).
  • D3 semantic entry field-min-length-malformed-or-misplaced-refused (major-18 one-file shard + gen:migration-registry).
  • Changeset: @objectstack/spec minor (launch-window convention), adr-0087: registered.

Ruled pre-step: corpus & generator sweep (gate condition for landing)

The ruling required a grep of the corpus and generators for existing minLength: 0 / negative / fractional output before landing, with a hard stop if a shipped customer-facing generator emits minLength: 0.

  • Malformed values: zero hits repo-wide (minLength: 0|-N|N.M over *.ts,tsx,js,json,yml,yaml,md,mdx, node_modules/dist excluded). No cleanup rides the PR; no generator forks back — the stop condition did not fire.
  • Placement triage (every remaining minLength occurrence): examples/** — no field-level minLength at all; spec fixtures (field.test.ts) — text fields, legal; packages/rest/src/import-dryrun-parity.test.tstext + minLength: 3, legal; packages/rest/src/export-format.test.ts — a lookup fixture deliberately carrying the eight retired presentation keys (minLength: 2 among them) to pin that they do NOT travel into the export meta map — it never crosses FieldSchema.parse, i.e. outside this rule's consumption radius, and fix(spec): maxLength is a positive integer, authorable only on bounded-string field types #11989 left its maxLength: 20 twin untouched for the same reason; fast-check fc.string({ minLength }) sites — a different API entirely; skills/objectstack-data/rules/field-types.md documents minLength only on text/textarea/password — all inside the set, no edit needed (no skills/** diff in this PR).

Member 2 — #11950: the deferred #11566maxLength ADR-0087 ledger entry (commit 2)

The #11566 enforcement (PR #11989) shipped without its ledger entry (registry serialized behind an in-flight change in that wave; deferral recorded in that PR's own changeset). This commit registers it: D3 semantic entry field-max-length-malformed-or-misplaced-refused (major-18 shard + regeneration), following the #8321scale/precision template, semantics authoritative to PR #11989's actual diff.

One deliberate correction to the card body's wording, measured against record-validator.ts: "nothing ever enforced the malformed value" is true for misplaced keys (the validator's bounded-string branch never read them — inert by construction) but NOT for malformed values on bounded-string types — the raw comparison consumed them (maxLength: 0 accepted only empty strings; a negative value refused every write; 12.5 behaved as "at most 12"). The entry states the measured truth and routes those to the judgment half (re-declare the intended bound), which is also why deleting them is not sold as behaviour-preserving. Same split applied to the minLength entry (0/negative genuinely unfailable; fractional consumed as "at least the next integer up").

Clause-② for this member: no — the entry registers an already-landed narrowing; no accept/reject behaviour moves in commit 2 (no D2 conversion is added; the load path is untouched).

Dispatch-premise timeline (both members)

  1. The "signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 landed / 12-member set" interaction note was not yet true at this branch's base (577fabf): the set had ten members, record-validator.ts still hand-coded its list, and signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 was open with PR feat(data): signature and qrcode join the bounded-string family end to end #12119 in flight. Member 1 was first implemented set-driven at ten (the superRefine reads the set; enforcement matched because the validator's hand list equalled it).
  2. feat(data): signature and qrcode join the bounded-string family end to end #12119 landed mid-flight (a11c1a5). The merge of origin/main stacks both intents: minLength moves with the set exactly as that PR moved maxLength — both form rows at the twelve, the describe strings name signature/qrcode, the superRefine message now enumerates the set itself (adopting signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875's driver-sql's varchar-sizing type list and spec's BOUNDED_STRING_FIELD_TYPES must now agree, and nothing pins them — the same three-lists-disagree defect #11566 was filed for, one layer down #12017-two-copies fix rather than a prose copy), tests pin the twelve and add color to the wrong-type representatives. The changeset and both ledger entries state the twelve-member landing truth (the maxLength entry keeps the historically-accurate ten of fix(spec): maxLength is a positive integer, authorable only on bounded-string field types #11989's own landing, with the signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 growth noted).

Verification

Full suite at the post-#11875 merged tree (branch commit 1f68923, identical spec surface carried to final head 5f50d3e — the second merge brought no overlap with this diff):

  • pnpm --filter @objectstack/spec testTest Files 426 passed (426) / Tests 11364 passed (11364) (includes the new spec: minLength still has the pre-#11566 defect pair — validated as no more than a number, authorable on every field type #11949 block: refusal envelopes for 0/-5/12.5 and ten wrong-type representatives incl. color, twelve-type byte-identical round-trip, minLength: 1 lower-bound pin, absence pin).
  • pnpm --filter @objectstack/spec typecheck — green (same lock run, command-exit 0).
  • pnpm --filter @objectstack/objectql exec vitest run src/validation/record-validator.test.ts92 passed (the write-seam consumer, now set-driven post-signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875; no validator edit needed for minLength — the branch already applies min_length to every set member).
  • pnpm --filter @objectstack/lint exec vitest run src/validate-predicate-path-refs.test.ts54 passed; the RHS-census equality holds at the re-derived 41.
  • pnpm --filter @objectstack/spec check:generated — gate's own line at final head 5f50d3e: "✓ All 14 generated artifacts are up to date."
  • Gate union at final head 5f50d3e, re-run after the last origin/main merge — every family derived by scripts/pm/dispatch-gates.mjs (no paths; stderr stamps repo + commit) plus the dispatch-named set, all green: changeset-gate-self-tests · cross-package-test-inputs · doc-anchors · doc-authoring · doc-formula-expressions · doc-security-posture · docs-audit-scope · docs-redirects · empty-state · liveness · merge-driver · objectui-changeset · published-files · published-readme-links · query-options-erasure · quick-reference-counts · react-page-adapter-contract · role-word · slot-lookup · spec-parsed-alias · strictness-ledger · test-source-alias · type-source-resolution · variant-docs · adr-0087-registration · changeset-no-major · nul-bytes · ci-filter-parity (spelling note: the runnable form is node scripts/check-ci-filter-parity.mjs; there is no pnpm check:ci-filter-parity script).
  • Repo-wide pnpm lint and the full multi-package farm are CI's runs; local verification was scoped to the affected packages plus every path-derived gate above (declared narrowing — CI runs the farm on this PR).

H17 note: zero currency surface in this diff (all currency mentions in git diff origin/main...HEAD are unchanged context lines); currencyConfig / currency-fraction-digits.ts untouched.

Out-of-scope finding filed: #12174 (ui FormFieldBaseSchema carries the same pre-#11566 constraint-shape defect pair; applicability story differs — measured readers needed before template vs enforce-or-remove).


Generated by Claude Code

…d-string field types
Issue #11949 context (maintainer ruling 2026-08-25, option B): the #11566
template applies in full. Shape tightens to z.number().int().min(1) — the
lower bound is 1, so minLength: 0 is refused loudly ("no minimum" is
expressed by omitting the key, not by declaring a vacuous truth).
Applicability converges on BOUNDED_STRING_FIELD_TYPES via a superRefine twin
of maxLength's; both authoring-form rows align to the same set
(field.form.ts previously showed the key for 3 types, object.form.ts for 9).
Card relationship is declared in the PR body, not here.
Ruled pre-step: corpus + generator grep for minLength: 0 / negative /
fractional output — zero hits repo-wide, so no cleanup rides the PR and no
customer-facing generator fork exists.
- field.zod.ts: shape + applicability superRefine + set doc comment
- field.form.ts / object.form.ts: minLength rows aligned to the ten
- field.test.ts: refusal envelopes (path + code + message) for 0 / -5 / 12.5
and for nine wrong-type representatives; byte-identical round-trip on all
ten bounded-string types; minLength: 1 lower-bound pin; absence stays
absent
- lint RHS-rule census re-derived 44 -> 41 (3 fewer == literal comparisons
after the in-list respell of the field-form row — the #11989 precedent)
- D3 semantic entry field-min-length-malformed-or-misplaced-refused
(major-18 one-file shard + gen:migration-registry)
- changeset: @objectstack/spec minor (launch-window convention), adr-0087:
registered
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
…DR-0087 ledger
Issue #11950 context: the #11566 enforcement (PR #11989) shipped without its
ADR-0087 ledger entry — the migrations registry was serialized behind an
in-flight change in that wave — and this commit lands the missing half as a
major-18 D3 semantic entry, following the #8321 scale/precision template.
Semantics are authoritative to PR #11989's actual diff: refused shapes
(0 / negative / non-integer on any type), refused placement (any value
outside the ten-member BOUNDED_STRING_FIELD_TYPES set as of that landing),
forms convergence, byte-identical round-trip for well-formed declarations.
The entry separates the mechanical half (misplaced keys were inert by
construction — the validator's bounded-string branch never read them) from
the judgment half (malformed values on bounded-string types WERE consumed by
the raw comparison — maxLength: 0 accepted only empty strings, a negative
value refused every write — so the author must re-declare the bound they
meant). No accept/reject behaviour moves in this commit. Card relationship
is declared in the PR body, not here.
- D3 semantic entry field-max-length-malformed-or-misplaced-refused
(major-18 one-file shard + gen:migration-registry)
- changeset: @objectstack/spec minor, adr-0087: registered
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
…1949-minlength-defect-pair
Semantic merge, both intents stacked: minLength rows/describe/superRefine
message and tests move with the set exactly as #12119 moved maxLength's —
forms at the twelve, message enumerates the set itself (the #12017
two-copies lesson), wrongTypes gains 'color', accept pin covers
signature/qrcode. Generated artifacts deferred to the regeneration commit
per the os-regen procedure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
Discharges the os-regen deferral the merge commit recorded: registry
regenerated from the (merge-updated) semantic shards; field.mdx rendered
from the merged schema (minLength row at the twelve-member set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 25, 2026
@github-actions

github-actionsBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx(via minLength (literal))
  • content/docs/concepts/metadata-driven.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/external-datasources.mdx(via FieldSchema (symbol))
  • content/docs/data-modeling/field-types.mdx(via FieldSchema (symbol), minLength (literal))
  • content/docs/data-modeling/fields.mdx(via minLength (literal))
  • content/docs/data-modeling/validation-rules.mdx(via FieldSchema (symbol), minLength (literal))
  • content/docs/deployment/troubleshooting.mdx(via FieldSchema (symbol))
  • content/docs/deployment/validating-metadata.mdx(via FieldSchema (symbol))
  • content/docs/getting-started/quick-reference.mdx(via FieldSchema (symbol))
  • content/docs/kernel/contracts/data-engine.mdx(via FieldSchema (symbol))
  • content/docs/protocol/backward-compatibility.mdx(via FieldSchema (symbol))
  • content/docs/protocol/objectql/schema.mdx(via minLength (literal))
  • content/docs/protocol/objectql/types.mdx(via FieldSchema (symbol))
  • content/docs/protocol/objectui/index.mdx(via minLength (literal))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via FieldSchema (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 494279cb31f1d92adab959763085e19c923a8652packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8403770dbb5c2a64bcb4c3b390246d6a5b629c52 — the merge of head 6a2b4ed83e3e1f9573f7dbd5acc83bb2bf466860 into base 494279cb31f1d92adab959763085e19c923a8652, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8403770dbb5c2a64bcb4c3b390246d6a5b629c52 && git checkout 8403770dbb5c2a64bcb4c3b390246d6a5b629c52
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 494279cb31f1d92adab959763085e19c923a8652 6a2b4ed83e3e1f9573f7dbd5acc83bb2bf466860 && git checkout -B drift-repro 494279cb31f1d92adab959763085e19c923a8652 && git merge --no-ff 6a2b4ed83e3e1f9573f7dbd5acc83bb2bf466860
node scripts/docs-audit/affected-docs.mjs --json 494279cb31f1d92adab959763085e19c923a8652

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 494279cb31f1d92adab959763085e19c923a8652 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-litant
os-litant marked this pull request as ready for review August 25, 2026 13:47
@os-litant
os-litant added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 9abe4e4Aug 25, 2026
35 checks passed
@os-litant
os-litant deleted the claude/issue-11949-minlength-defect-pair branch August 25, 2026 14:25
os-litant pushed a commit that referenced this pull request Aug 25, 2026
)
gen:migration-registry (byte-identical — main's generated regions already
carried the #12191 semantic entries), rebuild restoring the
ui/ObjectGridProps:defaultSort [RETIRED] surface mark, gen:docs restoring the
retired reference row. Sibling entries and implementation bodies asserted
present (field-max/min-length semantic entries, object editMode, minLength).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-litant@claude