From 1aed237dae1656e0c202ff4906558a480deb887e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 11:45:59 +0000 Subject: [PATCH 1/4] refactor(app-shell): drop the dead sys_position display entry from approverIdentity `position` has been registry-bound in the installed spec since 18 (objectstack#22710), so no `data` approver binding names `sys_position` and its `DISPLAY_FIELDS_BY_OBJECT` row is read by nothing. The `approverValueIsName` docblock still said `position` answers through the data arm; it now names the registry arm. `useApproverDirectory`'s header listed `sys_position` among the directory objects it reads and used it as the label-leg example; both re-worded to the registry read. Part of objectui#7611 (release record 6357827416, item 2). Claude-Session: https://claude.ai/code/session_01WvJkpohCWbizJavd86HTZz Co-authored-by: Claude --- packages/app-shell/src/hooks/useApproverDirectory.ts | 12 ++++++------ packages/app-shell/src/utils/approverIdentity.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/app-shell/src/hooks/useApproverDirectory.ts b/packages/app-shell/src/hooks/useApproverDirectory.ts index 28aa69cf5a..92859a6e94 100644 --- a/packages/app-shell/src/hooks/useApproverDirectory.ts +++ b/packages/app-shell/src/hooks/useApproverDirectory.ts @@ -22,9 +22,9 @@ * * ## What it reads, and why the staffing leg is separate * - * - one `find` per distinct directory OBJECT (`sys_position`, `sys_team`, - * `sys_business_unit`, `sys_user`), batched with `$in` over that object's - * values — the label leg for a kind the spec binds to a DATA row; + * - one `find` per distinct directory OBJECT a `data` binding names (a team, + * department or user row), batched with `$in` over that object's values — + * the label leg for a kind the spec binds to a DATA row; * - one registry read per metadata TYPE, for a kind the spec binds to the * registry instead (`{ source: 'registry', type }`, objectui#7611 Q3b): * the console's metadata store, `ensureType()` — the @@ -38,9 +38,9 @@ * the position's machine NAME under either binding, so this leg does not * depend on which one the spec declares. * - * The two legs fail INDEPENDENTLY. A business-app reader may hold list access to - * `sys_position` and none to `sys_user_position`; `find` rejects that with - * `OBJECT_API_DISABLED` rather than an empty page. Folding the legs together + * The two legs fail INDEPENDENTLY. A business-app reader may read the position + * registry and hold no list access to `sys_user_position`; `find` rejects that + * with `OBJECT_API_DISABLED` rather than an empty page. Folding the legs together * would let a permission error render as `销售经理(暂无在岗人员)` — a fabricated * fact on a governance surface. A failed staffing leg therefore leaves `holders` * `undefined` (unknown), which `approverDisplay` renders as silence, while the diff --git a/packages/app-shell/src/utils/approverIdentity.ts b/packages/app-shell/src/utils/approverIdentity.ts index 509d46942c..77afa4b9e9 100644 --- a/packages/app-shell/src/utils/approverIdentity.ts +++ b/packages/app-shell/src/utils/approverIdentity.ts @@ -104,7 +104,6 @@ export interface ApproverDirectoryBinding { * columns without a second edit here. */ const DISPLAY_FIELDS_BY_OBJECT: Record = { - sys_position: ['label', 'name'], sys_team: ['label', 'name'], sys_business_unit: ['label', 'name'], sys_user: ['full_name', 'name', 'display_name', 'email'], @@ -149,9 +148,10 @@ const KNOWN_APPROVER_KINDS = new Set(Object.keys(APPROVER_VALUE_SOURCES)); * * Read off the published binding, never off a list of kinds: a registry-bound * kind (objectui#7611 Q3b) names a declared item and has no id at all, and a - * `data` kind commits a name exactly when its `valueField` is `name`. Under the - * installed spec `position` answers through the second arm, under a spec - * carrying objectstack#22710 through the first — the same answer either way. + * `data` kind commits a name exactly when its `valueField` is `name`. The + * installed spec binds `position` to the registry (objectstack#22710), so it + * answers through the first arm; the second arm reads a `data` binding the same + * way, so a kind the spec moves is followed with no edit here. */ export function approverValueIsName(kind: string): boolean { return !!APPROVER_REGISTRY_TYPES[kind] || APPROVER_DIRECTORY_BINDINGS[kind]?.valueField === 'name'; From 7a541d4dc6dd97aaca88fd330d77af6d0525bb42 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 12:05:16 +0000 Subject: [PATCH 2/4] fix(app-shell): the pre-publish capability lint reads the registry's capabilities `validateCapabilityReferences` resolves a reference against the built-in capabilities, `stack.capabilities` (the `defineCapability` declarations) and `systemPermissions` grants. The console's pseudo-stack carried the first and the last only, so a capability a package declares with `defineCapability` and no permission set grants was reported as resolving nowhere on Publish from Home. Measured live against objectstack main 830a63a: an object draft requiring `showcase.restricted_ops` raised "2 capability reference(s) resolve nowhere" beside a misspelled control; with the registry list read, only the misspelling is reported. The pseudo-stack now carries `GET /api/v1/meta/capability`. A failed read leaves the pass on its other sources, still advisory. Part of objectui#7611 (release record 6357827416, item 3). Claude-Session: https://claude.ai/code/session_01WvJkpohCWbizJavd86HTZz Co-authored-by: Claude --- .changeset/7611-registry-capability-lint.md | 9 ++ ...lityLint.registryCapabilities-7611.test.ts | 92 +++++++++++++++++++ .../app-shell/src/preview/capabilityLint.ts | 18 +++- 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 .changeset/7611-registry-capability-lint.md create mode 100644 packages/app-shell/src/preview/capabilityLint.registryCapabilities-7611.test.ts diff --git a/.changeset/7611-registry-capability-lint.md b/.changeset/7611-registry-capability-lint.md new file mode 100644 index 0000000000..61548786e7 --- /dev/null +++ b/.changeset/7611-registry-capability-lint.md @@ -0,0 +1,9 @@ +--- +'@object-ui/app-shell': patch +--- + +Publish from Home no longer reports a `defineCapability` declaration as a capability that resolves nowhere (objectui#7611). + +The advisory capability-reference lint that runs over pending drafts before **Publish** handed `@objectstack/lint`'s `validateCapabilityReferences` the drafts and the permission sets only, so a capability a package declares with `defineCapability` and no permission set grants yet was flagged on every object, field, action or app draft that required it. The lint now also reads the capabilities the registry serves (`GET /api/v1/meta/capability`) into the rule's `capabilities` source. A misspelled capability is still reported. If that read fails, the lint runs on the other sources as before and never blocks publishing. + +Internal, no behaviour change: `approverIdentity` drops a display-column entry for `sys_position`, which no approver binding has named since `position` became registry-bound. diff --git a/packages/app-shell/src/preview/capabilityLint.registryCapabilities-7611.test.ts b/packages/app-shell/src/preview/capabilityLint.registryCapabilities-7611.test.ts new file mode 100644 index 0000000000..aedd8d6a57 --- /dev/null +++ b/packages/app-shell/src/preview/capabilityLint.registryCapabilities-7611.test.ts @@ -0,0 +1,92 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#7611 — the pre-publish capability lint reads the capabilities the + * registry serves. + * + * `validateCapabilityReferences` resolves a reference against three sources: + * the built-in platform capabilities, `stack.capabilities` (the + * `defineCapability` declarations) and `systemPermissions` grants. The console + * assembled only the first and the last, so a capability declared with + * `defineCapability` and granted by no permission set was reported as + * resolving nowhere on Publish from Home. The pseudo-stack now carries the + * registry's capability list (`GET /api/v1/meta/capability`). + * + * The rule is `@objectstack/lint`'s own, not a stand-in: the verdict pinned + * here is the one the console's Publish toast shows. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { validateCapabilityReferences } from '@objectstack/lint'; +import { lintDraftCapabilityReferences } from './capabilityLint'; + +const draft = (item: Record) => Promise.resolve({ item }); + +/** Two object drafts: one names a declared-only capability, one a misspelling. */ +const REQUIRED: Record = { + declared_only: 'acme.restricted_ops', + misspelled: 'acme.restricted_opz', +}; + +const PENDING = [ + { type: 'object', name: 'declared_only', packageId: null }, + { type: 'object', name: 'misspelled', packageId: null }, +]; + +function fakeClient(capabilityList: () => Promise) { + return { + getDraft: vi.fn((type: string, name: string) => + type === 'object' ? draft({ name, requiredPermissions: [REQUIRED[name]] }) : Promise.resolve(null), + ), + list: vi.fn((type: string) => + type === 'capability' + ? capabilityList() + : Promise.resolve([{ item: { name: 'member_default', systemPermissions: [] } }]), + ), + }; +} + +describe('lintDraftCapabilityReferences — registry capabilities (objectui#7611)', () => { + it('resolves a capability the registry serves though no permission set grants it', async () => { + const client = fakeClient(() => + Promise.resolve([{ name: 'acme.restricted_ops', label: 'Restricted Operations', scope: 'org' }]), + ); + + const warnings = await lintDraftCapabilityReferences(client, PENDING, validateCapabilityReferences); + + expect(client.list).toHaveBeenCalledWith('capability', {}); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('object "misspelled"'); + expect(warnings[0]).toContain('"acme.restricted_opz"'); + }); + + it('reads the registry row as the list serves it, `{ item }` envelope included', async () => { + const client = fakeClient(() => Promise.resolve([{ item: { name: 'acme.restricted_ops', scope: 'org' } }])); + + const warnings = await lintDraftCapabilityReferences(client, PENDING, validateCapabilityReferences); + + expect(warnings.map((w) => w.split(':')[0])).toEqual(['object "misspelled"']); + }); + + it('CONTROL — the same drafts warn twice when the registry does not serve the capability', async () => { + const client = fakeClient(() => Promise.resolve([])); + + const warnings = await lintDraftCapabilityReferences(client, PENDING, validateCapabilityReferences); + + expect(warnings.map((w) => w.split(':')[0])).toEqual(['object "declared_only"', 'object "misspelled"']); + }); + + it('a failed capability read leaves the pass advisory: the rule still runs on the other sources', async () => { + const client = fakeClient(() => Promise.reject(new Error('network'))); + + const warnings = await lintDraftCapabilityReferences(client, PENDING, validateCapabilityReferences); + + expect(warnings).toHaveLength(2); + }); +}); diff --git a/packages/app-shell/src/preview/capabilityLint.ts b/packages/app-shell/src/preview/capabilityLint.ts index 57710f9a3e..3a9ac20426 100644 --- a/packages/app-shell/src/preview/capabilityLint.ts +++ b/packages/app-shell/src/preview/capabilityLint.ts @@ -63,10 +63,11 @@ async function loadRule(): Promise { * human-readable warning strings (empty = clean or lint unavailable). * * The author-time "known capability" set the rule resolves against is the - * union of built-in platform capabilities and every `systemPermissions` grant on - * the environment's permission sets (published ∪ pending permission drafts) — - * so the assembled pseudo-stack includes the PUBLISHED permission sets, not - * just drafts. + * union of built-in platform capabilities, every capability the environment's + * registry serves (`defineCapability` declarations, the `stack.capabilities` + * source the rule reads), and every `systemPermissions` grant on the + * environment's permission sets (published ∪ pending permission drafts) — so + * the assembled pseudo-stack includes the PUBLISHED catalog, not just drafts. * * @param ruleOverride test seam — inject the rule instead of feature-detecting. */ @@ -126,12 +127,21 @@ export async function lintDraftCapabilityReferences( .catch(() => null) ?? Promise.resolve(null), ), ); + // Declaration side: the capabilities the registry serves + // (`GET /api/v1/meta/capability`, objectui#7611). A capability declared + // with `defineCapability` and granted by no permission set is still + // declared; leaving this source out of the pseudo-stack told the author it + // "resolves nowhere" when the rule itself would have resolved it. + const capabilities = await (client.list?.('capability', {}) as Promise) + ?.then((rows) => (Array.isArray(rows) ? rows.map(unwrap).filter(Boolean) : [])) + .catch(() => []) ?? []; const stack: Record = { objects: bodies.filter((b, i) => b && linted[i].type === 'object'), apps: bodies.filter((b, i) => b && linted[i].type === 'app'), actions: bodies.filter((b, i) => b && linted[i].type === 'action'), permissions: [...publishedPerms, ...permDrafts.filter(Boolean)], + capabilities, }; return rule(stack) From df629b124d67687c0e1b06b2fbc96163854da1c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 12:05:29 +0000 Subject: [PATCH 3/4] docs(guide): name the sources the pre-publish capability lint resolves against Part of objectui#7611 (release record 6357827416, item 3). Claude-Session: https://claude.ai/code/session_01WvJkpohCWbizJavd86HTZz Co-authored-by: Claude --- content/docs/guide/console.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/content/docs/guide/console.md b/content/docs/guide/console.md index 4aa3281c43..927b3525dd 100644 --- a/content/docs/guide/console.md +++ b/content/docs/guide/console.md @@ -94,6 +94,10 @@ that everyone will see the button there. An unheld capability is not the same problem as a *misspelled* one: a capability string registered nowhere is caught separately, by the advisory capability-reference lint that runs over pending drafts during **Publish**. +It resolves a reference against the platform's built-in capabilities, the +capabilities the registry serves (a package's `defineCapability` declarations) +and every permission set's `systemPermissions`, so a declared capability that +no set grants yet is not reported. ## Configuration From b398f2ae78a841468176e9f6c0a7475364e93054 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 12:11:49 +0000 Subject: [PATCH 4/4] =?UTF-8?q?chore(changeset):=20declare=20Clause-?= =?UTF-8?q?=E2=91=A1=20and=20Metadata-protocol=20on=20the=207611=20changes?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01WvJkpohCWbizJavd86HTZz Co-authored-by: Claude --- .changeset/7611-registry-capability-lint.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.changeset/7611-registry-capability-lint.md b/.changeset/7611-registry-capability-lint.md index 61548786e7..f80e0e1162 100644 --- a/.changeset/7611-registry-capability-lint.md +++ b/.changeset/7611-registry-capability-lint.md @@ -7,3 +7,6 @@ Publish from Home no longer reports a `defineCapability` declaration as a capabi The advisory capability-reference lint that runs over pending drafts before **Publish** handed `@objectstack/lint`'s `validateCapabilityReferences` the drafts and the permission sets only, so a capability a package declares with `defineCapability` and no permission set grants yet was flagged on every object, field, action or app draft that required it. The lint now also reads the capabilities the registry serves (`GET /api/v1/meta/capability`) into the rule's `capabilities` source. A misspelled capability is still reported. If that read fails, the lint runs on the other sources as before and never blocks publishing. Internal, no behaviour change: `approverIdentity` drops a display-column entry for `sys_position`, which no approver binding has named since `position` became registry-bound. + +Clause-②: no +Metadata-protocol: none