init: readme 작성 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 13:26:18 +09:00
parent 7fb4217f7c
commit c708cbcf9a
317 changed files with 24223 additions and 1 deletions
+4
View File
@@ -0,0 +1,4 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
@@ -0,0 +1,2 @@
#Fri Jul 24 13:25:57 KST 2026
gradle.version=9.2.0
+5
View File
@@ -0,0 +1,5 @@
# demo
## 빠른 시작
Run `./gradlew doesNotExist` to start, then read [the docs](docs/missing.md).
@@ -0,0 +1,7 @@
plugins {
java
}
tasks.register("test") {
description = "Run the tests"
}
Binary file not shown.
@@ -0,0 +1,2 @@
#Fri Jul 24 13:25:55 KST 2026
gradle.version=9.2.0
+14
View File
@@ -0,0 +1,14 @@
plugins {
java
}
group = "com.example.demo"
version = "0.1.0"
tasks.register("bootRun") {
description = "Run the application"
}
tasks.register("test") {
description = "Run the tests"
}
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
rootProject.name = "demo"
include(":core-domain")
+5
View File
@@ -0,0 +1,5 @@
public class App {
public static void main(String[] args) {
System.out.println("demo");
}
}
@@ -0,0 +1,2 @@
#Fri Jul 24 13:26:00 KST 2026
gradle.version=8.9
+11
View File
@@ -0,0 +1,11 @@
# Order Status
Order Status는 주문 처리 상태를 확인하는 백엔드 서비스 예제입니다.
## 빠른 시작
다음 명령으로 개발 서버를 시작합니다.
```bash
./gradlew bootRun
```
@@ -0,0 +1 @@
tasks.register("bootRun")
@@ -0,0 +1 @@
rootProject.name = "order-status"
+13
View File
@@ -0,0 +1,13 @@
# Slug Lite
Slug Lite는 제목 문자열을 URL 경로에 쓸 수 있는 짧은 식별자로 바꾸는 Python 라이브러리입니다.
## 빠른 시작
패키지의 `slugify` 함수를 가져와 문자열을 전달합니다.
```python
from slug_lite import slugify
slugify("Hello World")
```
+3
View File
@@ -0,0 +1,3 @@
[project]
name = "slug-lite"
version = "1.0.0"
+11
View File
@@ -0,0 +1,11 @@
# Weather Note
Weather Note는 오늘의 날씨 메모를 터미널에 저장하는 작은 명령줄 도구입니다.
## 빠른 시작
다음 명령을 실행하면 새 메모 파일의 경로가 출력됩니다.
```bash
weather-note
```
+6
View File
@@ -0,0 +1,6 @@
[project]
name = "weather-note"
version = "1.0.0"
[project.scripts]
weather-note = "weather_note:main"
+285
View File
@@ -0,0 +1,285 @@
import json
import pytest
from readme_harness.claims import bind_claims, blocks, reader_markdown
from readme_harness.core import apply_reviewed_readme, audit_readme, build_readme
from readme_harness.facts import extract_repository_facts
from readme_harness.quality import validate_reader_quality
from readme_harness.review import candidate_hash
def _repo(tmp_path):
repo = tmp_path / "demo"
repo.mkdir()
(repo / "pyproject.toml").write_text(
'[project]\nname = "demo"\nversion = "1.0.0"\n'
'[project.scripts]\ndemo = "demo:main"\n',
encoding="utf-8",
)
return repo
def test_facts_are_small_and_evidence_backed(tmp_path):
facts = extract_repository_facts(_repo(tmp_path))
assert facts["project-name"] == "demo"
assert facts["commands"][0]["command"] == "demo"
assert facts["facts"][0]["evidence"][0]["path"] == "pyproject.toml"
def test_reader_output_has_no_internal_markers_and_claims_are_external():
source = "# Demo\n\n설명입니다. <!-- claim-id: old -->\n\n## 실행\n\n`demo`를 실행합니다.\n"
visible = reader_markdown(source)
binding, errors = bind_claims(visible, [{"section": "실행", "block": 0, "fact-ids": ["F-1"]}])
assert "claim-id" not in visible
assert not errors
assert binding == [{
"section": "실행", "block": 0, "kind": "paragraph",
"text-hash": blocks(visible)[1]["text-hash"], "fact-ids": ["F-1"],
}]
def test_reader_quality_rejects_placeholders_and_duplicate_blocks():
markdown = (
"# Demo\n\n## 빠른 시작\n\n<repo-id>를 확인하세요.\n\n"
"이 문장은 독자에게 같은 내용을 반복해서 보여 주는 충분히 긴 문장입니다.\n\n"
"## 입력과 결과\n\n이 문장은 독자에게 같은 내용을 반복해서 보여 주는 충분히 긴 문장입니다.\n"
)
result = validate_reader_quality(markdown)
assert not result.ok
assert any("placeholder" in error for error in result.errors)
assert any("duplicated" in error for error in result.errors)
def test_build_writes_four_artifacts_and_never_overwrites_readme(tmp_path):
repo = _repo(tmp_path)
original = "# Old\n"
(repo / "README.md").write_text(original, encoding="utf-8")
draft = tmp_path / "draft.md"
draft.write_text("# Demo\n\n프로젝트 설명입니다.\n\n## 실행\n\n`demo`를 실행합니다.\n", encoding="utf-8")
facts = extract_repository_facts(repo)
facts["claim-bindings"] = [{"section": "실행", "block": 0, "fact-ids": ["F-PROJECT-NAME"]}]
result = build_readme(repo, draft, facts=facts)
assert result.status == "REVIEW_REQUIRED"
assert (repo / "README.md").read_text(encoding="utf-8") == original
assert sorted(path.name for path in (repo / ".readme-harness").iterdir()) == [
"README.generated.md", "README.patch", "facts.json", "validation.json",
]
def test_passing_review_is_fifth_artifact_and_explicit_apply(tmp_path):
repo = _repo(tmp_path)
(repo / "README.md").write_text("# Old\n", encoding="utf-8")
draft_text = "# Demo\n\n프로젝트 설명입니다.\n"
draft = tmp_path / "draft.md"
draft.write_text(draft_text, encoding="utf-8")
review = {
"verdict": "PASS",
"candidate-hash": candidate_hash(draft_text),
"findings": [],
}
result = build_readme(repo, draft, review=review)
assert result.status == "READY"
assert (repo / ".readme-harness" / "review.json").is_file()
assert (repo / "README.md").read_text(encoding="utf-8") == "# Old\n"
apply_reviewed_readme(repo)
assert (repo / "README.md").read_text(encoding="utf-8") == draft_text
def test_audit_produces_no_candidate_or_patch(tmp_path):
repo = _repo(tmp_path)
(repo / "README.md").write_text("# Demo\n\n설명입니다.\n", encoding="utf-8")
result = audit_readme(repo)
assert result.status == "PASS"
assert sorted(path.name for path in (repo / ".readme-harness").iterdir()) == [
"facts.json", "validation.json",
]
def _ready_build(tmp_path):
repo = _repo(tmp_path)
original = "# Old\n"
(repo / "README.md").write_text(original, encoding="utf-8")
draft = tmp_path / "ready.md"
generated = "# Demo\n\n프로젝트 설명입니다.\n"
draft.write_text(generated, encoding="utf-8")
review = {"verdict": "PASS", "candidate-hash": candidate_hash(generated), "findings": []}
result = build_readme(repo, draft, review=review)
assert result.status == "READY"
return repo, original
def test_generated_readme_tampering_blocks_apply(tmp_path):
repo, _ = _ready_build(tmp_path)
(repo / ".readme-harness" / "README.generated.md").write_text("# Tampered\n", encoding="utf-8")
with pytest.raises(ValueError, match="generated"):
apply_reviewed_readme(repo)
def test_review_tampering_blocks_apply(tmp_path):
repo, _ = _ready_build(tmp_path)
review_path = repo / ".readme-harness" / "review.json"
review = json.loads(review_path.read_text(encoding="utf-8"))
review["findings"].append({"severity": "minor", "section": "opening", "message": "changed"})
review_path.write_text(json.dumps(review), encoding="utf-8")
with pytest.raises(ValueError, match="review.json changed"):
apply_reviewed_readme(repo)
def test_target_readme_change_after_patch_blocks_apply(tmp_path):
repo, _ = _ready_build(tmp_path)
(repo / "README.md").write_text("# Human edit\n", encoding="utf-8")
with pytest.raises(ValueError, match="target README changed"):
apply_reviewed_readme(repo)
def test_repository_change_after_validation_blocks_apply(tmp_path):
repo, _ = _ready_build(tmp_path)
(repo / "new-source.py").write_text("VALUE = 1\n", encoding="utf-8")
with pytest.raises(ValueError, match="repository changed"):
apply_reviewed_readme(repo)
def test_apply_target_outside_repository_is_rejected(tmp_path):
repo, _ = _ready_build(tmp_path)
with pytest.raises(ValueError, match="inside the repository"):
apply_reviewed_readme(repo, target="../outside.md")
def test_apply_symlink_to_outside_repository_is_rejected(tmp_path):
repo = _repo(tmp_path)
outside = tmp_path / "outside.md"
outside.write_text("outside\n", encoding="utf-8")
(repo / "README.link").symlink_to(outside)
draft = tmp_path / "draft.md"
draft.write_text("# Demo\n", encoding="utf-8")
with pytest.raises(ValueError, match="inside the repository"):
build_readme(repo, draft, target_readme="README.link")
def test_secret_in_generated_readme_fails_without_patch(tmp_path):
repo = _repo(tmp_path)
draft = tmp_path / "secret.md"
draft.write_text("# Demo\n\nJWT_SECRET=supersecretvalue123\n", encoding="utf-8")
result = build_readme(repo, draft)
assert result.status == "FAILED"
assert not (repo / ".readme-harness" / "README.patch").exists()
@pytest.mark.parametrize("secret", [
"JWT_SECRET=supersecretvalue123",
"postgres://user:hunter2@db:5432/app",
"-----BEGIN RSA PRIVATE KEY-----",
"AKIAABCDEFGHIJKLMNOP",
])
def test_supported_secret_patterns_all_fail(tmp_path, secret):
repo = _repo(tmp_path)
draft = tmp_path / "secret-pattern.md"
draft.write_text(f"# Demo\n\n{secret}\n", encoding="utf-8")
result = build_readme(repo, draft)
assert result.status == "FAILED"
assert any("secrets" in error for error in result.errors)
def test_broken_relative_link_fails_without_patch(tmp_path):
repo = _repo(tmp_path)
draft = tmp_path / "broken-link.md"
draft.write_text("# Demo\n\n[없는 문서](docs/missing.md)\n", encoding="utf-8")
result = build_readme(repo, draft)
assert result.status == "FAILED"
assert any("path" in error for error in result.errors)
assert not (repo / ".readme-harness" / "README.patch").exists()
def test_unknown_command_fails_without_patch(tmp_path):
repo = _repo(tmp_path)
draft = tmp_path / "unknown-command.md"
draft.write_text("# Demo\n\n```bash\ndemo-does-not-exist\n```\n", encoding="utf-8")
result = build_readme(repo, draft)
assert result.status == "FAILED"
assert any("command" in error for error in result.errors)
assert not (repo / ".readme-harness" / "README.patch").exists()
@pytest.mark.parametrize("markdown", [
"# Demo\n\n```bash\ndemo\n",
"# Demo\n\n### Skipped heading\n",
"# Demo\n\n## Same\n\n## Same\n",
"# Demo\n\n![](image.png)\n",
])
def test_markdown_safety_failures_block_patch(tmp_path, markdown):
repo = _repo(tmp_path)
if "image.png" in markdown:
(repo / "image.png").write_bytes(b"image")
draft = tmp_path / "bad-markdown.md"
draft.write_text(markdown, encoding="utf-8")
result = build_readme(repo, draft)
assert result.status == "FAILED"
assert any("markdown" in error for error in result.errors)
assert not (repo / ".readme-harness" / "README.patch").exists()
def test_repository_metadata_symlink_is_not_followed(tmp_path):
repo = tmp_path / "linked"
repo.mkdir()
outside = tmp_path / "outside.toml"
outside.write_text('[project]\nname = "outside-secret-name"\n', encoding="utf-8")
(repo / "pyproject.toml").symlink_to(outside)
facts = extract_repository_facts(repo)
assert facts["project-name"] == "linked"
assert "outside-secret-name" not in json.dumps(facts)
def test_failed_quality_review_cannot_leave_or_apply_patch(tmp_path):
repo = _repo(tmp_path)
(repo / "README.md").write_text("# Old\n", encoding="utf-8")
draft_text = "# Demo\n\n프로젝트 설명입니다.\n"
draft = tmp_path / "draft.md"
draft.write_text(draft_text, encoding="utf-8")
review = {
"verdict": "NEEDS_FIX",
"candidate-hash": candidate_hash(draft_text),
"findings": [{"severity": "major", "section": "opening", "message": "첫 행동이 없습니다."}],
}
result = build_readme(repo, draft, review=review)
assert result.status == "NEEDS_REVISION"
assert not (repo / ".readme-harness" / "README.patch").exists()
with pytest.raises(ValueError, match="not READY"):
apply_reviewed_readme(repo)
def test_target_is_unchanged_until_explicit_apply(tmp_path):
repo, original = _ready_build(tmp_path)
assert (repo / "README.md").read_text(encoding="utf-8") == original
apply_reviewed_readme(repo)
assert (repo / "README.md").read_text(encoding="utf-8") != original
+21
View File
@@ -0,0 +1,21 @@
from pathlib import Path
import pytest
from readme_harness.core import audit_readme
QUALITY_ROOT = Path(__file__).parent / "quality"
@pytest.mark.parametrize("fixture", ["python-cli", "backend-service", "library"])
def test_small_quality_fixture(fixture, tmp_path):
repo = QUALITY_ROOT / fixture
readme = (repo / "README.md").read_text(encoding="utf-8")
result = audit_readme(repo, output_dir=tmp_path / fixture)
assert result.status == "PASS"
assert len(readme) < 2_000
assert "## 빠른 시작" in readme
assert not any(term in readme for term in ("claim-id", "QUALITY_REVIEWED", "visual-plan", "run-id"))

Some files were not shown because too many files have changed in this diff Show More