Skip to content

Commit 54ae855

Browse files
Fix repo boundary violations
1 parent 7129840 commit 54ae855

5 files changed

Lines changed: 132 additions & 9 deletions

File tree

REPO_BOUNDARY.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Repository Boundary
2+
3+
This repository contains one active packaged product and several dormant or reference code islands. The active product must stay independently installable, testable, and importable without importing those islands directly.
4+
5+
## Active Runtime Path
6+
7+
The active runtime is `mythic_vibe_cli/`.
8+
9+
Code under this path may import the Python standard library, declared package dependencies, and other modules inside `mythic_vibe_cli/`. It must not directly import dormant top-level repository packages such as `ai`, `core`, `systems`, `sessions`, `imports`, `yggdrasil`, `mindspark_thoughtform`, `ollama`, `whisper`, or `chatterbox`.
10+
11+
## Dormant Islands
12+
13+
Dormant islands remain source, reference, research, or optional integration material until an adapter makes the boundary explicit. Optional integrations must be reached through a small module inside `mythic_vibe_cli/`, guarded by configuration or feature flags, and documented by an ADR when the boundary is non-trivial.
14+
15+
## Required Checks
16+
17+
Before merging boundary-sensitive work, run:
18+
19+
```bash
20+
mythic-vibe doctor --repo-boundary --path .
21+
```
22+
23+
The command verifies this file, the active product boundary docs, dormant island docs, and the ADRs that define the import rules.

mythic_vibe_cli/ai/providers/yggdrasil.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323

2424
from __future__ import annotations
2525

26+
import importlib
2627
import os
28+
import sys
2729
from dataclasses import dataclass, field
30+
from pathlib import Path
2831
from typing import Any
2932

3033
from .base import (
@@ -38,6 +41,48 @@
3841

3942
ISLAND_ENABLED_ENV = "MYTHIC_ISLAND_YGGDRASIL_ENABLED"
4043
_TRUTHY = {"1", "true", "yes", "on"}
44+
_REPO_ROOT = Path(__file__).resolve().parents[3]
45+
46+
47+
def _module_is_from_dormant_repo_path(module: Any, package_name: str) -> bool:
48+
raw_path = getattr(module, "__file__", "") or ""
49+
if not raw_path:
50+
return False
51+
try:
52+
path = Path(raw_path).resolve()
53+
return path.is_relative_to(_REPO_ROOT / package_name)
54+
except (OSError, ValueError):
55+
return False
56+
57+
58+
def _import_external_package(package_name: str) -> Any:
59+
existing = sys.modules.get(package_name)
60+
if existing is not None:
61+
if _module_is_from_dormant_repo_path(existing, package_name):
62+
raise ImportError(
63+
f"{package_name} resolves to dormant repo path, not an external package"
64+
)
65+
return existing
66+
67+
spec = importlib.util.find_spec(package_name)
68+
if spec is None:
69+
raise ImportError(f"{package_name} package not found")
70+
origin = spec.origin or ""
71+
search_locations = list(spec.submodule_search_locations or [])
72+
for raw_path in [origin, *search_locations]:
73+
if not raw_path:
74+
continue
75+
try:
76+
path = Path(raw_path).resolve()
77+
except (OSError, ValueError):
78+
continue
79+
if path == _REPO_ROOT / package_name or path.is_relative_to(
80+
_REPO_ROOT / package_name
81+
):
82+
raise ImportError(
83+
f"{package_name} resolves to dormant repo path, not an external package"
84+
)
85+
return importlib.import_module(package_name)
4186

4287

4388
def is_island_enabled() -> bool:
@@ -62,21 +107,19 @@ def _try_import_yggdrasil() -> Any | None:
62107
available so the operator can install whichever one their
63108
deployment ships."""
64109
try:
65-
import yggdrasil # type: ignore[import-not-found]
110+
return _import_external_package("yggdrasil")
66111
except ImportError:
67112
return None
68-
return yggdrasil
69113

70114

71115
def _try_import_wyrdforge() -> Any | None:
72116
"""Best-effort try-import for the canonical published WYRD
73117
package. Returns the ``wyrdforge`` module object, or ``None``
74118
when the package isn't on ``sys.path``. Phase F.2 (additive)."""
75119
try:
76-
import wyrdforge # type: ignore[import-not-found]
120+
return _import_external_package("wyrdforge")
77121
except ImportError:
78122
return None
79-
return wyrdforge
80123

81124

82125
@dataclass

mythic_vibe_cli/voice/transcribe.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525

2626
from __future__ import annotations
2727

28+
import importlib
2829
import os
30+
import sys
2931
import tempfile
3032
import wave
3133
from dataclasses import dataclass, field
@@ -41,6 +43,7 @@
4143
DEFAULT_MIC_SAMPLE_RATE = 16_000
4244
DEFAULT_MIC_CHANNELS = 1
4345
DEFAULT_MIC_DURATION = 5.0
46+
_REPO_ROOT = Path(__file__).resolve().parents[2]
4447

4548

4649
class MissingExtraError(RuntimeError):
@@ -56,6 +59,48 @@ def __init__(self, extra: str, install_hint: str) -> None:
5659
self.install_hint = install_hint
5760

5861

62+
def _module_is_from_dormant_repo_path(module: Any, package_name: str) -> bool:
63+
raw_path = getattr(module, "__file__", "") or ""
64+
if not raw_path:
65+
return False
66+
try:
67+
path = Path(raw_path).resolve()
68+
return path.is_relative_to(_REPO_ROOT / package_name)
69+
except (OSError, ValueError):
70+
return False
71+
72+
73+
def _import_external_package(package_name: str) -> Any:
74+
existing = sys.modules.get(package_name)
75+
if existing is not None:
76+
if _module_is_from_dormant_repo_path(existing, package_name):
77+
raise ImportError(
78+
f"{package_name} resolves to dormant repo path, not an external package"
79+
)
80+
return existing
81+
82+
spec = importlib.util.find_spec(package_name)
83+
if spec is None:
84+
raise ImportError(f"{package_name} package not found")
85+
origin = spec.origin or ""
86+
search_locations = list(spec.submodule_search_locations or [])
87+
candidate_paths = [origin, *search_locations]
88+
for raw_path in candidate_paths:
89+
if not raw_path:
90+
continue
91+
try:
92+
path = Path(raw_path).resolve()
93+
except (OSError, ValueError):
94+
continue
95+
if path == _REPO_ROOT / package_name or path.is_relative_to(
96+
_REPO_ROOT / package_name
97+
):
98+
raise ImportError(
99+
f"{package_name} resolves to dormant repo path, not an external package"
100+
)
101+
return importlib.import_module(package_name)
102+
103+
59104
@dataclass(frozen=True)
60105
class TranscriptionRequest:
61106
"""Input payload. ``source_path`` is the only required field —
@@ -189,14 +234,13 @@ class WhisperTranscriber:
189234

190235
def __post_init__(self) -> None:
191236
try:
192-
import whisper # type: ignore[import-not-found]
237+
self._module = _import_external_package("whisper")
193238
except ImportError as exc:
194239
raise MissingExtraError(
195240
"openai-whisper",
196241
"Install with `pip install openai-whisper`. "
197242
"Note: whisper requires ffmpeg on PATH for audio decoding.",
198243
) from exc
199-
self._module = whisper
200244

201245
def transcribe(self, request: TranscriptionRequest) -> TranscriptionResult:
202246
if self._module is None: # pragma: no cover — __post_init__ guards

mythic_vibe_cli/voice/tts.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from dataclasses import dataclass, field
3535
from typing import Any, Protocol, runtime_checkable
3636

37-
from .transcribe import MissingExtraError
37+
from .transcribe import MissingExtraError, _import_external_package
3838

3939

4040
TTS_ENABLED_ENV = "MYTHIC_VOICE_TTS_ENABLED"
@@ -168,13 +168,12 @@ class ChatterboxEngine:
168168

169169
def __post_init__(self) -> None:
170170
try:
171-
import chatterbox # type: ignore[import-not-found]
171+
self._module = _import_external_package("chatterbox")
172172
except ImportError as exc:
173173
raise MissingExtraError(
174174
"chatterbox",
175175
"Install with `pip install chatterbox` (open-source TTS).",
176176
) from exc
177-
self._module = chatterbox
178177

179178
# ---- Additive 2026-05-02: Modern Chatterbox API adapter ------------
180179
#

tests/test_island_isolation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from __future__ import annotations
1414

1515
import os
16+
from pathlib import Path
1617
import unittest
1718
from unittest import mock
1819

@@ -212,6 +213,19 @@ def test_missing_yggdrasil_dep_does_not_block_other_providers(self) -> None:
212213
self.assertIn("copy-paste", providers)
213214
self.assertTrue(providers["copy-paste"].validate_config().configured)
214215

216+
def test_yggdrasil_try_import_does_not_load_dormant_repo_island(self) -> None:
217+
from mythic_vibe_cli.ai.providers.yggdrasil import _try_import_yggdrasil
218+
219+
module = _try_import_yggdrasil()
220+
if module is None:
221+
return
222+
223+
raw_path = getattr(module, "__file__", "")
224+
self.assertTrue(raw_path)
225+
module_path = Path(raw_path).resolve()
226+
repo_root = Path(__file__).resolve().parents[1]
227+
self.assertFalse(module_path.is_relative_to(repo_root / "yggdrasil"))
228+
215229
def test_missing_mindspark_dep_does_not_block_other_providers(self) -> None:
216230
from mythic_vibe_cli.ai.providers.mindspark import MindSparkProvider
217231
from mythic_vibe_cli.ai.registry import ProviderRegistry

0 commit comments

Comments
 (0)