| 1 | package org.egothor.methodatlas; | |
| 2 | ||
| 3 | import java.io.FilterOutputStream; | |
| 4 | import java.io.IOException; | |
| 5 | import java.io.OutputStream; | |
| 6 | import java.io.OutputStreamWriter; | |
| 7 | import java.io.PrintWriter; | |
| 8 | import java.nio.charset.StandardCharsets; | |
| 9 | import java.nio.file.Files; | |
| 10 | import java.nio.file.Path; | |
| 11 | import java.util.logging.Level; | |
| 12 | import java.util.logging.Logger; | |
| 13 | ||
| 14 | import org.egothor.methodatlas.ai.ManualConsumeEngine; | |
| 15 | import org.egothor.methodatlas.ai.AiSuggestionEngine; | |
| 16 | import org.egothor.methodatlas.api.TestDiscoveryConfig; | |
| 17 | import org.egothor.methodatlas.command.AiRuntimeBuilder; | |
| 18 | import org.egothor.methodatlas.command.ApplyTagsCommand; | |
| 19 | ||
| 20 | import org.egothor.methodatlas.emit.ClassificationOverride; | |
| 21 | import org.egothor.methodatlas.emit.OutputMode; | |
| 22 | import org.egothor.methodatlas.command.ApplyTagsFromCsvCommand; | |
| 23 | import org.egothor.methodatlas.command.CheckPromptsCommand; | |
| 24 | import org.egothor.methodatlas.command.CommentRedaction; | |
| 25 | import org.egothor.methodatlas.command.DiffCommand; | |
| 26 | import org.egothor.methodatlas.command.EvalAccuracyCommand; | |
| 27 | import org.egothor.methodatlas.command.GitHubAnnotationsCommand; | |
| 28 | import org.egothor.methodatlas.command.JsonCommand; | |
| 29 | import org.egothor.methodatlas.command.ManualPrepareCommand; | |
| 30 | import org.egothor.methodatlas.command.OverrideLoader; | |
| 31 | import org.egothor.methodatlas.command.PluginLoader; | |
| 32 | import org.egothor.methodatlas.command.SarifCommand; | |
| 33 | import org.egothor.methodatlas.command.ScanCommand; | |
| 34 | import org.egothor.methodatlas.command.ScanOrchestrator; | |
| 35 | import org.egothor.methodatlas.command.RedactionAbortedException; | |
| 36 | import org.egothor.methodatlas.coverage.CoverageFacade; | |
| 37 | import org.egothor.methodatlas.attest.AttestFacade; | |
| 38 | import org.egothor.methodatlas.evidencereport.EvidenceReportFacade; | |
| 39 | import org.egothor.methodatlas.gap.GapFacade; | |
| 40 | import org.egothor.methodatlas.receipt.ReceiptFacade; | |
| 41 | import org.egothor.methodatlas.evidence.EvidenceFramework; | |
| 42 | import org.egothor.methodatlas.evidence.EvidencePackCommand; | |
| 43 | import org.egothor.methodatlas.evidence.EvidencePackOptions; | |
| 44 | import org.egothor.methodatlas.evidence.GenSigningKeyCommand; | |
| 45 | ||
| 46 | /** | |
| 47 | * Command-line entry point that parses the arguments and routes the invocation | |
| 48 | * to the matching {@link org.egothor.methodatlas.command.Command} or utility | |
| 49 | * mode. | |
| 50 | * | |
| 51 | * <p> | |
| 52 | * This class owns no discovery or parsing logic of its own. It parses arguments | |
| 53 | * into a {@link CliConfig} (via {@link CliArgs}), selects the operating mode, | |
| 54 | * builds the collaborators that mode needs, and delegates execution. Test | |
| 55 | * discovery is performed by the language plugins on the classpath (loaded | |
| 56 | * through {@link java.util.ServiceLoader}); AI enrichment, output emission, and | |
| 57 | * the evidence-pack, coverage, and receipt features are each handled by their | |
| 58 | * own command or facade. | |
| 59 | * </p> | |
| 60 | * | |
| 61 | * <h2>Source-Derived Metadata</h2> | |
| 62 | * | |
| 63 | * <p> | |
| 64 | * For each discovered test method, the application reports: | |
| 65 | * </p> | |
| 66 | * <ul> | |
| 67 | * <li>fully qualified class name</li> | |
| 68 | * <li>method name</li> | |
| 69 | * <li>inclusive line count of the method declaration</li> | |
| 70 | * <li>JUnit {@code @Tag} values declared on the method</li> | |
| 71 | * </ul> | |
| 72 | * | |
| 73 | * <h2>AI Enrichment</h2> | |
| 74 | * | |
| 75 | * <p> | |
| 76 | * When AI support is enabled, the application submits each discovered test | |
| 77 | * class to an {@link org.egothor.methodatlas.ai.AiSuggestionEngine} and merges | |
| 78 | * the returned method-level suggestions into the emitted output. | |
| 79 | * </p> | |
| 80 | * | |
| 81 | * <h2>Manual AI Workflow</h2> | |
| 82 | * | |
| 83 | * <p> | |
| 84 | * Operators who cannot access an AI API directly can use the two-phase manual | |
| 85 | * workflow: | |
| 86 | * </p> | |
| 87 | * <ol> | |
| 88 | * <li><b>Prepare phase</b> ({@code -manual-prepare}): the application scans | |
| 89 | * test sources and writes one work file per class to the specified directory. | |
| 90 | * Each work file contains operator instructions and the full AI prompt (with | |
| 91 | * class source embedded). No CSV output is produced in this phase.</li> | |
| 92 | * <li><b>Consume phase</b> ({@code -manual-consume}): the application reads | |
| 93 | * operator-saved AI response files ({@code <stem>.response.txt}) from the | |
| 94 | * response directory and produces the final enriched CSV. Classes whose | |
| 95 | * response file is absent receive empty AI columns.</li> | |
| 96 | * </ol> | |
| 97 | * | |
| 98 | * <h2>Supported Command-Line Options</h2> | |
| 99 | * | |
| 100 | * <ul> | |
| 101 | * <li>{@code -config <path>} — loads default values from a YAML configuration | |
| 102 | * file; command-line flags override YAML values</li> | |
| 103 | * <li>{@code -plain} — emits plain text output instead of CSV</li> | |
| 104 | * <li>{@code -sarif} — emits SARIF 2.1.0 JSON output</li> | |
| 105 | * <li>{@code -json} — emits a flat JSON array; each element carries the same | |
| 106 | * fields as CSV with {@code tags} and {@code ai_tags} as arrays and numeric | |
| 107 | * values as JSON numbers</li> | |
| 108 | * <li>{@code -ai} — enables AI-based enrichment</li> | |
| 109 | * <li>{@code -ai-provider <provider>} — selects the AI provider</li> | |
| 110 | * <li>{@code -ai-model <model>} — selects the provider-specific model</li> | |
| 111 | * <li>{@code -ai-base-url <url>} — overrides the provider base URL</li> | |
| 112 | * <li>{@code -ai-api-key <key>} — supplies the AI API key directly</li> | |
| 113 | * <li>{@code -ai-api-key-env <name>} — resolves the AI API key from an | |
| 114 | * environment variable</li> | |
| 115 | * <li>{@code -ai-taxonomy <path>} — loads taxonomy text from an external | |
| 116 | * file</li> | |
| 117 | * <li>{@code -ai-taxonomy-mode <mode>} — selects the built-in taxonomy | |
| 118 | * variant</li> | |
| 119 | * <li>{@code -ai-max-class-chars <count>} — limits class source size submitted | |
| 120 | * to AI</li> | |
| 121 | * <li>{@code -ai-timeout-sec <seconds>} — sets the AI request timeout</li> | |
| 122 | * <li>{@code -ai-max-retries <count>} — sets the retry limit for AI | |
| 123 | * operations</li> | |
| 124 | * <li>{@code -ai-confidence} — requests a confidence score for each AI | |
| 125 | * security classification; adds an {@code ai_confidence} column to the | |
| 126 | * output</li> | |
| 127 | * <li>{@code -min-confidence <threshold>} — silently drops methods whose AI | |
| 128 | * confidence score is below {@code threshold} (range {@code 0.0–1.0}); only | |
| 129 | * effective when {@code -ai-confidence} is also enabled</li> | |
| 130 | * <li>{@code -ai-cache <path>} — loads a previous scan CSV produced with | |
| 131 | * {@code -content-hash -ai} as an AI result cache; classes whose | |
| 132 | * {@code content_hash} matches a cache entry are classified without an API | |
| 133 | * call; changed and new classes are classified normally</li> | |
| 134 | * <li>{@code -file-suffix <suffix>} — matches source files by name suffix | |
| 135 | * (default: {@code Test.java}); may be repeated to match multiple patterns, | |
| 136 | * e.g. {@code -file-suffix Test.java -file-suffix IT.java}; the first | |
| 137 | * occurrence replaces the default</li> | |
| 138 | * <li>{@code -test-annotation <name>} — recognises methods annotated with | |
| 139 | * {@code name} as test methods; may be repeated; the first occurrence replaces | |
| 140 | * the JVM provider's default set (JUnit 5 {@code Test}, {@code ParameterizedTest}, | |
| 141 | * {@code RepeatedTest}, {@code TestFactory}, {@code TestTemplate})</li> | |
| 142 | * <li>{@code -emit-metadata} — emits {@code # key: value} comment lines | |
| 143 | * before the header row describing the tool version, scan timestamp, and | |
| 144 | * taxonomy configuration</li> | |
| 145 | * <li>{@code -apply-tags} — instead of emitting a report, writes | |
| 146 | * AI-generated {@code @DisplayName} and {@code @Tag} annotations back to | |
| 147 | * the scanned source files; requires AI enrichment to be enabled</li> | |
| 148 | * <li>{@code -content-hash} — includes a SHA-256 fingerprint of each class | |
| 149 | * source as a {@code content_hash} column in CSV/plain output and as a SARIF | |
| 150 | * property; useful for detecting which classes changed between scans</li> | |
| 151 | * <li>{@code -security-only} — suppresses non-security methods from the output; | |
| 152 | * only methods whose AI classification (or override) has | |
| 153 | * {@code securityRelevant=true} are emitted; requires AI enrichment or a | |
| 154 | * classification override file to have any effect</li> | |
| 155 | * <li>{@code -manual-prepare <workdir> <responsedir>} — runs the manual AI | |
| 156 | * prepare phase, writing work files to {@code workdir} and empty response stubs | |
| 157 | * to {@code responsedir}; the two paths may be identical</li> | |
| 158 | * <li>{@code -manual-consume <workdir> <responsedir>} — runs the manual AI | |
| 159 | * consume phase, reading response files from {@code responsedir} and emitting | |
| 160 | * the final enriched CSV</li> | |
| 161 | * <li>{@code -diff <before.csv> <after.csv>} — compares two MethodAtlas scan | |
| 162 | * outputs and emits a delta report showing added, removed, and modified test | |
| 163 | * methods; all other flags are ignored when {@code -diff} is present</li> | |
| 164 | * <li>{@code -apply-tags-from-csv <path>} — instead of emitting a report, | |
| 165 | * applies the annotation decisions recorded in the reviewed CSV back to the | |
| 166 | * source files; the CSV is treated as a complete desired-state specification: | |
| 167 | * every test method's {@code @Tag} set and {@code @DisplayName} are driven | |
| 168 | * entirely by the corresponding CSV row</li> | |
| 169 | * <li>{@code -mismatch-limit <n>} — when used with {@code -apply-tags-from-csv}, | |
| 170 | * aborts without modifying any source file if the number of mismatches between | |
| 171 | * the CSV and the current source tree reaches or exceeds {@code n}; {@code -1} | |
| 172 | * (the default) logs mismatches as warnings and proceeds</li> | |
| 173 | * <li>{@code -promote-ai} — <strong>risky, not recommended</strong> opt-in for | |
| 174 | * {@code -apply-tags-from-csv}: fills blank tags/display_name from the AI columns | |
| 175 | * (unvalidated AI output reaches source); off by default</li> | |
| 176 | * <li>{@code -help} / {@code --help} / {@code -h} — print usage and exit</li> | |
| 177 | * <li>{@code -emit-source-root} — adds a {@code source_root} column (CSV) and a | |
| 178 | * {@code SRCROOT=} token (plain text) identifying which scan root each record | |
| 179 | * came from; essential in multi-root projects where the same fully qualified | |
| 180 | * class name can appear under different source trees; no effect on SARIF or | |
| 181 | * GitHub Annotations output</li> | |
| 182 | * </ul> | |
| 183 | * | |
| 184 | * <p> | |
| 185 | * Any remaining non-option arguments are interpreted as root paths to scan. If | |
| 186 | * no scan path is supplied, the current working directory is scanned. | |
| 187 | * </p> | |
| 188 | * | |
| 189 | * @see org.egothor.methodatlas.ai.AiSuggestionEngine | |
| 190 | * @see org.egothor.methodatlas.api.SourcePatcher | |
| 191 | * @see org.egothor.methodatlas.emit.OutputEmitter | |
| 192 | * @see org.egothor.methodatlas.emit.SarifEmitter | |
| 193 | * @see org.egothor.methodatlas.command.Command | |
| 194 | * @see #main(String[]) | |
| 195 | */ | |
| 196 | @SuppressWarnings("PMD.CyclomaticComplexity") | |
| 197 | public final class MethodAtlasApp { | |
| 198 | ||
| 199 | private static final String FLAG_DIFF = "-diff"; | |
| 200 | private static final String FLAG_REQUIRE_JUSTIFICATION = "-require-justification"; | |
| 201 | private static final String FLAG_DIFF_OVERRIDE_FILE = "-override-file"; | |
| 202 | private static final int SINGLE_SINK = 1; | |
| 203 | ||
| 204 | /** Logger for receipt-emission warnings; receipt failures never abort the scan. */ | |
| 205 | private static final Logger LOG = Logger.getLogger(MethodAtlasApp.class.getName()); | |
| 206 | ||
| 207 | /** Tool version fallback when the JAR manifest carries no Implementation-Version. */ | |
| 208 | private static final String DEV_VERSION = "dev"; | |
| 209 | ||
| 210 | /** Exit code returned when CLI validation rejects the requested invocation. */ | |
| 211 | private static final int EXIT_BAD_ARGS = 2; | |
| 212 | /** Exit code used when an evidence-pack framework token cannot be parsed. */ | |
| 213 | private static final int EXIT_BAD_FRAMEWORK = 2; | |
| 214 | /** Exit code returned when the run fails with an expected, reported error. */ | |
| 215 | private static final int EXIT_RUN_ERROR = 1; | |
| 216 | ||
| 217 | /** | |
| 218 | * Prevents instantiation of this utility class. | |
| 219 | */ | |
| 220 | private MethodAtlasApp() { | |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Program entry point. | |
| 225 | * | |
| 226 | * <p> | |
| 227 | * Delegates all work to {@link #run(String[], PrintWriter)}. The expected | |
| 228 | * failure modes — an I/O error traversing a scan tree, a rejected option, or | |
| 229 | * an AI engine that cannot be built — are caught here, reported as a single | |
| 230 | * concise line on {@code System.err}, and turned into exit code | |
| 231 | * {@value #EXIT_RUN_ERROR}; the raw stack trace is not shown. Unexpected | |
| 232 | * runtime failures are deliberately left to propagate so genuine defects | |
| 233 | * still surface with a full trace. A non-zero status is also returned when a | |
| 234 | * source file could not be processed. | |
| 235 | * </p> | |
| 236 | * | |
| 237 | * @param args command-line arguments | |
| 238 | */ | |
| 239 | public static void main(String[] args) { | |
| 240 | // Wrap System.out in a guarded stream whose close() only flushes. | |
| 241 | // This lets try-with-resources manage the PrintWriter (satisfying | |
| 242 | // SpotBugs CloseResource and PMD UseTryWithResources) without | |
| 243 | // permanently closing System.out (satisfying Error Prone's | |
| 244 | // ClosingStandardOutputStreams check). | |
| 245 | OutputStream guarded = new FilterOutputStream(System.out) { | |
| 246 | @Override | |
| 247 | public void close() throws IOException { | |
| 248 | flush(); // flush but do NOT close System.out | |
| 249 | } | |
| 250 | }; | |
| 251 | int exitCode; | |
| 252 | try (PrintWriter out = new PrintWriter(new OutputStreamWriter(guarded, StandardCharsets.UTF_8), true)) { | |
| 253 | exitCode = run(args, out); | |
| 254 | } catch (IOException | IllegalArgumentException | IllegalStateException e) { | |
| 255 | // Expected, actionable failures: report one concise line, no stack | |
| 256 | // trace. Unexpected RuntimeExceptions are not caught here so real | |
| 257 | // bugs still surface with a full trace. | |
| 258 |
2
1. main : removed conditional - replaced equality check with false → NO_COVERAGE 2. main : removed conditional - replaced equality check with true → NO_COVERAGE |
String detail = e.getMessage() != null ? e.getMessage() : e.toString(); |
| 259 | System.err.println("methodatlas: " + detail); | |
| 260 | exitCode = EXIT_RUN_ERROR; | |
| 261 | } | |
| 262 |
2
1. main : removed conditional - replaced equality check with false → NO_COVERAGE 2. main : removed conditional - replaced equality check with true → NO_COVERAGE |
if (exitCode != 0) { |
| 263 | System.exit(exitCode); | |
| 264 | } | |
| 265 | } | |
| 266 | ||
| 267 | /** | |
| 268 | * Executes a full application run by routing to the appropriate | |
| 269 | * {@link org.egothor.methodatlas.command.Command} implementation. | |
| 270 | * | |
| 271 | * <p> | |
| 272 | * This method is the primary entry point for programmatic and test use. It | |
| 273 | * parses arguments, identifies the requested operating mode, constructs the | |
| 274 | * matching command object, and delegates execution to it. | |
| 275 | * </p> | |
| 276 | * | |
| 277 | * @param args command-line arguments | |
| 278 | * @param out writer that receives all emitted output | |
| 279 | * @return {@code 0} if all files were processed successfully, {@code 1} if | |
| 280 | * any file produced a parse or processing error | |
| 281 | * @throws IOException if traversal of a configured file tree fails | |
| 282 | * @throws IllegalArgumentException if an option is unknown, if a required | |
| 283 | * option value is missing, or if an option | |
| 284 | * value cannot be parsed | |
| 285 | * @throws IllegalStateException if AI support is enabled but the AI engine | |
| 286 | * cannot be created successfully | |
| 287 | */ | |
| 288 | /* default */ static int run(String[] args, PrintWriter out) throws IOException { | |
| 289 | // -help is handled before argument parsing so it works even with no | |
| 290 | // other (or otherwise invalid) arguments and never trips the | |
| 291 | // "Unknown argument" path. | |
| 292 | for (String arg : args) { | |
| 293 |
6
1. run : removed conditional - replaced equality check with false → KILLED 2. run : removed conditional - replaced equality check with false → KILLED 3. run : removed conditional - replaced equality check with true → KILLED 4. run : removed conditional - replaced equality check with true → KILLED 5. run : removed conditional - replaced equality check with false → KILLED 6. run : removed conditional - replaced equality check with true → KILLED |
if ("-help".equals(arg) || "--help".equals(arg) || "-h".equals(arg)) { |
| 294 | Usage.print(out); | |
| 295 | return 0; | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | // -diff and -gen-signing-key are utility modes handled before full | |
| 300 | // argument parsing; each owns its own small set of flags and never | |
| 301 | // participates in the scan pipeline, so all other flags are ignored. | |
| 302 |
3
1. run : removed conditional - replaced comparison check with false → KILLED 2. run : removed conditional - replaced comparison check with true → KILLED 3. run : changed conditional boundary → KILLED |
for (int i = 0; i < args.length; i++) { |
| 303 |
2
1. run : removed conditional - replaced equality check with false → KILLED 2. run : removed conditional - replaced equality check with true → KILLED |
if (FLAG_DIFF.equals(args[i])) { |
| 304 |
4
1. run : changed conditional boundary → SURVIVED 2. run : removed conditional - replaced comparison check with false → SURVIVED 3. run : Replaced integer addition with subtraction → SURVIVED 4. run : removed conditional - replaced comparison check with true → KILLED |
if (i + 2 >= args.length) { |
| 305 | throw new IllegalArgumentException( | |
| 306 | "-diff requires two arguments: -diff <before.csv> <after.csv>"); | |
| 307 | } | |
| 308 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → SURVIVED |
return buildDiffCommand(args, i).execute(out); |
| 309 | } | |
| 310 |
2
1. run : removed conditional - replaced equality check with false → SURVIVED 2. run : removed conditional - replaced equality check with true → KILLED |
if (GenSigningKeyCommand.FLAG_GEN_SIGNING_KEY.equals(args[i])) { |
| 311 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → NO_COVERAGE |
return GenSigningKeyCommand.run(args, out); |
| 312 | } | |
| 313 |
2
1. run : removed conditional - replaced equality check with false → SURVIVED 2. run : removed conditional - replaced equality check with true → KILLED |
if (EvalAccuracyCommand.FLAG_EVAL_ACCURACY.equals(args[i])) { |
| 314 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → NO_COVERAGE |
return EvalAccuracyCommand.fromArgs(args).execute(out); |
| 315 | } | |
| 316 |
2
1. run : removed conditional - replaced equality check with false → SURVIVED 2. run : removed conditional - replaced equality check with true → KILLED |
if (CheckPromptsCommand.FLAG_CHECK_PROMPTS.equals(args[i])) { |
| 317 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → NO_COVERAGE |
return CheckPromptsCommand.fromArgs(args).execute(out); |
| 318 | } | |
| 319 | } | |
| 320 | ||
| 321 | CliConfig cliConfig = CliArgs.parse(args); | |
| 322 |
2
1. run : removed conditional - replaced equality check with true → KILLED 2. run : removed conditional - replaced equality check with false → KILLED |
if (cliConfig == null) { |
| 323 | // CliArgs already printed an actionable stderr message; signal | |
| 324 | // bad-args via the conventional exit code 2. | |
| 325 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → KILLED |
return EXIT_BAD_ARGS; |
| 326 | } | |
| 327 | ||
| 328 | // Load the coverage mapping up-front so a malformed file aborts the run | |
| 329 | // before the scan starts, avoiding wasted work and ambiguous errors. | |
| 330 | CoverageFacade.Handle coverageHandle; | |
| 331 | try { | |
| 332 | coverageHandle = prepareCoverageHandle(cliConfig); | |
| 333 | } catch (IOException | IllegalArgumentException e) { | |
| 334 | System.err.println("Error loading coverage mapping: " + e.getMessage()); | |
| 335 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → KILLED |
return EXIT_BAD_ARGS; |
| 336 | } | |
| 337 | ||
| 338 | GapFacade.Handle gapHandle = prepareGapHandle(cliConfig); | |
| 339 | EvidenceReportFacade.Handle evidenceHandle = prepareEvidenceReportHandle(cliConfig); | |
| 340 | AttestFacade.Handle attestHandle = prepareAttestHandle(cliConfig); | |
| 341 | ||
| 342 | // Establish the run identity once and place it in the thread-local | |
| 343 | // context so the JUL formatter (Item 20) can prepend the correlation | |
| 344 | // id to every log record emitted during this invocation. clear() in | |
| 345 | // the finally block keeps the thread-local from outliving the run | |
| 346 | // when MethodAtlas is invoked programmatically (the standard CLI | |
| 347 | // exits the JVM anyway). | |
| 348 | String version = MethodAtlasApp.class.getPackage().getImplementationVersion(); | |
| 349 | ScanRun scanRun = ScanRun.create(version, cliConfig.toString()); | |
| 350 | ScanRunContext.set(scanRun); | |
| 351 | try { | |
| 352 | int exit = runWithScanRun(out, cliConfig, coverageHandle, gapHandle, evidenceHandle, | |
| 353 | attestHandle); | |
| 354 | writeSideCarReports(cliConfig, version, coverageHandle, gapHandle, evidenceHandle, | |
| 355 | attestHandle); | |
| 356 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → KILLED |
return exit; |
| 357 | } catch (RedactionAbortedException e) { | |
| 358 | // Fail-closed by design: a clear one-line reason (file and language), no stack | |
| 359 | // trace, and no side-car reports for an aborted run. | |
| 360 | System.err.println("methodatlas: " + e.getMessage()); | |
| 361 |
1
1. run : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::run → KILLED |
return EXIT_RUN_ERROR; |
| 362 | } finally { | |
| 363 | ScanRunContext.clear(); | |
| 364 | } | |
| 365 | } | |
| 366 | ||
| 367 | /** | |
| 368 | * Writes all side-car reports that were requested for this run. | |
| 369 | * Extracted from {@link #run} to keep that method's cyclomatic complexity in check. | |
| 370 | * | |
| 371 | * @param cliConfig parsed CLI configuration | |
| 372 | * @param version resolved tool version string | |
| 373 | * @param coverageHandle coverage handle; {@code null} when coverage is not active | |
| 374 | * @param gapHandle gap-report handle; {@code null} when gap report is not active | |
| 375 | * @param evidenceHandle evidence-report handle; {@code null} when not active | |
| 376 | * @param attestHandle attestation handle; {@code null} when not active | |
| 377 | */ | |
| 378 | private static void writeSideCarReports(CliConfig cliConfig, String version, | |
| 379 | CoverageFacade.Handle coverageHandle, GapFacade.Handle gapHandle, | |
| 380 | EvidenceReportFacade.Handle evidenceHandle, AttestFacade.Handle attestHandle) { | |
| 381 |
2
1. writeSideCarReports : removed conditional - replaced equality check with false → KILLED 2. writeSideCarReports : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.emitReceipt()) { |
| 382 | emitReceipt(cliConfig, version); | |
| 383 | } | |
| 384 |
2
1. writeSideCarReports : removed conditional - replaced equality check with false → KILLED 2. writeSideCarReports : removed conditional - replaced equality check with true → KILLED |
if (coverageHandle != null) { |
| 385 | writeCoverage(cliConfig, version, coverageHandle); | |
| 386 | } | |
| 387 |
2
1. writeSideCarReports : removed conditional - replaced equality check with false → SURVIVED 2. writeSideCarReports : removed conditional - replaced equality check with true → KILLED |
if (gapHandle != null) { |
| 388 | writeGapReport(cliConfig, version, gapHandle); | |
| 389 | } | |
| 390 |
2
1. writeSideCarReports : removed conditional - replaced equality check with false → SURVIVED 2. writeSideCarReports : removed conditional - replaced equality check with true → KILLED |
if (evidenceHandle != null) { |
| 391 | writeEvidenceReport(cliConfig, version, evidenceHandle); | |
| 392 | } | |
| 393 |
2
1. writeSideCarReports : removed conditional - replaced equality check with false → SURVIVED 2. writeSideCarReports : removed conditional - replaced equality check with true → KILLED |
if (attestHandle != null) { |
| 394 | writeAttestation(cliConfig, version, attestHandle); | |
| 395 | } | |
| 396 | } | |
| 397 | ||
| 398 | /** | |
| 399 | * Constructs a {@link DiffCommand} from raw args, scanning for | |
| 400 | * {@code -require-justification} and {@code -override-file} beyond the two | |
| 401 | * positional CSV paths. | |
| 402 | * | |
| 403 | * @param args raw argument array; {@code args[diffIdx]} must equal {@code -diff} | |
| 404 | * @param diffIdx index of the {@code -diff} flag in {@code args} | |
| 405 | * @return configured command | |
| 406 | */ | |
| 407 | private static DiffCommand buildDiffCommand(String[] args, int diffIdx) { | |
| 408 |
1
1. buildDiffCommand : Replaced integer addition with subtraction → KILLED |
Path diffBefore = Path.of(args[diffIdx + 1]); |
| 409 |
1
1. buildDiffCommand : Replaced integer addition with subtraction → KILLED |
Path diffAfter = Path.of(args[diffIdx + 2]); |
| 410 | boolean requireJustification = false; | |
| 411 | Path diffOverrideFile = null; | |
| 412 |
3
1. buildDiffCommand : removed conditional - replaced comparison check with false → SURVIVED 2. buildDiffCommand : removed conditional - replaced comparison check with true → KILLED 3. buildDiffCommand : changed conditional boundary → KILLED |
for (int j = 0; j < args.length; j++) { |
| 413 |
2
1. buildDiffCommand : removed conditional - replaced equality check with true → SURVIVED 2. buildDiffCommand : removed conditional - replaced equality check with false → SURVIVED |
if (FLAG_REQUIRE_JUSTIFICATION.equals(args[j])) { |
| 414 | requireJustification = true; | |
| 415 |
6
1. buildDiffCommand : removed conditional - replaced comparison check with true → NO_COVERAGE 2. buildDiffCommand : changed conditional boundary → NO_COVERAGE 3. buildDiffCommand : removed conditional - replaced comparison check with false → NO_COVERAGE 4. buildDiffCommand : removed conditional - replaced equality check with false → SURVIVED 5. buildDiffCommand : Replaced integer addition with subtraction → NO_COVERAGE 6. buildDiffCommand : removed conditional - replaced equality check with true → SURVIVED |
} else if (FLAG_DIFF_OVERRIDE_FILE.equals(args[j]) && j + 1 < args.length) { |
| 416 |
1
1. buildDiffCommand : Replaced integer addition with subtraction → NO_COVERAGE |
diffOverrideFile = Path.of(args[j + 1]); |
| 417 | } | |
| 418 | } | |
| 419 |
1
1. buildDiffCommand : replaced return value with null for org/egothor/methodatlas/MethodAtlasApp::buildDiffCommand → KILLED |
return new DiffCommand(diffBefore, diffAfter, requireJustification, diffOverrideFile); |
| 420 | } | |
| 421 | ||
| 422 | /** | |
| 423 | * Loads the coverage mapping when {@code -emit-coverage} is active. | |
| 424 | * | |
| 425 | * @param cliConfig parsed CLI configuration | |
| 426 | * @return prepared handle or {@code null} when coverage mode is not active | |
| 427 | * @throws IOException if the mapping file cannot be read | |
| 428 | * @throws IllegalArgumentException if the mapping file fails validation | |
| 429 | */ | |
| 430 | private static CoverageFacade.Handle prepareCoverageHandle(CliConfig cliConfig) | |
| 431 | throws IOException { | |
| 432 |
2
1. prepareCoverageHandle : removed conditional - replaced equality check with false → KILLED 2. prepareCoverageHandle : removed conditional - replaced equality check with true → KILLED |
if (!cliConfig.emitCoverage()) { |
| 433 | return null; | |
| 434 | } | |
| 435 |
1
1. prepareCoverageHandle : replaced return value with null for org/egothor/methodatlas/MethodAtlasApp::prepareCoverageHandle → KILLED |
return CoverageFacade.prepare(cliConfig.coverageMappingFile(), cliConfig.minConfidence()); |
| 436 | } | |
| 437 | ||
| 438 | /** | |
| 439 | * Writes the coverage report. Errors are logged and swallowed so a | |
| 440 | * coverage-write failure never demotes a successful scan. | |
| 441 | * | |
| 442 | * @param cliConfig parsed CLI configuration | |
| 443 | * @param version resolved tool version | |
| 444 | * @param handle prepared coverage handle; never {@code null} | |
| 445 | */ | |
| 446 | private static void writeCoverage(CliConfig cliConfig, String version, | |
| 447 | CoverageFacade.Handle handle) { | |
| 448 |
2
1. writeCoverage : removed conditional - replaced equality check with true → SURVIVED 2. writeCoverage : removed conditional - replaced equality check with false → SURVIVED |
String toolVersion = version != null ? version : DEV_VERSION; |
| 449 |
2
1. writeCoverage : removed conditional - replaced equality check with true → SURVIVED 2. writeCoverage : removed conditional - replaced equality check with false → KILLED |
Path target = cliConfig.coverageFile() != null |
| 450 | ? cliConfig.coverageFile() | |
| 451 | : Path.of(CoverageFacade.DEFAULT_COVERAGE_FILENAME); | |
| 452 | try { | |
| 453 | handle.write(toolVersion, target); | |
| 454 | } catch (IOException e) { | |
| 455 | if (LOG.isLoggable(Level.WARNING)) { | |
| 456 | LOG.log(Level.WARNING, | |
| 457 | "Could not write coverage report: {0}", e.getMessage()); | |
| 458 | } | |
| 459 | } | |
| 460 | } | |
| 461 | ||
| 462 | /** | |
| 463 | * Prepares a gap-report handle when {@code -gap-report} is active. | |
| 464 | * | |
| 465 | * @param cliConfig parsed CLI configuration | |
| 466 | * @return prepared handle, or {@code null} when gap-report mode is not active | |
| 467 | */ | |
| 468 | private static GapFacade.Handle prepareGapHandle(CliConfig cliConfig) { | |
| 469 |
2
1. prepareGapHandle : removed conditional - replaced equality check with false → SURVIVED 2. prepareGapHandle : removed conditional - replaced equality check with true → SURVIVED |
if (!cliConfig.gapReport()) { |
| 470 | return null; | |
| 471 | } | |
| 472 |
1
1. prepareGapHandle : replaced return value with null for org/egothor/methodatlas/MethodAtlasApp::prepareGapHandle → NO_COVERAGE |
return GapFacade.prepare(cliConfig.minConfidence()); |
| 473 | } | |
| 474 | ||
| 475 | /** | |
| 476 | * Writes the security-domain gap report. Errors are logged and swallowed so | |
| 477 | * a gap-report write failure never demotes a successful scan. | |
| 478 | * | |
| 479 | * @param cliConfig parsed CLI configuration | |
| 480 | * @param version resolved tool version | |
| 481 | * @param handle prepared gap-report handle; never {@code null} | |
| 482 | */ | |
| 483 | private static void writeGapReport(CliConfig cliConfig, String version, | |
| 484 | GapFacade.Handle handle) { | |
| 485 |
2
1. writeGapReport : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeGapReport : removed conditional - replaced equality check with false → NO_COVERAGE |
String toolVersion = version != null ? version : DEV_VERSION; |
| 486 |
2
1. writeGapReport : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeGapReport : removed conditional - replaced equality check with true → NO_COVERAGE |
Path target = cliConfig.gapReportFile() != null |
| 487 | ? cliConfig.gapReportFile() | |
| 488 | : Path.of(GapFacade.DEFAULT_GAP_REPORT_FILENAME); | |
| 489 | try { | |
| 490 | handle.write(toolVersion, target); | |
| 491 | } catch (IOException e) { | |
| 492 | if (LOG.isLoggable(Level.WARNING)) { | |
| 493 | LOG.log(Level.WARNING, | |
| 494 | "Could not write gap report: {0}", e.getMessage()); | |
| 495 | } | |
| 496 | } | |
| 497 | } | |
| 498 | ||
| 499 | /** | |
| 500 | * Prepares an evidence-report handle when {@code -evidence-report} is active. | |
| 501 | * | |
| 502 | * @param cliConfig parsed CLI configuration | |
| 503 | * @return prepared handle, or {@code null} when evidence-report mode is not active | |
| 504 | */ | |
| 505 | private static EvidenceReportFacade.Handle prepareEvidenceReportHandle(CliConfig cliConfig) { | |
| 506 |
2
1. prepareEvidenceReportHandle : removed conditional - replaced equality check with true → SURVIVED 2. prepareEvidenceReportHandle : removed conditional - replaced equality check with false → SURVIVED |
if (!cliConfig.evidenceReport()) { |
| 507 | return null; | |
| 508 | } | |
| 509 |
1
1. prepareEvidenceReportHandle : replaced return value with null for org/egothor/methodatlas/MethodAtlasApp::prepareEvidenceReportHandle → NO_COVERAGE |
return EvidenceReportFacade.prepare(cliConfig.minConfidence(), cliConfig.paths()); |
| 510 | } | |
| 511 | ||
| 512 | /** | |
| 513 | * Prepares an attestation collector when {@code -attest} is active. | |
| 514 | * | |
| 515 | * @param cliConfig parsed CLI configuration | |
| 516 | * @return prepared handle, or {@code null} when attest mode is not active | |
| 517 | */ | |
| 518 | private static AttestFacade.Handle prepareAttestHandle(CliConfig cliConfig) { | |
| 519 |
2
1. prepareAttestHandle : removed conditional - replaced equality check with true → SURVIVED 2. prepareAttestHandle : removed conditional - replaced equality check with false → SURVIVED |
if (!cliConfig.attest()) { |
| 520 | return null; | |
| 521 | } | |
| 522 |
1
1. prepareAttestHandle : replaced return value with null for org/egothor/methodatlas/MethodAtlasApp::prepareAttestHandle → NO_COVERAGE |
return AttestFacade.prepare(cliConfig.minConfidence(), cliConfig.paths()); |
| 523 | } | |
| 524 | ||
| 525 | /** | |
| 526 | * Writes the security evidence report. Errors are logged and swallowed so | |
| 527 | * a report-write failure never demotes a successful scan. | |
| 528 | * | |
| 529 | * @param cliConfig parsed CLI configuration | |
| 530 | * @param version resolved tool version | |
| 531 | * @param handle prepared evidence-report handle; never {@code null} | |
| 532 | */ | |
| 533 | private static void writeEvidenceReport(CliConfig cliConfig, String version, | |
| 534 | EvidenceReportFacade.Handle handle) { | |
| 535 |
2
1. writeEvidenceReport : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEvidenceReport : removed conditional - replaced equality check with true → NO_COVERAGE |
String toolVersion = version != null ? version : DEV_VERSION; |
| 536 |
2
1. writeEvidenceReport : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEvidenceReport : removed conditional - replaced equality check with false → NO_COVERAGE |
Path target = cliConfig.evidenceReportFile() != null |
| 537 | ? cliConfig.evidenceReportFile() | |
| 538 | : Path.of(EvidenceReportFacade.DEFAULT_REPORT_FILENAME); | |
| 539 | try { | |
| 540 | handle.write(toolVersion, target); | |
| 541 | } catch (IOException e) { | |
| 542 | if (LOG.isLoggable(Level.WARNING)) { | |
| 543 | LOG.log(Level.WARNING, | |
| 544 | "Could not write evidence report: {0}", e.getMessage()); | |
| 545 | } | |
| 546 | } | |
| 547 | } | |
| 548 | ||
| 549 | private static void writeAttestation(CliConfig cliConfig, String version, | |
| 550 | AttestFacade.Handle handle) { | |
| 551 |
2
1. writeAttestation : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeAttestation : removed conditional - replaced equality check with true → NO_COVERAGE |
String toolVersion = version != null ? version : DEV_VERSION; |
| 552 |
2
1. writeAttestation : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeAttestation : removed conditional - replaced equality check with false → NO_COVERAGE |
Path target = cliConfig.attestFile() != null |
| 553 | ? cliConfig.attestFile() | |
| 554 | : Path.of(AttestFacade.DEFAULT_ATTEST_FILENAME); | |
| 555 | try { | |
| 556 | handle.write(toolVersion, target); | |
| 557 | } catch (IOException e) { | |
| 558 | if (LOG.isLoggable(Level.WARNING)) { | |
| 559 | LOG.log(Level.WARNING, | |
| 560 | "Could not write attestation manifest: {0}", e.getMessage()); | |
| 561 | } | |
| 562 | } | |
| 563 | } | |
| 564 | ||
| 565 | /** | |
| 566 | * Writes a reproducibility receipt for the just-completed scan. | |
| 567 | * | |
| 568 | * <p> | |
| 569 | * Failures are logged at WARNING level and swallowed so a receipt-write | |
| 570 | * error never turns a successful scan into a non-zero exit code. | |
| 571 | * </p> | |
| 572 | * | |
| 573 | * @param cliConfig parsed CLI configuration whose | |
| 574 | * {@link CliConfig#emitReceipt()} is already known to be | |
| 575 | * {@code true} | |
| 576 | * @param version tool version resolved from the JAR manifest; {@code null} | |
| 577 | * resolves to {@code "dev"} | |
| 578 | */ | |
| 579 | private static void emitReceipt(CliConfig cliConfig, String version) { | |
| 580 |
2
1. emitReceipt : removed conditional - replaced equality check with false → SURVIVED 2. emitReceipt : removed conditional - replaced equality check with true → KILLED |
String toolVersion = version != null ? version : DEV_VERSION; |
| 581 | String modeName = cliConfig.outputMode().name(); | |
| 582 | try { | |
| 583 | ReceiptFacade.emit(cliConfig, toolVersion, modeName); | |
| 584 | } catch (IOException e) { | |
| 585 | if (LOG.isLoggable(Level.WARNING)) { | |
| 586 | LOG.log(Level.WARNING, | |
| 587 | "Could not write reproducibility receipt: {0}", e.getMessage()); | |
| 588 | } | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | /** | |
| 593 | * Builds a composite extra sink from all active side-channel handles. | |
| 594 | * When only one handle is active its own sink is returned directly; | |
| 595 | * when multiple are active their {@code record} calls are fanned out in order. | |
| 596 | * | |
| 597 | * @param coverageHandle coverage handle; may be {@code null} | |
| 598 | * @param gapHandle gap-report handle; may be {@code null} | |
| 599 | * @param evidenceHandle evidence-report handle; may be {@code null} | |
| 600 | * @param attestHandle attestation handle; may be {@code null} | |
| 601 | * @return optional sink; empty when no feature is active | |
| 602 | */ | |
| 603 | @SuppressWarnings("PMD.NPathComplexity") | |
| 604 | private static java.util.Optional<org.egothor.methodatlas.emit.TestMethodSink> buildExtraSink( | |
| 605 | CoverageFacade.Handle coverageHandle, GapFacade.Handle gapHandle, | |
| 606 | EvidenceReportFacade.Handle evidenceHandle, AttestFacade.Handle attestHandle) { | |
| 607 | org.egothor.methodatlas.emit.TestMethodSink coverageSink = | |
| 608 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → KILLED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
coverageHandle == null ? null : coverageHandle.asSink(); |
| 609 | org.egothor.methodatlas.emit.TestMethodSink gapSink = | |
| 610 |
2
1. buildExtraSink : removed conditional - replaced equality check with true → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with false → KILLED |
gapHandle == null ? null : gapHandle.asSink(); |
| 611 | org.egothor.methodatlas.emit.TestMethodSink evidenceSink = | |
| 612 |
2
1. buildExtraSink : removed conditional - replaced equality check with true → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with false → KILLED |
evidenceHandle == null ? null : evidenceHandle.asSink(); |
| 613 | org.egothor.methodatlas.emit.TestMethodSink attestSink = | |
| 614 |
2
1. buildExtraSink : removed conditional - replaced equality check with true → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with false → KILLED |
attestHandle == null ? null : attestHandle.asSink(); |
| 615 | java.util.List<org.egothor.methodatlas.emit.TestMethodSink> active = new java.util.ArrayList<>(); | |
| 616 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → KILLED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
if (coverageSink != null) { |
| 617 | active.add(coverageSink); | |
| 618 | } | |
| 619 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
if (gapSink != null) { |
| 620 | active.add(gapSink); | |
| 621 | } | |
| 622 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
if (evidenceSink != null) { |
| 623 | active.add(evidenceSink); | |
| 624 | } | |
| 625 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
if (attestSink != null) { |
| 626 | active.add(attestSink); | |
| 627 | } | |
| 628 |
2
1. buildExtraSink : removed conditional - replaced equality check with false → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with true → KILLED |
if (active.isEmpty()) { |
| 629 | return java.util.Optional.empty(); | |
| 630 | } | |
| 631 |
2
1. buildExtraSink : removed conditional - replaced equality check with true → SURVIVED 2. buildExtraSink : removed conditional - replaced equality check with false → SURVIVED |
if (active.size() == SINGLE_SINK) { |
| 632 |
1
1. buildExtraSink : replaced return value with Optional.empty for org/egothor/methodatlas/MethodAtlasApp::buildExtraSink → KILLED |
return java.util.Optional.of(active.get(0)); |
| 633 | } | |
| 634 |
1
1. buildExtraSink : replaced return value with Optional.empty for org/egothor/methodatlas/MethodAtlasApp::buildExtraSink → NO_COVERAGE |
return java.util.Optional.of( |
| 635 | (fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion) -> { | |
| 636 | for (org.egothor.methodatlas.emit.TestMethodSink sink : active) { | |
| 637 | sink.record(fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion); | |
| 638 | } | |
| 639 | }); | |
| 640 | } | |
| 641 | ||
| 642 | private static int runWithScanRun(PrintWriter out, CliConfig cliConfig, | |
| 643 | CoverageFacade.Handle coverageHandle, GapFacade.Handle gapHandle, | |
| 644 | EvidenceReportFacade.Handle evidenceHandle, AttestFacade.Handle attestHandle) throws IOException { | |
| 645 | AiRuntimeBuilder aiRuntimeBuilder = new AiRuntimeBuilder(); | |
| 646 | ClassificationOverride override = new OverrideLoader().load(cliConfig.overrideFile()); | |
| 647 | AiResultCache aiCache = aiRuntimeBuilder.buildCache(cliConfig.aiCacheFile()); | |
| 648 | ||
| 649 | TestDiscoveryConfig discoveryConfig = | |
| 650 | new TestDiscoveryConfig(cliConfig.fileSuffixes(), cliConfig.testMarkers(), cliConfig.properties()); | |
| 651 | ||
| 652 | // One PluginLoader + one ScanOrchestrator are shared by every command in | |
| 653 | // this run; both are stateless and the providers they resolve are owned | |
| 654 | // (and closed) by the command that requested them. When -emit-coverage or | |
| 655 | // -gap-report is active the orchestrator carries the extra sink(s) as a | |
| 656 | // fan-out — every command mode sees the same fan-out automatically. | |
| 657 | PluginLoader pluginLoader = new PluginLoader(); | |
| 658 | java.util.Optional<org.egothor.methodatlas.emit.TestMethodSink> extraSink = | |
| 659 | buildExtraSink(coverageHandle, gapHandle, evidenceHandle, attestHandle); | |
| 660 |
2
1. runWithScanRun : removed conditional - replaced equality check with true → SURVIVED 2. runWithScanRun : removed conditional - replaced equality check with false → KILLED |
CommentRedaction redaction = cliConfig.aiOptions().redactComments() |
| 661 | ? new CommentRedaction(pluginLoader.loadRedactors(discoveryConfig)) | |
| 662 | : CommentRedaction.none(); | |
| 663 | ScanOrchestrator scanOrchestrator = new ScanOrchestrator(pluginLoader, extraSink, | |
| 664 | cliConfig.parallelAi(), redaction); | |
| 665 | ||
| 666 | // Manual prepare phase: write AI prompt work files; no CSV output. | |
| 667 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.manualMode() instanceof ManualMode.Prepare prepare) { |
| 668 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → KILLED |
return new ManualPrepareCommand(prepare, cliConfig, discoveryConfig, pluginLoader).execute(out); |
| 669 | } | |
| 670 | ||
| 671 | // Determine AI engine: manual consume reads from files; normal mode calls APIs. | |
| 672 | AiSuggestionEngine aiEngine; | |
| 673 |
2
1. runWithScanRun : removed conditional - replaced equality check with true → KILLED 2. runWithScanRun : removed conditional - replaced equality check with false → KILLED |
if (cliConfig.manualMode() instanceof ManualMode.Consume consume) { |
| 674 | aiEngine = new ManualConsumeEngine(consume.responseDir()); | |
| 675 | } else { | |
| 676 | aiEngine = aiRuntimeBuilder.buildEngine(cliConfig.aiOptions()); | |
| 677 | } | |
| 678 | ||
| 679 | // Apply-tags-from-csv mode: apply reviewed CSV decisions to source files. | |
| 680 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.applyTagsFromCsvFile() != null) { |
| 681 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → KILLED |
return new ApplyTagsFromCsvCommand(cliConfig, discoveryConfig, pluginLoader).execute(out); |
| 682 | } | |
| 683 | ||
| 684 | // Apply-tags mode: annotate source files; no report emitted. | |
| 685 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.applyTags()) { |
| 686 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → SURVIVED |
return new ApplyTagsCommand(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 687 | pluginLoader, scanOrchestrator).execute(out); | |
| 688 | } | |
| 689 | ||
| 690 | // Evidence-pack mode: produces a tamper-evident self-contained directory | |
| 691 | // (SARIF + CSV + manifest + optional ZeroEcho signature). Must precede the | |
| 692 | // SARIF/JSON/GitHub-annotation branches because those formats are | |
| 693 | // re-used internally; the dispatch decision is owned by this command. | |
| 694 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → SURVIVED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.evidencePackFramework() != null) { |
| 695 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → NO_COVERAGE |
return dispatchEvidencePack(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 696 | scanOrchestrator); | |
| 697 | } | |
| 698 | ||
| 699 | // SARIF mode: buffer all records; write JSON once after the scan completes. | |
| 700 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.outputMode() == OutputMode.SARIF) { |
| 701 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → SURVIVED |
return new SarifCommand(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 702 | scanOrchestrator).execute(out); | |
| 703 | } | |
| 704 | ||
| 705 | // JSON mode: buffer all records; write flat JSON array after scan completes. | |
| 706 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.outputMode() == OutputMode.JSON) { |
| 707 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → SURVIVED |
return new JsonCommand(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 708 | pluginLoader, scanOrchestrator).execute(out); | |
| 709 | } | |
| 710 | ||
| 711 | // GitHub Annotations mode: emit ::notice/::warning workflow commands. | |
| 712 |
2
1. runWithScanRun : removed conditional - replaced equality check with false → KILLED 2. runWithScanRun : removed conditional - replaced equality check with true → KILLED |
if (cliConfig.outputMode() == OutputMode.GITHUB_ANNOTATIONS) { |
| 713 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → SURVIVED |
return new GitHubAnnotationsCommand(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 714 | scanOrchestrator).execute(out); | |
| 715 | } | |
| 716 | ||
| 717 | // CSV / PLAIN mode: emit incrementally (default). | |
| 718 |
1
1. runWithScanRun : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::runWithScanRun → KILLED |
return new ScanCommand(cliConfig, discoveryConfig, aiEngine, override, aiCache, |
| 719 | pluginLoader, scanOrchestrator).execute(out); | |
| 720 | } | |
| 721 | ||
| 722 | /** | |
| 723 | * Parses the {@code -evidence-pack} framework token, assembles | |
| 724 | * {@link EvidencePackOptions}, runs the {@link EvidencePackCommand}, and | |
| 725 | * prints a one-line summary to {@code stderr}. The signing key is read from | |
| 726 | * a ZeroEcho keyring file (a plaintext {@code KeyringStore}); ZeroEcho | |
| 727 | * keyrings are not password-protected, so no password is collected. | |
| 728 | * | |
| 729 | * @param cliConfig parsed CLI configuration | |
| 730 | * @param discoveryConfig discovery configuration forwarded to providers | |
| 731 | * @param aiEngine AI engine, or {@code null} | |
| 732 | * @param override classification override | |
| 733 | * @param aiCache AI result cache | |
| 734 | * @param scanOrchestrator shared scan orchestrator | |
| 735 | * @return exit code propagated from {@link EvidencePackCommand#execute()} | |
| 736 | * or {@value #EXIT_BAD_FRAMEWORK} when the framework token is invalid | |
| 737 | * @throws IOException if the underlying scan or I/O fails | |
| 738 | */ | |
| 739 | private static int dispatchEvidencePack(CliConfig cliConfig, TestDiscoveryConfig discoveryConfig, | |
| 740 | AiSuggestionEngine aiEngine, ClassificationOverride override, AiResultCache aiCache, | |
| 741 | ScanOrchestrator scanOrchestrator) throws IOException { | |
| 742 | EvidenceFramework framework; | |
| 743 | try { | |
| 744 | framework = EvidenceFramework.parse(cliConfig.evidencePackFramework()); | |
| 745 | } catch (IllegalArgumentException e) { | |
| 746 | System.err.println(e.getMessage()); | |
| 747 |
1
1. dispatchEvidencePack : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::dispatchEvidencePack → NO_COVERAGE |
return EXIT_BAD_FRAMEWORK; |
| 748 | } | |
| 749 | EvidencePackOptions packOptions = new EvidencePackOptions( | |
| 750 | framework, | |
| 751 | cliConfig.evidencePackDir(), | |
| 752 | cliConfig.evidencePackOverwrite(), | |
| 753 | cliConfig.evidencePackKeyringFile(), | |
| 754 | cliConfig.evidencePackKeyringEnv(), | |
| 755 | cliConfig.evidencePackKeyAlias(), | |
| 756 | cliConfig.evidencePackSignAlgo()); | |
| 757 | EvidencePackCommand command = new EvidencePackCommand(cliConfig, packOptions, discoveryConfig, | |
| 758 | aiEngine, override, aiCache, scanOrchestrator); | |
| 759 | int exit = command.execute(); | |
| 760 | announcePack(command, packOptions); | |
| 761 |
1
1. dispatchEvidencePack : replaced int return with 0 for org/egothor/methodatlas/MethodAtlasApp::dispatchEvidencePack → NO_COVERAGE |
return exit; |
| 762 | } | |
| 763 | ||
| 764 | /** | |
| 765 | * Prints the one-line outcome banner that documents how the pack was | |
| 766 | * produced. Always written to {@code System.err} so the stdout stream | |
| 767 | * remains usable for piping. | |
| 768 | * | |
| 769 | * @param command executed command, used to retrieve the absolute path | |
| 770 | * @param packOptions options driving the command | |
| 771 | */ | |
| 772 | private static void announcePack(EvidencePackCommand command, EvidencePackOptions packOptions) { | |
| 773 | String absolute = command.outputDir().toString(); | |
| 774 |
4
1. announcePack : removed conditional - replaced equality check with false → NO_COVERAGE 2. announcePack : removed conditional - replaced equality check with true → NO_COVERAGE 3. announcePack : removed conditional - replaced equality check with false → NO_COVERAGE 4. announcePack : removed conditional - replaced equality check with true → NO_COVERAGE |
if (packOptions.keyringFile() == null && packOptions.keyringEnv() == null) { |
| 775 | System.err.println("evidence pack written to " + absolute | |
| 776 | + " (unsigned — no keyring supplied)"); | |
| 777 | return; | |
| 778 | } | |
| 779 |
2
1. announcePack : removed conditional - replaced equality check with true → NO_COVERAGE 2. announcePack : removed conditional - replaced equality check with false → NO_COVERAGE |
String algo = packOptions.signatureAlgorithm() != null |
| 780 | ? packOptions.signatureAlgorithm() : "Ed25519 (from keyring)"; | |
| 781 | Path signed = command.outputDir().resolve("manifest.sha256.signed"); | |
| 782 |
2
1. announcePack : removed conditional - replaced equality check with true → NO_COVERAGE 2. announcePack : removed conditional - replaced equality check with false → NO_COVERAGE |
if (Files.exists(signed)) { |
| 783 | System.err.println("evidence pack written to " + absolute | |
| 784 | + " (signed: " + algo + ")"); | |
| 785 | } else { | |
| 786 | System.err.println("evidence pack written to " + absolute | |
| 787 | + " (WARNING: signing failed — check log)"); | |
| 788 | } | |
| 789 | } | |
| 790 | } | |
Mutations | ||
| 258 |
1.1 2.2 |
|
| 262 |
1.1 2.2 |
|
| 293 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 302 |
1.1 2.2 3.3 |
|
| 303 |
1.1 2.2 |
|
| 304 |
1.1 2.2 3.3 4.4 |
|
| 308 |
1.1 |
|
| 310 |
1.1 2.2 |
|
| 311 |
1.1 |
|
| 313 |
1.1 2.2 |
|
| 314 |
1.1 |
|
| 316 |
1.1 2.2 |
|
| 317 |
1.1 |
|
| 322 |
1.1 2.2 |
|
| 325 |
1.1 |
|
| 335 |
1.1 |
|
| 356 |
1.1 |
|
| 361 |
1.1 |
|
| 381 |
1.1 2.2 |
|
| 384 |
1.1 2.2 |
|
| 387 |
1.1 2.2 |
|
| 390 |
1.1 2.2 |
|
| 393 |
1.1 2.2 |
|
| 408 |
1.1 |
|
| 409 |
1.1 |
|
| 412 |
1.1 2.2 3.3 |
|
| 413 |
1.1 2.2 |
|
| 415 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 416 |
1.1 |
|
| 419 |
1.1 |
|
| 432 |
1.1 2.2 |
|
| 435 |
1.1 |
|
| 448 |
1.1 2.2 |
|
| 449 |
1.1 2.2 |
|
| 469 |
1.1 2.2 |
|
| 472 |
1.1 |
|
| 485 |
1.1 2.2 |
|
| 486 |
1.1 2.2 |
|
| 506 |
1.1 2.2 |
|
| 509 |
1.1 |
|
| 519 |
1.1 2.2 |
|
| 522 |
1.1 |
|
| 535 |
1.1 2.2 |
|
| 536 |
1.1 2.2 |
|
| 551 |
1.1 2.2 |
|
| 552 |
1.1 2.2 |
|
| 580 |
1.1 2.2 |
|
| 608 |
1.1 2.2 |
|
| 610 |
1.1 2.2 |
|
| 612 |
1.1 2.2 |
|
| 614 |
1.1 2.2 |
|
| 616 |
1.1 2.2 |
|
| 619 |
1.1 2.2 |
|
| 622 |
1.1 2.2 |
|
| 625 |
1.1 2.2 |
|
| 628 |
1.1 2.2 |
|
| 631 |
1.1 2.2 |
|
| 632 |
1.1 |
|
| 634 |
1.1 |
|
| 660 |
1.1 2.2 |
|
| 667 |
1.1 2.2 |
|
| 668 |
1.1 |
|
| 673 |
1.1 2.2 |
|
| 680 |
1.1 2.2 |
|
| 681 |
1.1 |
|
| 685 |
1.1 2.2 |
|
| 686 |
1.1 |
|
| 694 |
1.1 2.2 |
|
| 695 |
1.1 |
|
| 700 |
1.1 2.2 |
|
| 701 |
1.1 |
|
| 706 |
1.1 2.2 |
|
| 707 |
1.1 |
|
| 712 |
1.1 2.2 |
|
| 713 |
1.1 |
|
| 718 |
1.1 |
|
| 747 |
1.1 |
|
| 761 |
1.1 |
|
| 774 |
1.1 2.2 3.3 4.4 |
|
| 779 |
1.1 2.2 |
|
| 782 |
1.1 2.2 |