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:
@@ -79,8 +79,8 @@ id = "contract-parser-v2"
|
|||||||
canonical_source = "corviduo-project-template"
|
canonical_source = "corviduo-project-template"
|
||||||
canonical_path = "docs/contracts/contract_parser.py"
|
canonical_path = "docs/contracts/contract_parser.py"
|
||||||
consumer_path = "docs/contracts/contract_parser.py"
|
consumer_path = "docs/contracts/contract_parser.py"
|
||||||
pinned_sha256_16 = "f1fdfdb6914c7b20"
|
pinned_sha256_16 = "e10a4460ba9fd560"
|
||||||
pinned_at = "2026-05-17T05:30:00+00:00"
|
pinned_at = "2026-06-15T08:53:22+00:00"
|
||||||
|
|
||||||
[[pins]]
|
[[pins]]
|
||||||
id = "contract-drift-check-v1"
|
id = "contract-drift-check-v1"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.
|
Extracts all structured fields from a contract file without using an LLM.
|
||||||
Proves the format is machine-parseable by a simple tool.
|
Proves the format is machine-parseable by a simple tool.
|
||||||
@@ -100,9 +100,12 @@ class Contract:
|
|||||||
REQUIRED_FRONTMATTER_V1 = [
|
REQUIRED_FRONTMATTER_V1 = [
|
||||||
"contract_version", "module", "purpose", "language", "min_complexity",
|
"contract_version", "module", "purpose", "language", "min_complexity",
|
||||||
]
|
]
|
||||||
REQUIRED_FRONTMATTER_V2 = [
|
REQUIRED_FRONTMATTER_V2_MODULE = [
|
||||||
"contract_version", "module", "purpose", "language", "complexity",
|
"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_V1 = ["depends_on", "used_by", "estimated_loc"]
|
||||||
RECOMMENDED_FRONTMATTER_V2 = ["depends_on", "used_by", "estimated_loc", "confidence"]
|
RECOMMENDED_FRONTMATTER_V2 = ["depends_on", "used_by", "estimated_loc", "confidence"]
|
||||||
REQUIRED_BODY_SECTIONS = ["Context", "Data flow", "Invariants"]
|
REQUIRED_BODY_SECTIONS = ["Context", "Data flow", "Invariants"]
|
||||||
@@ -111,7 +114,18 @@ VALID_COMPLEXITIES_V2 = {"low", "medium", "high"}
|
|||||||
VALID_PRE_SEVERITIES = {"hard", "soft"}
|
VALID_PRE_SEVERITIES = {"hard", "soft"}
|
||||||
VALID_POST_CATEGORIES = {"return_value", "state_change", "side_effect", "exception"}
|
VALID_POST_CATEGORIES = {"return_value", "state_change", "side_effect", "exception"}
|
||||||
VALID_STEP_TYPES = {"setup", "sequential", "branch", "loop", "error_handler", "cleanup"}
|
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
|
@dataclass
|
||||||
@@ -125,8 +139,13 @@ def validate_contract(contract: Contract) -> list[ValidationIssue]:
|
|||||||
issues: list[ValidationIssue] = []
|
issues: list[ValidationIssue] = []
|
||||||
fm = contract.frontmatter
|
fm = contract.frontmatter
|
||||||
is_v2 = contract.version.startswith("2")
|
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
|
recommended_fm = RECOMMENDED_FRONTMATTER_V2 if is_v2 else RECOMMENDED_FRONTMATTER_V1
|
||||||
valid_complexities = VALID_COMPLEXITIES_V2 if is_v2 else VALID_COMPLEXITIES_V1
|
valid_complexities = VALID_COMPLEXITIES_V2 if is_v2 else VALID_COMPLEXITIES_V1
|
||||||
complexity_key = "complexity" if is_v2 else "min_complexity"
|
complexity_key = "complexity" if is_v2 else "min_complexity"
|
||||||
@@ -198,10 +217,11 @@ def validate_contract(contract: Contract) -> list[ValidationIssue]:
|
|||||||
))
|
))
|
||||||
|
|
||||||
# v2: validate test categories
|
# v2: validate test categories
|
||||||
|
valid_test_cats = VALID_TEST_CATEGORIES_V21 if is_v21 else VALID_TEST_CATEGORIES_V20
|
||||||
for test in fn.tests:
|
for test in fn.tests:
|
||||||
if test.category not in VALID_TEST_CATEGORIES:
|
if test.category not in valid_test_cats:
|
||||||
issues.append(ValidationIssue(
|
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.
|
# Modifier tags (e.g. "tracer") get the same vocabulary check.
|
||||||
for tag in test.tags:
|
for tag in test.tags:
|
||||||
@@ -546,8 +566,13 @@ def print_human(contract: Contract) -> None:
|
|||||||
is_v2 = contract.version.startswith("2")
|
is_v2 = contract.version.startswith("2")
|
||||||
complexity_key = "complexity" if is_v2 else "min_complexity"
|
complexity_key = "complexity" if is_v2 else "min_complexity"
|
||||||
|
|
||||||
print(f"Module: {fm.get('module', '?')}")
|
issue_scoped = is_v2 and _is_issue_scoped(contract)
|
||||||
print(f"Purpose: {fm.get('purpose', '?')}")
|
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"Version: {contract.version}")
|
||||||
print(f"Complexity: {fm.get(complexity_key, '?')}")
|
print(f"Complexity: {fm.get(complexity_key, '?')}")
|
||||||
print(f"Est. LOC: {fm.get('estimated_loc', '?')}")
|
print(f"Est. LOC: {fm.get('estimated_loc', '?')}")
|
||||||
@@ -596,7 +621,9 @@ def print_list(contract: Contract) -> None:
|
|||||||
fm = contract.frontmatter
|
fm = contract.frontmatter
|
||||||
is_v2 = contract.version.startswith("2")
|
is_v2 = contract.version.startswith("2")
|
||||||
complexity_key = "complexity" if is_v2 else "min_complexity"
|
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:
|
for fn in contract.functions:
|
||||||
params_short = fn.params[:60] + ("..." if len(fn.params) > 60 else "")
|
params_short = fn.params[:60] + ("..." if len(fn.params) > 60 else "")
|
||||||
print(f" FN {fn.name}({params_short}) -> {fn.return_type}")
|
print(f" FN {fn.name}({params_short}) -> {fn.return_type}")
|
||||||
|
|||||||
Reference in New Issue
Block a user