# Worked Example

This is the five-minute teacher report workflow for the public demo: mint Events demo credentials, write one `content_viewed` event for the seeded Events demo student, read the activity stream, and print a teacher-facing report. It calls the live Events Alpha surface only.

The public Events demo token is tenant-scoped. This example uses seeded demo ids `student-ada-001` and `content-fractions-video-01` so readback stays deterministic. In real or reviewer tenants where Events `studentId` values are People & Orgs `person_id` values, resolve display names through People & Orgs after the Events read.

Prerequisites: Node 18 or newer. Set `EVENTS_BASE_URL` only if an operator gives you a non-default URL.

## Copy-Paste Workflow

```bash
node --input-type=module <<'NODE'
import { randomUUID } from "node:crypto";

const eventsBase = process.env.EVENTS_BASE_URL || "https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/implementation/api";
const studentId = process.env.STUDENT_ID || "student-ada-001";
const contentId = process.env.CONTENT_ID || "content-fractions-video-01";
const runId = randomUUID();
const happenedAt = new Date().toISOString();
const since = new Date(Date.parse(happenedAt) - 60_000);
const until = new Date(Date.parse(happenedAt) + 60_000);

async function call(label, method, url, { token, tenantId = "demo", body, idempotencyKey } = {}) {
  const headers = {};
  if (token) headers.Authorization = `Bearer ${token}`;
  if (token) headers["X-Timeback-Tenant"] = tenantId;
  if (body) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
  const requestBody = body ? JSON.stringify(body) : undefined;
  let lastError;
  for (let attempt = 1; attempt <= 4; attempt += 1) {
    const response = await fetch(url, {
      method,
      headers,
      body: requestBody,
      signal: AbortSignal.timeout(20000)
    });
    const text = await response.text();
    const json = text ? JSON.parse(text) : null;
    if (response.ok) return { label, status: response.status, json };
    lastError = new Error(`${label} returned HTTP ${response.status}: ${text}`);
    if (response.status < 500 || attempt === 4) break;
    await new Promise((resolve) => setTimeout(resolve, 1500 * attempt));
  }
  throw lastError;
}

const steps = [];
const mint = await call("POST Events /dev/mint", "POST", `${eventsBase}/dev/mint?tenantId=demo`);
steps.push({ call: mint.label, status: mint.status });

const token = mint.json.token;
const tenantId = mint.json.tenantId || "demo";
const activitySourceId = mint.json.defaultActivitySourceId;

const create = await call("POST Events /events", "POST", `${eventsBase}/events`, {
  token,
  tenantId,
  idempotencyKey: `teacher-activity-${runId}`,
  body: {
    events: [
      {
        sourceEventId: `urn:uuid:${runId}`,
        kind: "content_viewed",
        studentId,
        contentId,
        activitySourceId,
        happenedAt
      }
    ]
  }
});
steps.push({ call: create.label, status: create.status });
const created = create.json.data[0];

const eventList = await call(
  "GET Events /events",
  "GET",
  `${eventsBase}/events?studentId=${encodeURIComponent(studentId)}&happenedAtFrom=${encodeURIComponent(since.toISOString())}&happenedAtTo=${encodeURIComponent(until.toISOString())}&pageSize=50`,
  { token, tenantId }
);
steps.push({ call: eventList.label, status: eventList.status });

const detail = await call("GET Events /events/{eventId}", "GET", `${eventsBase}/events/${created.id}`, { token, tenantId });
steps.push({ call: detail.label, status: detail.status });

const events = eventList.json.data || [];
const sawCreatedEvent = events.some((event) => event.id === created.id);
if (!sawCreatedEvent) throw new Error("Created event was not returned by GET /events.");
if (detail.json.data.id !== created.id) throw new Error("Event detail returned a different id.");

console.log(JSON.stringify({
  result: "pass",
  steps,
  report: {
    studentId,
    studentLabel: studentId,
    since: since.toISOString(),
    createdEventId: created.id,
    eventCount: events.length,
    sawCreatedEvent,
    activities: events.map((event) => ({
      id: event.id,
      kind: event.kind,
      happenedAt: event.happenedAt,
      contentId: event.contentId,
      activitySourceId: event.activitySourceId
    }))
  },
  surfaceCallsOnly: true
}, null, 2));
NODE
```

## Expected Output

IDs, counts, and timestamps are runtime values. These fields should match exactly:

```json
{
  "result": "pass",
  "steps": [
    {"call": "POST Events /dev/mint", "status": 200},
    {"call": "POST Events /events", "status": 202},
    {"call": "GET Events /events", "status": 200},
    {"call": "GET Events /events/{eventId}", "status": 200}
  ],
  "report": {
    "studentId": "student-ada-001",
    "studentLabel": "student-ada-001",
    "sawCreatedEvent": true
  },
  "surfaceCallsOnly": true
}
```

The workflow formats returned Events fields. It does not carry an event-kind map, Caliper tuple map, source-import adapter map, roster name map, descriptor table, dedupe rule, score/mastery logic, minutes calendar, raw-payload reader, database query, or implementation-source dependency.

## Source-Shaped Import Check

Use the source-import path only for migration/reconciliation jobs that already have raw producer rows. Send source-shaped rows to `/source-imports`; do not pre-normalize them into Alpha event objects.

```bash
export EVENTS_BASE_URL="${EVENTS_BASE_URL:-https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/implementation/api}"
EVENTS_MINT="${EVENTS_MINT:-$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS -X POST "$EVENTS_BASE_URL/dev/mint?tenantId=demo")}"
TIMEBACK_TOKEN="${TIMEBACK_TOKEN:-$(printf '%s' "$EVENTS_MINT" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.parse(s).token))')}"
TENANT_ID="${TENANT_ID:-$(printf '%s' "$EVENTS_MINT" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.parse(s).tenantId))')}"
RUN_ID="${RUN_ID:-$(node -e 'process.stdout.write(require("node:crypto").randomUUID())')}"
SOURCE_DATE="$(node -e 'process.stdout.write(new Date().toISOString().slice(0,10))')"

curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS -X POST "$EVENTS_BASE_URL/source-imports" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID" \
  -H "Idempotency-Key: source-import-$RUN_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"sourceSystem\": \"timeback_production\",
    \"adapter\": \"timeback_learning_event_v1\",
    \"records\": [{
      \"id\": \"pf_skill_pack_$RUN_ID\",
      \"user_id\": \"student-ada-001\",
      \"date\": \"$SOURCE_DATE\",
      \"subject\": \"Math\",
      \"app\": \"AlphaMath\",
      \"course_id\": \"math-5-fractions\",
      \"activity_id\": \"content-fractions-video-01\",
      \"total_questions\": 8,
      \"correct_questions\": 7,
      \"xp_earned\": \"10\",
      \"mastered_units\": null,
      \"active_seconds\": \"420\",
      \"source_system\": \"timeback\",
      \"score_type\": \"accuracy\",
      \"score_given\": \"7\",
      \"max_score\": \"8\"
    }]
  }"
```

Success is HTTP `200` with `acceptedCount == materializedCount`, `import.status == "materialized"`, and rows readable through the public Events list/detail endpoint. `events:validation_failed` is HTTP `400`; `events:adapter_rejected` is HTTP `422`.
