v0.14.1
Lands the two validation fixes that were open as PRs #22 and #23 before the 0.14.0 shape-validation rewrite, rebased onto it. All three close gaps where a malformed guard committed silently and then never fired. The write gate stays diff-aware, so existing snapshots remain editable and only newly authored breakage is refused.
Fixed
- A condition leaf with the operator under the wrong key is named, with the fix.
apply_batchtakes each op as an untyped bag, so a leaf could arrive as{ left, op: "neq", right }(a host UI's visibility vocabulary): every consumer readoperator: undefinedand the comparison never fired, while dry_run reported valid. The shape pass now rejects unknown leaf keys and names the likely intent: "Did you mean "operator"?" forop, "Did you mean "right"?" forvalue/expected. Works at every depth of a composite condition. - A fabricated invariant consequent is rejected instead of silently inverting the invariant. An invariant is
{ name, condition, message }; authors writing an implication routinely invent a consequent field (mustHold,then,implies,ensure), which the builders cherry-picked away, collapsing "A implies B" into "A must always be true": the exact inverse of the intent, firing on legal data.buildInvariant/buildInvariantPatchnow throw against the raw input, naming the supported spelling (condition: { kind: "any", conditions: [{ kind: "not", condition: A }, B] }); the shape pass flags any key that survived into a stored snapshot; and the operations resource plus the rule schema description document the invariant shape and the operator vocabulary up front, which is what invited the wrong guess. - Enum domains are enforced on the values that actually flow through a path. An enum state definition names the CLOSED set of values a path may hold, but only
defaultValuewas checked: aset_statecould write a value outside the domain, and a condition could compare against one, a comparison that is provably dead. Both are now flagged in reference integrity (effects, rule and invariant conditions, feature-level invariants included), resolved throughvalueSetsexactly like the defaultValue check. Raw string literals only: an Expression resolves at run time, so it is left alone rather than guessed at. The driving defect: a plan enum of [free, premium] whose upgrade action wrote "family" left everyplan == "premium"gate silently denying the customer who paid the most.