Skip to content

validate subset: catch dangling references and bad literals in relevant / constraint #73

Description

@jstet

Where the gap is

XLSValidator.validateSubset checks the registry-driven subset: type allowlist, appearances, name/code uniqueness, choice-list resolvability. It does not look at the relevant or constraint columns — both can hold anything, including references to question names that don't exist or literals that aren't one of the question's choices.

That gap is the silent failure mode behind a real bug found in CorrelAid/formulaid#47 follow-up: a follow-up text question was written without a relevant expression (and with the label of a parent Sonstiges option), so Kobo showed it unconditionally. The wizard's own subset check approved the file, and only the user noticed in the field.

Proposed rule

For every row whose relevant or constraint is non-empty, validate:

  1. Referenced names exist. Every ${name} reference inside the expression points to a name that appears elsewhere in the survey sheet (case-insensitive, exact match after sanitization). Unknown names → error.
  2. Literals are valid choices. For ${x} = 'literal' and selected(${x}, 'literal'), literal must be one of x's choice codes (for select_*) or one of x's allowed value forms (for integer/decimal/text). Mismatched literal → warning (the file may still convert, but the predicate can never be true).
  3. Empty expression is fine. Absence of relevant / constraint is not a finding — XLSForm allows it. The rule fires only when the column is present.

Why it belongs in formtransform

  • It's a registry-driven structural check, like the existing name/uniqueness/choice-list rules.
  • It generalises to every XLSForm author, not just formulaid. Bad relevant expressions are a common silent failure across the Kobo/LimeSurvey/DDI ecosystems.
  • sanitize.ts in formulaid already rewrites relevant for renames (@${name} → @${renamed}); a real parser/validator closes the loop.

API shape

Reuse the existing SubsetViolation:

{ severity: 'error',   message: "relevant references unknown question: ${nonexistent}" }
{ severity: 'warning', message: "constraint literal 'foo' is not one of question 'x' choices" }

No change to the public surface; just more findings out of validateSubset. Same target: 'lstsv' | 'ddi' semantics: warn on rules that don't apply to one target.

Tests

  • A row whose relevant references an unknown name → error.
  • A row whose relevant has a literal that isn't in the referenced question's choices → warning.
  • A row with no relevant / constraint → no finding.
  • The fixture from the formulaid bug (real file, broken follow-up) → relevant findings.

Out of scope

  • Parsing the XLSForm expression language (boolean logic, function calls). The check stays structural: name + literal, like the sanitizer already does at sanitize.ts:107. A full parser belongs in a follow-up if the structural rule proves noisy.
  • Anything semantic ("this relevant doesn't make sense for the question's intent"). That's wizard-side.

Follow-up in formulaid

A wizard-specific quality check (Sonstiges-follow-up heuristic) will land in formulaid regardless; it complements this rule, doesn't depend on it. The upstream check here is the long-term answer for non-formulaid users.

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

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions