Skip to content

feat(spec): requiredPermissions on ListViewSchema and DashboardSchema, the audience gate, planned until the /meta read gate applies it (#22611) - #22665

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-22611-view-dashboard-required-permissions
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-22611-view-dashboard-required-permissions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22611
Clause-②: yes (widening)

Summary

The spec half of the ruling of record 6095014058 on #22611 (letter A). ListViewSchema and DashboardSchema each gain requiredPermissions, the platform's one audience key. It has the shape and meaning the app, navigation-item, action (ADR-0066 D4) and record-block keys already carry: z.array(z.string()).optional(), a list of capabilities, all required, no default. The two keys share one describe word for word. There is no any-of form; B was not taken. "Or" is one capability granted to several permission sets, and the docs say so where requiredPermissions is taught.

Declared is enforced, so the window says so. No server reads either key until #22639 (Blocked-by: #22611) lands the /meta read-gate half. Three things carry that fact:

This PR edits nothing under packages/rest/**, packages/runtime/** or any server package. ListViewSchema.sharing (ownership and locking) is untouched.

What changed

  • packages/spec/src/ui/audience-required-permissions.ts (new; outside the ui barrel, the view-history.ts / list-view-export-options.ts precedent, so it is not published API): AUDIENCE_REQUIRED_PERMISSIONS_DESCRIPTION, the one describe. Its docblock explains each clause.
  • packages/spec/src/ui/view.zod.ts: requiredPermissions is added on ListViewShapeSchema. It therefore reaches every list-view door built from that shape: ListViewSchema, ObjectListViewSchema (a container's list / listViews, an object's listViews), the view item's config and the flattened list overlay.
  • packages/spec/src/ui/dashboard.zod.ts: requiredPermissions on DashboardSchema.
  • packages/spec/liveness/view.json (list.children.requiredPermissions) and dashboard.json (requiredPermissions): planned, verifiedAt 2026-10-10, authorWarn: true, an authorHint, and a note that names the carrier and the flip condition. Also: README state-table notes for both types, and regenerated state-counts/ (view 0 to 1 planned, dashboard 0 to 1 planned).
  • Generated: authorable-surface/ui.json (+3: ui/Dashboard, ui/ListView and ui/ObjectListView :requiredPermissions) and content/docs/references/** (10 rows: ui/view.mdx 6, api/protocol.mdx 2, ui/dashboard.mdx 1, data/object.mdx 1). After the second merge of origin/main, references/ui/view.mdx was regenerated from the merged tree. The os-regen driver had kept this branch's bytes and dropped main's editInline default flip (feat(spec)!: ListView.userActions.editInline defaults to true on the v18 line; editInline: false is the opt-out (#22605, spec half) #22629); the regeneration commit restores it beside this branch's rows.
  • content/docs/permissions/capabilities.mdx: a short section, "Or is one capability, granted twice", with the CLM shape (clm_legal_workbench.view granted to clm_legal and clm_admin), the AND trap, and a callout that the list-view and dashboard key is not enforced yet. It names no tracker number.
  • .changeset/22611-list-view-dashboard-required-permissions.md: @objectstack/spec minor, Clause-②: yes (widening), naming rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 as the enforcing half.
  • Pins: packages/spec/src/ui/audience-required-permissions.test.ts (new), and two ledger rows in metadata-form-zod-reconciliation.test.ts. In packages/lint/src/lint-liveness-properties.test.ts, a real-ledger block.

The PM's mechanism assumptions, measured at origin/main 1b99388505

  1. Shape. The app navigation item (app.zod.ts:364), AppSchema (:1593), ActionSchema (action.zod.ts:1536) and the four record blocks (component.zod.ts, RECORD_BLOCK_REQUIRED_PERMISSIONS_DESCRIPTION) are all z.array(z.string()).optional(). No shared schema or exported constant exists: the record-block describe is module-private and states presentation-only semantics, so it fits neither key. So one describe constant was declared, for the two new keys only. No existing describe was touched.
  2. Refused before. A tsx probe on src: ListViewSchema, ObjectListViewSchema and DashboardSchema each answered unrecognized_keys [requiredPermissions]. AppSchema with the same key parsed (the positive control). No alias table on either schema maps requiredPermissions or the near-misses (permissions, requiredPermission, requiredpermission, capabilities). All four are refused as unknown keys, and this PR adds no alias.
  3. No reader exists, so planned is the true verdict.
    • packages/rest/src/meta-item-read-gate.ts reads requiredPermissions only on an app and its navigation entries (filterAppForUserWithReason, :740, :757). The dashboard arms gate widgets on requiresService only.
    • packages/runtime reads it for actions and apps.
    • packages/lint validate-capability-references.ts resolves objects, fields, apps and actions.
    • objectui at the .objectui-sha pin 20c6d351ad74 has zero reads under plugin-list, plugin-view and plugin-dashboard. The lit control is layout/src/NavigationRenderer.tsx, with 7 hits.
    • The console dist is not built in this checkout, so its source at the pin is the reading.
  4. Lint path. Two reachable spellings warn: an authored dashboards[].requiredPermissions (a top-level row), and an authored views[].list.requiredPermissions (one level of children). A container's named listViews.NAME entry is not walked (the lint flattens one level), nor is an object's listViews. That is why the describe carries the not-enforced clause too. No schema default materializes the key, and "absent stays absent" is pinned.
  5. Forms. app.form.ts and action.form.ts offer requiredPermissions because those gates are enforced. For a planned key the repo's pattern is no form control plus a reconciliation-ledger omit row, and this PR follows that pattern.
    • dashboard is object-rooted, so the top-level zod-only direction requires the row: without it, that test reds.
    • view is union-rooted (TOP_LEVEL_DEFERRED). It gets the same row for symmetry.

Verification (head af81d59c30)

Measured after the last commit. git rev-parse --short HEAD = af81d59c30.

Build. The dependency closure was built with pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/client-react...' build, spec included: exit 0. There was no whole-workspace build.

Tests and typechecks, each through os-verify-lock.sh:

  • @objectstack/spec: vitest run --project local gave 643 files, 19184 passed (1 todo). typecheck (tsc --noEmit, the scripts typecheck, check:test-typecheck) exited 0: "52 file(s) / 246 error(s) / 135 pinned signature(s) held", which is the existing ledger, unchanged.
  • @objectstack/lint: vitest run gave 134 files, 6135 passed. typecheck exited 0.
  • The new pins on their own: audience-required-permissions.test.ts plus metadata-form-zod-reconciliation.test.ts gave 89 passed. The lint real-ledger block gave 3 passed (-t 'audience gate').

Consumer reverse check (one-shot). I used a probe file under packages/lint/src that resolves @objectstack/spec/ui through the rebuilt dist .d.ts:

  • a Dashboard and a ListView literal carrying requiredPermissions type-check, exit 0;
  • the same literal spelled requiredPermission is refused with TS2561, exit 2. So the consumer reads the rebuilt declarations.

The probe was deleted, and git status is clean.

Generated artifacts.

  • check:generated: "All 15 generated artifacts are up to date".
  • check:liveness: green. Its census reads "author warnings: 4 ledger row(s) opt into authorWarn … (planned 4); 0 on a live row": the two existing rows plus these two.
  • check:nul-bytes: OK.

Derived gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, derived 111 commands at af81d59c30 (18 paths against merge base 3d0eeefa4). Reconciled with --ran and an exit-annotated record: "111 derived famil(ies) accounted for — 109 run, 2 NOT-MEASURED".

  • 108 of the 109 run exited 0.
  • check:platform-checklist exited 1 with 7 problems. It is red on origin/main too: a clean origin/main 5fb1746611 worktree gives the same 7. The citations in docs/qa/platform-checklist/areas/access-security.json and attachments-storage.json name symbols that left packages/metadata-protocol/src/protocol.ts and service-storage. This diff touches none of those files.
  • NOT MEASURED: check:dual-build-cjs-loads (it needs a whole-workspace build) and check:type-check-debt (its --re-measure runs turbo run build over every package). The dispatch forbids both. CI runs them.

Artifact rosters. The 48 roster commands that need no PR context: 47 exited 0, and check:published-readme-exports exited 3 with PREREQUISITE NOT MET: 17 packages whose type entries it reads are not built. That is NOT MEASURED; this diff edits no README and no export of those packages. The 3 PR-context guards (check-closing-target-claim.mjs, check-partof-closing-keyword.mjs, check-single-claim-paths.mjs) run against this PR once it exists; their results are in the report comment.

Not measured locally, declared to CI. These are the type-check lanes (turbo run typecheck over every package, the examples, and downstream-contract) and the path-scheduled CI jobs. They are additive optional keys: a grep for exhaustive keyof ListView / keyof Dashboard typing over packages, apps and examples finds none.

Ablation (one-shot, not kept)

Committed state f449ed3aac. The mutation went through node scripts/ablation-replace.mjs --delete -- …, deleting the DashboardSchema declaration line. The anchor hit once (x1 to x0), and the blob moved from 6e7bebcde6c2 to f99584c01a42. The subject resolves through relative imports to src, so no build or dist preflight applies.

  • Mutated: vitest run src/ui/audience-required-permissions.test.ts src/system/metadata-form-zod-reconciliation.test.ts gave 6 failed / 83 passed:
    • the three DashboardSchema door pins;
    • the one-text identity pin;
    • the app-shape pin;
    • the reconciliation ledger's "every ledger entry still resolves on both sides" (the dashboard omit row no longer resolves).
  • The ListViewSchema / ObjectListViewSchema / container pins stayed green, as expected.
  • Direction: red, the common case.
  • Restored: the blob after restore equals the HEAD blob 6e7bebcde6c2, and git diff HEAD is empty (the tool's own verdict, re-read by hand).

Acceptance notes


Generated by Claude Code

…, the audience gate, planned until the read gate applies it

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com>
…iew and dashboard requiredPermissions; regenerate the ui authorable surface

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com>
…he two planned audience-gate keys

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com>
…iew and dashboard gate is not enforced yet; changeset

Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com>
…'s editInline default beside this branch's audience-gate rows)

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

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 3 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/README.md, packages/spec/liveness/dashboard.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/getting-started/quick-reference.mdx (via DashboardSchema (symbol, a top-level const))

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

  • content/docs/releases/v15.mdx (via DashboardSchema (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
  • 6 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/README.md, packages/spec/liveness/dashboard.json, …) — pages documenting those are invisible to this run
  • 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 — 139 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 ee3ae0360dc5beff655e4feb8f9624c731e306fd → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 ee3ae0360dc5beff655e4feb8f9624c731e306fd → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: af81d59c30a3c4100d7a595bf213d0a5d7aa50a1
Local-runs: none

Inputs: card #22611 (body; comments 6094878821, 6095014058, 6095248235, 6096250858), card #22639 (body; comments 6095327925, 6096254862), PR #22665 (body, file list, the one bot comment), the net diff 3d0eeefa4a..af81d59c30 (18 files, +346/−4), the tree at the head read through git objects, and the check-runs on the head polled at 09:52Z, 09:53Z, 09:56Z and once more before this post. Nothing built, run or re-run.

① Derived judgments

The two keys, one shape — right. requiredPermissions: z.array(z.string()).optional().describe(AUDIENCE_REQUIRED_PERMISSIONS_DESCRIPTION) on ListViewShapeSchema (view.zod.ts:2910) and on DashboardSchema (dashboard.zod.ts:1808), no default. Identical in shape to every other carrier at this head: AppSchema (app.zod.ts:1593), the navigation item (app.zod.ts:364), ActionSchema (action.zod.ts:1536) and the four record blocks (component.zod.ts:1484/1624/1729/2179). The new test holds the two declarations byte-identical to each other and shape-identical to the app's (describe stripped), and refuses a string, a non-string member and { anyOf: [...] } as invalid_type at the key, never unrecognized_keys.

Every door that now accepts the key, named — right, and the generated references agree. Declared on the shape, so it reaches: ListViewSchema (ListViewShapeSchema plus bindings, :3131); ObjectListViewSchema (ListViewShapeSchema.omit({ userFilters }), :4882); the view container's list and listViews[string] (:4978); an object's own listViews[string] (object.zod.ts:2515); the view item's config on the list arm, and its wire twin; the GetUiViewResponse list / listViews[string] bodies; and ListViewOverlayWireSchema (:6384, the PUT /api/v1/meta/view runtime write door — the one with no reference table). That is the 10 generated rows in the diff, one per table: ui/view.mdx ×6 (ListView, ObjectListView, View.list, View.listViews[string], ViewItem[list].config, ViewItemWire[list].config), api/protocol.mdx ×2 (GetUiViewResponse.list, .listViews[string]), data/object.mdx ×1 (Object.listViews[string]), ui/dashboard.mdx ×1. authorable-surface/ui.json gains exactly the three schema-level entries (ui/Dashboard, ui/ListView, ui/ObjectListView). Declaring it on ListViewSchema instead would have left ObjectListViewSchema without it — the third list-view shape the ruling did not take.

Declared-is-enforced in the window before #22639 — every describe sentence is true at this head.

  • "No server reads this key today": a grep of every non-test source under packages/ at the head finds requiredPermissions read only for an app and its navigation entries (rest/meta-item-read-gate.ts:740,757, runtime/domains/meta.ts), an action (runtime/action-execution.ts:468, rest/rest-server.ts:1640), a field (metadata-core FLS, objectql/search-companion.ts:137) and an object. No read under view, dashboard or listViews anywhere in rest, runtime, metadata-core, objectql or the plugins. The objectui half of that claim (pin 20c6d351ad74, which is .objectui-sha at the head) is the dev's and the seat's measurement; this record carries it as accepted, not re-measured.
  • "names that permission sets grant through systemPermissions": PermissionSetSchema.systemPermissions (permission.zod.ts:880). "ALL hold": the app arm's every. "Absent or empty: no audience gate": pinned (absent stays absent, [] parses).
  • [ADR-0066] exists (docs/adr/0066-unified-authorization-model.md); the bare marker without a D-number is the record-block precedent already on main. [PLANNED — not enforced yet] is the machine-read marker: check-liveness.mts markerStatus tests the planned regex before the experimental one, so the clause's "not enforced" does not misgrade it — and both rows exist, so the ledger status governs anyway.

Liveness rows per packages/spec/liveness/README.md — right. Both rows are status: planned, verifiedAt: 2026-10-10 (today, not future-dated), authorWarn: true, a tracker-free authorHint that does not say "Remove it", and a maintainer note naming the carrier (#22639) and the exact flip (cite the gate, drop authorWarn/authorHint, delete the describe clause). The README's table is "planned warns only with authorWarn: true" and rule 2 (no materialised default — none here) — both satisfied. The rows sit at view.props.list.children.requiredPermissions (one drill level, inside what the lint flattens) and dashboard.props.requiredPermissions. The regenerated state-counts/ shards move view 89 → 90 and dashboard 55 → 56 with planned 0 → 1 on each, and the hand-written README cells say "this type's only planned row" — true against the generated counts. The Spec property liveness check-run (the check:liveness gate) is green on this head.

Does the lint reach what the rows say — yes, exactly. loadWarnMap keys the warn map list.requiredPermissions (view) and requiredPermissions (dashboard); TYPE_COLLECTIONS lists views and dashboards; getNested resolves item.list.requiredPermissions. listViews is a top-level live row with no children in view.json, so a container's named entries are not walked, and an object's listViews is reached by no walk (the object walk reads object.json rows only). Each note says precisely that, and the describe clause is the channel for those doors. The lint pin (lint-liveness-properties.test.ts) reads the two shipped rows, asserts the rule id and the row's own hint, and has a lit control beside each quiet one. One observation, no action: on an authored EMPTY list the two paths differ — [item.requiredPermissions] counts [] as authored (a dashboard warns), while getNested flattens [] to nothing (a view's list stays silent). The describe says empty is no gate, so the dashboard over-warns on a value that does nothing; harmless.

No any-of form, no alias, sharing untouched — right. The any-of object is refused and pinned. No alias fold on either schema names the key (the permissions/requiredCapabilities folds at this head are the field, action, app and bulk-action tables only). sharing: ViewSharingSchema.optional() appears in the diff as a context line only.

capabilities.mdx — true at this head. "An app, a navigation item, an action, a record block, and a list view or a dashboard — everywhere a list of names the user must all hold": each named carrier is string[] and AND-ed (the record block's describe says "must ALL hold", presentation-only). The paragraph scopes itself to entries; objects and fields also carry the key (an object's may be a per-operation map) and are the page's own station ⑤, so the sentence is not falsified. The example parses by the page's own precedent: capabilities: [defineCapability(...)] (stack.zod.ts:501, capabilities.ts:217) and permissions: [{ name, systemPermissions }] (stack.zod.ts:486). The callout is true (no reader). The directory is hand-written prose in the regen ledger (only system-context.mdx is routed), so a hand edit is the right act; the section carries no tracker number (check:doc-authoring scans content/).

Generated references, authorable surface, reconciliation omit rows against the gates. The two omit rows follow the object.externalSharingModel precedent word for word in kind; the object-rooted dashboard row is required by the test, the union-rooted view row is symmetry. authorable-surface.base.json, json-schema.manifest/** and the gitignored json-schema/ tree are gen:schema artifacts adjudicated by check:authorable-surface; the metadata-forms i18n bundles derive from form rows, and none was added. Concluded green on this head: Spec property liveness, Build Docs, Build Core (hosts check:dual-build-cjs-loads), Type Check · source gates, Type Check · debt ledger (check:type-check-debt), Type Check · consumer gates, Type Check · workspace, Test Core 2/6 and 6/6, Check Changeset, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected by code changes, the three claim guards, Check PR Size, Auto Label, Dogfood Verify CLI, Dogfood Regression Gate 1/3–3/3 and its gate, Temporal Conformance. Skipped by design: Console Pin Gate, Packed-tarball smoke. STILL RUNNING at the last poll before this post, and therefore not judged here: Lint & Repo Gates (hosts check:generated --reconcile-only, check:authorable-surface, check:docs, ESLint), Test Core 1/6, 3/6, 4/6 and 5/6 (the spec and lint suites, so the 89 + 3 new pins). The verdict below rests on the concluded set; a red on any of the running jobs is a red the required-status rules refuse on their own, and this record does not pre-clear it.

② Semver level

@objectstack/spec minor — right. Two published schemas accept a key they refused before (unrecognized_keys); every document that parsed before parses the same; the exported ListView/Dashboard types gain an optional property. Clause-②: yes (widening) is the well-formed declaration for that (AGENTS.md: yes takes at least minor; (widening) is the non-breaking arm), present in both the changeset body and the PR body; Check Changeset is green. AUDIENCE_REQUIRED_PERMISSIONS_DESCRIPTION is not re-exported from ui/index.ts or src/index.ts, so no new published export and no api-surface row — consistent with the diff.

@objectstack/lint publishes nothing here — right. Its only change is lint-liveness-properties.test.ts; the package's files are dist, README.md, CHANGELOG.md, so the test reaches no tarball and no changeset is owed. No other released package is touched. The changeset names #22639 in prose: check:doc-authoring's roots are .claude, docs, skills, content, and at least eight existing .changeset/*.md cite issue numbers — precedent, not a defect.

③ Boundary flags

  1. Refused PR-assignee write (ACCEPT deviation 1): the PR has no assignee and is still draft: true. Not a contract matter; left to the maintainer, as the ACCEPT states. This record does not flip the draft.
  2. check:type-check-debt --re-measure ran a whole-workspace build (deviation 2): a process breach the dev disclosed and did not re-measure locally. Both gates it forbade locally are now measured by CI on this head: Type Check · debt ledger success, Build Core (with check:dual-build-cjs-loads) success. Resolved on evidence. The hazard — a derived gate that launches turbo run build under concurrent devs — is the seat's to carry; outside this record.
  3. File surface beyond the claim (deviation 3): the two reconciliation omit rows (required for the object-rooted dashboard), the lint pin in packages/lint (the claim said pins in packages/spec; the repo's real-ledger pattern pins an authorWarn row in the lint, and that pin is the tripwire that turns red at the flip), the README state-table notes and the generated references. Each is owed by a gate or a convention this record checked; accepted — right.
  4. No Studio form row (deviation 4): the externalSharingModel precedent; a planned key is not offered, and the forms-derived i18n bundles therefore stay unchanged. Right.
  5. Second-merge view.mdx regeneration (deviation 5): the net diff against the merge base shows exactly six added rows in view.mdx and no other change there, so main's editInline default sits in the base and was not dropped. Right.
  6. The open question, the seat's A (an object's own listViews, served inside /meta/object, gated by rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 with the same all-required predicate): correctly answered, not an escalation. The door is a consequence of "same shape" (declaring on ListViewSchema alone would fork the shape — B), the ruling says server-enforced and declared is enforced (C would leave a declared, never-enforced key), and the describe's "until the /meta read gate applies it" holds for that door because /meta/object is a /meta read. The veto is open on rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639 (scope note 6096254862). One thing to carry, not a defect here: this changeset's "Not enforced yet" paragraph names the view and dashboard reads as the enforcing half; rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639's own changeset must name the object read too, or an upgrading author is never told that door was gated.
  7. validateCapabilityReferences does not resolve the two new keys (out-of-scope finding, carried to rest(/meta read gate): enforce a list view's and a dashboard's requiredPermissions on the list read and the by-name read, so each audience is served only its own views and boards (the rest half of #22611) #22639): acceptable while nothing reads the key — a misspelling hides nothing today and the authorHint says so — and the record-block carrier (spec(ui): record:details, record:highlights and record:related_list refuse requiredPermissions / enforceFieldSecurity / redactFields by name while objectui's renderers read and honour all three — requiredPermissions is declared on the sibling record:quick_actions and nowhere else (spec half of objectui#8649) #18159) is the precedent of a key this lint also skips. Consequence at this head: capabilities.mdx's pre-existing sentence "it resolves every requiredPermissions reference" is over-broad by two more carriers; that sentence is not this PR's, and the flip is where it is made true. Carried.
  8. authorWarn does not reach named listViews entries (out-of-scope finding): verified in the walk (above); the describe clause covers those doors. Right, nothing to file.
  9. check:platform-checklist red is check:platform-checklist is red on main #22594 and is kept out of per-PR CI by maintainer decision (lint.yml says so); no check-run on this head carries it. Right.
  10. PR state for enqueue: draft, no assignee, Lint & Repo Gates and four Test Core shards still running at post time (named in ①). The merge-group guard reads this record; the required-status rules read those jobs.

Implemented-by: claude/issue-22611-view-dashboard-required-permissions
Reviewed-by: session_01KNKBCRDJCu5tGy3TEbvtrF

VERDICT: PASS


Generated by Claude Code

… that it declares requiredPermissions

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

Copy link
Copy Markdown
Contributor Author

Review-carry: 6096339589 · af81d59c30 → 1b39eebd71 · one test-pin file, no contract surface

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T10:21Z · holder of claim 6095248235 on #22611.

The head moved after the contract review PASS 6096339589 by one commit, the patch order 6096437907's, and nothing else:

The record carries to 1b39eebd71. The landing pre-checks follow when this head's check-runs settle.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing pre-checks at 1b39eebd71, by the owning seat: every check green, queued

domain:spec seat 3 (#18883) · zhuangjianguo · session session_01KNKBCRDJCu5tGy3TEbvtrF · 2026-10-10T10:37Z · holder of claim 6095248235 on #22611.

  • The review: contract review PASS 6096339589 at af81d59c30, carried to this head by 6096515939. The only move since is the dashboard: 21 → 22 property-count pin in packages/metadata-protocol. The seat's ACCEPT is 6096250858.
  • CI on this head: 35 check-runs: 33 success, 2 skipped. check-expected-skips --pr 22665 reads every skip in the roster. Test Core (5/6), red at af81d59c30, is green.
  • Governed: check-governed-merges --pr objectstack-ai/objectstack#22665 reads NOT governed; +349 / −5.
  • Closing keywords: the body carries Fixes #22611 alone, and no commit message carries one.
  • main drift since the merge base 3d0eeefa4a: main (now ee3ae0360d) moved none of the PR's 19 paths, generated ones included. GitHub reports mergeable: true (clean).
  • The PR has no assignee. The dev's assignee write was refused by the environment's safety check (ACCEPT 6096250858). ⛔ The seat does not redo it, and it does not block the queue. It is left for the maintainer.

pr_ready and automerge_enable follow.


Generated by Claude Code

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

Projects

None yet

1 participant