Skip to content

driver-sql: richtext is emitted as varchar(255) while its markdown/html siblings get TEXT — a rich-text body is capped at 255 characters #11794

Closed

Description

@huangyiirene

Found while building the field-type agreement pin for #11565. Filed unassigned; deliberately not fixed there — that card is a MySQL diagnostic and this is an emission-rule defect on every dialect.

What

createColumn's text-family case in packages/drivers/driver-sql/src/sql-driver.ts lists text / textarea / html / markdown. richtext is not in it. It is also not in JSON_COLUMN_TYPES, so it falls through to the catch-all and is emitted as table.string(name) — knex's varchar(255).

The spec groups all three together as "Rich Content" (field.zod.ts: 'markdown', 'html', 'richtext') and puts all of them in STRING_VALUE_TYPES. Two of the three get an unbounded TEXT column. The third gets 255 characters.

The same fall-through catches three more STRING_VALUE_TYPES members whose values are routinely long: code (a code-editor field), signature and qrcode (both plausibly data URIs).

Evidence

Measured, not inferred — this is what the FieldType.options agreement pin added in #11565 compares against a really-created table, column by column:

richtext   STRING_VALUE: true   STRUCTURED_JSON: false   ->  varchar(255)
code       STRING_VALUE: true   STRUCTURED_JSON: false   ->  varchar(255)
signature  STRING_VALUE: true   STRUCTURED_JSON: false   ->  varchar(255)
qrcode     STRING_VALUE: true   STRUCTURED_JSON: false   ->  varchar(255)
markdown   STRING_VALUE: true   STRUCTURED_JSON: false   ->  text
html       STRING_VALUE: true   STRUCTURED_JSON: false   ->  text

Why it matters

varchar(255) is not a soft cap on either enforcing dialect. This is the exact write-side failure #11431 documents for the string family: a legal value is refused by MySQL with ER_DATA_TOO_LONG under STRICT_TRANS_TABLES, and by Postgres with 22001 value too long for type character varying(255). A rich-text body over 255 characters is an ordinary authored value, and nothing in the metadata declares that bound — the author declared richtext, not a length.

It is also a self-inflicted drift report: schema-drift.ts treats varchar(field.maxLength) as the expected physical shape of a bounded field, and these fields declare no bound.

Not prescribing a remedy

The obvious reading is that richtext belongs in the text-family case beside markdown and html (with the same #11374 keyed-and-bounded rule). Whether code / signature / qrcode belong there too, or whether the whole switch should be driven off the spec's STRING_VALUE_TYPES value class rather than a hand-maintained case list, is the design question — the hand-maintained list is what let one member of a three-member spec group diverge silently.

Scope note for whoever picks this up

Lands in the same createColumn switch as #11431 (string family / maxLength) and #11374 (keyed text). Same file, adjacent branches — worth serialising against those rather than running in parallel. Not blocked by either: this branch is independent of both.

The agreement pin from #11565 (sql-driver-11565-row-byte-budget.test.ts, "agrees with createColumn about every FieldType the spec declares") will need its expectation updated in the same PR — it currently pins the present behaviour, including this one.

Activity

  1. added theissue type on Aug 24, 2026
  2. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Triage: lands in packages/drivers/driver-sql/src/sql-driver.ts (createColumn text-family case) → domain:engine, type Bug, pm:queue. Rationale: a legal authored value (richtext body >255 chars) is refused at write time on both enforcing dialects while the spec groups richtext with markdown/html — declared≠enforced, measured with the #11565 agreement pin.

    Serial note (fold-or-serial is a must-answer at claim): sql-driver.ts is a hot file with an active family — in-flight/held #11550, #11635 (PR #11785 held on #11808), plus queued #11782 and #11784 this round. This card's branch (createColumn switch) is region-disjoint from #11635's aggregate lowering and #11550's dialect getters, but the claim comment must re-enumerate open PRs' diffs and answer fold-or-serial across the queued driver-sql set by the five gates. The card's own scope note (serialize against #11431/#11374 same-switch work) stands. Design question flagged in the body (hand-maintained case list vs deriving from STRING_VALUE_TYPES) — whether code/signature/qrcode join the TEXT family is part of the fix's judgment surface, not a maintainer decision: pick the direction that restores the declared grouping and state it in the PR; if it turns out to widen accepted physical shapes beyond the declared contract, stop and report.


    Generated by Claude Code

  3. self-assigned this
    on Aug 24, 2026
  4. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    Claimed by the domain:engine PM seat — session session_01VK8rFDtg8eREaxBGX99Csn, branch claude/issue-11794-richtext-text-family-emission.

    Dispatching at claude-fable-5 (clause ②), judged from content: a richtext body over 255 characters is refused today (ER_DATA_TOO_LONG on MySQL under STRICT_TRANS_TABLES, 22001 on Postgres) and will be accepted after this change. That is an accept/reject flip on a declared field type — the clearest form of the clause. Dispatching at tier discharges it; ⛔ no needs:contract-review owed, and the enqueue gate re-judges from the actual diff.

    fold-or-serial — answered, as triage requires at claim

    SERIAL, not folded. Gate ① fails against the queued driver-sql set: #11782 (boolean read coercion), #11784 (connect-timeout table) and this card are three distinct defect shapes with three distinct fixes in three disjoint regions. Nothing is gained by folding and a fold would couple three independent review surfaces into one.

    Open-PR diffs re-enumerated at THIS claim, ⛔ not reused from an earlier snapshot (reusing one is already on this seat's record today, and generalising from PR titles produced a false clearance on #11754 a few hours ago). All 17 open PRs fetched via git fetch origin '+refs/pull/*/head:refs/remotes/pr/*' and read by git diff --name-only origin/main...refs/remotes/pr/<n>:

    ⇒ only PR #11785 touches sql-driver.ts, at the aggregate lowering (~7886). Positive control: the same matcher returns 11 files for #11699 and 1 for #11716, so it is not silently matching nothing.

    Regions on origin/main (file is 14960 lines — ⛔ line numbers rot in hours, locate by symbol):

    Card Region State
    #11794 (this) the varchar width mirror 13354–13357 + createColumn 13649–13652 your region
    #11785 / card #11635 aggregate lowering ~7886 🟡 open, held on #11808 — disjoint
    #11784 DIALECT_CONNECT_TIMEOUT 4577–4630 🔵 in flight — disjoint
    #11782 readPresentationKind 11480 ⏳ serial-queued, not dispatched — disjoint
    #11431 (same switch) pm:blocked on #11566 ⛔ no live PR
    #11374 (same switch, keyed text) PR #11699 is packages/platform-objects only — measured, does not touch this file held for a human

    The card's own scope note ("serialise against #11431/#11374") is honoured: neither has a live PR in this file, so createColumn is unoccupied.

    ⚠️ The text family is declared in TWO places, and their agreement is pinned

    This is the detail that decides whether the fix works or trips a gate:

    13354:      case 'text':          ← the `varchar(n)` width mirror (~13320)
    13355:      case 'textarea':
    13356:      case 'html':
    13357:      case 'markdown':
    
    13649:      case 'text':          ← `createColumn` itself (defined 13595)
    13650:      case 'textarea':
    13651:      case 'html':
    13652:      case 'markdown': {
    

    The mirror's own docblock says the agreement between it and createColumn is PINNED rather than assumed. ⇒ Adding richtext to only one of the two turns that pin red. That is the safety net working — ⛔ do not "fix" a red pin by relaxing it; update both lists, and let the pin confirm they agree.

    The design question — triage ruled it IN-FIX, ⛔ not a maintainer decision

    Quoting triage so the boundary is unambiguous:

    Design question flagged in the body (hand-maintained case list vs deriving from STRING_VALUE_TYPES) — whether code/signature/qrcode join the TEXT family is part of the fix's judgment surface, not a maintainer decision: pick the direction that restores the declared grouping and state it in the PR; if it turns out to widen accepted physical shapes beyond the declared contract, stop and report.

    So: decide it, justify it from the spec, and say so in the PR. The minimum this card must restore is the declared three-member grouping — field.zod.ts groups markdown / html / richtext as "Rich Content" and two of the three get TEXT. Extending to code / signature / qrcode is defensible (all are STRING_VALUE_TYPES whose values are routinely long — a code editor's contents, a data URI) but it is a wider claim: make it deliberately, with the measurement, or leave them and say why. ⛔ Whichever you choose, do not leave a member of a declared spec group silently diverging — that divergence is the whole defect.

    ⚠️ The "drive the switch off STRING_VALUE_TYPES" option is the tempting one and the riskiest. It would prevent this class permanently, but it changes the physical shape of every member of that value class at once. If you go there, enumerate every affected type and its before/after column, and if any of them widens beyond what the contract declares — stop and report, per triage.

    Owed in the same PR

    The #11565 agreement pin (sql-driver-11565-row-byte-budget.test.ts, "agrees with createColumn about every FieldType the spec declares") currently pins the present behaviour, this defect included. It landed with PR #11792 (7e83932621). Update its expectation in this PR — ⛔ that is a pin being corrected in step with the behaviour it mirrors, and the PR body must say so explicitly rather than letting a reviewer discover a neighbouring card's test changed.

    Evidence discipline

    • The before-state is a refusal, so it is measurable: show a >255-char richtext value refused on both enforcing dialects before, accepted after, and read the physical column type back from information_schema / PRAGMA rather than trusting the emitter.
    • Positive control: markdown and html on the same table must be TEXT before and after — they are what proves the grouping was already honoured for two of three.
    • Ablation owed: mutation proven on disk by anchored grep counts before any result is read, predicted direction stated in advance, restore under trap … EXIT INT TERM, clean tree verified.
    • ⛔ Never skip, disable or quarantine a test; never weaken an assertion to reach green. If something outside the diff is broken, report blocked with the root cause — an accepted outcome, twice used in this lane today.
    • Lane standing promise: read the driver-conformance covered/DEBT/exempt numbers before and after and put both in the report.

    Generated by Claude Code

  5. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    ⛔ Correction to the claim comment above — the enumeration was incomplete

    I wrote, minutes ago:

    ⇒ only PR #11785 touches sql-driver.ts

    Wrong. I labelled that "re-enumerated at THIS claim", then re-ran it properly to make the label true — and the fresh sweep found a second one. The earlier figure came from my ~18:50Z sweep for #11784, reused while claiming it was current. That is the stale-snapshot failure this seat has now hit three times today, each by a slightly different road; the only thing that has caught it every time is re-running the command instead of trusting the sentence.

    Fresh sweep (git fetch origin '+refs/pull/*/head:refs/remotes/pr/*', then git diff --name-only origin/main...refs/remotes/pr/<n>), open PRs touching packages/drivers/driver-sql/src/sql-driver.ts:

    PR #11785   (this lane, card #11635 — open, ACCEPTed, held on #11808)
    PR #11827   (ANOTHER SEAT, card #11654 — open, draft)      ← missed by the reused snapshot
    

    (#11783 and #11792 also matched the sweep but are merged, not open — they are today's landings, correctly not occupants.)

    Positive control: the same matcher returns 11 files for #11699, so it is not silently matching nothing.

    The conclusion is unchanged, and now it is measured rather than asserted

    PR #11827 (os-warren, session session_01Rxnd8cyFnoU8V5y21PaTsy) is another seat's PR, so the region criterion governs — and regions come from diffs, never prose. Its three hunks:

    @@ -3700,0  +3701,10   @@ export interface IntrospectedColumn …
    @@ -14764,0 +14775,17   @@ export class SqlDriver …
    @@ -14860   +14887,25   @@ export class SqlDriver …
    

    It is the SQLite arm of introspectUniqueConstraints plus a type declaration — 3701 / 14775 / 14887. This card's region is the varchar width mirror at 13354–13357 and createColumn at 13649–13652.

    ⇒ Disjoint on both sides: the nearest approach is ~1,100 lines (13649 → 14775), and neither touches the other's symbols. The dispatch stands.

    Updated occupancy table for sql-driver.ts

    Occupant Region State
    #11794 (this) width mirror 13354–13357, createColumn 13649–13652 🔵 dispatching
    #11784 (this lane) DIALECT_CONNECT_TIMEOUT 4577–4630 🔵 in flight
    #11785 (this lane, card #11635) aggregate lowering ~7886 🟡 open, held on #11808
    #11827 (other seat, card #11654) 3701 / 14775 / 14887 🟡 open, draft
    #11782 (this lane) readPresentationKind 11480 ⏳ serial-queued

    ⚠️ Note for the dev: #11827 is likely to land under you and touches this file. If it does, merge main in rather than rebasing (⛔ never rewrite history on another seat's branch — and merging keeps your own checkout valid), then re-read your two case lists to confirm nothing moved. Its hunks are far from yours, so a clean merge is expected; verify rather than assume.


    Generated by Claude Code

  6. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    Blocked-by: Fable 5 model quota exhausted at the account level (⛔ not an issue — see the unlock action)
    Unlock-action: WAIT for the Fable 5 quota to reset, then re-dispatch at CONTRACT_REVIEW_TIER. ⛔ Do NOT re-dispatch this card below tier. ⚠️ Substantial correct work already exists uncommitted — resume it, do not restart from scratch.

    What happened

    The dispatched dev terminated mid-run on a platform quota, not on anything about this card:

    Agent terminated early due to an API error: You've reached your Fable 5 limit.

    Its last recorded step was "Now build the dependency closure under the verify lock" — i.e. it had finished the edit and was entering verification.

    ⛔ Nothing was committed and nothing was pushed. The branch claude/issue-11794-richtext-text-family-emission does exist on origin, but it points at bb41868654, which is origin/main's own HEAD — an empty branch pointer, ⛔ not a partial PR and not reviewable work. Anyone reading the remote will see a branch with zero commits; that is expected, and it is not the state of the work.

    The work that survives, and it is good

    Uncommitted in the worktree /home/user/objectstack-11794, backed up as a verified patch (it reverse-applies against the tree exactly, so the backup is byte-faithful):

     packages/drivers/driver-sql/src/sql-driver.ts | 37 insertions(+), 9 deletions(-)
     packages/drivers/driver-sql/src/sql-driver-11794-richtext-text-family.test.ts | 199 lines (new)
    

    Both case lists were updated — the requirement whose omission would have tripped the mirror/createColumn agreement pin:

    13358:      case 'richtext':      ← the varchar-width mirror
    13674:      case 'richtext':      ← createColumn itself
    

    The design question triage delegated was decided, and justified from measurement rather than analogy. The dev took the wider option — code / signature / qrcode join the text family alongside richtext — on the ground that each is a STRING_VALUE_TYPES member whose stored value IS the declared value, with field-zoo cited for what actually gets written (a data-URI PNG for signature, the editor's contents for code). It fenced the two that must stay in the catch-all and said why: secret persists an opaque sys_secret ref (ADR-0100), so its stored value is not the declared one at all, and color is short by construction.

    ⭐ It also corrected a pre-existing factual error rather than working around it: the catch-all's own comment listed code among option-valued types, which the field-zoo measurement contradicts. The comment now says so explicitly instead of leaving a stale sentence next to a changed behaviour.

    What remains

    1. The driver-sql (MySQL): nothing sees the 65535-byte row budget, so a table with many wide declared bounds fails CREATE with an error naming no field #11565 agreement pin update (sql-driver-11565-row-byte-budget.test.ts) — owed in the same PR and not yet touched.
    2. Changeset.
    3. Verification: gate union, driver-conformance before/after readings, and the ablation.
    4. ⚠️ The clause ② judgment on the widening: triage's fence was "if it turns out to widen accepted physical shapes beyond the declared contract, stop and report." The dev chose the wider option; ⛔ that choice has not yet been measured against the contract, and it is exactly the surface the tier exists to scrutinise.

    ⛔ Why this is not being re-dispatched at a lower tier

    The obvious shortcut — finish the last 30% at opus, since the edit is already written — is refused deliberately. What remains is not clerical: item 4 above is the clause ② surface, and the card's accept/reject flip (a rich-text body over 255 characters going from refused to accepted on both enforcing dialects) is the reason this was dispatched at CONTRACT_REVIEW_TIER = 'claude-fable-5' in the first place. Finishing the contract judgment below tier is the precise incident class scripts/pm/dispatch-gates.mjs documents against — "a below-tier dispatch flipping accept-to-reject behaviour in the ledger, the same hole passed three times in one day."

    ⇒ The card waits. ⛔ A seat does not trade a contract-review tier for throughput, least of all on the card that most needs it.

    Card state

    pm:dispatched → pm:blocked, assignee cleared. ⚠️ Region note for the serial queue: this card's region (13354–13358 and 13650–13674) is not released while the work sits — a second card entering createColumn would conflict with the resumed work. Treat createColumn as occupied.


    Generated by Claude Code

  7. removed their assignment
    on Aug 24, 2026
  8. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    Tier ruling — dispatched BELOW CONTRACT_REVIEW_TIER by maintainer decision

    Maintainer, 2026-08-24, live PM chat. ⛔ Quoted verbatim, not translated:

    「fable5 没了,可以用opus 处理完」

    Recorded because it is a deliberate exception, not a shortcut. This is a clause ② card — it flips a richtext body over 255 characters from refused to accepted — so it normally dispatches at CONTRACT_REVIEW_TIER (claude-fable-5, scripts/pm/dispatch-gates.mjs). That tier's quota is exhausted at account level. This seat raised the objection three times and refused to downgrade on its own initiative; the maintainer has now authorised it explicitly.

    ⇒ ⛔ This is not a precedent. The standing rule is unchanged: a clause ② card must never be re-dispatched below tier for throughput. It took a named human decision to move this one.

    What compensates for the lower tier

    The judgment the tier exists to protect is fenced harder in the dispatch, and will be reviewed harder here:

    ⚠️ The prior dev took the WIDER option — admitting code / signature / qrcode alongside richtext — and never measured it against triage's fence, which reads verbatim:

    pick the direction that restores the declared grouping and state it in the PR; if it turns out to widen accepted physical shapes beyond the declared contract, stop and report.

    So the resuming dev is instructed to enumerate each of the four types, before and after, with its actual physical column on every dialect, read back from information_schema / PRAGMA rather than from the emitter — and if any of them admits a shape the declared contract does not, to stop and report blocked, naming which.

    ⛔ It is explicitly told that reverting the three extras and shipping richtext alone is a good outcome, since that satisfies the card's stated minimum (the declared three-member "Rich Content" grouping). The already-written code is not a reason to widen.

    ⚠️ The enqueue gate re-judges clause ② from the PR's actual diff regardless of dispatch tier — the diff is a fact where the dispatch was a prediction. That check is unaffected by this exception.

    State

    WIP preserved as commit e58d120ee6 on claude/issue-11794-richtext-text-family-emission (pushed on maintainer authorisation — the work was uncommitted in an ephemeral container and would have been lost). ⛔ It is an explicit WIP commit, not a PR — resume from it, do not restart.

    Per the maintainer's instruction, the resulting PR goes to triage review rather than being enqueued by this seat.


    Generated by Claude Code

  9. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor
    {
      "issue": 11794,
      "status": "needs_decision",
      "branch": "claude/issue-11794-richtext-text-family-emission",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11876",
      "premise_still_valid": true,
      "summary": "Resumed the WIP branch (merged origin/main in, never rebased). The card's defect is fixed and the card's stated minimum is met: `richtext` now takes an unbounded TEXT column beside its declared `markdown`/`html` Rich Content siblings, on every dialect. THE CLAUSE 2 FENCE FIRED, and that is the headline. The prior dev's wider option (richtext + code + signature + qrcode) was measured against the fence for the first time, and 2 of the 4 failed it: I reverted `signature` and `qrcode` and shipped `richtext` + `code`. The deciding test is not analogy — an unbounded TEXT column is correct for a type exactly when the WRITE SEAM enforces its declared `maxLength`, which is the invariant `schema-drift.ts` already rests on in as many words ('A TEXT column refuses nothing a maxLength allows ... the bound is enforced at the write seam'). Measured with validateRecord at maxLength 64 / 100-char value: text, textarea, markdown, html, richtext and code are REFUSED; signature and qrcode are ACCEPTED, i.e. nothing bounds them anywhere, so TEXT would trade an under-accepting column for an over-accepting one - a physical surface WIDER than the declared contract. `code` is kept because it measures INSIDE the enforced set, has the identical defect (1000 chars refused on both enforcing dialects before, accepted after), and the card body names it; dropping it is a two-case-label revert if the PM prefers the strict minimum. TWO PREMISE CORRECTIONS the PM should read: (1) the #11565 agreement pin does NOT need an expectation update - it is agreement-shaped, not behaviour-shaped (it reads FieldType.options and compares varcharColumnChars against columnInfo), so it goes green with both lists moved and red with only one; verified by RUNNING it 10/10 before, after, and under ablation; the file is unchanged. (2) I report `needs_decision` rather than the dispatch's literal `blocked`: a draft PR exists and the card's minimum is met, and the fenced residue is a contract DECISION, filed as #11875. Flagging the wording conflict rather than silently picking a side."
      ,
      "tests": "All at final commit 161cd39112 (git rev-parse --short HEAD, run AFTER the last commit). Live servers provisioned locally as CI does: MySQL 8.0.46 utf8mb4 STRICT_TRANS_TABLES time_zone='+08:00', Postgres 16, OS_EXPECT_LIVE_DIALECT_MATRIX=1, TZ=America/New_York. (1) PACKAGE SUITE: `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2` -> 'Test Files 135 passed (135) / Tests 2713 passed | 1 skipped (2714)', lock VERDICT command-exit 0. An earlier run showed 3 red in sql-driver-temporal-conformance and sql-driver-datetime-mysql-storage; root cause was MY provisioning, not the tree - those suites assert a NON-UTC server and a three-way zone skew ('on a UTC server the timezone bugs are invisible'). After setting MySQL time_zone='+08:00' and TZ=America/New_York they are 156/156 green. Nothing was skipped, quarantined or weakened. (2) TYPECHECK: `pnpm --filter @objectstack/driver-sql typecheck` exit=0. (3) LINT: repo-wide `pnpm lint` (eslint . --no-inline-config) exit=0 in 85s - run WHOLE, so there is no narrowing to justify. (4) GATE UNION: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` re-derived AT 161cd39112 (stderr confirms 'tree of objectstack-ai/objectstack at commit 161cd39112'; 3 paths vs merge base 589758d22). All 14 path-matched families run with exit codes captured BEFORE any pipe (each into its own log, `bash -c ... > log 2>&1; e=$?`), all exit=0: changeset-gate-self-tests, driver-conformance, objectui-changeset, published-files, slot-lookup, test-source-alias, type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test. Plus all 5 convention-triggered families for 'adds or edits a test file' + check-nul-bytes, all exit=0. RATCHET: `pnpm check:type-check-debt` exit=0 on the built workspace closure (turbo run build --filter=./packages/* --filter=./packages/*/* first, exit=0); its own verdict line: 'check-type-check-coverage --re-measure: OK - 32 ledger entries re-measured in 231.7s, 1898 raw tsc errors total, none above its recorded number'. No gate was skipped. (5) ABLATION: direction predicted IN WRITING BEFORE the run - removing 'richtext'+'code' from BOTH lists turns the #11794 file RED on the SQLite physical-shape test, the set pin, the keyed-semantics test and BOTH live cells; the SQLite round-trip stays GREEN (SQLite enforces no varchar width); the #11565 pin stays GREEN. Mutation proven on disk BEFORE any result was read, by anchored grep counts on the text intended to change: ^      case 'richtext':$ went 2 -> 0 and ^      case 'code': went 2 -> 0, injected bare-brace anchor present (a zero-hit edit exits 0 and reads like a successful ablation, so the count is the evidence, not the editor's exit code). Restore under trap '<restore>' EXIT INT TERM from a byte copy taken before mutating; TRAP fired, tree verified clean (git status --porcelain empty) and counts back to 2/2. OBSERVED = EXACTLY THE PREDICTION: 5 failed / 14 passed across the three files, SQLite round-trip green, #11565 green. REBUILD LEG: none exists for this pair and that is a property of the resolution path, not an omission - the suite imports '../src/index.js' and the package's vitest.config.ts aliases @objectstack/spec to src, so nothing here resolves through dist. Dependency closure was built anyway before any judgement (pnpm --filter '@objectstack/driver-sql^...' build, exit=0). (6) CLAUSE 2 PHYSICAL ENUMERATION - read back from information_schema / PRAGMA, never the emitter, BEFORE (ablated tree) and AFTER, on all three dialects. richtext & code: sqlite varchar(255)->TEXT; postgres 'character varying(255)' + write REFUSED 22001 -> 'text' + ACCEPTED; mysql varchar(255) + write REFUSED ER_DATA_TOO_LONG -> 'text' + ACCEPTED. signature & qrcode: unchanged varchar(255)/character varying(255), still REFUSED on both enforcing dialects before and after. POSITIVE CONTROL: markdown/html/text are text|TEXT on all three dialects BEFORE and AFTER with the 1000-char write accepted in all six live cells - the grouping already held for two of three and only the claimed types moved. NON-VACUITY: secret/color/select/string stay varchar(255) and are REFUSED by the server at 1000 chars in the SAME runs, so the acceptances are not a lenient session. (7) WRITE-SEAM MEASUREMENT (the fence's deciding evidence): validateRecord({type:T,maxLength:64},{f:'y'.repeat(100)},'insert') -> REFUSED 'VALIDATION_FAILED f must be <= 64 characters (got 100)' for text/textarea/markdown/html/richtext/code; ACCEPTED (no max_length branch) for signature/qrcode/secret/color/string. (8) DRIVER-CONFORMANCE LEDGER, lane standing promise, read BOTH sides: BEFORE (detached worktree pinned at 589758d22c, since removed) 'OK - 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'; AFTER identical, 'OK - 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'. (9) SERIAL SWEEP re-run at this push, not reused: all open PR heads fetched (+refs/pull/*/head:refs/remotes/pr/*), git diff --name-only origin/main...refs/remotes/pr/N over all 22 open PRs -> exactly ONE touches sql-driver.ts, #11785 at hunks @@7750/7765/7794 (aggregate lowering). Mine are @@13385/13679/13828 - ~5,500 lines apart, no shared symbol. #11827 is no longer open. Positive control: same matcher returns 11 files for #11699. aggregate(), introspectUniqueConstraints and the dialect getters untouched."
      ,
      "open_questions": [
        {
          "question": "`signature` and `qrcode` have the same live defect as `richtext` (a data-URI PNG is refused at 255 chars on both enforcing dialects, measured) but NO write-seam enforcement of `maxLength` anywhere, so they cannot join the TEXT family without widening the accepted physical surface past the declared contract - which is exactly what triage's fence forbids. How should their bound be established? Filed as #11875. This is the fenced item; the PR ships without them.",
          "options": [
            "A - Give the write seam a bound: add signature/qrcode to the record-validator's max_length branch, THEN move them into the TEXT family. Declared = enforced, the data-URI defect goes away, and the invariant this PR states stays true for every member.",
            "B - Treat them as string-family (declaredVarcharLength): varchar(maxLength), TEXT above MAX_VARCHAR_CHARS. Honours a declared bound, but an UNDECLARED signature still takes varchar(255) and still refuses an ordinary data URI - the defect survives for the common case.",
            "C - Rule `maxLength` inapplicable to them under ADR-0049 enforce-or-remove and refuse the key at publish time; then TEXT is unambiguously correct and no validator change is needed.",
            "D - Do nothing: leave them varchar(255). Status quo, and it is what the PR ships as the interim state, asserted out loud in the live-dialect suite rather than left undocumented."
          ],
          "recommendation": "A, on all four axes. REAL BUSINESS NEED: not speculative - field-zoo writes a data-URI PNG for `signature`, and a 1000-char value is measured REFUSED today on both enforcing dialects, so a working signature capture is broken on MySQL and Postgres right now. LONG-TERM SOUNDNESS: A is the only option that makes the type satisfy the invariant the rest of the driver already rests on (schema-drift's 'the bound is enforced at the write seam'), rather than adding a second, type-specific rule; B leaves declared != enforced for the undeclared case and C removes an authorable key that a signature-size limit is a legitimate thing to want. HARD TO GET WRONG FOR AI-AUTHORED METADATA: today Field.signature({ maxLength: 64 }) parses, publishes and binds NOTHING - the exact silent-acceptance shape a strict schema is supposed to prevent; A makes the declaration bind at the write seam with a field-named ADR-0112 envelope, which is the loud-refusal direction. STARTUP SCOPE: A is the smallest change of the three that fix anything - two entries in one existing type list plus two case labels - and expands no capability surface; C is the cheapest of all but is a removal that needs the maintainer's ADR-0049 ruling, so it is a decision, not a dev call. If the maintainer disagrees that signature/qrcode should ever carry a length bound, C is the correct answer and I would take it - which is precisely why this is escalated rather than guessed."
        },
        {
          "question": "Should `code` ship in this PR, or should the branch be trimmed to `richtext` alone (the card's stated minimum)?",
          "options": [
            "A - Keep `code` (what the PR does). It measures INSIDE the write-seam-enforced set, so it does not widen anything; it has the identical measured defect (1000 chars refused on both enforcing dialects before, accepted after); the card body names it as affected; and it lives in the same two case lists and the same gate family, so it adds no verification surface.",
            "B - Trim to `richtext` alone. Strictly minimal, matches the dispatch's 'shipping richtext alone is a perfectly good outcome'."
          ],
          "recommendation": "A, but this is a genuinely separable claim and I am flagging it rather than burying it: dropping `code` is a two-case-label revert plus a changeset sentence, and I will do it on request. The reason to keep it is that leaving a MEASURED member of the enforced set behind reproduces the exact root cause the card names - a hand-maintained list where one member of a group diverges silently - and the PR now pins that set as a whole, which only works if the set is stated correctly."
        }
      ],
      "out_of_scope_findings": [
        "filed as #11875: `signature` / `qrcode` have no `maxLength` enforcement anywhere (measured: the record-validator's max_length branch omits them, and `maxLength` is a universally-admitted FieldSchema key), so a declared bound binds nothing and they cannot join the TEXT family - while a data-URI signature is refused at 255 chars today on both enforcing dialects. Unassigned, no labels, searched for duplicates first (none). Related to #11431, not a sub-issue of it: that card is a bound that exists and is not honoured; this is a bound honoured by nothing at all."
      ]
    }

    Notes for the PM that do not fit the JSON

    • Worktree /home/user/objectstack-11794 deliberately KEPT (not torn down per the usual post-PR cleanup): this card carries an open escalation and was already resumed from that worktree once. Deps installed, closure built, git status clean at 161cd39112. Say the word and I will remove it.
    • Local live servers left running for the next driver-sql card: Postgres 16 at postgres://postgres:postgres@127.0.0.1:5432/postgres, MySQL 8.0.46 at mysql://root:root@127.0.0.1:3306/conformance with SET GLOBAL time_zone = '+08:00'. The temporal suites require that non-UTC server plus TZ=America/New_York, otherwise they correctly refuse to prove anything.
    • PR body cosmetic: GitHub's sanitizer ate one short <…> placeholder in the serial-sweep sentence, so it reads refs/remotes/pr/ where it should read refs/remotes/pr/<n>. Not corrected, to avoid a second sanitizer pass over the body for a cosmetic gain.
    • ⛔ I did not touch labels, the assignee, or the PR's draft state.

    Generated by Claude Code


    Generated by Claude Code

  10. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    os-dev-report

    ⚠️ Re-post of the comment directly above, because its marker did not survive. The HTML-comment marker was eaten by GitHub's body sanitizer on write — read back and confirmed gone, so that comment is invisible to a marker scan. There is no comment-edit path from this seat, so the record is re-posted here starting with the literal text instead. This comment is the authoritative report; the one above is the same JSON without a usable marker.

    {
      "issue": 11794,
      "status": "needs_decision",
      "branch": "claude/issue-11794-richtext-text-family-emission",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11876",
      "premise_still_valid": true,
      "summary": "Resumed the WIP branch (merged origin/main in, never rebased). The card's defect is fixed and the card's stated minimum is met: `richtext` now takes an unbounded TEXT column beside its declared `markdown`/`html` Rich Content siblings, on every dialect. THE CLAUSE 2 FENCE FIRED, and that is the headline. The prior dev's wider option (richtext + code + signature + qrcode) was measured against the fence for the first time and 2 of the 4 failed it: I reverted `signature` and `qrcode` and shipped `richtext` + `code`. The deciding test is not analogy - an unbounded TEXT column is correct for a type exactly when the WRITE SEAM enforces its declared `maxLength`, the invariant `schema-drift.ts` already rests on in as many words ('A TEXT column refuses nothing a maxLength allows ... the bound is enforced at the write seam'). Measured with validateRecord at maxLength 64 / 100-char value: text, textarea, markdown, html, richtext and code are REFUSED; signature and qrcode are ACCEPTED, i.e. nothing bounds them anywhere, so TEXT would trade an under-accepting column for an over-accepting one - a physical surface WIDER than the declared contract. `code` is kept because it measures INSIDE the enforced set, has the identical defect (1000 chars refused on both enforcing dialects before, accepted after), and the card body names it; dropping it is a two-case-label revert if the PM prefers the strict minimum. TWO PREMISE CORRECTIONS the PM should read: (1) the #11565 agreement pin does NOT need an expectation update - it is agreement-shaped, not behaviour-shaped (it reads FieldType.options and compares varcharColumnChars against columnInfo), so it goes green with both lists moved and red with only one; verified by RUNNING it 10/10 before, after, and under ablation; the file is unchanged by this PR. (2) I report `needs_decision` rather than the dispatch's literal `blocked`: a draft PR exists and the card's minimum is met, and the fenced residue is a contract DECISION, filed as #11875. Flagging the wording conflict rather than silently picking a side.",
      "tests": "All at final commit 161cd39112 (git rev-parse --short HEAD, run AFTER the last commit). Live servers provisioned locally as CI does: MySQL 8.0.46 utf8mb4 STRICT_TRANS_TABLES time_zone='+08:00', Postgres 16, OS_EXPECT_LIVE_DIALECT_MATRIX=1, TZ=America/New_York. (1) PACKAGE SUITE: `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2` -> 'Test Files 135 passed (135) / Tests 2713 passed | 1 skipped (2714)', lock VERDICT command-exit 0. An earlier run showed 3 red in sql-driver-temporal-conformance and sql-driver-datetime-mysql-storage; root cause was MY provisioning, not the tree - those suites assert a NON-UTC server and a three-way zone skew ('on a UTC server the timezone bugs are invisible'). After setting MySQL time_zone='+08:00' and TZ=America/New_York they are 156/156 green. Nothing was skipped, quarantined or weakened. (2) TYPECHECK: `pnpm --filter @objectstack/driver-sql typecheck` exit=0. (3) LINT: repo-wide `pnpm lint` (eslint . --no-inline-config) exit=0 in 85s - run WHOLE, so there is no narrowing to justify. (4) GATE UNION: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` re-derived AT 161cd39112 (stderr confirms 'tree of objectstack-ai/objectstack at commit 161cd39112'; 3 paths vs merge base 589758d22). All 14 path-matched families run with exit codes captured BEFORE any pipe (each into its own log, `bash -c ... > log 2>&1; e=$?`), all exit=0: changeset-gate-self-tests, driver-conformance, objectui-changeset, published-files, slot-lookup, test-source-alias, type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test. Plus all 5 convention-triggered families for 'adds or edits a test file' + check-nul-bytes, all exit=0. RATCHET: `pnpm check:type-check-debt` exit=0 on the built workspace closure (turbo run build --filter=./packages/* --filter=./packages/*/* first, exit=0); its own verdict line: 'check-type-check-coverage --re-measure: OK - 32 ledger entries re-measured in 231.7s, 1898 raw tsc errors total, none above its recorded number'. No gate was skipped. (5) ABLATION: direction predicted IN WRITING BEFORE the run - removing 'richtext'+'code' from BOTH lists turns the #11794 file RED on the SQLite physical-shape test, the set pin, the keyed-semantics test and BOTH live cells; the SQLite round-trip stays GREEN (SQLite enforces no varchar width); the #11565 pin stays GREEN. Mutation proven on disk BEFORE any result was read, by anchored grep counts on the text intended to change: the anchored richtext case label went 2 -> 0 and the anchored code case label went 2 -> 0, injected bare-brace anchor present (a zero-hit edit exits 0 and reads like a successful ablation, so the count is the evidence, not the editor's exit code). Restore under a trap on EXIT INT TERM from a byte copy taken before mutating; TRAP fired, tree verified clean (git status --porcelain empty) and counts back to 2/2. OBSERVED = EXACTLY THE PREDICTION: 5 failed / 14 passed across the three files, SQLite round-trip green, #11565 green. REBUILD LEG: none exists for this pair and that is a property of the resolution path, not an omission - the suite imports '../src/index.js' and the package's vitest.config.ts aliases @objectstack/spec to src, so nothing here resolves through dist. Dependency closure was built anyway before any judgement (pnpm --filter '@objectstack/driver-sql^...' build, exit=0). (6) CLAUSE 2 PHYSICAL ENUMERATION - read back from information_schema / PRAGMA, never the emitter, BEFORE (ablated tree) and AFTER, on all three dialects. richtext and code: sqlite varchar(255)->TEXT; postgres 'character varying(255)' + write REFUSED 22001 -> 'text' + ACCEPTED; mysql varchar(255) + write REFUSED ER_DATA_TOO_LONG -> 'text' + ACCEPTED. signature and qrcode: unchanged varchar(255)/character varying(255), still REFUSED on both enforcing dialects before and after. POSITIVE CONTROL: markdown/html/text are text|TEXT on all three dialects BEFORE and AFTER with the 1000-char write accepted in all six live cells - the grouping already held for two of three and only the claimed types moved. NON-VACUITY: secret/color/select/string stay varchar(255) and are REFUSED by the server at 1000 chars in the SAME runs, so the acceptances are not a lenient session. (7) WRITE-SEAM MEASUREMENT (the fence's deciding evidence): validateRecord with {type:T, maxLength:64} and a 100-char value -> REFUSED 'VALIDATION_FAILED f must be <= 64 characters (got 100)' for text/textarea/markdown/html/richtext/code; ACCEPTED (no max_length branch) for signature/qrcode/secret/color/string. (8) DRIVER-CONFORMANCE LEDGER, lane standing promise, read BOTH sides: BEFORE (detached worktree pinned at 589758d22c, since removed) 'OK - 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'; AFTER identical, 'OK - 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'. (9) SERIAL SWEEP re-run at this push, not reused: all open PR heads fetched (+refs/pull/*/head:refs/remotes/pr/*), git diff --name-only origin/main...refs/remotes/pr/N over all 22 open PRs -> exactly ONE touches sql-driver.ts, #11785 at hunks 7750/7765/7794 (aggregate lowering). Mine are 13385/13679/13828 - ~5,500 lines apart, no shared symbol. #11827 is no longer open. Positive control: same matcher returns 11 files for #11699. aggregate(), introspectUniqueConstraints and the dialect getters untouched.",
      "open_questions": [
        {
          "question": "`signature` and `qrcode` have the same live defect as `richtext` (a data-URI PNG is refused at 255 chars on both enforcing dialects, measured) but NO write-seam enforcement of `maxLength` anywhere, so they cannot join the TEXT family without widening the accepted physical surface past the declared contract - exactly what triage's fence forbids. How should their bound be established? Filed as #11875. This is the fenced item; the PR ships without them.",
          "options": [
            "A - Give the write seam a bound: add signature/qrcode to the record-validator's max_length branch, THEN move them into the TEXT family. Declared = enforced, the data-URI defect goes away, and the invariant this PR states stays true for every member.",
            "B - Treat them as string-family (declaredVarcharLength): varchar(maxLength), TEXT above MAX_VARCHAR_CHARS. Honours a declared bound, but an UNDECLARED signature still takes varchar(255) and still refuses an ordinary data URI - the defect survives for the common case.",
            "C - Rule `maxLength` inapplicable to them under ADR-0049 enforce-or-remove and refuse the key at publish time; then TEXT is unambiguously correct and no validator change is needed.",
            "D - Do nothing: leave them varchar(255). Status quo, and what the PR ships as the interim state, asserted out loud in the live-dialect suite rather than left undocumented."
          ],
          "recommendation": "A, on all four axes. REAL BUSINESS NEED: not speculative - field-zoo writes a data-URI PNG for `signature`, and a 1000-char value is measured REFUSED today on both enforcing dialects, so signature capture is broken on MySQL and Postgres right now. LONG-TERM SOUNDNESS: A is the only option that makes the type satisfy the invariant the rest of the driver already rests on, rather than adding a second type-specific rule; B leaves declared != enforced for the undeclared case, and C removes an authorable key that a signature-size limit is a legitimate thing to want. HARD TO GET WRONG FOR AI-AUTHORED METADATA: today Field.signature({maxLength:64}) parses, publishes and binds NOTHING - the exact silent-acceptance shape a strict schema exists to prevent; A makes the declaration bind at the write seam with a field-named ADR-0112 envelope, the loud-refusal direction. STARTUP SCOPE: A is the smallest change of the options that fix anything - two entries in one existing type list plus two case labels - and expands no capability surface; C is cheaper still but is a removal needing the maintainer's ADR-0049 ruling, so it is a decision, not a dev call. If the maintainer holds that signature/qrcode should never carry a length bound, C is the correct answer and I would take it - which is exactly why this is escalated rather than guessed."
        },
        {
          "question": "Should `code` ship in this PR, or should the branch be trimmed to `richtext` alone (the card's stated minimum)?",
          "options": [
            "A - Keep `code` (what the PR does). It measures INSIDE the write-seam-enforced set so it widens nothing; it has the identical measured defect (1000 chars refused on both enforcing dialects before, accepted after); the card body names it as affected; and it lives in the same two case lists and the same gate family, adding no verification surface.",
            "B - Trim to `richtext` alone. Strictly minimal, matches the dispatch's 'shipping richtext alone is a perfectly good outcome'."
          ],
          "recommendation": "A, but this is a genuinely separable claim and I am flagging it rather than burying it: dropping `code` is a two-case-label revert plus a changeset sentence, and I will do it on request. The reason to keep it is that leaving a MEASURED member of the enforced set behind reproduces the exact root cause the card names - a hand-maintained list where one member of a group diverges silently - and the PR now pins that set as a whole, which only works if the set is stated correctly."
        }
      ],
      "out_of_scope_findings": [
        "filed as #11875: `signature` / `qrcode` have no `maxLength` enforcement anywhere (measured: the record-validator's max_length branch omits them, and `maxLength` is a universally-admitted FieldSchema key), so a declared bound binds nothing and they cannot join the TEXT family - while a data-URI signature is refused at 255 chars today on both enforcing dialects. Unassigned, no labels, searched for duplicates first (none). Related to #11431, not a sub-issue of it: that card is a bound that exists and is not honoured; this is a bound honoured by nothing at all."
      ]
    }

    Notes for the PM that do not fit the JSON

    • Worktree /home/user/objectstack-11794 deliberately KEPT rather than torn down: this card carries an open escalation and was already resumed from that worktree once. Deps installed, closure built, git status clean at 161cd39112. Say the word and I will remove it.
    • Local live servers left running for the next driver-sql card: Postgres 16 at postgres://postgres:postgres@127.0.0.1:5432/postgres, MySQL 8.0.46 at mysql://root:root@127.0.0.1:3306/conformance with SET GLOBAL time_zone = '+08:00'. The temporal suites require that non-UTC server plus TZ=America/New_York, or they correctly refuse to prove anything.
    • ⛔ I did not touch labels, the assignee, or the PR's draft state.

    Generated by Claude Code


    Generated by Claude Code

  11. huangyiirene commented on Aug 24, 2026

    @huangyiirene
    CollaboratorAuthor

    ACCEPT — PR #11876. ⭐ The clause ② fence fired, and that is the result.

    Reviewer of record: domain:engine seat, huangyiirene, session session_01VK8rFDtg8eREaxBGX99Csn. Checked against the diff and the source, not the report. ⛔ Landing held — last section.

    ⭐ The fence fired — this is exactly what the tier downgrade risked, and it was caught

    This card was dispatched below CONTRACT_REVIEW_TIER by maintainer decision (recorded above at 5401803355). The compensation I promised was that the one unmeasured contract judgment would be fenced harder. It was, and it changed the answer: the prior dev's wider option was measured against triage's fence for the first time, and 2 of 4 failed it. signature and qrcode reverted; richtext + code shipped.

    ⛔ Had this gone out as written by the earlier run, it would have widened the accepted physical surface past the declared contract — precisely what triage forbade.

    The deciding test is a mechanism, not an analogy — verified independently

    The dev's criterion: an unbounded TEXT column is correct for a type exactly when the write seam enforces its declared maxLength. I checked the source rather than the report — packages/objectql/src/validation/record-validator.ts:554:

    if (t === 'text' || t === 'textarea' || t === 'email' || t === 'url' || t === 'phone'
     || t === 'password' || t === 'markdown' || t === 'html' || t === 'richtext' || t === 'code') {

    ⇒ richtext and code are inside the enforced set; signature and qrcode are not there at all.

    So for signature/qrcode, TEXT would remove the column's bound while nothing else bounds them anywhere — trading an under-accepting column for an over-accepting one. That is a widening, not a fix. For richtext/code the declared bound is still enforced at the seam, so the column going TEXT widens nothing.

    ⭐ This is the same invariant schema-drift.ts already rests on in as many words ("A TEXT column refuses nothing a maxLength allows … the bound is enforced at the write seam"). ⇒ The card was resolved against an invariant the codebase already held, not against a judgment call.

    ⛔ My dispatch brief was wrong about the #11565 pin — correcting it

    I instructed, in both briefs, that sql-driver-11565-row-byte-budget.test.ts "currently pins the present behaviour, this defect included" and must have its expectation updated. That is false. The dev pushed back and is right; I verified:

    const types = FieldType.options as readonly string[];
    expect(types.length).toBeGreaterThan(40);      // its own non-vacuity control
    const mirrored = (driver as any).varcharColumnChars({ type: t }, undefined);
    … expect(mismatched).toEqual([]);

    It is agreement-shaped, not behaviour-shaped: it reads the spec's type registry and compares the mirror's answer against the actually created column. Move a type in both lists and it stays green; move it in one and it goes red. ⇒ No expectation update was owed, and the file is correctly untouched in this diff (3 files changed, it is not among them). The dev verified by running it 10/10 before, after, and under ablation rather than reasoning about it — the right response to a PM instruction that contradicts the code.

    My call on the two open questions

    1. code ships — option A. This is mine to decide (triage put the direction in-fix) and I take the dev's reasoning: code measures inside the enforced set so it widens nothing, carries the identical measured defect (1000 chars refused on both enforcing dialects before, accepted after), is named in the card body, and adds no verification surface. Decisively — leaving a measured member of the enforced set behind reproduces the exact root cause this card names: a hand-maintained list where one member of a declared group diverges silently. The PR now pins that set as a set, which is only true if the set is stated correctly. ⛔ Do not trim to richtext alone.

    2. needs_decision over my literal blocked — the dev is right to flag rather than silently pick. My brief said "stop and report blocked", written for the case where the fence fires and nothing shippable remains. Here the fence fired and the card's stated minimum is met and a draft PR exists, so blocked would misreport it. ⇒ Vocabulary gap in my brief, not a deviation by the dev.

    Evidence

    The fenced residue is a real contract question, correctly escalated

    #11875 — signature / qrcode have the live defect (a data-URI PNG is refused at 255 chars today on both enforcing dialects, measured) but no maxLength enforcement anywhere, so Field.signature({maxLength: 64}) parses, publishes, and binds nothing. ⛔ That is a contract decision, not a dev call, and it is filed unassigned and ungraded — correctly. ⛔ I do not adjudicate it from this seat; the dev's four-axis recommendation (A: give the write seam the bound, then move them) is on the card as a recommendation.

    ⛔ Landing held

    PR is draft, head 161cd39112, CI opened 22:36Z and has not converged. Entry needs every check green, not the required subset. Per the maintainer's instruction this goes to triage review, ⛔ not enqueued by this seat.

    ⚠️ If Test Core (1/6) comes back red, read the guard banner first — that shard is #11808's kill, ⛔ not this diff.


    Generated by Claude Code

  12. self-assigned this
    on Aug 25, 2026
  13. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    pm:blocked → pm:dispatched — the block was a quota hold, and the work is finished

    domain:engine seat, session session_01W6HFzyH98W1YaQXhJUJt6o. Taking over the landing duty from the shift that ended 2026-08-24T23:10Z.

    Why the label was false

    This card was moved to pm:blocked when the Fable 5 quota was exhausted (~19:2xZ, account level) and no clause ② card could be dispatched at CONTRACT_REVIEW_TIER. That hold was then discharged by an explicit maintainer tier ruling (5401803355), the card was resumed at opus, and PR #11876 was completed and made ready at 23:0xZ.

    So by the time the shift closed, pm:blocked described nothing that was still true — it pointed at a quota condition that had already been ruled around. Re-measured now:

    reading
    PR #11876 open · draft: false · mergeable_state: clean · not merged
    head 161cd391127e
    closing keyword Closes #11794

    ⇒ pm:dispatched is the honest state: implementation complete, PR live, landing outstanding.

    ⛔ What this seat is NOT doing

    Not enqueueing it. The maintainer's instruction of 2026-08-24 (verbatim) 「用opus 处理完 然后pr等分诊审核」 routes this PR to triage review, not to the merge queue. It stays with triage until triage rules.

    ⚠️ For whoever picks it up: "done" is not "landed." This PR and #11699 are both ready and both unmerged, and that is deliberate, not an oversight.

    ⭐ The result on this card worth preserving

    The clause ② fence fired here and changed the answer. Dispatched below CONTRACT_REVIEW_TIER on a named human decision, the compensating control was to fence the one unmeasured contract judgment harder — and measured against that fence, 2 of the 4 candidate types failed and were reverted. The criterion is now pinned: a type may take an unbounded TEXT column exactly when the write seam enforces its declared maxLength. signature and qrcode do not, so an unbounded column there would be over-accepting, i.e. wider than the declared contract.

    ⛔ The tier exception that made this possible is not a precedent. It took an explicit maintainer decision and came within one measurement of shipping what triage had forbidden.

    The still-open half (signature / qrcode are refused at 255 chars today) is filed as #11875 and is ⛔ not addressed by this PR.


    Generated by Claude Code

  14. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    Contributor

    Contract review: PASS — PR #11876 (Closes this card) at head 161cd39112. Reviewed the actual diff (fetched refs/pull/11876/head, all 3 files read), not the report.

    Handoff defect noted first: the dispatching domain:engine seat's handoff comment (5402718088) sent this PR "to triage review" — but no needs:contract-review label was ever hung on either carrier, so the label-driven review cron never saw it. The review ran now on the maintainer's direct report. Dispatching seat: the handoff IS the label; a comment alone delivers nothing. This matters double here, because the card was dispatched below CONTRACT_REVIEW_TIER by explicit maintainer quota ruling (5401803355, not a precedent) with the review chain as the named compensation — the one link that then never got armed.

    The review itself:

    • Tier is correct: patch for @objectstack/driver-sql. The declared contract for richtext/code (valueSchemaFor ⇒ unbounded string, maxLength enforced at the write seam) was under-implemented by the catch-all's varchar(255); this restores the physical surface to the declared contract, not past it — the established fix-to-match-contract patch class. The changeset states the measured before/after on both enforcing dialects.
    • The fence held, and it changed the answer — that is the review's headline. Triage's clause-② fence ("if it widens accepted physical shapes beyond the declared contract, stop") was measured against all four candidates: richtext/code have the record-validator's max_length branch (refusal measured at the seam, field-named ADR-0112 envelope) and move; signature/qrcode do not (a maxLength: 64 declaration binds nothing) and are deliberately left refusing ordinary values at 255, with the still-open half asserted in a running test and chartered as 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. An earlier pass had moved all four; the fence reverted two. Exactly what the below-tier dispatch's compensation was for.
    • Membership is now a mechanism, not a list: the criterion (unbounded TEXT ⇔ write seam enforces the declared bound) is the invariant schema-drift.ts already rests on, and the whole unkeyed-unbounded set is pinned so the next member must be stated on purpose — closing the card's actual root cause, not just the instance.
    • driver-sql: the platform-objects schema does not sync onto MySQL — unbounded string fields become TEXT, which MySQL refuses to index #11374 keyed semantics preserved for both new members (keyed+bounded ⇒ varchar(maxLength), keyed+unbounded ⇒ named refusal), varcharColumnChars mirror moved in the same stroke, and the driver-sql (MySQL): nothing sees the 65535-byte row budget, so a table with many wide declared bounds fails CREATE with an error naming no field #11565 agreement pin correctly untouched — the dev falsified the dispatch brief's claim by running it (10/10 before/after/ablated), which is the right direction of authority.
    • Evidence quality: dialect shapes read back from information_schema/PRAGMA never the emitter; negative controls in the same runs (oversized writes into deliberately-unmoved columns still server-refused) make the acceptances non-vacuous; ablation with on-disk mutation proof matched its pre-run prediction including the cells predicted to stay green.

    Tier reading this round: get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (read fresh from origin/main scripts/pm/dispatch-gates.mjs:3070). Standing authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」, plus the maintainer's direct report today. No label to clear — none was ever on either carrier; this comment is the review of record. Merge remains the maintainer's; the PR is ready-flipped and deliberately not enqueued.


    Generated by Claude Code

  15. os-warren commented on Aug 25, 2026

    @os-warren
    Collaborator

    MERGED and closed — pm:dispatched residue stripped

    PR #11876 is on main. Verified by content, ⛔ not by the API's merged field:

    case 'richtext':  @ origin/main:…/sql-driver.ts  → 2   (was 0)
    CONTROL createColumn                             → 24
    squash: b826390f7d fix(driver-sql): richtext and code take an unbounded TEXT column,
            restoring the declared Rich Content grouping (#11876)
    

    The card was auto-closed by the PR's Closes #11794; the pm:dispatched label was left behind. Stripped now — patrol shape H22, the residue this lane left on all 8 cards it closed on 2026-08-24.

    ⭐ The result worth preserving

    richtext and code move to the unbounded TEXT family; signature and qrcode deliberately do not. The membership test is pinned and is not an aesthetic judgement:

    A type may take an unbounded TEXT column exactly when the write seam enforces its declared maxLength.

    record-validator.ts refuses an over-long richtext/code with a field-named VALIDATION_FAILED; it has no max_length branch for signature/qrcode, so for those two an unbounded column would trade an under-accepting column for an over-accepting one — wider than the declaration, on every dialect.

    ⇒ That fence was applied for the first time on this card and changed the answer: an earlier pass had taken the wider option (all four), and 2 of 4 failed the measurement and were reverted.

    ⚠️ The still-open half — signature/qrcode are refused at 255 characters today, and Field.signature({ maxLength: 64 }) parses while binding nothing — is #11875, in the decision box. ⛔ Not resolved by this PR, and the suite asserts that refusal out loud rather than leaving the cost in a comment nobody runs.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions