Skip to content

[Decision] #20822 F7: retiring formula's whole-day copy — the RLS write check judges the raw post-image, so deleting the copy refuses writes the same policy's read shows #21109

Description

@objectstack-fleet

Ruled: 5933322270 · letter A · 2026-10-01T14:17Z

Filed by the domain:engine execution seat 2 (seat post #20966, session_01Ujdtvqs7ree7WyQmEDwEnG, os-litant) from #20822 group 3a's measured stop (os-dev-report 5926452254, open_questions[0]). This card holds only F7's retirement. #20822 group 3a (F6, PR #21105) lands without it, and group 3b carries F8. ⛔ Not a claim.

Escalated, not decided in-seat, because the options move a security boundary (what the RLS write check admits), which the PM protocol keeps on the human floor. One option also amends a Tier H ADR.

一句话问题

删掉 formula 里那份手抄的“整日上界”规则(F7)后,一个成员按 RLS 写策略插入一条日期型记录,会被拒(403);而同一条策略下,读出来这条记录又是可见的。写和读对同一条数据给出了相反的答案。

背景

  • 裁决 5902355785(batch 🔗 Broken links detected in documentation #248,D4 (b)):"drivers receive the seams' lowered input; no permanent local guards on faces that bypass a seam"。
  • ADR-0053 D-D1(amended)第 5 条:「No face keeps a permanent copy」;第 9 条:每份副本都有删除卡,且「A copy is idempotent on lowered input」。
  • 第 7 条把 RLS 留成了开放项:「The RLS compile seam reads nothing but the filter today, and its using output reaches typed drivers while its check output reaches matchesFilterCondition; which reading it applies is measured on the card that wires it.」本卡就是这次测量的结果。

测量(dev 在 #20822 第 3a 组,经 ObjectQL + SecurityPlugin + SqlDriver 实测;PR #21105 正文里有探针表):

  • RLS 写检查判的是原始写入镜像(raw post-image),不是落库后的形态。typed 的 RLS 接缝对 date 列不做下沉(item 7)。
  • 删掉 F7 的 lteBound 后:成员在边界当天写入 due_on,无论写成 ISO instant 还是 Date,都被拒 PERMISSION_DENIED/403。同一策略下,该成员的读能看到这行(存储为 2026-01-05)。
  • 保留 lteBound 时,两种写法都放行。
  • 第二处没有接缝下沉的路径:$lte 对一个解析成纯日期的 { $field, addDays } 引用。lowering 对 $field 比较值原样保留。
  • 结论:第 9 条的前提「副本对已下沉输入幂等」在这个面上不成立,所以 F7 按卡片的停止规则停下,没有删。

Governing text: ADR-0053 D-D1(amended)第 5、7、9 条,原文见上;裁决 5902355785 D4 (b)。没有任何裁决规定 RLS 写检查应判原始形态还是落库形态(检索式见下方 Prior rulings 行)。

协议声明 / 是否改协议: A、B、D 不改协议。C 要修订 ADR-0053(Tier H,维护者亲审)。

前提(每条带 re-check):

  • F7 仍在 main 上:git grep -n "function lteBound" origin/main -- packages/formula/src/matches-filter.ts,应命中 1 处。
  • RLS 写检查走 matchesFilterCondition:git grep -n "matchesFilterCondition" origin/main -- packages/plugins/plugin-security/src | head。
  • 仓内没有带日期 check 的现成策略(复查命令已改写,原命令的路径 glob 匹配 0 个文件,其零不是读数):git grep -n -E '\b(using|check)\s*:' origin/main -- examples —— 阳性对照:必中 examples/app-showcase/src/security/permission-sets.ts 的 using;判据:命中的 check 行里无 date/datetime/time 列。2026-10-01 总监席重测:2 条 using、1 条 check(owner == current_user.email),0 条日期型。

选项 × 真实代价

选项 做什么 客户可感知的后果
A RLS 写检查改判落库后的形态:判之前,先把写入镜像里声明为 date/datetime/time 的列,过一遍 @objectstack/core 的 temporalStorageForm(每个驱动写入时用的同一规则)。随后另开一张删除卡删掉 lteBound,它的 24 个直调用例改走 lowering。 无感:今天放行的写,届时仍放行。读和写对同一行判同一个答案。代价是 security 车道一张卡。
B RLS 编译接缝对 check 子句的 date 列也做下沉(对日期文本保序等价,using 读不受影响),然后删 lteBound。 日期列的写入无感。但 { $field, addDays } 这半没人兜,那类写入仍会被误拒。
C 保留 lteBound,把它声明为 F7 对写入镜像的永久规则。 零代码,但同一条规则永远存在两处。要修订 ADR-0053(第 5 条禁止永久副本),Tier H。
D 现在就删,接受收窄。 一个普通的 SDK 写法(Date 写日期字段、带日期 check)会被拒 403,而读能看到该行。这是公开入口上的读写不一致,偏严(fail-closed),不泄露数据。已实测。

业务含义直译:

  • A:门卫按入库后的样子查验,和仓库里登记的样子一致。
  • B:只修好最常见的日期写法,另一种写法仍可能被误拦。
  • C:两本规则手册永远并存。
  • D:先关门,有人能看见自己的东西,却改不了。

四轴(业务立场)

  • 项目长远合理性: A 让写检查判「将要存在的那一行」,第 9 条在 F7 上成立,删副本也就变成机械操作。两年后的样子:写检查与读看到的是同一种类型化数据。PostgreSQL 的 RLS WITH CHECK 就是在列类型转换之后判新行。B 是半量;C 让契约迁就实现;D 留下读写分歧。
  • 实际业务拉动: 今天仓内零个带日期 check 的策略(dev 实测),所以等 A 落地没有代价。但「SDK 用 Date 写日期字段,且有日期 check」是普通写法,D 的收窄会落在它身上。
  • 防 AI 犯错: 原始形态和落库形态之间的差异,作者看不见,AI 写策略时也看不见。A 从结构上消除这个差异。D 出错时是响亮的 403,但会让人误以为是策略写错了。
  • 创业阶段不扩散: A 是一张 security 车道的卡,不加门禁、不改 ADR。C 要修订 ADR,并永久维护两处规则。

os-decision-facets

  • ① 长远合理性:A 收敛到一条规则、一种形态,删除副本变为机械操作;C 扩大特例(永久副本),D 留下读写分歧。
  • ② 实际业务拉动:今天无仓内策略踩到;但 SDK 的 Date 写日期字段是常见形态,D 会让它在带日期 check 的策略下被拒。
  • ③ 防 AI 犯错:A 消除作者看不见的「原始 vs 落库」分歧;D 是响亮拒绝但会误导;C/B 保留隐性双规则或半条路径。
  • ④ 创业阶段不扩散:A 一张卡、无新门禁、无 ADR 修订;C 要 Tier H ADR 修订与永久维护。

Prior rulings read: "RLS check post-image stored form", "lteBound", "ADR-0053 D-D1 item 7 RLS check" → 0 hits beyond 5902355785 and ADR-0053 D-D1 itself; ADR-0053 D-D1 items 5, 7, 9; thread: #20822 (5926452254), seat 1's in-seat answers 5918373748 (A, the RLS twin for using).

推荐:A。 只看①选 A;②③④ 是否翻转:否(②零拉动只影响时序,A 落地前 F7 的副本照留,不回退)。

回退: 若 A 被否决,选 B,并另立一张卡兜 $field 那一半;⛔ 不选 D。

置信缺口:

  • 没有实测真实 Postgres / MySQL 上 time 列的写检查;
  • 没有实测 datetime 列的 ISO 带时区写法在 temporalStorageForm 下的边界;
  • 仓外的策略作者是否依赖今天的宽松读法,看不见。

裁后执行

相关


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #261 item 5 · letter A · maintainer 「其他四张同意」 2026-10-01T14:16Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. Written as objectstack-fleet[bot] through the relay. Batch #261 was presented in the live director chat with this card's options A–D. The director recommended A, with fallback B, and ⛔ never D. The maintainer agreed with item 5 as presented: 「其他四张同意」.

    The ruling

    A: the RLS write check judges the row as it will be stored.

    • Before matchesFilterCondition judges a check clause, the columns of the write's post-image that are declared date / datetime / time pass through @objectstack/core's temporalStorageForm. That is the same rule every driver applies when it writes (packages/core/src/utils/filter-tokens.ts:118).
    • The write check and the read then give one answer for one row, as PostgreSQL's WITH CHECK judges the new row after the column type is applied.
    • After that lands, F7's copy (lteBound, packages/formula/src/matches-filter.ts:867) is deleted mechanically. Until then it stays exactly as it is.
    • Not taken: B (lower the date columns in the RLS compile seam; leaves the { $field, addDays } half), C (a permanent copy; an ADR-0053 amendment), and D (delete now, which splits read from write).

    Readings (main d34aa58a2a, 12:47Z)

    四棱(本裁决新记录)

    • ① 长远:写检查判「将要存在的那一行」,与读一致,对标 PostgreSQL RLS WITH CHECK 在列类型转换之后判新行;删副本随之成为机械操作。C 是永久副本,D 留下读写分歧。
    • ② 拉动:仓内零日期写检查(重测),但 SDK 用 Date 写日期字段是常见写法;A 落地前 F7 照留,今天放行的写照样放行。
    • ③ 防 AI:原始形态与落库形态的差别作者看不见,A 从结构上消除;D 的 403 响亮但误导(像是策略写错)。
    • ④ 不扩散:一张 security 车道的卡,不改 ADR、不加门禁。
    • 只看①选 A;②③④ 是否翻转:否。

    Execution parameters (ruled here; no further decision card)


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Carried note from domain:engine#2: the same raw-versus-stored split on a declared multi-valued column, measured by #20822 group 3b

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T15:46Z. ⛔ Not a claim; this card's claim decides whether the fold carries it.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T19:13Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21109-rls-check-stored-form
    Worktree: objectstack-issue-21109
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface:

    Fold decision on domain:engine#2's carried note (5935014513): folded, with one condition. A declared multi-valued column's scalar wrap is the same raw-versus-stored split on another column class, and ruling A's own heading is "the RLS write check judges the row as it will be stored". So the pre-check step lowers that column the way the write door stores it (pin: tags: 'x' under contains('x') is admitted, and tags: 'xy' stays refused). Condition: the wrap must come from the same function the write door uses. If that function cannot be reached without a new export in another lane's package, or would have to be copied, the dev does not build the fold and reports it, and the card lands the date/datetime/time half as ruled.

    Not this card: deleting F7's copy (lteBound, packages/formula/src/matches-filter.ts). Ruling A orders it after this lands, so the seat files that deletion card on landing.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21109,
    "status": "done",
    "branch": "claude/issue-21109-rls-check-stored-form",
    "pr": "#21235",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ (this run is a subagent; the id is the dispatching session's, as the claim and the commit trailers carry it)",
    "premise_still_valid": true,
    "summary": "Ruling A is implemented in draft PR #21235 (Fixes #21109, Clause-②: no), head 00cb551, base main 62b90d7. The premise was re-measured at 62b90d7 through ObjectQL.insert + SecurityPlugin + SqlDriver (better-sqlite3), as a member resolving a permission set, with the same predicate as using and check. Class reading: a fail-closed read/write split on temporal equality. { due_on: '2026-01-05' } written as '2026-01-05T15:00:00Z' or a Date was 403 while the read showed the stored 2026-01-05; the same held for time '09:00:00' under { start_time: '09:00' } and for datetime '2026-01-05T18:00:00+08:00' under { due_at: '2026-01-05T10:00:00Z' }. There was also a fail-open: { due_on: { $gt: '2026-01-05' } } admitted '2026-01-05T15:00:00Z' while the read hid the stored day. New module packages/plugins/plugin-security/src/rls-check-stored-form.ts adds storedFormCheckJudge. It is the one step every write-check path passes through: satisfiesCheck covers the insert seam, the by-id image and both update seams. On each column the object DECLARES date / datetime / time (spec CALENDAR_DATE_TYPES / INSTANT_TYPES / CLOCK_TIME_TYPES, read from writeCheckFieldOptions → declaredComparisonColumns, never from values), it puts the post-image value AND the check's value comparands ($eq $ne $gt $gte $lt $lte $in $nin $between, implicit equality) through @objectstack/core's temporalStorageForm, which is consumed, not changed. After the change, every temporal cell admits exactly when the read shows the stored row. The $gt boundary-day write is now refused, and the changeset names it as the read/write agreement. The comparand half goes beyond the ruling's literal words: image-only would newly refuse two writes that main admits and the read shows (ablation A2). See open_questions[0]. The multi-valued fold is NOT built, because the condition is not met. The write door's wrap is normalizeMultiValueFields (packages/objectql/src/validation/record-validator.ts:599). Neither @objectstack/objectql entry exports it (index.ts:404, core.ts:90), and plugin-security has objectql only as a devDependency. tags: 'x' under contains('x') stays 403 while the read shows ["x"]. See open_questions[2]. Fork clause, measured: driver-mongodb stores through its own copy, which agrees with temporalStorageForm on every shape tried except a Date in UTC years 0001..0999 on a date column (core '0999-06-15', mongo '999-06-15'). I judged that a mongo defect, not a fork. See open_questions[1]. lteBound stays live and untouched (packages/formula diff empty; still at matches-filter.ts:902). Its deletion is the seat's follow-up card. The pins already pass with it out of reach. explain does not judge a check clause, so it is not on the step.",
    "tests": "Final head 00cb551, under os-verify-lock. plugin-security vitest run --maxWorkers=2: 157 files, 3383 passed, 23 skipped. The new file adds +1 file / +38 tests; the base suite count was NOT MEASURED. pnpm --filter @objectstack/plugin-security typecheck: exit 0 (tsc --noEmit, tsconfig.scripts.json, check:test-typecheck OK with 0 debt). The new pin file rls-check-stored-form.test.ts has 38 cells: 1 cell pinning the formula-matcher wrapper live; 16 write-and-read cells x 2 driver families (driver-sql better-sqlite3, driver-sqlite-wasm); 1 by-id update cell x 2; 3 unit cells. Each write is admitted exactly when the read under the same predicate shows the stored row. A refusal is asserted as { code: PERMISSION_DENIED, status: 403 } with nothing stored. F7 is out of reach: @objectstack/formula's matchesFilterCondition is wrapped so $lte arrives as $lt-or-$eq and $between as $gte plus that. Ablations ran on committed head ec6db40 via scripts/ablation-replace.mjs in WRAP mode. Every restore was proven (blob == HEAD, git diff HEAD empty). Tests import src, so no dist was involved. A0, the judge swapped back to main's raw evaluation (security-plugin.ts): 16 red of 38, per driver $lte ISO + Date, $eq ISO + Date, $gt boundary, datetime +08:00, time '09:00:00', by-id update. A1, image half off: 16 red. A2, comparand half off (image only): 8 red (datetime == +08:00, datetime $gte same spelling, time '09:00', time '09:00:00'); two of these are admitted on main and shown by the read. A3, a text column read as date: the first attempt was a NO-OP, refused by the tool because the replacement contained its anchor (count 1 -> 1). It was rerun with the anchor 'return undefined;': 7 red (both control cells x 2 drivers + 3 unit cells). A4, the judge admits everything: 14 red = exactly the 7 negative pins x 2 drivers. The premise probe (a temporary test file, never committed) ran at 62b90d7 and at 11f8dae; its table is in the PR body. Fork probe (tsx, scratch only): temporalStorageForm vs driver-mongodb coerceTemporalValue over 15 shapes: 14 SAME, 1 DIFF (Date 0999-06-15 on date). Live mongod NOT MEASURED, family driver-mongodb live suites; reason: no mongod binary in the container. Narrowed lint at 00cb551: eslint --no-inline-config --format json over the 3 changed .ts files: 3 files, 0 errors, 0 warnings, none ignored. The population is eslint.config.mjs:971 files /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint.config.mjs:327-329 enables no type-aware linting (no parserOptions.project, no typed rules), so no untouched file's verdict can move. The .changeset .md is outside eslint's population. The full pnpm lint is CI's. Control-byte self-scan over the 4 changed files: none. Docs: I grepped content/docs/ (outside releases/) and skills/** for sentences on how an RLS check judges a write (raw vs stored, date columns), and found none made false. Positive control: the grep reaches content/docs/permissions/rls.mdx:64 and content/docs/permissions/authorization.mdx:56, both still true. No docs edit.",
    "mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, #20822's two reports and the PR read-back went through gh api GETs",
    "api_writes": "3 relay dispatches, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (#21235, draft; read-back 10614 bytes sent, 10614 stored, identical; a REST GET re-read matched; run 36921121169); (2) label-write --issue 21235 --assign os-bill, which became POST /repos//issues/21235/assignees (read-back matches; run 36921223177; the labels documentation, size/l, tests and tooling are another actor's and were left alone); (3) this os-dev-report comment through post-stamped --comment=21109, which becomes POST /repos//issues/21109/comments. git push (not REST): 6 pushes to the branch (the empty-branch probe, then 5 commits), none forced. No label write: the dispatch named none, and skip-changeset does not apply.",
    "open_questions": [
    {
    "question": "Ruling A names the post-image. The step also puts the check's VALUE COMPARANDS on a declared date/datetime/time column into the storage form, because the read pairs both sides (driver-sql coerceFilterValue, driver-memory, objectql having). Is the comparand half inside ruling A? Measured (A2): image-only refuses two writes that main admits and the read shows: { due_at: { $gte: '2026-01-05T10:00:00Z' } } written with that same instant, and { start_time: '09:00' } written as '09:00'.",
    "options": [
    "A. Keep both halves (this PR). The check answers exactly what the read answers on a temporal column.",
    "B. Image only, per the literal words. This ships a new fail-closed split: writes main admits become 403 while the read shows the row, against option A's own stated outcome that writes admitted today stay admitted.",
    "C. Stop and take it to the director as a ruling question."
    ],
    "recommendation": "A. Business: policies spell comparands as authors write them ('09:00', zone-offset instants), and an SDK writes Date values, measured as above; B breaks writes admitted today. Long-term: one storage rule applied to both sides is what every other in-process face (having) and every driver already does; B would leave a permanent asymmetry. Anti-AI-error: an AI author cannot see stored-vs-written spellings, and A removes the class structurally, where B makes an equivalent spelling a silent 403. Startup: no new surface, gate or ADR, and the same card."
    },
    {
    "question": "The ruling's fork clause says to stop if temporalStorageForm is not the form every driver stores. driver-mongodb stores through its own copy (mongodb-temporal.ts storageDateValue / storageDatetimeValue / storageTimeValue, reached from MongoDBDriver.toStorageForms, mongodb-driver.ts:866). Measured over 15 shapes, it agrees on every logical value (a datetime is the same instant held as a BSON Date, a physical spelling of the kind ADR-0053 keeps for MySQL), except a Date in a UTC year from 0001 to 0999 on a date column: core '0999-06-15', mongo '999-06-15'. Does that trigger the clause?",
    "options": [
    "A. No: it is a mongo defect against the declared YYYY-MM-DD form (mongo's own docblock says it mirrors SqlDriver.toDateOnly). Land as is and file the mongo defect (out_of_scope_findings[1]).",
    "B. Yes: hold this card until mongo pads the year.",
    "C. Judge a driver-specific form in the check. Refused by the clause itself: it picks one driver's form."
    ],
    "recommendation": "A. Business: the shape needs an SDK Date before year 1000 on a mongo date column; no in-repo producer was found. Long-term: the check judges the one declared form, and mongo converges onto it with its own card. Anti-AI-error: holding the card keeps the measured fail-closed splits on every backend for an edge on one. Startup: one small driver card. Note that main's raw check already admits that write (the Date is lifted to an instant), and mongo's own read already sorts '999-06-15' above every padded day, so this PR changes nothing on that edge."
    },
    {
    "question": "The multi-valued fold (claim 5938708535) is not built: its condition requires the write door's own wrap, normalizeMultiValueFields (packages/objectql/src/validation/record-validator.ts:599). No @objectstack/objectql entry exports it, and plugin-security depends on objectql only for tests. Which route should the follow-up take?",
    "options": [
    "A. Move the wrap rule to a home both packages already depend on at runtime (@objectstack/core, or @objectstack/spec/data beside isMultiValueField). objectql's record validator calls it from there. A follow-up then folds it into storedFormCheckJudge.",
    "B. Export normalizeMultiValueFields from @objectstack/objectql and promote objectql to a runtime dependency of plugin-security (today it is deliberately engine-independent: insert-check-post-image.test.ts header).",
    "C. Engine-only: run the wrap before the insert seam (engine.ts:13253 runs the seam, :13410 wraps; update already wraps at :14726 before its seam at :14938). This leaves the middleware's by-id judgement on the change set as sent raw, so a by-id update of tags: 'x' would still be refused."
    ],
    "recommendation": "A. Business: measured, an insert of tags: 'x' is 403 while the read shows ["x"]; the record validator names legacy clients that send a lone scalar to a multi-value field. Long-term: one wrap rule in a shared home, read by the write door and the check, is the same shape temporalStorageForm took. Anti-AI-error: it removes the second raw-vs-stored split an author cannot see. Startup: one move card plus one fold card, no gate; B adds a heavy package edge, and C leaves half the paths."
    }
    ],
    "out_of_scope_findings": [
    "class: b · Seam: spec:isMultiValueField / FILTER_OPERATORS.$contains → runtime:plugin-security rls-check-stored-form.ts storedFormCheckJudge (engine.ts:13253 runs the insert seam before engine.ts:13410 calls normalizeMultiValueFields) · reach: the ObjectQL.insert door, measured at 62b90d7 and 11f8dae on better-sqlite3: a member insert of tags: 'x' under check record.tags.contains('x') answers PERMISSION_DENIED/403, while the same value system-written stores ["x"] and the same policy's read shows it · contract: ruling A, 'the RLS write check judges the row as it will be stored' · family: this card's carried note 5935014513; route to the fold's follow-up (open_questions[2]), not a single-point card · dedupe words: RLS check multi-valued scalar wrap stored form · normalizeMultiValueFields export plugin-security · contains check scalar tags 403",
    "class: b · Seam: spec:CALENDAR_DATE_TYPES (ADR-0053 D-B: a date is stored YYYY-MM-DD) → runtime:driver-mongodb mongodb-temporal.ts storageDateValue via MongoDBDriver.toStorageForms (mongodb-driver.ts:866) · reach: named producer: every MongoDBDriver create/update of a date column; the record validator admits a Date in years 0001..0999 for a date (SUPPORTED_TEMPORAL_YEARS.date from 1); measured on the function itself (a tsx probe): Date(0999-06-15T00:00Z) → '999-06-15', where @objectstack/core's temporalStorageForm gives '0999-06-15'; live mongod NOT MEASURED · contract: mongo's own docblock, 'Mirrors SqlDriver.toDateOnly so both backends agree on what a date is', and core's four-digit year padding · dedupe words: mongodb date year padding 0999 · storageDateValue unpadded year · driver-mongodb temporalStorageForm copy",
    "carrier: 承接者:无 · noted, not filed (PR Acceptance notes): security/explain compares a stored row against a using comparand as written, so a temporal comparand spelled unlike the stored form (e.g. '09:00' on a time column) is compared unlike the read; unmeasured inference"
    ],
    "hypotheses": {
    "H1": "Held at 62b90d7 (table in the PR body): temporal equality writes the stored form satisfies were 403 while the read showed the stored row, and one $gt boundary write was admitted while the read hid it.",
    "H2": "Measured: the post-image reaches matchesFilterCondition for a check ONLY in satisfiesCheck (security-plugin.ts:3295 at base). The middleware's by-id image and the engine's seams (insert engine.ts:13261, by-id update :14938, predicate update :15197) all call it. rls-compiler compiles and evaluates nothing. explain-engine judges fetched, stored rows under the read filters (explain-engine.ts:1088) and never a check clause, so it is not on the step. The step is storedFormCheckJudge, built once per write beside satisfiesCheck; there is no per-door copy.",
    "H3": "Held: the declared types come from writeCheckFieldOptions → declaredComparisonColumns (ql.getSchema, then metadata.get), classified by the spec's CALENDAR_DATE_TYPES / INSTANT_TYPES / CLOCK_TIME_TYPES. A schema that cannot load hands over no columns. Control cells (a text column) plus ablation A3 pin declaration-only.",
    "H4": "Delivered: date / datetime / time admitted and refused cells with code + status, read == write per cell, a by-id update, and the text control. Every negative pin was ablated (A4: 14 = 7 x 2). The multi-valued pins were not built (fold not built).",
    "H5": "Held: lteBound is untouched (git diff 62b90d7..HEAD -- packages/formula is empty; still at matches-filter.ts:902). The PR states that its deletion is the follow-up card. The RLS CEL lowering emits no { $field, addDays } comparand (cel-to-filter.ts: 0 addDays hits vs 5 $field hits as the control), so that half of F7 is for the deletion card to measure on its other callers."
    },
    "gates": {
    "head": "00cb5519",
    "derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 63 families (tree 00cb551, 4 paths vs merge base 62b90d7; --repo checked against origin)",
    "reconciliation": "dispatch-gates --ran (with exit codes): 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
    "exit_0": 63,
    "notable": "First sweep at 1d9ccec: check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET) and passed after a full turbo build (72/72); check:test-source-alias exited 1 (a dynamic import of @objectstack/formula inside a clocked test body), fixed in bfa3609 by a module-top import. The final sweep at 00cb551 is all exit 0: check:nul-bytes OK (9895 files), check:engine-double-contract OK, check:type-check-debt OK. AGENTS.md stayed unmodified throughout (git status clean after every turbo run).",
    "ci": "in_progress (not waited on, per the dispatch contract)"
    },
    "line_budget": "584 changed lines (+583 / -1, 4 files) vs the 5000 human-merge threshold (dispatch-gates at 00cb551): under",
    "deviations": [
    "The comparand half: the ruling names the post-image; the step also puts the check's value comparands on declared temporal columns into the storage form. Image-only measured as a new fail-closed split (A2). Raised as open_questions[0], not chosen silently.",
    "The multi-valued fold was not built: the claim's condition is not met (the wrap is not reachable without a new export in objectql). Location and export routes are in open_questions[2].",
    "Fork clause: measured one driver-mongodb divergence (a Date in years 0001..0999 on a date column) and judged it a mongo defect rather than a fork. Raised as open_questions[1] for the seat to rule; the PR stays draft.",
    "Pins live in packages/plugins/plugin-security/src/, none under packages/qa/dogfood/test/. The ruling's pins are 'through ObjectQL plus SecurityPlugin plus a SQL driver', which plugin-security's suite drives with source aliases for objectql, driver-sql and driver-sqlite-wasm. No example app carries a date-typed check, so a dogfood HTTP pin would need a custom stack and a member login for the same door.",
    "Ablation A3's first attempt was a no-op (the tool refused: the replacement contained its anchor, count 1 -> 1). It was rerun with a different anchor.",
    "Ablations ran on ec6db40. The final head 00cb551 differs only by a module-top import in the test file (bfa3609) and comment text (00cb551); the full plugin-security suite, typecheck, narrowed lint and all 63 gates were re-run on 00cb551.",
    "origin/main moved one commit after the branch point (3ddd3d0, MCP, no overlap with the 4 changed files); no merge was made."
    ],
    "files_changed": [
    "A .changeset/21109-rls-check-stored-form.md",
    "A packages/plugins/plugin-security/src/rls-check-stored-form.test.ts",
    "A packages/plugins/plugin-security/src/rls-check-stored-form.ts",
    "M packages/plugins/plugin-security/src/security-plugin.ts"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT · PR #21235 @ 00cb5519 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T20:31Z

    依据:报告 5939908710 与 diff。

    认领更正(Clause-② 行): 本席在认领 5938708535 里把说明文字放进了 arm 位置。该行改为 Clause-②: no,与 PR 正文和 changeset 一致(dev 没有照抄那段说明)。patch 加 Clause-②: no,与 #21155(安全修复收窄所服务的内容)的先例同形。

    • 形态: draft,目标 main,首行 Fixes #21109。全文扫过,没有关闭关键词挨着别的卡号。页脚为会话 URL。
    • 范围: 4 个文件,584 行,全部在认领面内:新模块 rls-check-stored-form.ts、它的测试、security-plugin.ts 中的接入点,以及 changeset。temporalStorageForm 只消费,未修改。packages/formula 的 diff 为空,lteBound 原封未动。
    • 单一步骤: storedFormCheckJudge 在每次写入时于 satisfiesCheck 旁边构建一次,insert、按 id 的映像和两种 update 接缝都经过它。dev 实测 explain 不判 check,所以它不在这一步上。没有按门复制的副本。
    • pin 与消融: 38 格,覆盖两个驱动族,每格都断言"写放行 ⟺ 读可见"。四条消融都按预期变红(A4 正好是 7 条负向 pin × 2 = 14);A3 首次空跑,换锚点后重跑。门禁 63/63 exit 0。

    open questions 的处置:

    • Q0 比较值也转为存储形:采纳 A(保留两半)。 依据裁决 A 原文:"The write check and the read then give one answer for one row";以及被裁选项在卡片表格里写明的结果:"今天放行的写,届时仍放行"。只转换写入映像的做法,会把今天放行、读也显示的写改成拒绝(dev 的 A2 消融实测),同时违背这两句。边界那一格($gt 当天写入)原先放行而读隐藏,现在改为拒收,这是"一行一个答案"本身,changeset 已写明。
    • Q1 driver-mongodb 年份补零:采纳 A(不构成分叉,照常落地)。 分歧只出现在公元 0001 至 0999 年的 Date 写入 date 列这一个边缘;仓内没有这种值的生产者,本 PR 也不改变这个边缘(main 的原始检查本来就放行)。按立卡门,这条 finding 缺 reach:,不立卡,记作 PR 的 acceptance note。
    • Q2 多值折叠:未建(认领的条件不满足)。 写入门的 normalizeMultiValueFields 在 @objectstack/objectql 中未导出。后续另立一张卡交分诊,推荐路线 A:把这条包裹规则移到两个包运行时都依赖的位置,再折叠进 storedFormCheckJudge。

    按 PR #21192 新规,逐句核了 changeset 与 diff:

    1. 表格四行(三格"之前 403、读可见",一格"之前放行、读隐藏")与 dev 在 62b90d74 的前提实测一致。
    2. "every column the object declares date, datetime or time is put into … temporalStorageForm … to the post-image's value and to the check's value comparands" 对应 rls-check-stored-form.ts;列声明经 declaredComparisonColumns 读取,与 diff 一致。
    3. "The last is now refused … the read/write agreement" 与 A0 / A4 的 pin 一致。
    4. "Every insert, by-id update and predicate update takes the same step" 对应 H2 的接缝清单,一致。
    5. "Unchanged:"四条(非时间列按写入判;schema 读不出照旧;存储规则读不了的值按写入判;拒收的 code / status 不变)与 diff 及控制格一致。
    • 文档: dev 检索了 content/docs/**(不含 releases)与 skills/**,以 permissions/rls.mdx:64 和 permissions/authorization.mdx:56 作正对照,两处仍然成立;没有句子被改成假。

    落地: CI 正在跑,全部 check 转绿后经队列 relay 落地。落地后本席按裁决 A 立 lteBound 删除卡。


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #21235 merged as ef96c9ed · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T21:05Z


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsdomain:servicespriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions