{
"$type": "com.atproto.lexicon.schema",
"defs": {
"assessConfig": {
"description": "과제와 채점 **방식**이 필수다. `runners`(실행기)는 선택 — 루브릭 전용 평가는 실행기가 필요 없다. \"graders에 verifier가 있으면 runners가 필수\"는 조건부 제약이라 Lexicon으로 표현할 수 없다(oneOf·if/then 없음) — 저작 파이프라인이 검사한다.",
"properties": {
"assignments": {
"maxLength": 300,
"type": "string"
},
"graders": {
"description": "채점 **방식**만. 실행기는 runner로 분리했다(ADR-0014) — 종전 enum이 방식과 실행기를 섞어 방식 없는 조합도 통과시켰다.",
"items": {
"enum": [
"verifier",
"rubric"
],
"type": "string"
},
"maxLength": 4,
"type": "array"
},
"runners": {
"description": "채점 실행기의 레지스트리 키(NSID). 형식만 검사하고 유효성은 레지스트리가 판정한다(ADR-0014·0015).",
"items": {
"format": "nsid",
"maxLength": 317,
"type": "string"
},
"maxLength": 8,
"type": "array"
}
},
"required": [
"assignments",
"graders"
],
"type": "object"
},
"assessWidget": {
"description": "평가 위젯. **소스 근거 없는 순수 제안이다**(S6 RESULT, Q33).",
"properties": {
"config": {
"ref": "#assessConfig",
"type": "ref"
},
"type": {
"const": "assess",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"attribution": {
"description": "파일 헤더에서 기계 수집. 손으로 옮기면 과소 귀속이 된다(matrix-spec 실측: 주체 11~12개).",
"properties": {
"coverage": {
"ref": "#attributionCoverage",
"type": "ref"
},
"holders": {
"items": {
"ref": "#attributionHolder",
"type": "ref"
},
"maxLength": 500,
"type": "array"
}
},
"required": [
"coverage",
"holders"
],
"type": "object"
},
"attributionCoverage": {
"properties": {
"filesIngested": {
"minimum": 0,
"type": "integer"
},
"filesWithNotice": {
"minimum": 0,
"type": "integer"
}
},
"required": [
"filesIngested",
"filesWithNotice"
],
"type": "object"
},
"attributionHolder": {
"properties": {
"files": {
"minimum": 0,
"type": "integer"
},
"name": {
"maxLength": 200,
"type": "string"
},
"years": {
"maxLength": 32,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"corpus": {
"description": "인제스트 대상. ADR-0011: 렌더 URL이 아니라 리비전으로 고정한다(Q39).",
"properties": {
"attribution": {
"ref": "#attribution",
"type": "ref"
},
"commit": {
"description": "재현성의 근거. 렌더 사이트 URL은 빌드 커밋 정보가 없어 재빌드 불가.",
"maxLength": 64,
"minLength": 7,
"type": "string"
},
"exclude": {
"items": {
"ref": "#excludeRule",
"type": "ref"
},
"maxLength": 200,
"type": "array"
},
"include": {
"items": {
"ref": "#includeRule",
"type": "ref"
},
"maxLength": 200,
"type": "array"
},
"license": {
"ref": "#licenseRecord",
"type": "ref"
},
"notice": {
"ref": "#noticeRecord",
"type": "ref"
},
"packs": {
"items": {
"ref": "#packRef",
"type": "ref"
},
"maxLength": 64,
"type": "array"
},
"repo": {
"format": "uri",
"type": "string"
},
"rights": {
"ref": "#rightsAxes",
"type": "ref"
},
"upstream": {
"description": "포크 원본.",
"format": "uri",
"type": "string"
}
},
"required": [
"repo",
"commit",
"rights",
"license",
"notice",
"attribution"
],
"type": "object"
},
"countComparator": {
"description": "개수·일수 지표 전용. 값에 스케일이 없다 — `value: 30`은 30회 또는 30일이다.",
"properties": {
"metric": {
"enum": [
"attempts",
"elapsedDays"
],
"type": "string"
},
"op": {
"enum": [
"lt",
"lte",
"gt",
"gte",
"eq",
"ne"
],
"type": "string"
},
"then": {
"enum": [
"reference",
"explorer",
"drill",
"sandbox",
"assess",
"masteryMap"
],
"type": "string"
},
"value": {
"description": "스케일 없는 정수. 상한 36500은 도메인 측정이 아니라 **오타·폭주 방지용 임의 상한**이다(elapsedDays 100년치). attempts도 같은 상한을 공유한다.",
"maximum": 36500,
"minimum": 0,
"type": "integer"
}
},
"required": [
"metric",
"op",
"value",
"then"
],
"type": "object"
},
"drillConfig": {
"description": "카드셋과 스케줄러가 없으면 복습이 성립하지 않는다.",
"properties": {
"cardSet": {
"maxLength": 300,
"type": "string"
},
"scheduler": {
"description": "제약 6번: 자체 스케줄러 금지.",
"enum": [
"fsrs"
],
"type": "string"
}
},
"required": [
"cardSet",
"scheduler"
],
"type": "object"
},
"drillWidget": {
"description": "복습 위젯.",
"properties": {
"config": {
"ref": "#drillConfig",
"type": "ref"
},
"type": {
"const": "drill",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"excludeRule": {
"description": "제외 사유를 닫힌 enum으로 강제한다 — 자유 문자열은 검사할 수 없다(ADR-0005).",
"properties": {
"path": {
"maxLength": 500,
"type": "string"
},
"reason": {
"enum": [
"unconfirmed_content",
"superseded",
"not_learning_material",
"build_artifact",
"rights_denied",
"duplicate"
],
"type": "string"
}
},
"required": [
"path",
"reason"
],
"type": "object"
},
"explorerWidget": {
"description": "탐색 위젯. config 형태는 reference와 같아 같은 def을 가리킨다 — 지금 같다는 사실을 숨기지 않는다.",
"properties": {
"config": {
"ref": "#indexConfig",
"type": "ref"
},
"type": {
"const": "explorer",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"flow": {
"properties": {
"entry": {
"enum": [
"reference",
"explorer",
"drill",
"sandbox",
"assess",
"masteryMap"
],
"type": "string"
},
"rules": {
"items": {
"closed": true,
"description": "ADR-0015: 천분율 지표와 개수·일수 지표는 값의 스케일이 달라 한 def에 담을 수 없다. 종전 단일 `#comparator`는 `elapsedDays > 30`(30일)을 0.03으로 읽었고 `> 2000`(2000일)은 maximum 1000에 걸려 표현 자체가 불가능했다.",
"refs": [
"#ratioComparator",
"#countComparator"
],
"type": "union"
},
"maxLength": 32,
"type": "array"
}
},
"required": [
"entry",
"rules"
],
"type": "object"
},
"includeRule": {
"properties": {
"path": {
"maxLength": 500,
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
},
"indexConfig": {
"description": "reference·explorer 공용. 색인 없이는 두 위젯 모두 렌더할 대상이 없으므로 `index`가 필수다.",
"properties": {
"groupBy": {
"enum": [
"apiSurface",
"module",
"chapter",
"theorem",
"text",
"version"
],
"type": "string"
},
"index": {
"enum": [
"markdown",
"openapi",
"eventSchema",
"theoremGraph",
"annotatedText"
],
"type": "string"
},
"secondaryIndex": {
"enum": [
"markdown",
"openapi",
"eventSchema",
"theoremGraph",
"annotatedText"
],
"type": "string"
}
},
"required": [
"index"
],
"type": "object"
},
"licenseRecord": {
"description": "ADR-0011 6항: 계산값. 저자가 손으로 적지 않는다.",
"properties": {
"perFileChecked": {
"description": "리포 단위 LICENSE 선언만 믿지 않고 파일별로 검사했는가.",
"type": "boolean"
},
"spdx": {
"maxLength": 64,
"type": "string"
},
"textRef": {
"accept": [
"text/plain"
],
"description": "Apache-2.0 §4(a): 라이선스 전문 사본. 링크가 아니라 동봉이다. [미구현] 스키마는 조건부 required를 표현할 수 없다. 강제는 빌드 게이트(미작성) 몫이며 현재 강제되지 않는다.",
"maxSize": 1000000,
"type": "blob"
}
},
"required": [
"spdx",
"perFileChecked"
],
"type": "object"
},
"main": {
"description": "도메인 학습 앱의 선언적 정의. 코드가 아니라 데이터다. ADR-0005: 필드는 닫힌 enum·비교기·레지스트리 ID만 허용한다.",
"key": "tid",
"record": {
"properties": {
"corpus": {
"ref": "#corpus",
"type": "ref"
},
"flow": {
"ref": "#flow",
"type": "ref"
},
"forkOf": {
"description": "포크 계보. 자기 신고 필드이므로 위조 가능하다.",
"format": "at-uri",
"type": "string"
},
"license": {
"description": "앱 정의 자체의 라이선스(SPDX).",
"maxLength": 64,
"type": "string"
},
"name": {
"description": "표시 이름. 제3자 상표를 포함하면 trademarkClearance 없이 publish 거부(ADR-0011 7항).",
"maxGraphemes": 100,
"maxLength": 300,
"type": "string"
},
"offer": {
"format": "at-uri",
"type": "string"
},
"pattern": {
"description": "소스 유형 기반 템플릿. enum이므로 새 패턴은 Lexicon 개정을 요구한다.",
"enum": [
"spec-corpus",
"book-corpus",
"problem-set",
"text-corpus"
],
"type": "string"
},
"runtime": {
"description": "렌더러 최소 버전 요구.",
"maxLength": 32,
"type": "string"
},
"trademarkClearance": {
"description": "제3자 상표 사용의 사전 서면 동의 근거. 없으면 상표 포함 명명 불가.",
"ref": "#trademarkClearance",
"type": "ref"
},
"version": {
"maxLength": 32,
"type": "string"
},
"widgets": {
"items": {
"closed": true,
"description": "ADR-0015: 위젯 타입마다 별도 def이고 config의 필수 필드가 다르므로 닫힌 union이다. 데이터에는 `$type`이 필수이며 형식은 전체 NSID(`uk.gdeo.learn.app#drillWidget`)다 — atproto Lexicon 스펙 \"union variants must always include $type\". 종전 plain ref는 config에 required가 하나도 없어 `config: {}`를 통과시켰다(반례 확인 2026-09-10).",
"refs": [
"#referenceWidget",
"#explorerWidget",
"#drillWidget",
"#sandboxWidget",
"#assessWidget",
"#masteryMapWidget"
],
"type": "union"
},
"maxLength": 16,
"minLength": 1,
"type": "array"
}
},
"required": [
"name",
"version",
"runtime",
"pattern",
"license",
"corpus",
"widgets",
"flow"
],
"type": "object"
},
"type": "record"
},
"masteryMapConfig": {
"description": "스킬 그래프 참조가 없으면 그릴 것이 없다.",
"properties": {
"graph": {
"maxLength": 300,
"type": "string"
}
},
"required": [
"graph"
],
"type": "object"
},
"masteryMapWidget": {
"description": "숙달도 맵 위젯.",
"properties": {
"config": {
"ref": "#masteryMapConfig",
"type": "ref"
},
"type": {
"const": "masteryMap",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"noticeRecord": {
"description": "빌드 타임 검사 결과. present:true인데 textRef가 없으면 빌드 실패. [미구현] 이 조건은 Lexicon required로 표현되지 않으며 빌드 게이트가 아직 없다.",
"properties": {
"present": {
"type": "boolean"
},
"textRef": {
"accept": [
"text/plain"
],
"maxSize": 1000000,
"type": "blob"
}
},
"required": [
"present"
],
"type": "object"
},
"packRef": {
"properties": {
"ref": {
"format": "at-uri",
"type": "string"
},
"visibility": {
"description": "ADR-0001: public만 AppView 인덱싱 대상. gated는 미리보기 메타만.",
"enum": [
"public",
"gated",
"private"
],
"type": "string"
}
},
"required": [
"ref",
"visibility"
],
"type": "object"
},
"ratioComparator": {
"description": "ADR-0005: 평가기가 아니라 비교기다. 표현식 문자열을 두지 않는다. 이 def은 **비율 지표 전용**이다.",
"properties": {
"metric": {
"enum": [
"mastery",
"retrievability"
],
"type": "string"
},
"op": {
"enum": [
"lt",
"lte",
"gt",
"gte",
"eq",
"ne"
],
"type": "string"
},
"then": {
"enum": [
"reference",
"explorer",
"drill",
"sandbox",
"assess",
"masteryMap"
],
"type": "string"
},
"valueMilli": {
"description": "천분율(0~1000). Lexicon에 float가 없으므로 정수로 표현한다 — 0.6은 600이다. 필드명에 스케일을 박아 개수·일수와 섞이지 않게 한다.",
"maximum": 1000,
"minimum": 0,
"type": "integer"
}
},
"required": [
"metric",
"op",
"valueMilli",
"then"
],
"type": "object"
},
"referenceWidget": {
"description": "참조 위젯. 코퍼스를 색인으로 훑는다.",
"properties": {
"config": {
"ref": "#indexConfig",
"type": "ref"
},
"type": {
"const": "reference",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"rightsAxes": {
"description": "ADR-0011: 권리는 단일 라이선스 문자열이 아니라 축이다. 판정 단위는 문서다.",
"properties": {
"commercial": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"copyleft": {
"enum": [
"none",
"share_alike"
],
"type": "string"
},
"derivative": {
"enum": [
"allow",
"deny"
],
"type": "string"
},
"excerpt_limit": {
"default": "none",
"enum": [
"none",
"ietf-one-fifth"
],
"type": "string"
},
"license_id": {
"description": "unknown의 기본 동작은 link_only다(ADR-0011 3항).",
"enum": [
"CC0-1.0",
"CC-BY-4.0",
"CC-BY-SA-4.0",
"Apache-2.0",
"MIT",
"W3C-Software-and-Document",
"W3C-Document",
"IETF-TLP",
"arXiv-nonexclusive-1.0",
"CC-BY-NC-SA-4.0",
"CC-BY-NC-ND-4.0",
"public-domain",
"unknown"
],
"type": "string"
},
"notice_obligations": {
"items": {
"enum": [
"attribution",
"license_text",
"change_indication",
"ietf_legends",
"w3c_status"
],
"type": "string"
},
"maxLength": 8,
"type": "array"
},
"verbatim_excerpt": {
"enum": [
"allow",
"deny"
],
"type": "string"
}
},
"required": [
"license_id",
"verbatim_excerpt",
"derivative",
"commercial",
"copyleft"
],
"type": "object"
},
"sandboxConfig": {
"description": "실행 환경을 지목하는 레지스트리 키가 없으면 샌드박스가 없다.",
"properties": {
"provider": {
"description": "플랫폼 소유 provider 레지스트리(`uk.gdeo.learn.providerRegistry`)의 키. **enum이 아니다**(ADR-0014) — 종전 enum 3개가 첫 코스의 샌드박스 3개와 1:1 대응해 확정 제약 8번을 어겼다. **키는 NSID다**(ADR-0015) — 종전 `format: nsid-like`는 스펙 공식 목록 11종에 없는 발명값이었고 공식 검증기 동작이 미확인이었다. 제3자는 자기 도메인 권한으로 등록한다(`com.example.provider.foo`). 구문은 스키마가, 존재 여부는 레지스트리가 판정한다. maxLength 317은 NSID 전체 길이 상한(스펙, 확인일 2026-09-10). 컨테이너 이미지를 지목하지 않는다(ADR-0005).",
"format": "nsid",
"maxLength": 317,
"type": "string"
}
},
"required": [
"provider"
],
"type": "object"
},
"sandboxWidget": {
"description": "샌드박스 위젯. **소스 근거 없는 순수 제안이다**(S6 RESULT, Q33).",
"properties": {
"config": {
"ref": "#sandboxConfig",
"type": "ref"
},
"type": {
"const": "sandbox",
"description": "union의 `$type`과 중복이지만 const로 고정돼 불일치가 불가능하다. `flow.entry`·`then`의 enum이 이 값을 가리킨다.",
"type": "string"
}
},
"required": [
"type",
"config"
],
"type": "object"
},
"trademarkClearance": {
"properties": {
"evidenceRef": {
"description": "사전 서면 동의의 근거 참조.",
"maxLength": 500,
"type": "string"
},
"grantedAt": {
"format": "datetime",
"type": "string"
},
"grantedBy": {
"maxLength": 200,
"type": "string"
},
"mark": {
"maxLength": 100,
"type": "string"
}
},
"required": [
"mark",
"grantedBy",
"evidenceRef"
],
"type": "object"
}
},
"id": "uk.gdeo.learn.app",
"lexicon": 1
}