Skip to content

feat(spec,objectql,plugin-security): one shared filter lowering, run once at the engine and RLS seams (#5930 step 2) - #20794

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-5930-step2-seam-lowering
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-5930-step2-seam-lowering

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #5930 — step 2 of ruling 5902355785 (the seam lowering in the engine / RLS seams). Steps 3 (the analytics-face seams and the F5 / F11 vocabulary) and 4 (the per-face deletions) remain, so the card stays open.
Clause-②: yes

What this does

This implements ADR-0053 D-D1 as amended on 2026-09-30, items 1–7 and 9. The bare-day upper bound, the $between split and the NULL-polarity guards are applied once, by one shared FilterCondition → FilterCondition lowering. It runs at the engine and RLS seams, after the comparand doors and after filter-token resolution. Drivers receive the lowered filter.

  • @objectstack/spec/data: new pure module filter-lowering.ts, exporting lowerFilterCondition(filter, options?) and FilterLoweringOptions. It is exported from the ./data subpath only, never the root entry (the ruling's D3). The rules:
    1. $between becomes $gte min and $lte max. A range holding a { $field } or a non-pair is left whole, for the face that refuses it.
    2. A $lte on a bare YYYY-MM-DD becomes $lt nextUtcCalendarDay(day), in the calendar-string domain, never a storage form (D-A1). UNBOUNDED_ABOVE turns a lone $lte into { $null: false }, and a $between keeps its minimum. Instants and Dates are never widened.
    3. NULL polarity, cell for cell from the four hand copies' tables. $ne of a value, $nin and $notContains get { $or: [{ f: { $null: true } }, { f: op }] } (非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298). Every leaf of a $not operand is made total ($not 的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {} 一个是 TRUE 一个是 FALSE #5146).
  • @objectstack/objectql engine.ts: one stage function, resolveThenLowerWhere (resolve, then lower), is the only way any filter position resolves. That covers find, findOne and count (resolveWhereTokens); update and delete (withResolvedWhere); and aggregate's where, each aggregations[i].filter and having. resolveWhereTokens and withResolvedWhere now require the lowering options, so no verb can resolve without lowering. The judge (judgeWhereAdmission) runs the same stage. The type reader for where and aggregations[i].filter is the object's declared type === 'datetime', the same test SqlDriver indexes datetimeFields by. For having it is the aggregated row's column types (aggregatedRowColumnTypes, where max(datetime) is datetime).
  • @objectstack/plugin-security: judgeCompiledComparands (the RLS compile seam, serving using and check) lowers every compiled policy filter right after the two faces. RlsFieldGuard gains an optional datetime set. SecurityPlugin fills it from the same declaration pass as the field-name set (loadObjectFieldNames) and hands it in at both compile sites. A guard without types reads no column as datetime.
  • scripts/adr-anchors/packages__spec__src__data__filter-lowering.ts.json: pins ADR-0053 to the module (Prime Directive [WIP] Add Chinese version of the documentation #13).
  • Changeset .changeset/5930-shared-filter-lowering.md: @objectstack/spec minor, @objectstack/objectql and @objectstack/plugin-security patch. It cites ADR-0053 D-D1 (amended).

No face copy is deleted, no driver file is touched, and the analytics where / preview door, the read scope and the memory cube face are untouched (step 3).

The stop line was reached: one evaluator's answers move, on rows with no value

The acceptance is answer invariance. Measured, the answers of every driver face stay the same. The engine's own in-process evaluator for aggregations[i].filter and having (F8, having-filter.ts) moves, and only on rows or groups with no value. Before this change F8 was the only face that disagreed with the others on those rows. After it, all faces agree.

A/B probe, not committed. Each face answers the filter as written and the lowered filter, on sqlite SqlDriver (F1), InMemoryDriver (F3), matchesFilterCondition (F7) and matchesAggregationFilter (F8):

Case set Filters Cells Moved Filters where the faces disagree, before → after
FILTER_LOGIC_CASES over FILTER_LOGIC_ROWS 36 144 0 0 → 0
TEMPORAL_CASES (plus the resolved tokenFilters) over TEMPORAL_ROWS 32 128 0 0 → 0
the same plus a row with no value, each filter also under $not, plus $between / $ne / $nin / $notContains probes 70 280 14, all F8 14 → 0

All 14 moved cells are a $between on a datetime column with a row whose value is null. F8 kept that row in the range (7 cells) and dropped it under $not (7 cells). F1, F3 and F7 exclude it from the range and keep it under $not, which is the #5146 / #5298 reading. A second probe found the same class on a number column: { $not: { amount: { $lt: 5 } } } dropped a null amount in F8, because JS compares null < 5 as true. Everywhere else the null row is kept. That probe covered the per-aggregation filter and having, one cell each.

The decision on whether to keep the two aggregate seams wired is in the report on #5930, with the four-axis frame. This PR carries option A (keep them). Dropping them (option B) removes the two aggregate hunks and their two pin rows.

Mechanism hypotheses — which held

  • H1 held: the doors run in lowerWhereFilterArray and tokens resolve after it on every verb. One helper (resolveThenLowerWhere) holds "resolve, then lower", and every verb has its own pin.
  • H2 — measured: yes, a compiled policy CAN carry an unresolved date token. record.signed_on <= '{today}' compiles to { signed_on: { $lte: '{today}' } }, passes both faces, and reaches using's drivers and check's matchesFilterCondition verbatim. Nothing resolves a placeholder on either RLS clause. The RLS lowering therefore runs after the faces (item 3) and reads '{today}' as a non-day string it leaves as written, the same as every face does today. This is pinned.
  • H3 — measured. (a) The RLS seam could not read declared types: RlsFieldGuard carried names only, but the types are in the same declaration loadObjectFieldNames reads. (b) No in-repo or example policy compares any column against a bare day or a date token: 72 non-test using/check predicate lines, all ==, in, == null, != null or 1 == 1. So no real policy's rows or admitted writes change under either reading. The seam takes the typed reading, because the type-blind one would move using answers on SQL for a non-datetime column (a text column holding day-prefixed strings, and $lte '9999-12-31' on text) in constructible policies.
  • H4 held on F1/F3/F7; F8 is the stop-line item above. Face suites are green after the change. Before: main at 085ca6bc1c has Test Core (6/6), Temporal Conformance (live PG + MySQL) and Dogfood Regression Gate green.
  • H5 held: the output introduces only $and, $or, $lt, $gte, $lte and $null. The unit table pins that closure over FILTER_LOGIC_CASES, TEMPORAL_CASES and every row. F1, F2, F3, F6, F7 and F8 already compile those.
  • H6 held: the three polarity functions and the $not totaliser are the SQL copies' tables cell for cell. The copies stay.

Evidence (all on head 9ca3698b67)

  • New pins:
    • packages/spec/src/data/filter-lowering.test.ts: 46 tests. The rule table, the item-7 scope, idempotence over the table and both case sets, vocabulary closure, copy-on-write, provenance, and pass-through.
    • packages/objectql/src/engine-shared-filter-lowering-seam.test.ts: 11 tests, one per verb and position, plus {today} resolved-then-widened, the typed scope, the last supported day, copy-on-write and the judge.
    • packages/plugins/plugin-security/src/rls-shared-lowering-seam.test.ts: 14 tests. Both clauses through RLSCompiler, plus SecurityPlugin.getReadFilter and computeWriteCheckFilter fed the declared datetime set.
  • Existing shape pins updated to expect the lowered driver input. No asserted row count changed. The door suites for number comparands, text operators and filter arrays now compare against the lowering of the door's output. rls-compiled-comparand-faces gets the same treatment. rls-empty-membership-polarity's not in shape is updated, and its admitted-row count stays 3.
  • Suites after the change:
    • spec: 578 files, 17065 passed.
    • objectql: 341 files, 6729 passed.
    • plugin-security: 148 files, 3216 passed.
    • driver-sql: 200 files passed and 11 skipped (live PG/MySQL cells).
    • driver-memory: 65 files, 1470 passed.
    • driver-turso: 80 files, 2195 passed.
    • driver-sqlite-wasm: 36 files, 675 passed.
    • driver-mongodb: 29 files passed; 5 skipped, because they need a real mongod.
    • formula: 42 files, 1240 passed.
    • typecheck is green for spec, objectql, plugin-security, the five drivers and formula. check:driver-conformance is OK (50 cells).
  • Ablations, each with the seam committed and then mutated on disk through scripts/ablation-replace.mjs and restored (blob equals HEAD, git diff HEAD empty):
    • engine resolveThenLowerWhere without the lowering: 8 of 11 seam pins red, across every verb and all three aggregate positions.
    • RLS judgeCompiledComparands without the lowering: 8 of 14 red.
    • SecurityPlugin without the datetime hand-off: the 2 plugin-level pins red.
    • The pins that stay green are the pass-through rows, which hold with or without the lowering.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands derived 103 families; all 103 were run with exit 0, and --ran reports 0 NOT-MEASURED and 0 UNRUN. Three gates (check:dual-build-cjs-loads, check:i18n, check:type-check-debt) first refused with PREREQUISITE NOT MET. They were re-run green after turbo run build --filter='./packages/*' --filter='./packages/*/*'. check:generated shows 15 of 15 up to date after regenerating api-surface/ and export-origins/.
  • Lint, as a narrowed run:
    • The population is the eslint.config.mjs block files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'].
    • eslint --no-inline-config --format json over the 13 changed source files reports 13 files, 0 errors and 0 warnings.
    • The config enables no type-aware linting (every parserOptions is ecmaVersion / sourceType only), so this diff cannot move a verdict on an untouched file.
  • NOT MEASURED locally: live PostgreSQL / MySQL (the CI job covers them), a real mongod, and Turso remote against a live server.
  • Changed lines: 1318 (+1289 / −29, 17 files).

Acceptance notes


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/plugin-security, @objectstack/spec, touching 43 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 8acdae9d8f0fc71cabe1671e7ec622213cc4f546.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 70 pages)
  • 5 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 — 141 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 8acdae9d8f0fc71cabe1671e7ec622213cc4f546 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4f8720eea10306a776162b9fc1c68eaca689729c — the merge of head 36e5ce8828a20a32ba415108d429c6f2141ca738 into base 8acdae9d8f0fc71cabe1671e7ec622213cc4f546, 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 4f8720eea10306a776162b9fc1c68eaca689729c && git checkout 4f8720eea10306a776162b9fc1c68eaca689729c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8acdae9d8f0fc71cabe1671e7ec622213cc4f546 36e5ce8828a20a32ba415108d429c6f2141ca738 && git checkout -B drift-repro 8acdae9d8f0fc71cabe1671e7ec622213cc4f546 && git merge --no-ff 36e5ce8828a20a32ba415108d429c6f2141ca738

node scripts/docs-audit/affected-docs.mjs --json 8acdae9d8f0fc71cabe1671e7ec622213cc4f546

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 8acdae9d8f0fc71cabe1671e7ec622213cc4f546 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9ca3698b6706acbc2f1cbc3f9a4fd485b54d9ed5
Local-runs: none

PR #20794 (Part of #5930, step 2 of ruling 5902355785), read against ADR-0053 D-D1 as amended on origin/main (the ten-item Amended block), the card's body and all 22 comments, the PR body, its 17-file list and the net diff against main (+1289 / −29) at the head above, and the head's check-runs as they stood when read. The head had not moved. Read-only: git reads of the diff and of origin/main, REST reads of the card, the PR and the check-runs; nothing built, run or re-run.

① Derived judgments

Conformance to the amended D-D1, item by item (1–7, 9):

  • Item 1 (the rules, one lowering) — right. packages/spec/src/data/filter-lowering.ts: a $between with two literal ends becomes $gte its minimum and $lte its maximum, and the same $lte arm then reads the maximum (lowerBounds); a bare-day $lte becomes $lt nextUtcCalendarDay(day); on UNBOUNDED_ABOVE a lone $lte keeps { $null: false } and a $between keeps its $gte alone; an instant, a Date, an impossible day and a { $field } end are left as written (nextUtcCalendarDay answers null; isLiteralRange refuses an object end); $gte / $gt / $lt keep their anchor. The three NULL-polarity tables (nullValueSatisfiesOperator, operatorIsNullTotal, nullGuardForFieldSpec) are cell for cell sql-driver.ts 5052–5170 on main, and the other three copies' nullValueSatisfiesOperator arms read identically. The output introduces only $and, $or, $lt, $gte, $lte, $null — pinned over the rule table, FILTER_LOGIC_CASES and TEMPORAL_CASES.
  • Item 2 (the seams this step owns) — right. The engine's where admission on every verb, aggregations[i].filter, having and the judge, all through one resolveThenLowerWhere; the RLS compile seam judgeCompiledComparands, serving using and check at its one call site. The analytics where / preview door, the read scope and the memory cube face are untouched (step 3), as the ruling orders.
  • Item 3 (after the doors AND after token resolution) — right, verified per verb on the head: the doors run in lowerWhereFilterArray at 11320 (find), 11618 (findOne), 13082 (update), 15732 (delete), 16250 (count), 16359 plus the having doors 16567–16600 (aggregate); resolve-then-lower runs at 11400, 11673, 13097, 15737, 16267 and 16631 / 16645 / 16661; the judge at 1267 then 1268. resolveWhereTokens and withResolvedWhere take the lowering options as a required argument, and the only resolveFilterTokens call left in engine.ts sits inside resolveThenLowerWhere, so no position resolves without lowering. Pinned: {today} resolves to the day and then widens, on find and on update. RLS: the lowering runs after both faces; for the seam's own consumers no token stage exists on either clause (the engine composes using inside the middleware, after its resolve stage; check goes straight to matchesFilterCondition), so "after resolution" holds by construction — with one carry, under H2 in ③.
  • Item 4 (@objectstack/spec/data, never the root entry) — right. data/index.ts re-exports the module; src/index.ts re-exports no data/index; api-surface/root.json and api-assembled.json carry neither symbol; api-surface/data.json and export-origins/data.json gain exactly the two, in sorted position. The module imports ./calendar-day and ./filter-subtree-provenance only — no dependency edge added.
  • Item 5 (drivers receive the lowered filter) — right. Recording-driver pins on all six verbs and a middleware witness on the three aggregate positions; the door suites now compare the driver's input against the lowering of the door's output; no driver file is touched.
  • Item 6 (no storage form; widen before conversion) — right. The rewrite emits nextUtcCalendarDay's YYYY-MM-DD string or { $null: false }, never a temporalStorageForm; it runs at the seam before any face converts, so D-E3's order is structural.
  • Item 7 (column-type scope) — right, with a reading. Engine: declaredDatetimeLowering reads fields[column].type === 'datetime', the test SqlDriver indexes datetimeFields by (11884 / 11975 on main); having reads aggregatedRowColumnTypes (max / min of a datetime field is datetime, a day bucket is date, count a number). RLS: RlsFieldGuard.datetime, filled in the same loadObjectFieldNames pass as declared and handed to both compileFilter sites — the only two in the package. The dev's reading that a typed seam whose declaration is missing at run time reads NO column as datetime (rather than going type-blind) is the reading that keeps the item's own headline true, "the column-type scope is unchanged": SqlDriver widens nothing without a declaration, so type-blind there would widen columns no driver widens and move SQL answers. The ADR's sentence "a seam that cannot … applies the rewrite type-blind" describes the structurally type-blind seams whose faces are type-blind today (step 3's); it does not license a typed seam to widen past its drivers. The module also scopes rule 1 (the $between split) together with rule 2 on a typed seam; that is likewise "the scope SqlDriver holds" (calendarDayBetweenRewrite is datetime-scoped) and answer-invariant. Both readings are right; the step-4 deletion cards need the first one in writing, which the PR's acceptance note carries.
  • Item 9 (interim idempotence) — right. lower(lower(x)) deep-equals lower(x) over the rule table and both case sets, typed and type-blind; an already-lowered filter comes back by reference (the escape and the requirement are recognised); every face's copy stays.

Published surfaces the diff implies:

  • @objectstack/spec/data: lowerFilterCondition (function) and FilterLoweringOptions (interface) added — right, and Clause-②: yes is the true declaration for it.
  • @objectstack/plugin-security: RlsFieldGuard IS a published type — not named on index.ts, but reachable from the entry type graph through RLSCompiler (index.ts line 12) and its public compileFilter(policies, executionContext?, clause?, fieldGuard?: RlsFieldGuard). The optional datetime?: ReadonlySet member is a widening of that accept set: every guard a caller passed before still compiles, and a new key is accepted. As a change it is right; its grade is judged in ②. compileFilter's output values change (lowered) with no type change, and its consumers already compile the closed vocabulary.
  • @objectstack/objectql: no public signature moves (resolveWhereTokens / withResolvedWhere are private; judgeFilter gains no verdict, pinned). What moves is the driver's input on every verb (shape only) and the in-process F8 evaluator's answers on rows with no value — judged in ②.
  • The filter accept set is unchanged at every seam: the lowering never refuses, and the doors still run first.

Review faces, sentence by sentence: the changeset, the module TSDoc, the anchor and the PR body are true against the code as read — the rule list; the copy-on-write / idempotent / never-refuses / provenance / closed-vocabulary contract (carryProvenance marks every replaced node, and filterSubtreeProvenanceOf is own-key, so the pin in filter-lowering.test.ts is the right witness); the seam list; "both compile sites"; "no face copy is deleted, no driver file is touched"; the counts (17 files, +1289 / −29; 13 source files; 46 / 11 / 14 pins — the spec table is 36 rows plus 10); the rls-empty-membership-polarity admitted count staying 3. Anchor: filename derived from the path, the file / adrs / invariant shape, and the module cites ADR-0053. One count differs harmlessly: my own grep of using: / check: predicate lines under packages/, examples/ and apps/ (non-test) finds 88, not 72, and 0 of them compare a column with an ordering operator or a {token} — the claim that matters holds under the wider net.

Check-runs on the head, as read (not waited for): success — Auto Label, filter, Check Changeset, Check PR Size, Governed Surface Queue Guard, the three card / branch / single-writer guards, Spec property liveness, Flag docs affected by code changes, Check Documentation Links, Type Check · source gates, Type Check · debt ledger, Dogfood Regression Gate (1/3), and the Vercel status; skipped (rostered) — Console Pin Gate, Build Docs, Packed-tarball smoke; in_progress — Build Core, Test Core (1/6 through 6/6), Temporal Conformance (live PG + MySQL), Dogfood Regression Gate (2/3 and 3/3), Dogfood Verify CLI, Lint & Repo Gates, Type Check · consumer gates, Type Check · workspace. None failing. Their conclusions are the gate verdicts; the seat owns convergence.

② Semver level

③ Boundary flags

  • Stop line (seat answered A) — answered, conforms: ADR item 2 names aggregations[i].filter and having among the seams; the moved F8 answers are the ruled NULL polarity; graded patch and stated in the changeset. Not escalated.
  • H2 (nothing resolves a placeholder on either RLS clause) — answered for this PR's consumers: right, and the RLS lowering's place after the faces is item 3's order. One carry: a third consumer of using DOES resolve tokens after the seam — the analytics read scope (read-scope-sql.ts line 1077 and analytics-service.ts line 1371 on main). A policy {today} bound therefore reaches F9 lowered-as-literal and is resolved to a bare day there; F9 widens nothing today ([finding] service-analytics read scope: compileScopedFilterToSql applies no whole-day upper bound and binds a temporal comparand as written, so an RLS $lte on a bare day drops the rest of that day in NativeSQL analytics #20733). Not this PR's seam: the step-3 card's lowering at compileScopedFilterToSql's entry must sit after that resolver (item 3) — carry to step 3. The check-clause literal compare against '{today}' is pre-existing, measured at the compile seam only, carrier none — recorded as the seat recorded it.
  • H3 (the typed reading at the RLS seam) — answered: right. My own grep: 0 of 88 in-repo predicate lines compare a column against a day or a token, so the "rows or admitted writes change" trigger for the maintainer is not met and the choice is in-lane; typed moves no driver's answer, type-blind would move SQL using answers on text columns.
  • security-plugin.ts outside the claim — answered: accepted as the seam's input, not a seam; both compileFilter sites (the only two) receive the set; the cache is filled only where fieldNamesCache is, so a non-null declared always has its datetime twin. It carries the one FAIL item above.
  • ADR anchor and api-surface regeneration — answered: right (see ①); the root and assembled surfaces are correctly unchanged.
  • Item 7 "no declaration ⇒ no datetime" — answered: right (see ①); carry the sentence to the step-4 deletion cards, as the PR's acceptance note already does.
  • NOT MEASURED live PostgreSQL / MySQL / mongod / Turso remote — answered: PG and MySQL are CI's Temporal Conformance job on this head (in_progress as read; its conclusion is the verdict); mongod and Turso remote have no CI cell and rest on item 9 plus the closed vocabulary (F2 / F6 compile $or, $null, $lt, $gte) — the ruling's recorded confidence gap, unchanged by this PR.
  • Out-of-scope notes — answered: F8's own null coercion stays in having-filter.ts for any caller that reaches applyHaving / matchesAggregationFilter without the seam (F8's step-4 deletion card, under A); the RLS check placeholder, carrier none; F9 receiving the lowered using, step 3 (with the H2 carry above).
  • Serial — fix(objectql,platform-objects,metadata-protocol): read sys_migration flag rows through findOne, ending the paging warning on every upgraded boot (#20648) #20766 merged in; [finding] a no-operator object under a lookup, master_detail or json field answers per driver: the declared nested-relation filter returns no rows on memory and a 400 on SQL, and a json object comparand deep-equals on memory and is refused on SQL #20745 unlanded, other regions of engine.ts; whichever lands second merges main.
  • The dev's open_questions holds one entry (the stop line) — answered above.

Implemented-by: claude/issue-5930-step2-seam-lowering
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: FAIL

One item: ② @objectstack/plugin-security graded patch for an additive widening of a published accept set (RlsFieldGuard.datetime); minor is owed. Everything else conforms; a re-graded head takes a fresh record.


Generated by Claude Code

…Guard.datetime widens a published accept set

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 36e5ce8828a20a32ba415108d429c6f2141ca738
Local-runs: none

Delta review of PR #20794 (Part of #5930, step 2 of ruling 5902355785) after the FAIL record 5905611995 on 9ca3698b6706acbc2f1cbc3f9a4fd485b54d9ed5. Inputs, and nothing else: that record in full (its one FAIL item and the fix it prescribed); the diff 9ca3698b67..36e5ce8828 and the changeset's full text at the head; the os-dev-report addendum 5905899813 on #5930; the head's check-runs as they stood when read. The PR itself was read through REST only to confirm the head, which had not moved. Read-only: git fetch into a ref of my own, then git diff, git show and git log; REST reads of the two comments, the PR and the check-runs; nothing built, run or re-run.

① Derived judgments

The FAIL item is fixed as prescribed — right. The delta is one commit, 36e5ce8828, a fast-forward of 9ca3698b67: git diff --name-status lists one path, .changeset/5930-shared-filter-lowering.md, and the diff is one line, '@objectstack/plugin-security': patch becoming '@objectstack/plugin-security': minor (+1 / −1). That is the fix record 5905611995 prescribed, word for word. minor is the right level and not merely a higher one: the Check Changeset step's WHICH LEVEL prose (.github/workflows/pr-automation.yml; the maintainer's ruling of 2026-09-04, decision batch #35, on #15294) gives an additive widening of a published accept set "at least minor"; the code is unchanged since 9ca3698b67, where 5905611995 verified that nothing is removed or renamed, so no BREAKING banner and no ADR-0087 disposition is owed and major is not in question; and the feat( commit type now agrees with the act instead of sitting above a patch. Check Changeset is success on the head as read.

No sentence of the changeset needed to change with the level — right. Read in full at the head: no sentence names a level. The two sentences the widening touches already name it — "SecurityPlugin now hands the compile seam the object's declared datetime columns (RlsFieldGuard.datetime). A guard without that set treats no column as datetime." — and "Nothing is removed or renamed, and there is nothing to migrate." is exactly what minor, as against a breaking level, asserts. Clause-②: yes already covered the widening, since yes takes at least minor; the per-package line now says the same thing. Every sentence 5905611995 read as true is byte-identical and stays true.

The addendum agrees with the diff. 5905899813 reports patch_round: 1, previous_head: 9ca3698b67, head: 36e5ce8828, files_changed exactly the changeset, "1 file, +1/-1", "No sentence in the body names a level … so nothing else changed", the code unchanged under the seat's answer A, open_questions: [] and out_of_scope_findings: []. Its gate lines (103 derived families with exit 0, check:generated 15 of 15, the three changeset gates clean) are the dev's own runs; the gate verdicts are the check-runs below. Its two deviations (the worktree re-created from the pushed head; three commands from a time-budgeted batch re-run on their own, each with an exit code) change nothing about the diff.

Check-runs on the head, as they stood when read (this record posted 2026-09-30T07:04Z; not waited for): success — Auto Label, filter, Check Changeset, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Governed Surface Queue Guard, Spec property liveness, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch (12); skipped, rostered — Build Docs, Console Pin Gate, Packed-tarball smoke (3); in_progress — Build Core, Test Core (1/6 through 6/6), Temporal Conformance (live PG + MySQL), Dogfood Regression Gate (1/3 through 3/3), Dogfood Verify CLI, Lint & Repo Gates, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Type Check · workspace (17); the Vercel status pending. 32 check-runs, none failing. Their conclusions are the gate verdicts; the seat owns convergence. Three that were success on 9ca3698b67 (Type Check · source gates, Type Check · debt ledger, Dogfood Regression Gate 1/3) are in_progress here only because this head's run is younger.

② Semver level

  • @objectstack/plugin-security minor — right; the FAIL item is closed (①).
  • @objectstack/spec minor — unchanged from 9ca3698b67; right, as 5905611995 judged (two new ./data exports).
  • @objectstack/objectql patch — unchanged; right, as judged (no public signature moves; the F8 answer change is the ruled NULL polarity applied at a seam item 2 names, a fix).
  • Clause-②: yes — unchanged, present, no arm; right, as judged.
  • Changeset truthfulness — every sentence unchanged and still true as read; the three levels now agree with the acts they grade.

③ Boundary flags

Implemented-by: claude/issue-5930-step2-seam-lowering
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS

The one FAIL item of 5905611995 is fixed as prescribed and nothing else moved; every other judgment of that record carries over to this head unchanged.


Generated by Claude Code

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 protocol:data size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants