AiResultCache.java

1
package org.egothor.methodatlas;
2
3
import java.io.IOException;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.HashMap;
7
import java.util.List;
8
import java.util.Map;
9
import java.util.Optional;
10
11
import org.egothor.methodatlas.ai.AiClassSuggestion;
12
import org.egothor.methodatlas.ai.AiMethodSuggestion;
13
import org.egothor.methodatlas.api.ScanRecord;
14
import org.egothor.methodatlas.emit.DeltaReport;
15
16
/**
17
 * In-memory cache of AI classification results loaded from a previous MethodAtlas scan output.
18
 *
19
 * <p>Cache entries are keyed by {@code content_hash} — the per-class SHA-256 fingerprint
20
 * produced by the {@code -content-hash} flag. A cache hit means the class source is identical
21
 * to the previous run; the stored AI classification is reused without an API call.</p>
22
 *
23
 * <p>When the source CSV was produced without {@code -content-hash}, the {@code content_hash}
24
 * column is absent and no entries are loaded. All lookups return empty and the AI is called
25
 * normally for every class.</p>
26
 *
27
 * <p>Instances are obtained via {@link #load(Path)} or the no-op {@link #empty()} singleton.</p>
28
 *
29
 * @see MethodAtlasApp
30
 */
31
public final class AiResultCache {
32
33
    private final Map<String, AiClassSuggestion> byHash;
34
    private int hits;
35
    private int misses;
36
37
    private AiResultCache(Map<String, AiClassSuggestion> byHash) {
38
        this.byHash = byHash;
39
    }
40
41
    /** Returns an empty cache that always produces misses. */
42
    public static AiResultCache empty() {
43 1 1. empty : replaced return value with null for org/egothor/methodatlas/AiResultCache::empty → KILLED
        return new AiResultCache(Map.of());
44
    }
45
46
    /**
47
     * Loads a cache from a MethodAtlas CSV output file.
48
     *
49
     * <p>Only rows with a non-empty {@code content_hash} value and a non-{@code null}
50
     * {@code ai_security_relevant} column (AI was enabled for that scan) are included.
51
     * Rows missing either field are silently skipped.</p>
52
     *
53
     * @param csvPath path to a MethodAtlas CSV produced with {@code -content-hash -ai}
54
     * @return loaded cache; never {@code null}
55
     * @throws IOException if the file cannot be read
56
     */
57
    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
58
    public static AiResultCache load(Path csvPath) throws IOException {
59
        List<ScanRecord> records = DeltaReport.loadRecords(csvPath);
60
61
        Map<String, List<ScanRecord>> grouped = new HashMap<>();
62
        for (ScanRecord r : records) {
63 4 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
3. load : removed conditional - replaced equality check with false → KILLED
4. load : removed conditional - replaced equality check with true → KILLED
            if (r.contentHash() != null && !r.contentHash().isEmpty()
64 2 1. load : removed conditional - replaced equality check with true → KILLED
2. load : removed conditional - replaced equality check with false → KILLED
                    && r.aiSecurityRelevant() != null) {
65 1 1. lambda$load$0 : replaced return value with Collections.emptyList for org/egothor/methodatlas/AiResultCache::lambda$load$0 → KILLED
                grouped.computeIfAbsent(r.contentHash(), k -> new ArrayList<>()).add(r);
66
            }
67
        }
68
69 1 1. load : Replaced integer multiplication with division → SURVIVED
        Map<String, AiClassSuggestion> cache = new HashMap<>(grouped.size() * 2);
70
        for (Map.Entry<String, List<ScanRecord>> entry : grouped.entrySet()) {
71
            List<AiMethodSuggestion> methods = new ArrayList<>(entry.getValue().size());
72
            for (ScanRecord r : entry.getValue()) {
73
                methods.add(new AiMethodSuggestion(
74
                        r.method(),
75
                        Boolean.TRUE.equals(r.aiSecurityRelevant()),
76
                        r.aiDisplayName(),
77 2 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
                        r.aiTags() != null ? r.aiTags() : List.of(),
78
                        r.aiReason(),
79 2 1. load : removed conditional - replaced equality check with false → SURVIVED
2. load : removed conditional - replaced equality check with true → KILLED
                        r.aiConfidence() != null ? r.aiConfidence() : 0.0,
80 2 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
                        r.aiInteractionScore() != null ? r.aiInteractionScore() : 0.0));
81
            }
82
            cache.put(entry.getKey(), new AiClassSuggestion(null, null, null, null, methods));
83
        }
84
85 1 1. load : replaced return value with null for org/egothor/methodatlas/AiResultCache::load → KILLED
        return new AiResultCache(cache);
86
    }
87
88
    /**
89
     * Returns the cached classification for the class with the given content hash,
90
     * or empty when the hash is absent from the cache.
91
     *
92
     * @param contentHash SHA-256 fingerprint of the class source, or {@code null}
93
     * @return cached suggestion, or empty on a miss or null hash
94
     */
95
    public Optional<AiClassSuggestion> lookup(String contentHash) {
96 2 1. lookup : removed conditional - replaced equality check with true → KILLED
2. lookup : removed conditional - replaced equality check with false → KILLED
        if (contentHash == null) {
97 1 1. lookup : Replaced integer addition with subtraction → KILLED
            misses++;
98
            return Optional.empty();
99
        }
100
        AiClassSuggestion cached = byHash.get(contentHash);
101 2 1. lookup : removed conditional - replaced equality check with false → KILLED
2. lookup : removed conditional - replaced equality check with true → KILLED
        if (cached != null) {
102 1 1. lookup : Replaced integer addition with subtraction → KILLED
            hits++;
103
        } else {
104 1 1. lookup : Replaced integer addition with subtraction → KILLED
            misses++;
105
        }
106 1 1. lookup : replaced return value with Optional.empty for org/egothor/methodatlas/AiResultCache::lookup → KILLED
        return Optional.ofNullable(cached);
107
    }
108
109
    /**
110
     * Returns {@code true} when this cache contains at least one entry.
111
     *
112
     * <p>When {@code false}, content hashes do not need to be computed for lookups
113
     * because all results would be misses regardless.</p>
114
     */
115
    public boolean isActive() {
116 3 1. isActive : replaced boolean return with true for org/egothor/methodatlas/AiResultCache::isActive → KILLED
2. isActive : removed conditional - replaced equality check with true → KILLED
3. isActive : removed conditional - replaced equality check with false → KILLED
        return !byHash.isEmpty();
117
    }
118
119
    /** Returns the number of successful cache lookups so far. */
120
    public int hits() {
121 1 1. hits : replaced int return with 0 for org/egothor/methodatlas/AiResultCache::hits → KILLED
        return hits;
122
    }
123
124
    /** Returns the number of unsuccessful cache lookups so far. */
125
    public int misses() {
126 1 1. misses : replaced int return with 0 for org/egothor/methodatlas/AiResultCache::misses → KILLED
        return misses;
127
    }
128
}

Mutations

43

1.1
Location : empty
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced return value with null for org/egothor/methodatlas/AiResultCache::empty → KILLED

63

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

3.3
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutContentHashColumn_producesInactiveCache(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

4.4
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

64

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutAiColumns_producesInactiveCache(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

65

1.1
Location : lambda$load$0
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:lookup_nullHash_returnsMissAndIncrementsMisses(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/AiResultCache::lambda$load$0 → KILLED

69

1.1
Location : load
Killed by : none
Replaced integer multiplication with division → SURVIVED
Covering tests

77

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_methodSuggestionFieldsRestoredCorrectly(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

79

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:lookup_nullHash_returnsMissAndIncrementsMisses(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : load
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

80

1.1
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_methodSuggestionFieldsRestoredCorrectly(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

85

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutAiColumns_producesInactiveCache(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/AiResultCache::load → KILLED

96

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : lookup
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_noImportsAddedWhenNothingAnnotated(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

97

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:lookup_nullHash_returnsMissAndIncrementsMisses(java.nio.file.Path)]
Replaced integer addition with subtraction → KILLED

101

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
removed conditional - replaced equality check with true → KILLED

102

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
Replaced integer addition with subtraction → KILLED

104

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
Replaced integer addition with subtraction → KILLED

106

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
replaced return value with Optional.empty for org/egothor/methodatlas/AiResultCache::lookup → KILLED

116

1.1
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced boolean return with true for org/egothor/methodatlas/AiResultCache::isActive → KILLED

2.2
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
removed conditional - replaced equality check with true → KILLED

3.3
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

121

1.1
Location : hits
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
replaced int return with 0 for org/egothor/methodatlas/AiResultCache::hits → KILLED

126

1.1
Location : misses
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced int return with 0 for org/egothor/methodatlas/AiResultCache::misses → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1