Skip to content

feat(A1): 变异执行器+定向初筛+属性/裁判+分数台账(IR-0004 AC-1/2 rev6,卡 .github#322/#323) - #104

Merged
randypanding merged 3 commits into
mainfrom
quality-instruments-a1
Aug 25, 2026
Merged

feat(A1): 变异执行器+定向初筛+属性/裁判+分数台账(IR-0004 AC-1/2 rev6,卡 .github#322/#323)#104
randypanding merged 3 commits into
mainfrom
quality-instruments-a1

Conversation

@randypanding

@randypanding randypanding commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

变更(pipeline/testing/{mutation,property})

  1. run_mutation.py(AC-1):mutmut 探测+自研 8 AST 算子降级路径(无第三方依赖也能出分);score 全机械;基线不过即中止 exit 2
  2. directed.py(AC-1):LLM 定向变异体候选机械初筛(幽灵/坏语法/空位点拒绝+预演杀死率)
  3. invariants.py + judge.py(AC-2 交叉锚定链):四属性×七生成器(hypothesis 缺失时种子化随机 N=200 等价执行);裁判把存活变异体逐属性执行、零杀死=平凡拒收、judge_log 逐条可追溯
  4. ledger.py:分数 JSONL sha256 链(篡改定位到行)
  5. mutation-weekly.yml:周日 06:17 + reusable(target_repo);55 自测全绿(含故意弱套件留存活者的杀伤率算术验证)
    55 用例全绿(约 16s 离线)+ CLI 端到端冒烟(20 变异体/60% 分/裁判 8 存活→2 accepted 3 trivial rejected)。

依据

IR-0004 rev6(.github#359);LLM 只出候选、判定全机械(rev6 边界)。

Cards: Cloudbird-Software/.github#322 Cloudbird-Software/.github#323

依据

ADR-0085(PM 优先范式——LLM 只出候选、判定全机械);IR-0004 rev6(.github#359)。

Copilot AI lite review requested due to automatic review settings August 25, 2026 01:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 3 minutes.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9a51da5-1cf6-460e-9635-b30a34726839

📥 Commits

Reviewing files that changed from the base of the PR and between 87e36b1 and f327c17.

📒 Files selected for processing (9)
  • .github/workflows/mutation-weekly.yml
  • pipeline/INSTRUMENTS-A1.md
  • pipeline/testing/mutation/__init__.py
  • pipeline/testing/mutation/directed.py
  • pipeline/testing/mutation/ledger.py
  • pipeline/testing/mutation/run_mutation.py
  • pipeline/testing/property/__init__.py
  • pipeline/testing/property/invariants.py
  • pipeline/testing/property/judge.py
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch quality-instruments-a1

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add A1 mutation/property scoring tools with tamper-evident ledger + weekly workflow

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add stdlib-first mutation executor with mutmut fallback and baseline-gate scoring.
• Add mechanical directed candidate screening and property-based cross-anchor judge.
• Add sha256-chained JSONL score ledger and a reusable weekly GitHub workflow.
Diagram

graph TD
  A[".github workflow"] --> B["run_mutation.py"] --> C["mutation_result.json"] --> D["judge.py"] --> E["judged.json"]
  B --> F["directed.py"]
  D --> G["invariants.py"]
  B --> H["ledger.py"]
  subgraph Legend
    direction LR
    _wf["Workflow"] ~~~ _cli["CLI tool"] ~~~ _data[("Artifact / Data")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Depend solely on mutmut (drop builtin engine)
  • ➕ Much less custom AST logic to maintain
  • ➕ Closer alignment with a widely-used mutation framework
  • ➖ Violates the stated constraint of stdlib-only runtime fallback
  • ➖ Harder to run in constrained/offline environments; scoring becomes dependency-sensitive
2. Use a dedicated mutation framework (e.g., cosmic-ray) for Python-only flow
  • ➕ Built-in operator sets and reporting; potentially richer mutation strategies
  • ➕ Less bespoke code around mutation execution
  • ➖ Introduces heavier third-party dependency surface and CI complexity
  • ➖ May not provide re-applicable per-site survivor metadata in the desired format
3. Store scores in Git notes/tags instead of a JSONL hash chain
  • ➕ Leverages Git integrity model; no custom tamper detection needed
  • ➕ Can be kept out of the working tree
  • ➖ Harder to consume as an artifact across repos/workflows
  • ➖ More operational friction for append-only history and line-level tamper pinpointing

Recommendation: Current approach fits the IR boundary well: it keeps all judgment mechanical and remains functional without third-party installs via builtin AST mutations, seeded generators, and deterministic exit-code scoring. Retain the builtin fallback + explicit ledger verification; consider the Git-notes alternative only if repository artifact storage becomes a maintenance burden.

Files changed (9) +2032 / -0

Enhancement (7) +1824 / -0
__init__.pyInitialize mutation instruments package +1/-0

Initialize mutation instruments package

• Adds a package marker and module-level description for mutation executor/screening/ledger tooling.

pipeline/testing/mutation/init.py

directed.pyAdd mechanical directed mutation candidate pre-screen +244/-0

Add mechanical directed mutation candidate pre-screen

• Implements a deterministic filter for LLM-proposed mutation candidates: repo path containment, AST parseability, nearby operator site discovery, and a scratch-copy preview run. Supports policy modes (any/survived/killed) and emits accepted/rejected lists with reason codes.

pipeline/testing/mutation/directed.py

ledger.pyAdd append-only sha256 hash-chain score ledger +173/-0

Add append-only sha256 hash-chain score ledger

• Implements JSONL ledger append/verify/show commands with a canonicalized sha256 hash chain (prev_hash linkage). Verification reports exact line-level corruption and rejects structurally invalid records.

pipeline/testing/mutation/ledger.py

run_mutation.pyAdd stdlib-first mutation executor with mutmut fallback +607/-0

Add stdlib-first mutation executor with mutmut fallback

• Adds a mutation runner supporting mutmut (when available) or a builtin AST engine with 8 operators, scratch workcopies, and per-mutant suite reruns. Enforces baseline pass (exit 2 on failure), emits a structured result JSON and optional Markdown summary, and provides a minimal stdlib-only test runner fallback.

pipeline/testing/mutation/run_mutation.py

__init__.pyInitialize property instruments package +1/-0

Initialize property instruments package

• Adds a package marker and module-level description for property executor and judge tooling.

pipeline/testing/property/init.py

invariants.pyAdd YAML-driven property registry and executor with Hypothesis fallback +538/-0

Add YAML-driven property registry and executor with Hypothesis fallback

• Implements manifest parsing (PyYAML if available, else mini YAML), seeded generators for 7 types, and four property kinds (commutative/idempotent/roundtrip/invariant). Runs via hypothesis when installed or deterministic random sampling otherwise, emitting per-property pass/fail/error results.

pipeline/testing/property/invariants.py

judge.pyAdd cross-anchor judge to reject trivial properties +260/-0

Add cross-anchor judge to reject trivial properties

• Loads mutation results, re-applies surviving builtin-engine mutants on a scratch copy, and re-runs properties to count kills per property. Rejects properties that fail baseline or kill zero mutants as trivial, and writes judged.json/rejected.json plus a JSONL audit log of every evaluation.

pipeline/testing/property/judge.py

Documentation (1) +100 / -0
INSTRUMENTS-A1.mdDocument A1 test-effectiveness instruments and IR-0004 mapping +100/-0

Document A1 test-effectiveness instruments and IR-0004 mapping

• Adds usage and behavior documentation for the mutation executor, directed pre-screen, property invariants/judge, score ledger, and weekly workflow. Clarifies the “LLM proposes, machines decide” boundary and summarizes offline self-tests.

pipeline/INSTRUMENTS-A1.md

Other (1) +108 / -0
mutation-weekly.ymlAdd reusable scheduled mutation-scoring workflow +108/-0

Add reusable scheduled mutation-scoring workflow

• Introduces a reusable workflow_call + weekly cron workflow to run mutation scoring against a target repo. Installs pinned mutmut/pytest, runs the mutation executor, appends/verifies the score ledger, publishes a job summary, and uploads artifacts.

.github/workflows/mutation-weekly.yml

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Mutmut glob passed as path 🐞 Bug ≡ Correctness
Description
run_mutmut() passes a file-glob like "src/**/*.py" to mutmut via --paths-to-mutate, but that flag is
for source directories/packages, not selecting individual files; with the workflow always installing
mutmut, engine=auto will pick mutmut and may fail or score the wrong scope.
Code

pipeline/testing/mutation/run_mutation.py[R459-462]

+        cmd = [sys.executable, "-m", "mutmut", "run",
+               "--paths-to-mutate", module_glob, "--no-progress"]
+        proc = subprocess.run(cmd, cwd=str(work), capture_output=True, text=True,
+                              timeout=overall_timeout)
Relevance

●●● Strong

Workflow supplies file globs while mutmut expects paths; this can mis-scope or fail the primary
auto-selected engine.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow derives a file glob (e.g. src/**/*.py) and passes it into run_mutation.py --module.
In mutmut mode, the code forwards that same value to mutmut run as --paths-to-mutate, but mutmut
documents/issues clarify paths_to_mutate is for source directories/packages rather than file-glob
selection, so this is a semantic mismatch that can break or mis-scope the run.

pipeline/testing/mutation/run_mutation.py[452-463]
.github/workflows/mutation-weekly.yml[54-71]
🌐 Mutmut maintainers/users note paths_to_mutate is often misunderstood: it is meant to point at the source code directories/packages needed to run tests, not to select specific files to mutate.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`run_mutmut()` uses `--paths-to-mutate <module_glob>` where `<module_glob>` is a recursive file glob (e.g. `src/**/*.py`). In mutmut, `paths_to_mutate` is intended to point at source directories/packages needed to run tests, not a file-selection glob. With the weekly workflow installing mutmut, `--engine auto` will select the mutmut engine and this mismatch can break the run or produce an unintended mutation scope.

### Issue Context
- The workflow computes `MODULE_GLOB` as a file glob (e.g. `src/**/*.py`).
- The builtin engine correctly treats it as a glob.
- The mutmut engine should either:
 1) translate that glob to one or more source root directories, or
 2) ignore `module_glob` for mutmut and rely on mutmut’s own configuration/selection mechanism, or
 3) force `--engine builtin` in the scheduled workflow.

### Fix Focus Areas
- pipeline/testing/mutation/run_mutation.py[459-463]
- .github/workflows/mutation-weekly.yml[54-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Ledger append doesn’t validate 🐞 Bug ☼ Reliability
Description
append_entry() assumes the last record has a valid "hash" and never validates the existing hash
chain before extending, so a corrupted/tampered ledger can cause a KeyError crash or be extended
without an explicit integrity failure at append time.
Code

pipeline/testing/mutation/ledger.py[R67-69]

+    records = _read_records(path)
+    prev_hash = records[-1]["hash"] if records else GENESIS
+    record = {
Relevance

●●● Strong

Directly indexes untrusted ledger tail; validating the chain before append is a clear reliability
fix.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new append implementation directly indexes records[-1]["hash"] without checking required
fields and without verifying the chain. This can crash on malformed-but-JSON records and does not
itself enforce “refuse to extend” semantics.

pipeline/testing/mutation/ledger.py[61-82]
pipeline/testing/mutation/ledger.py[85-125]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`append_entry()` reads prior records and unconditionally uses `records[-1]["hash"]` as `prev_hash`. If the last record is missing `hash` (or the file is otherwise corrupt-but-JSON), this raises `KeyError` (uncaught) instead of a controlled `LedgerError`. Also, `append_entry()` does not verify the existing chain before appending, so callers can extend a tampered ledger unless they remember to call `verify()` separately.

### Issue Context
The workflow currently calls `append` and then `verify`, but the CLI/tool should be robust and fail-closed on its own. The module docstring claims it “refuses to extend a structurally corrupt ledger”.

### Fix Focus Areas
- pipeline/testing/mutation/ledger.py[61-82]
- pipeline/testing/mutation/ledger.py[85-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Hypothesis mislabels config errors 🐞 Bug ≡ Correctness
Description
The Hypothesis engine catches all exceptions as "failed", so manifest/configuration errors
(ManifestError) become test failures instead of "error", which misreports operator mistakes and
breaks parity with the random engine’s error handling.
Code

pipeline/testing/property/invariants.py[R432-435]

+    try:
+        _run()
+    except Exception as exc:  # noqa: BLE001 - hypothesis re-raises the falsification
+        return {"name": spec["name"], "property": spec["property"],
Relevance

●●● Strong

Blanket Hypothesis exception handling visibly contradicts the random engine’s explicit ManifestError
classification.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
_check_property() raises ManifestError for invalid invariant specs, but _run_hypothesis()
wraps execution in a blanket exception handler that reports any exception as failed, unlike
_run_random() which explicitly classifies ManifestError as error.

pipeline/testing/property/invariants.py[325-335]
pipeline/testing/property/invariants.py[432-446]
pipeline/testing/property/invariants.py[370-374]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
In `_run_hypothesis()`, `ManifestError` thrown from `_check_property()` is caught under a broad `except Exception` and returned as `status: failed`. In `_run_random()`, `ManifestError` is treated as `status: error`. This makes the same bad manifest look like a property falsification under Hypothesis.

### Issue Context
`_check_property()` can raise `ManifestError` at runtime (e.g., invariant properties missing `gen.params.check`). With Hypothesis installed, `engine=auto` selects Hypothesis and the result classification becomes inconsistent.

### Fix Focus Areas
- pipeline/testing/property/invariants.py[322-335]
- pipeline/testing/property/invariants.py[384-446]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Pycache can skew mutations 🐞 Bug ☼ Reliability
Description
The builtin mutation engine repeatedly overwrites the same file and reruns tests without disabling
bytecode caching or clearing __pycache__, which can (on coarse mtime filesystems or same-size
rewrites) cause Python to reuse stale .pyc and yield incorrect killed/survived results.
Code

pipeline/testing/mutation/run_mutation.py[R403-407]

+                mutated = mutate_at(source, site["index"])
+                original = work_file.read_text(encoding="utf-8")
+                work_file.write_text(mutated, encoding="utf-8", newline="\n")
+                run = run_test_suite(work, runner=runner, timeout=per_mutant_timeout)
+                work_file.write_text(original, encoding="utf-8", newline="\n")
Relevance

●● Moderate

Potential cache-staleness risk, but pytest disables cacheprovider and scratch copies exclude caches;
impact depends on runner behavior.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The builtin engine writes mutated/restored source and calls run_test_suite() repeatedly;
run_test_suite() invokes pytest (or the minimal runner) without -B and without clearing caches.
Separately, the judge includes an explicit __pycache__ cleanup helper, showing this is a known
hazard in this codebase.

pipeline/testing/mutation/run_mutation.py[284-306]
pipeline/testing/mutation/run_mutation.py[399-408]
pipeline/testing/property/judge.py[116-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The builtin engine mutates a file, runs the suite, then restores the file, but it never clears `__pycache__` and doesn’t run Python with `-B`/`PYTHONDONTWRITEBYTECODE`. In repeated runs, Python may load cached bytecode that no longer matches the current source, which can silently misclassify mutants.

### Issue Context
The judge explicitly clears `__pycache__` between mutant/property runs, indicating the project already recognizes cache staleness as a correctness risk.

### Fix Focus Areas
- pipeline/testing/mutation/run_mutation.py[284-306]
- pipeline/testing/mutation/run_mutation.py[391-419]
- pipeline/testing/property/judge.py[116-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Web pages:
  +7 more
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 9/18, lines 2032/200; both must reach the floor). Router rationale: This is a broad, behavior-heavy addition spanning mutation execution, AST rewriting, property generation/judging, hash-chain integrity, and CI workflow behavior, creating many independent paths with subtle correctness and operational failure modes.

Grey Divider

Tip of the day
💡 Did you know, you can hide the parts of a finding you never read, like the evidence or the agent prompt

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +459 to +462
cmd = [sys.executable, "-m", "mutmut", "run",
"--paths-to-mutate", module_glob, "--no-progress"]
proc = subprocess.run(cmd, cwd=str(work), capture_output=True, text=True,
timeout=overall_timeout)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Mutmut glob passed as path 🐞 Bug ≡ Correctness

run_mutmut() passes a file-glob like "src/**/*.py" to mutmut via --paths-to-mutate, but that flag is
for source directories/packages, not selecting individual files; with the workflow always installing
mutmut, engine=auto will pick mutmut and may fail or score the wrong scope.
Agent Prompt
### Issue description
`run_mutmut()` uses `--paths-to-mutate <module_glob>` where `<module_glob>` is a recursive file glob (e.g. `src/**/*.py`). In mutmut, `paths_to_mutate` is intended to point at source directories/packages needed to run tests, not a file-selection glob. With the weekly workflow installing mutmut, `--engine auto` will select the mutmut engine and this mismatch can break the run or produce an unintended mutation scope.

### Issue Context
- The workflow computes `MODULE_GLOB` as a file glob (e.g. `src/**/*.py`).
- The builtin engine correctly treats it as a glob.
- The mutmut engine should either:
  1) translate that glob to one or more source root directories, or
  2) ignore `module_glob` for mutmut and rely on mutmut’s own configuration/selection mechanism, or
  3) force `--engine builtin` in the scheduled workflow.

### Fix Focus Areas
- pipeline/testing/mutation/run_mutation.py[459-463]
- .github/workflows/mutation-weekly.yml[54-71]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +67 to +69
records = _read_records(path)
prev_hash = records[-1]["hash"] if records else GENESIS
record = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Ledger append doesn’t validate 🐞 Bug ☼ Reliability

append_entry() assumes the last record has a valid "hash" and never validates the existing hash
chain before extending, so a corrupted/tampered ledger can cause a KeyError crash or be extended
without an explicit integrity failure at append time.
Agent Prompt
### Issue description
`append_entry()` reads prior records and unconditionally uses `records[-1]["hash"]` as `prev_hash`. If the last record is missing `hash` (or the file is otherwise corrupt-but-JSON), this raises `KeyError` (uncaught) instead of a controlled `LedgerError`. Also, `append_entry()` does not verify the existing chain before appending, so callers can extend a tampered ledger unless they remember to call `verify()` separately.

### Issue Context
The workflow currently calls `append` and then `verify`, but the CLI/tool should be robust and fail-closed on its own. The module docstring claims it “refuses to extend a structurally corrupt ledger”.

### Fix Focus Areas
- pipeline/testing/mutation/ledger.py[61-82]
- pipeline/testing/mutation/ledger.py[85-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +403 to +407
mutated = mutate_at(source, site["index"])
original = work_file.read_text(encoding="utf-8")
work_file.write_text(mutated, encoding="utf-8", newline="\n")
run = run_test_suite(work, runner=runner, timeout=per_mutant_timeout)
work_file.write_text(original, encoding="utf-8", newline="\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Pycache can skew mutations 🐞 Bug ☼ Reliability

The builtin mutation engine repeatedly overwrites the same file and reruns tests without disabling
bytecode caching or clearing __pycache__, which can (on coarse mtime filesystems or same-size
rewrites) cause Python to reuse stale .pyc and yield incorrect killed/survived results.
Agent Prompt
### Issue description
The builtin engine mutates a file, runs the suite, then restores the file, but it never clears `__pycache__` and doesn’t run Python with `-B`/`PYTHONDONTWRITEBYTECODE`. In repeated runs, Python may load cached bytecode that no longer matches the current source, which can silently misclassify mutants.

### Issue Context
The judge explicitly clears `__pycache__` between mutant/property runs, indicating the project already recognizes cache staleness as a correctness risk.

### Fix Focus Areas
- pipeline/testing/mutation/run_mutation.py[284-306]
- pipeline/testing/mutation/run_mutation.py[391-419]
- pipeline/testing/property/judge.py[116-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +432 to +435
try:
_run()
except Exception as exc: # noqa: BLE001 - hypothesis re-raises the falsification
return {"name": spec["name"], "property": spec["property"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. Hypothesis mislabels config errors 🐞 Bug ≡ Correctness

The Hypothesis engine catches all exceptions as "failed", so manifest/configuration errors
(ManifestError) become test failures instead of "error", which misreports operator mistakes and
breaks parity with the random engine’s error handling.
Agent Prompt
### Issue description
In `_run_hypothesis()`, `ManifestError` thrown from `_check_property()` is caught under a broad `except Exception` and returned as `status: failed`. In `_run_random()`, `ManifestError` is treated as `status: error`. This makes the same bad manifest look like a property falsification under Hypothesis.

### Issue Context
`_check_property()` can raise `ManifestError` at runtime (e.g., invariant properties missing `gen.params.check`). With Hypothesis installed, `engine=auto` selects Hypothesis and the result classification becomes inconsistent.

### Fix Focus Areas
- pipeline/testing/property/invariants.py[322-335]
- pipeline/testing/property/invariants.py[384-446]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@randypanding
randypanding merged commit 2054b44 into main Aug 25, 2026
40 of 42 checks passed
@randypanding
randypanding deleted the quality-instruments-a1 branch August 25, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants