Skip to content

Commit b0510ce

Browse files
os-zhuangclaude
andauthored
docs(showcase): dogfood the react-tier authoring loop + golden Renewals page (ADR-0081) (#2489)
End-to-end proof that the react-tier loop closes: a kind:'react' page authored straight from the generated component contract (react-blocks.md) passes `os validate`, and the same page with a wrong prop is caught. - examples/app-showcase: add renewals-pipeline.page.ts — a Renewals Pipeline scenario exercising five server-connected blocks (ListView, RecordHighlights, ObjectChart, RecordRelatedList, ObjectForm-as-drawer), every prop taken from the contract. Wired into the stack; validates clean; typechecks. - docs/audits/2026-06-react-tier-authoring-dogfood.md: captures the run — clean page → "Validation passed" (exit 0); injecting a missing required objectName on <ObjectChart> + an onSucces typo on <ObjectForm> → prop gate flags the error (exit 1) and the typo warning. Demonstrates generated contract → author reads it → validate enforces it compose into a working loop. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c1b2ff7 commit b0510ce

4 files changed

Lines changed: 190 additions & 2 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# React-tier authoring dogfood (ADR-0081)
2+
3+
Goal: prove the loop the react-tier was built for actually closes — **an author
4+
(human or AI) writes a `kind:'react'` page knowing every component's props from
5+
the contract, and `os validate` catches it when they don't.** Not "the gate has
6+
unit tests" — an end-to-end run through the real CLI on a real app.
7+
8+
## The loop
9+
10+
1. **Contract**`skills/objectstack-ui/references/react-blocks.md` lists every
11+
injected block (`<ObjectForm>`, `<ListView>`, `<ObjectChart>`, `<RecordHighlights>`,
12+
`<RecordRelatedList>`, `<RecordPath>`, …) and the exact props each accepts,
13+
tagged `data` / `binding` / `controlled` / `callback`. It is **generated** from
14+
the spec schemas (`packages/spec/src/ui/react-blocks.ts`), so it can't drift
15+
into fiction.
16+
2. **Author**`examples/app-showcase/src/pages/renewals-pipeline.page.ts` was
17+
written straight from that contract (no guessing): a renewals manager works a
18+
`<ListView>` of accounts, and selecting one drives `<RecordHighlights>` +
19+
`<ObjectChart>` + `<RecordRelatedList>` and a slide-out `<ObjectForm formType="drawer">`.
20+
Five server-connected blocks, every prop taken from the contract.
21+
3. **Gate**`os validate` step 3d (`validateReactPageProps`, ADR-0081 Phase 2)
22+
parses each react page's real JSX and checks block usage against the contract.
23+
24+
## Evidence
25+
26+
**Authored-correctly → passes.** With the page wired into the showcase stack:
27+
28+
```
29+
→ Checking React-source page props (ADR-0081)...
30+
✓ Validation passed (98ms) # exit 0
31+
```
32+
33+
**Authored-wrong → caught.** Injecting two realistic mistakes — dropping the
34+
required `objectName` binding on `<ObjectChart>`, and a `onSucces` typo of the
35+
`onSuccess` callback on `<ObjectForm>`:
36+
37+
```
38+
⚠ page "showcase_renewals_pipeline" › <ObjectForm>: <ObjectForm> has prop "onSucces" — did you mean "onSuccess"?
39+
✗ React-source page prop check failed (1 issue)
40+
• page "showcase_renewals_pipeline" › <ObjectChart>: <ObjectChart> is missing the required prop "objectName".
41+
rule: react-prop-missing-required at pages[29].source
42+
# exit 1
43+
```
44+
45+
The missing required binding is an **error** (fails the build); the near-miss prop
46+
name is a **warning** (likely typo, surfaced but non-fatal — the contract's data
47+
props are a curated subset so arbitrary unknown props aren't flagged, keeping
48+
false positives near zero).
49+
50+
## Conclusion
51+
52+
The three pieces — **generated contract**, **author reads it**, **validate enforces
53+
it** — compose into a working loop. An AI handed `react-blocks.md` writes correct
54+
props, and a wrong prop is caught at `os validate` time before it ever renders.
55+
`renewals-pipeline.page.ts` stays in the showcase as the golden, validated example.

examples/app-showcase/objectstack.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChartGalleryDashboard, OpsDashboard } from './src/dashboards/index.js';
2222
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/datasets/index.js';
2323
import { allReports } from './src/reports/index.js';
2424
import { allActions } from './src/actions/index.js';
25-
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';
25+
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';
2626
import { allFlows } from './src/flows/index.js';
2727
import { allWebhooks } from './src/webhooks/index.js';
2828
import { allHooks } from './src/hooks/index.js';
@@ -154,7 +154,7 @@ export default defineStack({
154154
apps: [ShowcaseApp],
155155
portals: allPortals,
156156
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
157-
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],
157+
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],
158158
dashboards: [ChartGalleryDashboard, OpsDashboard],
159159
books: allBooks,
160160
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],

examples/app-showcase/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { InvoiceConsolePage } from './invoice-console.page.js';
2323
export { TaskDeskPage } from './task-desk.page.js';
2424
export { PageVariablesPage } from './page-variables.page.js';
2525
export { ContactFormPage } from './contact-form.page.js';
26+
export { RenewalsPipelinePage } from './renewals-pipeline.page.js';
2627
export {
2728
TaskBoardPage,
2829
TaskCalendarPage,
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { definePage } from '@objectstack/spec/ui';
4+
5+
/**
6+
* Renewals Pipeline — a `kind:'react'` business scenario (ADR-0081).
7+
*
8+
* Authored as a DOGFOOD of the react-tier component contract
9+
* (skills/objectstack-ui/references/react-blocks.md): every block prop below is
10+
* taken straight from the contract — no guessing. A renewals manager works a list
11+
* of accounts on the left; selecting one drives a 360° panel on the right
12+
* (highlights + the account's invoices + a value-by-status chart) and a slide-out
13+
* <ObjectForm drawer> to update the account in place.
14+
*
15+
* Blocks exercised, with the contract props each accepts:
16+
* <ListView> objectName(req) · viewType · filters · columns · searchableFields · navigation · onRowClick
17+
* <RecordHighlights> objectName · recordId · fields · layout
18+
* <RecordRelatedList> objectName · recordId · relationshipField · columns · limit · showViewAll · title
19+
* <ObjectChart> objectName(req) · aggregate · title · showLegend
20+
* <ObjectForm> objectName(req) · mode · recordId · formType · drawerSide · drawerWidth · onSuccess · onCancel
21+
*/
22+
export const RenewalsPipelinePage = definePage({
23+
name: 'showcase_renewals_pipeline',
24+
label: 'Renewals Pipeline (React)',
25+
type: 'home',
26+
kind: 'react',
27+
source: `
28+
function Page() {
29+
const [sel, setSel] = React.useState(null);
30+
const [editing, setEditing] = React.useState(false);
31+
const [reload, setReload] = React.useState(0);
32+
const [stage, setStage] = React.useState('active');
33+
34+
const STAGES = [
35+
{ id: 'active', label: 'Active' },
36+
{ id: 'at_risk', label: 'At risk' },
37+
{ id: 'churned', label: 'Churned' },
38+
];
39+
40+
return (
41+
<div className="flex h-full gap-4 p-4">
42+
<div className="flex w-1/2 flex-col gap-3">
43+
<div>
44+
<h1 className="text-lg font-semibold">Renewals Pipeline</h1>
45+
<p className="text-sm text-muted-foreground">Work the renewal book by lifecycle stage.</p>
46+
</div>
47+
<div className="flex gap-2">
48+
{STAGES.map((s) => (
49+
<button
50+
key={s.id}
51+
onClick={() => { setStage(s.id); setSel(null); }}
52+
className={
53+
'rounded-md border px-3 py-1.5 text-sm ' +
54+
(stage === s.id ? 'border-primary bg-primary/10 font-medium' : 'border-border')
55+
}
56+
>
57+
{s.label}
58+
</button>
59+
))}
60+
</div>
61+
<ListView
62+
objectName="showcase_account"
63+
viewType="grid"
64+
filters={['status', '=', stage]}
65+
columns={['name', 'status']}
66+
searchableFields={['name']}
67+
navigation={{ mode: 'none' }}
68+
onRowClick={(record) => { setSel(record.id); setEditing(false); }}
69+
/>
70+
</div>
71+
72+
<div className="flex w-1/2 flex-col gap-4 overflow-auto">
73+
{!sel ? (
74+
<div className="flex h-full items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
75+
Select an account to see its renewal picture.
76+
</div>
77+
) : (
78+
<>
79+
<div className="flex items-center justify-between">
80+
<h2 className="text-base font-semibold">Account 360</h2>
81+
<button
82+
onClick={() => setEditing(true)}
83+
className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
84+
>
85+
Edit account
86+
</button>
87+
</div>
88+
89+
<RecordHighlights
90+
objectName="showcase_account"
91+
recordId={sel}
92+
fields={['name', 'status']}
93+
layout="horizontal"
94+
/>
95+
96+
<ObjectChart
97+
objectName="showcase_invoice"
98+
aggregate={{ field: 'total', function: 'sum', groupBy: 'status' }}
99+
title="Invoice value by status"
100+
showLegend={true}
101+
/>
102+
103+
<RecordRelatedList
104+
objectName="showcase_account"
105+
recordId={sel}
106+
relationshipField="account"
107+
columns={['name', 'status', 'total']}
108+
limit={5}
109+
showViewAll={true}
110+
title="Invoices"
111+
/>
112+
113+
{editing ? (
114+
<ObjectForm
115+
objectName="showcase_account"
116+
mode="edit"
117+
recordId={sel}
118+
formType="drawer"
119+
drawerSide="right"
120+
drawerWidth="480px"
121+
onSuccess={() => { setEditing(false); setReload((n) => n + 1); }}
122+
onCancel={() => setEditing(false)}
123+
/>
124+
) : null}
125+
</>
126+
)}
127+
</div>
128+
</div>
129+
);
130+
}
131+
`,
132+
});

0 commit comments

Comments
 (0)