Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
432f037
ci(mutation): mutmut pilot over the stats, gates and cache key
napetrov Oct 22, 2026
d57ed0e
docs: name the gates the mutation pilot covers in CONTRIBUTING
antonwolfy Oct 22, 2026
c5c6715
ci(mutation): lock mutmut apart from the tools lock; time out the run…
napetrov Oct 22, 2026
e54316b
test(mutation): pin the regression defaults, score flags, both-arms f…
napetrov Oct 22, 2026
c6f1bfe
ci(pip-audit): audit requirements-mutation.lock.txt with the other locks
napetrov Oct 22, 2026
91d9ae6
Merge remote-tracking branch 'intel/main' into ci/mutation-pilot
napetrov Oct 22, 2026
b5785d6
docs(security): name requirements-mutation.lock.txt in the audited locks
antonwolfy Oct 23, 2026
60a99b6
test(locks): check the pip-audit command, not the workflow text
xaleryb Oct 23, 2026
55b32d8
test(stats): pin bootstrap_ci's default seed and resample count
xaleryb Oct 23, 2026
60eb92b
Merge intel/main into ci/mutation-pilot
napetrov Oct 23, 2026
705e015
test(stats, gates): kill the surviving tail-index, seed, flag and ren…
napetrov Oct 23, 2026
d27b2e3
ci(mutation): flag a partial run in the step summary
napetrov Oct 23, 2026
9f5e03e
test(caching): cover padded and mixed-case off values of the cache sw…
antonwolfy Oct 25, 2026
3ea86b2
ci(mutation): word the incomplete-run banner from the parts that apply
antonwolfy Oct 25, 2026
e854307
Merge remote-tracking branch 'intel/main' into sweep1025/pr178
napetrov Oct 26, 2026
b74f999
Merge remote-tracking branch 'intel/main' into ci/mutation-pilot
napetrov Oct 26, 2026
9b13d65
Merge remote-tracking branch 'intel/main' into ci/mutation-pilot
napetrov Oct 27, 2026
ad72f2a
Merge branch 'main' of https://github.com/intel/dokimi into pr178-mut…
napetrov Oct 29, 2026
a9c43fd
Address review: drop the seed claim from the confidence test
napetrov Oct 29, 2026
edd5ab2
Address review: pin the confidence mean_ci forwards to the bootstrap
napetrov Oct 29, 2026
31e9e2c
Address review: tell a relative zero-variance tolerance from an absol…
napetrov Oct 29, 2026
6968fb5
Address review: pin the two-pair floor of arm_significance from below
napetrov Oct 29, 2026
5060fea
Address review: make the score_cv "mean below one" test use a mean be…
napetrov Oct 29, 2026
536a2f8
Thread: PRR_kwDOVBg7g88AAAABTpwoWA: already fixed in c5c6715e
napetrov Oct 29, 2026
c4ddf2c
Thread: PRR_kwDOVBg7g88AAAABTqISXg: already fixed in c5c6715e, 705e01…
napetrov Oct 29, 2026
8c765d4
Thread: PRR_kwDOVBg7g88AAAABT2Oopw: already fixed in 705e015c
napetrov Oct 29, 2026
c253fa1
Thread: PRR_kwDOVBg7g88AAAABT2jElg: already fixed in b5785d6c
napetrov Oct 29, 2026
eb605b5
Thread: PRR_kwDOVBg7g88AAAABT31gsA: already fixed in 705e015c, d27b2e37
napetrov Oct 29, 2026
985dd86
Thread: PRR_kwDOVBg7g88AAAABUGpGQQ: already fixed in d27b2e37
napetrov Oct 29, 2026
4d83c76
Thread: PRR_kwDOVBg7g88AAAABUSescw: already fixed in 9b13d655
napetrov Oct 29, 2026
2fe56da
Thread: PRR_kwDOVBg7g88AAAABU0Q3YA: already fixed in 9b13d655
napetrov Oct 29, 2026
7a0dc61
Thread: IC_kwDOVBg7g88AAAABe0rN9w: already fixed in d27b2e37, 60eb92b4
napetrov Oct 29, 2026
7f7f5e6
Address review: pin the prefix breakpoint for a non-Claude model on B…
napetrov Oct 30, 2026
0bb8187
Address review: pin the one-resample bootstrap mean to a literal
napetrov Oct 30, 2026
b14891f
Address review: make the zero-variance scaling test fail without the …
napetrov Oct 30, 2026
00e2bf5
Address review: pin the Wilcoxon p-value to its literal
napetrov Oct 30, 2026
51eecbc
Address review: assert the measured flag in the report JSON
napetrov Oct 30, 2026
1418e6f
Address review: keep one constraint test for both tools locks
napetrov Oct 30, 2026
3234c2e
Address review: compare the mutmut pin with the lock, not a literal
napetrov Oct 30, 2026
4b3147c
Address review: pin the disabled gate for a missing or empty band list
napetrov Oct 30, 2026
1317896
Address review: a NaN band metric violates, a None metric raises
napetrov Oct 30, 2026
b7d043b
Merge remote-tracking branch 'intel/main' into ci/mutation-pilot
napetrov Oct 30, 2026
5450d63
fix(run): --strict exits 2 on a band the gate cannot read
napetrov Oct 30, 2026
601a67f
docs(changelog): say run --strict reads the critical bands strictly
napetrov Oct 30, 2026
9357193
docs: keep the comment blocks this PR adds to two lines
napetrov Oct 30, 2026
91db7ee
docs(readme): note run --strict exits 2 on an unreadable band
antonwolfy Oct 30, 2026
25dc6a8
test(stats): fold the n check into the relative tolerance test
antonwolfy Oct 30, 2026
17c78e0
ci(mutation): run the judge-cache and arm-runner tests against mutants
napetrov Oct 30, 2026
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
1 change: 1 addition & 0 deletions .github/.licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ header:
- 'dokimi/requirements.lock.txt'
- 'dokimi/requirements-docs.lock.txt'
- 'dokimi/requirements-ci.lock.txt'
- 'dokimi/requirements-mutation.lock.txt'
# Read by agents verbatim: a header would become part of the instructions,
# persona prompt or skill being measured.
- 'AGENTS.md'
Expand Down
2 changes: 1 addition & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
// and updates the SHA and the tag together.
"pre-commit": { enabled: true },
"pip-compile": {
managerFilePatterns: ["/(^|/)requirements(-ci)?\\.lock\\.txt$/"],
managerFilePatterns: ["/(^|/)requirements(-ci|-mutation)?\\.lock\\.txt$/"],
},
// Inside a range: bump the lock only. Outside it: move the range too.
rangeStrategy: "update-lockfile",
Expand Down
96 changes: 96 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

name: mutation

# Mutation pilot: mutmut over the measurement core (statistics, gate decisions, cache key), where a surviving mutant
# is a wrong number that ships silently. Weekly and informational: the score goes to the step summary, never a failure.
on:
workflow_dispatch:
schedule:
- cron: '17 5 * * 1'

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
working-directory: dokimi

jobs:
mutmut:
name: mutmut (pilot modules)
runs-on: ${{ github.repository_owner == 'intel' && 'intel-ubuntu-latest' || 'ubuntu-latest' }}
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · low
Nit: dependency caching is not enabled, so each weekly/manual run downloads the same pinned dependency closure again. Enable setup-python's pip cache, following docs.yml, and include both installed lockfiles in cache-dependency-path so changes to either invalidate the cache.

Suggestion:

Suggested change
python-version: '3.13'
python-version: '3.13'
cache: pip
cache-dependency-path: |
dokimi/requirements.lock.txt
dokimi/requirements-mutation.lock.txt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it is worth doing. Keeping this PR as is so the approved head stays unchanged; the pip cache (same pattern as docs.yml) goes in a follow-up.

# The runtime lock for the tests, and mutmut's own lock (requirements-mutation.txt),
# which no other workflow installs.
- name: Install deps
run: pip install --require-hashes -r requirements.lock.txt -r requirements-mutation.lock.txt
- name: Run mutmut
# Crashes are reported below, not failed on. The step timeout sits inside the job's 90 minutes, so the summary
# step still runs; a job-level timeout would fail the whole job instead.
timeout-minutes: 75
continue-on-error: true
run: mutmut run --max-children "$(nproc)"
Comment thread
antonwolfy marked this conversation as resolved.
Resolved
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
continue-on-error: true here plus || true on mutmut results mean a broken run (mutmut crash, bad config, or a missing test file) leaves the job green. Only the summary text shows it. That is acceptable for a pilot, but nobody reads a summary on a green weekly job. Consider failing the job, or emitting a ::warning:: annotation, when the results file is empty or has not-checked mutants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping continue-on-error: the weekly job is a pilot and should not go red on a mutmut hiccup. A follow-up adds a ::warning:: annotation when the results file is empty or has not checked mutants, so an incomplete run is visible on the run page, not only in the step summary.

- name: Score and survivors
if: always()
run: |
mutmut results --all true > mutmut-results.txt || true
python - <<'EOF' >> "$GITHUB_STEP_SUMMARY"
import collections

counts = collections.defaultdict(collections.Counter)
survivors = collections.defaultdict(list)
with open("mutmut-results.txt", encoding="utf-8") as fh:
for line in fh:
name, sep, status = line.strip().rpartition(": ")
if not sep:
continue
# <module>.x_<function>__mutmut_<n>, or x<sep><class><sep><method>
module = name.rsplit(".", 1)[0]
counts[module][status] += 1
if status == "survived":
survivors[module].append(name)

print("## Mutation pilot\n")
if not counts:
print("No results: see the `Run mutmut` step log.")
else:
print("| Module | Killed | Survived | Other | Score |")
print("|---|---:|---:|---:|---:|")
for module in sorted(counts):
c = counts[module]
killed, survived = c["killed"], c["survived"]
other = sum(c.values()) - killed - survived
scored = killed + survived
score = f"{100 * killed / scored:.1f}%" if scored else "n/a"
print(f"| `{module}` | {killed} | {survived} | {other} | {score} |")
unchecked = sum(c["not checked"] for c in counts.values())
unscored = sorted(m for m, c in counts.items() if not c["killed"] + c["survived"])
if unchecked or unscored:
parts = []
if unchecked:
parts.append(f"{unchecked} mutants were not checked (a step timeout or a crash cut the run short)")
if unscored:
parts.append(f"nothing was scored in {', '.join(f'`{m}`' for m in unscored)}")
print("\n> **Incomplete run: do not read the scores as final.** "
+ "; ".join(parts) + ". See the `Run mutmut` step log.")
print("\nOther: no tests, timeout, suspicious or not checked.")
print("`mutmut show <name>` prints a mutant's diff.\n")
for module in sorted(survivors):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Has mutation.yml been dispatched on this branch, and does the summary table render with real module names and survivor lists?

.github/workflows/mutation.yml:93-102 parses name: status lines. If the format differs (for example a status without ': ', or names without a dotted module), every line is skipped and the step prints 'No results' while the run itself is green.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not dispatched: workflow_dispatch only works for a workflow on the default branch, so it cannot run on intel/dokimi before merge, and Actions is off on the fork. I ran it locally instead, on 9b13d65, with mutmut 3.8.0 from requirements-mutation.lock.txt (Python 3.13). The scope was cut to dokimi/caching.py and tests/test_caching.py to keep it short. Then I fed mutmut results --all true to this step's script verbatim. The real lines look like dokimi.caching.x__env_flag__mutmut_1: killed: indented, : before the status, dotted module. The script parsed all 324 and rendered | \dokimi.caching` | 275 | 38 | 11 | 87.9% | plus a survivor list of 38 names (dokimi.caching.x_build_messages__mutmut_1` and so on). If a later mutmut changes the format, the step prints "No results", and the run still goes green, since the workflow is informational. The first scheduled run after merge is where that would show.

print(f"<details><summary>{module}: {len(survivors[module])} survivors</summary>\n")
print("```")
print("\n".join(survivors[module]))
print("```\n</details>\n")
EOF
14 changes: 4 additions & 10 deletions .github/workflows/pip-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,13 @@ jobs:
# pip-audit comes from the hash-locked tools lock (requirements-ci.txt pins it).
- name: Install tools
run: pip install --require-hashes -r requirements-ci.lock.txt
# The locks are the full closure (requirements-docs.lock.txt for the
# documentation site, requirements-ci.lock.txt for the tools), so --no-deps --disable-pip audits them as written
# without resolving or installing anything. Each --ignore-vuln is
# an advisory SECURITY.md documents and dokimi/osv-scanner.toml ignores;
# add or drop them together, in all three.
# pip-audit skips a pin whose marker is false on this 3.13 runner (the
# pins for older interpreters, for one), so markers are stripped, a pin
# the locks share is kept once, a name pinned twice goes to a second file,
# and the trailing `\` of a hashed pin is dropped (its --hash lines are skipped).
# Full-closure locks, so --no-deps --disable-pip audits them as written; markers are stripped (3.13 skips false ones).
# Each --ignore-vuln is in SECURITY.md and osv-scanner.toml: add or drop it in all three, together.
- name: pip-audit
run: |
sed -e 's/ *\\$//' -e 's/ *;.*//' \
requirements.lock.txt requirements-docs.lock.txt requirements-ci.lock.txt | sort -u \
requirements.lock.txt requirements-docs.lock.txt requirements-ci.lock.txt \
requirements-mutation.lock.txt | sort -u \
| awk -F'==' '/^[A-Za-z0-9]/ { print > (ENVIRON["RUNNER_TEMP"] "/pins" seen[$1]++ ".txt") }'
for f in "$RUNNER_TEMP"/pins*.txt; do
pip-audit -r "$f" --no-deps --disable-pip --progress-spinner off \
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ coverage.json
/dokimi/outcomes-result.json
/dokimi/journeys-result.json
/dokimi/journeys-result.md
# mutmut's working copy and results (`mutmut run`, the mutation workflow)
/dokimi/mutants/
/dokimi/mutmut-results.txt

# Local environments and credentials
.venv/
Expand Down
21 changes: 19 additions & 2 deletions dokimi/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,36 @@ pre-commit run --all-files # the whole tree, as CI's pre-com
mypy and pytest are not hooks: they need the full environment and pytest takes
minutes. `make ci-local` runs both; `make hooks` runs mypy before a push.

**Mutation pilot.** mutmut (pinned in `requirements-mutation.txt`, locked apart from the tools lock) mutates the
measurement core: the statistics in `dokimi/eval/stats.py` and
`binary_stats.py`, the score, regression, critical-band and trust gates in
`dokimi/gate/` (not `novelty_gate.py`) and the response-cache key in
`dokimi/caching.py` (the list is `only_mutate` under `[tool.mutmut]` in
`pyproject.toml`). A mutant that survives is a change to that code, such as
a flipped comparison, an off-by-one or a dropped term, that no test notices.
The `mutation` workflow runs it weekly and on demand and writes the score per
module and the survivors to the job summary; it never fails a build. From
`dokimi/`: `mutmut run --max-children 8`, then `mutmut results` and
`mutmut show <name>` for a survivor's diff. A test that kills a survivor
asserts the value the mutant changes. Some survivors are equivalent (the mutant
computes the same result, e.g. a `<` that differs only at a value no input
reaches); leave those alone. `mutants/` is scratch and git-ignored.

Dependencies install from `requirements.lock.txt`; the docs build's from
`requirements-docs.lock.txt` on top of it, and the tools CI, `make dev`, the
release job and the eval image use (`requirements-ci.txt`) from
`requirements-ci.lock.txt`. That lock also pins the build backend, setuptools:
package-smoke and the release job build with `python -m build --no-isolation`,
so the published sdist and wheel are not built by a backend resolved fresh from
PyPI. When you change a range in
PyPI. The mutation workflow adds mutmut's closure
(`requirements-mutation.txt`) from `requirements-mutation.lock.txt`, which
nothing else installs, and pip-audit audits with the others. When you change a range in
`requirements*.txt`, regenerate the lock with the command in its header: run it
from `dokimi/`, and add `--upgrade` to pick up new versions (without it, uv
keeps the current pins). The command's `--exclude-newer=P7D` skips releases
less than 7 days old; if a security fix is newer than that, its vulnerability PR
fails to resolve, so regenerate the lock by hand without the flag. The docs lock
and the tools lock (`requirements-ci.lock.txt`) are constrained to the main one,
and the tools locks (`requirements-ci.lock.txt`, `requirements-mutation.lock.txt`) are constrained to the main one,
so regenerate them (command in each header) after the main lock changes too;
`tests/test_dependency_locks.py` fails while a pin they share differs.

Expand Down
2 changes: 1 addition & 1 deletion dokimi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ The default static benchmark is configured in `benchmarks/spec.v1.yaml` and curr
| `readability` | Estimates text readability with a Flesch-Kincaid-style score. |
| `example_pass_rate` | Executes supported fenced code examples in isolation. |

Weights are normalized across enabled metrics. The benchmark also supports soft gates, hard gates, critical bands, and regression thresholds. Hard gates and critical bands are enforced only when `--strict` is passed. With `--strict`, a spec without a `golden_manifest` also exits 1 when no document was measured. A spec with one, as `spec.v1.yaml` has, exits 2 with or without `--strict` when fewer documents than its `min_docs` match, none included. An empty or absolute `include` pattern exits 2 as well. Exit 1 is not only the `--strict` verdict: a document that cannot be read, or an `--out-json` or `--out-md` path that cannot be written, also exits 1, with an `Error:` or `❌` line on stderr saying which.
Weights are normalized across enabled metrics. The benchmark also supports soft gates, hard gates, critical bands, and regression thresholds. Hard gates and critical bands are enforced only when `--strict` is passed. With `--strict`, a spec without a `golden_manifest` also exits 1 when no document was measured. A spec with one, as `spec.v1.yaml` has, exits 2 with or without `--strict` when fewer documents than its `min_docs` match, none included. An empty or absolute `include` pattern exits 2 as well. With `--strict`, a critical band the gate cannot read (an unknown condition, or a `None` metric) exits 2 too. Exit 1 is not only the `--strict` verdict: a document that cannot be read, or an `--out-json` or `--out-md` path that cannot be written, also exits 1, with an `Error:` or `❌` line on stderr saying which.

## LLM and treatment evaluation workflow

Expand Down
3 changes: 2 additions & 1 deletion dokimi/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ The project is pre-1.0; only the latest `main` is supported. Fixes land on
## Known advisories in dependencies

`pip-audit` against the locks (`requirements.lock.txt`,
`requirements-docs.lock.txt` and `requirements-ci.lock.txt`) reports these advisories with no
`requirements-docs.lock.txt`, `requirements-ci.lock.txt` and
`requirements-mutation.lock.txt`) reports these advisories with no
fix the locks can take:

- **filelock 3.32.7 — thirteen advisories** (fixed in 4.2.1, GHSA-j8f7-rjxc-mr56
Expand Down
15 changes: 15 additions & 0 deletions dokimi/changelog.d/20261021_160000_napetrov_mutation-pilot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
### Added

- **Mutation pilot over the measurement core** — `.github/workflows/mutation.yml`
runs mutmut 3.8.0 (hash-pinned in its own `requirements-mutation.lock.txt`, installed by that workflow alone) weekly and on demand over
`eval/stats.py`, `eval/binary_stats.py`, the score, regression, critical-band and
trust gates and `caching.py`, and writes the score per module and the surviving
mutants to the job summary. It never fails a build. New tests kill the survivors
that were real gaps (inclusive thresholds, the Wilcoxon veto, per-condition metric
keys, the bootstrap tail index, the cache key's role and sampling parameters), which
takes the pilot from 75.3% to 92.0% of mutants killed. Configuration:
`[tool.mutmut]` in `pyproject.toml`; usage: CONTRIBUTING.md.

### Changed

- **`run --strict` reads the critical bands strictly** — a NaN metric now violates its band; it passed silently before. A band the gate cannot read (an unknown condition, or a `None` metric) exits 2 and names the spec, instead of ending in a traceback.
6 changes: 5 additions & 1 deletion dokimi/dokimi/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ def cmd_run(args: argparse.Namespace) -> None:
from dokimi.gate.critical_bands import check_critical_bands

hard_gate = check_gate(result["summary"], spec, "hard")
bands = check_critical_bands(result["summary"], spec)
try:
bands = check_critical_bands(result["summary"], spec)
except ValueError as exc: # a band the gate cannot read: a bad spec, named, not a traceback
Comment thread
antonwolfy marked this conversation as resolved.
Resolved
print(f"Error: {spec_path}: {exc}", file=sys.stderr)
sys.exit(2)

# Exit 1 if hard gate fails
if hard_gate.enabled and not hard_gate.passed:
Expand Down
30 changes: 22 additions & 8 deletions dokimi/dokimi/gate/critical_bands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import math
from dataclasses import dataclass


Expand Down Expand Up @@ -36,6 +37,19 @@ def has_violations(self) -> bool:
return any(v.violated for v in self.violations)


def _metric(summary: dict, key: str, condition: str) -> float:
"""The summary value a condition reads. A missing key reads as 0.0; None is an error."""
raw = summary.get(key, 0.0)
if raw is None:
raise ValueError(f"critical_bands {condition}: summary metric {key!r} is None, not a number")
return float(raw)


def _below(actual: float, threshold: float) -> bool:
"""Strictly below the threshold. NaN is not a number the gate can pass, so it violates."""
return math.isnan(actual) or actual < threshold


def check_critical_bands(summary: dict, spec: dict) -> CriticalBandsResult:
"""Check if summary violates any critical bands.

Expand All @@ -62,20 +76,20 @@ def check_critical_bands(summary: dict, spec: dict) -> CriticalBandsResult:
threshold = float(band.get("value", 0.0))

if condition == "score_below":
actual = float(summary.get("score", 0.0))
violated = actual < threshold
actual = _metric(summary, "score", "score_below")
violated = _below(actual, threshold)
violations.append(BandViolation(condition, threshold, actual, violated))
elif condition == "coverage_below":
actual = float(summary.get("coverage", 0.0))
violated = actual < threshold
actual = _metric(summary, "coverage", "coverage_below")
violated = _below(actual, threshold)
violations.append(BandViolation(condition, threshold, actual, violated))
elif condition == "freshness_below":
actual = float(summary.get("freshness_lite", 0.0))
violated = actual < threshold
actual = _metric(summary, "freshness_lite", "freshness_below")
violated = _below(actual, threshold)
violations.append(BandViolation(condition, threshold, actual, violated))
elif condition == "readability_below":
actual = float(summary.get("readability", 0.0))
violated = actual < threshold
actual = _metric(summary, "readability", "readability_below")
violated = _below(actual, threshold)
violations.append(BandViolation(condition, threshold, actual, violated))

return CriticalBandsResult(enabled=True, violations=violations)
33 changes: 33 additions & 0 deletions dokimi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,36 @@ filterwarnings = [
# Test order is shuffled and `random` reseeded per test (pytest-randomly), so a
# test that leans on another's leftovers fails. The header prints the seed;
# `--randomly-seed=<seed>` replays that order, `-p no:randomly` keeps file order.

# Mutation pilot (mutmut 3.x, run by mutation.yml): only_mutate is the statistics, gate decisions and cache key.
# novelty_gate.py is left out (one 440-line function, copied per mutant). source_paths is the whole package.
[tool.mutmut]
source_paths = ["dokimi"]
only_mutate = [
"dokimi/eval/stats.py",
"dokimi/eval/binary_stats.py",
"dokimi/gate/critical_bands.py",
"dokimi/gate/regression.py",
"dokimi/gate/score_gate.py",
"dokimi/gate/trust_gate.py",
"dokimi/caching.py",
]
pytest_add_cli_args = ["-p", "no:randomly", "-p", "no:cacheprovider"]
pytest_add_cli_args_test_selection = [
"tests/test_stats.py",
"tests/test_stats_properties.py",
"tests/test_binary_stats.py",
"tests/test_novelty_stats_point_biserial.py",
"tests/test_pr26_properties.py",
"tests/test_skill_lift_summary.py",
"tests/test_model_compare.py",
"tests/test_repo_compare.py",
"tests/test_critical_bands.py",
"tests/test_regression.py",
"tests/test_score_gate.py",
"tests/test_gate_call_sites.py",
"tests/test_trust_gate.py",
"tests/test_caching.py",
"tests/test_judge_response_cache.py",
"tests/test_arm_runner.py",
]
Loading
Loading