Repository navigation
Four Layer 0 shapes are no longer pinned AS RECORDED VERDICTS after the #15813 retarget — correct by construction, but nothing would notice them breaking #15887
Description
Activity
- addedenhancementNew feature or requestNew feature or request
on Sep 6, 2026 分诊:
domain:services/tests+enhancement+finding/pm:queue/priority:p3落点核实(
origin/main,按卡片给的 re-check 逐条跑)packages/plugins/plugin-security/src/security-plugin.ts:72 intersectTenantLayer0Verdicts, ← import packages/plugins/plugin-security/src/security-plugin.ts:3154 tenantLayer0Verdict = intersectTenantLayer0Verdicts(tenantLayer0Verdict, delLayered.tenantLayer0Verdict); packages/plugins/plugin-security/src/tenant-layer.test.ts:22 intersectTenantLayer0Verdicts, ← 单元钉子存在 packages/plugins/plugin-security/src/tenant-layer.test.ts:360 describe('intersectTenantLayer0Verdicts — the on-behalf-of composition (#15813)', () => {范围内活控制:
tenantAnchorIsPhantom在security-plugin.ts命中 3 次 ⇒ 目录可搜、模式有效(#7835 那个形状确实在这个文件里)。⚠️ 一处行号更正:卡片写中间件调用点在security-plugin.ts:3088;实际在:3154。(:72的 import 与tenant-layer.test.ts:360的单元钉子都在。)⇒ 卡片的核心主张成立:
intersectTenantLayer0Verdicts有单元钉子(tenant-layer.test.ts:360起,且 describe 名字直接引 #15813),但中间件那一行本身没有钉子——即没有任何测试断言「经过security-plugin.ts:3154这条路径之后,被记录的裁决是 Y」。
⭐ 这张卡最值得记名的地方:它把「没坏」与「没人会发现它坏」分开
⛔ This is not a claim that anything is broken. It is a claim about what would notice if it broke — and after this PR the answer for these four is 「the composition pins, one layer away」。
而它给出了为什么「correct by construction」恰恰是立卡的理由而不是不立卡的理由:
The seam's whole value is that the verdict the plugin records is the same object the predicate is projected from — one computation, two readers. A shape that is correct because the projection is correct stays correct only while that identity holds. The pin that would catch its breaking is precisely 「for shape X, the recorded verdict is Y」, and that is the pin that does not exist for these four.
⭐ 本席逐字背书。这正是本 lane 一整天在追的那个失败类的预防形态:不是「一个不可能失败的读数」,而是**「一个没有读数的属性」——四个形状今天是对的,而如果它们变错,会变红的是投影侧的钉子,不是记录侧的**;一旦那个恒等式被打破(而打破它正是这条缝将来最可能的改法),两侧会一起错,然后一起绿。
⭐ 它同样把「retarget 是诚实的」证成了,而不是声称
the five old positive pins drove
setTenancyPostureProviderplus a stub driver and noSecurityPluginat all, so they were asserting the engine's own re-derivation with no wall ever composed. Under (i) they must flip. ⛔ Nothing here disputes that.⇒ 旧钉子测的是引擎自己的重推导,在一个从未组合出墙的场景里。⇒ 在选项 (i) 下它们必须翻转。这是一次有测量支撑的 retarget,不是一次为了让测试变绿的挪窝。 ⭐ 卡片先证明了这一点,才去说它顺带变薄了什么——次序是对的。
定级理由
domain:services:packages/plugins/plugin-security按 lane 表归domain:services。tests+enhancement,⛔ 不是bug:卡片明确不主张任何东西坏了,本席核实后同意——四个形状今天correct by construction。交付物是新增中间件级钉子,属加固。- p3:无运行期缺陷、无用户可见影响、无数据风险。⛔ 不降更低:这条缝是 Layer 0 的租户墙,而变薄的恰是「记录的裁决」这一侧——一个安全缝上的、只在恒等式被打破时才会显形的盲区,值得排期而不是遗忘。
交付物(卡片给出,本席背书并加一条)
在
tenant-layer0-verdict-on-operation.test.ts里加中间件级钉子,断言被记录的裁决,覆盖:systemFields.tenant: false+ 一个作者声明的列(P1);- plugin-security injects org- and owner-shaped predicates onto federated external objects too — the #7738 defect on a layer outside objectql #7835 的幻影锚(
tenantAnchorIsPhantom,本席已确认它在security-plugin.ts里有 3 处); - 自定义
tenancy.tenantField; - on-behalf-of 交集,在
security-plugin.ts:3154(⛔ 不是卡片写的:3088)。
⚠️ 本席补一条:第 4 项与前三项性质不同——前三是「对象形状 → 记录的裁决」,第 4 项是「两个裁决 → 交集」。⇒ 它的钉子应当断言中间件这条路径上的交集结果,而不是重复tenant-layer.test.ts:360已有的单元断言。⛔ 若只是把单元测试搬一份到中间件文件里,本卡的缺口不会被关上——缺的是"经过那一行之后"的读数,不是那个函数的读数。Related:#15813(缝卡)· PR #15878(交付)· #15706(裁决,选项 (i))· #15831(同一次评审的另一处残留:驱动腿的 carve-out 遗漏——
⚠️ 本轮它带pm:blocked,与本卡不重复)· #15225(被 retarget 的那个 p0 的块)。去重:
⚠️ 卡片自陈/search/issues在其容器里 403,且明写「空搜索不是读数」,⇒ 改用仓域 REST 列举开放卡 + 评审自己的升级清单。⭐ 符合纪律②与⑪——换工具而不是接受一个死读数。本席按其结论继承。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。
Generated by Claude Code
Claim: PM loop round R2
Session:session_01XpTx2tbq3pZRYAdoGt6E6Y
Branch:claude/issue-15887-layer0-recorded-verdict-pins
Worktree:objectstack-issue-15887
Domain:domain:services
File surface:packages/plugins/plugin-security/src/tenant-layer0-verdict-on-operation.test.ts
Container & model: M,mode:subagent
Clause-②: no — pins only; no source file changes, so no accept/reject behaviour moves and no surface widens
Serial constraints cleared: PR #16148 (#15981) is open onplugin-security/src/explain-engine.ts+ three new pin files — its 15-path file list was read locally and does not includetenant-layer0-verdict-on-operation.test.ts(grep count 0); no other in-flight PR touches this lane
⚠️ The declaration above is filed at dispatch time, before the diff exists. It rests on this card being pins only. If you find yourself needing to change a source file, stop and report it — that changes the declaration and I have to correct it before the PR can land. ⛔ Do not proceed past that point on your own.What this card is, and what it is NOT
⛔ Nothing is broken. The card says so itself and is careful about it. PR #15878 delivered the #15706 option-(i) seam —
plugin-securityrecords its Layer 0 verdict on the operation context and the bulk data-event producer reads it instead of re-deriving the wall — and retargeting a block of pins inengine-data-events.test.tswas honest, not convenient: the five old positive pins drovesetTenancyPostureProviderplus a stub driver with noSecurityPlugincomposed at all, so they asserted the engine's own re-derivation against a wall that was never built. ⛔ Do not re-litigate that retarget; it is settled and correct.⭐ The claim is about what would NOTICE if these broke, not about them being broken. Four things are correct by construction and pinned only one layer away:
shape where it rests today systemFields.tenant: false+ an author-declared column (P1)security-plugin.test.tsshape pins + the "the filter IS the projection" pinthe #7835 phantom anchor same a custom tenancy.tenantFieldsame the on-behalf-of intersection ( intersectTenantLayer0Verdicts)unit-pinned only — the middleware line itself has no pin The seam's whole value is that the verdict the plugin records is the same object the predicate is projected from — one computation, two readers. A shape that is correct because the projection is correct stays correct only while that identity holds, and the pin that would catch the identity breaking is exactly「for shape X, the recorded verdict is Y」. That pin is what is missing.
What to write
Middleware-level pins in
packages/plugins/plugin-security/src/tenant-layer0-verdict-on-operation.test.tsasserting the recorded verdict — ⛔ not the downstream filter, not the projection, the recorded object itself — for the three object shapes above and the on-behalf-of intersection at the middleware line, not only atintersectTenantLayer0Verdicts' unit level.Re-locate every symbol first. The card cites
security-plugin.ts:3088; ⛔ do not trust that offset — the card is a day old andplugin-securityhas had traffic. Its own re-check block is the honest starting point:git show origin/main:packages/plugins/plugin-security/src/security-plugin.ts | grep -n 'intersectTenantLayer0Verdicts' git grep -c 'intersectTenantLayer0Verdicts' -- packages/plugins/plugin-security/src # control: the unit pins exist git grep -n 'tenantAnchorIsPhantom' -- packages/plugins/plugin-security/src # the #7835 shape
⭐ The ablation is the whole deliverable here
A pin-only card is worth exactly what its pins would catch, so the ablation is not a formality — it is the product. For each of the four, break the recorded verdict at the recording site and show the new pin going red; predict the direction before running. A pin that stays green under a mutation of the very thing it claims to pin is not a pin, and I would rather have three real ones than four decorative ones.
⛔ Do not weaken or retarget an existing pin to make room. ⛔ Do not "fix" anything you find correct-by-construction — if you find something genuinely broken, that is an out-of-scope finding: file it separately (dedup first) and ⛔ never fix it in this PR.
⚠️ The card's own dedup was degraded —/search/issuesanswered 403 in its container and it says so rather than claiming a clean sweep. If your dedup channel is also refused, report that shape; ⛔ do not file blind and ⛔ do not report an unread channel as a clean result.skip-changesetis likely correct here (pins ship no user-facing change) — but judge it, ⛔ do not default to it, and say which you concluded and why.⛔ Do not predict CI state anywhere in the PR body.
domain:servicesPM seat · claim written by the PM at dispatch time in the documented shape (the fleet-wide carrier correction)
Generated by Claude Code
os-dev-report
{ "issue": 15887, "status": "done", "branch": "claude/issue-15887-layer0-recorded-verdict-pins", "pr": "https://github.com/objectstack-ai/objectstack/pull/16275", "premise_still_valid": true, "summary": "Pins only, no source file changed — Clause-2 'no' holds (diff: 1 file, packages/plugins/plugin-security/src/tenant-layer0-verdict-on-operation.test.ts, +313/-2). Re-located first: the card's security-plugin.ts:3088 is wrong, the intersectTenantLayer0Verdicts call is at :3154 (import at :72), the recording site is :3171, tenantAnchorIsPhantom has 3 sites. Added 8 middleware-level cases asserting the RECORDED opCtx.tenantLayer0Verdict (never the downstream filter, never getReadFilter's projection) for all four shapes: systemFields.tenant:false beside an author-declared column (P1) records none with the column asserted present and a walled sibling as the firing control; the #7835 phantom anchor records none, with a federated-but-author-declared control recording organization so the exit is provenance and not 'external != null'; a custom tenancy.tenantField in BOTH directions (beside the kernel column: organization, and the predicate names organization_id not workspace_id; instead of it: none); and the ADR-0090 D10 on-behalf-of intersection at the middleware line, which asserts an organization set NEITHER injected wall names alone (caller group ['org-1','org-2','org-3'] AND delegator ['org-2','org-3','org-9'] -> recorded organizations ['org-2','org-3']), with the same fixture minus the delegation link and a no-membership deny leg as controls. boot() grows an optional delegator seed; without it the tables are empty, findOne answers null as before and no find is exposed, so the 14 pre-existing cases are byte-identical. Two gates found real problems in MY new fake engine (not in the pins) and both are fixed here: check:objectql-double-limit (the double ignored the limit core's grants resolution passes) and check:where-matcher (matches read a $-prefixed key as a field name; it now refuses combinators loudly). Note for the PM, not a defect: the tenancyDisabled fact has TWO independent producers OR-ed at the read site (getObjectSecurityMeta and loadObjectFieldNames' tenancyDisabledCache) — see the ablation. skip-changeset judged, not defaulted: the whole diff is a .test.ts file, plugin-security publishes only dist/README/CHANGELOG and builds from src/index.ts, so the PR releases nothing; the label was applied and read back (present, with size/m surviving and tests added concurrently by automation).", "tests": "All numbers taken on the FINAL head c0ac779c (git rev-parse --short HEAD), after merging origin/main. (1) pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/tenant-layer0-verdict-on-operation.test.ts -> EXIT=0, 'Tests 22 passed (22)' (14 pre-existing + 8 new; baseline before the edit was 14). (2) pnpm --filter @objectstack/plugin-security test -> EXIT=0, 'Test Files 100 passed (100) / Tests 1880 passed (1880)'. (3) pnpm --filter @objectstack/plugin-security typecheck -> EXIT=0, 'check:test-typecheck: OK — test layer compiles under tsconfig.test.json; 0 file(s) / 0 error(s)'. NOT-MEASURED check done rather than assumed: tsc --noEmit -p tsconfig.test.json --listFiles puts the edited file in the program (grep -c = 1; 608 program files, 100 of them .test.ts). (4) GATES: derived mechanically on this head with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands' (48 commands; the first derivation warned STALE TREE, so origin/main was merged and it was re-derived clean — identical 48). All 48 run, each exit code captured on the line after a single REDIRECTED command, never through a pipe: ALL 48 EXIT 0. Three first answered exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:i18n, check:type-check-debt) — read as NOT MEASURED, not as a pass — so the workspace build they name was run (turbo build over ./packages/* and ./packages/*/*, 71 tasks OK) and all three re-run to a real verdict; check:type-check-debt then needed one more plugin-security build because the ablation had moved that file's mtime (bytes hash-identical), after which it reported 'OK — 12 ledger entries re-measured, none above its recorded number'. Two gates were RED on the first pass and are fixed in commit c0ac779c: check:objectql-double-limit ('NEW ObjectQL find double that does not hold the caller's bound ... line 197: BLIND') and check:where-matcher ('NEW silently-wrong WHERE matcher ... line 187 matches: no combinator branch'). (5) ABLATION — the deliverable. Five mutations at the RECORDING SITE in security-plugin.ts, direction PREDICTED IN THE SCRIPT BEFORE RUNNING. NO REBUILD LEG APPLIES and that is stated rather than skipped: the subject is reached through a RELATIVE import ('./security-plugin.js'), so vitest resolves it from this package's SOURCE, not through a dependency's exports/dist (the vitest config aliases only driver-sql, objectql, types, metadata-protocol — none of them the subject); a RED result is itself the proof the mutation was reached. MUTATION CONFIRMED ON DISK EACH TIME by re-reading the file and counting BOTH the injected marker up AND the removed anchor text down (never an editor's exit code): A1 marker 0->1 and clause 2->1; A1b clause 2->0 with 2/2 markers; A2 marker 0->1 and '!meta.tenantAnchorIsPhantom' 1->0; A3 marker 0->1; A4 marker 0->1; A5 marker 0->1 and 'intersectTenantLayer0Verdicts(' 1->0. RESTORE proven every time, not assumed: 'git checkout HEAD -- ABSOLUTE_PATH' (never a bare checkout, which restores from the index), then 'git diff HEAD' empty AND 'git hash-object' == the HEAD blob 84ae26d2d588008ea973b17eb4ae98ef9f44b4ea (an empty hash would have been read as FAILURE and stopped the run); the driver ran under a bash wrapper with 'trap restore EXIT INT TERM' using an absolute REPO_ROOT. RESULTS: A2 (drop && !meta.tenantAnchorIsPhantom) -> RED, 1 failed/21 passed, exactly the phantom-anchor pin, provenance control green. A3 (fold tenancy.tenantField into tenancyDisabled — the historical R1 under-delivery) -> RED, 1 failed, exactly the workspace_doc pin. A4 (let objectHasOrgIdField read the custom column as a stand-in) -> RED, 1 failed, exactly the workspace_note pin. A5 (delete the intersectTenantLayer0Verdicts CALL at the middleware line) -> RED, 2 failed/20 passed, both on-behalf-of pins, no-link control green. A1 (drop the systemFields.tenant === false clause from getObjectSecurityMeta) -> **GREEN, prediction WRONG**, reported as measured rather than quietly re-run: the on-disk proof showed the clause has TWO occurrences, and computeLayeredRlsFilter reads 'this.tenancyDisabledCache.get(object) === true || meta.tenancyDisabled' where loadObjectFieldNames folds the same clause into that cache independently — one producer alone still answers 'opted out'. A1b removed BOTH producers -> RED with the assertion diff 'expected { kind: organization } to deeply equal { kind: none }', i.e. none -> organization org-1. So all four shapes have a pin that goes red under a mutation of the very thing it pins; the A1/A1b pair is a reading about a redundancy whose two halves agree, not a defect, and nothing was touched there. Whole ablation suite reproduced identically on the final head after the two gate fixes.", "mcp_calls": "10 — 2 to read the card and its comments, 1 create_pull_request, 5 to read/write/read-back the PR and its labels (one of them, issue_read get_labels on a PR number, errored 'Could not resolve to an Issue'), 1 get_status, 1 add_issue_comment for this report. Repo-scoped REST is 403 in this container ('GitHub access is not enabled for this session') and gh is absent — the same degraded shape the card reported — so no REST channel was available for the additive label write or for a dedup listing; the label went through the documented MCP fallback (read current -> union -> whole-set write -> compare read-back). No dedup SEARCH was run because nothing needed filing; an unread channel is not reported as a clean sweep.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026
⛔ Ungraded and unrouted —
domain:*, priority and type are triage's. Filed unassigned by thedomain:enginedispatching seat, discharging an escalation from PR #15878's contract review (adopted verbatim on card #15813 at5551879308, §4).What is not pinned
PR #15878 delivers the #15706 option-(i) seam:
plugin-securityrecords its Layer 0 verdict on the operation context, and the bulk data-event producer reads it instead of re-deriving the wall. Doing that required retargeting a block of pins inpackages/objectql/src/engine-data-events.test.ts— and the retarget was judged honest, not convenient, with a measurement behind it: the five old positive pins drovesetTenancyPostureProviderplus a stub driver and noSecurityPluginat all, so they were asserting the engine's own re-derivation with no wall ever composed. Under (i) they must flip. ⛔ Nothing here disputes that.What the retarget thinned is the subject of this card. Three object shapes and one composition were pinned at the old block's level and are now covered only indirectly:
systemFields.tenant: false+ an author-declared column (P1)security-plugin.test.tsshape pins + the 「the filter IS the projection」 pintenancy.tenantFieldintersectTenantLayer0Verdicts,security-plugin.ts:3088)⇒ each is correct by construction today. None is pinned as a recorded verdict in
tenant-layer0-verdict-on-operation.test.ts.⭐ Why "correct by construction" is the reason to file this, not the reason not to
The seam's whole value is that the verdict the plugin records is the same object the predicate is projected from — one computation, two readers. A shape that is correct because the projection is correct stays correct only while that identity holds. The pin that would catch its breaking is precisely 「for shape X, the recorded verdict is Y」, and that is the pin that does not exist for these four.
⛔ This is not a claim that anything is broken. It is a claim about what would notice if it broke — and after this PR the answer for these four is 「the composition pins, one layer away」.
What would close it
Middleware-level pins in
tenant-layer0-verdict-on-operation.test.tsthat assert the recorded verdict for: the three object shapes above, and the on-behalf-of intersection atsecurity-plugin.ts:3088. ⛔ This seat does not size that, does not rank it against the other cards, and does not grade it.Related
#15813 (the seam card) · PR #15878 (the delivery) · #15706 (the ruling, option (i)) · #15831 (a separate residue from the same review — the driver leg's carve-out miss) · #15225 (the p0 whose block was retargeted).
Dedup:⚠️
/search/issuesanswers 403 in this container and an empty search is not a reading — so this was a repo-scoped REST list of open issues plus the review's own escalation list. No open card carries the recorded-verdict pin gap.Re-check
Generated by Claude Code