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
55 changes: 55 additions & 0 deletions docs/audits/2026-06-react-tier-authoring-dogfood.md
Original file line number Diff line number Diff line change
@@ -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 (`<ObjectForm>`, `<ListView>`, `<ObjectChart>`, `<RecordHighlights>`,
`<RecordRelatedList>`, `<RecordPath>`, …) 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
`<ListView>` of accounts, and selecting one drives `<RecordHighlights>` +
`<ObjectChart>` + `<RecordRelatedList>` and a slide-out `<ObjectForm formType="drawer">`.
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 `<ObjectChart>`, and a `onSucces` typo of the
`onSuccess` callback on `<ObjectForm>`:

```
⚠ page "showcase_renewals_pipeline" › <ObjectForm>: <ObjectForm> has prop "onSucces" — did you mean "onSuccess"?
✗ React-source page prop check failed (1 issue)
• page "showcase_renewals_pipeline" › <ObjectChart>: <ObjectChart> 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.
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/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';
Expand Down Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions examples/app-showcase/src/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
132 changes: 132 additions & 0 deletions examples/app-showcase/src/pages/renewals-pipeline.page.ts
Original file line number Diff line number Diff line change
@@ -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
* <ObjectForm drawer> to update the account in place.
*
* Blocks exercised, with the contract props each accepts:
* <ListView> objectName(req) · viewType · filters · columns · searchableFields · navigation · onRowClick
* <RecordHighlights> objectName · recordId · fields · layout
* <RecordRelatedList> objectName · recordId · relationshipField · columns · limit · showViewAll · title
* <ObjectChart> objectName(req) · aggregate · title · showLegend
* <ObjectForm> 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 (
<div className="flex h-full gap-4 p-4">
<div className="flex w-1/2 flex-col gap-3">
<div>
<h1 className="text-lg font-semibold">Renewals Pipeline</h1>
<p className="text-sm text-muted-foreground">Work the renewal book by lifecycle stage.</p>
</div>
<div className="flex gap-2">
{STAGES.map((s) => (
<button
key={s.id}
onClick={() => { setStage(s.id); setSel(null); }}
className={
'rounded-md border px-3 py-1.5 text-sm ' +
(stage === s.id ? 'border-primary bg-primary/10 font-medium' : 'border-border')
}
>
{s.label}
</button>
))}
</div>
<ListView
objectName="showcase_account"
viewType="grid"
filters={['status', '=', stage]}
columns={['name', 'status']}
searchableFields={['name']}
navigation={{ mode: 'none' }}
onRowClick={(record) => { setSel(record.id); setEditing(false); }}
/>
</div>

<div className="flex w-1/2 flex-col gap-4 overflow-auto">
{!sel ? (
<div className="flex h-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
Select an account to see its renewal picture.
</div>
) : (
<>
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">Account 360</h2>
<button
onClick={() => setEditing(true)}
className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
>
Edit account
</button>
</div>

<RecordHighlights
objectName="showcase_account"
recordId={sel}
fields={['name', 'status']}
layout="horizontal"
/>

<ObjectChart
objectName="showcase_invoice"
aggregate={{ field: 'total', function: 'sum', groupBy: 'status' }}
title="Invoice value by status"
showLegend={true}
/>

<RecordRelatedList
objectName="showcase_account"
recordId={sel}
relationshipField="account"
columns={['name', 'status', 'total']}
limit={5}
showViewAll={true}
title="Invoices"
/>

{editing ? (
<ObjectForm
objectName="showcase_account"
mode="edit"
recordId={sel}
formType="drawer"
drawerSide="right"
drawerWidth="480px"
onSuccess={() => { setEditing(false); setReload((n) => n + 1); }}
onCancel={() => setEditing(false)}
/>
) : null}
</>
)}
</div>
</div>
);
}
`,
});