Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
The Problem
FileArtifactStore.prune is above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
Method
Mutation-CRAP
Complexity
Mutation score
FileArtifactStore.prune
23.50
7
0.30
Most of its complexity and most of its 23 mutants were not its own. lock.write and use are inline functions, so their code is compiled into prune:
lock.write gives back every read hold before it takes the write lock, and takes them again afterwards, for a caller that upgrades from the read lock; a prune never holds it, so those loops never run and no test can tell their mutants apart;
use carries the closing of the stream with its exception handling.
The few mutants that were its own — the journal line and its condition — no test looked at.
Non-Goals
No change in behaviour: the same directories are deleted under the same lock, with the same journal line.
No change to persist, which takes the read lock as before.
The Scenarios
Feature: pruning removes what no result keeps, and says so
Scenario#125.01: prune says in the journal what it removed, and is silent when nothing goes
So that the journal tells which artifacts went, without a line for every poll that removed nothing.
Given an artifact directory with nested reports that no kept result references
When the store is pruned
Then the directory is deleted with everything in it, and its key is returned
and the journal says pruned 1 artifact dir(s): [<key>]
When the store is pruned again
Then nothing is returned and the journal says nothing
When the store never persisted anything
Then a prune removes nothing and creates no branches directory
Verified by
FileArtifactStoreUnitTestprune says in the journal what it removed, and is silent when nothing goes (Scenario#125.01), prune of a store that never persisted anything removes nothing and creates nothing (Scenario#125.01), and the earlier prune tests, including concurrent persists and a prune do not interfere
The Solution
prune takes the write lock itself, lock() and unlock() in finally, and hands the work to pruneUnreferenced. pruneUnreferenced lists the directory completely through childrenOf, then deletes what no kept key names, then writes the journal line.
Listing first and deleting afterwards also stops deleting entries out of a directory stream that is still being read.
A local PIT run of FileArtifactStore afterwards:
Method
Mutation-CRAP
Complexity
Mutation score
FileArtifactStore.prune
1.04
1
0.67
FileArtifactStore.pruneUnreferenced
5.05
5
0.88
FileArtifactStore.childrenOf
1.30
1
0.33
The highest score left in the class is copyArtifactDirs at 5.93.
The project maximum is now UiFormats.repoStatusLine at 16.03, so the gate goes down from 24 to 17.
Additional Changes
maxMutationCrap in build.gradle.kts lowered from 24 to 17.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## The Problem
`FileArtifactStore.prune` is above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:
| Method | Mutation-CRAP | Complexity | Mutation score |
|---------------------------|---------------|------------|----------------|
| `FileArtifactStore.prune` | 23.50 | 7 | 0.30 |
Most of its complexity and most of its 23 mutants were not its own.
`lock.write` and `use` are inline functions, so their code is compiled into `prune`:
- `lock.write` gives back every read hold before it takes the write lock, and takes them again afterwards, for a caller that upgrades from the read lock; a prune never holds it, so those loops never run and no test can tell their mutants apart;
- `use` carries the closing of the stream with its exception handling.
The few mutants that were its own — the journal line and its condition — no test looked at.
## Non-Goals
- No change in behaviour: the same directories are deleted under the same lock, with the same journal line.
- No change to `persist`, which takes the read lock as before.
## The Scenarios
### Feature: pruning removes what no result keeps, and says so
#### Scenario#125.01: prune says in the journal what it removed, and is silent when nothing goes
So that the journal tells which artifacts went, without a line for every poll that removed nothing.
- **Given** an artifact directory with nested reports that no kept result references
- **When** the store is pruned
- **Then** the directory is deleted with everything in it, and its key is returned
- **and** the journal says `pruned 1 artifact dir(s): [<key>]`
- **When** the store is pruned again
- **Then** nothing is returned and the journal says nothing
- **When** the store never persisted anything
- **Then** a prune removes nothing and creates no `branches` directory
##### Verified by
- [FileArtifactStoreUnitTest](../../src/test/kotlin/de/hoennig/werkator/artifacts/FileArtifactStoreUnitTest.kt) `prune says in the journal what it removed, and is silent when nothing goes (Scenario#125.01)`, `prune of a store that never persisted anything removes nothing and creates nothing (Scenario#125.01)`, and the earlier prune tests, including `concurrent persists and a prune do not interfere`
## The Solution
`prune` takes the write lock itself, `lock()` and `unlock()` in `finally`, and hands the work to `pruneUnreferenced`.
`pruneUnreferenced` lists the directory completely through `childrenOf`, then deletes what no kept key names, then writes the journal line.
Listing first and deleting afterwards also stops deleting entries out of a directory stream that is still being read.
A local PIT run of `FileArtifactStore` afterwards:
| Method | Mutation-CRAP | Complexity | Mutation score |
|---------------------------------------|---------------|------------|----------------|
| `FileArtifactStore.prune` | 1.04 | 1 | 0.67 |
| `FileArtifactStore.pruneUnreferenced` | 5.05 | 5 | 0.88 |
| `FileArtifactStore.childrenOf` | 1.30 | 1 | 0.33 |
The highest score left in the class is `copyArtifactDirs` at 5.93.
The project maximum is now `UiFormats.repoStatusLine` at 16.03, so the gate goes down from 24 to 17.
## Additional Changes
- `maxMutationCrap` in `build.gradle.kts` lowered from 24 to 17.
- Version 1.37.6 with its entry in `releases.html`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
mi
changed title from WIP: Artifact store pruning split into its steps to Artifact store pruning split into its steps, takes the write lock directly, gate down to 17 (v1.37.6)2026-10-05 19:38:46 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The Problem
FileArtifactStore.pruneis above the Kotlin target of 15, measured by the nightly mutation build of 2026-10-05:FileArtifactStore.pruneMost of its complexity and most of its 23 mutants were not its own.
lock.writeanduseare inline functions, so their code is compiled intoprune:lock.writegives back every read hold before it takes the write lock, and takes them again afterwards, for a caller that upgrades from the read lock; a prune never holds it, so those loops never run and no test can tell their mutants apart;usecarries the closing of the stream with its exception handling.The few mutants that were its own — the journal line and its condition — no test looked at.
Non-Goals
persist, which takes the read lock as before.The Scenarios
Feature: pruning removes what no result keeps, and says so
Scenario#125.01: prune says in the journal what it removed, and is silent when nothing goes
So that the journal tells which artifacts went, without a line for every poll that removed nothing.
pruned 1 artifact dir(s): [<key>]branchesdirectoryVerified by
prune says in the journal what it removed, and is silent when nothing goes (Scenario#125.01),prune of a store that never persisted anything removes nothing and creates nothing (Scenario#125.01), and the earlier prune tests, includingconcurrent persists and a prune do not interfereThe Solution
prunetakes the write lock itself,lock()andunlock()infinally, and hands the work topruneUnreferenced.pruneUnreferencedlists the directory completely throughchildrenOf, then deletes what no kept key names, then writes the journal line.Listing first and deleting afterwards also stops deleting entries out of a directory stream that is still being read.
A local PIT run of
FileArtifactStoreafterwards:FileArtifactStore.pruneFileArtifactStore.pruneUnreferencedFileArtifactStore.childrenOfThe highest score left in the class is
copyArtifactDirsat 5.93.The project maximum is now
UiFormats.repoStatusLineat 16.03, so the gate goes down from 24 to 17.Additional Changes
maxMutationCrapinbuild.gradle.ktslowered from 24 to 17.releases.html.🤖 Generated with Claude Code
WIP: Artifact store pruning split into its stepsto Artifact store pruning split into its steps, takes the write lock directly, gate down to 17 (v1.37.6)