AiCacheStore.java

1
package org.egothor.methodatlas;
2
3
import java.io.BufferedWriter;
4
import java.io.IOException;
5
import java.nio.charset.StandardCharsets;
6
import java.nio.file.Files;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.Collection;
10
import java.util.List;
11
import java.util.logging.Level;
12
import java.util.logging.Logger;
13
14
import tools.jackson.core.JacksonException;
15
import tools.jackson.databind.json.JsonMapper;
16
17
/**
18
 * Reads and writes the unified AI result cache as a JSON Lines file: one
19
 * {@link AiCacheEntry} per line.
20
 *
21
 * <p>
22
 * JSON Lines is chosen over a single JSON array so the file streams, appends
23
 * cleanly, and a single corrupt line degrades to one lost cache entry rather than
24
 * an unreadable file. The format is independent of the stable per-method scan CSV
25
 * (which cannot carry credential verdicts or a prompt signature) and of the
26
 * separate credential CSV; it is the sole carrier of cache state.
27
 * </p>
28
 *
29
 * <p>
30
 * This class is a stateless, thread-safe utility holder.
31
 * </p>
32
 *
33
 * @since 4.1.0
34
 */
35
public final class AiCacheStore {
36
37
    private static final Logger LOG = Logger.getLogger(AiCacheStore.class.getName());
38
39
    /** Shared, thread-safe JSON mapper (immutable after build). */
40
    private static final JsonMapper MAPPER = JsonMapper.builder().build();
41
42
    private AiCacheStore() {
43
        // utility class
44
    }
45
46
    /**
47
     * Returns {@code true} if {@code file} appears to be a unified JSON-Lines cache
48
     * rather than a legacy scan CSV, by inspecting the first non-whitespace byte.
49
     *
50
     * @param file path to inspect; never {@code null}
51
     * @return {@code true} when the first non-blank character is an opening brace
52
     * @throws IOException if the file cannot be read
53
     */
54
    public static boolean looksLikeJsonLines(Path file) throws IOException {
55 2 1. looksLikeJsonLines : replaced boolean return with false for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE
2. looksLikeJsonLines : replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE
        return looksLikeJsonLines(Files.readAllLines(file, StandardCharsets.UTF_8));
56
    }
57
58
    /**
59
     * Returns {@code true} if the already-read {@code lines} appear to be a unified
60
     * JSON-Lines cache, by inspecting the first non-blank line.
61
     *
62
     * <p>
63
     * This line-based overload lets a caller read the file once and drive both the
64
     * format decision and {@link #read(List)} from the same in-memory copy.
65
     * </p>
66
     *
67
     * @param lines file lines, already read; never {@code null}
68
     * @return {@code true} when the first non-blank character is an opening brace
69
     */
70
    /* default */ static boolean looksLikeJsonLines(List<String> lines) {
71
        for (String line : lines) {
72
            String trimmed = line.strip();
73 2 1. looksLikeJsonLines : removed conditional - replaced equality check with true → SURVIVED
2. looksLikeJsonLines : removed conditional - replaced equality check with false → KILLED
            if (!trimmed.isEmpty()) {
74 3 1. looksLikeJsonLines : removed conditional - replaced equality check with false → KILLED
2. looksLikeJsonLines : replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → KILLED
3. looksLikeJsonLines : removed conditional - replaced equality check with true → KILLED
                return trimmed.charAt(0) == '{';
75
            }
76
        }
77 1 1. looksLikeJsonLines : replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE
        return false;
78
    }
79
80
    /**
81
     * Reads all cache entries from a JSON-Lines file. Blank lines are ignored and a
82
     * single unparseable line is skipped (logged at {@code FINE}) so a partially
83
     * corrupt cache never aborts a scan.
84
     *
85
     * @param file path to the cache file; never {@code null}
86
     * @return the parsed entries in file order; never {@code null}; may be empty
87
     * @throws IOException if the file cannot be read
88
     */
89
    public static List<AiCacheEntry> read(Path file) throws IOException {
90 1 1. read : replaced return value with Collections.emptyList for org/egothor/methodatlas/AiCacheStore::read → NO_COVERAGE
        return read(Files.readAllLines(file, StandardCharsets.UTF_8));
91
    }
92
93
    /**
94
     * Parses cache entries from already-read JSON-Lines {@code lines}. Blank lines are
95
     * ignored and a single unparseable line is skipped (logged at {@code FINE}) so a
96
     * partially corrupt cache never aborts a scan.
97
     *
98
     * @param lines file lines, already read; never {@code null}
99
     * @return the parsed entries in line order; never {@code null}; may be empty
100
     */
101
    /* default */ static List<AiCacheEntry> read(List<String> lines) {
102
        List<AiCacheEntry> entries = new ArrayList<>(lines.size());
103
        for (String line : lines) {
104
            String trimmed = line.strip();
105 2 1. read : removed conditional - replaced equality check with false → SURVIVED
2. read : removed conditional - replaced equality check with true → KILLED
            if (trimmed.isEmpty()) {
106
                continue;
107
            }
108
            try {
109
                entries.add(MAPPER.readValue(trimmed, AiCacheEntry.class));
110
            } catch (JacksonException e) {
111
                LOG.log(Level.FINE, e, () -> "Skipping unparseable AI cache line");
112
            }
113
        }
114 1 1. read : replaced return value with Collections.emptyList for org/egothor/methodatlas/AiCacheStore::read → KILLED
        return entries;
115
    }
116
117
    /**
118
     * Writes cache entries to a JSON-Lines file, one entry per line, overwriting any
119
     * existing file. Parent directories are created if absent.
120
     *
121
     * @param file    destination path; never {@code null}
122
     * @param entries entries to write; never {@code null}
123
     * @throws IOException if the file cannot be written
124
     */
125
    public static void write(Path file, Collection<AiCacheEntry> entries) throws IOException {
126
        Path parent = file.toAbsolutePath().getParent();
127 2 1. write : removed conditional - replaced equality check with true → SURVIVED
2. write : removed conditional - replaced equality check with false → SURVIVED
        if (parent != null) {
128
            Files.createDirectories(parent);
129
        }
130
        try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
131
            for (AiCacheEntry entry : entries) {
132 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write(MAPPER.writeValueAsString(entry));
133 1 1. write : removed call to java/io/BufferedWriter::write → SURVIVED
                writer.write('\n');
134
            }
135
        }
136
    }
137
}

Mutations

55

1.1
Location : looksLikeJsonLines
Killed by : none
replaced boolean return with false for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE

2.2
Location : looksLikeJsonLines
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE

73

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

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

74

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

2.2
Location : looksLikeJsonLines
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → KILLED

3.3
Location : looksLikeJsonLines
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

77

1.1
Location : looksLikeJsonLines
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE

90

1.1
Location : read
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/AiCacheStore::read → NO_COVERAGE

105

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

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

114

1.1
Location : read
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:unifiedCache_roundTripsClassificationAndVerdicts(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/AiCacheStore::read → KILLED

127

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

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

132

1.1
Location : write
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:unifiedCache_roundTripsClassificationAndVerdicts(java.nio.file.Path)]
removed call to java/io/BufferedWriter::write → KILLED

133

1.1
Location : write
Killed by : none
removed call to java/io/BufferedWriter::write → SURVIVED
Covering tests

Active mutators

Tests examined


Report generated by PIT 1.22.1