|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * # The section / page-component **editability boundary** (#7887) |
| 5 | + * |
| 6 | + * Maintainer ruling, 2026-08-12: `FormSectionSchema` (`ui/view.zod.ts`) and |
| 7 | + * `PageComponentSchema` (`ui/page.zod.ts`) gate **visibility only**; editability |
| 8 | + * lives on fields. No `disabled` / `readonly` / `readonlyWhen` slot is added to |
| 9 | + * either shape, and no alias row is registered for them — an alias names a key |
| 10 | + * the shape must then accept, and one the runtime does not honour is the |
| 11 | + * ADR-0049 declared-but-unenforced class this repo is retiring elsewhere. |
| 12 | + * |
| 13 | + * What the ruling *does* buy the author is this module: the rejection stops |
| 14 | + * being bare and starts naming where the key belongs. |
| 15 | + * |
| 16 | + * ## Package-internal on purpose — this module is NOT in `shared/index.ts` |
| 17 | + * |
| 18 | + * It sits beside `strict-object.ts` and `alias-probe.ts` in the set of shared |
| 19 | + * modules the barrel deliberately does not re-export. An unknown-key options |
| 20 | + * table is machinery for declaring schemas in *this* package, and its own type |
| 21 | + * (`StrictObjectOptions`) is not public either — a published const of an |
| 22 | + * unpublishable type is an export no consumer can even annotate. |
| 23 | + * |
| 24 | + * It also keeps the #7887 claim exactly true: the card's whole deliverable is |
| 25 | + * that nothing observable moves except the sentence an author reads, and the |
| 26 | + * package's public API surface (`check:api-surface`) does not move at all. |
| 27 | + */ |
| 28 | + |
| 29 | +import type { StrictObjectOptions } from './strict-object'; |
| 30 | +import type { KeySetGuidance } from './suggestions.zod'; |
| 31 | +import { VISIBILITY_STRICT_OPTIONS } from './visibility'; |
| 32 | + |
| 33 | +/** |
| 34 | + * The editability vocabulary an author reaches for on a shape that gates |
| 35 | + * **visibility only**. |
| 36 | + * |
| 37 | + * Every spelling here is rejected by `FormSectionSchema` and |
| 38 | + * `PageComponentSchema` today and stays rejected: this set changes the MESSAGE, |
| 39 | + * never the verdict. `readOnly` sits alongside `readonly` because set |
| 40 | + * membership is matched case-sensitively (the rename channel is what folds |
| 41 | + * case, and a set match `continue`s past it). |
| 42 | + */ |
| 43 | +const EDITABILITY_BOUNDARY_KEYS = [ |
| 44 | + 'disabled', |
| 45 | + 'disabledWhen', |
| 46 | + 'readonly', |
| 47 | + 'readOnly', |
| 48 | + 'readonlyWhen', |
| 49 | + 'editable', |
| 50 | +] as const; |
| 51 | + |
| 52 | +/** |
| 53 | + * The ruling rendered as the thing an author actually reads: **boundary, not |
| 54 | + * gap.** |
| 55 | + * |
| 56 | + * Deliberately points at **`readonlyWhen`** and not at `disabledWhen`: |
| 57 | + * `field.zod.ts` renames `disabled → readonly` and records in its own comment |
| 58 | + * that "a field has `readonlyWhen`, not `disabledWhen`" (#7832). Naming |
| 59 | + * `disabledWhen` here would send an author to a key that exists on no field |
| 60 | + * surface at all — a rejection that hands them their next rejection. |
| 61 | + */ |
| 62 | +const EDITABILITY_BOUNDARY_GUIDANCE: KeySetGuidance = { |
| 63 | + name: 'EDITABILITY_BOUNDARY_KEYS', |
| 64 | + keys: EDITABILITY_BOUNDARY_KEYS, |
| 65 | + prescription: |
| 66 | + 'Editability is a FIELD-level concern. This shape gates VISIBILITY only — a ' |
| 67 | + + 'deliberate boundary, not a missing key (#7887): a section / page component has ' |
| 68 | + + 'no read-only semantics of its own to enforce. Write `readonly: true` (or the ' |
| 69 | + + 'conditional `readonlyWhen` predicate) on the form field(s) inside it instead; to ' |
| 70 | + + 'hide the whole section or component, use `visibleWhen`.', |
| 71 | +}; |
| 72 | + |
| 73 | +/** |
| 74 | + * {@link VISIBILITY_STRICT_OPTIONS} for the two shapes that gate visibility and |
| 75 | + * **nothing else** — `FormSectionSchema` and `PageComponentSchema`. |
| 76 | + * |
| 77 | + * ## Why the boundary prescription is filed HERE and not in the shared table |
| 78 | + * |
| 79 | + * `VISIBILITY_STRICT_OPTIONS` has **three** consumers, and the third — |
| 80 | + * `FormFieldSchema` — is the one view/page shape that *does* answer `disabled`, |
| 81 | + * through its own `aliases: { disabled: 'readonly' }` row (`view.zod.ts`, whose |
| 82 | + * comment at that site rejects shared-table filing for exactly this reason). |
| 83 | + * Adding `EDITABILITY_BOUNDARY_KEYS` to the shared options would land it on that |
| 84 | + * table too, and the consequences are not cosmetic: |
| 85 | + * |
| 86 | + * - `strictUnknownKeyError` consults exact `guidance` → `guidanceSets` → |
| 87 | + * `aliases`, and a set match `continue`s past the rename. The field author who |
| 88 | + * writes `disabled` would stop seeing *"Did you mean `disabled` → `readonly`?"* |
| 89 | + * and start being told editability is somewhere else — on the one surface |
| 90 | + * where it is right there. |
| 91 | + * - `alias-integrity.test.ts` would go **red**, not quietly wrong, in two |
| 92 | + * places: its #7889 check fails any alias row a guidanceSet on the same table |
| 93 | + * already consumes, and its #6619 check fails a set member the shape |
| 94 | + * *declares* — which `readonly` is, on `FormFieldSchema`. |
| 95 | + * |
| 96 | + * So the two visibility-only shapes take these options and `FormFieldSchema` |
| 97 | + * keeps the bare ones. The prescription text is written once, here. |
| 98 | + */ |
| 99 | +export const VISIBILITY_ONLY_STRICT_OPTIONS: StrictObjectOptions = { |
| 100 | + ...VISIBILITY_STRICT_OPTIONS, |
| 101 | + guidanceSets: [ |
| 102 | + // Declaration order decides among sets. Nothing in |
| 103 | + // `EDITABILITY_BOUNDARY_KEYS` matches `VISIBILITY_KEY_PATTERN` |
| 104 | + // (`/vis|conceal|hidden|show.?when/i`), so the order is not load-bearing — |
| 105 | + // pinned in `editability-boundary.test.ts` so it cannot quietly become so. |
| 106 | + ...(VISIBILITY_STRICT_OPTIONS.guidanceSets ?? []), |
| 107 | + EDITABILITY_BOUNDARY_GUIDANCE, |
| 108 | + ], |
| 109 | +}; |
0 commit comments