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
[finding] Proposal from #8711 half-2: a declaration-side ratchet (ADR-0049 class) — enumerate declared security keys, not primitives #8894
Filed by the triage seat to preserve a proposal that would otherwise be buried when #8711 closes. Unassigned, finding — awaiting first-touch grading on its own merits. ⛔ Deliberately not ruled in #8711's 2026-08-15 adjudication: that ruling took option A (narrow the matrix header) and explicitly left this extension undecided, because a new gate is a standing obligation deserving its own decision.
The proposal (verbatim substance from #8711's dev measurement, comment 5300437444)
The authz-conformance matrix cannot ratchet primitives — there is no syntactic signature for "a predicate that decides a grant" (isRowActive looks exactly like any other .filter()), which is why widening it was measured unachievable. But the ADR-0049 defect class is "a declared security key nothing reads", and declared keys ARE mechanically enumerable:
Deactivate-style actions whose label/description text promises an enforcement effect.
A declaration-side ratchet — every enumerated declared key must be claimed by a matrix row or a named exemption — would have caught the active flag while it was still inert, in the experimental direction #8711 says was owed, without needing to discover enforcement sites at all.
The usual gate-design questions: enumeration oracle (which objects/fields count as "security keys"?), false-positive surface measured before any red set is enforced (the #6451 discipline), and maintenance cost vs. the measured recurrence (two instances in one week). Gate additions change what CI demands of every future card ⇒ if promoted, the decision is the maintainer's.
Refs: #8711 (the ruling that deferred this) · #8613 (the enforcement that exposed the gap) · #8811 (second instance) · ADR-0049.
First-touch grading (triage seat): promoted finding → needs-user-decision. A new CI gate is a standing obligation on every future card, and the #8711 ruling deliberately left this extension for its own decision — the inbox is its designed destination. Not auto-adjudicable: prisms ③ and ④ point opposite ways.
Four-prism block:
Platform long-term coherence: a declaration-side ratchet closes the ADR-0049 class at its source — declared keys are mechanically enumerable where enforcement sites are not; coherent with declared=enforced.
AI-agent error-resistance: strongly for — the platform's core failure mode is an agent declaring a key the runtime never enforces; this catches it while still inert.
Startup scope discipline: against — a new gate is permanent maintenance, and both the enumeration oracle and the false-positive surface are unmeasured.
Recommendation: premised approval of a measurement, not a red gate — one card to (1) define the enumeration oracle (security-object fields + enforcement-promising action text), (2) run it and measure the would-be red set and false positives (#6451 discipline), reporting results before anything enforces. If the measured false-positive surface is small, the gate lands in the experimental direction #8711 said was owed; if not, this closes with the measurement attached as the record. Fork clause for whoever implements: if the oracle cannot be defined mechanically, report back — ⛔ do not widen it by judgment.
Triage: this comment is from the triage seat (scheduled run) — no claim.
Maintainer ruling (2026-08-16, recorded by PM session session_01NYgmGheCzM6NrHZN436Cxf). The maintainer approved the report's recommendation for the gate-infrastructure group (「主线1 3 4:同意」; this card's recommendation was "approve the measurement only, no red gate").
Ruled: a measurement, not a gate. One card to:
Define the enumeration oracle (platform-object fields on security objects + actions whose label/description text promises an enforcement effect).
Disposition on the reading: if the measured false-positive surface is small, the gate lands in the experimental direction #8711 said was owed (that promotion comes back through the inbox with the numbers attached); if not, this closes with the measurement as the record. Fork clause, binding on the implementing dev: if the oracle cannot be defined mechanically, report back — ⛔ do not widen it by judgment.
Label flipped needs-user-decision → pm:queue (scoped to the measurement) in the same stroke.
PM dispatch: claimed for the domain:devx lane under the 2026-08-16 maintainer ruling above — a measurement, not a gate. pm:queue → pm:dispatched. Session session_011RB4waLuNbdruCo6X9oobm, branch claude/issue-8894-declared-security-key-oracle.
Dispatched with the ruling's fork clause intact and load-bearing: if the enumeration oracle cannot be defined mechanically, the dev reports that back as the result — ⛔ it is not widened by judgment to make the card land something. A card that closes with an honest "the oracle is not mechanizable, here is why" is a successful outcome of this dispatch, not a failed one.
Wiring boundary, added PM-side so the no-gate ruling cannot be eroded by convenience: ⛔ no workflow file is touched, ⛔ no check:* entry is added to the root package.json — the instrument is run as node scripts/... on demand. That last one is also plain same-file serialization: #9014 is in flight adding a check:docs-redirects entry to that exact file.
Dev claim: session session_011RB4waLuNbdruCo6X9oobm, branch claude/issue-8894-declared-security-key-oracle. Re-read all comments at claim time — no earlier claim from a different session is present (the PM dispatch comment above carries this same session ID).
Working the 2026-08-16 maintainer ruling as written: a measurement, not a gate. Fork clause held intact — if the enumeration oracle cannot be defined mechanically, that is reported back as the result rather than widened by judgment.
The measurement — run at origin/main6468df1, instrument at d05610292
Per the 2026-08-16 ruling: oracle defined, run, numbers below, nothing enforced. The fork clause fires, but not on the whole proposal — it fires on exactly one of its three parts, and saying which is the useful result.
Verdict in one line
Enumeration is mechanical. Read-determination is not, and the prose half is not. The oracle's read-detection is GREEN on sys_permission_set.active in its inert state — the very defect the class is named after — so it does not detect the class it claims to. The card's literal design (enumerate + require a human claim) does not have that failure, because it makes no automatic liveness judgement at all; it is a different and much larger proposal, measured separately below.
Part A — which objects are security objects · MECHANICAL
PLATFORM_OBJECTS_BY_PACKAGE (packages/spec/src/system/constants/platform-object-names.ts) is a curated registry whose owning packages carry conformance tests asserting it matches what they register. Its plugin-security + plugin-sharing groups are the security objects: 10 objects, no heuristic.
⚠️ The boundary is a judgement, not a derivation. Identity/auth objects (sys_api_key, sys_session, sys_oauth_*, sys_two_factor, sys_member, …) carry security-bearing keys too but live in platform-objects alongside jobs, settings, email and reports, and no marker on an object says "security" — there is no security: true, no category enum. Sensitivity measured: including them takes the surface from 10 objects / 121 field keys / 9 actions to 29 / 326 / 40. Any ratchet has to pick, and the pick is a ruling.
Part B — the declared keys · MECHANICAL
Every platform object is one ObjectSchema.create({ … }) literal, so fields keys and actions entries come off the TypeScript AST exactly. 121 declared field keys, of which 33 are framework columns (id, created_at, updated_at, created_by, updated_by, organization_id) ⇒ 88 candidate security keys, plus 9 declared actions. Stable under reformatting; nothing guessed.
Part C — is the key READ · ✗ NOT MECHANICAL
Corpus: 4359 TypeScript files, 1878 runtime (non-test, non-declaration). Three scopings, widest to narrowest — token anywhere in runtime source; in a file that also names the object; in a file that names the object in code (comments blanked, since a doc comment is prose, not a read).
Would-be red set: 6 / 8 / 9 of 88.
The calibration, which is the number that settles it
The ruling asks for detection of the class, so the oracle is checked the way an ablation checks a gate: remove every file that genuinely reads a ground-truth key, and demand the key goes red. (Necessary because this checkout's git history is shallow — its root commit already contains #8613, so the pre-enforcement tree cannot be checked out.) Each seam was hand-adjudicated file by file.
ground-truth key
live now (scoped)
ablated
verdict
sys_permission_set.active
9
5
✗ MISSES
sys_position.active
10
5
✗ MISSES
sys_user_permission_set.valid_from
3
0
✓ detects
sys_user_position.valid_until
7
0
✓ detects
2 of 4 missed — and they are the two the card was filed about. With all six real readers removed, sys_permission_set.active still scores 5, on metadata-protocol/protocol.ts, permission-set-projection.ts, spec/api/protocol.zod.ts, spec/data/object.zod.ts and examples/app-showcase/src/data/seed/index.ts. Not one reads the column. They are: the sys_metadata publication state state: 'active' (an unrelated concept sharing a word), a z.enum(['draft','active']), the deprecation message for the spec property object.active that ADR-0049 itself removed, and a seed that sets status: 'active' on demo accounts — a value, not even a key.
row-active.ts, the ONE predicate that reads the column, names sys_permission_set only in its JSDoc. Under object-scoping-with-comments its entire evidentiary weight is an English sentence. (Worth recording: the first implementation of the comment stripper used ts.createScanner, which desyncs on real files with no parser to disambiguate /; it left a JSDoc backtick intact and scored it as a code-level read. Parser-driven leaf-token walking fixes it — and getChildren() exposes JSDoc as child nodes, so a naive leaf walk reproduces the same bug through a different door. Even separating code from prose was not free.)
False-positive surface · HAND-ADJUDICATED · 4 of 9 = 44%
Every key in the narrowest red set, settled by reading the code rather than re-running the oracle:
key
verdict
why
sys_user_permission_set.last_certified_at
TP
ADR-0091 D5; zero occurrences outside declaration + generated i18n
sys_user_permission_set.certified_by
TP
no producer, no consumer
sys_user_position.last_certified_at
TP
same on the sibling table
sys_user_position.certified_by
TP
same
sys_capability.active
TP
genuinely unenforced — but already adjudicated by the maintainer 2026-08-13 (#8535); under a ratchet this is a row needing a named exemption, not a finding
sys_record_share.granted_by
FP
read at runtime — sharing-plugin.ts:873 filters { $or: [{ recipient_id: … }, { granted_by: exec.userId }] }, deciding which shares a caller may see. Invisible because the read is an unquoted object-literal key in a where clause
sys_audience_binding_suggestion.resolved_by
FP
written at suggested-audience-bindings.ts:687,721; workflow provenance, no boundary claimed
sys_audience_binding_suggestion.resolved_at
FP
written at :342,377,688,722; same shape
sys_share_link.last_used_at
FP
written at share-link-service.ts:492,621; admin-facing telemetry
Note why the one true false positive is false: an ObjectQL where clause spells a column as a bare object-literal key, which is the same syntax as a write. Teaching the oracle that shape makes every seed and every insert read as a "read". Reads and writes are not textually separable here, so the 44% is not a threshold waiting to be tuned.
Part C′ — "actions whose text promises an enforcement effect" · ✗ NOT MECHANICAL
This is the half the dispatch flagged as most likely to defeat mechanization, judged separately as instructed. Keyword oracle over each action's label/description/confirmText/successMessage, against 9 hand-labelled actions:
TP=2 FP=3 FN=0 TN=4 — precision 2/5 (40%), recall 2/2. Only 4 of 9 actions carry any prose beyond a one-word label; the rest have nothing to read.
The object's own noun."Permission set activated" matches permission because the object is called a permission set. Every security object leaks its domain vocabulary into every one of its action strings, so signal and noise share a dictionary.
⚠️ And the deeper problem, which no classifier fixes: the text and the enforcement are independent variables. sys_capability's dialog once promised enforcement that never existed, and today denies an effect it still does not have. Reading the promise tells you what was written, never what runs — so even a perfect prose classifier would only enumerate claims, and the ADR-0049 defect is a claim/behaviour mismatch.
Design 1 — the card's literal proposal, which is a different thing
The card says "every enumerated declared key must be claimed by a matrix row or a named exemption." That contains no automatic read-detection. It is the shape packages/spec/liveness/ already uses, and it needs only Parts A+B, which hold. Measured on its own terms:
Mechanizable: yes.
Would-be red set at landing: 88 of 88 (121 incl. framework columns; 240 if the identity boundary is drawn wider). No ledger for platform-object ROW columns exists, so every key is unclaimed on day one.
Catches the ground truth: 4/4, by construction — an unclaimed key is red, including both keys Part C misses.
Costs: 88 human verdicts with evidence, then per-key maintenance forever.
Cannot make those verdicts true. The sibling ledger measured its own human-verdict error rate at 10 wrong out of 13 re-verified entries (77%), and sys_capability.active is the local proof: its Deactivate prose asserted enforcement that never existed, so a ledger row written from the declaration would have said live.
The genuinely new fact this surfaced: the ADR-0049 declaration surface has two halves, and only one is governed. check:liveness covers spec schema properties (field.type, object.enable.clone). It explicitly does not cover platform-object row columns — #8613's own changeset says so: "active is a ROW property of a sys_permission_set record, not a key on PermissionSetSchema." Those 88 keys are governed by nothing at all, and both #8894 ground-truth instances live there.
Disposition
Against the ruling's stated test — "if the measured false-positive surface is small, the gate lands in the experimental direction; if not, this closes with the measurement as the record" — 44% is not small, and the calibration failure is worse than the false positives: a gate that is green on the defect it exists to catch has negative value, because it is read as coverage. ⇒ no gate proposed, no check:* entry, no workflow touched. This closes with the measurement as the record.
What I am not doing is quietly converting the miss into a recommendation for Design 1. It is mechanizable and it would have caught both instances, but it is an 88-row ledger backfill on a surface nothing governs — a standing obligation on every future card, exactly the class the triage seat and the ruling both said is the maintainer's. It is stated above with its costs so it can be decided on its own merits, not landed as a rider on a measurement.
Filed separately: #9046 — the four ADR-0091 D5 recertification columns (last_certified_at / certified_by on both grant tables) are declared and nothing writes or reads them. Genuine ADR-0049-class, found by hand while adjudicating the red set. Unassigned, finding, not repaired here.
Reproducibility. The instrument is committed on claude/issue-8894-declared-security-key-oracle (d05610292) — pushed, no PR, nothing to merge. The ruling said land nothing if the measurement does not support promotion, and it does not; the branch exists only so the numbers above are re-runnable (node scripts/measure-declared-security-keys.mjs, plus --include-identity and --self-test) rather than taken on trust. Delete it freely — flagging it as a deliberate deviation rather than leaving it unsaid.
{
"issue": 8894,
"status": "done",
"branch": "claude/issue-8894-declared-security-key-oracle",
"pr": null,
"premise_still_valid": true,
"summary": "Took the FORK: the oracle cannot be defined mechanically in the part that matters, so no gate is proposed, no check:* entry added, no workflow touched, and NO PR opened. Full numbers posted as issue comment 5306337804. The oracle splits into three parts with different verdicts, and judging them separately is the result. Enumeration is MECHANICAL both ways: security objects come off the curated PLATFORM_OBJECTS_BY_PACKAGE registry (plugin-security + plugin-sharing = 10 objects), and their declared keys come off the TypeScript AST exactly (121 field keys, 33 framework columns, 88 candidate security keys, 9 actions). Read-determination is NOT mechanical and fails the calibration the dispatch demanded: ablate every real reader of sys_permission_set.active and the key still scores 5 files of 'evidence' while inert, so the oracle would have been GREEN across its entire pre-#8613 life -- 2 of 4 ground-truth instances missed, and they are the two the card was filed about. Action-prose detection is also not mechanical: 40% precision on 9 hand-labelled actions, and it INVERTS on deactivate_capability, whose text says 'Authorization is NOT affected' -- the one action deliberately rewritten (#8535) to withdraw a false promise is the one it flags as making one. The card's LITERAL design (enumerate + require a human claim, no auto-detection) is measured separately in the comment: mechanizable, catches 4/4, but is an 88-row ledger backfill on a surface nothing governs -- stated with costs for the inbox, deliberately NOT recommended as a rider on a measurement.",
"tests": "No behaviour changed -- the only file added is an unwired script under scripts/, so there is no package to test and no changeset owed (scripts/ is not published source). Gate union re-derived AFTER the final commit with `node scripts/pm/dispatch-gates.mjs scripts/measure-declared-security-keys.mjs`: '99 check famil(ies) discovered across 25 workflow file(s)... No check family names the given paths in its own source' (0 matched, 35 undetermined, 64 silent) -- no path-matched or convention-triggered family. Ran the standing any-edit family anyway: `pnpm check:nul-bytes` -> 'check-nul-bytes --self-test: 75 assertions', 'check-nul-bytes: OK (scanned 5953 text file(s) -- 5953 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)', exit 0, at HEAD d05610292 (`git rev-parse --short HEAD` quoted from that run; clean tree, no commit follows it). Plus a manual control-byte scan of the added file (`grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`) -> no matches. Instrument self-test: `node scripts/measure-declared-security-keys.mjs --self-test` -> 'self-test OK (9 action labels, 4 calibration instances)'; it asserts the two measured blind spots deliberately (readShaped must NOT match `isRowActive(r)`; the prose oracle MUST invert on 'Authorization is NOT affected') so neither defect can be silently tuned away. CALIBRATION is the load-bearing evidence and it is an ablation, not an assertion: each ground-truth key's complete real-reader set was hand-adjudicated file by file, then removed from the corpus. sys_permission_set.active scoped 9 -> 5 (MISS), sys_position.active 10 -> 5 (MISS), sys_user_permission_set.valid_from 3 -> 0 (detects), sys_user_position.valid_until 7 -> 0 (detects). No rebuild is involved anywhere -- the instrument reads .ts source, never dist/, so no dist ablation preflight applies. Two self-corrections worth recording because both moved reported numbers: (1) the first comment-stripper used ts.createScanner, which desyncs on real files and left a JSDoc backtick scoring as a code-level read -- replaced with a parser-driven leaf-token walk that also had to skip JSDoc child NODES; (2) the first calibration seams were under-specified, mislabelling five genuine readers of valid_until as 'false signals' and overstating the failure as 3-missed. Re-adjudicated to 2-missed, which is what is reported.",
"open_questions": [
{
"question": "The measurement surfaced a governance gap that is NOT what the card asked about and is bigger than it: the ADR-0049 declaration surface has two halves and only one is governed. `check:liveness` covers spec SCHEMA properties (field.type, object.enable.clone); it explicitly does not cover platform-object ROW columns -- #8613's own changeset says so verbatim ('`active` is a ROW property of a sys_permission_set record, not a key on PermissionSetSchema'). Those 88 security keys are governed by nothing, and BOTH #8894 ground-truth instances live there. Should that surface get a ledger?",
"options": [
"A -- Nothing. Close #8894 with the measurement; the class keeps being found by hand, roughly as it has been (three instances in one week: #8613, #8811, #8535).",
"B -- Extend the EXISTING packages/spec/liveness ledger to platform-object row columns on security objects. Reuses live machinery (ratchet, evidence-path resolution, verifiedAt, evidenceScope, authorWarn), one vocabulary for authors. Cost: 88 verdicts with evidence, and the boundary question (10 security objects, or 29 with identity = 240 keys) becomes a ruling.",
"C -- A new, narrower ledger for exactly the RBAC grant catalogues (sys_permission_set, sys_position, sys_user_*), ~40 keys. Cheaper backfill, but a second ledger with its own drift surface -- the shape #8711 just finished narrowing a claim about."
],
"recommendation": "A now; B only if the maintainer wants it, and it is genuinely theirs to want. Three axes. (1) Real business need -- measured, and it cuts BOTH ways. The recurrence is real: three instances in one week, and #9046 is a fourth found within an hour of pointing an enumerator at the surface, so the class is not exhausted. But the measured consumer of a ledger is a human reading it, and this repo has a hard number for how well that works: 10 of 13 re-verified entries in the sibling ledger were WRONG (77%). sys_capability.active is the local proof -- its Deactivate prose asserted enforcement that never existed, so a row written from the declaration would have said 'live' and the ledger would have certified the exact falsehood it exists to catch. A backfill of 88 verdicts under startup focus buys a catalogue whose error rate is measured and high. (2) Long-term soundness -- B is the contract-first shape and A is not: leaving a governed and an ungoverned half of one ADR's surface is the drift PD #10 names, and it is why the enumeration halves are worth keeping alive (they are the reusable part, and the reason the instrument is preserved rather than deleted). But soundness argues for ONE ledger, so if anything is built it is B, never C -- a second ledger over the same ADR is the divergence #8711 just spent a card narrowing. (3) Making AI-written code hard to get wrong -- this axis is why I stop at A rather than recommending B. The failure mode is an agent declaring a key the runtime never enforces; a ledger catches that ONLY if the verdict is right, and the 77% number says a ledger row is a place for an agent to write a confident wrong sentence with a citation. Compare what actually worked here: an ENUMERATION with no verdict attached found #9046 in minutes, because 'here are 88 keys, four have no reader at all' is checkable, while 'this key is live because X' is not. So the axes do not align, and I am not manufacturing agreement: (2) wants B, (3) wants the enumerator without the ledger, (1) splits. That is a scope call about a standing obligation on every future card, which the triage seat and the 2026-08-16 ruling both placed with the maintainer -- so it is stated here rather than started."
}
],
"out_of_scope_findings": [
"filed as #9046: the ADR-0091 D5 recertification columns `last_certified_at` and `certified_by` are declared on BOTH sys_user_permission_set and sys_user_position, and a whole-tree search over packages/ apps/ examples/ (tests included) finds them only in the two .object.ts declarations, the generated i18n bundles, and CHANGELOG prose -- no producer and no consumer. Genuine ADR-0049 class: a 'Last Certified At' column whose description promises it records a recertification review is a compliance claim (SOX/ISO access reviews) the platform does not implement. Searched first, keyword and file-path over open issues; nothing covers it (#8802 is a defect in ADR-0091 window ENFORCEMENT, a different thing). Labelled `finding`, no `pm:queue`, unassigned, no domain label set. Not repaired here -- this card measures, repairing is a different card with a different owner."
]
}
2026-08-16 裁定写明了两条出路:假阳性面小 ⇒ 走 experimental 方向;否则 「this closes with the measurement as the record」。测量说读取判定那一半根本不可机械化,所以走的是后一条。派发词也提前把这一支写成了合格结局,免得实施者为了「让卡片落点东西」而把 oracle 撑宽——那条 fork clause 这次真的挡住了东西。
Activity
os-project-manager commentedon Aug 15, 2026
First-touch grading (triage seat): promoted
finding→needs-user-decision. A new CI gate is a standing obligation on every future card, and the #8711 ruling deliberately left this extension for its own decision — the inbox is its designed destination. Not auto-adjudicable: prisms ③ and ④ point opposite ways.Four-prism block:
sys_permission_set.activepre-sys_permission_set.activeandsys_position.activeare unenforced too — both Deactivate dialogs promise access stops, and it does not #8613; [finding] The ADR-0056 D10 authz matrix has no row for the ADR-0091 grant validity windows either — a second instance of #8711's gap #8811's ADR-0091 validity windows) — measured recurrence, not speculation.Recommendation: premised approval of a measurement, not a red gate — one card to (1) define the enumeration oracle (security-object fields + enforcement-promising action text), (2) run it and measure the would-be red set and false positives (#6451 discipline), reporting results before anything enforces. If the measured false-positive surface is small, the gate lands in the
experimentaldirection #8711 said was owed; if not, this closes with the measurement attached as the record. Fork clause for whoever implements: if the oracle cannot be defined mechanically, report back — ⛔ do not widen it by judgment.Triage: this comment is from the triage seat (scheduled run) — no claim.
Generated by Claude Code
os-zhuang commentedon Aug 16, 2026
Maintainer ruling (2026-08-16, recorded by PM session
session_01NYgmGheCzM6NrHZN436Cxf). The maintainer approved the report's recommendation for the gate-infrastructure group (「主线1 3 4:同意」; this card's recommendation was "approve the measurement only, no red gate").Ruled: a measurement, not a gate. One card to:
Disposition on the reading: if the measured false-positive surface is small, the gate lands in the
experimentaldirection #8711 said was owed (that promotion comes back through the inbox with the numbers attached); if not, this closes with the measurement as the record. Fork clause, binding on the implementing dev: if the oracle cannot be defined mechanically, report back — ⛔ do not widen it by judgment.Label flipped
needs-user-decision→pm:queue(scoped to the measurement) in the same stroke.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
PM dispatch: claimed for the
domain:devxlane under the 2026-08-16 maintainer ruling above — a measurement, not a gate.pm:queue→pm:dispatched. Sessionsession_011RB4waLuNbdruCo6X9oobm, branchclaude/issue-8894-declared-security-key-oracle.Dispatched with the ruling's fork clause intact and load-bearing: if the enumeration oracle cannot be defined mechanically, the dev reports that back as the result — ⛔ it is not widened by judgment to make the card land something. A card that closes with an honest "the oracle is not mechanizable, here is why" is a successful outcome of this dispatch, not a failed one.
Wiring boundary, added PM-side so the no-gate ruling cannot be eroded by convenience: ⛔ no workflow file is touched, ⛔ no
check:*entry is added to the rootpackage.json— the instrument is run asnode scripts/...on demand. That last one is also plain same-file serialization: #9014 is in flight adding acheck:docs-redirectsentry to that exact file.Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Dev claim: session
session_011RB4waLuNbdruCo6X9oobm, branchclaude/issue-8894-declared-security-key-oracle. Re-read all comments at claim time — no earlier claim from a different session is present (the PM dispatch comment above carries this same session ID).Working the 2026-08-16 maintainer ruling as written: a measurement, not a gate. Fork clause held intact — if the enumeration oracle cannot be defined mechanically, that is reported back as the result rather than widened by judgment.
Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
The measurement — run at
origin/main6468df1, instrument atd05610292Per the 2026-08-16 ruling: oracle defined, run, numbers below, nothing enforced. The fork clause fires, but not on the whole proposal — it fires on exactly one of its three parts, and saying which is the useful result.
Verdict in one line
Enumeration is mechanical. Read-determination is not, and the prose half is not. The oracle's read-detection is GREEN on
sys_permission_set.activein its inert state — the very defect the class is named after — so it does not detect the class it claims to. The card's literal design (enumerate + require a human claim) does not have that failure, because it makes no automatic liveness judgement at all; it is a different and much larger proposal, measured separately below.Part A — which objects are security objects · MECHANICAL
PLATFORM_OBJECTS_BY_PACKAGE(packages/spec/src/system/constants/platform-object-names.ts) is a curated registry whose owning packages carry conformance tests asserting it matches what they register. Itsplugin-security+plugin-sharinggroups are the security objects: 10 objects, no heuristic.sys_api_key,sys_session,sys_oauth_*,sys_two_factor,sys_member, …) carry security-bearing keys too but live inplatform-objectsalongside jobs, settings, email and reports, and no marker on an object says "security" — there is nosecurity: true, no category enum. Sensitivity measured: including them takes the surface from 10 objects / 121 field keys / 9 actions to 29 / 326 / 40. Any ratchet has to pick, and the pick is a ruling.Part B — the declared keys · MECHANICAL
Every platform object is one
ObjectSchema.create({ … })literal, sofieldskeys andactionsentries come off the TypeScript AST exactly. 121 declared field keys, of which 33 are framework columns (id,created_at,updated_at,created_by,updated_by,organization_id) ⇒ 88 candidate security keys, plus 9 declared actions. Stable under reformatting; nothing guessed.Part C — is the key READ · ✗ NOT MECHANICAL
Corpus: 4359 TypeScript files, 1878 runtime (non-test, non-declaration). Three scopings, widest to narrowest — token anywhere in runtime source; in a file that also names the object; in a file that names the object in code (comments blanked, since a doc comment is prose, not a read).
Would-be red set: 6 / 8 / 9 of 88.
The calibration, which is the number that settles it
The ruling asks for detection of the class, so the oracle is checked the way an ablation checks a gate: remove every file that genuinely reads a ground-truth key, and demand the key goes red. (Necessary because this checkout's git history is shallow — its root commit already contains #8613, so the pre-enforcement tree cannot be checked out.) Each seam was hand-adjudicated file by file.
sys_permission_set.activesys_position.activesys_user_permission_set.valid_fromsys_user_position.valid_until2 of 4 missed — and they are the two the card was filed about. With all six real readers removed,
sys_permission_set.activestill scores 5, onmetadata-protocol/protocol.ts,permission-set-projection.ts,spec/api/protocol.zod.ts,spec/data/object.zod.tsandexamples/app-showcase/src/data/seed/index.ts. Not one reads the column. They are: thesys_metadatapublication statestate: 'active'(an unrelated concept sharing a word), az.enum(['draft','active']), the deprecation message for the spec propertyobject.activethat ADR-0049 itself removed, and a seed that setsstatus: 'active'on demo accounts — a value, not even a key.The failure is symmetric and that is the point:
resolve-authz-context.ts— the single seam every transport resolves authorization through — contains zero occurrences of the tokenactive. It callsisRowActive(r). So doessecurity-plugin.ts's dbLoader. The indirection that made the primitive un-ratchetable in The ADR-0056 D10 authz conformance matrix has no row for the grant-catalogueactiveflag, and its completeness check cannot notice #8711 defeats the declaration side too, one layer down.row-active.ts, the ONE predicate that reads the column, namessys_permission_setonly in its JSDoc. Under object-scoping-with-comments its entire evidentiary weight is an English sentence. (Worth recording: the first implementation of the comment stripper usedts.createScanner, which desyncs on real files with no parser to disambiguate/; it left a JSDoc backtick intact and scored it as a code-level read. Parser-driven leaf-token walking fixes it — andgetChildren()exposes JSDoc as child nodes, so a naive leaf walk reproduces the same bug through a different door. Even separating code from prose was not free.)False-positive surface · HAND-ADJUDICATED · 4 of 9 = 44%
Every key in the narrowest red set, settled by reading the code rather than re-running the oracle:
sys_user_permission_set.last_certified_atsys_user_permission_set.certified_bysys_user_position.last_certified_atsys_user_position.certified_bysys_capability.activesys_record_share.granted_bysharing-plugin.ts:873filters{ $or: [{ recipient_id: … }, { granted_by: exec.userId }] }, deciding which shares a caller may see. Invisible because the read is an unquoted object-literal key in a where clausesys_audience_binding_suggestion.resolved_bysuggested-audience-bindings.ts:687,721; workflow provenance, no boundary claimedsys_audience_binding_suggestion.resolved_at:342,377,688,722; same shapesys_share_link.last_used_atshare-link-service.ts:492,621; admin-facing telemetryNote why the one true false positive is false: an ObjectQL
whereclause spells a column as a bare object-literal key, which is the same syntax as a write. Teaching the oracle that shape makes every seed and every insert read as a "read". Reads and writes are not textually separable here, so the 44% is not a threshold waiting to be tuned.Part C′ — "actions whose text promises an enforcement effect" · ✗ NOT MECHANICAL
This is the half the dispatch flagged as most likely to defeat mechanization, judged separately as instructed. Keyword oracle over each action's label/description/confirmText/successMessage, against 9 hand-labelled actions:
TP=2 FP=3 FN=0 TN=4 — precision 2/5 (40%), recall 2/2. Only 4 of 9 actions carry any prose beyond a one-word label; the rest have nothing to read.
Two distinct prose failures, neither tunable:
deactivate_capability's confirm text says "Authorization is NOT affected". Keyword matching has no negation, so the one action whose prose was deliberately rewritten (sys_capability.activeis read by nothing — the Deactivate action tells the admin grants stop resolving, and they do not #8535) to withdraw a false promise is the one flagged as making one. Exactly backwards, on the exact case the class cares about.permissionbecause the object is called a permission set. Every security object leaks its domain vocabulary into every one of its action strings, so signal and noise share a dictionary.sys_capability's dialog once promised enforcement that never existed, and today denies an effect it still does not have. Reading the promise tells you what was written, never what runs — so even a perfect prose classifier would only enumerate claims, and the ADR-0049 defect is a claim/behaviour mismatch.Design 1 — the card's literal proposal, which is a different thing
The card says "every enumerated declared key must be claimed by a matrix row or a named exemption." That contains no automatic read-detection. It is the shape
packages/spec/liveness/already uses, and it needs only Parts A+B, which hold. Measured on its own terms:sys_capability.activeis the local proof: its Deactivate prose asserted enforcement that never existed, so a ledger row written from the declaration would have saidlive.The genuinely new fact this surfaced: the ADR-0049 declaration surface has two halves, and only one is governed.
check:livenesscovers spec schema properties (field.type,object.enable.clone). It explicitly does not cover platform-object row columns — #8613's own changeset says so: "activeis a ROW property of asys_permission_setrecord, not a key onPermissionSetSchema." Those 88 keys are governed by nothing at all, and both #8894 ground-truth instances live there.Disposition
Against the ruling's stated test — "if the measured false-positive surface is small, the gate lands in the
experimentaldirection; if not, this closes with the measurement as the record" — 44% is not small, and the calibration failure is worse than the false positives: a gate that is green on the defect it exists to catch has negative value, because it is read as coverage. ⇒ no gate proposed, nocheck:*entry, no workflow touched. This closes with the measurement as the record.What I am not doing is quietly converting the miss into a recommendation for Design 1. It is mechanizable and it would have caught both instances, but it is an 88-row ledger backfill on a surface nothing governs — a standing obligation on every future card, exactly the class the triage seat and the ruling both said is the maintainer's. It is stated above with its costs so it can be decided on its own merits, not landed as a rider on a measurement.
Filed separately: #9046 — the four ADR-0091 D5 recertification columns (
last_certified_at/certified_byon both grant tables) are declared and nothing writes or reads them. Genuine ADR-0049-class, found by hand while adjudicating the red set. Unassigned,finding, not repaired here.Reproducibility. The instrument is committed on
claude/issue-8894-declared-security-key-oracle(d05610292) — pushed, no PR, nothing to merge. The ruling said land nothing if the measurement does not support promotion, and it does not; the branch exists only so the numbers above are re-runnable (node scripts/measure-declared-security-keys.mjs, plus--include-identityand--self-test) rather than taken on trust. Delete it freely — flagging it as a deliberate deviation rather than leaving it unsaid.Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
{ "issue": 8894, "status": "done", "branch": "claude/issue-8894-declared-security-key-oracle", "pr": null, "premise_still_valid": true, "summary": "Took the FORK: the oracle cannot be defined mechanically in the part that matters, so no gate is proposed, no check:* entry added, no workflow touched, and NO PR opened. Full numbers posted as issue comment 5306337804. The oracle splits into three parts with different verdicts, and judging them separately is the result. Enumeration is MECHANICAL both ways: security objects come off the curated PLATFORM_OBJECTS_BY_PACKAGE registry (plugin-security + plugin-sharing = 10 objects), and their declared keys come off the TypeScript AST exactly (121 field keys, 33 framework columns, 88 candidate security keys, 9 actions). Read-determination is NOT mechanical and fails the calibration the dispatch demanded: ablate every real reader of sys_permission_set.active and the key still scores 5 files of 'evidence' while inert, so the oracle would have been GREEN across its entire pre-#8613 life -- 2 of 4 ground-truth instances missed, and they are the two the card was filed about. Action-prose detection is also not mechanical: 40% precision on 9 hand-labelled actions, and it INVERTS on deactivate_capability, whose text says 'Authorization is NOT affected' -- the one action deliberately rewritten (#8535) to withdraw a false promise is the one it flags as making one. The card's LITERAL design (enumerate + require a human claim, no auto-detection) is measured separately in the comment: mechanizable, catches 4/4, but is an 88-row ledger backfill on a surface nothing governs -- stated with costs for the inbox, deliberately NOT recommended as a rider on a measurement.", "tests": "No behaviour changed -- the only file added is an unwired script under scripts/, so there is no package to test and no changeset owed (scripts/ is not published source). Gate union re-derived AFTER the final commit with `node scripts/pm/dispatch-gates.mjs scripts/measure-declared-security-keys.mjs`: '99 check famil(ies) discovered across 25 workflow file(s)... No check family names the given paths in its own source' (0 matched, 35 undetermined, 64 silent) -- no path-matched or convention-triggered family. Ran the standing any-edit family anyway: `pnpm check:nul-bytes` -> 'check-nul-bytes --self-test: 75 assertions', 'check-nul-bytes: OK (scanned 5953 text file(s) -- 5953 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)', exit 0, at HEAD d05610292 (`git rev-parse --short HEAD` quoted from that run; clean tree, no commit follows it). Plus a manual control-byte scan of the added file (`grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'`) -> no matches. Instrument self-test: `node scripts/measure-declared-security-keys.mjs --self-test` -> 'self-test OK (9 action labels, 4 calibration instances)'; it asserts the two measured blind spots deliberately (readShaped must NOT match `isRowActive(r)`; the prose oracle MUST invert on 'Authorization is NOT affected') so neither defect can be silently tuned away. CALIBRATION is the load-bearing evidence and it is an ablation, not an assertion: each ground-truth key's complete real-reader set was hand-adjudicated file by file, then removed from the corpus. sys_permission_set.active scoped 9 -> 5 (MISS), sys_position.active 10 -> 5 (MISS), sys_user_permission_set.valid_from 3 -> 0 (detects), sys_user_position.valid_until 7 -> 0 (detects). No rebuild is involved anywhere -- the instrument reads .ts source, never dist/, so no dist ablation preflight applies. Two self-corrections worth recording because both moved reported numbers: (1) the first comment-stripper used ts.createScanner, which desyncs on real files and left a JSDoc backtick scoring as a code-level read -- replaced with a parser-driven leaf-token walk that also had to skip JSDoc child NODES; (2) the first calibration seams were under-specified, mislabelling five genuine readers of valid_until as 'false signals' and overstating the failure as 3-missed. Re-adjudicated to 2-missed, which is what is reported.", "open_questions": [ { "question": "The measurement surfaced a governance gap that is NOT what the card asked about and is bigger than it: the ADR-0049 declaration surface has two halves and only one is governed. `check:liveness` covers spec SCHEMA properties (field.type, object.enable.clone); it explicitly does not cover platform-object ROW columns -- #8613's own changeset says so verbatim ('`active` is a ROW property of a sys_permission_set record, not a key on PermissionSetSchema'). Those 88 security keys are governed by nothing, and BOTH #8894 ground-truth instances live there. Should that surface get a ledger?", "options": [ "A -- Nothing. Close #8894 with the measurement; the class keeps being found by hand, roughly as it has been (three instances in one week: #8613, #8811, #8535).", "B -- Extend the EXISTING packages/spec/liveness ledger to platform-object row columns on security objects. Reuses live machinery (ratchet, evidence-path resolution, verifiedAt, evidenceScope, authorWarn), one vocabulary for authors. Cost: 88 verdicts with evidence, and the boundary question (10 security objects, or 29 with identity = 240 keys) becomes a ruling.", "C -- A new, narrower ledger for exactly the RBAC grant catalogues (sys_permission_set, sys_position, sys_user_*), ~40 keys. Cheaper backfill, but a second ledger with its own drift surface -- the shape #8711 just finished narrowing a claim about." ], "recommendation": "A now; B only if the maintainer wants it, and it is genuinely theirs to want. Three axes. (1) Real business need -- measured, and it cuts BOTH ways. The recurrence is real: three instances in one week, and #9046 is a fourth found within an hour of pointing an enumerator at the surface, so the class is not exhausted. But the measured consumer of a ledger is a human reading it, and this repo has a hard number for how well that works: 10 of 13 re-verified entries in the sibling ledger were WRONG (77%). sys_capability.active is the local proof -- its Deactivate prose asserted enforcement that never existed, so a row written from the declaration would have said 'live' and the ledger would have certified the exact falsehood it exists to catch. A backfill of 88 verdicts under startup focus buys a catalogue whose error rate is measured and high. (2) Long-term soundness -- B is the contract-first shape and A is not: leaving a governed and an ungoverned half of one ADR's surface is the drift PD #10 names, and it is why the enumeration halves are worth keeping alive (they are the reusable part, and the reason the instrument is preserved rather than deleted). But soundness argues for ONE ledger, so if anything is built it is B, never C -- a second ledger over the same ADR is the divergence #8711 just spent a card narrowing. (3) Making AI-written code hard to get wrong -- this axis is why I stop at A rather than recommending B. The failure mode is an agent declaring a key the runtime never enforces; a ledger catches that ONLY if the verdict is right, and the 77% number says a ledger row is a place for an agent to write a confident wrong sentence with a citation. Compare what actually worked here: an ENUMERATION with no verdict attached found #9046 in minutes, because 'here are 88 keys, four have no reader at all' is checkable, while 'this key is live because X' is not. So the axes do not align, and I am not manufacturing agreement: (2) wants B, (3) wants the enumerator without the ledger, (1) splits. That is a scope call about a standing obligation on every future card, which the triage seat and the 2026-08-16 ruling both placed with the maintainer -- so it is stated here rather than started." } ], "out_of_scope_findings": [ "filed as #9046: the ADR-0091 D5 recertification columns `last_certified_at` and `certified_by` are declared on BOTH sys_user_permission_set and sys_user_position, and a whole-tree search over packages/ apps/ examples/ (tests included) finds them only in the two .object.ts declarations, the generated i18n bundles, and CHANGELOG prose -- no producer and no consumer. Genuine ADR-0049 class: a 'Last Certified At' column whose description promises it records a recertification review is a compliance claim (SOX/ISO access reviews) the platform does not implement. Searched first, keyword and file-path over open issues; nothing covers it (#8802 is a defect in ADR-0091 window ENFORCEMENT, a different thing). Labelled `finding`, no `pm:queue`, unassigned, no domain label set. Not repaired here -- this card measures, repairing is a different card with a different owner." ] }Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
PM 复核:接受分叉支。⛔ 无 PR、无门禁、无
check:*条目、无 workflow 改动 —— 这是本卡的成功结局,不是失败。2026-08-16 裁定写明了两条出路:假阳性面小 ⇒ 走
experimental方向;否则 「this closes with the measurement as the record」。测量说读取判定那一半根本不可机械化,所以走的是后一条。派发词也提前把这一支写成了合格结局,免得实施者为了「让卡片落点东西」而把 oracle 撑宽——那条 fork clause 这次真的挡住了东西。判据是消融,不是断言 —— 这是本次唯一站得住的证据形态
裁定和派发都要求:一个连
sys_permission_set.active在失效期都认不出来的 oracle,不算在检测它声称的那一类。实测就是这么做的——把每个真值键的真实读者集合逐文件人工裁定后整体移除,再看 oracle 还打几分:sys_permission_set.activesys_position.activesys_user_permission_set.valid_fromsys_user_position.valid_until⇒ 4 漏 2,而漏掉的正是立卡所依据的那两个(#8613 的⚠️ 这个数字如果只靠「跑一遍看它报不报」是测不出来的——键今天有真实读者,不消融就永远是「检出」。
active)。也就是说这个 oracle 会在该键整个 pre-#8613 失效期里保持绿色。动作散文那半更清楚:9 条人工标注上 40% 精确率,并且在
deactivate_capability上反向——那条动作的文案恰恰写着 "Authorization is NOT affected",是 #8535 专门改写来撤回一个假承诺的那一条,而 oracle 把它标成「正在做出承诺」。唯一一条被人特意修诚实的动作,是它认错得最彻底的那条。两条防回填的自检也留在了脚本里(
readShaped不得匹配isRowActive(r);散文 oracle 必须在 "Authorization is NOT affected" 上反向),所以这两个缺陷不能被后来的人悄悄调没。三处诚实值得单独记
ts.createScanner,在真实文件上会失步,导致一个 JSDoc 反引号被当成代码级读取;②第一版消融接缝定义过窄,把valid_until的五个真实读者误判为「假信号」,把失败夸大成 3 漏。**报出来的是重新裁定后的 2 漏。**⇒ 量具自己错过两次,并且两次都是往「结论更惊人」的方向错,被抓住了。last_certified_at/certified_by) are declared on both grant tables and nothing writes or reads them #9046。卡片关闭,测量即记录
按裁定的第二条出路关闭。⛔ 分叉里那个 A/B/C 治理问题不是本卡问的问题,它比本卡大(ADR-0049 的声明面有两半,
check:liveness只管 spec schema 属性、明确不管平台对象的行列,而本卡两个真值实例都住在没人管的那一半)——单独走收件箱,不夹带在一张已裁定为「只测量」的卡里关掉。Generated by Claude Code