chore: 문서를 작성할 때 한국어의 표현 작성 스킬 추가 및 1인칭 관점의 글 작성 검증 테스트 추가
This commit is contained in:
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.
@@ -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()
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user