Skip to content

api+dbt: support static panel as a structural grouping element in the publish gate #68

Description

@countercheck

Source: docs update for randomization, 2026-06-11 (design doc §5 deferred decisions, survey-engine-design-doc.md)
Severity: Low (no shipped survey needs it today)
Category: Authoring ergonomics / publish-gate type surface

Problem

A static panel (a non-repeating SurveyJS grouping container) is rejected at publish today — see KNOWN_QUESTION_TYPES:

KNOWN_QUESTION_TYPES = (
    FREE_TEXT_TYPES | OPTION_TYPES | MATRIX_TYPES | REPEATING_TYPES | SCALAR_TYPES
)

_iter_elements() walks every name-bearing element on each page and _validate_questions() rejects any whose type isn't in that set. A { "type": "panel", "name": "...", "elements": [...] } falls into the net and the gate says "unsupported type" — even though structurally it isn't a question at all (no answer, no option_key, no value_* column, no warehouse row).

The validator's allowlist is a question-type allowlist. A static panel belongs in a third category — name-bearing element the gate descends through rather than treats as a publish unit. That distinction doesn't exist today.

Motivation

This surfaced in the randomization docs work (design doc §3.5 "Randomization and display-order capture"): the natural way to get block-aware shuffling for a reusable scale that doesn't fit a matrix shape — e.g., a mix of radiogroups plus a free-text item belonging to one instrument — is to put the items in a static panel with questionsOrder: "random" and tag the panel with construct_block. With static panel unsupported, the only block-aware-randomization shape today is matrix-as-block, which constrains item types to matrix cells.

Scope

  1. Validator (api/survey_engine/validation.py) — _iter_elements() recurses into panel.elements; _validate_questions() skips the panel itself but lints its children. Name-uniqueness, dangling-visibleIf, and duplicate-option checks already operate on the flattened set, so they come along once iteration descends.
  2. dbt staging (dbt/models/intermediate/int_survey_questions.sql, int_survey_options.sql) — descend into static-panel elements without emitting a sub-question for the panel itself. Distinct from matrix/paneldynamic, where the container does decompose into sub-questions.
  3. construct_block inheritance (design doc §3.5 "Construct membership") — current rule is "inherits on matrix/paneldynamic because the warehouse generates sub-questions there." A static panel doesn't generate sub-questions, so panel-level inheritance would be a different choice — pure authoring convenience rather than a warehouse-shape consequence. Probably worth doing (it's the whole point of grouping a scale's items), but a separate design call to make explicit.
  4. construct_tag_stability test — needs to know that an authored construct_block on a panel container is inherited rather than the leaf's own tag, so a leaf-tag-changes-across-versions check doesn't fire spuriously when the inheritance source moved.
  5. Pattern + docs — add a patterns/static_panel.json (and possibly fold it into patterns/randomization.json); update patterns/README.md, the type-support table in survey-engine-for-researcher.md, and remove the §5 deferral from the design doc when shipped.

Constraints

  • Naming: a static panel's name lives in the same global namespace as question names (publish gate already enforces uniqueness). A panel with the same name as a question must still be rejected; the iteration change has to preserve this.
  • The publish gate's randomization-vs-routing safety section (design doc §3.6) already covers Panel.questionsOrder in spirit — values are stable across re-orderings, so visibleIf is order-invariant. No new gate needed.

Out of scope

  • Survey-level page randomization (Survey.pagesOrder) — separate §5 deferral, requires a custom runtime shuffler (not native SurveyJS).
  • The choice_display_order marts surface — separate §5 deferral; raw capture is already in place.

When to revisit

A survey author wants a randomized block of mixed-type items (not a matrix shape) and the matrix-as-block workaround is genuinely the wrong fit. Until then, deferred.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions