fix(lint)!: refuse a sharing-rule condition that compares a field with a json or multiple field when it is authored - #20375
Conversation
…h a json or multiple field Stage 2g of #19886 (the sharing-rule twin of stage 2f). The condition lowers, so the seeder seeds the rule, and driver-sql then refuses every criteria query it runs by declared type; SharingRuleService reads the refusal as matching no record, so the rule grants nothing. validateSharingRuleEnforceability now reports it, through the RLS rule's listHoldingComparisons (exported, not copied). Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…olding arm Clause-2: no (narrowing), BREAKING; ADR-0087 not-required (already-registered cel-predicate-one-value-comparand-refused). Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
…olding arm Building the whole stack's object graph up front threw on an unreadable reference carrier before the anchor arm could refuse it with its own label, flipping the #18550 pin. The arm now indexes the rule's anchor lazily, once, and only for a condition that lowered: the only object a lowered sharing criterion can address. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f4528129143f4ddc8a26aeea21fa415fd82e6c1f && git checkout f4528129143f4ddc8a26aeea21fa415fd82e6c1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5a6267f4862556fd7422cd161cd931b03aa17b72 79a2ad1e5146fb987c280fa585bd93b7b963580a && git checkout -B drift-repro 5a6267f4862556fd7422cd161cd931b03aa17b72 && git merge --no-ff 79a2ad1e5146fb987c280fa585bd93b7b963580a
node scripts/docs-audit/affected-docs.mjs --json 5a6267f4862556fd7422cd161cd931b03aa17b72
|
Contract reviewServed-tier: Read: the PR body, the diff against the merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Blocking items: none. Every judgment in ① is RIGHT on my own measurements; the semver level, the ADR-0087 disposition and every changeset sentence in ② are right; ③ carries one unit-level cell no door can reach, the quieter per-write diagnostic, the three out-of-scope items and a clean landing state, none this PR's regression. |
…o fields of different comparison classes when it is authored (objectstack-ai#20347) (objectstack-ai#20403) Fixes objectstack-ai#20347 Clause-②: yes (narrowing) The spec half of the objectstack-ai#20347 triage split (`5862073027`), dispatched on claim `5863797885`. Base `eee09742`, head `bef47d1d`. The engine half is objectstack-ai#20355, which stays open and reads the export this PR adds. The changeset declares `Clause-②: yes (narrowing)`, BREAKING, `minor` on both `@objectstack/spec` (new exports, a widening) and `@objectstack/lint` (a new authoring refusal, a narrowing). ## What changes - **One classification, exported once** (`packages/spec/src/data/filter-cross-field-comparison-class.ts`, re-exported from `@objectstack/spec/data`, beside `filter-text-operator-declared-type.ts`). - Six classes (`CROSS_FIELD_COMPARISON_CLASSES`: `numeric`, `text`, `boolean`, `date`, `datetime`, `time`) and three families with none (`CROSS_FIELD_NO_CLASS_REASONS`: `list-or-object`, `file`, `formula`). - `CROSS_FIELD_COMPARISON_TYPE_CLASSES` classifies every `FieldType` member exactly once, by reference to the existing `field-value.zod.ts` sets. Nothing is re-listed. - Two pure verdicts. `crossFieldColumnVerdict(field)` answers one declared column; `multiple: true` on a multi-capable type holds a list. `crossFieldComparisonVerdict(left, right)` answers two: `comparable`, `cross-class`, `no-class`, or `unjudged` for a type outside `FieldType`. - It is lifted case for case from driver-sql's module-private `crossFieldComparisonClass` (the objectstack-ai#5222 boundary). `sql-driver.ts` is untouched: objectstack-ai#20355 rewires it, and PR objectstack-ai#20372 holds that file. - **Parity with driver-sql, run against both** (`packages/drivers/driver-sql/src/sql-driver-20347-cross-field-class-parity.test.ts`). One object declares every `FieldType` member (49), plus the 6 multi-capable members flagged `multiple: true`. Every ordered pair (55 × 55 = 3,025) is compiled as `{ a: { $eq: { $field: b } } }` on a real `:memory:` SQLite driver. The driver's admit or refuse must equal `crossFieldComparisonVerdict(a, b) === 'comparable'` on every pair. A refusal counts only in the cross-field boundary's own withheld `INVALID_FILTER` / 400 form (`withheldFilterDiagnosticOf` non-null), never by prose. - **The authoring door** (`packages/lint`). - `validateRlsPredicateEnforceability` gains a cross-class arm. `crossClassComparisons` reads the lowered filter's `{ $field }` sites against the declared field map. It reports `rls-predicate-unenforceable` for every comparison whose two columns are not `comparable`: `==`, `!=`, `>`, `>=`, `<`, `<=`, either side, under `!` too. - It covers `using` and `check` on every operation. - `validateSharingRuleEnforceability` reads the same function and reports `sharing-rule-unlowerable-condition` on a sharing rule's lowered `condition`. - A comparison against a list or an object stays the existing objectstack-ai#19886 arm's finding, so no comparison is reported twice. The new arm runs ahead of the engine-judge pass, like the list arm: one defect, one finding. - The finding names each comparison, each column's declared type and class (or why it has none), and the clause's measured run-time consequence. The hint lists every class with the declared types it holds, derived from the spec table. ## Measured before (lint as on `main`), then after Real `os validate` (`packages/cli/bin/run-dev.js validate` on a probe stack), plus the real plugin-security + ObjectQL on driver-sql (`better-sqlite3` `:memory:`, one RLS policy on a `text` / `number` / `image` / `formula` object). | predicate | `os validate` before | `find` (`using`) | insert (`check`) | insert (`using` as check) | by-id update / delete (`using`) | `os validate` after | |:--|:--|:--|:--|:--|:--|:--| | `record.status != record.amount` (text vs number) | valid, exit 0 | `INVALID_FILTER` / 400 | admitted, stored | admitted, stored | 403 / 403 | `rls-predicate-unenforceable`, exit 1 | | `record.status != record.photo` (text vs image) | valid, exit 0 | 400 | admitted, stored | admitted, stored | 403 / 403 | refused, exit 1 | | `record.status != record.is_open` (text vs formula; the card's NOT MEASURED cell) | valid, exit 0 | 400 | admitted, stored | admitted, stored | 403 / 403 | refused, exit 1 | | `record.amount > record.status` (number vs text) | — | 400 | 403 (JS `5 > 'open'` is false) | 403 | 403 / 403 | refused (lint unit and door pins) | | control `record.status != record.note` (text vs text) | valid, exit 0 | rows `[r1]` | admitted | admitted | updated / deleted | valid, exit 0 | The `check` rows on `insert` read the same at `os validate`: valid before, `rls-predicate-unenforceable` after. Sharing-rule conditions, measured at the real `os validate`, first with the arm ablated (the before-state) and then restored: `record.status != record.amount` and `record.status != record.photo` went from valid (exit 0) to `sharing-rule-unlowerable-condition` (exit 1). The control `record.status != record.note` stayed valid. At run time the seeded rule's criteria query meets the same driver-sql refusal the list-holding class meets (objectstack-ai#20375 measured that path). The write-check answer is whatever JavaScript's comparison of the two raw values gives, so the permissive side of the policy is the write. That half is objectstack-ai#20355's. ## Census (expected 0): 0 A script over `git ls-files examples packages` (tests, fixtures, docs, generated bundles excluded; 3,140 files at `bef47d1d`) extracts every `using` / `check` / `condition` string literal: 163. It lowers each through the real `compileCelToFilter` (RLS through `sqlPredicateToCel` first); 105 lower. It then lists every `{ $field }` comparison: 2. - `examples/app-showcase/src/data/hooks/index.ts:88`: `record.spent > record.budget`, a hook condition, both `number`. - `packages/lint/scripts/check-doc-formula-expressions.mjs:1396`: `record.a > record.b`, a gate fixture. Neither is an RLS predicate or a sharing-rule condition, and both are same-class. The only programmatic predicate constant is `OWNERSHIP_FLOOR_PREDICATE` (`created_by == current_user.id`), which is not field-to-field. So no shipped policy or sharing condition moves, and nothing re-grades to p1. The cloud repository was not in this session: NOT MEASURED. ## Ablation (one-time proof, committed state `bef47d1d`) Two ablations, both run from the committed state `bef47d1d`, each through `scripts/ablation-replace.mjs`. That tool landed each mutation (anchor count 1 to 0, blob changed) and restored it (the blob equals `HEAD`, and `git diff HEAD` is empty). A shell `trap` re-checked each restore by hash. The direction observed is the normal one: red. 1. **The lint arm.** The guard line in `crossClassComparisons` was replaced with an unconditional `continue`, so the arm reports nothing. `ablation-dist-preflight` found the marker in 4 built `@objectstack/lint` files, so the mutation reached the `dist/` the CLI consumes. - lint unit, the four cross-class and list-holding files: **525 failed / 485 passed** of 1,010. Restored: **1,010 / 1,010 passed**. - CLI integration `rls-policy-authoring-admission.test.ts`: **6 failed / 33 passed**. The 6 are exactly the new REFUSED rows. Restored: **39 / 39 passed**. - Real `os validate`, 9 cells. Ablated: all nine exit 0 with no finding, which is the before-state, sharing cells included. Restored: the 3 RLS `using` cells, the 2 RLS `check` cells and the 2 sharing cells exit 1, each with exactly one finding; both controls exit 0. - On restore, `ablation-dist-preflight --absent` passed its `dist/` reading (the marker is absent from all 14 built files). Its tree reading exited 3 only because two untracked scratch files were present at that moment; both are deleted now. 2. **The driver half of the parity pin.** Temporarily, never committed: in `sql-driver.ts`'s `crossFieldComparisonClass`, `if (type === 'time') return 'time'` was changed to return `'datetime'`. The parity test imports driver source, so no build was needed. Result: **2 failed / 54 passed**. The two are `f_datetime` and `f_time`, naming exactly `f_datetime vs f_time: spec says cross-class, driver admitted` and its mirror. Restored: **56 / 56 passed**, blob equal to `HEAD`. ## Tests (at `bef47d1d`) All at `bef47d1d`, after the last commit, on a shared box. - `@objectstack/spec` - `vitest run --project local src/data`: 103 files, **3,458 passed**, 1 todo. The new classification test contributes 19. - `typecheck` (tsc, scripts and the test layer): exit 0. - `@objectstack/lint` - `pnpm test`: 115 files, **5,314 passed**. - `typecheck` (with the test layer): exit 0. - `@objectstack/driver-sql` - The parity test plus the two existing cross-field suites (`sql-driver-cross-field-reference`, `sql-driver-cross-field-conformance`): **221 passed**, 2 skipped. The parity test alone: 56 passed, one test per probe column (55 × 55 pairs), plus the coverage pin. - `typecheck`: exit 0. - `@objectstack/cli` - `--project integration test/rls-policy-authoring-admission.test.ts`, the only CLI file touched (integration tier): **39 passed**, 9 of them new. - `typecheck`: exit 0. - The unit tier is declared to CI: no CLI source file and no unit-tier file changed. - Real `os validate` over the examples: `app-crm`, `app-multi-package` and `app-todo` exit 0, with 0 `rls-predicate-*` / `sharing-rule-*` findings. `app-showcase` is NOT MEASURED this way: its config imports `@objectstack/connector-mcp`, which is outside this worktree's build closure. Its security files are in the text census above. - Spec generated artifacts: `check:generated` named `api-surface/` and `export-origins/` stale, both additive only. Both were regenerated with their generators, and `check:api-surface` and `check:export-origins` are green. - Gates: `dispatch-gates --ran` accounts for 88 of 88 derived families. 86 exited 0. Two are NOT MEASURED, and CI owns both: - `check:dual-build-cjs-loads` answered PREREQUISITE NOT MET: it needs a full `pnpm build`. - `check:type-check-debt`: its `--re-measure` passed the 400 s local timeout. The kill left `packages/spec/dist` without declarations, so the spec was rebuilt (64 `.d.ts`) before every lint, driver-sql and cli reading above. - The derivation warned that the tree is behind `origin/main` by one family file (`scripts/cross-package-test-inputs.mjs`). `check:cross-package-test-inputs` was run from this tree and is green. ## Decisions - **Formula has no class, whatever its `returnType`.** That is driver-sql's answer: a formula is virtual, with no column to reference. The text-operator door reads `returnType`, but a column-to-column comparison needs a column on both sides. The measured runtime agrees (400 on the read). - **The file family is refused by name.** That is driver-sql's answer too (the ADR-0104 dual-encoding window), so `image == image` is refused as well. - **A type outside `FieldType` is `unjudged`.** A driver's aliases (`integer`, `object`, the absent-type `string` default) stay layered in the driver, as `field-value.zod.ts`'s header says every alias does. objectstack-ai#20355's rewire keeps those aliases above the export. At the door, an out-of-vocabulary type is Zod's to refuse, and the arm reports nothing. - **Registry-injected columns are judged** by the definition the registry provisions. `record.status != record.created_at` is refused (text vs datetime), because the driver sees the same column. `id` has no definition in the graph, so it is not judged. - **Same rule ids as the list arm.** The author's edit is the same kind: rewrite which two columns are compared. - **Two existing pins changed**, one in each objectstack-ai#19886 list-holding test. "A single-valued `file` field is one value" asserted *no finding at all* for `record.status != record.subject` with `subject` a single `file`. driver-sql refuses that comparison (the file family has no class), so the no-finding reading was never the runtime's. Each pin now asserts that the list arm stays silent and the class arm refuses once. `select` / `lookup` / `user` keep the no-finding pin. - **File surface beyond the claim, both required by the dispatch.** The driver-sql parity test: the classification can only be run "against both" there, and it adds no line to `sql-driver.ts`. And `validate-sharing-rule-enforceability.ts` plus its tests: the direction covers sharing conditions, and that rule is where they are judged. ## Acceptance notes - `listHoldingComparisons` still reads `STRUCTURED_JSON_TYPES` + `isMultiValueField` directly. That is the same family as the export's `list-or-object` reason, and the two agree by construction (pinned in the spec test), but it is two spellings. Converging it onto `crossFieldColumnVerdict` is the natural edit for whoever next touches that function (carrier: objectstack-ai#20355 or the next objectstack-ai#19886-family change). Noted, not filed. - The metadata save door for a `sharing_rule` does not run `validateSharingRuleEnforceability`, as objectstack-ai#20375 recorded. The new sharing arm therefore shows at `os validate` / `os build` / `os lint` only, like the list arm. Noted, not filed. - The `check` consequence sentence describes today's write check, which admits by raw comparison. When objectstack-ai#20355 moves the write check onto this classification, that sentence changes in the same change (a code comment at `crossClassConsequence` says so). --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19886
Clause-②: no
Stage 2g of #19886: the sharing-rule twin of stage 2f, and the only remainder item of release
5861981062. Seat answer A in the 2f ACCEPT (5861343773): "Stage 2g (thevalidate-sharing-rule-enforceability.tsarm) measures the sharing runtime (rule-criteria.ts→engine.find→driver-sql) FIRST. It reuses this PR's classification". Claim5862004488. Base67047171, head79a2ad1e. The changeset declaresClause-②: no (narrowing), BREAKING,@objectstack/lintminor, following the 2d / 2e / 2f precedent.What changes
validateSharingRuleEnforceability(packages/lint) gets one arm. When a sharing rule'sconditionlowers, the arm reads the lowered filter against the declared field map of the rule's anchor object. It reportssharing-rule-unlowerable-conditionfor every field-to-field comparison (==,!=and the four ordering operators, either side, under!too) in which either column is DECLARED to hold a list or an object.listHoldingComparisonsfromvalidate-rls-predicate-enforceability.ts, the 2f helper. That helper reads the spec'sSTRUCTURED_JSON_TYPESandisMultiValueField, the two sets driver-sql refuses such a comparison by. The share is the smallest one that works:exportonlistHoldingComparisonsand on itsListHoldingComparisonresult type, plus one comment sentence. No RLS line changes behaviour, and the 2f pins run unchanged (see Tests).#18550pin red in the first run. The lazy anchor-scoped graph keeps that pin byte-identical.sharing-rule-unlowerable-condition. The fix is the same rewrite of the condition, and the same class's literal spelling (record.status == ['a', 'b']) is already reported under that id, refused by the compiler rather than by the driver. The id's one-line doc now names both halves. A separate id was weighed and not taken (see Decisions).os validate/os build/os lintrun this rule. The metadata save door for asharing_ruledoes not run it, as before (see Acceptance notes). No gate is added and no runtime seam changes.Files:
exportkeywords, one comment sentence);The cli admission parity test (
rls-policy-authoring-admission.test.ts) has no sharing-rule family, because the sharing rule never crosses the save door, so it is untouched.The refusal text (new)
Message, from the real
os validateat head:Hint:
The class sentence is the 2f RLS arm's sentence, word for word; a pin holds the two equal. The hint drops the RLS hint's
current_useralternative, because a sharing condition that readscurrent_useris refused (sharing-rule-runtime-variable-condition).Zone 1 P1, measured FIRST (before any edit, at
67047171)A probe drove the real
SharingServicePluginover a realObjectQL:init, thenstart, then thekernel:readyhandlers: seeding throughbootstrapDeclaredSharingRulesfromregisterApp'ssharingRulescollection, and hook binding;kernel:bootstrappedbackfill;Setup:
privateobject;statusis a member oftags, so a rule that "should match" exists;jsonlist and object,multiplelookup,multiselect,multipleuser;==,!=,!(==),>, less-or-equal; both orders)!=/==text, literal,json != null)criteria_json; every criteria queryINVALID_FILTER/ 400; one WARN[sharing-rule] criteria query failedper rule at the backfill; 0 grants; the recipient reads 0 rows; after the insert and update, still 0 grants, with only engine-levelFind operation failedwarns that name no rule!=text: r1, r2, then r4 after the insert){ $field }comparand as a literal:!=grants every row and==/ ordering grant nothing==text grants nothing) — this is #15104, not the list classmongodb-filter.ts) refuses every{ $field }comparand, scalar or list.SqlDriver. The remote transport was not measured.Verdict: P1 holds. On the SQL drivers the runtime refuses the comparison, and the sharing path turns the refusal into a silent zero-share: the rule is declared, seeded, and grants nothing. The prohibition does not apply.
Zone 2, measured
1. The authoring doors.
json,address,multiplelookup,multiselect,multipleuser) × 2 orders, plus a list-against-list cell and a compound one;node packages/cli/bin/run.js validate --json) and the save door (saveMetaItem({ type: 'sharing_rule' })over a realObjectQL, sqlite-wasm).67047171(base)current_user→ runtime-variable)79a2ad1e(head)2. The runtime. P1 above. The door that answers is the driver, through
engine.find: driver-sql'scrossFieldComparisonClass, by declared type.plugin-sharingcatches the refusal (findMatchingRows/matchRecord) and reads it as "no match".3. Where the arm lives.
compileCelToFilter(condition, { variables: {} }), the seeder's exact call, returns the same loweredFilterConditionshape the RLS rule walks. The probe's seededcriteria_jsonshows it:{"status":{"$ne":{"$field":"tags"}}}and{"$not":{"status":{"$eq":{"$field":"tags"}}}}. So the smallest arm is one call to the 2f helper onresult.filterwhenresult.ok.4. Census (narrowing).
67047171,packages/**+examples/**, non-test: 3 declared sharing-rule conditions (app-showcase×2, aqa/downstream-contractfixture), all field against literal. 0 field-to-field, 0 against ajson/multiplecolumn.main96eb092(thels-remotetip): 0 declared sharing-rule conditions.os validateat head overapp-crm,app-multi-package,app-showcaseandapp-todo: 0sharing-rule-*findings. Error and warning counts are unchanged from base for the three apps that ran there.app-showcasecould not load at base before its dependency closure was built, and at head it is valid with 0 errors.Tests
All at head
79a2ad1e, in one run throughos-verify-lock.sh(VERDICT command-exit 0, held 603s on a shared box):@objectstack/lintbuild exit 0 (DTS emitted, 4/4 declaration files).@objectstack/lint, the targeted five files (the new pin, the existing sharing tests, the 2f RLS list-holding pin, the rule-id barrel and the wiring guard): 5 files / 480 tests pass.@objectstack/lint, full package: 113 files / 4704 tests pass. The existing sharing-rule pins and the 2f RLS pins run unchanged, including the#18550unreadable-carrier pin that the first draft of this arm turned red.@objectstack/linttypecheck: exit 0 (source and test layer).@objectstack/cliunitproject: 230 files / 3296 pass.integrationproject, the admission parity pin: 30 / 30 (named by the dispatch as a consumer suite; not touched).@objectstack/plugin-sharing, full package (read-only consumer; no pin flips): 37 files / 913 pass.The new pin has 64 tests:
os validaterule table.The first run was at
53275286, before the lazy-graph fix:lintfull 1 failure out of 4704, the#18550pin;cli unit230 / 3296;plugin-sharing37 / 913. That failure is why the graph is anchor-scoped and lazy.Ablation
From committed
79a2ad1e, run byablation.shwith anEXIT INT TERMtrap:scripts/ablation-replace.mjsreplacedif (listHolding) findings.push(listHolding);with a guard that never pushes, carrying the markerABLATION_19886G. Anchor 1 to 0, marker 0 to 1, bloba95f00aftof507e6a3.pnpm --filter @objectstack/lint buildexit 0.ablation-dist-preflight.mjs @objectstack/lintfound the marker in all 4 built entries.os validateover the 85-cell stack, with the ablated dist: the 72 leak cells are clean again, the 10 controls clean, and the 3 firing controls unchanged. The arm alone carries the refusal, through the built CLI.git checkout HEAD -- ABS_PATH: blob equal to HEAD (a95f00af),git diff HEADempty. Rebuilt with exit 0; the preflight--absentreads the marker absent from all 14 built files and the tree clean.Gates
Re-derived at head with
dispatch-gates.mjs --commands --repo objectstack-ai/objectstack. The change set is these 4 paths against merge base67047171, and it is the same 60 commands as at53275286. Every one was run at79a2ad1e, with exit codes recorded to disk before they were read. The--ranreconciliation reads 60 derived, 59 run, 1 NOT MEASURED, 0 UNRUN:54 light gates: exit 0.
5 build-reading gates exit 0:
check:dts-closure,check:lean-entry-closure,check:published-files,check:sourcemap-no-sources-contentandcheck:type-check-debt.NOT MEASURED:
check:dual-build-cjs-loads, reason: exit 3, PREREQUISITE NOT MET. Six packages outside the built closures have nodist/(studio, client-react, embedder-openai, knowledge-ragflow, organizations, service-cluster-redis), and the gate needs a whole-treepnpm build. As a proxy, lint'sdist/index.cjsanddist/runtime.cjsbothrequire()cleanly.ADR-0087:
not-required (already-registered cel-predicate-one-value-comparand-refused).surfacenamessharingRules[].conditionand this exact class: "a field compared with another field (==, !=, or an ordering operator) where either column holds a list or an object on the record, as a json column or a multiple lookup does".replacementcarries the prescription.check-adr-0087-registrationexits 0 and reads the marker as[BREAKING+clause-②-narrowing] not-required (already-registered).Decisions settled in this PR (open to the reviewer)
The id: reuse
sharing-rule-unlowerable-condition(chosen) or addsharing-rule-unenforceable-condition.rls-predicate-unenforceable).--jsonis not sent to "rewrite into the pushdown subset".Acceptance notes
validateSharingRuleEnforceabilityisCLI_ONLY. Its wiring entry inauthoring-rules.tsrecords the runtime crossing as pending: the snapshot does not carrysharingRules. Measured at base and at head:saveMetaItem({ type: 'sharing_rule' })accepts all 85 cells, including the 3 firing controls this rule has always reported at the CLI. This is pre-existing and not specific to this class, so it is not widened here.{ $field }comparison as a literal, scalar ones included. Measured on the sharing path:!=over-shares every row. That is [finding] driver-memory's own reference matcher has no$fieldarm — a cross-field comparand (bare or withaddDays) reaching it is presumably compared as a literal object rather than resolved or refused (grep reading, to be measured) #15104 (frozen; test and demo only), unchanged here.matchRecordwith no sharing-level WARN for a non-fieldrule. Only the engine logsFind operation failed, naming no rule. That is runtime diagnostics, read only here (plugin-sharingis fenced). The new finding says "a WARN line in the server log", which holds for the boot backfill.reasondescribes the stage 2d outcome for this class: the write-check evaluator refusing it. That fits RLS, not a sharing condition, where the driver refuses the criteria query. Thesurfaceandreplacementfit, so no registration is owed. The reason text is not edited here.What remains on #19886
The release's remainder list (
5861981062) held stage 2g only, and this PR closes it, henceFixes. Carried elsewhere: #20347 (cross-class comparisons, a different family) and #15104 (frozen, driver-memory).Generated by Claude Code