chore: 문서를 작성할 때 한국어의 표현 작성 스킬 추가 및 1인칭 관점의 글 작성 검증 테스트 추가

This commit is contained in:
DongHyeonka
2026-07-29 16:48:03 +09:00
parent c39406bbdd
commit 41501b5d06
520 changed files with 95494 additions and 2231 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
View File
@@ -25,6 +25,26 @@ class CliTests(unittest.TestCase):
self.assertTrue((workspace / "pipeline.mock.json").is_file())
self.assertIsInstance(json.loads((workspace / "brief.json").read_text(encoding="utf-8")), dict)
def test_collect_builds_local_evidence_pack(self) -> None:
root = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as temp:
output = Path(temp) / "sources.json"
with redirect_stdout(StringIO()):
code = main([
"collect",
"--root", str(root / "examples/corpus/llm-wiki-mini"),
"--query", "application-core Spring DI 수동 등록 이유",
"--top-k", "6",
"--output", str(output),
])
self.assertEqual(code, 0)
data = json.loads(output.read_text(encoding="utf-8"))
self.assertTrue(data["sources"])
self.assertEqual(
data["sources"][0]["path"],
"raw/branch-notes/feature-application-port-usecase-contract.md",
)
if __name__ == "__main__":
unittest.main()
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from claridoc.corpus import build_query_from_brief, collect_sources
from claridoc.models import Brief
from claridoc.utils import read_json
ROOT = Path(__file__).resolve().parents[1]
CORPUS = ROOT / "examples" / "corpus" / "llm-wiki-mini"
class CorpusTests(unittest.TestCase):
def test_decision_rationale_chunk_ranks_first(self) -> None:
pack = collect_sources(
CORPUS,
"application-core Spring DI 수동 Configuration 보일러플레이트 선택 이유 대안 가드레일",
top_k=8,
)
self.assertGreaterEqual(len(pack.sources), 4)
first = pack.sources[0]
self.assertEqual(first.path, "raw/branch-notes/feature-application-port-usecase-contract.md")
self.assertEqual(first.heading, "결정 사항")
self.assertIn("수동 등록", first.facts[0])
self.assertIn("D13", first.decision_ids)
def test_paths_are_repository_relative_and_line_ranges_are_recorded(self) -> None:
pack = collect_sources(CORPUS, "application-core 경계 검증", top_k=6)
self.assertTrue(pack.sources)
for source in pack.sources:
self.assertFalse(source.path.startswith("/"))
self.assertTrue(source.url.startswith("repo:///"))
self.assertIsNotNone(source.line_start)
self.assertIsNotNone(source.line_end)
self.assertGreaterEqual(source.line_end or 0, source.line_start or 0)
def test_brief_query_retrieves_rationale_and_current_state(self) -> None:
brief = Brief.from_dict(
read_json(ROOT / "examples" / "briefs" / "application-core-spring-di-blog.json")
)
pack = collect_sources(CORPUS, build_query_from_brief(brief), top_k=12)
paths = {source.path for source in pack.sources}
self.assertIn("raw/branch-notes/feature-application-port-usecase-contract.md", paths)
self.assertIn("wiki/projects/ca-tmpl/clean-architecture-package-layout.md", paths)
def test_heading_without_body_is_not_collected_as_evidence(self) -> None:
pack = collect_sources(CORPUS, "Spring component stereotype scanning", top_k=20)
self.assertFalse(
any(
source.heading == "Spring component stereotype and scanning notes"
and source.facts == ["# Spring component stereotype and scanning notes"]
for source in pack.sources
)
)
def test_missing_default_directories_are_allowed(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
(root / "wiki/projects").mkdir(parents=True)
(root / "wiki/projects/example.md").write_text(
"# Example\n\n## 결정\n\n선택 이유와 대안을 기록한다.\n",
encoding="utf-8",
)
pack = collect_sources(root, "선택 이유 대안")
self.assertEqual(len(pack.sources), 1)
if __name__ == "__main__":
unittest.main()
+94 -3
View File
@@ -4,12 +4,12 @@ import unittest
from pathlib import Path
from claridoc.lint import lint_document
from claridoc.models import ProviderSpec, Severity, SourcePack
from claridoc.models import Brief, ProviderSpec, Severity, SourcePack
from claridoc.prompts import drafting_prompt
from claridoc.providers.base import ProviderRequest
from claridoc.providers.mock import MockProvider
from claridoc.structures import create_outline
from tests.helpers import make_brief, make_sources
from tests.helpers import brief_dict, make_brief, make_sources
class LintTests(unittest.TestCase):
@@ -40,7 +40,7 @@ class LintTests(unittest.TestCase):
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
report = lint_document(text.replace("[S1]", "[S404]"), brief, outline, sources)
report = lint_document(text + "\n\nUnsupported marker [S404].\n", brief, outline, sources)
self.assertIn("EVD001", {issue.code for issue in report.issues})
def test_non_s_prefixed_source_id_is_recognized(self) -> None:
@@ -89,6 +89,97 @@ class LintTests(unittest.TestCase):
controlled_report = lint_document(controlled, brief, outline, sources)
self.assertNotIn("SAFE001", {issue.code for issue in controlled_report.issues})
def test_reader_facing_meta_and_internal_markers_are_rejected(self) -> None:
brief = make_brief()
sources = make_sources()
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
text += "\n제공된 근거 팩은 다음 사실을 확인 대상으로 제시한다. [S1]\n"
report = lint_document(text, brief, outline, sources)
codes = {issue.code for issue in report.issues}
self.assertIn("META001", codes)
self.assertIn("EVD007", codes)
def test_access_date_boilerplate_is_rejected(self) -> None:
brief = make_brief()
sources = make_sources()
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
text += "\nThe example is checked 2026-07-23 as of this document.\n"
report = lint_document(text, brief, outline, sources)
self.assertIn("DATE001", {issue.code for issue in report.issues})
def test_choice_without_reason_is_rejected(self) -> None:
brief = make_brief()
sources = make_sources()
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
text += "\nWe intentionally selected Framework X.\n"
report = lint_document(text, brief, outline, sources)
self.assertIn("RAT001", {issue.code for issue in report.issues})
def test_formulaic_korean_ordinal_paragraphs_are_flagged(self) -> None:
data = brief_dict()
data["title"] = "기술적 선택을 설명하는 글"
data["language"] = "ko-KR"
data["reader_goal"] = "안전한 구현 방식을 선택한다"
data["core_message"] = "기술 선택은 문제와 비용을 함께 설명해야 한다."
data["constraints"]["style_profile"] = "woowahan_tech_blog_ko"
brief = Brief.from_dict(data)
sources = make_sources()
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
text += (
"\n첫 번째 제약은 모듈 소유권이 나뉜다는 점이다.\n\n"
"두 번째 제약은 배포 시간을 바꿀 수 없다는 점이다.\n\n"
"세 번째 제약은 운영 지표가 부족하다는 점이다.\n"
)
report = lint_document(text, brief, outline, sources)
self.assertIn("STYLE001", {issue.code for issue in report.issues})
self.assertEqual(report.metrics["formulaic_ordinal_opening_count"], 3)
def test_numbered_procedure_is_not_formulaic_ordinal_prose(self) -> None:
data = brief_dict()
data["title"] = "기술적 선택을 설명하는 글"
data["language"] = "ko-KR"
data["reader_goal"] = "안전한 구현 방식을 선택한다"
data["core_message"] = "기술 선택은 문제와 비용을 함께 설명해야 한다."
data["constraints"]["style_profile"] = "woowahan_tech_blog_ko"
brief = Brief.from_dict(data)
sources = make_sources()
outline = create_outline(brief, sources)
provider = MockProvider(ProviderSpec(provider="mock"))
text = provider.generate(ProviderRequest("draft", drafting_prompt(brief, outline, sources), Path.cwd())).text
text += "\n1. 현재 상태를 확인한다.\n2. 변경한다.\n3. 결과를 검증한다.\n"
report = lint_document(text, brief, outline, sources)
self.assertNotIn("STYLE001", {issue.code for issue in report.issues})
def test_golden_application_core_example_has_no_material_lint_issue(self) -> None:
root = Path(__file__).resolve().parents[1]
from claridoc.corpus import build_query_from_brief, collect_sources
from claridoc.utils import read_json
brief = Brief.from_dict(read_json(root / "examples/briefs/application-core-spring-di-blog.json"))
sources = collect_sources(
root / "examples/corpus/llm-wiki-mini",
build_query_from_brief(brief),
)
outline = create_outline(brief, sources)
text = (root / "examples/golden/application-core-spring-di-boundary.md").read_text(encoding="utf-8")
report = lint_document(text, brief, outline, sources)
material = [issue for issue in report.issues if issue.severity in {Severity.BLOCKER, Severity.ERROR}]
self.assertEqual(material, [])
self.assertNotIn("[S1]", text)
self.assertNotIn("제공된 근거 팩", text)
self.assertNotIn("2026-07-23 기준", text)
self.assertNotIn("STYLE001", {issue.code for issue in report.issues})
self.assertNotIn("첫 번째 제약은", text)
self.assertIn("수동 선언하는 반복", text)
if __name__ == "__main__":
unittest.main()
+4
View File
@@ -33,6 +33,8 @@ class PipelineTests(unittest.TestCase):
self.assertTrue(result.final_path.is_file())
self.assertTrue(result.report_path.is_file())
self.assertTrue(result.manifest_path.is_file())
self.assertTrue((output / "final" / "provenance.md").is_file())
self.assertTrue((output / "final" / "evidence-map.json").is_file())
run_data = json.loads((output / "run.json").read_text(encoding="utf-8"))
self.assertTrue(run_data["passed"])
self.assertTrue(any("deterministic mocks" in warning for warning in result.warnings))
@@ -43,6 +45,8 @@ class PipelineTests(unittest.TestCase):
paths = {item["path"] for item in manifest["files"]}
self.assertIn("final/document.md", paths)
self.assertIn("provider-events.jsonl", paths)
self.assertIn("final/provenance.md", paths)
self.assertIn("final/evidence-map.json", paths)
self.assertNotIn("manifest.json", paths)
for item in manifest["files"]:
artifact = output / item["path"]
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
import unittest
from claridoc.models import Brief, LintReport
from claridoc.prompts import drafting_prompt, review_prompt, revision_prompt
from claridoc.structures import create_outline
from tests.helpers import brief_dict, make_sources
class PromptTests(unittest.TestCase):
def _korean_blog(self) -> Brief:
data = brief_dict()
data["title"] = "기술적 선택을 설명하는 글"
data["language"] = "ko-KR"
data["reader_goal"] = "안전한 구현 방식을 선택한다"
data["core_message"] = "기술 선택은 문제와 비용을 함께 설명해야 한다."
data["constraints"]["style_profile"] = "woowahan_tech_blog_ko"
return Brief.from_dict(data)
def test_korean_blog_prompts_separate_information_structure_from_sentence_form(self) -> None:
brief = self._korean_blog()
sources = make_sources()
outline = create_outline(brief, sources)
lint_report = LintReport(score=100.0, word_count=0, issues=[], metrics={})
draft = drafting_prompt(brief, outline, sources)
review = review_prompt(brief, outline, sources, "# draft", lint_report, "editor")
revision = revision_prompt(brief, outline, sources, "# draft", lint_report, [])
self.assertIn("semantic order, never as a sentence template", draft)
self.assertIn("첫 번째 제약은", draft)
self.assertIn("Information-architecture labels must not leak", review)
self.assertIn("real ordered sequences", review)
self.assertIn("Remove repeated ordinal sentence scaffolding", revision)
self.assertIn("real procedure, method, layer, or figure", revision)
if __name__ == "__main__":
unittest.main()