AttestCollector.java
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.attest;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import org.egothor.methodatlas.ai.AiMethodSuggestion;
import org.egothor.methodatlas.emit.TestMethodSink;
/**
* Streaming sink that builds an {@link AttestReport} from scan records observed
* during a single run.
*
* <p>
* The collector implements {@link TestMethodSink} so the orchestration layer
* can hand it the same record stream that drives CSV/SARIF emitters, without a
* second scan and without coupling the emitters to attestation logic.
* </p>
*
* <p>
* Only AI-classified, security-relevant methods whose confidence meets the
* configured threshold are included in the manifest. All other methods are
* counted toward {@code totalMethods} but not listed.
* </p>
*
* <p>
* Package-private because nothing outside the {@code attest} package needs to
* construct one; {@link AttestFacade} is the sole external entry point.
* </p>
*/
final class AttestCollector implements TestMethodSink {
/** Schema version of the produced manifest. */
private static final String SCHEMA_VERSION = "1";
/** Minimum AI confidence required to include a method in the manifest. */
private final double minConfidence;
/** Scan root paths forwarded to the report header. */
private final List<String> scanRoots;
/** Running count of all methods seen (security-relevant or not). */
private final AtomicInteger totalSeen = new AtomicInteger();
/** Accumulated security-relevant entries in encounter order. Thread-safe for parallel-AI mode. */
private final List<AttestEntry> entries = new CopyOnWriteArrayList<>();
/**
* Creates a collector for the attestation manifest.
*
* @param minConfidence minimum AI confidence threshold; values below this are
* not included in the manifest
* @param scanRoots scan root paths shown in the report header
*/
/* default */ AttestCollector(double minConfidence, List<String> scanRoots) {
this.minConfidence = minConfidence;
this.scanRoots = List.copyOf(scanRoots);
}
/**
* Records a test method for potential inclusion in the attestation manifest.
*
* @param fqcn fully qualified class name
* @param method test method name
* @param beginLine ignored
* @param loc ignored
* @param contentHash SHA-256 fingerprint of the enclosing class source, or
* {@code null} when {@code -content-hash} was not used
* @param tags ignored (attestation relies on AI classification only)
* @param displayName ignored
* @param suggestion optional AI classification
*/
@Override
@SuppressWarnings("PMD.UseObjectForClearerAPI")
public void record(String fqcn, String method, int beginLine, int loc, String contentHash,
List<String> tags, String displayName, AiMethodSuggestion suggestion) {
totalSeen.incrementAndGet();
if (suggestion == null || !suggestion.securityRelevant()) {
return;
}
if (suggestion.confidence() < minConfidence) {
return;
}
List<String> aiTags = suggestion.tags() != null
? List.copyOf(suggestion.tags()) : List.of();
entries.add(new AttestEntry(fqcn, method, contentHash, aiTags));
}
/**
* Builds the final {@link AttestReport}.
*
* @param toolVersion resolved tool version string; never {@code null}
* @return populated attestation manifest
*/
/* default */ AttestReport buildReport(String toolVersion) {
String commitSha = CommitShaResolver.resolve();
List<AttestEntry> snapshot = Collections.unmodifiableList(new ArrayList<>(entries));
return new AttestReport(
SCHEMA_VERSION,
Instant.now().toString(),
toolVersion,
commitSha,
scanRoots,
totalSeen.get(),
snapshot.size(),
snapshot);
}
}