Skip to content
Merged
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
63 changes: 7 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,75 +28,26 @@ jobs:
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- run: python -m pip install --upgrade pip
# Install dev extras so pyright can resolve runtime/test dependencies
# while it analyzes the package, tests, examples, and maintained scripts.
- run: pip install -e ".[dev]"
- run: pyright
# The dashboard's REST types are generated from the FastAPI app's
# OpenAPI schema, so an unmirrored response-shape change has to fail
# the build rather than a panel. It lives in this job rather than in
# ``webui`` because the generator imports the package: this is the job
# that already has it installed, and duplicating the install would
# double CI's heaviest step for one diff. ``webui`` keeps the
# sibling parity check for the committed bundle.
- name: Verify the generated dashboard types are current
run: python scripts/generate_webui_types.py --check

test-matrix:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
python-version: "3.13"
cache: pip
cache-dependency-path: pyproject.toml
- run: python -m pip install --upgrade pip
- run: pip install -e ".[dev]"
# Coverage is informational only — no --cov-fail-under, and --cov stays out
# of pyproject addopts so local runs without pytest-cov keep working.
- run: pytest -q -m "not gpu" --cov=drowse --cov-report=term-missing

# Stable aggregator so branch protection can require a single ``test`` context
# instead of every ``test-matrix (x.y)`` leg by name. ``if: always()`` makes the
# gate run even when a leg fails, and the explicit result check turns a failed or
# skipped matrix into a failed gate (a skipped required check would otherwise let
# a PR merge).
test:
runs-on: ubuntu-latest
needs: [test-matrix]
if: always()
steps:
- name: Verify the test matrix succeeded
run: |
if [ "${{ needs.test-matrix.result }}" != "success" ]; then
echo "test-matrix did not succeed: ${{ needs.test-matrix.result }}"
exit 1
fi

build:
# Validates the PEP 621 dynamic-version setup in pyproject.toml: confirms
# ``python -m build`` resolves ``drowse.__version__`` via AST parse without
# executing the module (which would import torch and break the build).
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build
- name: Seed a stale dashboard build cache
run: python scripts/check_package_isolation.py seed
- run: python -m build --sdist --wheel
- name: Verify wheel dashboard files match the source distribution
run: python scripts/check_package_isolation.py check
- name: Verify wheel imports and version round-trips
run: |
pip install dist/drowse_ai-*.whl
python -c "import drowse; from importlib.metadata import version; assert version('drowse.ai') == drowse.__version__; print('version:', drowse.__version__)"
- run: pytest -q -m "not gpu" -n auto

webui:
runs-on: ubuntu-latest
Expand Down
7 changes: 3 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ deprecation shims or second spellings.

## Package map and layer order

`tests/invariants/test_layering.py` enforces this order, lowest first. A
The order runs lowest first. A
module may import its own layer or any lower one. Every import counts:
module-level, function-level and `if TYPE_CHECKING:`. A lower layer that needs
a higher object types it against a narrow `Protocol` of its own
Expand Down Expand Up @@ -306,9 +306,8 @@ use the autospec `tests.support.server.mock_session`. GPU tests (`gpu` marker)
load `HuggingFaceTB/SmolLM2-360M-Instruct` unless `DROWSE_TEST_MODEL` names
another; `gpu_gemma` needs gated Gemma access. Every bug fix lands with a
regression test. A test never touches the real home (an autouse fixture sets a
scratch `DROWSE_HOME`). CI runs ruff, pyright, the types `--check`, the CPU
suite on Python 3.11–3.13, the package-isolation build and the webui build
with a dist-drift check. Details: `tests/AGENTS.md`.
scratch `DROWSE_HOME`). CI runs ruff, pyright, the CPU suite on Python 3.13
(`-n auto`) and the webui build with a dist-drift check. Details: `tests/AGENTS.md`.

## State and environment

Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ next steered forward.
| Conversation tree, recipes, replay, transcripts | `loom/` |
| Protocols, dashboard mount, CLI | `server/`, `web/`, `cli/` |

Dependencies point downward only (`tests/invariants/test_layering.py`). A
Dependencies point downward only. A
collaborator gets its dependencies through its constructor; nothing outside
`drowse/session/` reads a session private. That rule is what lets each
collaborator be tested without a session and lets the browser runtime replace
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pytest -q tests/gpu/test_smoke.py # real-model smoke tests
```

Tests marked `gpu` need CUDA or Apple Silicon MPS and may download model weights.
CI runs the non-GPU suite on Python 3.11, 3.12, and 3.13.
CI runs the non-GPU suite on Python 3.13.

## Static checks

Expand Down
2 changes: 1 addition & 1 deletion drowse/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The `drowse` console script (`drowse.cli.main:main`; `python -m drowse` is the
same entry point): nine verb groups — `serve`, `manifold`, `pack`, `config`,
`experiment`, `template`, `lens`, `sae`, `unit` — as thin frontends over
`DrowseSession` and the `store`/`fit`/`lens`/`sae`/`units` packages. The CLI is in the top layer beside
`server`, `web` and `notebook` (`tests/invariants/test_layering.py`): it may
`server`, `web` and `notebook`: it may
import any lower layer and never reaches a session private (`session._x`).

## Module map
Expand Down
2 changes: 1 addition & 1 deletion drowse/fit/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Corpora in, fitted manifold tensors out: the one extraction pipeline, corpus
generation, the neutral-activation capture behind the whitener, the offline
bake, cross-model alignment orchestration, and the naturalness eval. `fit`
sits above `generation` and below `loom`/`session`
(`tests/invariants/test_layering.py`), so it never imports the session; hosts
so it never imports the session; hosts
are Protocols (`ModelHandle`, `NaturalnessHost`) that `DrowseSession`
satisfies. The math it calls is in `geometry/AGENTS.md` (Fisher basis, frame,
share, discover coords, topology, RBF, σ-field, fold). The folder format,
Expand Down
4 changes: 2 additions & 2 deletions drowse/generation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ routing, the transaction) is `session/engine.py`; the multi-row runner is
`session/batch.py` (see `drowse/session/AGENTS.md`).

Layer: above `readout`, `lens`, `sae`, `steering.composer`/`hooks`; below `fit`,
`loom`, `session`. `tests/invariants/test_layering.py` counts `TYPE_CHECKING`
imports too, so collaborators here type against a narrow `Protocol`
`loom`, `session`. `TYPE_CHECKING`
imports count too, so collaborators here type against a narrow `Protocol`
(`render.ConversationHistory`) or a lower-layer class.

| Module | Owns |
Expand Down
2 changes: 1 addition & 1 deletion drowse/geometry/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ whitener, the direction view (fold, `Profile`, GGUF) and cross-model transfer.

Pure tensor math. Geometry sits directly above `errors`/`naming`/`events` and
`base` in the layer order and imports nothing else from drowse
(`tests/invariants/test_layering.py`; `drowse.__version__` is exempt). The
(`drowse.__version__` is exempt). The
manifold tensor codec is `store/manifold_tensors.py`; capture and fit
orchestration are `fit/` (`pipeline.py`, `node_stats.py`,
`capture.py::compute_dls_axes`); the gains that scale the kernel are in
Expand Down
2 changes: 1 addition & 1 deletion drowse/lens/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ atom `K_l ẽ_v` and the `tokens-in` decomposition. `read.py` reads any vector
at a place through both, by the four-lens rule. Inside a session the face is
`session.lens` (`LensInstrument`). The package sits beside `sae`, above `readout` and below
`steering.composer`, and imports only `errors`, `base`, `model` and `readout`
(`tests/invariants/test_layering.py`, which counts `TYPE_CHECKING` imports).
(`TYPE_CHECKING` imports included).

| Module | Owns |
|---|---|
Expand Down
4 changes: 2 additions & 2 deletions drowse/loom/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ recipes; the verbs, `restore_tree`, the joint-logprob cache),

## Layering

`loom` sits above `fit` and below `session`. `tests/invariants/test_layering.py`
enforces this, `TYPE_CHECKING` imports included. Lower layers never import
`loom` sits above `fit` and below `session`. `TYPE_CHECKING`
imports count too. Lower layers never import
loom: `generation/render.py` types the tree as `ConversationHistory`, and
`generation/finalize.py` takes `tree: Any`.

Expand Down
2 changes: 1 addition & 1 deletion drowse/model/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ loading and identity, where the residual blocks are and what they compute
(`BlockSpec`), unit directions, the logits path, layer selectors, token spans, chat-template facts, role substitution, the scene
stitcher, and compile/StaticCache plumbing. The package imports only
`errors`/`naming`/`events`, `base` and `geometry`
(`tests/invariants/test_layering.py`) and holds no session state: every
and holds no session state: every
function takes a model, tokenizer or config.
Render *policy* (which path a conversation takes) lives in
`generation/decode.py::build_chat_input` and `generation/render.py`; this
Expand Down
2 changes: 1 addition & 1 deletion drowse/readout/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ other three families (`drowse/units/AGENTS.md`);
## Layering

`readout` sits above `steering.{grammar,terms,triggers}` and below
`lens`/`sae`/`units` (`tests/invariants/test_layering.py`; `TYPE_CHECKING`
`lens`/`sae`/`units` (`TYPE_CHECKING`
imports count). It must not import `lens`, `sae`, `units`,
`steering.composer`/`hooks`, `generation` or
anything above; collaborators from those layers are typed against the
Expand Down
2 changes: 1 addition & 1 deletion drowse/sae/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local training, and the `sae` read instrument (`session.sae`,

`sae` shares a layer with `lens` (above `readout`, below `steering.composer`):
it never imports `generation`, `fit`, `loom` or `session`, type-only imports
included (`tests/invariants/test_layering.py`). `DrowseSession.__init__` wires
included. `DrowseSession.__init__` wires
every dependency explicitly.

| Module | Owns |
Expand Down
5 changes: 2 additions & 3 deletions drowse/server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ an API path.
## Rules for editing

- `server/` is in the top layer with `cli`, `web`, `notebook`. It never reads
a session-private attribute (`tests/invariants/test_layering.py`), and no
server module imports a sibling's `_private` helper
(`tests/invariants/test_boundary_guard.py`); shared helpers go in
a session-private attribute, and no server module imports a sibling's
`_private` helper; shared helpers go in
`runtime.py`, `errors.py`, `operations.py`, or `request_helpers.py`.
- Native request bodies subclass `NativeRequest` (`extra="forbid"`), nested
objects included. Ollama bodies are `extra="allow"` (clients send fields
Expand Down
2 changes: 1 addition & 1 deletion drowse/session/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ collaborator with explicit constructor dependencies, not into session state.
Outside this package nothing reads a session private (`session._x`, `sess._x`,
`<x>.session._x`, `self._session._x`); modules inside it may (the engine calls
`_start_loom_assistant`, `_plan_authored_prompt_capture`, `_steering_composer`).
`tests/invariants/test_layering.py` enforces this and pins `session` directly
`session` sits directly
below `cli`/`server`/`web`/`notebook`. Two collaborators take the session as a
Protocol-typed handle, so keep those properties stable: `Extraction`
(`fit.pipeline.ModelHandle`: `model`, `tokenizer`, `model_id`, `device`,
Expand Down
2 changes: 1 addition & 1 deletion drowse/steering/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ injected, where, when, and how strongly.
| `composer.py` | `SteeringComposer` (stack, scopes, resolution, composition, plan cache, stack queries, `term_layers`), `SteeringScope`, `TermLayers`, `resolve_scope_role`, `select_term_layers`, `affine_manifold_push`, the `ArtifactSource` / `NeutralSource` protocols |
| `hooks.py` | `SteeringPlan`, `_lower_affine_subspaces`, `AffineOp`, `SteeringHook`, `UnitTerm` / `UnitOp` / `UnitHook` (unit ablation), `SteeringManager`, `install_persistent_offset_hooks`, the gain constants |

Layering (`tests/invariants/test_layering.py`): `grammar`/`terms`/`triggers`
Layering: `grammar`/`terms`/`triggers`
sit above `store` (parsing resolves bare atoms against installed manifolds via
`store.selectors`) and below `readout`; `composer`/`hooks` sit above
`lens`/`sae` and below `generation`. Lower layers that must refuse work under
Expand Down
4 changes: 2 additions & 2 deletions drowse/store/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ alignment caches.

Layer: above `model`, below `steering.grammar` (`parse_expr` resolves bare atoms
through `store.selectors`). Store imports only `errors`/`naming`/`events`, `base`,
`geometry`, `model` and itself; `tests/invariants/test_layering.py` counts
function-level and `TYPE_CHECKING` imports too. No barrel module: import from the
`geometry`, `model` and itself; function-level
and `TYPE_CHECKING` imports count too. No barrel module: import from the
submodule. This package owns formats and folder primitives; the fit's folder
transactions (authoring snapshot, compare-and-swap publish) are `fit/publish.py`,
the weight-free fit no-op proof `fit/preflight.py`, the capture cache
Expand Down
2 changes: 1 addition & 1 deletion drowse/units/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ with the other steering hooks in `steering/hooks.py`.
| `reads.py` | `UnitReader` (static reads, `top`, `find`, `stats`), the results (`NeuronRead`/`NeuronPoleRead`, `HeadRead`, `PlaceRead`/`UnitHalf`, `Firing`, `TopResult`/`TopHit`, `FindResult`), `ContrastCache`/`ContrastStats`, `own_token_z`, `copy_score`, `logit_moments`, `token_logits` |
| `catalog.py` | the neuron-lens catalog: `CATALOG_REPO`/`CATALOG_REVISION`/`CATALOG_MODELS`, `fetch_catalog`, `catalog_status`, `list_catalogs`, `remove_catalog`, `UnitCatalog` (rows, labels, `url`), `CatalogBinding` (the session's view), `unit_url`, `LABEL_CREDIT`, the errors |

Layer 7, beside `lens` and `sae` (`tests/invariants/test_layering.py`): it
Layer 7, beside `lens` and `sae`: it
imports `model`, `store`, `readout`, `lens` and below, never
`steering.composer`, `generation` or the session. The composer (layer 8)
reaches it through its `UnitSource` protocol; the session wires everything
Expand Down
24 changes: 2 additions & 22 deletions drowse/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,29 +77,9 @@ npm run build # vite build into ../drowse/web/dist + check-build-isolatio
- The SAE description indexes in `lib/data/sae-descriptions/` ship on purpose (as
`assets/gemma-3-{270m,1b,4b}-it.json`): they label local SAELens gemma-scope-2 releases
offline. Regenerate them with `node scripts/build-sae-descriptions.mjs`.
- CI's `build` job seeds a stale `build/lib` cache, then runs
`scripts/check_package_isolation.py check`. The wheel's dashboard files must equal the
sdist's and the source tree's.
- `npm run check` runs `svelte-check`, `check-theme.mjs` (every referenced CSS custom
property must be declared, here or in `@stdlogits/ledger`, so an old token name fails
here), `check-ledger.mjs` (the Ledger ratchet, below), `check-runtime-boundary.mjs`, the
`test:*` node suites and `check-test-coverage.mjs`. The last requires every
`scripts/*.test.mjs` to be reachable from `check`, so a new test file needs a line in
`package.json`.
- `check-ledger.mjs` counts, per file, the departures from Ledger: borders; outlines outside
a `:focus`/`:focus-visible` rule; shadows other than a marked token's 2px same-colour spread
(the tip's `--float` is Ledger's own, so any use here counts); monospace; capitals;
letter-spaced text; easing off the one curve; animations that loop; colours outside the
tokens; colour mixes outside oklch (the accent's) and oklab (a grey's: Chrome renders a
near-grey mixed in oklch with a red cast); and text coloured by the depth accent (`--tint`,
`--deep`, `--late`, or a component's own accent property). It reads `style:prop={…}`
directives as `prop: …` declarations. It fails if any count differs from
`webui/ledger-ratchet.json`, which holds every count at zero: the bridge that once mapped
the old token names onto Ledger is gone, and with it the ratchet's bridge category. A count
that rises fails; one that falls asks for `node scripts/check-ledger.mjs --write`, which
lowers the ratchet and never raises it. A category the ratchet does not record yet fails
the check until `--write` takes today's count as its first ceiling. `--cut` (the 6.0 cut)
requires every count at zero.
here), `check-runtime-boundary.mjs` and the `test:*` node suites.

## Generated REST types (`lib/types.gen.ts`)

Expand Down Expand Up @@ -674,7 +654,7 @@ token).
the theme preflight), the 404 for a missing asset or any file the bundle lacks, the SPA
fallback, traversal, a public shell under an
API key, `web=False`, and an empty dist. `tests/invariants/test_webui_types_parity.py`
gates the generated types, and `test_layering` places `web` in the top layer.
gates the generated types.

## Deliberately absent

Expand Down
Loading
Loading