Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Vendored symbols from ``context_intelligence`` for standalone installability.

The hook module's runtime needs four symbols from the parent bundle's
``context_intelligence`` package. Copying them here lets the hook install
and mount standalone — without requiring the parent bundle on PYTHONPATH
or in the install tree.

Provenance (last synced from upstream):
- AMPLIFIER_DIR, SETTINGS_PATH, _parse_settings_yaml
→ context_intelligence/config.py
- workspace_slug
→ context_intelligence/reconstruct/discover.py

If you change behaviour in either of those upstream files, mirror the change
here. The two copies are aligned by convention, not by shared import — the
hook MUST NOT import from ``context_intelligence`` at runtime, or it stops
being standalone-installable in environments that strip ``[tool.uv.sources]``
(notably amplifier-agent's ``--no-sources`` activator policy).

This module is internal to the hook package. External callers should not
import from here; if you need the same logic in your own code, copy it.
"""

from __future__ import annotations

import logging
from pathlib import Path

log = logging.getLogger("amplifier_module_hook_context_intelligence._vendored")

# ---------------------------------------------------------------------------
# From context_intelligence/config.py
# ---------------------------------------------------------------------------

AMPLIFIER_DIR = Path.home() / ".amplifier"
SETTINGS_PATH = AMPLIFIER_DIR / "settings.yaml"


def _parse_settings_yaml(path: Path) -> dict:
"""Minimal YAML parser for settings.yaml -- good enough for the flat keys
we need without requiring PyYAML.

Returns a dict with CI server config keys (``server_url``, ``api_key``) if found.
"""
result: dict[str, str] = {}
if not path.is_file():
return result

try:
# Try PyYAML first
import yaml

with open(path) as f:
data = yaml.safe_load(f)
if isinstance(data, dict):
ci_cfg = (
data.get("overrides", {}).get("hook-context-intelligence", {}).get("config", {})
)
if isinstance(ci_cfg, dict):
if "context_intelligence_server_url" in ci_cfg:
result["server_url"] = ci_cfg["context_intelligence_server_url"]
if "context_intelligence_api_key" in ci_cfg:
result["api_key"] = ci_cfg["context_intelligence_api_key"]
except ImportError:
# Fallback: crude line-based extraction
try:
text = path.read_text()
in_ci_section = False
for line in text.splitlines():
stripped = line.strip()
if "hook-context-intelligence" in stripped:
in_ci_section = True
continue
if in_ci_section:
if stripped.startswith("context_intelligence_server_url:"):
val = stripped.split(":", 1)[1].strip().strip("'\"")
result["server_url"] = val
elif stripped.startswith("context_intelligence_api_key:"):
val = stripped.split(":", 1)[1].strip().strip("'\"")
result["api_key"] = val
# If we hit a non-indented line, we've left the section
if not line.startswith(" ") and not line.startswith("\t") and stripped:
if "context_intelligence" not in stripped:
in_ci_section = False
except OSError:
pass
except Exception as exc:
log.debug("Could not parse %s: %s", path, exc)
return result


# ---------------------------------------------------------------------------
# From context_intelligence/reconstruct/discover.py
# ---------------------------------------------------------------------------


def workspace_slug(project_dir: str) -> str:
"""Derive the workspace slug from an absolute project directory path.

Converts the absolute path to a slug by replacing every ``/`` with ``-``.

Examples::

workspace_slug("/home/bkrabach/dev/attractor-dev-machine")
# -> "-home-bkrabach-dev-attractor-dev-machine"

Parameters
----------
project_dir:
Absolute path to the project directory.

Returns
-------
str
Slug derived from the absolute path.
"""
import os

return os.path.abspath(project_dir).replace("/", "-")
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from pathlib import Path
from typing import Any

from context_intelligence.config import SETTINGS_PATH, _parse_settings_yaml
from context_intelligence.reconstruct.discover import workspace_slug
from ._vendored import SETTINGS_PATH
from ._vendored import _parse_settings_yaml
from ._vendored import workspace_slug

_DEFAULT_BASE_PATH = "~/.amplifier/projects"
_DEFAULT_PROJECT_SLUG = "default"
Expand Down
2 changes: 0 additions & 2 deletions modules/hook-context-intelligence/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ license = "MIT"
dependencies = [
"httpx>=0.28.1",
"idna>=3.15",
"amplifier-bundle-context-intelligence",
]

[project.entry-points."amplifier.modules"]
Expand Down Expand Up @@ -36,7 +35,6 @@ dev = [

[tool.uv.sources]
amplifier-core = { git = "https://github.com/microsoft/amplifier-core", rev = "v1.4.1" }
amplifier-bundle-context-intelligence = { path = "../.." }

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ def test_unix_path_produces_expected_slug(self) -> None:
assert _slugify_path("/home/user/project") == "-home-user-project"

def test_unix_path_matches_workspace_slug(self) -> None:
from context_intelligence.reconstruct.discover import workspace_slug
from amplifier_module_hook_context_intelligence._vendored import workspace_slug

path = "/home/user/project"
assert _slugify_path(path) == workspace_slug(path)
Expand Down
50 changes: 29 additions & 21 deletions modules/hook-context-intelligence/tests/test_hook_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Test that the hook module pyproject.toml declares the context_intelligence dependency.
"""Test that the hook module pyproject.toml does NOT depend on its parent bundle.

TDD: This test is written FIRST and will FAIL until pyproject.toml is updated.
Design: The hook module should be installable independently of its parent bundle
so it can be used in environments that compose modules individually.

TDD: These tests are written to enforce decoupling from the parent bundle.
"""

from __future__ import annotations
Expand All @@ -17,36 +20,41 @@ def _load_pyproject() -> dict:


class TestHookDependencies:
"""Verify hook module's pyproject.toml declares amplifier-bundle-context-intelligence."""

def test_amplifier_bundle_context_intelligence_in_dependencies(self) -> None:
"""amplifier-bundle-context-intelligence must appear in the dependencies list."""
"""Verify hook module's pyproject.toml does NOT depend on its parent bundle.

Per the design proposal: the hook module should be installable independently
of its parent bundle so it can be used in environments that compose modules
individually (e.g. amplifier-agent's --no-sources install policy).
"""

def test_amplifier_bundle_context_intelligence_not_in_dependencies(self) -> None:
"""amplifier-bundle-context-intelligence must NOT appear in dependencies."""
data = _load_pyproject()
deps: list[str] = data["project"]["dependencies"]
dep_names = [d.split(">=")[0].split("==")[0].strip() for d in deps]
assert "amplifier-bundle-context-intelligence" in dep_names, (
f"Expected 'amplifier-bundle-context-intelligence' in dependencies, got: {deps}"
assert "amplifier-bundle-context-intelligence" not in dep_names, (
"The hook module must not declare its parent bundle as a runtime "
"dependency. Doing so makes the module uninstallable in environments "
"that strip [tool.uv.sources] at install time."
)

def test_uv_sources_has_path_entry_for_bundle(self) -> None:
"""[tool.uv.sources] must have path = '../..' for amplifier-bundle-context-intelligence."""
def test_uv_sources_does_not_reference_bundle(self) -> None:
"""[tool.uv.sources] must NOT have a path entry for the parent bundle."""
data = _load_pyproject()
sources: dict = data.get("tool", {}).get("uv", {}).get("sources", {})
assert "amplifier-bundle-context-intelligence" in sources, (
f"Expected 'amplifier-bundle-context-intelligence' in [tool.uv.sources], got: {sources}"
assert "amplifier-bundle-context-intelligence" not in sources, (
"The hook module must not pin its parent bundle via [tool.uv.sources]. "
"This entry is invisible under --no-sources install and produces an "
"unresolvable dependency."
)
entry = sources["amplifier-bundle-context-intelligence"]
assert entry.get("path") == "../..", f"Expected path = '../..', got: {entry}"

def test_dependencies_list_has_httpx_and_bundle(self) -> None:
"""The production dependencies must include httpx and amplifier-bundle-context-intelligence.
amplifier-core is NOT a production dep — it is runtime-provided by the Amplifier CLI.
"""
def test_dependencies_list_has_httpx_only(self) -> None:
"""Production dependencies must be httpx (and nothing else from this project)."""
data = _load_pyproject()
deps: list[str] = data["project"]["dependencies"]
assert any("httpx" in d for d in deps), f"httpx not found in {deps}"
assert any("amplifier-bundle-context-intelligence" in d for d in deps), (
f"amplifier-bundle-context-intelligence not found in {deps}"
assert any("httpx" in d for d in deps), "httpx is required for HTTP dispatch"
assert not any("amplifier-bundle-context-intelligence" in d for d in deps), (
"Parent bundle must not be a runtime dependency of the hook."
)
assert not any("amplifier-core" in d for d in deps), (
f"amplifier-core must not be a production dep (runtime-provided): {deps}"
Expand Down
Loading