diff --git a/content/docs/ai/skills-reference.mdx b/content/docs/ai/skills-reference.mdx index ca5368fc22..f356bc2dec 100644 --- a/content/docs/ai/skills-reference.mdx +++ b/content/docs/ai/skills-reference.mdx @@ -43,7 +43,7 @@ ObjectStack ships **11 skills** — one per authoring domain, plus process skill | # | Skill | Domain | Path | What it covers | | :--- | :--- | :--- | :--- | :--- | -| 1 | [Platform](#platform) | `platform` | `skills/objectstack-platform/` | Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, adapters, scaffolding), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). | +| 1 | [Platform](#platform) | `platform` | `skills/objectstack-platform/` | Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, scaffolding), declaring platform capabilities (`requires:` — which service plugins boot), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). | | 2 | [Data](#data) | `data` | `skills/objectstack-data/` | Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security, data `lifecycle` retention/TTL/rotation, metadata `protection` locks, and external / federated datasources (`defineDatasource`) — and the seeds (`defineSeed()`) that load fixtures and reference data alongside them. | | 3 | [Query](#query) | `query` | `skills/objectstack-query/` | Construct ObjectQL queries — filters, sorting, pagination, aggregation, relation expansion, and full-text search. | | 4 | [UI](#ui) | `ui` | `skills/objectstack-ui/` | Author ObjectStack UI metadata — Views (list/form/kanban/calendar/gantt), Apps (navigation), Pages (structured plus the HTML and React source-authoring tiers, ADR-0080/0081), Dashboards, Reports, Charts, Actions, and package Docs (`src/docs/*.md`). | @@ -61,13 +61,13 @@ ObjectStack ships **11 skills** — one per authoring domain, plus process skill **Domain** `platform` · **Path** `skills/objectstack-platform/` -Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, adapters, scaffolding), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). +Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, scaffolding), declaring platform capabilities (`requires:` — which service plugins boot), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). -Use when the user is writing `objectstack.config.ts`, building a plugin or driver, wiring a framework adapter, running `os` CLI commands, or planning deployment. +Use when the user is writing `objectstack.config.ts`, building a plugin or driver, turning a platform capability on, mounting the Hono HTTP layer, running `os` CLI commands, or planning deployment. Do not use for data schema design (see objectstack-data) or query patterns (see objectstack-query); data lifecycle hooks (beforeInsert / afterUpdate) belong in objectstack-data — only kernel / service-level events live here. -**Tags:** `project`, `defineStack`, `driver`, `adapter`, `plugin`, `kernel`, `service`, `DI`, `lifecycle`, `cli`, `deploy`, `ops` +**Tags:** `project`, `defineStack`, `driver`, `hono`, `plugin`, `kernel`, `service`, `requires`, `capability`, `DI`, `lifecycle`, `cli`, `deploy`, `ops` --- diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 4213bf9880..a45203b999 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -419,8 +419,6 @@ export const CEILINGS = new Map([ // objectstack-platform ['skills/objectstack-platform/evals/README.md', 514], ['skills/objectstack-platform/references/plugin-hooks.md', 2628], - ['skills/objectstack-platform/rules/bootstrap-patterns.md', 1093], - ['skills/objectstack-platform/rules/plugin-hooks-events.md', 985], ['skills/objectstack-platform/rules/plugin-lifecycle.md', 2408], ['skills/objectstack-platform/rules/service-registry.md', 2331], diff --git a/skills/README.md b/skills/README.md index 8111a0462e..0195e48301 100644 --- a/skills/README.md +++ b/skills/README.md @@ -29,7 +29,7 @@ apps too). | Skill | Domain | What it covers | |:------|:-------|:---------------| -| [Platform](./objectstack-platform/SKILL.md) | `platform` | Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, adapters, scaffolding), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). | +| [Platform](./objectstack-platform/SKILL.md) | `platform` | Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers project setup (`defineStack`, drivers, scaffolding), declaring platform capabilities (`requires:` — which service plugins boot), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). | | [Data](./objectstack-data/SKILL.md) | `data` | Design ObjectStack data schemas — objects, fields, field conditional rules, relationships, validations, indexes, lifecycle hooks, permissions, row-level security, data `lifecycle` retention/TTL/rotation, metadata `protection` locks, and external / federated datasources (`defineDatasource`) — and the seeds (`defineSeed()`) that load fixtures and reference data alongside them. | | [Query](./objectstack-query/SKILL.md) | `query` | Construct ObjectQL queries — filters, sorting, pagination, aggregation, relation expansion, and full-text search. | | [UI](./objectstack-ui/SKILL.md) | `ui` | Author ObjectStack UI metadata — Views (list/form/kanban/calendar/gantt), Apps (navigation), Pages (structured plus the HTML and React source-authoring tiers, ADR-0080/0081), Dashboards, Reports, Charts, Actions, and package Docs (`src/docs/*.md`). | diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 37287f4a6e..d5be48dbe4 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -2,12 +2,14 @@ name: objectstack-platform description: > Bootstrap, configure, extend, and operate ObjectStack runtimes. Covers - project setup (`defineStack`, drivers, adapters, scaffolding), plugin and + project setup (`defineStack`, drivers, scaffolding), declaring platform + capabilities (`requires:` — which service plugins boot), plugin and service development (PluginContext, DI, kernel hooks like `kernel:ready`), and operations (CLI commands, migrations, deployment, test harnesses via LiteKernel). Use when the user is writing - `objectstack.config.ts`, building a plugin or driver, wiring a framework - adapter, running `os` CLI commands, or planning deployment. Do not use for + `objectstack.config.ts`, building a plugin or driver, turning a platform + capability on, mounting the Hono HTTP layer, running `os` CLI commands, or + planning deployment. Do not use for data schema design (see objectstack-data) or query patterns (see objectstack-query); data lifecycle hooks (beforeInsert / afterUpdate) belong in objectstack-data — only kernel / service-level events live here. @@ -17,144 +19,15 @@ metadata: author: objectstack-ai version: "1.3" domain: platform - tags: project, defineStack, driver, adapter, plugin, kernel, service, DI, lifecycle, cli, deploy, ops + tags: project, defineStack, driver, hono, plugin, kernel, service, requires, capability, DI, lifecycle, cli, deploy, ops --- # Platform — ObjectStack Bootstrap & Plugin System -Expert instructions for two related concerns: - -1. **Project setup** — scaffolding new projects, writing - `objectstack.config.ts`, picking drivers and adapters, the runtime boot - sequence (the original "quickstart" skill). -2. **Plugin development** — building plugins, registering services, - wiring kernel hook / event handlers, working with `ObjectKernel` vs - `LiteKernel` (the original "plugin" skill). - -Both areas share the same `defineStack()` / kernel surface, which is why -they live in one skill. - ---- - -## When to Use This Skill - -- Creating a **new ObjectStack project** from scratch. -- Scaffolding a **new project** with the bundled `blank` template. -- Writing or modifying **`objectstack.config.ts`** (`defineStack()` config). -- Selecting a **database driver** (Memory, SQL, MongoDB). -- Integrating with a **web framework** (Hono via `@objectstack/hono` / `@objectstack/plugin-hono-server`). -- Understanding the **runtime boot sequence** and plugin loading order. -- Setting up **multi-app composition** with `composeStacks()`. -- Answering **"how do I get started?"** questions. - ---- - -## The App / Platform Boundary - -An ObjectStack app is a **simplified implementation of business features**: -author metadata under the platform's spec, guided by these skills, and check it -with the `os` commands ([Verify your work](#verify-your-work)). Never rebuild -what the platform owns. - -- **Business features belong in the app; capability belongs in the platform.** - A missing default, a wrong diagnostic, a shape the spec refuses — the fix is - upstream. Raise it there; do not compensate for it here. -- **A platform defect means waiting for the platform fix.** No defensive coding, - no shape tolerance, no hand-written predicate re-implementing a platform rule, - and never "land the half we can" — that spends the contract-first option and - leaves a decision half-executed. Record the block against the platform issue - so it is machine-visible; before resuming, confirm the version you **pin** - carries the fix (merged upstream ≠ present on your pin) and re-run the - defect's own reproduction. -- **A bad platform default is a default to fix**, not something to work around - at every call site. - ---- - -## The Template - -`blank` is the only template `create-objectstack` offers, and it is the default: - -- Bundled with `create-objectstack` — works offline, no network fetch -- One example object, in-memory driver, Hono server -- A clean slate to extend with the metadata this skill describes - -The five remote content templates (`todo`, `compliance`, `content`, -`contracts`, `procurement`) are **retired** — delisted from the marketplace and -no longer maintained. Do not recommend them; asking for one by name is refused. -Build domain metadata on top of `blank` instead. - -### Scaffolding Command - -```bash -# Interactive — prompts for a name -npx create-objectstack - -# Direct — skip prompts (blank is the default, and the only, template) -npx create-objectstack my-app -``` - ---- - -## Project Structure Conventions - -Every ObjectStack project follows this directory structure: - -``` -my-app/ -├── objectstack.config.ts # ← THE entry point — defineStack() -├── package.json -├── tsconfig.json -└── src/ - ├── objects/ # Business object definitions - │ ├── task.object.ts # → exports a single object - │ └── index.ts # → barrel: export * from './task.object' - ├── views/ # Optional: UI view definitions - │ ├── task.view.ts - │ └── index.ts - ├── apps/ # Optional: app definitions (nav, pages) - │ ├── main.app.ts - │ └── index.ts - ├── flows/ # Optional: automation flows - │ ├── task.flow.ts - │ └── index.ts - ├── actions/ # Optional: action definitions - │ ├── task.action.ts - │ └── index.ts - ├── dashboards/ # Optional: dashboards - ├── reports/ # Optional: reports - ├── datasets/ # Optional: analytics datasets - ├── i18n/ # Optional: translation bundles - └── handlers/ # Optional: runtime hook handlers -``` - -### Naming Conventions - -| Concept | Convention | Example | -|:--------|:-----------|:--------| -| File names | `{name}.{type}.ts` | `task.object.ts`, `main.app.ts` | -| Machine names | `snake_case` | `project_task`, `first_name` | -| Config keys | `camelCase` | `maxLength`, `defaultValue` | -| Barrel exports | `Object.values(imported)` | `objects: Object.values(objects)` | - ---- - -## CRM Blueprint (Reference Implementation) - -When scaffolding a production-style metadata app, align with this -CRM-style layout: - -| Blueprint Area | CRM Reference | What to Reuse | -|:--|:--|:--| -| Stack assembly | `objectstack.config.ts` | Single `defineStack()` root aggregating all metadata collections | -| By-type directories | `src/{objects,views,pages,actions,flows,...}` | Domain-per-folder layout with barrel exports | -| Typed aggregates | `src/*/index.ts` | Export `allFlows` / `allAgents` / `allSkills` typed arrays | -| Runtime capabilities | `requires: ['ai','automation','analytics','auth','ui','approvals','sharing']` | Declare opt-in capabilities explicitly | -| Security assembly | `src/profiles/*` + `src/sharing/*` | Compose `permissions` and `sharingRules` in stack root | -| Localization assembly | `src/translations/*` + `i18n` | Keep per-locale files and central bundle registration | - -Use this as the default template for “metadata application” requests before -simplifying to a blank-style minimal stack. +Two concerns over one `defineStack()` / kernel surface: **project setup** +(`objectstack.config.ts`, drivers, the boot sequence) and **plugin +development** (plugins, services, kernel hook / event handlers, +`ObjectKernel` vs `LiteKernel`). --- @@ -294,16 +167,17 @@ export default defineStack({ To disable (advanced — e.g., objects provided by another plugin): ```typescript -export default defineStack({ ... }, { strict: false }); +export default defineStack(config, { strict: false }); ``` ### Compile Artifact and Runtime Metadata Boundary ObjectStack runtime metadata must come from source files during local development or -from a compiled artifact. Do not configure a project runtime to read or write +from a compiled artifact. Do not configure an environment runtime to read or write metadata through its business database. ```bash +# the CLI ships an `os` binary; `objectstack` is an alias for it objectstack compile # -> dist/objectstack.json @@ -316,11 +190,11 @@ Runtime rule of thumb: |:--------|:----------------|:--------------| | Local dev | TS files or `dist/objectstack.json` | Business rows only | | Production runtime | Artifact API response | Business rows only | -| Control plane | Published JSON in metadata storage | Project revisions, history, overlays | +| Control plane | Published JSON in metadata storage | Environment revisions, history, overlays | When generating `objectstack.config.ts`, keep object names short and -`snake_case`; never set `tableName`, and do not add `sys_metadata` objects to a -project runtime manifest. +`snake_case`; never set `tableName`, and do not add `sys_metadata` objects to an +environment runtime manifest. --- @@ -335,15 +209,115 @@ manifest: { type: 'app', // app | plugin | driver | module | ... name: 'Acme CRM', // Human-readable display name description: 'CRM system', // Optional description + engines: { protocol: '^17' }, // Metadata-protocol major this app targets } ``` +**`manifest.engines.protocol`:** the metadata-protocol major the app is authored +against. `create-objectstack` stamps it into every project it emits (and all +three example apps carry it). The runtime checks it **before it loads +anything**, so a runtime outside the range refuses the app at the boundary with +the exact migration command instead of crashing later. Change it when you +deliberately move to a new protocol major — never to silence a mismatch. + **Object naming:** The object `name` is the canonical identifier and equals the physical table name. Embed any domain prefix directly in the name (e.g. `name: 'crm_account'`); the object-level `namespace` *field* is retired (ADR-0129 D3) and refused at load. **`manifest.namespace` (ADR-0048):** Optional, but **enforced once set**. When a package declares `manifest.namespace: 'crm'`, every `object.name` must start with `crm_` or `defineStack` errors (`validateNamespacePrefix` in `@objectstack/spec`); the legacy `__` double-underscore form is rejected, and `sys_`-prefixed names are platform-reserved and exempt. The namespace is also a package-ownership key — installing two packages that both claim `crm` fails with `NamespaceConflictError` (downgrade to a warning with `OS_METADATA_COLLISION=warn`). `os lint` additionally emits a non-fatal `naming/namespace-prefix` warning for bare-named UI/automation items (app, page, dashboard, flow, action, report, dataset) when a namespace is set. --- +## The App / Platform Boundary + +An ObjectStack app is a **simplified implementation of business features**: +author metadata under the platform's spec, guided by these skills, and check it +with the `os` commands ([Verify your work](#verify-your-work)). Never rebuild +what the platform owns. + +- **Business features belong in the app; capability belongs in the platform.** + A missing default, a wrong diagnostic, a shape the spec refuses — the fix is + upstream. Raise it there; do not compensate for it here. +- **A platform defect means waiting for the platform fix.** No defensive coding, + no shape tolerance, no hand-written predicate re-implementing a platform rule, + and never "land the half we can" — that spends the contract-first option and + leaves a decision half-executed. Record the block against the platform issue + so it is machine-visible; before resuming, confirm the version you **pin** + carries the fix (merged upstream ≠ present on your pin) and re-run the + defect's own reproduction. +- **A bad platform default is a default to fix**, not something to work around + at every call site. + +--- + +## The Template + +`blank` is the only template `create-objectstack` offers, and it is the default: + +- Bundled with `create-objectstack` — works offline, no network fetch +- One example object, and `requires: ['automation']` plus the three generic + connector executors in `plugins:`. The memory driver and the Hono server are + NOT in the file — the CLI auto-registers both at boot +- A clean slate to extend with the metadata this skill describes + +The five remote content templates (`todo`, `compliance`, `content`, +`contracts`, `procurement`) are **retired** — delisted from the marketplace and +no longer maintained. Do not recommend them; asking for one by name is refused. +Build domain metadata on top of `blank` instead. + +### Scaffolding Command + +```bash +# Interactive — prompts for a name +npx create-objectstack + +# Direct — skip prompts (blank is the default, and the only, template) +npx create-objectstack my-app +``` + +--- + +## Project Structure Conventions + +Every ObjectStack project follows this directory structure: + +``` +my-app/ +├── objectstack.config.ts # ← THE entry point — defineStack() +├── package.json +├── tsconfig.json +└── src/ + ├── objects/ # Business object definitions + │ ├── task.object.ts # → exports a single object + │ └── index.ts # → barrel: export * from './task.object' + ├── views/ # Optional: UI view definitions + │ ├── task.view.ts + │ └── index.ts + ├── apps/ # Optional: app definitions (nav, pages) + │ ├── main.app.ts + │ └── index.ts + ├── flows/ # Optional: automation flows + │ ├── task.flow.ts + │ └── index.ts + ├── actions/ # Optional: action definitions + │ ├── task.action.ts + │ └── index.ts + ├── dashboards/ # Optional: dashboards + ├── reports/ # Optional: reports + ├── datasets/ # Optional: analytics datasets + ├── i18n/ # Optional: translation bundles + └── handlers/ # Optional: runtime hook handlers +``` + +### Naming Conventions + +| Concept | Convention | Example | +|:--------|:-----------|:--------| +| File names | `{name}.{type}.ts` | `task.object.ts`, `main.app.ts` | +| Machine names | `snake_case` | `project_task`, `first_name` | +| Config keys | `camelCase` | `maxLength`, `defaultValue` | +| Barrel exports | `Object.values(imported)` | `objects: Object.values(objects)` | + +--- + ## Driver Selection Guide Drivers are the storage layer. Pick based on your environment: @@ -356,33 +330,28 @@ Drivers are the storage layer. Pick based on your environment: | **SQLite WASM** | `@objectstack/driver-sqlite-wasm` | Browser / WebContainer | `SqliteWasmDriver` — in-process, no server | | **Turso** | `@objectstack/driver-turso` | Edge, serverless, multi-tenant | **Cloud / EE only** — ships with the ObjectStack cloud / enterprise distribution, not the open framework. The open-core CLI recognizes `libsql://` URLs but **fails loudly** (`UnsupportedDriverError`) | -### Usage Pattern +### Wiring a driver — you usually do not -```typescript -import { DriverPlugin } from '@objectstack/runtime'; +Under `os dev` / `os serve` / `os start` the CLI **resolves the driver itself** +from the database URL and registers `DriverPlugin` for you (memory in dev, SQL +in prod). Do **not** put a driver in your config's `plugins:` array: no example +app does, and the `plugins:` key is for plugins the CLI cannot infer (connector +executors, your own plugins). Pick a driver by setting the DB URL, not by +writing code. -// Development (in-memory, zero config) -import { InMemoryDriver } from '@objectstack/driver-memory'; -new DriverPlugin(new InMemoryDriver()) +Construct `DriverPlugin` yourself only when **you** own the runtime — embedding +via `Runtime` / `ObjectKernel`, or a test that boots a kernel directly: -// Production (SQLite) +```typescript +import { DriverPlugin } from '@objectstack/runtime'; import { SqlDriver } from '@objectstack/driver-sql'; -new DriverPlugin(new SqlDriver({ - client: 'better-sqlite3', - connection: { filename: './data/app.db' }, - useNullAsDefault: true, -})) - -// Production (PostgreSQL) -new DriverPlugin(new SqlDriver({ - client: 'pg', - connection: process.env.DATABASE_URL, -})) + +new DriverPlugin(new SqlDriver({ client: 'pg', connection: process.env.DATABASE_URL })); ``` --- -## Adapter Selection Guide +## HTTP Layer (Hono) The HTTP layer is Hono-based. Two packages exist: @@ -462,6 +431,69 @@ CLI: `os serve` / `os dev` mode-dependent — dev hops to the next free port, production fails loudly. See [Ports & networking](#ports--networking). +### `requires:` — which service plugins boot + +Step 4's list is the fixed core. Every other service plugin is opt-in, and +`requires: [...]` on the stack root is what turns it on. The CLI expands each +token through the `CAPABILITY_PROVIDERS` registry in +`packages/cli/src/commands/serve.ts` — all 20 of its entries: + +| Token | Provider package | +|:--|:--| +| `automation` | `@objectstack/service-automation` — flows, and any declarative `connectors:` entry | +| `analytics` `cache` `storage` `queue` `job` `messaging` `realtime` `settings` `sms` | `@objectstack/service-` + the token | +| `marketplace` | `@objectstack/service-package` | +| `audit` `email` `sharing` `reports` `approvals` `webhooks` | `@objectstack/plugin-` + the token | +| `pinyin-search` | `@objectstack/plugin-pinyin-search` | +| `mcp` | `@objectstack/mcp` | +| `triggers` | `@objectstack/trigger-record-change`, plus `trigger-schedule` and `trigger-api`. **Pair it with `job`** — schedule and time-relative triggers run on the job service | + +The other eight tokens in the vocabulary are **not** in that map and do not +resolve through it: + +- **Tier-gated** — `ai`, `ai-studio`, `i18n`, `ui`, `auth` have no provider + entry; dedicated blocks in `serve.ts` `run()` open their tier instead + (`ai`/`ai-studio` through the intent-driven AI block, the other three through + their tier blocks). +- **Enterprise / cloud** — `hierarchy-security` has no open-edition provider and + ships in `@objectstack/security-enterprise`, loaded through `plugins[]`; + `ai-seat` and `governance` are resolved only by cloud's objectos-runtime. + +The authoritative list of all 28 is `PLATFORM_CAPABILITY_TOKENS` +(`@objectstack/spec`, `kernel/platform-capabilities.ts`) — an unknown token is +**rejected by `defineStack` at authoring time**, not at boot. + +Five rules that change what you write: + +- **Precedence:** `requires` › `tiers` › `--preset` › built-in default. An + explicit instance in `plugins:` always shadows capability resolution. +- **Declaring is a demand.** A capability YOU declared whose provider package is + absent is a hard boot error; one the platform auto-injects for you stays + best-effort (warn and continue). +- **`auth` implies `email`.** Auth callbacks (password reset, email + verification, magic link, invitation) need the mail service, so the CLI + appends `email` whenever `auth` is required. +- **Keep `automation` whenever `plugins:` lists a connector** — connector + executors register their provider factories with it, and without it they have + nowhere to register and boot fails. +- **Pair `triggers` with `job`.** `triggers` alone arms record-change triggers; + schedule and time-relative triggers run on the job service, so autolaunched + scheduled flows stay silent without `job`. + +### `onEnable` — where an app binds runtime code + +`objectstack.config.ts` may export `onEnable` beside its default stack. +`AppPlugin` invokes it during boot and hands the app live runtime handles: this +is the one seam where declarative metadata reaches imperative code (registering +action handlers, giving a job its data handle, provisioning a fixture +datasource). All three example apps use it. + +```typescript +export const onEnable = async (ctx: { ql: { registerAction: (...a: unknown[]) => void } }) => { + registerTaskActionHandlers(ctx.ql); +}; +``` + ### Plugin Loading Order Matters Plugins initialize in registration order. Key dependencies: @@ -497,74 +529,28 @@ const kernel = runtime.getKernel(); ## Multi-App Composition -Use `composeStacks()` to merge multiple apps into one runtime: - -```typescript -import { composeStacks, defineStack } from '@objectstack/spec'; -import CrmApp from './apps/crm/objectstack.config'; -import TodoApp from './apps/todo/objectstack.config'; - -const combined = composeStacks([CrmApp, TodoApp], { - objectConflict: 'error', // Throw on duplicate object names - manifest: 'last', // Use last stack's manifest -}); - -export default combined; -``` - -### Conflict Strategies - -| Strategy | Behavior | -|:---------|:---------| -| `'error'` (default) | Throw if two stacks define the same object name | -| `'override'` | Last stack wins — later definition replaces earlier | -| `'merge'` | Shallow-merge objects with same name (later fields win) | - -### Host Pattern (Plugins as AppPlugin) - -For a hosting environment where each app runs isolated: - -```typescript -import { Runtime, DriverPlugin, AppPlugin } from '@objectstack/runtime'; -import { ObjectQLPlugin } from '@objectstack/objectql'; -import { AuthPlugin } from '@objectstack/plugin-auth'; - -export default defineStack({ - manifest: { id: 'platform-host', type: 'app', version: '1.0.0', name: 'Platform' }, - plugins: [ - new ObjectQLPlugin(), - new DriverPlugin(new SqlDriver({ ... })), - new AuthPlugin({ secret: process.env.AUTH_SECRET }), - new AppPlugin(CrmApp), // contributes objects: crm_account, crm_lead, ... - new AppPlugin(TodoApp), // contributes objects: task, ... - ], -}); -``` +Host several apps in one runtime by registering an `AppPlugin` per app — this is +how real multi-app composition happens (`packages/cli/src/commands/serve.ts`). +Each app contributes its objects under their canonical `name`; names are +globally unique and equal the physical table name, so use them directly in +queries, hooks, formulas, and REST URLs. -Each app registers its objects by their canonical `name`. Object names are globally unique and equal the physical table name — use them directly in queries, hooks, formulas, and REST URLs. +A merge-at-authoring-time alternative, `composeStacks()`, exists in +`@objectstack/spec` (`stack.zod.ts`) with `objectConflict` / +`manifest` strategies. No app in this repo uses it — read the schema before +reaching for it. --- ## Seed Data -Declarative data loading for bootstrapping, demos, and testing: +The stack's `data:` collection is authored with `defineSeed()`, which +**objectstack-data** owns — go there for `externalId` matching, `env:` scoping, +and which keys are derived. In particular `object` is derived from the object +definition: never write it by hand, and never hand-write a raw +`data: [{ object: … }]` literal. -```typescript -export default defineStack({ - // ... objects, apps, etc. - data: [ - { - object: 'task', - mode: 'upsert', // 'upsert' | 'insert' | 'update' | 'ignore' | 'replace' - externalId: 'subject', // Idempotency key for upsert matching - records: [ - { subject: 'Learn ObjectStack', status: 'open', priority: 'high' }, - { subject: 'Build first app', status: 'open', priority: 'medium' }, - ], - }, - ], -}); -``` +`mode` decides what a seed run does to rows that already exist: | Mode | Behavior | |:-----|:---------| @@ -572,7 +558,7 @@ export default defineStack({ | `insert` | Always insert (fails on duplicate) | | `update` | Only update found records; ignore new ones | | `ignore` | Insert if not exists, skip otherwise | -| `replace` | Drop and re-insert all records | +| `replace` | ⚠️ **Data loss** — drops and re-inserts all records | --- @@ -623,43 +609,9 @@ A minimal but complete project from scratch: } ``` -**`src/objects/task.object.ts`**: - -```typescript -import { Field } from '@objectstack/spec/data'; - -export default { - name: 'task', - label: 'Task', - fields: { - title: Field.text({ label: 'Title', required: true }), - description: Field.textarea({ label: 'Description' }), - status: Field.select({ - label: 'Status', - options: [ - { label: 'Open', value: 'open' }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - defaultValue: 'open', - }), - priority: Field.select({ - label: 'Priority', - options: [ - { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, - { label: 'High', value: 'high' }, - ], - defaultValue: 'medium', - }), - due_date: Field.date({ label: 'Due Date' }), - }, - indexes: [ - { fields: ['status'] }, - { fields: ['due_date'] }, - ], -}; -``` +**`src/objects/task.object.ts`** — one `ObjectSchema.create({ … })` call. Field +types, `indexes:` and the rest of the object surface are **objectstack-data**'s; +the scaffolder's own `note.object.ts` is the shape to copy. **`src/objects/index.ts`**: ```typescript @@ -696,25 +648,13 @@ os dev --ui # Part 2 — Plugin Development & Kernel Extension -## When to Use This Skill - -- You are creating a **new plugin** (driver, server, service, app feature) -- You need to **register or consume services** via the DI container -- You are using the **hook/event system** for inter-plugin communication -- You need to choose between **ObjectKernel** and **LiteKernel** -- You are debugging **plugin loading order** or dependency resolution -- You need to configure **graceful shutdown**, timeouts, or health checks -- You are implementing **service factories** with lifecycle management - ---- - ## Quick Reference — Detailed Rules For comprehensive documentation with incorrect/correct examples: - **[Plugin Lifecycle](./rules/plugin-lifecycle.md)** — 3-phase lifecycle (init/start/destroy), execution order, complete examples - **[Service Registry](./rules/service-registry.md)** — DI container, factories, lifecycles (singleton/transient/scoped), core fallbacks -- **[Hooks & Events](./rules/plugin-hooks-events.md)** — Kernel hooks & events reference (record-level lifecycle hooks → [objectstack-data](../objectstack-data/SKILL.md)) +- **[Hooks & Events](./references/plugin-hooks.md)** — Kernel hooks & events reference (record-level lifecycle hooks → [objectstack-data](../objectstack-data/SKILL.md)) --- @@ -732,30 +672,15 @@ For comprehensive documentation with incorrect/correct examples: | **Core fallbacks** | Auto-injects in-memory fallbacks | Not available | | **Config validation** | Zod schema validation per plugin | Not available | -### Decision Guide +### A third answer: no kernel at all -``` -What environment are you targeting? -│ -├── Production server / full application? -│ └── ✅ ObjectKernel -│ • Full DI with factories and scopes -│ • Health monitoring and auto-recovery -│ • Graceful shutdown with timeout -│ • Startup failure rollback -│ -├── Serverless / edge (Cloudflare Workers, Deno Deploy)? -│ └── ✅ LiteKernel -│ • Minimal memory footprint -│ • Fast cold start -│ • No background health checks -│ -└── Unit tests (vitest)? - └── ✅ LiteKernel - • Simple setup, fast teardown - • No system requirement validation - • No shutdown signal handlers -``` +If the host only needs the **data engine** — query / CRUD / hooks / validation — +neither kernel is the answer. Import `ObjectQL` from +`@objectstack/objectql/core` (ADR-0076): no kernel, no `ObjectQLPlugin`, no +metadata-management layer, and the *same* `ObjectSchema.create({ … })` +definitions a full backend ships. `examples/embed-objectql` is the worked +example; it is the right shape for a thin, latency-sensitive host such as a +gateway. ### ObjectKernel Configuration @@ -862,7 +787,7 @@ Built-in kernel events: `kernel:ready`, `kernel:bootstrapped`, > registered for `'data:beforeInsert'` will register successfully and then > **silently never fire**. Kernel hooks are for platform lifecycle only. -See [rules/plugin-hooks-events.md](./rules/plugin-hooks-events.md) for the kernel event list, payloads, and patterns. +See [references/plugin-hooks.md](./references/plugin-hooks.md) for the kernel event list, payloads, and patterns. ### Logger @@ -941,12 +866,6 @@ const AuditPlugin: Plugin = { export default AuditPlugin; ``` -> To audit **record writes** (who inserted/updated which record), register -> engine lifecycle hooks instead — e.g. -> `ctx.getService('objectql').on('afterInsert', 'task', async (hookCtx) => …)` -> in `start()`, or the declarative `hooks:` collection. See -> **objectstack-data** for the engine hook contract. - --- ## Using Plugins @@ -965,7 +884,7 @@ await kernel.use(AuditPlugin); await kernel.bootstrap(); // Services are now available -const audit = kernel.getService('audit'); +const audit = kernel.getService('audit'); ``` --- @@ -1031,7 +950,7 @@ metadata is read-only and artifact/file backed: - Do **not** register `sys_metadata` or `sys_metadata_history` from an ObjectStack runtime plugin. Those persistence tables belong to the control plane. - (Exception: an *isolated project kernel* may opt into `sys_metadata` + (Exception: an *isolated environment kernel* may opt into `sys_metadata` hydration from its own DB — the general boundary otherwise stands.) - Do **not** call `MetadataManager.setDataEngine()` automatically from `MetadataPlugin.start()`. Project databases must contain business rows only. @@ -1055,30 +974,15 @@ await kernel.use(new MetadataPlugin({ ## Health Monitoring (ObjectKernel Only) -```typescript -const MyPlugin: Plugin & { healthCheck(): Promise } = { - name: 'com.example.db', - version: '1.0.0', - - async init(ctx) { /* ... */ }, +A plugin opts in by adding an `async healthCheck()` returning +`{ healthy: boolean; message?: string; details?: Record }` +(`PluginHealthStatus`, importable from `@objectstack/core`). Return `healthy: +false` rather than throwing. The kernel side is three calls: - async healthCheck() { - try { - await this.pool.query('SELECT 1'); - return { healthy: true, message: 'Database connected' }; - } catch (err) { - return { healthy: false, message: 'Database unreachable', details: { error: err.message } }; - } - }, -}; - -// Check health +```typescript const health = await kernel.checkPluginHealth('com.example.db'); const allHealth = await kernel.checkAllPluginsHealth(); - -// Get startup metrics -const metrics = kernel.getPluginMetrics(); -// Map — plugin name → startup duration in ms +const metrics = kernel.getPluginMetrics(); // Map ``` --- @@ -1106,9 +1010,8 @@ The live toggle surfaces are runtime configuration, not authored metadata: # Part 3 — Operations: CLI, Testing, Deployment -The `@objectstack/cli` package ships an `os` binary (alias: `objectstack`). -Every project gets the same command surface — `npm install` does not need to -be re-run when commands are added. +Every project gets the same `os` command surface — `npm install` does not need +to be re-run when commands are added. ## Daily-loop commands @@ -1222,6 +1125,17 @@ Port resolution is the same for `os dev` and `os start` (both spawn `os serve`): > (a local desired-state ledger) plus runtime-identity bind v2 (environment-less > self-hosted binding). +## Shipping a plugin + +Authoring stops at `kernel.use(plugin)`; these three make it distributable. +`manifest.type: 'plugin'` is what marks the package as one. + +| Command | What it does | +|:--|:--| +| `os plugin build [--out dist/x.osplugin]` | Bundle from `objectstack.plugin.json` into a reproducible ustar+gzip `ID-VERSION.osplugin` with per-file `sha256-` integrity | +| `os plugin sign` | Sign the built artifact | +| `os plugin publish` | Upload it to the catalog | + ## Testing pattern Use `LiteKernel` for unit / integration tests — it skips the cloud bits and @@ -1275,8 +1189,7 @@ describe('stack boot', () => { ## Health & observability - **Health endpoints:** the HTTP dispatcher exposes `GET /health` and - `GET /ready` under the API prefix (see "Health Monitoring" - earlier in this skill). + `GET /ready` under the API prefix. - **Logs:** plugins log via `ctx.logger`. Logger config is a **kernel construction** option, not a `defineStack` key: `new ObjectKernel({ logger: { level: 'info', format: 'json' } })`. @@ -1296,7 +1209,7 @@ describe('stack boot', () => { | LiteKernel test passes, ObjectKernel boot fails | Test missed a plugin the CLI auto-registers — compare your test's `use()` list against the `os dev` boot log | | Hot reload misses new objects | Barrel `src/objects/index.ts` not re-exporting — check the file | | Login works but **Setup / Studio missing** | The logged-in user isn't a platform admin. Setup/Studio are gated by `setup.access` / `studio.access` on `admin_full_access`, auto-granted only to the first registered **human** (`bootstrapPlatformAdmin`). The `usr_system` seed identity is skipped, so it can't steal the grant. Either sign up first (`--seed-admin`/`--fresh` does this) or check `sys_user_permission_set` for a cross-tenant (`organization_id = NULL`) `admin_full_access` link on your user. Don't edit nav code first. | -| A permission set is declared but grants nobody anything | Declaring a set is not assigning it. Assignment is a `sys_user_permission_set` row whose `permission_set_id` is the `sys_permission_set` **record id**, never the set's `name` — see "Assigning a permission set to a user" in **objectstack-data**. | +| A permission set is declared but grants nobody anything | Declaring a set is not assigning it — see "Assigning a permission set to a user" in **objectstack-data**. | --- diff --git a/skills/objectstack-platform/references/plugin-hooks.md b/skills/objectstack-platform/references/plugin-hooks.md index 0235266408..5e35fa507e 100644 --- a/skills/objectstack-platform/references/plugin-hooks.md +++ b/skills/objectstack-platform/references/plugin-hooks.md @@ -73,13 +73,14 @@ event names (`beforeFind`, `afterFind`, `beforeInsert`, `afterInsert`, Because `ctx.hook()` accepts any string, a handler registered for `'data:beforeInsert'` on the kernel bus registers "successfully" and then -**silently never fires**. If you need per-record validation, defaults, or -audit trails, go to the **objectstack-data** skill (`rules/hooks.md`, -`references/data-hooks.md`). +**silently never fires**. For per-record validation, defaults or audit +trails see [objectstack-data](../../objectstack-data/references/data-hooks.md). ## Custom Hooks -Create your own hooks following the convention: `{plugin-namespace}:{event-name}`. +Create your own hooks following the convention: `{plugin-namespace}:{event-name}` +— namespace required, lower-case: `auth:user-login`, `billing:invoice-paid`; +never `userLogin` (no namespace) or `auth:USER_LOGIN`. ```typescript // In your plugin @@ -235,20 +236,6 @@ ctx.hook('kernel:ready', () => console.log('B')); Handlers for one event run **sequentially and are awaited**; the next lifecycle phase does not begin until every handler settles. -## Hook Naming Conventions - -Follow the pattern: `{namespace}:{event-name}` - -**Good names:** -- `auth:user-login` -- `sales:opportunity-created` -- `billing:invoice-paid` -- `analytics:event-tracked` - -**Bad names:** -- `userLogin` (no namespace) -- `auth:USER_LOGIN` (use lowercase) - ## Testing Hooks `kernel.context` is **protected** — tests cannot call @@ -283,46 +270,21 @@ describe('Hook System', () => { await kernel.shutdown(); }); - - it('passes arguments to hook handler', async () => { - const kernel = new LiteKernel({ logger: { level: 'silent' } }); - let receivedData: any; - let probe!: PluginContext; - - kernel.use({ - name: 'test-plugin', - async init(ctx) { - probe = ctx; - ctx.hook('test:event', async (data) => { - receivedData = data; - }); - }, - }); - - await kernel.bootstrap(); - await probe.trigger('test:event', { foo: 'bar' }); - - expect(receivedData).toEqual({ foo: 'bar' }); - - await kernel.shutdown(); - }); }); ``` -## Best Practices - -1. **Use kernel hooks for platform lifecycle only** — boot, shutdown, - metadata reload, seed settle. Record lifecycle → engine hooks - (objectstack-data). -2. **Pick the right boot anchor** — route/service registration in - `kernel:ready`; reconcile/backfill in `kernel:bootstrapped`; socket - `listen()` in `kernel:listening`; seed-dependent reconcilers on - `app:seeded` (and make them idempotent). -3. **Catch errors unless you want to abort** — handler errors propagate and - can fail bootstrap. -4. **Rebind on `metadata:reloaded`** — anything derived from boot-time - metadata goes stale after a hot reload / publish. -5. **Use descriptive custom hook names** — follow `{namespace}:{event-name}`. -6. **Document custom hooks** — what they do, what arguments they pass. -7. **Test hook handlers** — trigger through a probe plugin's `PluginContext` - (never `kernel.context`, which is protected). +## Rules of thumb + +✅ Kernel hooks carry **platform lifecycle only**; record lifecycle is an +engine hook (objectstack-data). +✅ Pick the anchor by what must already have happened: route/service +registration → `kernel:ready`; reconcile/backfill → `kernel:bootstrapped`; +socket `listen()` → `kernel:listening`; seed-dependent (idempotent) work → +`app:seeded`. +✅ Catch handler errors unless you want to abort boot — they propagate. +✅ Rebind anything derived from boot-time metadata on `metadata:reloaded`. + +❌ Never subscribe to a `data:*` kernel event — it registers and never fires. +❌ Never `kernel.context.trigger(...)` in a test — `context` is protected; +trigger through a probe plugin's `PluginContext`. +❌ Never create a circular plugin dependency — both kernels throw. diff --git a/skills/objectstack-platform/rules/bootstrap-patterns.md b/skills/objectstack-platform/rules/bootstrap-patterns.md deleted file mode 100644 index 42b027f203..0000000000 --- a/skills/objectstack-platform/rules/bootstrap-patterns.md +++ /dev/null @@ -1,142 +0,0 @@ -# Project Bootstrap Patterns - -Guide for bootstrapping ObjectStack projects with defineStack(). - -## Basic Stack Configuration - -There is **no `driver:` key** on `defineStack()`. Drivers are plugins: wrap -them in `DriverPlugin` and put them in `plugins:`. - -A `driver:` entry is **not** a harmless no-op. The top level of a stack -definition rejects undeclared keys, so the stack does not load: - -``` -defineStack validation failed (1 issue): - - ✗ (root): Unrecognized key(s) on this stack definition: `driver`. … - The declared keys are enumerated by `ObjectStackDefinitionSchema` - (@objectstack/spec, stack.zod.ts) and in the stack-definition - reference docs. -``` - -TypeScript refuses it earlier still, at compile time: - -``` -error TS2353: Object literal may only specify known properties, -and 'driver' does not exist in type 'ObjectStackDefinitionInput'. -``` - -The `manifest` requires `id`, `version`, `type`, and `name` -(`ManifestSchema` is strict — a missing required field throws at -`defineStack()` time). - -```typescript -import { defineStack } from '@objectstack/spec'; -import { DriverPlugin } from '@objectstack/runtime'; -import { InMemoryDriver } from '@objectstack/driver-memory'; - -export default defineStack({ - manifest: { - id: 'com.example.crm', // required — reverse domain style - version: '1.0.0', // required — semver - type: 'app', // required - name: 'My CRM', // required — human-readable - description: 'Customer relationship management system', - }, - plugins: [ - new DriverPlugin(new InMemoryDriver()), - ], - objects: [ - /* ... */ - ], -}); -``` - -For production, swap the driver: - -```typescript -import { SqlDriver } from '@objectstack/driver-sql'; - -new DriverPlugin(new SqlDriver({ - client: 'pg', // 'pg' | 'mysql' | 'better-sqlite3' - connection: process.env.DATABASE_URL!, -})) -``` - -## Driver Selection - -| Driver | Package | Use Case | -|:-------|:--------|:---------| -| `InMemoryDriver` | `@objectstack/driver-memory` | Development, testing | -| `SqlDriver` | `@objectstack/driver-sql` | Production — PostgreSQL / MySQL / SQLite via Knex | -| `MongoDBDriver` | `@objectstack/driver-mongodb` | Production — document store | -| `SqliteWasmDriver` | `@objectstack/driver-sqlite-wasm` | Browser / WebContainer | -| `TursoDriver` | `@objectstack/driver-turso` | **Cloud / EE only** — not in the open framework; the open-core CLI fails loudly on `libsql://` URLs | - -## HTTP Layer - -| Package | Export | Use Case | -|:--------|:-------|:---------| -| `@objectstack/plugin-hono-server` | `HonoServerPlugin` | ObjectStack hosts the server (what `os dev` / `os serve` register) | -| `@objectstack/hono` | `createHonoApp({ kernel, prefix })` | Embed ObjectStack routes in your own Hono app / deploy target | - -There are no `@objectstack/adapter-*` packages. - -## Incorrect vs Correct - -### ❌ Incorrect — `driver:` Key (Rejected at Load) - -```typescript -export default defineStack({ - manifest: { id: 'com.example.app', version: '1.0.0', type: 'app', name: 'App' }, - driver: new DriverPlugin(new InMemoryDriver()), // ❌ Not a defineStack key — defineStack throws - objects: [/* ... */], -}); -``` - -### ✅ Correct — Driver as a Plugin - -```typescript -export default defineStack({ - manifest: { id: 'com.example.app', version: '1.0.0', type: 'app', name: 'App' }, - plugins: [new DriverPlugin(new InMemoryDriver())], // ✅ plugins: collection - objects: [/* ... */], -}); -``` - -### ❌ Incorrect — Incomplete Manifest - -```typescript -export default defineStack({ - manifest: { - name: 'my-crm', // ❌ Missing id, version, type — defineStack throws - }, - objects: [/* ... */], -}); -``` - -### ✅ Correct — Manifest with All Required Fields - -```typescript -export default defineStack({ - manifest: { - id: 'com.example.crm', - version: '1.0.0', - type: 'app', - name: 'My CRM', - }, - objects: [/* ... */], -}); -``` - -## Best Practices - -1. **Choose appropriate driver** — Match to deployment environment -2. **Use environment variables** — Don't hardcode credentials -3. **Complete the manifest** — `id`, `version`, `type`, `name` are required -4. **Put drivers in `plugins:`** — There is no `driver:` key -5. **Organize objects** — Group by domain/module - ---- - -See parent skill for complete documentation: [../SKILL.md](../SKILL.md) diff --git a/skills/objectstack-platform/rules/plugin-hooks-events.md b/skills/objectstack-platform/rules/plugin-hooks-events.md deleted file mode 100644 index ee8448eabe..0000000000 --- a/skills/objectstack-platform/rules/plugin-hooks-events.md +++ /dev/null @@ -1,108 +0,0 @@ -# Plugin Hooks & Events (Reference) - -> **Note:** This document is a compact pointer. Complete documentation lives in -> the canonical reference: -> **→ [references/plugin-hooks.md](../references/plugin-hooks.md)** - ---- - -## Quick Reference - -### Hook Registration - -Register hook handlers in `init()` or `start()`: - -```typescript -async init(ctx: PluginContext) { - // Kernel lifecycle hook - ctx.hook('kernel:ready', async () => { - ctx.logger.info('System ready'); - }); - - // Metadata hot-reload / publish announcement - ctx.hook('metadata:reloaded', async (payload?: { changed?: string[] }) => { - ctx.logger.info('Metadata reloaded', { changed: payload?.changed }); - }); -} -``` - -### Triggering Custom Events - -```typescript -async start(ctx: PluginContext) { - await ctx.trigger('my-plugin:initialized', { version: '1.0.0' }); -} -``` - -### Built-in Kernel Events - -| Event | Fires | Payload | -|:------|:------|:--------| -| `kernel:ready` | All plugins started (route/service registration phase) | (none) | -| `kernel:bootstrapped` | After every `kernel:ready` handler settles (reconcile/backfill anchor) | (none) | -| `kernel:listening` | After bootstrapped — HTTP servers open their socket here | (none) | -| `kernel:shutdown` | Shutdown begins | (none) | -| `app:seeded` | An app's inline seed attempt settled | `{ appId, overBudget }` | -| `metadata:reloaded` | Metadata hot-reload or publish (dev reload, publish-drafts) | `{ changed: string[], metadata? }` | -| `external.schema.drift` | Federated datasource schema drift detected | `{ datasource, object, diffs }` | - -There is no `metadata:changed` event — the real name is `metadata:reloaded`. - -### ⚠️ No `data:*` Kernel Events - -**Record-level lifecycle logic does not live on the kernel bus.** The engine -dispatches unprefixed events (`beforeInsert`, `afterUpdate`, …) with a single -`HookContext` argument — author them via the `hooks:` collection or -`ql.on('beforeInsert', 'task', async (ctx) => { … })` on the `objectql` -service. A kernel handler registered for `'data:beforeInsert'` registers -without error and **silently never fires**. Kernel hooks are for platform -lifecycle only → see **[objectstack-data](../../objectstack-data/SKILL.md)**. - -### Custom Hooks - -Follow the convention: `{plugin-namespace}:{event-name}` - -```typescript -// Trigger -await ctx.trigger('analytics:pageview', { path: '/dashboard', userId: '123' }); - -// Subscribe -ctx.hook('analytics:pageview', async (data) => { - console.log('Page viewed:', data.path); -}); -``` - ---- - -## Rules of Thumb - -✅ **DO:** -1. Use kernel hooks for **platform lifecycle only** (boot, shutdown, metadata - reload, seed settle) -2. Do reconcile/backfill work in `kernel:bootstrapped`, not `kernel:ready` -3. Open server sockets in `kernel:listening` -4. Catch handler errors unless you want to abort boot — errors propagate -5. Follow the naming convention: `{namespace}:{event-name}` - -❌ **DON'T:** -1. Don't subscribe to `data:*` kernel events — they don't exist and never fire -2. Don't call `kernel.context.trigger(...)` in tests — `context` is protected; - capture a `PluginContext` from a probe plugin -3. Don't create circular dependencies between plugins (both kernels throw) - ---- - -## Hook Execution Order - -Hooks execute in **registration order** within each event (which follows -plugin initialization order). Handlers run sequentially and are awaited. - ---- - -## See Also - -- **[references/plugin-hooks.md](../references/plugin-hooks.md)** — Full kernel hooks documentation (payloads, patterns, testing) -- **[objectstack-data/SKILL.md](../../objectstack-data/SKILL.md)** — Data lifecycle hooks (engine-level) -- **[objectstack-data/references/data-hooks.md](../../objectstack-data/references/data-hooks.md)** — The 8-event engine hook guide -- **[Plugin Lifecycle](./plugin-lifecycle.md)** — 3-phase plugin lifecycle -- **[Service Registry](./service-registry.md)** — DI container and service management diff --git a/skills/objectstack-platform/rules/plugin-lifecycle.md b/skills/objectstack-platform/rules/plugin-lifecycle.md index fe92399ed0..28d79d0a44 100644 --- a/skills/objectstack-platform/rules/plugin-lifecycle.md +++ b/skills/objectstack-platform/rules/plugin-lifecycle.md @@ -107,11 +107,6 @@ async init(ctx: PluginContext) { } ``` -> Kernel hooks cover **platform lifecycle only**. Record-level lifecycle -> (`beforeInsert` / `afterUpdate` / …) runs on the ObjectQL engine — there -> are no `data:*` kernel events (a handler for one would silently never -> fire). See **objectstack-data**. - ## Phase 2: start() — Active Behavior **Purpose:** Execute business logic that requires all services to be available. @@ -198,36 +193,12 @@ async start(ctx: PluginContext) { } ``` -### ❌ Incorrect — Using getService() in init() Without a Declared Dependency - -```typescript -const CachePlugin: Plugin = { - name: 'com.example.cache', - - async init(ctx: PluginContext) { - const db = ctx.getService('db-pool'); // ❌ May not exist yet - ctx.registerService('cache', new Cache(db)); - }, -}; -``` +### Declaring a dependency before `getService()` in `init()` -### ✅ Correct — Declare the Dependency, Then getService() in init() Is Safe - -```typescript -const CachePlugin: Plugin = { - name: 'com.example.cache', - dependencies: ['com.example.db'], // ✅ db plugin inits first - - async init(ctx: PluginContext) { - const db = ctx.getService('db-pool'); // ✅ Guaranteed registered - ctx.registerService('cache', new Cache(db)); - }, -}; -``` - -Never register `null` as a placeholder: `registerService` throws on a -duplicate key, so you can't re-register later, and `getService()` treats a -falsy value as missing and throws anyway. +`getService()` in `init()` is safe only for a plugin named in +`dependencies`. The worked ❌/✅ pair, and why `null` is never a valid +placeholder, are in +[service-registry.md](./service-registry.md#incorrect-vs-correct). ### ❌ Incorrect — Missing destroy() @@ -289,52 +260,3 @@ See the **Complete Plugin Example** (AuditPlugin) in plugin that registers a service in `init()`, subscribes to the `kernel:ready` / `metadata:reloaded` kernel events, and cleans up in `destroy()`. - -## Best Practices - -1. **Keep init() fast** — Only register services, don't do heavy work -2. **Use start() for connections** — Database, network, external services -3. **Always implement destroy()** — Release resources properly -4. **Declare dependencies explicitly** — Don't assume service availability -5. **Use try/catch in destroy()** — Cleanup should never throw -6. **Check service availability** — Use try/catch or hasService() for optional services -7. **Use ctx.logger** — Don't use console.log directly -8. **Avoid circular dependencies** — Design for linear dependency graph -9. **Version your plugin** — Use semantic versioning -10. **Use reverse domain names** — e.g., `com.example.plugin-name` - -## Testing Lifecycle - -```typescript -import { describe, it, expect } from 'vitest'; -import { LiteKernel } from '@objectstack/core'; -import MyPlugin from './plugin'; - -describe('MyPlugin Lifecycle', () => { - it('registers service in init phase', async () => { - const kernel = new LiteKernel({ logger: { level: 'silent' } }); - kernel.use(MyPlugin); - await kernel.bootstrap(); - - const service = kernel.getService('my-service'); - expect(service).toBeDefined(); - - await kernel.shutdown(); - }); - - it('cleans up in destroy phase', async () => { - const kernel = new LiteKernel(); - kernel.use(MyPlugin); - await kernel.bootstrap(); - - // Verify resource is created - const service = kernel.getService('my-service'); - expect(service.isConnected()).toBe(true); - - await kernel.shutdown(); - - // Verify resource is cleaned up - expect(service.isConnected()).toBe(false); - }); -}); -``` diff --git a/skills/objectstack-platform/rules/service-registry.md b/skills/objectstack-platform/rules/service-registry.md index 493f6a11ba..13b0901bbd 100644 --- a/skills/objectstack-platform/rules/service-registry.md +++ b/skills/objectstack-platform/rules/service-registry.md @@ -144,17 +144,9 @@ async start(ctx: PluginContext) { ## Well-Known Service Keys -| Service Key | Plugin Name | Package | -|:------------|:------------|:--------| -| `objectql` (also `data`) | `com.objectstack.engine.objectql` | `@objectstack/objectql` | -| `driver.*` | `com.objectstack.driver.*` | `@objectstack/driver-*` | -| `auth` | `com.objectstack.auth` | `@objectstack/plugin-auth` | -| `metadata` | `com.objectstack.metadata` | `@objectstack/metadata` | -| `realtime` | `com.objectstack.service.realtime` | `@objectstack/service-realtime` | -| `cache` | `com.objectstack.service.cache` | `@objectstack/service-cache` | - -The REST plugin (`com.objectstack.rest.api`, `@objectstack/rest`) registers -**no** service — there is no `rest` service key. +The full plugin-name -> service-key table (13 rows, including which plugins +register no service at all) lives in +[../SKILL.md](../SKILL.md#well-known-plugin-names--services). ## Core Fallback Injection @@ -266,54 +258,3 @@ async start(ctx: PluginContext) { ctx.replaceService('cache', new RedisCache(oldCache)); // ✅ Explicit replacement } ``` - -## Service Naming Conventions - -1. **Use lowercase, hyphen-separated names** — e.g., `db-pool`, `request-logger` -2. **Use namespaces for multiple instances** — e.g., `driver.postgres`, `driver.mysql` -3. **Use descriptive names** — e.g., `auth-service` not `as` -4. **Avoid abbreviations** — e.g., `database` not `db` (unless well-known like `db-pool`) - -## Testing Service Registration - -```typescript -import { describe, it, expect } from 'vitest'; -import { LiteKernel } from '@objectstack/core'; -import MyPlugin from './plugin'; - -describe('Service Registration', () => { - it('registers service in init phase', async () => { - const kernel = new LiteKernel(); - kernel.use(MyPlugin); - await kernel.bootstrap(); - - const service = kernel.getService('my-service'); - expect(service).toBeDefined(); - expect(service.name).toBe('MyService'); - - await kernel.shutdown(); - }); - - it('throws when service not found', async () => { - const kernel = new LiteKernel(); - await kernel.bootstrap(); - - expect(() => kernel.getService('non-existent')).toThrow(); - - await kernel.shutdown(); - }); -}); -``` - -## Best Practices - -1. **Register in init()** — All service registration in Phase 1 -2. **Consume in start()** — Use getService() only in Phase 2 -3. **Use try/catch for optional services** — Don't assume availability -4. **Use descriptive service keys** — Clear, namespaced names -5. **Declare dependencies** — Let kernel handle initialization order -6. **Use factories for lazy init** — Defer expensive creation -7. **Use scoped services for requests** — Request-specific contexts -8. **Don't register null** — Register a real instance or factory -9. **Use replaceService() explicitly** — Don't re-register -10. **Document your services** — What they do, what they depend on