From d85ab43d58e01c6e328a0d46e57b1666d830a6f9 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 15 Jun 2026 02:09:51 -0700 Subject: [PATCH] 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. --- .corviduo-canonicals.toml | 4 +-- docs/contracts/contract_parser.py | 45 ++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.corviduo-canonicals.toml b/.corviduo-canonicals.toml index 32a5027..de41e75 100644 --- a/.corviduo-canonicals.toml +++ b/.corviduo-canonicals.toml @@ -79,8 +79,8 @@ id = "contract-parser-v2" canonical_source = "corviduo-project-template" canonical_path = "docs/contracts/contract_parser.py" consumer_path = "docs/contracts/contract_parser.py" -pinned_sha256_16 = "f1fdfdb6914c7b20" -pinned_at = "2026-05-17T05:30:00+00:00" +pinned_sha256_16 = "e10a4460ba9fd560" +pinned_at = "2026-06-15T08:53:22+00:00" [[pins]] id = "contract-drift-check-v1" diff --git a/docs/contracts/contract_parser.py b/docs/contracts/contract_parser.py index 981a4b6..cd3977a 100644 --- a/docs/contracts/contract_parser.py +++ b/docs/contracts/contract_parser.py @@ -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}")