From 43789ba4cc48861ffb281c0e21fd3978b4af0784 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:11:47 +0800 Subject: [PATCH] docs(skills): author the 16 factory domains via defineX, not bare literals (#2035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example apps were migrated to the `defineX` factories in #2088/#2095 and a lint guard keeps them clean — but the skills and hand-written docs still taught the old bare-literal pattern (`: Page = {}`, `: Action = {}`, `: PermissionSet = {}`, …). Skills are the corpus AI authors from, so this directly undercut the #2035 north star: leaving the unsafe pattern as the thing AI copies. Converts every bare output/Input-type metadata literal for the 16 factory domains to its `defineX(...)` factory call, across: - skills/objectstack-ui/SKILL.md (8: Page/Action/Report/Cube) - content/docs/guides/security.mdx (8: PermissionSet/SharingRule) - content/docs/{guides/standards,concepts/index,concepts/architecture, getting-started/architecture,protocol/objectui/record-alert}.* (7) Imports are rewritten in place (preserving each block's import source — root vs subpath) or injected where the snippet had none. Pre-existing `defineView` / `defineFlow` examples are untouched. `check:skill-docs` stays green (generated references come from frontmatter, unchanged). Docs-only — no package code, no changeset. Code blocks in MDX/skills are not type-checked by any gate, which is why they drifted; a lightweight MDX code-block lint is noted as a separate follow-up. --- content/docs/concepts/architecture.mdx | 6 +-- content/docs/concepts/index.mdx | 10 ++-- content/docs/getting-started/architecture.mdx | 6 +-- content/docs/guides/security.mdx | 46 ++++++++++-------- content/docs/guides/standards.mdx | 12 ++--- .../docs/protocol/objectui/record-alert.mdx | 6 +-- skills/objectstack-ui/SKILL.md | 48 +++++++++++-------- 7 files changed, 75 insertions(+), 59 deletions(-) diff --git a/content/docs/concepts/architecture.mdx b/content/docs/concepts/architecture.mdx index 5b930d9cd4..c9a6ce769a 100644 --- a/content/docs/concepts/architecture.mdx +++ b/content/docs/concepts/architecture.mdx @@ -144,12 +144,12 @@ That's the job of the other layers. ```typescript // packages/crm/src/permissions/sales_rep.permission.ts -import type { PermissionSet } from '@objectstack/spec/security'; +import { definePermissionSet } from '@objectstack/spec/security'; // A permission set is keyed by object name. Each object grants the four // CRUD verbs (allowCreate / allowRead / allowEdit / allowDelete), and // optional field-level security keyed by "object.field". -export const SalesRep: PermissionSet = { +export const SalesRep = definePermissionSet({ name: 'sales_rep', isProfile: true, objects: { @@ -163,7 +163,7 @@ export const SalesRep: PermissionSet = { fields: { 'customer.annual_revenue': { readable: true, editable: false }, // Read-only }, -}; +}); ``` ### Example: Workflow Automation diff --git a/content/docs/concepts/index.mdx b/content/docs/concepts/index.mdx index 12ccc1c19b..c403f7ed6a 100644 --- a/content/docs/concepts/index.mdx +++ b/content/docs/concepts/index.mdx @@ -150,9 +150,9 @@ Access control is declared as **permission set** metadata (the `PermissionSet` s ```typescript // packages/crm/src/permissions/sales_rep.permission.ts -import type { PermissionSet } from '@objectstack/spec'; +import { definePermissionSet } from '@objectstack/spec'; -export const salesRep: PermissionSet = { +export const salesRep = definePermissionSet({ name: 'sales_rep', label: 'Sales Rep', isProfile: true, @@ -167,10 +167,10 @@ export const salesRep: PermissionSet = { fields: { 'customer.annual_revenue': { readable: true, editable: false }, // Read-only }, -}; +}); // packages/crm/src/permissions/sales_manager.permission.ts -export const salesManager: PermissionSet = { +export const salesManager = definePermissionSet({ name: 'sales_manager', label: 'Sales Manager', isProfile: true, @@ -182,7 +182,7 @@ export const salesManager: PermissionSet = { allowDelete: true, }, }, -}; +}); ``` ### Example: Flow Automation diff --git a/content/docs/getting-started/architecture.mdx b/content/docs/getting-started/architecture.mdx index ceb9c5651f..665cb8f242 100644 --- a/content/docs/getting-started/architecture.mdx +++ b/content/docs/getting-started/architecture.mdx @@ -144,10 +144,10 @@ That's the job of the other layers. ```typescript // src/permissions/sales_rep.permission.ts -import type { PermissionSet } from '@objectstack/spec'; +import { definePermissionSet } from '@objectstack/spec'; // A permission set (here used as a profile) keyed by object and field. -export const SalesRepPermission: PermissionSet = { +export const SalesRepPermission = definePermissionSet({ name: 'sales_rep', label: 'Sales Rep', isProfile: true, @@ -163,7 +163,7 @@ export const SalesRepPermission: PermissionSet = { // . -> field-level security 'customer.annual_revenue': { readable: true, editable: false }, // Read-only }, -}; +}); ``` ### Example: Workflow Automation diff --git a/content/docs/guides/security.mdx b/content/docs/guides/security.mdx index 3e8bcbdc49..2392282ccb 100644 --- a/content/docs/guides/security.mdx +++ b/content/docs/guides/security.mdx @@ -62,9 +62,9 @@ A profile is modelled as a `PermissionSet` with `isProfile: true` (see `PermissionSetSchema`. ```typescript -import type { PermissionSet } from '@objectstack/spec/security'; +import { definePermissionSet } from '@objectstack/spec/security'; -export const SalesRepProfile: PermissionSet = { +export const SalesRepProfile = definePermissionSet({ name: 'sales_rep', label: 'Sales Representative', isProfile: true, @@ -101,7 +101,7 @@ export const SalesRepProfile: PermissionSet = { app_admin: 'hidden', // Not visible app_sales: 'visible', // Available }, -}; +}); ``` ### Object Permission Levels @@ -153,7 +153,9 @@ RLS (no compiler change; ADR-0055). Sharing rules still widen on top. #### Sales Representative ```typescript -export const SalesRepProfile: PermissionSet = { +import { definePermissionSet } from '@objectstack/spec/security'; + +export const SalesRepProfile = definePermissionSet({ name: 'sales_rep', isProfile: true, objects: { @@ -167,13 +169,15 @@ export const SalesRepProfile: PermissionSet = { fields: { 'account.annual_revenue': { readable: true, editable: false }, // Read-only }, -}; +}); ``` #### Sales Manager ```typescript -export const SalesManagerProfile: PermissionSet = { +import { definePermissionSet } from '@objectstack/spec/security'; + +export const SalesManagerProfile = definePermissionSet({ name: 'sales_manager', isProfile: true, objects: { @@ -187,13 +191,15 @@ export const SalesManagerProfile: PermissionSet = { }, // ... full access to sales objects }, -}; +}); ``` #### Service Agent ```typescript -export const ServiceAgentProfile: PermissionSet = { +import { definePermissionSet } from '@objectstack/spec/security'; + +export const ServiceAgentProfile = definePermissionSet({ name: 'service_agent', isProfile: true, objects: { @@ -206,7 +212,7 @@ export const ServiceAgentProfile: PermissionSet = { 'case.is_sla_violated': { readable: true, editable: false }, 'case.resolution_time_hours': { readable: true, editable: false }, }, -}; +}); ``` --- @@ -216,9 +222,9 @@ export const ServiceAgentProfile: PermissionSet = { Permission sets extend profile permissions without changing the profile. ```typescript -import type { PermissionSet } from '@objectstack/spec/security'; +import { definePermissionSet } from '@objectstack/spec/security'; -export const AdvancedReportingPermissionSet: PermissionSet = { +export const AdvancedReportingPermissionSet = definePermissionSet({ name: 'advanced_reporting', label: 'Advanced Reporting', @@ -238,15 +244,15 @@ export const AdvancedReportingPermissionSet: PermissionSet = { // System permissions are a flat array of capability strings systemPermissions: ['run_reports', 'export_reports', 'create_dashboards'], -}; +}); -export const BulkDataPermissionSet: PermissionSet = { +export const BulkDataPermissionSet = definePermissionSet({ name: 'bulk_data_access', label: 'Bulk Data Access', objects: {}, systemPermissions: ['bulk_api', 'view_all_data'], -}; +}); ``` ### Built-in Permission Sets @@ -418,9 +424,9 @@ export const OrganizationDefaults = { Share records based on field criteria: ```typescript -import type { SharingRule } from '@objectstack/spec/security'; +import { defineSharingRule } from '@objectstack/spec/security'; -export const AccountTeamSharingRule: SharingRule = { +export const AccountTeamSharingRule = defineSharingRule({ name: 'account_team_sharing', label: 'Share Active Customers with Sales Team', type: 'criteria', @@ -437,7 +443,7 @@ export const AccountTeamSharingRule: SharingRule = { // Access level granted: read | edit | full accessLevel: 'edit', -}; +}); ``` #### Recipient types @@ -466,7 +472,9 @@ evaluated (`afterInsert` / `afterUpdate`); `role_and_subordinates` walks the Share based on record owner characteristics: ```typescript -export const OpportunityOwnerSharingRule: SharingRule = { +import { defineSharingRule } from '@objectstack/spec/security'; + +export const OpportunityOwnerSharingRule = defineSharingRule({ name: 'opportunity_owner_sharing', label: 'Share Sales Rep Opportunities with Managers', type: 'owner', @@ -485,7 +493,7 @@ export const OpportunityOwnerSharingRule: SharingRule = { }, accessLevel: 'read', -}; +}); ``` ### Analytics and Dataset Read Scope diff --git a/content/docs/guides/standards.mdx b/content/docs/guides/standards.mdx index cbdab82f53..3c73b0a46e 100644 --- a/content/docs/guides/standards.mdx +++ b/content/docs/guides/standards.mdx @@ -241,9 +241,9 @@ A profile is just a `PermissionSet` with `isProfile: true`. There is no separate `Profile` type — both share the same `PermissionSetSchema`. ```typescript -import type { PermissionSet } from '@objectstack/spec/security'; +import { definePermissionSet } from '@objectstack/spec/security'; -export const MyProfile: PermissionSet = { +export const MyProfile = definePermissionSet({ name: 'profile_name', label: 'Profile Label', isProfile: true, @@ -271,15 +271,15 @@ export const MyProfile: PermissionSet = { app_sales: 'visible', // Visible app_admin: 'hidden', // Hidden }, -}; +}); ``` ### Sharing Rule Pattern ```typescript -import type { SharingRule } from '@objectstack/spec/security'; +import { defineSharingRule } from '@objectstack/spec/security'; -export const MySharingRule: SharingRule = { +export const MySharingRule = defineSharingRule({ name: 'rule_name', object: 'object_name', type: 'criteria', // 'criteria' | 'owner' @@ -294,7 +294,7 @@ export const MySharingRule: SharingRule = { }, accessLevel: 'edit', // 'read' | 'edit' | 'full' -}; +}); ``` --- diff --git a/content/docs/protocol/objectui/record-alert.mdx b/content/docs/protocol/objectui/record-alert.mdx index 79863cefd9..e2d0bd7ef2 100644 --- a/content/docs/protocol/objectui/record-alert.mdx +++ b/content/docs/protocol/objectui/record-alert.mdx @@ -41,9 +41,9 @@ 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`: ```ts -import type { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; -export const SysUserDetailPage: Page = { +export const SysUserDetailPage = definePage({ name: 'sys_user_detail', label: 'User', object: 'sys_user', @@ -70,7 +70,7 @@ export const SysUserDetailPage: Page = { ], // …other slots: header, actions, highlights, details, tabs, discussion }, -}; +}); ``` The `alerts` slot is rendered between the page header (`header/actions`) and the highlights strip — the standard enterprise UX placement for contextual notices. diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index ddf0a3d72f..c5a4893602 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -452,13 +452,13 @@ filter; keep `filter` on the widget when binding a dataset. ### Report Configuration ```typescript -import type { ReportInput } from '@objectstack/spec/ui'; +import { defineReport } from '@objectstack/spec/ui'; // ADR-0021: a report binds a `dataset` and selects `rows` (dimensions) + // `values` (measures) BY NAME. The `opportunity_metrics` dataset defines the // object, the `amount_sum` measure, and the `forecast_category` + `close_date` // (dateGranularity: 'quarter') dimensions — see Guides → Analytics Datasets. -export const PipelineCoverageReport: ReportInput = { +export const PipelineCoverageReport = defineReport({ name: 'pipeline_coverage_by_quarter', label: 'Pipeline Coverage (Quarter)', type: 'matrix', @@ -469,7 +469,7 @@ export const PipelineCoverageReport: ReportInput = { runtimeFilter: { stage: { $ne: 'closed_lost' } }, // drilldown defaults true — click a cell to open the underlying records; set false to disable. chart: { type: 'bar', xAxis: 'forecast_category', yAxis: 'amount_sum' }, -}; +}); ``` > **`dateGranularity`** lives on the dataset's date **dimension** @@ -514,7 +514,9 @@ duplicate asset. of its own — never restate what the view already defines. ```typescript -export const TaskWorkbenchPage: Page = { +import { definePage } from '@objectstack/spec/ui'; + +export const TaskWorkbenchPage = definePage({ name: 'task_workbench', type: 'list', object: 'task', @@ -525,7 +527,7 @@ export const TaskWorkbenchPage: Page = { appearance: { allowedVisualizations: ['grid'] }, // locked userActions: { sort: true, search: true, filter: false }, }, -}; +}); ``` --- @@ -570,10 +572,10 @@ which contain components. ### Example — Record Detail Page ```typescript -import { Page } from '@objectstack/spec/ui'; +import { definePage } from '@objectstack/spec/ui'; import { ConvertLeadAction } from '../actions/lead.actions'; -export const LeadDetailPage: Page = { +export const LeadDetailPage = definePage({ name: 'lead_detail_page', label: 'Lead Detail', type: 'record_detail', @@ -613,7 +615,7 @@ export const LeadDetailPage: Page = { }, // left_sidebar / main / right_sidebar regions follow… ], -}; +}); ``` > **Variable substitution** — `{first_name}`, `{current_user.first_name}`, @@ -1026,9 +1028,9 @@ widgets can compose without hand-rolling each query. Register under `defineStack({ analyticsCubes: [...] })`. ```typescript -import type { Cube } from '@objectstack/spec/data'; +import { defineCube } from '@objectstack/spec/data'; -export const opportunityCube: Cube = { +export const opportunityCube = defineCube({ name: 'opportunity', title: 'Opportunities', sql: 'opportunity', // underlying object name (snake_case) @@ -1044,7 +1046,7 @@ export const opportunityCube: Cube = { account_industry: { name: 'account_industry', label: 'Industry', type: 'string', sql: 'account.industry' }, owner: { name: 'owner', label: 'Owner', type: 'string', sql: 'owner' }, }, -}; +}); ``` ### Cube Best Practices @@ -1102,7 +1104,9 @@ the current record. `record.` resolves identically on every surface wrap a predicate in `${…}` or `{…}` braces (see `objectstack-formula`). ```typescript -export const ReassignLeadAction: Action = { +import { defineAction } from '@objectstack/spec/ui'; + +export const ReassignLeadAction = defineAction({ name: 'reassign_lead', label: 'Reassign Lead', objectName: 'lead', @@ -1115,7 +1119,7 @@ export const ReassignLeadAction: Action = { undoable: true, // success toast offers Undo; Ctrl+Z works too successMessage: 'Lead reassigned.', errorMessage: "Couldn't reassign this lead — try again.", -}; +}); ``` ### Examples @@ -1123,10 +1127,10 @@ export const ReassignLeadAction: Action = { **Flow-typed action** (delegates to a screen flow): ```typescript -import type { Action } from '@objectstack/spec/ui'; +import { defineAction } from '@objectstack/spec/ui'; import { P } from '@objectstack/spec'; -export const ConvertLeadAction: Action = { +export const ConvertLeadAction = defineAction({ name: 'convert_lead', label: 'Convert Lead', objectName: 'lead', @@ -1138,13 +1142,15 @@ export const ConvertLeadAction: Action = { confirmText: 'Are you sure you want to convert this lead?', successMessage: 'Lead converted successfully!', refreshAfter: true, -}; +}); ``` **Modal-typed action** (collect params, then execute server body): ```typescript -export const AddToCampaignAction: Action = { +import { defineAction } from '@objectstack/spec/ui'; + +export const AddToCampaignAction = defineAction({ name: 'create_campaign', label: 'Add to Campaign', objectName: 'lead', @@ -1173,7 +1179,7 @@ export const AddToCampaignAction: Action = { }, successMessage: 'Leads added to campaign!', refreshAfter: true, -}; +}); ``` ### Opening in a New Tab (`opensInNewTab` / `newTabUrl`) @@ -1188,7 +1194,9 @@ alongside `opensInNewTab: true`, and the target endpoint must enforce its own auth (the new tab carries no in-app session context). ```typescript -export const OpenInvoicePdfAction: Action = { +import { defineAction } from '@objectstack/spec/ui'; + +export const OpenInvoicePdfAction = defineAction({ name: 'open_invoice_pdf', label: 'Open PDF', objectName: 'invoice', @@ -1196,7 +1204,7 @@ export const OpenInvoicePdfAction: Action = { opensInNewTab: true, newTabUrl: '/api/v1/invoice/{recordId}/pdf', // zero-roundtrip; endpoint self-auths locations: ['record_header'], -}; +}); ``` ### Action Parameter Patterns