Skip to content

fix(driver-sql): honour a string field's declared maxLength in emitted DDL - #11564

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11431-string-maxlength-varchar
Aug 24, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-11431-string-maxlength-varchar

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #11431

createColumn mapped the string family — string / email / url / phone / password — with a bare table.string(name), so every column took knex's default width of 255 and the field's own maxLength was never read.

Part of, not Fixes: the card enumerates lookup / user / auto_number alongside the five, and this PR deliberately does not change those three. The reasons are measured and below; the remaining question ("should maxLength mean anything on a lookup or an autonumber?") is a spec decision, not a driver one.

Measured, before and after — live MySQL 8.0.46 and Postgres 16

Through the driver's own initObjects, reading information_schema rather than the DDL emitted:

field before after (both dialects)
email({ maxLength: 400 }) varchar(255) varchar(400)
url({ maxLength: 1024 }) varchar(255) varchar(1024)
phone({ maxLength: 20 }) varchar(255) varchar(20)
email() — no bound varchar(255) varchar(255)
url({ maxLength: 100000 }) varchar(255) text
lookup / user / autonumber / secret / select varchar(255) varchar(255)

The write the card reports, a 300-character value into the maxLength: 1024 column:

before  MySQL     ER_DATA_TOO_LONG
        Postgres  22001 value too long for type character varying(255)
after   MySQL     ACCEPTED
        Postgres  ACCEPTED

Drift against the table the driver had just created, with no rows in it — 10 findings before, 5 after, and the 5 that remain are exactly the families this PR leaves alone:

before  widen_varchar  wide_email/wide_url/legacy_string/a_secret/a_select/huge_url  [warning/safe]
        narrow_varchar narrow_phone/a_lookup/a_user/an_autonumber                    [error/DESTRUCTIVE]
after   widen_varchar  a_secret, a_select                                            [warning/safe]
        narrow_varchar a_lookup, a_user, an_autonumber                               [error/destructive]

Narrowing — the fence, and why I read it differently than the dispatch did

The dispatch order fenced this to widening only, and asked to be corrected with a measurement rather than obeyed. Here is the measurement.

createColumn has exactly four call sites, and every one is a CREATE TABLE or an ALTER TABLE ADD COLUMN for a column that does not exist yet (sql-driver.ts 8054, 8063, 8508, 8525). The column it sizes is always empty. rebuildSqliteTablePatched — the one path that re-materialises a populated table — does not call it. So honouring maxLength: 20 at create time cannot truncate anything: there is nothing in the column.

Narrowing an existing populated varchar(255) is a different road, and this PR does not touch it: it is still the narrow_varchar drift op, category destructive, behind os migrate apply --allow-destructive. The differ's narrowing arm is unchanged in behaviour.

The trigger the order named — "would this newly make an existing deployment emit a truncating ALTER" — is therefore not met, in the strong sense: narrow_varchar for a maxLength: 20 field on a varchar(255) column is emitted on main today, identically, before this branch exists. The measurement is in the before-table above. What the change removes is the false half of that: a table the driver itself had just created reporting a destructive narrowing against its own empty columns.

If the fence was meant to hold regardless of that reasoning, the narrowing half is one condition in declaredVarcharLength and can be dropped — but it would leave maxLength: 20 inert and the self-drift in place.

Upper bound

Measured, not read off a doc page:

MySQL utf8mb4   varchar(16383) creates · varchar(16384) -> ERROR 1074 (max = 16383)
Postgres        varchar(10485760) creates · 10485761 -> length for type varchar cannot exceed 10485760
SQLite          records the declared type verbatim, enforces nothing

One ceiling for every dialect, and it is the lowest of the three (16383) — the alternative is one declaration with three physical shapes. Above it the column becomes TEXT rather than being clamped, because a clamp would reinstate the exact defect being fixed: a column narrower than the declaration, refusing writes the declaration permits. TEXT refuses nothing the author declared, and the bound is still enforced at the write seam by the record validator's max_length check — with a field-named ADR-0112 envelope instead of a raw ER_DATA_TOO_LONG.

⚠️ One hazard this cannot see, reported rather than fixed. MySQL also caps the sum of a row's declared byte widths at 65535. Measured: 15 × varchar(1024) creates, 16 × varchar(1024) is ERROR 1118 Row size too large; 64 × varchar(255) (today's shape) creates and 65 does not. Postgres has no such limit (40 × varchar(4096) creates cleanly — it TOASTs). No object in this repo comes near it — the widest string-family bound declared anywhere here is 2000, and no object carries more than six such fields — but an authored app could, and createColumn is per-column so no decision inside it can see the budget. Filed separately rather than solved here, since wiring a diagnostic means wrapping the shared createTable call that sibling cards are on.

The three families left at 255, each for a measured reason

  • lookup / user — the column holds the referenced row's id, not the declared value. MySQL is content with a mismatched FK (varchar(20) child → varchar(255) parent id creates cleanly, and Postgres accepts it too), so the type system gives no warning — but a platform id is 26 characters, so INSERT … VALUES ('01JQ8XKZ9M4N7P2R5T6V8W0Y3B') into varchar(20) is ERROR 1406 Data too long. Honouring the bound here would make the column structurally incapable of holding any id. (My first hypothesis — that the FK itself would be refused — was wrong; measuring it is what found the real and worse failure.)
  • autonumber — the value is runtime-issued. maxLength has no write-time counterpart on this type (the record validator's textual branch does not cover it, and runtime-owned types never reach it), so binding the DDL to it creates a refusal with nothing declaring it.
  • the catch-all — secret persists an opaque sys_secret ref rather than the credential (ADR-0100); select / radio / code / tree store option machine names or ids. None of them stores the string the bound describes.

Why schema-drift.ts is in the diff

Two corrections, both required for this change to be coherent rather than optional cleanups — the emitter and the differ disagreeing about which declarations count is the defect class this card exists to close, and both of these would have re-opened it one case to the left.

  1. A malformed maxLength is no longer read as a bound. maxLength: 0 took the narrowing arm (0 > 255 is false) and planned varchar(0); maxLength: 12.5 planned varchar(12.5) — both at severity error, category destructive, i.e. as work --allow-destructive should go do. The differ now applies the same positive-integer predicate the emitter does.
  2. A MySQL TEXT column is no longer diffed as a varchar 65535 wide. Measured on the same two columns: MySQL reports character_maximum_length = 65535 for TEXT, Postgres reports NULL — so this was MySQL-only and invisible on Postgres. Every bounded unkeyed text field, the shape createColumn deliberately leaves as TEXT, diffed as "declared 4000, column allows 65535" and produced a narrow_varchar at severity error / category destructive against a freshly created empty table. Measured on live MySQL against sys_email's real field shapes:
narrow_varchar to_addresses varchar(65535) -> varchar(4000) [error/destructive]
narrow_varchar subject      varchar(65535) -> varchar(998)  [error/destructive]

Reverse verification

The pins were run against pre-fix source, rebuilt, with the mutation confirmed on disk before the run (an editing tool's exit code is not evidence) and a trap … EXIT INT TERM restoring both files:

declaredVarcharLength in sql-driver.ts : 0 (expect 0)
isCharacterColumn in schema-drift.ts   : 0 (expect 0)
rebuilt dist; declaredVarcharLength in dist: 0 (expect 0)

Test Files  1 failed | 125 passed (126)
     Tests  6 failed | 2515 passed (2521)
   x emits varchar(maxLength) in BOTH directions, and 255 without a declaration
   x falls back to TEXT past the varchar ceiling instead of clamping
   x accepts a write the declaration allows — the whole defect, in one assertion   (live mysql)
   x reports no varchar drift against a table it just created                      (live mysql)
   x accepts a write the declaration allows — the whole defect, in one assertion   (live postgres)
   x reports no varchar drift against a table it just created                      (live postgres)

The 4 pins that stay green pre-fix are the ones asserting unchanged behaviour — malformed bound → 255, the excluded families → 255, and the negative half on both dialects. That negative half is the one that would catch a fix that widened everything to silence the symptom: varchar(20) must really still refuse 300 characters, and it does.

Verification

All on bdf9b6da06, the final commit of this branch.

  • pnpm --filter @objectstack/driver-sql test with OS_TEST_MYSQL_URL and OS_TEST_POSTGRES_URL set — 126 files, 2521 tests passed, 10 of them new.
  • pnpm --filter @objectstack/driver-sql typecheck — clean.
  • 33 repo gates, derived from the actual diff via node scripts/pm/dispatch-gates.mjs (not from the dispatch list, which predated the docs edit) — all pass. check:query-options-erasure red first at "test surface grew 240 -> 241"; the new test's untyped options bag was replaced with a char_length read from the server, which is the fact being pinned anyway.
  • Ratchet families re-run on the final commit after the last edit: 13/13 pass on bdf9b6da06, working tree clean.

Docs

content/docs/protocol/objectql/types.mdx asserted the old shape in three places this diff falsifies — the email mapping bullet, the phone storage sentence, and the Type Conversion Matrix row. Corrected, plus a footnote stating the rule in both directions and the TEXT fallback, and naming the neighbouring rows that deliberately do not follow it.

Fences

⛔ Line 12728's keyed-text branch (col = keyable === null ? table.text(name) : table.string(name, keyable)) is not in this diff — #11374 is unruled and may reshape it. declaredVarcharLength deliberately mirrors keyableTextLength without sharing code with it: the two answer different questions, and a shared helper would couple a settled decision to an unsettled one. toDateOnly, the aggregate lowering region, and introspectForeignKeys are untouched.

Generated by Claude Code


Generated by Claude Code

os-project-manager and others added 2 commits August 24, 2026 02:55
…d DDL

`createColumn` mapped the string family (string/email/url/phone/password) with
a bare `table.string(name)`, so every column took knex's default of 255 and the
field's `maxLength` was never read. Measured through the driver's own
`initObjects` on live MySQL 8.0.46 and Postgres 16: a `maxLength: 400` email, a
`maxLength: 1024` url and a `maxLength: 20` phone all landed as `varchar(255)`,
and a 300-character write the declaration plainly permits was refused by both
(`ER_DATA_TOO_LONG`; `22001 value too long for type character varying(255)`).

The bound now binds in both directions. Narrowing is not a destructive
migration: `createColumn`'s four call sites are all CREATE TABLE / ALTER ADD
COLUMN, so the column it sizes is always empty. Narrowing an existing populated
column stays where it was — the `narrow_varchar` drift op, behind
`os migrate apply --allow-destructive`.

Past the varchar ceiling (measured: MySQL refuses 16384 with ERROR 1074) the
column becomes TEXT rather than being clamped, since clamping would reinstate
the defect. lookup/user, autonumber and the catch-all keep 255 — none of them
stores the declared value.

schema-drift.ts is in the diff because the emitter and the differ must agree on
which declarations count: it now applies the same positive-integer predicate
(`maxLength: 0` planned `varchar(0)` at severity error/destructive), and no
longer reads a MySQL TEXT column as a varchar 65535 wide (measured: MySQL
reports character_maximum_length 65535 for TEXT, Postgres NULL — so every
bounded unkeyed text column reported a permanent destructive narrow_varchar on
MySQL only).

Part of #11431

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
The Type Conversion Matrix, the `email` mapping bullet and the `phone` storage
sentence all asserted VARCHAR(255) for the string family, which this branch's
diff falsifies. Corrected in place, with a footnote naming the rule in both
directions, the TEXT fallback above 16383, and — deliberately — the neighbouring
rows that do NOT follow it (select/radio, lookup/master_detail/tree, autonumber).

Also drops an untyped query-options bag the new test had introduced; the read-back
now asserts `char_length` from the server, which is the fact being pinned anyway.

Part of #11431

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 9 documentable anchor(s).

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

  • content/docs/data-modeling/drivers.mdx (via SqlDriver (symbol))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol))
  • content/docs/plugins/packages.mdx (via SqlDriver (symbol))
  • content/docs/protocol/kernel/index.mdx (via SqlDriver (symbol))
  • content/docs/protocol/kernel/lifecycle.mdx (via SqlDriver (symbol))
  • content/docs/protocol/objectql/query-syntax.mdx (via SqlDriver (symbol))

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

  • content/docs/releases/v17.mdx (via SqlDriver (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
  • 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

Coarse fallback — 9 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 ba8420b58d36077fe79ca8ce0f201bb31f8be2bc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 24d66eda76aab4f201b39456cc16d45e1df66b9f — the merge of head bdf9b6da06e89cdd2c50d0bd3f7c44257177b35c into base ba8420b58d36077fe79ca8ce0f201bb31f8be2bc, 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 24d66eda76aab4f201b39456cc16d45e1df66b9f && git checkout 24d66eda76aab4f201b39456cc16d45e1df66b9f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba8420b58d36077fe79ca8ce0f201bb31f8be2bc bdf9b6da06e89cdd2c50d0bd3f7c44257177b35c && git checkout -B drift-repro ba8420b58d36077fe79ca8ce0f201bb31f8be2bc && git merge --no-ff bdf9b6da06e89cdd2c50d0bd3f7c44257177b35c

node scripts/docs-audit/affected-docs.mjs --json ba8420b58d36077fe79ca8ce0f201bb31f8be2bc

⚠️ 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 ba8420b58d36077fe79ca8ce0f201bb31f8be2bc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 24, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 24, 2026 03:48
@os-zhuang
os-zhuang enabled auto-merge August 24, 2026 03:49
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit c49afd0 Aug 24, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11431-string-maxlength-varchar branch August 24, 2026 04:10
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ui#11466, objectstack-ai#11574, objectstack-ai#11578, objectstack-ai#11581 and objectstack-ai#11583) (objectstack-ai#21710)

Fixes objectstack-ai#21696
Clause-②: no

This moves the bundled Console's objectui pin from `ab1879721595`
(objectstack-ai#21625) to objectui `main` as of the write time,
`2e818d0b51ecdf8fdd9fcbf4b916bcd7fe9a9cf6`, which is past `973fc20f3`.
The Console now carries objectui#11574 (the phantom "(empty)" groups
fix), objectui#11578, objectui#11581, objectui#11576, objectui#11583 and
objectui#11466 (V1 declared node slot). It also carries objectui's own
17.7.0 package release.

⛔ This is not a release act. Version PR objectstack-ai#21352 is untouched, and no tag,
publish or Release was made. Whether 17.7.0 ships with this pin is the
maintainer's call.

## Range

`ab1879721595..2e818d0b51ec` has 22 non-merge commits. `git merge-base
--is-ancestor` exits 0 against objectui `origin/main` for `973fc20f3`,
and `2e818d0b51ec` was objectui `main` when it was read (`git ls-remote`
just before this PR).

From the changesets objectui declared over the range:
- 17 releasing changesets, 1 release-nothing, and 6 commits that carry
no changeset.
- 12 of the 18 changesets were already consumed by objectui's release
`b493919c7`. Each was read from the commit that added it.
- 2 breaking entries, both annotated by the author and both from one
commit.

The range was listed by measurement, not copied from the card. Exactly
one commit subject in the range carries `!`:

`git log --format='%h %s' ab187972..2e818d0b5 | grep -E '^[0-9a-f]+
[a-z]+(\([^)]*\))?!:'` gives `83e3f8377
feat(types,react,core,plugin-dashboard)!: … (objectui#11466)`.

| objectui landing | commit | note |
|---|---|---|
| objectui#11466: node slot and `SchemaRenderer`'s `schema` take
`DeclaredNode`; the dashboard producers name declared nodes; an
envelope's object-metric retires | `83e3f8377` | **declared breaking**,
both breaking entries |
| objectui#11574: a grouping-field switch paints only the server's
groups | `2f54dca53` | smoke below |
| objectui#11581 / objectstack-ai#11578 / objectstack-ai#11576: Create View doors share one builder;
a refused save is said; chart views bind an ADR-0021 dataset |
`b65aa5e65`, `f1c937966`, `d0097af2e` | |
| objectui#11583: a refused metadata write on the view, report and draft
surfaces is said | `e8c0b9614` | past the card's `973fc20f3` |
| objectui#11598: dashboard slot-entry and chart-producer reads compile
without `BaseSchema`'s index signature | `2e818d0b5` (the pin itself) |
|
| objectui#11572 / objectstack-ai#10380: interface and object page relay stored view
config | `09036173c`, `068564691` | |
| objectui#11588: grid summary footer reads currency/precision from the
object field only | `d768c3178` | |
| objectui#11216: `object-kanban` declares `grouping` as the spec's
`GroupingConfig` | `a7557a7d4` | |
| objectui#11573, objectstack-ai#11564: `AnyComponentSchema` / flex child typing |
`bdc9049ed`, `b10c68e58` | |
| objectui release 17.7.0 | `b493919c7` | removes 2726 consumed
`.changeset/*.md` files |

## ADR-0087 disposition of the declared-breaking entries

Both entries are `not-required (no-migration-prescription)`. The
disposition is in `.changeset/console-2e818d0b51ec.md`, which replaces
the script's `adr-0087: TODO` placeholder.

| objectui PR | commit | entry | disposition |
|---|---|---|---|
| objectui#11466 (PR objectui#11512) | `83e3f8377` | A node slot and
`SchemaRenderer`'s `schema` prop take `DeclaredNode` | not-required
(no-migration-prescription). This is objectui's own TypeScript face
(`@object-ui/types` / `@object-ui/react`). No ObjectStack schema
declares or references it. |
| objectui#11466 (PR objectui#11512) | `83e3f8377` | An `object-metric`
node in a dashboard widget's legacy `component` envelope draws the
retired-format prompt | not-required (no-migration-prescription). An
ObjectStack author cannot write that envelope: `ui/dashboard.zod.ts`
refuses a widget's `component` key by name as objectui-internal, and no
example authors one. The showcase's `object-metric` page tiles still
draw their numbers (smoke below). |

`check-adr-0087-registration --base origin/main` gives "1
declared-breaking changeset(s), each carrying an ADR-0087 disposition".
`check-changeset-no-major --base origin/main` gives "This diff
introduces no `major` bump".

## What changed here

- **`.objectui-sha` and `.changeset/console-2e818d0b51ec.md`** were
written by `scripts/bump-objectui.sh
2e818d0b51ecdf8fdd9fcbf4b916bcd7fe9a9cf6 --no-commit` with
`OBJECTUI_ROOT` set to a read-only clone of objectui. The level was
auto-set to `minor`. No `../objectui` sibling exists in this container.
- **`scripts/sdui-manifest.record.json`** was re-recorded by `node
scripts/gen-sdui-manifest-node.mjs` over the tree that `pnpm
objectui:build` built at the pin. **`sdui.manifest.json` is
byte-identical**: 107 components, sha256 `0ead67c1111d…` at both pins,
so no component input moved. The record moves its pin, its `modulesRoot`
and objectui's workspace version (17.6.0 to 17.7.0).
- **`packages/sdui-parser/objectui-lockstep.json`** was re-recorded with
`pnpm gen:sdui-lockstep` against
`OBJECTUI_ROOT=.cache/objectui-2e818d0b51ec`. It records 214 grammar
lines (blob `0131f27cf86d`), 25 codes and containment predicate
`76c18fb95d1f`. All are unchanged, so no port is owed. objectui's
`packages/sdui-parser/src` is byte-identical across the range; only its
CHANGELOG and package.json moved.
- **The 54 asserting pin citations in `packages/spec/src`** were
re-measured at the new pin, not restamped:
- Every anchor in each asserting record was resolved against objectui's
tree at the old pin and mapped through `git diff -U0 ab187972
2e818d0b5`.
  - Four cited files changed:
- `plugin-dashboard/src/index.tsx` (+13/-1, objectui#11466). Lines were
added above the `object-metric` registration, so its start moves `244`
to `256` and the icon input moves `269` to `281`. Both lines are
byte-identical, still `{ name: 'icon', type: 'string' }`.
- `packages/types/src/objectql.ts`. objectui#11216's `grouping` member
landed below the cited `limit` member, so `:4720` did not move.
- `content/docs/plugins/plugin-kanban.mdx`. A `grouping` row landed
below the `limit` row, and the page still teaches `limit: 250`.
- `packages/react/src/SchemaRenderer.tsx` (+21/-20, objectui#11466).
Only the type import, the `SchemaRendererProps` docblock and the
`schema` type changed. The `properties.*` hoist, its strip list and the
`createElement` spread did not, so `object-timeline`'s prop channel
still carries `data`.
  - Every other cited file is byte-identical across the hop.
- Each record gains a dated 2026-10-04 hop sentence and keeps its
`ab1879721` history.
- **The six migration entries' corpus counts** were re-taken with `git
grep -o -F`. That method first reproduced every `ab1879721` number:
10267 files, `objectstack` 16377, `@objectstack/spec` 6665, `timeout`
1348, `useState` 2476, `TTL` 180, `tenant` 1238, `Span` 491.
- The new numbers are 7579 files, 17227, 7134, 1351, 2477, 182, 1317 and
505. The file count drops because objectui's 17.7.0 release removed 2726
consumed changesets.
- Every zero is still zero: 98 tokens were checked, the export lists of
the three cited spec files plus every named key. The only non-zero
tokens are the expected `Span` / `SpanSchema`. The 14 new `Span` hits
are `Spanish` prose.
- `packages/spec/src/migrations/registry.ts` was regenerated with
`gen:migration-registry`.
- **`.changeset/objectui-pin-citations-2e818d0b51ec.md`** is a
`@objectstack/spec` patch, because the `FormField.span` describe and six
migration descriptions name the pin.
`content/docs/references/ui/view.mdx` was regenerated by
`check:generated --fix`.
- **`main` was merged** (`7e0066af7a`) with
`scripts/pm/os-regen-merge.sh` in merge commit `a0a35cb30f`, with no
conflict hunk. Afterwards:
  - `check:generated` reports all 15 artifacts current.
- main's three new semantic entries
(`ui-object-form-fields-names-typed`, `ui-object-gantt-markers-typed`,
`ui-object-timeline-mapping-typed`) are present in the regenerated
registry.
- The pin-citation gate still counts 54 asserting citations. main added
historical ones only (108 to 118).

No example, test or gate needed adapting, and no code changed outside
generated records, citations and changesets.

## Browser smoke: `examples/app-showcase` with the Console built at
`2e818d0b51ec`

The Console was built by `pnpm objectui:build`. The build log reports:
"Console dist matches the objectui pin (objectui@2e818d0b51ec)",
"Single-zod canary: exactly one zod version literal
{major:4,minor:6,patch:5}" and "Console bundle carries THIS tree's
@objectstack/spec, and only it".

The server ran `pnpm dev -- --fresh --ui --no-watch -p 41853` (with
`OS_PORT=41853`) on its own ephemeral DB with the seeded admin. Headless
Chromium (`/opt/pw-browsers/chromium`) drove it, with console messages,
page errors and every non-2xx response captured. Only the PIDs this run
started were stopped.

**Dashboards.** objectui#11466 touches the dashboard producers. There
are no page errors and no "retired" prompt on any of the four
dashboards.

| dashboard | metric tiles (value read in the DOM) | charts drawn |
console errors |
|---|---|---|---|
| Delivery Operations `showcase_ops_dashboard` | Active Projects 2,
At-Risk (Red) 1, Awaiting Review 2, Total Budget 1,090,000 | 4 | 404
`/api/v1/usage/storage`, 404 `/favicon.ico` |
| Revenue Pulse `showcase_revenue_pulse` | Invoices 7, Invoiced Subtotal
1,920, Accounts Signed 3, Accounts (all time) 14, Paid Rate 42.9% | 4 |
404 `/api/v1/usage/storage` |
| Chart Gallery `showcase_chart_gallery` | Total Tasks 10, Avg Progress
47.0, Total Spent 616,000 | 13 | 404 `/api/v1/usage/storage` |
| System Overview `system_overview` (setup) | Total Users 3, Active
Sessions 3, Login Events 3, Config Changes 0 | 2 | 404
`/api/v1/usage/storage` |

The Delivery Operations tiles match the REST data on the same server:
- `showcase_project?status=active` returns 2 rows, `?health=red` returns
1, and the budget sum is 1,090,000.
- `showcase_task?status=in_review` returns 2 rows.
- `showcase_task` has 10 rows, matching the Chart Gallery's Total Tasks.
- `showcase_account` has 14 rows, matching Revenue Pulse's all-time
count.

The `object-metric` page tiles that this repo authors also draw:
- My Work shows Open Tasks 8, In Review 2 and At-Risk Projects 1.
- Command Center (大屏) shows 2 / 8 / 2 / 1 / 14 / 1.1M.

Neither page has a page error.

**Grouping-field switch (objectui#11574).** The drive was Tasks list,
then Group, then Add group field (it selects Title). The select then
switched to Priority, Status and Priority. The painted groups are
counted from the DOM, and the server answers are each `POST
/api/v1/data/showcase_task/query` with `groupBy`, all 200:

| step | painted groups | server rows | "(empty)" groups | stuck
"Loading grid…" |
|---|---|---|---|---|
| Title | 10 (one per task) | 10 | 0 | 0 |
| Priority | 4: High 3, Low 1, Medium 4, Urgent 2 | 4 | 0 | 0 |
| Status | 5: Backlog 2, Done 2, In Progress 2, In Review 2, To Do 2 | 5
| 0 | 0 |
| Priority | 4: High 3, Low 1, Medium 4, Urgent 2 | 4 | 0 | 0 |

The same sequence painted 13, 8 and 8 groups (9, 3 and 4 phantom) at
`ab1879721595`, as objectstack-ai#21625's acceptance notes recorded. **No phantom
"(empty)" groups appear at this pin.**

**Console errors across the run.** The only ones are the browser's
"Failed to load resource: 404" lines for `/api/v1/usage/storage` and
`/favicon.ico`, and the run has 0 page errors. The `usage/storage` 404
was already recorded in objectstack-ai#21625's smoke.

## objectstack-ai#21671 lockstep reading

- objectui's `packages/sdui-parser/src/validate.ts` at `2e818d0b5` is
byte-identical to `ab1879721595`.
- `checkType` (`:450-466`) still grades `type-mismatch` with `severity:
arms.includes('enum') ? 'error' : 'warning'`. `checkMemberTypes`
(`:407-419`) grades `member-type-mismatch` the same way.
- This repo grades both `error` since PR objectstack-ai#21678
(`packages/sdui-parser/src/validate.ts:446`, `:506`). **The gap still
stands at this pin.**
- This repo's "Known lead" note
(`packages/sdui-parser/src/validate.ts:336-340`) names objectui's copy
"at the `.objectui-sha` pin" without a sha, so it is still true and
needs no update. The bump route regenerates nothing there, and
`validate.ts` is not edited.
- `check:sdui-lockstep` does not compare severity, so this is a reading,
not a gate result.

## Gates and tests (head `a0a35cb30f`)

- `node scripts/pm/dispatch-gates.mjs --commands` derived 127 commands
from the 21-path diff. That is the same set before and after the merge.
- At `378d424915` (pre-merge) and again at `a0a35cb30f`, all 127 exited
0. `--ran` reports "127 derived, 127 run, 0 NOT-MEASURED, 0 UNRUN", with
every exit code recorded.
- Also exit 0:
- `check:objectui-pin-citations --verify-anchors` with objectui at the
pin: 54 asserting citations match `2e818d0b5`, and 7 content assertions
are verified.
- `check:objectui-bump` (20 assertions across 5 cases),
`check:sdui-lockstep`, `check-sdui-manifest`, `check:console-sha`,
`check:console-injection`, `check:migration-registry`.
- `@objectstack/spec check:generated`: all 15 artifacts current after
the merge.
- `pnpm --filter @objectstack/spec test`: 612 files, 18185 passed, 1
todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/sdui-parser test`: 225 passed.
- `eslint --no-inline-config` on the 15 changed TS files: 0 errors and 0
warnings. The lint population is `**/*.{ts,…}`
(`eslint.config.mjs:971`). The config enables no type-aware linting
(`:328`), so this diff cannot move a judgment on an untouched file.
Repo-wide `pnpm lint` is left to CI.

## Acceptance notes

- `ui/component.zod.ts`'s `object-grid` `columns` record (the ⚠️
paragraph on `plugin-grid/src/useColumnSummary.ts:558-568`) says, about
`ab1879721595`, that the footer summary reads `currency` /
`defaultCurrency` / `precision` / `scale` off an authored column.
objectui#11588 (`d768c3178`, inside this pin) retires that read. The
record is in the historical spelling and stays true of the pin it names,
so it is left as is.
- Two anchors in records that cite no sha are outside the pin-citation
gate's population and were not re-measured:
- `ObjectView.tsx:882`, in `ui/view.zod.ts`'s [objectstack-ai#5074] block and two view
tests.
- `plugin-dashboard/src/index.tsx:204`, in the `ObjectMetricPropsSchema
icon liveness` test.
- Both already read an unrelated line at `ab1879721595`, and they shift
to `:936` and `:214` here.
- Writes: no PR assignee or label was written. This dispatch's write
budget names neither.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants