chore(canonicals): sync contract-parser to v2.1 (e10a4460)

Pulls the upstream contract_parser.py canonical: v2.1 + issue-scoped
contract validation (target_module/scope/prd frontmatter, four new
test categories scenario/trace/adversarial/property, issue-aware
human/list output). Re-pins pinned_sha256_16 + pinned_at in the
manifest. canonical_drift.py reports 7/7 OK; no sync-induced contract
regressions.
This commit is contained in:
vh
2026-06-15 02:09:51 -07:00
parent 5cdb69a6f3
commit d85ab43d58
2 changed files with 38 additions and 11 deletions
+36 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Reference parser for .contract.md files (v1.0 and v2.0).
"""Reference parser for .contract.md files (v1.0, v2.0, and v2.1).
Extracts all structured fields from a contract file without using an LLM.
Proves the format is machine-parseable by a simple tool.
@@ -100,9 +100,12 @@ class Contract:
REQUIRED_FRONTMATTER_V1 = [
"contract_version", "module", "purpose", "language", "min_complexity",
]
REQUIRED_FRONTMATTER_V2 = [
REQUIRED_FRONTMATTER_V2_MODULE = [
"contract_version", "module", "purpose", "language", "complexity",
]
REQUIRED_FRONTMATTER_V2_ISSUE = [
"contract_version", "target_module", "scope", "language", "complexity", "prd",
]
RECOMMENDED_FRONTMATTER_V1 = ["depends_on", "used_by", "estimated_loc"]
RECOMMENDED_FRONTMATTER_V2 = ["depends_on", "used_by", "estimated_loc", "confidence"]
REQUIRED_BODY_SECTIONS = ["Context", "Data flow", "Invariants"]
@@ -111,7 +114,18 @@ VALID_COMPLEXITIES_V2 = {"low", "medium", "high"}
VALID_PRE_SEVERITIES = {"hard", "soft"}
VALID_POST_CATEGORIES = {"return_value", "state_change", "side_effect", "exception"}
VALID_STEP_TYPES = {"setup", "sequential", "branch", "loop", "error_handler", "cleanup"}
VALID_TEST_CATEGORIES = {"happy", "error", "boundary", "edge", "security"}
VALID_TEST_CATEGORIES_V20 = {"happy", "error", "boundary", "edge", "security"}
VALID_TEST_CATEGORIES_V21 = VALID_TEST_CATEGORIES_V20 | {"scenario", "trace", "adversarial", "property"}
_ISSUE_PATH_RE = re.compile(r"docs/contracts/issues/\d+\.contract\.md$")
def _is_issue_scoped(contract: Contract) -> bool:
"""Detect issue-scoped contracts per CONTRACT-FORMAT § 2.1.I."""
if contract.source_path and _ISSUE_PATH_RE.search(str(contract.source_path)):
return True
return "prd" in contract.frontmatter
@dataclass
@@ -125,8 +139,13 @@ def validate_contract(contract: Contract) -> list[ValidationIssue]:
issues: list[ValidationIssue] = []
fm = contract.frontmatter
is_v2 = contract.version.startswith("2")
is_v21 = contract.version.startswith("2.1")
issue_scoped = is_v2 and _is_issue_scoped(contract)
required_fm = REQUIRED_FRONTMATTER_V2 if is_v2 else REQUIRED_FRONTMATTER_V1
if is_v2:
required_fm = REQUIRED_FRONTMATTER_V2_ISSUE if issue_scoped else REQUIRED_FRONTMATTER_V2_MODULE
else:
required_fm = REQUIRED_FRONTMATTER_V1
recommended_fm = RECOMMENDED_FRONTMATTER_V2 if is_v2 else RECOMMENDED_FRONTMATTER_V1
valid_complexities = VALID_COMPLEXITIES_V2 if is_v2 else VALID_COMPLEXITIES_V1
complexity_key = "complexity" if is_v2 else "min_complexity"
@@ -198,10 +217,11 @@ def validate_contract(contract: Contract) -> list[ValidationIssue]:
))
# v2: validate test categories
valid_test_cats = VALID_TEST_CATEGORIES_V21 if is_v21 else VALID_TEST_CATEGORIES_V20
for test in fn.tests:
if test.category not in VALID_TEST_CATEGORIES:
if test.category not in valid_test_cats:
issues.append(ValidationIssue(
"warning", f"{prefix}: test {test.name!r} category {test.category!r} not in {sorted(VALID_TEST_CATEGORIES)}"
"warning", f"{prefix}: test {test.name!r} category {test.category!r} not in {sorted(valid_test_cats)}"
))
# Modifier tags (e.g. "tracer") get the same vocabulary check.
for tag in test.tags:
@@ -546,8 +566,13 @@ def print_human(contract: Contract) -> None:
is_v2 = contract.version.startswith("2")
complexity_key = "complexity" if is_v2 else "min_complexity"
print(f"Module: {fm.get('module', '?')}")
print(f"Purpose: {fm.get('purpose', '?')}")
issue_scoped = is_v2 and _is_issue_scoped(contract)
if issue_scoped:
print(f"Target: {fm.get('target_module', '?')}")
print(f"Scope: {fm.get('scope', '?')}")
else:
print(f"Module: {fm.get('module', '?')}")
print(f"Purpose: {fm.get('purpose', '?')}")
print(f"Version: {contract.version}")
print(f"Complexity: {fm.get(complexity_key, '?')}")
print(f"Est. LOC: {fm.get('estimated_loc', '?')}")
@@ -596,7 +621,9 @@ def print_list(contract: Contract) -> None:
fm = contract.frontmatter
is_v2 = contract.version.startswith("2")
complexity_key = "complexity" if is_v2 else "min_complexity"
print(f"{fm.get('module', '?')} [{fm.get(complexity_key, '?')}]")
issue_scoped = is_v2 and _is_issue_scoped(contract)
label = fm.get('target_module', '?') if issue_scoped else fm.get('module', '?')
print(f"{label} [{fm.get(complexity_key, '?')}]")
for fn in contract.functions:
params_short = fn.params[:60] + ("..." if len(fn.params) > 60 else "")
print(f" FN {fn.name}({params_short}) -> {fn.return_type}")