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
        for (String line : Files.readAllLines(file, StandardCharsets.UTF_8)) {
56
            String trimmed = line.strip();
57 2 1. looksLikeJsonLines : removed conditional - replaced equality check with true → SURVIVED
2. looksLikeJsonLines : removed conditional - replaced equality check with false → KILLED
            if (!trimmed.isEmpty()) {
58 3 1. looksLikeJsonLines : removed conditional - replaced equality check with true → KILLED
2. looksLikeJsonLines : removed conditional - replaced equality check with false → KILLED
3. looksLikeJsonLines : replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → KILLED
                return trimmed.charAt(0) == '{';
59
            }
60
        }
61 1 1. looksLikeJsonLines : replaced boolean return with true for org/egothor/methodatlas/AiCacheStore::looksLikeJsonLines → NO_COVERAGE
        return false;
62
    }
63
64
    /**
65
     * Reads all cache entries from a JSON-Lines file. Blank lines are ignored and a
66
     * single unparseable line is skipped (logged at {@code FINE}) so a partially
67
     * corrupt cache never aborts a scan.
68
     *
69
     * @param file path to the cache file; never {@code null}
70
     * @return the parsed entries in file order; never {@code null}; may be empty
71
     * @throws IOException if the file cannot be read
72
     */
73
    public static List<AiCacheEntry> read(Path file) throws IOException {
74
        List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
75
        List<AiCacheEntry> entries = new ArrayList<>(lines.size());
76
        for (String line : lines) {
77
            String trimmed = line.strip();
78 2 1. read : removed conditional - replaced equality check with false → SURVIVED
2. read : removed conditional - replaced equality check with true → KILLED
            if (trimmed.isEmpty()) {
79
                continue;
80
            }
81
            try {
82
                entries.add(MAPPER.readValue(trimmed, AiCacheEntry.class));
83
            } catch (JacksonException e) {
84
                LOG.log(Level.FINE, e, () -> "Skipping unparseable AI cache line");
85
            }
86
        }
87 1 1. read : replaced return value with Collections.emptyList for org/egothor/methodatlas/AiCacheStore::read → KILLED
        return entries;
88
    }
89
90
    /**
91
     * Writes cache entries to a JSON-Lines file, one entry per line, overwriting any
92
     * existing file. Parent directories are created if absent.
93
     *
94
     * @param file    destination path; never {@code null}
95
     * @param entries entries to write; never {@code null}
96
     * @throws IOException if the file cannot be written
97
     */
98
    public static void write(Path file, Collection<AiCacheEntry> entries) throws IOException {
99
        Path parent = file.toAbsolutePath().getParent();
100 2 1. write : removed conditional - replaced equality check with true → SURVIVED
2. write : removed conditional - replaced equality check with false → SURVIVED
        if (parent != null) {
101
            Files.createDirectories(parent);
102
        }
103
        try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
104
            for (AiCacheEntry entry : entries) {
105 1 1. write : removed call to java/io/BufferedWriter::write → KILLED
                writer.write(MAPPER.writeValueAsString(entry));
106 1 1. write : removed call to java/io/BufferedWriter::write → SURVIVED
                writer.write('\n');
107
            }
108
        }
109
    }
110
}

Mutations

57

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

2.2
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

58

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

2.2
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

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

61

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

78

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

87

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

100

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

105

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

106

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