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
- 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.
- 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.
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.
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.
- 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.
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 — seeKNOWN_QUESTION_TYPES:_iter_elements()walks every name-bearing element on each page and_validate_questions()rejects any whosetypeisn'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, nooption_key, novalue_*column, no warehouse row).The validator's allowlist is a question-type allowlist. A static
panelbelongs 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 staticpanelwithquestionsOrder: "random"and tag the panel withconstruct_block. With static panel unsupported, the only block-aware-randomization shape today is matrix-as-block, which constrains item types to matrix cells.Scope
_iter_elements()recurses intopanel.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.elementswithout emitting a sub-question for the panel itself. Distinct from matrix/paneldynamic, where the container does decompose into sub-questions.construct_blockinheritance (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.construct_tag_stabilitytest — needs to know that an authoredconstruct_blockon 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.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
namelives 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.Panel.questionsOrderin spirit — values are stable across re-orderings, sovisibleIfis order-invariant. No new gate needed.Out of scope
Survey.pagesOrder) — separate §5 deferral, requires a custom runtime shuffler (not native SurveyJS).choice_display_ordermarts 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.