/** * The composition config contract — shared across the policy boundary. * * `CompositionPattern` is what the `composition/cardinality` and * `composition/co-occurrence` rules read from * `governance_rule_config.options.patterns`. Brief 03 defined it inline in the * rule; brief 04 lifted it here because the same shape now has to round-trip * across three boundaries that must not all hand-roll it: * - the rule's `isCompositionPattern` guard (validates untyped policy facts), * - cloud policy storage + delivery (`apps/cloud` validates on write against * `compositionPatternSchema` before persisting), and * - fragment authoring sugar (`lowerCompositionContract` lowers a fragment's * `contract.composition` block to the same `CompositionPattern[]`). * * Defining it once as a zod schema means the wire shape and the runtime guard * are the same artifact — the guard is `safeParse().success`, never a second * copy that can drift from the type. * * Browser-safe: imports `zod` only. No facts, no Node, no JSX. */ import { z } from "zod"; // ── The rule-input contract ────────────────────────────────────────────── /** Which container a pattern applies to. `component` = canonical componentId. */ export const compositionRegionSelectorSchema = z.object({ component: z.string().optional(), /** Marker attribute (e.g. `data-fui-region`). Typed; matched in a later brief. */ marker: z.string().optional(), role: z.string().optional(), }); /** Which children inside the region the constraint counts. */ export const compositionChildSelectorSchema = z.object({ component: z.string(), prop: z.string().optional(), value: z.string().optional(), }); export const compositionConstraintSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("cardinality"), max: z.number().optional(), min: z.number().optional(), }), z.object({ kind: z.literal("co-occurrence"), requires: z.object({ prop: z.string(), value: z.string() }), }), ]); export const compositionPatternSchema = z.object({ region: compositionRegionSelectorSchema, select: compositionChildSelectorSchema, constraint: compositionConstraintSchema, }); export type CompositionRegionSelector = z.infer; export type CompositionChildSelector = z.infer; export type CompositionConstraint = z.infer; export type CompositionPattern = z.infer; /** * Type guard for a single policy entry. Config arrives untyped from cloud * policy / fragment prose and the facts layer passes `options` through without * a shape, so a malformed entry would otherwise emit garbage findings. The rule * filters `options.patterns` through this; a bad entry is skipped, the rest keep * scanning (architecture failure mode). */ export function isCompositionPattern(value: unknown): value is CompositionPattern { return compositionPatternSchema.safeParse(value).success; } // ── Authoring sugar → constraint (Layer 3, brief 04 §10) ────────────────── /** * The structured block a fragment author writes in `contract.composition`. This * is the enforceable form of the prose they already keep (`relations`, * `usage.whenNot`); `lowerCompositionContract` turns it into the same * `CompositionPattern[]` the rule reads from cloud policy. v0 is this explicit * block — free-text inference from `whenNot` strings is out of scope. */ export const compositionAuthoringEntrySchema = z.object({ inRegion: z.string(), rule: z.union([ z.object({ atMost: z.number(), of: compositionChildSelectorSchema }), z.object({ when: z.object({ component: z.string(), prop: z.string(), value: z.string(), }), require: z.object({ prop: z.string(), value: z.string() }), }), ]), }); export type CompositionAuthoringEntry = z.infer; /** * Lower a fragment's `contract.composition` block to the policy patterns the * rule consumes. The authoring shape ({ inRegion, rule }) and the rule shape * ({ region, select, constraint }) converge here, so fragment-authored and * cloud-delivered constraints reach the rule as one identical config. */ export function lowerCompositionContract( entries: CompositionAuthoringEntry[] | undefined ): CompositionPattern[] { return (entries ?? []).map((entry) => { const region = { component: entry.inRegion }; if ("atMost" in entry.rule) { return { region, select: entry.rule.of, constraint: { kind: "cardinality", max: entry.rule.atMost }, }; } return { region, select: entry.rule.when, constraint: { kind: "co-occurrence", requires: entry.rule.require }, }; }); }