Skip to content

Commit c295ab7

Browse files
os-zhuangclaude
andcommitted
feat(spec): JSX-source page kind (ADR-0080 M3a)
PageSchema gains kind:'jsx' + source (the authoritative JSX text, compiled to the tree at save time) + requires (plugin provenance). A superRefine enforces ADR-0078 completeness: a jsx page with no source fails loudly at author time. full/slotted pages unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 827b9b6 commit c295ab7

1 file changed

Lines changed: 28 additions & 8 deletions

File tree

packages/spec/src/ui/page.zod.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,8 @@ export const PageSchema = lazySchema(() => z.object({
358358
*
359359
* Only meaningful when `type === 'record'`. Ignored otherwise.
360360
*/
361-
kind: z.enum(['full', 'slotted']).default('full')
362-
.describe('Page override mode: full (default) or slotted (partial overrides)'),
361+
kind: z.enum(['full', 'slotted', 'jsx']).default('full')
362+
.describe('Page override mode: full | slotted | jsx (ADR-0080 JSX-source authoring)'),
363363

364364
/**
365365
* Slot override map for slotted record pages.
@@ -384,13 +384,33 @@ export const PageSchema = lazySchema(() => z.object({
384384
tabs: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(),
385385
discussion: z.union([PageComponentSchema, z.array(PageComponentSchema)]).optional(),
386386
}).optional().describe('Slot override map for slotted pages'),
387+
388+
/**
389+
* JSX-source authoring (ADR-0080). When `kind === 'jsx'`, `source` is the
390+
* source-of-truth: a constrained JSX/HTML+Tailwind text compiled by
391+
* `@objectstack/sdui-parser` into the SchemaNode tree at SAVE time — parse,
392+
* never execute. `regions` then hold the DERIVED tree (a cache; the source
393+
* wins on any mismatch). For `full`/`slotted` pages `source` is unused.
394+
*/
395+
source: z.string().optional()
396+
.describe("JSX-source page text — authoritative when kind==='jsx'; compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute)"),
397+
/** Plugin namespaces the JSX source references — inferred at compile, checked at save AND load (ADR-0048 provenance). */
398+
requires: z.array(z.string()).optional()
399+
.describe('Plugin namespaces the JSX source references (validated at save and load)'),
400+
}).superRefine((page, ctx) => {
401+
// ADR-0080 + ADR-0078 (completeness): a `kind:'jsx'` page with no `source`
402+
// is silently inert — fail loudly at author time, do not render an empty page.
403+
if (page.kind === 'jsx' && !(typeof page.source === 'string' && page.source.trim().length > 0)) {
404+
ctx.addIssue({
405+
code: 'custom',
406+
path: ['source'],
407+
message: "A jsx page requires a non-empty `source` (ADR-0080: JSX is the source-of-truth).",
408+
});
409+
}
387410
}));
388-
// PageSchema has no cross-field (`superRefine`) requirements by design. It once
389-
// required `recordReview`/`blankLayout` for the `record_review`/`blank` types
390-
// (both removed — unrendered roadmap, see PAGE_TYPE_ROADMAP) and `slots` for
391-
// `kind: 'slotted'` (dropped — an empty slotted page validly renders the
392-
// synthesized default). Each of those was a "required-but-unauthorable field
393-
// blocks the Studio create form" trap; none survives.
411+
// PageSchema's only cross-field rule is the ADR-0080 jsx-source completeness
412+
// check above. It once also required `recordReview`/`blankLayout` and `slots`
413+
// (all removed — unrendered roadmap / "required-but-unauthorable" Studio traps).
394414

395415
export type Page = z.infer<typeof PageSchema>;
396416
/** Authoring input for {@link Page} — defaulted fields are optional. */

0 commit comments

Comments
 (0)