Skip to content

Commit 9ad1198

Browse files
os-zhuangclaude
andauthored
docs(adr): ADR-0051 inline metadata views in docs (metadata fenced block) (#1940)
Concretizes the "inline metadata views" ADR-0046 §3.5/§6.7 already reserved but left without a syntax. A ```metadata fenced block compiles to a new read-only `element:metadata_viewer` SDUI component, resolved live and permission-projected at read time. Key decisions: - Fenced code block (already-legal CommonMark per §3.4) over directives/MDX — does not reopen the trust boundary. - Body is a declarative reference (data, not code), so it stays on the data side of the ADR-0025/§3.4 boundary that banned MDX. - Compiles 1:1 to one ObjectUI component — one runtime, one schema, one permission path; no parallel renderer. - Kernel unchanged: doc.content stays inert (§3.1); resolution is render-time only. - Scope metadata-only, phased state_machine -> flow -> permission; `object` and a general `objectui` fence deferred. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d28cc73 commit 9ad1198

1 file changed

Lines changed: 292 additions & 0 deletions

File tree

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# ADR-0051: Inline metadata views in docs — the ` ```metadata ` fenced block
2+
3+
**Status**: Proposed (2026-06-16)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0046](./0046-package-docs-as-metadata.md) (`doc`/`book` as metadata — §3.4 syntax boundary, §3.5 *derived content is rendered, never written*, §6.7 anonymous live-embed guardrail), [ADR-0020](./0020-state-machine-converge-and-enforce.md) (a record state machine is a `state_machine` validation rule on an object, not a standalone type), [ADR-0047](./0047-object-ui-run-modes.md) (ObjectUI is the server-driven UI runtime), [ADR-0048](./0048-cross-package-metadata-collision.md) (package-scoped single-item resolution), [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove posture for governed properties), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (AI as primary author)
6+
**Consumers**: `@objectstack/spec` (`DocSchema` unchanged; new `element:metadata_viewer` entry in `ComponentPropsMap`; the fenced-block body schema), `@objectstack/cli` (publish lint: parse + validate body + reference-liveness), `@objectstack/console` (the SDUI doc renderer lifts ` ```metadata ` fences into the component), `apps/docs` portal (anonymous degradation per ADR-0046 §6.7), `@objectstack/core` (kernel **unchanged**`doc` stays inert)
7+
**Pilot**: `os-tianshun-mtc`
8+
9+
---
10+
11+
## TL;DR
12+
13+
ADR-0046 §3.5 already ruled that anything reconstructible from the manifest —
14+
field dictionaries, option lists, validation conditions, **permission
15+
matrices** — is a *metadata view the platform renders live*, never authored
16+
prose, and §6.7 named the same thing "inline metadata views / live metadata
17+
embed" when it wrote the anonymous guardrail. Neither specified how an author
18+
**places** such a view inside a doc. This ADR specifies exactly that, and
19+
nothing more:
20+
21+
- **One syntax: a ` ```metadata ` fenced code block.** Its body is a small,
22+
declarative reference — *which* metadata to render and *how* — not code:
23+
24+
````md
25+
```metadata
26+
type: state_machine
27+
object: order
28+
name: order_lifecycle
29+
mode: diagram
30+
detail: business
31+
```
32+
````
33+
34+
- **It compiles to one SDUI component, `element:metadata_viewer`** — a new,
35+
read-only, `embeddableInDoc` entry in `ComponentPropsMap`. The doc renderer
36+
is an ObjectUI surface; a metadata embed is an ObjectUI node like any other.
37+
**One runtime, one schema, one permission path** — never a parallel renderer.
38+
- **The block is data, not code** — a reference the *platform's own* trusted
39+
component resolves. That is what keeps it on the safe side of the ADR-0025
40+
trust boundary that §3.4 drew to ban MDX; this ADR does **not** reopen that
41+
ban.
42+
- **Live transclusion**: the view is rendered from *current* metadata at read
43+
time. Change the flow, the doc's diagram changes. A copy can go stale; a
44+
reference cannot — that is the entire reason to build this rather than tell
45+
authors to paste a screenshot.
46+
- **Scope is metadata-only, and phased**: `state_machine` first, `flow` second,
47+
`permission` last (it gates on render-time projection). `object` is deferred
48+
(§5). A general "embed any ObjectUI component" fence (` ```objectui `) is an
49+
explicit non-goal here, kept as a future escape hatch (§4).
50+
51+
The kernel is untouched: `doc.content` stays an inert Markdown string (ADR-0046
52+
§3.1). Resolution is a **render-time** concern, invisible to the load path.
53+
54+
## 1. Context
55+
56+
- **The reader is the deciding variable, not the metadata.** A `flow` is "open
57+
the Studio designer" for a developer and "read the doc" for a business user,
58+
auditor, or PM who has no Studio and never will. Documentation earns its place
59+
only for the reader who cannot get the answer by *using the running system*:
60+
before they have access, before a feature ships, to see the *why*, or to see a
61+
cross-cutting whole that **no single live screen presents** (the full legal
62+
transition space of a record, every role's access to an object). Live,
63+
interactive artifacts a reader *can* reach — a `dashboard`, a `report`, a list
64+
`view` — are best experienced in the system; docs should **deep-link** to
65+
those, never embed a frozen copy. That deep-link case is out of scope here.
66+
- **What is left needs a synthesized, read-only view that the live UI never
67+
shows as one picture**: a state machine's whole transition graph, a flow's
68+
process shape and approval policy, an object's permission matrix. ADR-0046
69+
§3.5 already classified these as *rendered, never written*, and §6.7 reserved
70+
"inline metadata views" in its security guardrail. The concept is blessed; the
71+
**syntax and resolution mechanism are unspecified**. This ADR supplies them.
72+
- **The frontend is server-driven UI (ADR-0047).** ObjectUI renders a
73+
component tree (`ComponentPropsMap`, `record:*` / `element:* `/ `ai:*`) from
74+
metadata. A metadata embed must therefore be an **ObjectUI component**, not a
75+
Markdown-only side-channel — otherwise we build a second rendering path that
76+
ObjectUI's evolution can never extend. Conversely, the doc must stay portable
77+
inert Markdown (ADR-0046's whole thesis): the seam between portable prose and
78+
a live view is the one thing this ADR designs.
79+
- **AI writes most docs (ADR-0033).** The authored surface must be flat, fixed,
80+
and lintable so an LLM author cannot subtly corrupt it, and so a typo on a
81+
metadata name is caught the same way field-ref typos already are.
82+
83+
## 2. Goals & Non-goals
84+
85+
### Goals
86+
87+
- A single authored syntax — the ` ```metadata ` fence — with a typed,
88+
lintable body schema.
89+
- A new read-only SDUI component, `element:metadata_viewer`, that the fence
90+
compiles to; the doc renderer reuses the ObjectUI runtime to render it.
91+
- Render-time semantics: live transclusion, package-scoped resolution
92+
(ADR-0048), two distinct projections (authoring *detail* vs automatic
93+
*permission*), and graceful degradation (anonymous, missing-ref).
94+
- Publish lint: parse fences, validate bodies, and check that every referenced
95+
metadata item is live (the enforce-or-remove posture of ADR-0049).
96+
97+
### Non-goals
98+
99+
- **A general component-embed fence (` ```objectui `).** Embedding *arbitrary*
100+
ObjectUI nodes (charts, AI panels, interactive forms) is deferred to a later
101+
escape hatch (§4). v1 embeds *metadata views only*.
102+
- **Deep-linking to live surfaces** (`dashboard` / `report` / `view`). Those
103+
are reached in the system; a plain Markdown link suffices. Out of scope.
104+
- **The inline `object://` semantic link** (ADR-0046 §3.3) stays the *inline*
105+
form ("mention"); the ` ```metadata ` fence is the *block* form ("embed").
106+
Both are the same intent at different altitudes; the inline form remains
107+
deferred and only required to parse.
108+
- **Any kernel change.** `doc` stays inert (ADR-0046 §3.1); `content` is never
109+
parsed on the load path.
110+
111+
### Explicitly deferred
112+
113+
`object` embeds (wrong altitude for their reader — a hand-authored conceptual
114+
model beats an auto-rendered schema that dumps every system field; §5); the
115+
` ```objectui ` general fence; cross-package metadata embeds beyond the
116+
dependency set ADR-0046 §3.3 already resolves; an `object://` inline preview.
117+
118+
## 3. Design
119+
120+
### 3.1 The syntax — a fenced code block, body is a declarative reference
121+
122+
A ` ```metadata ` fenced block names *what* to render and *how*. The body is
123+
parsed as YAML and validated against the schema below (illustrative; exact field
124+
names settle in the spec PR):
125+
126+
```ts
127+
// The fence body. Compiles 1:1 to element:metadata_viewer props (§3.2).
128+
const MetadataEmbedSchema = z.object({
129+
type: z.enum(['state_machine', 'flow', 'permission']), // 'object' deferred (§5)
130+
name: z.string(), // target item; resolved package-scoped (ADR-0048)
131+
object: z.string().optional(), // REQUIRED for object-scoped kinds
132+
// (state_machine is a validation rule ON an object — ADR-0020;
133+
// permission renders a matrix FOR an object)
134+
mode: z.enum(['diagram', 'matrix', 'summary']).optional(), // default per type
135+
detail: z.enum(['business', 'technical']).optional().default('business'), // authoring projection (§3.5)
136+
});
137+
```
138+
139+
Choosing a **fenced code block** over directives/MDX/shortcodes is deliberate:
140+
141+
- It is **already valid CommonMark + GFM** — the exact grammar ADR-0046 §3.4
142+
permits. We adopt a primitive that was always legal; we do **not** relax the
143+
"no MDX / no directives / no shortcodes" boundary.
144+
- It **degrades to meaningful text**: a renderer that does not know `metadata`
145+
shows a code block reading `type: state_machine / name: order_lifecycle` — a
146+
human still learns "a state-machine view named order_lifecycle belongs here,"
147+
unlike a broken `<Component/>` or a stray `:::`.
148+
- It is **block-level by nature** (these are block diagrams) and **trivially
149+
lintable** (scan fences whose info string is `metadata`, validate one object).
150+
- It is the industry-blessed convention for embedded views (Mermaid, PlantUML,
151+
math) — not a bespoke dialect.
152+
153+
The info string is the bare token `metadata`. The doc renderer owns its
154+
interpretation, so the genericity of the word carries no cross-tool collision
155+
risk inside ObjectStack; should cross-tool rendering ever matter, namespacing to
156+
`os:metadata` is a non-breaking later move.
157+
158+
### 3.2 It compiles to one SDUI component — one runtime, never a parallel path
159+
160+
The fence is **sugar over a single ObjectUI node**. The doc renderer lifts each
161+
` ```metadata ` block into:
162+
163+
```ts
164+
// packages/spec/src/ui/component.zod.ts — new ComponentPropsMap entry
165+
'element:metadata_viewer': MetadataEmbedSchema, // read-only; embeddableInDoc: true
166+
```
167+
168+
`element:metadata_viewer` resolves `type` + `name` (+ `object`) against the live
169+
metadata registry and dispatches to the registered viewer for that type — the
170+
existing `MetadataViewerContribution` mechanism (preview mode) in
171+
`packages/spec/src/studio/plugin.zod.ts`. `flow` reuses the read-only render
172+
descriptors already specified in `packages/spec/src/studio/flow-builder.zod.ts`;
173+
`state_machine` renders the transition graph of the rule in
174+
`packages/spec/src/automation/state-machine.zod.ts`; `permission` renders an
175+
object's matrix from `packages/spec/src/security/permission.zod.ts`.
176+
177+
This is the load-bearing decision: **rendering a doc is rendering an ObjectUI
178+
page** that is mostly `element:markdown` prose with metadata-viewer islands.
179+
There is one component model, one props schema, one render-time permission path
180+
— the same one pages use. The earlier rejected `osembed`-with-bespoke-YAML
181+
design (§4) failed precisely by introducing a second schema and renderer; this
182+
does not, because the body *is* a component's props.
183+
184+
> **Why a narrow `metadata` fence and not the general `objectui` fence —
185+
> trust boundary.** ADR-0046 §3.4 banned MDX because "MDX is code, and rendering
186+
> publisher-supplied code crosses the ADR-0025 trust boundary; Markdown is
187+
> data." A ` ```metadata ` body is **pure declarative data**: a reference plus
188+
> two enums, no expressions, no actions, sanitizable. The view it produces is
189+
> drawn by the *platform's own trusted component*, from the *package's own
190+
> already-governed metadata* — the publisher supplies a reference, the platform
191+
> supplies the renderer. A general ` ```objectui ` body, by contrast, could
192+
> carry CEL expressions and action bindings (`element:form.onSubmit`,
193+
> `element:button`) — i.e. code — and would reopen the boundary §3.4 closed.
194+
> The metadata-only scope is therefore not just smaller; it is the form that
195+
> stays on the *data* side of the line.
196+
197+
### 3.3 Live transclusion — a reference, never a copy
198+
199+
The view is rendered from **current** metadata every time the doc is read. This
200+
is the whole justification for the feature: a pasted screenshot or hand-drawn
201+
diagram rots the moment the flow changes; a reference is always correct. Three
202+
properties follow and are mandatory — drop any one and the embed degrades into a
203+
stale, wrong-altitude copy of the Studio designer (the failure ADR-0046 §3.5
204+
forbids):
205+
206+
1. **Live-derived** — resolved at render time, never materialized into
207+
`content`.
208+
2. **Projected** — to the authored *detail* and the reader's *permissions*
209+
(§3.5).
210+
3. **Narrative-owned** — the human writes the *why*; the system fills the
211+
*what*. The author places a reference, not a rendering.
212+
213+
Resolution is **package-scoped** (ADR-0048): `name` resolves within the doc's
214+
own package, then its dependencies (the same set ADR-0046 §3.3 resolves
215+
cross-package doc links against). A reference to a metadata item the reader's
216+
grant does not cover, or that does not exist, **degrades to a placeholder** — it
217+
never blocks render (mirroring §3.3's broken-link posture).
218+
219+
### 3.4 Two projections — keep them separate
220+
221+
| projection | when | who controls | what it does |
222+
|:--|:--|:--|:--|
223+
| **detail** | authoring | the author (`detail:` field) | altitude: `business` collapses technical flow nodes (HTTP, script, error boundaries) to the business-meaningful steps + approvals; `technical` shows the full graph |
224+
| **permission** | render time | **the platform, automatically, per reader** | strips what the reader may not see (a fraud-check branch, a permission row for a role they cannot view) before drawing |
225+
226+
These are orthogonal and must not be conflated. `detail` is an authored
227+
intent; **permission projection is an automatic security behavior**, never
228+
author-controlled. Note the name: this is *not* `book.audience` (ADR-0046 §6.7),
229+
which is an *access tier* on the publication unit. To avoid that collision the
230+
authoring knob is `detail`, not `audience`.
231+
232+
### 3.5 Security — inherits the §6.7 guardrail, adds nothing the kernel must parse
233+
234+
- **Anonymous degradation is inherited verbatim.** ADR-0046 §6.7 already
235+
mandates that any live-metadata embed "must not resolve for an anonymous
236+
request; it degrades to a hidden / 'sign in to view' placeholder." A
237+
` ```metadata ` block is exactly such an embed. In the authenticated console
238+
(ObjectUI runtime present) it resolves, live and permission-projected; in the
239+
anonymous `apps/docs` portal it degrades. No new rule — the same rule, now
240+
with a concrete syntax to govern.
241+
- **Permission projection is enforced at the resolver, not the UI.** Like
242+
§6.7's gating, the metadata-viewer resolver applies the reader's permissions
243+
server-side; a client that strips chrome must not be able to surface a hidden
244+
branch or row.
245+
- **The kernel stays inert (ADR-0046 §3.1).** `content` is never parsed on the
246+
load path; the fence is a render-time concern only. `doc` remains
247+
`supportsOverlay: false`.
248+
249+
### 3.6 Publish lint — parse, validate, check liveness
250+
251+
`os build` / publish lint (the same pass that already enforces §3.4's bans and
252+
§3.3's link resolution) gains one check on `doc.content`:
253+
254+
1. Extract every fence whose info string is `metadata`; parse the body.
255+
2. Validate it against `MetadataEmbedSchema`; a malformed body **fails publish**
256+
(it is authored structure, not prose). A `type` typo yields a did-you-mean
257+
against the enum — the same affordance flow-condition field typos already get.
258+
3. Resolve `type` + `name` (+ `object`) against the package and its
259+
dependencies; an embed pointing at a **dead or renamed** metadata item is
260+
reported. This rides the existing reference-validation / liveness machinery
261+
and follows ADR-0049's enforce-or-remove posture: a doc that references a
262+
non-live item is a broken reference, surfaced at publish, degraded (not
263+
crashed) at render.
264+
265+
## 4. Alternatives considered
266+
267+
| Alternative | Verdict |
268+
|:--|:--|
269+
| **` ```objectui ` general component fence now** (body = any `{type, props}` node) | Rejected *for v1*, kept as a future escape hatch. A general body can carry expressions and action bindings — code — reopening the §3.4 trust boundary. The `metadata` fence stays on the data side. When arbitrary embeds are genuinely needed, add ` ```objectui ` as a power-user form that **shares this runtime** (`metadata` is sugar over `element:metadata_viewer`; `objectui` is the full node) — narrow-sugar + general-form, one renderer, not two systems. |
270+
| **` ```osembed ` with a bespoke YAML schema** (an earlier turn) | Rejected. A body unrelated to the component model spawns a second schema, a second validation path, a second renderer dispatch — the parallel-system smell. Binding the body to `ComponentPropsMap` (§3.2) avoids it entirely. |
271+
| **MDX / JSX components** | Banned by ADR-0046 §3.4 (code across the ADR-0025 trust boundary); couples docs to React; destroys "content is inert data." |
272+
| **remark-directive (` :::metadata{...} `)** | Semantically the cleanest ("typed block with attributes"), but it *is* a directive — the thing §3.4 bans — and needs a new parser. The fenced block buys the same capability with zero boundary change and cleaner degradation. Correct elsewhere; wrong in this repo. |
273+
| **Obsidian transclusion (`![[name]]`)** | Not CommonMark; no room for `type` / `mode` / `detail`; overloads image syntax. |
274+
| **HTML-comment sentinels (`<!-- embed:... -->`)** | Valid CommonMark but no first-class fields, easy to mistype, semantics invisible — a hack. |
275+
| **A pasted screenshot / hand-drawn diagram** | Rots on the next metadata change (ADR-0046 §3.5). Live transclusion exists precisely to kill this. |
276+
277+
## 5. Phasing
278+
279+
Ordered by value ÷ (risk + staleness). Each phase is independently shippable.
280+
281+
| Phase | Scope | Depends on |
282+
|:--|:--|:--|
283+
| **P1 — `state_machine`** | the ` ```metadata ` fence grammar; `MetadataEmbedSchema`; `element:metadata_viewer` (state-machine renderer over ADR-0020 rules); publish lint (parse + validate + liveness); console lift + render | this ADR |
284+
| **P2 — `flow`** | `flow` kind via the read-only flow-builder descriptors; the `detail: business` altitude projection (collapse technical nodes) — the projection is *mandatory* here, or the embed is a worse Studio | P1 |
285+
| **P3 — `permission`** | `permission` matrix kind; **render-time permission projection** per reader; verified against the §6.7 anonymous guardrail | P2, and a permission-projecting resolver |
286+
| **Deferred** | `object` (wrong altitude — prefer a hand-authored conceptual model); ` ```objectui ` general fence; `object://` inline preview ||
287+
288+
`state_machine` leads because its graph is small, naturally at business altitude
289+
(states *are* business states), and low-sensitivity — the cleanest proof that
290+
"edit the metadata, the doc's diagram follows." `permission` trails because it is
291+
the highest-value-to-auditors **and** highest-risk kind: it ships only once the
292+
render-time projection and the §6.7 gate are both in place.

0 commit comments

Comments
 (0)