From 0436b1e2bfd79c0498420401ff3329f27e43077f Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:17:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(spec):=20spec=E2=86=94frontend=20conforman?= =?UTF-8?q?ce=20check=20for=20react=20blocks=20(+=20first=20audit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Claude Opus 4.8 --- .../2026-06-react-blocks-conformance.md | 60 +++++++++++++ packages/spec/package.json | 3 +- .../scripts/check-react-blocks-conformance.ts | 86 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 docs/audits/2026-06-react-blocks-conformance.md create mode 100644 packages/spec/scripts/check-react-blocks-conformance.ts diff --git a/docs/audits/2026-06-react-blocks-conformance.md b/docs/audits/2026-06-react-blocks-conformance.md new file mode 100644 index 0000000000..9ac5ef33ac --- /dev/null +++ b/docs/audits/2026-06-react-blocks-conformance.md @@ -0,0 +1,60 @@ +# Spec ↔ frontend conformance — react blocks (2026-06) + +**Question** (raised in review): we can't guarantee the frontend (objectui) +components actually implement the props the backend spec protocol declares — +should we confirm it? + +**Answer**: confirmed — they diverge. Below is the first run of the conformance +check (`packages/spec/scripts/check-react-blocks-conformance.ts`), comparing the +spec schemas referenced by `REACT_BLOCKS` against the live objectui +registry-inputs manifest (`sdui.manifest.json`). + +## How to read this + +The registry `inputs` are the **designer palette** — a curated subset the visual +editor exposes — NOT the component's full prop surface. The component reads its +full config from the spec schema at render. So: + +- **frontend-only** props (component declares an input the spec does not) are the + reliable, actionable divergence: the spec is missing them or they are an + undocumented extension. → fix the spec (or document the extension). +- **spec-only** props are a *softer* signal: mostly the palette being a subset of + the protocol (expected), not proof the component ignores the prop. A block with + **zero inputs** (the `record:*` family) declares no designer inputs at all. +- **matched** props appear in both. + +## Findings (first run) + +| block | matched | spec-only | frontend-only | notes | +|---|--:|--:|--:|---| +| `` (object-form) | 1 | 6 | **14** | frontend-only: formType, drawer*, modal*, split*, tab*, layout, columns, … — real component extensions absent from `FormViewSchema`. | +| `` (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. | +| `` (object-chart) | 0 | 12 | 1 | the chart component's inputs (objectName/data/filter/aggregate) differ from `ChartConfigSchema` (title/series/axes/…). | +| `` (record:details) | 0 | 4 | 0 | **component declares zero inputs** — props live only in the spec. | +| `` (record:highlights) | 0 | 2 | 0 | zero inputs. | +| `` (record:related_list) | 0 | 9 | 0 | zero inputs. | +| `` (record:path) | 0 | 2 | 0 | zero inputs. | + +**Summary**: 79 spec-only, 17 frontend-only, 0 blocks missing from the frontend. + +## What this tells us (for an ADR) + +1. There is **no single machine-readable "authoritative component prop surface"** + today: the spec is the protocol, the registry inputs are a designer palette + (subset), and the React prop types are the implementation. They are not kept in + lockstep by any test — which is exactly the gap this audit confirms. +2. **Recommendation**: treat the spec as the protocol source of truth; make the + registry inputs a faithful (documented) projection of it; add the genuine + **frontend-only** props (e.g. `object-form` formType/drawer*/modal*) to the + spec so the protocol covers what the component accepts; and run this check in + CI (with a console manifest dump) as a ratchet so new divergence is caught. +3. The `record:*` blocks declaring **zero inputs** means the visual designer can't + configure them — likely a real gap to close. + +## Running it + +``` +# produce a manifest from the live registry (objectui), then: +MANIFEST=/path/to/sdui.manifest.json pnpm --filter @objectstack/spec check:react-conformance +# add --strict to fail on divergence (once triaged). +``` diff --git a/packages/spec/package.json b/packages/spec/package.json index a251ee4655..2b0f6121e0 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -196,7 +196,8 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "check:liveness": "tsx scripts/liveness/check-liveness.mts", - "gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts" + "gen:react-blocks": "tsx scripts/build-react-blocks-contract.ts", + "check:react-conformance": "tsx scripts/check-react-blocks-conformance.ts" }, "keywords": [ "objectstack", diff --git a/packages/spec/scripts/check-react-blocks-conformance.ts b/packages/spec/scripts/check-react-blocks-conformance.ts new file mode 100644 index 0000000000..cedb9bbd6c --- /dev/null +++ b/packages/spec/scripts/check-react-blocks-conformance.ts @@ -0,0 +1,86 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Spec ↔ frontend conformance report (ADR-0081 follow-up). Confirms the +// objectui components ACTUALLY implement the props the spec protocol declares +// for each curated react block. The spec is the protocol; the frontend must +// conform. This surfaces (and can ratchet) the divergence. +// +// - spec-only : the spec schema declares a prop the component does NOT expose +// as a registry input → frontend hasn't implemented the protocol. +// - frontend-only: the component exposes an input the spec does NOT declare → +// undocumented extension (or the spec is behind). +// +// The frontend side is the objectui registry-inputs manifest (sdui.manifest.json, +// produced from the live registry — see objectui scripts/dump-public-manifest.mjs). +// Provide it with MANIFEST=/path/to/sdui.manifest.json. Without it, the check +// reports "manifest unavailable" and exits 0 (same manifest-optional posture as +// the html-tier gate). +// +// Run: MANIFEST=… pnpm --filter @objectstack/spec check:react-conformance + +process.env.OS_EAGER_SCHEMAS = '1'; + +import fs from 'fs'; +import { z } from 'zod'; +import { REACT_BLOCKS } from '../src/ui/react-blocks'; + +const MANIFEST = process.env.MANIFEST; +const FAIL_ON_DIVERGENCE = process.argv.includes('--strict'); + +function specProps(schema: any): string[] { + try { + let js: any = z.toJSONSchema(schema, { unrepresentable: 'any' } as any); + if (js?.$ref && js?.$defs) js = js.$defs[String(js.$ref).split('/').pop()!] ?? js; + return Object.keys(js?.properties ?? {}).filter((k) => !['aria', 'type', 'id', 'className', 'style'].includes(k)); + } catch { + return []; + } +} + +function manifestInputs(manifest: any, schemaType: string): string[] | null { + const comps = manifest?.components ?? manifest ?? {}; + // keys may be bare ('object-form') or namespaced ('plugin-form:object-form'). + const entry = + comps[schemaType] ?? + Object.entries(comps).find(([k]) => k === schemaType || k.endsWith(`:${schemaType}`))?.[1]; + if (!entry) return null; + const inputs = (entry as any).inputs ?? []; + return inputs.map((i: any) => i?.name).filter(Boolean); +} + +if (!MANIFEST || !fs.existsSync(MANIFEST)) { + console.log('⚠ react-blocks conformance: manifest unavailable (set MANIFEST=…) — skipping.'); + process.exit(0); +} + +const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); +let totalSpecOnly = 0; +let totalMissingComp = 0; +const overlay = (b: (typeof REACT_BLOCKS)[number]) => new Set(b.interactions.map((i) => i.name)); + +console.log('# Spec ↔ frontend conformance (react blocks)\n'); +for (const b of REACT_BLOCKS) { + if (!b.schema) continue; + const spec = new Set(specProps(b.schema)); + const inputs = manifestInputs(manifest, b.schemaType); + if (inputs === null) { + console.log(`✗ <${b.tag}> (${b.schemaType}): NO component in the manifest — not registered or not public.`); + totalMissingComp++; + continue; + } + const inputSet = new Set(inputs); + const ov = overlay(b); + const specOnly = [...spec].filter((p) => !inputSet.has(p) && !ov.has(p)); + const frontendOnly = [...inputSet].filter((p) => !spec.has(p) && !ov.has(p)); + const matched = [...spec].filter((p) => inputSet.has(p)); + totalSpecOnly += specOnly.length; + const status = specOnly.length === 0 ? '✓' : '⚠'; + console.log(`${status} <${b.tag}> (${b.schemaType}): ${matched.length} matched, ${specOnly.length} spec-only, ${frontendOnly.length} frontend-only`); + if (specOnly.length) console.log(` spec declares but component lacks: ${specOnly.join(', ')}`); + if (frontendOnly.length) console.log(` component exposes but spec lacks: ${frontendOnly.join(', ')}`); +} +console.log(`\nSummary: ${totalSpecOnly} spec-only divergences, ${totalMissingComp} blocks missing from the frontend.`); +if (FAIL_ON_DIVERGENCE && (totalSpecOnly > 0 || totalMissingComp > 0)) { + console.error('Conformance check failed (--strict).'); + process.exit(1); +}