feat(evidence): structure v3 domain rules for review
This commit is contained in:
@@ -319,6 +319,10 @@ _V2_EMPTY_LIST = "<!-- tht:empty-list -->"
|
||||
_V2_REVIEW_SEPARATOR = "<!-- tht:review-separator -->"
|
||||
_V2_REVIEW_FIELD = "<!-- tht:review-field -->"
|
||||
_V3_METADATA = re.compile(r"\A<!-- tht:metadata:([A-Za-z0-9+/=]+) -->\n")
|
||||
_V3_RAW_RULE = re.compile(
|
||||
r"\A<!-- tht:raw-rule:([A-Za-z0-9+/=]+) -->\n\n(.+)\Z",
|
||||
re.DOTALL,
|
||||
)
|
||||
_V3_KIND_LABELS = {
|
||||
"en": {
|
||||
"glossary": "Glossary",
|
||||
@@ -507,6 +511,72 @@ def _parse_v3_values(value: str, labels: dict[str, str]) -> dict[str, str]:
|
||||
return {key: "\n".join(lines) for key, lines in parsed.items()}
|
||||
|
||||
|
||||
def _leading_rule_label(value: str) -> tuple[str, str] | None:
|
||||
match = re.match(r"\A([^:;\n]{2,80}:)\s+(.+)\Z", value, re.DOTALL)
|
||||
if match is None:
|
||||
return None
|
||||
label = match.group(1)
|
||||
if len(label.removesuffix(":").split()) > 10:
|
||||
return None
|
||||
if label.lower() in {"http:", "https:"}:
|
||||
return None
|
||||
return label, match.group(2)
|
||||
|
||||
|
||||
def _render_v3_rule_detail(value: str) -> str:
|
||||
clauses = re.split(r"(?<=;)\s+", value)
|
||||
if len(clauses) < 3:
|
||||
return value
|
||||
rendered: list[str] = []
|
||||
for clause in clauses:
|
||||
labelled = _leading_rule_label(clause)
|
||||
if labelled is None:
|
||||
rendered.append(f"- {clause}")
|
||||
else:
|
||||
label, detail = labelled
|
||||
rendered.append(f"- **{label}** {detail}")
|
||||
return "\n".join(rendered)
|
||||
|
||||
|
||||
def _render_v3_rule_presentation(value: str) -> str:
|
||||
if re.search(r"(?m)^\s*(?:[-*+] |#{1,6} |```|>)", value):
|
||||
return value
|
||||
rendered: list[str] = []
|
||||
for paragraph in re.split(r"\n\s*\n", value):
|
||||
sentences = re.split(
|
||||
r"(?<=[.!?])\s+(?=[A-ZÀ-ÖØ-Þ0-9(`])",
|
||||
paragraph,
|
||||
)
|
||||
for sentence in sentences:
|
||||
labelled = _leading_rule_label(sentence)
|
||||
if labelled is None:
|
||||
rendered.append(_render_v3_rule_detail(sentence))
|
||||
continue
|
||||
label, detail = labelled
|
||||
rendered.append(f"### {label}\n\n{_render_v3_rule_detail(detail)}")
|
||||
return "\n\n".join(rendered)
|
||||
|
||||
|
||||
def _render_v3_rule(value: str) -> str:
|
||||
if "<!-- tht:raw-rule:" in value:
|
||||
raise ValueError("curated evidence rule contains a reserved marker")
|
||||
encoded = base64.b64encode(value.encode("utf-8")).decode("ascii")
|
||||
return (
|
||||
f"<!-- tht:raw-rule:{encoded} -->\n\n"
|
||||
f"{_render_v3_rule_presentation(value)}"
|
||||
)
|
||||
|
||||
|
||||
def _parse_v3_rule(value: str) -> str:
|
||||
match = _V3_RAW_RULE.fullmatch(value)
|
||||
if match is None:
|
||||
return value
|
||||
try:
|
||||
return base64.b64decode(match.group(1), validate=True).decode("utf-8")
|
||||
except (binascii.Error, UnicodeDecodeError, ValueError) as error:
|
||||
raise ValueError("curated evidence v3 rule metadata is malformed") from error
|
||||
|
||||
|
||||
def _render_v2_payload(value: CuratedEvidence, labels: dict[str, str]) -> list[str]:
|
||||
payload = value.payload
|
||||
if value.kind == "glossary":
|
||||
@@ -568,6 +638,12 @@ def _render_v2_payload(value: CuratedEvidence, labels: dict[str, str]) -> list[s
|
||||
|
||||
|
||||
def _render_v3_payload(value: CuratedEvidence, labels: dict[str, str]) -> list[str]:
|
||||
if value.kind == "domain":
|
||||
return [_render_v2_field(
|
||||
"rule",
|
||||
labels["rule"],
|
||||
_render_v3_rule(value.payload.rule),
|
||||
)]
|
||||
if value.kind != "enum":
|
||||
return _render_v2_payload(value, labels)
|
||||
payload = value.payload
|
||||
@@ -705,14 +781,22 @@ def _render_v3_metadata(value: CuratedEvidence) -> str:
|
||||
return f"<!-- tht:metadata:{encoded} -->"
|
||||
|
||||
|
||||
def _render_v3_body(value: CuratedEvidence) -> str:
|
||||
def _render_v3_body(value: CuratedEvidence, *, structured_domain_rule: bool = True) -> str:
|
||||
if "\n" in value.title:
|
||||
raise ValueError("curated evidence title must be single-line in v3")
|
||||
labels = _v2_labels(value.language)
|
||||
fields = [
|
||||
_render_v3_overview(value, labels),
|
||||
_render_v3_scope(value, labels),
|
||||
*_render_v3_payload(value, labels),
|
||||
*(
|
||||
_render_v3_payload(value, labels)
|
||||
if structured_domain_rule
|
||||
else (
|
||||
[_render_v2_field("rule", labels["rule"], value.payload.rule)]
|
||||
if value.kind == "domain"
|
||||
else _render_v3_payload(value, labels)
|
||||
)
|
||||
),
|
||||
_render_v2_field(
|
||||
"supporting_excerpts",
|
||||
labels["supporting_excerpts"],
|
||||
@@ -827,6 +911,8 @@ def _parse_v2_payload(
|
||||
def _parse_v3_payload(
|
||||
kind: str, fields: dict[str, str], labels: dict[str, str],
|
||||
) -> tuple[dict, set[str]]:
|
||||
if kind == "domain":
|
||||
return {"rule": _parse_v3_rule(fields.get("rule", ""))}, {"rule"}
|
||||
if kind != "enum":
|
||||
return _parse_v2_payload(kind, fields, labels)
|
||||
return {
|
||||
@@ -985,7 +1071,9 @@ def parse_curated_markdown(text: str, *, path: Path | None = None) -> CuratedEvi
|
||||
data["payload"] = data.pop(kind, None)
|
||||
evidence = CuratedEvidence.model_validate(data)
|
||||
if evidence.schema_version == 3 and dump_curated_markdown(evidence) != text:
|
||||
raise ValueError("curated evidence v3 presentation is not canonical")
|
||||
legacy = f"{_render_v3_metadata(evidence)}\n{_render_v3_body(evidence, structured_domain_rule=False)}"
|
||||
if legacy != text:
|
||||
raise ValueError("curated evidence v3 presentation is not canonical")
|
||||
if path is not None:
|
||||
_validate_kind_directory(path, evidence.kind)
|
||||
return evidence
|
||||
|
||||
Reference in New Issue
Block a user