Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/25832-objectui-skill-in-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): the `objectui` skill joins the published catalog, so `@objectstack/skills` ships the `@object-ui/*` authoring skill beside the `objectstack-*` ones

Clause-②: no
Metadata-protocol: none

- **What ships.** `skills/objectui/` (its `SKILL.md`, 11 guides, 3 rules, 11 evals and a README) moves in from the objectui repository's own `skills/` tree. A project that syncs `@objectstack/skills` now gets an `objectui` skill in its agent directory, and `npx skills add objectstack-ai/objectstack/skills` lists it on the `next` channel.
- **Stamped like the others.** Its `compatibility:` line now reads `Requires @objectstack/spec 18.x`, and `metadata.version` carries the workspace version, as on every catalog skill.
- **Content changes on the way in.** The guides use the platform's vocabulary: expression examples gate on `data.status` or a user position, not on a `role` field, and the expressions guide gates a combined condition with `visibleWhen` instead of the deprecated `visibleOn`. The `@object-ui/permissions` example keeps its real `roles` keys, and the plugin guide's color input passes `value ?? ''` instead of a hard-coded black fallback.
13 changes: 12 additions & 1 deletion packages/spec/scripts/check-skill-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,18 @@ const DOCS_DIR = path.resolve(REPO_ROOT, 'content/docs');
const SDK_DOCS_PAGES = ['api/client-sdk.mdx', 'kernel/runtime-services/data-service.mdx'];

const SKILLS_DOCS_ROOTS: SourceRoot[] = [
{ dir: path.resolve(REPO_ROOT, 'skills'), ext: '.md', label: 'skills', marker: '<!-- os:check -->' },
{
dir: path.resolve(REPO_ROOT, 'skills'),
ext: '.md',
label: 'skills',
marker: '<!-- os:check -->',
// The objectui bundle joined this catalog at the cutover (objectstack#25803)
// with its own reading of the same marker: it marks `json` fences as well as
// `ts`/`tsx`, and compiles them against `@object-ui/*`. Its gate is
// `ui/scripts/check-skill-examples.mjs`, which still judges every marked fence
// in it; read here, its 70 `json` markers are orphans by this gate's rule.
exclude: [path.resolve(REPO_ROOT, 'skills/objectui')],
},
{
dir: DOCS_DIR,
ext: '.mdx',
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-skill-identifier-liveness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(HERE, '..');

const ROOTS = ['skills'];
const IMPL_ROOTS = ['packages'];
const IMPL_ROOTS = ['packages', 'ui/packages'];
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.turbo']);
const EXTENSIONS = new Set(['.md']);
const IMPL_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json']);
Expand Down
41 changes: 41 additions & 0 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,47 @@ export const CEILINGS = new Map([
['skills/objectstack-platform/references/operations.md', 3125],
['skills/objectstack-platform/references/plugin-development.md', 3137],
['skills/objectstack-upgrade/references/examples-upgrade.md', 1197],

// ── 2026-10-26 the objectui bundle joins the catalog — decision C, #25832 ──
// The maintainer ruled (decision C, 2026-10-25, recorded as item 17d of the
// cutover card #25752) that objectui's published skill bundle moves from
// `ui/skills/objectui` into this catalog, so one `@objectstack/skills` package
// serves both products. The 27 files arrived from the objectui repository,
// where they shipped unpriced by this gate. Each row is an INITIAL
// measurement of the moved file on the PR that moves it, after that PR's own
// edits (the reserved-word rewrite, the `visibleWhen` key, the stamped
// `metadata.version`, whose line is not priced), pinned AT it — zero
// headroom, on the same terms as the #12392 extension and the evals rows.
// No existing row moves. Adding the rows is the pricing act the header
// reserves to the maintainer; the PR that adds them is Tier H and lands only
// on the maintainer's approval, which is that word.
['skills/objectui/README.md', 735],
['skills/objectui/SKILL.md', 1852],
['skills/objectui/evals/app-composition.json', 810],
['skills/objectui/evals/auth-permissions.json', 366],
['skills/objectui/evals/data-integration.json', 471],
['skills/objectui/evals/i18n.json', 439],
['skills/objectui/evals/mobile.json', 452],
['skills/objectui/evals/page-builder.json', 560],
['skills/objectui/evals/plugin-development.json', 492],
['skills/objectui/evals/project-setup.json', 786],
['skills/objectui/evals/protocol.json', 761],
['skills/objectui/evals/schema-expressions.json', 536],
['skills/objectui/evals/testing.json', 466],
['skills/objectui/guides/app-composition.md', 1079],
['skills/objectui/guides/architecture.md', 1045],
['skills/objectui/guides/auth-permissions.md', 3866],
['skills/objectui/guides/data-integration.md', 4451],
['skills/objectui/guides/i18n.md', 1413],
['skills/objectui/guides/mobile.md', 1349],
['skills/objectui/guides/page-builder.md', 3411],
['skills/objectui/guides/plugin-development.md', 4184],
['skills/objectui/guides/project-setup.md', 2727],
['skills/objectui/guides/schema-expressions.md', 6571],
['skills/objectui/guides/testing.md', 3101],
['skills/objectui/rules/composition.md', 1624],
['skills/objectui/rules/protocol.md', 4368],
['skills/objectui/rules/styling.md', 2699],
]);

/**
Expand Down
3 changes: 2 additions & 1 deletion scripts/role-word-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@
"skills/objectstack-query/rules/filters.md": 2,
"skills/objectstack-ui/rules/list-views.md": 1,
"skills/objectstack-ui/rules/navigation.md": 1,
"skills/objectstack-upgrade/references/examples-upgrade.md": 1
"skills/objectstack-upgrade/references/examples-upgrade.md": 1,
"skills/objectui/guides/auth-permissions.md": 13
}
4 changes: 2 additions & 2 deletions skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ A few common decision points where the right skill isn't obvious:
**objectstack-data**.
- **Labels vs. bundles** — a `label` you want translated is **objectstack-i18n**.
- **Upgrade vs. platform** — a protocol-major move is **objectstack-upgrade**.
- **Rendering this metadata** — the consuming UI is
[objectui `skills/objectui/`](https://github.com/objectstack-ai/objectui).
- **Rendering this metadata** — the consuming UI is the `@object-ui/*`
packages, and their own skill is [`objectui`](./objectui/SKILL.md) in this catalog.

---

Expand Down
File renamed without changes.
7 changes: 3 additions & 4 deletions ui/skills/objectui/SKILL.md → skills/objectui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
name: objectui
description: Universal Server-Driven UI (SDUI) engine for building JSON-driven React interfaces with Shadcn design quality. Use for schema-driven page building, plugin development, expression bindings, data integration, testing, auth/permissions, i18n, mobile responsiveness and project setup with the `@object-ui/*` packages. Triggers on ObjectUI, SchemaRenderer, JSON UI schemas, SDUI, metadata-driven UIs, `@object-ui/*`. Do NOT use for server-side ObjectStack concerns (data modelling, API endpoints, automation, formulas, agents) — those belong to the `objectstack-*` skills.
license: Apache-2.0
compatibility: Tracks the @objectstack/spec range each @object-ui/* package declares in its own manifest; read it there. This file states no version literal, per the doc-version-claims ratchet.
compatibility: Requires @objectstack/spec 18.x; each @object-ui/* package declares its exact @objectstack/spec range in its own manifest.
metadata:
internal: true
author: objectstack-ai
version: "2.0"
version: "18.1.0"
domain: ui
tags: sdui, schema-renderer, json-ui, react, shadcn, tailwind, plugin, expression
user-invocable: false
Expand All @@ -31,7 +30,7 @@ kanbans, CRUDs, forms, grids.
| Start a new project — CLI, Vite, Tailwind, config files | [`guides/project-setup.md`](./guides/project-setup.md) |
| Write unit / DOM / E2E tests for components, schemas and plugins | [`guides/testing.md`](./guides/testing.md) |
| Responsive behaviour, breakpoints, touch gestures | [`guides/mobile.md`](./guides/mobile.md) |
| Auth, roles, tenants, client-side permission guards | [`guides/auth-permissions.md`](./guides/auth-permissions.md) |
| Auth, positions, tenants, client-side permission guards | [`guides/auth-permissions.md`](./guides/auth-permissions.md) |
| Multi-language text and formatters | [`guides/i18n.md`](./guides/i18n.md) |

**Rules — read before writing schemas.** These are the non-negotiables, and they
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ interface BaseSchema {
className?: string;

/** Dynamic Behavior */
hidden?: boolean | ExpressionWire; // e.g. "${data.role != 'admin'}"
hidden?: boolean | ExpressionWire; // e.g. "${data.status != 'active'}"
disabled?: boolean | ExpressionWire; // same wire as hidden

/** Layout Slots */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,15 @@ function isTenantAdmin(user: AuthUser): boolean {

`AuthUser` extends the spec's `IAuthService` principal — `id`, `email`, `name`,
plus the `positions` / `tenantId` an authorization decision reads — and adds the
display-only fields better-auth returns to the client: `image`, `role`,
`emailVerified`. The index signature (`[key: string]: unknown`) is deliberate:
display-only fields better-auth returns to the client, spelled as better-auth
spells them:

<!-- os:vendor-wire better-auth -->
```typescript
image?: string; role?: string; emailVerified?: boolean;
```

The index signature (`[key: string]: unknown`) is deliberate:
better-auth projects an app's own user columns onto this object, and no local
type can enumerate them.

Expand Down Expand Up @@ -110,7 +117,7 @@ import { LoginForm, RegisterForm, ForgotPasswordForm, UserMenu } from '@object-u
// LoginForm includes email/password fields, social login buttons, forgot password link
<LoginForm onSuccess={() => navigate('/dashboard')} />

// UserMenu shows user avatar, name, role with dropdown for profile/settings/logout
// UserMenu shows user avatar and name with a dropdown for profile/settings/logout
<UserMenu />
```

Expand Down Expand Up @@ -142,28 +149,28 @@ import { useAuth } from '@object-ui/auth';
import type { ObjectPermissionConfig, RoleDefinition } from '@object-ui/types';
declare function AppContent(): null; // stands in for your own app tree

// A role definition carries identity and inheritance only. Its grants live in
// A `RoleDefinition` carries identity and inheritance only. Its grants live in
// `ObjectPermissionConfig.roles` below, keyed by object — that is the single
// wired home for "what may this role do".
const roles: RoleDefinition[] = [
// wired home for "what may this grant key do".
const definitions: RoleDefinition[] = [
{ name: 'admin', label: 'Administrator' },
{ name: 'viewer', label: 'Viewer', inherits: ['base'] }, // Role inheritance
{ name: 'viewer', label: 'Viewer', inherits: ['base'] }, // inherits another definition
];

const permissions: ObjectPermissionConfig[] = [
{
object: 'contacts',
publicAccess: ['read'], // Public actions (no auth needed)
// `roles` is required, and it is the level that decides *who* a rule
// applies to. Field and row rules nest under the role name; they carry
// applies to. Field and row rules nest under the grant key; they carry
// no `roles` member of their own.
roles: {
admin: {
actions: ['create', 'read', 'update', 'delete', 'export'],
},
viewer: {
actions: ['read'],
// Only admin sees salary: the restriction is declared on the role
// Only admin sees salary: the restriction is declared on the key
// it restricts, and admin simply has no entry for the field.
fieldPermissions: [{ field: 'salary', read: false, mask: '****' }],
},
Expand All @@ -189,7 +196,7 @@ function App() {
const { user } = useAuth();
return (
<PermissionProvider
roles={roles}
roles={definitions}
permissions={permissions}
userRoles={user?.positions || ['viewer']}
user={user ?? undefined}
Expand All @@ -201,7 +208,7 @@ function App() {
```

The keys under `roles` are matched against the `userRoles` you pass to the
provider (after inheritance is expanded through the `roles` array), so a grant
provider (after each key's `inherits` chain is expanded), so a grant
key such as `owner` above works whether or not it also has a `RoleDefinition`
entry — the definitions array is read only to expand `inherits`.

Expand Down Expand Up @@ -252,8 +259,8 @@ function ContactActions({ contact }: { contact: { salary?: number } }) {
When `check(object, action, record?)` is called:

1. Check `publicAccess` — if action is public, allow immediately
2. Resolve effective roles (with inheritance chain)
3. Check role-based action permissions
2. Resolve the effective grant keys (`userRoles` expanded through `inherits`)
3. Check the action grants under each key
4. If record provided + row permissions exist: evaluate row-level filter
5. Return `{ allowed, fieldRestrictions?, rowFilter?, reason? }`

Expand Down Expand Up @@ -387,7 +394,7 @@ Here's the typical nesting order for a full-featured app:
```typescript
<BrowserRouter>
<AuthProvider authUrl="/api/v1/auth" client={authClient}>
<PermissionProvider roles={roles} permissions={perms} userRoles={userRoles}>
<PermissionProvider roles={definitions} permissions={perms} userRoles={userRoles}>
<SchemaRendererProvider dataSource={authenticatedDataSource}>
<AuthGuard fallback={<LoginPage />}>
<AppShell>
Expand All @@ -405,5 +412,5 @@ Here's the typical nesting order for a full-featured app:
- Nesting PermissionProvider outside AuthProvider — userRoles can't be resolved from auth state.
- Using `hidden` expressions for security — they only hide UI elements. Always enforce permissions server-side.
- Forgetting `createAuthenticatedFetch` — API calls go without Bearer token, getting 401 errors.
- Hardcoding role checks in JSX instead of using PermissionGuard or usePermissions.
- Hardcoding permission checks in JSX instead of using PermissionGuard or usePermissions.
- Not providing fallback UI for AuthGuard/PermissionGuard — users see blank screens.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Before writing implementation code, define:

- Page purpose (dashboard, list, detail, form, wizard, board).
- Required data inputs and output actions.
- User roles and visibility rules.
- User positions and visibility rules.
- Interaction model (navigation, submit, bulk actions, modals).

Then produce a first schema draft.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export function ColorField({
<Input
type="color"
className={className}
value={value || '#000000'}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
disabled={readonly || disabled}
// `error` drives the a11y state ONLY. The form renderer already prints
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Writing and debugging dynamic expressions in ObjectUI schemas. The expression sy
ObjectUI uses a two-tier expression evaluator:

1. **Template expressions** — strings containing `${...}` placeholders: `"Hello ${user.name}"`
2. **Condition expressions** — raw boolean expressions without wrappers: `data.role === 'admin'`
2. **Condition expressions** — raw boolean expressions without wrappers: `data.status === 'active'`

Both are parsed by a **recursive-descent parser** (no `eval()`, no `new Function()`). This means expressions are CSP-safe and work under strict Content Security Policy headers.

Expand Down Expand Up @@ -101,8 +101,8 @@ When the entire string is a single `${expression}`, the result preserves its typ

### Multiple interpolations
```
"${user.firstName} ${user.lastName} (${user.role})"
→ "Alice Smith (admin)"
"${user.firstName} ${user.lastName} (${user.email})"
→ "Alice Smith (alice@example.com)"
```

## Available scope variables
Expand Down Expand Up @@ -245,22 +245,22 @@ Each condition field has two forms — a shorthand and an `On` suffix:

<!-- os:check -->
```jsonc
{ "hidden": "${data.role !== 'admin'}" } // template expression
{ "hidden": "${data.status !== 'active'}" } // template expression
```

<!-- os:check -->
```jsonc
{ "hiddenOn": "data.role !== 'admin'" } // raw expression (no ${} needed)
{ "hiddenOn": "data.status !== 'active'" } // raw expression (no ${} needed)
```

The `On` variants accept raw expressions without `${}` wrapping — the entire string is the expression.

### Visibility patterns

**Role-based:**
**Position-based:**
<!-- os:check -->
```json
{ "hidden": "${data.userRole !== 'admin'}" }
{ "hidden": "${data.userPosition !== 'admin'}" }
```

**Status-based:**
Expand All @@ -278,7 +278,7 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s
**Combined conditions:**
<!-- os:check -->
```json
{ "visibleOn": "data.isAuthenticated && data.permissions.canEdit" }
{ "visibleWhen": "data.isAuthenticated && data.permissions.canEdit" }
```

### Disabled patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe('ExpressionEvaluator', () => {
// own literal, so the test fails with the source string as `received`.
const evaluator = new ExpressionEvaluator({
data: { name: 'Alice', count: 42, items: [1, 2, 3] },
user: { role: 'admin' },
user: { position: 'admin' },
});

it('evaluates template expressions', () => {
Expand All @@ -115,7 +115,7 @@ describe('ExpressionEvaluator', () => {
});

it('evaluates boolean conditions', () => {
expect(evaluator.evaluateExpression('user.role === "admin"')).toBe(true);
expect(evaluator.evaluateExpression('user.position === "admin"')).toBe(true);
});

it('handles missing variables safely', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ value must clear before it reaches the screen.
| Field | Evaluation Type | Return Type | Example |
|---|---|---|---|
| `content` | Template (`${}`) | string | `"content": "Total: ${data.total}"` |
| `hidden` | Condition | boolean | `"hidden": "${data.role !== 'admin'}"` |
| `hidden` | Condition | boolean | `"hidden": "${data.status !== 'active'}"` |
| `hiddenOn` | Condition | boolean | `"hiddenOn": "data.status === 'draft'"` |
| `visible` | Condition | boolean | `"visible": "${data.isActive}"` |
| `visibleOn` | Condition | boolean | `"visibleOn": "data.permissions.canView"` |
Expand Down
4 changes: 2 additions & 2 deletions ui/content/docs/guide/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: "Install the ObjectUI Agent Skill into Claude Code, Cursor, Codex,

ObjectUI ships an official **Agent Skill** — a structured bundle of rules, guides, and evals that teaches AI coding agents how to build pages, plugins, and integrations the *right* way (schema-first, no-touch zones respected, English-only, expression-aware).

The skill is published on [skills.sh](https://skills.sh/objectstack-ai/objectui) and lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectui/tree/main/skills/objectui) inside this repository.
The skill is published on [skills.sh](https://skills.sh/objectstack-ai/objectui) and lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectstack/tree/main/skills/objectui) inside this repository.

[![skills.sh](https://skills.sh/b/objectstack-ai/objectui)](https://skills.sh/objectstack-ai/objectui)

Expand Down Expand Up @@ -103,4 +103,4 @@ The CLI is idempotent — running it again refreshes the local copy in place.

## Contributing

The skill source lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectui/tree/main/skills/objectui). PRs are welcome — see [`skills/objectui/README.md`](https://github.com/objectstack-ai/objectui/blob/main/skills/objectui/README.md) for the maintenance rules (guide ↔ eval naming, English-only, eval assertion format).
The skill source lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectstack/tree/main/skills/objectui). PRs are welcome — see [`skills/objectui/README.md`](https://github.com/objectstack-ai/objectstack/blob/main/skills/objectui/README.md) for the maintenance rules (guide ↔ eval naming, English-only, eval assertion format).
Loading
Loading