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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user