gdmutant 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. gdmutant-0.1.0/.gitignore +53 -0
  2. gdmutant-0.1.0/AGENTS.md +147 -0
  3. gdmutant-0.1.0/CHANGELOG.md +251 -0
  4. gdmutant-0.1.0/CODE_OF_CONDUCT.md +166 -0
  5. gdmutant-0.1.0/CONTRIBUTING.md +99 -0
  6. gdmutant-0.1.0/LICENSE +21 -0
  7. gdmutant-0.1.0/PKG-INFO +184 -0
  8. gdmutant-0.1.0/README.md +158 -0
  9. gdmutant-0.1.0/corpus/gut_test/test_independent_gut.gd +18 -0
  10. gdmutant-0.1.0/corpus/gut_test/test_turn_order_gut.gd +25 -0
  11. gdmutant-0.1.0/corpus/harness/run_tests.gd +73 -0
  12. gdmutant-0.1.0/corpus/project.godot +12 -0
  13. gdmutant-0.1.0/corpus/test/test_independent.gd +20 -0
  14. gdmutant-0.1.0/corpus/test/test_turn_order.gd +23 -0
  15. gdmutant-0.1.0/corpus/turn_order.gd +32 -0
  16. gdmutant-0.1.0/docs/credits.md +64 -0
  17. gdmutant-0.1.0/docs/decisions/0001-write-the-engine-in-python-not-gdscript.md +68 -0
  18. gdmutant-0.1.0/docs/decisions/0002-mutation-mechanism-source-span-editing.md +43 -0
  19. gdmutant-0.1.0/docs/decisions/0003-mutation-application-strategy.md +95 -0
  20. gdmutant-0.1.0/docs/decisions/0004-equivalent-mutant-ignore-annotation.md +63 -0
  21. gdmutant-0.1.0/docs/decisions/0005-exit-code-test-runner-convention.md +86 -0
  22. gdmutant-0.1.0/docs/decisions/0006-operator-scoped-ignore-and-ignored-status.md +51 -0
  23. gdmutant-0.1.0/docs/decisions/0007-statement-deletion-with-a-return-path-guard.md +70 -0
  24. gdmutant-0.1.0/docs/decisions/0008-method-body-mutation-manual-spotcheck.md +108 -0
  25. gdmutant-0.1.0/docs/decisions/0009-pre-commit-for-local-dev-checks.md +101 -0
  26. gdmutant-0.1.0/docs/decisions/0010-pypi-trusted-publishing.md +109 -0
  27. gdmutant-0.1.0/docs/decisions/0011-runner-agnostic-adapter-seam.md +216 -0
  28. gdmutant-0.1.0/docs/decisions/0012-merge-time-local-ship-time-cloud.md +133 -0
  29. gdmutant-0.1.0/docs/decisions/0013-windows-local-mutation-testing.md +135 -0
  30. gdmutant-0.1.0/docs/design/DESIGN.md +307 -0
  31. gdmutant-0.1.0/docs/gdmutant-guide.md +482 -0
  32. gdmutant-0.1.0/docs/mutation-testing.md +152 -0
  33. gdmutant-0.1.0/docs/releasing.md +264 -0
  34. gdmutant-0.1.0/docs/survivors/README.md +187 -0
  35. gdmutant-0.1.0/gdmutant/__init__.py +15 -0
  36. gdmutant-0.1.0/gdmutant/adapters/__init__.py +3 -0
  37. gdmutant-0.1.0/gdmutant/adapters/gdscript/__init__.py +505 -0
  38. gdmutant-0.1.0/gdmutant/adapters/gdscript/runner.py +546 -0
  39. gdmutant-0.1.0/gdmutant/cli.py +1786 -0
  40. gdmutant-0.1.0/gdmutant/engine/__init__.py +6 -0
  41. gdmutant-0.1.0/gdmutant/engine/adapter.py +30 -0
  42. gdmutant-0.1.0/gdmutant/engine/explain.py +466 -0
  43. gdmutant-0.1.0/gdmutant/engine/htmlreport.py +1465 -0
  44. gdmutant-0.1.0/gdmutant/engine/loop.py +931 -0
  45. gdmutant-0.1.0/gdmutant/engine/mutants.py +100 -0
  46. gdmutant-0.1.0/gdmutant/engine/operators/__init__.py +140 -0
  47. gdmutant-0.1.0/gdmutant/engine/report.py +318 -0
  48. gdmutant-0.1.0/gdmutant/engine/runner.py +217 -0
  49. gdmutant-0.1.0/gdmutant/engine/spans.py +88 -0
  50. gdmutant-0.1.0/gdmutant/engine/survivor_reference.py +292 -0
  51. gdmutant-0.1.0/gdmutant/examples/gdmutant-hello-world.gd +6 -0
  52. gdmutant-0.1.0/gdmutant/py.typed +0 -0
  53. gdmutant-0.1.0/pyproject.toml +231 -0
  54. gdmutant-0.1.0/tests/conftest.py +72 -0
  55. gdmutant-0.1.0/tests/js/harness.js +511 -0
  56. gdmutant-0.1.0/tests/test_action_pin.py +86 -0
  57. gdmutant-0.1.0/tests/test_check_licenses.py +162 -0
  58. gdmutant-0.1.0/tests/test_check_mutation_baseline_cap.py +105 -0
  59. gdmutant-0.1.0/tests/test_check_published_package.py +447 -0
  60. gdmutant-0.1.0/tests/test_check_readme_images.py +402 -0
  61. gdmutant-0.1.0/tests/test_check_release_tag.py +323 -0
  62. gdmutant-0.1.0/tests/test_cli.py +3732 -0
  63. gdmutant-0.1.0/tests/test_docs_frontmatter.py +159 -0
  64. gdmutant-0.1.0/tests/test_dogfood_gdunit4.py +106 -0
  65. gdmutant-0.1.0/tests/test_end_to_end.py +52 -0
  66. gdmutant-0.1.0/tests/test_explain.py +430 -0
  67. gdmutant-0.1.0/tests/test_flags_are_documented.py +56 -0
  68. gdmutant-0.1.0/tests/test_gdmutant_guide.py +98 -0
  69. gdmutant-0.1.0/tests/test_gdscript_adapter.py +669 -0
  70. gdmutant-0.1.0/tests/test_gdtoolkit_integration.py +57 -0
  71. gdmutant-0.1.0/tests/test_gdunit_runner.py +477 -0
  72. gdmutant-0.1.0/tests/test_gut_runner.py +471 -0
  73. gdmutant-0.1.0/tests/test_harden_github.py +696 -0
  74. gdmutant-0.1.0/tests/test_htmlreport.py +672 -0
  75. gdmutant-0.1.0/tests/test_htmlreport_behaviour.py +546 -0
  76. gdmutant-0.1.0/tests/test_local_check_parity.py +220 -0
  77. gdmutant-0.1.0/tests/test_loop.py +1489 -0
  78. gdmutant-0.1.0/tests/test_mutants.py +103 -0
  79. gdmutant-0.1.0/tests/test_mutation_baseline_inputs.py +251 -0
  80. gdmutant-0.1.0/tests/test_no_em_dashes_in_source.py +80 -0
  81. gdmutant-0.1.0/tests/test_operators.py +148 -0
  82. gdmutant-0.1.0/tests/test_packaging.py +139 -0
  83. gdmutant-0.1.0/tests/test_poodle_config.py +105 -0
  84. gdmutant-0.1.0/tests/test_public_readiness.py +1923 -0
  85. gdmutant-0.1.0/tests/test_report.py +484 -0
  86. gdmutant-0.1.0/tests/test_run_gitleaks.py +76 -0
  87. gdmutant-0.1.0/tests/test_runner.py +215 -0
  88. gdmutant-0.1.0/tests/test_selftest_live.py +459 -0
  89. gdmutant-0.1.0/tests/test_smoke.py +48 -0
  90. gdmutant-0.1.0/tests/test_spans.py +104 -0
  91. gdmutant-0.1.0/tests/test_step_summary.py +264 -0
  92. gdmutant-0.1.0/tests/test_survivor_reference.py +128 -0
  93. gdmutant-0.1.0/uv.lock +1480 -0
@@ -0,0 +1,53 @@
1
+ # Secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.pem
6
+ *.key
7
+ # Python (engine + gdtoolkit adapter are Python-natural)
8
+ __pycache__/
9
+ *.pyc
10
+ .venv/
11
+ .venv-*/
12
+ dist/
13
+ *.egg-info/
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ htmlcov/
19
+ .mutmut-cache
20
+ # mutmut v3 writes a generated copy of the project + its result DB here while it runs
21
+ mutants/
22
+ mutmut-results.db
23
+ mutmut-run.log
24
+ # poodle (local mutation testing, see docs/decisions/0013) writes its temp copies here; the
25
+ # trailing * also catches ad-hoc manual-testing dirs like .poodle-temp-manual/
26
+ poodle-run.log
27
+ .poodle-temp*/
28
+ # Godot (test fixtures may include a small Godot project)
29
+ .godot/
30
+ # Godot self-test fixture (tests/test_selftest_live.py): the GdUnit4 addon is downloaded
31
+ # (scripts/install_gdunit4.py), and reports + .uid import artifacts are regenerated by
32
+ # `godot --import` — none of them are committed.
33
+ corpus/addons/
34
+ corpus/reports/
35
+ *.uid
36
+ # Live self-test with --basetemp=.live-tmp (CI reuses the Stryker JSON it writes there)
37
+ .live-tmp/
38
+ # OS/editor
39
+ .DS_Store
40
+ Thumbs.db
41
+ .idea/
42
+ *.swp
43
+
44
+ # Agent scratch output — never commit ad-hoc dumps or agent-local config
45
+ *_dump.txt
46
+ .claude/
47
+
48
+ # The private word list tests/test_public_readiness.py scans for (GDMUTANT_PRIVATE_TERMS).
49
+ # It belongs outside this tree entirely, and committing it would publish the exact list of words
50
+ # the guard exists to keep unpublished. This is the belt to that guard's braces: a copy dropped
51
+ # in here by accident cannot be staged at all. tests/test_public_readiness.py pins that this
52
+ # pattern is here and that git really does honour it.
53
+ *private-terms*
@@ -0,0 +1,147 @@
1
+ ---
2
+ type: how-to
3
+ status: active
4
+ created: 2026-07-11
5
+ ---
6
+
7
+ # gdmutant: contributor & AI-assistant guide
8
+
9
+ A language-agnostic mutation-testing tool for GDScript/Godot. It
10
+ mutates a project's source (flip `>`↔`>=`, `and`↔`or`, bump a number, …), reruns the tests per
11
+ mutant, and reports survivors: lines a bug could live on that no test catches. This is the
12
+ fast-orientation guide for anyone (human or AI) *contributing to* gdmutant's own source. The
13
+ product rationale is in [`README.md`](README.md), and the authoritative design is in
14
+ [`docs/design/DESIGN.md`](docs/design/DESIGN.md). Driving gdmutant as a tool (invoking the CLI,
15
+ not editing its code) is a different job: see
16
+ [`docs/gdmutant-guide.md`](docs/gdmutant-guide.md) instead.
17
+
18
+ ## Setup
19
+
20
+ gdmutant is a [uv](https://docs.astral.sh/uv/) project. Install uv, then:
21
+
22
+ ```sh
23
+ uv sync --frozen # fetches the pinned Python (via .python-version) + locked deps into .venv
24
+ ```
25
+
26
+ Run commands with `uv run`.
27
+
28
+ The live self-test suites below need a real Godot binary, which `uv sync` does not provide (`uv`
29
+ owns only Python here). `mise.toml` pins the same Godot release CI runs against. Install
30
+ [mise](https://mise.jdx.dev/), then `mise install` fetches it. `mise which godot` prints the path
31
+ to pass as `GDMUTANT_GODOT`. Never add Python to `mise.toml`. That stays `uv`'s alone.
32
+
33
+ ## Build · test
34
+
35
+ ```sh
36
+ uv run ruff check . # lint
37
+ uv run ruff format --check . # format
38
+ uv run mypy gdmutant # type check (strict)
39
+ uv run pytest # tests + coverage
40
+ uv run pip-audit # dependency audit
41
+ ```
42
+
43
+ Two suites are env-gated and auto-skip in a plain `uv run pytest` (so `verify` stays
44
+ Godot-free). Run them when touching the adapter, runners, or CLI file-handling:
45
+
46
+ ```sh
47
+ # Live self-test: drive the shipped CLI against a real Godot on the corpus.
48
+ GDMUTANT_GODOT=godot uv run pytest tests/test_selftest_live.py
49
+ # Dogfood harness: run gdmutant against a real GdUnit4 checkout — parse coverage + the
50
+ # whole-directory regression guard (Godot-free, ~5s). Point it at any GdUnit4 clone:
51
+ GDMUTANT_GDUNIT4_CLONE=<path-to-a-gdUnit4-checkout> uv run pytest tests/test_dogfood_gdunit4.py
52
+ ```
53
+
54
+ ## Tech stack (decided)
55
+
56
+ - Language: Python 3.12 (pinned via `.python-version`), managed with uv (hash-pinned
57
+ `uv.lock`, with uv itself floored in `pyproject.toml`'s `[tool.uv]`). Rationale for Python over GDScript:
58
+ [`docs/decisions/0001`](docs/decisions/0001-write-the-engine-in-python-not-gdscript.md).
59
+ - Runtime dependency: [`gdtoolkit`](https://github.com/Scony/godot-gdscript-toolkit), the
60
+ GDScript parser the adapter mutates.
61
+ - Test-runner adapters: GdUnit4 and GUT are peer JUnit-XML adapters over one runner contract
62
+ (both run via `godot --headless`, and neither is privileged in the engine), plus the framework-neutral
63
+ exit-code command runner for any harness without JUnit output. See
64
+ [`docs/decisions/0011`](docs/decisions/0011-runner-agnostic-adapter-seam.md).
65
+ - Report format: the `mutation-testing-elements` JSON schema (renders in its HTML viewer).
66
+
67
+ ## Conventions
68
+
69
+ - `main` is protected, and new behavior comes with tests: this is a testing tool, so we hold
70
+ ourselves to it.
71
+ - CI gate: `ruff` + `mypy` + `pytest` + `pip-audit`, plus a gitleaks secret scan and zizmor
72
+ (workflow security). GitHub Actions are SHA-pinned (Dependabot bumps them).
73
+ - Windows is a deployment target, not just a dev machine. gdmutant is a cross-platform Python
74
+ CLI people run on Windows, so test on Windows for real, not just Linux. Two concrete traps to
75
+ watch for: console output can crash under the legacy `cp1252` code page, and `python3` can resolve
76
+ to a *different* interpreter than `python` (see `.pre-commit-config.yaml`'s header for the guard).
77
+ - `ci.yml` runs automatically on every pull request and push to `main` (restored 2026-08-04, ahead
78
+ of going public — why it was ever manual-only, and why it's back:
79
+ [ADR-0012](docs/decisions/0012-merge-time-local-ship-time-cloud.md)). The unbypassable gate is
80
+ still `publish.yml`'s release-time run of `verify` on both Linux and Windows, before every real
81
+ release. Run the same checks locally any time:
82
+
83
+ ```
84
+ uv run python scripts/verify_local.py
85
+ uv run python scripts/verify_local.py --job license-check # or any other ci.yml job
86
+ uv run python scripts/verify_local.py --list # show a job's commands without running them
87
+ ```
88
+
89
+ This script reads its commands out of `ci.yml` rather than restating them, so local, CI, and the
90
+ release gate can't drift apart.
91
+
92
+ If every tool fails with `uv trampoline failed to canonicalize script path`, the venv's
93
+ launcher shims are stale, usually after a `uv` version bump. Fix: `uv sync --frozen --reinstall`.
94
+ - Keep the engine language-neutral: no GDScript-specific assumptions in `gdmutant/engine/`.
95
+ Language specifics live only in `gdmutant/adapters/<lang>/`.
96
+ - The mutation-operator core is deterministic, the reproducible mode a CI check can trust. Any
97
+ future LLM-semantic mode stays out of it.
98
+ - **Recurring bug one: a gate that passes without checking anything.** Seen five times. A test that
99
+ skipped when `git` was missing. A mutation hook that ran zero mutants. A license check over an
100
+ empty package list. A script that exited 0 after all its writes failed. A mutation run scored
101
+ against an already-broken baseline, where every mutant "died" so the 100% meant nothing.
102
+ This is a list of shapes, not a list of things already fixed. Assume at least one is live in the
103
+ tree right now, because that has been true every time anyone checked.
104
+ Ask of any gate: what happens when its input is missing, empty, or unreadable? Silence is the
105
+ wrong answer. Make it fail, or make it say out loud that it did not run.
106
+ - **Recurring bug two: two paths that should agree, and one checks less.** A second entry point
107
+ running fewer checks than the first. A message fixed in one place but not its twin. Two things
108
+ writing to stdout at once. Coverage and mutation testing cannot catch these. Both ask "is this
109
+ path correct?", and the bug is "do these two paths match?". So when you change one of a pair, say
110
+ what every member of the pair does now, including the ones already right.
111
+ - Sensitive paths (CI, scripts, toolchain, the mutation-operator catalog, and the GDScript
112
+ adapter) are listed in `CODEOWNERS` for documentation only. It enforces no review (a sole
113
+ maintainer can't approve their own PR). `main` requires a pull request and, alongside
114
+ `Workflow security (zizmor)` (which reads the workflow files and nothing else), the checks
115
+ `ci.yml`'s now-restored triggers make possible: `Verify` on both platforms, `Secret scan
116
+ (gitleaks)`, and both `Self-test` gates — see `scripts/harden_github.py`'s `REQUIRED_JOBS` for
117
+ the exact, current list. `.github/CODEOWNERS` carries the full list of what branch protection
118
+ enforces. Read changes to these paths carefully before merging. The GDScript adapter is the real
119
+ technical risk, since a wrong mutant means a silently wrong survivor report.
120
+
121
+ ## Design goals (keep these in mind)
122
+
123
+ - Ship fast: a working v0.1 that mutates one real module and prints survivors beats a framework.
124
+ - Standalone, no AI required: a normal CLI, usable from the README alone.
125
+ - Generic engine, per-language adapters: build the loop once. A new language = one small adapter.
126
+
127
+ ## Docs: where things live
128
+
129
+ - [`README.md`](README.md): what it is and why.
130
+ - [`docs/design/DESIGN.md`](docs/design/DESIGN.md): authoritative design (goals, FG/NF requirements, architecture).
131
+ - [`CHANGELOG.md`](CHANGELOG.md): what's landed and in progress (scope / non-goals live in `DESIGN.md`).
132
+ - `docs/mutation-testing.md`: the suite is mutation-tested against itself.
133
+ - `docs/decisions/NNNN-*.md`: append-only ADRs (`ls` is the index).
134
+ - [`docs/releasing.md`](docs/releasing.md): the maintainer runbook for cutting a release to PyPI.
135
+ - [`docs/credits.md`](docs/credits.md): third-party licenses.
136
+
137
+ Live docs open with YAML frontmatter (`type` / `status` / `created`). Build only from
138
+ `status: active`. Two files are deliberately exempt, because they are rendered somewhere that has
139
+ no frontmatter support and would print it as a visible heading: `README.md` (it is the package's
140
+ PyPI description) and `.github/PULL_REQUEST_TEMPLATE.md` (it is pasted verbatim into every new pull
141
+ request's body). `tests/test_docs_frontmatter.py` pins both halves of that rule.
142
+
143
+ ## Non-goals (v0.1)
144
+
145
+ Coverage-gated mutant selection, the optional LLM-semantic mutant mode, and any second-language
146
+ adapter (TypeScript is out of scope for v0.1).
147
+ Finishing the GDScript path on a real fixture beats breadth.
@@ -0,0 +1,251 @@
1
+ ---
2
+ type: record
3
+ status: active
4
+ created: 2026-07-18
5
+ ---
6
+
7
+ # Changelog
8
+
9
+ All notable changes to gdmutant are recorded here. The format follows
10
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
11
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
+
13
+ ## [0.1.0] - 2026-08-04
14
+
15
+ gdmutant mutates real GDScript and reports survivors end-to-end via the standalone `gdmutant run`
16
+ CLI.
17
+
18
+ ### Added
19
+
20
+ - AST-based mutation of GDScript via [gdtoolkit](https://github.com/Scony/godot-gdscript-toolkit),
21
+ with a re-parse validity guard so invalid mutants are never run.
22
+ - Operators: comparison, boolean, arithmetic, constant, numeric-literal, compound assignment,
23
+ modulo, unary-not, and statement-deletion.
24
+ - Generation-time exclusions for token positions the language itself rules out as meaningful
25
+ mutants, so they never reach the report as survivors: a `%` used for string formatting, a `+` that
26
+ is string concatenation (GDScript's `String` defines no `-`), a `+=` that appends to a string (same
27
+ reason), and a property declaration's initializer whose stored value no getter can read back.
28
+ These change the mutation score, and upward: an excluded mutant leaves the denominator instead
29
+ of counting as a survivor. That is deliberate and is the honest direction: the language already
30
+ settles all four, so none is a gap a test could ever have closed. The first three are invalid
31
+ GDScript, and the property initializer is valid GDScript whose mutant is inert, because the value
32
+ it stores can never be read back. It does mean, though, that a score is not comparable across a
33
+ version that added an exclusion. Each is recognised from the parse tree only
34
+ where the shape is certain (a `String`-typed *variable* is still mutated), because reporting noise
35
+ is a smaller failure than hiding a real gap. `docs/survivors/README.md` states the full list and
36
+ its effect on the score for users.
37
+ - Framework-agnostic test runners over one shared runner contract (a runner-agnostic adapter seam):
38
+ GdUnit4 and GUT as first-class peer JUnit-XML adapters (`--runner gdunit4` / `--runner gut`,
39
+ neither privileged in the engine), plus an exit-code runner (`--runner command`) for any headless
40
+ harness without JUnit output, no addon required. Every runner upholds a crash-safety guarantee: a
41
+ load/compile crash surfaces as a kill or error, never a silent zero-test pass. A whole-run
42
+ `tests == 0` check catches the empty-report shape, but GUT needs more: it skips a suite that fails
43
+ to compile, runs the rest green, and exits 0, so the adapter also treats any run whose test count
44
+ drops below the healthy baseline's as an error, with a non-determinism canary that warns (never
45
+ errors) when a later run reports more tests than the baseline.
46
+ - Multi-file and directory targets: mutate several files or a whole directory in one pass with a
47
+ per-file breakdown and one aggregate mutation score.
48
+ - `--jobs N` runs N mutants in parallel, each on its own copy of the project so in-place mutation
49
+ can't collide: same verdicts as a serial run (process isolation, the per-mutant timeout is scaled
50
+ by N so contention can't cause a false timeout), just faster (measured ~3× at `--jobs 4` on a real
51
+ GdUnit4 module).
52
+ - Test suites are skipped by default on directory targets (by `test/`/`tests/` folder, `test_*.gd` /
53
+ `*_test.gd` / `*Test.gd` name, or `extends GdUnitTestSuite` / `GutTest`), with an `--exclude` glob
54
+ (and a `.gdmutant.toml` `exclude` list) to skip anything else.
55
+ - Reports: a console survivor summary that explains each gap (what's untested, why it matters, where
56
+ to start a test), the
57
+ [`mutation-testing-elements`](https://github.com/stryker-mutator/mutation-testing-elements) JSON
58
+ schema (`--json`), and a genuinely self-contained HTML report (`--html`): one file with every
59
+ style, script and image inlined, so it opens with no network at all and works as a CI artifact or
60
+ an email attachment. It marks the exact changed characters in your source, groups mutants into
61
+ findings (one spot, one operator, the unit of work a single test closes), and inlines the
62
+ per-operator survivor reference so an offline reader can still look up what an operator means. A
63
+ multi-file run opens on a file index ordered by survivors. Replaces an earlier page that inlined
64
+ the report JSON but loaded the generic viewer from a CDN, and so rendered blank offline.
65
+ - Every finding in the HTML report has an address: `path:line:column:operator`, the tuple it
66
+ was grouped by, so it is the same string every time the report is regenerated from source that has
67
+ not moved. The selected finding lives in the URL, so a reload keeps your place and "look at this
68
+ survivor" is a link you can send. A link that no longer resolves falls back to the file it named,
69
+ or to the file index, and never to the wrong finding.
70
+ - The HTML report shows each file by its project-relative path. A report is made to travel, and an
71
+ absolute path from the machine that produced it carries that machine's username and directory
72
+ layout into every row while telling a reader nothing they can act on. A file genuinely outside the
73
+ project keeps its absolute path, because there is no shorter honest name for it. This changes what
74
+ the page *displays*, and the displayed path is what a deep link is keyed on. Note the limit: the
75
+ `--json` report and the copy of it embedded in the HTML file are both unchanged, because their
76
+ keys are identifiers other tooling resolves. A report file still contains the absolute paths, it
77
+ just no longer shows them.
78
+ - The embedded report is downloadable from the page, so the JSON in a report someone mailed you is
79
+ reachable without View Source. No request and no new data, the bytes are the page's own. The
80
+ toolbar button now says `JSON`, not just a bare down-arrow, and the download itself is indented
81
+ (`json.dumps(..., indent=2)`), matching what `--json <path>` writes to disk instead of one
82
+ unreadable line.
83
+ - The file index sorts on any column (score, file, survived, caught, mutants), ascending or
84
+ descending. Most survivors first stays the default, because it is the only order that answers
85
+ "where do I start". Score would rank 1 survivor in 5 mutants level with 100 in 500.
86
+ - The header's rare-status counts are clickable, and reach the mutants behind them through the
87
+ filter the file view already has. The three stay three things: a timeout is a kill and only a
88
+ performance signal, a compile error is the re-parse guard working, and a runtime error is the
89
+ actionable one, a valid mutant that ran and measured nothing because the harness fell over.
90
+ - The browser's back button returns to the file index instead of leaving the report. Structural
91
+ moves (opening a file, going back to the index) get a history entry. Stepping between findings
92
+ still does not, so a long file cannot bury the reader under a back press per finding.
93
+ - `.gdmutant.toml` for persisted per-project flags, and `--dry-run` to list mutants without running
94
+ Godot.
95
+ - Live self-test against real Godot in CI, pinning both runner paths to exact per-mutant outcomes.
96
+ - A GitHub Action, so a consumer's CI runs gdmutant from a few lines of `uses:` YAML instead of a
97
+ hand-rolled install step: it sets up Python and Godot, installs gdmutant, runs it against the
98
+ consumer's project, and writes the surviving mutants (with their explanations) to the job
99
+ summary, right where a reviewer already looks.
100
+ - `gdmutant example` writes a small bundled GDScript file (`gdmutant-hello-world.gd`) to try
101
+ `--dry-run` on, so the Quickstart no longer asks a first-time reader to hand-copy a snippet into
102
+ a file themselves. Refuses to overwrite an existing file of the same name.
103
+
104
+ ### Changed
105
+
106
+ - `--progress none` now silences the whole progress stream, not just the heartbeat: the plan
107
+ line, the heartbeat, the per-mutant lines, the closing wall-clock line, and the "preparing the
108
+ project" / "running the unmutated (baseline) suite" notices are all gone. `auto` and `plain`
109
+ still print all of it. The console summary, survivor blocks, and the reports themselves are
110
+ unaffected.
111
+ - `--since <ref>` with no lines changed now writes a valid, empty report instead of leaving
112
+ stdout empty. The exit code and the stderr explanation are unchanged: still 0, still the same
113
+ "nothing to mutate" note, still no tests run and no Godot booted. New: the `--json` or `--html`
114
+ report is written (every given file listed, an empty `mutants` list, no score key), the job
115
+ summary is emitted if `--report step-summary` was asked for, `--project` and the report targets
116
+ are validated the same as on a real run, and a malformed `# gdmutant: ignore[...]` pragma warns.
117
+ - The HTML report now opens in dark mode by default. The toggle to switch to light is unchanged.
118
+ - The count badge on a mark with multiple findings sits further outside the mark's border, so it
119
+ no longer covers part of the character underneath.
120
+
121
+ ### Fixed
122
+
123
+ - Six survivor explanations stated something that is not true. The text reaches every user on every
124
+ run, through the console block, the JSON report and the HTML report, and the docs page and the
125
+ shipped copy of it were pinned to each other, so all three said the same wrong thing in sync.
126
+ - Modulo blamed a clean multiple ("where `%`, `*`, and `/` can produce indistinguishable
127
+ results"). A clean multiple is where those three differ most: `6 % 3` is 0, `6 * 3` is 18,
128
+ `6 / 3` is 2. A reader was told the test that works cannot work. The reason is arithmetic's,
129
+ which is what the entry's own "assert the exact result" advice already implied: nothing pins
130
+ the result.
131
+ - Comparison said the swapped operators "differ on exactly one input", which is false for the
132
+ `==` ↔ `!=` pair the operator also produces, and which the entry names in its own example.
133
+ Those two are complements and differ on every input.
134
+ - The list of shapes that are never generated called all four "code GDScript rejects". Three
135
+ are, resting on `String` having no `-`. The fourth, a property initializer no getter can read
136
+ back, is valid GDScript that is merely inert, so an auditor was sent hunting a syntax error
137
+ that is not there.
138
+ - Numeric said "a numeric literal", but only bare decimal integers are mutated: `0.5`, `0xFF`
139
+ and `1_000` produce nothing, so a float bound was never covered.
140
+ - Enum member described only a `numeric` mutant on a member's value, though any mutant inside
141
+ an `enum` block is explained there, including an arithmetic one on `A = 1 + 0`.
142
+ - Compound assign said a string `+=` is "not mutated at all". It gets no compound-assign
143
+ mutant, but the line still gets a statement deletion.
144
+ - `# gdmutant: ignore[statement-deletion]` no longer draws a warning that it "suppresses nothing".
145
+ The annotation always worked. The validator checked names against the token operator catalog
146
+ alone, and statement deletion is structural, so it lives in the GDScript adapter instead. The
147
+ tool was contradicting its own documented advice to scope an ignore with the `mutatorName` from
148
+ the report.
149
+ - The "executable not found" message is mode-aware. Under `--runner command` the executable
150
+ comes from the `--command` string, not from `--godot`, so the message now says that, states that
151
+ `--godot` has no effect in that mode, and shows the user's own command back with the path slot
152
+ marked. It previously recommended `--godot`, which that mode does not read: setting it returned
153
+ the byte-identical error.
154
+ - `--runner command` says up front when the project has no Godot import cache (`.godot/`). On a
155
+ fresh checkout Godot imports every asset before it will run anything, minutes of silence that
156
+ reads as a hang. The JUnit runners do that warm-up themselves (and the "preparing the project"
157
+ notice now says it can take minutes). The exit-code runner cannot, so it names the one command
158
+ that fixes it instead.
159
+ - Survivors that are unkillable by where they sit explain themselves, instead of handing out
160
+ advice nobody can follow. Two places qualify:
161
+ - inside an `assert`: a failed assert aborts the Godot process, so no in-process test can
162
+ pass on the original and fail on the mutant. On defensive code these can be most of a file's
163
+ survivors.
164
+ - on an `enum` member's value: code that names the member moves with it, so nothing observes
165
+ the number. The generic numeric advice ("add a test at the boundary this number sets") is
166
+ meaningless for a tag that has no boundary.
167
+
168
+ Nothing is skipped and no score changes. Every one of these mutants is still generated, still
169
+ run, and still counted. Enum values are deliberately *not* suppressed: bitflag enums and any enum
170
+ that is serialised have values that really are read as numbers, and gdmutant reads one file at a
171
+ time, so it cannot see a numeric use in another file, a save format, or engine code. Suppressing
172
+ them would hide exactly the bugs that matter most. The reference explains what would make one
173
+ killable and leaves the call to you.
174
+
175
+ Every surface agrees: the console block, the JSON report, the HTML page and the job summary all
176
+ resolve their explanation and their link through one rule, so the page can never offer an
177
+ operator's reference beside a contradicting explanation. The `statement-deletion` reference also
178
+ now names the redundant initializer (`_cells = PackedByteArray()` where the declaration
179
+ already default-initialises it) as its commonest legitimate equivalent, with the check that
180
+ confirms one.
181
+ - Progress is measured, not predicted. The up-front `estimated ≈ 24s` figure is gone. It was
182
+ wrong in both directions at once: 1.7–3.4× *under* on a real project (it counted neither
183
+ gdmutant's own per-mutant work nor timeouts, which were four minutes of one 6m24s run) and, since
184
+ it never took `--jobs` into account, roughly N× *over* under `--jobs N`. In its place, three
185
+ lines that `--progress {auto,plain,none}` controls together (`none` silences all three, `auto`
186
+ and `plain` print all three):
187
+ - before, the facts: `18 mutants to run. Baseline suite 1.4s; each mutant is capped at 30s.`
188
+ The cap is the part that paces the wait, and it says how long silence is normal.
189
+ - during, a heartbeat: `… 7/18 done in 1m 12s — 2 survived, 1 timed out.` Every 30s on a
190
+ terminal, every 60s or 10% of mutants (whichever is rarer) in a log or under `CI=true`, and
191
+ always once at the end of each file.
192
+ - after, the wall-clock every other test runner prints and gdmutant did not:
193
+ `Done in 6m 32s — 18 mutants, 8 timed out (4m 0s of that). Baseline suite 1.4s.` The timeout
194
+ cost is broken out because it is the cost nobody can see.
195
+
196
+ No finish time is forecast anywhere. One was built and measured against a real Godot project
197
+ first: on an even workload it tracked the true finish to within 5%, but on the shape that
198
+ actually matters (hanging mutants arriving after the rate has settled), it read 3.2s at 25%
199
+ done for a run that took 58.0s, so it was dropped.
200
+ - `--json -` combined with `--html` no longer writes the `Wrote HTML report to ...`
201
+ confirmation into the JSON stream. It goes to stderr instead, so stdout stays valid JSON.
202
+ - `--json -` combined with `--report step-summary` and no `$GITHUB_STEP_SUMMARY` set is now
203
+ refused up front with exit 2, naming both flags and how to fix it, instead of printing the
204
+ report to stdout and poisoning the JSON.
205
+ - `--report-path` (or a cloned project's own `.gdmutant.toml`) could resolve outside `--project`
206
+ via an absolute override or `../` traversal. Every run deletes whatever sits at that path first,
207
+ so this was a delete-anything-on-disk primitive reachable with no flags at all. Now refused
208
+ unless the resolved path stays inside the project.
209
+ - `.gdmutant.toml`'s `project` key now requires `--trust-config`, alongside the existing
210
+ `command`/`godot` gate: it roots every subprocess's cwd and, under `--jobs`, what gets copied
211
+ once per worker, the same "this file decides what happens on your machine" shape those two keys
212
+ already guarded against.
213
+ - The HTML report's script-embedding escape only rewrote `</`, missing `<!--<script` (needs no
214
+ slash) which could push a browser's HTML parser into a state that swallows the report's own
215
+ closing tag, blanking the page past that point. Every `<` is escaped now.
216
+
217
+ ### Safety
218
+
219
+ - Mutations are applied in place and restored after each mutant and on exit. gdmutant warns on
220
+ uncommitted changes, and `--require-clean` makes that a hard stop.
221
+ - `--require-clean` refuses anything it could not confirm, not just changes it could see. A file
222
+ git ignores, a file outside any repository, a machine with no git, and a symlink whose target
223
+ is unbacked all used to pass the check silently (the ignored file and the symlink being the
224
+ worst of them, since git holds no copy of either). Without the flag, a file gdmutant cannot
225
+ judge still says nothing. A gitignored one now warns, which it did not before, because that is
226
+ the case gdmutant can positively tell has no copy anywhere.
227
+ - A git command that fails for a reason other than "no repository here" (dubious ownership, a
228
+ corrupted repository) now reports what git actually said, including the fix it suggests,
229
+ instead of a generic "not inside a git working tree". When the source is a symlink, the message
230
+ names the file git was actually asked about, so a report about a target outside every repository
231
+ does not read as a report about the link.
232
+ - A source file is never left half-written. Each rewrite is staged in a temporary file beside the
233
+ target and renamed over it, so the path always holds one whole version or the other, never
234
+ something truncated or cut off mid-token. If that cannot be done (no room for the temporary
235
+ file, a failed flush, a lock that will not clear, or a file marked read-only), gdmutant stops
236
+ and says so instead of attempting a write that could truncate the file. That promise is about
237
+ the write, not the run: the same write happens twice per mutant, once to apply it and once to
238
+ restore the original, and a failure on the restore leaves the mutant on disk, not your
239
+ original. Every message names which of the two is actually there and points at git to recover
240
+ it.
241
+ - Under `--jobs N`, a worker only ever writes inside its own copy of the project. A source file
242
+ that is not under `--project` has no copy to mutate, so the run is refused with an explanation
243
+ instead of writing outside the copy, which used to report every mutant as a survivor because
244
+ the mutation never reached the project the tests ran against.
245
+ - `.gdmutant.toml` cannot decide what gdmutant executes. Its `command` and `godot` keys name a
246
+ program to run, and the file is read from the project directory, so in a project you cloned,
247
+ somebody else wrote it. Either key makes gdmutant refuse the whole run, with an explanation, and
248
+ exit 2 without running anything, unless you add `--trust-config` or name the program on the
249
+ command line yourself, which always wins over the file. The refusal fires only where the file
250
+ would change what happens, so a key repeating a value you already passed, or the one gdmutant
251
+ would have used anyway, goes through in silence.
@@ -0,0 +1,166 @@
1
+ ---
2
+ type: how-to
3
+ status: active
4
+ created: 2026-07-31
5
+ ---
6
+
7
+ # Contributor Covenant Code of Conduct
8
+
9
+ ## Our Pledge
10
+
11
+ We pledge to make our community welcoming, safe, and equitable for all.
12
+
13
+ We are committed to fostering an environment that respects and promotes the dignity, rights, and
14
+ contributions of all individuals, regardless of characteristics including race, ethnicity, caste,
15
+ color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or
16
+ expression, sexual orientation, language, philosophy or religion, national or social origin,
17
+ socio-economic position, level of education, or other status. The same privileges of participation
18
+ are extended to everyone who participates in good faith and in accordance with this Covenant.
19
+
20
+ ## Encouraged Behaviors
21
+
22
+ While acknowledging differences in social norms, we all strive to meet our community's expectations
23
+ for positive behavior. We also understand that our words and actions may be interpreted differently
24
+ than we intend based on culture, background, or native language.
25
+
26
+ With these considerations in mind, we agree to behave mindfully toward each other and act in ways
27
+ that center our shared values, including:
28
+
29
+ 1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
30
+ 2. Engaging **kindly and honestly** with others.
31
+ 3. Respecting **different viewpoints** and experiences.
32
+ 4. **Taking responsibility** for our actions and contributions.
33
+ 5. Gracefully giving and accepting **constructive feedback**.
34
+ 6. Committing to **repairing harm** when it occurs.
35
+ 7. Behaving in other ways that promote and sustain the **well-being of our community**.
36
+
37
+ ## Restricted Behaviors
38
+
39
+ We agree to restrict the following behaviors in our community. Instances, threats, and promotion of
40
+ these behaviors are violations of this Code of Conduct.
41
+
42
+ 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal
43
+ attention after any clear request to stop.
44
+ 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a
45
+ community member or group of people.
46
+ 3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis
47
+ of immutable identities or traits.
48
+ 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate
49
+ in the context or purpose of the community.
50
+ 5. **Violating confidentiality.** Sharing or acting on someone's personal or private information
51
+ without their permission.
52
+ 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person
53
+ or group.
54
+ 7. Behaving in other ways that **threaten the well-being** of our community.
55
+
56
+ ### Other Restrictions
57
+
58
+ 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone
59
+ else to evade enforcement actions.
60
+ 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
61
+ 3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside
62
+ the norms of the community.
63
+ 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or
64
+ describes any other restricted behaviors.
65
+
66
+ ## Reporting an Issue
67
+
68
+ Tensions can occur between community members even when they are trying their best to collaborate.
69
+ Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces
70
+ encouraged behaviors and norms that can help avoid conflicts and minimize harm.
71
+
72
+ When an incident does occur, it is important to report it promptly. To report a possible violation,
73
+ open a private report with
74
+ **[GitHub's "Report a vulnerability" form](https://github.com/kphutt/gdmutant/security/advisories/new)**
75
+ (repository → **Security** → **Advisories** → **Report a vulnerability**). That form is the same
76
+ private channel
77
+ [`SECURITY.md`](https://github.com/kphutt/gdmutant/blob/main/.github/SECURITY.md) names, and it is
78
+ the project's only private
79
+ inbox. It carries conduct reports as well as security ones, and a report sent there is visible only
80
+ to the maintainers. Please do not open a public issue for a conduct problem.
81
+
82
+ Where this Code of Conduct says **Community Moderators**, read *the maintainers of this project*.
83
+ gdmutant is maintained by one person, so a report about that person's own conduct has nowhere
84
+ neutral to go inside the project. Send those to
85
+ [GitHub's abuse reporting](https://github.com/contact/report-abuse) instead, which is handled by
86
+ GitHub rather than by anyone here.
87
+
88
+ Community Moderators take reports of violations seriously and will make every effort to respond in a
89
+ timely manner. They will investigate all reports of code of conduct violations, reviewing messages,
90
+ logs, and recordings, or interviewing witnesses and other participants. Community Moderators will
91
+ keep investigation and enforcement actions as transparent as possible while prioritizing safety and
92
+ confidentiality. In order to honor these values, enforcement actions are carried out in private with
93
+ the involved parties, but communicating to the whole community may be part of a mutually agreed upon
94
+ resolution.
95
+
96
+ ## Addressing and Repairing Harm
97
+
98
+ This project has no enforcement process of its own, so it uses the Contributor Covenant's suggested
99
+ ladder as written, below.
100
+
101
+ If an investigation by the Community Moderators finds that this Code of Conduct has been violated,
102
+ the following enforcement ladder may be used to determine how best to repair harm, based on the
103
+ incident's impact on the individuals involved and the community as a whole. Depending on the
104
+ severity of a violation, lower rungs on the ladder may be skipped.
105
+
106
+ 1. Warning
107
+ 1. Event: A violation involving a single incident or series of incidents.
108
+ 2. Consequence: A private, written warning from the Community Moderators.
109
+ 3. Repair: Examples of repair include a private written apology, acknowledgement of
110
+ responsibility, and seeking clarification on expectations.
111
+ 2. Temporarily Limited Activities
112
+ 1. Event: A repeated incidence of a violation that previously resulted in a warning, or the first
113
+ incidence of a more serious violation.
114
+ 2. Consequence: A private, written warning with a time-limited cooldown period designed to
115
+ underscore the seriousness of the situation and give the community members involved time to
116
+ process the incident. The cooldown period may be limited to particular communication channels
117
+ or interactions with particular community members.
118
+ 3. Repair: Examples of repair may include making an apology, using the cooldown period to reflect
119
+ on actions and impact, and being thoughtful about re-entering community spaces after the
120
+ period is over.
121
+ 3. Temporary Suspension
122
+ 1. Event: A pattern of repeated violation which the Community Moderators have tried to address
123
+ with warnings, or a single serious violation.
124
+ 2. Consequence: A private written warning with conditions for return from suspension. In general,
125
+ temporary suspensions give the person being suspended time to reflect upon their behavior and
126
+ possible corrective actions.
127
+ 3. Repair: Examples of repair include respecting the spirit of the suspension, meeting the
128
+ specified conditions for return, and being thoughtful about how to reintegrate with the
129
+ community when the suspension is lifted.
130
+ 4. Permanent Ban
131
+ 1. Event: A pattern of repeated code of conduct violations that other steps on the ladder have
132
+ failed to resolve, or a violation so serious that the Community Moderators determine there is
133
+ no way to keep the community safe with this person as a member.
134
+ 2. Consequence: Access to all community spaces, tools, and communication channels is removed. In
135
+ general, permanent bans should be rarely used, should have strong reasoning behind them, and
136
+ should only be resorted to if working through other remedies has failed to change the
137
+ behavior.
138
+ 3. Repair: There is no possible repair in cases of this severity.
139
+
140
+ This enforcement ladder is intended as a guideline. It does not limit the ability of Community
141
+ Managers to use their discretion and judgment, in keeping with the best interests of our community.
142
+
143
+ ## Scope
144
+
145
+ This Code of Conduct applies within all community spaces, and also applies when an individual is
146
+ officially representing the community in public or other spaces. Examples of representing our
147
+ community include using an official email address, posting via an official social media account, or
148
+ acting as an appointed representative at an online or offline event.
149
+
150
+ ## Attribution
151
+
152
+ This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at
153
+ [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
154
+
155
+ Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under
156
+ CC BY-SA 4.0. To view a copy of this license, visit
157
+ [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
158
+
159
+ For answers to common questions about Contributor Covenant, see the FAQ at
160
+ [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations
161
+ are provided at
162
+ [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
163
+ Additional enforcement and community guideline resources can be found at
164
+ [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The
165
+ enforcement ladder was inspired by the work of
166
+ [Mozilla's code of conduct team](https://github.com/mozilla/inclusion).