Nodes/genereticore/ꘅ Decision Scores Validator
ComfyUI Node

ꘅ Decision Scores Validator

Catch a rigged decision JSON before it drives your graph

By languel·Created 11 days ago·Updated 8 days ago· 0
ꘅ Decision Scores Validator
    • choice
    • decision_json
    ◄choices_json["blue", "orange"]►
    ◄decision_json{"choice":"blue","scores":{"blue":0.8,"orange":0.2}}►

    What it's for

    ꘅ Decision Scores Validator takes a decision JSON you supply - hand-written, or produced by some other node or provider - and puts it through the same contract the rest of the pack expects. It runs no AI. It's a bouncer.

    Two jobs. First, teaching: the pack's sample 01 - Read a decision - no model uses this node so you can watch a choice and a normalized distribution appear without downloading anything or waiting for inference. That's a genuinely good way to learn the data shape before you involve a model.

    Second, and more useful in practice: it's the adapter for foreign score sources. If you produce decisions anywhere that isn't ꘅ Local Decision - a different node pack, an external program, an LLM that you asked for JSON (and which, as usual, half-followed the format) - this node is where you find out whether the payload is coherent, before ꘅ Choice to Value turns a bad label into a bad number.

    Inputs, outputs

    choices_json - the allowed labels. ["blue", "orange"] by default. The author's tooltip is worth quoting because people get this wrong: these are answer names, not scores.

    decision_json - the payload, defaulting to {"choice":"blue","scores":{"blue":0.8,"orange":0.2}}.

    Two outputs: choice (STRING) and decision_json (STRING). On success, decision_json isn't a passthrough - the node re-serializes a normalized object with a probabilities map and a probability_kind field of normalized_uncalibrated_scores. That label is the author being straight with you: dividing scores by their sum does not make them probabilities.

    What it actually checks

    Reading the validation code is more honest than reading a summary, because the rules are strict in a couple of places you might not expect:

    • The object must contain exactly choice and scores. An extra "reason" key fails.
    • The scores object must contain exactly the labels in choices_json - no missing entries, no strangers.
    • Every score must be a finite number between 0 and 1. Negative scores, 999, NaN, or true all fail.
    • The scores must have non-zero total mass.
    • choice must name the highest-scoring label.

    That last check has the best error message in the pack. If you edit blue down to 0.1 and leave orange at 0.2 without touching choice, you get told that choice is "blue" at 0.1 but "orange" has the highest score at 0.2, and then - the important part - editing scores does not automatically change choice. That's a deliberate design choice: the selector never silently disagrees with the numbers it's shipping.

    On failure the node doesn't crash the queue. It returns an ExecutionBlocker for both outputs, which cleanly skips everything downstream, and shows the reason inline on the node: Needs correction: … plus a note that connected outputs were not updated. So the graph stops quietly instead of half-running.

    Install

    cd ComfyUI/custom_nodes
    git clone https://github.com/languel/genereticore
    

    Restart, or grab it from ComfyUI Manager by searching GeneretiCore. It sits under Genereti / Inference as ꘅ Decision Scores Validator.

    No model, no helper, no macOS requirement. The pack's base pyproject declares zero dependencies and this node uses json plus the contract module - plus Comfy's ExecutionBlocker, which it imports lazily inside the failure path. That import is the one thing that can bite you on an old Comfy install: if comfy_execution.graph_utils isn't available, the failure path is where you'll find out.

    Where people get burned

    The blocking is invisible in the queue log. An invalid payload doesn't throw; execution just stops at this node and everything downstream is skipped. If your graph "did nothing" with no red error, read the text on the validator's own face before you go hunting.

    Editing scores doesn't move the choice. The mismatch error message says this outright, and people still try it. Change both, or accept the block.

    Reaching for it when you already have a decision node. ꘅ Local Decision already produces conforming JSON, so inserting the validator into that chain buys you nothing but a click. The validator is for supplied scores. Well - almost nothing: it's a legitimate last-resort assert if you don't trust the producer, and it's the only place in the pack that will tell you which label should have won.

    Assuming normalization means anything. probabilities sums to 1, which is tidy and completely unrelated to whether the model is right. The pack's own framing is that scores are uncalibrated model-reported numbers; a beautifully normalized 0.9 can be confidently wrong. Design for that - keep a threshold, or a "stay where you are if it's close" rule - rather than treating the distribution as confidence.

    CategoryGenereti / Inference

    Inputs (2)

    NameTypeDefaultDescription
    choices_jsonSTRING["blue", "orange"]Allowed labels. These are answer names, not scores.
    decision_jsonSTRING{"choice":"blue","scores":{"blue":0.8,"orange":0.2}}The choice must name a highest-scoring label. If blue becomes 0.1 and orange stays 0.2, set choice to orange too. Scores need not sum to 1; the viewer shows normalized probabilities.

    Outputs (2)

    NameTypeDescription
    choiceSTRING—
    decision_jsonSTRING—