After completing a task, always:
- Lint with
ruff check . - Type check
src/withpython -m mypy --config-file pyproject.toml src - Build the project with
uv build - Run tests with
pytest
src/glitchlings/- Package entry point and CLI wiring.__init__.pyexposes the public API (Auggie builder, glitchlings,Gaggle,summon,AttackConfighelpers,SAMPLE_TEXT,TranscriptTarget).__main__.pyroutespython -m glitchlingsto the CLI entry point inmain.py.main.pyimplements the CLI: parser construction, attack config loading, glitchling summoning, and optional diff/report output.auggie.pyprovides the fluent roster builder;constants.py/runtime_config.pyhold defaults.
src/glitchlings/attack/- Attack orchestrator and tokenization/metrics helpers.core.pydefinesAttack,AttackResult, and tokenizer/metric resolution.core_planning.py(pure) builds attack plans;core_execution.pyexecutes plans through tokenizers and glitchlings.analysis.pyprovidesSeedSweep,GridSearch, andTokenizerComparisontools for parameter exploration.compose.py,encode.py, andmetrics_dispatch.pyare pure helpers used by the reports.tokenization.pyandmetrics.pyhandle impure tokenizer loading and Rust metric bridges.tokenizer_metrics.pyprovides tokenizer analysis metrics (compression ratio, vocabulary coverage).
src/glitchlings/zoo/- Core glitchling implementations and orchestration.core.pyhousesGlitchling/Gaggle, dataset helpers, transcript targeting, and pipeline caching.core_planning.py(pure) builds execution plans and normalises pipeline descriptors;core_execution.pydispatches plans through the Rust pipeline or Python fallbacks.corrupt_dispatch.py(pure) resolves transcript targets and assembles corruption results;rng.pyhandles seed derivation.- Glitchlings: Typogre, Hokey, Mim1c, Wherewolf, Pedant (
zoo/pedant/), Jargoyle, Rushmore (duplication/adjacent swap/zero-width), Redactyl, Scannequin, Zeedub.
src/glitchlings/util/- Shared helpers includingSAMPLE_TEXT, keyboard neighbour and shift maps, transcript helpers, and diff utilities.adapters.pyprovidescoerce_gaggle()for normalizing glitchling inputs across DLC integrations.
src/glitchlings/protocols.py- Protocol definitions for dependency inversion (e.g.,Corruptorprotocol allows attack module to work with glitchlings without circular imports).src/glitchlings/assets/- Bundled data (homoglyphs, homophones, Hokey assets, OCR confusions, pipeline assets) plus lexeme dictionaries underlexemes/(synonyms, colors, corporate, academic, cyberpunk, lovecraftian).src/glitchlings/conf/- Configuration schema, dataclasses, and loaders for YAML attack configs.src/glitchlings/compat/- Optional dependency loaders (datasets, tokenizers, PyTorch, Lightning, Hugging Face).src/glitchlings/dev/- Doc refresh helpers (python -m glitchlings.dev.docs/glitchlings-refresh-docs).src/glitchlings/dlc/- Optional DLC integrations._shared.pyprovides shared utilities for dataset column resolution and batch corruption.prime.pyintegrates with theverifiersenvironments and Prime/HF connectors.pytorch.pyandpytorch_lightning.pyprovide PyTorch Dataset/DataModule wrappers.huggingface.pywraps Hugging Face datasets with corruption transforms.langchain.pyprovides LangChain integration helpers.nemo.pyprovides NVIDIA NeMo DataDesigner column generator plugin.gutenberg.pyprovides access to the Gutenberg corpus.
benchmarks/- Performance harnesses (pipeline_benchmark.py) covering Python and Rust execution paths.docs/- Field guide, development notes, CLI/Attack/config docs, and generated references (cli.md,configuration.md,attack.md,monster-manual.md,glitchling-gallery.md). Regenerate generated pages withpython -m glitchlings.dev.docs.tests/- Pytest suite covering orchestration, determinism, DLC hooks, CLI, and Rust parity.- Highlights:
tests/core/test_core_planning.py(plan building/pipeline descriptors),tests/core/test_corrupt_dispatch.py(transcript targeting),tests/attack/test_attack.py(Attack orchestration, tokenization, metrics),tests/core/test_hybrid_pipeline.py(Rust pipeline parity),tests/cli/test_cli.py(CLI contract),tests/dlc/test_prime_echo_chamber.py(Prime DLC),tests/core/test_parameter_effects.py(argument coverage).
- Highlights:
- Target Python 3.10+ (see
pyproject.toml). - Follow the import order used in the package: standard library, third-party, then local modules.
- Every new glitchling must:
- Subclass
Glitchling, settingscopeandorderviaAttackWave/AttackOrderfromcore.py. - Accept keyword-only parameters in
__init__, forwarding them throughsuper().__init__so they are tracked byset_param. - Drive all randomness through the instance's RNG and the boundary helpers in
zoo.rng; do not rely on module-level RNG state. - Provide a
pipeline_operationdescriptor when the Rust pipeline can accelerate the behaviour (usebuild_pipeline_descriptorhelpers when applicable); returnNonewhen only the Python path is valid. - Preserve transcript targeting and pattern masking by routing corruption through
Glitchling.corruptrather than bypassing it.
- Subclass
- Keep helper functions small and well-scoped; include docstrings that describe behaviour and note any determinism considerations.
- When mutating token sequences, preserve whitespace and punctuation via separator-preserving regex splits (see
zoo/transforms.py). - CLI work should continue the existing UX: validate inputs with
ArgumentParser.error, keep deterministic output ordering, and gate optional behaviours behind explicit flags. - Treat Rust failures as fatal: the compiled backend must import cleanly, surface identical signatures, and stay in lockstep with the Python shims.
- Run the full suite with
pytestfrom the repository root.
- Expose configurable parameters via
set_paramso fixtures intests/test_glitchlings_determinism.pycan reset seeds predictably. - Derive RNGs from the enclosing context (
Gaggle.derive_seedand helpers inzoo.rng) instead of using global state. - Keep pipeline descriptors and plan inputs deterministic (avoid unordered mappings, normalise layouts before returning).
- When sampling subsets (e.g., replacements or deletions), stabilise candidate ordering before selecting to keep results reproducible.
- Preserve transcript turn ordering and pattern masks when assembling results (use
corrupt_dispatchhelpers where appropriate).
The Rust extension (rust/zoo/) provides high-performance implementations via PyO3.
rust/zoo/src/
├── lib.rs # PyO3 module, FFI boundary, PyOperationConfig dispatch
├── pipeline.rs # Pipeline orchestration, batch execution
├── operations.rs # TextOperation trait, OperationRng, common operations
├── text_buffer.rs # Segment-aware text representation (protected regions)
├── rng.rs # DeterministicRng for reproducible operations
├── cache.rs # Content-addressed caching for layouts/assets
├── resources.rs # Embedded asset loading (homophones, homoglyphs)
├── keyboard_typos.rs # Typogre: keyboard proximity typos
├── homoglyphs.rs # Mim1c: Unicode confusable substitution
├── homophones.rs # Wherewolf: homophone replacement
├── word_stretching.rs # Hokey: word elongation
├── lexeme_substitution.rs # Jargoyle: synonym/lexeme replacement
├── grammar_rules.rs # Pedant: grammar rule application
├── zero_width.rs # Zeedub: zero-width character insertion
└── metrics.rs # Token metrics (delta, edit distance, compression)
- Rebuild after changes:
uv build -Uq - No fallback mode: Rust must compile and import - there is no Python-only mode
- Determinism: Use
DeterministicRngfromrng.rs- neverthread_rng()in operation logic - Signature parity: Keep exports synchronized with
internal/rust_ffi.pyshims - Test parity:
tests/core/test_hybrid_pipeline.pyverifies Rust/Python equivalence
- Create
src/my_op.rsimplementingTextOperationtrait - Add
mod my_op;tolib.rs - Add variant to
PyOperationConfigenum with#[pyo3(from_item_all)]fields - Add match arm in
build_operation()to construct the operation - If direct Python access needed, add
#[pyfunction]export - Add Python shim in
internal/rust_ffi.py - Add parity tests comparing Rust vs Python output
cd rust/zoo
cargo bench --bench baseline_performanceFlamegraphs are generated in target/criterion/*/profile/flamegraph.svg.
- The CLI lists built-in glitchlings (
glitchlings --list) and can show diffs; updateBUILTIN_GLITCHLINGSand help text when introducing new creatures. - Keep documentation synchronised: update
README.md,docs/index.md, per-glitchling reference pages,MONSTER_MANUAL.md, and generated docs (docs/cli.md,docs/monster-manual.md,docs/glitchling-gallery.md) when behaviours or defaults change. Regenerate generated pages viapython -m glitchlings.dev.docsorglitchlings-refresh-docs. - When editing keyboard layouts or homoglyph mappings, ensure downstream consumers continue to work with lowercase keys (
util.KEYNEIGHBORS). - Rebuild the Rust extension after touching
rust/zoo/(e.g.,uv build -Uq). Verify the Rust backend builds in every environment (CI, local, release) and fix import errors immediately - there is no supported Python-only mode anymore.
The codebase explicitly separates pure (functionally deterministic) code from impure (side-effectful) code. This architecture discourages AI agents from adding unnecessary defensive code by keeping validation and transformation concerns separate. See docs/development.md for the full specification.
These modules contain only pure functions - same inputs always produce same outputs:
| Module | Purpose |
|---|---|
zoo/validation.py |
Parameter validation and normalization |
zoo/transforms.py |
Text tokenization, transformation utilities, word splitting |
zoo/rng.py |
Seed resolution and RNG helpers |
zoo/core_planning.py |
Orchestration plan construction and pipeline descriptor normalization |
zoo/corrupt_dispatch.py |
Transcript target resolution and result assembly scaffolding |
compat/types.py |
Pure type definitions for optional dependency loading |
conf/types.py |
Pure dataclass definitions for configuration (RuntimeConfig, AttackConfig) |
constants.py |
Centralized default values and constants (no I/O operations) |
protocols.py |
Protocol definitions for dependency inversion (Corruptor, etc.) |
attack/compose.py |
Pure result assembly for Attack (extract_transcript_contents, build_*_result) |
attack/encode.py |
Pure encoding utilities (encode_single, encode_batch, describe_tokenizer) |
attack/metrics_dispatch.py |
Pure metric dispatch logic (is_batch, validate_batch_consistency) |
attack/core_planning.py |
Pure attack plan construction and validation |
When writing code in pure modules:
- Trust that inputs are already validated - do NOT add defensive
Nonechecks - Do NOT import from impure modules (
internal/rust.py,compat/loaders.py,conf/loaders.py,attack/core.py,attack/tokenization.py,attack/metrics.py,zoo/core.py,zoo/core_execution.py) - Do NOT use
random.Random()instantiation - accept pre-computed random values - Do NOT catch exceptions around trusted internal calls
- Use only standard library imports or other pure modules
These modules handle IO, FFI, and mutable state:
internal/rust.py/internal/rust_ffi.py- Low-level Rust FFI loader and primitivescompat/loaders.py- Optional dependency loading with lazy import machineryconf/loaders.py- Configuration file loading, caching, and Gaggle constructionzoo/core.py/zoo/core_execution.py- Glitchling orchestration, transcript-aware corruption, Rust pipeline executionattack/core.py/attack/core_execution.py- Attack orchestrator and execution dispatchattack/tokenization.py/attack/metrics.py- Tokenizer resolution and Rust metric loadingattack/analysis.py- Analysis tools (SeedSweep, GridSearch, TokenizerComparison)util/adapters.py- Gaggle coercion and normalization helpersdlc/*- All DLC integrations (PyTorch, HuggingFace, LangChain, NeMo, etc.)
Validation belongs at module boundaries where untrusted input enters:
- CLI argument parsing (
main.py) - Public API entry points (
Glitchling.__init__,Attack.__init__) - Configuration loaders and orchestration bridges (
conf/,zoo/core.py)
Use zoo/validation.py functions at these boundaries:
# Correct: validate at boundary, trust inside
class MyGlitchling(Glitchling):
def __init__(self, *, rate: float = 0.1, **kwargs):
super().__init__(**kwargs)
self.rate = clamp_rate(rate) # boundary validation
def _transform(self, text: str) -> str:
# Trust self.rate is valid - no defensive checks here
return apply_transformation(text, self.rate)# Wrong: defensive checks inside transformation
def apply_transformation(text: str, rate: float) -> str:
if rate is None: # DON'T DO THIS
rate = 0.1
if not 0 <= rate <= 1: # DON'T DO THIS
raise ValueError("rate out of range")
...For deterministic behaviour, accept seeds or pre-computed random values instead of RNG objects:
# Pure function: accepts pre-computed value
def select_word(words: list[str], random_index: int) -> str:
return words[random_index]
# Boundary: resolves seed, generates random values
def corrupt(self, text: str) -> str:
seed = resolve_seed(self.seed, self.rng)
rng = random.Random(seed)
index = rng.randrange(len(words))
return select_word(words, index)When adding new code, check which layer the file belongs to:
- Pure modules (
zoo/validation.py,zoo/transforms.py,zoo/rng.py,zoo/core_planning.py,zoo/corrupt_dispatch.py,compat/types.py,conf/types.py,constants.py,protocols.py,attack/compose.py,attack/encode.py,attack/metrics_dispatch.py,attack/core_planning.py): trust inputs, no side effects - Boundary modules (
main.py,__init__methods,zoo/core.py,attack/core.py): validate thoroughly once - Impure modules (
internal/rust.py,compat/loaders.py,conf/loaders.py,attack/tokenization.py,attack/metrics.py,attack/core_execution.py,attack/analysis.py,zoo/core_execution.py,util/adapters.py,dlc/*): side effects allowed
The test suite in tests/core/test_purity_architecture.py enforces import conventions automatically.