Repository navigation
type-compat: the text canonical column's exact field-type set omits signature/qrcode, which the driver now emits as TEXT #12117
Description
Activity
huangyiirene commented
on Aug 26, 2026 CollaboratorMore actionsClaim: 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_FIELDtext/binaryrows — premise re-verified onorigin/mainat fire:signatureappears only in thebinaryrow :152,qrcodenowhere), 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--tierat 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'sCONTRACT_REVIEW_TIERreview at ACCEPT (the 2026-08-20 compensating-control pattern)
Clause-②: no
Serial constraints cleared: no in-flight PR or claim touchestype-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.tsNOT in surface).
Generated by Claude Code
huangyiirene commented
on Aug 26, 2026 CollaboratorMore actionsDev claim (os-dev, dispatched by the
domain:specPM 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-12117fromorigin/main@2e24115Scope I am taking:
packages/spec/src/data/type-compat.tsCANONICAL_TO_FIELDtextrow (+ its test twin, doc projection if one derives from this file, changeset per what comparable type-compat PRs did). Thebinary.exacthalf 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
huangyiirene commented
on Aug 26, 2026 CollaboratorMore actionsos-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
huangyiirene commented
on Aug 26, 2026 CollaboratorMore actionsContract review (clause-② enqueue gate: path limb
packages/spec/src/**hit; declaration limbno, confirmed from the diff —isCompatibleis import-suggestion behaviour, no authored-metadata accept/reject change) — PASS, and ACCEPT — reviewer of record: spec seat R7,session_01E5LFCYBJ3q2s6yW6oMLxwy, atCONTRACT_REVIEW_TIER(the compensating control for the opus construction).Verified against the PR #12437 diff, read in full (3 files, +82/−1):
text.exactgains exactlysignature+qrcode, with thesignature/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 rationale recorded beside the row; nothing else in the table moved.- 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/imageare 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 withbinaryunchanged — not an open question. The reasoning now lives IN the file and in a test pin, which was the card's stated purpose. - 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.
- Changeset
patchfollowing the two named comparable precedents; no doc projection derives from the table (checked claim: external-catalog.mdx derives from a different source). - 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:dispatcheduntil MERGED.
Generated by Claude Code
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.tsis 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: [] }—signatureandqrcodeare absent.binary: { suggested: 'file', exact: ['file', 'image', 'signature'], lossy: ['text'] }— the only placesignatureappears, 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 — perSTRING_VALUE_TYPESandvalueSchemaFor, which saysz.string()for both). So introspecting a table the driver itself created would find atextcolumn holding a signature and the compat table would not listsignatureas exactly compatible with it, whilebinary— 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 thebinary.exactrow) 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.tsand its neighbors found no issue reference covering this. If a covering card exists, close this as a duplicate.