feat(evidence): structure v3 domain rules for review

This commit is contained in:
Codex
2026-08-26 12:38:30 +02:00
parent 9d4f994d3e
commit 9898726069
9 changed files with 254 additions and 24 deletions
+19 -10
View File
@@ -707,22 +707,31 @@ def migrate_workspace_evidence(
documents_by_id = {document.id: document for document in documents}
if len(documents_by_id) != len(documents):
raise EvidencePreparationError("duplicate_evidence_id")
migrated = tuple(sorted(
document.id for document in documents if document.schema_version in {1, 2}
))
unchanged = tuple(sorted(
document.id for document in documents if document.schema_version == 3
))
upgraded = {
evidence_id: document.model_copy(update={"schema_version": 3})
for evidence_id, document in documents_by_id.items()
}
migrated_ids: list[str] = []
unchanged_ids: list[str] = []
curated_root = evidence_root / "curated"
for evidence_id, document in upgraded.items():
path = curated_root / document.kind / f"{evidence_id.removeprefix('evidence:')}.md"
try:
presentation_is_current = path.read_text(encoding="utf-8") == dump_curated_markdown(
document,
)
except (OSError, UnicodeDecodeError):
presentation_is_current = False
target = unchanged_ids if presentation_is_current else migrated_ids
target.append(evidence_id)
migrated = tuple(sorted(migrated_ids))
unchanged = tuple(sorted(unchanged_ids))
if not migrated:
return EvidenceMigrationReport(
migrated=(),
unchanged=unchanged,
findings=validate_workspace_evidence(workspace_root).findings,
)
upgraded = {
evidence_id: document.model_copy(update={"schema_version": 3})
for evidence_id, document in documents_by_id.items()
}
findings = _stage_and_apply_authoring_tree(workspace_root, upgraded, manifest)
return EvidenceMigrationReport(
migrated=migrated,
+91 -3
View File
@@ -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