Skip to content

Commit 7be6fdb

Browse files
committed
Added updated files from dev repo
1 parent c5a2a1e commit 7be6fdb

30 files changed

Lines changed: 3254 additions & 190 deletions

governance_ledger/cli.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
list_contracts,
1717
show_artifact,
1818
)
19+
from governance_ledger.replay import (
20+
replay_admissibility,
21+
replay_governance_compilation,
22+
verify_authority_lineage,
23+
)
1924
from governance_ledger.summary import (
2025
build_pr_summary,
2126
format_publish_summary,
@@ -69,6 +74,31 @@ def main(argv: Sequence[str] | None = None) -> int:
6974
show_parser.add_argument("path")
7075
show_parser.add_argument("--json", action="store_true")
7176

77+
replay_authority_parser = subparsers.add_parser(
78+
"replay-authority",
79+
help="replay governance source into compilation report and authority contract",
80+
)
81+
replay_authority_parser.add_argument("--source", required=True)
82+
replay_authority_parser.add_argument("--report")
83+
replay_authority_parser.add_argument("--contract")
84+
replay_authority_parser.add_argument("--json", action="store_true")
85+
86+
replay_execution_parser = subparsers.add_parser(
87+
"replay-execution",
88+
help="replay deterministic admissibility from authority contract and execution state",
89+
)
90+
replay_execution_parser.add_argument("--contract", required=True)
91+
replay_execution_parser.add_argument("--execution-state", required=True)
92+
replay_execution_parser.add_argument("--json", action="store_true")
93+
94+
verify_lineage_parser = subparsers.add_parser(
95+
"verify-lineage",
96+
help="verify authority provenance lineage against an optional compilation report",
97+
)
98+
verify_lineage_parser.add_argument("--contract", required=True)
99+
verify_lineage_parser.add_argument("--report")
100+
verify_lineage_parser.add_argument("--json", action="store_true")
101+
72102
args = parser.parse_args(argv)
73103

74104
if args.command == "run":
@@ -115,11 +145,29 @@ def main(argv: Sequence[str] | None = None) -> int:
115145
result = check_validation_directory(args.generated_dir)
116146
elif args.command == "list":
117147
result = list_contracts(args.contracts_dir)
148+
elif args.command == "replay-authority":
149+
result = replay_governance_compilation(
150+
source_text=Path(args.source).read_text(encoding="utf-8"),
151+
expected_report=_read_json_arg(args.report),
152+
expected_contract=_read_json_arg(args.contract),
153+
)
154+
elif args.command == "replay-execution":
155+
result = replay_admissibility(
156+
authority_contract=_read_json_arg(args.contract),
157+
execution_state=_read_json_arg(args.execution_state),
158+
)
159+
elif args.command == "verify-lineage":
160+
result = verify_authority_lineage(
161+
authority_contract=_read_json_arg(args.contract),
162+
compilation_report=_read_json_arg(args.report),
163+
)
118164
else:
119165
result = show_artifact(args.path)
120166

121167
if getattr(args, "json", False):
122168
print(json.dumps(result, indent=2, sort_keys=True))
169+
if args.command in {"replay-authority", "replay-execution", "verify-lineage"}:
170+
return 0 if _replay_ok(result) else 1
123171
elif args.command == "run":
124172
print(format_run_summary(result))
125173
elif args.command == "publish":
@@ -131,6 +179,9 @@ def main(argv: Sequence[str] | None = None) -> int:
131179
print(format_contract_list(result))
132180
elif args.command == "show":
133181
print(format_artifact(args.path, result))
182+
elif args.command in {"replay-authority", "replay-execution", "verify-lineage"}:
183+
print(_format_replay_summary(result))
184+
return 0 if _replay_ok(result) else 1
134185
else:
135186
print(
136187
"\n".join(
@@ -148,5 +199,37 @@ def main(argv: Sequence[str] | None = None) -> int:
148199
return 0
149200

150201

202+
def _read_json_arg(path: str | None) -> dict | None:
203+
if path is None:
204+
return None
205+
return json.loads(Path(path).read_text(encoding="utf-8"))
206+
207+
208+
def _replay_ok(result: dict) -> bool:
209+
if "replay_verified" in result:
210+
return bool(result["replay_verified"])
211+
if "lineage_verified" in result:
212+
return bool(result["lineage_verified"])
213+
return not result.get("diagnostics")
214+
215+
216+
def _format_replay_summary(result: dict) -> str:
217+
status = "VERIFIED" if _replay_ok(result) else "FAILED"
218+
lines = [
219+
"[Governance Replay]",
220+
"",
221+
f"Status: {status}",
222+
]
223+
if result.get("authority_ref"):
224+
lines.append(f"Authority: {result['authority_ref']}")
225+
if result.get("contract_hash"):
226+
lines.append(f"Contract hash: {result['contract_hash']}")
227+
diagnostics = result.get("diagnostics") or []
228+
if diagnostics:
229+
lines.extend(["", "Diagnostics:"])
230+
lines.extend(f" {item['text']}" for item in diagnostics)
231+
return "\n".join(lines)
232+
233+
151234
if __name__ == "__main__":
152235
raise SystemExit(main())

governance_ledger/diagnostics.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
6+
GOVERNANCE_DIAGNOSTIC_V1 = "governance_diagnostic.v1"
7+
8+
DIAGNOSTIC_DOMAINS = {
9+
"G1": "authority_threshold",
10+
"G3": "lifecycle",
11+
"G4": "artifact",
12+
"G5": "role_integrity",
13+
"G7": "governance_lint",
14+
"G8": "lineage",
15+
}
16+
17+
DIAGNOSTIC_SEVERITIES = {"error", "warning", "info"}
18+
19+
20+
def build_diagnostic(
21+
*,
22+
code: str,
23+
severity: str,
24+
title: str,
25+
detail: str,
26+
recommendation: str | None = None,
27+
) -> dict[str, Any]:
28+
if severity not in DIAGNOSTIC_SEVERITIES:
29+
raise ValueError(f"unsupported diagnostic severity: {severity}")
30+
diagnostic = {
31+
"schema_version": GOVERNANCE_DIAGNOSTIC_V1,
32+
"type": "compiler_diagnostic",
33+
"severity": severity,
34+
"code": code,
35+
"domain": diagnostic_domain(code),
36+
"title": title,
37+
"detail": detail,
38+
"blocks_publication": severity == "error",
39+
"text": f"{severity.upper()} {code}: {title}. {detail}",
40+
}
41+
if recommendation:
42+
diagnostic["recommendation"] = recommendation
43+
validate_diagnostic(diagnostic)
44+
return diagnostic
45+
46+
47+
def validate_diagnostic(diagnostic: Any) -> None:
48+
if not isinstance(diagnostic, dict):
49+
raise ValueError("diagnostic must be an object")
50+
if diagnostic.get("schema_version") != GOVERNANCE_DIAGNOSTIC_V1:
51+
raise ValueError("unsupported diagnostic schema_version")
52+
for field in ["type", "severity", "code", "domain", "title", "detail", "text"]:
53+
if not isinstance(diagnostic.get(field), str) or not diagnostic[field]:
54+
raise ValueError(f"diagnostic {field} must be a non-empty string")
55+
if diagnostic["type"] != "compiler_diagnostic":
56+
raise ValueError("diagnostic type must be compiler_diagnostic")
57+
if diagnostic["severity"] not in DIAGNOSTIC_SEVERITIES:
58+
raise ValueError("unsupported diagnostic severity")
59+
if not isinstance(diagnostic.get("blocks_publication"), bool):
60+
raise ValueError("diagnostic blocks_publication must be boolean")
61+
if diagnostic["blocks_publication"] != (diagnostic["severity"] == "error"):
62+
raise ValueError("diagnostic publication impact does not match severity")
63+
64+
65+
def diagnostic_domain(code: str) -> str:
66+
return DIAGNOSTIC_DOMAINS.get(code[:2], "general")

governance_ledger/extract.py

Lines changed: 12 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
from __future__ import annotations
44

5-
import re
6-
from copy import deepcopy
75
from typing import Any
86

9-
from governance_ledger.patterns import (
10-
ROLE_PATTERNS,
11-
SEPARATION_PATTERNS,
12-
THRESHOLD_PATTERNS,
7+
from governance_ledger.statement_normalizer import (
8+
normalize_operator,
9+
normalize_policy_text,
10+
normalize_role,
11+
normalize_text,
12+
parse_amount,
1313
)
1414

1515
DEFAULT_CONTRACT_ID = "finance-policy"
@@ -18,86 +18,20 @@
1818

1919
def extract_constraints(text: str) -> dict[str, Any]:
2020
"""Extract v0.1 governance constraints from policy text."""
21-
policy: dict[str, Any] = {
22-
"contract_id": DEFAULT_CONTRACT_ID,
23-
"contract_version": DEFAULT_CONTRACT_VERSION,
24-
"authority": {"required_roles": []},
25-
"approvals": {"thresholds": []},
26-
}
27-
28-
normalized_text = _normalize_text(text)
29-
30-
for pattern in ROLE_PATTERNS:
31-
for match in re.finditer(pattern, normalized_text, flags=re.IGNORECASE):
32-
_append_unique(policy["authority"]["required_roles"], _normalize_role(match["role"]))
33-
34-
if any(re.search(pattern, normalized_text, flags=re.IGNORECASE) for pattern in SEPARATION_PATTERNS):
35-
policy["authority"]["separation_of_duties"] = True
36-
37-
for pattern in THRESHOLD_PATTERNS:
38-
for match in re.finditer(pattern, normalized_text, flags=re.IGNORECASE):
39-
requires_role = match.groupdict().get("requires_role")
40-
if requires_role:
41-
requires_role = _normalize_role(requires_role)
42-
elif policy["authority"]["required_roles"]:
43-
requires_role = policy["authority"]["required_roles"][0]
44-
else:
45-
requires_role = "manager"
46-
_append_unique_threshold(
47-
policy["approvals"]["thresholds"],
48-
{
49-
"field": "amount",
50-
"operator": ">",
51-
"value": _parse_amount(
52-
match["amount"],
53-
match.groupdict().get("suffix"),
54-
),
55-
"requires_role": requires_role,
56-
},
57-
)
58-
59-
return _without_empty_sections(policy)
21+
return normalize_policy_text(text)
6022

6123

6224
def _normalize_text(text: str) -> str:
63-
return " ".join(text.strip().split())
25+
return normalize_text(text)
6426

6527

6628
def _normalize_role(role: str) -> str:
67-
normalized = role.strip().lower().replace("-", "_")
68-
if normalized.endswith("s") and not normalized.endswith("ss"):
69-
return normalized[:-1]
70-
return normalized
29+
return normalize_role(role)
7130

7231

7332
def _parse_amount(amount: str, suffix: str | None = None) -> int:
74-
value = float(amount.replace(",", ""))
75-
if suffix and suffix.lower() in {"m", "million"}:
76-
value *= 1_000_000
77-
return int(value)
78-
79-
80-
def _append_unique(values: list[str], value: str) -> None:
81-
if value and value not in values:
82-
values.append(value)
83-
84-
85-
def _append_unique_threshold(thresholds: list[dict[str, Any]], threshold: dict[str, Any]) -> None:
86-
if threshold not in thresholds:
87-
thresholds.append(threshold)
88-
89-
90-
def _without_empty_sections(policy: dict[str, Any]) -> dict[str, Any]:
91-
cleaned = deepcopy(policy)
33+
return parse_amount(amount, suffix)
9234

93-
if (
94-
not cleaned["authority"].get("required_roles")
95-
and "separation_of_duties" not in cleaned["authority"]
96-
):
97-
cleaned.pop("authority")
98-
elif not cleaned["authority"].get("required_roles"):
99-
cleaned["authority"].pop("required_roles")
100-
if not cleaned["approvals"]["thresholds"]:
101-
cleaned.pop("approvals")
10235

103-
return cleaned
36+
def _normalize_operator(phrase: str) -> str:
37+
return normalize_operator(phrase)

0 commit comments

Comments
 (0)