Skip to content

feat(spec): an object declares its default row scope, defaultRowScope, the RLS policy shape without object - #24398

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-24338-object-default-row-scope
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-24338-object-default-row-scope

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #24338
Clause-②: yes

The spec half of ruling E′ on #24164 (ruling comment 6250748677, items 1 and 7). ObjectSchema gains one optional key, defaultRowScope: one row-level security policy, written like a permission set's rowLevelSecurity entry without object, which the declaring object implies. Nothing reads the key yet. Its reader is the collection-point card #24340, its writers are the declarations card #24341, and the ADR note is #24339. No object that exists today changes behaviour: at the base commit b2e78701f7 the word defaultRowScope occurs nowhere in this tree, and nowhere in objectui at the .objectui-sha pin 0707b4ae37 (git grep exit 1 on both, against a sharingModel control that hits on both).

The shape, and why it is built this way

RowLevelSecurityPolicySchema (contract unchanged) DefaultRowScopeSchema (new, @objectstack/spec/security)
members name, label, description, object, operation, using, check, positions, enabled, plus the priority and tags tombstones the same member instances, without object, priority and tags
cross-field rules at least one of using / check; no check on a select or delete policy the same function
near-miss aliases roles/role to positions, withcheck to check, condition/filter/where to using the same table
object required refused by name, with guidance: it is implied, delete it
surface name "this RLS policy" "this object's default row scope"
  • Why not .omit({ object: true }). zod 4.6.5 throws on it: .omit() cannot be used on object schemas containing refinements (measured with a probe; the policy schema carries a .superRefine). So the new schema is rebuilt from the same three parts, each shared by reference, not copied: the policy schema's own shape minus the left-out keys, the refinement (now the named function refineRowLevelSecurityPolicy), and the alias table (now the const RLS_POLICY_ALIASES).
  • Every rule that does not need object is kept. Both rules read only operation, using and check. The schema has no object-dependent default and no object alias. The one place object still appears is the inherited check describe, which defines an "applicable" policy as one whose object matches; that text is shared, not forked.
  • Why the two tombstones are left out too. priority and tags are retiredKey() tombstones and accept no value. Leaving them out changes nothing the schema accepts: both are still refused, as unknown keys. It also keeps their upgrade text, which is about the rowLevelSecurity[] path, off a surface that never had either key, and keeps two [RETIRED] rows out of a new authorable-surface entry. The input type is pinned at compile time as exactly the policy input type with object, priority and tags omitted.
  • RowLevelSecurityPolicySchema keeps its contract; only its source moved. The inline .superRefine body moved into the named function verbatim (the 45 lines were compared by a script before the swap: identical), and the alias object moved into the const. Its rows in the authorable-surface, authorable-defaults and api-surface shards and its dropped-refinement entry are untouched by this diff, and its existing tests pass. This is a deviation from the claim's "no change to RowLevelSecurityPolicySchema itself", read as "no change to its contract": the .omit route the claim names throws, and sharing the rule by name is the alternative to copying it.

The key and its describe

defaultRowScope: DefaultRowScopeSchema.optional(), placed after sharingModel. Its describe is published text that SDKs and AI authors read, so it states:

  • the shape: a permission set's rowLevelSecurity entry without object, with object refused;
  • the four semantics of ruling item 2, as the contract the security plugin's row-policy collection is to meet once it reads the key: the default is to apply when no held set carries a row policy for the object; an authored set policy is to be used as authored; a superuser-bypass principal is to be unaffected; an object without the key keeps its behaviour;
  • its writers, by what they are and as what is to happen, since no object declares the key yet: "The platform's identity objects are to declare it for their own tables." (narrowed in patch round 1 from the present-tense "Declared by the platform's identity objects for their own tables."; the changeset, the TSDoc, the liveness note and the form ledger row were narrowed the same way);
  • once, plainly: "Not applied yet: no server reads this key in this release, so declaring it filters no rows. Until the collection applies it, restrict rows with a permission set's rowLevelSecurity policy."

No tracker number appears in the describe, the guidance or the history sentence (check:doc-authoring). The TSDoc beside the key and the schema names the cards.

What else moves, each by its own tool

  • Liveness ledger (packages/spec/liveness/object.json): defaultRowScope is planned, with authorWarn and an authorHint. That is the pending-reader spelling the action.outcomeMessages row used: authoring the key raises one liveness-planned-property warning (probed: one finding on an object that declares it, zero on the control). The row is drilled into eight planned children, because check:liveness refuses an undeclared container inheritance, and deferring to permission/rowLevelSecurity fails its equality check (the key sets differ by object, priority, tags). Only the container carries authorWarn, so the warning fires once. The note gives the flip condition: go live on the commit that lands plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340, citing collectRLSPolicies, and drop authorWarn / authorHint in the same edit.
  • Generated artefacts, all by pnpm --filter @objectstack/spec check:generated --fix after a build: authorable-surface/{data,security}.json, authorable-defaults/security.json, json-schema.manifest/security.json, api-surface/security.json, export-origins/security.json, declaration-map/security.json, liveness/state-counts/object.md, the reference pages under content/docs/references/**, and the strictness-ledger counts (docs/audits/2026-07-unknown-key-strictness-ledger.counts/security.md: one more strict site in rls.zod.ts).
  • dropped-refinements.baseline.json (hand-edited by design, no generator): one new entry, security/DefaultRowScope at the root, and the new defaultRowScope site added to the nine entries that embed ObjectSchema (data/Object, the three system/* change-set shapes and five api/* responses), as the build printed them; measured moved 239/771 to 240/781, which dropped-refinements.test.ts holds equal to the entries. The refinement is the policy's own; it has no JSON Schema form, exactly as on security/RowLevelSecurityPolicy, which is already listed.
  • Studio form: the object form does not offer the key. The root-ledger row in metadata-form-zod-reconciliation.test.ts records why: no server applies the key yet, so a form row would let an author declare a row restriction that filters nothing; its writers are to be the platform objects, declared in code, and none declares it yet. Revisit when the reader lands.
  • composeStacks with objectConflict: 'merge': the composer derives the refused fixed-shape config objects from ObjectSchema.shape, so it now refuses two stacks that declare a different defaultRowScope on one object, like access or publicSharing, and the refusal text's list of config objects names defaultRowScope too. No composition changes today, because no stack declares the key. The two literal lists in compose-stacks-merge-{collection,config-object}-refusal.test.ts gained the key in shape order.
  • Field-level-security projection (@objectstack/metadata-core, patch round 2, commit 91b2f39f3): OBJECT_REFERENCE_POSITIONS in object-schema-fls-references.ts classifies defaultRowScope as one rule entry (ruleEntry, the per-entry test ruleEntries already applies). When an object document is served to a caller who cannot read some fields, the whole policy is dropped if its using, check or prose names one of them, and served unchanged otherwise: the validations posture, because deleting only using would serve a policy that reads as restricting nothing. No behaviour moves until an object declares the key.
  • Served /meta/types count (@objectstack/metadata-protocol, same commit): the object control in protocol.meta-types-degenerate-derivation.test.ts moves 45 to 46, with its history sentence.
  • Changeset .changeset/24338-object-default-row-scope.md: minor on @objectstack/spec and patch on @objectstack/metadata-core, with the line Clause-②: yes (widening).

Tests

packages/spec/src/data/object-default-row-scope.test.ts, the card's three cases plus a control:

  1. a policy without object parses, with enabled: true defaulted;
  2. a policy carrying object is refused: one issue, unrecognized_keys, at path defaultRowScope, keys ['object'], and the message carries the implied-object guidance;
  3. an unknown key is still refused (unrecognized_keys, keys ['appliesTo']);
  4. control: an object without the key parses, and its output has no defaultRowScope.

Also: the same through the object metadata write door (getMetadataTypeSchema('object')); both cross-field rules (no clause: custom at defaultRowScope; check on select: custom at defaultRowScope.check); the tombstoned keys refused as unknown keys; the members are the policy schema's own instances; and the compile-time type equality.

Ablation (one, on the behaviour this diff adds): keep object in the members, through node scripts/ablation-replace.mjs (anchor 1 to 0, blob b68921414f53 to 26896837459d). Expected direction red, observed red and wider than the object case alone: 8 of 10 failed, because a kept object is required, so every scope without it fails. The control and the compile-time case stayed green. Restored: blob equals the HEAD blob b68921414f53, and git diff HEAD is empty. No build is involved, because the test imports src through a relative path.

Reverse verification of the rebuilt .d.ts downstream: a temporary file in packages/platform-objects/src calling ObjectSchema.create({ …, defaultRowScope: { name, operation: 'select', using } }) typechecks (exit 0). A stale declaration file would refuse the key through NoExcessObjectKeys. Appending a second call with operation: 'nope' fails with TS2322 at the probe (exit 1). The probe was removed and git status is clean.

Patch round 2, on the final head e254e52d0, through os-verify-lock.sh:

  • pnpm --filter @objectstack/metadata-core test: 18 files, 770 tests passed (769 before plus the new pin).
  • protocol.meta-types-degenerate-derivation.test.ts: 18 passed.
  • pnpm --filter @objectstack/metadata-core typecheck (both tsc legs): exit 0.
  • The new pin in object-schema-fls-references.test.ts (commit e254e52d0) has three legs. A using naming a denied field drops the scope. A using naming only readable fields keeps it unchanged. A denied field named like the policy's own schema word operation keeps it too, because in a classified position a key is a schema word, not a field reference.
  • Ablation of the ruleEntry line, two runs through node scripts/ablation-replace.mjs. First run: only the key-coverage test went red, and the pin's first two legs stayed green. The unclassified fail-safe drops on a mention as well, so those legs could not tell the classification from it. That is why the third leg was added. Second run, with it: the pin and the coverage test both red (2 failed, 54 passed). Restored each time: blob equals HEAD, git diff HEAD empty.

Patch round 1 (describe and TSDoc wording only), on 62170b56e: pnpm --filter @objectstack/spec check:generated --fix rebuilt spec, regenerated content/docs/references/{data/object,api/metadata,system/migration}.mdx and re-checked all 14 artefacts green. object-default-row-scope.test.ts and metadata-form-zod-reconciliation.test.ts: 86 passed. pnpm --filter @objectstack/spec typecheck: exit 0. pnpm check:doc-authoring: exit 0 (9445 describe strings clean).

Round-0 local runs, on 8964dfbbb (after merging origin/main), through os-verify-lock.sh:

  • pnpm --filter @objectstack/spec test: 696 files passed, 21246 tests passed, 1 todo.
  • pnpm --filter @objectstack/spec typecheck: exit 0; the test layer compiles with no new errors.
  • pnpm --filter @objectstack/spec check:generated --fix: all 14 generated artefacts up to date, measured against a dist built by that run.
  • pnpm --filter @objectstack/spec exec vitest run --project repo src/security/rls-tags-retirement.test.ts: 14 passed (pre-merge head 00929288f; the merge did not touch it).
  • Downstream: pnpm --filter @objectstack/platform-objects typecheck exit 0, and 8 @objectstack/lint files that read ObjectSchema.shape or the liveness ledger: 1191 tests passed.

Who imports ObjectSchema in a way an added optional key could affect

Measured, not assumed. In this repo, git grep for keyof ServiceObject, keyof ObjectSchema and keyof z.input of the object schema finds only spec's own NoExcessObjectKeys, which an added key widens. The runtime enumerations of Object.keys(ObjectSchema.shape) are the two composeStacks pins, updated above, and the lint tests above, which stay green. Two more runtime pins enumerate the key set under spellings round 0's search missed, and Test Core (5/6, 6/6) went red on them at 8964dfbbb and 62170b56e. One is packages/metadata-core/src/object-schema-fls-references.test.ts ("classify each new ObjectSchema key in OBJECT_REFERENCE_POSITIONS", which reads declaredKeys(ObjectSchema)). The other is packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (the served object property count). Both are fixed in this PR, in commit 91b2f39f3: defaultRowScope is classified as one rule entry, and the count moves to 46. Commit e254e52d0 adds the pin beside the classification. The seat extended the claim's file surface to these files in patch round 2. In objectui at the pin, object-schema-metadata-spec-derivation.test.ts asserts the document type equals ServiceObject plus an empty client delta. That holds structurally for an added optional key, and its runtime half asserts toContain, not an exact key count.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on the final head e254e52d0 derives 112 families: the 111 it derived on 8964dfbbb and 62170b56e, plus pnpm check:durability-log-level, which metadata-core brings in (exit 0). --ran reconciliation on e254e52d0: 112 accounted, 109 run, 3 NOT MEASURED, 108 exit 0. The changeset gates are among them: check-changeset-no-major, check-empty-changeset and check-adr-0087-registration against origin/main, all exit 0.

  • 108 exit 0 on e254e52d0 (107 of 111 on 8964dfbbb and on 62170b56e).
  • pnpm --filter @objectstack/spec run check:skill-examples, pnpm check:dual-build-cjs-loads, pnpm check:lean-entry-closure: exit 3, PREREQUISITE NOT MET. They need built client-react, every package's dist, and objectql's dist, which this worktree does not have. NOT MEASURED, and left to CI.
  • pnpm check:platform-checklist: exit 1, and not from this diff. docs/qa/platform-checklist/areas/access-security.json:2823 cites packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired, a symbol that file does not declare at origin/main (nor at the base b2e78701f7). This diff touches neither file. By maintainer decision the gate is not in per-PR CI (lint.yml note), and its daily watchdog on main is the reporting channel.
  • pnpm check:dev-prereqs (local-only family): exit 0.

Acceptance notes


Generated by Claude Code

…pe without object

WIP: schema, refinement sharing and pins; generated artefacts follow.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…angeset, dropped-refinement sites

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…ope; drill its ledger row

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…ard and liveness counts

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 10 changed file(s) yielded no anchor (packages/spec/api-surface/security.json, packages/spec/authorable-defaults/security.json, packages/spec/authorable-surface/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-driven.mdx (via ObjectSchemaBase (symbol, a top-level const object))
  • content/docs/protocol/objectql/security.mdx (via RowLevelSecurityPolicySchema (symbol, a top-level const))

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

  • content/docs/releases/v13.mdx (via RowLevelSecurityPolicySchema (symbol, a top-level const))
  • content/docs/releases/v17/17-0.mdx (via RowLevelSecurityPolicySchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via RowLevelSecurityPolicySchema (symbol, a top-level const))

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
  • 10 changed file(s) yielded no anchor (packages/spec/api-surface/security.json, packages/spec/authorable-defaults/security.json, packages/spec/authorable-surface/data.json, …) — pages documenting those are invisible to this run
  • 8 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 — 143 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 d79dc4883c5698cf05680a545758bdbba0d8a7a2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d83d4a340f61f41be45eb9f77575d2f2a8839771 — the merge of head e254e52d0a9feea667b7b2d35caf7676e33e573b into base d79dc4883c5698cf05680a545758bdbba0d8a7a2, 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 d83d4a340f61f41be45eb9f77575d2f2a8839771 && git checkout d83d4a340f61f41be45eb9f77575d2f2a8839771
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d79dc4883c5698cf05680a545758bdbba0d8a7a2 e254e52d0a9feea667b7b2d35caf7676e33e573b && git checkout -B drift-repro d79dc4883c5698cf05680a545758bdbba0d8a7a2 && git merge --no-ff e254e52d0a9feea667b7b2d35caf7676e33e573b

node scripts/docs-audit/affected-docs.mjs --json d79dc4883c5698cf05680a545758bdbba0d8a7a2

⚠️ 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 d79dc4883c5698cf05680a545758bdbba0d8a7a2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24398 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T21:04Z

REWORK (patch round 1, same claim 6254274655, same dev) at head 8964dfbbb3b1a16317d259abb43c3332b3bc5059, before the contract review. One describe sentence, and the seat's answer to the dev's open question. Everything else in the seat's read holds: the shape, the shared member instances and refinement, the refusals, the liveness row, and the composeStacks refusal.


Generated by Claude Code

…ppen, not as present fact

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24398 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T21:46Z

REWORK (patch round 2, same claim 6254274655, same dev) at head 62170b56e8ebc3d4544bd0d82848b6e362a2536e, before the contract review. Round 1's wording was pushed. The dev then stopped on a file-surface breach, as the claim says, and reported it (os-dev-report 6255493376).

  • The red, read from CI, not from the report: on 8964dfbbb, Test Core (5/6) fails packages/metadata-core/src/object-schema-fls-references.test.ts ("classify each new ObjectSchema key in OBJECT_REFERENCE_POSITIONS: expected [ 'defaultRowScope' ] to deeply equal []"), and Test Core (6/6) fails packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts ("expected 46 to be 45"). Both are this PR's own key, so they are this PR's to fix.
  • File surface extended by the seat (the claiming seat follows its own breakage): packages/metadata-core/src/object-schema-fls-references.ts and its test, packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts, and the changeset. No open PR touches these files, and no in-flight claim names them. Declared to the domain:engine seat on [PM seat] domain:engine — 🟢 os-project-manager #6367.
  • What the push carries:
    • the held patch: defaultRowScope: ruleEntry in OBJECT_REFERENCE_POSITIONS (one policy, dropped whole when it mentions a denied field, the validations posture), and the count 45 → 46 with its history sentence;
    • one pin beside it in object-schema-fls-references.test.ts, the way the file pins its other rule positions: an object whose defaultRowScope.using names a denied field is served without the key, and the control (no denied field named) keeps it unchanged;
    • @objectstack/metadata-core in the changeset, at the bump and in the form the repo's changeset gate takes. No behaviour moves until an object declares the key; say so there.
  • The PR body: one combined set of pr_body_edits, written against the live body (still the round-0 text: the seat has not applied round 1's six pairs), covering rounds 1 and 2, with the downstream-importers section saying the two pins are fixed in this PR, not held.
  • Push once after the fix and the pin are green locally: pnpm --filter @objectstack/metadata-core test, the meta-types file, metadata-core typecheck, and the gate re-derivation on the final head. CI is read on the new head.
  • After the push: the seat checks the new head and runs the contract review at CONTRACT_REVIEW_TIER. needs:contract-review goes on the PR then. This is the last patch round before the review (the review-loop limit is two).

Generated by Claude Code

…one rule entry; the served object count moves to 46

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
…on from the fail-safe

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24398 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T22:20Z

ACCEPT at head e254e52d0a9feea667b7b2d35caf7676e33e573b (claim 6254274655, patch rounds 1 and 2: REWORK 6255131226 and 6255540014; reports 6255493376 and 6255833703 on #24338). A contract review at CONTRACT_REVIEW_TIER is owed: the schema face (object.zod.ts, rls.zod.ts), Clause-②: yes. It runs on this head, isolated from the seat. needs:contract-review is on the PR until its record is adopted. The PR lands after a PASS there and a fully green head.

item reading
Shape Draft, base main, assignee zhuangjianguo. Line 1 of the body is Fixes #24338, line 2 is Clause-②: yes, and there is no other closing keyword. 26 files, +549/−83 against the merge base 1eae21f971. Not governed. The changeset is minor on @objectstack/spec and patch on @objectstack/metadata-core, with Clause-②: yes (widening). The seat applied the dev's eight body pairs; read back, the bytes are identical.
The key ObjectSchema.defaultRowScope, optional: one policy of DefaultRowScopeSchema. It is the RLS policy's own member instances, without object, priority and tags. It reuses the same named refinement and the same alias table. object is refused by name, with guidance. The shape is ruling E′ item 1 as written: one policy (answer A, 6255131226).
Round 1 The describe, the TSDoc on the key and on the schema, the changeset, the liveness note and the form ledger row now say what is to happen. They do not describe a reader or writer that exists. "Not applied yet: no server reads this key in this release" is said once, plainly.
Round 2 Test Core 5/6 and 6/6 were red on 8964dfbbb and 62170b56e, read from the job logs. The cause was two domain:engine pins that enumerate ObjectSchema's keys. The seat extended the file surface to them (declared on #6367, 6255534103). OBJECT_REFERENCE_POSITIONS classifies defaultRowScope as one rule entry, the validations posture. The served object count moves 45 → 46. The new pin has three legs. Its third leg (a denied field named operation) is the one that tells the classification from the unclassified fail-safe. The dev's ablation showed the two-leg pin could not go red without it. Accepted as a deviation in the seat's favour.
Carried The A answer and the merged predicate's divergence (when org_user_ids is unresolved) → #24341, rider 6255156624. The authoring-time lint covers only permissions[].rowLevelSecurity → #24340, rider 6255162631.
For the contract review to weigh Is the describe as narrow as today's enforcement (Prime Directive #10)? Does DefaultRowScopeSchema refuse and accept exactly what the policy does, minus object? Is the metadata-core classification the right posture for a served object document? Is minor + (widening) right for the spec half and patch for metadata-core?
Local runs (dev, final head) metadata-core 770 tests passed; the meta-types file 18 passed; metadata-core typecheck exit 0. The spec rounds: 696 files / 21246 tests passed on 8964dfbbb; round 1 re-ran its touched files, typecheck, check:generated (14/14) and check:doc-authoring. Gates: 112 derived, reconciled with --ran; 108 exit 0. Three are NOT MEASURED (dist prerequisites) and left to CI. check:platform-checklist is red on main too, and not from this diff.
CI At the time of this record, on e254e52d0: 20 success, 7 skipped, 18 in progress (Test Core 1–6/6 among them). The landing waits for every check.
Landing After a contract-review PASS and a fully green head: pr_ready then automerge_enable. The merge closes #24338. That unblocks #24341 (its only Blocked-by:) and moves #24340 one step closer: it still waits on #24339.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e254e52d0a9feea667b7b2d35caf7676e33e573b
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, packages/spec/src/security/rls.zod.ts + packages/spec/src/data/object-default-row-scope.test.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/stack.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/suggestions.zod.ts, packages/spec/src/shared/retired-key.ts, packages/spec/liveness/object.json, packages/spec/liveness/state-counts/object.md, packages/spec/dropped-refinements.baseline.json, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/spec/src/compose-stacks-merge-config-object-refusal.test.ts, packages/spec/src/compose-stacks-merge-collection-refusal.test.ts, packages/spec/api-surface/security.json, packages/spec/authorable-surface/data.json, packages/spec/authorable-surface/security.json, packages/spec/authorable-defaults/security.json, packages/metadata-core/src/object-schema-fls-references.ts, packages/metadata-core/src/object-schema-fls-references.test.ts, packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts, packages/lint/src/validate-rls-predicate-enforceability.ts, packages/lint/src/validate-org-axis-red-lines.test.ts, packages/cli/src/commands/validate.ts, .changeset/24338-object-default-row-scope.md

Inputs read: card #24338 (body and all five comments: the claim 6254274655, the three dev reports, the file-surface extension 6255545754), ruling E′ on #24164 (6250748677), the PR body and file list, the net diff of the head against the merge base 1eae21f9719db38bff33477f4613216c370554f1 (26 files, +549/−83; git merge-base confirms that base), the two riders this PR carried out (6255156624 on #24341, 6255162631 on #24340), objectui at the console pin 0707b4ae37 (read-only), and the check-runs on the head. Read-only throughout: the head was fetched into a private ref, nothing was built, run or re-run; the record template was rendered with record-recognisers --template --paths over the PR's 26 paths, which returns the two hits above.

Check-runs on e254e52d0, read at 2026-10-18T22:22:00Z and again at 2026-10-18T22:27:44Z (50 runs): Lint & Repo Gates, Build Core, Build Docs, Governed Surface Queue Guard, Spec property liveness, Check Changeset (three runs, all success, the last completing between the two reads), Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, every Lint · leg, Check PR Size, Check Documentation Links, the claim and single-writer guards: success. Skipped by design: Console Pin Gate, Temporal Conformance, the dogfood shards, the packed-tarball smoke. Still in progress at the second read: Test Core (1/6 … 6/6) and Type Check · workspace. Final reading at 2026-10-18T22:49:44Z (52 runs, after polling every 30 seconds): every run completed, 42 success and 10 skipped, none failed. The five required contexts all conclude success: Lint & Repo Gates (22:19:52Z), TypeScript Type Check (22:33:00Z), Build Core (22:21:07Z), Governed Surface Queue Guard (22:16:08Z) and Test Core (22:49:31Z, all six shards green, including 5/6 and 6/6, the two that were red on the earlier heads 8964dfbbb and 62170b56e on exactly the two pins commit 91b2f39f3 fixes).

① Derived judgments

  1. ObjectSchema accept set widens by exactly one optional key, defaultRowScope. Right. Additive: an object that parsed before parses unchanged and its parsed output carries no defaultRowScope (the control case in object-default-row-scope.test.ts). The key is reached at every door an object parse reaches because there is one declaration: the object metadata write door binds ObjectSchema (kernel/metadata-type-schemas.ts:91), ObjectStackDefinitionSchema.objects is z.array(ObjectSchema) (stack.zod.ts:642), so defineStack and os validate (cli/src/commands/validate.ts:349, ObjectStackDefinitionSchema.safeParse) parse it too. The refusal of object is located: strictObject closes the shape (z.object(shape, { error }).strict()), the unrecognized_keys issue lands at path defaultRowScope with keys: ['object'], and suggestions.zod.ts answers an exact guidance entry with the prescription bullet and suppresses the rename channel, so the message carries "object is implied on a default row scope … Delete the key." Pinned on the authoring door and on the registry door.

  2. DefaultRowScopeSchema is the policy schema minus object, shared by reference. Right, and the sharing is structural rather than textual: members is destructured from RowLevelSecurityPolicySchema.shape (same member instances, pinned with toBe per key), the cross-field rules are the one named function refineRowLevelSecurityPolicy (reads operation, using, check, never object), and the near-miss table is the one const RLS_POLICY_ALIASES (targets positions, check, using, all members of the scope). Members, rules and aliases cannot drift; only surface, history and the object guidance are the scope's own. Accept set, judged against the policy: the policy requires object, the scope refuses it; every other value the policy accepts the scope accepts, with the same enabled: true default and the same two refusals (no clause: custom at defaultRowScope; check on select: custom at defaultRowScope.check). The two tombstones priority and tags are retiredKey() = z.never().optional(), which accepts only absence, so leaving them out changes nothing either schema accepts; the one difference is the refusal text (the policy answers with the upgrade prescription for rowLevelSecurity[], the scope answers as an unknown key), which is the right text for a surface that never had the key. The compile-time pin holds the input type equal to the policy input type with object, priority and tags omitted. The zod 4 .omit() refusal on a refined object schema is real (the policy schema carries .superRefine), so rebuilding from shared parts is the sound route, not a workaround.

  3. RowLevelSecurityPolicySchema's contract did not move. Right. The removed inline .superRefine body and the added function body carry the same two issues, the same messages and the same check path; the alias object moved verbatim into the const. Its rows in authorable-surface/security.json, authorable-defaults/security.json, api-surface/security.json, the security/RowLevelSecurityPolicy entry in dropped-refinements.baseline.json and its generated rls.mdx table are untouched by the diff; the diff to those shards is only the DefaultRowScope additions. The claim's "no change to RowLevelSecurityPolicySchema itself" is met as a contract statement, and the source restructure is declared in the PR body and the round-0 report as a deviation rather than slipped in.

  4. Public surface of @objectstack/spec/security gains three exports (DefaultRowScopeSchema, DefaultRowScope, DefaultRowScopeParsed), one JSON Schema (security/DefaultRowScope), eight authorable rows and one default row. Right: all additive, each shard moved by its generator, declaration-map and export-origins consistent with the source file.

  5. composeStacks with objectConflict: 'merge' now refuses two stacks declaring a different defaultRowScope. Right, and it is new-key behaviour only. The refusal set is derived from ObjectSchema.shape at first use (objectUnmergeableKeys() in stack.zod.ts, declaresConfigObject on an optional fixed-shape object); the composer is not in the diff. Nothing that composes today narrows: git grep defaultRowScope on the head outside packages/spec finds only the changeset, the generated reference pages carrying the describe, the metadata-core classification and its pin, and the meta-types count's history sentence; no stack, no object, no example declares the key, and objectui at the pin has zero hits. The two literal pins gained the key in shape order.

  6. @objectstack/metadata-core: defaultRowScope: ruleEntry in OBJECT_REFERENCE_POSITIONS. Right posture. ruleEntry is the per-entry test ruleEntries already applies to validations (mentionsDenied(entry, denied, 'include', 'classified')): the scope is one CEL rule over this object's record whose prose describes the field, and the mask is a served-document projection, never enforcement. Dropping the policy whole is correct next to validations: scrubbing only using would serve a policy with no predicate, which reads as restricting nothing and would not re-parse (the refinement requires using or check). The pin's third leg does separate the classification from the unclassified fail-safe: in mentionsDenied, the 'all' reading walks with testKeys = true and tests every object key with stringMentions(key, denied), so a denied field named operation drops the scope on the fail-safe path, while the 'classified' reading walks with testKeys = false and never tests keys, so the scope survives. The dev's ablation measured exactly that (two legs: coverage test alone red; three legs: pin and coverage test red). Without the classification the coverage test classify each new ObjectSchema key in OBJECT_REFERENCE_POSITIONS is red, which is what turned Test Core red on the two earlier heads and is fixed on this one.

  7. @objectstack/metadata-protocol: the served object property count moves 45 to 46. Right: a test-only change with its history sentence, no published source moved, no changeset owed.

  8. Liveness ledger. Right. defaultRowScope is planned with authorWarn and an authorHint, drilled into eight planned children (the gate refuses an undeclared container and would refuse a deferral to permission/rowLevelSecurity whose key set differs), only the container warns, and the note states the flip condition in the action.outcomeMessages spelling: live on the commit that lands plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340, citing collectRLSPolicies, dropping authorWarn / authorHint in the same edit. State counts 0 to 8 planned, 54 to 62 classified. Spec property liveness: success.

  9. Studio form. Right. The root-path omit row for object.defaultRowScope records a why that is exactly Prime Directive chore: version packages #10: a form row would let an author declare a row restriction that filters nothing, and the writers are to be code-declared platform objects, none of which declares it yet.

  10. dropped-refinements.baseline.json. Right. One new root entry security/DefaultRowScope plus the defaultRowScope site under each of the nine ObjectSchema embeddings (data/Object, three system/* change-set shapes, five api/* responses): 240/781 from 239/771 is one schema and ten sites, which is 1 + 9. The refinement is the policy's own and has no JSON Schema form, exactly as security/RowLevelSecurityPolicy, already listed.

  11. Prime Directive chore: version packages #10, the claim as narrow as the enforcement. Holds. The published describe states every semantic as a contract to be met ("is to apply", "is to be used as authored", "is to be unaffected"), names the writers as "are to declare", and says once, plainly: "Not applied yet: no server reads this key in this release, so declaring it filters no rows." The changeset says "What it does today: nothing." The authorHint says "Not applied yet". The form why says "none declares it yet". The TSDoc on DefaultRowScopeSchema says "Neither exists yet". The clause "the one step every read, write and explain passes through" is a claim about today's tree and is consistent with ruling item 2's naming of collectRLSPolicies in security-plugin.ts, which exists with call sites on the read, write-check and explain paths. No describe string carries a tracker number (check:doc-authoring is in the green Lint legs); the card numbers sit in TSDoc and the ledger note, where they belong. One wording note, not a defect: the TSDoc on the key opens with "Layer 1 silence on an object … then means this policy rather than no filter", the ruled meaning in the present tense; the same block immediately says the describe states the contract, not shipped behaviour, and that no server reads the key until plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340 lands, so no reader of that comment is told a reader or writer is shipped.

  12. The guidance, history and alias texts of the new surface are clean: no tracker number, the object guidance names the fix and the place a cross-object policy belongs (rowLevelSecurity in a permission set), the alias table offers no tombstone as a rename target (acceptsNothing keeps priority / tags out of the candidate list on the policy; on the scope they are simply absent).

② Semver level

minor on @objectstack/spec with Clause-②: yes (widening): right. A new optional authorable key on a published payload (ObjectSchema, served on /meta/types and inside every response that embeds an object) plus three new exports is a pure widening; nothing is removed, renamed or narrowed, so no BREAKING banner and no ADR-0087 marker is owed (check-adr-0087-registration raises a signal only for the narrowing arm). patch on @objectstack/metadata-core: right. The field-level-security projection gains a classification for a key no served document carries yet; it changes no public surface and refines served-document behaviour only on documents that declare the key. @objectstack/metadata-protocol moved a test only and owes no changeset. The PR body's Clause-②: yes carries no arm while the changeset carries (widening): both are well-formed (the arm is optional), they agree, and the level axis is satisfied by @objectstack/spec at minor, a package whose published source the diff moves. Check Changeset (which runs check-changeset-no-major --base at the merge base, check-empty-changeset and check-adr-0087-registration) concluded success on all three of its runs on this head.

③ Boundary flags

Implemented-by: claude/issue-24338-object-default-row-scope
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T22:52Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 18, 2026 22:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 18, 2026 22:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 31e7d0b Oct 18, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24338-object-default-row-scope branch October 18, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants