Repository navigation
feat(spec): a roster object declares its membership keys, ObjectSchema.membershipKeys - #25513
Conversation
…a.membershipKeys ADR-0105 D11's metadata twin, spec half: MembershipKeySchema (userField, valueField, filter, label, description; closed), the key map refusing a non-snake_case name and the kernel context names, valueField equal to userField refused, defineStack / composeStacks refusing one key on two objects, the object-extension guidance, a planned liveness row, and the rls.zod.ts examples that said the runtime stages keys nothing stages. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
…ide the RLS policy The membership-key shape moves into security/rls.zod.ts beside DefaultRowScopeSchema (the precedent's home), exports MembershipKeyName, and the generated references, surfaces, dropped-refinement ledger rows and strictness counts follow. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
…es the new collection key Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 175c276fff034f876d009946068cba6118aedad9 && git checkout 175c276fff034f876d009946068cba6118aedad9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a285ff29a18ac6ae2a9a72943f58733ce31f299a b2b98c417d23dadbccd99f7eff966c38cd3c0cc3 && git checkout -B drift-repro a285ff29a18ac6ae2a9a72943f58733ce31f299a && git merge --no-ff b2b98c417d23dadbccd99f7eff966c38cd3c0cc3
node scripts/docs-audit/affected-docs.mjs --json a285ff29a18ac6ae2a9a72943f58733ce31f299a
|
Contract reviewServed-tier: Inputs read at 2026-10-23T17:18Z: card #25234 (body and all 8 comments; the design comment 6344270438 and the ruling 6355640929, letter A, 2026-10-23T02:04Z, are the binding text), PR #25513 (body, 26-file list, net diff ① Derived judgmentsAccept-set changes the diff implies, each against the ruled closed shape (userField, valueField, optional filter in the lowerable CEL subset with no
Riders checked in text: rider 1 (Layer 0 system read, independent of audience/posture) and rider 2 (keys audience-neutral) are stated in the The objectql registry ② Semver levelChangeset
Clause-②: ③ Boundary flagsDev's declared deviations (
Dev's Which of the 13 ruled refusals land here vs PR ③: the PR body's table is right. Here: 1, 2 (shape + name rule), 3 (stack doors; the registry half is PR ③), the U = F half of 6, 8, 9, 10 at the spec door. PR ③: 4, 5, the type half of 6, 7, 11, 12, 13. PR ②: the bound's enforcement, the Layer 0 read, explain
Tier H row for Compile-time reach: CI on the head at 2026-10-23T17:18Z: 24 check-runs Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
Requested by Tesla · project thread
Part of #25234
Clause-②: yes (widening)
Metadata-protocol: added
PR 1 of 3 of ruling A on #25234 (ruling comment 6355640929, on design comment 6344270438): the spec half.
ObjectSchemagains one optional key,membershipKeys, on the ROSTER object: each entry makescurrent_user.KEYthe distinct values ofvalueFieldon this object's rows whoseuserFieldis the signed-in user and on which the optionalfilterholds. Nothing reads the key yet. Its reader is PR 2 (plugin-security'sstageRlsMembership, beside the registered code resolver), and the authoring rules are PR 3. This PR leaves the card open; theskills/objectstack-data/rules/security.mdrow (Tier H) lands separately, by the seat.No object that exists today changes behaviour: at the base
ab510f42e7the wordmembershipKeysoccurs nowhere in this tree (git grepexit 1, against adefaultRowScopecontrol that hits underpackages/plugins).The shape
security/rls.zod.ts, not a new module. The design sketched a newmembership-key.zod.ts; the order says a new module only if the precedent made one, and feat(spec): an object declares its default row scope, defaultRowScope, the RLS policy shape without object #24398 putDefaultRowScopeSchemainrls.zod.ts. Measured on the first push: a new module undersecurity/is picked up bygen:skill-refs, which rewritesskills/objectstack-data/references/_index.mdandskills/objectstack-ui/references/_index.md, and this PR's claim excludesskills/**. Inrls.zod.tsthe generated skill references do not move.superRefine, not as the record's key schema. A refused record key reaches the author only as zod's genericInvalid key in record(measured), which hides which rule the name broke. The record-level check raises onecustomissue atmembershipKeys.KEYnaming the rule: snake_case, or the reserved list, read fromRESERVED_RLS_MEMBERSHIP_KEYSincontracts/rls-membership-resolver.tsand never copied.labelanddescriptionarez.string(), as on the RLS policy and on the object itself. The design's sketch namedI18nLabelSchema, aui/render-time union nothing in the security tree imports. Its consumer is PR 2's explaindetail, which takes plain text.user,value,field,where,condition) plususerColumn,ownerField,valueColumnandusing.objectgets the "implied, declare it on the roster object" prescription;limit/maxValuesget "no authorable size: 1,000 per request, a platform constant".ObjectExtensionSchemagains amembershipKeysguidance line, so the refusal names the owning-package rule.The 13 ruled refusals: which the shape carries, which PR 3 carries
RESERVED_RLS_MEMBERSHIP_KEYSdefineStackandcomposeStackscross-reference, below. The runtime gate against the registry (other installed packages) is PR 3keys): PR 3superRefineatvalueField; the field-type half is PR 3filterthat does not lower, names a missing column, or readscurrent_user.*rls-predicate-*judge with X as target: PR 3ObjectExtensionSchemarefuses the key, now with a prescription. See the acceptance note on the registry'sextenddoorrls-predicate-*extension: PR 3What else moves, each by its own tool
defineStack/composeStacks, refusal 3.duplicateMembershipKeyFindings(instack.zod.ts) counts each key by distinct declaring-object name. The one-stack door runs beside the duplicate default-record-page check, before the object-count early return. The composition door runs in the artifact cross-reference pass over the composed objects, after the conflict strategy, so an object'override'replaced no longer counts and one'merge'combined counts once. It runs under every manifest strategy, because the key namespace is the deployment's. Both doors raise theSTACK_CROSS_REFERENCE_INVALID/ 422 envelope.composeStackswithobjectConflict: 'merge':membershipKeysis record-typed, so the composer's derived collection set picks it up with nostack.zod.tsedit. Two stacks declaring a differentmembershipKeyson one object are refused likelistViews. The literal lists incompose-stacks-merge-collection-refusal.test.tsandcompose-stacks-collection-pipe-arm.test.tsgained the key in shape order, plus one fixture row.rls.zod.tsexample fix (the design's carrier note). Use cases 4 and 5, the two@examplecomments and theusingTSDoc said the runtime stagesterritory_account_ids/ pre-resolvesteam_member_ids. Nothing in this tree stages either. They now say these are app-supplied keys. A new "Where a membership key comes from" section names the two sources: a registered resolver, and the declared key, marked not applied yet. A sixth use case shows the portal key. No published describe string changed meaning.packages/spec/liveness/object.json):membershipKeysisplannedwithauthorWarnand anauthorHint. It is drilled into 5plannedchildren, becausecheck:livenessrefuses an undeclared container inheritance (measured: it nameduserField, valueField, filter, label, description). Only the container warns. Probed withlintLivenessPropertiesFromLedgerDirfrompackages/lint: oneliveness-planned-propertyfinding on an object declaring the key, zero on the control. The note gives the flip condition:liveon the commit that lands PR 2, citingstageRlsMembership, withauthorWarn/authorHintdropped in the same edit.omitrow inmetadata-form-zod-reconciliation.test.tsrecords why: no server stages the key yet, and the ruling makes the form row an objectui card.@objectstack/metadata-core):OBJECT_REFERENCE_POSITIONSclassifiesmembershipKeysasrecordOf(ruleEntry). A key whoseuserField,valueFieldorfilternames a field the caller cannot read is dropped whole, the map goes when nothing is left, and the key NAME is never read as a field. The classification pin inobject-schema-fls-references.test.tsotherwise reds, as it did on feat(spec): an object declares its default row scope, defaultRowScope, the RLS policy shape without object #24398./meta/typescount (@objectstack/metadata-protocoltest only):objectmoves 46 to 47, with its history sentence.pnpm --filter @objectstack/spec check:generated --fixafter a build: the reference pages undercontent/docs/references/**,authorable-surface/{data,security}.json,json-schema.manifest/security.json,api-surface/security.json,export-origins/security.json,declaration-map/security.json,liveness/state-counts/object.md, and the strictness-ledger counts (one more strict site inrls.zod.ts).dropped-refinements.baseline.json(hand-edited by design): three new entries,security/MembershipKeyName,security/MembershipKeyandsecurity/MembershipKeys. The two new sitesmembershipKeysandmembershipKeys.valueTypeare added to the nine entries that embedObjectSchema, as the build printed them.measuredmoves 242/813 to 245/835. Neither rule has a projectable arm:bannedKeyswould publish the reserved list, but only as a refinement on the record root, which loses the per-key message above.MembershipKey/MembershipKeyParsed,MembershipKeys/MembershipKeysParsed,MembershipKeyName/MembershipKeyNameParsed(check:spec-parsed-aliasgreen)..changeset/25234-object-membership-keys.md:minoron@objectstack/spec,patchon@objectstack/metadata-core, with theClause-②andMetadata-protocollines.Tests
packages/spec/src/data/object-membership-keys.test.ts, 25 tests:srm_supplier_contactwithportal_supplier_ids) parses unchanged, throughObjectSchemaand through theobjectmetadata write door; the minimal two-member declaration parses; control: an object without the key parses and its output has no key.codeandpath: every name inRESERVED_RLS_MEMBERSHIP_KEYS(6, read from the contract), three non-snake_case names,valueFieldequal touserField(atvalueField),objectandlimit/maxValues(unrecognized_keyswith their prescriptions), an unknown member, a missinguserField/valueField, and the key on anObjectExtensionSchema.defineStack: one key on two objects is refused (envelopeSTACK_CROSS_REFERENCE_INVALID/ 422, one issue naming both objects and theirstack.objects[i]positions); control: two different keys are accepted.composeStacks: one key on two objects across two stacks is refused (envelope, naming both objects and the stacks that define them), also undermanifest: 'preserve'. Controls: two different keys compose, and one object declared identically by both stacks under'merge'counts once. A different declaration of one key on one object under'merge'is the collection refusal, not this one.Also
object-schema-fls-references.test.tsgained a three-leg pin: dropped onvalueField, dropped onfilter, kept otherwise; the map removed when every key goes; a key named like a denied field, and a denied field named like a member (filter), cost nothing.Ablation, one, on the behaviour this diff adds: the one-stack door's
errors.push(...collectDuplicateMembershipKeyErrors(config))replaced by a comment, throughnode scripts/ablation-replace.mjs(anchor 1 to 0, blob208905c1ae7cto98fa82b5718f). Expected direction red, observed red: 1 failed (thedefineStackrefusal), 24 passed. The composition door is a separate call and its tests stayed green, as they should. Restored: blob equals the HEAD blob208905c1ae7c, andgit diff HEADis empty. No build is involved, because the test importssrcthrough a relative path.Reverse verification of the rebuilt
.d.tsdownstream: a temporary file inpackages/platform-objects/srccallingObjectSchema.create({ …, membershipKeys: { portal_supplier_ids: { userField, valueField, filter } } })typechecks (exit 0). A stale declaration file would refuse the key throughNoExcessObjectKeys. The same call withvalueField: 5fails with TS2322 at the probe (exit 2). A third leg found that an unknown member inside an entry (limit: 5) typechecks (exit 0): the compile-time excess-key guard does not reach into record values, so the runtime parse is the refusal there, as pinned above. The probe was removed andgit statusis clean.On the final head
b2b98c41, throughos-verify-lock.sh:pnpm --filter @objectstack/spec test: 713 files passed, 21684 tests passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/metadata-core exec vitest run: 19 files, 796 tests passed.pnpm --filter @objectstack/metadata-core typecheck: exit 0.protocol.meta-types-degenerate-derivation.test.ts: 18 passed.ObjectSchema.shapeor the liveness ledger: 4@objectstack/lintfiles (694 tests) and 4@objectstack/objectqlfiles (160 tests) passed, on a closure rebuilt from this head (pnpm --filter 'PKG...' build).Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on the final headb2b98c41derives 116 families. All 116 were run on that head, one at a time, exit codes captured before any pipe: 116 exit 0.--ranreconciliation: 116 derived, 116 run, 0 NOT MEASURED, 0 unrun. The changeset gates are among them (check-changeset-no-major,check-adr-0087-registrationand their self-tests, againstorigin/main).b595703efound one real finding, fixed inb2b98c41:check:spec-parsed-alias(ADR-0122) asked forMembershipKeyParsedasz.inferandParsedtwins for the map and the name. Its other non-zero codes were exit 3 (PREREQUISITE NOT MET: a staledistafter a test edit, and thelintdist not yet built), which the final run on a rebuilt closure answers 0.check-plugin-teardown-shape --self-testneeds the pinned fixture commit621a4876in this shallow clone; fetched with--depth=1, then exit 0.pnpm check:dev-prereqs(local-only family, this diff touches the spec build): exit 0.Acceptance notes
extenddoor, for PR 2 or 3.ObjectExtensionSchemarefusesmembershipKeys, butSchemaRegistry.registerObjectin@objectstack/objectqlaccepts a full object body underownership: 'extend'. FordefaultRowScopeit refuses that by name (EXTENSION_DEFAULT_ROW_SCOPE_REFUSED_CODE,registry.ts). It has no such refusal formembershipKeys(read atregistry.ts, theownership === 'extend'check beside the field-type door). Whether the extension merge then carries the key onto the owner's object is NOT MEASURED here. Today it has no effect either way, because nothing reads the key. The ruling places cross-package reach in the shape (refusal 10), and this door is outside this PR's file surface. Carrier: Spec design: declarative membership keys for RLS (current_user.<key> from a lookup on records whose user field is the current user) #25234 PR 2, before the reader stages a key.NoExcessObjectKeysdoes not judge members inside amembershipKeysentry (leg 3 above). The runtime parse refuses them with a prescription at every door. This is a property of the record-valued slot, not of this key.rls.zod.ts's strict-site count moves 4 to 5 in the strictness ledger (the newstrictObjectentry); the map itself is az.record, not a site.Generated by Claude Code