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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/skills-stale-teaching-after-ui-merge.md
Original file line number Diff line number Diff line change
@@ -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 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.
17 changes: 16 additions & 1 deletion content/docs/ai/skills-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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. |

---

Expand Down Expand Up @@ -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
Expand Down
32 changes: 27 additions & 5 deletions packages/spec/scripts/build-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -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])) {
Expand All @@ -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() : '';
Expand Down Expand Up @@ -274,17 +281,32 @@ 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() {
const check = process.argv.includes('--check');

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);
Expand Down
6 changes: 4 additions & 2 deletions skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

<!-- END GENERATED: skills -->

Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@
"must_contain": ["beforeInsert", "beforeUpdate", "language: 'js'", "'api.read'", "ctx.api.object("],
"must_not_contain": ["handler:", "HookContext", "'http.fetch'"]
}
},
{
"id": 11,
"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:"]
}
}
]
}
32 changes: 32 additions & 0 deletions skills/objectstack-data/evals/objects-fields-relationships.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@
"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": []
}
}
]
}
1 change: 0 additions & 1 deletion skills/objectstack-data/references/data-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
3 changes: 2 additions & 1 deletion skills/objectstack-data/rules/datasources.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions skills/objectstack-data/rules/indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'] },
]
```

Expand Down
10 changes: 6 additions & 4 deletions skills/objectstack-data/rules/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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({}),
Expand Down Expand Up @@ -99,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
Expand Down
22 changes: 11 additions & 11 deletions skills/objectstack-data/rules/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
// Parent: Account
export default ObjectSchema.create({
name: 'account',
sharingModel: 'private',
fields: {
name: { type: 'text', required: true },
}
Expand All @@ -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,
},
}
});
Expand All @@ -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: {
Expand All @@ -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',
Expand Down Expand Up @@ -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 },
}
Expand All @@ -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 },
}
Expand All @@ -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',
Expand All @@ -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: {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Loading
Loading