# Worked Example

Build the six Learning Report panels for Ada from the demo tenant. This example calls the live Analytics API and prints report JSON. It does not compute, derive, normalize, or reconcile any metric.

This example uses Ada's Learning Report rows. The source-import proof recipe uses `student-analytics-integration-001` on a separate date and may accumulate repeated demo proof imports; that proof row is not Ada's XP panel.

Requirements: Node 18+ for built-in fetch. No jq is required.

Run from any shell:

    export ANALYTICS_BASE_URL="https://platform3-andymontgomery-9773s-projects.vercel.app/analytics/alpha/implementation/api"
    node --input-type=module - <<'NODE'
    const baseUrl = process.env.ANALYTICS_BASE_URL;
    const mint = await fetch(baseUrl + "/dev/mint?tenantId=demo", { method: "POST" });
    if (!mint.ok) throw new Error("demo mint failed " + mint.status + ": " + await mint.text());
    const { token, tenantId = "demo" } = await mint.json();
    const headers = { authorization: "Bearer " + token, "x-timeback-tenant": tenantId };
    const endpoints = {
      timeCommitment: {
        path: "/alpha/analytics/v1/school-day-minutes?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&subjectId=math&startDate=2026-05-01&endDate=2026-06-01&limit=100",
        id: "school_day_minutes_demo_student_01ht7g3yzv7qb5n4ykq1k0z9a9_math_may"
      },
      xp: {
        path: "/alpha/analytics/v1/xp-rollups?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&subjectId=math&windowKind=term&startDate=2026-01-01&endDate=2026-06-01&limit=100",
        id: "xp_rollup_demo_student_01ht7g3yzv7qb5n4ykq1k0z9a9_term"
      },
      accuracy: {
        path: "/alpha/analytics/v1/accuracy-rollups?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&subjectId=math&windowKind=term&startDate=2026-01-01&endDate=2026-06-01&limit=100",
        id: "accuracy_rollup_canonical_89ce4e22de47b487fc4150d7"
      },
      mastery: {
        path: "/alpha/analytics/v1/mastery-deltas?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&kcId=kc_math_fraction_equivalence_04&stateDimension=durable_mastery&startDate=2026-05-01&endDate=2026-06-01&limit=100",
        id: "mastery_delta_demo_student_01ht7g3yzv7qb5n4ykq1k0z9a9"
      },
      map: {
        path: "/alpha/analytics/v1/map-growth-rollups?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&subjectId=math&termId=term_2026_winter&growthWindow=winter_to_winter&normsSet=2025",
        id: "map_growth_rollup_demo_student_01ht7g3yzv7qb5n4ykq1k0z9a9"
      },
      completion: {
        path: "/alpha/analytics/v1/completion-rollups?studentId=student_01HT7G3YZV7QB5N4YKQ1K0Z9A9&subjectId=math&completionScope=course&scopeId=course_math_grade_4_powerpath&startDate=2026-01-01&endDate=2026-06-01",
        id: "completion_rollup_demo_student_01ht7g3yzv7qb5n4ykq1k0z9a9"
      }
    };
    async function get(name, config) {
      const path = config.path;
      const response = await fetch(baseUrl + path, { headers });
      const text = await response.text();
      if (!response.ok) throw new Error(path + " failed: " + response.status + " " + text);
      const payload = JSON.parse(text);
      if (!Array.isArray(payload.data) || payload.data.length === 0) throw new Error(path + " returned no rows");
      const row = payload.data.find((item) => item.id === config.id);
      if (!row) throw new Error(name + " did not include pinned demo row " + config.id);
      return row;
    }
    const report = {};
    for (const [name, config] of Object.entries(endpoints)) {
      report[name] = await get(name, config);
    }
    console.log(JSON.stringify({
      studentId: "student_01HT7G3YZV7QB5N4YKQ1K0Z9A9",
      subjectId: "math",
      source: "Analytics Alpha surface",
      computedLocally: false,
      panels: {
        timeCommitment: {
          activeMinutes: report.timeCommitment.active_minutes,
          enrolledSchoolDays: report.timeCommitment.enrolled_school_day_count,
          minutesPerEnrolledSchoolDay: report.timeCommitment.minutes_per_enrolled_school_day,
          policyRef: report.timeCommitment.school_day_policy_ref
        },
        xp: {
          positiveXp: report.xp.positive_xp,
          negativeXp: report.xp.negative_xp,
          netXp: report.xp.net_xp,
          dailyXpGoal: report.xp.daily_xp_goal,
          enrolledSchoolDays: report.xp.enrolled_school_day_count,
          xpGoal: report.xp.xp_goal,
          xpRemaining: report.xp.xp_remaining,
          xpGoalPercent: report.xp.xp_goal_percent,
          xpCompletionPercent: report.xp.xp_completion_percent,
          onTrack: report.xp.on_track,
          policyRef: report.xp.policy_ref
        },
        accuracy: {
          correctQuestionCount: report.accuracy.correct_question_count,
          incorrectQuestionCount: report.accuracy.incorrect_question_count,
          totalQuestionCount: report.accuracy.total_question_count,
          accuracyPercent: report.accuracy.accuracy_percent,
          policyRef: report.accuracy.policy_ref
        },
        mastery: {
          kcId: report.mastery.kc_id,
          transitionKind: report.mastery.transition_kind,
          previousValue: report.mastery.previous_value,
          newValue: report.mastery.new_value,
          deltaValue: report.mastery.delta_value
        },
        map: {
          ritScore: report.map.rit_score,
          growthX: report.map.growth_x,
          growthXTarget: report.map.growth_x_target,
          onTrack: report.map.on_track
        },
        completion: {
          gradeLevel: report.completion.grade_level,
          isMainCourse: report.completion.is_main_course,
          progressSourceKind: report.completion.progress_source_kind,
          completedCount: report.completion.completed_count,
          expectedCount: report.completion.expected_count,
          completionPercent: report.completion.completion_percent
        }
      }
    }, null, 2));
    NODE

Expected shape: the output contains six panels, source is Analytics Alpha surface, and computedLocally is false. The script selects the published demo answer rows by returned Analytics row id, then renders response fields. It does not add, divide, dedupe, parse terms, apply calendars, compute accuracy, compute Growth X, derive progress from MAP/RIT/R90, or count completion.

The MAP panel intentionally uses `growthWindow=winter_to_winter`; that is the current Ada demo row selected by the verifier config and checked against the live convergence answer key.

Proof step from the installed pack root:

    node checks/verify-skill-pack.mjs --write-evidence

The default verifier uses `checks/expected.json` for selectors, fetches the live integration convergence key for platform-owned answers, and compares event time facts, school-day minutes, XP, accuracy, attempts, response latency, hint usage, mastery deltas, MAP Growth X, and completion. Set `ANALYTICS_RUN_IMPORT=1` when you also want the optional source-shaped processed_facts import smoke. If a panel is missing or a call fails, preserve the Problem JSON and report the surface gap.
