Skip to content

A boolean comparand is judged only at the engine door: the RLS compile seam and analytics NativeSQL pass a string against a declared boolean field as written (the family of #21333) #21376

Description

@objectstack-fleet

Filing gate: ① a defect family with named positions, class (b), a Seam: the spec's boolean comparand verdict, which PR #21372 adds, is consumed by the engine's field-aware filter door alone. Two compilers that build filters outside that door skip it.

  • Position 1: reach: the security exception, because a row-level policy's exclusion is not applied. It was measured in-process, not over HTTP.
  • Position 2: reach: a public door, measured.

Filed by domain:engine#1 (seat post #6367, session_017xfMoEjKUuSh2xYB8sCozp) from #21333's dev report 5948452642 (out_of_scope_findings 1 and 2). PR #21372's at-tier contract review 5948769828 (③) judged both reach readings sufficient and one family card the right carrier. Reader who acts: triage grades and routes. Both positions are in domain:services packages. ⛔ Not a claim.

The family

#21333 (PR #21372, not yet merged at this filing) makes the engine judge a comparand against a declared boolean / toggle field (or a formula returning boolean):

  • true / false pass as written;
  • 1 / 0, "1" / "0" and "true" / "false" narrow to the boolean they name;
  • any other string is refused INVALID_FILTER / 400.

The verdict and its words are a new @objectstack/spec/data contract (filter-boolean-comparand-declared-type.ts). Every filter that reaches engine.find / aggregate / update / delete passes through it. The two compilers below build their filters themselves, so a string against a boolean column reaches the driver as written. On SQLite (SqlDriver) a stored boolean is 1 / 0, and the string 'true' equals neither.

Positions

  1. The RLS compile seam. packages/plugins/plugin-security/src/rls-compiler.ts: the compileCelToFilter output, which the security middleware ANDs into the read after the caller-filter door. Measured by the dev with SecurityPlugin's real middleware over a real ObjectQL on SqlDriver (SQLite), as a member, over two rows (one true, one false), at PR fix(objectql)!: a string comparand against a boolean field is narrowed to its boolean, or refused 400, at the engine filter door #21372's head:

    • a using comparing the boolean field with the boolean literal true shows the true row (the control);
    • the same comparison with the STRING 'true' shows no row;
    • its negation with the string (!=) shows BOTH rows. The exclusion the author wrote is not applied: fail-open on read.
    • A non-canonical string ('yes') shows no row, silently.

    No in-repo producer writes such a predicate (git grep over examples, packages and skills: 0). The seam already runs the number arm of the same family since 7aab75920 (formula: retire F7's whole-day copy (lteBound in matches-filter.ts) now that the RLS write check judges the stored form (#21109, PR #21235); its direct-call cases move to the storage-form lowering #21242): narrowPolicyNumberComparands in judgeCompiledComparands. The boolean arm is its twin.

  2. service-analytics' NativeSQL strategy. It compiles a dataset's runtimeFilter (and the analytics where) to SQL itself, past the engine door. Measured by the dev through POST /api/v1/analytics/dataset/query: RestServer's route handler over AnalyticsServicePlugin's own composition on SqlDriver (SQLite), NativeSQL answering, two rows. Results:

    runtimeFilter answer engine-door answer
    { flag: true } 200, count 1 1
    { flag: "true" } 200, count 0 1
    { flag: { $ne: "true" } } 200, count 2 1
    { flag: "yes" } 200, count 0 400 INVALID_FILTER
    { flag: 1 } 200, count 1 1

Governing text

Scope for whoever takes it (⛔ not a ruling)

Dedupe

Through mcp__github__search_issues, repo-scoped, open and closed, and a scan of all 150 open issues by title and body:

None covers either position.

Dedupe words: boolean comparand RLS seam · using predicate string 'true' · NativeSQL runtimeFilter boolean · analytics where boolean narrowing · boolean declared-type door outside engine


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p2 · domain:services · area:access · pm:blocked. Both compilers outside the engine door consume the spec's boolean verdict

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T09:56Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only.

    Blocked-by: #21333

    Why security. Position 1 lets an authored row-level policy's exclusion go unapplied on read. That is fail-open, of the comparand-spelling class.

    Why p2.

    • The reach needs an authored policy predicate that spells a boolean as text, and no in-repo producer writes one.
    • Position 2 answers a wrong count, not a refusal.

    Raise rule: if the claim's first measurement finds a shipped authoring surface that emits such a predicate, this goes to p1. That covers a template, a skill, or Studio's policy condition builder at the .objectui-sha pin.

    Routing. Both positions are domain:services:

    • position 1, plugin-security rls-compiler.ts;
    • position 2, service-analytics' NativeSQL strategy.

    Why blocked. The verdict both positions consume lands with PR #21372 (#21333). It is now the spec lane's, since triage's answer A. A claim before that merge would build against an unmerged contract.

    Direction: the card's scope, accepted.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Transition: pm:blocked → pm:queue · unlock scan by domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d) after #21333 landed · 2026-10-02T10:45Z · ⛔ Not a claim; no assignee.

  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-02T12:51Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21376-boolean-comparand-compilers
    Worktree: objectstack-issue-21376
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface:

    Direction quoted to the dev from triage 5949734891: "Position 1: the boolean arm sits beside the existing number arm in judgeCompiledComparands. It refuses through the existing refused-comparand route, and narrows a canonical spelling copy-on-write. ⛔ No second rule: the verdict is the spec's. Position 2: the NativeSQL compiler runs the same verdict on the dataset runtimeFilter and the analytics where. It answers what the engine door answers. Pins: each measured cell answers the engine-door column. The negation cell hides the excluded row. The controls are unchanged." Raise rule: "if the claim's first measurement finds a shipped authoring surface that emits such a predicate, this goes to p1."


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21424",
      "issue": 21376,
      "status": "done",
      "branch": "claude/issue-21376-boolean-comparand-compilers",
      "session": "session_01DiCSbmJrkzNhuEAier4VoJ — the dispatching PM session (mode:subagent; the container reads CLAUDE_CODE_REMOTE_SESSION_ID=cse_01DiCSbmJrkzNhuEAier4VoJ)",
      "premise_still_valid": true,
      "summary": "RAISE RULE: no shipped producer found. git grep at 6d67ad5ec over examples, packages (create-objectstack templates included) and skills found 0 RLS predicates and 0 analytics filters that spell a boolean as text. Studio's condition builder at .objectui-sha 89cad75d5 is NOT MEASURED, because objectui is not checked out. Position 1 (rls-compiler.ts): the boolean arm now rides the number arm's walk in judgeCompiledComparands (narrowPolicyNumberComparands -> narrowPolicyComparands, one shared field-spec walker). It reads booleanComparandFieldVerdict / booleanComparandDoorVerdict / booleanComparandRefusalMessage off the guard's existing per-column type map. A refusal takes the existing refused-comparand route: deny sentinel on the read, PERMISSION_DENIED/403 on the write, and a WARN detail rooted at the clause. A canonical spelling narrows copy-on-write. Position 2 (native-sql-strategy.ts): the same verdict runs on the where (the dataset runtimeFilter arrives merged into it), each measure filter and the dataset scope. The condition is lowered by lowerAnalyticsWhere and each member is judged at the column resolveStorageTarget resolves it to. A refusal is invalidFilterError (INVALID_FILTER/400), raised before any statement runs. Every card cell now answers the engine-door column on SQLite and PostgreSQL 16, read and write alike; the negation cell hides the excluded row; the control == true does not move. Measured facts beside the brief: (1) PM assumption 4 is half-falsified. On main the compiled policy filter is NOT shared across requests (compileCelToFilter keeps no cache, and compileFilter runs per read and per write check). The narrowing is copy-on-write anyway, pinned by deep-freezing every compiled filter. (2) The == 1 write cell moves: 403 -> admitted for a true row, because the write check now agrees with the read once 1 narrows to true. (3) After merging main, 45efcfa3d had widened the spec verdict (a number other than 1/0, a Date and an array are refused). Both compilers followed with no code change; a == 2 cell pins it. Write deviation: label-write for the PR assignee os-bill was DENIED by the auto-mode classifier [External System Writes]. It was not retried by any other route, so the PR assignee is unset and needs the seat: node scripts/pm/label-write.mjs --repo objectstack-ai/objectstack --issue 21424 --assign os-bill.",
      "tests": "Base measurements at 6d67ad5ec, by scratch probes deleted before commit. P1: SecurityPlugin over ObjectQL on SqlDriver (SQLite and a private PostgreSQL 16.14). P2: RestServer POST /api/v1/analytics/dataset/query route handler, plus AnalyticsService.query / queryDataset on the native and engine faces. Both reproduce the card table exactly; on PG, 'yes' was read as true at both positions. Pins: rls-boolean-comparand-door.test.ts 26 passed and native-sql-boolean-comparand-door.test.ts 68 passed at ef3ea8700, with OS_TEST_POSTGRES_URL set (SQLite and PG cells both ran). Full test scripts at 78e4f3eb2: plugin-security 161 files, 3513 passed, 45 skipped; service-analytics 169 files, 3828 passed, 123 skipped. Typecheck at ef3ea8700: plugin-security (check:test-typecheck OK) and service-analytics both exit 0. The first run found 2 TS2345 in the new pin; ef3ea8700 fixes them. Gates: dispatch-gates derived 96 at 78e4f3eb2, all exit 0. Four first exited 3 (PREREQUISITE NOT MET) and were rerun after building what they read: check:skill-examples, check:i18n, check:dual-build-cjs-loads, and check:type-check-debt (the last first overran a 9-min timeout, then finished). --ran: 96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN. At ef3ea8700 (the only later commit is a 2-line test type fix): check:type-check-coverage, check:type-check-debt, check:test-source-alias, check:cross-package-test-inputs and check:nul-bytes rerun, all exit 0; the gate list is unchanged. Lint, narrowed at ef3ea8700: eslint --no-inline-config --format json on the 4 changed TS files read 4 files with 0 errors and 0 warnings. --print-config shows no parserOptions.project or projectService for any of them, and eslint.config.mjs states it never enables type-aware linting, so untouched files' verdicts cannot move. .md/.mdx files are outside its files globs. Repo-wide pnpm lint is declared to CI. Ablations at 95bf8c4d0, via scripts/ablation-replace.mjs (each anchor hit 1 -> 0, blob changed, restore blob == HEAD, git diff HEAD empty). The subject resolves via relative import to src/, so no dist leg applies. Results on SQLite: boolean arm removed -> RLS pin 12 failed / 1 passed (negation shows f,t again); RLS narrowing removed -> 7 failed / 6 passed (the refusals stay green); NativeSQL narrowing removed -> 16 failed / 16 passed; NativeSQL verdict call removed -> 22 failed / 10 passed.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "1 relay stroke so far. fleet-write dispatch POST /repos/objectstack-ai/objectstack/dispatches executed pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), as objectstack-fleet[bot] in run 37020447380. The read-back found 12592 bytes sent and 12592 stored, identical. The label-write for the PR assignee was attempted once and denied by the classifier before any request: 0 writes. This os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21376/comments, goes through post-stamped. Plus git push of the branch: the empty-branch probe first, then the WIP commits and the merge commit.",
      "open_questions": [
        {
          "question": "Clause-② arm. The claim and the dispatch fix the line as `Clause-②: no`, so the changeset is patch and carries no arm. Measured, the change narrows the accept set at both positions: { flag: \"yes\" }, { flag: 2 } and the like answered 200 at the analytics native path and now answer 400, and a policy comparing \"yes\" on PostgreSQL was enforced and now drops. The number twin at the same RLS seam (7aab75920) declared `Clause-②: no (narrowing)` with minor, BREAKING. The card's own scope also read it as \"no (narrowing)\".",
          "options": [
            "A — keep `Clause-②: no` + patch, as the dispatch ordered: these inputs were already refused by the engine door, so the platform contract did not change",
            "B — `Clause-②: no (narrowing)` + minor, BREAKING, with an ADR-0087 disposition marker in the changeset, as the number twin did; the seat rewrites the PR body line 2 and the changeset"
          ],
          "recommendation": "B, by the four axes. Real business need: no in-repo producer was found, so the practical blast radius is small either way. Long-term coherence: one declaration convention for the twin arms of one door; A makes the boolean twin read as non-breaking where the number twin read as breaking. Preventing AI authoring mistakes: a CHANGELOG that says BREAKING is what an upgrading agent greps after a 400, while A hides the refusal behind a patch. Startup scope: B adds no gate and no surface, only the honest label. Left for the seat, because the dispatch fixed A."
        }
      ],
      "out_of_scope_findings": [
        "class: b · reach: measured at a public door's handler on SQLite at 78e4f3eb2 — AnalyticsService.query, which POST /analytics/query relays verbatim, with the NativeSQL face over a number field. { amount: \"abc\" } answered 200 count 0, { amount: { $lte: \"9999-12-31\" } } 200 count 2, { amount: { $ne: \"abc\" } } 200 count 2, and { amount: true } 200 count 0, where the engine-door (ObjectQL) face answered INVALID_FILTER / 400 for each. · contract: filter-number-comparand-declared-type.ts, \"the door refuses a non-numeric string against a number field with INVALID_FILTER / 400, naming the field, on every driver and position, before any bind\" · Seam: spec:numberComparandDoorVerdict → runtime:NativeSQLStrategy.compileClauses (packages/services/service-analytics/src/strategies/native-sql-strategy.ts) · this card's family: the number arm of the same declared-type door skipped by the same compiler. Hand it to the family close-out card rather than a single-point card. The fix shape is this PR's judgedBooleanComparands walk with numberComparandDoorVerdict as a second arm. · dedupe words: NativeSQL number comparand · analytics where numeric string INVALID_FILTER · native-sql number door · runtimeFilter number comparand refused · declared-type door outside engine analytics",
        "carrier: 承接者:无 · noted, not filed — RlsFieldGuard.number now feeds both declared-type arms (it always recorded every declared column); a class-neutral rename would touch security-plugin.ts, outside this card's surface. Recorded in the PR's Acceptance notes only."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: REVISE (declaration only) · PR #21424 at ef3ea8700 · 2026-10-02T14:39Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ · under triage 5949734891. ⛔ Classes and positions only.

    The code is accepted as built. The PR comes back for its declaration line only. No code change is asked.

    Accepted (read against the diff)

    • Position 1: the boolean arm rides the number arm's walk in judgeCompiledComparands (narrowPolicyComparands, one shared walker), reads the spec's booleanComparand* verdicts off the guard's type map, and refuses through the existing refused-comparand route: the deny sentinel on read, 403 on write. ⛔ No second rule.
    • Position 2: native-sql-strategy.ts runs the same verdict on the where (the merged runtimeFilter), each measure filter and the dataset scope, at the column resolveStorageTarget resolves, and refuses INVALID_FILTER / 400 before any statement runs.
    • Raise rule: no in-repo producer was found (0 hits). Studio's condition builder is NOT MEASURED. p2 stands.
    • Measured shifts, accepted as consequences of the ruling:
      • Zone 2 assumption 4 was half-falsified: the compiled filter is not shared, but copy-on-write is pinned anyway by deep-freezing it.
      • The == 1 write cell now admits a true row, agreeing with the read.
      • 45efcfa3d's widened spec verdict is followed with no code change, pinned by a == 2 cell.
    • Proof: pins on SQLite and PostgreSQL 16; ablations (4 legs) all red; 96 gates derived and run.

    Revise: the dev's open question 1 → B, by this seat.

    So:

    1. The changeset:
      • bump both packages to minor;
      • the declaration line reads Clause-②: no (narrowing);
      • add the **BREAKING** banner (the launch-window convention for accept-set narrowings);
      • add the ADR-0087 disposition marker, reasoned like the number twin's (no authorable key, spelling, export or stored shape moves; which boolean the author meant is not a ledger rewrite);
      • keep the one-line fix: write true / false.
    2. PR body line 2: the same declaration (one body PATCH, read back).
    3. Re-run the changeset gates (check-adr-0087-registration, check-changeset-no-major and the rest that dispatch-gates.mjs --commands names for .changeset/**), merge origin/main if it has moved, push and report.

    Write denial, recorded: the dev's label-write (PR #21424 assignee os-bill) was denied by the session's auto-mode classifier. ⛔ The seat does not re-run a write denied to its dev. It is reported to the maintainer in chat, and the assignee stays unset until the maintainer acts or allows it.

    Out-of-scope:

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21424",
      "issue": 21376,
      "status": "done",
      "branch": "claude/issue-21376-boolean-comparand-compilers",
      "session": "session_01DiCSbmJrkzNhuEAier4VoJ — the dispatching PM session (mode:subagent; the container reads CLAUDE_CODE_REMOTE_SESSION_ID=cse_01DiCSbmJrkzNhuEAier4VoJ)",
      "premise_still_valid": true,
      "summary": "REVISE round (declaration only, PM review 5954832189, open question 1 answered B). No code change. The changeset now reads: both packages minor; Clause-②: no (narrowing); the BREAKING banner (\"ships as minor under the launch-window convention for accept-set narrowings\"); one ADR-0087 marker, not-required (no-migration-prescription), reasoned as the number twin 7aab75920 reasoned (no authorable key, spelling, export or stored shape moves; which boolean the author meant is not a ledger rewrite; the other categories are closed on facts); and the one-line fix, write true / false. PR #21424 body line 2 is now `Clause-②: no (narrowing)`, through one issue_patch via the relay: 12604 bytes sent and stored, identical. The diff against the stored body is line 2 only. origin/main was merged at 2fd5e16a9. After that merge, bdd3654f2 landed on main. It touches lint, metadata-protocol and spec ui/system only (three-dot file list: 0 files in either package or this PR's paths), so it was not merged again; the queue rebuilds onto current main. AGENTS.md was not touched by any gate (tree clean after every run). The PR assignee was not re-attempted, as ordered.",
      "tests": "All at head 2fd5e16a9 (pushed; the remote branch equals local). Changeset gates: node scripts/check-adr-0087-registration.mjs --base origin/main exits 0 and reads the changeset as [BREAKING+clause-②-narrowing], not-required (no-migration-prescription), accepted. check-changeset-no-major --base origin/main exits 0, and driven offline with the live PR #21424 payload (--event) its LEVEL AXIS reads \"declares clause-② no (narrowing), and no package whose packages/**/src/** it moves is graded patch\". check-empty-changeset --base origin/main exits 0 (\"1 declaring changeset(s) added; no changeset from the merge base modified or deleted\"). The rest dispatch-gates names for .changeset/** also exit 0: check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census, check:published-files, and the --self-test of adr-0087-registration, changeset-no-major and empty-changeset, release-rehearsal-clone and release-pending-publish. The full derived union was rerun: dispatch-gates --commands at 2fd5e16a9 derives 96 (the same list as before), all 96 exit 0 after a full turbo build, and --ran reads 96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN. Test scripts: pnpm --filter @objectstack/plugin-security test gives 161 files, 3513 passed, 45 skipped, exit 0; pnpm --filter @objectstack/service-analytics test gives 169 files, 3828 passed, 123 skipped, exit 0. CI on 2fd5e16a9 as read: 42 check runs, 38 success, 4 skipped, 0 failure; Test Core 1/6 to 6/6, Check Changeset and Lint & Repo Gates are success. The import-template-route timeout tracked as #21428 did not recur on this head.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "This round, 2 relay strokes as objectstack-fleet[bot] through POST /repos/objectstack-ai/objectstack/dispatches. (1) issue_patch, PATCH /repos/objectstack-ai/objectstack/issues/21424 (PR body line 2), run 37022425470: 12604 bytes sent and 12604 stored, identical. (2) This os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21376/comments. Plus git push of the changeset commit 45a4b8f9d and the merge commit 2fd5e16a9 (plain commits, no force push, rebase or amend). The previous round's writes stand: pr_create (#21424) and the first os-dev-report comment 5954735361. The PR-assignee write was not re-attempted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: #21426 (filed by the seat) · the native-SQL number arm, class b, measured in the previous round; nothing new this round",
        "carrier: 承接者:无 · noted, not filed — RlsFieldGuard.number naming, in the PR's Acceptance notes only"
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: ACCEPT · PR #21424 at 2fd5e16a9 · 2026-10-02T15:25Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ · follows REVISE 5954832189. ⛔ Classes and positions only.

    The declaration-only revision is in 45a4b8f9d, read against the diff:

    • Both packages are minor.
    • The changeset reads Clause-②: no (narrowing), with the **BREAKING** banner and one ADR-0087 not-required (no-migration-prescription) marker reasoned like the number twin (7aab75920).
    • The one-line fix, "write true / false", is kept.
    • PR body line 2 matches (one PATCH, read back).
    • No code changed.
    • check-adr-0087-registration reads it as [BREAKING+clause-②-narrowing] and accepted. check-changeset-no-major's level axis, driven with the live PR payload, is satisfied.

    Head and landing

    Open item, outside this PR's merge: the PR has no assignee. The dev's label-write was denied by the session's classifier, and ⛔ the seat does not re-run a denied write. It is reported to the maintainer and does not gate the queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions