Skip to content

fix(driver-turso)!: the remote filter compiler refuses the JSON-column family and answers $contains by membership (#21178) - #21208

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21178-remote-json-column-gate
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21178-remote-json-column-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21178
Clause-②: yes (narrowing)

What changes

RemoteTransport.buildWhereSQL (@objectstack/driver-turso, the filter compiler every remote-mode TursoDriver read and filtered write uses) now applies the JSON-column half of the filter contract exactly as the local face (SqlDriver, which local and replica mode inherit) does, from the same shared home in @objectstack/core:

  • Refusal. On a field the driver stores as a JSON TEXT column, every operator in JSON_COLUMN_INCOMPATIBLE_OPERATORS is refused with INVALID_FILTER / 400 before any statement runs: in an operator map (at the top of the per-operator loop, ahead of every arm), in the bare { field: value } spelling, and in the bare { field: null } spelling. The message and the withheld diagnostic are jsonColumnOperatorRefusalText's output, byte for byte the local face's, through this transport's existing withheld-refusal seam ([A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220 provenance, diagnostic sink).
  • Membership. $contains / $notContains on such a column answer membership through jsonMembershipPredicate('sqlite', ...) (libSQL is SQLite); the negated form sits inside nullSafeNegative, so a row with no value satisfies $notContains. A scalar string column keeps the substring test.
  • ⛔ No copy of the set, the sentence or the construct in driver-turso; no remote-only dialect.

The widening (for the contract review)

The transport keeps no schema, so it learns "is this a JSON-stored column" the way it learns every other declared fact, through an injected resolver. This adds ONE optional public method on the exported RemoteTransport class, and one type:

  • RemoteTransport.setJsonColumnResolver(resolver: JsonColumnResolver): void
  • type JsonColumnResolver: a function taking (object: string, field: string) and returning boolean, exported from remote-transport.ts and NOT re-exported from index.ts, like its siblings NonTextColumnResolver and DeclaredValueShapeResolver; it reaches the published .d.ts only as the method's parameter type.

TursoDriver's constructor wires it to the inherited SqlDriver.isJsonColumn, beside setDeclaredValueShapeResolver (constructor wiring only in turso-driver.ts; the upsert regions are untouched). In remote mode registerRemoteFieldMetadata calls registerExternalObject, which fills the same jsonFields registry the local face's gate and membership reading ask, so both faces read one population. A RemoteTransport driven standalone without the resolver treats no column as JSON-stored and compiles as before. This is the fourth sibling of setFilterColumnSql, setNonTextColumnResolver and setDeclaredValueShapeResolver; the last of those shipped as "New optional API" in commit fb386074's changeset. The seat's answer to the fork is on the card (claim amendment 5935032674, option A, open to the maintainer's veto).

Why (measured at base 0b12b9ea, on the libsql SQLite stub harness)

Over a multiple: true lookup holding ["u1","u2"] r1, ["u2"] r2, ["u3","u1"] r3, ["u10"] r4 and null r5, the remote face answered, while the local face answered the right-hand column:

filter remote, before local (the contract)
$contains: 'u1' r1, r3, r4 (u10 by substring) r1, r3
$notContains: 'u1' r2, r5 r2, r4, r5
$nin: ['u1'], $ne: 'u1' all five rows (fail-open) INVALID_FILTER / 400
$eq, $in, bare equality no row INVALID_FILTER / 400
$lt / $lte 'u1' r1-r4 (lexicographic over the serialization) INVALID_FILTER / 400
$startsWith: '[', $endsWith: ']' r1-r4 INVALID_FILTER / 400
$icontains: 'u1' r1, r3, r4 INVALID_FILTER / 400
{ owners: null } r5 INVALID_FILTER / 400
json field, $contains: 'u1' r1, r4 (text inside the object) no row (array-only membership)

Pins

  • turso-local-remote-json-column-parity.test.ts (new): one fixture on BOTH faces; every case asserts remote equals local AND the canonical answer. It iterates the refused set from JSON_COLUMN_INCOMPATIBLE_OPERATORS as it stands (27 members at base), asserting code + status + equality with jsonColumnOperatorRefusalText(...).message (never the literal words); the bare infix spellings in an operator map stay refused on both faces. Also: bare equality, null equality ({f: null}, $eq: null, $ne: null), the gate under $and / $or / $not, the population (a tags field and a json field), count(); membership u1 vs ["u10"], u10, $notContains complement with the NULL row, tags, the json object, bind alignment beside sibling predicates, count(); the scalar text-field control ($contains, $nin, $eq, bare and null equality, $startsWith unchanged); $null / $exists still answered.
  • remote-transport-compile-refusal-seam.test.ts: the enumeration requires every seam refusal method to have a row, so jsonColumnOperator gets one per position (operator map, bare value, bare null) on a half-2 transport told exactly one column is JSON-stored. Policy / author / unmarked / merged-arm provenance all hold.

Ablation (one-time proofs, via scripts/ablation-replace.mjs, restore proven by blob hash and empty git diff HEAD)

The suite imports ./turso-driver.js from source (vitest), so no dist/ leg applies.

  • Unwire the resolver in turso-driver.ts (this.remoteTransport.setJsonColumnResolver( replaced by a no-op call; anchor 1 to 0, blob a9affc72 to b90200ef): the parity suite goes 24 failed / 19 passed of 43, as predicted. Red: all 14 operator-map refusals, bare equality, depth, population, both count() pins, u1 membership, $notContains, the json object, bind alignment, null equality. Still green, as predicted: the 13 bare-infix rows (both faces refuse as an object comparand whatever the gate), the set check, u10 and tags membership (substring coincides), the scalar controls, presence.
  • Disable the membership arm in remote-transport.ts (pushJsonMembership answers false): 5 failed / 38 passed, exactly the five membership pins whose substring answer differs; every refusal pin stays green.
  • Restored: git diff HEAD empty, blob equals HEAD in both legs.

Filter-semantics compile surfaces, one conclusion per face

  1. driver-sql applyFilterCondition (sql-driver.ts): already conformant — it is the contract; the parity suite's local column is its answer, and driver-sqlite-wasm and local/replica driver-turso inherit it. Not edited.
  2. driver-turso RemoteTransport.buildWhereSQL (remote-transport.ts): changed (this PR).
  3. service-analytics compileScopedFilterToSql (read-scope-sql.ts): out of scope — another face; it already reaches the shared membership construct through contains-membership-sql.ts (imports jsonMembershipPredicate).
  4. service-analytics lowerAnalyticsWhere (strategies/filter-normalizer.ts): out of scope — another face, same shared construct as 3.
  5. formula matchesFilterCondition (matches-filter.ts): out of scope — another face, and seat 2's in-flight #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula, having); the memory reference matcher retires (D6) #20822 group 3b owns it.
  6. objectql applyHaving / matchesHaving (having-filter.ts): out of scope — another face (it already imports the shared set and sentence), and seat 2's #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula, having); the memory reference matcher retires (D6) #20822 group 3b owns it.
  7. driver-memory query path (refuses the family since 45ce12a4, filter-refusal.ts imports the shared set) and driver-mongodb translateFieldOperators (mongodb-filter.ts): out of scope — other faces, not touched.

Tests and gates (all on HEAD c67a136e unless noted)

  • pnpm --filter @objectstack/driver-turso exec vitest run --maxWorkers=2: 84 files passed, 2286 tests passed, 33 skipped (exit 0, via os-verify-lock).
  • pnpm --filter @objectstack/driver-turso typecheck: exit 0; tsc --listFiles includes both edited test files.
  • pnpm check:driver-conformance: before (base 0b12b9ea) 50 covered / 0 DEBT / 0 exempt; after (c67a136e) 50 covered / 0 DEBT / 0 exempt. The ledger did not move.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 63 commands at c67a136e; all 63 were run and reconciled with --ran (each line recording its exit code): 62 run, 1 NOT MEASURED. pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: it reads every package's dist/, and a repo-wide build is CI's); narrowed in its place, require of driver-turso's CJS build and import of its ESM build both load and expose setJsonColumnResolver. check-plugin-teardown-shape --self-test and check:lean-entry-closure first exited 3 on prerequisites (a fixture commit outside the shallow clone; objectql's dist/) and exited 0 after fetching that commit and building objectql's closure.
  • check-adr-0087-registration --base origin/main: the changeset reads as declared-breaking with one disposition, not-required (no-migration-prescription); check-changeset-no-major: no major bump.
  • Lint, narrowed to the 4 touched TS files: eslint --no-inline-config --format json read 4 files, 0 errors, 0 warnings. All four are in eslint .'s population (--print-config resolves each). The config never enables type-aware linting (the resolved parserOptions are ecmaVersion and sourceType only, and eslint.config.mjs says so in its header), so this diff cannot move a verdict on any untouched file. The full pnpm lint is CI's.
  • Downstream consumers (cli, qa/dogfood, runtime, service-datasource) are declared to CI: none references RemoteTransport's API, and the one remote-mode consumer test (date-bucket-parity-turso) aggregates without a JSON-field filter.

Changeset

.changeset/21178-remote-json-column-gate.md: @objectstack/driver-turso minor, BREAKING banner, Clause-②: yes (narrowing), ADR-0087 disposition not-required (no-migration-prescription), the new optional API named, and the migration text (write $contains for "holds this member", an $or of $contains for any-of, $not around either for the exclusion, $null / $exists / $empty for presence).

Siblings

Acceptance notes

  • The local face's gate reads the operator, not the comparand, so on a JSON column it refuses the equality spellings of a null comparand ({ f: null }, $eq: null, $ne: null) although IS NULL is a well-formed presence question there; this PR matches it on the remote face (that is the parity invariant), and $null / $exists / $empty remain the presence spellings on both. Noted, not filed: no declared contract says otherwise ($eq is a declared member of the set). Carrier: none.
  • Refusal order differs from the local face only for a doubly-wrong filter: the remote gate runs ahead of each arm's own comparand gate (the remote comparand gate lives inside each arm), so { owners: { $in: [{ ... }] } } reads the JSON-column sentence remotely and the comparand sentence locally. Both are INVALID_FILTER / 400.
  • The bare infix spellings (=, in, …) in an operator map are refused on both faces as an object comparand, with different wording per face; pre-existing, not JSON-specific.

Generated by Claude Code

claude added 2 commits October 1, 2026 15:53
… gate and answers $contains by membership

RemoteTransport.buildWhereSQL now refuses every operator in @objectstack/core's
JSON_COLUMN_INCOMPATIBLE_OPERATORS on a column the driver stores as JSON text,
with the shared jsonColumnOperatorRefusalText sentence, and answers
$contains / $notContains there through jsonMembershipPredicate('sqlite'), the
negated form NULL-safe. The JSON-column population is the driver's own
jsonFields registry, handed down through a new optional
RemoteTransport.setJsonColumnResolver that TursoDriver wires to the inherited
SqlDriver.isJsonColumn, beside setDeclaredValueShapeResolver.

A local/remote parity suite holds both faces to one answer over the whole
shared set, the membership pair, the bare and null equality spellings and a
scalar text-field control.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…-refusal seam table; add the changeset

The seam enumeration requires every refusal method that goes through the
withheld seam to have a row: jsonColumnOperator gets one per position (operator
map, bare value, bare null), on a half-2 transport told that exactly one column
is JSON-stored. The class is read from jsonColumnOperatorRefusalText, never
spelled in the test. The bare-null position of buildWhereSQL now asks the gate
too, as the local face's bare-value positions do.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7213aa27b0367f41d157a772dad1e41ddc8a7626 — the merge of head c67a136e94b876497ea177a6b49cd12fd3b3eefe into base 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7213aa27b0367f41d157a772dad1e41ddc8a7626 && git checkout 7213aa27b0367f41d157a772dad1e41ddc8a7626
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 c67a136e94b876497ea177a6b49cd12fd3b3eefe && git checkout -B drift-repro 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1 && git merge --no-ff c67a136e94b876497ea177a6b49cd12fd3b3eefe

node scripts/docs-audit/affected-docs.mjs --json 0d421041d14cfc716e85e9bdd1da3d57d4eb3aa1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c67a136e94b876497ea177a6b49cd12fd3b3eefe
Local-runs: none

Inputs read: card #21178 (body and all five comments: triage 5933993910, claim 5934703166, os-dev-report 5934971162, claim amendment 5935032674, os-dev-report 5935977677); PR #21208 body, file list (5 files, +553/-0) and the net diff; the check-runs on the head. Verified against origin/main at 5e5ce48c by git show / git grep reads only: the three-dot diff origin/main...c67a136e equals the PR's five-file list, and no commit on main after the branch base 0b12b9ea touches packages/drivers/driver-turso, the two @objectstack/core modules the diff imports, or driver-sql's sql-driver.ts.

① Derived judgments

Accept-set changes, all on the REMOTE face of TursoDriver (RemoteTransport.buildWhereSQL, the one where compiler every remote-mode door shares):

  1. Operator-map gate — on a JSON-stored column every member of JSON_COLUMN_INCOMPATIBLE_OPERATORS (27 at base) is refused INVALID_FILTER / 400 at the top of the per-operator loop, ahead of every arm. Right. Same position as SqlDriver.assertOperatorAppliesToColumn on the local face (the operator-map branch of sql-driver.ts), the set read from @objectstack/core, no copy in driver-turso (the diff's only new imports are the three shared symbols). One ordering difference, named in the PR's acceptance notes: the local face runs its comparand gate before the column gate, the remote face runs the column gate before each arm's comparand gate, so a filter wrong in both ways reads the JSON sentence remotely and the comparand sentence locally. Both are the same ADR-0112 envelope through the same withheld seam; accepted, not a fork of the accept set.
  2. Bare { field: value } gate — implicit = refused on a JSON column, after the comparand gate. Right. The local face's bare-value loop does the same with bare = true, in that order.
  3. Bare { field: null } gate — refused on a JSON column instead of compiling IS NULL. Right. The local face's top-level loop does not branch on null; its = gate fires whatever the comparand, so the previous remote answer (the NULL row) was the fork the card is about. $null / $exists / $empty stay the presence spellings on both faces and the parity suite pins them (§4). The changeset says "whatever the comparand, null included", so a consumer is told.
  4. $contains on a JSON column — membership through jsonMembershipPredicate('sqlite', ...). Right. libSQL is SQLite; the spec's contract (filter.zod.ts, "A JSON-stored column changes what $contains ASKS") declares membership on a multiple: true field or a JSON_COLUMN_TYPES member and substring on a scalar column; the column is emitted as the plain quoted identifier (no storage-form rewrite, as locally), the comparand is handed over raw (as applyJsonMembership does), binds are pushed in placeholder order; the arm order (comparand gate, non-text gate, membership, substring) is the local face's order.
  5. $notContains on a JSON column — NOT of the same construct inside nullSafeNegative. Right. The exact twin of applyJsonMembership with negate = true through applyNullSafeNegative; the complement includes the NULL row, pinned.
  6. The missing-construct guard — a thrown error rather than a fall-through to the substring emitter when the predicate returns no construct. Right (loud, not a fallback); unreachable for the constant 'sqlite' dialect.
  7. Scalar columns, and a transport handed no resolver — unchanged. Right. isJsonColumn answers false without a resolver; the parity suite's scalar title control and the seam test's other rows hold the old answers.
  8. The population — the driver's own jsonFields registry via the inherited SqlDriver.isJsonColumn, filled in remote mode by registerRemoteFieldMetadata calling registerExternalObject (keyed by object name). Right, and it is what triage's "no third copy" and the card's "no remote-only copy" required; no re-derivation from field types in the transport.
  9. Doors — the gate sits in the one compiler, so find, findOne, count, updateMany, deleteMany, aggregate and distinct are covered as the changeset says; the parity suite pins find and count. Right.

Public-surface changes:

  1. RemoteTransport.setJsonColumnResolver(resolver) — one new optional public method on a class index.ts exports. Right to book as a widening (Clause-②: yes): the precedent is commit fb386074 (filter: the engine's compile surfaces answer $empty by the field's declared type (driver-sql and heirs, turso remote, driver-memory, driver-mongodb, formula, objectql having) — ruling A on #20399 #20444), which shipped setDeclaredValueShapeResolver as "New optional API" under Clause-②: yes (widening). driver-turso carries no api-surface baseline, so no gate records this; this record is the control. The wiring in turso-driver.ts is 11 lines in the constructor directly after setDeclaredValueShapeResolver, and the upsert regions are untouched ([security] driver upsert: a tenant-scoped upsert keyed on a globally-unique business column can merge into, and re-parent, another tenant's row #21185 stays region-disjoint).
  2. type JsonColumnResolver — exported from remote-transport.ts, not re-exported from index.ts (index.ts is not in the file list), so it reaches the published .d.ts only as the method's parameter type, exactly as NonTextColumnResolver and DeclaredValueShapeResolver do (index.ts re-exports only FilterColumnSqlResolver). Right, and the PR body states it accurately.
  3. Nothing else moves: no edit to @objectstack/core, driver-sql, spec, docs or workflows; none of the five paths is in GOVERNED_SURFACES; no hand-written page enumerates the transport's setters or states a remote-mode filter semantic, so no docs drift is owed. Right.

Pins:

  1. turso-local-remote-json-column-parity.test.ts iterates the refused set from the module (with a non-emptiness guard so the loop cannot be vacuous), asserts remote equals local AND the canonical answer, compares messages by equality with jsonColumnOperatorRefusalText(...) so [finding] driver-sql's JSON-column refusal is about 800 characters and is cut at the REST envelope's 500, so no caller reads the sentence saying the field and operator were withheld #21067's reword cannot flip them, and covers the card's table, u1 against ["u10"], the scalar control, the tags and json population, depth under $and / $or / $not, count(), bind alignment and presence. Right.
  2. remote-transport-compile-refusal-seam.test.ts: jsonColumnOperator goes through withheldRefusal, so the enumeration half requires a DOORS row; the diff adds one DOORS row and two ARMS rows (bare value, bare null) and injects a resolver naming exactly one column, so every other row compiles as before. Right.

Check-runs on c67a136e, read 2026-10-01T16:50:27Z: Build Core success; Dogfood Regression Gate success (shards 1/3, 2/3, 3/3 success); Temporal Conformance (live PG + MySQL) success; Governed Surface Queue Guard success; Test Core shards 2/6 through 6/6 success, 1/6 in progress; Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates success, Type Check · workspace in progress, and the TypeScript Type Check aggregator they feed not yet created on this head; Lint & Repo Gates in progress; Check Changeset success (the job that runs check-adr-0087-registration and check-changeset-no-major against the merge base); Check PR Size, Check Documentation Links, the three card-claim guards, Auto Label, Dogfood Verify CLI and filter success; Build Docs, Console Pin Gate and Packed-tarball smoke skipped. No run failed or was cancelled. The in-progress required contexts are not yet verdicts; the queue enforces them, and that is not a FAIL here.

② Semver level

.changeset/21178-remote-json-column-gate.md: @objectstack/driver-turso minor, a BREAKING banner, the migration text in prose, and exactly one ADR-0087 marker reading not-required (no-migration-prescription). Matches what the diff publishes.

Clause-②: yes (narrowing)

Read as scripts/pm/clause2-line.mjs reads it: yes because setJsonColumnResolver widens @objectstack/driver-turso's exported surface; (narrowing) because the remote face's accept set narrows (27 operators refused on a JSON-stored column) and $contains / $notContains change rows there. The PR body and the changeset carry the same line, and it matches the claim amendment 5935032674.

③ Boundary flags

Implemented-by: claude/issue-21178-remote-json-column-gate
Reviewed-by: session_017xfMoEjKUuSh2xYB8sCozp

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 16:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 16:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 862f12c Oct 1, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21178-remote-json-column-gate branch October 1, 2026 17:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants