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
32 changes: 32 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copilot Instructions for multiplex

*multiplex* is a modular framework for prototyping LLM-based mutation testing. One run mutates a single Java method (located via tree-sitter), generates mutants with an LLM, splices each mutant back into the source file, and runs the target project's tests to see which mutants are killed.

## Read the docs before reading source

Detailed, task-scoped documentation lives in `docs/` — load only the file relevant to the task:

- `docs/ARCHITECTURE.md` — end-to-end pipeline, approaches, execution backends, output artifacts.
- `docs/MODULE_REFERENCE.md` — every function's signature, behavior, and side effects.
- `docs/CONFIG.md` — full `config.yml` schema.
- `docs/EXTENDING.md` — checklists for adding approaches, execution backends, or LLM providers.
- `docs/DEVELOPMENT.md` — setup, conventions, CI, and the current list of known bugs/gotchas.

## Commands

```bash
uv sync -p 3.13 # install deps (pin 3.13: tiktoken fails to build on 3.14)
uv run pytest tests # all tests — must run from repo root (relative resource paths)
uv run pytest tests/checks/test_compiles.py::test_name # one test
uv run ruff check . # lint (matches CI)
uv run ./multiplex ./path/to/config.yml # run the tool
```

## Rules that prevent broken changes

- **Import convention**: the tool runs as a directory (`uv run ./multiplex`), so `multiplex/` itself is on `sys.path`. Modules inside `multiplex/` import each other **without** the package prefix (`from model import Model`, `from util.io import ...`); tests import **with** it (`from multiplex.checks... import ...`). Match the style of the file being edited.
- Adding a mutant-generation approach touches three places: a new `multiplex/approach/<name>/` package with `controller.py`, an `APPROACH_PROMPT_KEYS` entry in `multiplex/prompts.py`, and a dispatch branch in `multiplex/__main__.py`. Only the selected approach's `system_prompts` keys are required; `resolve_prompts` validates the approach and its keys up front, raising `SystemExit` before any destructive step.
- Mutant files must be complete replacement methods written to `output/<approach>-mutants/mutant_N.java`; they are spliced verbatim over the original method's byte range.
- tree-sitter versions are pinned (`tree-sitter==0.23.2`, `tree-sitter-java==0.23.5`); do not bump them casually — the parsing code depends on that API.
- A runnable end-to-end example lives in `examples/` (self-contained Maven project, `basic` approach, `mvn` backend): `uv run multiplex ./examples/config.yml`. The STPA mutant loop still drops its last item — check `docs/DEVELOPMENT.md` § Known issues.
- Keep pure logic (parsing, splicing, checks) separate from `model.make_request` call sites — LLM calls are not mocked in tests.
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,9 @@ cython_debug/
.idea/*
.vscode/*

# OS
.DS_Store
# OS
.DS_Store

# multiplex run artifacts (created under <projectroot> at runtime)
output/
*.orig
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ uv run ./multiplex ./path/to/config.yml # run the tool
## Critical rules

- **Import convention**: modules inside `multiplex/` import each other **without** the package prefix (`from model import Model`); tests import **with** it (`from multiplex.checks... import ...`). Match the file you are editing. Rationale in `docs/DEVELOPMENT.md`.
- Adding an approach touches three places (package + prompt keys + dispatch); all `system_prompts` config keys are read unconditionally at startup. See `docs/EXTENDING.md`.
- Adding an approach touches three places (package + `APPROACH_PROMPT_KEYS` entry in `multiplex/prompts.py` + dispatch branch). Only the selected approach's `system_prompts` keys are required, validated up front by `resolve_prompts`. See `docs/EXTENDING.md`.
- tree-sitter versions are pinned; do not bump them casually.
- Check `docs/DEVELOPMENT.md` § Known issues before building on `execute/maven.py` (broken) or the STPA mutant loop (off-by-one); `execute/defects4j.py` is the reference execution backend.
- A runnable example lives in `examples/`: `uv run multiplex ./examples/config.yml` (self-contained Maven project, needs `mvn` + a JDK + a running Ollama). See `docs/DEVELOPMENT.md` § Example.
- The STPA mutant loop has a verified off-by-one — see `docs/DEVELOPMENT.md` § Known issues.
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ uv run ./multiplex ./path/to/config.yml # run the tool
## Critical conventions

- **Import convention**: the tool runs as a directory (`uv run ./multiplex`), so `multiplex/` itself is on `sys.path`. Modules inside `multiplex/` import each other **without** the package prefix (`from model import Model`, `from util.io import ...`); tests import **with** it (`from multiplex.checks... import ...`). Match the style of the file you are editing.
- Pipeline entry point and dispatch: `multiplex/__main__.py`. Adding an approach touches three places (new `approach/<name>/controller.py` package, prompt keys in config **and** the `prompts` dict in `__main__.py`, dispatch branch) — see docs/EXTENDING.md.
- All `system_prompts` config keys are read unconditionally at startup; a missing key is an immediate KeyError.
- Pipeline entry point and dispatch: `multiplex/__main__.py`. Adding an approach touches three places (new `approach/<name>/controller.py` package, a `APPROACH_PROMPT_KEYS` entry in `multiplex/prompts.py`, dispatch branch in `__main__.py`) — see docs/EXTENDING.md.
- Only the selected approach's `system_prompts` keys are required; `resolve_prompts` in `multiplex/prompts.py` validates them (and the approach name) up front, raising `SystemExit` before any destructive step.
- `<projectroot>/output/` is wiped at the start of every run; the target source file is edited in place and restored from a `.orig` backup.
- Before relying on `execute/maven.py` or the STPA mutant loop, check docs/DEVELOPMENT.md § Known issues — both have verified bugs (Maven backend is broken; use `execute/defects4j.py` as reference).
- A runnable end-to-end example lives in `examples/` (self-contained Maven project, `basic` approach, `mvn` backend): `uv run multiplex ./examples/config.yml` (needs `mvn`, a JDK, and a running Ollama). See docs/DEVELOPMENT.md § Example.
- The STPA mutant loop has a verified off-by-one (drops its last UCA) — see docs/DEVELOPMENT.md § Known issues.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Ensure Ollama is running when using *multiplex* for this example.
llm:
model: ollama/gpt-oss:20b
endpoint: http://127.0.0.1:11434
tokenenvvar: # env var name storing token (NOT TOKEN)
token_env_var: # env var name storing token (NOT TOKEN)
```
[IMPORTANT]
Security Note: Never hardcode API keys in your config file. Set an environment variable and reference its name in the tokenenvvar field.
Comment thread
MgnMtn marked this conversation as resolved.
Expand All @@ -68,6 +68,18 @@ Once configured and modules are set up, users can run *multiplex* using the foll
uv run /path/to/multiplex ./path/to/config.yml
```

#### Try the bundled example
A self-contained example (a small Maven project mutated with the `basic`
approach) is included. With `mvn` + a JDK on your `PATH` and a running Ollama
(`ollama pull gpt-oss:20b`, or edit `llm.model` in the config), run from the
repo root:
```bash
uv run multiplex ./examples/config.yml
```
Results are written to `examples/project/example/output/basic-mutants/`
(`mutant_summary.csv`). See [`examples/README.md`](examples/README.md) for
details.

## 🧩 Existing Modules
*multiplex* currently includes five mutant generation modules and two execution and evaluation modules:

Expand Down
8 changes: 6 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@ Selected by `project.runtool`:
each compilable mutant's tests with a timeout of 5× baseline. A surviving
mutant is one whose test run reports `Failing tests: 0`. Requires the
`defects4j` CLI and a `JDK_11` env var (see DEVELOPMENT.md).
- `mvn` → `maven.py` — **currently broken/incomplete**; do not rely on it
(details in DEVELOPMENT.md § Known issues).
- `mvn` → `maven.py` — self-contained backend for plain Maven projects. Runs
`mvn -f <project_root> clean test`; a mutant survives if the build passes
(`BUILD SUCCESS`). Baselines the original first (aborts if its tests fail),
then per mutant does the same equivalence → rewrite → compilable → test →
summary flow as the Defects4J backend. Used by the runnable example under
`examples/` (see DEVELOPMENT.md § Example).

## Output artifacts

Expand Down
18 changes: 12 additions & 6 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
`uv run ./multiplex ./path/to/config.yml`.
Template: [`examples/config.yml`](../examples/config.yml).

All keys below are read unconditionally by `multiplex/__main__.py` — a missing
key raises `KeyError` at startup, even for approaches you are not running.
Only the keys the selected `mutation.approach` needs are required. `project`,
`mutation`, and `llm` keys are always required; `system_prompts` keys are
required per-approach (see that section). Missing required keys fail fast at
startup with an actionable `SystemExit`, before any output is wiped.

## `project`

Expand All @@ -15,13 +17,13 @@ key raises `KeyError` at startup, even for approaches you are not running.
| `filename` | path | The `.java` source file containing the method under test. Backed up to `<filename>.orig` during the run. |
| `method` | string | Name of the method (or constructor) to mutate. |
| `line` | int | 1-based line number of the method **name identifier** in `filename` (not annotations above it). Both `method` and `line` must match for extraction to succeed; disambiguates overloads. |
| `runtool` | `mvn` \| `d4j` | Execution backend. `d4j` (Defects4J) is the complete one; `mvn` is currently broken (DEVELOPMENT.md § Known issues). |
| `runtool` | `mvn` \| `d4j` | Execution backend. `mvn` runs a plain Maven project (`mvn clean test`) and is used by the runnable `examples/` setup; `d4j` targets a Defects4J checkout (needs the `defects4j` CLI + `JDK_11`). |

## `mutation`

| Key | Type | Meaning |
|-----|------|---------|
| `approach` | `basic` \| `hazop` \| `stpa` \| `mutahunter` \| `llmorpheus` | Mutant-generation approach. Any other value prints "Invalid approach" and continues to the execution phase (which then fails on the missing mutants dir). |
| `approach` | `basic` \| `hazop` \| `stpa` \| `mutahunter` \| `llmorpheus` | Mutant-generation approach. An unknown value fails fast at startup with a `SystemExit` listing the valid approaches. |

## `llm`

Expand All @@ -36,8 +38,12 @@ example config use `token_env_var`. `token_env_var` is correct.

## `system_prompts`

All nine keys are required (multi-line YAML strings, typically `|` blocks).
User prompts are constructed in code; only system prompts live in config.
Multi-line YAML strings (typically `|` blocks). Only the keys used by the
selected `mutation.approach` are required — prompts for other approaches may be
omitted. The approach→required-keys mapping is `APPROACH_PROMPT_KEYS` in
`multiplex/prompts.py`; a missing or empty required key raises a `SystemExit`
naming it. User prompts are constructed in code; only system prompts live in
config.

| Key | Used by |
|-----|---------|
Expand Down
62 changes: 47 additions & 15 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ uv run ./multiplex ./path/to/config.yml # run the tool
CI (GitHub Actions, on push/PR to `main`): `test.yml` runs
`uv run pytest tests` on Python 3.13; `ruff.yml` runs `uv run ruff check`.

## Example

A self-contained end-to-end example lives under `examples/`:

```bash
uv run multiplex ./examples/config.yml # from the repo root
```

- Target: `examples/project/example/` — a tiny Maven project with one method
(`com.example.Classifier.classify`) and JUnit tests pinning its behavior.
- `examples/config.yml` uses the `basic` approach and the `mvn` runtool.
- Prerequisites: `mvn` + a JDK 11+ on PATH (the project targets Java 11; if
`mvn` picks up an older JDK via `JAVA_HOME` the compile fails with "release
version 11 not supported"), and a running Ollama serving the model
named in `llm.model` (default `gpt-oss:20b`; point it at any model you have,
e.g. `ollama pull gpt-oss:20b`). Any LiteLLM-supported endpoint works if you
edit `llm`.
- Output lands in `examples/project/example/output/basic-mutants/`
(`mutant_N.java` plus `mutant_summary.csv`). Run artifacts (`output/`,
`*.orig`, Maven `target/`) are git-ignored.
- The `basic` approach makes 10 LLM calls and the `mvn` backend runs the test
suite once per compilable mutant, so a full run takes a few minutes (longer
on a slow/local model).

## Import convention (critical)

The tool is executed as a directory (`uv run ./multiplex`), which puts
Expand Down Expand Up @@ -58,21 +82,28 @@ same process; keep the two worlds separate.

Bugs — fix or work around, don't replicate:

1. **`execute/maven.py` is broken**: `run_mutants` calls `rewrite_method` with
5 args (signature takes 4 → TypeError), never calls its own
`_execute_mutant` (so `mvn test` never runs), and passes a directory to
`check_mutant_compilable` (which parses a single file). Use
`execute/defects4j.py` as the reference implementation.
2. **STPA off-by-one**: `approach/stpa/code_generator.py` loops
1. **STPA off-by-one**: `approach/stpa/code_generator.py` loops
`range(0, ucas_count - 1)`, silently skipping the last UCA.
3. **Method-not-found crash**: `extract_method_from_file` returns `None` on a
2. **Method-not-found crash**: `extract_method_from_file` returns `None` on a
miss, but `__main__.py` unpacks it as a tuple → opaque TypeError. Symptom:
check `project.method`/`project.line` in the config (`line` must be the
line of the method-name identifier).
4. **README key mismatch**: README shows `llm.tokenenvvar`; the code reads
3. **README key mismatch**: README shows `llm.tokenenvvar`; the code reads
`llm.token_env_var` (the example config is correct).
5. **`tests/execute/test_defects4j.py` is an empty stub** — the execute layer
has zero test coverage.
4. **`tests/execute/test_defects4j.py` is an empty stub** — the Defects4J
backend has no test coverage. (The Maven backend is covered by
`tests/execute/test_maven.py`, which mocks `mvn` via `_execute`; that file's
`conftest.py` adds `multiplex/` to `sys.path` so the flat-import backend
module is importable — the pattern to copy for a Defects4J test.)

Recently fixed (kept here so stale references elsewhere are recognizable):
- The `prompts` dict in `__main__.py` used to read **all** `system_prompts`
keys unconditionally, so any missing key crashed every run with `KeyError` —
the shipped example config itself crashed this way. Now only the selected
approach's keys are required, validated up front via `multiplex/prompts.py`.
- `execute/maven.py` used to be broken (wrong-arity `rewrite_method` call, never
ran `mvn test`, passed a directory to `check_mutant_compilable`). It now
mirrors `execute/defects4j.py` and drives the runnable example (§ Example).

Behavioral gotchas — by design (or at least current design), be aware:

Expand All @@ -85,9 +116,10 @@ Behavioral gotchas — by design (or at least current design), be aware:
restore).
- "Compilable" means *parses without tree-sitter ERROR nodes* — no compiler
runs; type errors and unresolved symbols count as compilable.
- An invalid `mutation.approach` value only prints "Invalid approach" and
falls through to the execution phase.
- All nine `system_prompts` keys are read at startup regardless of the chosen
approacha missing key is a KeyError before anything runs.
- An invalid `mutation.approach` value, or a missing/empty required
`system_prompts` key for the chosen approach, raises `SystemExit` at startup
(before the output wipe). Only the selected approach's prompt keys are
requiredsee `multiplex/prompts.py`.
- Mutahunter prompts are not bundled (licensing); the user pastes them into
the config.
the config. Running the `mutahunter` approach without them fails fast with a
clear message.
14 changes: 8 additions & 6 deletions docs/EXTENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ the multi-prompt-chain template.
mutant = mutant.removeprefix("```java")
mutant = mutant.split("```", 1)[0]
```
2. **Register the prompt key(s)**: add `<name>_...` entries to the `prompts`
dict in `multiplex/__main__.py` *and* to the `system_prompts` section of
every config file (including `examples/config.yml`). All keys in that dict
are read unconditionally — omitting one breaks **every** run with KeyError,
not just runs of your approach.
2. **Register the prompt key(s)**: add a `"<name>": [...]` entry to
`APPROACH_PROMPT_KEYS` in `multiplex/prompts.py` listing the
`system_prompts` keys your approach reads. Only the selected approach's keys
are required at runtime, so users running other approaches need not define
yours (and vice versa). Document the keys in `docs/CONFIG.md`; adding them to
`examples/config.yml` (a commented stub is fine) is optional but helpful.
3. **Register the dispatch branch**: add an `elif config['mutation']['approach']
== "<name>":` branch in `multiplex/__main__.py` calling
`<name>.main(model, output_path, prompts)`, plus the corresponding
Expand All @@ -44,7 +45,8 @@ the multi-prompt-chain template.

## Add an execution/evaluation backend

Model on `multiplex/execute/defects4j.py` (not `maven.py`, which is broken).
Model on `multiplex/execute/defects4j.py` or `multiplex/execute/maven.py`
(both follow the same flow; `maven.py` is the simpler, self-contained one).

1. Create `multiplex/execute/<name>.py` exposing:
```python
Expand Down
27 changes: 23 additions & 4 deletions docs/MODULE_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ class Model:
- If `"azure" in endpoint`, sets `AZURE_AI_API_BASE` and `AZURE_AI_API_KEY`.
- `messages` is a LiteLLM/OpenAI-style list of `{"role": ..., "content": ...}`.

## prompts.py

```python
APPROACH_PROMPT_KEYS: dict[str, list[str]] # approach name -> required system_prompts keys
resolve_prompts(config, approach) -> dict # {key: prompt} for the approach's keys
```

- `resolve_prompts` reads only the selected approach's keys from
`config["system_prompts"]`. Raises `SystemExit` (actionable message) if
`approach` is unknown or any required key is missing/empty. Pure function, no
I/O — called by `__main__.py` before any destructive step. Importable in tests
as `from multiplex.prompts import ...` (no sibling imports, so it works under
both the flat runtime import and the package-prefixed test import).

## util/io.py

```python
Expand Down Expand Up @@ -162,7 +176,12 @@ run_mutants(project_root, filename, output_path,
method_start_byte, method_end_byte, duplicate, approach)
```

- **Broken — do not use as reference.** Calls `rewrite_method` with a stale
5-argument signature (TypeError), never invokes its own `_execute_mutant`
(`mvn clean test`), and calls `check_mutant_compilable` on a directory.
Model a fix on `execute/defects4j.py`. See DEVELOPMENT.md § Known issues.
- `_execute(project_root) -> bool`: runs `mvn -f <project_root> clean test`;
True if the output contains `BUILD SUCCESS` (all tests green → mutant
survived), False otherwise (mutant killed, incl. compile failures Maven
catches that tree-sitter does not).
- `run_mutants`: same flow as the Defects4J backend — baseline the original
project (raises `IOError` if its tests fail), then per mutant: restore from
backup → equivalence check → rewrite → compilable check → (if compilable) run
tests → append `[name, equivalent, compilable, survives]` → write
`mutant_summary.csv`. Drives the runnable example under `examples/`.
Loading
Loading