CatalyzeUpDocs
impact pulse / product

Structured Assessments

Structured Assessments Specification

Overview

Structured Assessments unify the generic survey system with the Maslow Pyramid Assessment into a single engine. Instead of maintaining two separate systems (surveys with flat scoring and Maslow with hardcoded pyramid logic), the survey model gains question groups and result visualization types, making the Maslow assessment a configuration of the generic system rather than a separate codebase.

This enables organizations to create any grouped, scored assessment (wellbeing indices, team health checks, competency frameworks) with rich visualizations, all using the same survey builder, scoring engine, and analytics pipeline.

Related Specs: Scoring System | Maslow Assessment | Database Schema

Problem Statement

The current system has two independent paths:

Issues:

  • Two separate API surfaces (/surveys/* and /maslow/*) for fundamentally the same thing: asking questions and scoring answers
  • Maslow questions, weights, and scoring logic are hardcoded in Python, not configurable
  • No way for organizations to create their own structured assessments (e.g., a "Team Culture" pyramid or a "Wellbeing Radar")
  • Two separate database collections storing similar data
  • Dashboard aggregation code must query both systems independently
  • Frontend maintains parallel result rendering paths

Solution: Question Groups + Result Types

The Core Abstraction

A question group is an ordered collection of questions within a survey that share a label, color, and scoring aggregation. This is exactly what a Maslow "level" is. ✅

A result type tells the frontend how to render the grouped results: as a pyramid, radar chart, grouped bars, or flat score. ✅

With these two additions, the Maslow assessment becomes: a survey template with 6 question groups, 24 weighted rating questions, and result_type: "pyramid".

Data Model Changes

Survey Model (additions)

interface Survey {
  // ... existing fields (title, description, org_id, status, etc.)

  // NEW: How to visualize grouped results
  result_type: "flat" | "pyramid" | "radar" | "breakdown"

  // NEW: Ordered list of question groups
  question_groups: QuestionGroup[]

  // NEW: Optional link to a template this survey was created from
  template_id?: string
}

QuestionGroup Model (new)

interface QuestionGroup {
  id: string
  name: string           // e.g., "Vital", "Safety & Development", "Esteem"
  order: number          // Display order (0 = base of pyramid, N = top)
  color: string          // Theme color for this group (e.g., "red", "blue")
  tint?: string          // Color variant (e.g., "700", "500")
  description?: string   // Optional group description shown to user
}

Question Model (additions)

interface Question {
  // ... existing fields (text, type, required, order, scoring_config, options)

  // NEW: Which group this question belongs to (null = ungrouped)
  group_id?: string

  // NEW: Category weight for group-level aggregation
  weight: number  // default: 1.0

  // NEW: Rating label descriptions (for rich slider UI)
  rating_labels?: Record<number, string>
}

Survey Result Model (additions)

interface SurveyResult {
  // ... existing fields (score, score_breakdown)

  // NEW: Group-level aggregated results
  group_results?: GroupResult[]

  // NEW: Overall percentage from group aggregation
  overall_percentage?: number
}

interface GroupResult {
  group_id: string
  group_name: string
  color: string
  tint?: string
  total_score: number
  max_possible_score: number
  percentage: number
  question_scores: QuestionScore[]
}

interface QuestionScore {
  question_id: string
  category: string        // Human-readable label
  score: number
  max_possible_score: number
  percentage: number
}

Database Schema Changes

surveys collection (additions)

{
  // ... existing fields
  result_type: "pyramid",                    // NEW
  template_id: "maslow-needs-v1",            // NEW (optional)
  question_groups: [                         // NEW
    {
      id: "grp_vital",
      name: "Vital",
      order: 0,
      color: "red",
      tint: "700",
      description: "Basic survival and biological needs"
    },
    {
      id: "grp_physiological",
      name: "Physiological",
      order: 1,
      color: "pink",
      tint: "700"
    }
    // ... more groups
  ]
}

questions collection (additions)

{
  // ... existing fields
  group_id: "grp_vital",                     // NEW (references question_groups[].id)
  weight: 800,                               // NEW (default: 1.0)
  rating_labels: {                           // NEW (optional)
    "0": "No reliable water source...",
    "1": "Struggling daily...",
    "5": "Abundant access..."
  }
}

survey_responses collection (additions)

{
  // ... existing fields (id, survey_id, responses, score, score_breakdown)
  group_results: [                           // NEW
    {
      group_id: "grp_vital",
      group_name: "Vital",
      color: "red",
      total_score: 2100,
      max_possible_score: 2900,
      percentage: 72.4,
      question_scores: [
        {
          question_id: "q_water",
          category: "Water",
          score: 800,
          max_possible_score: 800,
          percentage: 100.0
        }
        // ... more questions
      ]
    }
    // ... more groups
  ],
  overall_percentage: 68.5
}

Scoring Engine Changes

Current Flow

submit response → score each question → sum total → store flat score

New Flow

submit response → score each question → group by group_id → aggregate per group → compute overall → store grouped results

Group Aggregation Logic

def aggregate_group_scores(
    questions: List[dict],
    question_scores: Dict[str, float],
    groups: List[dict],
) -> List[GroupResult]:
    """Aggregate per-question scores into group-level results."""
    results = []

    for group in sorted(groups, key=lambda g: g["order"]):
        group_questions = [q for q in questions if q.get("group_id") == group["id"]]

        total_score = 0
        max_possible = 0
        question_results = []

        for q in group_questions:
            weight = q.get("weight", 1.0)
            raw_score = question_scores.get(q["id"], 0)
            max_raw = q.get("scoring_config", {}).get("max_value", 5)

            weighted_score = (raw_score / max_raw) * weight if max_raw > 0 else 0
            total_score += weighted_score
            max_possible += weight

            question_results.append({
                "question_id": q["id"],
                "category": q["text"],  # or a dedicated label field
                "score": weighted_score,
                "max_possible_score": weight,
                "percentage": (weighted_score / weight * 100) if weight > 0 else 0,
            })

        results.append({
            "group_id": group["id"],
            "group_name": group["name"],
            "color": group["color"],
            "tint": group.get("tint"),
            "total_score": total_score,
            "max_possible_score": max_possible,
            "percentage": (total_score / max_possible * 100) if max_possible > 0 else 0,
            "question_scores": question_results,
        })

    return results

This is the same math the Maslow calculator does today, just generalized to work with any set of groups. ✅

Survey Templates

A template is a pre-built survey configuration that can be instantiated for an organization. Templates are stored in a survey_templates collection. ✅

Template Schema

{
  id: "maslow-needs-v1",
  name: "Maslow Needs Assessment",
  description: "Measures well-being across Maslow's hierarchy of needs",
  version: "1.0",
  result_type: "pyramid",
  question_groups: [
    { id: "vital", name: "Vital", order: 0, color: "red", tint: "700" },
    { id: "physiological", name: "Physiological", order: 1, color: "pink", tint: "700" },
    { id: "safety_development", name: "Safety & Development", order: 2, color: "orange", tint: "600" },
    { id: "love_belonging", name: "Love & Belonging", order: 3, color: "yellow", tint: "500" },
    { id: "esteem", name: "Esteem", order: 4, color: "green", tint: "700" },
    { id: "self_actualization", name: "Self-Actualization", order: 5, color: "blue", tint: "600" }
  ],
  questions: [
    {
      group_id: "vital",
      text: "How would you rate your access to water?",
      type: "RATING",
      weight: 800,
      scoring_config: { type: "direct_scale", min_value: 0, max_value: 5 },
      rating_labels: {
        "0": "No reliable water source, leading to extreme vulnerability",
        "1": "Struggling daily for water, often resorting to unsafe sources",
        "2": "Inconsistent water access, facing shortages and quality concerns",
        "3": "Somewhat reliable water source, but quality is not always proper",
        "4": "Steady water source meeting daily needs, satisfactory quality",
        "5": "Abundant access to high-quality water, exceeding daily needs"
      }
    }
    // ... 23 more questions
  ],
  created_at: ISODate("..."),
  is_system: true  // System templates cannot be edited by users
}

Template Instantiation

POST /api/v1/surveys/from-template
{
  "template_id": "maslow-needs-v1",
  "organization_id": "org_123"
}

This creates a real survey with all the groups, questions, and scoring configs copied from the template. The organization can then optionally customize it (add questions, change weights) before publishing.

Built-in Templates

Template Groups Questions Result Type Use Case
Maslow Needs Assessment 6 levels 24 pyramid Individual wellbeing measurement
Team Health Check 5 dimensions 20 radar Team culture and dynamics
Program Impact Baseline 4 areas 16 breakdown Pre/post intervention measurement

Frontend Visualization

Result Type Rendering

The results page uses the result_type field to select the visualization component:

function ResultVisualization({ survey, results }: Props) {
  switch (survey.result_type) {
    case "pyramid":
      return <PyramidVisualization groups={results.group_results} />  // ✅
    case "radar":
      return <RadarVisualization groups={results.group_results} />  // ✅
    case "breakdown":
      return <GroupedBreakdown groups={results.group_results} />  // ✅
    case "flat":
    default:
      return <FlatScoreDisplay score={results.score} breakdown={results.score_breakdown} />  // ✅
  }
}

Pyramid Visualization (existing, adapted)

The current MaslowPyramid component is already built for this. The only change: instead of reading from MaslowSurveyResult.levels, it reads from GroupResult[]. The shape is identical.

Radar Visualization (new)

For assessments like "Team Health Check" where groups represent dimensions on a radar/spider chart. Each axis = one group, distance from center = group percentage.

Grouped Breakdown (new)

For assessments where a hierarchical view is more useful than a specific shape. Shows each group as a collapsible section with per-question progress bars inside. This is essentially what the Maslow "Detailed Breakdown" section already does.

Survey Taking: Group Per Page

When a survey has question_groups, the survey-taking UI switches from one-question-per-page to one-group-per-page. ✅ All questions belonging to the same group are displayed together on a single screen, with the group header (name, color, description) at the top.

This mirrors the original Maslow questionnaire UX where each pyramid level was one page with all its category questions visible at once. The respondent answers all questions in a group, then clicks "Next" to move to the next group.

Behavior

Survey Type Navigation Page Content
Flat (no groups) One question per page, "Question X of Y" Single question card
Grouped One group per page, "Group X of Y (group name)" Group header + all questions in that group

Group Page Layout

┌──────────────────────────────────────┐
│ [■ color] Group Name                 │
│ Optional group description           │
│                                      │
│ ┌──────────────────────────────────┐ │
│ │ Question 1 text         [Rating] │ │
│ │ Rating label description         │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ Question 2 text         [Rating] │ │
│ │ Rating label description         │ │
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ Question 3 text         [Rating] │ │
│ └──────────────────────────────────┘ │
│                                      │
│ [Previous]              [Next Group] │
└──────────────────────────────────────┘

Progress

The progress bar reflects group-level progress: (currentGroupIndex + 1) / totalGroups. The progress bar color matches the current group's color. The counter shows "Group X of Y" with the group name.

Validation

All required questions in the current group must be answered before the respondent can advance to the next group. The "Next" button is disabled until all required questions have responses.

Ungrouped Questions

If a survey has question_groups but some questions have no group_id, those ungrouped questions appear as individual one-per-page screens after all grouped pages, maintaining backward compatibility with the flat question flow.

API Changes

New Endpoints ✅

POST   /api/v1/surveys/from-template        # Create survey from template
GET    /api/v1/templates                     # List available templates
GET    /api/v1/templates/{id}                # Get template details

Modified Endpoints

POST   /api/v1/surveys                       # Now accepts question_groups and result_type
POST   /api/v1/surveys/{id}/respond           # Response scoring now includes group aggregation
GET    /api/v1/surveys/{id}/responses/{rid}   # Response now includes group_results

Deprecated Endpoints (after migration) ✅

GET    /api/v1/maslow/levels                  # Replaced by template content
POST   /api/v1/maslow/submit                  # Replaced by survey respond
GET    /api/v1/maslow/my-results              # Replaced by survey response history
GET    /api/v1/maslow/result/{id}             # Replaced by survey response detail

Migration Strategy

Phase 1: Extend the Survey Model

Add question_groups, result_type, and weight/rating_labels/group_id to the survey and question models. Existing surveys continue to work with result_type: "flat" (default) and no groups.

No breaking changes. All existing surveys and Maslow assessments continue to function.

Phase 2: Build Group Aggregation

Extend the scoring engine with aggregate_group_scores(). When a survey has question_groups, the response includes both the flat score and the grouped results.

Phase 3: Template System

Create the survey_templates collection. Convert MASLOW_PYRAMID_LEVELS into the first template (maslow-needs-v1). Add the /from-template endpoint.

Phase 4: Frontend Unification

Update the results page to switch on result_type. The existing MaslowPyramid, insights generator, and analytics components are refactored to read from the generic GroupResult[] shape.

Phase 5: Maslow Migration

  • Create a migration script that converts existing maslow_responses documents into survey_responses linked to a Maslow survey instance
  • Update the dashboard to read Maslow data from the unified collection
  • Deprecate the /api/v1/maslow/* endpoints (keep them as thin wrappers during transition)
  • Remove the hardcoded MASLOW_PYRAMID_LEVELS constant, maslow_calculator.py, and maslow/routes.py

Phase 6: New Assessment Types

With the infrastructure in place, build additional templates (Team Health, Program Impact) and add the radar chart visualization.

Mapping: Current Maslow → Unified Model

Current Maslow Concept Unified Equivalent
MASLOW_PYRAMID_LEVELS constant Survey template maslow-needs-v1
Maslow "level" (e.g., Vital, Esteem) QuestionGroup with order and color
Maslow "category" (e.g., Water, Food) Question with group_id and weight
maslow_calculator.py Generic scoring engine + aggregate_group_scores()
MaslowSurveyResult.levels SurveyResult.group_results
MaslowPyramid component PyramidVisualization reading from GroupResult[]
/api/v1/maslow/submit POST /api/v1/surveys/{id}/respond
maslow_responses collection survey_responses collection
Hardcoded rating descriptions Question.rating_labels field
MaslowAnswers (category_name → 0-5) responses (question_id → value)

Survey Builder UI

The survey creation/editing interface uses a two-panel layout that replaces the old single-column scrolling form. Wireframes are in catalyzeup-flows at /impact-pulse/surveys/builder. ✅

Layout

┌─────────────────────────────────────────────────────────────┐
│ Survey Builder [DRAFT]          [Test Survey] [Save] [Publish] │
│ Design your survey with questions, groups, and scoring.       │
├─────────────────────────────────────────────────────────────┤
│ Title: [___________]  Org: [___▼]  Type: [___▼]              │
│ Description: [_____]  Result Viz: [___▼]  [▸ More Settings]  │
├─────────────────────┬───────────────────────────────────────┤
│ QUESTION OUTLINE    │ EDITOR PANEL                          │
│                     │                                       │
│ UNGROUPED           │ (Selected question or group editor)   │
│  ▲▼ Question 1 [R]  │                                       │
│                     │ Edit Question                         │
│ ■ Physical Needs (3)│ ┌──────────────────────────────────┐  │
│  ▲▼ Question 2 [R]  │ │ Question Text: [____________]    │  │
│  ▲▼ Question 3 [R]  │ │ Type: [Rating▼] Group: [___▼]   │  │
│  ▲▼ Question 4 [R]  │ │ ☑ Required                       │  │
│  [+ Question]       │ │                                  │  │
│                     │ │ Rating Configuration             │  │
│ ■ Safety (2)        │ │ [0][1][2][3][4][5]               │  │
│  ▲▼ Question 5 [R]  │ │ Weight: [1]                      │  │
│  ▲▼ Question 6 [R]  │ └──────────────────────────────────┘  │
│  [+ Question]       │                                       │
│                     │                                       │
│ [+ Question]        │                                       │
│ [+ Group]           │                                       │
├─────────────────────┴───────────────────────────────────────┤
│ 3 groups   7 questions   7 scored       [Test] [Save] [Pub] │
└─────────────────────────────────────────────────────────────┘

Left Panel: Question Outline

The left panel shows the complete question structure as a navigable tree:

  • Ungrouped questions appear at the top under an "UNGROUPED" label
  • Groups appear in order with a color-coded left border, showing group name and question count badge
  • Questions within groups are indented under their group with a thinner color border
  • ▲/▼ arrows on each question and group allow reordering within its context
  • Groups are collapsible (▶/▼ toggle) to manage long surveys
  • Each question shows its text (truncated) and a type badge (Rating, Yes/No, Choice, Text)
  • "+ Question" button appears inside each group and at the bottom for ungrouped
  • "+ Group" button at the bottom creates a new group

Clicking a question or group in the outline selects it and shows its editor in the right panel. The selected item is highlighted.

Right Panel: Question Editor

When a question is selected:

  • Question Text (textarea)
  • Type dropdown (Rating 0-5, Yes/No, Multiple Choice, Text Response)
  • Group dropdown (assign/move question to a group, or "(Ungrouped)")
  • Required checkbox
  • Type-specific configuration:
    • Rating: Shows 0-5 scale preview with labels (N/A, Poor, Fair, Good, V.Good, Excellent), weight multiplier input
    • Yes/No: Enable scoring checkbox, points for Yes/No inputs
    • Multiple Choice: Option list with add/remove, enable scoring checkbox, per-option point values
    • Text: No scoring available note

When a group is selected:

  • Group Name input
  • Description textarea
  • Color picker (8 color swatches: red, orange, yellow, green, blue, purple, pink, gray)
  • Info about questions in the group and how groups affect survey taking

Top Section: Survey Details (Compact)

Survey metadata is shown in a compact 2-column grid (not a long scrolling form):

  • Row 1: Title | Organization + Survey Type
  • Row 2: Description | Result Visualization
  • Expandable "More Settings" for: require email, allow multiple attempts, max attempts

Actions

  • Test Survey button navigates to admin test mode
  • Save Draft saves without publishing
  • Publish makes the survey live
  • Actions appear both in the header and footer for accessibility

Admin Test Mode

Admins can take their own survey in test mode to verify the respondent experience without saving any data. Wireframes at /impact-pulse/surveys/1/admin-test. ✅

Behavior

  • A prominent yellow warning banner shows: "TEST MODE: You are taking this survey as an admin. No responses will be saved. This is exactly how respondents will see the survey."
  • The survey-taking UI is identical to the respondent experience (group-per-page, progress bars, rating buttons)
  • Admin skips the email entry step (their identity is already known)
  • On completion, results are shown (pyramid/breakdown visualization, per-group scores, per-question breakdown)
  • A second warning on the results page confirms: "These are test results only. Nothing has been saved to the database."
  • Reset button clears all responses to test again
  • Back to Builder button returns to the survey builder

API

GET /api/v1/surveys/{id}/preview    # Returns full survey data for test mode (OWNER/ADMIN only)

No POST endpoint is called on "submit" in test mode. All scoring is calculated client-side for display purposes only.

Email Requirement for Survey Taking

When taking a survey, email is mandatory by default. The intro screen collects the respondent's email before they can begin. ✅

Intro Screen Flow

┌──────────────────────────────────────────┐
│ Maslow Needs Assessment                  │
│ Rate each statement from 0 to 5...       │
│                                          │
│ ┌──────────────────────────────────────┐ │
│ │ Before you begin                     │ │
│ │                                      │ │
│ │ Email Address (required)             │ │
│ │ [your@email.com________________]     │ │
│ │                                      │ │
│ │ 16 questions  |  5 sections          │ │
│ │ ~5 minutes    |  estimated time      │ │
│ └──────────────────────────────────────┘ │
│                                          │
│ Survey Sections                          │
│ ■ 1. Physical Needs (4 questions)        │
│ ■ 2. Safety & Security (3 questions)     │
│ ■ 3. Love & Belonging (3 questions)      │
│ ■ 4. Esteem (3 questions)                │
│ ■ 5. Self-Actualization (3 questions)    │
│                                          │
│ [Begin Survey]                           │
└──────────────────────────────────────────┘

Validation

  • Email format is validated client-side before allowing the survey to start
  • Empty email shows: "Email is required to take this survey."
  • Invalid format shows: "Please enter a valid email address."
  • The require_email setting on the survey controls whether email is mandatory (defaults to true)

What This Enables

  1. Organizations create their own assessments with grouped scoring and rich visualizations, using the survey builder
  2. Multiple visualization types from the same data (a team health survey could be viewed as radar or grouped bars)
  3. Template marketplace where system and community templates can be shared
  4. Unified analytics across all assessment types, no special-case code for Maslow
  5. Consistent API surface for all survey types, simplifying frontend and mobile development
  6. Custom Maslow variants where organizations can modify weights, add categories, or remove levels to fit their context
  7. Admin test mode so survey creators can verify the respondent experience before publishing
  8. Survey builder with group management so groups, question reordering, and scoring are all configurable in the UI