WorkingSetReportTable.java

package network.ike.plugin.ws;

import network.ike.plugin.support.GoalReportBuilder;
import network.ike.workspace.WorkingSet;

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

/**
 * Renders the shared working-set report table — one row per
 * {@link WorkingSet.Member}, the aggregator included — framing a goal's
 * output as <em>its effect on the working set</em> (#766, under epic #764).
 *
 * <p>Columns are {@code [Member · Kind · Version · Branch · SHA · Effect]}.
 * Because the aggregator (workspace root) is a first-class member, it is
 * always a row, so the staleness a subproject-only table hid — the root left
 * on {@code 1-<feature>-SNAPSHOT} (#763) — is visible. The {@code Effect}
 * column states what the goal did or will do to that member: a
 * <em>planned</em> effect for a {@code -draft} goal, an <em>applied</em>
 * effect for {@code -publish} (e.g. {@code tagged + pushed},
 * {@code version-stripped → 1-SNAPSHOT}, {@code skipped (no-op)}).
 *
 * <p>Content only: this builds Markdown through {@link GoalReportBuilder};
 * {@code WorkspaceReport.write()} still owns the frame.
 */
public final class WorkingSetReportTable {

    /** The fixed column headers, in order. */
    public static final List<String> HEADERS =
            List.of("Member", "Kind", "Version", "Branch", "SHA", "Effect");

    /**
     * SHA cell for the checkpoint manifest's self-pin: the aggregator commit
     * that records the set cannot cite its own not-yet-made SHA.
     */
    public static final String SELF_COMMIT = "n/a — this commit";

    /** Placeholder for an absent or not-applicable cell. */
    public static final String NONE = "—";

    private WorkingSetReportTable() {}

    /**
     * One working-set member's row of report data.
     *
     * @param member  the working-set member — carries its name and
     *                {@link WorkingSet.Member.Kind kind}
     * @param version the member's version, or {@code null}/blank for none
     * @param branch  the member's branch, or {@code null}/blank for none
     * @param sha     the member's short HEAD SHA, {@link #SELF_COMMIT} for the
     *                checkpoint self-pin, or {@code null}/blank for none
     * @param effect  what the goal did or will do to this member
     */
    public record Row(WorkingSet.Member member, String version, String branch,
                      String sha, String effect) {}

    /**
     * Render {@code rows} as the working-set table within a section, with the
     * default {@code Effect} final column (for a mutating goal).
     *
     * @param report  the report builder to append to
     * @param section the section title (e.g. {@code "Working set"})
     * @param rows    one row per member, aggregator included
     * @return {@code report}, for chaining
     */
    public static GoalReportBuilder render(GoalReportBuilder report,
                                           String section, List<Row> rows) {
        return renderWithHeaders(report, section, HEADERS, rows);
    }

    /**
     * Render {@code rows} as the working-set table, naming the final column —
     * {@code "Effect"} for a mutating goal, {@code "Status"} for a read-only
     * goal (e.g. {@code overview}, {@code release-status}). The aggregator is
     * included as a row either way.
     *
     * @param report     the report builder to append to
     * @param section    the section title (e.g. {@code "Working set"})
     * @param lastColumn the header for the final column
     * @param rows       one row per member, aggregator included
     * @return {@code report}, for chaining
     */
    public static GoalReportBuilder render(GoalReportBuilder report,
                                           String section, String lastColumn,
                                           List<Row> rows) {
        List<String> headers = List.of("Member", "Kind", "Version", "Branch",
                "SHA", lastColumn);
        return renderWithHeaders(report, section, headers, rows);
    }

    private static GoalReportBuilder renderWithHeaders(GoalReportBuilder report,
                                                       String section,
                                                       List<String> headers,
                                                       List<Row> rows) {
        List<String[]> tableRows = new ArrayList<>();
        for (Row row : rows) {
            tableRows.add(new String[]{
                    row.member().name(),
                    kindLabel(row.member().kind()),
                    orNone(row.version()),
                    orNone(row.branch()),
                    shaCell(row.sha()),
                    orNone(row.effect())
            });
        }
        return report.section(section).table(headers, tableRows);
    }

    /**
     * The lowercase label for a member kind, as it appears in the Kind column.
     *
     * @param kind the member kind
     * @return {@code "aggregator"} or {@code "subproject"}
     */
    static String kindLabel(WorkingSet.Member.Kind kind) {
        return kind == WorkingSet.Member.Kind.AGGREGATOR
                ? "aggregator" : "subproject";
    }

    private static String shaCell(String sha) {
        if (sha == null || sha.isBlank()) {
            return NONE;
        }
        if (sha.equals(SELF_COMMIT) || sha.equals(NONE)) {
            return sha;
        }
        return "`" + sha + "`";
    }

    private static String orNone(String value) {
        return (value == null || value.isBlank()) ? NONE : value;
    }
}