Handbook
Survey Assembly and Coverage Engine
Architecture target (AFF v0.1). Implementation flags live in implementation-status.md. Do not treat this document as a shipped product contract.
Updated
Architecture target (AFF v0.1). Implementation flags live in implementation-status.md. Do not treat this document as a shipped product contract.
Objective
Assemble a short survey that respects mandatory content, measurement continuity, PDP needs, cooldowns, respondent relevance, anonymity, and long-term coverage.
Pure random choice is not sufficient. It can repeatedly select popular items, starve low-weight items, break bundles, and make full coverage unpredictable.
Selection pipeline
1. Create the survey context
Inputs:
- tenant and entitlements
- subject, respondent, relationship, and roles
- active pack assignments
- survey window and cadence
- quota: minimum, target, soft maximum, hard maximum
- active PDP goals and milestones
- exposure ledger
- coverage cycles
- quiet-period and suppression policies
2. Build candidates
A question variant is a candidate only when:
- pack and assignment are active
- subject and respondent are in scope
- relationship and role are applicable
- a variant exists for the cadence and language
- observation-window requirements are met
- it is not retired or suppressed
- cooldown and fatigue limits permit it, unless a permitted override applies
- it does not conflict with a selected item
- data-collection purpose and consent permit it
3. Reserve hard requirements
Reserve, in order:
- legal or compliance acknowledgements, if any
- one-time mandatory program items
- PDP
force_next_onceitems - overdue anchors required for trend continuity
- required bundle items
If hard requirements exceed the hard maximum, do not silently drop them. Apply a configured overflow policy:
- split into two micro-surveys
- deliver mandatory items now and defer lower-priority items with an alert
- temporarily exceed the soft maximum up to the hard maximum
- require admin resolution before launch
4. Add anchors
Anchors should be stable enough to show trends but not dominate every survey. The pack defines the desired anchor count and each anchor’s own cooldown.
5. Add rotating questions and bundles
Use a constrained weighted deficit approach. A candidate’s score may include:
- coverage deficit: has it been asked in the current cycle?
- overdue amount: how far beyond first eligibility is it?
- tenant priority
- construct coverage gap
- PDP relevance
- respondent suitability
- need for additional observations because of high Not Observed or low sample size
- fatigue penalty
- semantic similarity penalty to questions already selected
- recent exposure penalty to the same construct
Illustrative score:
score =
5 * mandatory_boost
+ 4 * pdp_boost
+ 3 * coverage_deficit
+ 2 * overdue_factor
+ 1 * tenant_priority
+ 1 * evidence_need
- 3 * fatigue_penalty
- 2 * semantic_similarity
- 2 * construct_saturation
The coefficients are product policy, not psychometric constants.
6. Add optional qualitative prompt
Use a targeted prompt when there is room and the pack permits it. Avoid generic “anything else” by default.
7. Validate assembled survey
- quota and hard maximum
- no duplicate family unless deliberately paired
- relationship applicability
- observation-window compatibility
- bundle integrity
- language availability
- mandatory completion
- anonymity and purpose rules
8. Freeze and log
Store the selected variant text and metadata in survey items. Record deterministic selection inputs, seed, candidate list hash, and decision reasons.
Deterministic randomness
Randomness may break ties, but selection should be reproducible:
seed = hash(tenant_id, survey_window_id, subject_id, respondent_id, pack_version)
This enables audit, debugging, and consistent previews.
Coverage-cycle model
Each rotation pool has a coverage cycle containing eligible coverage units:
- question family
- question variant
- atomic bundle
- construct target
The normal unit should be the family, so a wording-version change does not reset coverage.
A family is covered when at least one eligible variant is presented to the intended relationship in the cycle. A stricter pack may require coverage by every relationship.
Feasibility test
Before publication, calculate:
required_units = anchor_units_due + mandatory_units + rotation_units + atomic_bundle_costs
available_slots = surveys_in_target_horizon * rotating_slots_per_survey
Full coverage is feasible only when available slots can satisfy all eligible units after accounting for bundles, cooldowns, and relationship routing.
The simulator should produce:
- minimum possible completion period
- expected completion period
- worst-case completion period
- starving items
- impossible bundles
- quota changes needed to reach the target
Bundle handling
Atomic bundle
All items are selected together. The bundle has one selection score and consumes the sum of its item costs.
Splittable bundle
The engine selects between min_pick and max_pick. Internal selection may use its own coverage ledger.
Branching bundle
A screening item is shown first. Follow-ups are assembled dynamically only when the answer activates them. Reserve a potential quota budget or use a second survey page.
Mutually exclusive bundle
Only one branch can be selected in a survey or coverage cycle.
Pseudocode
assemble(context):
candidates = filter_library(context)
selected = []
selected += reserve_required(candidates, context)
enforce_overflow_policy(selected, context)
selected += select_due_anchors(candidates, context, remaining_slots)
while remaining_slots > 0:
feasible = candidates_that_fit(selected, remaining_slots)
if feasible is empty:
break
next = weighted_deficit_select(feasible, context)
selected += materialize(next)
selected += maybe_add_qualitative_prompt(context, remaining_slots)
validate(selected, context)
freeze_and_log(selected, context)
return selected
Recommended default mixes
| Cadence | Typical size | Anchor | Rotation | PDP/event | Open text |
|---|---|---|---|---|---|
| Daily | 1–2 | 0–1 | 1 | 0–1 | 0 |
| Weekly | 3–5 | 1 | 1–3 | 0–1 | 0–1 |
| Monthly | 6–10 | 2 | 3–6 | 1–2 | 1 |
| Quarterly | 12–24 | 4–8 | 6–12 | 2–4 | 1–2 |
| Annual | 30–50 | 15–25 | 10–20 | 3–6 | 3–5 |
These are defaults, not universal limits.