AttestFacade.java

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.attest;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;

import org.egothor.methodatlas.emit.TestMethodSink;

/**
 * Single public entry point used by {@code MethodAtlasApp} to drive
 * {@code -attest}.
 *
 * <p>
 * The package's data records, collector, and writer are all package-private so
 * they remain implementation details. The facade exposes only the operations
 * the CLI needs:
 * </p>
 * <ol>
 *   <li>prepare a collector (returning a handle that must be threaded through
 *       the orchestrator as an extra sink);</li>
 *   <li>after the scan, ask the same handle to write the attestation
 *       manifest.</li>
 * </ol>
 *
 * <p>
 * The manifest is a compact JSON file listing every AI-classified
 * security-relevant test method, the commit SHA (resolved from CI environment
 * variables), and optional content hashes. It is designed to be signed
 * externally with {@code cosign sign-blob} or GPG and attached to a release as
 * a tamper-evident record of which security tests existed at that point.
 * </p>
 */
public final class AttestFacade {

    /**
     * Default filename used when {@code -attest-file} is not supplied.
     */
    public static final String DEFAULT_ATTEST_FILENAME = "security-tests.attestation.json";

    private AttestFacade() {
        // Utility class.
    }

    /**
     * Prepares an attestation collector.
     *
     * @param minConfidence AI minimum-confidence threshold; AI-only evidence below
     *                      this value is excluded from the manifest
     * @param scanRoots     scan root paths written to the manifest header
     * @return handle ready for use by the orchestrator and writer
     */
    public static Handle prepare(double minConfidence, List<Path> scanRoots) {
        List<String> rootStrings = scanRoots.stream()
                .map(Path::toString)
                .collect(Collectors.toUnmodifiableList());
        AttestCollector collector = new AttestCollector(minConfidence, rootStrings);
        return new Handle(collector);
    }

    /**
     * Opaque handle returned by {@link #prepare(double, List)}. Carries the
     * collector through the scan and yields the manifest when the scan is done.
     */
    public static final class Handle {

        /** Backing collector — owned by this handle, never exposed directly. */
        private final AttestCollector collector;

        private Handle(AttestCollector collector) {
            this.collector = collector;
        }

        /**
         * Returns the {@link TestMethodSink} that the orchestrator must receive
         * every per-method record on.
         *
         * @return collector as a sink
         */
        public TestMethodSink asSink() {
            return collector;
        }

        /**
         * Builds the attestation manifest and writes it to {@code outputFile}.
         *
         * @param toolVersion resolved tool version string
         * @param outputFile  destination path
         * @throws IOException if the file cannot be written
         */
        public void write(String toolVersion, Path outputFile) throws IOException {
            AttestReport report = collector.buildReport(toolVersion);
            AttestWriter.write(report, outputFile);
        }
    }
}