# Runnable Check

This is the shortest acceptance check for the Events Alpha skill pack. It mints Events demo credentials, writes one `content_viewed` event for the seeded Events demo student `student-ada-001`, imports one source-shaped TimeBack row through `/source-imports`, reads the same events through list and detail endpoints, reads the activity source, probes the read-only attendance and discipline moment lists, and compares only returned platform facts.

The expected answer comes from the platform at runtime. The default demo student id is a seeded Events fixture id; it is not parsed from a name, URL, token body, or People & Orgs demo row. The check does not carry an event-kind map, roster name map, descriptor table, source-import adapter logic, soft-delete dedupe rule, score calculation, minutes calculation, database query, or static answer table.

Prerequisites: `curl`, Node 18 or newer, and a shell with `mktemp`. Every `curl` call has a 20-second timeout plus bounded retry for transient 5xx transport failures, so a live API outage fails loudly instead of hanging.

```bash
export EVENTS_BASE_URL="${EVENTS_BASE_URL:-https://platform3-andymontgomery-9773s-projects.vercel.app/events/alpha/implementation/api}"

EVENTS_MINT=$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS -X POST "$EVENTS_BASE_URL/dev/mint?tenantId=demo")

export 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))')"
export 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))')"
export STUDENT_ID="${STUDENT_ID:-student-ada-001}"
export CONTENT_ID="${CONTENT_ID:-content-fractions-video-01}"
export ACTIVITY_SOURCE_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).defaultActivitySourceId))')"
export RUN_ID="$(node -e 'process.stdout.write(require("node:crypto").randomUUID())')"
export HAPPENED_AT="$(node -e 'process.stdout.write(new Date().toISOString())')"
export SINCE="$(node -e 'process.stdout.write(new Date(Date.parse(process.env.HAPPENED_AT)-60000).toISOString())')"
export UNTIL="$(node -e 'process.stdout.write(new Date(Date.parse(process.env.HAPPENED_AT)+120000).toISOString())')"

BODY="$(mktemp)"
cat > "$BODY" <<JSON
{
  "events": [
    {
      "sourceEventId": "urn:uuid:$RUN_ID",
      "kind": "content_viewed",
      "studentId": "$STUDENT_ID",
      "contentId": "$CONTENT_ID",
      "activitySourceId": "$ACTIVITY_SOURCE_ID",
      "happenedAt": "$HAPPENED_AT"
    }
  ]
}
JSON

CREATE_JSON=$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS -X POST "$EVENTS_BASE_URL/events" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID" \
  -H "Idempotency-Key: events-skill-check-$RUN_ID" \
  -H "Content-Type: application/json" \
  --data-binary "@$BODY")

export EVENT_ID="$(printf '%s' "$CREATE_JSON" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.parse(s).data[0].id))')"

SOURCE_IMPORT_JSON=$(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: events-skill-source-$RUN_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"sourceSystem\": \"timeback_production\",
    \"adapter\": \"timeback_learning_event_v1\",
    \"importId\": \"events-skill-source-$RUN_ID\",
    \"records\": [
      {
        \"id\": \"pf_skill_pack_$RUN_ID\",
        \"user_id\": \"$STUDENT_ID\",
        \"date\": \"$(node -e 'process.stdout.write(new Date().toISOString().slice(0,10))')\",
        \"subject\": \"Math\",
        \"app\": \"AlphaMath\",
        \"course_id\": \"math-5-fractions\",
        \"activity_id\": \"$CONTENT_ID\",
        \"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\"
      }
    ]
  }")

export SOURCE_IMPORT_EVENT_ID="$(printf '%s' "$SOURCE_IMPORT_JSON" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.parse(s).data[0].id))')"

EVENTS_JSON="$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS "$EVENTS_BASE_URL/events?studentId=$STUDENT_ID&modifiedSince=$(node -e 'process.stdout.write(encodeURIComponent(process.env.SINCE))')&pageSize=50" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID")"
DETAIL_JSON="$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS "$EVENTS_BASE_URL/events/$EVENT_ID" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID")"
SOURCE_JSON="$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS "$EVENTS_BASE_URL/activity-sources/$ACTIVITY_SOURCE_ID" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID")"

FROM_DATE="$(node -e 'const d=new Date(); d.setUTCDate(d.getUTCDate()-30); process.stdout.write(d.toISOString().slice(0,10))')"
TO_DATE="$(node -e 'const d=new Date(); d.setUTCDate(d.getUTCDate()+1); process.stdout.write(d.toISOString().slice(0,10))')"

ATTENDANCE_JSON="$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS "$EVENTS_BASE_URL/attendance-events?studentId=$STUDENT_ID&from=$FROM_DATE&to=$TO_DATE&pageSize=50" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID")"
DISCIPLINE_JSON="$(curl --max-time 20 --retry 3 --retry-all-errors --retry-delay 2 -fsS "$EVENTS_BASE_URL/discipline-events?studentId=$STUDENT_ID&from=$FROM_DATE&to=$TO_DATE&pageSize=50" \
  -H "Authorization: Bearer $TIMEBACK_TOKEN" \
  -H "X-Timeback-Tenant: $TENANT_ID")"

CREATE_JSON="$CREATE_JSON" SOURCE_IMPORT_JSON="$SOURCE_IMPORT_JSON" EVENTS_JSON="$EVENTS_JSON" DETAIL_JSON="$DETAIL_JSON" SOURCE_JSON="$SOURCE_JSON" ATTENDANCE_JSON="$ATTENDANCE_JSON" DISCIPLINE_JSON="$DISCIPLINE_JSON" SOURCE_IMPORT_EVENT_ID="$SOURCE_IMPORT_EVENT_ID" node - <<'NODE'
const created = JSON.parse(process.env.CREATE_JSON).data[0];
const sourceImport = JSON.parse(process.env.SOURCE_IMPORT_JSON);
const imported = sourceImport.data?.[0];
const events = JSON.parse(process.env.EVENTS_JSON).data ?? [];
const detail = JSON.parse(process.env.DETAIL_JSON).data;
const source = JSON.parse(process.env.SOURCE_JSON).data;
const attendance = JSON.parse(process.env.ATTENDANCE_JSON).data ?? [];
const discipline = JSON.parse(process.env.DISCIPLINE_JSON).data ?? [];

const checks = {
  createdEventReturned: Boolean(created.id),
  sourceImportMaterialized: sourceImport.acceptedCount === 1 && sourceImport.materializedCount === 1 && sourceImport.import?.status === "materialized",
  sourceImportReturnedEvent: Boolean(imported?.id) && imported.id === process.env.SOURCE_IMPORT_EVENT_ID,
  listContainsCreatedEvent: events.some((event) => event.id === created.id),
  listContainsImportedEvent: events.some((event) => event.id === imported.id),
  detailMatchesCreatedEvent: detail.id === created.id,
  sourceMatchesEvent: source.id === created.activitySourceId,
  createdStudentMatchesDemoBoundary: created.studentId === process.env.STUDENT_ID,
  createdContentMatchesDemoBoundary: created.contentId === process.env.CONTENT_ID,
  attendanceReadReturnedList: Array.isArray(attendance),
  disciplineReadReturnedList: Array.isArray(discipline),
  noAttendanceRawPayload: attendance.every((row) => !("payloadJson" in row) && !("rawPayload" in row)),
  noDisciplineNarrativeOrRawPayload: discipline.every((row) => !("payloadJson" in row) && !("rawPayload" in row) && !("description" in row))
};

for (const [name, passed] of Object.entries(checks)) {
  if (!passed) throw new Error(`Events Alpha runnable check failed: ${name}`);
}

console.log(JSON.stringify({
  result: "pass",
  studentId: created.studentId,
  contentId: created.contentId,
  createdEventId: created.id,
  createdKind: created.kind,
  importedEventId: imported.id,
  sourceImportKind: imported.kind,
  sourceImportMaterialized: true,
  listedCreatedEvent: true,
  listedImportedEvent: true,
  detailMatches: true,
  sourceName: source.name,
  attendanceRows: attendance.length,
  disciplineRows: discipline.length,
  checkedBySurfaceCallsOnly: true
}, null, 2));
NODE

rm -f "$BODY"
```

Passing output proves the pack can reproduce the platform's own answer for a teacher activity report, call the source-shaped migration path without local adapter logic, and call the current read-only administrative moment lists without local descriptor or soft-delete logic. It does not prove Results, Content, NWEA, Ed-Fi writes, or People & Orgs behavior.
