Skip to content

Commit b987a80

Browse files
os-zhuangclaude
andcommitted
docs(adr): ADR-0085 — object semantic roles over per-surface hint blocks
Presentation intent on ObjectSchema is declared only as cross-surface semantic roles (nameField / compactLayout / stageField / fieldGroups). Deletes the detail passthrough block and views.* reader paths, types stageField (string | false), converges fieldGroups collapse semantics on one enum, single-sources the group derivation in @objectstack/spec. Surfaced by the objectui#2149 review: two-way fieldGroups key drift (spec defaultExpanded vs UI collapsible/collapsed), typed-but-no-op hideReferenceRail, zero-author views.*/detail.* keys across both repos. Supersedes the detail.* passthrough approach of objectui#2065/#2148 §6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 18b431e commit b987a80

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# ADR-0085: Presentation intent on an object is declared as cross-surface semantic roles, never as per-surface hint blocks (delete `detail`, retire `views.*`, type `stageField`)
2+
3+
**Status**: Proposed (2026-07-02)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove trichotomy), [ADR-0078](./0078-no-silently-inert-metadata.md) (completeness gate; "AI is the author, silent no-op manufactures false completion"), [ADR-0032](./0032-unified-expression-layer.md) (validate-by-default, no silent failure), ADR-0079 (`nameField` — the canonical object-level semantic-role precedent)
6+
**Supersedes**: the `detail.*` passthrough-block approach adopted by [objectui#2065](https://github.com/objectstack-ai/objectui/issues/2065) and proposed in [objectui#2148](https://github.com/objectstack-ai/objectui/issues/2148) §方案-6 (that mechanism was the right escape hatch *given* the spec of the day; this ADR replaces the mechanism, not the goals — both issues' goals ship)
7+
**Consumers**: `@objectstack/spec` (`ObjectSchema`), `@object-ui` (plugin-detail synth, plugin-form, app-shell `RecordDetailView`, object designer), templates/examples, the `objectstack-ui` authoring skill, cloud AI-build prompts
8+
**Surfaced by**: the objectui [PR #2149](https://github.com/objectstack-ai/objectui/pull/2149) review — wiring `fieldGroups` into detail pages exposed the full pathology of the hint channels (inventory below)
9+
10+
---
11+
12+
## TL;DR
13+
14+
The object protocol accumulated **three parallel channels** for "how should this object present itself", and every one of them is broken in a way invisible to its authors:
15+
16+
| Channel | State found (2026-07) |
17+
|---|---|
18+
| `objectDef.views.*` (`views.form.sections`, `views.detail.highlightFields`, …) | Readers-only legacy. `ObjectSchema.create()` rejects the key; **zero authors** exist in framework or objectui (examples, templates, packages, docs all grepped). Dead code with a doc trail that still teaches it. |
19+
| `detail: { … }.passthrough()` | Spec types 3 keys; the UI reads **9**. Of the typed keys, `hideReferenceRail` is a **no-op for spec authors** (the rail is default-off and the only enabling key, `showReferenceRail`, is untyped — spec typed the useless half of the pair). Passthrough bred wild riders (`relatedLayout`, `showReferenceRail`) that parse fine and do whatever the console of the week does. |
20+
| `fieldGroups[]` (typed, ADR-worthy MVP) | **Two-way key drift**: spec declares `defaultExpanded`/`icon`/`description`/`visibleOn` — no UI consumer reads any of them; the UI (form + detail) reads `collapsible`/`collapsed` — the spec rejects/strips both. Designer-authored objects (bare-key DB hydration) collapse correctly; spec-authored packages silently can't. |
21+
22+
Each key individually looks like a small fix. The **generator** of the bug class is architectural: a per-surface hint block plus passthrough invites every surface to mint its own dialect, and none of the three gates (validity lint, liveness ledger, prove-it-runs) sees the drift because each key is "valid" somewhere.
23+
24+
**Decision.** An object declares presentation intent **only as object-level semantic roles** — facts about the data a machine cannot infer, consumed by *every* surface: `nameField` (what a record is called), `highlightFields` (which fields matter most — **renamed from `compactLayout`** via the ADR-0079 alias pattern), `stageField` (which field is the linear lifecycle; `false` = declared non-linear), `fieldGroups` (how fields cluster semantically). The `detail` block is **deleted** and `views.*` readers are **deleted**. Per-surface presentation toggles are **rejected at this layer** — full-page control already exists via assigned pages.
25+
26+
---
27+
28+
## Context
29+
30+
objectui#2148 set out to make the detail page honour `fieldGroups` (only forms did) and discovered along the way that every detail-page hint the console documented was written on keys the spec rejects. PR #2149 fixed both correctly *within the rules of the day*: it routed hints through the one spec-writable location — the passthrough `detail` block. Reviewing that PR, we inventoried the entire hint surface across both repos and found the table above.
31+
32+
Three observations turn this from cleanup into an architecture decision:
33+
34+
1. **The bug class regenerates.** `views.*` was objectui's pre-spec dialect; when spec rejected it, authors routed around through `detail` passthrough; passthrough then bred `showReferenceRail`/`relatedLayout` riders. Killing keys without killing the channel just moves the dialect.
35+
2. **Every failure mode is a silent no-op**, which ADR-0078 identifies as the worst possible shape when AI is the primary author: the AI writes `detail.stagefield` or `fieldGroups[].collapsed`, gets a success envelope, and reports done. A human might notice the page didn't change; an agent will not.
36+
3. **The survivors all share one property.** Working through the nine `detail.*` keys, each either (a) duplicated an existing semantic role, (b) toggled the presentation of *correct* information, or (c) injected business intent no machine can infer. Only class (c) survived scrutiny — and class (c) keys are never page-scoped. That property is the invariant worth writing down.
37+
38+
## Decision
39+
40+
### 1. The semantic-role invariant
41+
42+
`ObjectSchema` carries presentation-relevant metadata **only** as object-level semantic roles. A semantic role:
43+
44+
- states a fact about the business meaning of the data (**not** about one page's layout);
45+
- is consumed by **every** surface where it is meaningful — grids, forms, detail pages, drawers, previews, AI summaries;
46+
- is named after the *fact*, never after a page (`stageField`, not `detail.stageField`).
47+
48+
The four roles, three of which already exist:
49+
50+
| Role | Question it answers | Consumers (non-exhaustive) |
51+
|---|---|---|
52+
| `nameField` (ADR-0079) | What is this record called? | headers, links, lookups, breadcrumbs |
53+
| `highlightFields: string[]` **(renamed from `compactLayout`)** | Which fields matter most, in priority order? (`[0]` wins where only one field fits) | default list/grid columns (ObjectGrid, ObjectView, InterfaceListPage), child-record previews, `record:details` auto layout, **detail highlight strip (first 4)**; future: kanban card fields — Salesforce compact-layout semantics |
54+
| `stageField: string \| false` **(newly typed)** | Which field is the linear lifecycle? `false` = the status-like field is *non-linear*; suppress stage heuristics | detail path/stepper; future: kanban default grouping, list badges, report bucketing |
55+
| `fieldGroups[]` + `Field.group` | How do fields cluster semantically? | form sections, detail sections, drawers, designer |
56+
57+
**Why the rename.** `compactLayout` is a misnomer twice over: the value is an ordered field list, not a layout (no structure — the name primes an AI author to expect columns/sections), and its heaviest real consumer is default list columns, which are not a "compact" context. `highlightFields` is already the renderer-side term of art (`HighlightField`, `record:highlights`, `HeaderHighlight`, `deriveFieldGroupSections`' sibling `deriveHighlightFields`, `HighlightFieldsProvider`) and the name the retired docs taught (`views.detail.highlightFields`) — renaming the protocol key unifies the metadata vocabulary with the renderer vocabulary, one fewer mapping for an AI to hold. Mechanics follow ADR-0079's `displayNameField → nameField` precedent exactly: `compactLayout` is accepted as a parse-time alias, copied onto `highlightFields`, both preserved on output, describe marks the old key deprecated. In-repo authors (35 platform-objects + examples) migrate mechanically in the same PR.
58+
59+
Zero-config stays zero-config: heuristics (name-based stage detection, auto highlight derivation, auto primary/"more details" split) remain the defaults; roles override them; `stageField: false` is the one explicit "stop guessing" signal.
60+
61+
### 2. The admission test for future keys
62+
63+
A new presentation-intent key enters `ObjectSchema` only if **both** hold:
64+
65+
1. it injects business intent **not inferable** from the schema (the machine cannot know whether `status` is ordinal, or which four fields this business watches);
66+
2. it binds to the object **across surfaces** — if the honest name for the key contains a page name, it fails.
67+
68+
Keys that merely hide/re-arrange correct information (`hideRelatedTab`, `showReferenceRail`, `relatedLayout`, `useFieldGroups`, explicit `detail.sections`) are rejected at this layer. The supported path for per-page control is the one that already exists and is honestly scoped: an **assigned page** (full page schema). If a genuine relationship-level need appears (e.g. "this child object is noise on every parent"), it gets modeled at the relationship layer, not as a page toggle.
69+
70+
### 3. Deletions (all evidenced zero-author)
71+
72+
- **`detail` block removed from `ObjectSchema`** — all three typed keys. `renderViaSchema` retires *together with* the legacy monolith renderer path in objectui (separate execution PR; the key is only that path's steering wheel, deleting it alone saves nothing). `hideReferenceRail`/`hideRelatedTab` are removed outright (the former is a proven no-op for spec authors; the latter's raison d'être — overriding rail/tab dedup — died with the rail).
73+
- **`views.*` reader paths deleted** from `RecordDetailView` (3 sites). No deprecation window: a deprecation window protects existing authors, and the authored population is measured zero.
74+
- **No second "important fields" list.** The detail highlight strip consumes `highlightFields` (first 4) → existing heuristic; it does not get its own key. One curated list, every surface.
75+
- **`fieldGroups.visibleOn` removed from the MVP schema** (ADR-0049: no consumer anywhere → remove; re-add with its enforcement when a consumer ships). `icon`/`description` get wired by the shared derivation below or entered in the liveness ledger with a milestone.
76+
77+
### 4. Collapse semantics converge on one enum
78+
79+
`fieldGroups[].collapse: 'none' | 'expanded' | 'collapsed'` (default `'none'`) replaces spec `defaultExpanded` **and** UI `collapsible`/`collapsed`. One key, three valid states, no expressible contradiction (`collapsible:false, collapsed:true` dies with the pair). `defaultExpanded` and the UI pair are accepted as normalization-time aliases for one minor, then dropped; a designer-data migration maps existing bare-key rows.
80+
81+
### 5. One shared derivation, every surface
82+
83+
The grouping semantics — declared order, empty groups dropped, ungrouped fields to a trailing untitled bucket, collapse passthrough, audit/system-field handling — are **protocol semantics**, authored once as a pure helper (`deriveFieldGroupLayout(def)`) in `@objectstack/spec` (the ADR-0078 §2 shared-predicate pattern). `@object-ui` plugin-form, plugin-detail synth, app-shell, and the designer consume it; the two existing near-identical objectui copies are deleted. objectui already pins its spec version, so semantics version with the protocol.
84+
85+
### 6. Author-time guardrails (two-tier, per the existing lint idiom)
86+
87+
Warnings (fragile): `Field.group` references an undeclared group key; a declared group no field references; `stageField`/`compactLayout` entries naming fields that don't exist. These are the completeness predicates ADR-0078 expects for this type — the runtime stays forgiving (unknown group → ungrouped bucket), the author gets told.
88+
89+
## Consequences
90+
91+
- **Spec surface change is breaking on paper, dead in practice.** Removal of the `detail` block keys falls under the ADR-0059 §4 discussion; since every removed key is provably unauthorable or no-op (evidence in this ADR), the documented dead-export exception (PR #2272 precedent) plausibly applies. Release owner decides major vs. exception at version time.
92+
- **objectui PR #2149 simplifies**: keep the `fieldGroups` detail wiring and tests (the valuable 80%); drop all `detail.*`/`views.*` reads; `detectStatusField` reads top-level `stageField` only; highlight derivation reads `highlightFields` (with the `compactLayout` alias honoured until the spec bump lands); the never-merged `detail.sections`/`sectionGroups`/`useFieldGroups` additions are dropped before they become contract.
93+
- **The rename sweep is mechanical but wide**: ~36 in-repo authors (35 platform-objects + examples) plus five objectui consumer sites (ObjectGrid, ObjectView ×2, InterfaceListPage, RecordDetailView child preview, `record:details` auto layout) move to `highlightFields`; external/DB metadata keeps parsing via the alias. A follow-up wires kanban card fields to the role (today: view-level `cardFields` + heuristic only).
94+
- **Objects with `fieldGroups` change appearance on detail pages** (grouped cards instead of auto two-section). Intended; release-noted; recoverable by editing the groups. No per-surface escape hatch is provided — that was decided deliberately under §2.
95+
- **Docs and skills** teaching `views.*` or `detail.*` are updated in the same change series (the docs-accuracy harness owns the sweep).
96+
- **What this does *not* restrict**: assigned pages keep full per-page power; `record:reference_rail` remains a renderer capability composable in a page schema — it just has no object-level switch.
97+
98+
## Alternatives considered
99+
100+
- **Type the `detail` block (strict) and keep it** — rejected. It legitimizes the per-surface config layer that generated the bug class; every future page grows a sibling block (`list.*`, `kanban.*`), each a new dialect.
101+
- **Keep passthrough, add lint warnings for unknown keys** — rejected per ADR-0032/0078: warnings on a success path do not stop an AI author; the envelope still says done.
102+
- **Keep a `useFieldGroups: false` / `detail.sections` escape hatch for the detail redesign** — rejected. The risk it hedges is aesthetic and recoverable (edit the groups); the cost is a permanent per-surface divergence concept plus (for `sections`) an exclusive field list whose omissions silently hide data. Re-adding a hatch later is a minor; carrying an unused one is forever.
103+
- **Add `highlightFields` alongside `compactLayout`** — rejected. Two curated "important fields" lists drift; Salesforce solved this the same way: the compact layout drives the highlights panel. (Resolved instead as a rename: one list, the clearer name.)
104+
- **Keep the `compactLayout` name** — rejected. The name claims a structure the value doesn't have, misses its own biggest consumer (default list columns), and mismatches the renderer vocabulary that already says "highlight" everywhere. A protocol whose keys are named for what they actually do is itself an AI guardrail; the rename costs one alias (ADR-0079 machinery already exists) and a mechanical in-repo sweep.
105+
- **Deprecation window for `views.*`** — rejected as cargo cult: there is no author to deprecate for. Windows are for populations, not for ghosts.

0 commit comments

Comments
 (0)