From af6b6321ce576254866a910d09de45937d107798 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 13:58:19 +0000 Subject: [PATCH 1/2] docs(skills): stop teaching objectui as a separate repo, list it in the catalog index, repair dead anchors, author sharingModel in object examples - objectui README / page-builder and the catalog README point at ui/ in this repository and the pinned experimental_sync install. - build-skill-docs admits every skill directory that carries a SKILL.md and is not metadata.internal (the template-consistency predicate), so the generated index and docs page list objectui; regenerated. - data-hooks.md drops a link to a section the data SKILL.md no longer has; plugin-lifecycle.md and service-registry.md point at references/plugin-development.md, and the hand-written row count goes. - bootstrap.md pins 18.3.0. - Complete ObjectSchema.create examples in relationships.md, lifecycle.md and datasources.md author sharingModel; indexing.md names the injected organization_id tenant column; query teaches $empty; the upgrade roster says objectstack-pm-dispatch is not in the 18 catalog. - Eval cases for the above; paid by same-file deletions of restatement. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAjQXxnSqEopTLRDyHp --- .../skills-stale-teaching-after-ui-merge.md | 18 ++++++++++ content/docs/ai/skills-reference.mdx | 17 +++++++++- packages/spec/scripts/build-skill-docs.ts | 32 ++++++++++++++--- skills/README.md | 6 ++-- .../hooks-security-seeds-datasources.json | 10 ++++++ .../evals/objects-fields-relationships.json | 34 +++++++++++++++++++ .../objectstack-data/references/data-hooks.md | 1 - skills/objectstack-data/rules/datasources.md | 3 +- skills/objectstack-data/rules/indexing.md | 10 ++++-- skills/objectstack-data/rules/lifecycle.md | 8 +++-- .../objectstack-data/rules/relationships.md | 22 ++++++------ .../references/bootstrap.md | 10 +++--- .../rules/plugin-lifecycle.md | 2 +- .../rules/service-registry.md | 4 +-- skills/objectstack-query/SKILL.md | 6 ++++ .../evals/filters-pagination-search.json | 10 ++++++ skills/objectstack-query/rules/filters.md | 4 +-- skills/objectstack-upgrade/SKILL.md | 3 +- skills/objectui/README.md | 13 +++---- skills/objectui/guides/page-builder.md | 2 +- 20 files changed, 171 insertions(+), 44 deletions(-) create mode 100644 .changeset/skills-stale-teaching-after-ui-merge.md diff --git a/.changeset/skills-stale-teaching-after-ui-merge.md b/.changeset/skills-stale-teaching-after-ui-merge.md new file mode 100644 index 00000000000..56c69878bc6 --- /dev/null +++ b/.changeset/skills-stale-teaching-after-ui-merge.md @@ -0,0 +1,18 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): drop the "separate objectui repo" teaching, list objectui in the catalog index, repair three dead anchors, and author `sharingModel` in the object examples + +Clause-②: no +Metadata-protocol: none + +- `objectui`: the README installs the skill from `@objectstack/skills` with the pinned `experimental_sync` command the catalog README teaches, instead of `npx skills add objectstack-ai/objectui`, and its source link and `page-builder.md`'s `plugin-gantt` link point at `ui/` in this repository. The catalog README's "Related repositories" entry points there too. +- The generated skill index in `skills/README.md` and the docs Skills reference now list `objectui` (eleven skills). The generator admits every skill directory with a `SKILL.md` that is not `metadata.internal`, instead of only `objectstack-*` names. +- Dead links fixed: `data-hooks.md`'s reference to a hooks section the data `SKILL.md` no longer has is removed, and `plugin-lifecycle.md` / `service-registry.md` point at `references/plugin-development.md`, where the Complete Plugin Example and the Well-Known Plugin Names table live. +- `bootstrap.md`'s starting `package.json` pins `18.3.0`, the version every skill now carries. +- Every complete `ObjectSchema.create` example in `relationships.md`, `lifecycle.md` and `datasources.md` authors `sharingModel`, because a publish without one is refused with `security-owd-unset`. Master-detail children use `controlled_by_parent`, and the read-only external object uses `public_read`. +- `indexing.md`: the tenant column is `organization_id`, injected by `systemFields.tenant`, not an authored `tenant_id`. +- `objectstack-query`: teaches `$empty`, the is-empty-by-declared-type operator that the view filters `is_empty` / `is_not_empty` lower to. +- `objectstack-upgrade`: the `skills remove` roster says `objectstack-pm-dispatch` is not in the 18 catalog. +- New eval cases cover the authored `sharingModel`, the `organization_id` tenant index and `$empty`. The touched files stay inside their token ceilings by removing restatements in the same files. No schema, key or behaviour changes. diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index 69d601e6e41..44507dc1a8d 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -45,7 +45,7 @@ npm create objectstack@latest my-app {/* BEGIN GENERATED: skills (packages/spec/scripts/build-skill-docs.ts) — DO NOT EDIT */} -ObjectStack ships **10 skills** — one per authoring domain, plus process skills for how a project is delivered. Each is self-contained: an AI assistant loads only the ones a task needs. +ObjectStack ships **11 skills** — one per authoring domain, plus process skills for how a project is delivered. Each is self-contained: an AI assistant loads only the ones a task needs. ## Quick Reference @@ -61,6 +61,7 @@ ObjectStack ships **10 skills** — one per authoring domain, plus process skill | 8 | [i18n](#i18n) | `i18n` | `skills/objectstack-i18n/` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the source layout. | | 9 | [Formula](#formula) | `expression` | `skills/objectstack-formula/` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | 10 | [Upgrade](#upgrade) | `process` | `skills/objectstack-upgrade/` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | +| 11 | [ObjectUI](#objectui) | `ui` | `skills/objectui/` | Universal Server-Driven UI (SDUI) engine for building JSON-driven React interfaces with Shadcn design quality. | --- @@ -204,6 +205,20 @@ Do not use to author new metadata (the domain skills cover that), or to reconcil --- +### ObjectUI + +**Domain** `ui` · **Path** `skills/objectui/` + +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. + +**Tags:** `sdui`, `schema-renderer`, `json-ui`, `react`, `shadcn`, `tailwind`, `plugin`, `expression` + +--- + {/* END GENERATED: skills */} ## Skill anatomy diff --git a/packages/spec/scripts/build-skill-docs.ts b/packages/spec/scripts/build-skill-docs.ts index 754fdd0b385..0c115a65b8b 100644 --- a/packages/spec/scripts/build-skill-docs.ts +++ b/packages/spec/scripts/build-skill-docs.ts @@ -119,6 +119,7 @@ const DISPLAY: Array<{ name: string; label: string }> = [ { name: 'objectstack-i18n', label: 'i18n' }, { name: 'objectstack-formula', label: 'Formula' }, { name: 'objectstack-upgrade', label: 'Upgrade' }, + { name: 'objectui', label: 'ObjectUI' }, ]; // ── Frontmatter parser ─────────────────────────────────────────────────────── @@ -152,7 +153,12 @@ function parseFrontmatter(name: string, label: string): Skill { let tags: string[] = []; for (let i = 0; i < lines.length; i++) { - if (/^description:\s*>/.test(lines[i])) { + // A plain one-line scalar (`description: text…`) is the other shape a + // catalog entry uses (objectui's); the folded `>` block is read below. + const plain = lines[i].match(/^description:\s*([^>\s].*)$/); + if (plain) { + description = plain[1].replace(/\s+/g, ' ').trim(); + } else if (/^description:\s*>/.test(lines[i])) { const buf: string[] = []; i++; while (i < lines.length && /^\s+\S/.test(lines[i])) { @@ -178,8 +184,9 @@ function parseFrontmatter(name: string, label: string): Skill { // Split the description into summary / use-when / do-not-use, keeping each // clause verbatim (no paraphrasing — the frontmatter is the source). - const useIdx = description.search(/\bUse when\b/); - const notIdx = description.search(/\bDo not use\b/); + // objectui spells the clauses "Use for" / "Do NOT use"; the first match wins. + const useIdx = description.search(/\bUse (?:when|for)\b/); + const notIdx = description.search(/\bDo not use\b/i); const summary = (useIdx >= 0 ? description.slice(0, useIdx) : description).trim(); const useWhen = useIdx >= 0 ? description.slice(useIdx, notIdx >= 0 ? notIdx : undefined).trim() : ''; @@ -274,6 +281,15 @@ function spliceBlock(file: string, block: string, style: CommentStyle): string { return content.slice(0, b) + block + content.slice(e + end.length); } +/** + * The `metadata.internal` predicate `template-consistency.test.ts` applies to a + * SKILL.md frontmatter, spelled the same way so the two cannot disagree. + */ +function isMarkedInternal(raw: string): boolean { + const fm = /^---\n([\s\S]*?)\n---/.exec(raw)?.[1] ?? ''; + return /^metadata:\s*$/m.test(fm) && /^ +internal:\s*true\s*(#.*)?$/m.test(fm); +} + // ── Main ───────────────────────────────────────────────────────────────────── function main() { @@ -281,10 +297,16 @@ function main() { assertWatchHintsDeclareTheReadSurface(); - // Catalog ⇄ DISPLAY must be in lockstep. + // Catalog ⇄ DISPLAY must be in lockstep. A catalog entry is every child that + // carries a SKILL.md and is not `metadata.internal` — the boundary + // `template-consistency.test.ts` (create-objectstack) holds the published + // tree to — not a name prefix, which left `objectui` out of the index. const onDisk = fs .readdirSync(SKILLS_DIR) - .filter((d) => d.startsWith('objectstack-') && fs.existsSync(path.resolve(SKILLS_DIR, d, ENTRYPOINT_FILE))); + .filter((d) => { + const entry = path.resolve(SKILLS_DIR, d, ENTRYPOINT_FILE); + return fs.existsSync(entry) && !isMarkedInternal(fs.readFileSync(entry, 'utf-8')); + }); const configured = new Set(DISPLAY.map((d) => d.name)); const missing = onDisk.filter((d) => !configured.has(d)); const extra = DISPLAY.filter((d) => !onDisk.includes(d.name)).map((d) => d.name); diff --git a/skills/README.md b/skills/README.md index 14be5e05e67..5efad3d21d2 100644 --- a/skills/README.md +++ b/skills/README.md @@ -54,6 +54,7 @@ apps too). | [i18n](./objectstack-i18n/SKILL.md) | `i18n` | Author ObjectStack translation bundles — object/field labels, view text, app navigation strings, automation messages — and configure locale fallback, coverage reporting, and the source layout. | | [Formula](./objectstack-formula/SKILL.md) | `expression` | Author CEL expressions used across ObjectStack — formula fields, field conditional rules (`visibleWhen`, `readonlyWhen`, `requiredWhen`), validation / sharing / visibility predicates, flow conditions, and dynamic seed values. This is the companion skill that objectstack-data, -ui, -api and -automation each tell you to load alongside them. | | [Upgrade](./objectstack-upgrade/SKILL.md) | `process` | Upgrade an ObjectStack metadata project across a protocol major — run the deterministic conversion chain, then work the semantic residue the chain cannot express (intent choices, custom code on retired APIs, stale prose) to a decision with the project's owner, and finish with a green `validate` plus a human-readable upgrade report. | +| [ObjectUI](./objectui/SKILL.md) | `ui` | Universal Server-Driven UI (SDUI) engine for building JSON-driven React interfaces with Shadcn design quality. | @@ -133,6 +134,7 @@ A few common decision points where the right skill isn't obvious: --- -## Related repositories +## Related code -- [`../objectui`](https://github.com/objectstack-ai/objectui) — Studio UI (separate repo). +- [`ui/`](https://github.com/objectstack-ai/objectstack/tree/main/ui) — the ObjectUI + packages (`@object-ui/*`), in this repository. diff --git a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json index c28441d8693..14cd81315ec 100644 --- a/skills/objectstack-data/evals/hooks-security-seeds-datasources.json +++ b/skills/objectstack-data/evals/hooks-security-seeds-datasources.json @@ -90,6 +90,16 @@ "must_contain": ["INVALID_FIELD", "is_not_null"], "must_not_contain": ["starts_with"] } + }, + { + "id": 10, + "prompt": "Expose the remote `suppliers` table of our `warehouse` datasource as a read-only `ext_supplier` object every member can read.", + "expected_output": "A complete `ObjectSchema.create` with `datasource: 'warehouse'`, `external: { remoteName: 'suppliers' }` and an authored `sharingModel: 'public_read'` — a publish with none is refused (`security-owd-unset`), and on 'private' a member reads no row of a binding whose remote rows carry no owner.", + "files": [], + "assertions": { + "must_contain": ["ObjectSchema.create", "remoteName", "sharingModel: 'public_read'"], + "must_not_contain": ["columnName:"] + } } ] } diff --git a/skills/objectstack-data/evals/objects-fields-relationships.json b/skills/objectstack-data/evals/objects-fields-relationships.json index 6d5b00fcb29..64de825640d 100644 --- a/skills/objectstack-data/evals/objects-fields-relationships.json +++ b/skills/objectstack-data/evals/objects-fields-relationships.json @@ -135,6 +135,40 @@ "name: 'crm_account'" ] } + }, + { + "id": 8, + "prompt": "Model a `project` and its `milestone` rows; a milestone belongs to exactly one project and is deleted with it. Give me both object files.", + "expected_output": "Two complete `ObjectSchema.create` calls, each with an authored `sharingModel` (a publish without one is refused with `security-owd-unset`): `project` with `sharingModel: 'private'`, `milestone` with `sharingModel: 'controlled_by_parent'` and a `master_detail` field to `project` with `deleteBehavior: 'cascade'`.", + "files": [], + "assertions": { + "must_contain": [ + "ObjectSchema.create", + "sharingModel: 'private'", + "sharingModel: 'controlled_by_parent'", + "master_detail", + "deleteBehavior: 'cascade'" + ], + "must_not_contain": [ + "many_to_many", + "deleteBehavior: 'set_null'" + ] + } + }, + { + "id": 9, + "prompt": "Our app runs multi-tenant. Add an index on `ticket` for the list every agent opens: their organization's open tickets, newest first.", + "expected_output": "`indexes: [{ fields: ['organization_id', 'status', 'created_at'] }]` — the tenant column is `organization_id`, injected by `systemFields.tenant`, so it is indexed but not declared as a field; an index naming an undeclared `tenant_id` is refused. The platform already adds the single-column `organization_id` index on a multi-tenant host.", + "files": [], + "assertions": { + "must_contain": [ + "indexes", + "organization_id" + ], + "must_not_contain": [ + "tenant_id" + ] + } } ] } diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 73bfb04581f..ce0099d3474 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -958,7 +958,6 @@ body: { language: 'js', source: ` ## References - `node_modules/@objectstack/spec/src/data/hook.zod.ts` — Hook schema definition, HookContext interface -- [Project hooks pattern](../SKILL.md#lifecycle-hooks) — Hook integration in the data skill - [objectstack-platform/references/plugin-hooks.md](../../objectstack-platform/references/plugin-hooks.md) — plugin/kernel hooks (a different extension point) - [objectstack-automation](../../objectstack-automation/SKILL.md) — Flows and Workflows - [objectstack-platform/SKILL.md](../../objectstack-platform/SKILL.md) — hook test harnesses (vitest units, `LiteKernel`) diff --git a/skills/objectstack-data/rules/datasources.md b/skills/objectstack-data/rules/datasources.md index 9e0434719cb..d7417b22886 100644 --- a/skills/objectstack-data/rules/datasources.md +++ b/skills/objectstack-data/rules/datasources.md @@ -22,6 +22,7 @@ An object on an external datasource binds to its remote table via `external`: ObjectSchema.create({ name: 'ext_customer', datasource: 'warehouse', + sharingModel: 'public_read', external: { remoteName: 'customers', // remote TABLE (object name may differ) // remoteSchema: 'public', // optional schema/namespace (pg/mysql) @@ -32,7 +33,7 @@ ObjectSchema.create({ ``` - ❌ **`field.columnName` does not exist — on ANY object.** Authoring it is a - parse error everywhere, not only on a federated object. + parse error everywhere. - **Walled posture (`OS_TENANCY_POSTURE` `isolated`/`group`): every external object states its organization scope** — its own `organization_id` field (or `columnMap` onto it), `tenancy: { enabled: false }` (shared), or diff --git a/skills/objectstack-data/rules/indexing.md b/skills/objectstack-data/rules/indexing.md index d8d57c80504..1f74236fb48 100644 --- a/skills/objectstack-data/rules/indexing.md +++ b/skills/objectstack-data/rules/indexing.md @@ -206,10 +206,16 @@ indexes: [ ### Multi-Tenant Queries +The tenant column is `organization_id`, injected by `systemFields.tenant` — not a +field you declare (an index naming an undeclared `tenant_id` is refused). On a +multi-tenant host the platform adds its single-column index. A non-unique +composite may lead with it; a unique one says `unique: 'organization'` instead +(above): + ```typescript -// Query: WHERE tenant_id = X AND ... +// Query: WHERE organization_id = X AND ... indexes: [ - { fields: ['tenant_id', 'status', 'created_at'] }, + { fields: ['organization_id', 'status', 'created_at'] }, ] ``` diff --git a/skills/objectstack-data/rules/lifecycle.md b/skills/objectstack-data/rules/lifecycle.md index dc018468cce..004d0185d40 100644 --- a/skills/objectstack-data/rules/lifecycle.md +++ b/skills/objectstack-data/rules/lifecycle.md @@ -3,8 +3,7 @@ Guide for declaring how long an object's data lives and how its space is reclaimed (ADR-0057). Distinct from **lifecycle hooks** (`beforeInsert` / `afterUpdate` …), which run logic on -writes: this page covers the rows' retention, rotation and archival -— and, at its end, the save notice a hook hands the saver. +writes: this page covers the rows' retention, rotation and archival. ## Why This Exists @@ -35,6 +34,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; // ✅ Telemetry stream: bounded by a rotation window export const ApiCallLog = ObjectSchema.create({ name: 'api_call_log', + sharingModel: 'private', lifecycle: { class: 'telemetry', retention: { maxAge: '14d' }, // reap past 14 days (created_at) @@ -49,6 +49,7 @@ export const ApiCallLog = ObjectSchema.create({ // ✅ Ephemeral token: TTL on its own expiry field export const ImportTicket = ObjectSchema.create({ name: 'import_ticket', + sharingModel: 'private', lifecycle: { class: 'transient', ttl: { field: 'expires_at', expireAfter: '1d' }, // reap 1 day after expires_at @@ -61,10 +62,11 @@ export const ImportTicket = ObjectSchema.create({ // ✅ Compliance ledger: hot window, then cold storage export const ConsentLog = ObjectSchema.create({ name: 'consent_log', + sharingModel: 'private', lifecycle: { class: 'audit', retention: { maxAge: '90d' }, - archive: { after: '90d', to: 'archive', keep: '7y' }, // must: after === maxAge + archive: { after: '90d', to: 'archive', keep: '7y' }, }, fields: { subject: Field.text({}), diff --git a/skills/objectstack-data/rules/relationships.md b/skills/objectstack-data/rules/relationships.md index a7146cbb152..28ab027f182 100644 --- a/skills/objectstack-data/rules/relationships.md +++ b/skills/objectstack-data/rules/relationships.md @@ -22,6 +22,7 @@ // Parent: Account export default ObjectSchema.create({ name: 'account', + sharingModel: 'private', fields: { name: { type: 'text', required: true }, } @@ -30,12 +31,13 @@ export default ObjectSchema.create({ // Child: Contact (independent lifecycle) export default ObjectSchema.create({ name: 'contact', + sharingModel: 'private', fields: { first_name: { type: 'text', required: true }, account_id: { type: 'lookup', reference: 'account', - required: false, // Contact can exist without account + required: false, }, } }); @@ -47,6 +49,7 @@ export default ObjectSchema.create({ // Parent: Invoice export default ObjectSchema.create({ name: 'invoice', + sharingModel: 'private', fields: { invoice_number: { type: 'text', required: true }, total: { @@ -63,6 +66,7 @@ export default ObjectSchema.create({ // Child: Line Item (owned by parent) export default ObjectSchema.create({ name: 'invoice_line_item', + sharingModel: 'controlled_by_parent', fields: { invoice_id: { type: 'master_detail', @@ -117,6 +121,7 @@ an omitted `required` as `true`. // Side A: Project export default ObjectSchema.create({ name: 'project', + sharingModel: 'private', fields: { name: { type: 'text', required: true }, } @@ -125,6 +130,7 @@ export default ObjectSchema.create({ // Side B: Employee export default ObjectSchema.create({ name: 'employee', + sharingModel: 'private', fields: { name: { type: 'text', required: true }, } @@ -133,6 +139,7 @@ export default ObjectSchema.create({ // Junction: Project Assignment export default ObjectSchema.create({ name: 'project_assignment', + sharingModel: 'private', fields: { project_id: { type: 'lookup', @@ -159,6 +166,7 @@ export default ObjectSchema.create({ ```typescript export default ObjectSchema.create({ name: 'category', + sharingModel: 'private', fields: { name: { type: 'text', required: true }, parent_category: { @@ -235,6 +243,7 @@ atomic transaction**. // On the CHILD object's FK field: export default ObjectSchema.create({ name: 'invoice_line', + sharingModel: 'controlled_by_parent', fields: { invoice: { type: 'master_detail', @@ -393,13 +402,4 @@ The [master_detail](#one-to-many-master_detail) pattern above. ### ✅ Correct — Junction Object Pattern -```typescript -// Create explicit junction object with two lookup fields -export default ObjectSchema.create({ - name: 'post_tag', - fields: { - post_id: { type: 'lookup', reference: 'post', required: true }, - tag_id: { type: 'lookup', reference: 'tag', required: true }, - }, -}); -``` +The [junction object](#many-to-many-junction-object) pattern above. diff --git a/skills/objectstack-platform/references/bootstrap.md b/skills/objectstack-platform/references/bootstrap.md index dc8634f0b21..77bcee30a66 100644 --- a/skills/objectstack-platform/references/bootstrap.md +++ b/skills/objectstack-platform/references/bootstrap.md @@ -144,13 +144,13 @@ A minimal but complete project from scratch: "validate": "objectstack validate" }, "dependencies": { - "@objectstack/spec": "18.1.0", - "@objectstack/runtime": "18.1.0", - "@objectstack/driver-memory": "18.1.0", - "@objectstack/plugin-hono-server": "18.1.0" + "@objectstack/spec": "18.3.0", + "@objectstack/runtime": "18.3.0", + "@objectstack/driver-memory": "18.3.0", + "@objectstack/plugin-hono-server": "18.3.0" }, "devDependencies": { - "@objectstack/cli": "18.1.0", + "@objectstack/cli": "18.3.0", "typescript": "^5.3.0" } } diff --git a/skills/objectstack-platform/rules/plugin-lifecycle.md b/skills/objectstack-platform/rules/plugin-lifecycle.md index beeac14dfb8..add54d285ba 100644 --- a/skills/objectstack-platform/rules/plugin-lifecycle.md +++ b/skills/objectstack-platform/rules/plugin-lifecycle.md @@ -256,7 +256,7 @@ circular **service-factory** dependency graphs in ObjectKernel ## Complete Plugin Example See the **Complete Plugin Example** (AuditPlugin) in -[../SKILL.md](../SKILL.md#complete-plugin-example) — a full three-phase +[../references/plugin-development.md](../references/plugin-development.md#complete-plugin-example) — a full three-phase plugin that registers a service in `init()`, subscribes to the `kernel:ready` / `metadata:reloaded` kernel events, and cleans up in `destroy()`. diff --git a/skills/objectstack-platform/rules/service-registry.md b/skills/objectstack-platform/rules/service-registry.md index 13b0901bbd4..ab08b7f595e 100644 --- a/skills/objectstack-platform/rules/service-registry.md +++ b/skills/objectstack-platform/rules/service-registry.md @@ -144,9 +144,9 @@ async start(ctx: PluginContext) { ## Well-Known Service Keys -The full plugin-name -> service-key table (13 rows, including which plugins +The full plugin-name -> service-key table (including which plugins register no service at all) lives in -[../SKILL.md](../SKILL.md#well-known-plugin-names--services). +[../references/plugin-development.md](../references/plugin-development.md#well-known-plugin-names--services). ## Core Fallback Injection diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index c5e825ddf6b..e69454aa4b0 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -158,11 +158,17 @@ Full table, `$like` portability and the `$ilike` boundary: **[filter rules](./ru |:---------|:--------|:------------| | `$null` | Is null check | `IS NULL` / `IS NOT NULL` | | `$exists` | Has a value | `IS NOT NULL` / `IS NULL` | +| `$empty` | Is empty by the field's declared type | `IS NULL`, plus `''` (text types) or `[]` (multi-value) | ```typescript { where: { deleted_at: { $null: true } } } +{ where: { notes: { $empty: true } } } // false = has content ``` +A view filter's `is_empty` / `is_not_empty` lowers to `$empty`. A face that judges +by declared type refuses it on a column it holds no declaration for (the built-in +`id`, for one) — use `$null` there. + ### Logical Operators Combine conditions with `$and`, `$or`, and `$not`: diff --git a/skills/objectstack-query/evals/filters-pagination-search.json b/skills/objectstack-query/evals/filters-pagination-search.json index ee40fea8a31..99beada79fd 100644 --- a/skills/objectstack-query/evals/filters-pagination-search.json +++ b/skills/objectstack-query/evals/filters-pagination-search.json @@ -60,6 +60,16 @@ "must_contain": ["$now: true", "addDays", "$field: 'grace_days_offset'", "$lt"], "must_not_contain": ["subDays", "type: 'formula'"] } + }, + { + "id": 7, + "prompt": "Find tasks with no notes. `notes` is a textarea, and a note saved blank (an empty string) counts as none.", + "expected_output": "`engine.find('task', { where: { notes: { $empty: true } } })` — `$empty` judges by the field's declared type: on a text-like field it matches null AND the empty string, where `$null: true` misses ''. `$empty: false` is the complement (has content); a view filter's `is_empty` / `is_not_empty` lowers to it. Not `$regex` (retired) and not a hand-built `$or` of `$null` and `$eq: ''`.", + "files": [], + "assertions": { + "must_contain": ["$empty: true"], + "must_not_contain": ["$regex", "$options"] + } } ] } diff --git a/skills/objectstack-query/rules/filters.md b/skills/objectstack-query/rules/filters.md index 328aedfc6ae..4b86b2e0422 100644 --- a/skills/objectstack-query/rules/filters.md +++ b/skills/objectstack-query/rules/filters.md @@ -24,6 +24,7 @@ Comprehensive guide for building ObjectStack query filters. | String | `$ilike` | `$like`, case-blind | `{ code: { $ilike: 'prj-%' } }` | | Null | `$null` | `IS NULL` / `IS NOT NULL` | `{ deleted_at: { $null: true } }` | | Null | `$exists` | `IS NOT NULL` / `IS NULL` | `{ metadata: { $exists: true } }` | +| Null | `$empty` | `IS NULL`, `''` or `[]` by type | `{ notes: { $empty: true } }` | **Case sensitivity is part of the contract** (`filter.zod.ts`): "`$contains` / `$notContains` / `$startsWith` / `$endsWith` compare CASE-SENSITIVELY. @@ -127,14 +128,13 @@ only; use the comparison operators for numbers and dates. `where: { deleted_at: null }` works — a bare null lowers to `IS NULL` on both paths — but it reads as "equals null" and has no `IS NOT NULL` spelling. Write -`{ deleted_at: { $null: true } }` instead; `$null: false` is `IS NOT NULL`. +`{ deleted_at: { $null: true } }` instead. ## Date Filtering Patterns For **ad-hoc queries in application code**, compute the date yourself: ```typescript -// Records created in the last 7 days (compute date in application code) where: { created_at: { $gte: new Date('2025-01-01') } } diff --git a/skills/objectstack-upgrade/SKILL.md b/skills/objectstack-upgrade/SKILL.md index d8e80d2619b..bdb996f7ea8 100644 --- a/skills/objectstack-upgrade/SKILL.md +++ b/skills/objectstack-upgrade/SKILL.md @@ -118,7 +118,8 @@ deleting them on merge, is the usual arrangement — decide it with the owner. A 17.x project's skills came from `skills add`; their `skills-lock.json` entries read `sourceType: github`, so the sync skips them (exit 0) and the agent keeps -reading 17.x skill text. Remove them (a missing name is skipped), add +reading 17.x skill text. Remove them (a missing name is skipped; +`objectstack-pm-dispatch` is not in the 18 catalog, only an older install has it), add `@objectstack/skills` at the installed spec's version with the manager that installed the project, copy it: diff --git a/skills/objectui/README.md b/skills/objectui/README.md index a30202ffa7e..ab98d564e7f 100644 --- a/skills/objectui/README.md +++ b/skills/objectui/README.md @@ -1,17 +1,18 @@ # ObjectUI Skill -[![skills.sh](https://skills.sh/b/objectstack-ai/objectui)](https://skills.sh/objectstack-ai/objectui) - A single, tree-based Agent Skill consolidating all ObjectUI development knowledge, aligned with [shadcn/ui's skill structure](https://github.com/shadcn-ui/ui/tree/main/skills/shadcn). Works with Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, Gemini, Cline, and other agents on [skills.sh](https://skills.sh). ## Install +The skill ships in `@objectstack/skills` beside the `objectstack-*` skills. Install +that package as the [catalog README](https://github.com/objectstack-ai/objectstack/blob/main/skills/README.md) teaches (other package managers +and agents are there too): + ```bash -npx skills add objectstack-ai/objectui +pnpm add -D "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")" +npx skills@1.7.2 experimental_sync --agent claude-code --copy -y ``` -The CLI auto-detects your agent and writes the skill to the right location (e.g. `.claude/skills/objectui/` for Claude Code, `.github/copilot/skills/objectui/` for GitHub Copilot). See [skills.sh/docs](https://skills.sh/docs) for the full list of supported agents. - Once installed, the skill activates automatically when you describe ObjectUI work (schema-driven pages, plugins, `@object-ui/*` packages, etc.). No further configuration required. ## Layout @@ -61,4 +62,4 @@ The published packages this skill covers. Nothing checks this list against the ## Reference - shadcn/ui skill structure: https://github.com/shadcn-ui/ui/tree/main/skills/shadcn -- ObjectUI repository: https://github.com/objectstack-ai/objectui +- ObjectUI source: https://github.com/objectstack-ai/objectstack/tree/main/ui diff --git a/skills/objectui/guides/page-builder.md b/skills/objectui/guides/page-builder.md index 603d5a4d603..a7a8d19628d 100644 --- a/skills/objectui/guides/page-builder.md +++ b/skills/objectui/guides/page-builder.md @@ -300,7 +300,7 @@ When pages need heavy widgets (grids, forms, kanbans, charts), import the plugin Those three `gantt` keys are the required ones; every other option — the tree, dependency, baseline, resource-view, quick-filter, working-calendar and read-only surfaces — is optional and documented in -[`@object-ui/plugin-gantt`'s README](https://github.com/objectstack-ai/objectui/blob/main/packages/plugin-gantt/README.md). +[`@object-ui/plugin-gantt`'s README](https://github.com/objectstack-ai/objectstack/tree/main/ui/packages/plugin-gantt). A flat `startDateField` / `endDateField` / `titleField` on the node is refused by name; the refusal points at `properties.gantt.KEY`, the block's one home. From bc879f054dff61f4a3f6c9217e40d2d30396ed2c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Nov 2026 15:03:54 +0000 Subject: [PATCH 2/2] docs(skills): restate the archive/maxAge equality, add objectui to the 17.x remove roster, loosen two eval exclusions - lifecycle.md: the archive-window counter-example now says archive.after must equal retention.maxAge (the parse-time rule in object.zod.ts). - objectstack-upgrade: a 17.x project installed objectui from objectstack-ai/objectui, so the skills remove roster names it too. - Two new eval cases no longer fail an answer that warns against tenant_id or $regex/$options by name. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAjQXxnSqEopTLRDyHp --- .changeset/skills-stale-teaching-after-ui-merge.md | 2 +- .../objectstack-data/evals/objects-fields-relationships.json | 4 +--- skills/objectstack-data/rules/lifecycle.md | 2 +- skills/objectstack-query/evals/filters-pagination-search.json | 2 +- skills/objectstack-upgrade/SKILL.md | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.changeset/skills-stale-teaching-after-ui-merge.md b/.changeset/skills-stale-teaching-after-ui-merge.md index 56c69878bc6..754f4b05cd0 100644 --- a/.changeset/skills-stale-teaching-after-ui-merge.md +++ b/.changeset/skills-stale-teaching-after-ui-merge.md @@ -14,5 +14,5 @@ Metadata-protocol: none - Every complete `ObjectSchema.create` example in `relationships.md`, `lifecycle.md` and `datasources.md` authors `sharingModel`, because a publish without one is refused with `security-owd-unset`. Master-detail children use `controlled_by_parent`, and the read-only external object uses `public_read`. - `indexing.md`: the tenant column is `organization_id`, injected by `systemFields.tenant`, not an authored `tenant_id`. - `objectstack-query`: teaches `$empty`, the is-empty-by-declared-type operator that the view filters `is_empty` / `is_not_empty` lower to. -- `objectstack-upgrade`: the `skills remove` roster says `objectstack-pm-dispatch` is not in the 18 catalog. +- `objectstack-upgrade`: the `skills remove` roster also removes a 17.x `objectui` install (sourced from `objectstack-ai/objectui`), and says `objectstack-pm-dispatch` is not in the 18 catalog. - New eval cases cover the authored `sharingModel`, the `organization_id` tenant index and `$empty`. The touched files stay inside their token ceilings by removing restatements in the same files. No schema, key or behaviour changes. diff --git a/skills/objectstack-data/evals/objects-fields-relationships.json b/skills/objectstack-data/evals/objects-fields-relationships.json index 64de825640d..935d7c4d79f 100644 --- a/skills/objectstack-data/evals/objects-fields-relationships.json +++ b/skills/objectstack-data/evals/objects-fields-relationships.json @@ -165,9 +165,7 @@ "indexes", "organization_id" ], - "must_not_contain": [ - "tenant_id" - ] + "must_not_contain": [] } } ] diff --git a/skills/objectstack-data/rules/lifecycle.md b/skills/objectstack-data/rules/lifecycle.md index 004d0185d40..95c75b593a5 100644 --- a/skills/objectstack-data/rules/lifecycle.md +++ b/skills/objectstack-data/rules/lifecycle.md @@ -101,7 +101,7 @@ lifecycle: { class: 'telemetry' } // ❌ Policies on record-class — business truth is permanent lifecycle: { class: 'record', retention: { maxAge: '30d' } } -// ❌ Archive window detached from the hot window +// ❌ archive.after must equal retention.maxAge lifecycle: { class: 'audit', retention: { maxAge: '90d' }, archive: { after: '30d', to: 'archive' } } // ❌ Free-form durations diff --git a/skills/objectstack-query/evals/filters-pagination-search.json b/skills/objectstack-query/evals/filters-pagination-search.json index 99beada79fd..c5a4761cc25 100644 --- a/skills/objectstack-query/evals/filters-pagination-search.json +++ b/skills/objectstack-query/evals/filters-pagination-search.json @@ -68,7 +68,7 @@ "files": [], "assertions": { "must_contain": ["$empty: true"], - "must_not_contain": ["$regex", "$options"] + "must_not_contain": [] } } ] diff --git a/skills/objectstack-upgrade/SKILL.md b/skills/objectstack-upgrade/SKILL.md index bdb996f7ea8..a485e335709 100644 --- a/skills/objectstack-upgrade/SKILL.md +++ b/skills/objectstack-upgrade/SKILL.md @@ -126,7 +126,7 @@ installed the project, copy it: ```bash npx skills remove objectstack-ai objectstack-api objectstack-automation \ objectstack-data objectstack-formula objectstack-i18n objectstack-platform \ - objectstack-pm-dispatch objectstack-query objectstack-ui objectstack-upgrade -y + objectstack-pm-dispatch objectstack-query objectstack-ui objectstack-upgrade objectui -y pnpm add -D "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")" # npm: npm install --save-dev "@objectstack/skills@…" · yarn: yarn add -D "@objectstack/skills@…" npx skills@1.7.2 experimental_sync --agent claude-code --copy -y