본문으로 건너뛰기

Architecture

Curriculum OS - 학습 경로 합성

학습 커리큘럼을 outcome 그래프 + prerequisite DAG로 끌어올려 학습 경로를 합성하는 레이어.

Curriculum OS

학습 커리큘럼을 단순 카테고리 트리에서 outcome 그래프로 끌어올린 레이어다. 사용자/teacher가 도메인(목적)을 지정하면 prerequisite을 따라 레슨 순서를 짜고, 커버되지 않는 outcome은 gap으로 리포트한다. Skill OS가 "도구 선택"이라면, Curriculum OS는 "학습 경로 합성"이다.

5계층 구조 (Phase 1~3 완성형)

taxonomy (SSOT YAML - outcomes/domains/capabilityClaims/taskFamilies/lessonOutcomes)
  ↓
lessonGraph (outcome 매핑 + prerequisite DAG + section/role 메타)
  ↓
engine (composer + reviewScheduler + outcomeCredit + outcomeMastery + analyticsTimeline + learnerStateBridge)
  ↓
{REST API, teacher tools}
  ↓
UI surfaces (MasterPlanPanel + MasteryPanel + TodayReviewsCard + AnalyticsPanel)

SSOT - curricula/python/_taxonomy.yml

세 가지 vocabulary를 한 파일에 둔다.

  • outcomes: 학생이 한 레슨을 마치고 얻는 단위 능력. ID는 <도메인 prefix>.<능력명> lowerCamelCase. 예: python.variables, pandas.aggregate, automation.browser.session.
  • domains: 학습자가 실제로 달성하려는 실무 목표. 승격 경로는 versioned capabilityClaims와 required TaskFamily를 함께 가진다.
  • taskFamilies: 같은 underlying rule을 acquisition, transfer, retrieval, application의 다른 표면 조건으로 관찰하는 평가 가족. outcome evidence slice, fixture, CheckSpec version, checker corpus와 artifact contract를 소유한다.
  • lessonOutcomes: <category>/<contentId> 키별 backfill. 레슨 YAML 메타에 outcomes/prerequisites가 있으면 그 쪽이 우선, 없으면 여기 값으로 채운다.
  • sectionOutcomes (Phase 2b): 같은 항목 안의 dict - {sectionId: [outcome, ...]}. 다중 outcome lesson 에서 어느 섹션이 어느 outcome 을 검증하는지 매핑한다. 미지정 시 lesson outcomes 전체로 fallback.
  • lessonRole (Phase 2d): concept | practice | project. project 는 deliverable-driven plan 합성 시 우측 칼럼에 배치되는 마지막 lesson.

새 레슨을 만들 때 권장 순서:

  1. 필요한 outcome/domain이 없으면 _taxonomy.yml에 먼저 추가
  2. 레슨 YAML metaoutcomes/prerequisites/estimatedMinutes 적기 (권장) 또는 lessonOutcomes에 등록
  3. tests/curriculum/testCurriculumOs.py가 그래프 무결성을 검증

planComposer 알고리즘

결정적(deterministic). 같은 입력이면 같은 plan을 낸다.

  1. 목표 outcome 해석 - goal.domain.targetOutcomes ∪ goal.outcomes
  2. projectIntent 매칭 (Phase 2d) - 한국어/영어 키워드를 통해 카테고리 boost, 매칭된 project lesson 들의 outcomes 도 target 에 추가.
  3. skipMasteredOutcomes 필터 (Phase 1) - MASTERY_THRESHOLD 이상 outcome 은 target/expansion 모두 차단.
  4. Backward expansion - 각 target outcome에 대해 best lesson을 선택하고, 그 레슨의 prerequisite outcomes를 큐에 다시 넣는 BFS. 카테고리 우선순위 + sortKey 휴리스틱.
  5. project lesson 강제 포함 (Phase 2d) - projectIntent 매칭 시 backward expansion 이 입문 lesson 으로 같은 outcome 을 cover 했더라도 deliverable lesson 까지 도달.
  6. 위상 정렬 - 선택된 레슨 사이의 prerequisite 의존을 그래프로 만들고 Kahn's algorithm으로 정렬. 사이클이 있으면 sortKey fallback.
  7. 완료 필터 - progressTracker에서 completedAt이 있는 레슨 제외 (옵션).
  8. deliverableOnly 분리 (Phase 2d) - mastery>=0.6 인 outcome 만으로 구성된 concept lesson 은 droppedSteps 로.
  9. maxMinutes cropping (Phase 2a) - prerequisite 순서 유지하며 시간 예산 초과 step 을 droppedSteps 로.
  10. 3단 분리 (Phase 2d) - lessonRole 기준으로 conceptSteps / practiceSteps / projectSteps 합성.
  11. Gap 리포트 - provider 레슨이 없는 outcome은 PlanGap으로 분리 반환 (gaps), 학습자 mastery 가 낮으면 dynamicGaps.

PRIMARY_CATEGORY_ORDER에 카테고리 우선순위가 명시되어 있다 (기초가 앞).

Legacy 진도 진단과 현재 권위

outcomeCredit.py, outcomeMastery.py, reviewScheduler.py, 수동 validation과 unified blend는 기존 화면과 진단 호환을 위해 남아 있다. 이 경로는 현재 capability assurance, application, golden publication을 바꾸지 못한다. 현재 단계 변경 권위는 AttemptObserved -> canonical LearningEvent -> MasteryPolicy@2 -> CapabilityProjection 한 경로다.

새 credit은 explicit assessmentRole: assurance | application과 promoted TaskFamily identity가 모두 있는 strong check만 쓴다. unmarked legacy assessment는 formative 실행과 feedback에는 쓰지만 새 assurance credit을 만들지 않는다.

Phase 3 - Analytics & Bridge

학습 패턴을 시간축으로 + 두 mastery source 통합:

  • analyticsTimeline.py: 일별 DailySnapshot (masteredCount, lessons/sections/credits today, hintHistogram, domainsTouched). JSONL append-only, idempotent per day.
  • learnerStateBridge.py: outcomeMastery (progress 합성) 와 learnerState (EMA SQLite) 를 0.6/0.4 blend 한 UnifiedOutcomeMastery. 두 저장소는 합치지 않음 - 합성 view 만.
  • /api/curriculum/analytics / /analytics/summary / /mastery/unified 엔드포인트.

API 계약

Endpoint Method Payload 반환
/api/curriculum/taxonomy GET - {outcomes, domains}
/api/curriculum/master-plan POST {domain?, outcomes?, excludeCompleted?, excludeKeys?, skipMasteredOutcomes?, maxMinutes?, projectIntent?, deliverableOnly?} MasterPlan (steps, gaps, droppedSteps, conceptSteps/practiceSteps/projectSteps, projectMatches, totalMinutes, summary)
/api/curriculum/gaps GET ?domain= (옵션) {gaps: [{domainId, domainLabel, missing}]}
/api/curriculum/mastery GET - MasteryReport (outcomes, domains, masteredOutcomeCount, totalOutcomeCount)
/api/curriculum/capabilities/{domainId} GET - current-version 보증, 적용과 evidence receipt
/api/curriculum/artifacts/{contentHash} GET - 검증 후 보존한 Local artifact bytes
/api/curriculum/mastery/unified GET - UnifiedMasteryReport (progress + learnerState blend)
/api/curriculum/outcomes/validate POST {outcomeId, validated} toggle 결과
/api/curriculum/check POST {... category?, contentId?, sectionId?} CheckResult + creditedOutcomes + autoValidatedOutcomes
/api/curriculum/reviews GET - {reviews, totalDue}
/api/curriculum/reviews/{cat}/{id} POST {success} updated ReviewState
/api/curriculum/analytics GET ?days=30 {snapshots, totalSnapshots}
/api/curriculum/analytics/summary GET - 30 일 집계

unknown domain/outcome ID는 400 + curriculum_unknown_domain / curriculum_unknown_outcome.

Teacher 도구

teacher-tool-loop에 등록된 7개:

도구 용도
list-curriculum-domains 도메인/outcome 카탈로그
resolve-learning-goal 자연어 → 도메인 후보 랭킹
search-curricula 키워드/카테고리/outcome 기준 레슨 검색
compose-master-plan 도메인/outcome → 순서대로 정렬된 레슨 plan
inspect-curriculum 특정 레슨의 meta/intro 조회
list-curriculum-gaps 도메인별 미충족 outcome 리포트
propose-curriculum-draft 갭을 채울 새 강의 초안만 반환 (사람이 검토·작성)

bulk generation 금지: propose-curriculum-draft는 outline + 메타데이터만 돌려준다. 실제 강의 YAML은 사람이 작성한다 (CLAUDE.md, feedback_curriculum_no_bulk_generation).

프론트엔드

세 가지 surface 가 등록되어 있다.

  • SurfaceMode = "plan"은 legacy 진도 진단과 경로 합성을 제공한다. capability 성취는 curriculum home의 검증된 기본 경로 카드가 canonical projection으로 표시한다.
  • masterPlanPanel.tsx:
    • 도메인 칩 선택 → 자동 plan 합성
    • 시간 예산 / 이미 익힌 능력 건너뛰기 토글
    • projectIntent 텍스트 입력 → deliverableOnly 자동 활성 + 매칭 키워드 chip + 단일↔3단 뷰 토글
    • 3단 뷰 (TieredPlanBody): 개념(zinc) → 실습(sky) → 프로젝트(emerald) 칼럼
    • TodayReviewsCard: 오늘 due review 리스트 + 통과/실패 버튼
    • MasteryPanel: outcome 별 progress + credit count + 자동/수동 검증 토글
    • step 클릭 → selectCurriculumCategory/Content → 해당 레슨
    • gap "초안 요청" 버튼 → chat surface + propose-curriculum-draft 자동 입력
  • SurfaceMode = "analytics" - analyticsPanel.tsx:
    • mastered/30일 lessons/sections/credits stat 카드 4 개
    • SVG mastery 추세 라인 차트
    • 힌트 분포 바 차트
    • 최근 활동 도메인 chip
  • 학습 셀 실행 뒤 curriculumSurface.tsx가 결과를 자동 평가하고, 통과·실패 근거와 필요한 다음 수정을 셀 바로 아래에 표시한다. 별도 검증 버튼, hint reveal, prediction diff, 수동 outcome credit 카드는 사용하지 않는다.

무결성 게이트

tests/curriculum/testCurriculumOs.py가 그래프 무결성을 검사한다. 아래는 절대 수치가 아니라 불변식이며, 케이스는 카탈로그가 커져도 그대로 유지된다:

  • 모든 domain.targetOutcomes가 outcome 카탈로그에 존재
  • 모든 lessonOutcomes의 outcomes/prerequisites가 카탈로그에 존재
  • 레슨 메타의 outcomes가 taxonomy backfill보다 우선
  • 합성기가 prerequisite 순서를 지킴
  • 완료된 레슨이 plan에서 제외됨
  • gap이 정확히 리포트됨
  • 모든 레슨이 plan 그래프에서 보임 (orphan 0개)
  • 모든 outcome이 어떤 레슨에서 제공됨 (uncovered 0개)
  • 모든 도메인이 비어있지 않은 plan을 만듦 (gap 0개)
  • 같은 입력은 같은 plan을 낸다 (결정성 스냅샷)
  • API 엔드포인트 통합 테스트 (FastAPI TestClient)

tests/curriculum/auditCurriculumWeakness.py가 영구 게이트로 등록되어 있다 (tests/run.py gate curriculum-weakness-audit):

  • 정적 신호: orphanInPlan, noExercise (intro 00_* 제외), exerciseWithoutCheck, proseOnlyCheck, noHint, shortGoal, sectionIdMissing (Phase 2b)
  • 카테고리 신호: categoryWithoutProject (Phase 2d). 면제 카테고리는 PROJECT_EXEMPT_CATEGORIES가 소유하며 현재 builtins, excel, practical, devTools, resilience다. 나머지 카테고리는 lessonRole: project 레슨을 최소 1개 가져야 한다.
  • 각 신호별 임계치를 넘으면 게이트 실패. 대부분 0이고 proseOnlyCheck만 현재 부채를 고정한 ratchet이다.
  • 리포트: output/test-runner/curriculum-weakness-audit/curriculum-weakness-report.json

검증 실태와 수렴 방향

exercise를 가진 468개 레슨의 실제 검증 상태다. 숫자보다 중요한 것은 세 상태가 서로 다른 문제라는 점이다.

상태 레슨
산문만 (proseOnlyCheck) 236 {noError: "...", resultCheck: "..."}처럼 typekind도 없다. 사람이 읽는 설명문이고 어떤 엔진도 실행하지 않는다
약한 실행 검사 225 type: noError뿐. 예외만 안 나면 통과하고 정답 여부는 보지 않는다
정답 판정 검사 7 outputExact, contains, 또는 CheckSpec v2

체크 어휘가 두 런타임으로 갈라져 있는 것이 현재 가장 큰 부채다. src/codaro/curriculum/exerciseCheck.pyoutput/variable/contains/noError를 실행하고, editor/src/lib/learningAttemptCheck.ts는 CheckSpec v2와 outputExact를 실행한다. 두 집합은 겹치지 않는다. learningAttemptCheck.tsevidence 등급(none/practice/strong)이 학습자에게 실제로 보이는 계약이므로, 수렴 목표는 CheckSpec v2 하나이며 나머지는 마이그레이션 대상이다.

작업 순서는 지표 정직화, 계약 수렴, 커버리지 상승이다. 커버리지 상승은 레슨 콘텐츠 작업이라 사람이 직접 쓴다. 앞의 두 단계가 끝나야 어느 레슨에 어떤 검사를 써야 하는지가 기계적으로 정해진다.

확장 가이드

새 도메인 추가:

  1. _taxonomy.ymldomains:에 항목 추가 - id, label, description, targetOutcomes
  2. targetOutcomes에 들어가는 outcome이 모두 존재하는지 확인
  3. 해당 outcome을 제공하는 레슨이 없으면 gap으로 리포트됨 - list-curriculum-gaps로 확인
  4. 갭을 채우려면 새 강의를 직접 작성 (생성기 스크립트 금지)

새 outcome 추가:

  1. outcomes:에 항목 추가
  2. 어느 레슨이 이 outcome을 제공하는지 lessonOutcomes에 매핑 - 또는 그 레슨 YAML의 meta.outcomes에 등록
  3. 이 outcome을 prerequisite으로 갖는 다른 레슨이 있다면 cycle이 생기지 않게 확인

캐시는 lazy - 첫 호출 시 로드, 이후 메모리 보존. 핫 리로드는 CurriculumOsCache.invalidate().