From 0c57048fffe5c1ae9a68107922e2efc486aa2e9e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:33:59 +0000 Subject: [PATCH 1/7] wip(skills): move the objectui bundle into the root skills/ catalog (patch 19 re-applied on main) Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- packages/spec/scripts/check-skill-examples.ts | 13 ++- scripts/check-skill-identifier-liveness.mjs | 2 +- {ui/skills => skills}/objectui/README.md | 0 {ui/skills => skills}/objectui/SKILL.md | 3 +- .../objectui/evals/app-composition.json | 0 .../objectui/evals/auth-permissions.json | 0 .../objectui/evals/data-integration.json | 0 .../objectui/evals/i18n.json | 0 .../objectui/evals/mobile.json | 0 .../objectui/evals/page-builder.json | 0 .../objectui/evals/plugin-development.json | 0 .../objectui/evals/project-setup.json | 0 .../objectui/evals/protocol.json | 0 .../objectui/evals/schema-expressions.json | 0 .../objectui/evals/testing.json | 0 .../objectui/guides/app-composition.md | 0 .../objectui/guides/architecture.md | 0 .../objectui/guides/auth-permissions.md | 0 .../objectui/guides/data-integration.md | 0 {ui/skills => skills}/objectui/guides/i18n.md | 0 .../objectui/guides/mobile.md | 0 .../objectui/guides/page-builder.md | 0 .../objectui/guides/plugin-development.md | 2 +- .../objectui/guides/project-setup.md | 0 .../objectui/guides/schema-expressions.md | 0 .../objectui/guides/testing.md | 0 .../objectui/rules/composition.md | 0 .../objectui/rules/protocol.md | 0 .../objectui/rules/styling.md | 0 ui/content/docs/guide/agent-skills.md | 4 +- .../skill-guide-data-table-binding.test.tsx | 6 +- .../skill-guide-provider-envelope.test.tsx | 10 ++- .../skill-guide-permission-config.test.tsx | 8 +- .../__tests__/list-bind-only-11405.test.ts | 6 +- .../__tests__/check-doc-json-fences.test.ts | 3 +- .../check-governed-queue-guard.test.ts | 6 +- .../__tests__/check-skill-examples.test.ts | 11 +-- .../__tests__/doc-version-claims.test.ts | 24 +++++- .../layered-read-declared-path-4016.test.ts | 6 +- ui/scripts/check-doc-json-fences.mjs | 7 +- ui/scripts/check-shell-escape-residue.mjs | 18 +++- ui/scripts/check-skill-eval-tokens.mjs | 19 +++-- ui/scripts/check-skill-examples.mjs | 15 ++-- ui/scripts/check-skills-paths.mjs | 9 +- ui/scripts/markdown-test-inputs.mjs | 26 +----- ui/scripts/skill-bundle-root.mjs | 83 +++++++++++++++++++ 46 files changed, 212 insertions(+), 69 deletions(-) rename {ui/skills => skills}/objectui/README.md (100%) rename {ui/skills => skills}/objectui/SKILL.md (97%) rename {ui/skills => skills}/objectui/evals/app-composition.json (100%) rename {ui/skills => skills}/objectui/evals/auth-permissions.json (100%) rename {ui/skills => skills}/objectui/evals/data-integration.json (100%) rename {ui/skills => skills}/objectui/evals/i18n.json (100%) rename {ui/skills => skills}/objectui/evals/mobile.json (100%) rename {ui/skills => skills}/objectui/evals/page-builder.json (100%) rename {ui/skills => skills}/objectui/evals/plugin-development.json (100%) rename {ui/skills => skills}/objectui/evals/project-setup.json (100%) rename {ui/skills => skills}/objectui/evals/protocol.json (100%) rename {ui/skills => skills}/objectui/evals/schema-expressions.json (100%) rename {ui/skills => skills}/objectui/evals/testing.json (100%) rename {ui/skills => skills}/objectui/guides/app-composition.md (100%) rename {ui/skills => skills}/objectui/guides/architecture.md (100%) rename {ui/skills => skills}/objectui/guides/auth-permissions.md (100%) rename {ui/skills => skills}/objectui/guides/data-integration.md (100%) rename {ui/skills => skills}/objectui/guides/i18n.md (100%) rename {ui/skills => skills}/objectui/guides/mobile.md (100%) rename {ui/skills => skills}/objectui/guides/page-builder.md (100%) rename {ui/skills => skills}/objectui/guides/plugin-development.md (99%) rename {ui/skills => skills}/objectui/guides/project-setup.md (100%) rename {ui/skills => skills}/objectui/guides/schema-expressions.md (100%) rename {ui/skills => skills}/objectui/guides/testing.md (100%) rename {ui/skills => skills}/objectui/rules/composition.md (100%) rename {ui/skills => skills}/objectui/rules/protocol.md (100%) rename {ui/skills => skills}/objectui/rules/styling.md (100%) create mode 100644 ui/scripts/skill-bundle-root.mjs diff --git a/packages/spec/scripts/check-skill-examples.ts b/packages/spec/scripts/check-skill-examples.ts index bf5153085f1..bcbe28054e2 100644 --- a/packages/spec/scripts/check-skill-examples.ts +++ b/packages/spec/scripts/check-skill-examples.ts @@ -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: '' }, + { + dir: path.resolve(REPO_ROOT, 'skills'), + ext: '.md', + label: 'skills', + marker: '', + // 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', diff --git a/scripts/check-skill-identifier-liveness.mjs b/scripts/check-skill-identifier-liveness.mjs index 3e9d5193747..816be9bd089 100644 --- a/scripts/check-skill-identifier-liveness.mjs +++ b/scripts/check-skill-identifier-liveness.mjs @@ -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']); diff --git a/ui/skills/objectui/README.md b/skills/objectui/README.md similarity index 100% rename from ui/skills/objectui/README.md rename to skills/objectui/README.md diff --git a/ui/skills/objectui/SKILL.md b/skills/objectui/SKILL.md similarity index 97% rename from ui/skills/objectui/SKILL.md rename to skills/objectui/SKILL.md index cb3012b1d42..ac50a3f417c 100644 --- a/ui/skills/objectui/SKILL.md +++ b/skills/objectui/SKILL.md @@ -2,9 +2,8 @@ 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" domain: ui diff --git a/ui/skills/objectui/evals/app-composition.json b/skills/objectui/evals/app-composition.json similarity index 100% rename from ui/skills/objectui/evals/app-composition.json rename to skills/objectui/evals/app-composition.json diff --git a/ui/skills/objectui/evals/auth-permissions.json b/skills/objectui/evals/auth-permissions.json similarity index 100% rename from ui/skills/objectui/evals/auth-permissions.json rename to skills/objectui/evals/auth-permissions.json diff --git a/ui/skills/objectui/evals/data-integration.json b/skills/objectui/evals/data-integration.json similarity index 100% rename from ui/skills/objectui/evals/data-integration.json rename to skills/objectui/evals/data-integration.json diff --git a/ui/skills/objectui/evals/i18n.json b/skills/objectui/evals/i18n.json similarity index 100% rename from ui/skills/objectui/evals/i18n.json rename to skills/objectui/evals/i18n.json diff --git a/ui/skills/objectui/evals/mobile.json b/skills/objectui/evals/mobile.json similarity index 100% rename from ui/skills/objectui/evals/mobile.json rename to skills/objectui/evals/mobile.json diff --git a/ui/skills/objectui/evals/page-builder.json b/skills/objectui/evals/page-builder.json similarity index 100% rename from ui/skills/objectui/evals/page-builder.json rename to skills/objectui/evals/page-builder.json diff --git a/ui/skills/objectui/evals/plugin-development.json b/skills/objectui/evals/plugin-development.json similarity index 100% rename from ui/skills/objectui/evals/plugin-development.json rename to skills/objectui/evals/plugin-development.json diff --git a/ui/skills/objectui/evals/project-setup.json b/skills/objectui/evals/project-setup.json similarity index 100% rename from ui/skills/objectui/evals/project-setup.json rename to skills/objectui/evals/project-setup.json diff --git a/ui/skills/objectui/evals/protocol.json b/skills/objectui/evals/protocol.json similarity index 100% rename from ui/skills/objectui/evals/protocol.json rename to skills/objectui/evals/protocol.json diff --git a/ui/skills/objectui/evals/schema-expressions.json b/skills/objectui/evals/schema-expressions.json similarity index 100% rename from ui/skills/objectui/evals/schema-expressions.json rename to skills/objectui/evals/schema-expressions.json diff --git a/ui/skills/objectui/evals/testing.json b/skills/objectui/evals/testing.json similarity index 100% rename from ui/skills/objectui/evals/testing.json rename to skills/objectui/evals/testing.json diff --git a/ui/skills/objectui/guides/app-composition.md b/skills/objectui/guides/app-composition.md similarity index 100% rename from ui/skills/objectui/guides/app-composition.md rename to skills/objectui/guides/app-composition.md diff --git a/ui/skills/objectui/guides/architecture.md b/skills/objectui/guides/architecture.md similarity index 100% rename from ui/skills/objectui/guides/architecture.md rename to skills/objectui/guides/architecture.md diff --git a/ui/skills/objectui/guides/auth-permissions.md b/skills/objectui/guides/auth-permissions.md similarity index 100% rename from ui/skills/objectui/guides/auth-permissions.md rename to skills/objectui/guides/auth-permissions.md diff --git a/ui/skills/objectui/guides/data-integration.md b/skills/objectui/guides/data-integration.md similarity index 100% rename from ui/skills/objectui/guides/data-integration.md rename to skills/objectui/guides/data-integration.md diff --git a/ui/skills/objectui/guides/i18n.md b/skills/objectui/guides/i18n.md similarity index 100% rename from ui/skills/objectui/guides/i18n.md rename to skills/objectui/guides/i18n.md diff --git a/ui/skills/objectui/guides/mobile.md b/skills/objectui/guides/mobile.md similarity index 100% rename from ui/skills/objectui/guides/mobile.md rename to skills/objectui/guides/mobile.md diff --git a/ui/skills/objectui/guides/page-builder.md b/skills/objectui/guides/page-builder.md similarity index 100% rename from ui/skills/objectui/guides/page-builder.md rename to skills/objectui/guides/page-builder.md diff --git a/ui/skills/objectui/guides/plugin-development.md b/skills/objectui/guides/plugin-development.md similarity index 99% rename from ui/skills/objectui/guides/plugin-development.md rename to skills/objectui/guides/plugin-development.md index 44709edc2de..fed0ac9fb12 100644 --- a/ui/skills/objectui/guides/plugin-development.md +++ b/skills/objectui/guides/plugin-development.md @@ -276,7 +276,7 @@ export function ColorField({ onChange(e.target.value)} disabled={readonly || disabled} // `error` drives the a11y state ONLY. The form renderer already prints diff --git a/ui/skills/objectui/guides/project-setup.md b/skills/objectui/guides/project-setup.md similarity index 100% rename from ui/skills/objectui/guides/project-setup.md rename to skills/objectui/guides/project-setup.md diff --git a/ui/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md similarity index 100% rename from ui/skills/objectui/guides/schema-expressions.md rename to skills/objectui/guides/schema-expressions.md diff --git a/ui/skills/objectui/guides/testing.md b/skills/objectui/guides/testing.md similarity index 100% rename from ui/skills/objectui/guides/testing.md rename to skills/objectui/guides/testing.md diff --git a/ui/skills/objectui/rules/composition.md b/skills/objectui/rules/composition.md similarity index 100% rename from ui/skills/objectui/rules/composition.md rename to skills/objectui/rules/composition.md diff --git a/ui/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md similarity index 100% rename from ui/skills/objectui/rules/protocol.md rename to skills/objectui/rules/protocol.md diff --git a/ui/skills/objectui/rules/styling.md b/skills/objectui/rules/styling.md similarity index 100% rename from ui/skills/objectui/rules/styling.md rename to skills/objectui/rules/styling.md diff --git a/ui/content/docs/guide/agent-skills.md b/ui/content/docs/guide/agent-skills.md index 4c6411f3dc6..96cbe301816 100644 --- a/ui/content/docs/guide/agent-skills.md +++ b/ui/content/docs/guide/agent-skills.md @@ -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) @@ -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). diff --git a/ui/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx b/ui/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx index f24cfc8b967..6d59793ede5 100644 --- a/ui/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx +++ b/ui/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx @@ -92,6 +92,10 @@ import type { DataSource } from '@object-ui/types'; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, '../../../..'); +// The published skill bundle is in the objectstack monorepo's `skills/` catalog +// since the cutover (decision C, objectstack#25803), one level above this tree, +// so its `skills/objectui/...` paths are read from there. +const skillsBase = path.resolve(repoRoot, '..'); const GUIDES = { 'skills/objectui/guides/schema-expressions.md': 'schema-expressions', @@ -102,7 +106,7 @@ const GUIDES = { const GUIDE_PATHS = Object.keys(GUIDES) as (keyof typeof GUIDES)[]; function readGuide(rel: string): string { - return fs.readFileSync(path.join(repoRoot, rel), 'utf8'); + return fs.readFileSync(path.join(skillsBase, rel), 'utf8'); } /** diff --git a/ui/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx b/ui/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx index 05a66996a86..5b150fbdc35 100644 --- a/ui/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx +++ b/ui/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx @@ -81,7 +81,11 @@ import type { DataSource } from '@object-ui/types'; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, '../../../..'); -const skillsRoot = path.join(repoRoot, 'skills/objectui'); +// The published skill bundle is in the objectstack monorepo's `skills/` catalog +// since the cutover (decision C, objectstack#25803), one level above this tree, +// so its `skills/objectui/...` paths are read from there. +const skillsBase = path.resolve(repoRoot, '..'); +const skillsRoot = path.join(skillsBase, 'skills/objectui'); // The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These // mirror the two published guides' `data-table` example, which is why they @@ -123,7 +127,7 @@ function publishedFiles(): string[] { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) walk(full); - else out.push(path.relative(repoRoot, full)); + else out.push(path.relative(skillsBase, full)); } }; walk(skillsRoot); @@ -131,7 +135,7 @@ function publishedFiles(): string[] { } function readSkillFile(rel: string): string { - return fs.readFileSync(path.join(repoRoot, rel), 'utf8'); + return fs.readFileSync(path.join(skillsBase, rel), 'utf8'); } describe('#5372 behaviour — a provider `dataSource` into a `data-table`', () => { diff --git a/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx b/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx index 7cf1355259f..662a5f50053 100644 --- a/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx +++ b/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx @@ -47,7 +47,11 @@ import { usePermissions } from '../usePermissions'; const here = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(here, '../../../..'); -const GUIDE = path.join(repoRoot, 'skills/objectui/guides/auth-permissions.md'); +// The published skill bundle is in the objectstack monorepo's `skills/` catalog +// since the cutover (decision C, objectstack#25803), one level above this tree, +// so its `skills/objectui/...` paths are read from there. +const skillsBase = path.resolve(repoRoot, '..'); +const GUIDE = path.join(skillsBase, 'skills/objectui/guides/auth-permissions.md'); // --- 8< --- guide example begin --- 8< --- // A role definition carries identity and inheritance only. Its grants live in @@ -115,7 +119,7 @@ describe('objectui#4812 — the guide example is the shape the types declare', ( const start = guide.findIndex((l) => l === want[0]); expect( start, - `first line of the pinned example not found in ${path.relative(repoRoot, GUIDE)}`, + `first line of the pinned example not found in ${path.relative(skillsBase, GUIDE)}`, ).toBeGreaterThan(-1); expect(guide.slice(start, start + want.length)).toEqual(want); }); diff --git a/ui/packages/types/src/__tests__/list-bind-only-11405.test.ts b/ui/packages/types/src/__tests__/list-bind-only-11405.test.ts index 57ab3f73ea7..b2d69f3d792 100644 --- a/ui/packages/types/src/__tests__/list-bind-only-11405.test.ts +++ b/ui/packages/types/src/__tests__/list-bind-only-11405.test.ts @@ -63,6 +63,10 @@ const stripJsonComments: (source: string) => string = stripUntyped; /** Rooted at THIS file, never at `process.cwd()`. */ const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..', '..', '..', '..'); +// The published skill bundle is in the objectstack monorepo's `skills/` catalog +// since the cutover (decision C, objectstack#25803), one level above this tree, +// so its `skills/objectui/...` paths are read from there. +const SKILLS_BASE = resolve(REPO_ROOT, '..'); const GUIDES = [ 'skills/objectui/guides/data-integration.md', @@ -133,7 +137,7 @@ type Fence = { guide: string; fenceLine: number; teachesWrong: boolean; node: Re function bindOnlyListFences(): Fence[] { const out: Fence[] = []; for (const guide of GUIDES) { - const { fences } = scanSkillFences(readFileSync(resolve(REPO_ROOT, guide), 'utf8')); + const { fences } = scanSkillFences(readFileSync(resolve(SKILLS_BASE, guide), 'utf8')); for (const fence of fences) { if (!fence.marked || fence.kind !== 'json') continue; // The marker's own gate already requires every marked fence to parse. diff --git a/ui/scripts/__tests__/check-doc-json-fences.test.ts b/ui/scripts/__tests__/check-doc-json-fences.test.ts index b67c0738af0..a6cd2742a4b 100644 --- a/ui/scripts/__tests__/check-doc-json-fences.test.ts +++ b/ui/scripts/__tests__/check-doc-json-fences.test.ts @@ -33,6 +33,7 @@ import { FRAGMENT_MARKER as SNIPPET_FRAGMENT_MARKER, FRAGMENT_MARKER_EXAMPLES as SNIPPET_FRAGMENT_MARKER_EXAMPLES, } from '../check-doc-snippet-types.mjs'; +import { labelPath } from '../skill-bundle-root.mjs'; import { packageReadmePages as familyPackageReadmes, ROOT_PAGES as FAMILY_ROOT_PAGES } from '../check-doc-component-types.mjs'; // The sibling gate over the skill bundle — imported HERE, never by the gate: the // gate spells its opt-in line itself, and this file pins the two spellings equal. @@ -433,7 +434,7 @@ describe('the scan surface — the published skill bundle (objectui#12204)', () // Non-vacuous: the bundle carries declared counter-examples; none would make the loop below pass over nothing. expect(pages.size, 'no declared fragment under the bundle').toBeGreaterThan(0); for (const doc of pages) { - const scan = scanSkillFences(fs.readFileSync(path.join(repoRoot, doc), 'utf8')); + const scan = scanSkillFences(fs.readFileSync(labelPath(repoRoot, doc), 'utf8')); expect(scan.orphans, `${doc}: orphaned opt-in lines`).toEqual([]); } }); diff --git a/ui/scripts/__tests__/check-governed-queue-guard.test.ts b/ui/scripts/__tests__/check-governed-queue-guard.test.ts index 3a4c65f68e6..ac528f726e7 100644 --- a/ui/scripts/__tests__/check-governed-queue-guard.test.ts +++ b/ui/scripts/__tests__/check-governed-queue-guard.test.ts @@ -11,6 +11,7 @@ import { GOVERNED_SURFACES, governedPathsIn, } from '../check-governed-queue-guard.mjs'; +import { skillBundleBase } from '../skill-bundle-root.mjs'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; @@ -115,8 +116,11 @@ describe('the governed surface is the ruled definition, and it is real', () => { // these types from the `.mjs` (allowJs, checkJs off), so the optional-chain // spelling is a real type error here rather than a stylistic preference. const target = 'prefix' in surface ? surface.prefix : surface.exact; + // The published `skills/` catalog is the monorepo root's since the cutover + // (decision C, `scripts/skill-bundle-root.mjs`), so that row is read from there. + const base = surface.id === 'skills-catalog' ? skillBundleBase(ROOT) : ROOT; expect( - fs.existsSync(path.join(ROOT, target)), + fs.existsSync(path.join(base, target)), `GOVERNED_SURFACES declares ${target}, which does not exist. A governed path nobody can ` + `edit is a rule about nothing; either the surface moved (update the register) or the ` + `entry was speculative (drop it).`, diff --git a/ui/scripts/__tests__/check-skill-examples.test.ts b/ui/scripts/__tests__/check-skill-examples.test.ts index 0bb315151e8..ac1bc83268c 100644 --- a/ui/scripts/__tests__/check-skill-examples.test.ts +++ b/ui/scripts/__tests__/check-skill-examples.test.ts @@ -43,6 +43,7 @@ import { rootDeclaredSpecifiers, specifierRoot, } from '../check-doc-snippet-types.mjs'; +import { labelPath } from '../skill-bundle-root.mjs'; /** * objectui#7359 — the test for `scripts/check-skill-examples.mjs`. @@ -309,7 +310,7 @@ describe('the corpus this gate governs', () => { const guides = listGuides(repoRoot) as string[]; const scans = guides.map((g) => ({ guide: g, - scan: scanSkillFences(fs.readFileSync(path.join(repoRoot, g), 'utf8')) as Scan, + scan: scanSkillFences(fs.readFileSync(labelPath(repoRoot, g), 'utf8')) as Scan, })); it('carries no orphan marker', () => { @@ -351,7 +352,7 @@ describe('the corpus this gate governs', () => { // reads as a marker and claims nothing. It would surface as an orphan only // if it matched the orphan scan's own literal — which it does not. for (const guide of guides) { - const source = fs.readFileSync(path.join(repoRoot, guide), 'utf8'); + const source = fs.readFileSync(labelPath(repoRoot, guide), 'utf8'); for (const [index, line] of source.split('\n').entries()) { if (!/os:check/.test(line)) continue; expect(line, `${guide}:${index + 1} carries a near-spelling of the marker`).toBe(MARKER); @@ -377,7 +378,7 @@ describe('the shrink-only floor on the marked population', () => { type Breach = { category: string; count: number; floor: number }; const corpusMarked = (listGuides(repoRoot) as string[]).flatMap( (guide) => - (scanSkillFences(fs.readFileSync(path.join(repoRoot, guide), 'utf8')) as Scan).fences.filter( + (scanSkillFences(fs.readFileSync(labelPath(repoRoot, guide), 'utf8')) as Scan).fences.filter( (f) => f.marked, ), ); @@ -835,7 +836,7 @@ describe('the ROOT BOUND, and its declared debt (objectui#7463 item 2)', () => { const lastColon = site.lastIndexOf(':'); const guide = site.slice(0, lastColon); const line = Number(site.slice(lastColon + 1)); - const source = fs.readFileSync(path.join(repoRoot, guide), 'utf8'); + const source = fs.readFileSync(labelPath(repoRoot, guide), 'utf8'); const { fences: all } = scanSkillFences(source) as { fences: { fenceLine: number; marked: boolean }[]; }; @@ -1279,7 +1280,7 @@ describe('the shadowed-published-type assertion (objectui#7646)', () => { const guides = new Map( (listGuides(repoRoot) as string[]).map((g) => [ g, - scanSkillFences(fs.readFileSync(path.join(repoRoot, g), 'utf8')) as Scan, + scanSkillFences(fs.readFileSync(labelPath(repoRoot, g), 'utf8')) as Scan, ]), ); for (const row of (KNOWN_SHADOWED_PUBLISHED_TYPES as Map).keys()) { diff --git a/ui/scripts/__tests__/doc-version-claims.test.ts b/ui/scripts/__tests__/doc-version-claims.test.ts index 4e6ca427faa..a3d0776902d 100644 --- a/ui/scripts/__tests__/doc-version-claims.test.ts +++ b/ui/scripts/__tests__/doc-version-claims.test.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { rootWorkflowsDir } from './helpers/root-ci'; +import { labelPath, skillBundleBase, skillsWalk } from '../skill-bundle-root.mjs'; /** * objectui#3697: nothing in this repository looks at a version literal written @@ -628,6 +629,15 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../ */ const SCAN_ROOTS = ['content/docs', 'packages/*/README.md', 'skills'] as const; +/** + * The `skills` root is the published bundle, which the cutover moved into the + * objectstack monorepo's catalog one level up (decision C, `skill-bundle-root.mjs`). + * Its files keep their `skills/objectui/...` labels, read from there. + */ +const skillsBase = skillBundleBase(repoRoot); +const relOf = (file: string): string => + path.relative(file.startsWith(path.join(skillsBase, 'skills') + path.sep) ? skillsBase : repoRoot, file); + const UNSCANNED_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.turbo', '.git']); /** @@ -783,7 +793,7 @@ function walk(dir: string, out: string[] = []): string[] { /** Expands one scan root. The only glob syntax is a whole segment that is `*`. */ function collectFiles(root: string): string[] { - const abs = path.join(repoRoot, root); + const abs = root === 'skills' ? skillsWalk(repoRoot).dir : path.join(repoRoot, root); if (!root.includes('*')) { try { if (fs.statSync(abs).isFile()) return [abs]; @@ -813,7 +823,7 @@ function collectFiles(root: string): string[] { * lines are not, because ```ts is not a claim. */ function claimsIn(file: string): Claim[] { - const rel = path.relative(repoRoot, file); + const rel = relOf(file); const lines = fs.readFileSync(file, 'utf8').split('\n'); const found: Claim[] = []; const headings: { level: number; text: string }[] = []; @@ -1149,6 +1159,12 @@ const KNOWN_CLAIMS: KnownClaim[] = [ // --- skills/objectui (objectui#4981) ----------------------------------------- // The agent-facing surface, unscanned until that card. See the header section for // the measurement and for why two of these blocks anchor and one does not. + { + file: 'skills/objectui/SKILL.md', + claim: '@objectstack/spec 18.x', + kind: 'anchored', + why: 'The `compatibility:` pin of the frontmatter. The bundle moved into the objectstack monorepo\'s published `skills/` catalog at the cutover (decision C, objectstack#25803), and that catalog REQUIRES the pin: the repo root\'s `scripts/check-skill-compatibility-version.mjs` reds a skill that declares none and reds this one when its major differs from `packages/spec/package.json`, whose `--fix` derives it. That gate is the anchor, run outside this tree, so no field here can name it. The sentence this replaced stated no version for the reason this file exists; it now states one that a gate re-derives.', + }, { file: 'skills/objectui/SKILL.md', claim: 'TypeScript 5.0', @@ -1379,7 +1395,7 @@ const readmeLines = new Map(); function linesOf(rel: string): string[] { let cached = readmeLines.get(rel); if (cached === undefined) { - cached = fs.readFileSync(path.join(repoRoot, rel), 'utf8').split('\n'); + cached = fs.readFileSync(labelPath(repoRoot, rel), 'utf8').split('\n'); readmeLines.set(rel, cached); } return cached; @@ -1470,7 +1486,7 @@ interface PeerBlock { * statements these blocks make, and nine of the other 16 were wrong. */ const peerBlocks: PeerBlock[] = scannedFiles - .map((abs) => path.relative(repoRoot, abs).split(path.sep).join('/')) + .map((abs) => relOf(abs).split(path.sep).join('/')) .flatMap((rel) => { const pkgDir = PACKAGE_README.exec(rel)?.[1]; if (pkgDir === undefined) return []; diff --git a/ui/scripts/__tests__/layered-read-declared-path-4016.test.ts b/ui/scripts/__tests__/layered-read-declared-path-4016.test.ts index 98a811b85e4..03daf719879 100644 --- a/ui/scripts/__tests__/layered-read-declared-path-4016.test.ts +++ b/ui/scripts/__tests__/layered-read-declared-path-4016.test.ts @@ -5,6 +5,8 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { skillsWalk } from '../skill-bundle-root.mjs'; + /** * objectui#4016 ratchet — nothing in this repo reaches the metadata LAYERED * projection by hanging a query flag on the ordinary item read. @@ -95,7 +97,9 @@ describe('objectui#4016 ratchet · nothing reaches the layered projection by que } }) .flatMap((src) => walkSources(src)); - const guides = walkMarkdown(path.join(repoRoot, 'skills')); + // The published bundle is in the monorepo's `skills/` catalog since the + // cutover (decision C, `scripts/skill-bundle-root.mjs`). + const guides = walkMarkdown(skillsWalk(repoRoot).dir); // A scan that scanned nothing passes for the wrong reason. Both floors sit // far below the real counts (1327 sources / 18 guides when this was diff --git a/ui/scripts/check-doc-json-fences.mjs b/ui/scripts/check-doc-json-fences.mjs index bad21809254..94405f9e450 100644 --- a/ui/scripts/check-doc-json-fences.mjs +++ b/ui/scripts/check-doc-json-fences.mjs @@ -236,6 +236,7 @@ import { } from './check-doc-snippet-types.mjs'; import { PACKAGE_READMES, ROOT_PAGES, packageReadmePages } from './check-doc-component-types.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +import { labelPath, skillBundleBase } from './skill-bundle-root.mjs'; import { closesFence, openFence } from './markdown-fence-scan.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -346,7 +347,9 @@ export const SCAN_SURFACE = Object.freeze([ Object.freeze({ leg: `every ${SKILL_PAGE_EXTENSIONS.join(' / ')} page under ${SKILLS_ROOT}/`, anchor: SKILLS_ANCHOR, - pages: (root) => pagesUnder(root, SKILLS_ROOT, SKILL_PAGE_EXTENSIONS), + // The bundle is in the monorepo's catalog since the cutover; its pages keep + // their `skills/objectui/...` labels (`skill-bundle-root.mjs`). + pages: (root) => pagesUnder(skillBundleBase(root), SKILLS_ROOT, SKILL_PAGE_EXTENSIONS), }), ]); @@ -608,7 +611,7 @@ export function analyze({ root = repoRoot, judge, tolerant, nodeTypes, parseJson let fenceCount = 0; for (const doc of documents) { - const { fences, markers } = scanJsonFences(readFileSync(join(root, doc), 'utf8')); + const { fences, markers } = scanJsonFences(readFileSync(labelPath(root, doc), 'utf8')); fenceCount += fences.length; for (const marker of markers) { if (!marker.consumed) { diff --git a/ui/scripts/check-shell-escape-residue.mjs b/ui/scripts/check-shell-escape-residue.mjs index b102300179b..ece4dc6b1fc 100644 --- a/ui/scripts/check-shell-escape-residue.mjs +++ b/ui/scripts/check-shell-escape-residue.mjs @@ -181,6 +181,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +import { labelPath, skillsWalk } from './skill-bundle-root.mjs'; import { scanFences } from './check-doc-fence-languages.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -351,16 +352,25 @@ export const RESIDUE_PATTERNS = Object.freeze([ */ export const FENCE_FLOOR = 400; +/** + * Where a declared root is on disk, and the base its documents are labelled from. + * `skills` is the published bundle, in the monorepo's catalog since the cutover + * (`skill-bundle-root.mjs`); its documents keep their `skills/objectui/...` labels. + */ +function locate(root, spec) { + return spec === 'skills' ? skillsWalk(root) : { dir: join(root, spec), base: root }; +} + /** Every document under one resolved root, repo-relative, in a stable order. */ export function listDocuments(root, spec, kind) { - const abs = join(root, spec); + const { dir: abs, base } = locate(root, spec); if (kind === 'file') return [spec]; const out = []; const walk = (dir) => { for (const entry of readdirSync(dir).sort()) { const p = join(dir, entry); if (statSync(p).isDirectory()) walk(p); - else if (DOC_EXTENSIONS.some((ext) => entry.endsWith(ext))) out.push(relative(root, p).split(sep).join('/')); + else if (DOC_EXTENSIONS.some((ext) => entry.endsWith(ext))) out.push(relative(base, p).split(sep).join('/')); } }; walk(abs); @@ -373,7 +383,7 @@ export function listDocuments(root, spec, kind) { * @returns {{ spec: string, kind: string, ok: boolean, problem: string | null }} */ export function resolveRoot(root, { spec, kind }) { - const abs = join(root, spec); + const abs = locate(root, spec).dir; if (!existsSync(abs)) return { spec, kind, ok: false, problem: 'does not exist' }; const isDir = statSync(abs).isDirectory(); if (kind === 'file' && isDir) return { spec, kind, ok: false, problem: 'is declared a file but is a directory' }; @@ -482,7 +492,7 @@ export function scan(root, { roots = SCAN_ROOTS, fenceFloor = FENCE_FLOOR, cover for (const rel of documents) { let source; try { - source = readFileSync(join(root, rel), 'utf8'); + source = readFileSync(labelPath(root, rel), 'utf8'); } catch { continue; // symlink, gitlink, unreadable -- nothing to judge } diff --git a/ui/scripts/check-skill-eval-tokens.mjs b/ui/scripts/check-skill-eval-tokens.mjs index 680ce36854d..91b4589e5a7 100644 --- a/ui/scripts/check-skill-eval-tokens.mjs +++ b/ui/scripts/check-skill-eval-tokens.mjs @@ -172,6 +172,7 @@ import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +import { skillsWalk } from './skill-bundle-root.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -291,19 +292,23 @@ function walk(dir, predicate, out = []) { export function listBundles(root) { const bundles = []; for (const scanRoot of SCAN_ROOTS) { - const base = join(root, scanRoot); - if (!existsSync(base) || !statSync(base).isDirectory()) continue; + // `skills` is the published bundle, in the monorepo's catalog since the + // cutover (`skill-bundle-root.mjs`); its labels stay `skills/objectui/...`. + const { dir: top, base: labelBase } = scanRoot === 'skills' ? skillsWalk(root) : { dir: join(root, scanRoot), base: root }; + if (!existsSync(top) || !statSync(top).isDirectory()) continue; const seen = new Set(); - for (const evalFile of walk(base, (name) => name.endsWith('.json'))) { + for (const evalFile of walk(top, (name) => name.endsWith('.json'))) { const evalsDir = dirname(evalFile); if (evalsDir.split(sep).pop() !== 'evals') continue; const bundleDir = dirname(evalsDir); if (seen.has(bundleDir)) continue; seen.add(bundleDir); - bundles.push(bundleDir); + bundles.push({ dir: bundleDir, base: labelBase }); } } - return bundles.sort().map((dir) => ({ dir, rel: relative(root, dir).split(sep).join('/') })); + return bundles + .sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0)) + .map(({ dir, base }) => ({ dir, base, rel: relative(base, dir).split(sep).join('/') })); } /** The row key. One file, one eval, one token — the unit a per-row fix removes. */ @@ -332,7 +337,7 @@ export function analyze({ root, baseline = KNOWN_UNTAUGHT_EVAL_TOKENS }) { for (const bundle of listBundles(root)) { const guideFiles = walk(bundle.dir, (name) => name.endsWith('.md')); const guides = guideFiles.map((full) => ({ - rel: relative(root, full).split(sep).join('/'), + rel: relative(bundle.base, full).split(sep).join('/'), text: readFileSync(full, 'utf8'), })); if (guides.length === 0) { @@ -348,7 +353,7 @@ export function analyze({ root, baseline = KNOWN_UNTAUGHT_EVAL_TOKENS }) { const record = { rel: bundle.rel, guides: guides.map((g) => g.rel), evalFiles: [], evalCount: 0, emptyMustContain: 0 }; for (const full of evalFiles) { - const evalPath = relative(root, full).split(sep).join('/'); + const evalPath = relative(bundle.base, full).split(sep).join('/'); record.evalFiles.push(evalPath); let doc; try { diff --git a/ui/scripts/check-skill-examples.mjs b/ui/scripts/check-skill-examples.mjs index 36aa5f085a7..d6e45920bc7 100644 --- a/ui/scripts/check-skill-examples.mjs +++ b/ui/scripts/check-skill-examples.mjs @@ -386,6 +386,7 @@ import { peerMappedCount, } from './check-doc-snippet-types.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +import { labelPath, skillsWalk } from './skill-bundle-root.mjs'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -1234,16 +1235,18 @@ export function scanSkillFences(source) { /** Every guide in the scan set, repo-relative, in a stable order. */ export function listGuides(root = repoRoot) { const out = []; - const walk = (dir) => { + const walk = (dir, base) => { for (const entry of readdirSync(dir).sort()) { const p = join(dir, entry); - if (statSync(p).isDirectory()) walk(p); - else if (entry.endsWith('.md')) out.push(relative(root, p).split(sep).join('/')); + if (statSync(p).isDirectory()) walk(p, base); + else if (entry.endsWith('.md')) out.push(relative(base, p).split(sep).join('/')); } }; for (const rootName of SCAN_ROOTS) { - const dir = join(root, rootName); - if (existsSync(dir)) walk(dir); + // `skills` is the published bundle, in the monorepo's catalog since the + // cutover (`skill-bundle-root.mjs`); its labels stay `skills/objectui/...`. + const { dir, base } = rootName === 'skills' ? skillsWalk(root) : { dir: join(root, rootName), base: root }; + if (existsSync(dir)) walk(dir, base); } return out; } @@ -1354,7 +1357,7 @@ export function analyze({ root = repoRoot, measure = false, baseline = KNOWN_BAR const guides = listGuides(root); const scans = new Map(); for (const guide of guides) { - scans.set(guide, scanSkillFences(readFileSync(join(root, guide), 'utf8'))); + scans.set(guide, scanSkillFences(readFileSync(labelPath(root, guide), 'utf8'))); } const findings = []; diff --git a/ui/scripts/check-skills-paths.mjs b/ui/scripts/check-skills-paths.mjs index 081d97c71c8..f68731f93ce 100644 --- a/ui/scripts/check-skills-paths.mjs +++ b/ui/scripts/check-skills-paths.mjs @@ -141,6 +141,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +import { skillsWalk } from './skill-bundle-root.mjs'; /** * The scan surface: markdown under each of these directories, relative to the @@ -301,12 +302,16 @@ export function scan(root, baseline = readBaseline(root)) { let resolved = 0; for (const scanRoot of SCAN_ROOTS) { - const rootFiles = markdownFiles(path.join(root, scanRoot)); + // The `skills` root is the published bundle, which the cutover moved into + // the monorepo's catalog one level up (`skill-bundle-root.mjs`). Its labels + // stay `skills/objectui/...`; its path tokens still name this tree's files. + const walk = scanRoot === 'skills' ? skillsWalk(root) : { dir: path.join(root, scanRoot), base: root }; + const rootFiles = markdownFiles(walk.dir); const checkedBefore = checked; const resolvedBefore = resolved; for (const full of rootFiles) { - const file = path.relative(root, full); + const file = path.relative(walk.base, full); for (const hit of extractPathTokens(readFileSync(full, 'utf8'))) { if (hit.pattern) { patterns.push({ file, ...hit }); diff --git a/ui/scripts/markdown-test-inputs.mjs b/ui/scripts/markdown-test-inputs.mjs index f1329c999df..351cb40682a 100644 --- a/ui/scripts/markdown-test-inputs.mjs +++ b/ui/scripts/markdown-test-inputs.mjs @@ -327,14 +327,13 @@ export const ADJUDICATED = new Map([ 'packages/components/src/__tests__/readme-shadcn-sync-categories.test.ts', { reads: ['packages/components/README_SHADCN_SYNC.md'] }, ], + // The three guides it reads moved with the published bundle into the objectstack + // monorepo's `skills/` catalog at the cutover (decision C, objectstack#25803), outside + // this tree, so this ledger can no longer name them; their trigger is the monorepo's. [ 'packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx', { - reads: [ - 'skills/objectui/guides/data-integration.md', - 'skills/objectui/guides/schema-expressions.md', - 'skills/objectui/rules/protocol.md', - ], + reads: [], walker: 'not-markdown: concatenates plugin package `.tsx` sources', }, ], @@ -412,10 +411,6 @@ export const ADJUDICATED = new Map([ 'packages/layout/src/__tests__/readme-sidebar-nav-example.test.ts', { reads: ['packages/layout/README.md'], notRead: ['README.md'] }, ], - [ - 'packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx', - { reads: ['skills/objectui/guides/auth-permissions.md'] }, - ], [ 'packages/plugin-calendar/src/readme-calendar-view-schema.test.ts', { reads: ['packages/plugin-calendar/README.md'] }, @@ -566,19 +561,6 @@ export const ADJUDICATED = new Map([ 'packages/types/src/__tests__/held-public-block-arms-10872.test.ts', { reads: ['content/docs/guide/quick-start.md'] }, ], - // objectui#11405. Reads every marked fence in the two guides that authors a - // bind-only `list`, with the marker gate's own `scanSkillFences`, and judges it - // through `safeValidateSchema` and the strict face -- so an edit to either guide - // is an edit to this test's input. - [ - 'packages/types/src/__tests__/list-bind-only-11405.test.ts', - { - reads: [ - 'skills/objectui/guides/data-integration.md', - 'skills/objectui/guides/schema-expressions.md', - ], - }, - ], // objectui#6152 round 9 -- the corpus census of `object-calendar` containers. // It walks the authored trees, reads every markdown document in them that // names `object-calendar`, and parses each fenced element's `calendar` diff --git a/ui/scripts/skill-bundle-root.mjs b/ui/scripts/skill-bundle-root.mjs new file mode 100644 index 00000000000..ee45b5a109a --- /dev/null +++ b/ui/scripts/skill-bundle-root.mjs @@ -0,0 +1,83 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * skill-bundle-root -- the ONE answer to "which directory are `skills/objectui/...` + * labels relative to?" + * + * import { SKILL_BUNDLE, skillBundleBase } from './skill-bundle-root.mjs'; + * const base = skillBundleBase(root); // root, or the monorepo root above it + * const guide = readFileSync(join(base, `${SKILL_BUNDLE}/rules/protocol.md`), 'utf8'); + * + * ## Why + * + * The published objectui skill bundle was `skills/objectui/` in this tree. At the + * cutover it moves into the objectstack monorepo's published catalog, the + * workspace root's `skills/` (decision C, objectstack#25752 item 17d; moved by + * objectstack#25803), one level above this tree. Every reader here still names + * the bundle's files `skills/objectui/...`; that label is now relative to the + * monorepo root rather than to `uiRoot()`. This module decides which. + * + * ## The rule + * + * `root` itself when `/skills/objectui` exists: a fixture that writes the + * bundle into a scratch root, or a tree that still carries it. Else the PARENT + * when `/skills/objectui` exists: the monorepo. Else `root`, so the + * reader's own empty-population refusal still names the path it expected. + * + * The probe is a directory under a root already known, never a marker search, so + * `scripts/check-ui-root-lookups.mjs` does not judge it. + * + * ⛔ Node builtins only, like `ui-root.mjs`. + */ + +import { existsSync } from 'node:fs'; +import path from 'node:path'; + +import { uiRoot } from './ui-root.mjs'; + +/** The bundle's directory, as every reader spells its labels. */ +export const SKILL_BUNDLE = 'skills/objectui'; + +/** + * The directory `SKILL_BUNDLE` labels are relative to, for a reader rooted at `root`. + * + * @param {string} [root] the reader's own root (`uiRoot()` unless a fixture hands one in) + * @returns {string} + */ +export function skillBundleBase(root = uiRoot()) { + if (existsSync(path.join(root, SKILL_BUNDLE))) return root; + const parent = path.dirname(root); + if (existsSync(path.join(parent, SKILL_BUNDLE))) return parent; + return root; +} + +/** + * The absolute path of a repo-relative label: a `skills/objectui/...` label from + * `skillBundleBase(root)`, every other label from `root`. + * + * @param {string} root + * @param {string} label + * @returns {string} + */ +export function labelPath(root, label) { + const base = label === SKILL_BUNDLE || label.startsWith(`${SKILL_BUNDLE}/`) ? skillBundleBase(root) : root; + return path.join(base, label); +} + +/** + * The walk a reader of this tree's `skills/` directory makes: the directory, and + * the base its labels are relative to. + * + * Here `skills/` held the bundle and nothing else, so walking it walked the + * bundle. The monorepo's `skills/` is the whole published catalog, every + * objectstack skill beside this one, so there the walk narrows to the bundle. + * + * @param {string} [root] + * @returns {{ dir: string, base: string }} + */ +export function skillsWalk(root = uiRoot()) { + const base = skillBundleBase(root); + return base === root + ? { dir: path.join(root, 'skills'), base: root } + : { dir: path.join(base, SKILL_BUNDLE), base }; +} From 7cd8bbe89eb19e714e89331c86037e768f049e7a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:38:21 +0000 Subject: [PATCH 2/7] wip(skills): retire the reserved word and the visibleOn alias from the objectui bundle Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- scripts/role-word-baseline.json | 3 +- skills/objectui/SKILL.md | 2 +- skills/objectui/guides/architecture.md | 2 +- skills/objectui/guides/auth-permissions.md | 37 +++++++++++-------- skills/objectui/guides/page-builder.md | 2 +- skills/objectui/guides/schema-expressions.md | 16 ++++---- skills/objectui/guides/testing.md | 4 +- skills/objectui/rules/protocol.md | 2 +- .../skill-guide-permission-config.test.tsx | 26 ++++++------- 9 files changed, 51 insertions(+), 43 deletions(-) diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json index 6e89b81606e..93019ec52c4 100644 --- a/scripts/role-word-baseline.json +++ b/scripts/role-word-baseline.json @@ -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 } diff --git a/skills/objectui/SKILL.md b/skills/objectui/SKILL.md index ac50a3f417c..ed54d87767b 100644 --- a/skills/objectui/SKILL.md +++ b/skills/objectui/SKILL.md @@ -30,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 diff --git a/skills/objectui/guides/architecture.md b/skills/objectui/guides/architecture.md index 41cd002aa76..2394860e4de 100644 --- a/skills/objectui/guides/architecture.md +++ b/skills/objectui/guides/architecture.md @@ -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 */ diff --git a/skills/objectui/guides/auth-permissions.md b/skills/objectui/guides/auth-permissions.md index 01d373dc606..baeb980c3db 100644 --- a/skills/objectui/guides/auth-permissions.md +++ b/skills/objectui/guides/auth-permissions.md @@ -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: + + +```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. @@ -110,7 +117,7 @@ import { LoginForm, RegisterForm, ForgotPasswordForm, UserMenu } from '@object-u // LoginForm includes email/password fields, social login buttons, forgot password link 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 ``` @@ -142,12 +149,12 @@ 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[] = [ @@ -155,7 +162,7 @@ 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: { @@ -163,7 +170,7 @@ const permissions: ObjectPermissionConfig[] = [ }, 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: '****' }], }, @@ -189,7 +196,7 @@ function App() { const { user } = useAuth(); return ( - + }> @@ -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. diff --git a/skills/objectui/guides/page-builder.md b/skills/objectui/guides/page-builder.md index d2b1d07709f..942bde08b7c 100644 --- a/skills/objectui/guides/page-builder.md +++ b/skills/objectui/guides/page-builder.md @@ -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. diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index 4423e5a0484..9b97e3c4a69 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -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. @@ -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 @@ -245,22 +245,22 @@ Each condition field has two forms — a shorthand and an `On` suffix: ```jsonc -{ "hidden": "${data.role !== 'admin'}" } // template expression +{ "hidden": "${data.status !== 'active'}" } // template expression ``` ```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:** ```json -{ "hidden": "${data.userRole !== 'admin'}" } +{ "hidden": "${data.userPosition !== 'admin'}" } ``` **Status-based:** @@ -278,7 +278,7 @@ The `On` variants accept raw expressions without `${}` wrapping — the entire s **Combined conditions:** ```json -{ "visibleOn": "data.isAuthenticated && data.permissions.canEdit" } +{ "visibleWhen": "data.isAuthenticated && data.permissions.canEdit" } ``` ### Disabled patterns diff --git a/skills/objectui/guides/testing.md b/skills/objectui/guides/testing.md index 040d76da47f..dd91068838e 100644 --- a/skills/objectui/guides/testing.md +++ b/skills/objectui/guides/testing.md @@ -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', () => { @@ -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', () => { diff --git a/skills/objectui/rules/protocol.md b/skills/objectui/rules/protocol.md index 09bd3502583..9826f3456e3 100644 --- a/skills/objectui/rules/protocol.md +++ b/skills/objectui/rules/protocol.md @@ -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"` | diff --git a/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx b/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx index 662a5f50053..7066bb9890e 100644 --- a/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx +++ b/ui/packages/permissions/src/__tests__/skill-guide-permission-config.test.tsx @@ -54,12 +54,12 @@ const skillsBase = path.resolve(repoRoot, '..'); const GUIDE = path.join(skillsBase, 'skills/objectui/guides/auth-permissions.md'); // --- 8< --- guide example begin --- 8< --- -// 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[] = [ @@ -67,7 +67,7 @@ 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: { @@ -75,7 +75,7 @@ const permissions: ObjectPermissionConfig[] = [ }, 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: '****' }], }, @@ -145,7 +145,7 @@ describe('objectui#4812 — the guide example is the shape the types declare', ( // the unguarded `objectConfig.roles[roleName]`. const asViewer = () => evaluatePermission({ - roles, + roles: definitions, permissions, userRoles: ['viewer'], user: { id: 'v', roles: ['viewer'] }, @@ -157,7 +157,7 @@ describe('objectui#4812 — the guide example is the shape the types declare', ( expect( evaluatePermission({ - roles, + roles: definitions, permissions, userRoles: ['admin'], user: { id: 'a', roles: ['admin'] }, @@ -169,7 +169,7 @@ describe('objectui#4812 — the guide example is the shape the types declare', ( it('returns the row filter verbatim — the package interpolates nothing', () => { const result = evaluatePermission({ - roles, + roles: definitions, permissions, userRoles: ['owner'], user: { id: 'u-1', roles: ['owner'] }, @@ -211,7 +211,7 @@ describe('objectui#4812 — a malformed config denies rather than crashing the r it('evaluator: non-public action on a roles-less config is a denial, not a TypeError', () => { const call = () => evaluatePermission({ - roles, + roles: definitions, permissions: ROLELESS, userRoles: ['viewer'], user: { id: 'v', roles: ['viewer'] }, @@ -226,7 +226,7 @@ describe('objectui#4812 — a malformed config denies rather than crashing the r it('evaluator: the publicAccess channel still answers first', () => { expect( evaluatePermission({ - roles, + roles: definitions, permissions: ROLELESS, userRoles: ['viewer'], user: { id: 'v', roles: ['viewer'] }, @@ -238,7 +238,7 @@ describe('objectui#4812 — a malformed config denies rather than crashing the r it('provider: every read survives a roles-less config', () => { render( - + , ); @@ -254,7 +254,7 @@ describe('objectui#4812 — a malformed config denies rather than crashing the r it('provider: a well-formed config is unaffected by the guard', () => { render( - + , ); From a6224c3f9ac722d4302759e2a5c15e836cea0389 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:39:00 +0000 Subject: [PATCH 3/7] wip(skills): stamp the objectui bundle's metadata.version from the workspace Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- skills/objectui/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectui/SKILL.md b/skills/objectui/SKILL.md index ed54d87767b..a30dc12c1cd 100644 --- a/skills/objectui/SKILL.md +++ b/skills/objectui/SKILL.md @@ -5,7 +5,7 @@ license: Apache-2.0 compatibility: Requires @objectstack/spec 18.x; each @object-ui/* package declares its exact @objectstack/spec range in its own manifest. metadata: 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 From 015ef308038ab9da4c98574c1dd03fb814861519 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:40:12 +0000 Subject: [PATCH 4/7] wip(skills): price the 27 objectui bundle files at their landed counts Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- scripts/check-skills-token-ratchet.mjs | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 60c216d5986..2811c044162 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -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], ]); /** From 67d2568397f1af473454ee2190810b2776f6a410 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:41:04 +0000 Subject: [PATCH 5/7] wip(skills): point the catalog README and the ui skills lock at the moved bundle Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- skills/README.md | 4 ++-- ui/skills-lock.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/README.md b/skills/README.md index 9e9b29eec34..2f255bd7e30 100644 --- a/skills/README.md +++ b/skills/README.md @@ -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. --- diff --git a/ui/skills-lock.json b/ui/skills-lock.json index d6f378796f4..1a9dae460a6 100644 --- a/ui/skills-lock.json +++ b/ui/skills-lock.json @@ -4,7 +4,7 @@ "objectui": { "source": "local", "sourceType": "local", - "path": "./skills/objectui" + "path": "../skills/objectui" }, "shadcn": { "source": "shadcn/ui", From ee5b12c339e1ba8e25a4fe20b7a25cf8f6f85d63 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:47:14 +0000 Subject: [PATCH 6/7] wip(skills): changeset for the objectui skill in @objectstack/skills Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- .changeset/25832-objectui-skill-in-catalog.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/25832-objectui-skill-in-catalog.md diff --git a/.changeset/25832-objectui-skill-in-catalog.md b/.changeset/25832-objectui-skill-in-catalog.md new file mode 100644 index 00000000000..c8f32a8896f --- /dev/null +++ b/.changeset/25832-objectui-skill-in-catalog.md @@ -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, 12 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 drops its `|| '#000000'` fallback (`value ?? ''`). From 12e779cd1d0ffa20bf44ec9e68d9d860e39722a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 10:47:23 +0000 Subject: [PATCH 7/7] wip(skills): changeset counts Claude-Session: https://claude.ai/code/session_01XB8hLKWX18hti3qKrQAJfm Co-authored-by: Claude --- .changeset/25832-objectui-skill-in-catalog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/25832-objectui-skill-in-catalog.md b/.changeset/25832-objectui-skill-in-catalog.md index c8f32a8896f..d75e5fef381 100644 --- a/.changeset/25832-objectui-skill-in-catalog.md +++ b/.changeset/25832-objectui-skill-in-catalog.md @@ -7,6 +7,6 @@ docs(skills): the `objectui` skill joins the published catalog, so `@objectstack Clause-②: no Metadata-protocol: none -- **What ships.** `skills/objectui/` (its `SKILL.md`, 11 guides, 3 rules, 12 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. +- **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 drops its `|| '#000000'` fallback (`value ?? ''`). +- **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.