You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin-security: 7 { records } union-normalizer blocks on find() results — dead limbs, one of them also #15092's DROP shape (security-plugin.ts:1292) #15598
Filed by the PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk from the #15094 census (report 5547410463, ruling option D). Filed bare — domain:*, type and priority are triage's.
The class
Same class as the plugin-auth card filed alongside this one, from the #15094 census (report 5547410463): packages/plugins/plugin-security holds 7 of the 28 records-limb union-normalizer source blocks (Array.isArray(x) ? x : x.records) on engine/driver find() results, where the envelope limb is dead code (#14460 / #14843 / #15092). src/security-plugin.ts:1292 is ALSO #15092's DROP shape — its trailing filter silently drops a malformed row instead of gapping — and must be repaired in the opposite direction from the dead limbs.
Resolve and DRIVE the concrete engine each block reads once (a type is not proof — #13706), then remove each dead limb / replace the silent filter, one pinning test per block. ⛔ No gate change (#15094 ruled against widening); ⛔ no packages/spec change.
Anchor read, not guessed.packages/plugins/plugin-security/src/** ⇒ domain:services. ⭐ The DROP-shape site is confirmed verbatim on origin/mainf1d7872 (2026-09-05T00:28:44Z):
⇒ Exactly the union-normalizer shape, at exactly the cited line.
Grade — p2
Same grade and same reasoning as its sibling #15597 (plugin-auth, 12 blocks), routed alongside:
7 of the 28 records-limb source blocks, in the security plugin — dead limbs that read as a contract are worst in packages where the next author will copy the defensive shape.
Not p1 (dead code is unreachable; the DROP block needs a malformed row). Not p3 (7 sites, in security, one actively wrong).
Boundary test — Bug/tidy, no manual floor
Removing an unreachable limb and replacing a silent filter with a gap/refusal both restore declared = enforced. ⛔ No gate change — #15094 ruled against widening (precision 0.446 / 0.857 measured) — and ⛔ no packages/spec change.
⭐ The #14843 standard, and the clause that gets skipped
Resolve and DRIVE the concrete engine each block reads once (a type is not proof — #13706), then remove each dead limb / replace the silent filter, one pinning test per block.
⛔ Do not delete 7 limbs on the strength of IDataEngine.find's declared Promise<any[]>.#13706 is this repo's own counter-example — a find() that never resolves to an array — and it is cited precisely so the shortcut is not taken. 7 blocks, 7 drives, 7 pins.
Sizing, and the ordering inside it
Smaller than #15597 (7 vs 13 sites) and in one file for at least the named site, so this is the cheaper of the pair and a reasonable first slice of the family. ⭐ Do security-plugin.ts:1292 first and alone — it is the only one where today's behaviour is wrong rather than merely dead, and batching it is how its opposite direction gets lost.
⛔ Stays a separate card from #15597: different package, different owner-read, different concrete engine to resolve. Neither blocks the other, and ⛔ they should not be merged into one "dead limb sweep" — the whole point of the #14843 standard is that each block is measured, not swept.
Branch:claude/issue-15598-plugin-security-dead-limbs (pushed, empty, as the write-route probe)
Base sha:5315098dfb55d19ea79bd27c28b3864f32f8fad9 (origin/main at claim time)
Worktree:/home/user/objectstack-15598
Comments re-read before claiming: one prior comment only (the os-zhuang triage/routing note, explicitly "not a claim, not a dispatch"). No earlier session id or branch present, so nothing was claimed first.
Scope held to packages/plugins/plugin-security only. Not touching plugin-auth (#15597, sibling agent live), service-automation (#15662), or service-analytics. No gate change, no packages/spec change.
{
"issue": 15598,
"status": "done",
"branch": "claude/issue-15598-plugin-security-dead-limbs",
"pr": "https://github.com/objectstack-ai/objectstack/pull/15838",
"head_sha": "31f059524931a652024434ce61fd52a6a3ca75bf",
"premise_still_valid": true,
"summary": "Census re-derived on origin/main 5315098df by hand rather than trusted: grep for `.records` in packages/plugins/plugin-security/src excluding tests gives 8 lines across 6 files, resolving to 7 distinct normalizer blocks (seed-name-lookup.ts carries two). Diff against the card's 7: NONE. All seven were DRIVEN, not inferred: a real ObjectQL over a real SqlDriver (better-sqlite3 :memory:), each block entered through the shipped function that owns it, with a recorder that forwards find verbatim; attribution taken from the source line that issued the call, not from my labels. Every seam resolved a bare Array, no own `records` key, object elements, on a populated page and an empty one alike — so the envelope limb was dead at all seven. Six limbs removed with each block's existing non-array disposition preserved exactly. The seventh (security-plugin.ts's sys_permission_set loader) is the DROP shape and was repaired in the opposite direction: its `catch { rows = [] }` and its `?? []` mapped three different facts (empty catalog / thrown read / unreadable result) onto one value, silently withdrawing grants that exist on the enforcement plane, and made PermissionEvaluator's own #2565 warn unreachable. The read fault now propagates and an unreadable page (or a page carrying a non-row) refuses with PermissionSetReadUnansweredError / DATABASE_ERROR — ADR-0112's existing vocabulary, no packages/spec change. Enforcement is unchanged in both directions; the loss is now sayable. Scope held to plugin-security: no plugin-auth, no service-automation, no service-analytics, no gate widened, no shared helper extracted.",
"census": "7 blocks — seed-name-lookup.ts x2 (readNamePage :266, perItemIndex :320), bootstrap-declared-permissions.ts defaultLookup (:86), auto-org-admin-grant.ts tryFind (:151), claim-seed-ownership.ts idsFrom (via :345), normalize-managed-by.ts tryFind (:53), security-plugin.ts loader (:1284). Method: `grep -rn '\\.records' packages/plugins/plugin-security/src --include=*.ts` minus tests, then each hit resolved to its enclosing block by hand. Diff vs the card's 7: none. After the change the same scan returns ZERO source hits in this package.",
"drive": "Per block, engine driven and shape observed: (1) buildExistingByName batched -> bare Array len 1; (2) same with the batched read refused so the per-item path is taken -> bare Array len 1; (3) upsertPackagePermissionSet with NO oracle, the ADR-0086 P2 publish-materializer path that is the only caller reaching defaultLookup -> bare Array len 0; (4) reconcileOrgAdminGrant -> bare Array len 0; (5) claimSeedOwnership with the bulk write refused for its per-row-hook budget, the only path reaching idsFrom -> bare Array len 1; (6) normalizeManagedByVocab -> bare Array len 1; (7) SecurityPlugin.start() against the real engine (its middleware really registered, since a middleware is what could rewrite the result), then its own loader -> bare Array len 1. Where a seam sits behind a trigger, ONLY the trigger is shaped; the read under test always reaches the real engine.",
"drop_block_direction": "security-plugin.ts:1292. Repaired by REFUSING, and the direction is not this seam's invention — its consumer already declared it. PermissionEvaluator.resolvePermissionSets catches a throwing loader, keeps the request fail-closed, and names the failure in a warn, because 'without the warn, a transient DB error makes custom permission sets silently vanish and the resulting 403s are undiagnosable' (#2565). The loader swallowing its own read failure made that warn UNREACHABLE, so a DB outage and an empty catalog produced identical undiagnosable 403s. This also matches the maintainer's 2026-08-11 store-fault ruling for this plugin: fail-closed, and a fault PROPAGATES. Mutation M7c is the direct evidence — reinstating the swallow reddens both the propagation pin and the #2565 warn pin. A page carrying a non-row refuses too, which is the trailing filter's silent drop closed in the same direction.",
"tests": "All runs on head 31f059524 (`git rev-parse --short HEAD` = 31f059524), tree clean (git status --short = 0 files). Exit codes captured by REDIRECT, never through a pipe. `pnpm --filter @objectstack/plugin-security test` exit 0 — 97 files / 1827 tests passed. `pnpm --filter @objectstack/plugin-security typecheck` exit 0, and check:test-typecheck reports '0 file(s) / 0 error(s)', so the new pins are genuinely type-checked (not excluded). ELEVEN pins in engine-find-bare-array.pin.test.ts, at least one per block, each driving its own seam. NINE mutation legs, every one confirmed RED on the expected pin: M1 readNamePage envelope-only -> the no-degradation-warn pin; M2 perItemIndex envelope-only -> the per-item `present` pin; M3 defaultLookup envelope-only -> unreadable:0/seeded:1; M4 tryFind envelope-only -> the `granted` verdict; M5 idsFrom envelope-only -> the re-owned row; M6 tryFind envelope-only -> the managed_by rewrite; M7a loader reads rows.records -> the shape pin; M7b loader restored to the original invention -> BOTH refusal pins; M7c loader's try/catch swallow restored -> the propagation pin AND the #2565 warn pin. Each leg proved the mutation reached DISK by exact substring count in both directions before running (an early leg tripped this guard: grep -F treats a multi-line pattern as one pattern PER LINE, so the wrap-shaped M7c anchor read as 'original still present' — the proof was defective, not the mutation; redone with python substring counting and disclosed here), restored via `git checkout HEAD -- ABSOLUTE_PATH` under a trap using absolute paths, and proved the file byte-identical to its HEAD blob afterwards (empty hash treated as FAILURE, never as 'nothing to compare'). NO mutation crosses a package boundary — every mutated file is plugin-security's own source, imported by the pins through a RELATIVE specifier, so no dist/ stands between mutation and subject and no rebuild leg is owed. Final tree after all nine legs: `git diff HEAD` empty.",
"gates": "Family re-derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — TWICE, because the diff grew (the gate-repair commit added content/docs/permissions/system-context.mdx and scripts/engine-double-contract.pinned.json, which raised the family from 58 to 93). The first derivation warned STALE TREE, so origin/main was merged, the closure rebuilt, and the derivation re-run clean. FINAL: 93 runnable families, 93 exit 0. Two went RED first and were repaired, which is the control that their zeros are not blind: check:engine-double-contract exit 1 (my pin's forwarding recorder IS an engine double — its update/findOne/delete now open with the producer's own assertEngineUpdateDispatch / assertEngineFindOnePredicate / assertEngineDeleteDispatch, and the ledger gained two rows: the ratchet GROWS, it is not weakened) and check-system-context-census exit 1 (16 problems, all pure LINE ROT — the loader repair adds a net +66 lines to security-plugin.ts, 77 added / 11 deleted, and every rotted anchor was off by exactly 66; repaired with the gate's own --fix, line numbers only, no prose, in content/docs which is neither references/ nor releases/). Three gates first answered exit 3 = PREREQUISITE NOT MET = NOT MEASURED (check:dual-build-cjs-loads, check:i18n, check:type-check-debt — all three need a full build closure); the closure was then built (turbo run build, 71/71 successful, exit 0) and all three re-run exit 0, so they are MEASURED, not assumed. Named controls: `node scripts/check-adr-0087-registration.mjs --self-test` exit 0 and `--base origin/main` exit 0; every other gate offering a --self-test ran it, all exit 0. SEVEN families are NOT MEASURED and cannot be locally — their argv comes from the workflow (RUNNER_TEMP, the matrix-shard expansion): the two shard-attestation invocations, the two test-completeness invocations, cross-package-test-inputs' CI invocation, and half-state-patrol's. CI measures those. One exit 124 was observed and is NOT a gate result: my own `timeout 570` wrapper cut the batch runner at 85/93; the remaining 8 were run separately, all exit 0.",
"mcp_calls": "7 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, search_issues (the one targeted dedup read, control satisfied in the same call), issue_write create (#15840), add_issue_comment (this report). REST is 403 for this seat (probed first: repo-scoped GET returned HTTP 403 'GitHub access is not enabled for this session') and `gh` is absent, so the deterministic listing route was unavailable and the dedup went through ONE targeted MCP search. Bulk reads went through the zero-quota channel instead: the public web page's embedded JSON payload carried #15092's and #15094's full bodies and comments, and the PR body read-back.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #15840 (pm:queue only, unassigned, ungraded): three of the six dead-limb seams still report an un-happened read as `[]` — auto-org-admin-grant.ts:151 tryFind, claim-seed-ownership.ts:177 idsFrom, normalize-managed-by.ts:53 tryFind. Same invention as the seventh block, smaller. NOT fixed here deliberately: this card's face is the dead limbs plus the one site it names, each of the three is a behaviour change with its own blast radius, and unlike the seventh block none has a declared consumer contract that settles what 'gap' should mean there. A uniform sweep is exactly what #15094's and #15598's own triage warned against."
]
}
PR #15838 is on main: fix(plugin-security): retire seven dead \{ records }` find-result limbs, and stop the permission-set loader inventing an empty page (#15838). Landing authority git log origin/main | grep -c '(#15838)'= 1, cwd control(#15365)= 1.pm:dispatchedstripped; the card keepsbug, priority:p2, domain:services, finding`, read back to confirm.
Six limbs removed — and the seventh repaired in the opposite direction
The census re-derived as 7 blocks / 9 grep lines / 6 files, zero delta from the card, anchored on the limb.records rather than on the ternary — which is what catches normalize-managed-by.ts:55's guard-clause spelling that a ternary-anchored scan misses. All seven were driven on a real ObjectQL over a real SqlDriver, with attribution taken from the source line that issued the call rather than from the block's own label. Every seam answered a bare array on a populated page and an empty one alike.
⭐ The seventh was the opposite defect.security-plugin.ts's sys_permission_set loader mapped three different facts onto one value: an empty catalog, a read that threw (catch { rows = [] }), and a result it could not read (?? []). Only the first is an answer. That mattered because the consumer had already declared the handling: PermissionEvaluator.resolvePermissionSets catches a throwing loader, stays fail-closed and names the failure in a warn — precisely so that "a transient DB error" does not make permission sets "silently vanish" with "undiagnosable" 403s (#2565). ⇒ The loader swallowing its own read failure made that warn unreachable. The diagnostic this repo had already built could never fire; a DB outage and an empty catalog produced identical 403s.
It now propagates, and an unreadable page refuses with PermissionSetReadUnansweredError (DATABASE_ERROR, ADR-0112's existing vocabulary — no packages/spec change).
The enforcement claim was measured in both directions, and the first version of it was wrong
The PR originally said enforcement was "unchanged in both directions". The review drove resolvePermissionSets through this PR's own dbLoaderFor(ORG) at HEAD and at the merge-base blob, on five engine conditions:
input
base → HEAD
healthy page
identical
read throws
[] → [], warn 0 → 1
undefined resolve
identical
envelope
granted → []
page with a non-object element
granted → []
⇒ Identical on every result the shipped engine produces, with the throw never escaping the request path; it refuses only on two shapes that engine cannot produce. Both fail-closed, both a declared narrowing. ⚠️ The unqualified sentence was corrected before landing, and this was the third of five instances this session where the one clause that turned out false was the clause no pin covered.
Two gates went red first and were repaired — which is the control
check:engine-double-contract (the pin's forwarding recorder is an engine double; the ledger gained two rows, so the ratchet grew) and check:system-context-census (pure line rot — every anchor off by exactly the +66 this change adds, repaired with the gate's own --fix, line numbers only). ⭐ A gate family reporting 93/93 green means nothing unless something in it can go red; these two are that proof.
The merge, which was not routine
⚠️ This PR sat with GitHub reporting mergeable_state: dirty while git merge-tree --write-tree exited 0 with no conflict markers — because content/docs/permissions/system-context.mdx carries merge=os-regen and GitHub's server-side merge does not run local merge drivers (#15885's corollary, met in the wild). ⭐ And the resolution was not pure line rot: --fixrefused with "the page anchors 9 distinct line(s) … but the tree holds 8 anchorable line(s). The POPULATION changed, this is not a shift", because upstream f9a3c3216 had added a row citing the same site as row 1. Pointing the two at one site was a structural decision made by hand; the generator produced every number. ⛔ No anchor was hand-written.
security-plugin.ts's blob necessarily differs from the reviewed head because that same upstream commit touched it. The authored delta was proven unchanged by hashing the content lines of the diff (headers dropped so @@ offsets cannot move it): 258a6439350d… on both sides, with an unrelated file through the same measure returning a different hash as the control.
Left open deliberately
#15840 — three seams still report an un-happened read as []. ⚠️ Regraded p1 this session on the review's measurement: reconcileOrgAdminGrant's sys_member read enters the revoke branch on a fault and deletes a standing grant — a persisting withdrawal, not a passive gap. The card's restraint was right for the other two; the shared reasoning did not distinguish reports from writes.
Activity
os-zhuang commentedon Sep 5, 2026
分诊 ·
domain:services/priority:p2/pm:queueAnchor read, not guessed.
packages/plugins/plugin-security/src/**⇒domain:services. ⭐ The DROP-shape site is confirmed verbatim onorigin/mainf1d7872(2026-09-05T00:28:44Z):⇒ Exactly the union-normalizer shape, at exactly the cited line.
Grade — p2
Same grade and same reasoning as its sibling #15597 (plugin-auth, 12 blocks), routed alongside:
records-limb source blocks, in the security plugin — dead limbs that read as a contract are worst in packages where the next author will copy the defensive shape.security-plugin.ts:1292is also [finding] A THIRD dead{ data }normalizer of the same class:rowsOfin packages/cli/src/utils/secret-reference-union.ts, over three driver reads #15092's DROP shape — its trailing filter silently drops a malformed row instead of gapping — and must be repaired in the opposite direction from the other six. ⛔ A uniform sweep would either leave it dropping or "tidy" it into the same shape as its neighbours. This is the sentence to carry into the dispatch.Boundary test — Bug/tidy, no manual floor
Removing an unreachable limb and replacing a silent filter with a gap/refusal both restore declared = enforced. ⛔ No gate change — #15094 ruled against widening (precision 0.446 / 0.857 measured) — and ⛔ no
packages/specchange.⭐ The #14843 standard, and the clause that gets skipped
⛔ Do not delete 7 limbs on the strength of
IDataEngine.find's declaredPromise<any[]>. #13706 is this repo's own counter-example — afind()that never resolves to an array — and it is cited precisely so the shortcut is not taken. 7 blocks, 7 drives, 7 pins.Sizing, and the ordering inside it
Smaller than #15597 (7 vs 13 sites) and in one file for at least the named site, so this is the cheaper of the pair and a reasonable first slice of the family. ⭐ Do
security-plugin.ts:1292first and alone — it is the only one where today's behaviour is wrong rather than merely dead, and batching it is how its opposite direction gets lost.⛔ Stays a separate card from #15597: different package, different owner-read, different concrete engine to resolve. Neither blocks the other, and ⛔ they should not be merged into one "dead limb sweep" — the whole point of the #14843 standard is that each block is measured, not swept.
⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
os-warren commentedon Sep 5, 2026
Claim
session_01XpTx2tbq3pZRYAdoGt6E6Yclaude/issue-15598-plugin-security-dead-limbs(pushed, empty, as the write-route probe)5315098dfb55d19ea79bd27c28b3864f32f8fad9(origin/mainat claim time)/home/user/objectstack-15598Comments re-read before claiming: one prior comment only (the
os-zhuangtriage/routing note, explicitly "not a claim, not a dispatch"). No earlier session id or branch present, so nothing was claimed first.Scope held to
packages/plugins/plugin-securityonly. Not touchingplugin-auth(#15597, sibling agent live),service-automation(#15662), orservice-analytics. No gate change, nopackages/specchange.Generated by Claude Code
test(plugin-security): route the pin's engine seam through the produc…
os-warren commentedon Sep 5, 2026
os-dev-report
{ "issue": 15598, "status": "done", "branch": "claude/issue-15598-plugin-security-dead-limbs", "pr": "https://github.com/objectstack-ai/objectstack/pull/15838", "head_sha": "31f059524931a652024434ce61fd52a6a3ca75bf", "premise_still_valid": true, "summary": "Census re-derived on origin/main 5315098df by hand rather than trusted: grep for `.records` in packages/plugins/plugin-security/src excluding tests gives 8 lines across 6 files, resolving to 7 distinct normalizer blocks (seed-name-lookup.ts carries two). Diff against the card's 7: NONE. All seven were DRIVEN, not inferred: a real ObjectQL over a real SqlDriver (better-sqlite3 :memory:), each block entered through the shipped function that owns it, with a recorder that forwards find verbatim; attribution taken from the source line that issued the call, not from my labels. Every seam resolved a bare Array, no own `records` key, object elements, on a populated page and an empty one alike — so the envelope limb was dead at all seven. Six limbs removed with each block's existing non-array disposition preserved exactly. The seventh (security-plugin.ts's sys_permission_set loader) is the DROP shape and was repaired in the opposite direction: its `catch { rows = [] }` and its `?? []` mapped three different facts (empty catalog / thrown read / unreadable result) onto one value, silently withdrawing grants that exist on the enforcement plane, and made PermissionEvaluator's own #2565 warn unreachable. The read fault now propagates and an unreadable page (or a page carrying a non-row) refuses with PermissionSetReadUnansweredError / DATABASE_ERROR — ADR-0112's existing vocabulary, no packages/spec change. Enforcement is unchanged in both directions; the loss is now sayable. Scope held to plugin-security: no plugin-auth, no service-automation, no service-analytics, no gate widened, no shared helper extracted.", "census": "7 blocks — seed-name-lookup.ts x2 (readNamePage :266, perItemIndex :320), bootstrap-declared-permissions.ts defaultLookup (:86), auto-org-admin-grant.ts tryFind (:151), claim-seed-ownership.ts idsFrom (via :345), normalize-managed-by.ts tryFind (:53), security-plugin.ts loader (:1284). Method: `grep -rn '\\.records' packages/plugins/plugin-security/src --include=*.ts` minus tests, then each hit resolved to its enclosing block by hand. Diff vs the card's 7: none. After the change the same scan returns ZERO source hits in this package.", "drive": "Per block, engine driven and shape observed: (1) buildExistingByName batched -> bare Array len 1; (2) same with the batched read refused so the per-item path is taken -> bare Array len 1; (3) upsertPackagePermissionSet with NO oracle, the ADR-0086 P2 publish-materializer path that is the only caller reaching defaultLookup -> bare Array len 0; (4) reconcileOrgAdminGrant -> bare Array len 0; (5) claimSeedOwnership with the bulk write refused for its per-row-hook budget, the only path reaching idsFrom -> bare Array len 1; (6) normalizeManagedByVocab -> bare Array len 1; (7) SecurityPlugin.start() against the real engine (its middleware really registered, since a middleware is what could rewrite the result), then its own loader -> bare Array len 1. Where a seam sits behind a trigger, ONLY the trigger is shaped; the read under test always reaches the real engine.", "drop_block_direction": "security-plugin.ts:1292. Repaired by REFUSING, and the direction is not this seam's invention — its consumer already declared it. PermissionEvaluator.resolvePermissionSets catches a throwing loader, keeps the request fail-closed, and names the failure in a warn, because 'without the warn, a transient DB error makes custom permission sets silently vanish and the resulting 403s are undiagnosable' (#2565). The loader swallowing its own read failure made that warn UNREACHABLE, so a DB outage and an empty catalog produced identical undiagnosable 403s. This also matches the maintainer's 2026-08-11 store-fault ruling for this plugin: fail-closed, and a fault PROPAGATES. Mutation M7c is the direct evidence — reinstating the swallow reddens both the propagation pin and the #2565 warn pin. A page carrying a non-row refuses too, which is the trailing filter's silent drop closed in the same direction.", "tests": "All runs on head 31f059524 (`git rev-parse --short HEAD` = 31f059524), tree clean (git status --short = 0 files). Exit codes captured by REDIRECT, never through a pipe. `pnpm --filter @objectstack/plugin-security test` exit 0 — 97 files / 1827 tests passed. `pnpm --filter @objectstack/plugin-security typecheck` exit 0, and check:test-typecheck reports '0 file(s) / 0 error(s)', so the new pins are genuinely type-checked (not excluded). ELEVEN pins in engine-find-bare-array.pin.test.ts, at least one per block, each driving its own seam. NINE mutation legs, every one confirmed RED on the expected pin: M1 readNamePage envelope-only -> the no-degradation-warn pin; M2 perItemIndex envelope-only -> the per-item `present` pin; M3 defaultLookup envelope-only -> unreadable:0/seeded:1; M4 tryFind envelope-only -> the `granted` verdict; M5 idsFrom envelope-only -> the re-owned row; M6 tryFind envelope-only -> the managed_by rewrite; M7a loader reads rows.records -> the shape pin; M7b loader restored to the original invention -> BOTH refusal pins; M7c loader's try/catch swallow restored -> the propagation pin AND the #2565 warn pin. Each leg proved the mutation reached DISK by exact substring count in both directions before running (an early leg tripped this guard: grep -F treats a multi-line pattern as one pattern PER LINE, so the wrap-shaped M7c anchor read as 'original still present' — the proof was defective, not the mutation; redone with python substring counting and disclosed here), restored via `git checkout HEAD -- ABSOLUTE_PATH` under a trap using absolute paths, and proved the file byte-identical to its HEAD blob afterwards (empty hash treated as FAILURE, never as 'nothing to compare'). NO mutation crosses a package boundary — every mutated file is plugin-security's own source, imported by the pins through a RELATIVE specifier, so no dist/ stands between mutation and subject and no rebuild leg is owed. Final tree after all nine legs: `git diff HEAD` empty.", "gates": "Family re-derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` — TWICE, because the diff grew (the gate-repair commit added content/docs/permissions/system-context.mdx and scripts/engine-double-contract.pinned.json, which raised the family from 58 to 93). The first derivation warned STALE TREE, so origin/main was merged, the closure rebuilt, and the derivation re-run clean. FINAL: 93 runnable families, 93 exit 0. Two went RED first and were repaired, which is the control that their zeros are not blind: check:engine-double-contract exit 1 (my pin's forwarding recorder IS an engine double — its update/findOne/delete now open with the producer's own assertEngineUpdateDispatch / assertEngineFindOnePredicate / assertEngineDeleteDispatch, and the ledger gained two rows: the ratchet GROWS, it is not weakened) and check-system-context-census exit 1 (16 problems, all pure LINE ROT — the loader repair adds a net +66 lines to security-plugin.ts, 77 added / 11 deleted, and every rotted anchor was off by exactly 66; repaired with the gate's own --fix, line numbers only, no prose, in content/docs which is neither references/ nor releases/). Three gates first answered exit 3 = PREREQUISITE NOT MET = NOT MEASURED (check:dual-build-cjs-loads, check:i18n, check:type-check-debt — all three need a full build closure); the closure was then built (turbo run build, 71/71 successful, exit 0) and all three re-run exit 0, so they are MEASURED, not assumed. Named controls: `node scripts/check-adr-0087-registration.mjs --self-test` exit 0 and `--base origin/main` exit 0; every other gate offering a --self-test ran it, all exit 0. SEVEN families are NOT MEASURED and cannot be locally — their argv comes from the workflow (RUNNER_TEMP, the matrix-shard expansion): the two shard-attestation invocations, the two test-completeness invocations, cross-package-test-inputs' CI invocation, and half-state-patrol's. CI measures those. One exit 124 was observed and is NOT a gate result: my own `timeout 570` wrapper cut the batch runner at 85/93; the remaining 8 were run separately, all exit 0.", "mcp_calls": "7 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, search_issues (the one targeted dedup read, control satisfied in the same call), issue_write create (#15840), add_issue_comment (this report). REST is 403 for this seat (probed first: repo-scoped GET returned HTTP 403 'GitHub access is not enabled for this session') and `gh` is absent, so the deterministic listing route was unavailable and the dedup went through ONE targeted MCP search. Bulk reads went through the zero-quota channel instead: the public web page's embedded JSON payload carried #15092's and #15094's full bodies and comments, and the PR body read-back.", "open_questions": [], "out_of_scope_findings": [ "filed as #15840 (pm:queue only, unassigned, ungraded): three of the six dead-limb seams still report an un-happened read as `[]` — auto-org-admin-grant.ts:151 tryFind, claim-seed-ownership.ts:177 idsFrom, normalize-managed-by.ts:53 tryFind. Same invention as the seventh block, smaller. NOT fixed here deliberately: this card's face is the dead limbs plus the one site it names, each of the three is a behaviour change with its own blast radius, and unlike the seventh block none has a declared consumer contract that settles what 'gap' should mean there. A uniform sweep is exactly what #15094's and #15598's own triage warned against." ] }Generated by Claude Code
os-warren commentedon Sep 5, 2026
Landed —
06c762e25PR #15838 is on
main:fix(plugin-security): retire seven dead \{ records }` find-result limbs, and stop the permission-set loader inventing an empty page (#15838). Landing authoritygit log origin/main | grep -c '(#15838)'= 1, cwd control(#15365)= 1.pm:dispatchedstripped; the card keepsbug,priority:p2,domain:services,finding`, read back to confirm.Six limbs removed — and the seventh repaired in the opposite direction
The census re-derived as 7 blocks / 9 grep lines / 6 files, zero delta from the card, anchored on the limb
.recordsrather than on the ternary — which is what catchesnormalize-managed-by.ts:55's guard-clause spelling that a ternary-anchored scan misses. All seven were driven on a realObjectQLover a realSqlDriver, with attribution taken from the source line that issued the call rather than from the block's own label. Every seam answered a bare array on a populated page and an empty one alike.⭐ The seventh was the opposite defect.
security-plugin.ts'ssys_permission_setloader mapped three different facts onto one value: an empty catalog, a read that threw (catch { rows = [] }), and a result it could not read (?? []). Only the first is an answer. That mattered because the consumer had already declared the handling:PermissionEvaluator.resolvePermissionSetscatches a throwing loader, stays fail-closed and names the failure in a warn — precisely so that "a transient DB error" does not make permission sets "silently vanish" with "undiagnosable" 403s (#2565). ⇒ The loader swallowing its own read failure made that warn unreachable. The diagnostic this repo had already built could never fire; a DB outage and an empty catalog produced identical 403s.It now propagates, and an unreadable page refuses with
PermissionSetReadUnansweredError(DATABASE_ERROR, ADR-0112's existing vocabulary — nopackages/specchange).The enforcement claim was measured in both directions, and the first version of it was wrong
The PR originally said enforcement was "unchanged in both directions". The review drove
resolvePermissionSetsthrough this PR's owndbLoaderFor(ORG)at HEAD and at the merge-base blob, on five engine conditions:[]→[], warn 0 → 1undefinedresolve[][]⇒ Identical on every result the shipped engine produces, with the throw never escaping the request path; it refuses only on two shapes that engine cannot produce. Both fail-closed, both a declared narrowing.⚠️ The unqualified sentence was corrected before landing, and this was the third of five instances this session where the one clause that turned out false was the clause no pin covered.
Two gates went red first and were repaired — which is the control
check:engine-double-contract(the pin's forwarding recorder is an engine double; the ledger gained two rows, so the ratchet grew) andcheck:system-context-census(pure line rot — every anchor off by exactly the +66 this change adds, repaired with the gate's own--fix, line numbers only). ⭐ A gate family reporting 93/93 green means nothing unless something in it can go red; these two are that proof.The merge, which was not routine
mergeable_state: dirtywhilegit merge-tree --write-treeexited 0 with no conflict markers — becausecontent/docs/permissions/system-context.mdxcarriesmerge=os-regenand GitHub's server-side merge does not run local merge drivers (#15885's corollary, met in the wild). ⭐ And the resolution was not pure line rot:--fixrefused with "the page anchors 9 distinct line(s) … but the tree holds 8 anchorable line(s). The POPULATION changed, this is not a shift", because upstreamf9a3c3216had added a row citing the same site as row 1. Pointing the two at one site was a structural decision made by hand; the generator produced every number. ⛔ No anchor was hand-written.security-plugin.ts's blob necessarily differs from the reviewed head because that same upstream commit touched it. The authored delta was proven unchanged by hashing the content lines of the diff (headers dropped so@@offsets cannot move it):258a6439350d…on both sides, with an unrelated file through the same measure returning a different hash as the control.Left open deliberately
#15840 — three seams still report an un-happened read as⚠️ Regraded p1 this session on the review's measurement:
[].reconcileOrgAdminGrant'ssys_memberread enters the revoke branch on a fault and deletes a standing grant — a persisting withdrawal, not a passive gap. The card's restraint was right for the other two; the shared reasoning did not distinguish reports from writes.Generated by Claude Code
fix(plugin-security): retire seven dead `{ records }` find-result lim…
test(plugin-security): three read-fault readings for the seams object…