ComplianceFindingsCollector.java

package com.cloudforgeci.api.core.rules;

import java.util.ArrayList;
import java.util.List;

/**
 * Thread-local side channel for framework-rule ({@link com.cloudforge.core.interfaces.FrameworkRules#install})
 * ADVISORY-mode findings -- see {@link NagReportReader} for cdk-nag's own equivalent, file-based
 * mechanism.
 *
 * <p>Every {@code FrameworkRules} implementation's {@code Node.addValidation} callback returns an
 * empty list in {@code ComplianceMode.ADVISORY} specifically so {@code app.synth()} doesn't throw
 * -- that's the whole point of advisory mode not blocking synthesis. But an empty return also
 * means the violation detail (until now, only ever written to a {@code Logger.warning} call) never
 * reaches any caller as data. A caller that wants to actually show those findings to a user (an
 * on-demand advisory compliance check, not a real deploy) has no other way to see them, since the
 * {@code Node.addValidation} return value is the only channel back out of the callback and it
 * has to stay empty.</p>
 *
 * <p>A plain {@link ThreadLocal} is safe here specifically because {@code CloudForgeSynthesizer
 * #synthesize} already serializes every synth call behind a single static lock (see that class's
 * own javadoc for why) -- no two synth calls ever run concurrently in the same JVM, regardless of
 * which thread invokes them, so there's no risk of one synth's findings leaking into another's.</p>
 */
public final class ComplianceFindingsCollector {

    private static final ThreadLocal<List<NagReportReader.ComplianceFinding>> CURRENT = new ThreadLocal<>();

    private ComplianceFindingsCollector() {
    }

    /** Call before {@code app.synth()} to start collecting on this thread. A synth run that never
     *  calls this (every normal deploy today) leaves {@link #record} a no-op, so this is fully
     *  opt-in and changes nothing for existing callers. */
    public static void start() {
        CURRENT.set(new ArrayList<>());
    }

    /** Called from a {@code FrameworkRules} implementation's ADVISORY branch, alongside (not
     *  instead of) its existing {@code LOG.warning} calls. */
    public static void record(List<ComplianceRule> failedRules) {
        List<NagReportReader.ComplianceFinding> list = CURRENT.get();
        if (list == null || failedRules == null) {
            return;
        }
        for (ComplianceRule rule : failedRules) {
            list.add(new NagReportReader.ComplianceFinding(
                rule.ruleId(),
                "WARNING",
                null,
                rule.description() + rule.errorMessage().map(msg -> " - " + msg).orElse("")));
        }
    }

    /** Stops collecting and returns everything gathered since the matching {@link #start()} --
     *  always pair this with a {@code finally} block, or a later synth on the same thread would
     *  otherwise keep appending to a stale, already-returned list indefinitely. */
    public static List<NagReportReader.ComplianceFinding> drain() {
        List<NagReportReader.ComplianceFinding> list = CURRENT.get();
        CURRENT.remove();
        return list == null ? List.of() : List.copyOf(list);
    }
}