Prechádzať zdrojové kódy

r1: ONE goal_branches parser — GoalService.parse_goal_branches (typed repo/branch/pushed rows, the array shape the daemon emits, strict boolean push flags) feeds BOTH the completion-accept gate and the goal page's chips; the gate's objects/arrays-are-TRUE truthiness and the chips' objects/arrays-are-FALSE copy die together with the legacy object form the daemon never sent, killing the latent gate-vs-chips divergence (arch item behind the F9 gate).

clanker 1 mesiac pred
rodič
commit
8d254a3426

+ 79 - 50
src/Services/GoalService.vala

@@ -36,6 +36,7 @@ namespace ClankerSaddle.Services {
     public const string TASK_KIND_TASK = "task";
     public const string TASK_KIND_GOAL_RUN = "goal_run";
 
+
     /** Task creators: enqueueing is creator-only. */
     public const string CREATOR_HUMAN = "human";
     public const string CREATOR_GOAL = "goal";
@@ -1075,9 +1076,11 @@ namespace ClankerSaddle.Services {
          * F9): the accept REFUSES while any daemon-reported goal
          * branch is unpushed — push-after-each-merge is enforced at
          * the operator's confirmation, read from the LIVE per-saddle
-         * probe flags (never a stored snapshot). Unknown flags (an
-         * older daemon that never reported goal_branches) leave the
-         * daemon-side push rule as the enforcing layer.
+         * probe flags (never a stored snapshot) through the ONE
+         * {@link parse_goal_branches} parser (the same rows the goal
+         * page's chips render). Unknown flags (an older daemon that
+         * never reported goal_branches) leave the daemon-side push
+         * rule as the enforcing layer.
          */
         private async void refuse_unpushed_origin(Goal goal, TaskEntity run) throws Error {
             int64 saddle_id = 0;
@@ -1101,38 +1104,10 @@ namespace ClankerSaddle.Services {
                 return;
             }
             var unpushed = new Vector<string>();
-            try {
-                var node = Json.from_string(raw);
-                if (node.get_node_type() == Json.NodeType.ARRAY) {
-                    foreach (var element in node.get_array().get_elements()) {
-                        if (element.get_node_type() != Json.NodeType.OBJECT) {
-                            continue;
-                        }
-                        var obj = element.get_object();
-                        string branch = obj.has_member("branch")
-                            ? (obj.get_string_member("branch") ?? "") : "";
-                        string repo = obj.has_member("repo")
-                            ? (obj.get_string_member("repo") ?? "") : "";
-                        string label = repo.length > 0 && branch.length > 0
-                            ? @"$(repo): $(branch)"
-                            : (branch.length > 0 ? branch : repo);
-                        bool pushed = obj.has_member("pushed")
-                            ? goal_node_truthy(obj.get_member("pushed")) : false;
-                        if (label.length > 0 && !pushed) {
-                            unpushed.add(label);
-                        }
-                    }
-                } else if (node.get_node_type() == Json.NodeType.OBJECT) {
-                    foreach (var member in node.get_object().get_members()) {
-                        var value = node.get_object().get_member(member);
-                        if (value.get_node_type() == Json.NodeType.VALUE
-                                && !goal_node_truthy(value)) {
-                            unpushed.add(member);
-                        }
-                    }
+            foreach (var row in parse_goal_branches(raw)) {
+                if (!row.pushed) {
+                    unpushed.add(row.label());
                 }
-            } catch (Error parse_error) {
-                return;
             }
             if (unpushed.length > 0) {
                 var labels = new StringBuilder();
@@ -1147,27 +1122,81 @@ namespace ClankerSaddle.Services {
             }
         }
 
-        /** A JSON value as a boolean truth (objects/arrays: present). */
-        private static bool goal_node_truthy(Json.Node node) {
-            if (node.get_node_type() == Json.NodeType.VALUE) {
-                var value = node.get_value();
-                if (value.type() == typeof(bool)) {
-                    return ((!)value).get_boolean();
-                }
-                if (value.type() == typeof(string)) {
-                    return ((!)value).dup_string() != "false";
+        // ------------------------------------------------------------------
+        // Helpers
+        // ------------------------------------------------------------------
+
+        /**
+         * One daemon-reported goal-branch push flag: a row of the
+         * /status `goal_branches` array (the ONLY shape the daemon
+         * emits — one `{repo, branch, pushed}` row per involved
+         * repo).
+         */
+        public class GoalBranchPush : Object {
+            public string repo { get; set; default = ""; }
+            public string branch { get; set; default = ""; }
+            public bool pushed { get; set; }
+
+            /** The display/gate label: "repo: branch" when both are known. */
+            public string label() {
+                return repo.length > 0 && branch.length > 0
+                    ? @"$(repo): $(branch)" : (branch.length > 0 ? branch : repo);
+            }
+        }
+
+        /**
+         * The ONE parser of the daemon's goal_branches report (plan
+         * 15 C4): an array of `{repo, branch, pushed}` rows, read by
+         * BOTH consumers — the completion-accept gate
+         * ({@link refuse_unpushed_origin}) and the goal page's push
+         * chips — so the two can never diverge on the same payload
+         * again. `pushed` is the daemon's boolean (an integer counts
+         * as non-zero; anything else reads unpushed). A row missing
+         * its branch labels by its repo alone; label-less rows drop.
+         * Unparseable input parses as no rows.
+         */
+        public static Vector<GoalBranchPush> parse_goal_branches(string raw) {
+            var rows = new Vector<GoalBranchPush>();
+            try {
+                var node = Json.from_string(raw);
+                if (node.get_node_type() != Json.NodeType.ARRAY) {
+                    return rows;
                 }
-                if (value.type() == typeof(int64)) {
-                    return ((!)value).get_int64() != 0;
+                foreach (var element in node.get_array().get_elements()) {
+                    if (element.get_node_type() != Json.NodeType.OBJECT) {
+                        continue;
+                    }
+                    var obj = element.get_object();
+                    var row = new GoalBranchPush();
+                    row.repo = obj.has_member("repo")
+                        ? (obj.get_string_member("repo") ?? "") : "";
+                    if (obj.has_member("branch")) {
+                        row.branch = obj.get_string_member("branch") ?? "";
+                    } else if (row.repo.length > 0) {
+                        row.branch = row.repo;
+                        row.repo = "";
+                    }
+                    if (obj.has_member("pushed")) {
+                        var pushed = obj.get_member("pushed");
+                        if (pushed.get_node_type() == Json.NodeType.VALUE) {
+                            var value = pushed.get_value();
+                            if (value.type() == typeof(bool)) {
+                                row.pushed = ((!)value).get_boolean();
+                            } else if (value.type() == typeof(int64)) {
+                                row.pushed = ((!)value).get_int64() != 0;
+                            }
+                        }
+                    }
+                    if (row.label().length > 0) {
+                        rows.add(row);
+                    }
                 }
+            } catch (Error e) {
+                // Unparseable push state reads as no rows.
             }
-            return true;
+            return rows;
         }
 
-        // ------------------------------------------------------------------
-        // Helpers
-        // ------------------------------------------------------------------
-
         private async Goal require_goal(int64 goal_id) throws Error {
             var goal = yield get_goal(goal_id);
             if (goal == null) {

+ 10 - 72
src/entrypoints/GoalDetailEntrypoint.vala

@@ -746,7 +746,15 @@ namespace ClankerSaddle {
             if (open_session_saddle > 0 && polls != null) {
                 string flags = ((!)polls).goal_branches_for(open_session_saddle);
                 if (flags.length > 0) {
-                    parse_push_state(flags, push_branches);
+                    // The SAME parser the completion-accept gate reads
+                    // (GoalService.parse_goal_branches) feeds the chips —
+                    // gate and display cannot diverge on one payload.
+                    foreach (var row in GoalService.parse_goal_branches(flags)) {
+                        push_branches.add(new GoalPushBranchView() {
+                            branch = row.label(),
+                            pushed = row.pushed
+                        });
+                    }
                 }
             }
         }
@@ -756,7 +764,7 @@ namespace ClankerSaddle {
          * the task page's computed display (`display_of_detail`) with
          * the queue batch lookups.
          */
-        private static async Dictionary<string, GoalTaskDisplay> task_display_map(
+private static async Dictionary<string, GoalTaskDisplay> task_display_map(
                 TaskService tasks, int64 goal_id) throws GLib.Error {
             var map = new Dictionary<string, GoalTaskDisplay>();
             var listed = new Vector<TaskEntity>();
@@ -777,76 +785,6 @@ namespace ClankerSaddle {
             return map;
         }
 
-        /**
-         * Parses the daemon's goal_branches report into display rows:
-         * both the array form ([{"repo": "r", "branch": "g/x",
-         * "pushed": true}] — the daemon's shape, labelled "r: g/x" so
-         * multi-repo rows stay distinguishable) and the object form
-         * ({"g/x": true}) are accepted.
-         */
-        public static void parse_push_state(string raw, Vector<GoalPushBranchView> rows) {
-            try {
-                var node = Json.from_string(raw);
-                if (node.get_node_type() == Json.NodeType.ARRAY) {
-                    foreach (var element in node.get_array().get_elements()) {
-                        if (element.get_node_type() != Json.NodeType.OBJECT) {
-                            continue;
-                        }
-                        var obj = element.get_object();
-                        var row = new GoalPushBranchView();
-                        string repo = obj.has_member("repo")
-                            ? (obj.get_string_member("repo") ?? "") : "";
-                        string branch = "";
-                        if (obj.has_member("branch")) {
-                            branch = obj.get_string_member("branch") ?? "";
-                        } else if (repo.length > 0) {
-                            branch = repo;
-                            repo = "";
-                        }
-                        if (repo.length > 0 && branch.length > 0) {
-                            row.branch = @"$(repo): $(branch)";
-                        } else {
-                            row.branch = branch;
-                        }
-                        if (obj.has_member("pushed")) {
-                            var pushed = obj.get_member("pushed");
-                            row.pushed = node_truthy(pushed);
-                        }
-                        if (row.branch.length > 0) {
-                            rows.add(row);
-                        }
-                    }
-                } else if (node.get_node_type() == Json.NodeType.OBJECT) {
-                    foreach (var member in node.get_object().get_members()) {
-                        var value = node.get_object().get_member(member);
-                        if (value.get_node_type() != Json.NodeType.VALUE) {
-                            continue;
-                        }
-                        rows.add(new GoalPushBranchView() {
-                            branch = member,
-                            pushed = node_truthy(value)
-                        });
-                    }
-                }
-            } catch (Error e) {
-                // Unparseable push state renders as no rows.
-            }
-        }
-
-        private static bool node_truthy(Json.Node node) {
-            if (node.get_node_type() != Json.NodeType.VALUE) {
-                return false;
-            }
-            var value = node.get_value();
-            if (value.holds(typeof(bool))) {
-                return value.get_boolean();
-            }
-            if (value.holds(typeof(int64)) || value.holds(typeof(long))) {
-                return value.get_int64() != 0;
-            }
-            return false;
-        }
-
         private static string format_stamp(DateTime? stamp) {
             return stamp != null ? ((!)stamp).format("%Y-%m-%d %H:%M") : "";
         }