diff --git a/SESSION_FOCUS.md b/SESSION_FOCUS.md index 90b7d92d..3998a3b2 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 51)* --- @@ -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 51 Summary: Minimum Viable Society Validation + Constraint Alignment (COMPLETE) + +| Task | Status | Notes | +|------|--------|-------| +| T1: validate_minimum_viable() + Constraint alignment | DONE | Resolves Sprint 49 audit P5+P6. `validate_minimum_viable()` added to `web4/role.py`: checks 7 base-mandatory roles, ≥2 distinct fillers (operational), witnessing capacity (operational). `Constraint` dataclass aligned with Rust: `threshold: float` + `hard: bool` replacing untyped `value: Any`. 1 new export (369 total), 14 new tests (2670 total), 0 new files. JSON schema + test vectors updated. | + ### Sprint 50 Summary: Add SocietyRole + RoleAssignment to Python SDK (COMPLETE) | Task | Status | Notes | @@ -196,9 +202,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**: 2656 passing +- **Tests**: 2670 passing - **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 bf869909..d3263edb 100644 --- a/docs/SPRINT.md +++ b/docs/SPRINT.md @@ -1,12 +1,48 @@ # Web4 Sprint Plan **Created**: 2026-03-14 -**Updated**: 2026-05-14 (Sprint 50) +**Updated**: 2026-05-14 (Sprint 51) **Phase**: Development **Track**: web4 (Legion) --- +## Sprint 51: Minimum Viable Society Validation + Constraint Alignment (2026-05-14) + +Resolves two remaining autonomous-actionable items from the Sprint 49 +cross-language audit fix queue: P5 (validate_minimum_viable) and P6 +(Constraint threshold + hard flag alignment). + +### T1: validate_minimum_viable() + Constraint alignment +**Status**: DONE +**Completed**: 2026-05-14 +**Authorized by**: Sprint 49 audit fix queue P5+P6. Policy-reviewed and approved. +**Scope**: +Two changes to advance cross-language parity: + +1. **P5 — `validate_minimum_viable()`** (MEDIUM): New function in `web4/role.py` + implementing `inter-society-protocol.md` §6.2 semantic viability checks. + Takes a list of `RoleAssignment` and an `is_operational` flag. Checks: + (a) all 7 base-mandatory roles are filled, (b) ≥2 distinct role-filling + entities when operational, (c) Witness or Auditor role assigned when + operational. Returns list of error strings. Cross-language parity with + `web4-core/src/society.rs::validate_minimum_viable()`. + +2. **P6 — Constraint alignment** (MEDIUM): Updated `Constraint` dataclass in + `web4/r6.py`: replaced untyped `value: Any` with `threshold: float` + + `hard: bool = True`, matching `web4-core/src/r6.rs::Constraint`. Updated + `to_dict()`, `from_dict()` (backward-compatible: accepts legacy `value` + key), `Rules.check_constraint()`, JSON schemas, and test vectors. + +**Result**: 1 new export (369 total), 14 new tests (2670 total), 0 new files. +mypy --strict clean, ruff lint/format clean. JSON schema and test vectors +updated for new Constraint wire format. + +**Remaining from audit**: P4 (MetabolicState reconciliation — needs operator +decision), P7 (SocietyState role integration — needs operator decision). + +--- + ## Sprint 50: Add SocietyRole + RoleAssignment to Python SDK (2026-05-14) Implements the top 3 items from the Sprint 49 cross-language audit fix queue: diff --git a/web4-standard/implementation/sdk/tests/test_r6.py b/web4-standard/implementation/sdk/tests/test_r6.py index a996bea8..0c5ed670 100644 --- a/web4-standard/implementation/sdk/tests/test_r6.py +++ b/web4-standard/implementation/sdk/tests/test_r6.py @@ -80,7 +80,7 @@ def test_prohibition_overrides_permission(self): def test_constraint_minimum(self): rules = Rules( constraints=[ - Constraint(constraint_type="atp_minimum", value=50), + Constraint(constraint_type="atp_minimum", threshold=50), ] ) assert rules.check_constraint("atp_minimum", 100) @@ -90,7 +90,7 @@ def test_constraint_minimum(self): def test_constraint_maximum(self): rules = Rules( constraints=[ - Constraint(constraint_type="rate_limit", value=100), + Constraint(constraint_type="rate_limit", threshold=100), ] ) assert rules.check_constraint("rate_limit", 50) @@ -100,7 +100,7 @@ def test_constraint_maximum(self): def test_no_matching_constraint(self): rules = Rules( constraints=[ - Constraint(constraint_type="atp_minimum", value=50), + Constraint(constraint_type="atp_minimum", threshold=50), ] ) # No constraint for "rate_limit" → passes @@ -856,8 +856,8 @@ def _make_full_action(self) -> R7Action: law_hash="sha256:governance_v2", society="lct:web4:society:genesis", constraints=[ - Constraint(constraint_type="atp_minimum", value=50), - Constraint(constraint_type="rate_limit", value=100), + Constraint(constraint_type="atp_minimum", threshold=50), + Constraint(constraint_type="rate_limit", threshold=100), ], permissions=["read", "analyze"], prohibitions=["delete"], @@ -1296,7 +1296,7 @@ def test_full_action_validates(self): rules=Rules( law_hash="sha256:abc", society="lct:society:genesis", - constraints=[Constraint(constraint_type="atp_minimum", value=50)], + constraints=[Constraint(constraint_type="atp_minimum", threshold=50)], permissions=["read"], ), role=Role( @@ -1394,7 +1394,7 @@ def _make_full_action(self): rules=Rules( law_hash="lh1", society="soc1", - constraints=[Constraint("trust_min", 0.5)], + constraints=[Constraint("trust_min", 0.5, True)], permissions=["read"], prohibitions=["delete"], ), diff --git a/web4-standard/implementation/sdk/tests/test_r6_roundtrip.py b/web4-standard/implementation/sdk/tests/test_r6_roundtrip.py index 22e8cef8..d0ec403b 100644 --- a/web4-standard/implementation/sdk/tests/test_r6_roundtrip.py +++ b/web4-standard/implementation/sdk/tests/test_r6_roundtrip.py @@ -26,17 +26,30 @@ class TestConstraintRoundTrip: def test_basic(self) -> None: - c = Constraint(constraint_type="rate_limit", value=100) + c = Constraint(constraint_type="rate_limit", threshold=100) assert Constraint.from_dict(c.to_dict()) == c - def test_string_value(self) -> None: - c = Constraint(constraint_type="witness_required", value="true") + def test_hard_default(self) -> None: + c = Constraint(constraint_type="witness_quorum", threshold=3.0) + assert c.hard is True + assert Constraint.from_dict(c.to_dict()) == c + + def test_soft_constraint(self) -> None: + c = Constraint(constraint_type="rate_limit", threshold=100, hard=False) + assert c.hard is False assert Constraint.from_dict(c.to_dict()) == c def test_float_value(self) -> None: - c = Constraint(constraint_type="atp_minimum", value=50.5) + c = Constraint(constraint_type="atp_minimum", threshold=50.5) assert Constraint.from_dict(c.to_dict()) == c + def test_legacy_value_key(self) -> None: + """from_dict accepts legacy 'value' key for backward compatibility.""" + d = {"type": "atp_minimum", "value": 42} + c = Constraint.from_dict(d) + assert c.threshold == 42.0 + assert c.hard is True + class TestRulesRoundTrip: def test_empty(self) -> None: @@ -48,7 +61,7 @@ def test_full(self) -> None: law_hash="abc123", society="test-society", constraints=[ - Constraint("rate_limit", 100), + Constraint("rate_limit", 100.0), Constraint("atp_minimum", 10.0), ], permissions=["read", "write"], diff --git a/web4-standard/implementation/sdk/tests/test_role.py b/web4-standard/implementation/sdk/tests/test_role.py index 2f67f688..c4add531 100644 --- a/web4-standard/implementation/sdk/tests/test_role.py +++ b/web4-standard/implementation/sdk/tests/test_role.py @@ -498,9 +498,7 @@ def test_context_mandatory_excluded(self) -> None: # ── validate_minimum_viable ─────────────────────────────────── -def _make_assignment( - role: SocietyRole, filler: str = "entity-001" -) -> RoleAssignment: +def _make_assignment(role: SocietyRole, filler: str = "entity-001") -> RoleAssignment: """Helper to create a minimal RoleAssignment for testing.""" return RoleAssignment( role=role, @@ -528,11 +526,7 @@ def test_empty_roles_fails(self) -> None: def test_missing_one_base_mandatory(self) -> None: """Missing a single base-mandatory role is reported.""" - roles = [ - _make_assignment(r) - for r in BASE_MANDATORY_ROLES - if r != SocietyRole.TREASURER - ] + roles = [_make_assignment(r) for r in BASE_MANDATORY_ROLES if r != SocietyRole.TREASURER] errors = validate_minimum_viable(roles) assert len(errors) == 1 assert "treasurer" in errors[0] diff --git a/web4-standard/implementation/sdk/web4/__init__.py b/web4-standard/implementation/sdk/web4/__init__.py index 170c4227..7425e0ce 100644 --- a/web4-standard/implementation/sdk/web4/__init__.py +++ b/web4-standard/implementation/sdk/web4/__init__.py @@ -27,7 +27,7 @@ - Deserialization — generic JSON-LD dispatcher for all Web4 types - Generation — produce minimal valid JSON-LD documents for any Web4 type -23 modules + MCP server, 368 exports, 2627 tests, 3 behavioral functions, 8 MCP tools, 7 CLI subcommands. +23 modules + MCP server, 369 exports, 2627 tests, 3 behavioral functions, 8 MCP tools, 7 CLI subcommands. v0.26.0: CI quality gates (strict mypy, ruff lint, ruff format) enforced on every PR. These modules define the canonical data types and algorithms specified in the web4-standard. They work offline (no network services required) and are designed to be @@ -397,6 +397,7 @@ SocietyRole, RoleAssignment, bootstrap_society_roles, + validate_minimum_viable, BASE_MANDATORY_ROLES, ) @@ -817,6 +818,7 @@ "SocietyRole", "RoleAssignment", "bootstrap_society_roles", + "validate_minimum_viable", "BASE_MANDATORY_ROLES", # security "CryptoSuiteId", diff --git a/web4-standard/implementation/sdk/web4/r6.py b/web4-standard/implementation/sdk/web4/r6.py index c8335099..013af40c 100644 --- a/web4-standard/implementation/sdk/web4/r6.py +++ b/web4-standard/implementation/sdk/web4/r6.py @@ -124,19 +124,45 @@ class ReputationComputationError(R7Error): @dataclass(frozen=True) class Constraint: - """A single constraint within Rules.""" + """A single constraint within Rules. - constraint_type: str # e.g. "rate_limit", "atp_minimum", "witness_required" - value: Any # threshold or limit + Cross-language parity with ``web4-core/src/r6.rs::Constraint``. + + Attributes: + constraint_type: Constraint kind (e.g. "rate_limit", "min_atp", + "witness_quorum"). + threshold: Numeric threshold value for the constraint. + hard: If True the constraint blocks execution; if False it warns + but allows the action to proceed. Defaults to True. + """ + + constraint_type: str + threshold: float + hard: bool = True def to_dict(self) -> Dict[str, Any]: - """Serialize to dict with 'type' and 'value' keys.""" - return {"type": self.constraint_type, "value": self.value} + """Serialize to dict.""" + return { + "type": self.constraint_type, + "threshold": self.threshold, + "hard": self.hard, + } @classmethod def from_dict(cls, d: Dict[str, Any]) -> "Constraint": - """Deserialize from dict produced by to_dict().""" - return cls(constraint_type=d["type"], value=d["value"]) + """Deserialize from dict. + + Accepts either ``threshold`` (current) or ``value`` (legacy) key + for the numeric threshold. + """ + raw = d.get("threshold", d.get("value")) + if raw is None: + raise KeyError("Constraint dict must contain 'threshold' or 'value'") + return cls( + constraint_type=d["type"], + threshold=float(raw), + hard=d.get("hard", True), + ) @dataclass @@ -165,8 +191,9 @@ def check_constraint(self, constraint_type: str, actual_value: float) -> bool: """Check a specific constraint. Returns True if no constraint of that type exists.""" for c in self.constraints: if c.constraint_type == constraint_type: - if isinstance(c.value, (int, float)): - return actual_value >= c.value if constraint_type.endswith("_minimum") else actual_value <= c.value + return ( + actual_value >= c.threshold if constraint_type.endswith("_minimum") else actual_value <= c.threshold + ) return True def to_dict(self) -> Dict[str, Any]: @@ -940,9 +967,7 @@ def from_jsonld(cls, doc: Dict[str, Any]) -> R7Action: """ # 1. Rules rules_data = doc.get("rules", {}) - constraints = [ - Constraint(constraint_type=c["type"], value=c["value"]) for c in rules_data.get("constraints", []) - ] + constraints = [Constraint.from_dict(c) for c in rules_data.get("constraints", [])] rules = Rules( law_hash=rules_data.get("lawHash", ""), society=rules_data.get("society", ""), diff --git a/web4-standard/implementation/sdk/web4/role.py b/web4-standard/implementation/sdk/web4/role.py index e3349139..735bab17 100644 --- a/web4-standard/implementation/sdk/web4/role.py +++ b/web4-standard/implementation/sdk/web4/role.py @@ -381,18 +381,12 @@ def validate_minimum_viable( # 2. Internal differentiation — at least 2 distinct role-filling entities unique_fillers = {ra.filling_entity_lct_id for ra in roles} if len(unique_fillers) < 2: - errors.append( - "Minimum viable society requires at least 2 distinct" - " role-filling entities" - ) + errors.append("Minimum viable society requires at least 2 distinct role-filling entities") # 3. Witnessing capacity — Witness or Auditor role must be assigned has_witness = SocietyRole.WITNESS in assigned_roles has_auditor = SocietyRole.AUDITOR in assigned_roles if not has_witness and not has_auditor: - errors.append( - "Minimum viable society requires witnessing capacity" - " (Witness or Auditor role)" - ) + errors.append("Minimum viable society requires witnessing capacity (Witness or Auditor role)") return errors diff --git a/web4-standard/implementation/sdk/web4/schema_registry.json b/web4-standard/implementation/sdk/web4/schema_registry.json index e0834c72..ee9d5f40 100644 --- a/web4-standard/implementation/sdk/web4/schema_registry.json +++ b/web4-standard/implementation/sdk/web4/schema_registry.json @@ -2564,13 +2564,18 @@ "type": "object", "required": [ "type", - "value" + "threshold" ], "properties": { "type": { "type": "string" }, - "value": {} + "threshold": { + "type": "number" + }, + "hard": { + "type": "boolean" + } }, "additionalProperties": false }, diff --git a/web4-standard/schemas/r7-action-jsonld.schema.json b/web4-standard/schemas/r7-action-jsonld.schema.json index 869426c8..b266a19b 100644 --- a/web4-standard/schemas/r7-action-jsonld.schema.json +++ b/web4-standard/schemas/r7-action-jsonld.schema.json @@ -58,10 +58,11 @@ "type": "array", "items": { "type": "object", - "required": ["type", "value"], + "required": ["type", "threshold"], "properties": { "type": { "type": "string" }, - "value": {} + "threshold": { "type": "number" }, + "hard": { "type": "boolean" } }, "additionalProperties": false }, diff --git a/web4-standard/test-vectors/schema-validation/r7-action-jsonld-validation.json b/web4-standard/test-vectors/schema-validation/r7-action-jsonld-validation.json index 0b5624a7..45409fa6 100644 --- a/web4-standard/test-vectors/schema-validation/r7-action-jsonld-validation.json +++ b/web4-standard/test-vectors/schema-validation/r7-action-jsonld-validation.json @@ -48,8 +48,8 @@ "lawHash": "sha256:law_hash_002", "society": "lct:web4:society:research", "constraints": [ - {"type": "time_limit", "value": 3600}, - {"type": "budget_cap", "value": 100} + {"type": "time_limit", "threshold": 3600, "hard": true}, + {"type": "budget_cap", "threshold": 100, "hard": true} ], "permissions": ["read", "analyze"], "prohibitions": ["delete", "modify"]