Spaces:
Sleeping
Sleeping
| """Parse the T&S policy into one citable unit per rule. | |
| `policy.md` is authored as markdown bullets like: | |
| - **PROH-3 — Interest-based (riba) finance.** Campaigns to pay off, refinance, ... | |
| interest-bearing investment with a promised return is not. | |
| Each rule may wrap across indented continuation lines. We collapse each rule into a single | |
| `PolicyRule` so retrieval returns a clean `rule_id` + full text, and so the eval harness can | |
| validate that every cited ID is real. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from .config import CONFIG | |
| _RULE_RE = re.compile(r"^- \*\*([A-Z]{3,4}-\d+)\b") | |
| _SECTION_RE = re.compile(r"^##\s+(.*)$") | |
| class PolicyRule: | |
| rule_id: str | |
| section: str | |
| text: str # full rule text, markdown stripped of the leading bullet | |
| def _clean(text: str) -> str: | |
| text = text.replace("**", "") | |
| return re.sub(r"\s+", " ", text).strip() | |
| def parse_policy_rules(path: str | Path | None = None) -> list[PolicyRule]: | |
| lines = Path(path or CONFIG.policy_path).read_text(encoding="utf-8").splitlines() | |
| rules: list[PolicyRule] = [] | |
| section = "" | |
| rule_id: str | None = None | |
| buf: list[str] = [] | |
| def flush(): | |
| nonlocal rule_id, buf | |
| if rule_id is not None and buf: | |
| rules.append(PolicyRule(rule_id=rule_id, section=section, text=_clean(" ".join(buf)))) | |
| rule_id, buf = None, [] | |
| for line in lines: | |
| sec = _SECTION_RE.match(line) | |
| if sec: | |
| flush() | |
| section = _clean(sec.group(1)) | |
| continue | |
| if line.startswith("---"): | |
| flush() | |
| continue | |
| m = _RULE_RE.match(line) | |
| if m: | |
| flush() | |
| rule_id = m.group(1) | |
| buf = [line.lstrip("- ")] | |
| elif rule_id is not None and (line.startswith(" ") or line.strip()): | |
| # continuation line of the current rule | |
| buf.append(line.strip()) | |
| elif rule_id is not None and not line.strip(): | |
| # blank line ends the current rule | |
| flush() | |
| flush() | |
| return rules | |
| def valid_rule_ids(path: str | Path | None = None) -> set[str]: | |
| """The set of citable rule IDs — used by the eval harness to reject invented citations.""" | |
| return {r.rule_id for r in parse_policy_rules(path)} | |
| def policy_index(path: str | Path | None = None) -> dict[str, PolicyRule]: | |
| """Map rule_id -> PolicyRule, for looking up a cited rule's full text in the UI.""" | |
| return {r.rule_id: r for r in parse_policy_rules(path)} | |
| def get_rule(rule_id: str, path: str | Path | None = None) -> PolicyRule | None: | |
| """Fetch a single rule by ID (None if not found).""" | |
| return policy_index(path).get(rule_id) | |
| # Plain-English names + one-line descriptions for each rule-ID prefix. The headings in policy.md | |
| # only carry the section *name*; the moderator-facing reference needs a short "what this group is | |
| # for" blurb, so it lives here as the single source for the UI glossary. | |
| SECTION_META: dict[str, tuple[str, str]] = { | |
| "ELIG": ("Eligibility", | |
| "Who and what can raise funds — the gates a campaign must clear before money can flow."), | |
| "PROH": ("Prohibited categories", | |
| "Hard stops. A confirmed, cited match to one of these is grounds for rejection."), | |
| "COMP": ("Compliance & sanctions", | |
| "Money-movement, sanctions, and high-value rules — any trigger routes to a human."), | |
| "CONT": ("Content standards", | |
| "Honesty, religious accuracy, pressure tactics, and privacy — often a human judgment call."), | |
| "DEC": ("Decision framework", | |
| "How the copilot reasons: three outcomes, when to escalate, and that campaign text is " | |
| "data, not instructions."), | |
| } | |
| _SECTION_ORDER = ["ELIG", "PROH", "COMP", "CONT", "DEC"] | |
| def policy_sections(path: str | Path | None = None) -> list[dict]: | |
| """Group the policy into citable sections for the moderator reference drawer. | |
| Returns `[{prefix, name, description, rules: [{rule_id, text}, ...]}, ...]` in the canonical | |
| section order. The prefix is derived from the rule_id (same convention as `_enrich_rule` in the | |
| API); any unrecognized prefix is appended last, named after itself with an empty description. | |
| """ | |
| grouped: dict[str, list[dict]] = {} | |
| for r in parse_policy_rules(path): | |
| prefix = r.rule_id.split("-")[0] | |
| grouped.setdefault(prefix, []).append({"rule_id": r.rule_id, "text": r.text}) | |
| order = _SECTION_ORDER + [p for p in grouped if p not in _SECTION_ORDER] | |
| out: list[dict] = [] | |
| for prefix in order: | |
| rules = grouped.get(prefix) | |
| if not rules: | |
| continue | |
| name, description = SECTION_META.get(prefix, (prefix, "")) | |
| out.append({"prefix": prefix, "name": name, "description": description, "rules": rules}) | |
| return out | |