# Source-Shaped TimeBack Ingest

Use this only with a write-approved token. The caller sends raw TimeBack-shaped records and lets the surface normalize server-side into stock CASE rows. Do not pre-normalize to CASE in the agent or migration app.

## Request

```sh
export CURRICULUM_BASE_URL="${CURRICULUM_BASE_URL:-https://platform3-andymontgomery-9773s-projects.vercel.app/curriculum/alpha/implementation/api}"
export CURRICULUM_TOKEN="${CURRICULUM_TOKEN:?write-approved token required}"
SUFFIX="$(date +%s)"

curl -fsSL -X POST "$CURRICULUM_BASE_URL/alpha/curriculum/v1/ingest/timeback-course-refs" \
  -H "Authorization: Bearer $CURRICULUM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: curriculum-source-ingest-$SUFFIX" \
  -d '{
    "source_system":"timeback",
    "records":[
      {
        "course_id":"tb-skill-pack-course-'$SUFFIX'",
        "course_name":"Skill Pack Source Course",
        "learning_group_id":"class_math_g3_skill_pack",
        "subject":"Mathematics",
        "grade_level":3,
        "grade_levels":[3],
        "course_grade_mode":"single_grade",
        "total_xp":1200
      },
      {
        "source_table":"course_components",
        "sourced_id":"tb-skill-pack-unit-'$SUFFIX'",
        "course_sourced_id":"tb-skill-pack-course-'$SUFFIX'",
        "sort_order":1,
        "title":"Skill Pack Source Container"
      },
      {
        "source_table":"component_resources",
        "sourced_id":"tb-skill-pack-resource-membership-'$SUFFIX'",
        "component_kind":"lesson",
        "course_id":"tb-skill-pack-course-'$SUFFIX'",
        "course_component_sourced_id":"tb-skill-pack-unit-'$SUFFIX'",
        "resource_sourced_id":"tb-skill-pack-resource-'$SUFFIX'",
        "position":1,
        "title":"Skill Pack Ordered Resource Membership",
        "scope_kc_refs":["dddddddd-dddd-4ddd-8ddd-ddddddddddd0"],
        "expected_xp":12,
        "content_id":"58612cab-9c46-426c-8c20-0e9f19c807c5",
        "content_version_id":"efcf3561-3a66-4825-9588-e792ef20c312",
        "content_kind":"question"
      },
      {
        "source_table":"courses",
        "sourced_id":"tb-skill-pack-real-course-'$SUFFIX'",
        "status":"active",
        "title":"Skill Pack Real-Shaped Vocabulary Course",
        "course_code":"VOCAB-4",
        "school_year_sourced_id":"school-year-2026",
        "subjects":["Vocabulary"],
        "grades":["4"],
        "total_xp":2800
      }
    ]
  }' | jq '{accepted_count, materialized, status, records}'
```

## Producer-Authoritative No-KC Lesson

Use this direct component shape only when the producer has explicitly decided that one exact content-bearing lesson has no authored KC attribution. The article version must already be Content release-eligible, and the renderer must accept `article`.

```sh
curl -fsSL -X POST "$CURRICULUM_BASE_URL/alpha/curriculum/v1/components" \
  -H "Authorization: Bearer $CURRICULUM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data-binary "$(jq -n --arg component_id "$COMPONENT_ID" --arg document_id "$DOCUMENT_ID" --arg course_id "$COURSE_ID" --arg content_id "$ARTICLE_CONTENT_ID" --arg version_id "$ARTICLE_CONTENT_VERSION_ID" '{component_id:$component_id,document_id:$document_id,kind:"lesson",name:"Producer-authored study skill",parent_component_id:$course_id,position:1,alpha:{publication_status:"published",target_ref:{module:"content",content_kind:"article",content_id:$content_id,content_version_id:$version_id},renderer_ref:"renderer:interactive-lesson",scope_kc_refs:[],kc_scope_decision:"authoritative_no_kc"}}')" \
  | jq '{component_id,parent_component_id,position,target_ref,renderer_ref,scope_kc_refs,kc_scope_decision}'

curl -fsSL "$CURRICULUM_BASE_URL/alpha/curriculum/v1/components/$COMPONENT_ID" -H "Authorization: Bearer $CURRICULUM_TOKEN" \
  | jq '{component_id,parent_component_id,position,target_ref,renderer_ref,scope_kc_refs,kc_scope_decision}'
curl -fsSL "$CURRICULUM_BASE_URL/alpha/curriculum/v1/courses/$COURSE_ID/tree" -H "Authorization: Bearer $CURRICULUM_TOKEN" \
  | jq --arg id "$COMPONENT_ID" '.components[] | select(.component_id==$id) | {component_id,parent_component_id,position,target_ref,renderer_ref,scope_kc_refs,kc_scope_decision}'
```

Replay the identical body with the identical `Idempotency-Key`: the API returns the original component and creates no additional component or containment edge. GET detail, tree, and `next-lesson` echo the exact immutable Content pair, empty scope, and decision. That lesson's `kc_version_snapshot` is empty, so it creates no KC or mastery evidence. Empty scope without the decision, a decision with non-empty scope, or a decision without the exact Content target and renderer fails closed. Never infer similarity, course-root, standards, or Content-tag bindings.

## Source Of Real-Course Lesson Structure

Curriculum owns course membership and order. Content owns flat artifacts and never implies which lesson belongs to a course or what comes next. TimeBack production's structural source is `courses -> course_components -> component_resources -> resources`: course components preserve parentage and sort order, component resources preserve ordered membership, and each raw resource passes through Content before Curriculum stores the returned typed target.

Every TimeBack membership must supply a non-empty array of authoritative active tenant-owned Knowledge Component UUIDs in `scope_kc_refs`; that bulk adapter remains unchanged until its source owns an explicit no-KC decision field. The DB reconcile reads the authored cluster from `component_resources.metadata.scope_kc_refs`. Platform3 preserves multi-KC lessons and does not choose clustering grain. A TimeBack learning-objective, standard, semantic KC code, source id, or CASE id is not a platform KC UUID: create the registry KC, capture its returned canonical `kc_id`, and use only that UUID. After structure is complete, publish the KC identities, approve the complete course snapshot, and require `next-lesson` to echo exact firm KC versions; ordinary assignment never firms them. `component_resources.lesson_type=quiz` remains a lesson unless an authoritative feed also supplies the passing and failure policy required for a mastery gate. The direct `authoritative_no_kc` component shape above is the sole sanctioned empty lesson scope.

The existing bulk route above is the TimeBack lesson-structure ingest door; there is no separate inferred Content-composition route. A top-level course-ref row alone is a valid catalog/enrollment anchor but cannot arm Jump In.

Every course and component is materialized as draft first. Assembly reads back the exact intended tree, publishes descendants bottom-up, and publishes the course root last. Same-run retry reuses deterministic component and edge ids. `next-lesson` chooses the first ordered lesson/review/practice/mastery-gate frontier before checking publication; the course root, that frontier, and every containment ancestor must be published. Draft, retired, or missing state fails closed and never exposes a later node.

Repository operators use `node loop/scripts/curriculum-timeback-structure-reconcile.mjs --course-refs <exact refs> --json` for a dry run and review its import-ready counts, typed findings, and full `reviewedDigest`. A missing Curriculum root enters that plan only when the active TimeBack course carries exactly `metadata.platform3Course={audience:"personalized",courseKind:"static",studentId:"<non-empty>"}`; no title or enrollment fallback is allowed. Apply requires the unchanged digest with `--apply --reviewed-digest sha256:...`; `--allow-source-missing` may still skip typed source-empty/KC-unscoped selections, and every applied course gets exact identity, full ordered-tree/KC/Content readback with a PII-free digest, plus next-lesson proof. Personalized next-lesson accepts only the exact source-owned learner id; missing and wrong ids receive the same not-found response before any Content eligibility call.

## Expected Behavior

- Success returns HTTP 202 with materialized `cf_documents`, `cf_items`, and `cf_associations` counts.
- Validation failures return HTTP 400 with `curriculum:validation_failed`.
- Adapter rejections return HTTP 422 with `curriculum:adapter_rejected`.
- A successful structured import materializes readable draft CASE rows, verifies exact tree readback, then publishes descendants and finally the course root before `/courses/{courseId}/next-lesson` can route.
- Every Content target carries `content_id`, `content_version_id`, and `content_kind`; Curriculum validates it and every non-empty `scope_kc_refs` value against the active owning runtime row before persistence, then revalidates the exact Content tuple at launch. The sole empty-scope case is the explicit content-bearing `authoritative_no_kc` lesson above. Missing, unavailable, malformed, mismatched, or ineligible responses fail closed, and `latest_version_id` is never substituted.
- A known course with no publication-safe lesson/review/practice/mastery-gate chain returns HTTP 409 with `curriculum:lesson_structure_missing`; an early root publish returns `curriculum:publication_incomplete`; a published graph without a matching immutable approval and complete firm KC-version snapshot returns `curriculum:course_not_routable`; an unknown course remains HTTP 404 with `curriculum:not_found`.

The public caller fields are raw source fields such as `course_id`, `course_name`, `subject`, `grade_level`, `grade_levels`, `course_grade_mode`, or real source-table rows such as `source_table=courses`, `sourced_id`, `title`, `subjects`, and `grades`. First-party reconciliation maps raw `course_components` to placeholder containers and fully scoped `component_resources` to lessons, preserving source order and passing only Content-owned target fields across the module boundary. Adapter-owned provenance fields are read-back only; do not send them in request bodies.

## Social Studies C3 Source Evidence

C3 Framework imports use canonical `subject_id=social_studies`. Preserve source-document bundle, snapshot, checksum, and row-evidence references supplied by the source project on every imported framework or standard. The PK-12 readiness contract has fourteen lanes (pre-K, kindergarten, and grades 1-12); readiness metadata never authorizes synthesizing standards rows. Economics, history, geography, and civics may identify course grain, but they do not become `subject_id` values.

## Parsed Standards Framework Directory

If a source project already has a `parsed/` directory of standards-framework JSON files, use the installed helper. It accepts only top-level framework objects shaped like `{fw_id, name, authority, source_url, subject, standards:[...]}` and skip-warns arrays, scratch files, and non-framework JSON instead of failing the whole ingest. It still calls the canonical Curriculum surface; it does not write CASE rows directly.

```sh
node checks/import-standards-framework-dir.mjs \
  --parsed-dir ./parsed \
  --base-url "$CURRICULUM_BASE_URL" \
  --token "$CURRICULUM_TOKEN" \
  --source-system incept
```

Inside the Platform3 repository, use the implementation helper at `loop/curriculum/artifacts/alpha/implementation/impl/scripts/import-standards-framework-dir.mjs`; installed skill-pack users use `checks/import-standards-framework-dir.mjs` from this pack. Do not `JSON.parse` every `parsed/*.json` and assume it is a framework object. A scratch array or notes file in the directory must be skipped with a warning, not allowed to break the run or become CASE data.

Expected XP is student-independent. The GOALS spine backfill is governed by `alpha.policy.curriculum.expected_xp_backfill.v2026-06-25`: prefer `reporting.rpt2_course.total_xp`, then per-grade course XP, then `reporting.rpt2_subject.typical_xp_per_grade_level`, with the declared literal-1500 final guard only for a missing subject floor. After that policy selection, missing expected XP is rejected; do not default it to zero or compute it from per-student progress actuals.

If the migration app has to compute component ids, parse subject from a title, map source kinds to CASE itself, or repair parentage locally, report a surface gap. The adapter owns that normalization.
