TagAiDrift.java

package org.egothor.methodatlas;

import java.util.List;
import java.util.Locale;

import org.egothor.methodatlas.ai.AiMethodSuggestion;

/**
 * Describes the agreement between a source-level {@code @Tag("security")} annotation
 * and the AI classification of the same test method.
 *
 * <p>Both the static annotation and the AI judgment are independent sources of truth.
 * When they disagree the discrepancy is called <em>drift</em>:</p>
 *
 * <ul>
 *   <li>{@link #TAG_ONLY} — the method carries {@code @Tag("security")} in source but
 *       the AI considers it non-security-relevant. The annotation may be stale,
 *       inaccurate, or applied to the wrong method. Tag-based CI gates and audit
 *       dashboards over-count security coverage.</li>
 *   <li>{@link #AI_ONLY} — the AI classifies the method as security-relevant but no
 *       {@code @Tag("security")} is present in source. Coverage dashboards and
 *       tag-based CI gates silently miss this test.</li>
 *   <li>{@link #NONE} — both sources agree; no action needed.</li>
 * </ul>
 *
 * <p>Drift detection requires an active AI classification. When no
 * {@link AiMethodSuggestion} is available (AI disabled, class too large, etc.)
 * {@link #compute} returns {@code null}.</p>
 *
 * @see MethodAtlasApp
 * @see org.egothor.methodatlas.emit.OutputEmitter
 * @see org.egothor.methodatlas.emit.SarifEmitter
 */
public enum TagAiDrift {

    /** Both sources agree — either both say security-relevant or neither does. */
    NONE,

    /**
     * Source code carries {@code @Tag("security")} but the AI disagrees.
     * The annotation may be stale, inaccurate, or applied to the wrong method.
     */
    TAG_ONLY,

    /**
     * AI classifies the method as security-relevant but no {@code @Tag("security")}
     * is present in source. Coverage dashboards and tag-based CI gates will miss it.
     */
    AI_ONLY;

    private static final String SECURITY_TAG_VALUE = "security";

    /**
     * Computes the drift between source-level security tags and the AI classification.
     *
     * @param sourceTags JUnit {@code @Tag} values extracted from the method
     * @param suggestion AI classification for the method, or {@code null} when AI
     *                   is disabled or unavailable
     * @return computed drift value, or {@code null} when {@code suggestion} is
     *         {@code null} (drift cannot be determined without AI classification)
     */
    public static TagAiDrift compute(List<String> sourceTags, AiMethodSuggestion suggestion) {
        if (suggestion == null) {
            return null;
        }
        boolean hasTag = sourceTags.stream()
                .anyMatch(SECURITY_TAG_VALUE::equalsIgnoreCase);
        boolean aiSaysSecure = suggestion.securityRelevant();
        if (hasTag == aiSaysSecure) {
            return NONE;
        }
        return hasTag ? TAG_ONLY : AI_ONLY;
    }

    /**
     * Returns the lowercase hyphenated string representation used in CSV and SARIF output.
     *
     * @return {@code "none"}, {@code "tag-only"}, or {@code "ai-only"}
     */
    public String toValue() {
        return name().toLowerCase(Locale.ROOT).replace('_', '-');
    }
}