Skip to content

Commit d15c716

Browse files
committed
feat(spec): register the deferred #11566 maxLength narrowing in the ADR-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
1 parent f081ac6 commit d15c716

3 files changed

Lines changed: 109 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
The #11566 `maxLength` narrowing (shipped in 17.x: `z.number().int().min(1)`, refused outside `BOUNDED_STRING_FIELD_TYPES`) is now registered in the ADR-0087 migration ledger (#11950) — the enforcement PR deliberately deferred the entry because the registry file was serialized behind an in-flight change. Following the #8321 `scale`/`precision` template, the major-18 semantic entry carries both halves: the mechanical one (delete the key where it was misplaced — inert by construction outside the write-time validator's bounded-string branch) and the judgment one (a malformed value on a bounded-string type WAS consumed by the validator's raw comparison — `maxLength: 0` accepted only empty strings, a negative value refused every write — so only the author knows the bound they meant; the entry tells them to re-declare it). `objectstack migrate meta`, `spec-changes.json` and the upgrade guide surface the entry at the major boundary; no accept/reject behaviour changes in this release.
6+
7+
<!-- adr-0087: registered field-max-length-malformed-or-misplaced-refused -->
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'field-max-length-malformed-or-misplaced-refused',
7+
surface: 'object field `maxLength` declarations — `maxLength: 0`, negative or non-integer '
8+
+ 'values on any type, and the key with any value on field types outside '
9+
+ '`BOUNDED_STRING_FIELD_TYPES` (`boolean`, `lookup`, `autonumber`, `formula`, `select`, '
10+
+ '`json`, `secret`, …)',
11+
replacement: 'a positive-integer `maxLength` (>= 1) on a bounded-string field type — `text`, '
12+
+ '`textarea`, `email`, `url`, `phone`, `password`, `markdown`, `html`, `richtext`, `code` '
13+
+ '— or no declaration at all. Deleting the key is mechanical and behaviour-preserving '
14+
+ 'for a MISPLACED declaration: the write-time validator only ever applied `max_length` '
15+
+ 'inside its bounded-string branch, so the key was inert by construction on every other '
16+
+ 'type. A MALFORMED value on a bounded-string type is the judgment case — the '
17+
+ 'validator\'s raw `>` comparison did consume it (`maxLength: 0` accepted only the '
18+
+ 'empty string, a negative value refused every write, `maxLength: 12.5` behaved as '
19+
+ '"at most 12"), and the SQL schema-drift planner consumed `maxLength: 0` as '
20+
+ '`varchar(0)` DDL until #11431 taught it to defend itself — so only the author knows '
21+
+ 'the bound they MEANT: re-declare it as a positive integer, or delete it deliberately '
22+
+ 'accepting the unbounding',
23+
reason:
24+
'#11566 (maintainer ruling 2026-08-24; enforcement shipped on the 17.x line in PR '
25+
+ '#11989 — accept-set narrowings ride minors, and this entry tells `migrate meta` users '
26+
+ 'at the major boundary; registration was deferred to #11950 because the registry file '
27+
+ 'was serialized behind an in-flight change when the enforcement landed). Shape: a '
28+
+ 'character length is a positive integer, so the key tightened from `z.number()` to '
29+
+ '`z.number().int().min(1)` — `maxLength: 0` measurably sent schema-drift planning '
30+
+ '`varchar(0)` DDL no server accepts, at severity error/destructive, before #11431 '
31+
+ 'taught that consumer to defend itself (the #8321 `precision`/`scale` house pattern). '
32+
+ 'Applicability: the key sat on the BASE field schema — authorable on `boolean` / '
33+
+ '`lookup` / `autonumber`, types where nothing bounded is stored — while the write-time '
34+
+ 'validator (objectql `record-validator.ts`) only ever enforced it on its ten '
35+
+ 'bounded-string types, the one list of the three that had a measured reader; that list '
36+
+ 'is promoted to the protocol as `BOUNDED_STRING_FIELD_TYPES`, the schema refuses the '
37+
+ 'key outside it (ADR-0078 declared=enforced), and both authoring forms '
38+
+ '(`field.form.ts`, previously 3 types; `object.form.ts`, previously 9) show the key '
39+
+ 'for exactly that set.',
40+
acceptanceCriteria:
41+
'Every field declaring `maxLength` carries a positive integer and is a bounded-string '
42+
+ 'type. Well-formed declarations (a positive-integer `maxLength` on a bounded-string '
43+
+ 'type) parse byte-identically to before; fields declaring no `maxLength` are '
44+
+ 'untouched, and absence stays absence — no default materializes. Deleting a misplaced '
45+
+ 'key changes no runtime behaviour (it sat outside the validator\'s bounded-string '
46+
+ 'branch and enforced nothing). For a malformed value on a bounded-string type the '
47+
+ 'author decides: re-declare the intended positive-integer bound (enforced by the '
48+
+ 'write-time validator from the next write on, and honoured by schema drift as '
49+
+ '`varchar(n)`), or delete the key and accept the type\'s unbounded/default column '
50+
+ 'shape — either way the accidental old behaviour (empty-only writes under '
51+
+ '`maxLength: 0`, unwritable fields under a negative value) is gone by decision, not '
52+
+ 'by silence.',
53+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5853,6 +5853,55 @@ const step18: MigrationStep = {
58535853
+ '`metadata_spec_invalid` and are refused on their next authoring-path save — re-declare '
58545854
+ 'the field deliberately when that happens.',
58555855
},
5856+
{
5857+
id: 'field-max-length-malformed-or-misplaced-refused',
5858+
surface: 'object field `maxLength` declarations — `maxLength: 0`, negative or non-integer '
5859+
+ 'values on any type, and the key with any value on field types outside '
5860+
+ '`BOUNDED_STRING_FIELD_TYPES` (`boolean`, `lookup`, `autonumber`, `formula`, `select`, '
5861+
+ '`json`, `secret`, …)',
5862+
replacement: 'a positive-integer `maxLength` (>= 1) on a bounded-string field type — `text`, '
5863+
+ '`textarea`, `email`, `url`, `phone`, `password`, `markdown`, `html`, `richtext`, `code` '
5864+
+ '— or no declaration at all. Deleting the key is mechanical and behaviour-preserving '
5865+
+ 'for a MISPLACED declaration: the write-time validator only ever applied `max_length` '
5866+
+ 'inside its bounded-string branch, so the key was inert by construction on every other '
5867+
+ 'type. A MALFORMED value on a bounded-string type is the judgment case — the '
5868+
+ 'validator\'s raw `>` comparison did consume it (`maxLength: 0` accepted only the '
5869+
+ 'empty string, a negative value refused every write, `maxLength: 12.5` behaved as '
5870+
+ '"at most 12"), and the SQL schema-drift planner consumed `maxLength: 0` as '
5871+
+ '`varchar(0)` DDL until #11431 taught it to defend itself — so only the author knows '
5872+
+ 'the bound they MEANT: re-declare it as a positive integer, or delete it deliberately '
5873+
+ 'accepting the unbounding',
5874+
reason:
5875+
'#11566 (maintainer ruling 2026-08-24; enforcement shipped on the 17.x line in PR '
5876+
+ '#11989 — accept-set narrowings ride minors, and this entry tells `migrate meta` users '
5877+
+ 'at the major boundary; registration was deferred to #11950 because the registry file '
5878+
+ 'was serialized behind an in-flight change when the enforcement landed). Shape: a '
5879+
+ 'character length is a positive integer, so the key tightened from `z.number()` to '
5880+
+ '`z.number().int().min(1)` — `maxLength: 0` measurably sent schema-drift planning '
5881+
+ '`varchar(0)` DDL no server accepts, at severity error/destructive, before #11431 '
5882+
+ 'taught that consumer to defend itself (the #8321 `precision`/`scale` house pattern). '
5883+
+ 'Applicability: the key sat on the BASE field schema — authorable on `boolean` / '
5884+
+ '`lookup` / `autonumber`, types where nothing bounded is stored — while the write-time '
5885+
+ 'validator (objectql `record-validator.ts`) only ever enforced it on its ten '
5886+
+ 'bounded-string types, the one list of the three that had a measured reader; that list '
5887+
+ 'is promoted to the protocol as `BOUNDED_STRING_FIELD_TYPES`, the schema refuses the '
5888+
+ 'key outside it (ADR-0078 declared=enforced), and both authoring forms '
5889+
+ '(`field.form.ts`, previously 3 types; `object.form.ts`, previously 9) show the key '
5890+
+ 'for exactly that set.',
5891+
acceptanceCriteria:
5892+
'Every field declaring `maxLength` carries a positive integer and is a bounded-string '
5893+
+ 'type. Well-formed declarations (a positive-integer `maxLength` on a bounded-string '
5894+
+ 'type) parse byte-identically to before; fields declaring no `maxLength` are '
5895+
+ 'untouched, and absence stays absence — no default materializes. Deleting a misplaced '
5896+
+ 'key changes no runtime behaviour (it sat outside the validator\'s bounded-string '
5897+
+ 'branch and enforced nothing). For a malformed value on a bounded-string type the '
5898+
+ 'author decides: re-declare the intended positive-integer bound (enforced by the '
5899+
+ 'write-time validator from the next write on, and honoured by schema drift as '
5900+
+ '`varchar(n)`), or delete the key and accept the type\'s unbounded/default column '
5901+
+ 'shape — either way the accidental old behaviour (empty-only writes under '
5902+
+ '`maxLength: 0`, unwritable fields under a negative value) is gone by decision, not '
5903+
+ 'by silence.',
5904+
},
58565905
{
58575906
id: 'field-min-length-malformed-or-misplaced-refused',
58585907
surface: 'object field `minLength` declarations — `minLength: 0`, negative or non-integer '

0 commit comments

Comments
 (0)