Skip to content

type-compat: the text canonical column's exact field-type set omits signature/qrcode, which the driver now emits as TEXT #12117

Description

@os-warren

Found while implementing #11875 (signature/qrcode joined the driver's TEXT family under the 2026-08-25 maintainer ruling). Filed unassigned, observation-class — no measured consumer breakage; recording the drift so the next reader of type-compat.ts is told.

The observation

packages/spec/src/data/type-compat.ts (CANONICAL_TO_FIELD) maps each canonical SQL column type to the ObjectStack field types it is exactly/lossily compatible with:

  • text: { suggested: 'text', exact: ['text', 'textarea', 'email', 'url', 'phone', 'markdown', 'html', 'richtext', 'code', 'select', 'color'], lossy: [] } — signature and qrcode are absent.
  • binary: { suggested: 'file', exact: ['file', 'image', 'signature'], lossy: ['text'] } — the only place signature appears, reading it as a binary payload.

After #11875, an unbounded TEXT column is the platform's OWN emitted physical shape for signature / qrcode (their stored value is a string — routinely a data-URI — per STRING_VALUE_TYPES and valueSchemaFor, which says z.string() for both). So introspecting a table the driver itself created would find a text column holding a signature and the compat table would not list signature as exactly compatible with it, while binary — a column shape the driver never emits for this type — would.

Why this is a finding and not part of #11875

The compat table serves external-column introspection/import suggestions, a different surface from the authoring/write/storage seams the ruling covered, and its membership semantics ("which field types can this existing column serve") were not part of the ruled route. Whether signature/qrcode (and possibly the binary.exact row) should move is a small self-contained decision for whoever owns this table.

Dedup note

The pre-filing issue search could not run in the filing session (repo-scoped REST returns 403 "GitHub access is not enabled for this session"); local grep of type-compat.ts and its neighbors found no issue reference covering this. If a covering card exists, close this as a duplicate.

Activity

  1. self-assigned this
    on Aug 26, 2026
  2. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round R7, wave 2
    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
    Branch: claude/issue-12117-typecompat-signature-qrcode
    Worktree: objectstack-issue-12117
    Domain: domain:spec
    File surface: packages/spec/src/data/type-compat.ts (CANONICAL_TO_FIELD text/binary rows — premise re-verified on origin/main at fire: signature appears only in the binary row :152, qrcode nowhere), its test twin, regenerated doc projections if any derive from it, changeset (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — dispatch-gates --tier at fire: no path-derived mandate; content limb judged NOT clause-② (a compat/suggestion table, no authored-metadata accept/reject change) — but the path limb (packages/spec/src/**) means the PR passes the clause-② enqueue gate via this seat's CONTRACT_REVIEW_TIER review at ACCEPT (the 2026-08-20 compensating-control pattern)
    Clause-②: no
    Serial constraints cleared: no in-flight PR or claim touches type-compat.ts — wave-1 PRs #12417 (src/kernel/+src/api/+migrations), #12408 (src/ui/+src/data/feed.zod.ts), #12418 (scripts/+lint.yml) all disjoint; wave-2 sibling #12223 (src/api/endpoint.zod.ts, src/cloud/package.zod.ts, docs prose, cli-command-ids baseline) disjoint; H17 on-hold trigger-file index: no hit (field.zod.ts NOT in surface).


    Generated by Claude Code

  3. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Dev claim (os-dev, dispatched by the domain:spec PM seat, R7 wave 2)

    Session: session_01E5LFCYBJ3q2s6yW6oMLxwy
    Branch: claude/issue-12117-typecompat-signature-qrcode (pushed empty at start as the write-route probe; landed)
    Worktree: ../objectstack-issue-12117 from origin/main @ 2e24115

    Scope I am taking: packages/spec/src/data/type-compat.ts CANONICAL_TO_FIELD text row (+ its test twin, doc projection if one derives from this file, changeset per what comparable type-compat PRs did). The binary.exact half is a measurement, not an assumed edit — I grep the monorepo for a real binary-payload signature import path and report the reading; a genuine dual-storage story comes back as an open question rather than a guessed change.


    Generated by Claude Code

  4. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 12117,
      "status": "done",
      "branch": "claude/issue-12117-typecompat-signature-qrcode",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12437",
      "premise_still_valid": true,
      "summary": "Premise re-verified on origin/main @ 2e24115 before editing: `signature` appeared only in the `binary` row (type-compat.ts:152) and `qrcode` nowhere in the file. Added both to CANONICAL_TO_FIELD's `text` row `exact` set, so `isCompatible` now reports a TEXT column as exactly compatible with the two field types the driver itself emits as TEXT after #11875 — before this, `validateObject` introspecting a table the driver had just created reported `type_mismatch` against the very field that wrote the column. Left `binary.exact` unchanged on measurement (see the binary-half entry in `tests`), and recorded the reading in the file plus a test pin, which was #12117's stated purpose. Changeset: `patch` on @objectstack/spec, following the comparable text-family PRs (richtext-code-text-family-emission.md, signature-qrcode-bounded-text-family.md) — this moves exported `isCompatible` behaviour, so not a skip-changeset case. files_changed (3): packages/spec/src/data/type-compat.ts, packages/spec/src/data/type-compat.test.ts, .changeset/type-compat-text-signature-qrcode.md. No doc projection regenerated because none derives from this table: external-catalog.mdx is generated from external-catalog.zod.ts and only names the matrix in two field descriptions; ADR-0015 §4.6 is prose with no copy of the table. Deviations: (1) repo-wide `pnpm lint` left to CI — declared narrowing, not a skip; (2) three gates first returned PREREQUISITE NOT MET on the unbuilt worktree, so the full closure was built and they were re-run green rather than reported as measured from the refusal.",
      "tests": "All gate/test runs went through scripts/pm/os-verify-lock.sh; exit captured before any pipe (`cmd > file 2>&1; EXIT=$?`); every verdict below is the gate's own printed line, never a bare $?. UNION RUN ON FINAL COMMIT c1d4445 (git rev-parse --short HEAD at run time; `git status --porcelain` empty, and nothing was committed after). BUILD FIRST: `pnpm --filter '@objectstack/spec^...' --filter @objectstack/spec build` -> exit 0, 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present'. TESTS: `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/data/type-compat.test.ts src/data/field-value.test.ts` -> exit 0, 'Test Files 2 passed (2) | Tests 36 passed (36)'. TYPECHECK: `pnpm --filter @objectstack/spec run typecheck` -> exit 0, 'check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json'. NOT-MEASURED GUARD: `tsc --listFiles` confirms type-compat.ts is in the main program AND that BOTH type-compat.ts and type-compat.test.ts are in tsconfig.test.json's program — the green covers the edited test file rather than merely reporting over it. REVERSE-VERIFICATION (fix committed first at c1d4445, so the restore leg had a recovery point): predicted direction 'turns red'; mutation = revert the two types out of the `text` row via perl -0pi; mutation PROVEN ON DISK before measuring — worktree blob 5d8c699f07db41cc -> 6fcf721ad3f8b4dc, and the anchored text flipped (grep -c of the removed literal 1->0, of the injected literal 0->1); observed 'Test Files 1 failed (1) | Tests 2 failed | 13 passed (15)', both failures being the two new cases with 'AssertionError: expected false to be true' — the predicted direction. RESTORE LEG proven with equal weight: `git checkout HEAD -- \"$REPO_ROOT/<path>\"` (absolute path, HEAD named so the polluted index is bypassed, never bare `git checkout --`), then `git diff HEAD` empty, worktree blob back to 5d8c699f07db41cc == HEAD blob, grep -c of the restored literal back to 1. The script carried `trap '<restore>' EXIT INT TERM`, but the hash comparison is the proof, not the trap firing. No rebuild was needed on either leg: the test imports './type-compat' relatively inside its own package, so no dist/ sits between mutation and assertion (not a dist-resolved ablation). GATE FAMILIES: re-derived from the ACTUAL diff with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths passed; it read 3 paths vs merge base 2e2411571 — committed 0, working tree 2, untracked 1 at derivation time; --repo assertion held against this checkout's origin). All exit 0: check:nul-bytes ('OK (scanned 6872 text file(s) ... no raw ASCII control bytes)'), check:changeset-gate-self-tests ('116 assertions'), check:cross-package-test-inputs ('OK: 18 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob'), check:merge-driver, check:objectui-changeset, check:page-declaration-shape, check:published-files, check:slot-lookup, check:spec-parsed-alias ('ADR-0122 type-alias convention ... OK'), check:test-source-alias ('OK — 72 packages with tests scanned'), check:type-source-resolution ('OK — 93 tsc program(s) across 77 packages scanned'), check:query-options-erasure ('ratchet holds ... none new'), check:engine-double-contract ('397 (file, verb) row(s) held by the RETAINED ledger'), check:where-matcher ('302 matcher(s) discovered, 302 answer the combinator battery correctly'), check:type-check-coverage ('OK — 65/78 workspace packages type-checked'), check-adr-0087-registration, check-changeset-no-major ('This diff introduces no `major` bump'), check-ci-filter-parity, check-comment-mask-adoption, check-empty-changeset ('No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)'), check-plugin-teardown-shape, docs-audit/check-affected-docs ('457 cases pass'), docs-audit/check-drift-comment ('56 cases pass'), release-rehearsal-clone --self-test, spec check:empty-state, spec check:liveness, spec check:strictness-ledger, spec check:variant-docs. PREREQUISITE-BLOCKED THEN CLOSED: check-dev-prereqs, @objectstack/lint check:doc-formula-expressions and check:type-check-debt first printed 'PREREQUISITE NOT MET — the workspace is not built' (read as NOT MEASURED, not red); the full closure was then built (`pnpm exec turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*` -> 'Tasks: 70 successful, 70 total') and all three re-run exit 0 — check-dev-prereqs '67 package build artifacts present', doc-formula-expressions green, check:type-check-debt 'check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 273.0s, 1843 raw tsc error(s) total, none above its recorded number'. Repo-wide `pnpm lint` NOT run — declared narrowing to CI, which runs the farm exactly once regardless; this is a declared deviation, not a claimed measurement. BINARY-HALF MEASUREMENT (the triage's measurement task) — population: 5182 tracked .ts/.tsx/.mts/.mjs/.js files (`git ls-files | wc -l`). Query 1: files carrying the field type as a quoted literal, `git grep -l -E \"'(signature|qrcode)'\"` -> 20 files. Query 2: of those 20, files ALSO carrying binary-payload vocabulary (Buffer|Uint8Array|ArrayBuffer|bytea|varbinary|BLOB|binary) -> exactly 3, all controlled: (a) drivers/driver-sql/src/sql-driver.ts — its ONLY binary column emission is line 10735 `if (ty.includes('blob') || ty.includes('binary')) return t.binary(c.name)`, which is the REMOTE-COLUMN INTROSPECTION mapping (a foreign column's declared type), never a field-type->column mapping; the file's own `case 'signature': case 'qrcode':` sites at :14015 and :14371 both return the TEXT-family answer; (b) services/service-storage/src/storage-routes.ts:785,819 — a presigned-token CRYPTOGRAPHIC signature, not the field type; (c) spec/src/data/type-compat.ts — the table under review. CONCLUSION: zero paths in this monorepo store a `signature` field value as a binary payload, so no platform write depends on the `binary.exact` entry. Two further controls decided it should nonetheless STAY: (i) the `binary` row is not a stored-value-shape claim — `file` and `image` are FILE_REFERENCE_TYPES whose stored value is a reference-id STRING, so no member of that set round-trips as bytes either; the row is a content-class claim about a remote BLOB column in a FOREIGN schema, the only kind this matrix is ever asked about, and removing `signature` alone would single it out from file/image on the same axis with no evidence asking for that; (ii) membership is many-to-many by construction — a script over origin/main's own table parsed all 16 rows and found 9 field types already exact under MORE THAN ONE canonical column (text -> text,uuid,enum; select -> text,enum; tags -> json,array; multiselect -> json,array; number -> integer,bigint,decimal,float; autonumber, percent, currency, json likewise), and `isCompatible` is keyed on the COLUMN, so the two memberships answer questions about two different columns and can never collide. No dual-storage story was found, so per the triage's own branch condition this returns as a completed measurement with `binary` unchanged, not as an open question.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Contract review (clause-② enqueue gate: path limb packages/spec/src/** hit; declaration limb no, confirmed from the diff — isCompatible is import-suggestion behaviour, no authored-metadata accept/reject change) — PASS, and ACCEPT — reviewer of record: spec seat R7, session_01E5LFCYBJ3q2s6yW6oMLxwy, at CONTRACT_REVIEW_TIER (the compensating control for the opus construction).

    Verified against the PR #12437 diff, read in full (3 files, +82/−1):

    1. text.exact gains exactly signature + qrcode, with the 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 rationale recorded beside the row; nothing else in the table moved.
    2. The binary-half measurement is decisive and properly recorded: population 5182 files → 20 literal mentions → 3 co-occurrences, each controlled (remote-column introspection mapping / cryptographic signature / the table itself); plus the two structural controls (the row is a content-class claim — file/image are reference-id strings too; membership is many-to-many by construction, 9 precedents). Per triage's own branch condition this returns as a completed measurement with binary unchanged — not an open question. The reasoning now lives IN the file and in a test pin, which was the card's stated purpose.
    3. Tests: whole-row regression guard (the next text-family type must be added deliberately) + many-to-many pin; reverse-verification with mutation-proven-on-disk and an equally-proven restore leg.
    4. Changeset patch following the two named comparable precedents; no doc projection derives from the table (checked claim: external-catalog.mdx derives from a different source).
    5. No governed face ⇒ normal queue path.

    Landing: ready-flip + enqueue when EVERY check on PR #12437 is green (CI started 02:47Z). Card stays pm:dispatched until MERGED.


    Generated by Claude Code

  6. removed their assignment
    on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions