Skip to content

forms: both of the rule vocabulary's safety checks are bypassed by a single wrapper #544

Description

@Yaraslaut

Part of the sweep tracked in #518. Finding F26.

Summary

Two separate gaps in the same subsystem, both reachable by wrapping a rule in one combinator.

(a) Unsatisfiable-form detection stops at the first compound node. rejectUnsatisfiableRules (forms.hpp:2373-2381) skips any node without a fields key:

auto const kindEntry   = node->find("kind");
auto const fieldsEntry = node->find("fields");
if (kindEntry == node->end() || fieldsEntry == node->end()) {
    continue;
}

And/Or/Not emit conditions/condition, never fields (forms.hpp:1507-1514, 1555-1562, 1601-1606), so the loop continues past them without descending.

(b) A presentation rule is accepted as a nested condition. andOf/orOf/notOf (forms.hpp:1525-1529, 1573-1577, 1614-1617) constrain their operands only to "has test(const A&) const noexcept". VisibleWhen::test and ReadonlyWhen::test return true unconditionally by design (forms.hpp:1394-1397, 1448-1451), so they satisfy the shape and are accepted.

Verification status

Reproduced. Revision: master @ 4017228d. Compiled against the repo's pinned glaze.

(a):

Direct  (ruleList(exactlyOneOf(&a,&b)))                     -> THROWS UnsatisfiableFormError
Wrapped (ruleList(andOf(exactlyOneOf(&a,&b), engaged(&c)))) -> NO THROW, ships

Wrapped is genuinely dead: required is [a,b,c] while the rule caps a+b at exactly one, so allRequiredEngaged && allRulesSatisfied can never both hold.

(b) — andOf(visibleWhen(...), notEngaged(&a)) compiles and emits:

"when":{"kind":"and","conditions":[
   {"kind":"visibleWhen","fields":["a"],"when":{"kind":"engaged","fields":["a"]}},
   {"kind":"notEngaged","fields":["a"]}]}

In C++ the visibleWhen operand contributes a constant true, so the and collapses to its sibling. In the renderer, a when tree containing kind: "visibleWhen" hits no case in the condition vocabulary.

Not verified: I did not confirm the renderer's exact behaviour on the unknown condition kind (ignore vs. treat-as-false); either way client and server disagree.

Why these are defects and not design choices

For (a): the check's existence is what authors will trust. The identical contradiction is a hard build failure in one spelling and a silently unsubmittable form in the other — which is worse than not checking at all. forms.hpp:2342-2354 draws the tolerate/refuse line on consequence, deliberately and well; this is a gap in the implementation of that policy, not in the policy.

For (b): docs/spec/forms/forms.md:1744 ("Two evaluators, one corpus") states the invariant this breaks.

Suggested fix

(a) In rejectUnsatisfiableRules, when a node has no fields but has conditions (array) or condition (object), recurse into them with the same requiredNames. ~10 lines, no template changes.

(b) Give condition nodes a marker (static constexpr bool isCondition = true, or a Condition concept) and constrain andOf/orOf/notOf/requiredWhen/visibleWhen/readonlyWhen's when parameter on it. VisibleWhen/ReadonlyWhen/RequiredWhen do not get the marker.

Two cheap diagnostics fixes while here

  • andOf recovers the action type from its first operand only (forms.hpp:1527) and never checks the rest. Measured: andOf(engaged(&C::x), engaged(&B::y)) produces 153 lines of spew, first error inside std::is_nothrow_invocable in <type_traits>, the user's formRules line mentioned only around line 140. A requires (std::same_as<ConditionActionType<Cond0>, ConditionActionType<Conds>> && ...) makes it one line at the call site.
  • equals(&A::quantityMember, "URGENT") produces 83 lines, first error at forms.hpp:1092 (no match for 'operator=='), with no mention of equals. Same fix shape.
  • andOf(x) with a single operand compiles, though forms.hpp:1488 documents "at least two". Add a static_assert.

What would change the verdict

  • Close (a) if compound-node contradiction detection is declared out of scope — but then the direct spelling should stop throwing too, so the two agree.
  • Regression tests: the two repros above. A test using only the direct spelling passes with or without the fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: formsSubsystem: formsbugSomething isn't workingtriage: rescopeReal problem, wrong framing; rewrite before building

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions