diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 4955b9c1c..d615eb41b 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -3558,7 +3558,8 @@ "lane": "L7", "contract": "docs/release-control/v6/internal/subsystems/relay-runtime.md", "owned_prefixes": [ - "internal/relay/" + "internal/relay/", + "pulse-mobile:src/relay/" ], "owned_files": [ "internal/config/persistence_relay.go" @@ -3568,7 +3569,15 @@ "test_prefixes": [], "exact_files": [ "internal/config/persistence_relay_test.go", - "internal/relay/client_test.go" + "internal/relay/client_test.go", + "pulse-mobile:src/relay/__tests__/channel.test.ts", + "pulse-mobile:src/relay/__tests__/client-hardening.test.ts", + "pulse-mobile:src/relay/__tests__/client.test.ts", + "pulse-mobile:src/relay/__tests__/encryption.test.ts", + "pulse-mobile:src/relay/__tests__/identity.test.ts", + "pulse-mobile:src/relay/__tests__/protocol-contract.test.ts", + "pulse-mobile:src/relay/__tests__/protocol.test.ts", + "pulse-mobile:src/relay/__tests__/proxy.test.ts" ], "require_explicit_path_policy_coverage": true, "path_policies": [ @@ -3597,6 +3606,26 @@ "exact_files": [ "internal/config/persistence_relay_test.go" ] + }, + { + "id": "mobile-relay-runtime", + "label": "mobile relay runtime proof", + "match_prefixes": [ + "pulse-mobile:src/relay/" + ], + "match_files": [], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "pulse-mobile:src/relay/__tests__/channel.test.ts", + "pulse-mobile:src/relay/__tests__/client-hardening.test.ts", + "pulse-mobile:src/relay/__tests__/client.test.ts", + "pulse-mobile:src/relay/__tests__/encryption.test.ts", + "pulse-mobile:src/relay/__tests__/identity.test.ts", + "pulse-mobile:src/relay/__tests__/protocol-contract.test.ts", + "pulse-mobile:src/relay/__tests__/protocol.test.ts", + "pulse-mobile:src/relay/__tests__/proxy.test.ts" + ] } ] } diff --git a/docs/release-control/v6/internal/subsystems/relay-runtime.md b/docs/release-control/v6/internal/subsystems/relay-runtime.md index af3b21bf7..814d80601 100644 --- a/docs/release-control/v6/internal/subsystems/relay-runtime.md +++ b/docs/release-control/v6/internal/subsystems/relay-runtime.md @@ -15,15 +15,19 @@ ## Purpose -Own the desktop relay runtime, its persisted client configuration, and the -canonical reconnect, encryption, and relay-trust behavior for Pulse instance -bridging. +Own the desktop and mobile relay runtimes, their persisted relay state +boundaries, and the canonical reconnect, encryption, protocol, proxy, and +relay-trust behavior for Pulse instance bridging. ## Canonical Files 1. `internal/relay/client.go` 2. `internal/relay/protocol.go` 3. `internal/config/persistence_relay.go` +4. `pulse-mobile:src/relay/client.ts` +5. `pulse-mobile:src/relay/protocol.ts` +6. `pulse-mobile:src/relay/proxy.ts` +7. `pulse-mobile:src/relay/encryption.ts` ## Shared Boundaries @@ -34,7 +38,8 @@ bridging. 1. Add or change desktop relay reconnect, registration, drain, proxy-stream, or encrypted channel behavior through `internal/relay/` 2. Add or change relay control payload schemas, including mobile-visible push notification metadata, through `internal/relay/protocol.go` 3. Add or change persisted relay enablement, server URL, or reconnect-safe default loading through `internal/config/persistence_relay.go` -4. Keep desktop relay changes aligned with the governed mobile and server relay surfaces represented by the L7 lane evidence +4. Add or change mobile relay reconnect, drain, channel, encryption, proxy, or identity behavior through `pulse-mobile:src/relay/` +5. Keep desktop and mobile relay changes aligned with the governed server relay surfaces represented by the L7 lane evidence ## Forbidden Paths @@ -47,12 +52,14 @@ bridging. 1. Update this contract when new desktop relay runtime or persistence entry points become canonical 2. Keep relay client changes tied to explicit runtime proof in `internal/relay/client_test.go` 3. Keep persisted relay config loading tied to explicit proof in `internal/config/persistence_relay_test.go` +4. Keep mobile relay runtime changes tied to explicit proof in `pulse-mobile:src/relay/__tests__/` ## Current State -The desktop relay client had been carrying real runtime behavior without any -explicit subsystem ownership even though L7 already treats relay readiness as a -governed release lane. This contract makes that desktop boundary explicit. +The relay lane had been carrying real desktop and mobile runtime behavior +without fully explicit subsystem ownership even though L7 already treats relay +readiness as a governed release lane. This contract makes both relay-runtime +boundaries explicit. The canonical relay runtime now includes local CA-bundle trust via `SSL_CERT_FILE`, so self-hosted relay deployments can use a private CA without forking the dial path or disabling TLS verification globally. @@ -86,3 +93,7 @@ the runtime can read legacy plaintext relay settings, it must rewrite canonical encrypted storage immediately on load instead of leaving `instance_secret` or the relay identity private key on disk as a normal runtime path. +The mobile relay runtime is part of the same owned surface: reconnect drain +failover hints are one-shot recovery instructions, not permanent relay URL +overrides, so a successful failover reconnect must return future reconnects to +the instance's canonical relay URL unless the server sends a fresh drain hint. diff --git a/scripts/release_control/canonical_completion_guard_test.py b/scripts/release_control/canonical_completion_guard_test.py index 4da31c951..2c74ad446 100644 --- a/scripts/release_control/canonical_completion_guard_test.py +++ b/scripts/release_control/canonical_completion_guard_test.py @@ -18,41 +18,65 @@ from canonical_completion_guard import ( subsystem_matches_path, ) +WORKSPACE_REPOS_ROOT = REPO_ROOT.parent + + +def split_workspace_path(path: str) -> tuple[Path, str]: + if ":" not in path: + return REPO_ROOT, path + repo_id, rel = path.split(":", 1) + return WORKSPACE_REPOS_ROOT / repo_id, rel + + +def qualify_workspace_path(repo_root: Path, rel: str) -> str: + if repo_root == REPO_ROOT: + return rel + return f"{repo_root.name}:{rel}" + def owned_runtime_files(rule: dict) -> list[str]: owned: set[str] = set() search_roots: set[Path] = set() for prefix in rule.get("owned_prefixes", []): - root = REPO_ROOT / prefix + repo_root, rel_prefix = split_workspace_path(prefix.rstrip("/")) + root = repo_root / rel_prefix if root.exists(): search_roots.add(root if root.is_dir() else root.parent) continue parent = root.parent - while parent != REPO_ROOT and not parent.exists(): + while parent != repo_root and not parent.exists(): parent = parent.parent if parent.exists(): search_roots.add(parent) for root in search_roots: + repo_root = root + while repo_root != WORKSPACE_REPOS_ROOT and repo_root.parent != WORKSPACE_REPOS_ROOT: + repo_root = repo_root.parent + if repo_root.parent != WORKSPACE_REPOS_ROOT: + repo_root = REPO_ROOT for path in root.rglob("*"): if not path.is_file(): continue - rel = path.relative_to(REPO_ROOT).as_posix() - if is_test_or_fixture(rel) or is_ignored_runtime_file(rel): + rel = path.relative_to(repo_root).as_posix() + candidate = qualify_workspace_path(repo_root, rel) + if is_test_or_fixture(candidate) or is_ignored_runtime_file(candidate): continue - if subsystem_matches_path(rule, rel): - owned.add(rel) + if subsystem_matches_path(rule, candidate): + owned.add(candidate) for rel in rule.get("owned_files", []): - path = REPO_ROOT / rel + repo_root, repo_rel = split_workspace_path(rel) + path = repo_root / repo_rel if not path.exists() or not path.is_file(): continue - if is_test_or_fixture(rel) or is_ignored_runtime_file(rel): + candidate = qualify_workspace_path(repo_root, repo_rel) + if is_test_or_fixture(candidate) or is_ignored_runtime_file(candidate): continue - if subsystem_matches_path(rule, rel): - owned.add(rel) + if subsystem_matches_path(rule, candidate): + owned.add(candidate) return sorted(owned) @@ -220,6 +244,42 @@ class CanonicalCompletionGuardTest(unittest.TestCase): ], ) + def test_mobile_relay_runtime_change_requires_relay_contract(self): + required = infer_impacted_subsystems(["pulse-mobile:src/relay/client.ts"]) + self.assertEqual(set(required), {"relay-runtime"}) + + relay = required["relay-runtime"] + self.assertEqual( + relay["contract"], + "docs/release-control/v6/internal/subsystems/relay-runtime.md", + ) + self.assertEqual( + relay["touched_runtime_files"], + ["pulse-mobile:src/relay/client.ts"], + ) + self.assertEqual( + relay["verification_requirements"], + [ + { + "id": "mobile-relay-runtime", + "label": "mobile relay runtime proof", + "touched_runtime_files": ["pulse-mobile:src/relay/client.ts"], + "allow_same_subsystem_tests": False, + "test_prefixes": [], + "exact_files": [ + "pulse-mobile:src/relay/__tests__/channel.test.ts", + "pulse-mobile:src/relay/__tests__/client-hardening.test.ts", + "pulse-mobile:src/relay/__tests__/client.test.ts", + "pulse-mobile:src/relay/__tests__/encryption.test.ts", + "pulse-mobile:src/relay/__tests__/identity.test.ts", + "pulse-mobile:src/relay/__tests__/protocol-contract.test.ts", + "pulse-mobile:src/relay/__tests__/protocol.test.ts", + "pulse-mobile:src/relay/__tests__/proxy.test.ts", + ], + } + ], + ) + def test_windows_install_script_change_uses_shared_installer_policies(self): required = infer_impacted_subsystems(["scripts/install.ps1"]) self.assertEqual(set(required), {"agent-lifecycle", "deployment-installability"}) diff --git a/scripts/release_control/contract_audit.py b/scripts/release_control/contract_audit.py index 9b96efafa..63f8ffadb 100644 --- a/scripts/release_control/contract_audit.py +++ b/scripts/release_control/contract_audit.py @@ -66,6 +66,7 @@ PATH_SUFFIXES = ( ".yaml", ".yml", ) +WORKSPACE_REPOS_ROOT = REPO_ROOT.parent def sorted_casefold(values: list[str]) -> list[str]: @@ -107,17 +108,61 @@ def looks_like_repo_path(token: str) -> bool: return "/" in candidate or candidate.endswith(PATH_SUFFIXES) -def validate_repo_path_token(token: str, *, rel: str, heading: str, errors: list[str]) -> None: +def repo_roots_for_status(status_payload: dict[str, Any]) -> dict[str, Path]: + active_repos = [ + repo_id + for repo_id in status_payload.get("scope", {}).get("active_repos", []) + if isinstance(repo_id, str) and repo_id.strip() + ] + repo_roots = {REPO_ROOT.name: REPO_ROOT} + for repo_id in active_repos: + if repo_id == REPO_ROOT.name: + continue + repo_roots[repo_id] = WORKSPACE_REPOS_ROOT / repo_id + return repo_roots + + +def resolve_repo_path_token(token: str, *, repo_roots: dict[str, Path]) -> Path | None: raw = token.rstrip("/") if token.endswith("/") else token if not raw: - errors.append(f"{rel} {heading} contains non-clean repo-relative path {token!r}") - return + return None + repo_id = REPO_ROOT.name + repo_rel = raw + if ":" in raw: + repo_id, repo_rel = raw.split(":", 1) + if not repo_id: + return None candidate = Path(raw) normalized = candidate.as_posix() if candidate.is_absolute() or raw.startswith("../") or "/../" in raw or normalized != raw: + return None + repo_candidate = Path(repo_rel) + repo_normalized = repo_candidate.as_posix() + if ( + repo_candidate.is_absolute() + or repo_rel.startswith("../") + or "/../" in repo_rel + or repo_normalized != repo_rel + ): + return None + repo_root = repo_roots.get(repo_id) + if repo_root is None: + return None + return repo_root / repo_rel + + +def validate_repo_path_token( + token: str, + *, + rel: str, + heading: str, + errors: list[str], + repo_roots: dict[str, Path], +) -> None: + resolved = resolve_repo_path_token(token, repo_roots=repo_roots) + if resolved is None: errors.append(f"{rel} {heading} contains non-clean repo-relative path {token!r}") return - resolved = REPO_ROOT / raw if not resolved.exists(): errors.append(f"{rel} {heading} references missing path {token!r}") return @@ -151,7 +196,12 @@ def parse_contract_metadata(body_lines: list[str]) -> tuple[dict[str, Any] | Non return payload, errors -def audit_contract_text(rel: str, content: str) -> tuple[dict[str, Any], list[str]]: +def audit_contract_text( + rel: str, + content: str, + *, + repo_roots: dict[str, Path], +) -> tuple[dict[str, Any], list[str]]: errors: list[str] = [] path_references: list[dict[str, str]] = [] section_items: dict[str, list[str]] = {} @@ -200,13 +250,25 @@ def audit_contract_text(rel: str, content: str) -> tuple[dict[str, Any], list[st errors.append(f"{rel} section {heading!r} entries must include at least one repo path") continue for token in path_tokens: - validate_repo_path_token(token, rel=rel, heading=heading, errors=errors) + validate_repo_path_token( + token, + rel=rel, + heading=heading, + errors=errors, + repo_roots=repo_roots, + ) path_references.append({"heading": heading, "path": token}) if heading == "## Extension Points": for _, item in items: for token in re.findall(r"`([^`]+)`", item): if looks_like_repo_path(token): - validate_repo_path_token(token, rel=rel, heading=heading, errors=errors) + validate_repo_path_token( + token, + rel=rel, + heading=heading, + errors=errors, + repo_roots=repo_roots, + ) path_references.append({"heading": heading, "path": token}) return { @@ -284,6 +346,7 @@ def audit_contract_payload( for subsystem in registry_subsystems if isinstance(subsystem, dict) and isinstance(subsystem.get("contract"), str) } + repo_roots = repo_roots_for_status(status_payload) expected_subsystem_ids = { str(subsystem.get("id", "")).strip() for subsystem in registry_subsystems @@ -302,7 +365,7 @@ def audit_contract_payload( seen_subsystem_ids: set[str] = set() for rel in sorted(actual_contracts): - parsed, parse_errors = audit_contract_text(rel, contract_texts[rel]) + parsed, parse_errors = audit_contract_text(rel, contract_texts[rel], repo_roots=repo_roots) errors.extend(parse_errors) metadata = parsed.get("metadata") subsystem = expected_contracts.get(rel) @@ -420,7 +483,13 @@ def audit_contract_payload( ) continue shared_path = path_tokens[0] - validate_repo_path_token(shared_path, rel=rel, heading="## Shared Boundaries", errors=errors) + validate_repo_path_token( + shared_path, + rel=rel, + heading="## Shared Boundaries", + errors=errors, + repo_roots=repo_roots, + ) actual_shared_paths.append(shared_path) if shared_path in seen_shared_paths: errors.append( diff --git a/scripts/release_control/contract_audit_test.py b/scripts/release_control/contract_audit_test.py index eebfb8be1..f706a1e4b 100644 --- a/scripts/release_control/contract_audit_test.py +++ b/scripts/release_control/contract_audit_test.py @@ -295,6 +295,76 @@ Canonical alert identity is live runtime truth. "\n".join(report["errors"]), ) + def test_audit_contract_payload_accepts_cross_repo_contract_paths(self) -> None: + registry_payload = { + "subsystems": [ + { + "id": "relay-runtime", + "lane": "L7", + "contract": "docs/release-control/v6/internal/subsystems/relay-runtime.md", + } + ] + } + status_payload = { + "scope": { + "active_repos": ["pulse", "pulse-mobile"], + }, + "lanes": [{"id": "L7"}], + } + contract_texts = { + "docs/release-control/v6/internal/subsystems/relay-runtime.md": """# Relay Runtime Contract + +## Contract Metadata + +```json +{ + "subsystem_id": "relay-runtime", + "lane": "L7", + "contract_file": "docs/release-control/v6/internal/subsystems/relay-runtime.md", + "status_file": "docs/release-control/v6/internal/status.json", + "registry_file": "docs/release-control/v6/internal/subsystems/registry.json", + "dependency_subsystem_ids": [] +} +``` + +## Purpose + +Own relay runtime truth. + +## Canonical Files + +1. `pulse-mobile:src/relay/client.ts` + +## Shared Boundaries + +1. None. + +## Extension Points + +1. Add mobile relay reconnect behavior through `pulse-mobile:src/relay/` + +## Forbidden Paths + +1. Ad hoc mobile relay reconnect state. + +## Completion Obligations + +1. Keep mobile relay runtime changes tied to tests. + +## Current State + +Cross-repo relay ownership is explicit. +""", + } + + report = audit_contract_payload( + registry_payload=registry_payload, + status_payload=status_payload, + contract_texts=contract_texts, + ) + + self.assertEqual(report["errors"], []) + def test_audit_contract_payload_rejects_missing_extension_point_reference(self) -> None: registry_payload = { "subsystems": [ diff --git a/scripts/release_control/registry_audit.py b/scripts/release_control/registry_audit.py index f19a572eb..3a1e8a978 100644 --- a/scripts/release_control/registry_audit.py +++ b/scripts/release_control/registry_audit.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json +import os from pathlib import Path import re import subprocess @@ -61,20 +62,35 @@ def sorted_casefold(values: list[str]) -> list[str]: def tracked_repo_files() -> set[str]: - result = subprocess.run( - ["git", "ls-files", "-z"], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=False, - ) - files = { - entry.decode("utf-8") - for entry in result.stdout.split(b"\x00") - if entry - } + return tracked_workspace_files(active_repos=[REPO_ROOT.name], local_repo=REPO_ROOT.name) + + +def tracked_workspace_files(*, active_repos: list[str], local_repo: str) -> set[str]: + files: set[str] = set() + repos_root = REPO_ROOT.parent + for repo_id in active_repos: + repo_root = REPO_ROOT if repo_id == local_repo else repos_root / repo_id + if not repo_root.exists(): + continue + env = os.environ.copy() + if repo_root != REPO_ROOT: + env.pop("GIT_INDEX_FILE", None) + result = subprocess.run( + ["git", "ls-files", "-z"], + cwd=repo_root, + check=True, + capture_output=True, + text=False, + env=env, + ) + for entry in result.stdout.split(b"\x00"): + if not entry: + continue + rel = entry.decode("utf-8") + files.add(rel if repo_id == local_repo else f"{repo_id}:{rel}") + # Governance files are filesystem-only (gitignored). Supplement with any - # files that exist on disk under the subsystems/contracts directory so that + # files that exist on disk under the control-plane contracts directory so # contract path references in the registry resolve correctly. contracts_dir = Path(DEFAULT_CONTROL_PLANE["subsystems_dir_path"]) if contracts_dir.exists(): @@ -612,6 +628,12 @@ def render_pretty(report: dict[str, Any]) -> str: def main(argv: list[str] | None = None) -> int: args = parse_args(list(argv or [])) status_payload = load_status_payload(staged=args.staged) + scope = status_payload.get("scope", {}) + active_repos = [ + repo_id + for repo_id in scope.get("active_repos", []) + if isinstance(repo_id, str) and repo_id.strip() + ] or [REPO_ROOT.name] lane_ids = { lane.get("id") for lane in status_payload.get("lanes", []) @@ -619,7 +641,7 @@ def main(argv: list[str] | None = None) -> int: } report = audit_registry_payload( load_registry_payload(staged=args.staged), - tracked_files=tracked_repo_files(), + tracked_files=tracked_workspace_files(active_repos=active_repos, local_repo=REPO_ROOT.name), status_lane_ids=lane_ids, schema_contract=registry_schema_contract(staged=args.staged), ) diff --git a/scripts/release_control/registry_audit_test.py b/scripts/release_control/registry_audit_test.py index 73d7ed085..06f239753 100644 --- a/scripts/release_control/registry_audit_test.py +++ b/scripts/release_control/registry_audit_test.py @@ -53,6 +53,52 @@ class RegistryAuditTest(unittest.TestCase): self.assertEqual(report["summary"]["shared_ownership_count"], 0) self.assertEqual(report["subsystems"][0]["default_fallback_count"], 0) + def test_audit_registry_payload_accepts_cross_repo_owned_prefixes(self) -> None: + payload = { + "version": 12, + "shared_ownerships": [], + "subsystems": [ + { + "id": "relay-runtime", + "lane": "L7", + "contract": "docs/release-control/v6/internal/subsystems/relay-runtime.md", + "owned_prefixes": ["pulse-mobile:src/relay/"], + "owned_files": [], + "verification": { + "allow_same_subsystem_tests": True, + "test_prefixes": [], + "exact_files": [ + "pulse-mobile:src/relay/__tests__/client.test.ts", + ], + "require_explicit_path_policy_coverage": True, + "path_policies": [ + { + "id": "mobile-relay-runtime", + "label": "mobile relay runtime proof", + "match_prefixes": ["pulse-mobile:src/relay/"], + "match_files": [], + "allow_same_subsystem_tests": False, + "test_prefixes": [], + "exact_files": [ + "pulse-mobile:src/relay/__tests__/client.test.ts", + ], + } + ], + }, + } + ], + } + tracked_files = { + "docs/release-control/v6/internal/subsystems/relay-runtime.md", + "pulse-mobile:src/relay/__tests__/client.test.ts", + "pulse-mobile:src/relay/client.ts", + } + + report = audit_registry_payload(payload, tracked_files=tracked_files, status_lane_ids={"L7"}) + + self.assertEqual(report["errors"], []) + self.assertEqual(report["subsystems"][0]["owned_runtime_file_count"], 1) + def test_audit_registry_payload_flags_unknown_lane_and_missing_contract(self) -> None: payload = { "version": 12, diff --git a/scripts/release_control/subsystem_lookup.py b/scripts/release_control/subsystem_lookup.py index 389be703e..5ea783126 100644 --- a/scripts/release_control/subsystem_lookup.py +++ b/scripts/release_control/subsystem_lookup.py @@ -23,13 +23,27 @@ from canonical_completion_guard import ( from status_audit import audit_status_payload, load_status_payload from registry_audit import load_registry_payload +WORKSPACE_REPOS_ROOT = REPO_ROOT.parent + def normalize_input_path(raw: str) -> str: candidate = Path(raw.strip()) if candidate.is_absolute(): + candidate = candidate.resolve() try: - candidate = candidate.resolve().relative_to(REPO_ROOT) + candidate = candidate.relative_to(REPO_ROOT) except ValueError: + try: + repo_relative = candidate.relative_to(WORKSPACE_REPOS_ROOT) + except ValueError: + return candidate.as_posix() + parts = repo_relative.parts + if len(parts) >= 2: + repo_id = parts[0] + rel = Path(*parts[1:]).as_posix() + if repo_id == REPO_ROOT.name: + return rel + return f"{repo_id}:{rel}" return candidate.as_posix() return candidate.as_posix() diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index f3996a899..71246ff7e 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -62,6 +62,24 @@ class SubsystemLookupTest(unittest.TestCase): result = lookup_paths(["README.md"]) self.assertEqual(result["unowned_runtime_files"], ["README.md"]) + def test_lookup_paths_normalizes_cross_repo_absolute_runtime_paths(self) -> None: + result = lookup_paths([str(REPO_ROOT.parent / "pulse-mobile" / "src/relay/client.ts")]) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"relay-runtime"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["path"], "pulse-mobile:src/relay/client.ts") + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"relay-runtime"}, + ) + match = file_entry["matches"][0] + self.assertEqual(match["lane_context"]["lane_id"], "L7") + self.assertEqual(match["verification_requirement"]["id"], "mobile-relay-runtime") + def test_lookup_paths_assigns_shared_tag_badges_to_frontend_primitives(self) -> None: result = lookup_paths(["frontend-modern/src/components/shared/TagBadges.tsx"]) self.assertEqual(result["unowned_runtime_files"], [])