Skip to content

feat(lint)!: membership key declarations, and the row policies that read them, are judged when authored - #25550

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3kk0c4
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-3kk0c4

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread
Closes #25234
Clause-②: no (narrowing)
Metadata-protocol: narrowed

PR 3 of 3 of ruling A on #25234 (ruling 6355640929, design 6344270438, landing plan 6370632236): the authoring refusals. PR 1 (#25513) declared ObjectSchema.membershipKeys; PR 2 (#25528) stages them per request, and every miss fails closed. This PR refuses, where the metadata is written, the declarations that cannot resolve and the policies that read a key in a way that matches no rows: refusals 4 to 7 and 11 to 13 of the design table, plus refusal 3 at the save door, and the runtime-gate registration for object writes.

Refusals

# What Rule id CLI Save door
3 one key declared on two objects membership-key-declared-twice yes (defineStack already refuses it in one stack) yes, reading the live objects the written stack does not carry, outside the package closure too
4 a key the registered resolver also owns membership-key-resolver-collision not judged (no resolver visible) yes
5 userField missing, or not a single-valued user reference (user, or lookup / master_detail to sys_user, not multiple) membership-key-user-field-invalid yes yes
6 valueField missing, or not a reference (lookup, master_detail, user) membership-key-value-field-invalid yes yes
7 a key's filter that does not lower, names a column the roster lacks, compares with a list-holding column, is refused by the engine's judge, or reads any current_user value the existing rls-predicate-* ids, at objects[N].membershipKeys.KEY.filter yes yes
11 a declared key tested against a column whose values can never be the key's (buyer in current_user.portal_supplier_ids) new rls-predicate-membership-key-target yes yes
12 x in current_user.KEY that no kernel key, declaration or registered resolver owns rls-predicate-unknown-user-variable unchanged, unknowable yes
13 a declared key under == rls-predicate-unenforceable (the kernel list-key type fault) yes yes

Refusals 5 and 6 judge the roster object as merged with its extensions (objectExtensions, by priority). A column two packages write at the deciding priority is left unjudged. Refusal 11 reads the key's target from the declaration's valueField reference, only when exactly one object declares the key. It accepts the column that references that target, and id on the target object itself.

The save door

  • validateMembershipKeyDeclarations is a new gating rule, CLI_AND_RUNTIME, runtimeTypes: ['object'].
  • validateRlsPredicateEnforceability now declares runtimeTypes: ['permission', 'object']. On an object write it judges the object's key filters and defaultRowScope. Through the differential gate, it also refuses an object write that drops a field or a key a stored policy reads. A stored policy that already failed is not blamed on the write.
  • The host fact. assertRuntimeAuthoringRules reads the registered resolver's keys per publish from RLS_MEMBERSHIP_RESOLVER_SERVICE, [] when none is registered (protocol.ts#rlsMembershipResolverKeys). runRuntimeAuthoringRules takes them as rlsMembershipResolverKeys and hands the rules a membershipKeyOwners context: the resolver's keys, and which live objects declare which key. The declarations are read from the un-narrowed context.objects, so the package closure cannot hide a key. The written stack is authoritative for the objects it carries. With no service registry, or a resolver whose keys is not a list of strings, no ownership is judged, as on the CLI.

os explain

New entries for the four membership-key-* ids and rls-predicate-membership-key-target. Each says how a key resolves and that every miss fails closed. rls-predicate-unenforceable names the filter path. rls-predicate-unknown-user-variable gains three paragraphs: a declared key under ==; the CLI-versus-save-door split for an unowned key in a membership position; and the caller-free filter. All verdicts and hints are one line of at most 200 characters (pinned).

Pins moved, each with its reason in the diff

  • runtime-gate.object-writes.test.ts: the object-door roster and the clean-write rulesRun gain the two rules, as ruled joins.
  • validate-rls-predicate-enforceability.engine-judge.test.ts: runtimeTypes is ['permission', 'object'].
  • validate-rls-predicate-enforceability.test.ts: the os explain text the verdict moved to now says "not reported by os validate" where it said "never reported", because the save door now reports it.
  • packages/cli/test/rls-policy-authoring-admission.test.ts (real ObjectQL host): the app-staged current_user.holidays cases register a resolver owning holidays, which is how an app stages a key. A new case pins refusal 12 end to end: with no resolver, the CLI accepts and the save door answers 422 with rls-predicate-unknown-user-variable.

Tests

  • validate-membership-key-declarations.test.ts: 17, refusals 3 to 6, both doors, merged-object judging, an unjudged priority tie, os explain, verdict length.
  • validate-rls-predicate-enforceability.membership-keys.test.ts: 24, refusals 7, 11, 12 and 13, each with a CONTROL, both doors, the object write that drops a key a stored policy reads (refused) or keeps it (published), os explain, verdict length.
  • protocol.runtime-authoring-gate.test.ts: 4 host cases. No resolver: refused, nothing written. A resolver owning the key: published. A resolver owning other keys: refused. No registry, a resolver without keys, or non-string keys: published.

Ablations, each restored byte-identically from a saved copy:

  • Refusal 12 arm off: 3 red. Refusal 11 arm off: 7 red. Refusal 13 swap off: 2 red. Refusal 7 caller-read check off: 1 red. Refusal 7 judge pass off: 6 red. Stack authority over the stored declarations off: 1 red.
  • Each of the six findings.push sites in the declarations rule disabled in turn: 1, 6, 2, 1, 4 and 3 red.
  • The host's resolver read replaced by undefined: the two refusing host cases red, the two publishing ones green.

Suites:

  • @objectstack/lint: 158 files, 7237 passed. tsc --noEmit exit 0. dts build with OS_SKIP_DTS unset: 6/6 declaration files.
  • @objectstack/metadata-protocol: 261 files, 21457 passed, 19 skipped, before the 4 host cases; those 4 pass. tsc --noEmit exit 0.
  • @objectstack/cli rls-policy-authoring-admission.test.ts: 40 passed.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 71 families on this head (1,972 changed lines, under the human-merge threshold). All 71 were run one at a time under os-verify-lock.sh, exit codes captured before any pipe, after a dts build of the @objectstack/cli closure; so were 58 more the longer listing names. All exit 0 but the following.

  • One real finding, fixed in d16c4a71: check:docs-transcript-drift found four docs transcripts printing "author-time rules (51)". The validate registry now derives 52 (the declarations rule), so the four lines read 52.
  • Four are PR-context guards that judge nothing locally (exit 1 or 2 with "NOT WIRED" / "NOT MEASURED"): check-closing-target-claim, check-partof-closing-keyword, check-single-claim-paths and check-governed-queue-guard. CI runs them with the PR. check-partof-closing-keyword was also run with this body as PR_BODY and passes.

Changeset .changeset/25234-membership-key-authoring-refusals.md: minor on @objectstack/lint, patch on @objectstack/metadata-protocol, BREAKING with FROM, TO, Unchanged and Remedy, ADR-0087 not-required (no-migration-prescription). pnpm check:adr-0087-registration passes.


🤖 Generated with Claude Code

https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339

…read them when authored (#25234)

Adds validateMembershipKeyDeclarations (user/value field, declared twice,
resolver collision) and teaches validateRlsPredicateEnforceability the
membership-key refusals: a key's filter judged as an RLS clause, a key
tested against a column of another target, `==` on a declared key, and
(at the save door) a key nobody owns. The RLS rule now also runs for
object writes, and the publish gate reads the registered resolver's keys.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/metadata-protocol, touching 73 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/data-modeling/analytics.mdx (via valueField (symbol, a field of interface DeclaredMembershipKey))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES), userField (symbol, a field of interface DeclaredMembershipKey), validateRlsPredicateEnforceability (symbol, a top-level function), valueField (symbol, a field of interface DeclaredMembershipKey))
  • content/docs/ui/dashboards.mdx (via valueField (symbol, a field of interface DeclaredMembershipKey))
  • content/docs/ui/pages.mdx (via valueField (symbol, a field of interface DeclaredMembershipKey))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), valueField (symbol, a field of interface DeclaredMembershipKey))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via judgeFilter (symbol, a field of interface JudgeOptions), judgeFilter (literal, a string literal in judgeFilter; a string literal in judgePolicy; a string literal in validateRlsPredicateEnforceability))
  • content/docs/releases/v9.mdx (via valueField (symbol, a field of interface DeclaredMembershipKey))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • 3 anchor(s) matched too much of the corpus to be a work list: current_user (literal, 35 pages), master_detail (literal, 35 pages), sys_user (literal, 43 pages)
  • 18 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fa36efd3cbac99e314f8893fe04adef3ddc11187 — the merge of head cf991db317782291a2a5be83eafd822638b766cc into base 673ebfd4dcb592c8501c2ee3ccc25e85a6582056, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fa36efd3cbac99e314f8893fe04adef3ddc11187 && git checkout fa36efd3cbac99e314f8893fe04adef3ddc11187
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 cf991db317782291a2a5be83eafd822638b766cc && git checkout -B drift-repro 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 && git merge --no-ff cf991db317782291a2a5be83eafd822638b766cc

node scripts/docs-audit/affected-docs.mjs --json 673ebfd4dcb592c8501c2ee3ccc25e85a6582056

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…et their own door rows (#25234)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (seat post #6017) · os-tesla · session session_013odhA26dvzy7pQmevdTTvy · 2026-10-23T23:53Z

Head cf991db317 · card #25234 (PR ③ of 3) · claim 6368230661 · report 6376943195 · ruling 6355640929 (letter A) · landing plan 6370632236 · contract review `` (PASS, narrowing face).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 23:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 23:54
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cf991db317782291a2a5be83eafd822638b766cc
Local-runs: none
Scope: packages/lint/src/validate-membership-key-declarations.ts, packages/lint/src/validate-membership-key-declarations.test.ts, packages/lint/src/validate-rls-predicate-enforceability.ts, packages/lint/src/validate-rls-predicate-enforceability.membership-keys.test.ts, packages/lint/src/validate-rls-predicate-enforceability.engine-judge.test.ts, packages/lint/src/validate-rls-predicate-enforceability.test.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/lint/src/runtime-gate.object-writes.test.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/runtime-authoring-gate.ts, packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts, packages/cli/test/rls-policy-authoring-admission.test.ts, .changeset/25234-membership-key-authoring-refusals.md + packages/lint/src/object-graph.ts, packages/spec/src/data/field-value.zod.ts, packages/spec/src/contracts/rls-membership-resolver.ts, packages/plugins/plugin-security/src/object-membership-keys.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/objectql/src/registry.ts, examples/app-showcase/src/security/permission-sets.ts, packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/plugins/plugin-security/src/platform-ownership-policies.ts, packages/plugins/plugin-security/src/platform-tenant-policies.ts, packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, packages/platform-objects/src/identity/*.object.ts, packages/qa/dogfood/test/fixtures/analytics-admission-fixture.ts, packages/create-objectstack/src/templates/**; prose faces read for their claims: content/docs/deployment/validating-metadata.mdx, content/docs/deployment/cli.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx

Reviewed at 2026-10-23T23:54Z. Inputs: card #25234 (body and all 12 comments: design 6344270438, ruling 6355640929, landing plan 6370632236, the three os-dev-reports), PR #25550 (body, 20 files, net diff against main at the head: +1868/−122, merge-base e5eab06 = the #25528 landing, so PR 3 sits on PR 2 as the ruling's order 1 → 2 → 3 requires), PR #25513's refusal table, and the 42 check-runs on the head: 36 success, 6 skipped (opt-in and matrix placeholders), 0 failure. Those conclusions are the gate verdicts; nothing was built or run here.

① Derived judgments

The ruling's assignment to PR 3 (6355640929 §3: refusals 5–7 and 11–13, registration for object and permission writes at the runtime gate, os explain entries) and PR #25513's table (3's registry leg and 4 also deferred to PR 3). Every one is present, each with a test:

# Shipped as Test Judgment
3 (registry leg) membership-key-declared-twice; objectsDeclaringKey reads owners.declaredBy off the un-narrowed context.objects, the written stack authoritative for the objects it carries declarations suite: inside one stack (both sites reported), at the runtime door against a live object, outside the package closure (packageScope set, other roster in app.buyers), CONTROL for the stored self right
4 membership-key-resolver-collision, judged only when membershipKeyOwners is present (save door) declarations suite: refused with ['portal_supplier_ids'], CONTROL with ['team_ids'] and with no owners; runRuntimeAuthoringRules with rlsMembershipResolverKeys right
5 membership-key-user-field-invalid: missing column (with nearest-name hint), non-user type, lookup to another object, multiple: true; judged on the merged object (extension replacing U refused; unrelated extension CONTROL; a two-package priority tie left unjudged) declarations suite, 6 cases right. The merge mirrors registry.ts (priority ?? 200, ascending, package order at a tie), read at registry.ts:6351 and :6385. The reader (object-membership-keys.ts:471) does { [userField]: userId } with no type check, so every shape refused here is one that matches no row or the wrong rows at runtime: a true narrowing of inert declarations, not of working ones
6 membership-key-value-field-invalid: missing column, non-reference type; multiple lookup and user accepted declarations suite, 2 cases + CONTROL right. F = U is the schema's (PR 1), and the rule skips it (decl.valueField !== decl.userField) so it is not double-reported
7 the key's filter judged by judgeClause with the roster as target at objects[N].membershipKeys.KEY.filter, after callerValueRead (compile with current_user: {}) membership-keys suite: not lowerable, unknown column, reads current_user.id and a declared key, list-holding comparand, judge called with the declaring object, CONTROLs right. Matches the reader at object-membership-keys.ts:448–468, which compiles the filter through rlsCompiler.compileFilter with an empty context and misses filter-refused
11 new rls-predicate-membership-key-target: $in / $nin sites whose operand is the key's one-member probe, compared column must reference T or be id on T membership-keys suite: user ref, text column, id of another object, negated form, (and N more), defaultRowScope site; CONTROLs for the referencing column, record. prefix, id on T; not judged when two objects declare the key or the declaring object is not in the stack right. referenceTargetOf returns a single string (field-value.zod.ts:215, user → sys_user constant), so there is no polymorphic-reference false positive
12 at the save door only: unresolvedMemberships minus kernel keys, declarations and resolverKeys → rls-predicate-unknown-user-variable membership-keys suite (refused, 4 CONTROLs incl. CLI), runtime door for a permission write with [] / owned / undefined, 4 host cases in protocol.runtime-authoring-gate.test.ts, and the real-engine case in cli/test/rls-policy-authoring-admission.test.ts (CLI accepts, save door 422, nothing written) right, and exactly the design's row 12 (runtime gate exact; CLI unchanged unknowable)
13 a declared key bound as a list probe, so == earns rls-predicate-unenforceable via attributeTypeFault naming the key as a membership set membership-keys suite + CONTROL (undeclared stays unknown-user-variable) right
registration validateRlsPredicateEnforceability.runtimeTypes = ['permission','object']; validateMembershipKeyDeclarations new, CLI_AND_RUNTIME, runtimeTypes: ['object']; roster pins moved in runtime-gate.object-writes.test.ts and engine-judge.test.ts the two door tests right
os explain 4 new membership-key-* entries + rls-predicate-membership-key-target; unknown-user-variable gains the save-door split and the caller-free filter paragraph; unenforceable names the filter path both suites assert the texts and the 200-char one-line pins right

Host fact. protocol.ts#rlsMembershipResolverKeys reads RLS_MEMBERSHIP_RESOLVER_SERVICE per publish: [] with a registry and no resolver, undefined with no registry or an unreadable keys (then nothing is judged, as on the CLI). It reaches the rules through evaluateRuntimeAuthoringGate → runRuntimeAuthoringRules → membershipKeyOwnersOf(args.context?.objects, keys). The only callers of the gate are saveMetaItem (publish mode), publishMetaItem and restoredBodyWriter; no boot-time stack load passes through it. Right.

Beyond the 13, implied by the ruled registration and declared in the changeset's BREAKING section: (a) an object write's defaultRowScope is now judged at the object door (the rule already judged it on the CLI since #24452); (b) through the differential gate, an object write that drops a field or key a stored policy reads is refused, while a stored policy already failing is not blamed on the write (test: drop the key → refused; keep it → published). Both are the unavoidable consequence of running the RLS rule for object writes, which the design table and the ruling asked for; both fail closed in the ADR-0078 direction. Judged right; named here so the seat sees they are not in the 13-row table.

Narrowing census (what a new refusal could now reject, and whether it still passes):

  • In-repo RLS producers, read on main: examples/app-showcase/src/security/permission-sets.ts (current_user.email ×3), plugin-security/objects/default-permission-sets.ts (id, email, org_user_ids, organization_id), platform-ownership-policies.ts (id), platform-tenant-policies.ts (organization_id), plugin-audit/sys-audit-log.object.ts (organization_id), platform-objects/src/identity/*.object.ts defaultRowScope / policies (id, org_user_ids, organization_id, positions; the 20 isPlatformAdmin hits are visible CEL comments, not predicates), packages/qa/dogfood fixture (none). All kernel keys. Refusal 12 rejects none; 11/13 need a declared key and no producer declares membershipKeys yet (unreleased since PR 1). examples/: email, id, positions only (current_user.name is a CHANGELOG line). apps/: no current_user at all. packages/create-objectstack/src/templates: no current_user.
  • HotCRM at 4166f21 (this container's clone): RLS producers sales-manager.profile.ts, marketing-user.profile.ts use current_user.id only; can / positions / organizationId hits are app-visibility CEL. Passes. hotsrm: README-only per the design; the design's portal_supplier_ids example is the intended first consumer and is accepted by refusal 11's CONTROL.
  • objectui at 5fef032: team_ids / email_verified / role / nope appear in test files only. Not a producer here.
  • Every in-repo user of current_user. with a non-kernel key, non-test: none in a producer. The remaining hits are docs and comments: validate-rls-predicate-enforceability.ts (holidays, nope, KEY), rule-explanations.ts, rls-membership-resolver.ts, object.zod.ts and rls.zod.ts docblocks (portal_supplier_ids), packages/lint/scripts/check-doc-formula-expressions.mjs:1448 (team_member_ids as an @example string in a doc-gate corpus), CHANGELOGs. The test corpus that used a staged non-kernel key against a real engine (cli/test/rls-policy-authoring-admission.test.ts, current_user.holidays) now registers a resolver owning it, which is the ruled way an app stages a key.
  • Ordering (a third-party app relying on a resolver registered later at boot): the save door reads the resolver per publish, so any publish after the resolver's plugin registered it is accepted; a publish issued before that registration, on a host that has a services registry, is refused with rls-predicate-unknown-user-variable where it used to answer 200. But SecurityPlugin.start probes the resolver once (security-plugin.ts:2100–2104, "Absent is the norm — probe once"), so a resolver registered after the security plugin started is already never consulted at request time and the same policy already dropped on every request. The save door's refusal is therefore no earlier than the runtime's existing fail-closed, and it is the design's stated intent for row 12. No in-repo plugin registers a resolver (0 production registrants, unchanged since the design's premise 1). Judged right; the one honest residue is that a self-hosted app with a late-registering resolver gets a 422 at publish instead of a silent empty set, which is the ADR-0078 direction and is spelled out in the changeset's FROM / TO / Remedy.

Docs edit in cf991db (validating-metadata.mdx door table). The shared "declared on another collection" row kept sharing-rule conditions and —; the RLS predicates row is split out with ✓ᵖᵉᵒ (legend at line 498–505: ᵖᵉ permission, ᵒ object, in the rule's declared order) matching runtimeTypes: ['permission', 'object']; the declarations row carries ✓ᵒ matching runtimeTypes: ['object'], and says "save door only" for refusal 4. The prose paragraph records the two crossings (#20158, #25234). The three transcript counts move 51 → 52, consistent with the registry gaining one rule (check:docs-transcript-drift success on the head). Right.

② Semver level

.changeset/25234-membership-key-authoring-refusals.md: @objectstack/lint: minor, @objectstack/metadata-protocol: patch. Title feat(lint)!:; line-initial Clause-②: no (narrowing: …) with the arm the first token inside the parenthesis, and Metadata-protocol: narrowed (a value refused, a requirement tightened). Under AGENTS.md's level rule a narrowed kind takes at least minor on a package the diff moves: lint, which carries the rules, is minor; metadata-protocol's change is host-fact plumbing and patch is the same split PR 1 used (spec: minor, metadata-core: patch). The breaking carriers are all present: **BREAKING** banner, FROM, TO, Unchanged, Remedy, and exactly one adr-0087: not-required (no-migration-prescription) marker with its why, the same disposition #25205's narrowing changeset carried. No gate script (check-adr-0087-registration, check-changeset-no-major, check-changeset-fixed, clause2-line) requires a producer-census paragraph; #25205's "Measured before crossing" is not a repo convention (12 of the no (narrowing changesets on main carry no such line). No required marker line is missing. Check Changeset and Lint · repo & tooling gates are green on the head. The PR body carries the same two lines. Closes #25234 is the landing plan's assignment for PR 3; The card this PR closes must claim this branch, Part-of PR must not also close its card and No other open PR may claim the same issue are green.

③ Boundary flags

  • Dev report 6376943195 open_questions: []; out_of_scope_findings: []. Nothing to escalate.
  • Deviation 1 (refusal 3 also at the save door, not only the runtime gate against the registry): the save door is the runtime gate reading the registry's live objects; answered, right.
  • Deviation 2 (object-write registration judges defaultRowScope and refuses an object write that breaks a stored policy): answered in ① as the consequence of the ruled registration, declared in BREAKING; right.
  • Deviation 3 (holidays test cases register a resolver): the ruled way to stage a key; right.
  • Deviation 4 (door-table rows): verified against runtimeTypes in ①; right.
  • PR body flag "a column two packages write at the deciding priority is left unjudged": matches the registry, where the winner is registration order a stack does not state; accepted as unjudged rather than guessed.
  • Rider 3 of the ruling (no publish-time refusal for roster-object record writes): nothing here refuses a data write; the object-door refusals are of declarations. Consistent.
  • Carried forward, not blocking: the Tier H skills/objectstack-data/rules/security.md row (text in report 6374979058) lands as its own governed PR after this one, as the landing plan and the report's next say.

Implemented-by: claude/project-thread-3kk0c4
Reviewed-by: session_013odhA26dvzy7pQmevdTTvy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat correction · 2026-10-23T23:55Z: the ACCEPT above names its contract review with an empty id. The record is comment 6377122276 (PASS, narrowing face, head cf991db317), posted after the ACCEPT; the ACCEPT stands on it.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Spec design: declarative membership keys for RLS (current_user.<key> from a lookup on records whose user field is the current user)

2 participants