feat: block Korean prose contract violations
This commit is contained in:
@@ -15,6 +15,7 @@ from claridoc.models import (
|
|||||||
)
|
)
|
||||||
from claridoc.style_contracts import (
|
from claridoc.style_contracts import (
|
||||||
KOREAN_EXPERIENCE_CONTRACT_ID,
|
KOREAN_EXPERIENCE_CONTRACT_ID,
|
||||||
|
first_person_metrics,
|
||||||
korean_experience_contract_applies,
|
korean_experience_contract_applies,
|
||||||
plain_form_ending_locations,
|
plain_form_ending_locations,
|
||||||
)
|
)
|
||||||
@@ -213,6 +214,17 @@ def lint_document(text: str, brief: Brief, outline: Outline, sources: SourcePack
|
|||||||
if style_contract_applies
|
if style_contract_applies
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
experience_metrics = (
|
||||||
|
first_person_metrics(text)
|
||||||
|
if style_contract_applies
|
||||||
|
else {
|
||||||
|
"first_person_marker_count": 0,
|
||||||
|
"opening_has_first_person": False,
|
||||||
|
"experience_section_count": 0,
|
||||||
|
"marked_experience_section_count": 0,
|
||||||
|
"experience_section_coverage": 0.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
if plain_form_locations:
|
if plain_form_locations:
|
||||||
add(
|
add(
|
||||||
"STYLE002",
|
"STYLE002",
|
||||||
@@ -227,6 +239,30 @@ def lint_document(text: str, brief: Brief, outline: Outline, sources: SourcePack
|
|||||||
"current behavior. Preserve quoted material and code unchanged."
|
"current behavior. Preserve quoted material and code unchanged."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
style_contract_applies
|
||||||
|
and experience_metrics["experience_section_count"]
|
||||||
|
and (
|
||||||
|
not experience_metrics["opening_has_first_person"]
|
||||||
|
or experience_metrics["experience_section_coverage"] < 0.5
|
||||||
|
)
|
||||||
|
):
|
||||||
|
reasons: list[str] = []
|
||||||
|
if not experience_metrics["opening_has_first_person"]:
|
||||||
|
reasons.append("the opening has no 저는/제가 experience marker")
|
||||||
|
if experience_metrics["experience_section_coverage"] < 0.5:
|
||||||
|
reasons.append(
|
||||||
|
"fewer than half of substantive H2 sections establish first-person experience"
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
"STYLE003",
|
||||||
|
Severity.BLOCKER,
|
||||||
|
"The Korean experience-prose contract is incomplete: " + "; ".join(reasons) + ".",
|
||||||
|
suggestion=(
|
||||||
|
"Use 저는 or 제가 where the opening and major transitions describe "
|
||||||
|
"a supported observation, action, or decision. Do not add invented experience."
|
||||||
|
),
|
||||||
|
)
|
||||||
long_paragraph_count = 0
|
long_paragraph_count = 0
|
||||||
crowded_paragraph_count = 0
|
crowded_paragraph_count = 0
|
||||||
long_sentence_count = 0
|
long_sentence_count = 0
|
||||||
@@ -430,6 +466,7 @@ def lint_document(text: str, brief: Brief, outline: Outline, sources: SourcePack
|
|||||||
else "none"
|
else "none"
|
||||||
),
|
),
|
||||||
"plain_form_ending_count": len(plain_form_locations),
|
"plain_form_ending_count": len(plain_form_locations),
|
||||||
|
**experience_metrics,
|
||||||
"has_verification": has_verification,
|
"has_verification": has_verification,
|
||||||
"has_tradeoffs": has_tradeoffs,
|
"has_tradeoffs": has_tradeoffs,
|
||||||
"severity_counts": dict(severity_counts),
|
"severity_counts": dict(severity_counts),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Write Korean reader-facing prose as a supported first-person experience, not as
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_PLAIN_FORM_ENDING = re.compile(r"(?<!니)다(?=[.!?](?:\s|$))")
|
_PLAIN_FORM_ENDING = re.compile(r"(?<!니)다(?=[.!?](?:\s|$))")
|
||||||
|
_FIRST_PERSON = re.compile(r"(?:저는|제가)")
|
||||||
_FENCE = re.compile(r"^\s*(?:```|~~~)")
|
_FENCE = re.compile(r"^\s*(?:```|~~~)")
|
||||||
_HEADING = re.compile(r"^\s{0,3}(#{1,6})\s+(.+?)\s*$")
|
_HEADING = re.compile(r"^\s{0,3}(#{1,6})\s+(.+?)\s*$")
|
||||||
_IMAGE_ONLY = re.compile(r"^\s*!\[[^\]]*\]\([^)]*\)\s*$")
|
_IMAGE_ONLY = re.compile(r"^\s*!\[[^\]]*\]\([^)]*\)\s*$")
|
||||||
@@ -159,3 +160,37 @@ def plain_form_ending_locations(markdown: str) -> list[int]:
|
|||||||
for segment in reader_prose_segments(markdown):
|
for segment in reader_prose_segments(markdown):
|
||||||
locations.extend(segment.line for _ in _PLAIN_FORM_ENDING.finditer(segment.text))
|
locations.extend(segment.line for _ in _PLAIN_FORM_ENDING.finditer(segment.text))
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def first_person_metrics(markdown: str) -> dict[str, int | float | bool]:
|
||||||
|
segments = reader_prose_segments(markdown)
|
||||||
|
first_person_marker_count = sum(
|
||||||
|
len(_FIRST_PERSON.findall(segment.text))
|
||||||
|
for segment in segments
|
||||||
|
)
|
||||||
|
opening_has_first_person = bool(
|
||||||
|
segments and _FIRST_PERSON.search(segments[0].text)
|
||||||
|
)
|
||||||
|
|
||||||
|
section_markers: dict[str, bool] = {}
|
||||||
|
for segment in segments:
|
||||||
|
if segment.h2_title is None:
|
||||||
|
continue
|
||||||
|
section_markers.setdefault(segment.h2_title, False)
|
||||||
|
if _FIRST_PERSON.search(segment.text):
|
||||||
|
section_markers[segment.h2_title] = True
|
||||||
|
|
||||||
|
experience_section_count = len(section_markers)
|
||||||
|
marked_experience_section_count = sum(section_markers.values())
|
||||||
|
experience_section_coverage = (
|
||||||
|
marked_experience_section_count / experience_section_count
|
||||||
|
if experience_section_count
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"first_person_marker_count": first_person_marker_count,
|
||||||
|
"opening_has_first_person": opening_has_first_person,
|
||||||
|
"experience_section_count": experience_section_count,
|
||||||
|
"marked_experience_section_count": marked_experience_section_count,
|
||||||
|
"experience_section_coverage": round(experience_section_coverage, 3),
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user