diff --git a/.changeset/prose-example-gate-covers-docs.md b/.changeset/prose-example-gate-covers-docs.md new file mode 100644 index 0000000000..792fd1b152 --- /dev/null +++ b/.changeset/prose-example-gate-covers-docs.md @@ -0,0 +1,47 @@ +--- +"@objectstack/spec": patch +--- + +feat(spec): the example type-check gate now covers `content/docs`, not just `skills/` + +`check:skill-examples` compiles the TypeScript in prose against the built spec, so +an example that stops compiling fails CI instead of quietly teaching code that no +longer works. It does its job — it caught the broken `defineTool` example when +`tool.requiresConfirmation` was removed (#3715). + +But it only ever walked `skills/`: + +| Tree | files with `ts` blocks | compiled | +|---|---|---| +| `skills/` | 9 | **9** | +| `content/docs/` (hand-written) | 124 | **0** | + +The identical break in a docs page would have shipped. Docs examples are copied +verbatim by humans and AI exactly like skill examples, so a gate covering a +fraction of the surface it appears to cover reads as coverage — the same shape as +the stale-evidence and orphan-proof warnings fixed in #3857 / #3868. + +The walker is now a `SOURCE_ROOTS` list, and this lands the first batch: **164 +docs blocks across 63 pages, taking the gate from 32 to 196 checked examples**. +`content/docs/references/` is excluded — `build-docs.ts` regenerates it from the +schemas, so it cannot drift independently of its source. + +**The marker is now per-format.** MDX has no HTML comments: `` in +a `.mdx` fails the fumadocs build outright — *"Unexpected character `!`… to create +a comment in MDX, use `{/* text */}`"*. Caught by building the docs site, after +the first attempt broke 60+ pages. `skills/**/*.md` keeps ``; +`content/docs/**/*.mdx` uses `{/* os:check */}`. Both spellings are recognised for +**orphan** detection, so a wrong-format marker fails loudly rather than silently +checking nothing — the existing guard's philosophy, extended to the new failure +mode this change introduces. + +The batch was measured rather than guessed: marking all 780 docs blocks and +compiling showed which are self-contained. One subtlety worth recording — a block +that "passes" inside a 780-file program can be leaning on globals declared by +*other* blocks (a file with no import/export is a global script), so the set was +converged by recompiling and dropping newly-failing blocks until green. 164 stand +on their own. + +The remaining blocks are mostly fragments (a `columns: [...]` subtree), which the +gate's opt-in design already anticipates. Whether any are genuine rot is worth a +follow-up now that the machinery reaches them. diff --git a/content/docs/ai/actions-as-tools.mdx b/content/docs/ai/actions-as-tools.mdx index 4fe63247ea..1396e2ae9e 100644 --- a/content/docs/ai/actions-as-tools.mdx +++ b/content/docs/ai/actions-as-tools.mdx @@ -70,6 +70,7 @@ Add an optional `ai:` block to give the model a precise, LLM-facing description spec; `list_actions` surfaces `ai.description` to the model, falling back to the UI `label` when it is absent. +{/* os:check */} ```typescript export const triageCaseAction = { name: 'triage_case', diff --git a/content/docs/ai/agents.mdx b/content/docs/ai/agents.mdx index e1ce193058..916420c167 100644 --- a/content/docs/ai/agents.mdx +++ b/content/docs/ai/agents.mdx @@ -167,6 +167,7 @@ point at, and reach it through `@objectstack/mcp` (BYO-AI). ## Sales Assistant Agent +{/* os:check */} ```typescript import { defineAgent } from '@objectstack/spec/ai'; diff --git a/content/docs/api/environment-routing.mdx b/content/docs/api/environment-routing.mdx index f79fddeab1..86c29b7f4d 100644 --- a/content/docs/api/environment-routing.mdx +++ b/content/docs/api/environment-routing.mdx @@ -24,6 +24,7 @@ depending on request context. Enable scoped route registration in `objectstack.config.ts`: +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; diff --git a/content/docs/api/error-handling-client.mdx b/content/docs/api/error-handling-client.mdx index b406d64fa6..bfd760c344 100644 --- a/content/docs/api/error-handling-client.mdx +++ b/content/docs/api/error-handling-client.mdx @@ -17,6 +17,7 @@ This guide covers best practices for handling ObjectStack API errors in client a Every error response from ObjectStack follows this shape: +{/* os:check */} ```typescript interface ErrorResponse { success: false; // Always false for error responses @@ -56,6 +57,7 @@ Error `code` values are lowercase snake_case (`validation_error`, `permission_de Create a typed error parser to extract structured information from API responses: +{/* os:check */} ```typescript import type { ErrorResponse } from '@objectstack/spec/api'; diff --git a/content/docs/api/error-handling-server.mdx b/content/docs/api/error-handling-server.mdx index e7d784462d..69702d8f5b 100644 --- a/content/docs/api/error-handling-server.mdx +++ b/content/docs/api/error-handling-server.mdx @@ -23,6 +23,7 @@ lowercase snake_case members of `StandardErrorCode` (see `@objectstack/spec/api`), e.g. `validation_error`, `resource_conflict`, `permission_denied`. +{/* os:check */} ```typescript import type { ErrorResponse, StandardErrorCode } from '@objectstack/spec/api'; @@ -289,6 +290,7 @@ export const EnrichTaskWithAI: Hook = { Prevent cascading failures when an external service is down: +{/* os:check */} ```typescript class CircuitBreaker { private failures = 0; diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx index 2d9f53de66..3576a31883 100644 --- a/content/docs/automation/flows.mdx +++ b/content/docs/automation/flows.mdx @@ -9,6 +9,7 @@ A **Flow** is a visual automation that orchestrates business logic through conne ## Basic Structure +{/* os:check */} ```typescript const approvalFlow = { name: 'order_approval', @@ -564,6 +565,7 @@ Edges connect nodes and define the execution path: Flows use variables to pass data between nodes and to/from callers: +{/* os:check */} ```typescript variables: [ { name: 'input_id', type: 'text', isInput: true, isOutput: false }, @@ -738,6 +740,7 @@ is retired; model the same logic as a Flow. ### Record-change flow +{/* os:check */} ```typescript import type { Flow } from '@objectstack/spec/automation'; diff --git a/content/docs/automation/index.mdx b/content/docs/automation/index.mdx index acae7f5e64..25e64dd4a4 100644 --- a/content/docs/automation/index.mdx +++ b/content/docs/automation/index.mdx @@ -9,6 +9,7 @@ Automation is ObjectStack's process engine: you attach business logic to the dat The smallest useful automation is a hook (from the CRM example app): +{/* os:check */} ```typescript import type { Hook, HookContext } from '@objectstack/spec/data'; diff --git a/content/docs/automation/workflows.mdx b/content/docs/automation/workflows.mdx index c4ea232e64..c8fc92f37b 100644 --- a/content/docs/automation/workflows.mdx +++ b/content/docs/automation/workflows.mdx @@ -21,6 +21,7 @@ This page keeps the historical route but documents the current split. Use Flow when something should happen after a record event, schedule, button, or subflow call: +{/* os:check */} ```typescript import type { Flow } from '@objectstack/spec/automation'; @@ -63,6 +64,7 @@ registration/runtime. Use `StateMachineSchema` when the core requirement is "this object can only move through these states by these events." +{/* os:check */} ```typescript import type { StateMachineConfig } from '@objectstack/spec/automation'; diff --git a/content/docs/concepts/architecture.mdx b/content/docs/concepts/architecture.mdx index 581fc19d4f..9d765b0e3b 100644 --- a/content/docs/concepts/architecture.mdx +++ b/content/docs/concepts/architecture.mdx @@ -88,6 +88,7 @@ description — data (objects), automation (flows), access (permissions), and in ### Example: Defining a Customer Object +{/* os:check */} ```typescript // src/objects/customer.object.ts import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -148,6 +149,7 @@ That's the job of the other layers. ### Example: Permission Rules +{/* os:check */} ```typescript // src/permissions/sales_rep.permission.ts import { definePermissionSet } from '@objectstack/spec'; @@ -175,6 +177,7 @@ export const SalesRepPermission = definePermissionSet({ ### Example: Workflow Automation +{/* os:check */} ```typescript // src/flows/high_value_customer.flow.ts import { defineFlow } from '@objectstack/spec'; @@ -221,6 +224,7 @@ Kernel **orchestrates** these rules at runtime, independent of the data structur ### Example: List View +{/* os:check */} ```typescript // src/views/customer.view.ts import { defineView } from '@objectstack/spec'; @@ -245,6 +249,7 @@ export const CustomerView = defineView({ ### Example: Form View +{/* os:check */} ```typescript // src/views/customer.view.ts import { defineView } from '@objectstack/spec'; @@ -300,6 +305,7 @@ User clicks "Save" ### Step 2: UI Layer Sends Request +{/* os:check */} ```typescript // ObjectUI dispatches an action to Kernel const request = { @@ -375,6 +381,7 @@ for (const workflow of workflows) { ### Step 7: ObjectUI Updates Display +{/* os:check */} ```typescript // Kernel returns success response // UI optimistically updates the screen @@ -388,6 +395,7 @@ Here's how all three protocols collaborate for a **Kanban Board** feature: ### 1. ObjectQL: Define the Data +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -426,6 +434,7 @@ export const Opportunity = ObjectSchema.create({ ### 2. Kernel: Define Business Rules +{/* os:check */} ```typescript import { defineFlow } from '@objectstack/spec'; @@ -450,6 +459,7 @@ export const OpportunityWonFlow = defineFlow({ ### 3. ObjectUI: Define the Kanban View +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec'; diff --git a/content/docs/concepts/index.mdx b/content/docs/concepts/index.mdx index 770a241834..d72da067d2 100644 --- a/content/docs/concepts/index.mdx +++ b/content/docs/concepts/index.mdx @@ -41,6 +41,7 @@ ObjectStack centralizes the "Intent" into a **single Protocol Definition**: +{/* os:check */} ```typescript // ONE definition — everything else derives from it import { defineStack } from '@objectstack/spec'; @@ -57,6 +58,7 @@ export default defineStack({ ``` +{/* os:check */} ```typescript // Map keys become the `name` field automatically import { defineStack } from '@objectstack/spec'; diff --git a/content/docs/concepts/metadata-driven.mdx b/content/docs/concepts/metadata-driven.mdx index ebd121bd1b..e28c6d9b54 100644 --- a/content/docs/concepts/metadata-driven.mdx +++ b/content/docs/concepts/metadata-driven.mdx @@ -72,6 +72,7 @@ ObjectStack centralizes the "Intent" into a **single TypeScript-authored, Zod-va ### Example: The ObjectStack Way +{/* os:check */} ```typescript // ONE definition (in objectstack.config.ts) import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -112,6 +113,7 @@ In metadata-driven development, we embrace three core truths: The UI doesn't "build" a form; it **projects** the Object schema into visual components. +{/* os:check */} ```typescript // Conceptual: the schema IS the form. The runtime renders a view // definition (a FormView metadata object) — there is no FormView @@ -234,6 +236,7 @@ data, API, UI, and permissions in a single change — it can answer *"what break if I change this?"* instead of grepping and hoping. That turns AI from an autocomplete tool into a real co-maintainer. +{/* os:check */} ```typescript // All you need: import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -332,6 +335,7 @@ When defining objects and metadata in ObjectStack, follow these strict rules and ### 1. Always Use `ObjectSchema.create()` with `Field.*` Helpers **✅ Correct:** +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -476,6 +480,7 @@ export const Account = ObjectSchema.create({ ### Quick Reference +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/analytics.mdx b/content/docs/data-modeling/analytics.mdx index 9659eed895..8763923731 100644 --- a/content/docs/data-modeling/analytics.mdx +++ b/content/docs/data-modeling/analytics.mdx @@ -34,6 +34,7 @@ the object graph, and every surface references the same definition. ## Authoring a dataset +{/* os:check */} ```ts // src/datasets/sales.dataset.ts import { defineDataset } from '@objectstack/spec/ui'; @@ -110,6 +111,7 @@ A `metric` (KPI) widget omits `dimensions` and shows the single measure value. ## Binding a report +{/* os:check */} ```ts export const SalesByStageReport = { name: 'sales_by_stage', diff --git a/content/docs/data-modeling/external-datasources.mdx b/content/docs/data-modeling/external-datasources.mdx index d1de069528..bfe819fd43 100644 --- a/content/docs/data-modeling/external-datasources.mdx +++ b/content/docs/data-modeling/external-datasources.mdx @@ -26,6 +26,7 @@ the plain multi-datasource routing of **managed** databases ObjectStack owns, se Use `defineDatasource` with `schemaMode: 'external'`. The `external` block carries the federation policy (write gate, boot validation, credentials). +{/* os:check */} ```typescript import { defineDatasource } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx index 5616f66052..9b34255ef6 100644 --- a/content/docs/data-modeling/fields.mdx +++ b/content/docs/data-modeling/fields.mdx @@ -11,6 +11,7 @@ A **Field** defines an individual property within an Object. ObjectStack provide Fields are defined inside an Object's `fields` map using `Field.*` factory methods: +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/formulas.mdx b/content/docs/data-modeling/formulas.mdx index 104bfdb3b2..35c936a2df 100644 --- a/content/docs/data-modeling/formulas.mdx +++ b/content/docs/data-modeling/formulas.mdx @@ -34,6 +34,7 @@ plus the ObjectStack standard library. Every expression in metadata is persisted as the same envelope: +{/* os:check */} ```ts type Expression = { dialect: 'cel' | 'cron' | 'template'; @@ -85,6 +86,7 @@ At **input** time you may write a bare string for shorthand — the spec transforms it into the right envelope based on the field type. The compiled artifact always contains the full envelope (and, after M9.2, the AST). +{/* os:check */} ```ts import { F, P, cel, cron, tmpl } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/index.mdx b/content/docs/data-modeling/index.mdx index 90f1cff123..685705b262 100644 --- a/content/docs/data-modeling/index.mdx +++ b/content/docs/data-modeling/index.mdx @@ -9,6 +9,7 @@ Every ObjectStack application starts here: you declare **objects** (business ent A real object definition looks like this (from the CRM example app): +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/indexing.mdx b/content/docs/data-modeling/indexing.mdx index 7a0e3adf6f..e66b9a67c7 100644 --- a/content/docs/data-modeling/indexing.mdx +++ b/content/docs/data-modeling/indexing.mdx @@ -7,6 +7,7 @@ description: Optimize query performance with single-field, unique, compound, and Optimize query performance with indexes: +{/* os:check */} ```typescript indexes: [ // Single field index diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx index a9f967645c..23a67954e0 100644 --- a/content/docs/data-modeling/objects.mdx +++ b/content/docs/data-modeling/objects.mdx @@ -9,6 +9,7 @@ An **Object** is the foundational metadata type in ObjectStack. It defines a bus ## Basic Structure +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -192,6 +193,7 @@ entirely. Optimize query performance: +{/* os:check */} ```typescript indexes: [ { fields: ['name'], type: 'btree', unique: false }, @@ -281,6 +283,7 @@ listing the verb in `apiMethods`. ## Complete Example +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/queries.mdx b/content/docs/data-modeling/queries.mdx index 25d1eee9e2..69d71c59fa 100644 --- a/content/docs/data-modeling/queries.mdx +++ b/content/docs/data-modeling/queries.mdx @@ -16,6 +16,7 @@ Quick reference for building queries with the ObjectStack QuerySchema. ## Basic Query Structure +{/* os:check */} ```typescript const query = { object: 'task', @@ -76,6 +77,7 @@ const query = { ### `$and` — All conditions must match +{/* os:check */} ```typescript { where: { @@ -89,6 +91,7 @@ const query = { ### `$or` — Any condition can match +{/* os:check */} ```typescript { where: { @@ -102,6 +105,7 @@ const query = { ### `$not` — Negate a condition +{/* os:check */} ```typescript { where: { @@ -112,6 +116,7 @@ const query = { ### Nested Logic +{/* os:check */} ```typescript { where: { @@ -134,6 +139,7 @@ const query = { Sort results with `orderBy` (array of sort nodes): +{/* os:check */} ```typescript { orderBy: [ @@ -181,6 +187,7 @@ position from the previous page. There is no `keyset`/`after` query property. ### Select Specific Fields +{/* os:check */} ```typescript { fields: ['id', 'title', 'status', 'created_at'] @@ -189,6 +196,7 @@ position from the previous page. There is no `keyset`/`after` query property. ### Nested / Related Fields +{/* os:check */} ```typescript { fields: [ diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx index fe6ba9e3ad..c58c713fdf 100644 --- a/content/docs/data-modeling/schema-design.mdx +++ b/content/docs/data-modeling/schema-design.mdx @@ -181,6 +181,7 @@ editors. The group protocol is intentionally minimal (MVP): - Fields whose `group` is unset (or references an undeclared key) render in a default bucket after the declared groups. +{/* os:check */} ```typescript import { ObjectSchema } from '@objectstack/spec/data'; diff --git a/content/docs/data-modeling/validation.mdx b/content/docs/data-modeling/validation.mdx index 0ba0d6cd9e..27ada7d15d 100644 --- a/content/docs/data-modeling/validation.mdx +++ b/content/docs/data-modeling/validation.mdx @@ -20,6 +20,7 @@ Three patterns that look like validation rules are deliberately **not** rule typ Validations are defined on an Object's `validations` array: +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -268,6 +269,7 @@ When multiple validations exist, `priority` controls execution order: ## Complete Example +{/* os:check */} ```typescript validations: [ // Formula validation diff --git a/content/docs/deployment/tenancy-modes.mdx b/content/docs/deployment/tenancy-modes.mdx index ace8da8aa9..1e5a197f51 100644 --- a/content/docs/deployment/tenancy-modes.mdx +++ b/content/docs/deployment/tenancy-modes.mdx @@ -33,6 +33,7 @@ framework detects that and turns tenant isolation on. Rather than re-deriving "what mode is this?" from the env flag, a service probe, or row counts, the platform exposes a single `tenancy` kernel service: +{/* os:check */} ```ts interface TenancyService { mode: 'single' | 'multi'; // multi iff isolation is actually active diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx index 187789bc89..6e2a5dc56a 100644 --- a/content/docs/deployment/validating-metadata.mdx +++ b/content/docs/deployment/validating-metadata.mdx @@ -171,6 +171,7 @@ entry renders; opening it fails permission-denied for everyone except a holder of the platform's built-in wildcard admin set. It works while you browse as an administrator and breaks for the users the app ships permission sets for. +{/* os:check */} ```ts navigation: [{ id: 'nav_forecast', type: 'object', objectName: 'crm_forecast' }] // …and no permission set lists `crm_forecast` under `objects` → warning diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index 96b27abf7e..dff74f71c4 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -217,6 +217,7 @@ to the agent. ObjectStack turns those into loud, located build errors. Suppose the agent wrote the Resolve action's predicate as a **bare field reference** — `status` instead of `record.status`: +{/* os:check */} ```typescript // ✗ Wrong — bare `status`. Type-checks (it's just a string), but at runtime // it resolves to null, so the action is hidden on EVERY ticket. diff --git a/content/docs/getting-started/common-patterns.mdx b/content/docs/getting-started/common-patterns.mdx index e01da4d746..896185952b 100644 --- a/content/docs/getting-started/common-patterns.mdx +++ b/content/docs/getting-started/common-patterns.mdx @@ -28,6 +28,7 @@ Define a basic object with common fields for create, read, update, and delete op +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; @@ -57,6 +58,7 @@ export default defineStack({ ``` +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; @@ -92,6 +94,7 @@ export default defineStack({ Create a parent-child relationship between a master and its detail records. Note that `master_detail` defaults to `deleteBehavior: 'set_null'`; set `deleteBehavior: 'cascade'` explicitly if you want child records deleted with the parent. +{/* os:check */} ```typescript { objects: [ @@ -150,6 +153,7 @@ Create a parent-child relationship between a master and its detail records. Note Create a list view with pre-configured filters and column display. +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec'; @@ -173,6 +177,7 @@ export const taskViews = defineView({ Display records as a kanban board grouped by a status field. +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec'; @@ -195,6 +200,7 @@ export const taskBoard = defineView({ Define an application with a structured navigation menu. +{/* os:check */} ```typescript import { defineApp } from '@objectstack/spec'; @@ -247,6 +253,7 @@ or update in one flow**, use `record-after-write` (the create-OR-update union) instead of duplicating the definition — see [Flows › Create-or-update flow](/docs/automation/flows#create-or-update-flow-record-after-write). +{/* os:check */} ```typescript import { defineFlow } from '@objectstack/spec'; @@ -373,6 +380,7 @@ Define a multi-step approval flow for records. Configure an AI agent with specific tools and instructions. +{/* os:check */} ```typescript import { defineAgent } from '@objectstack/spec'; @@ -417,6 +425,7 @@ Implement pagination for large datasets using offset-based or cursor-based pagin ### Offset-Based (Simple) +{/* os:check */} ```typescript // Page 1 const page1 = { @@ -464,6 +473,7 @@ const nextPage = { Restrict field visibility and editability based on user profiles. +{/* os:check */} ```typescript { objects: [{ @@ -486,6 +496,7 @@ Restrict field visibility and editability based on user profiles. Who can actually read or edit a field is governed by permission sets — grant per-object CRUD, then narrow individual fields: +{/* os:check */} ```typescript import { definePermissionSet } from '@objectstack/spec'; @@ -510,6 +521,7 @@ Combine these patterns into a complete `defineStack()` configuration: +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; @@ -538,6 +550,7 @@ export default defineStack({ ``` +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx index 9f9a9d345e..326d03d041 100644 --- a/content/docs/getting-started/examples.mdx +++ b/content/docs/getting-started/examples.mdx @@ -121,6 +121,7 @@ pnpm dev:todo **1. Define an object** — `src/objects/task.object.ts`: +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx index 2b6da1c9ea..ec957745f4 100644 --- a/content/docs/getting-started/quick-reference.mdx +++ b/content/docs/getting-started/quick-reference.mdx @@ -266,6 +266,7 @@ Testing and quality assurance. ### Common Imports +{/* os:check */} ```typescript // Data protocols import { FieldSchema, ObjectSchema, QuerySchema } from '@objectstack/spec/data'; diff --git a/content/docs/kernel/contracts/auth-service.mdx b/content/docs/kernel/contracts/auth-service.mdx index a95e9fb0ba..26d8963c94 100644 --- a/content/docs/kernel/contracts/auth-service.mdx +++ b/content/docs/kernel/contracts/auth-service.mdx @@ -74,6 +74,7 @@ await authService.logout?.(session.id); The `AuthUser` object represents an authenticated user throughout the system. +{/* os:check */} ```typescript export interface AuthUser { /** User identifier */ @@ -116,6 +117,7 @@ if (user) { ## AuthSession Interface +{/* os:check */} ```typescript export interface AuthSession { /** Session identifier */ diff --git a/content/docs/kernel/contracts/cache-service.mdx b/content/docs/kernel/contracts/cache-service.mdx index d7d97eef9c..9ae03f3964 100644 --- a/content/docs/kernel/contracts/cache-service.mdx +++ b/content/docs/kernel/contracts/cache-service.mdx @@ -32,6 +32,7 @@ export interface ICacheService { `CacheStats` is returned by `stats()` for monitoring: +{/* os:check */} ```typescript export interface CacheStats { hits: number; // Total number of cache hits diff --git a/content/docs/kernel/contracts/data-engine.mdx b/content/docs/kernel/contracts/data-engine.mdx index b234dd31c8..7b44a3dca9 100644 --- a/content/docs/kernel/contracts/data-engine.mdx +++ b/content/docs/kernel/contracts/data-engine.mdx @@ -237,6 +237,7 @@ the listener exists so callers that report per-field success (e.g. the flow engine's `update_record` step) can surface a warning instead of a silent success. +{/* os:check */} ```typescript interface WriteObservabilityOptions { onFieldsDropped?: (event: DroppedFieldsEvent) => void; diff --git a/content/docs/kernel/contracts/storage-service.mdx b/content/docs/kernel/contracts/storage-service.mdx index e7e0a4cc65..4c16edf9a0 100644 --- a/content/docs/kernel/contracts/storage-service.mdx +++ b/content/docs/kernel/contracts/storage-service.mdx @@ -146,6 +146,7 @@ console.log(info.metadata); // { objectType: 'task', ... } Configuration options for upload behavior. +{/* os:check */} ```typescript export interface StorageUploadOptions { /** MIME content type */ @@ -226,6 +227,7 @@ const upload = await storageService.getPresignedUpload?.( ## Descriptor Types +{/* os:check */} ```typescript export interface PresignedUploadDescriptor { uploadUrl: string; @@ -273,6 +275,7 @@ const key = await storageService.completeChunkedUpload?.(uploadId, [ The return type for file metadata (`getInfo`, `list`). +{/* os:check */} ```typescript export interface StorageFileInfo { key: string; diff --git a/content/docs/kernel/runtime-services/examples.mdx b/content/docs/kernel/runtime-services/examples.mdx index f12151a4a7..bd4728a20d 100644 --- a/content/docs/kernel/runtime-services/examples.mdx +++ b/content/docs/kernel/runtime-services/examples.mdx @@ -7,6 +7,7 @@ description: Practical examples for flow nodes, hooks, and plugin event subscrip ## 1) Flow custom node: read related records +{/* os:check */} ```ts export async function run(ctx: any) { const { record: order } = await ctx.services.data.get('sales_order', ctx.input.orderId); @@ -25,6 +26,7 @@ export async function run(ctx: any) { ## 2) Hook: check sharing permission before mutation +{/* os:check */} ```ts export async function beforeUpdate(ctx: any) { const ok = await ctx.services?.sharing?.canEdit('contract', ctx.input.id, { diff --git a/content/docs/permissions/authentication.mdx b/content/docs/permissions/authentication.mdx index dbf339fdfa..2093f35325 100644 --- a/content/docs/permissions/authentication.mdx +++ b/content/docs/permissions/authentication.mdx @@ -227,6 +227,7 @@ console.log('Current user:', session.data.user); #### Request Password Reset +{/* os:check */} ```typescript // Direct API call const response = await fetch('http://localhost:3000/api/v1/auth/request-password-reset', { @@ -240,6 +241,7 @@ const response = await fetch('http://localhost:3000/api/v1/auth/request-password #### Reset Password with Token +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/reset-password', { method: 'POST', @@ -290,6 +292,7 @@ by OTP self-signup. Always available once the plugin is on: +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', { method: 'POST', @@ -306,6 +309,7 @@ fail loudly with `NOT_SUPPORTED`. The public config advertises real availability as `features.phoneNumberOtp`, which is what the Console login UI gates its "Sign in with verification code" mode and the SMS reset branch on. +{/* os:check */} ```typescript // Sign-in / verification: request a code, then verify (creates a session) await fetch('/api/v1/auth/phone-number/send-otp', { @@ -551,6 +555,7 @@ const { totpURI, backupCodes } = await response.json(); #### Verify 2FA Code +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/two-factor/verify-totp', { method: 'POST', @@ -584,6 +589,7 @@ new AuthPlugin({ #### Send Magic Link +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/magic-link', { method: 'POST', @@ -623,6 +629,7 @@ same flag. The plugin adds `sys_user.phone_number` (unique) and #### Sign In with Phone + Password +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', { method: 'POST', @@ -727,6 +734,7 @@ scrypt-hashed credential and can sign in immediately. #### Create a User Directly +{/* os:check */} ```typescript const response = await fetch('http://localhost:3000/api/v1/auth/admin/create-user', { method: 'POST', @@ -842,6 +850,7 @@ await client.auth.refreshToken('refresh-token-value'); All endpoints are available at `/api/v1/auth/*`: +{/* os:check */} ```typescript // Example: Login const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/email', { diff --git a/content/docs/permissions/permission-metadata.mdx b/content/docs/permissions/permission-metadata.mdx index 75bdbedfc2..de1232a132 100644 --- a/content/docs/permissions/permission-metadata.mdx +++ b/content/docs/permissions/permission-metadata.mdx @@ -9,6 +9,7 @@ A **Permission Set** defines what a user can do within the application. It contr ## Basic Structure +{/* os:check */} ```typescript const salesUserPermission = { name: 'sales_user', @@ -148,6 +149,7 @@ tabPermissions: { Grant system-level capabilities: +{/* os:check */} ```typescript systemPermissions: [ 'manage_users', @@ -165,6 +167,7 @@ systemPermissions: [ Define policies that restrict which records a user can access: +{/* os:check */} ```typescript rowLevelSecurity: [ { @@ -215,6 +218,7 @@ sets bound to [positions](/docs/permissions/positions). ## Complete Example +{/* os:check */} ```typescript const salesManagerPermission = { name: 'sales_manager', diff --git a/content/docs/permissions/permission-sets.mdx b/content/docs/permissions/permission-sets.mdx index 47a5cadb3f..37a8187156 100644 --- a/content/docs/permissions/permission-sets.mdx +++ b/content/docs/permissions/permission-sets.mdx @@ -13,6 +13,7 @@ via their [positions](/docs/permissions/positions), via direct grants, via the `everyone` anchor, and via the additive baseline. Any set that allows, wins; restriction is done by *not granting*. +{/* os:check */} ```typescript import { definePermissionSet } from '@objectstack/spec/security'; diff --git a/content/docs/permissions/permissions-matrix.mdx b/content/docs/permissions/permissions-matrix.mdx index c541035350..e960dd6b16 100644 --- a/content/docs/permissions/permissions-matrix.mdx +++ b/content/docs/permissions/permissions-matrix.mdx @@ -77,6 +77,7 @@ Permission sets are the only capability container (there is no Profile concept ### Configuration Example +{/* os:check */} ```typescript import { defineStack } from '@objectstack/spec'; diff --git a/content/docs/permissions/rls.mdx b/content/docs/permissions/rls.mdx index 762daa292c..1659ef4c6b 100644 --- a/content/docs/permissions/rls.mdx +++ b/content/docs/permissions/rls.mdx @@ -29,6 +29,7 @@ RLS is the expert escape hatch. Reach for it when Policies live on a permission set under `rowLevelSecurity`: +{/* os:check */} ```typescript import { definePermissionSet } from '@objectstack/spec'; diff --git a/content/docs/permissions/sso.mdx b/content/docs/permissions/sso.mdx index 96fad62f07..f50c0b088e 100644 --- a/content/docs/permissions/sso.mdx +++ b/content/docs/permissions/sso.mdx @@ -127,6 +127,7 @@ packages that prefer wiring providers in code, or contributing them through ### Quick start — Okta +{/* os:check */} ```ts const oidcProviders = [ { @@ -142,6 +143,7 @@ const oidcProviders = [ ### Quick start — Azure AD (OIDC) +{/* os:check */} ```ts const oidcProviders = [ { @@ -156,6 +158,7 @@ const oidcProviders = [ ### Quick start — Keycloak +{/* os:check */} ```ts const oidcProviders = [ { @@ -171,6 +174,7 @@ const oidcProviders = [ ### Multiple enterprise providers +{/* os:check */} ```ts const oidcProviders = [ { diff --git a/content/docs/plugins/adding-a-metadata-type.mdx b/content/docs/plugins/adding-a-metadata-type.mdx index a1dcf3037a..7d895b5bc2 100644 --- a/content/docs/plugins/adding-a-metadata-type.mdx +++ b/content/docs/plugins/adding-a-metadata-type.mdx @@ -78,6 +78,7 @@ Schema. The registry entry shape is the same in both cases. Place the schema under the matching domain folder (e.g. `packages/spec/src/ui/my-widget.zod.ts`): +{/* os:check */} ```ts import { z } from 'zod'; @@ -176,6 +177,7 @@ Common designer prop shapes already supported: No routing changes are required. The Studio app already includes the directory entry in its navigation: +{/* os:check */} ```ts // studio.app.ts navigation: [ diff --git a/content/docs/plugins/development.mdx b/content/docs/plugins/development.mdx index 340f141255..9adac9937d 100644 --- a/content/docs/plugins/development.mdx +++ b/content/docs/plugins/development.mdx @@ -58,6 +58,7 @@ Create `tsconfig.json`: A plugin package describes itself with a manifest validated by `ManifestSchema`. Create `src/manifest.ts`: +{/* os:check */} ```typescript import { ManifestSchema } from '@objectstack/spec/kernel'; @@ -127,6 +128,7 @@ export function createHelloPlugin(): Plugin { Plugins can register new objects: +{/* os:check */} ```typescript import { ObjectSchema } from '@objectstack/spec/data'; @@ -265,6 +267,7 @@ Declare the capabilities your plugin needs in its manifest's `permissions` block Always use structured errors: +{/* os:check */} ```typescript import type { EnhancedApiError } from '@objectstack/spec/api'; @@ -318,6 +321,7 @@ For plugins that extend the **ObjectStack Studio IDE**, use `defineStudioPlugin` ### Basic Studio Plugin +{/* os:check */} ```typescript import { defineStudioPlugin } from '@objectstack/spec/studio'; @@ -343,6 +347,7 @@ export const manifest = defineStudioPlugin({ Plugin IDs use reverse-domain notation and must match the pattern `^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$`: +{/* os:check */} ```typescript // ✅ Valid IDs 'objectstack.flow-designer' @@ -370,6 +375,7 @@ Studio plugins can declare six types of contributions: ### Full Example with All Contributions +{/* os:check */} ```typescript import { defineStudioPlugin } from '@objectstack/spec/studio'; diff --git a/content/docs/plugins/packages.mdx b/content/docs/plugins/packages.mdx index 94a676beac..392bc78e4d 100644 --- a/content/docs/plugins/packages.mdx +++ b/content/docs/plugins/packages.mdx @@ -30,6 +30,7 @@ ObjectStack is organized into **71 package manifests** across multiple categorie - **When to use**: Import types, schemas, and builder functions when authoring metadata. - **Documentation**: [Protocol Reference](/docs/references) +{/* os:check */} ```typescript import { defineStack, defineView } from '@objectstack/spec'; import * as Data from '@objectstack/spec/data'; diff --git a/content/docs/protocol/backward-compatibility.mdx b/content/docs/protocol/backward-compatibility.mdx index a7f0df6713..0452067156 100644 --- a/content/docs/protocol/backward-compatibility.mdx +++ b/content/docs/protocol/backward-compatibility.mdx @@ -32,6 +32,7 @@ ObjectStack follows [Semantic Versioning 2.0.0](https://semver.org/) (`MAJOR.MIN - **Enum values** are append-only within a MAJOR version. Existing values are never removed or renamed in MINOR/PATCH releases. - **Default values** in schemas are not changed in MINOR/PATCH releases unless fixing a documented bug. +{/* os:check */} ```typescript // These imports are guaranteed stable within a MAJOR version import { FieldSchema, ObjectSchema } from '@objectstack/spec/data'; diff --git a/content/docs/protocol/kernel/config-resolution.mdx b/content/docs/protocol/kernel/config-resolution.mdx index f3de61b9ca..4394b4895f 100644 --- a/content/docs/protocol/kernel/config-resolution.mdx +++ b/content/docs/protocol/kernel/config-resolution.mdx @@ -861,6 +861,7 @@ export async function someHandler({ context }) { ``` ### 4. Provide Sensible Defaults +{/* os:check */} ```typescript // ✓ GOOD: Plugin works out-of-box with defaults. // (Defaults ship as `default` on the plugin's settings-manifest diff --git a/content/docs/protocol/kernel/i18n-standard.mdx b/content/docs/protocol/kernel/i18n-standard.mdx index 5d92ca9c00..e807063d0a 100644 --- a/content/docs/protocol/kernel/i18n-standard.mdx +++ b/content/docs/protocol/kernel/i18n-standard.mdx @@ -218,6 +218,7 @@ const zh: TranslationData = { An admin (or an agent using the metadata API) authors one item per locale. The only difference from a file bundle is the top-level `locale`: +{/* os:check */} ```typescript import { defineTranslation } from '@objectstack/spec/system'; @@ -590,6 +591,7 @@ context.i18n.formatNumber(smallNumber, { Plugins register translation bundles in their manifest: +{/* os:check */} ```typescript // plugin.manifest.ts — a plugin manifest validated by `ManifestSchema` // from `@objectstack/spec/kernel` (there is no `definePlugin()` helper). @@ -646,6 +648,7 @@ const welcome = i18n.t('crm.account.welcome', 'en'); ObjectQL objects and fields can be **automatically translated**: +{/* os:check */} ```typescript // Object definition import { ObjectSchema, Field } from '@objectstack/spec/data'; diff --git a/content/docs/protocol/kernel/lifecycle.mdx b/content/docs/protocol/kernel/lifecycle.mdx index 46bda516dd..8e2951389e 100644 --- a/content/docs/protocol/kernel/lifecycle.mdx +++ b/content/docs/protocol/kernel/lifecycle.mdx @@ -439,6 +439,7 @@ non-trivial column renames). #### Declarative schema (the default) +{/* os:check */} ```typescript // src/objects/salesforce_account.object.ts import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -473,6 +474,7 @@ os generate migration --dry-run # Preview without writing The generated TypeScript file uses plain Knex-style `up(db)` / `down(db)` functions — no custom migration DSL: +{/* os:check */} ```typescript // migrations/20260101000000_migration.ts — auto-generated export async function up(db: any): Promise { @@ -735,6 +737,7 @@ await step2(); // If this fails, step1 is not reverted! ### 2. Version Migrations, Don't Modify Them Once a migration file is applied in production, **never modify it**. Generate a new one instead. +{/* os:check */} ```typescript // ✗ BAD: Modifying an existing migration file // migrations/20260101000000_migration.ts (ALREADY APPLIED) diff --git a/content/docs/protocol/kernel/metadata-service.mdx b/content/docs/protocol/kernel/metadata-service.mdx index f8cf93c5be..816340d38e 100644 --- a/content/docs/protocol/kernel/metadata-service.mdx +++ b/content/docs/protocol/kernel/metadata-service.mdx @@ -70,6 +70,7 @@ Loaders adapt different storage backends to a common interface. Each Loader declares its capabilities via a **Contract**: +{/* os:check */} ```typescript export interface MetadataLoaderContract { name: string; diff --git a/content/docs/protocol/kernel/plugin-spec.mdx b/content/docs/protocol/kernel/plugin-spec.mdx index bed90f862c..c2f111a9b8 100644 --- a/content/docs/protocol/kernel/plugin-spec.mdx +++ b/content/docs/protocol/kernel/plugin-spec.mdx @@ -240,6 +240,7 @@ A well-organized plugin follows this **standard structure**: Define database objects using ObjectQL schema syntax: +{/* os:check */} ```typescript // src/objects/account.object.ts import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -288,6 +289,7 @@ export const Account = ObjectSchema.create({ Define user interfaces using ObjectUI layout DSL: +{/* os:check */} ```typescript // src/views/account_list.view.ts import { defineView } from '@objectstack/spec'; @@ -357,6 +359,7 @@ export default defineTrigger({ Define plugin configuration with Zod for validation: +{/* os:check */} ```typescript // src/config.schema.ts import { z } from 'zod'; diff --git a/content/docs/protocol/kernel/runtime-capabilities.mdx b/content/docs/protocol/kernel/runtime-capabilities.mdx index 317d05d625..5595888e90 100644 --- a/content/docs/protocol/kernel/runtime-capabilities.mdx +++ b/content/docs/protocol/kernel/runtime-capabilities.mdx @@ -513,6 +513,7 @@ const capabilities = Always document which capabilities your feature requires: +{/* os:check */} ```typescript /** * Semantic Search Component diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index 3a6eac18c6..cca3c6a3d3 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -24,6 +24,7 @@ LIMIT 10; ``` **ObjectQL (Canonical Spec Format):** +{/* os:check */} ```typescript import type { QueryAST } from '@objectstack/spec/data'; @@ -292,6 +293,7 @@ const query: QueryAST = { ### Date Filters +{/* os:check */} ```typescript // Specific date where: { created_at: '2024-01-15' } @@ -306,6 +308,7 @@ where: { created_at: { $gte: '2024-01-01' } } ### Null Checks +{/* os:check */} ```typescript // Field IS NULL where: { manager_id: { $null: true } } @@ -796,6 +799,7 @@ The tuple/array format (e.g. `['status', '=', 'active']`) and the `filters` key Before entering the ObjectQL protocol or `IDataEngine`, tuple filters **must** be converted to the canonical `where` + `$op` object format using the `parseFilterAST()` utility from `@objectstack/spec/data`: +{/* os:check */} ```typescript import { parseFilterAST } from '@objectstack/spec/data'; diff --git a/content/docs/protocol/objectql/schema.mdx b/content/docs/protocol/objectql/schema.mdx index c5b506a71d..47b8a7dea6 100644 --- a/content/docs/protocol/objectql/schema.mdx +++ b/content/docs/protocol/objectql/schema.mdx @@ -46,6 +46,7 @@ label: Customer ``` **TypeScript (Recommended for strict validation):** +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -128,6 +129,7 @@ validations: **TypeScript Complete Example:** +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -621,6 +623,7 @@ Record-triggered logic is **not** an object-schema field — `triggers` (and `ho `src/objects/.hook.ts` module as a typed `Hook` and export it (or model the automation as a top-level `record_change` flow). +{/* os:check */} ```typescript // src/objects/customer.hook.ts import { Hook, HookContext } from '@objectstack/spec/data'; diff --git a/content/docs/protocol/objectql/state-machine.mdx b/content/docs/protocol/objectql/state-machine.mdx index 1784855b6d..9d96d87bec 100644 --- a/content/docs/protocol/objectql/state-machine.mdx +++ b/content/docs/protocol/objectql/state-machine.mdx @@ -24,6 +24,7 @@ In the era of **AI Agents**, field-level validation is not enough. Large Languag Add a `state_machine` rule to the object's `validations` array. The rule names the state `field` and declares a `transitions` map of `{ currentValue: [allowedNextValues] }`. +{/* os:check */} ```typescript import { ObjectSchema, Field } from '@objectstack/spec/data'; @@ -123,6 +124,7 @@ Because the transition table is data, both UIs and Agents can ask "from this sta For complex business objects (like `Lead`, `Opportunity`, or `Order`), a transition table can grow large. To keep your object definition readable, extract the rule(s) into a plain TypeScript constant — there is no special `*.state.ts` format or `StateMachineConfig` type for object lifecycles; it is just an array of validation rules. +{/* os:check */} ```typescript // src/objects/lead.validations.ts export const leadValidations = [ diff --git a/content/docs/protocol/objectui/index.mdx b/content/docs/protocol/objectui/index.mdx index 57291f5641..c0e379c39c 100644 --- a/content/docs/protocol/objectui/index.mdx +++ b/content/docs/protocol/objectui/index.mdx @@ -224,6 +224,7 @@ actions: ### FormView: Record Editing +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec/ui'; @@ -248,6 +249,7 @@ defineView({ ### ListView: Data Tables +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec/ui'; @@ -277,6 +279,7 @@ defineView({ ### Dashboard: Analytics Composition +{/* os:check */} ```typescript import { Dashboard } from '@objectstack/spec/ui'; diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index b46d7f0b81..e8f5e89ffe 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -780,6 +780,7 @@ interface FormField { ### Responsive Columns +{/* os:check */} ```typescript // Source: packages/spec/src/ui/responsive.zod.ts (BreakpointColumnMapSchema) // Grid columns (1-12) per named breakpoint. All entries optional. @@ -801,6 +802,7 @@ layer — data fields, view form sections/fields, and page components — aligni the `readonlyWhen` / `requiredWhen` family. The element is shown only when the expression evaluates truthy. +{/* os:check */} ```typescript // e.g. on a PageComponent: visibleWhen: "record.account_type == 'premium'" diff --git a/content/docs/protocol/objectui/record-alert.mdx b/content/docs/protocol/objectui/record-alert.mdx index 76c953bbcd..d1284153c6 100644 --- a/content/docs/protocol/objectui/record-alert.mdx +++ b/content/docs/protocol/objectui/record-alert.mdx @@ -40,6 +40,7 @@ Common use cases: `record:alert` is registered as a slot component in the slotted page schema. Add it to the `alerts` slot of your `*.page.ts`: +{/* os:check */} ```ts import { definePage } from '@objectstack/spec/ui'; @@ -88,6 +89,7 @@ The `visible` predicate uses the same CEL pipeline every condition in the framew Examples: +{/* os:check */} ```ts visible: 'record.id == os.user.id && record.email_verified == false' visible: 'record.status == "locked"' diff --git a/content/docs/protocol/objectui/widget-contract.mdx b/content/docs/protocol/objectui/widget-contract.mdx index 63a899a149..73d03c1076 100644 --- a/content/docs/protocol/objectui/widget-contract.mdx +++ b/content/docs/protocol/objectui/widget-contract.mdx @@ -204,6 +204,7 @@ implementation: Widget lifecycle hooks are defined on the manifest as **code-body strings** (not function references). They follow `WidgetLifecycleSchema`: +{/* os:check */} ```typescript interface WidgetLifecycle { onMount?: string; // Initialization when the widget mounts @@ -229,6 +230,7 @@ lifecycle: A widget can declare custom events it emits via `WidgetEventSchema`: +{/* os:check */} ```typescript interface WidgetEvent { name: string; // lowercase, dash-separated, e.g. 'value-change' @@ -255,6 +257,7 @@ events: A widget exposes configuration knobs through `WidgetPropertySchema`. These describe the options a builder can set when placing the widget: +{/* os:check */} ```typescript interface WidgetProperty { name: string; // camelCase @@ -284,6 +287,7 @@ properties: A widget manifest carries ARIA metadata through the shared `AriaProps` schema (`packages/spec/src/ui/i18n.zod.ts`). The supported attributes are intentionally minimal: +{/* os:check */} ```typescript interface AriaProps { ariaLabel?: string; // Accessible label for screen readers diff --git a/content/docs/ui/apps.mdx b/content/docs/ui/apps.mdx index 1f43edb79f..002a74d597 100644 --- a/content/docs/ui/apps.mdx +++ b/content/docs/ui/apps.mdx @@ -9,6 +9,7 @@ An **App** is a logical container that bundles objects, views, pages, and dashbo ## Basic Structure +{/* os:check */} ```typescript const crmApp = { name: 'crm', @@ -169,6 +170,7 @@ mobileNavigation: { ## Complete Example +{/* os:check */} ```typescript const projectApp = { name: 'project_management', diff --git a/content/docs/ui/create-vs-edit-form.mdx b/content/docs/ui/create-vs-edit-form.mdx index 768b9c7d45..ad1f9af719 100644 --- a/content/docs/ui/create-vs-edit-form.mdx +++ b/content/docs/ui/create-vs-edit-form.mdx @@ -36,6 +36,7 @@ The full edit form materialises each `field.group` into a section. You can omit The escape hatch is a **named form view** bound to the create entry point. Author it as a **sparse override** — mostly a list of field names — not a from-scratch restatement: +{/* os:check */} ```ts import { defineView } from '@objectstack/spec'; diff --git a/content/docs/ui/dashboards.mdx b/content/docs/ui/dashboards.mdx index 929d8ff14a..b366399bee 100644 --- a/content/docs/ui/dashboards.mdx +++ b/content/docs/ui/dashboards.mdx @@ -14,6 +14,7 @@ the dataset's `dimensions` and `values` by name. The dataset owns the base object, joins, and measures, so the same numbers stay consistent across every dashboard and report. +{/* os:check */} ```typescript const salesDashboard = { name: 'sales_overview', @@ -165,6 +166,7 @@ stay consistent across surfaces. > `aggregate` directly on the widget) was removed in the ADR-0021 single-form > cutover. Define a dataset and bind widgets to it instead. +{/* os:check */} ```typescript import { defineDataset } from '@objectstack/spec/ui'; @@ -346,6 +348,7 @@ dateRange: { Add interactive filter controls that apply to all widgets: +{/* os:check */} ```typescript globalFilters: [ { name: 'region', field: 'region', label: 'Region', type: 'select' }, @@ -365,6 +368,7 @@ range defaults to `dateRange.field ?? 'created_at'`). When a widget stores the concept under a different field — or should ignore a filter — declare `filterBindings` on the widget: +{/* os:check */} ```typescript widgets: [ // Default binding: dateRange → created_at, region → region. @@ -388,6 +392,7 @@ filter's own `field`. First define the dataset the widgets bind to: +{/* os:check */} ```typescript import { defineDataset } from '@objectstack/spec/ui'; @@ -410,6 +415,7 @@ export const projectTasksDataset = defineDataset({ Then bind the dashboard widgets to it: +{/* os:check */} ```typescript const projectDashboard = { name: 'project_overview', diff --git a/content/docs/ui/forms.mdx b/content/docs/ui/forms.mdx index cde840bd17..ce26f72ac4 100644 --- a/content/docs/ui/forms.mdx +++ b/content/docs/ui/forms.mdx @@ -51,6 +51,7 @@ Only the spec'd whitelist of form fields is accepted; everything else (status, o ## 1. Declare the form view +{/* os:check */} ```ts // hotcrm/src/views/lead.view.ts (https://github.com/objectstack-ai/hotcrm) import { defineView } from '@objectstack/spec'; @@ -105,6 +106,7 @@ an older runtime, or object hooks that branch on the `guest_portal` permission). When present it is still attached to the anonymous context, so keep it INSERT-only on the target object — the guest-safe shape (ADR-0090 D9). +{/* os:check */} ```ts // hotcrm/src/security/guest-portal.permission.ts import { definePermissionSet } from '@objectstack/spec/security'; @@ -128,6 +130,7 @@ Anonymous submitters cannot be trusted to set `lead_source`, `status`, `owner`, - Apply safe defaults (`status='new'`, `lead_source='web'`, …) - Strip any internal fields that slipped past the whitelist +{/* os:check */} ```ts // hotcrm/src/objects/lead.hook.ts import type { Hook, HookContext } from '@objectstack/spec/data'; @@ -344,6 +347,7 @@ formViews: { App actions can declare `type: 'form'` to open a FormView without resorting to free-form URLs. The `target` is the FormView name; the runtime navigates to `/console/forms/:name`. +{/* os:check */} ```ts // hotcrm/src/actions/new-lead.action.ts import { Action } from '@objectstack/spec/ui'; diff --git a/content/docs/ui/index.mdx b/content/docs/ui/index.mdx index b9fd415d47..6cc32d9717 100644 --- a/content/docs/ui/index.mdx +++ b/content/docs/ui/index.mdx @@ -9,6 +9,7 @@ The UI engine turns metadata into user interfaces: you declare **apps**, **views An app is a navigation shell over your objects (from the CRM example app): +{/* os:check */} ```typescript import { App } from '@objectstack/spec/ui'; diff --git a/content/docs/ui/pages.mdx b/content/docs/ui/pages.mdx index 1b9efdf520..7ac4790be5 100644 --- a/content/docs/ui/pages.mdx +++ b/content/docs/ui/pages.mdx @@ -9,6 +9,7 @@ A **Page** defines a custom UI layout using components, regions, and variables. ## Basic Structure +{/* os:check */} ```typescript const homePage = { name: 'sales_home', @@ -83,6 +84,7 @@ Earlier roadmap types (`dashboard`, `form`, `record_detail`, `record_review`, `o Regions define layout zones on the page. Each region contains components. +{/* os:check */} ```typescript regions: [ { @@ -154,6 +156,7 @@ Components may also carry `dataSource` (per-element object binding for multi-obj Pages can define local variables for managing state across components. +{/* os:check */} ```typescript variables: [ { @@ -173,6 +176,7 @@ Variable `type` accepts `'string'` (default), `'number'`, `'boolean'`, `'object' ## Complete Example +{/* os:check */} ```typescript const accountRecordPage = { name: 'account_detail', diff --git a/content/docs/ui/translations.mdx b/content/docs/ui/translations.mdx index 5c40c51f4b..d2194a2869 100644 --- a/content/docs/ui/translations.mdx +++ b/content/docs/ui/translations.mdx @@ -143,6 +143,7 @@ Files are not the only door. A `translation` metadata item — created in the Studio, through the metadata API, or by an agent — carries one locale's worth of the **same** groups a file bundle uses, plus the `locale` it translates: +{/* os:check */} ```ts import { defineTranslation } from '@objectstack/spec/system'; diff --git a/content/docs/ui/views.mdx b/content/docs/ui/views.mdx index daad8f60f3..ee837f9647 100644 --- a/content/docs/ui/views.mdx +++ b/content/docs/ui/views.mdx @@ -11,6 +11,7 @@ A **View** defines how records of an Object are displayed to users. ObjectStack Views are authored **per object** inside a `defineView({ ... })` container — the default `list`, named `listViews`, and named `formViews` all live in one document. The loader expands the container into independently addressable `.` view items that power the view switcher. +{/* os:check */} ```typescript // src/ui/views/task.view.ts import { defineView } from '@objectstack/spec'; @@ -118,6 +119,7 @@ List and form views share that one namespace — don't reuse a key. ### Column Configuration +{/* os:check */} ```typescript columns: [ { @@ -287,6 +289,7 @@ formViews: { Each field in a form section can be customized: +{/* os:check */} ```typescript fields: [ { @@ -320,6 +323,7 @@ fields: [ One container covering a default grid, a kanban saved view, and a tabbed edit form — mirroring `examples/app-showcase/src/ui/views/task.view.ts`: +{/* os:check */} ```typescript import { defineView } from '@objectstack/spec'; diff --git a/packages/spec/scripts/check-skill-examples.ts b/packages/spec/scripts/check-skill-examples.ts index 9100c65d8e..bae62bcc3f 100644 --- a/packages/spec/scripts/check-skill-examples.ts +++ b/packages/spec/scripts/check-skill-examples.ts @@ -45,12 +45,50 @@ import path from 'path'; const REPO_ROOT = path.resolve(__dirname, '../../..'); const SPEC_DIR = path.resolve(__dirname, '..'); -const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills'); const BUILD_DIR = path.resolve(SPEC_DIR, '.examples-build'); const SPEC_PKG_JSON = path.resolve(SPEC_DIR, 'package.json'); -/** Opt-in marker: the line directly above a fence opts that block into the gate. */ -const MARKER = ''; +/** + * Prose trees whose code examples are copied verbatim by humans and AI, and so + * must compile against the real spec. + * + * `skills/` was the original scope (#3094); `content/docs/` was not covered, and + * the gap had exactly the shape this gate exists to close. When + * `tool.requiresConfirmation` was removed (#3715) this gate caught the now-broken + * `defineTool` example in `skills/objectstack-ai/SKILL.md` — the identical break + * in a docs page would have shipped, because nothing compiled those pages. A + * gate covering a fraction of the surface it appears to cover reads as coverage. + * + * `content/docs/references/` is excluded: `build-docs.ts` regenerates it from the + * schemas, so its snippets cannot drift independently of their source. + */ +const SOURCE_ROOTS: Array<{ + dir: string; + ext: string; + label: string; + marker: string; + exclude?: string[]; +}> = [ + { dir: path.resolve(REPO_ROOT, 'skills'), ext: '.md', label: 'skills', marker: '' }, + { + dir: path.resolve(REPO_ROOT, 'content/docs'), + ext: '.mdx', + label: 'docs', + // MDX has no HTML comments — fumadocs-mdx fails the build outright on + // `` ("Unexpected character `!`… to create a comment in MDX, use + // `{/* text */}`"). The marker must follow each format's own comment syntax. + marker: '{/* os:check */}', + exclude: [path.resolve(REPO_ROOT, 'content/docs/references')], + }, +]; + +/** + * Every marker spelling, in any root. A block tagged with the WRONG root's + * spelling would be silently unchecked (and, in `.mdx`, would also break the + * docs build), so both forms are recognised for orphan detection and only the + * root's own form actually opts a block in. + */ +const ALL_MARKERS = ['', '{/* os:check */}']; const KEEP = process.argv.includes('--keep'); @@ -69,21 +107,32 @@ interface Example { fileName: string; } -/** Every `*.md` under a skill folder — SKILL.md plus references/rules notes. */ -function skillMarkdownFiles(): string[] { - if (!fs.existsSync(SKILLS_DIR)) return []; - const out: string[] = []; - const walk = (dir: string) => { - for (const e of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, e.name); - if (e.isDirectory()) walk(full); - else if (e.name.endsWith('.md')) out.push(full); - } - }; - for (const e of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) { - if (e.isDirectory()) walk(path.join(SKILLS_DIR, e.name)); +/** Every candidate prose file across `SOURCE_ROOTS`, with the root that owns it. */ +function sourceFiles(): Array<{ file: string; root: (typeof SOURCE_ROOTS)[number] }> { + const out: Array<{ file: string; root: (typeof SOURCE_ROOTS)[number] }> = []; + for (const root of SOURCE_ROOTS) { + if (!fs.existsSync(root.dir)) continue; + const walk = (dir: string) => { + if (root.exclude?.some((x) => dir === x || dir.startsWith(x + path.sep))) return; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); + if (e.isDirectory()) walk(full); + else if (e.name.endsWith(root.ext)) out.push({ file: full, root }); + } + }; + walk(root.dir); } - return out.sort(); + return out.sort((a, b) => a.file.localeCompare(b.file)); +} + +/** + * Flat, collision-free build-dir name for a block: `____.ts`. + * Path separators become `_` so two pages of the same basename in different + * folders (`ui/actions.mdx`, `protocol/objectui/actions.mdx`) cannot collide. + */ +function buildFileName(source: string, root: (typeof SOURCE_ROOTS)[number], n: number): string { + const relPath = path.relative(root.dir, source).replace(new RegExp(`\\${root.ext}$`), ''); + return `${root.label}__${relPath.split(path.sep).join('_')}__${n}.ts`; } /** @@ -96,10 +145,11 @@ function skillMarkdownFiles(): string[] { * json block) silently checks nothing — exactly the failure mode this gate * exists to prevent — so the caller treats an orphan as an error, not a no-op. */ -function extractFromFile(source: string): { examples: Example[]; orphans: number[] } { +function extractFromFile( + source: string, + root: (typeof SOURCE_ROOTS)[number], +): { examples: Example[]; orphans: number[] } { const lines = fs.readFileSync(source, 'utf-8').split('\n'); - const skillDir = path.relative(SKILLS_DIR, source).split(path.sep)[0]; - const base = path.basename(source, '.md'); const examples: Example[] = []; const claimed = new Set(); // MARKER line indices that opened a real block let n = 0; @@ -107,7 +157,7 @@ function extractFromFile(source: string): { examples: Example[]; orphans: number for (let i = 0; i < lines.length; i++) { const open = lines[i].match(/^```(ts|typescript)\s*$/); if (!open) continue; - const marked = i > 0 && lines[i - 1].trim() === MARKER; + const marked = i > 0 && lines[i - 1].trim() === root.marker; // Find the matching close fence regardless of marking, so `i` advances past // this block and we never treat its body as top-level markdown. let close = i + 1; @@ -120,7 +170,7 @@ function extractFromFile(source: string): { examples: Example[]; orphans: number source, bodyStartLine: i + 2, // 1-based line of body[0] code: body.join('\n'), - fileName: `${skillDir}__${base}__${n}.ts`, + fileName: buildFileName(source, root, n), }); } i = close; // skip to the close fence @@ -128,7 +178,9 @@ function extractFromFile(source: string): { examples: Example[]; orphans: number const orphans: number[] = []; for (let i = 0; i < lines.length; i++) { - if (lines[i].trim() === MARKER && !claimed.has(i)) orphans.push(i + 1); // 1-based + // Any marker spelling counts as an orphan claim — a wrong-format marker + // checks nothing, which is precisely what this guard exists to catch. + if (ALL_MARKERS.includes(lines[i].trim()) && !claimed.has(i)) orphans.push(i + 1); // 1-based } return { examples, orphans }; } @@ -260,13 +312,13 @@ function fail(message: string): never { } function main() { - console.log('🧪 Type-checking skill TypeScript examples...\n'); + console.log('🧪 Type-checking prose TypeScript examples (skills + docs)...\n'); - const files = skillMarkdownFiles(); + const files = sourceFiles(); const examples: Example[] = []; const orphans: string[] = []; - for (const file of files) { - const { examples: found, orphans: bad } = extractFromFile(file); + for (const { file, root } of files) { + const { examples: found, orphans: bad } = extractFromFile(file, root); examples.push(...found); for (const line of bad) orphans.push(`${rel(file)}:${line}`); } @@ -276,7 +328,8 @@ function main() { // no marker, because it looks intentional. if (orphans.length > 0) { fail( - `Found ${MARKER} not directly above a \`\`\`ts / \`\`\`typescript fence:\n\n` + + `Found an os:check marker not directly above a \`\`\`ts / \`\`\`typescript fence\n` + + `(or written in the wrong comment syntax for its file type):\n\n` + orphans.map((o) => ` - ${o}`).join('\n') + `\n\n The marker must be the line IMMEDIATELY above the code fence (no blank\n` + ` line between). Move it, or remove it if the block should not be checked.`, @@ -288,10 +341,10 @@ function main() { // A gate that checks nothing must not report success. if (examples.length === 0) { fail( - `No skill examples are marked for type-checking.\n\n` + + `No prose examples are marked for type-checking.\n\n` + ` Mark a self-contained, compilable block by putting\n\n` + - ` ${MARKER}\n\n` + - ` on the line directly above its \`\`\`ts fence in a skills/**/*.md file.\n` + + SOURCE_ROOTS.map((r) => ` ${r.marker} (in ${rel(r.dir)}/**/*${r.ext})`).join('\n') + `\n\n` + + ` on the line directly above its \`\`\`ts fence in ${SOURCE_ROOTS.map((r) => `${rel(r.dir)}/**/*${r.ext}`).join(' or ')}.\n` + ` (If you just removed the last marker, that is almost certainly a mistake.)`, ); } @@ -319,14 +372,14 @@ function main() { const diags = parseDiagnostics(output); if (code === 0 && diags.length === 0) { - console.log(`✅ ${examples.length} skill examples type-check against @objectstack/spec`); + console.log(`✅ ${examples.length} prose examples type-check against @objectstack/spec`); if (!KEEP) fs.rmSync(BUILD_DIR, { recursive: true, force: true }); return; } - // Remap every diagnostic back to skills/**/SKILL.md: so the author + // Remap every diagnostic back to the source page: so the author // reads the error against the file they actually edit, not the throwaway copy. - console.error(`\n✗ Skill TypeScript examples do not compile against @objectstack/spec:\n`); + console.error(`\n✗ Prose TypeScript examples do not compile against @objectstack/spec:\n`); const grouped = new Map(); for (const d of diags) { const ex = byFile.get(d.file); @@ -348,7 +401,7 @@ function main() { console.error( `\n These are examples an AI copies verbatim. Fix the example to match the\n` + - ` current spec, or drop its ${MARKER} marker if it is an intentional fragment.\n`, + ` current spec, or drop its os:check marker if it is an intentional fragment.\n`, ); if (!KEEP) fs.rmSync(BUILD_DIR, { recursive: true, force: true }); process.exit(1);