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
95 changes: 95 additions & 0 deletions .github/workflows/dep-tree-guard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Dependency Tree Guard

# This workflow protects the SDK's install footprint:
#
# 1. The DIRECT dependencies advertised by `pip install layerlens`
# must equal the baseline at
# `tests/instrument/_baselines/default_dependencies.txt`. New
# direct deps require explicit baseline updates in the same PR.
#
# 2. The TRANSITIVELY-RESOLVED package set must equal the baseline
# at `tests/instrument/_baselines/resolved_dependencies.txt`.
# A direct dep with permissive lower bounds can balloon the
# install size — this gate catches that.
#
# Both baselines are regenerable via:
# python scripts/regen_dep_baselines.py
#
# Run locally with `LAYERLENS_RESOLVE_DEPS=1 pytest tests/instrument/`.

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
default-install-guard:
name: Default install matches baseline
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install layerlens (no extras) and pytest
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pytest

- name: Run default-install guard tests
run: |
python -m pytest tests/instrument/test_default_install.py -v

resolved-tree-guard:
name: Resolved tree matches baseline
runs-on: ubuntu-latest
env:
CI: "true"
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Install pytest and tomli
run: |
python -m pip install --upgrade pip
python -m pip install pytest tomli

- name: Resolve transitive tree (diagnostic)
run: |
# Show the actual resolved tree in the workflow log so PR
# authors can see exactly what changed.
set -euo pipefail
{
echo "httpx>=0.23.0,<1"
echo "pydantic>=1.9.0,<3"
} | uv pip compile --python-version 3.9 -q --no-header --no-annotate \
--no-strip-extras --universal - || true

- name: Run resolved-tree guard tests
env:
LAYERLENS_RESOLVE_DEPS: "1"
run: |
python -m pytest tests/instrument/test_resolved_dep_tree.py -v

- name: Resolved-tree drift hint (on failure)
if: failure()
run: |
echo "::warning::If the failure is from a NEW transitive dep, decide:"
echo "::warning:: (a) tighten the version specifier on the offending direct dep,"
echo "::warning:: (b) regenerate the baseline if the new dep is acceptable:"
echo "::warning:: python scripts/regen_dep_baselines.py"
echo "::warning:: Commit the baseline update in the same PR."
99 changes: 99 additions & 0 deletions docs/adapters/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Contributing an adapter

This guide covers porting an adapter from `ateam` to `stratix-python` at
the quality bar required by CLAUDE.md.

## Quality gate (non-negotiable)

Every PR must produce all of:
- mypy `--strict` clean on the new files
- pyright clean (project config) on the new files
- ruff clean on the new files
- pytest green for the new tests
- A live integration test gated by `@pytest.mark.live` and the relevant
`*_API_KEY` env var (where the framework supports a real backing service)
- A runnable sample under `samples/instrument/<adapter>/`
- A reference doc under `docs/adapters/<category>-<name>.md`

CI matrix runs the new extra at both min-pin and latest-in-range.

## Naming convention

The `ateam` source uses `STRATIX*` class prefixes for public adapter classes
(e.g., `STRATIXCallbackHandler`, `STRATIXLangGraphAdapter`,
`STRATIXLiteLLMCallback`). When porting:

1. Rename the public class to `LayerLens*` (e.g., `STRATIXCallbackHandler` →
`LayerLensCallbackHandler`).
2. Add a backward-compat alias at module scope: `STRATIXCallbackHandler = LayerLensCallbackHandler`.
3. Note the alias in the adapter's reference doc with a deprecation timeline
(default: removed in v2.0).
4. Internal class names (`OpenAIAdapter`, `AnthropicAdapter`, etc.) that
were never prefixed in `ateam` stay as-is.

The `LiteLLMAdapter` port (`src/layerlens/instrument/adapters/providers/litellm_adapter.py`)
is the canonical example.

## Compatibility constraints

- **Python 3.8+**: do NOT use `StrEnum`, `from datetime import UTC`, PEP 604
union types in non-annotation contexts, or `match` statements. The
`_compat.pydantic` shim covers Pydantic v1↔v2 differences (`BaseModel`,
`Field`, `model_dump`, `field_validator`, `model_validator`).
- **No framework imports at SDK init time**: the framework SDK must be imported
only inside methods that the user explicitly calls (`connect`,
`_detect_framework_version`, etc.). The lazy-import test will catch
regressions.
- **No new required deps**: every framework SDK goes in `[project.optional-dependencies]`,
never in `[project] dependencies`. The default-install test enforces this.

## Adapter class checklist

When writing the new adapter class:

- [ ] Inherits from `BaseAdapter` (frameworks) or `LLMProviderAdapter` (LLMs)
- [ ] Sets `FRAMEWORK` and `VERSION` class attributes
- [ ] Implements `connect()`, `disconnect()`, `health_check()`,
`get_adapter_info()`, `serialize_for_replay()` (or inherits the LLM
provider variants)
- [ ] Exports `ADAPTER_CLASS = MyAdapter` at module scope (registry uses this
for lazy loading)
- [ ] Adds an entry to `_ADAPTER_MODULES` and `_FRAMEWORK_PACKAGES` in
`_base/registry.py`
- [ ] Adds a `pyproject.toml` extras entry with the framework's pip name and
version range; gates Python-version markers if the framework requires
3.10+
- [ ] Updates `tests/instrument/test_lazy_imports.py::_FORBIDDEN_PREFIXES`
with the framework's import name

## Test checklist

Three tiers:

1. **Unit tests** (`tests/instrument/adapters/<category>/test_<name>.py`):
- Mock the framework's SDK responses with `SimpleNamespace` objects
- Cover success path, error path, all wrapped methods, capture-config
gating, disconnect-restores-originals
- Assert on event types, payload fields, and structural invariants

2. **Sink-level e2e** (covered by the existing
`tests/instrument/test_sink_http_e2e.py`): every adapter that emits via
`HttpEventSink` benefits from this test suite — no new test needed unless
the adapter has a bespoke transport.

3. **Live integration** (`tests/instrument/adapters/<category>/test_<name>_live.py`):
- Module-level `pytestmark` skips without `<FRAMEWORK>_API_KEY`
- Hit the real service with a tiny request (max_tokens 5–10 to bound cost)
- Assert that real response field names map to your event payload fields —
this is what catches SDK schema drift

## Sample + doc checklist

- `samples/instrument/<adapter>/main.py`: runnable via `python -m
samples.instrument.<adapter>.main`. Checks for env vars; gives clear
diagnostic if missing. Uses `adapter.add_sink(sink)` (the public API).
- `samples/instrument/<adapter>/README.md`: install command, env-var summary,
what events the user will see, link to the reference doc.
- `docs/adapters/<category>-<name>.md`: install, quick start, events emitted
with table, framework-specific behavior, cost calculation notes, BYOK
notes, capture-config notes.
Loading
Loading