Skip to content

Commit e7ddd67

Browse files
os-zhuangclaude
andcommitted
docs(adr): ADR-0047 — two run modes for object UI (data views vs interface pages)
Formalizes the data-mode / interface-mode split for object list UI, the userFilters + runtime-visualization spec surface, asymmetric defaults, and the AI authoring decision rules. Grounded in live verification against app-showcase + objectui console (2026-06-12). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9fea621 commit e7ddd67

1 file changed

Lines changed: 234 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# ADR-0047: Two run modes for object UI — data views vs interface pages, user filters, and runtime visualization choice
2+
3+
**Status**: Proposed (2026-06-12)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0017](./0017-object-has-many-view.md) (independent view entities, `viewKind`), [ADR-0019](./0019-app-as-consumer-unit.md) (App is the consumer-facing unit — navigation decides what users see), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (draft · publish lifecycle), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (**AI is the long-term author of metadata — the design center this ADR inherits**)
6+
**Consumers**: `@objectstack/spec` (view + page Zod schemas), `@objectstack/objectql` (registry validation/diagnostics), `../objectui` (console `ObjectView` / `PageView`, `plugin-list`), framework templates (`hotcrm`, `app-showcase`), the `objectstack-ui` authoring skill
7+
8+
**Premise**: same as ADR-0033 — the platform is pre-launch, the primary author of UI metadata is an AI, and a human confirms. Every design choice below is therefore evaluated against two questions: *does it make the generated-by-default experience correct?* and *can an AI author get it wrong?*
9+
10+
---
11+
12+
## TL;DR
13+
14+
1. **Two run modes, both first-class.** *Data mode* (navigation → object): every list view of the object renders as a switcher tab; users may create personal views; the toolbar is permissive. *Interface mode* (navigation → page): an author-curated page **references** one view as its source and exposes only the controls the author enabled — filter tabs or dropdowns, a fixed (or whitelisted) visualization, selected user actions. This mirrors Airtable's Data vs Interfaces split and Power Platform's model-driven vs canvas split.
15+
2. **The iron rule: pages reference views, never restate them.** A page's `source` points at an object/view; columns, base filter, and sort are *inherited* from the view definition. The page schema carries presentation policy only — it has no field for columns, so the "page and view each declare columns, then drift" failure mode is unrepresentable.
16+
3. **`userFilters` becomes spec.** The end-user quick-filter surface (Airtable "User filters": element = `tabs | dropdown | toggle`, plus per-field config) is formalized in `ListViewSchema` and `InterfacePageConfig`. The client already implements and renders it (verified live, below); today it works only by accident of raw passthrough, with no type for authors and no Studio form.
17+
4. **Runtime visualization choice is an author-controlled whitelist.** `userActions.visualizations?: boolean | ViewType[]` at view level, `visualizations` at page level (superseding the misplaced `userFilters.elements` enum). Effective options = author whitelist ∩ types whose required field bindings resolve (kanban needs a select `groupBy`, calendar a date field, …). Data mode defaults open; interface mode defaults locked.
18+
5. **Defaults are asymmetric on purpose.** Data mode auto-derives quick filters from select/boolean fields and allows user views; interface mode is closed until the author opens it. An AI that emits *nothing* beyond objects + views + navigation gets a correct, complete system — "omission is correct" is the strongest guardrail we can give a generative author.
19+
6. **AI decision rule, encoded not implied.** Default output is objects + list views + navigation → objects. Interface pages are generated only on explicit signals (persona split, capability narrowing, portal/workspace language). The rule ships in the `objectstack-ui` skill; reference-integrity diagnostics become hard failures in the AI loop (per ADR-0033's draft gate).
20+
21+
---
22+
23+
## 1. Context — what exists (verified live, 2026-06-12)
24+
25+
The findings below come from running `examples/app-showcase` (:3000) against the
26+
objectui console (:5180), logging in, and exercising the HotCRM `crm_account`
27+
object — plus one runtime experiment: a view written through
28+
`PUT /api/v1/meta/view/:name` carrying `tabs` + `userFilters` that the spec does
29+
not declare.
30+
31+
**Already working:**
32+
33+
- **Auto-derived quick filters are live.** The `crm_account` list renders
34+
dropdown filters (类型 / 行业 / 是否活跃 / 更多) although the hotcrm template
35+
contains **zero** `userFilters` metadata — objectui's `ListView` derives them
36+
from the object's select/boolean fields. Selecting 行业=科技 correctly
37+
filters 5 → 2 records through the query pipeline.
38+
- **Metadata-driven `userFilters` round-trips end-to-end.** The experimental
39+
view saved with `userFilters: { element: 'dropdown', fields: [行业, 评级] }`
40+
was stored verbatim (registry validation is warn-only and stores the raw
41+
item), echoed back intact with `_diagnostics.valid: true`, and the console
42+
rendered **exactly the two configured dropdowns**, replacing the auto-derived
43+
set. The client component (`plugin-list/UserFilters.tsx`) supports all three
44+
elements (`dropdown` / `tabs` / `toggle`), lookup-backed options, counts and
45+
defaults.
46+
- **View switcher + user-created views (data mode)** work: 7 artifact views,
47+
overflow menu, "Manage all views…", runtime overlay create/delete (ADR-0005).
48+
- **Runtime visualization switching exists as a dormant component**
49+
(`plugin-list/ViewSwitcher.tsx`), gated by a `showViewSwitcher` prop that
50+
only Studio's view preview enables.
51+
52+
**Broken or absent:**
53+
54+
- **`ListViewSchema` has no `userFilters`.** The working client behavior has
55+
no spec type: TS authors get excess-property errors, Studio cannot render a
56+
form for it, and an AI grounded in the Zod schema will never emit it.
57+
- **View-level `tabs` don't reach the renderer.** The experiment's three
58+
filter tabs (全部 / 科技公司 / 金融公司) were stored and served but never
59+
rendered — the console's `ObjectView` forwards `listSchema.tabs` and drops
60+
`viewDef.tabs` (a one-line gap).
61+
- **`InterfacePageConfig` is schema-only.** No page in the running system
62+
carries `interfaceConfig`; nothing renders it. Its
63+
`userFilters.elements: ['grid','gallery','kanban']` enum conflates
64+
*visualization choice* with *filter element type*.
65+
- **No template exercises any of this.** `userFilters`, view `tabs`,
66+
`filterableFields`: zero occurrences across `app-showcase` and the
67+
`hotcrm`/`hr` templates — so the capability is invisible to both humans
68+
and few-shot-grounded AI authors.
69+
- **Registry validation is warn-only** (`registry.validate()` logs and stores
70+
the raw item). Tolerable for humans; for an AI loop it means a misspelled
71+
field name produces no feedback at all.
72+
73+
## 2. Prior art
74+
75+
| Platform | Data-mode equivalent | Interface-mode equivalent | Lesson taken |
76+
|---|---|---|---|
77+
| **Airtable** | Data tab: every view user-creatable, grid-first | Interfaces: author-curated pages; *User filters* (Elements: tabs / dropdowns), *Appearance → Visualizations* fixed by author, *User actions* toggles | The panel we are matching feature-for-feature; end users never create views inside an interface |
78+
| **Salesforce** | Object list views (user-creatable, admin-shareable, pinnable) | Lightning App Builder pages (curated regions/components) | List views remain the data asset; pages compose them. Personal vs shared views are permission-governed |
79+
| **Microsoft Power Platform** | Model-driven apps: UI *generated* from Dataverse views/forms | Canvas apps: fully curated | "Generated-by-default is correct" scales; full curation is opt-in because it carries maintenance cost |
80+
| **Notion** | Database with multiple views | Linked database views embedded in pages | Embeds are *references* to the source database — the iron rule in the wild |
81+
| **Retool (counter-example)** || Everything is a hand-built page | With no generated data mode, every list costs author time and drifts from schema changes — what we avoid by keeping data mode primary |
82+
83+
## 3. Decision
84+
85+
### 3.1 The model: three layers, navigation decides the mode
86+
87+
| Layer | Metadata | Role | Required? |
88+
|---|---|---|---|
89+
| Data asset | `object` + its list views (ADR-0017 view entities) | Authoritative definition of columns/filter/sort; serves data mode | Yes — every object |
90+
| Entry | `app.navigation` (ADR-0019) | `type: 'object'` → data mode; `type: 'page'` → interface mode | Yes |
91+
| Presentation | `page.interfaceConfig` | Curated end-user surface; references a view, adds policy | Optional, signal-driven |
92+
93+
The same object may appear behind both entries (ops team gets the object,
94+
business users get a page). The renderer (`plugin-list/ListView`) is shared;
95+
`ObjectView` and `PageView` are two orchestrators feeding it different schema.
96+
97+
### 3.2 The iron rule
98+
99+
`interfaceConfig` carries **no column, no base-filter, no sort definitions**
100+
only: `source` (object or object+view), `userFilters`, `visualizations`,
101+
`userActions`, `addRecord`, `showRecordCount`, and cosmetic appearance. Columns
102+
and data semantics always come from the referenced view. Schema-level
103+
unrepresentability is the strongest anti-drift and anti-AI-error mechanism we
104+
have; we do not rely on lint to forbid what the type can simply not express.
105+
106+
### 3.3 Spec changes (`@objectstack/spec`)
107+
108+
1. **`UserFiltersSchema`** (new, `ui/view.zod.ts`), modeled on the proven
109+
objectui client type:
110+
111+
```ts
112+
export const UserFilterFieldSchema = z.object({
113+
field: z.string(), // must exist on the source object
114+
label: I18nLabelSchema.optional(),
115+
type: z.enum(['select','multi-select','boolean','date-range','text']).optional(), // default: infer
116+
options: z.array(OptionSchema).optional(), // default: derive from field def
117+
showCount: z.boolean().optional(),
118+
defaultValues: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(),
119+
});
120+
121+
export const UserFiltersSchema = z.object({
122+
element: z.enum(['dropdown','tabs','toggle']), // Airtable "Elements"
123+
fields: z.array(UserFilterFieldSchema).optional(), // dropdown/toggle
124+
tabs: z.array(ViewTabSchema).optional(), // tabs element reuses ViewTabSchema
125+
showAllRecords: z.boolean().optional(),
126+
});
127+
```
128+
129+
Attached as `ListViewSchema.userFilters` (view-level convenience) and
130+
`InterfacePageConfigSchema.userFilters` (the primary home).
131+
`filterableFields` stays as the legacy shorthand; loaders auto-upgrade it
132+
to `userFilters.fields` entries.
133+
134+
2. **Runtime visualization whitelist.**
135+
`UserActionsConfigSchema.visualizations?: z.union([z.boolean(), z.array(ViewTypeEnum)])`
136+
(view level) and `InterfacePageConfigSchema.visualizations` (page level,
137+
full 8-type enum). The existing `userFilters.elements` 3-type enum is kept
138+
one release as a deprecated alias and folded in.
139+
140+
3. **Semantics, not rendering.** The spec defines *what the author allows*;
141+
capability filtering (does this object have a select field for kanban
142+
`groupBy`? a date field for calendar?) is renderer responsibility and must
143+
hide non-resolvable types rather than render with hardcoded fallbacks
144+
(`groupBy: 'status'` against an object without `status` renders garbage —
145+
observed in the current console code).
146+
147+
### 3.4 Asymmetric defaults
148+
149+
| | Data mode | Interface mode |
150+
|---|---|---|
151+
| Quick filters | Auto-derived from select/boolean fields (current live behavior, now blessed) | None until author configures `userFilters` |
152+
| Visualization switching | On (all capability-resolvable types); user preference persists via the per-user view-patch channel | Locked to authored visualization unless whitelisted |
153+
| User-created views | Allowed, governed by permissions + view `sharing` | Never |
154+
| Filter state | Session-scoped (URL-param sync is a later, orthogonal step) | Session-scoped |
155+
156+
### 3.5 AI authoring rules (inherits ADR-0033's draft gate)
157+
158+
1. **Default output**: objects + list views + navigation → objects. *No pages.*
159+
Rationale: data mode is a functional superset; a missing page costs polish,
160+
a superfluous page costs a permanently-maintained duplicate asset. The
161+
asymmetry dictates the default.
162+
2. **Generate an interface page only on explicit signals**: persona split
163+
("销售人员看到…", customer portal), capability narrowing ("users must not
164+
change views", "only filter by industry/type"), or curation language
165+
(workspace / 工作台 / Airtable-interface-like). Ambiguity resolves to *no
166+
page*.
167+
3. **The rule ships as text in the `objectstack-ui` skill** (decision tree +
168+
the iron rule + this ADR as reference), so the constraint binds at
169+
generation time, not only at validation time.
170+
4. **Diagnostics are hard failures for AI.** The metadata write path already
171+
returns `_diagnostics`; it gains reference-integrity checks — `page.source`
172+
resolves; every `userFilters.fields[].field` and `tabs[].filter[].field`
173+
exists on the source object; kanban `groupBy` is a select. Human authors
174+
keep warn-and-store (ADR-0005 tolerance); the ADR-0033 agent loop treats
175+
`valid: false` as a failed apply and self-corrects before draft review.
176+
5. **Templates are the few-shot corpus.** `hotcrm` gains one canonical
177+
interface page (e.g. a "销售工作台" referencing `all_accounts` with
178+
industry/type dropdowns and a locked grid) and one view with filter `tabs`;
179+
`app-showcase` covers the remaining permutations. Zero examples today is
180+
why zero AI generations use these features.
181+
182+
## 4. Non-goals
183+
184+
- **Per-user saved filter presets in the cloud** (Airtable lets interface
185+
users pin filter values) — session scope first; a `sys_user_view_state`
186+
channel is a separate decision.
187+
- **Ad-hoc filter-builder spec for end users** — the advanced FilterBuilder
188+
remains a client feature; its output never persists as metadata.
189+
- **Page composition beyond single-source lists** (multi-widget interface
190+
pages, cross-object layouts) — covered by the existing page/component
191+
schemas; this ADR only fixes the list-page run mode.
192+
- **Back-compat machinery** — pre-launch premise; `filterableFields` and
193+
`userFilters.elements` aliases are one-release courtesies, not commitments.
194+
195+
## 5. Consequences
196+
197+
**Positive.** The Airtable-parity gap (in-page filter tabs / dropdowns,
198+
author-controlled visualization) closes mostly by *formalizing what already
199+
runs*; one orchestrator line, one schema block, and template examples deliver
200+
the visible feature. AI authors get a closed loop: schema they can see,
201+
diagnostics that push back, examples to imitate, and a default that is correct
202+
when they emit nothing. The two modes stop being implicit — navigation type is
203+
the single, auditable mode switch.
204+
205+
**Negative / accepted.** Two places can now declare `userFilters`
206+
(view + page); precedence is fixed (page overrides view) and documented, but
207+
it is still a second place. Interface pages remain a thinner v1 than
208+
Airtable's (no per-user saved state). Warn-only validation persists for human
209+
paths — we accept schema-invalid human metadata surviving, as today, until
210+
ADR-0027 promotion tightens it.
211+
212+
## 6. Implementation plan
213+
214+
| Phase | Repo | Work | Size |
215+
|---|---|---|---|
216+
| 1 | framework | `UserFiltersSchema` + `userActions.visualizations` + `InterfacePageConfig` rework in spec; reference-integrity diagnostics | S |
217+
| 2 | objectui | `ObjectView`: forward `viewDef.tabs`; import spec types (kill the local duplicate); wire `showViewSwitcher` behind the whitelist + capability filter | S |
218+
| 3 | framework | Template examples (`hotcrm` workbench page, tabbed view); `objectstack-ui` skill decision rules | S |
219+
| 4 | objectui | `PageView` renders `interfaceConfig` (single-source list + `UserFilters` toolbar, locked toolbar policy) | M |
220+
| 5 | both | Studio authoring panel for user filters / visualizations (Airtable right-panel parity); AI loop treats diagnostics as hard failure | M |
221+
222+
Phases 1–3 are independently shippable and deliver the user-visible feature in
223+
data mode; phase 4 activates interface mode; phase 5 completes the authoring
224+
experience.
225+
226+
## 7. Open questions
227+
228+
1. Should interface-mode filter state sync to URL params for shareable links
229+
(Salesforce-style) in v1, or stay purely in memory?
230+
2. Does `userFilters` on the *view* level survive long-term, or do we
231+
eventually deprecate it in favor of page-level-only once PageView ships?
232+
3. Capability filtering for visualizations: renderer-local heuristics now —
233+
does the spec eventually need explicit per-type binding declarations
234+
(`kanban.groupByField` required) surfaced as authoring-time diagnostics?

0 commit comments

Comments
 (0)