|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import type { SemanticMigration } from '../../types.js'; |
| 4 | + |
| 5 | +// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code |
| 6 | +// span already, and a nested backtick would close it. |
| 7 | +export const entry: SemanticMigration = { |
| 8 | + id: 'field-rule-predicate-evaluated-slot-source-required', |
| 9 | + surface: |
| 10 | + 'a field-rule predicate, all three slots of the triad on FieldSchema — visibleWhen, ' |
| 11 | + + 'readonlyWhen and requiredWhen — and with them the PredicateSchema / PredicateInputSchema ' |
| 12 | + + 'public exports every other declaration site composes, authored either as an expression ' |
| 13 | + + 'envelope carrying only ast ({ dialect: \'cel\', ast: … } with no source), or with a source ' |
| 14 | + + 'that is blank after trimming, through the envelope key ' |
| 15 | + + '({ dialect: \'cel\', source: \' \' }) or the bare-string shorthand for it ' |
| 16 | + + '(visibleWhen: \' \'). Reachable wherever an object is authored or stored: ' |
| 17 | + + 'defineStack({ objects }) sources, an exported stack passed to objectstack validate, a ' |
| 18 | + + 'POST /objects body, and an object row already sitting in sys_metadata', |
| 19 | + replacement: |
| 20 | + 'a non-blank `source` — `{ dialect: \'cel\', source: \'record.status == \\\'paid\\\'\' }`, or the ' |
| 21 | + + 'bare string, or the `P` tagged template the examples use — if the rule was meant to gate; ' |
| 22 | + + 'or REMOVE the key entirely if the field was meant to carry no rule. ⚠️ Unlike the ' |
| 23 | + + 'flow-edge entry this one otherwise mirrors, removal here is BEHAVIOUR-PRESERVING and is a ' |
| 24 | + + 'safe default: all three slots resolve BOTH refused spellings to a no-op today (measured, ' |
| 25 | + + 'see `reason`), so deleting the key preserves exactly what the runtime already did. What ' |
| 26 | + + 'removal does NOT preserve is the author\'s INTENT — the rule was written to gate ' |
| 27 | + + 'something, and a removal records that it never did. Authoring the `source` is the ' |
| 28 | + + 'repair; removal is the honest way to say the rule was abandoned. An `ast` BESIDE a string ' |
| 29 | + + '`source` is untouched and stays admitted everywhere', |
| 30 | + reason: |
| 31 | + 'Card #17778, on the maintainer ruling for objectui#8069 (decision batch #119 item 3, ' |
| 32 | + + '2026-09-12: 「同意」 to A, Q2 yes, Q3 yes) and the standing ruling ' |
| 33 | + + '「我们的项目以objectstack 协议为准…协议不正确的应该先修改协议。」. ADR-0136 D1: a predicate is an ' |
| 34 | + + 'EVALUATED slot by definition, so `PredicateSchema` / `PredicateInputSchema` now compose ' |
| 35 | + + '`EvaluatedExpressionSchema` / `EvaluatedExpressionInputSchema` instead of the PERSISTENCE ' |
| 36 | + + 'contract `ExpressionSchema` / `ExpressionInputSchema`, and the field-rule triad binds ' |
| 37 | + + 'them. The CEL engine evaluates `source` alone (`cel-engine.ts` `evaluate`: "AST-only ' |
| 38 | + + 'evaluation not yet supported; persist `source`"), so both refused spellings parsed, ' |
| 39 | + + 'registered, passed `objectstack validate`, and then produced a rule that silently did ' |
| 40 | + + 'nothing. ⚠️ The three no-op directions are NOT the same mechanism, and each was measured ' |
| 41 | + + 'at its own end rather than inferred from the others: `visibleWhen` is never evaluated ' |
| 42 | + + 'server-side at the FIELD level at all (`rule-validator.ts`\'s `ConditionalFieldDef` has no ' |
| 43 | + + 'such member) and the renderer falls back to VISIBLE where no host publishes a predicate ' |
| 44 | + + 'scope; `readonlyWhen` reaches `isReadonlyWhenLocked`, whose unbound-ROOT arm returns ' |
| 45 | + + 'LOCKED but whose every-other-fault arm logs "change allowed through" and returns false, ' |
| 46 | + + 'and an unevaluable envelope takes that second arm — the declared lock is silently waived; ' |
| 47 | + + '`requiredWhen` logs and SKIPS the check. So one misspelled or unauthored predicate ' |
| 48 | + + 'produced a form that showed more, locked less and demanded less at once, with no state ' |
| 49 | + + 'that said "this rule did not run" — which is the composite objectui#8069 measured and ' |
| 50 | + + 'this entry\'s ruling closes. ⚠️ No D2 conversion is possible, and that is why this needs ' |
| 51 | + + 'a D3 entry rather than none: an `ast`-only envelope carries no `source` to derive one ' |
| 52 | + + 'from (lowering an AST to surface syntax is the compiler direction the platform does not ' |
| 53 | + + 'run), and a blank `source` names no predicate to reconstruct. Choosing between "author ' |
| 54 | + + 'the rule" and "drop the rule" is an intent question only the author can answer, which is ' |
| 55 | + + 'what this entry delegates. ⚠️ Scope of the RUNTIME half, stated so it is not read wider ' |
| 56 | + + 'than it is: ADR-0136 D2 (a faulting field-rule predicate refuses the SUBMIT, naming the ' |
| 57 | + + 'field and the rule) and D3 (visibility stays fail-OPEN at RENDER) are consequences ' |
| 58 | + + 'CONSUMERS deliver — `packages/spec` carries no business logic — and the renderer half ' |
| 59 | + + 'lands in objectui#8069. This entry is the AUTHORING refusal only. ⚠️ And one ' |
| 60 | + + 'stored-row consequence is deliberately left UNMEASURED rather than guessed: ' |
| 61 | + + '`applyConversionsToStoredItem` IS applied to `object` (only `flow` is skipped, ' |
| 62 | + + '`spec/src/conversions/stored.ts` and the same skip in ' |
| 63 | + + '`metadata/src/loaders/database-loader.ts` `rowToData`), but no conversion can repair ' |
| 64 | + + 'either refused spelling, so a stored row carrying one now meets a strict schema at ' |
| 65 | + + 'whichever seam parses it; which seam that is, and whether it degrades to a warn or ' |
| 66 | + + 'refuses the object, was NOT measured on this card and is named here as the open edge ' |
| 67 | + + 'rather than asserted. A repo-wide census over examples/, packages/, content/ and skills/ ' |
| 68 | + + 'at 03b7b8187 found ZERO field-rule predicates of either spelling against two live lit ' |
| 69 | + + 'controls (non-blank tagged-template predicates: 15 files; envelope-form predicates: 14 ' |
| 70 | + + 'files), so there is nothing in THIS repository to rewrite — a repo reading, which is why ' |
| 71 | + + 'the notification is registered here rather than skipped. ADR-0136, ADR-0087, ADR-0089, ' |
| 72 | + + 'ADR-0058, ADR-0124.', |
| 73 | + acceptanceCriteria: |
| 74 | + 'Grep every authored field-rule predicate — all three of `visibleWhen`, `readonlyWhen` and ' |
| 75 | + + '`requiredWhen` on a FIELD (not the per-OPTION `visibleWhen`, not the inline-column pair, ' |
| 76 | + + 'and not a view/page/action gate: those slots still compose `ExpressionInputSchema` and ' |
| 77 | + + 'are unaffected by this entry) — in `defineStack({ objects })` sources, exported stacks ' |
| 78 | + + 'and `POST /objects` bodies, and every object row in `sys_metadata`, for an envelope with ' |
| 79 | + + 'no `source` key and for a `source` (or bare string) that is empty after trimming. For ' |
| 80 | + + 'each hit decide, per the `replacement` note, whether the rule was meant to gate (author ' |
| 81 | + + 'the `source`) or was abandoned (remove the key) — removal is behaviour-preserving here, ' |
| 82 | + + 'so it is a safe default, but it records that the rule never ran. Two proofs. (1) For a ' |
| 83 | + + 'stack authored in config files, `objectstack validate` is clean: it locates each offender ' |
| 84 | + + 'with the `EVALUATED_EXPRESSION_SOURCE_REQUIRED` sentence at ' |
| 85 | + + '`objects.N.fields.N.visibleWhen` (and the two siblings) — a blank `source` inside an ' |
| 86 | + + 'envelope surfaces as one issue at `source`, while an `ast`-only envelope and a blank ' |
| 87 | + + 'bare string abort both arms of the input union and surface as one `invalid_union` at the ' |
| 88 | + + 'SLOT, carrying that same sentence. There is no CLI verb that lowers a stored row back ' |
| 89 | + + 'into a config file, so this proof does not reach an object that exists only in ' |
| 90 | + + '`sys_metadata`. (2) For stored rows, boot the stack and confirm each object still loads ' |
| 91 | + + 'and its form still renders: a row carrying either spelling no longer parses, and per the ' |
| 92 | + + '`reason`\'s last note the seam that reports it was not measured on this card — so read ' |
| 93 | + + 'the boot log for the object by name rather than expecting a specific message, and treat ' |
| 94 | + + 'a missing object as this entry\'s signal. An object whose field rules all carry a ' |
| 95 | + + 'non-blank `source` parses byte-identically to before.', |
| 96 | +}; |
0 commit comments