Uh oh!
There was an error while loading. Please reload this page.
feat(platform-objects): declare a sourced maxLength on sys_import_job.created_by (#11374 route A, last column) - #12058
Conversation
….created_by (#11374 route A, last column) Route A's remaining column. `sys_import_job.created_by` is keyed by `(created_by, created_at)` and declared no bound, so driver-sql emitted it TEXT and MySQL refused the index (`ER_BLOB_KEY_WITHOUT_LENGTH`), landing the object registered-but-broken. Per #11699's own measurement it was the only remaining such object outside the >768-character class. The bound is 255, derived by referenced-column transitivity from three converging in-repo producers rather than chosen: the column holds a `sys_user.id` (stamped from `context.userId` by the rest-server import route), and driver-sql creates every primary key as `table.string('id').primary()` = knex's varchar(255); a sibling declared as `Field.lookup('sys_user')` emits `DEFAULT_STRING_VARCHAR_CHARS` = 255; and the landed text declarations for the same value class (sys_metadata_audit.actor, sys_metadata_commit.actor, sys_view_definition.owner) are all 255. A minted platform id is 26 characters, so the floor is cleared with 229 characters of headroom, and 255 is within the 768-character utf8mb4 key ceiling so this stays out of the hash-shadow class. The route-A pin moves out of `identity/` and now enumerates every platform object the package exports. That directory scoping is exactly how this column escaped the first pass, so a new control asserts the enumeration reaches `audit/`, `metadata/` and `system/` columns by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
📓 Docs Drift CheckThis PR changes 1 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 19d01fbfdeb5daedb20115f1bfde2bf320b147a2 && git checkout 19d01fbfdeb5daedb20115f1bfde2bf320b147a2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3def551026963aed4c333b80fa5d82190ba35893 59897f5e49abffe5e7eef48eed16ef11ef4e5a68 && git checkout -B drift-repro 3def551026963aed4c333b80fa5d82190ba35893 && git merge --no-ff 59897f5e49abffe5e7eef48eed16ef11ef4e5a68
node scripts/docs-audit/affected-docs.mjs --json 3def551026963aed4c333b80fa5d82190ba35893
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #11374 — route A's last column.
Not a closing keyword by choice: #11374 also carries the C-half remainder (the
The defect
sys_import_job.created_byis keyed by(created_by, created_at)and declaredno
maxLength, sodriver-sqlemitted itTEXTand MySQL refused the index.Per #11699's own measurement it was the only remaining
ER_BLOB_KEY_WITHOUT_LENGTHobject outside the >768 class. Measured live, notrecalled — the driver's own message from the BEFORE leg:
The bound and where it comes from — the veto surface
Route A's method is that every bound is derived from a named producer and
stated so a reviewer can veto the row. One column, one row:
sys_import_job.created_byString(context?.userId ?? context?.user?.id ?? '')(packages/rest/src/rest-server.ts), i.e. asys_user.id.driver-sqlcreates every table's primary key astable.string('id').primary()— knex'svarchar(255)— so no id this column can ever receive exceeds 255. Confirmed physically in the same run:sys_import_job.idreads backvarchar(255)frominformation_schema.Two independent in-repo corroborations, both already landed, neither used as the
primary source:
actor column on a platform object is
Field.lookup('sys_user', …), whichdriver-sqlemits atDEFAULT_STRING_VARCHAR_CHARS= 255.sys_metadata_audit.actor("Acting principal — user id, system id, or'system'"),
sys_metadata_commit.actorandsys_view_definition.owneralldeclare
maxLength: 255.Floor cleared with headroom. A minted platform id is 26 characters (measured
on #11431, where honouring a bound below that made a column structurally unable
to hold any id at all). 255 leaves 229 characters of headroom.
Ceiling respected. 255 is well inside the 768-character utf8mb4 key ceiling
(
MAX_KEYABLE_VARCHAR_CHARS), so this stays out of the C class rather thanquietly merging the two ruled halves.
Measured on live servers — physical read-back, never the emitted DDL
MySQL 8.0.46 (
@@global.time_zone = '+08:00',STRICT_TRANS_TABLES, utf8mb4)and PostgreSQL 16.13, process
TZ=America/New_York. All 44 distinct exportedplatform objects through
syncSchema, both legs at the same tree apart from theone declaration.
syncSchemafailuresER_BLOB_KEY_WITHOUT_LENGTH+ 1ER_TOO_LONG_KEY)sys_import_jobitselfER_BLOB_KEY_WITHOUT_LENGTHcreated_byphysical columntext,CHARACTER_MAXIMUM_LENGTH65535varchar(255)idx_sys_import_job_created_by_created_atColumn shapes are read from
information_schema.COLUMNSand the index list frominformation_schema.STATISTICS— the physical catalog, not the DDL the driveremitted.
The accept/reject flip
On the strict MySQL server, against the physical column:
ER_DATA_TOO_LONG, rows 0ER_DATA_TOO_LONG, rows 0Refused, not truncated — which is why the changeset grades this
minor(behaviour-narrowing on a published object), matching #11699's tier. No value
the producing contract can emit is affected: the id being copied is itself
capped at 255 by its own column.
The pin: why this column escaped, and the fix for the class
audit/. It lived atidentity/identity-keyed-text-bounds.test.tsandimported
./indexfromidentity/, so its enumeration could only ever seeidentity objects. That is exactly how a keyed unbounded column in
audit/survived a pin whose stated job is to fail by name on keyed unbounded columns —
it policed a directory, not the defect class.
Extended rather than patched with a one-off assertion: the file moves to
packages/platform-objects/src/platform-keyed-text-bounds.test.tsandenumerates every object the package exports. Measured effect on the
enumeration:
sys_verification.value(already allowlisted, better-auth JSON blob) andsys_import_job.created_by(this PR). The widening drags in no new debt.A new vacuity control asserts the enumeration reaches
sys_import_job.created_by(audit/),sys_metadata.name(metadata/) andsys_setting.key(system/) — three different source directories — so a futurere-narrowing of the import fails by name instead of quietly enumerating less.
The
UNBOUNDABLEallowlist and its anti-rot test are unchanged.Ablation — mutation proven on disk before any result was read
Direction predicted in advance in both legs; every prediction held.
Leg A (pin discrimination, source level). Removed only
maxLength: 255,from the
created_byblock. Anchor was proven unique before writing(
ANCHOR HITS: 1) and the mutation proven on disk after(
created_bybound-block count 1 → 0;git diff --stat= 1 deletion). Greenbaseline 4 passed → red
Tests 1 failed | 3 passed (4), the failure namingexactly:
The red appeared with no build step between mutation and run, which is the
proof the suite read the mutated source rather than a stale artifact.
Leg B (physical level, the BEFORE measurement above). Same mutation, then
pnpm --filter @objectstack/platform-objects build, thenablation-dist-preflight.mjs … --absentconfirmed the bound was gone from all66 built files — only then was the probe's result read.
Restore verified, not trusted. Both legs restored from a
trap … EXIT INT TERMusinggit checkout HEAD --with an absolute path(never
git checkout -- FILE, which restores from the index the mutation stepitself wrote). Verified after each leg:
git hash-objectof the working fileequals
git rev-parse HEAD:path(75930e4af21ade22f4b21f0996b4265ce14a781c),and
git status --porcelainempty. The restore leg was then rebuilt andre-proven with the preflight in present mode — marker back in 4 built files,
so no mutated artifact is left behind to poison later runs in this worktree.
Gates
Union derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackwith nopaths passed (the script derives its own change set from the merge base). Its
stderr stamped the answer to this repo's tree. Every family run against the
final commit
59897f5e49, exit captured before any pipe.13 path-matched +
check:nul-bytes, all0:check:nul-bytes,check:changeset-gate-self-tests,check:objectui-changeset,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration.mjs,check-changeset-no-major.mjs,check-empty-changeset.mjs,check-plugin-teardown-shape.mjs,docs-audit/check-affected-docs.mjs,docs-audit/check-drift-comment.mjs,release-rehearsal-clone.mjs --self-test.Convention-triggered, all
0:check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:i18n.Suites and ratchets, at
59897f5e49:pnpm --filter @objectstack/platform-objects test→Test Files 30 passed (30) / Tests 475 passed (475)pnpm --filter @objectstack/platform-objects typecheck→ exit 0, script name echoed (tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json), so not a zero-matchpnpm lint(full repo,eslint . --no-inline-config) → exit 0 in 2m23s. A full run; no narrowing claimed.pnpm check:type-check-debt --re-measure, its own verdict line:check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 360.3s, 1898 raw tsc error(s) total, none above its recorded number.check:i18nfirst run exited 1 as a measured refusal (PREREQUISITE NOT MET — the workspace CLI is not built; nothing was checked), then green after building the CLI:check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).Out of scope, deliberately
sys_verification.value— better-auth stores OIDC authorization-code payloadsthere as a JSON blob; no bound provably admits every value. Stays allowlisted
by name with its reason (driver-sql (MySQL): full-value UNIQUE on >768-char token columns is inexpressible on utf8mb4 — hash-shadow-key route for the four ruled cases (C half of the #11374 ruling) #11627).
sys_account.issuerand the(issuer, account_id)composite — 2048 exceedsthe 768 key ceiling; that is the C class (driver-sql (MySQL): full-value UNIQUE on >768-char token columns is inexpressible on utf8mb4 — hash-shadow-key route for the four ruled cases (C half of the #11374 ruling) #11627 / #11627 population re-measured after the A half: three additional members of the >768/unboundable keyed-text class (resource_id 1024, sys_verification [value], sys_account [issuer+account_id]) #11701).
Found while doing this, filed separately, NOT fixed here
The same defect class exists in sibling packages my pin cannot reach: four
keyed, unbounded
Field.textcolumns inplugin-auditandplugin-security(
sys_activity.record_id,sys_audit_log.record_id,sys_audience_binding_suggestion.package_id/.permission_set_name). Filedunassigned rather than fixed — out of this card's declared surface.
Generated by Claude Code