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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/app-showcase/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/ui/dashboards/index.j
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/ui/datasets/index.js';
import { allReports } from './src/ui/reports/index.js';
import { allActions } from './src/ui/actions/index.js';
import { CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, AccountCockpitPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/ui/pages/index.js';
import { CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/ui/pages/index.js';
import { allFlows } from './src/automation/flows/index.js';
import { allWebhooks } from './src/automation/webhooks/index.js';
import { allHooks } from './src/data/hooks/index.js';
Expand Down Expand Up @@ -168,7 +168,7 @@ export default defineStack({
apps: [ShowcaseApp],
portals: allPortals,
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
pages: [CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, AccountCockpitPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage],
pages: [CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage],
dashboards: [ChartGalleryDashboard, OpsDashboard],
books: allBooks,
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],
Expand Down
60 changes: 60 additions & 0 deletions examples/app-showcase/src/automation/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1204,9 +1204,69 @@ export const InboundTaskWebhookFlow = defineFlow({
],
});

/**
* Inquiry Purge — the worked `get_record` + `delete_record` example, closing
* the CRUD node quartet (create: InboundTaskWebhookFlow · update:
* ReassignWizardFlow · get + delete: here). A janitor flow: fetch the
* already-closed inquiries (records mode), gate on whether any exist, delete
* by the same filter, and report. Config keys follow the executor contract
* exactly — `objectName` + `filter` (Prime Directive #12: no
* `object`/`filters` aliases). `runAs: 'system'` because a janitor acts
* across owners; autolaunched with no record trigger — invoke it on demand
* (API/subflow) rather than on every write.
*/
export const InquiryPurgeFlow = defineFlow({
name: 'showcase_inquiry_purge',
label: 'Purge Closed Inquiries',
description: 'Deletes inquiries already marked closed — demonstrates get_record + delete_record.',
type: 'autolaunched',
runAs: 'system',
nodes: [
{ id: 'start', type: 'start', label: 'Start' },
{
id: 'purge_check',
type: 'get_record',
label: 'Find closed inquiries',
// records mode + outputVariable: the result lands in the variable
// context, where the out-edge CEL below reads it (the engine routes on
// EDGE conditions — same contract as BudgetApprovalFlow's gate).
config: {
objectName: 'showcase_inquiry',
filter: { status: 'closed' },
mode: 'records',
limit: 200,
outputVariable: 'closedInquiries',
},
},
{ id: 'any_found', type: 'decision', label: 'Anything to purge?' },
{
id: 'purge',
type: 'delete_record',
label: 'Delete them',
config: { objectName: 'showcase_inquiry', filter: { status: 'closed' } },
},
{
id: 'report',
type: 'notify',
label: 'Report cleanup',
config: { topic: 'inquiry_purge', message: 'Closed inquiries purged.' },
},
{ id: 'end', type: 'end', label: 'End' },
],
edges: [
{ id: 'e1', source: 'start', target: 'purge_check' },
{ id: 'e2', source: 'purge_check', target: 'any_found' },
{ id: 'e3', source: 'any_found', target: 'purge', label: 'yes', condition: 'size(closedInquiries) > 0' },
{ id: 'e4', source: 'any_found', target: 'end', label: 'no', condition: 'size(closedInquiries) == 0' },
{ id: 'e5', source: 'purge', target: 'report' },
{ id: 'e6', source: 'report', target: 'end' },
],
});

export const allFlows = [
TaskCompletedFlow,
ReassignWizardFlow,
InquiryPurgeFlow,
BudgetApprovalFlow,
InvoiceDualSignoffFlow,
OneTaskSignoffSubflow,
Expand Down
60 changes: 58 additions & 2 deletions examples/app-showcase/src/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
},
validation: {
status: 'demonstrated',
files: ['src/data/objects/account.object.ts', 'src/data/objects/task.object.ts'],
files: [
'src/data/objects/account.object.ts',
'src/data/objects/task.object.ts',
'src/data/objects/project.object.ts',
'src/data/objects/invoice.object.ts',
],
notes:
'Authored inline via object `validations`. Only the runtime-enforced rule types (state_machine/script/cross_field) are demonstrated; the 6 unenforced types are tracked in https://github.com/objectstack-ai/framework/issues/1475 (Prime Directive #10).',
'Authored inline via object `validations`. Every declared rule type is now write-path enforced (rule-validator dispatches all of state_machine/script/cross_field/format/json_schema/conditional — ADR-0020 "no silent no-ops", closing the #1475 gap) and each is demonstrated: state_machine (task/project), script+cross_field (project), format/json_schema/conditional (account). Field-level requiredWhen/readonlyWhen are likewise enforced and demonstrated on invoice.',
},
hook: { status: 'demonstrated', files: ['src/data/hooks/index.ts'] },
seed: { status: 'demonstrated', files: ['src/data/seed/index.ts'] },
Expand Down Expand Up @@ -241,6 +246,11 @@ export const COVERAGE = {
source: 'ActionType + ACTION_LOCATIONS',
coveredBy: 'ui/actions/index.ts (script/url/flow/modal/api/form across all locations)',
},
flowNodeTypes: {
source: 'FlowNodeAction',
coveredBy:
'automation/flows/index.ts — CRUD quartet (create: InboundTaskWebhookFlow, update: ReassignWizardFlow, get+delete: InquiryPurgeFlow), screen/approval/wait/subflow/map/connector_action across the chain; BPMN gateway/boundary forms waived (FLOW_NODE_WAIVERS) in favor of the ADR-0031 structured containers.',
},
capabilityChains: {
security: 'security/index.ts — roles + permission set (CRUD + FLS + RLS) + sharing + policy',
automation: 'automation/flows/index.ts (incl. approval nodes) + automation/webhooks/index.ts + automation/jobs/index.ts + system/emails/index.ts',
Expand All @@ -254,6 +264,52 @@ export const COVERAGE = {
},
} as const;

/**
* Built-in flow node types (FlowNodeAction) the showcase deliberately does
* not author, with the reason. The coverage test asserts every OTHER member
* of the enum appears in at least one flow, and that each waiver names a
* real enum member with a substantive reason — same demonstrated-or-waived
* contract as the metadata kinds.
*/
export const FLOW_NODE_WAIVERS: Record<string, string> = {
parallel_gateway:
'BPMN-interop lowering target — the author-facing form is the ADR-0031 structured `parallel` container (FanOutNotifyFlow); bpmn-mapping lowers it to the gateway pair.',
join_gateway:
'The AND-join half of the pair bpmn-mapping derives from a structured `parallel` container — never hand-authored in examples.',
boundary_event:
'BPMN-interop form; the author-facing equivalents the showcase demos are the ADR-0031 `try_catch` container (ResilientSyncFlow) and `wait` timers (TaskFollowUpFlow).',
};

/**
* Collect every node `type` used across a set of flow definitions, including
* nodes nested inside structured-container regions (ADR-0031: a `parallel` /
* `loop` / `try_catch` container carries sub-graphs in its config).
*/
export function collectFlowNodeTypes(flows: Array<{ nodes?: Array<Record<string, unknown>> }>): Set<string> {
const used = new Set<string>();
const visitNodes = (nodes: unknown): void => {
if (!Array.isArray(nodes)) return;
for (const node of nodes) {
if (!node || typeof node !== 'object') continue;
const n = node as Record<string, unknown>;
if (typeof n.type === 'string') used.add(n.type);
if (n.config) visitContainer(n.config);
}
};
// Walk any nested { nodes: [...] } region a container config may carry.
const visitContainer = (value: unknown): void => {
if (!value || typeof value !== 'object') return;
if (Array.isArray(value)) { for (const v of value) visitContainer(v); return; }
const obj = value as Record<string, unknown>;
for (const [key, v] of Object.entries(obj)) {
if (key === 'nodes') visitNodes(v);
else visitContainer(v);
}
};
for (const flow of flows) visitNodes(flow.nodes);
return used;
}

/** Collect every field `type` used across a set of object definitions. */
export function collectFieldTypes(objects: Array<{ fields?: Record<string, { type?: string }> }>): Set<string> {
const used = new Set<string>();
Expand Down
16 changes: 15 additions & 1 deletion examples/app-showcase/src/data/seed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BusinessUnit } from '../objects/business-unit.object.js';
import { Team, ProjectMembership } from '../objects/team.object.js';
import { Product, Invoice, InvoiceLine } from '../objects/invoice.object.js';
import { Contact } from '../objects/contact.object.js';
import { Inquiry } from '../objects/inquiry.object.js';
import { FieldZoo } from '../objects/field-zoo.object.js';

/**
Expand Down Expand Up @@ -222,6 +223,19 @@ const invoiceLines = defineSeed(InvoiceLine, {
],
});

// Inquiries so the staff triage list (inquiry views + Contact Form page)
// renders on first boot — the "every view renders real data" principle. One
// per status; the `closed` row doubles as live prey for InquiryPurgeFlow.
const inquiries = defineSeed(Inquiry, {
mode: 'upsert',
externalId: 'email',
records: [
{ name: 'Priya Raman', email: 'priya@meridian.example', company: 'Meridian Labs', message: 'Interested in the delivery workspace for a 40-person team.', status: 'new', source: 'website' },
{ name: 'Tom Okafor', email: 'tom@brightline.example', company: 'Brightline Co', message: 'Following up on the demo — can we scope an invoicing pilot?', status: 'contacted', source: 'referral' },
{ name: 'Lena Fischer', email: 'lena@oldrequest.example', company: 'Archived GmbH', message: 'Old request, already resolved by support.', status: 'closed', source: 'website' },
],
});

const preferences = defineSeed(Preference, {
mode: 'upsert',
externalId: 'name',
Expand All @@ -230,4 +244,4 @@ const preferences = defineSeed(Preference, {
],
});

export const ShowcaseSeedData = [accounts, contacts, products, projects, tasks, categories, businessUnits, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
export const ShowcaseSeedData = [accounts, contacts, inquiries, products, projects, tasks, categories, businessUnits, teams, memberships, fieldZoo, invoices, invoiceLines, preferences];
19 changes: 19 additions & 0 deletions examples/app-showcase/src/docs/showcase_index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,22 @@ ObjectStack 协议的活体一致性夹具:每种字段类型、视图类型、

本页自己必须遵守的撰写规则,见
[文档撰写指南](./showcase_docs_guide.md)。

## 分域导览

每个协议域一篇走查,与 `src/` 目录结构一一对应:

- [数据](./showcase_tour_data.md) — 对象、字段、校验规则、钩子、种子数据、
对象扩展、分析 cube
- [界面](./showcase_tour_ui.md) — 应用、视图、页面、仪表盘、报表、数据集、
动作、主题、门户
- [自动化](./showcase_tour_automation.md) — 流程与审批、定时任务、Webhook、
连接器
- [系统](./showcase_tour_system.md) — 数据源与联邦、国际化、邮件、
文档即元数据、自定义端点
- [安全](./showcase_tour_security.md) — 角色、权限集、Profile、共享规则、
行级安全

**AI(agent / tool / skill)**是第六个协议域,这里刻意没有演示:agent 由平台
自有(ADR-0063),开源框架只经 MCP 暴露 AI 能力。覆盖清单以豁免条目如实记账——
见 [framework#2610](https://github.com/objectstack-ai/framework/issues/2610)。
71 changes: 33 additions & 38 deletions examples/app-showcase/src/docs/showcase_metadata_views_guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Live Metadata Views in Docs
description: Embed live, read-only views of state machines, flows, and permissions directly in the prose.
description: How to embed live, read-only views of state machines, flows, and permissions directly in the prose.
---

# Live Metadata Views in Docs
Expand All @@ -11,54 +11,49 @@ ever shows *their own slice* of the system. It never shows the whole shape of a
process, the full set of legal state transitions, or who can do what across an
object.

This page embeds those **live, read-only views** straight into the prose with a
` ```metadata ` fenced block. Each view is **resolved at read time** from the
current metadata — change the underlying state machine and the diagram below
changes with it. Nothing here is a screenshot (ADR-0051).
A ` ```metadata ` fenced block embeds that **live, read-only view** straight
into the prose. Each view is **resolved at read time** from the current
metadata — change the underlying rule and the diagram changes with it. Nothing
is a screenshot (ADR-0051).

## A record lifecycle — state machine
## The mechanism

A Task moves across a board. Which moves are legal is governed by the
`task_status_flow` state machine on the `showcase_task` object. Here it is,
rendered live from that rule:
A fence body is flat `key: value` data (not code):

```metadata
type: state_machine
object: showcase_task
name: task_status_flow
```md
&#96;&#96;&#96;metadata
type: state_machine # one of: state_machine · flow · permission
object: showcase_task # state_machine only — the rule lives on an object
name: task_status_flow # the metadata name; linted for liveness at build
detail: business # flow only — fold technical nodes away
&#96;&#96;&#96;
```

Projects have their own lifecycle, including terminal (dead-end) states:

```metadata
type: state_machine
object: showcase_project
name: project_status_flow
```
Two guarantees keep embeds honest:

## A process — flow
- **Build-time liveness lint** — a dead same-package reference (typo'd name,
deleted rule) fails `os build`, exactly like a broken doc link.
- **Read-time resolution** — the rendered view is projected from the metadata
the server is running *now*, never a stale copy.

The reassignment wizard, shown at the **business altitude** — purely technical
steps (scripts, record I/O) are folded away so the reader sees the process, not
the plumbing:
Here is one live sample — the `task_status_flow` state machine on
`showcase_task`, the rule that governs which board moves are legal:

```metadata
type: flow
name: showcase_reassign_wizard
detail: business
type: state_machine
object: showcase_task
name: task_status_flow
```

## Who can do what — permission
## Where the embeds live now

The `showcase_contributor` permission set, as an object-access matrix:
The guided tour embeds each view type **in the context that explains it**:

```metadata
type: permission
name: showcase_contributor
```

---
- state machines (task and the project lifecycle with terminal states) — in
the [Data tour](./showcase_tour_data.md)
- a flow at business altitude — in the
[Automation tour](./showcase_tour_automation.md)
- a permission access-matrix — in the
[Security tour](./showcase_tour_security.md)

> These four views are not authored prose — they are the live metadata,
> projected read-only into the document. See
> [the showcase overview](./showcase_index.md) for the rest of the workspace.
Back to the [overview](./showcase_index.md).
6 changes: 6 additions & 0 deletions examples/app-showcase/src/docs/showcase_tour_automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Trigger the automation yourself: complete a Task (Mark Done) and watch the
threshold and the `showcase_budget_approval` chain lands in **Workspace →
Approvals**.

> **Heads-up:** completing one task fires **six** flows on purpose — each
> teaches a different mechanism for the same trigger (script side-effect,
> Slack connector, REST connector, subflow reuse, parallel fan-out,
> try/catch resilience). Watch the Runs panel to see them all land from a
> single record change; a real app would consolidate these.

## Jobs, webhooks, connectors

- `src/automation/jobs/` — interval/cron jobs behind the schedule trigger
Expand Down
8 changes: 8 additions & 0 deletions examples/app-showcase/src/docs/showcase_tour_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ object: showcase_task
name: task_status_flow
```

Projects carry their own lifecycle, including terminal (dead-end) states:

```metadata
type: state_machine
object: showcase_project
name: project_status_flow
```

## Hooks & seed data

- `src/data/hooks/` — data-layer lifecycle hooks (before/after CRUD).
Expand Down
3 changes: 1 addition & 2 deletions examples/app-showcase/src/ui/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ export const ShowcaseApp = App.create({
children: [
{ id: 'nav_crm_workbench', type: 'page', pageName: 'showcase_crm_workbench', label: 'CRM Workbench · master/detail', icon: 'layout-dashboard' },
{ id: 'nav_task_desk', type: 'page', pageName: 'showcase_task_desk', label: 'Task Desk · drawer & modal', icon: 'panel-right-open' },
{ id: 'nav_account_cockpit', type: 'page', pageName: 'showcase_account_cockpit', label: 'Account Cockpit · live rollup', icon: 'satellite' },
{ id: 'nav_renewals_pipeline', type: 'page', pageName: 'showcase_renewals_pipeline', label: 'Renewals Pipeline · record blocks', icon: 'refresh-cw' },
{ id: 'nav_renewals_pipeline', type: 'page', pageName: 'showcase_renewals_pipeline', label: 'Renewals Pipeline · rollups & blocks', icon: 'refresh-cw' },
],
},
],
Expand Down
Loading