Skip to content

Commit 1d31bf1

Browse files
os-zhuangclaude
andauthored
feat(spec): spec↔frontend conformance check for react blocks (+ first audit) (#2480)
Confirms whether objectui components actually implement the props the spec protocol declares (the gap raised in review). check-react-blocks-conformance.ts compares the spec schemas referenced by REACT_BLOCKS against the live objectui registry-inputs manifest (sdui.manifest.json) and reports matched / spec-only / frontend-only props per block. First run (docs/audits/2026-06-react-blocks-conformance.md): 79 spec-only, 17 frontend-only, 0 missing — they DO diverge. Actionable: object-form's frontend- only formType/drawer*/modal* are real component extensions absent from the spec; the record:* blocks declare zero designer inputs. Report-only for now (run with --strict + a CI console-dump to ratchet once triaged). Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c1e3a65 commit 1d31bf1

3 files changed

Lines changed: 148 additions & 1 deletion

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Spec ↔ frontend conformance — react blocks (2026-06)
2+
3+
**Question** (raised in review): we can't guarantee the frontend (objectui)
4+
components actually implement the props the backend spec protocol declares —
5+
should we confirm it?
6+
7+
**Answer**: confirmed — they diverge. Below is the first run of the conformance
8+
check (`packages/spec/scripts/check-react-blocks-conformance.ts`), comparing the
9+
spec schemas referenced by `REACT_BLOCKS` against the live objectui
10+
registry-inputs manifest (`sdui.manifest.json`).
11+
12+
## How to read this
13+
14+
The registry `inputs` are the **designer palette** — a curated subset the visual
15+
editor exposes — NOT the component's full prop surface. The component reads its
16+
full config from the spec schema at render. So:
17+
18+
- **frontend-only** props (component declares an input the spec does not) are the
19+
reliable, actionable divergence: the spec is missing them or they are an
20+
undocumented extension. → fix the spec (or document the extension).
21+
- **spec-only** props are a *softer* signal: mostly the palette being a subset of
22+
the protocol (expected), not proof the component ignores the prop. A block with
23+
**zero inputs** (the `record:*` family) declares no designer inputs at all.
24+
- **matched** props appear in both.
25+
26+
## Findings (first run)
27+
28+
| block | matched | spec-only | frontend-only | notes |
29+
|---|--:|--:|--:|---|
30+
| `<ObjectForm>` (object-form) | 1 | 6 | **14** | frontend-only: formType, drawer*, modal*, split*, tab*, layout, columns, … — real component extensions absent from `FormViewSchema`. |
31+
| `<ListView>` (list-view) | 1 | 44 | 2 | the palette exposes objectName/viewType/fields/filters/sort/options; the 44 `ListViewSchema` config props are read at render, not surfaced as inputs. |
32+
| `<ObjectChart>` (object-chart) | 0 | 12 | 1 | the chart component's inputs (objectName/data/filter/aggregate) differ from `ChartConfigSchema` (title/series/axes/…). |
33+
| `<RecordDetails>` (record:details) | 0 | 4 | 0 | **component declares zero inputs** — props live only in the spec. |
34+
| `<RecordHighlights>` (record:highlights) | 0 | 2 | 0 | zero inputs. |
35+
| `<RecordRelatedList>` (record:related_list) | 0 | 9 | 0 | zero inputs. |
36+
| `<RecordPath>` (record:path) | 0 | 2 | 0 | zero inputs. |
37+
38+
**Summary**: 79 spec-only, 17 frontend-only, 0 blocks missing from the frontend.
39+
40+
## What this tells us (for an ADR)
41+
42+
1. There is **no single machine-readable "authoritative component prop surface"**
43+
today: the spec is the protocol, the registry inputs are a designer palette
44+
(subset), and the React prop types are the implementation. They are not kept in
45+
lockstep by any test — which is exactly the gap this audit confirms.
46+
2. **Recommendation**: treat the spec as the protocol source of truth; make the
47+
registry inputs a faithful (documented) projection of it; add the genuine
48+
**frontend-only** props (e.g. `object-form` formType/drawer*/modal*) to the
49+
spec so the protocol covers what the component accepts; and run this check in
50+
CI (with a console manifest dump) as a ratchet so new divergence is caught.
51+
3. The `record:*` blocks declaring **zero inputs** means the visual designer can't
52+
configure them — likely a real gap to close.
53+
54+
## Running it
55+
56+
```
57+
# produce a manifest from the live registry (objectui), then:
58+
MANIFEST=/path/to/sdui.manifest.json pnpm --filter @objectstack/spec check:react-conformance
59+
# add --strict to fail on divergence (once triaged).
60+
```

packages/spec/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@
196196
"test:watch": "vitest",
197197
"test:coverage": "vitest run --coverage",
198198
"check:liveness": "tsx scripts/liveness/check-liveness.mts",
199-
"gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts"
199+
"gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts",
200+
"check:react-conformance": "tsx scripts/check-react-blocks-conformance.ts"
200201
},
201202
"keywords": [
202203
"objectstack",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Spec ↔ frontend conformance report (ADR-0081 follow-up). Confirms the
4+
// objectui components ACTUALLY implement the props the spec protocol declares
5+
// for each curated react block. The spec is the protocol; the frontend must
6+
// conform. This surfaces (and can ratchet) the divergence.
7+
//
8+
// - spec-only : the spec schema declares a prop the component does NOT expose
9+
// as a registry input → frontend hasn't implemented the protocol.
10+
// - frontend-only: the component exposes an input the spec does NOT declare →
11+
// undocumented extension (or the spec is behind).
12+
//
13+
// The frontend side is the objectui registry-inputs manifest (sdui.manifest.json,
14+
// produced from the live registry — see objectui scripts/dump-public-manifest.mjs).
15+
// Provide it with MANIFEST=/path/to/sdui.manifest.json. Without it, the check
16+
// reports "manifest unavailable" and exits 0 (same manifest-optional posture as
17+
// the html-tier gate).
18+
//
19+
// Run: MANIFEST=… pnpm --filter @objectstack/spec check:react-conformance
20+
21+
process.env.OS_EAGER_SCHEMAS = '1';
22+
23+
import fs from 'fs';
24+
import { z } from 'zod';
25+
import { REACT_BLOCKS } from '../src/ui/react-blocks';
26+
27+
const MANIFEST = process.env.MANIFEST;
28+
const FAIL_ON_DIVERGENCE = process.argv.includes('--strict');
29+
30+
function specProps(schema: any): string[] {
31+
try {
32+
let js: any = z.toJSONSchema(schema, { unrepresentable: 'any' } as any);
33+
if (js?.$ref && js?.$defs) js = js.$defs[String(js.$ref).split('/').pop()!] ?? js;
34+
return Object.keys(js?.properties ?? {}).filter((k) => !['aria', 'type', 'id', 'className', 'style'].includes(k));
35+
} catch {
36+
return [];
37+
}
38+
}
39+
40+
function manifestInputs(manifest: any, schemaType: string): string[] | null {
41+
const comps = manifest?.components ?? manifest ?? {};
42+
// keys may be bare ('object-form') or namespaced ('plugin-form:object-form').
43+
const entry =
44+
comps[schemaType] ??
45+
Object.entries(comps).find(([k]) => k === schemaType || k.endsWith(`:${schemaType}`))?.[1];
46+
if (!entry) return null;
47+
const inputs = (entry as any).inputs ?? [];
48+
return inputs.map((i: any) => i?.name).filter(Boolean);
49+
}
50+
51+
if (!MANIFEST || !fs.existsSync(MANIFEST)) {
52+
console.log('⚠ react-blocks conformance: manifest unavailable (set MANIFEST=…) — skipping.');
53+
process.exit(0);
54+
}
55+
56+
const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
57+
let totalSpecOnly = 0;
58+
let totalMissingComp = 0;
59+
const overlay = (b: (typeof REACT_BLOCKS)[number]) => new Set(b.interactions.map((i) => i.name));
60+
61+
console.log('# Spec ↔ frontend conformance (react blocks)\n');
62+
for (const b of REACT_BLOCKS) {
63+
if (!b.schema) continue;
64+
const spec = new Set(specProps(b.schema));
65+
const inputs = manifestInputs(manifest, b.schemaType);
66+
if (inputs === null) {
67+
console.log(`✗ <${b.tag}> (${b.schemaType}): NO component in the manifest — not registered or not public.`);
68+
totalMissingComp++;
69+
continue;
70+
}
71+
const inputSet = new Set(inputs);
72+
const ov = overlay(b);
73+
const specOnly = [...spec].filter((p) => !inputSet.has(p) && !ov.has(p));
74+
const frontendOnly = [...inputSet].filter((p) => !spec.has(p) && !ov.has(p));
75+
const matched = [...spec].filter((p) => inputSet.has(p));
76+
totalSpecOnly += specOnly.length;
77+
const status = specOnly.length === 0 ? '✓' : '⚠';
78+
console.log(`${status} <${b.tag}> (${b.schemaType}): ${matched.length} matched, ${specOnly.length} spec-only, ${frontendOnly.length} frontend-only`);
79+
if (specOnly.length) console.log(` spec declares but component lacks: ${specOnly.join(', ')}`);
80+
if (frontendOnly.length) console.log(` component exposes but spec lacks: ${frontendOnly.join(', ')}`);
81+
}
82+
console.log(`\nSummary: ${totalSpecOnly} spec-only divergences, ${totalMissingComp} blocks missing from the frontend.`);
83+
if (FAIL_ON_DIVERGENCE && (totalSpecOnly > 0 || totalMissingComp > 0)) {
84+
console.error('Conformance check failed (--strict).');
85+
process.exit(1);
86+
}

0 commit comments

Comments
 (0)