Files
document-haness/build/lib/claridoc/structures.py
T

217 lines
30 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from claridoc.models import Brief, DocumentType, Outline, OutlineSection, SourcePack, ValidationError, unique_nonempty
from claridoc.utils import slugify
@dataclass(frozen=True, slots=True)
class SectionSpec:
intent: str
title_ko: str
title_en: str
question_ko: str
question_en: str
purpose_ko: str
purpose_en: str
must_include_ko: tuple[str, ...] = ()
must_include_en: tuple[str, ...] = ()
S = SectionSpec
STRUCTURE_SPECS: dict[DocumentType, tuple[SectionSpec, ...]] = {
DocumentType.TECHNICAL_BLOG: (
S("problem_scene", "코드보다 먼저 드러난 문제", "The problem that appeared before the code", "독자가 공감할 수 있는 구체적인 상황에서 어떤 문제가 드러났는가?", "What concrete situation exposed the problem?", "추상적인 글쓰기 계약이 아니라 실제 장면, 증상, 비용으로 시작한다.", "Open with a concrete scene, symptom, and cost rather than a writing contract.", ("구체적인 상황", "문제가 만든 비용", "이 글에서 풀 질문"), ("concrete situation", "cost of the problem", "question to answer")),
S("constraints", "문제를 어렵게 만든 제약", "Constraints that made the problem hard", "단순한 해법을 막은 프로젝트 제약은 무엇이었는가?", "Which project constraints ruled out a simple answer?", "현재 구조, 독자에게 필요한 배경, 확인된 사실과 미확인 영역을 분리한다.", "Separate current structure, necessary context, verified facts, and unknowns.", ("현재 구조", "제약", "확인된 사실과 사실 경계"), ("current structure", "constraints", "verified facts and boundaries")),
S("options", "검토한 선택지와 막힌 지점", "Options considered and where they failed", "어떤 대안들을 검토했고 각각 어디에서 비용이 생겼는가?", "Which alternatives were considered, and where did each incur cost?", "최소 두 선택지를 같은 기준으로 비교하고, 실패한 시도나 제외 이유를 숨기지 않는다.", "Compare at least two options on the same criteria and expose failed attempts or rejection reasons.", ("대안", "비교 기준", "제외 이유 또는 실패한 시도"), ("alternatives", "comparison criteria", "rejection reason or failed attempt")),
S("decision_rationale", "선택의 이유와 지킨 경계", "Why this choice was made and which boundary remained", "왜 이 선택을 했으며 무엇을 일부러 포기하거나 금지했는가?", "Why was this choice made, and what was deliberately rejected or constrained?", "선택을 제약, 이유, 대안, 수용 비용, 보완 가드레일까지 한 묶음으로 설명한다.", "Explain the choice as one unit: constraint, rationale, alternative, accepted cost, and guardrail.", ("선택", "왜 선택했는가", "대안", "수용한 비용", "가드레일"), ("choice", "why", "alternative", "accepted cost", "guardrail")),
S("mechanism", "선택이 코드와 흐름에 반영되는 방식", "How the choice appears in code and flow", "결정이 모듈, 인터페이스, 제어 흐름에 어떻게 반영되는가?", "How does the decision appear in modules, interfaces, and control flow?", "실제 이름과 경계를 사용해 인과 흐름을 설명하고, 하나의 구체적인 예시를 끝까지 따라간다.", "Use real names and boundaries to explain causality and carry one concrete example end to end.", ("실제 구성요소", "제어 또는 데이터 흐름", "구체적인 예시", "불변조건"), ("real components", "control or data flow", "concrete example", "invariant")),
S("evidence_verification", "결정이 지켜지는지 확인하는 방법", "How the decision is verified", "설명한 경계와 결과가 실제로 유지되는지 어떻게 확인하는가?", "How is the described boundary and outcome verified?", "테스트, 빌드 규칙, 관측값을 주장과 연결하고 검증 범위를 과장하지 않는다.", "Connect tests, build rules, and observations to claims without overstating verification.", ("검증 절차", "성공 기준", "검증하지 못한 범위"), ("verification procedure", "success criteria", "unverified scope")),
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.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")),
S("route", "전체 경로 미리보기", "Route preview", "어떤 순서로 결과에 도달하는가?", "In what sequence will the outcome be reached?", "독자가 길을 잃지 않도록 전체 단계를 먼저 지도처럼 제시한다.", "Preview the full route so the reader does not lose orientation.", ("단계 목록", "중간 체크포인트"), ("step list", "checkpoints")),
S("guided_steps", "단계별 구현", "Guided implementation", "각 단계에서 무엇을 하고 왜 하는가?", "What happens at each step, and why?", "한 단계에 한 행동을 두고 결과와 이유를 함께 설명한다.", "Use one action per step and explain its result and rationale.", ("번호가 있는 단계", "명령 또는 코드", "각 단계의 예상 결과"), ("numbered steps", "commands or code", "expected result per step")),
S("checkpoint", "중간 체크포인트", "Intermediate checkpoint", "여기까지 제대로 왔는지 어떻게 확인하는가?", "How can progress be checked here?", "실패를 조기에 발견할 수 있는 작은 검증을 제공한다.", "Provide a small verification that catches failure early.", ("확인 명령", "정상 출력", "틀렸을 때 되돌아갈 지점"), ("check command", "expected output", "recovery point")),
S("verification", "최종 검증", "Final verification", "완성 결과가 요구사항을 충족하는가?", "Does the result satisfy the requirement?", "재현 가능한 최종 테스트와 성공 기준을 제공한다.", "Provide a reproducible final test and success criteria.", ("테스트", "성공 기준", "정리 방법"), ("test", "success criteria", "cleanup")),
S("next_steps", "다음 단계", "Next steps", "이제 무엇을 확장하거나 연습해야 하는가?", "What should be extended or practiced next?", "학습 목표와 직접 연결된 다음 행동만 제안한다.", "Offer only next actions directly connected to the learning objective.", ("확장 과제", "관련 개념"), ("extension task", "related concept")),
),
DocumentType.HOW_TO: (
S("goal", "목표와 적용 조건", "Goal and applicability", "이 절차는 어떤 결과를 언제 제공하는가?", "What result does this procedure provide, and when?", "구체적인 작업 결과와 적용 조건을 먼저 밝힌다.", "State the concrete task outcome and applicability first.", ("결과", "적용 조건", "비적용 조건"), ("outcome", "when to use", "when not to use")),
S("prerequisites", "사전 조건", "Prerequisites", "실행 전에 무엇을 확인해야 하는가?", "What must be checked before execution?", "권한, 버전, 백업, 초기 상태를 확인한다.", "Check permissions, versions, backups, and initial state.", ("권한", "버전", "백업 또는 복구점"), ("permissions", "versions", "backup or recovery point")),
S("procedure", "실행 절차", "Procedure", "목표를 달성하려면 어떤 순서로 행동하는가?", "What sequence of actions achieves the goal?", "가장 짧고 안전한 순서로 번호가 있는 단계를 제시한다.", "Present numbered steps in the shortest safe order.", ("번호가 있는 단계", "명령", "단계별 예상 결과"), ("numbered steps", "commands", "expected result per step")),
S("verification", "결과 확인", "Verify the result", "작업이 성공했는지 어떻게 확인하는가?", "How is success verified?", "관측 가능한 성공 기준과 확인 명령을 제공한다.", "Provide observable success criteria and checks.", ("확인 명령", "성공 기준"), ("check command", "success criteria")),
S("rollback", "중단 및 롤백", "Stop and rollback", "실패하거나 중단해야 할 때 어떻게 원복하는가?", "How is the change reversed if it fails?", "중단 조건과 복구 절차를 명시한다.", "State stop conditions and recovery procedure.", ("중단 조건", "롤백 단계", "복구 확인"), ("stop conditions", "rollback steps", "recovery verification")),
S("troubleshooting", "자주 발생하는 문제", "Common problems", "대표적인 실패 신호와 해결법은 무엇인가?", "What are the common failure signals and fixes?", "증상-원인-조치 형태로 최소한의 진단을 제공한다.", "Provide concise symptom-cause-action diagnostics.", ("증상", "가능한 원인", "조치"), ("symptom", "likely cause", "action")),
S("next_steps", "관련 작업", "Related tasks", "이 작업과 직접 연결되는 다음 절차는 무엇인가?", "Which directly related procedure comes next?", "직접 관련된 후속 작업만 연결한다.", "Link only directly related follow-up tasks.", (), ()),
),
DocumentType.EXPLANATION: (
S("question", "질문과 핵심 답", "Question and core answer", "이 문서가 답하는 질문과 결론은 무엇인가?", "What question does this document answer, and what is the answer?", "질문, 범위, 핵심 답을 앞에 둔다.", "Front-load the question, scope, and core answer.", ("질문", "핵심 답", "범위"), ("question", "core answer", "scope")),
S("familiar_anchor", "익숙한 개념에서 출발하기", "Start from a familiar anchor", "독자의 기존 지식과 새 개념은 어떻게 연결되는가?", "How does the new concept connect to prior knowledge?", "비교와 대조로 새로운 개념의 위치를 잡는다.", "Locate the new concept through comparison and contrast.", ("비교 대상", "같은 점", "다른 점"), ("comparison", "similarities", "differences")),
S("mental_model", "멘털 모델", "Mental model", "어떤 추상화로 전체를 이해할 수 있는가?", "What abstraction explains the whole?", "구성요소와 관계를 단순한 모델로 제시한다.", "Present components and relationships as a simple model.", ("구성요소", "관계", "불변조건"), ("components", "relationships", "invariants")),
S("mechanism", "내부 동작과 인과 관계", "Mechanism and causality", "원인에서 결과까지 어떤 일이 일어나는가?", "What happens from cause to effect?", "시간 또는 인과 순서에 따라 메커니즘을 설명한다.", "Explain the mechanism in temporal or causal order.", ("시작 조건", "중간 과정", "결과"), ("starting condition", "intermediate process", "result")),
S("example", "구체적인 예시", "Concrete example", "추상 모델이 실제 사례에서는 어떻게 보이는가?", "What does the abstract model look like in practice?", "모델의 각 요소가 보이는 예시를 제공한다.", "Provide an example in which each model element is visible.", ("입력", "과정", "출력"), ("input", "process", "output")),
S("alternatives", "다른 관점과 대안", "Alternative views", "다른 설명이나 접근법과 무엇이 다른가?", "How does this differ from alternatives?", "대안을 공정하게 비교한다.", "Compare alternatives fairly.", ("대안", "선택 기준"), ("alternatives", "selection criteria")),
S("limits", "한계와 오해하기 쉬운 지점", "Limits and common misconceptions", "이 모델은 어디까지 유효하며 무엇을 설명하지 못하는가?", "Where does this model stop being useful?", "경계 조건과 흔한 오해를 명시한다.", "State boundary conditions and common misconceptions.", ("경계 조건", "오해", "예외"), ("boundary conditions", "misconceptions", "exceptions")),
S("implications", "실무적 의미", "Practical implications", "이 이해가 설계나 운영 판단을 어떻게 바꾸는가?", "How should this understanding change design or operations?", "개념을 실제 판단으로 연결한다.", "Connect the concept to real decisions.", ("판단 기준", "다음 행동"), ("decision criteria", "next action")),
),
DocumentType.REFERENCE: (
S("scope_version", "범위, 버전, 호환성", "Scope, version, and compatibility", "이 참조가 다루는 정확한 표면과 버전은 무엇인가?", "What exact surface and version does this reference cover?", "대상, 버전, 안정성, 비범위를 명시한다.", "State target, version, stability, and non-scope.", ("대상", "버전", "호환성"), ("target", "version", "compatibility")),
S("syntax", "구문 또는 스키마", "Syntax or schema", "정확한 형식은 무엇인가?", "What is the exact form?", "복사 가능한 정규 형식을 먼저 제공한다.", "Provide the canonical copyable form first.", ("정규 형식", "필수 요소", "선택 요소"), ("canonical form", "required elements", "optional elements")),
S("parameters", "매개변수와 필드", "Parameters and fields", "각 입력의 타입, 기본값, 제약은 무엇인가?", "What are the type, default, and constraints of each input?", "빠르게 찾을 수 있는 표로 입력을 정리한다.", "Organize inputs in a scannable table.", ("이름", "타입", "필수 여부", "기본값", "제약"), ("name", "type", "required", "default", "constraints")),
S("behavior", "동작과 반환값", "Behavior and return values", "정상 조건에서 무엇이 보장되는가?", "What is guaranteed under normal conditions?", "동작, 부작용, 반환, 불변조건을 정의한다.", "Define behavior, side effects, return values, and invariants.", ("동작", "반환", "부작용"), ("behavior", "returns", "side effects")),
S("errors", "오류와 경계 조건", "Errors and edge cases", "어떤 조건에서 어떤 오류가 발생하는가?", "Which conditions produce which errors?", "오류 코드, 조건, 대응을 구조화한다.", "Structure error codes, conditions, and responses.", ("오류", "발생 조건", "대응"), ("error", "condition", "response")),
S("examples", "최소 예시", "Minimal examples", "가장 작은 유효 사용법은 무엇인가?", "What is the smallest valid use?", "설명보다 조회에 적합한 짧은 예시를 제공한다.", "Provide short lookup-oriented examples.", ("최소 예시", "출력"), ("minimal example", "output")),
S("related", "관련 항목", "Related entries", "함께 조회해야 할 인접 항목은 무엇인가?", "Which adjacent entries should be consulted?", "직접 관련된 항목만 연결한다.", "Link only directly adjacent entries.", (), ()),
),
DocumentType.TROUBLESHOOTING: (
S("symptom", "증상과 판별 기준", "Symptom and identification", "어떤 관측으로 이 문제를 식별하는가?", "Which observations identify this problem?", "사용자가 보는 신호와 정확한 판별 조건을 제시한다.", "State visible signals and precise identification criteria.", ("증상", "로그 또는 지표", "판별 조건"), ("symptom", "logs or metrics", "identification")),
S("impact", "영향과 우선순위", "Impact and priority", "영향 범위와 대응 우선순위는 무엇인가?", "What is the blast radius and response priority?", "영향, 긴급도, 중단 조건을 명시한다.", "State impact, urgency, and stop conditions.", ("영향 범위", "긴급도", "중단 조건"), ("blast radius", "urgency", "stop conditions")),
S("safety", "진단 전 안전 조치", "Safety before diagnosis", "조사 전에 무엇을 보존하거나 차단해야 하는가?", "What must be preserved or isolated first?", "증거 보존, 백업, 변경 금지를 명시한다.", "State evidence preservation, backups, and change restrictions.", ("증거 보존", "백업", "권한"), ("evidence preservation", "backup", "permissions")),
S("diagnosis", "최소 진단 절차", "Minimal diagnostic path", "가장 적은 단계로 원인 범주를 어떻게 좁히는가?", "How can the cause category be narrowed with minimal steps?", "저비용·비파괴 검사부터 의사결정 트리로 진행한다.", "Use a decision path from low-cost, non-destructive checks.", ("번호가 있는 검사", "예상 관측", "분기 조건"), ("numbered checks", "expected observation", "branch condition")),
S("causes", "원인별 분기", "Cause branches", "각 관측은 어떤 원인과 연결되는가?", "Which cause corresponds to each observation?", "증거와 원인을 일대일로 연결한다.", "Map evidence to causes explicitly.", ("관측", "가능한 원인", "확신 수준"), ("observation", "likely cause", "confidence")),
S("fixes", "원인별 조치", "Fixes by cause", "확인된 원인별로 어떤 조치를 하는가?", "What action corresponds to each confirmed cause?", "최소 변경부터 조치하고 부작용을 경고한다.", "Apply the smallest change first and warn about side effects.", ("조치", "위험", "롤백"), ("action", "risk", "rollback")),
S("verification", "복구 확인", "Recovery verification", "복구와 재발 여부를 어떻게 확인하는가?", "How are recovery and recurrence checked?", "성공 기준, 관찰 기간, 재발 신호를 명시한다.", "State success criteria, observation period, and recurrence signals.", ("성공 기준", "관찰", "재발 신호"), ("success criteria", "observation", "recurrence signal")),
S("prevention", "재발 방지와 에스컬레이션", "Prevention and escalation", "무엇을 바꾸고 언제 상위 대응으로 넘기는가?", "What should change, and when should the issue be escalated?", "예방 조치, 소유자, 에스컬레이션 조건을 제시한다.", "State prevention, ownership, and escalation criteria.", ("예방", "소유자", "에스컬레이션 조건"), ("prevention", "owner", "escalation criteria")),
),
DocumentType.DESIGN_DOC: (
S("summary", "요약과 결정 요청", "Summary and decision request", "무엇을 결정해야 하며 추천안은 무엇인가?", "What must be decided, and what is recommended?", "결정 요청, 추천안, 핵심 이유를 앞에 둔다.", "Front-load the decision request, recommendation, and reasons.", ("결정 요청", "추천안", "핵심 이유"), ("decision", "recommendation", "rationale")),
S("context", "배경과 문제 정의", "Context and problem statement", "현재 상태의 어떤 문제가 변화를 요구하는가?", "What current-state problem requires change?", "현재 상태, 문제, 증거, 이해관계자를 정의한다.", "Define current state, problem, evidence, and stakeholders.", ("현재 상태", "문제", "영향"), ("current state", "problem", "impact")),
S("goals_non_goals", "목표와 비목표", "Goals and non-goals", "성공 범위와 의도적으로 제외하는 것은 무엇인가?", "What is success, and what is intentionally excluded?", "검증 가능한 목표와 비목표를 명시한다.", "State verifiable goals and non-goals.", ("목표", "성공 지표", "비목표"), ("goals", "success metrics", "non-goals")),
S("constraints", "요구사항과 제약", "Requirements and constraints", "설계가 반드시 만족해야 할 조건은 무엇인가?", "Which conditions must the design satisfy?", "기능·비기능 요구사항과 고정 제약을 구분한다.", "Separate functional, non-functional, and fixed constraints.", ("기능 요구", "비기능 요구", "제약"), ("functional", "non-functional", "constraints")),
S("options", "검토한 대안", "Options considered", "실현 가능한 대안과 비교 기준은 무엇인가?", "Which feasible options and comparison criteria exist?", "최소 두 대안을 같은 기준으로 비교한다.", "Compare at least two options using the same criteria.", ("대안", "비교 기준", "비교 결과"), ("options", "criteria", "comparison")),
S("decision", "선택과 근거", "Decision and rationale", "왜 이 선택이 제약 아래에서 최선인가?", "Why is this choice best under the constraints?", "결정, 근거, 받아들이는 비용을 명시한다.", "State decision, rationale, and accepted costs.", ("결정", "근거", "수용한 비용"), ("decision", "rationale", "accepted cost")),
S("architecture", "아키텍처와 데이터 흐름", "Architecture and data flow", "구성요소는 어떻게 상호작용하는가?", "How do components interact?", "경계, 인터페이스, 데이터 흐름, 불변조건을 설명한다.", "Explain boundaries, interfaces, data flow, and invariants.", ("구성요소", "인터페이스", "데이터 흐름", "불변조건"), ("components", "interfaces", "data flow", "invariants")),
S("failure_modes", "실패 모드와 보안", "Failure modes and security", "어떻게 실패하며 피해를 어떻게 제한하는가?", "How can it fail, and how is damage limited?", "실패 시나리오, 보안, 격리, 복구를 다룬다.", "Cover failure scenarios, security, isolation, and recovery.", ("실패 모드", "영향", "완화", "복구"), ("failure mode", "impact", "mitigation", "recovery")),
S("rollout", "마이그레이션과 롤아웃", "Migration and rollout", "어떻게 점진적으로 전환하고 되돌리는가?", "How is the change rolled out and reversed incrementally?", "단계, 호환성, 중단 기준, 롤백을 정의한다.", "Define phases, compatibility, stop criteria, and rollback.", ("단계", "중단 기준", "롤백"), ("phases", "stop criteria", "rollback")),
S("observability", "관측성과 검증", "Observability and validation", "성공과 이상을 어떤 신호로 판단하는가?", "Which signals indicate success or anomaly?", "지표, 로그, 추적, 테스트와 성공 기준을 정의한다.", "Define metrics, logs, traces, tests, and success criteria.", ("지표", "로그", "테스트", "성공 기준"), ("metrics", "logs", "tests", "success criteria")),
S("risks_open", "위험, 미해결 질문, 후속 결정", "Risks, open questions, and follow-ups", "결정 전에 남은 불확실성은 무엇인가?", "What uncertainty remains before or after the decision?", "위험, 가정, 소유자, 기한을 명시한다.", "State risks, assumptions, owners, and deadlines.", ("위험", "가정", "미해결 질문", "소유자"), ("risks", "assumptions", "open questions", "owner")),
),
}
def _rank_evidence_ids(brief: Brief, spec: SectionSpec, sources: SourcePack, *, limit: int) -> list[str]:
query = " ".join(
[
brief.title,
brief.core_message,
*brief.required_topics,
spec.title_ko if brief.is_korean else spec.title_en,
spec.question_ko if brief.is_korean else spec.question_en,
*(spec.must_include_ko if brief.is_korean else spec.must_include_en),
]
).casefold()
query_tokens = set(_evidence_tokens(query))
ranked: list[tuple[float, str]] = []
for position, source in enumerate(sources.sources):
searchable = " ".join(
[source.title, source.heading, source.notes, *source.facts, *source.claim_ids, *source.decision_ids]
).casefold()
overlap = len(query_tokens.intersection(_evidence_tokens(searchable)))
decision_bonus = 2.0 if spec.intent in {"options", "decision", "decision_rationale", "tradeoffs"} and (source.decision_ids or "결정" in searchable or "이유" in searchable or "rationale" in searchable) else 0.0
canonical_bonus = {"canonical-project": 1.8, "canonical-concept": 1.5, "branch-note": 1.4, "official-doc": 1.0, "company-tech-blog": 0.5}.get(source.source_type, 0.0)
score = overlap + decision_bonus + canonical_bonus + min(max(source.priority, 0.0), 20.0) * 0.02 - position * 0.0001
ranked.append((score, source.id))
ranked.sort(key=lambda item: (-item[0], item[1]))
selected = [source_id for score, source_id in ranked if score > 0][:limit]
return selected or [source.id for source in sources.sources[:limit]]
def _evidence_tokens(text: str) -> set[str]:
import re
return {token.casefold() for token in re.findall(r"[A-Za-z][A-Za-z0-9_.:@/-]*|[가-힣]{2,}", text)}
def create_outline(brief: Brief, sources: SourcePack | None = None) -> Outline:
specs = STRUCTURE_SPECS[brief.document_type]
sources = sources or SourcePack()
source_ids = [source.id for source in sources.sources]
sections: list[OutlineSection] = []
for index, spec in enumerate(specs):
korean = brief.is_korean
must_include = list(spec.must_include_ko if korean else spec.must_include_en)
if index == 0:
must_include = unique_nonempty(
[*must_include, brief.reader_goal, brief.core_message, *brief.scope, *brief.non_scope]
)
if spec.intent in {"context_problem", "mechanism", "worked_example", "evidence_verification", "example", "architecture", "options", "decision"}:
must_include = unique_nonempty([*must_include, *brief.required_topics])
evidence_ids: list[str] = []
if source_ids and spec.intent not in {"route", "action", "next_steps", "related", "conclusion"}:
evidence_ids = _rank_evidence_ids(brief, spec, sources, limit=4)
decision_requirements = []
if spec.intent in {"options", "decision", "decision_rationale", "tradeoffs"}:
decision_requirements = (
["상황·제약", "선택", "선택 이유", "검토한 대안", "수용한 비용", "보완 가드레일"]
if korean
else ["context and constraint", "choice", "rationale", "alternative", "accepted cost", "guardrail"]
)
sections.append(
OutlineSection(
id=f"{index + 1:02d}-{slugify(spec.intent)}",
intent=spec.intent,
title=spec.title_ko if korean else spec.title_en,
reader_question=spec.question_ko if korean else spec.question_en,
purpose=spec.purpose_ko if korean else spec.purpose_en,
must_include=must_include,
evidence_ids=evidence_ids,
decision_requirements=decision_requirements,
transition_to_next=(
"이 답을 바탕으로 다음 독자 질문으로 자연스럽게 연결한다."
if korean
else "Use this answer to bridge explicitly to the next reader question."
),
)
)
notes = [
"Each section answers one reader question.",
"The order moves from reader goal to context, model, mechanism, evidence, limits, and action as applicable.",
"Required section intents are a contract; a model may refine wording but must not remove or reorder them.",
]
return Outline(title=brief.title, document_type=brief.document_type, sections=sections, planning_notes=notes)
def reconcile_outline(base: Outline, candidate: Outline, sources: SourcePack) -> Outline:
if candidate.document_type != base.document_type:
raise ValidationError("planned outline changed the document type")
candidate_by_intent = {section.intent: section for section in candidate.sections}
if len(candidate_by_intent) != len(candidate.sections):
raise ValidationError("planned outline contains duplicate intents")
reconciled: list[OutlineSection] = []
for base_section in base.sections:
proposed = candidate_by_intent.get(base_section.intent)
if proposed is None:
raise ValidationError(f"planned outline removed required intent: {base_section.intent}")
invalid_evidence = sorted(set(proposed.evidence_ids) - sources.ids)
if invalid_evidence:
raise ValidationError(
f"outline section {base_section.intent} references unknown sources: {', '.join(invalid_evidence)}"
)
reconciled.append(
OutlineSection(
id=base_section.id,
intent=base_section.intent,
title=proposed.title,
reader_question=proposed.reader_question,
purpose=proposed.purpose,
must_include=unique_nonempty([*base_section.must_include, *proposed.must_include]),
evidence_ids=unique_nonempty([*base_section.evidence_ids, *proposed.evidence_ids]),
decision_requirements=unique_nonempty(
[*base_section.decision_requirements, *proposed.decision_requirements]
),
transition_to_next=proposed.transition_to_next or base_section.transition_to_next,
)
)
return Outline(
title=candidate.title or base.title,
document_type=base.document_type,
sections=reconciled,
planning_notes=unique_nonempty([*base.planning_notes, *candidate.planning_notes]),
)