Files
document-haness/tests/test_lint_document.py
T

797 lines
28 KiB
Python

from __future__ import annotations
import hashlib
import json
import os
import shutil
import sys
import time
from pathlib import Path
import pytest
from helpers import FIXTURES, read_json, run_cli, write_json
CANONICAL_SCRIPTS = (
Path(__file__).resolve().parents[1] / "skills" / "technical-doc-flow" / "scripts"
)
sys.path.insert(0, str(CANONICAL_SCRIPTS))
import lint_document as lint_document_module # noqa: E402
from lint_document import ( # noqa: E402
bounded_change_rate,
github_slug,
markdown_inline_links,
mask_markdown_metadata,
parse_headings,
protected_items,
)
def lint_fixture(tmp_path: Path, name: str, *extra: object):
base = FIXTURES / name
output = tmp_path / f"{name}-lint.json"
result = run_cli(
"lint_document.py",
"--document",
base / "document.md",
"--logic-map",
base / "logic-map.json",
"--term-ledger",
base / "term-ledger.json",
"--reader-contract",
base / "reader-contract.json",
"--output",
output,
*extra,
)
return result, read_json(output)
def lint_main_args(output: Path, *, document: Path | None = None) -> list[str]:
base = FIXTURES / "good"
return [
"--document",
str(document or base / "document.md"),
"--logic-map",
str(base / "logic-map.json"),
"--term-ledger",
str(base / "term-ledger.json"),
"--reader-contract",
str(base / "reader-contract.json"),
"--output",
str(output),
]
def test_good_golden_passes_without_findings(tmp_path: Path) -> None:
result, report = lint_fixture(tmp_path, "good")
assert result.returncode == 0, result.stderr
assert report["verdict"] == "pass"
assert report["findings"] == []
rules_path = CANONICAL_SCRIPTS.parent / "config" / "quality-rules.json"
assert report["rules_sha256"] == hashlib.sha256(
rules_path.read_bytes()
).hexdigest()
def test_bad_golden_covers_all_declared_failure_modes(tmp_path: Path) -> None:
result, report = lint_fixture(tmp_path, "bad")
expected = set(json.loads((FIXTURES / "bad" / "expected-rule-ids.json").read_text()))
actual = {finding["rule_id"] for finding in report["findings"]}
assert result.returncode == 1
assert expected <= actual
assert len(expected) >= 8
def test_malformed_alias_returns_input_error_report(tmp_path: Path) -> None:
ledger = read_json(FIXTURES / "good" / "term-ledger.json")
ledger["terms"][0]["aliases"] = [123]
ledger_path = tmp_path / "ledger.json"
output = tmp_path / "lint.json"
write_json(ledger_path, ledger)
result = run_cli(
"lint_document.py",
"--document",
FIXTURES / "good" / "document.md",
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
ledger_path,
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 2
assert read_json(output)["verdict"] == "input_error"
def test_schema_invalid_contract_cannot_receive_a_pass_report(tmp_path: Path) -> None:
reader = read_json(FIXTURES / "good" / "reader-contract.json")
reader["document_kind"] = "garbage"
reader_path = tmp_path / "reader-contract.json"
output = tmp_path / "lint.json"
write_json(reader_path, reader)
result = run_cli(
"lint_document.py",
"--document",
FIXTURES / "good" / "document.md",
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
reader_path,
"--output",
output,
)
assert result.returncode == 2
assert read_json(output)["verdict"] == "input_error"
assert "schema 위반" in read_json(output)["findings"][0]["message"]
def test_fence_with_trailing_text_is_not_a_closer(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
insertion = "```text\n```anything\n# 코드 안 가짜 제목\n```\n\n"
document = document.replace("## 문제", insertion + "## 문제")
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 0, read_json(output)["findings"]
def test_setext_heading_is_supported_and_comment_heading_is_ignored(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document = document.replace(
"# 캐시 실패를 줄이는 설명",
"캐시 실패를 줄이는 설명\n========================",
)
document = document.replace("## 문제", "<!--\n# 주석 속 가짜 제목\n-->\n\n## 문제")
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 0, read_json(output)["findings"]
def test_list_items_are_not_counted_as_one_oversized_sentence_paragraph(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n" + "\n".join(
f"{index}. 독자가 확인할 항목입니다." for index in range(1, 9)
)
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 0
assert "DOC-P002" not in {item["rule_id"] for item in read_json(output)["findings"]}
def test_fidelity_uses_typed_exact_multisets(tmp_path: Path) -> None:
baseline = tmp_path / "baseline.md"
baseline.write_text(
'# 기준\n\n```text\nalpha\n```\n\n`foo` https://example.com/api 500ms "정확한 인용"\n',
encoding="utf-8",
)
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += '\n`foobar` https://example.com/api-v2 5000ms "정확한 인용문"\n'
document_path = tmp_path / "document.md"
document_path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--baseline",
baseline,
"--output",
output,
)
report = read_json(output)
rule_ids = {item["rule_id"] for item in report["findings"]}
assert result.returncode == 1
assert {"DOC-F001", "DOC-F002", "DOC-F003", "DOC-F004", "DOC-F005"} <= rule_ids
assert report["fidelity"]["missing"] == 5
def test_fidelity_binds_values_to_nearby_meaning_instead_of_only_multisets(
tmp_path: Path,
) -> None:
baseline = tmp_path / "baseline.md"
baseline.write_text(
"# 기준\n\n"
"A 코드 `alpha`, B 코드 `beta`.\n"
"A 링크 https://example.com/a, B 링크 https://example.com/b.\n"
"A 지연 10ms, B 지연 20ms.\n"
'A 문구 "첫 번째", B 문구 "두 번째".\n',
encoding="utf-8",
)
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += (
"\nA 코드 `beta`, B 코드 `alpha`.\n"
"A 링크 https://example.com/b, B 링크 https://example.com/a.\n"
"A 지연 20ms, B 지연 10ms.\n"
'A 문구 "두 번째", B 문구 "첫 번째".\n'
)
document_path = tmp_path / "document.md"
document_path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--baseline",
baseline,
"--output",
output,
)
report = read_json(output)
rule_ids = {item["rule_id"] for item in report["findings"]}
assert result.returncode == 1
assert {"DOC-F002", "DOC-F003", "DOC-F004", "DOC-F005"} <= rule_ids
def test_number_units_are_protected_when_joined_to_korean_particles() -> None:
inventory = protected_items("약10ms를 기다리고 20초동안 관찰하며 30msfoo는 식별자로 둔다.")
assert inventory["number_unit"] == ["약10ms", "20초"]
def test_both_endpoint_unit_ranges_are_single_ordered_protected_values() -> None:
inventory = protected_items("지연 5ms-10ms, 비율 5%-10%, 비호환 5ms-10s.")
assert inventory["number_range"] == ["5ms-10ms", "5%-10%"]
assert inventory["number_unit"] == ["5ms", "10s"]
def test_reader_visible_multiline_and_entity_quotes_exclude_metadata_and_code() -> None:
inventory = protected_items(
'[링크](dest "metadata") `"inline"`\n'
'<pre>\n"raw code"\n</pre>\n'
'보이는 &quot;첫 줄\n둘째 줄&#34;과 '
'&ldquo;굽은 인용&#x201D;이다.\n'
)
assert inventory["quote"] == ['"첫 줄\n둘째 줄"', '“굽은 인용”']
@pytest.mark.parametrize("thematic_break", ["_ _ _", "* * *"])
def test_spaced_thematic_break_terminates_lazy_blockquote_inventory(
thematic_break: str,
) -> None:
inventory = protected_items(
f"> 첫 줄\n게으른 이어쓰기\n{thematic_break}\n분리된 본문\n"
)
assert inventory["quote"] == ["첫 줄\n게으른 이어쓰기"]
assert protected_items(
f'"첫 줄\n{thematic_break}\n분리된 둘째 줄"'
)["quote"] == []
def test_common_absolute_uri_schemes_are_protected() -> None:
uris = [
"http://ApiHost.test/HttpToken",
"https://ApiHost.test/HttpsToken",
"ftp://FileHost.test/FtpToken",
"ftps://FileHost.test/FtpsToken",
"file:///tmp/FileToken",
"mailto:UserToken@example.com",
"ssh://GitHost.test/SshToken",
"git://GitHost.test/GitToken",
]
assert protected_items("\n".join(uris))["url"] == uris
def test_inline_link_parser_always_progresses_and_supports_multiline_title() -> None:
assert markdown_inline_links("[x](dest") == []
links = markdown_inline_links('[go](#missing "long\n title")')
assert len(links) == 1
assert links[0][2:4] == (5, 13)
assert mask_markdown_metadata("[x\n\ny](TODO)") == "[x\n\ny](TODO)"
def test_heading_entities_require_semicolon_and_slugs_are_globally_unique() -> None:
assert github_slug("A &copy B") == "a-copy-b"
assert github_slug("A &copy; B") == "a-b"
assert [item.slug for item in parse_headings("## foo\n## foo-1\n## foo\n")] == [
"foo",
"foo-1",
"foo-2",
]
def test_multiline_inline_code_allows_non_interrupting_ordered_marker() -> None:
inventory = protected_items("text `a\n2. TODO\nc` tail")
assert inventory["inline_code"] == ["a 2. TODO c"]
def test_finalizer_change_rate_detects_nonlocal_reorder_and_is_linear_enough() -> None:
paragraphs = [f"문단 {index}에는 고유한 설명과 값 {index}가 있다." for index in range(5000)]
before = "\n\n".join(paragraphs)
after = "\n\n".join(reversed(paragraphs))
started = time.monotonic()
rate = bounded_change_rate(before, after)
elapsed = time.monotonic() - started
assert rate > 0.15
assert elapsed < 5.0
def test_finalizer_change_rate_allows_one_local_word_edit() -> None:
before = "이 문서는 캐시 장애의 원인과 검증 범위를 독자에게 차례로 설명한다."
after = "이 문서는 캐시 장애의 원인과 검증 한계를 독자에게 차례로 설명한다."
assert bounded_change_rate(before, after) <= 0.15
def test_finalizer_change_rate_detects_reorder_even_when_every_paragraph_is_edited() -> None:
paragraphs = [
f"문단 {index}은 캐시 동작 {index}와 고유한 검증 범위를 설명한다."
for index in range(120)
]
before = "\n\n".join(paragraphs)
after = "\n\n".join(f"{item} 보충" for item in reversed(paragraphs))
assert bounded_change_rate(before, after) > 0.15
def test_draft_baseline_hash_and_change_rate_are_reported(tmp_path: Path) -> None:
draft = tmp_path / "draft.md"
draft.write_text("# 완전히 다른 초안\n\n" + "다른 문장. " * 100, encoding="utf-8")
result, report = lint_fixture(tmp_path, "good", "--draft-baseline", draft)
assert result.returncode == 1
assert "FNL-001" in {item["rule_id"] for item in report["findings"]}
assert report["fidelity"]["draft_baseline"]["sha256"]
assert report["fidelity"]["finalization_change_rate"] > 0.15
def test_unregistered_candidate_is_a_default_hard_gate(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n등록하지 않은 NewCacheMode 후보가 있다.\n"
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
common = (
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert run_cli("lint_document.py", *common).returncode == 1
assert "DOC-T007" in {
item["rule_id"] for item in read_json(output)["findings"]
}
def test_fail_on_warning_changes_exit_code(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n\n" + ("긴" * 901) + "\n"
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
common = (
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert run_cli("lint_document.py", *common).returncode == 0
assert "DOC-P001" in {
item["rule_id"] for item in read_json(output)["findings"]
}
assert run_cli("lint_document.py", *common, "--fail-on", "warning").returncode == 1
def test_definition_window_threshold_is_actually_enforced(tmp_path: Path) -> None:
rules_path = (
Path(__file__).resolve().parents[1]
/ "skills"
/ "technical-doc-flow"
/ "config"
/ "quality-rules.json"
)
rules = read_json(rules_path)
rules["thresholds"]["term"]["definition_window_chars"] = 10
strict_rules = tmp_path / "quality-rules.json"
write_json(strict_rules, rules)
result, report = lint_fixture(tmp_path, "good", "--rules", strict_rules)
assert result.returncode == 1
assert "DOC-T001" in {item["rule_id"] for item in report["findings"]}
def test_definition_window_also_accepts_plain_definition_after_first_name(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document = document.replace(
"같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 되는 성질을 멱등성이라고 한다.",
"멱등성이라는 이름을 쓴다. 같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 되는 성질을 뜻한다.",
)
ledger = read_json(FIXTURES / "good" / "term-ledger.json")
ledger["terms"][0]["first_use"] = "멱등성이라는 이름을 쓴다."
document_path = tmp_path / "document.md"
ledger_path = tmp_path / "term-ledger.json"
output = tmp_path / "lint.json"
document_path.write_text(document, encoding="utf-8")
write_json(ledger_path, ledger)
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
ledger_path,
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 0, read_json(output)["findings"]
def test_hidden_html_comment_cannot_satisfy_term_first_use(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
first_use = (
"같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 되는 성질을 "
"멱등성이라고 한다."
)
document = document.replace(
first_use,
f"<!-- {first_use} -->\n\n멱등성을 사용한다.",
)
path = tmp_path / "document.md"
path.write_text(document, encoding="utf-8")
output = tmp_path / "lint.json"
result = run_cli(
"lint_document.py",
"--document",
path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 1
assert "DOC-T001" in {
item["rule_id"] for item in read_json(output)["findings"]
}
def test_ledger_term_must_actually_appear_and_first_use_must_name_it(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
original = (
"같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 되는 성질을 "
"멱등성이라고 한다."
)
generic = "같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같게 되는 성질을 설명한다."
document = document.replace(original, generic).replace("멱등성", "같은 결과 성질")
ledger = read_json(FIXTURES / "good" / "term-ledger.json")
ledger["terms"][0]["first_use"] = generic
document_path = tmp_path / "document.md"
ledger_path = tmp_path / "term-ledger.json"
output = tmp_path / "lint.json"
document_path.write_text(document, encoding="utf-8")
write_json(ledger_path, ledger)
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
ledger_path,
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 1
messages = [
item["message"]
for item in read_json(output)["findings"]
if item["rule_id"] == "DOC-T001"
]
assert any("실제로 등장하지" in message for message in messages)
assert any("first_use에 정식 용어" in message for message in messages)
@pytest.mark.parametrize(
"protected_name",
["document.md", "logic-map.json", "term-ledger.json", "reader-contract.json"],
)
def test_lint_output_cannot_overwrite_inputs(tmp_path: Path, protected_name: str) -> None:
base = FIXTURES / "good"
protected = tmp_path / protected_name
shutil.copyfile(base / protected_name, protected)
before = protected.read_bytes()
paths = {
"document.md": protected if protected_name == "document.md" else base / "document.md",
"logic-map.json": protected if protected_name == "logic-map.json" else base / "logic-map.json",
"term-ledger.json": protected if protected_name == "term-ledger.json" else base / "term-ledger.json",
"reader-contract.json": protected if protected_name == "reader-contract.json" else base / "reader-contract.json",
}
result = run_cli(
"lint_document.py",
"--document",
paths["document.md"],
"--logic-map",
paths["logic-map.json"],
"--term-ledger",
paths["term-ledger.json"],
"--reader-contract",
paths["reader-contract.json"],
"--output",
protected,
)
assert result.returncode == 2
assert protected.read_bytes() == before
def test_lint_existing_output_requires_same_tool_ownership(tmp_path: Path) -> None:
output = tmp_path / "report.json"
output.write_text('{"tool":"verify_run","sentinel":true}\n', encoding="utf-8")
before = output.read_bytes()
result = run_cli(
"lint_document.py",
"--document",
FIXTURES / "good" / "document.md",
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 2
assert output.read_bytes() == before
def test_lint_malformed_same_tool_output_is_not_overwritten(tmp_path: Path) -> None:
output = tmp_path / "report.json"
output.write_text('{"tool":"lint_document","sentinel":true}\n', encoding="utf-8")
before = output.read_bytes()
result = run_cli(
"lint_document.py",
"--document",
FIXTURES / "good" / "document.md",
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
assert result.returncode == 2
assert output.read_bytes() == before
@pytest.mark.parametrize("raced_entry", ["file", "symlink", "hardlink"])
def test_lint_publish_rejects_entry_created_after_preflight(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, raced_entry: str
) -> None:
output = tmp_path / "report.json"
protected = tmp_path / "document.md"
shutil.copyfile(FIXTURES / "good" / "document.md", protected)
protected_before = protected.read_bytes()
original_publish = lint_document_module.publish_report_json
def publish_after_race(prepared, value):
if raced_entry == "file":
prepared.path.write_bytes(b"unrelated sentinel\n")
elif raced_entry == "symlink":
prepared.path.symlink_to(protected)
else:
os.link(protected, prepared.path)
return original_publish(prepared, value)
monkeypatch.setattr(
lint_document_module,
"publish_report_json",
publish_after_race,
)
result = lint_document_module.main(lint_main_args(output, document=protected))
assert result == 2
assert protected.read_bytes() == protected_before
if raced_entry == "file":
assert output.read_bytes() == b"unrelated sentinel\n"
elif raced_entry == "symlink":
assert output.is_symlink()
assert output.resolve() == protected.resolve()
else:
assert not output.is_symlink()
assert os.path.samefile(output, protected)
def test_lint_publish_rejects_owned_report_changed_after_preflight(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
output = tmp_path / "report.json"
assert lint_document_module.main(lint_main_args(output)) == 0
original_publish = lint_document_module.publish_report_json
changed = output.read_bytes() + b"\n"
original_inode = output.stat().st_ino
def publish_after_race(prepared, value):
prepared.path.write_bytes(changed)
assert prepared.path.stat().st_ino == original_inode
return original_publish(prepared, value)
monkeypatch.setattr(
lint_document_module,
"publish_report_json",
publish_after_race,
)
result = lint_document_module.main(lint_main_args(output))
assert result == 2
assert output.stat().st_ino == original_inode
assert output.read_bytes() == changed
def test_lint_can_conditionally_refresh_owned_report(tmp_path: Path) -> None:
output = tmp_path / "report.json"
assert lint_document_module.main(lint_main_args(output)) == 0
assert lint_document_module.main(lint_main_args(output)) == 0
assert read_json(output)["tool"] == "lint_document"
def test_fidelity_does_not_count_values_hidden_in_html_comments(
tmp_path: Path,
) -> None:
baseline = tmp_path / "baseline.md"
baseline.write_text(
'# 기준\n\n`SecretToken987` https://example.com/unique-cache 731ms "고유 인용문"\n',
encoding="utf-8",
)
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += (
'\n<!-- `SecretToken987` https://example.com/unique-cache 731ms "고유 인용문" -->\n'
)
document_path = tmp_path / "document.md"
output = tmp_path / "lint.json"
document_path.write_text(document, encoding="utf-8")
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
FIXTURES / "good" / "term-ledger.json",
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--baseline",
baseline,
"--output",
output,
)
report = read_json(output)
assert result.returncode == 1
assert {"DOC-F002", "DOC-F003", "DOC-F004", "DOC-F005"} <= {
item["rule_id"] for item in report["findings"]
}
assert report["fidelity"]["missing"] == 4
def test_registered_multiword_term_suppresses_its_internal_candidate_only(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n중복 실행을 묶는 single-flight coordinator를 사용한다.\n"
ledger = read_json(FIXTURES / "good" / "term-ledger.json")
reader = read_json(FIXTURES / "good" / "reader-contract.json")
ledger["assumed_known"].append("single-flight coordinator")
reader["assumed_known"].append("single-flight coordinator")
document_path = tmp_path / "document.md"
ledger_path = tmp_path / "term-ledger.json"
reader_path = tmp_path / "reader-contract.json"
output = tmp_path / "lint.json"
document_path.write_text(document, encoding="utf-8")
write_json(ledger_path, ledger)
write_json(reader_path, reader)
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
FIXTURES / "good" / "logic-map.json",
"--term-ledger",
ledger_path,
"--reader-contract",
reader_path,
"--output",
output,
)
candidates = {
item["context"]
for item in read_json(output)["findings"]
if item["rule_id"] == "DOC-T007"
}
assert result.returncode == 0
assert "single-flight" not in candidates
def test_registered_prefix_does_not_hide_longer_unregistered_term(tmp_path: Path) -> None:
result, report = lint_fixture(tmp_path, "bad")
assert result.returncode == 1
candidates = {
item["context"]
for item in report["findings"]
if item["rule_id"] == "DOC-T007"
}
assert "HTTPX" in candidates