diff --git a/SESSION_FOCUS.md b/SESSION_FOCUS.md index 12b5846..83d5736 100644 --- a/SESSION_FOCUS.md +++ b/SESSION_FOCUS.md @@ -2,7 +2,7 @@ *Current sprint, SDK status, and active work. Updated by operator and autonomous sessions.* -*Last updated: 2026-05-14 (Sprint 50)* +*Last updated: 2026-05-14 (Sprint 52)* --- @@ -10,6 +10,12 @@ **See `docs/SPRINT.md` for full sprint plan and task details.** Do not duplicate sprint content here — SPRINT.md is the source of truth for task scope, status, and dependencies. +### Sprint 52 Summary: Python SDK Conformance Test Wiring (COMPLETE) + +| Task | Status | Notes | +|------|--------|-------| +| T1: Wire ATP + Society/Role conformance vectors into SDK tests | DONE | Operator burst-4 shipped 4 conformance JSON suites to `web4-standard/testing/conformance/` but no Python test asserted against them. Sprint 52 wires the two best-aligned suites: ATP (11 vectors + 2 meta → 13 pass, audit "best-aligned pair" confirmed empirically) and Society/Role (9 vectors + 2 meta → 8 pass, 3 strict-xfail with documented divergences citing audit P4, missing assigner predicate, constructor-vs-imperative federation pattern). 2 new test files, 0 product code modifications, 24 new tests (2691 pass + 3 xfail), mypy --strict clean, ruff clean. T3/V3 and R6/R7 conformance deferred. | + ### Sprint 51 Summary: Minimum Viable Society Validation + Constraint Alignment (COMPLETE) | Task | Status | Notes | @@ -202,9 +208,9 @@ See `docs/SPRINT.md` for full history. Highlights: JSON-LD serialization for all - **Version**: 0.26.0 - **Modules**: 23 library modules + MCP server entry point (trust, lct, atp, federation, r6, mrh, acp, dictionary, entity, capability, errors, metabolic, binding, society, role, reputation, security, protocol, mcp, attestation, validation, deserialize, generate, mcp_server) -- **Tests**: 2668 passing +- **Tests**: 2691 passing + 3 strict-xfail (Sprint 52 documented divergences: combined-state enum, assigner predicate, imperative federation actions) - **CLI**: `web4 info/validate/list-schemas/roundtrip/generate/selftest/trust` (7 subcommands) -- **Exports**: 368 symbols via `web4/__init__.py` +- **Exports**: 369 symbols via `web4/__init__.py` - **from_dict()**: 58 classmethods across 10 modules — all classes with to_dict()/as_dict() have matching from_dict() - **Dispatcher**: 23 types via `web4.from_jsonld()` (19 class-based + 3 function-based + TrustQuery) - **Generator**: 23 types via `web4.generate()` — minimal valid JSON-LD documents diff --git a/docs/SPRINT.md b/docs/SPRINT.md index d9384b1..3c2f64e 100644 --- a/docs/SPRINT.md +++ b/docs/SPRINT.md @@ -1,12 +1,73 @@ # Web4 Sprint Plan **Created**: 2026-03-14 -**Updated**: 2026-05-14 (Sprint 51) +**Updated**: 2026-05-14 (Sprint 52) **Phase**: Development **Track**: web4 (Legion) --- +## Sprint 52: Python SDK Conformance Test Wiring (2026-05-14) + +Operator burst-4 (commits `a2727b45`, `92454d6`, `0c39a9b6` at 12:03–12:04 PDT) +shipped the conformance test corpus to `web4-standard/testing/conformance/` +(4 JSON suites, 35 vectors total). The vectors are declared cross-language — +"Any Web4 implementation MUST produce identical results" — but no Python SDK +test currently asserts this. Sprint 52 wires the two best-aligned suites +(ATP and Society/Role) into the SDK test runner. This addresses Nova GPT's +#1 quick-win request (test vectors + conformance) on the Python side, and +partially advances Kimi's K2 gap (conformance test suite missing). + +### T1: Wire ATP and Society/Role conformance vectors into Python SDK tests +**Status**: DONE +**Completed**: 2026-05-14 +**Authorized by**: Operator burst-4 (conformance corpus shipped) + Nova/Kimi +cross-reviewer convergence (test vectors + conformance was Nova's #1 quick-win; +K2 named by Kimi rounds 1–4). Policy-reviewed and approved with binding +condition: any failing vector MUST be `pytest.mark.xfail` with reason; no +silent fixes (no assertion weakening, no vector edits, no SDK behavioral +changes to make vectors pass). +**Scope**: +1. **ATP conformance** (`tests/test_conformance_atp.py`): loads + `testing/conformance/atp-operations.json` (11 vectors across account, + transfer, sliding-scale categories) and asserts the Python `web4.atp` + module produces matching outputs. Sprint 49 audit named ATP the + best-aligned cross-language pair ("identical core semantics") — expected + high pass rate confirmed: **11/11 pass + 2 meta tests = 13 pass, 0 xfail**. +2. **Society/Role conformance** (`tests/test_conformance_society.py`): + loads `testing/conformance/society-roles.json` (9 vectors across + bootstrap, role, federation, minimum-viable categories) and asserts + the Python `web4.role` module produces matching outputs. **8 pass + 3 + strict-xfail with documented divergences**: + - `soc-002` (5-state lifecycle): Python splits combined enum into + `SocietyPhase` (3) + `MetabolicState` (separate axis). Cites audit P4. + - `role-004` (assigner-permission table): Python `role.py` does not + encode role-based permission to assign other roles. + - `fed-001` (imperative join/secede): Python `federation.Society` uses + constructor-hierarchy pattern (`parent=Society`, `children` list), not + imperative join/secede actions. +3. Sprint plan + session focus bookkeeping. + +**Result**: 2 new test files, 0 modifications to product code (verification- +only). 24 new tests (2691 passed + 3 xfailed), mypy --strict clean, +ruff lint/format clean. + +**Findings produced by xfails**: the three documented divergences are now +executable test markers, not just documentary audit findings. If the SDK ever +gains the corresponding surfaces (combined-state enum, assigner predicate, +imperative federation actions), the strict xfails will turn into XPASS +failures, forcing review and removal — preventing silent surface drift. + +**Out of bounds**: T3/V3 conformance vectors (`tensor-operations.json`) and +R6/R7 conformance vectors (`r6-r7-actions.json`) were NOT wired. Sprint 47 +documented 8 T3/V3 divergences between Rust and Python; their conformance +wiring needs a separate sprint that can also catalogue divergences (Python +SDK matches spec, Rust/WASM diverges). R6/R7 vectors may have been authored +before PR #187 Constraint shape changes and need a freshness check before +wiring. + +--- + ## Sprint 51: Minimum Viable Society Validation + Constraint Alignment (2026-05-14) Resolves two remaining autonomous-actionable items from the Sprint 49 diff --git a/web4-standard/implementation/sdk/tests/test_conformance_atp.py b/web4-standard/implementation/sdk/tests/test_conformance_atp.py new file mode 100644 index 0000000..722a478 --- /dev/null +++ b/web4-standard/implementation/sdk/tests/test_conformance_atp.py @@ -0,0 +1,192 @@ +"""Cross-language conformance tests for ATP/ADP operations. + +Loads ``web4-standard/testing/conformance/atp-operations.json`` and asserts +that the Python ``web4.atp`` module produces the documented expected outputs. + +The conformance vectors were shipped by the operator (commit 92454d6) and are +declared cross-language: "Any Web4 implementation MUST produce identical results +for these inputs." + +Sprint 49 cross-language audit named ATP as the best-aligned pair across Rust +and Python ("identical core semantics"), so a high pass rate is expected. +Where a vector cannot be satisfied without behavioral changes to the SDK, the +test is marked ``xfail`` with a reason citing the specific divergence — +silent fixes (assertion weakening, vector edits, or SDK edits to make vectors +pass) are explicitly forbidden by the Sprint 52 policy review. + +Suite version: 0.1.0 +""" + +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List + +import pytest + +from web4.atp import ATPAccount, sliding_scale, transfer + +CONFORMANCE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "..", "testing", "conformance") + + +def _load_suite() -> Dict[str, Any]: + path = os.path.join(CONFORMANCE_DIR, "atp-operations.json") + with open(path) as f: + return json.load(f) + + +SUITE = _load_suite() + + +# ── Account vectors ────────────────────────────────────────────── + + +@pytest.mark.parametrize( + "vector", + SUITE["account_vectors"], + ids=lambda v: v["id"], +) +def test_account_vector(vector: Dict[str, Any]) -> None: + operation = vector["operation"] + expected = vector["expected"] + + if operation == "new": + balance = vector["input"]["initial_balance"] + account = ATPAccount(available=balance, initial_balance=balance) + assert account.available == expected["available"] + assert account.locked == expected["locked"] + assert account.adp == expected["adp"] + assert account.total == expected["total"] + assert account.energy_ratio == expected["energy_ratio"] + return + + initial = vector["initial"] + account = ATPAccount( + available=initial["available"], + locked=initial["locked"], + adp=initial["adp"], + ) + + if operation == "lock": + amount = vector["input"]["amount"] + ok = account.lock(amount) + assert ok is True + assert account.available == expected["available"] + assert account.locked == expected["locked"] + assert account.total == expected["total"] + return + + if operation == "commit": + amount = vector["input"]["amount"] + committed = account.commit(amount) + assert committed == amount + assert account.available == expected["available"] + assert account.locked == expected["locked"] + assert account.adp == expected["adp"] + assert account.total == expected["total"] + assert account.energy_ratio == expected["energy_ratio"] + return + + if operation == "rollback": + amount = vector["input"]["amount"] + rolled = account.rollback(amount) + assert rolled == amount + assert account.available == expected["available"] + assert account.locked == expected["locked"] + assert account.adp == expected["adp"] + return + + if operation == "energy_ratio": + # atp-005: zero-balance neutral + assert account.energy_ratio == expected["energy_ratio"] + return + + pytest.fail(f"Unknown operation in account vector {vector['id']}: {operation}") + + +# ── Transfer vectors ───────────────────────────────────────────── + + +@pytest.mark.parametrize( + "vector", + SUITE["transfer_vectors"], + ids=lambda v: v["id"], +) +def test_transfer_vector(vector: Dict[str, Any]) -> None: + sender = ATPAccount(available=vector["sender"]["available"]) + receiver = ATPAccount(available=vector["receiver"]["available"]) + + inp = vector["input"] + amount = inp["amount"] + fee_rate = inp.get("fee_rate", 0.05) + max_balance = inp.get("max_balance") + + expected = vector["expected"] + + if expected.get("error"): + with pytest.raises(ValueError): + transfer(sender, receiver, amount, fee_rate=fee_rate, max_balance=max_balance) + return + + result = transfer(sender, receiver, amount, fee_rate=fee_rate, max_balance=max_balance) + + if "fee" in expected: + assert result.fee == expected["fee"] + assert result.actual_credit == expected["actual_credit"] + assert result.overflow == expected.get("overflow", 0.0) + assert result.sender_balance == expected["sender_balance"] + assert result.receiver_balance == expected["receiver_balance"] + + if expected.get("conservation_holds"): + # invariant: sender_deducted == actual_credit + fee + overflow + sender_deducted = vector["sender"]["available"] - result.sender_balance + assert sender_deducted == pytest.approx(result.actual_credit + result.fee + result.overflow) + + +# ── Sliding-scale vectors ──────────────────────────────────────── + + +@pytest.mark.parametrize( + "vector", + SUITE["sliding_scale_vectors"], + ids=lambda v: v["id"], +) +def test_sliding_scale_vector(vector: Dict[str, Any]) -> None: + inp = vector["input"] + result = sliding_scale( + quality=inp["quality"], + base_payment=inp["base_payment"], + zero_threshold=inp["zero_threshold"], + full_threshold=inp["full_threshold"], + ) + + if "expected" in vector: + assert result == vector["expected"] + else: + tolerance = vector.get("tolerance", 1e-10) + assert result == pytest.approx(vector["expected_approx"], abs=tolerance) + + +# ── Suite-level meta ───────────────────────────────────────────── + + +def test_suite_metadata() -> None: + """The suite version and shape are part of the conformance contract.""" + assert SUITE["suite"] == "ATP/ADP Operations" + assert SUITE["version"] == "0.1.0" + + # Counts match the README's documented vector budget. + assert len(SUITE["account_vectors"]) == 5 + assert len(SUITE["transfer_vectors"]) == 3 + assert len(SUITE["sliding_scale_vectors"]) == 3 + + +def test_all_vectors_have_ids() -> None: + """Every vector must have a stable id for cross-language reference.""" + seen: List[str] = [] + for category in ("account_vectors", "transfer_vectors", "sliding_scale_vectors"): + for v in SUITE[category]: + assert "id" in v, f"vector missing id in {category}: {v}" + assert v["id"] not in seen, f"duplicate vector id: {v['id']}" + seen.append(v["id"]) diff --git a/web4-standard/implementation/sdk/tests/test_conformance_society.py b/web4-standard/implementation/sdk/tests/test_conformance_society.py new file mode 100644 index 0000000..c7c5d41 --- /dev/null +++ b/web4-standard/implementation/sdk/tests/test_conformance_society.py @@ -0,0 +1,287 @@ +"""Cross-language conformance tests for Society & Role operations. + +Loads ``web4-standard/testing/conformance/society-roles.json`` and asserts +that the Python ``web4.role`` module produces the documented expected outputs. + +The conformance vectors were shipped by the operator (commit 92454d6) and are +declared cross-language: "Any Web4 implementation MUST produce identical results +for these inputs." + +Sprint 50 (PR #185) added ``SocietyRole``, ``RoleAssignment``, and +``bootstrap_society_roles``. Sprint 51 (PR #187) added +``validate_minimum_viable``. The role-related vectors are expected to pass +against this surface; lifecycle and authorization vectors that depend on +APIs not present in the Python SDK (combined 5-state phase enum, role-based +assigner permissions) are marked ``xfail`` with reasons citing the specific +audit divergence (P4) or missing surface — silent fixes are forbidden by +the Sprint 52 policy review. + +Suite version: 0.1.0 +""" + +from __future__ import annotations + +import json +import os +from typing import Any, Dict, List + +import pytest + +from web4.role import ( + BASE_MANDATORY_ROLES, + RoleAssignment, + SocietyRole, + bootstrap_society_roles, + validate_minimum_viable, +) + +CONFORMANCE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "..", "testing", "conformance") + + +def _load_suite() -> Dict[str, Any]: + path = os.path.join(CONFORMANCE_DIR, "society-roles.json") + with open(path) as f: + return json.load(f) + + +SUITE = _load_suite() + + +def _vec_by_id(category: str, vid: str) -> Dict[str, Any]: + for v in SUITE[category]: + if v["id"] == vid: + return v + raise KeyError(f"vector {vid!r} not in {category}") + + +# ── Bootstrap vectors ──────────────────────────────────────────── + + +def test_soc_001_solo_founder_bootstrap() -> None: + """soc-001: solo founder fills all 7 base-mandatory roles, each role has its own LCT.""" + vector = _vec_by_id("bootstrap_vectors", "soc-001") + expected = vector["expected"] + founder = vector["input"]["founder_lct"] + + assignments = bootstrap_society_roles(founder_lct_id=founder) + + assert len(assignments) == expected["role_count"] + + assigned_roles = [a.role.value for a in assignments] + assert sorted(assigned_roles) == sorted(expected["roles_present"]) + + if expected["all_roles_filled_by_founder"]: + assert all(a.filling_entity_lct_id == founder for a in assignments) + + if expected["each_role_has_own_lct"]: + role_lcts = {a.role_lct_id for a in assignments} + # Each role's LCT must be distinct and must NOT equal the founder's LCT + # (authority binds to role, not entity). + assert len(role_lcts) == expected["role_count"] + assert founder not in role_lcts + + +@pytest.mark.xfail( + reason=( + "soc-002 expects 5 combined states (genesis/bootstrap/operational/dormant/sunset). " + "Python SDK splits this into SocietyPhase (3: genesis/bootstrap/operational) plus " + "MetabolicState (separate axis). Sprint 49 audit P4 — operator decision pending." + ), + strict=True, +) +def test_soc_002_lifecycle_transitions() -> None: + """soc-002: 5-state lifecycle transitions. xfail per audit P4.""" + from web4.society import SocietyPhase # noqa: F401 (import only — vector references unsupported states) + + vector = _vec_by_id("bootstrap_vectors", "soc-002") + # The vector enumerates transitions across 5 states the SDK does not + # currently unify in a single enum. Touch each transition's "from" value + # against SocietyPhase membership to force the assertion to fail with a + # specific surface — strict xfail ensures we notice when the SDK gains a + # combined enum. + for transition in vector["transitions"]: + SocietyPhase(transition["from"]) + SocietyPhase(transition["to"]) + + +# ── Role vectors ───────────────────────────────────────────────── + + +def test_role_001_seven_base_mandatory() -> None: + """role-001: BASE_MANDATORY_ROLES contains exactly the 7 named roles.""" + vector = _vec_by_id("role_vectors", "role-001") + expected = vector["expected"] + + assert len(BASE_MANDATORY_ROLES) == expected["count"] + + role_values = [r.value for r in BASE_MANDATORY_ROLES] + assert sorted(role_values) == sorted(expected["roles"]) + + for role in BASE_MANDATORY_ROLES: + assert role.is_base_mandatory is True + + +def test_role_002_rotation_preserves_role_lct() -> None: + """role-002: rotate() changes filler, role-LCT stays the same.""" + vector = _vec_by_id("role_vectors", "role-002") + inp = vector["input"] + expected = vector["expected"] + + assignment = RoleAssignment( + role=SocietyRole(inp["role"]), + role_lct_id="lct:web4:role:policy_entity:001", + filling_entity_lct_id=inp["initial_filler"], + assigned_by="lct:web4:role:sovereign:001", + ) + original_role_lct = assignment.role_lct_id + + assignment.rotate(new_entity_lct_id=inp["new_filler"], rotated_by="lct:web4:role:sovereign:001") + + if expected["role_lct_unchanged"]: + assert assignment.role_lct_id == original_role_lct + + if expected["old_filler_no_longer_authorized"]: + assert assignment.is_authorized(inp["initial_filler"]) is False + + if expected["new_filler_authorized"]: + assert assignment.is_authorized(inp["new_filler"]) is True + + +def test_role_003_multi_holder_committee() -> None: + """role-003: multiple holders all authorized; multi_holder flag set.""" + vector = _vec_by_id("role_vectors", "role-003") + inp = vector["input"] + expected = vector["expected"] + + assignment = RoleAssignment( + role=SocietyRole(inp["role"]), + role_lct_id="lct:web4:role:witness:001", + filling_entity_lct_id=inp["primary"], + assigned_by="lct:web4:role:sovereign:001", + ) + for entity in inp["additional"]: + assert assignment.add_holder(entity) is True + + assert assignment.multi_holder is expected["multi_holder"] + assert len(assignment.all_holders) == expected["total_holders"] + + if expected["all_authorized"]: + assert assignment.is_authorized(inp["primary"]) is True + for entity in inp["additional"]: + assert assignment.is_authorized(entity) is True + + +@pytest.mark.xfail( + reason=( + "role-004 expects a role-based assigner-permission table " + "(only sovereign/administrator may assign). The Python SDK's role module " + "(role.py) does not encode this rule; it lives in `assigned_by` data, not " + "in a permission check. No specific audit item — surface gap surfaced by " + "operator's conformance vector." + ), + strict=True, +) +def test_role_004_assign_role_authorization() -> None: + """role-004: role-based assigner permissions. xfail — no SDK surface.""" + vector = _vec_by_id("role_vectors", "role-004") + + # If the SDK gains an `is_allowed_to_assign_roles(SocietyRole)` predicate, + # this is where it would be exercised. For now, the absence of such a + # predicate is the xfail. + from web4.role import is_allowed_to_assign_roles # noqa: F401 (intentional ImportError) + + for case in vector["cases"]: + del case # unreachable until SDK gains the surface + + +# ── Federation vectors ─────────────────────────────────────────── + + +@pytest.mark.xfail( + reason=( + "fed-001 expects imperative join_federation/secede actions on a society. " + "The Python SDK's federation.Society models hierarchy via the constructor " + "parameter `parent=Society` and the `children` list — no top-level join/" + "secede transitions. Different design axis; not a defect, but the conformance " + "vector assumes the imperative shape used by the Rust SDK." + ), + strict=True, +) +def test_fed_001_federation_lifecycle() -> None: + """fed-001: join/secede lifecycle. xfail — Python uses constructor-hierarchy pattern.""" + vector = _vec_by_id("federation_vectors", "fed-001") + + # If the SDK gains imperative join/secede on SocietyState, this is where the + # transitions would be exercised against vector["steps"]. + from web4.society import join_federation, secede_from_federation # noqa: F401 + + for step in vector["steps"]: + del step + + +# ── Minimum-viable vectors ─────────────────────────────────────── + + +def test_mvs_001_differentiation_fails() -> None: + """mvs-001: operational society with single filler fails differentiation check.""" + vector = _vec_by_id("minimum_viable_vectors", "mvs-001") + expected = vector["expected"] + + # Build all 7 base-mandatory roles, all filled by the same entity. + founder = "lct:web4:human:solo" + roles = bootstrap_society_roles(founder_lct_id=founder) + + errors = validate_minimum_viable(roles, is_operational=True) + + if not expected["valid"]: + assert errors # non-empty + joined = " ".join(errors).lower() + assert expected["error_contains"].lower() in joined + + +def test_mvs_002_missing_base_mandatory_fails() -> None: + """mvs-002: missing base-mandatory role fails validation.""" + vector = _vec_by_id("minimum_viable_vectors", "mvs-002") + expected = vector["expected"] + missing = vector["missing_role"] + + founder = "lct:web4:human:alice" + roles = bootstrap_society_roles(founder_lct_id=founder) + # Drop the archivist (or whichever role the vector names). + roles = [r for r in roles if r.role.value != missing] + + errors = validate_minimum_viable(roles, is_operational=False) + + if not expected["valid"]: + assert errors # non-empty + joined = " ".join(errors).lower() + assert expected["error_contains"].lower() in joined + + +# ── Suite-level meta ───────────────────────────────────────────── + + +def test_suite_metadata() -> None: + """The suite version and shape are part of the conformance contract.""" + assert SUITE["suite"] == "Society & Role Operations" + assert SUITE["version"] == "0.1.0" + + assert len(SUITE["bootstrap_vectors"]) == 2 + assert len(SUITE["role_vectors"]) == 4 + assert len(SUITE["federation_vectors"]) == 1 + assert len(SUITE["minimum_viable_vectors"]) == 2 + + +def test_all_vectors_have_ids() -> None: + """Every vector must have a stable id for cross-language reference.""" + seen: List[str] = [] + for category in ( + "bootstrap_vectors", + "role_vectors", + "federation_vectors", + "minimum_viable_vectors", + ): + for v in SUITE[category]: + assert "id" in v, f"vector missing id in {category}: {v}" + assert v["id"] not in seen, f"duplicate vector id: {v['id']}" + seen.append(v["id"])