| 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | // Copyright 2026 Egothor | |
| 3 | // Copyright 2026 Accenture | |
| 4 | package org.egothor.methodatlas; | |
| 5 | ||
| 6 | import java.security.MessageDigest; | |
| 7 | import java.security.NoSuchAlgorithmException; | |
| 8 | import java.time.Instant; | |
| 9 | import java.util.HexFormat; | |
| 10 | import java.util.Objects; | |
| 11 | import java.util.UUID; | |
| 12 | ||
| 13 | /** | |
| 14 | * Immutable record identifying one CLI invocation of MethodAtlas. | |
| 15 | * | |
| 16 | * <p> | |
| 17 | * Created once at the top of {@link MethodAtlasApp#run(String[], java.io.PrintWriter)} | |
| 18 | * and propagated through {@link ScanRunContext} so that every log record | |
| 19 | * emitted during the run can carry the correlation id. This makes it | |
| 20 | * possible to trace a single audit-trail artefact, a single SARIF file, or a | |
| 21 | * single CI log slice back to the exact run that produced it. | |
| 22 | * </p> | |
| 23 | * | |
| 24 | * <h2>Fields</h2> | |
| 25 | * | |
| 26 | * <ul> | |
| 27 | * <li>{@code runId} — a short hex-encoded random id (16 hex characters); | |
| 28 | * intentionally not a full UUID because the id appears in log lines | |
| 29 | * and CSV preamble where 36-character UUIDs are noisy.</li> | |
| 30 | * <li>{@code startedAt} — wall-clock timestamp at run construction.</li> | |
| 31 | * <li>{@code toolVersion} — implementation version reported by the | |
| 32 | * {@code methodatlas} JAR manifest; falls back to {@code "dev"} for | |
| 33 | * developer builds.</li> | |
| 34 | * <li>{@code configFingerprint} — SHA-256 of a stable textual rendering of | |
| 35 | * the parsed {@link CliConfig}. Lets two runs of the same configuration | |
| 36 | * be correlated across time and machine boundaries.</li> | |
| 37 | * </ul> | |
| 38 | * | |
| 39 | * @param runId short hex correlation id, non-empty | |
| 40 | * @param startedAt wall-clock time the run was started | |
| 41 | * @param toolVersion implementation version or {@code "dev"} | |
| 42 | * @param configFingerprint SHA-256 hex digest of the canonical configuration | |
| 43 | * text; never {@code null}, never empty | |
| 44 | * @since 1.0.0 | |
| 45 | */ | |
| 46 | public record ScanRun(String runId, Instant startedAt, String toolVersion, String configFingerprint) { | |
| 47 | ||
| 48 | /** | |
| 49 | * Compact constructor enforcing the documented invariants on the | |
| 50 | * component values. | |
| 51 | */ | |
| 52 | public ScanRun { | |
| 53 | Objects.requireNonNull(runId, "runId"); | |
| 54 | Objects.requireNonNull(startedAt, "startedAt"); | |
| 55 | Objects.requireNonNull(toolVersion, "toolVersion"); | |
| 56 | Objects.requireNonNull(configFingerprint, "configFingerprint"); | |
| 57 | if (runId.isBlank()) { | |
| 58 | throw new IllegalArgumentException("runId must not be blank"); | |
| 59 | } | |
| 60 | if (configFingerprint.isBlank()) { | |
| 61 | throw new IllegalArgumentException("configFingerprint must not be blank"); | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Creates a new {@code ScanRun} with a freshly generated correlation id | |
| 67 | * and the current wall-clock timestamp. | |
| 68 | * | |
| 69 | * <p> | |
| 70 | * The {@code configFingerprint} is the SHA-256 of {@code configText}, a | |
| 71 | * stable textual rendering of the parsed CLI configuration. Two runs | |
| 72 | * with byte-identical configuration produce the same fingerprint, | |
| 73 | * letting operators correlate scans without sharing the full config. | |
| 74 | * </p> | |
| 75 | * | |
| 76 | * @param toolVersion implementation version, or {@code "dev"} for | |
| 77 | * builds without a manifest version | |
| 78 | * @param configText canonical textual rendering of the parsed | |
| 79 | * {@link CliConfig}; must not be {@code null} | |
| 80 | * @return a fresh scan-run identifier | |
| 81 | */ | |
| 82 | public static ScanRun create(String toolVersion, String configText) { | |
| 83 | String runId = UUID.randomUUID().toString().replace("-", "").substring(0, 16); | |
| 84 |
1
1. create : replaced return value with null for org/egothor/methodatlas/ScanRun::create → KILLED |
return new ScanRun(runId, Instant.now(), |
| 85 |
4
1. create : removed conditional - replaced equality check with true → SURVIVED 2. create : removed conditional - replaced equality check with false → SURVIVED 3. create : removed conditional - replaced equality check with false → KILLED 4. create : removed conditional - replaced equality check with true → KILLED |
toolVersion == null || toolVersion.isBlank() ? "dev" : toolVersion, |
| 86 | sha256(configText)); | |
| 87 | } | |
| 88 | ||
| 89 | private static String sha256(String value) { | |
| 90 | try { | |
| 91 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); | |
| 92 |
1
1. sha256 : replaced return value with "" for org/egothor/methodatlas/ScanRun::sha256 → KILLED |
return HexFormat.of().formatHex(digest.digest( |
| 93 |
2
1. sha256 : removed conditional - replaced equality check with false → SURVIVED 2. sha256 : removed conditional - replaced equality check with true → KILLED |
value == null ? new byte[0] : value.getBytes(java.nio.charset.StandardCharsets.UTF_8))); |
| 94 | } catch (NoSuchAlgorithmException e) { | |
| 95 | throw new IllegalStateException("SHA-256 not available", e); | |
| 96 | } | |
| 97 | } | |
| 98 | } | |
Mutations | ||
| 84 |
1.1 |
|
| 85 |
1.1 2.2 3.3 4.4 |
|
| 92 |
1.1 |
|
| 93 |
1.1 2.2 |