diff --git a/docs/audits/2026-06-react-tier-authoring-dogfood.md b/docs/audits/2026-06-react-tier-authoring-dogfood.md new file mode 100644 index 0000000000..29e8145d43 --- /dev/null +++ b/docs/audits/2026-06-react-tier-authoring-dogfood.md @@ -0,0 +1,55 @@ +# React-tier authoring dogfood (ADR-0081) + +Goal: prove the loop the react-tier was built for actually closes — **an author +(human or AI) writes a `kind:'react'` page knowing every component's props from +the contract, and `os validate` catches it when they don't.** Not "the gate has +unit tests" — an end-to-end run through the real CLI on a real app. + +## The loop + +1. **Contract** — `skills/objectstack-ui/references/react-blocks.md` lists every + injected block (``, ``, ``, ``, + ``, ``, …) and the exact props each accepts, + tagged `data` / `binding` / `controlled` / `callback`. It is **generated** from + the spec schemas (`packages/spec/src/ui/react-blocks.ts`), so it can't drift + into fiction. +2. **Author** — `examples/app-showcase/src/pages/renewals-pipeline.page.ts` was + written straight from that contract (no guessing): a renewals manager works a + `` of accounts, and selecting one drives `` + + `` + `` and a slide-out ``. + Five server-connected blocks, every prop taken from the contract. +3. **Gate** — `os validate` step 3d (`validateReactPageProps`, ADR-0081 Phase 2) + parses each react page's real JSX and checks block usage against the contract. + +## Evidence + +**Authored-correctly → passes.** With the page wired into the showcase stack: + +``` +→ Checking React-source page props (ADR-0081)... +✓ Validation passed (98ms) # exit 0 +``` + +**Authored-wrong → caught.** Injecting two realistic mistakes — dropping the +required `objectName` binding on ``, and a `onSucces` typo of the +`onSuccess` callback on ``: + +``` +⚠ page "showcase_renewals_pipeline" › : has prop "onSucces" — did you mean "onSuccess"? +✗ React-source page prop check failed (1 issue) + • page "showcase_renewals_pipeline" › : is missing the required prop "objectName". + rule: react-prop-missing-required at pages[29].source + # exit 1 +``` + +The missing required binding is an **error** (fails the build); the near-miss prop +name is a **warning** (likely typo, surfaced but non-fatal — the contract's data +props are a curated subset so arbitrary unknown props aren't flagged, keeping +false positives near zero). + +## Conclusion + +The three pieces — **generated contract**, **author reads it**, **validate enforces +it** — compose into a working loop. An AI handed `react-blocks.md` writes correct +props, and a wrong prop is caught at `os validate` time before it ever renders. +`renewals-pipeline.page.ts` stays in the showcase as the golden, validated example. diff --git a/examples/app-showcase/objectstack.config.ts b/examples/app-showcase/objectstack.config.ts index 431b36238a..f1ea0032e9 100644 --- a/examples/app-showcase/objectstack.config.ts +++ b/examples/app-showcase/objectstack.config.ts @@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js'; import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js'; import { allReports } from './src/reports/index.js'; import { allActions } from './src/actions/index.js'; -import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage } from './src/pages/index.js'; +import { ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/pages/index.js'; import { allFlows } from './src/flows/index.js'; import { allWebhooks } from './src/webhooks/index.js'; import { allHooks } from './src/hooks/index.js'; @@ -154,7 +154,7 @@ export default defineStack({ apps: [ShowcaseApp], portals: allPortals, views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews], - pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage], + pages: [ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, AccountDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, InquiryTriagePage, AccountCockpitPage, InvoiceConsolePage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage], dashboards: [ChartGalleryDashboard, OpsDashboard], books: allBooks, datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset], diff --git a/examples/app-showcase/src/pages/index.ts b/examples/app-showcase/src/pages/index.ts index cf0fd72a5f..18259d52ab 100644 --- a/examples/app-showcase/src/pages/index.ts +++ b/examples/app-showcase/src/pages/index.ts @@ -23,6 +23,7 @@ export { InvoiceConsolePage } from './invoice-console.page.js'; export { TaskDeskPage } from './task-desk.page.js'; export { PageVariablesPage } from './page-variables.page.js'; export { ContactFormPage } from './contact-form.page.js'; +export { RenewalsPipelinePage } from './renewals-pipeline.page.js'; export { TaskBoardPage, TaskCalendarPage, diff --git a/examples/app-showcase/src/pages/renewals-pipeline.page.ts b/examples/app-showcase/src/pages/renewals-pipeline.page.ts new file mode 100644 index 0000000000..f7d2770bb2 --- /dev/null +++ b/examples/app-showcase/src/pages/renewals-pipeline.page.ts @@ -0,0 +1,132 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { definePage } from '@objectstack/spec/ui'; + +/** + * Renewals Pipeline — a `kind:'react'` business scenario (ADR-0081). + * + * Authored as a DOGFOOD of the react-tier component contract + * (skills/objectstack-ui/references/react-blocks.md): every block prop below is + * taken straight from the contract — no guessing. A renewals manager works a list + * of accounts on the left; selecting one drives a 360° panel on the right + * (highlights + the account's invoices + a value-by-status chart) and a slide-out + * to update the account in place. + * + * Blocks exercised, with the contract props each accepts: + * objectName(req) · viewType · filters · columns · searchableFields · navigation · onRowClick + * objectName · recordId · fields · layout + * objectName · recordId · relationshipField · columns · limit · showViewAll · title + * objectName(req) · aggregate · title · showLegend + * objectName(req) · mode · recordId · formType · drawerSide · drawerWidth · onSuccess · onCancel + */ +export const RenewalsPipelinePage = definePage({ + name: 'showcase_renewals_pipeline', + label: 'Renewals Pipeline (React)', + type: 'home', + kind: 'react', + source: ` +function Page() { + const [sel, setSel] = React.useState(null); + const [editing, setEditing] = React.useState(false); + const [reload, setReload] = React.useState(0); + const [stage, setStage] = React.useState('active'); + + const STAGES = [ + { id: 'active', label: 'Active' }, + { id: 'at_risk', label: 'At risk' }, + { id: 'churned', label: 'Churned' }, + ]; + + return ( +
+
+
+

Renewals Pipeline

+

Work the renewal book by lifecycle stage.

+
+
+ {STAGES.map((s) => ( + + ))} +
+ { setSel(record.id); setEditing(false); }} + /> +
+ +
+ {!sel ? ( +
+ Select an account to see its renewal picture. +
+ ) : ( + <> +
+

Account 360

+ +
+ + + + + + + + {editing ? ( + { setEditing(false); setReload((n) => n + 1); }} + onCancel={() => setEditing(false)} + /> + ) : null} + + )} +
+
+ ); +} +`, +});