v0.8.11
-
topological_orderno longer nondeterministic: the Kahn's-algorithm
helpers inbackend/ml_pipeline/_execution/graph_utils.pywere seeded from
asetof node ids, whose iteration order depends on insertion history —
sotopological_order(topological_order(nodes))could return a different
(still valid) order thantopological_order(nodes), breaking idempotency
and making execution order depend on how the node list happened to be
built. The helpers now take alist[str]in deterministic order:
topological_orderpasses input-list order (deduped via
dict.fromkeys), and_collect_ancestorspassessorted()BFS results.
Among simultaneously ready nodes the input-list order is preserved, so the
result is deterministic and idempotent. -
SonarCloud PR analysis flagged Vite build output: all 46 open issues on
PR #146 were instatic/ml_canvas/assets/*.js— minified frontend build
artifacts that SonarCloud's PR analysis picked up because the files changed
in the PR.static/ml_canvas/**is now insonar.exclusionsin
sonar-project.properties, so build output is no longer analyzed (the
scanner job in.github/workflows/pr_check.ymlreads this file directly). -
Drop-Rows canvas settings ignored (OC-13): the Drop Rows node's UI
stores a 0–100% "drop rows missing more than X%" slider, but the converter
sent keys the backend never read, so every canvas run silently ran as
"drop any missing".DropMissingRowsnow accepts amissing_threshold
percentage param (pandas + polars parity, mirroringDropMissingColumns):
rows with more than X% of the subset missing are dropped, and the
converter maps the checkbox / null / 0% tohow="any". The
DropMissingRowsArtifactgains the new field. -
Iterative Imputer canvas estimator choices ignored (OC-14): the
Imputation node's UI emits lowercase estimator aliases (decision_tree,
extra_trees,knn,bayesian_ridge) and the converter forwards them
verbatim, but_build_iterative_estimatorin
preprocessing/imputation/_common.pymatched only the exact documented
strings (DecisionTree,ExtraTrees,KNeighbors) — so every canvas run
silently fell back toBayesianRidgeregardless of the user's choice. The
alias is now normalized (lowercased, non-alphanumerics stripped) before
dispatch, so both the UI values and the documented aliases resolve to the
same regressor; unknown names still fall back toBayesianRidge. Added
JSON-driven alias cases for the four UI values. -
MinMax/Robust scaler range controls ignored on canvas (OC-15): the
Scaling node's UI stores the ranges as scalar fields
(feature_range_min/feature_range_max,quantile_range_min/
quantile_range_max), but the converter forwarded them verbatim while the
backend reads tuple keys (feature_range/quantile_range) — so every
canvas run silently used the defaults (0/1 and 25/75). The converter's
scale_numeric_featuresbranch now assembles the tuple keys from the
scalar fields (with the same defaults); no backend change was needed.
Added 4 vitest cases topipelineConverter.test.ts. -
Alias Replacement
punctuationmode was a silent no-op (OC-19): the
Standardize Values node's UI offers apunctuationmode ("Removes common
punctuation characters from text"), but the backend applier in
preprocessing/cleaning/alias.pyonly handled the alias-mapping modes —
punctuationresolved to an empty mapping, so the node did nothing. Both
engine paths now stripstring.punctuationonly (case and spaces preserved,
matching the UI wording; NaN passes through unchanged). Added JSON-driven
cases plus a pandas/polars NaN-passthrough parity test. -
Value Replacement UI help text contradicted actual behavior (OC-20): the
Standardize Values node's UI help text promised "If empty, applies to all
compatible columns," but the backend no-ops on an emptycolumnslist — the
intended repo convention (user_picked_no_columns: unchecked boxes mean "do
nothing for this node"). The help text now reads "Select columns to apply
replacements to. If no columns are selected, this node does nothing." No
backend change: the empty-columns no-op was already correct. Added a
contract-locking test (test_apply_empty_columns_is_noop, pandas/polars)
asserting emptycolumns+ a mapping leaves data unchanged. -
select_from_model'smax_featureswas Python-only (OC-53): the
backend readsconfig.get("max_features")and passes it to sklearn's
SelectFromModel, but the Feature Selection node's UI rendered only a
thresholdfield for theselect_from_modelmethod — so the feature-count
cap was unreachable from the canvas. The UI now offers an optional
"Max Features" numeric input for that method (empty = no cap, matching the
backend'sNonedefault). No converter change: thefeature_selection
branch already forwardsnode.dataunchanged. Added 2 vitest cases to
pipelineConverter.test.ts. -
Binning's "Precision (Decimals)" was UI-unreachable (OC-61): the
backend (bucketing.py) readsconfig.get("precision", 3)and the canvas
Binning node rendered a "Precision (Decimals)" input, but
pipelineConverter.tslisted theGeneralBinningparams explicitly and
omittedprecision, so the value was silently dropped. The converter now
forwardsprecision(omitted when unset, matching the backend's default of
3). Added 2 vitest cases topipelineConverter.test.ts. -
CalibratedClassifierCV base estimator discarded during tuning (OC-66):
the tuning engine builds the meta-estimator from
model_calculator.default_params, which hardcoded
estimator=LogisticRegression— so the user'sbase_estimatorselection
was silently ignored whenever the node was tuned (it only took effect in
plainfit).CalibratedClassifierCalculatornow routes the selection
through the structural-tuning hook (same mechanism as the ensemble
calculators):prepare_tuning_paramscapturesbase_estimator(flat or
nestedparamsshape) anddefault_paramsresolves it via the
BASE_ESTIMATORSfactory, with a warn-and-fallback to
logistic_regressionfor unknown keys. No backend change: both the
fixed-run and tuned paths already callprepare_tuning_paramsand exclude
STRUCTURAL_TUNING_KEYSfrom the search space. Added unit tests
(capture, resolution, fallback) and an integration test asserting the tuned
pipeline fits aRandomForestClassifierinsideCalibratedClassifierCV. -
KNN/Iterative imputers crashed on all-missing fitted columns (OC-16):
sklearn'sKNNImputer/IterativeImputersilently drop all-missing columns
fromtransform()output when the column was all-missing at fit time,
desyncing the artifact'scolumnslist from the imputer's width and
crashing_sklearn_transform_subset(IndexError in the polars branch,
ValueError in the pandas branch). Both calculators now call the new
drop_all_missing_columnshelper inpreprocessing/imputation/_common.py
duringfit: all-missing columns are dropped from the fit matrix (with a
warning naming them) so the artifact stays in lockstep with the imputer's
width, andfitreturns an empty artifact when every configured column is
all-missing (appliers pass that through as a no-op). Added 5 regression
tests (parametrized over KNN + Iterative, pandas + polars). -
SimpleImputer polars mean/median crashed on all-null columns (OC-17):
fitting aSimpleImputerwithmean/medianon an entirely-null column
storedfill_values[col] = Nonein the polars path (polarsmean()/
median()over an all-null column returnsNone), and_apply_polars
then calledpl.col(col).fill_null(None)— which raisesValueError: must specify either a fill value or strategy. The pandas path already skipped
Nonefills, so the engines diverged (pandas left the column all-null;
polars crashed)._apply_polarsnow passes the column through unchanged
when its fill value isNone, mirroring_apply_pandas. Added 3 regression
tests (polars no-crash for mean and median, plus a pandas/polars
engine-parity test). -
Engine trusted
config.nodeslist order without verifying topological
sort (OC-69): bothpredict_schemasand the engine's node loop iterated
config.nodesas-is, butvalidate_no_cycles()only detects cycles — it
never restores order. The canvas converter's BFS can emit an
acyclic-but-misordered list (a merge node is enqueued when any parent is
dequeued, not all), so a diamond merge fed by unequal-depth branches failed
with a cryptic "Artifact not found" error and silentNoneschema
degradation. A new publictopological_order()ingraph_utils.py(reusing
the existing Kahn's-algorithm helpers) now re-sorts the node list at both
consumer sites. Added 5 regression tests including a full engine e2e run of
a misordered diamond. -
Multiclass splits missing a class emitted binary-only metrics + null
curve points (OC-35): three places decided binary-vs-multiclass from the
labels present iny_trueinstead of the model's trained label set, so a
3-class model evaluated on a split containing only two classes was
misclassified as binary — it gained unweightedprecision/recall/f1
keys, lost a computablelog_loss(sklearn raised "2 vs 3. Please provide
labels"), and emitted ROC/PR curve points withnullcoordinates (NaN from
roc_curveon the absent class's all-zero one-vs-rest target). The binary
gate in_add_binary_unweighted_metricsnow resolvesmodel.classes_and
is binary ifflen(classes_) == 2(falling back to the unique-label count
only when the model exposes noclasses_);log_lossis now called with
labels=classes(the full trained label set); and the per-class curve loop
inevaluate_classification_modelskips any class absent from the split.
Added 3 regression tests.