/** * lib/page-spec-coded-entity.ts — Canonical Zod schema + helpers for the * pagespec `codedEntity` flag. * * SINGLE SOURCE OF TRUTH for the contract entité.md `**Code pattern**` line → * derive-code-specs (stamps/reconciles the flag) → the pagespec `codedEntity` * field → scaffold-component (readonly code field + create-only SmartCodeField * injection), scaffold-business / scaffold-controller / scaffold-api-client * (supplied create surface) and the audits that verify it (PRD-132, SCR-015, * DEV-UI-034). * * WHY: the flag was born a bare boolean (`codedEntity: true`) and five call * sites tested `=== true`. The business-label facet (« Référence » IS the * code) and the supplied-on-create facet need to ride the SAME flag without * breaking a single existing pagespec — hence the union: the legacy boolean * stays valid forever, the enriched object is opt-in, and every reader goes * through `isCodedEntity()` instead of `=== true`. * * Merge contract (multi-chantier safety): writers REWRITE only the slot they * own and PRESERVE every other key (`.passthrough()` keeps unknown slots * alive through a parse), so two facets authored by different derivations * never clobber each other. * * @see lib/code-pattern-grammar.ts (parses the BA facets this flag mirrors) * @see ba-develop/cli/derive-code-specs (stamps + reconciles the flag both ways) * @see development/frontend/component/cli/scaffold-component (readonly gate + injection) */ import { z } from 'zod' /** Pagespec `codedEntity`: the legacy boolean OR the enriched object form. */ export const CodedEntityFlagSchema = z.union([ z.boolean(), z .object({ /** Business label of the code (UI/i18n) — mirrors the `libellé « … »` facet. */ label: z .object({ fr: z.string().min(1).optional(), en: z.string().min(1).optional(), }) .optional(), /** An optional user/import-supplied code is sanctioned on CREATE — * mirrors the `surchargeable à la création` facet. Never on update. */ supplied: z.boolean().optional(), /** `{app}.{kebab(entity)}` — forwarded so the create-only SmartCodeField * can call /api/codes/{codeKey} without re-deriving. */ codeKey: z.string().min(1).optional(), /** Fields the mask's derived tokens reference (SmartCodeField inputs). */ codeInputFields: z.array(z.string().min(1)).optional(), }) .passthrough(), ]) export type CodedEntityFlag = z.infer /** True when the pagespec declares a coded entity — `true` or ANY object form. * THE reader that replaces every historical `=== true`. */ export function isCodedEntity(flag: unknown): boolean { if (flag === true) return true return typeof flag === 'object' && flag !== null && !Array.isArray(flag) } /** True when the flag sanctions a supplied code on the CREATE surface. */ export function isSupplied(flag: unknown): boolean { return ( typeof flag === 'object' && flag !== null && (flag as { supplied?: unknown }).supplied === true ) } /** * The business label to render for the code column/field in `locale`, or null * when the flag carries none (callers fall back to their own floor). * fr resolves fr ?? en; every other locale resolves en ?? fr — a label * authored in ONE language still beats the humanised property name. */ export function codedLabelOf(flag: unknown, locale: 'fr' | 'en' | 'it' | 'de'): string | null { if (typeof flag !== 'object' || flag === null) return null const label = (flag as { label?: { fr?: unknown; en?: unknown } }).label if (typeof label !== 'object' || label === null) return null const fr = typeof label.fr === 'string' && label.fr.trim() !== '' ? label.fr : null const en = typeof label.en === 'string' && label.en.trim() !== '' ? label.en : null return locale === 'fr' ? (fr ?? en) : (en ?? fr) } /** * Rewrite the `supplied` slot, PRESERVING every other key. A flag that would * become an otherwise-empty `{ supplied: false }` collapses back to the * canonical boolean `true` (byte-stable pagespecs for the common case). */ export function withSupplied(flag: CodedEntityFlag | undefined, supplied: boolean): CodedEntityFlag { const base: Record = typeof flag === 'object' && flag !== null ? { ...flag } : {} if (supplied) return { ...base, supplied: true } delete base['supplied'] return Object.keys(base).length === 0 ? true : (base as CodedEntityFlag) }