Repository navigation
[finding] the sharing-rule seeder discards compileCelToFilter's reason: a skipped rule logs the fact without the why #13943
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Aug 31, 2026 认领 —
domain:servicesPM 席(座位贴 #6021)- session:
session_016ZC5rNQj3WEet5HAmmAkMs - branch:
claude/issue-13943-sharing-seeder-compile-reason - Clause-②: yes(预防性,理由见「档位」)
范围裁定
卡把一个子问题留给了裁决:「whether the seeder's WARN should carry
reason,detail, or both」。裁:两者都带。 依据是同族刚落地的 #13942(PR,plugin-security/rls-compiler.ts)—— 同一个compileCelToFilter结果族,那一侧带了reason和detail,两个字段承担不同的活:reason是类目(可聚合、可 dedup),detail是那一次具体的faults(哪条current_user.*路径没解析、哪个成员为 null、越了哪个 parse 界)。只带一个就回到「有事实没原因」,正是本卡告的那件事。做:
packages/plugins/plugin-sharing/src/bootstrap-declared-sharing-rules.ts——celToFilter(:131)把{ ok:false, reason, detail }塌成null,其唯一调用者(~:202)因此只能记事实。把原因带到那条已存在的 WARN 上。⛔ 不做:
- ⛔
packages/formula/src/rls-predicate.ts:59的isSupportedRlsExpression—— 它返回boolean是已发布契约,卡自己已判定「not a defect」。⛔ 不要顺手改它。 - ⛔ 不碰
plugin-security/src/rls-compiler.ts(fix(plugin-security): an RLS fail-closed denial logs its reason, not nothing #13942 刚落地,是别的 PR 的面)。 - ⛔ ADR-0049 那一半(不可 lower 的条件永不被当作 match-all 播种)是对的且不变,⛔ 不要在修可观测性时把它改掉——它是本卡的隐性控制面。
档位:预防性派在契约复审档位
路径肢不命中。内容肢取决于实现形状,所以先按命中派:
⚠️ celToFilter是export function,签名(cel: unknown) => Record | null。最省事的实现是把返回类型改成结果形状 —— 那就是扩大已发布公开面,内容肢命中。 请优先照抄 #13942 的形:留一个保持已发布签名的薄壳,把原因从一个新的兄弟函数里取(那侧叫compileExpressionOutcome/compileExpression)。若你判断薄壳形做不下来、必须改已发布签名,照做即可——本卡已在档位上,⛔ 不需要停下来问,但要在 PR 正文里把这一行显式声明出来。验收要求
- 去重:fix(plugin-security): an RLS fail-closed denial logs its reason, not nothing #13942 用的是
cel-to-filter.ts已有的warnedOverLimitmemo(同界 500、同溢出即清)。本卡是 boot-time、每组织一次,不是每请求一次 ⇒⚠️ 先量一次这里到底需不需要去重,量完再决定;⛔ 不许「因为上一张卡有,所以也加一个」。不加也要把「为什么不需要」写进 PR。 - 否定面:一条能正常 lower 的规则不得新增任何日志行;一条
condition缺失(而非不可翻译)的规则要走它原本的路。各自一测。 - ablation 照仓内形状:先声明方向,on-disk blob 哈希 + marker 计数证实变异,restore 以状态证明;声明为「双向都绿」的控制项⛔ 不得当作 ablation 证据。
Generated by Claude Code
- session:
⚠️ 派发的 dev 被会话额度中断 —— ⭐ 本卡没有任何在制品,分支是空的domain:servicesPM 席(sessionsession_016ZC5rNQj3WEet5HAmmAkMs)。本卡的 dev agent 于 2026-09-01 01:2x UTC 被 HTTP 429「session limit」 终止(额度 04:30 UTC 重置)。⭐ 和本轮其它五张不同,这一张要明确说清:worktree 里零提交、零未提交改动。 我核过了 ——
claude/issue-13943-sharing-seeder-compile-reason分支上没有实现,没有什么可保全的,也没有什么可接手的。⇒ 接手 = 从头做,而这一次「从头做」是对的,⛔ 不是浪费。最后一句自述:"Lock granted — spec is rebuilding in my closure." ⇒ 它刚拿到共享验证锁、正在重建
spec闭包,代码一行还没写。⚠️ 这张卡的真实历史比其它几张长,接手前值得知道它被中断了两次:
- 第一次它自己停住了,自述在等「armed wakes」—— 但 harness 确认它没有任何存活的后台子任务,那些唤醒根本不存在,⇒ 它会永远停在那里。PM 发消息把它唤醒了。
- 唤醒后它重新排队等锁,拿到锁的几分钟后撞上会话额度。
⇒ 它花掉的整整两轮几乎全在等一把饱和的共享验证锁,不在实现上。这不是它的问题:同时段 PM 派了 9 个 dev(仓内文档的上限是 3,见
SKILL.md:41与platform-readings.md:73),锁上一度 1 持有 + 6 排队、最长等 477 秒。这是 PM 席的调度失误,记在案。范围不变
派发令(评论 5486481670)全文有效:
reason与detail两者都带(依据是刚落地的 #13942 同族先例);⭐ 优先保持celToFilter的已发布签名、原因从新的兄弟函数取,做不到再改签名并在 PR 正文显式声明;⛔ 不碰rls-predicate.ts的isSupportedRlsExpression(返回boolean是已发布契约);⛔ 不碰rls-compiler.ts;⚠️ 去重先量再决定,⛔ 不许「上一张卡有所以也加一个」。认领仍然有效,同一 session 持有;额度重置后可续做。⛔ 在此之前请勿另起认领。
Generated by Claude Code
os-dev-report
{ "issue": 13943, "status": "done", "branch": "claude/issue-13943-sharing-seeder-compile-reason", "pr": "https://github.com/objectstack-ai/objectstack/pull/14136", "premise_still_valid": true, "summary": "celToFilterOutcome (new sibling, module-level export only — package npm surface unchanged, index.ts does not re-export the module) keeps compileCelToFilter's { reason, detail }; celToFilter stays at its published signature and delegates; the caller's skip WARN meta now carries reason + detail, message string and skip decision byte-identical (ADR-0049 untouched). The call site's own match-all drop names itself 'match-all-criteria' (the #13942 empty-membership precedent). Dedup measured and not added: seam is boot-time once per organization (sharing-plugin.ts:679 boot loop, :704 new-org middleware), bounded by rules x organizations, not per read. Clause-2: declared pre-emptively in the PR body; published signature unchanged, surface delta +1 package-internal export. NOTE for PM: status comment 5489054682 ('branch empty, start from scratch') was a stale reading — the branch held both commits and the pushed state throughout; nothing was redone.", "tests": "At merged HEAD dbe2d6ea6 (readings re-cited from that sha): vitest src/sharing-rule.test.ts = Test Files 1 passed, Tests 112 passed (112), including the new negatives (clean-lowering rule emits zero warn lines; missing condition takes today's skip path, never seeded, meta names parse-error/empty expression). Ablation predicted-then-observed: WARN meta mutated back to { rule, condition }; mutation proven on disk (blob c2ccce614 vs HEAD d93189485; markers 'reason: cause.reason' 1 to 0, plain-meta shape 0 to 1); mutated-tree vitest exit 1 with exactly the two predicted cause-asserting tests red, 110 green (clean-path negative + outcome unit test declared controls, green both directions, not ablation evidence); restore proven by state (hash-object equals HEAD blob, git diff HEAD empty). No dist leg either way: suite resolves the subject via same-package src import, no rebuild in the loop; the closure WAS freshly built in the same lock window before the green leg. Typecheck: both tsc programs exit 0; tsc --listFiles shows bootstrap-declared-sharing-rules.ts in the program (1 hit); test file sits outside every tsc program (pre-existing posture, TEST_DEBT ledger entry 3) — re-measured by the ledger's method with the exclusion lifted: exactly 3 errors, identical pre-existing set, ratchet unmoved. Gate union (dispatch-gates.mjs --repo objectstack-ai/objectstack at dbe2d6ea6, 3 paths): 34 gates exit 0 by their own verdict lines, incl. engine-double-contract, where-matcher, query-options-erasure, type-check-coverage, i18n ('check-i18n-bundles: OK (9 package(s) — all bundles in sync)') and i18n-stale-fill after building the gate's own named prerequisite closure, targeted eslint on both edited files, nul-bytes. NOT MEASURED (declared, CI owns the farm): check:dual-build-cjs-loads exit 3 ('PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... NOT a pass: nothing was measured' — population is every publishable package's dist; required Build Core builds it) and check-test-completeness exit 3 ('Nothing was measured: this gate exited before parsing a single summary line' — reads the CI test farm's summaries). Neither counted as pass or red.", "mcp_calls": "0 — reads via git + public-repo payload channel + repo-scoped REST (probe HTTP 200); writes (draft PR, this comment) via repo-scoped REST", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
⛔ 更正:上一条状态注记(评论 5489054682)说「分支是空的、从头做是对的」——那句是假的,是我读错了
domain:servicesPM 席自认。实测该分支当时(以及全程)持有:3a2643911 feat(plugin-sharing): carry compileCelToFilter's reason and detail into the seeder's skip WARN 文件: .changeset/… · bootstrap-declared-sharing-rules.ts · sharing-rule.test.ts⇒ 不是空的,是完整实现,而且已经推上去了。dev 在返工消息里指出了这一点,并且没有听我的——它先读了盘上状态,所以一行都没有白做。
我错在哪里,精确地说
抢救扫描里我跑的是:
git log --oneline '@{u}..HEAD' | wc -l # 「已提交未推」它量的是未推送的提交数。分支已经推过,所以
@{u}就是origin/<branch>,读数当然是 0。⇒ 我把「没有未推的工作」当成了「没有工作」。⭐ 这是两个不同的命题,而我用一个只能回答前者的命令去断言了后者。
⚠️ 这正是我今晚一路在别人身上纠正的那一类错误——一个零没有配反向对照。 正确的读法本该是git log origin/main..HEAD(相对 main 的提交),或者直接git ls-remote origin refs/heads/<branch>。任何一个都会当场证伪那句话。后果与止损
- 零工作损失:分支早已推送,内容一直在远端;dev 复核了盘上状态而不是采信我的注记。
- 潜在后果不小:如果 dev 照我说的「从头做」,它会重做一份完整实现,并且很可能与已有提交冲突。⇒ 派发令里那句「⛔ 先从盘上复核状态,不要采信你的笔记」这次是替我兜的底,不是替它自己。
- 上一条注记里其余部分仍然成立:锁饱和确实是我 9 派对 3 的调度失误,那两轮确实几乎全花在排队上。只有「分支是空的」这一句是假的。
现状
PR #14136 已开(draft),范围与派发令一致:
celToFilterOutcome新兄弟函数保留{ reason, detail },celToFilter保持已发布签名并委托给它,调用方的 skip WARN 现在带reason+detail,消息串与 skip 判定逐字节不变(ADR-0049 未动)。去重按要求先量后决:该缝是 boot-time、每组织一次(sharing-plugin.ts:679启动循环、:704新组织中间件),受 规则数 × 组织数 约束而非每请求 ⇒ 不加,理由写进了 PR。
Generated by Claude Code
Measured while implementing the observability half of #13639 (PR #13942), which repaired the same shape one seam over. Filed unassigned; no severity asserted, routing is triage's.
Measured, at
577e49e00compileCelToFilterreturns a discriminated result —{ ok: false, reason, detail }— wheredetailnames the concrete fault: the CEL shape that would not lower, thecurrent_user.*path that did not resolve, the parse bound that was overrun.celToFilterin the sharing seeder collapses the whole thing tonull:Its one caller then logs the fact without the reason:
So an operator whose declared sharing rule is silently not granting learns that the condition did not translate, and gets the source text back, but not which part of it the compiler refused or why — the one fact that ends the search, computed and discarded one line earlier.
The
!okconsumer census this came fromEvery non-test call site of the compile-result family (
compileCelToFilter,isPushdownableCel,lowerCelAst), excludingdist/, comments and the defining module:reasondetailpackages/plugins/plugin-security/src/rls-compiler.tspackages/plugins/plugin-sharing/src/bootstrap-declared-sharing-rules.ts:132packages/formula/src/rls-predicate.ts:59(isSupportedRlsExpression)packages/lint/scripts/check-doc-formula-expressions.mjs:962packages/lint/src/validate-rls-predicate-enforceability.ts:283packages/lint/src/validate-sharing-rule-enforceability.ts:438Reverse control: the last two rows are what makes the zeroes readable — the same scan that reports
celToFilteras discarding finds two consumers that carrydetailinto user-facing text, so "discards" is a reading, not a blind spot.Why this is materially LESS severe than #13639, and what is left open
explainreports it asnarrowsrather thandenies— the operator debugging "this user sees no rows" gets no signal #13639's seam ran on every read.isSupportedRlsExpressionreturningbooleanis its published contract, not a defect: it is a shape gate, and both of its own consumers re-derive the explanation fromisPushdownableCelwhen they need one.⛔ Not established here: whether any deployment has actually been bitten, and whether the seeder's WARN should carry
reason,detail, or both. What is measured is that the information exists at the call site and is dropped.Related
#13639 / PR #13942 (the same shape in
plugin-security/rls-compiler.ts, repaired) · ADR-0058 D1 (compileCelToFilteris the one canonical lowering) · ADR-0049 (an unlowerable condition is never seeded as a permissive match-all — that half is correct and unchanged)Generated by Claude Code