diff --git a/schemas/brief.schema.json b/schemas/brief.schema.json index fd6e319..5a63cc3 100644 --- a/schemas/brief.schema.json +++ b/schemas/brief.schema.json @@ -21,6 +21,7 @@ "type": "string", "enum": [ "technical_blog", + "readme", "tutorial", "how_to", "explanation", diff --git a/schemas/outline.schema.json b/schemas/outline.schema.json index ed2111a..1e187cc 100644 --- a/schemas/outline.schema.json +++ b/schemas/outline.schema.json @@ -18,6 +18,7 @@ "type": "string", "enum": [ "technical_blog", + "readme", "tutorial", "how_to", "explanation", diff --git a/src/claridoc/models.py b/src/claridoc/models.py index 0137d17..63962b1 100644 --- a/src/claridoc/models.py +++ b/src/claridoc/models.py @@ -14,6 +14,7 @@ class ValidationError(ValueError): class DocumentType(str, Enum): TECHNICAL_BLOG = "technical_blog" + README = "readme" TUTORIAL = "tutorial" HOW_TO = "how_to" EXPLANATION = "explanation" diff --git a/src/claridoc/structures.py b/src/claridoc/structures.py index 6152a76..18e1a6b 100644 --- a/src/claridoc/structures.py +++ b/src/claridoc/structures.py @@ -32,6 +32,16 @@ STRUCTURE_SPECS: dict[DocumentType, tuple[SectionSpec, ...]] = { S("tradeoffs", "얻은 것, 잃은 것, 적용하지 않을 때", "What was gained, lost, and when not to apply it", "이 선택의 비용과 한계는 무엇이며 언제 다른 선택이 나은가?", "What are the costs and limits, and when is another choice better?", "프로젝트 지역 결정을 보편 법칙처럼 쓰지 않고, 적용 조건과 남은 위험을 제시한다.", "Do not universalize a project-local decision; state applicability and remaining risks.", ("얻은 것", "잃은 것", "적용 조건", "남은 위험"), ("gains", "costs", "applicability", "remaining risks")), S("conclusion", "결국 지키려던 것은 무엇이었나", "What the design was ultimately protecting", "세부 기술을 걷어냈을 때 남는 판단은 무엇인가?", "What judgment remains after removing implementation detail?", "앞 내용을 반복하지 않고, 문제와 선택을 연결하는 한 문장 판단으로 닫는다.", "Close with a compact judgment that reconnects the problem and choice without repetition.", ("압축된 판단", "독자가 자신의 환경에서 확인할 질문"), ("compressed judgment", "question for the reader's environment")), ), + DocumentType.README: ( + S("problem_value", "이 프로젝트가 필요한 이유", "Why this project exists", "어떤 구체적인 문제를 해결하며 왜 이 프로젝트가 필요한가?", "What concrete problem does this project solve, and why does it exist?", "독자가 겪는 문제와 프로젝트가 제공하는 가치를 실제 상황에서 설명한다.", "Explain the reader's problem and the project's value through a concrete situation.", ("문제 상황", "프로젝트 가치", "대상 독자"), ("problem context", "project value", "intended reader")), + S("principles", "동작 원칙과 지키는 경계", "Operating principles and boundaries", "사용 전에 알아야 할 핵심 원칙과 경계는 무엇인가?", "Which principles and boundaries must readers understand before use?", "프로젝트가 보장하는 동작과 의도적으로 보장하지 않는 범위를 구분한다.", "Separate guaranteed behavior from deliberately unsupported scope.", ("핵심 원칙", "보장 범위", "비보장 범위"), ("core principles", "guarantees", "non-guarantees")), + S("workflow", "전체 동작 흐름", "End-to-end workflow", "입력부터 결과와 검증 기록까지 어떤 순서로 진행되는가?", "How does work proceed from input to output and verification artifacts?", "주요 구성요소와 산출물이 이어지는 전체 흐름을 보여 준다.", "Show the end-to-end flow connecting components and artifacts.", ("입력", "주요 단계", "독자용 결과", "내부 산출물"), ("inputs", "main stages", "reader output", "internal artifacts")), + S("installation", "설치와 시작 전 준비", "Installation and prerequisites", "실행 전에 무엇을 설치하고 준비해야 하는가?", "What must be installed and prepared before use?", "지원 버전, 필수 도구, 설치 명령과 초기 상태를 설명한다.", "Explain supported versions, required tools, installation commands, and initial state.", ("지원 버전", "필수 도구", "설치 명령"), ("supported versions", "required tools", "installation commands")), + S("quickstart", "가장 작은 실행 예시", "Smallest useful run", "가장 짧은 경로로 어떤 유용한 결과를 확인할 수 있는가?", "What useful result can be observed through the shortest path?", "복사 가능한 최소 명령과 예상 결과, 확인 지점을 제공한다.", "Provide the smallest copyable command, expected result, and verification point.", ("최소 입력", "실행 명령", "예상 결과", "확인 방법"), ("minimal input", "run command", "expected result", "verification")), + S("configuration", "주요 설정과 선택 기준", "Configuration and selection criteria", "어떤 설정을 언제 선택하며 결과에 어떤 영향을 주는가?", "Which settings should be chosen when, and how do they affect the result?", "핵심 설정의 기본값, 선택 조건, 비용과 제한을 연결한다.", "Connect key configuration defaults to selection criteria, costs, and limits.", ("설정 항목", "기본값", "선택 조건", "영향"), ("settings", "defaults", "selection criteria", "effects")), + S("verification", "검증과 문제 확인", "Verification and diagnosis", "성공을 어떻게 확인하고 대표적인 실패를 어떻게 좁히는가?", "How is success verified and common failure narrowed down?", "관측 가능한 성공 기준과 비파괴 진단 경로를 제공한다.", "Provide observable success criteria and a non-destructive diagnostic path.", ("성공 기준", "확인 명령", "대표 실패 신호", "진단 경로"), ("success criteria", "check command", "failure signal", "diagnostic path")), + S("limits_next", "한계와 다음 행동", "Limits and next action", "어디까지 검증되었으며 다음에 무엇을 해야 하는가?", "What has been verified, where are the limits, and what comes next?", "근거 한계와 비지원 범위를 밝히고 직접 연결된 다음 행동으로 닫는다.", "State evidence limits and unsupported scope, then close with the next directly related action.", ("검증 범위", "한계", "비지원 항목", "다음 행동"), ("verified scope", "limits", "unsupported items", "next action")), + ), DocumentType.TUTORIAL: ( S("outcome", "완성 결과와 학습 목표", "Outcome and learning objective", "끝에서 무엇을 만들고 무엇을 배우는가?", "What will be built and learned?", "가시적인 결과와 학습 목표를 먼저 보여준다.", "Show the visible outcome and learning objective first.", ("완성 상태", "학습 목표", "예상 소요 범위"), ("finished state", "learning objective", "expected effort")), S("prerequisites", "시작 전 준비 사항", "Prerequisites", "시작 전에 무엇이 준비되어야 하는가?", "What must be ready before starting?", "필요 지식, 도구, 버전, 초기 상태를 명시한다.", "State required knowledge, tools, versions, and initial state.", ("지식", "도구와 버전", "초기 상태"), ("knowledge", "tools and versions", "initial state")), diff --git a/tests/test_models.py b/tests/test_models.py index 5315ab8..ecc562d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,6 +23,14 @@ class ModelTests(unittest.TestCase): self.assertEqual(brief.document_type, DocumentType.TECHNICAL_BLOG) self.assertEqual(Brief.from_dict(brief.to_dict()).title, brief.title) + def test_readme_brief_round_trip(self) -> None: + brief = Brief.from_dict(brief_dict("readme")) + self.assertEqual(brief.document_type, DocumentType.README) + self.assertEqual( + Brief.from_dict(brief.to_dict()).document_type, + DocumentType.README, + ) + def test_invalid_document_type_is_rejected(self) -> None: data = brief_dict() data["document_type"] = "essay" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index e10a3bb..2c8d95f 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -4,7 +4,11 @@ import json import unittest from pathlib import Path +import jsonschema + from claridoc.models import Brief, Outline, PipelineConfig, SourcePack +from claridoc.structures import create_outline +from tests.helpers import brief_dict, make_sources ROOT = Path(__file__).resolve().parents[1] @@ -57,6 +61,20 @@ class SchemaTests(unittest.TestCase): outline = Outline.from_dict(sample) self.assertEqual(outline.sections[0].intent, "promise") + def test_readme_is_accepted_by_brief_and_outline_schemas(self) -> None: + brief_data = brief_dict("readme") + brief_schema = json.loads( + (ROOT / "schemas" / "brief.schema.json").read_text(encoding="utf-8") + ) + outline_schema = json.loads( + (ROOT / "schemas" / "outline.schema.json").read_text(encoding="utf-8") + ) + + jsonschema.Draft202012Validator(brief_schema).validate(brief_data) + brief = Brief.from_dict(brief_data) + outline = create_outline(brief, make_sources()) + jsonschema.Draft202012Validator(outline_schema).validate(outline.to_dict()) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_structures.py b/tests/test_structures.py index 96690b6..42b1d8c 100644 --- a/tests/test_structures.py +++ b/tests/test_structures.py @@ -17,6 +17,23 @@ class StructureTests(unittest.TestCase): self.assertEqual(len(intents), len(set(intents)), document_type.value) self.assertGreaterEqual(len(intents), 7, document_type.value) + def test_readme_outline_preserves_reader_onboarding_order(self) -> None: + brief = Brief.from_dict(brief_dict("readme")) + outline = create_outline(brief, make_sources()) + self.assertEqual( + [section.intent for section in outline.sections], + [ + "problem_value", + "principles", + "workflow", + "installation", + "quickstart", + "configuration", + "verification", + "limits_next", + ], + ) + def test_reconcile_preserves_contract_order(self) -> None: brief = Brief.from_dict(brief_dict()) sources = make_sources()