Repository navigation
Description
Filed by the domain:engine PM seat while reviewing PR #12119 (#11875). ⛔ Not a defect in that PR, and not a reason to hold it — #12119 strictly improves the situation and declares this boundary honestly in its own changeset. Recorded so the remaining half is not rediscovered as a surprise.
What #11875 fixes, and where it stops
After #12119, signature / qrcode are in the TEXT family, so a newly created column holds a data URI correctly. The changeset states the boundary in as many words:
Nothing about existing tables changes —
createColumnruns onCREATE TABLEandALTER TABLE ADD COLUMN, so the column it sizes is always empty; a pre-existingsignature/qrcodecolumn staysvarchar(255)until an operator migrates it, and the additive sync never rewrites a column's type on its own.
That is correct and is standard additive-sync behaviour. The part not stated is what the drift reporter does about it.
The measurement
packages/drivers/driver-sql/src/schema-drift.ts, the varchar differ. declaredMaxLength !== undefined is a required condition of the whole branch:
const declaredMaxLength =
typeof field.maxLength === 'number' && Number.isInteger(field.maxLength) && field.maxLength > 0
? field.maxLength
: undefined;
if (
enforcesVarcharLength(dialect) &&
!declaresJsonColumn &&
declaredMaxLength !== undefined && // <-- the gate
isCharacterColumn(col.type) &&
typeof col.maxLength === 'number' &&
declaredMaxLength !== col.maxLength
) { … widen_varchar / narrow_varchar … }⇒ Split by whether the author declared a bound, on a pre-existing table:
| declaration | physical column | drift says | outcome |
|---|---|---|---|
Field.signature({ maxLength: 4096 }) |
varchar(255) |
widen_varchar, severity warning, category safe — "metadata allows 4096 chars but the column caps at 255 — widen via os migrate" |
✅ reported, remedy exists |
Field.signature() — no bound |
varchar(255) |
nothing — no number on the declared side to compare | ⛔ silent |
The second row is the common case (the card's own motivating example is field-zoo writing a data-URI PNG into a plain signature), and it is the same declared ≠ enforced shape the whole #11374 / #11431 / #11875 family exists to close — arriving one door further along, at the migration seam rather than the authoring or write seam.
Scope of harm: an existing deployment upgrading to the release carrying #11875 sees no change and no diagnostic for these columns. A 1000-character data URI is still refused by the server (22001 / ER_DATA_TOO_LONG) exactly as the card measured, and nothing anywhere says why the fix did not reach them.
Not claimed
- ⛔ No claim that feat(data): signature and qrcode join the bounded-string family end to end #12119 is wrong or incomplete against its ruling. The maintainer ruled the authoring / write-seam / storage triple, and it delivered all three.
- ⛔ No frequency measured — how many deployed tables actually carry a
signature/qrcodecolumn is unknown, and an inventory is probably the honest first step rather than a new drift op. - ⛔ No opinion on the remedy. Candidates, all needing their own measurement: a drift op keyed on TEXT-family membership rather than on a declared number; a one-off migration in the release carrying
signature/qrcodehave nomaxLengthenforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875; or accepting it and documenting the manualos migratestep. The second and third interact with release notes, which is not a code-PR surface.
Refs
#11875 (the ruling) · PR #12119 · #11794 (the invariant) · #11374 / #11431 (the family) · #11722 (a neighbouring applyMigrationEntries reporting gap, currently in flight)
Activity
os-zhuang commentedon Aug 27, 2026
Claim: PM loop round 2⚠️ If the chosen route changes what the platform accepts, or emits a migration that rewrites a column type, that is a different card — stop and report.
Session:
session_01LZbWd2jNV1FErXTPSS4DryBranch:
claude/issue-12121-textfamily-drift-visibilityWorktree:
objectstack-issue-12121Domain:
domain:engineFile surface:
packages/drivers/driver-sql/src/schema-drift.tsand its tests. ⛔packages/specREAD-ONLY (spec seat); ⛔content/docs/releases/**is release-owned and off-limits to a code PR (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: opusClause-②: no — a drift report is a diagnostic, not an accept/reject door.
Serial constraints cleared: ⛔ This card does NOT take the
sql-driver.tsserial slot — it lands inschema-drift.ts, a distinct file (verified by filename). Thesql-driver.tsslot was freed when PR #12714 (#12017) merged asd29e42f8, and queued sibling #12593 is the next claimant there — ⛔ not dispatched, so no collision either way. Neighbouring card #11722 (anapplyMigrationEntriesreporting gap) ispm:on-hold, not in flight. No open PR touchesschema-drift.ts.The defect — measured on
origin/mainat claim time, controls firingThe varchar differ's whole branch is gated on the author having declared a bound. Live at⚠️ locate by symbol
packages/drivers/driver-sql/src/schema-drift.ts:833(declaredMaxLength, ⛔ never by that line number):Controls:
widen_varchar/narrow_varchar→ 6 hits in that file;enforcesVarcharLength→ 3; the file is present. The probe addresses real code.⇒ On a pre-existing table, split by whether a bound was declared:
Field.signature({ maxLength: 4096 })varchar(255)widen_varchar, with a remedyField.signature()— no boundvarchar(255)The second row is the common case — the card's own motivating example is a data-URI PNG written into a plain
signature. After #11875/#12119 a new column is TEXT and correct; an existing deployment upgrading to that release sees no change and no diagnostic, and the server still refuses at22001/ER_DATA_TOO_LONGwith nothing anywhere saying why the fix did not reach it.⇒ Same
declared ≠ enforcedshape as the #11374 / #11431 / #11875 family, arriving one door further along — at the migration seam rather than the authoring or write seam.Section — TRIAGE'S ROUTE (⛔ measure-first; this is a route, not a ruling)
os migratestep: ⛔ this touches release-notes territory and therefore comes back as a REPORT, ⛔ never as a rider oncontent/docs/releases/. That directory is release-owned; a code PR editing it is exactly what the guardrail stops.signature/qrcodehave nomaxLengthenforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 is the card's third candidate and has the same release-notes entanglement — same treatment.⛔ What the card explicitly does NOT claim — do not manufacture these
⛔ Confirm or refute every reading above on the current ref before writing code.
premise_still_valid: falsewith no PR is a legitimate delivery.What "done" must show
A drift op that reports nothing is indistinguishable from no drift. Reproduce the silence first — a pre-existing
varchar(255)column with an undeclaredsignature/qrcodefield producing zero drift ops — then show the same case reported. Name a positive control proving the differ can emit an op at all in that same run (the declared-bound row above is the obvious one).Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Dev claim (subagent dispatched from the PM claim in comment 5440308890).
Session:
session_01LZbWd2jNV1FErXTPSS4Dry(same session as the dispatching PM seat — I am theos-devsubagent it launched, not a second independent claimant)Branch:
claude/issue-12121-textfamily-drift-visibility(pushed empty at claim time as the write-route probe; push returned 0)Worktree:
/home/user/objectstack-issue-12121Base:
d29e42f8b1b44b92ea58c1d05e619f27c027c3a9Assignee untouched (
os-zhuang, set by PM).Route acknowledged as a route, not a ruling: inventory first, then choose between the TEXT-family-membership drift op and a report on the manual
os migratestep. Fences acknowledged:packages/specREAD-ONLY,content/docs/releases/**off-limits entirely,sql-driver.tsnot mine (queued card #12593 owns it), #11722 not addressed here and remains open.Deliverable shape: reproduce the silence (undeclared
signature/qrcodeon a pre-existingvarchar(255)column producing zero drift ops) with a positive control in the same run, then the same case reported; at least one ablation with direction and exact count predicted in writing before mutation.Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
os-dev-report
{ "issue": 12121, "status": "done", "branch": "claude/issue-12121-textfamily-drift-visibility", "pr": "https://github.com/objectstack-ai/objectstack/pull/12733", "head_sha": "1a30fa37fdf9bcc7444690f748a60326eb4920e8", "premise_still_valid": true, "premise_evidence": "Re-verified on origin/main at d29e42f8 before writing code. The gate `declaredMaxLength !== undefined` is live in diffManagedTable; controls fired (widen_varchar/narrow_varchar present, enforcesVarcharLength present). Direct diffManagedTable calls confirmed the split exactly as the card states.", "clause2": "holds — no accept/reject door moved, and no migration that rewrites a column type was emitted. The new op is REPORT-ONLY with no reconciler arm (applyDriftOpInPlace matches no case, so os migrate apply reports it skipped). category 'needs_confirm' keeps the artifact boot gate verdict unchanged (it refuses only 'destructive') and keeps dev auto-reconcile away (it applies only 'safe'). Nothing the platform previously accepted is now rejected.", "route_taken": "in-code fix — a drift op keyed on TEXT-family membership rather than on a declared number", "route_measurement": "The inventory chose it, not preference. All five drift-reporting roads funnel through ONE function: boot-time [schema-drift] warn and os migrate plan / apply / the artifact-pinned boot gate / os migrate multi-value-columns all reach diffManagedTable via SqlDriver.detectManagedDrift. Measured silence on the pre-fix tree: 8 text-family types with no maxLength over varchar(255) each returned n=0 on postgres, signature also n=0 on mysql. So repairing the one function repairs every road at once, while the documentation candidate cannot tell a specific deployment whether IT has such a column — which is the actionability the card asked the inventory to establish.", "inventory_found": "Exactly one channel exists today by which a deployment learns, and it is a production write failure. Its message is a poor substitute for a report: the live probe behind objectql/src/driver-fault-redaction.ts measured pg 22001 as 'value too long for type character varying(20)' — identifier only, naming the TYPE not the column — and mysql 1406 as 'Data too long for column label at row 1'. No drift road, no CLI command, no boot diagnostic named the field, the cause or a remedy. Frequency remains UNMEASURED and is not claimed anywhere.", "summary": "Added a report-only drift op `manual_widen_varchar_to_text` in packages/drivers/driver-sql/src/schema-drift.ts, fired on the exact complement of the existing varchar branch (declaredMaxLength === undefined) for a TEXT-family field over a pre-existing character column on an enforcing dialect. Needs no keyed-column input and therefore no sql-driver.ts change: createColumn sizes a text-family column as `keyed ? keyableTextLength(field) : null` and keyableTextLength returns null without a positive-integer bound, so the emitter answers TEXT either way — pinned as such. A distinct op rather than reusing manual_column_type_change because os migrate multi-value-columns selects its ENTIRE population by that op type and would refuse these as remedy_not_recognized while offering a json conversion. The hand-written UNBOUNDED_TEXT_FIELD_TYPES set is held equal in both directions to the driver's own dispatch over every spec FieldType, probed rather than restated.", "tests": "All at head 1a30fa37 (the final commit). (1) BEFORE/AFTER measurement, same diffManagedTable calls: 8 text-family types with no bound over varchar(255) on postgres went n=0 -> n=1 manual_widen_varchar_to_text error/needs_confirm expected 'text' actual 'varchar(255)'; signature on mysql n=0 -> n=1; sqlite stayed n=0. POSITIVE CONTROL in the same run: signature maxLength 4096 stayed n=1 widen_varchar warning/safe on both dialects. NEGATIVE CONTROLS unchanged: real TEXT column n=0, mysql TEXT(65535) n=0, string/email/url/phone/password unbounded n=0, multiple:true still exactly the #11535 manual_column_type_change. (2) typecheck: `pnpm --filter @objectstack/driver-sql exec tsc --noEmit --listFiles` -> VERDICT command-exit 0, and --listFiles names BOTH edited files (1 hit each), so the green covers the new test rather than a program that never read it. (3) full package suite: `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2` -> 'Test Files 141 passed | 8 skipped (149)', 'Tests 2179 passed | 129 skipped (2308)', VERDICT command-exit 0; the 8 skips are live-dialect cells needing real PG/MySQL. Everything ran under scripts/pm/os-verify-lock.sh.", "ablations": [ { "name": "A — remove 'signature', 'qrcode' from UNBOUNDED_TEXT_FIELD_TYPES", "predicted_before_mutation": "RED, exactly 1 of 5 tests fails (the set pin); 4 pass, because the emission loop shrinks with the set — which is precisely why the pin exists", "observed": "1 failed | 4 passed (5); failing test named: 'holds the set equal to the driver text-family branch over every FieldType'", "mutation_confirmed_on_disk": "deleted-text grep count 1 -> 0, run refused outright if the count had not moved", "rebuild": "none needed and MEASURED not assumed — the suite imports ./schema-drift.js in-package and packages/drivers/driver-sql/dist contains ZERO mentions of manual_widen_varchar_to_text while assertions on it pass, so vitest reads source", "restore_proof": "git checkout HEAD -- <abs path> under trap EXIT INT TERM with absolute paths; blob 3c521e01d1fcbaffec0ccc2ebf1e3c8b6c34fc0d == HEAD:packages/drivers/driver-sql/src/schema-drift.ts and git diff HEAD empty" }, { "name": "B — short-circuit the new branch with 'false &&'", "predicted_before_mutation": "RED, exactly 2 of 5 tests fail (the emission test, and the couplings test which destructures undefined); 3 pass as control", "observed": "2 failed | 3 passed (5); failing tests named: 'reports every member exactly once, with the declared-bound row as the control in the same run' and 'cannot refuse a boot, and cannot be claimed by os migrate multi-value-columns'", "mutation_confirmed_on_disk": "injected-text grep count 0 -> 1, run refused outright if the count had not moved", "rebuild": "same as A — source-resolved, no dist involved", "restore_proof": "same blob-hash equality and empty git diff HEAD; final control run on the restored tree: 5 passed (5)" } ], "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-run AFTER the changeset existed (it added 8 changeset-triggered families the first derivation could not name)", "green": 26, "red": 0, "not_measured": 2, "exit_capture": "each gate run into its own log with $? captured immediately, before any pipe", "green_list": "check:nul-bytes, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:driver-conformance, check:objectql-double-limit, check:objectui-changeset, check:page-declaration-shape, check:pm-half-states, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-empty-changeset.mjs, check-ci-filter-parity.mjs, check-comment-mask-adoption.mjs, check-cross-package-test-inputs.mjs, check-plugin-teardown-shape.mjs, docs-audit/check-affected-docs.mjs, docs-audit/check-drift-comment.mjs, pm/release-rehearsal-clone.mjs --self-test", "not_measured_detail": [ "node scripts/pm/check-half-states.mjs exit 3 — its own printed verdict: 'PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential ... Nothing was swept ... it is no reading at all'. Exit 3 is the gate's own prerequisite code, not a failure. Its self-test half (pnpm check:pm-half-states) ran green.", "pnpm check:type-check-debt exit 1 — self-test half green ('47 semantic case(s) + 65 observation case(s) ... hold') and check-type-check-coverage printed 'OK'; only --re-measure refused: 'cannot run: 50 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk ... Build the closure first'. Exactly the refusal dispatch-gates warned of in advance." ] }, "deviations": [ "DECLARED NARROWING on check:type-check-debt's --re-measure half: not run, because it requires a full workspace build (turbo run build over all packages) that CI performs regardless. The narrowing is PROVEN, not assumed, on three readings taken from the gate's own configuration: (1) population — @objectstack/driver-sql appears in NONE of the gate's DEBT / TEST_DEBT / EXEMPT ledger literals in scripts/check-type-check-coverage.mjs, and --re-measure re-measures only ledgered packages; (2) count — the structural half check:type-check-coverage ran and printed 'OK — 65/78 workspace packages type-checked ... 13 in the DEBT ledger ... 1 exempt', exit 0; (3) invariance — driver-sql's tsconfig.json is include ['src/**/*'] with no test exclusion, so its tests sit inside the very tsc program whose green --listFiles run above named both edited files, and this diff touches no other package.", "No repo-wide pnpm lint sweep run locally — CI owns the farm run; the affected package's own typecheck and full suite were run instead.", "Commit messages carry no Co-Authored-By / Claude-Session trailers (an early-commit oversight). NOT repaired, because amending pushed commits would require a force-push, which is forbidden. The branch squashes on merge and the PR body carries the session URL footer." ], "files_changed": [ "packages/drivers/driver-sql/src/schema-drift.ts", "packages/drivers/driver-sql/src/schema-drift.unbounded-text-column.test.ts", ".changeset/drift-unbounded-text-column-over-stale-varchar.md" ], "changeset_grade": "minor on @objectstack/driver-sql — argued, not defaulted. Matches the directly analogous drift-op addition .changeset/drift-detect-multi-value-base-type-mismatch.md (#11535), which shipped minor. Two counts: detectManagedDrift emits a finding on existing deployments where it emitted none, and the exported DriftOp union gains a member (additive for producers, a widening for any consumer switching exhaustively). Not major — nothing removed, renamed or newly rejected.", "fences_respected": { "packages/spec": "untouched (read only, for BOUNDED_STRING_FIELD_TYPES and the maxLength schema)", "content/docs/releases/**": "untouched entirely", "sql-driver.ts": "untouched — read only. The branch was deliberately designed to need nothing from it, which is what freed card #12593's serial slot from any collision.", "#11722": "not touched, remains open; stated as out of scope in the PR body", "#12119": "not revisited, not blamed — the PR body states it delivered its ruling and declared this boundary honestly", "enqueue/auto-merge": "neither — PR opened as draft, landing left to PM" }, "mcp_calls": "9 — issue_read get, issue_read get_comments, add_issue_comment (claim), search_issues (one targeted duplicate check), issue_write (file #12732), create_pull_request, pull_request_read (body read-back), add_issue_comment (this report), issue_read get_comments (read-back of this report)", "open_questions": [], "out_of_scope_findings": [ "filed as #12732: the varchar differ's expected width disagrees with what createColumn would emit for a DECLARED bound, measured in two directions — (a) an unkeyed bounded TEXT-family field ({type:'text'|'richtext'|'signature'|'markdown', maxLength:50}) over a legacy varchar(255) reports narrow_varchar at severity error / category DESTRUCTIVE, which refuses the artifact-pinned boot, for a divergence the write seam already forecloses and whose remedy contradicts the emitter (unkeyed, createColumn makes TEXT); (b) a bound past MAX_VARCHAR_CHARS (16383) — authorable, since FieldSchema declares maxLength as z.number().int().min(1) with no upper bound — plans widen_varchar to varchar(100000), DDL MySQL refuses (ERROR 1074) while Postgres accepts, and is categorised 'safe' so dev auto-reconcile is eligible to run it. Filed unassigned, no labels, for PM triage. Deliberately NOT repaired here: it is the declared half (this card is the undeclared half) and the correct fix needs the keyed-column map threaded from sql-driver.ts, a file this card does not hold." ] }Generated by Claude Code
Generated by Claude Code
docs(cli): correct the drift-op superlative this PR falsifies (#12121)
fix(driver-sql): report an unbounded text-family field left on a pre-…