#!/usr/bin/env python3 """Audit issue 80's native RLV callback and service orchestration boundary.""" from __future__ import annotations import json import re from pathlib import Path import generate_api_shims import generate_surface ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "crates" / "libremetaverse-rlv" / "src" / "service.rs" STATE = ROOT / "crates" / "libremetaverse-rlv" / "src" / "state.rs" GENERATED = ROOT / "crates" / "libremetaverse-rlv" / "src" / "generated.rs" SUPPORT = ROOT / "tests" / "compat" / "src" / "rlv_support.rs" QUERY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_query_basics_semantics.rs" ACTION_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_attach_commands_semantics.rs" NOTIFY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_notify_semantics.rs" DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" CATALOG = ROOT / "api" / "public-api.json" STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") TYPES = { "T:LibreMetaverse.RLV.AttachmentRequest": "crate::service::AttachmentRequest", "T:LibreMetaverse.RLV.RlvActionCallbacksDefault": "crate::service::RlvActionCallbacksDefault", "T:LibreMetaverse.RLV.RlvCallbacksDefault": "crate::service::RlvCallbacksDefault", "T:LibreMetaverse.RLV.RlvCommandProcessor": "crate::service::RlvCommandProcessor", "T:LibreMetaverse.RLV.RlvService": "crate::service::RlvService", } def require_markers(path: Path, markers: tuple[str, ...]) -> None: text = path.read_text() missing = [marker for marker in markers if marker not in text] if missing: raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing)) def catalog_members() -> set[str]: catalog = json.loads(CATALOG.read_text()) assembly = next( value for value in catalog["assemblies"] if value["identity"]["name"] == "LibreMetaverse.RLV" ) return { member["doc_id"] for api_type in assembly["types"] if api_type["doc_id"] in TYPES for member in api_type["members"] } def generated_type_block(text: str, doc_id: str) -> str: marker = f"/// C# type: `{doc_id}`." start = text.find(marker) if start < 0: raise SystemExit(f"generated declaration is missing for {doc_id}") next_type = text.find("/// C# type:", start + len(marker)) return text[start:] if next_type < 0 else text[start:next_type] def main() -> None: for api_type, declaration in TYPES.items(): if generate_api_shims.NATIVE_TYPES.get(api_type) != declaration: raise SystemExit(f"issue 80 native type mapping is missing for {api_type}") members = catalog_members() if len(members) != 53: raise SystemExit(f"issue 80 expected 53 mapped members, found {len(members)}") source = SOURCE.read_text() if STUB_RE.search(source): raise SystemExit("issue 80 owned Rust stubs remain in service.rs") if re.search(r"\b(?:Mutex|RwLock|read_lock|write_lock)\b", source): raise SystemExit("service.rs must not acquire internal locks around host callbacks") generated = GENERATED.read_text() for api_type in TYPES: if STUB_RE.search(generated_type_block(generated, api_type)): raise SystemExit(f"issue 80 owned generated stubs remain for {api_type}") if "pub trait IRlvActionCallbacks: Send + Sync" not in generated: raise SystemExit("action callbacks are not thread-safe") if "pub trait IRlvQueryCallbacks: Send + Sync" not in generated: raise SystemExit("query callbacks are not thread-safe") catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text()) rlv_cases = [ case for case in catalog["tests"] if case.get("rust_file", "").startswith("tests/compat/tests/rlv_") ] if len(rlv_cases) != 616 or len({case["rust_file"] for case in rlv_cases}) != 24: raise SystemExit("issue 80 RLV parity catalog is not the complete 616-case slice") markers = generate_surface.scan_parity_markers(ROOT) evidence_keys = ("status", "rust_file", "rust_line", "rust_test", "rust_body_sha256") for case in rlv_cases: marker = markers.get(case["id"], []) if len(marker) != 1 or any(marker[0][key] != case[key] for key in evidence_keys): raise SystemExit(f"issue 80 parity metadata is stale for {case['id']}") require_markers(SOURCE, ( "MAX_RLV_MESSAGE_BYTES", "MAX_RLV_COMMANDS", "pub struct AttachmentRequest", "pub struct RlvActionCallbacksDefault", "pub struct RlvCallbacksDefault", "pub struct RlvCommandProcessor", "pub struct RlvService", "process_instant_message", "process_message", "process_restriction", "send_restriction_notification", "send_notification", "refresh_inventory", "report_inventory_offer_accepted", "report_item_attached", "report_send_public_message", "report_sit", "throw_if_cancellation_requested", "default_callbacks_observe_cancellation_without_side_effects", "attachment_request_hash_is_stable_and_value_based", )) require_markers(STATE, ( "set_action_callbacks", "notify_removed_restriction", "let removed = {", "let callbacks = read_lock", ".send_reply(", )) require_markers(SUPPORT, ( "struct RecordingActions", "struct RecordingQueries", "struct RlvHarness", "inventory_map.clone()", "RlvService::new", )) require_markers(QUERY_TESTS, ( "manual_blacklist_all", "manual_version", "get_environment_known_setting", "get_status_all_senders", "get_current_sit_id", )) require_markers(ACTION_TESTS, ( "attach_this_by_id", "attach_all_this_recursive", "attach_over_or_replace_uses_position_from_folder_name", )) require_markers(NOTIFY_TESTS, ( "notify_every_restriction", "notify_multiple_channels_filtered", "notify_inventory_offer", "notify_sit_and_stand_legal", )) require_markers(DOC, ( "Callback and service contract", "Send + Sync", "after releasing every manager lock", "completed inventory and agent", "616", "ubuntu-latest", )) require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_80.py",)) print( "issue 80 audit: native callbacks, typed actions and queries, bounded service " "dispatch, inventory/agent adapters, cancellation, post-lock notifications, " "recording-fake workflows, compatibility evidence, and docs are present" ) if __name__ == "__main__": main()