Skip to content

Releases: flyingriverhorse/Skyulf

v0.8.13

Choose a tag to compare

@github-actions github-actions released this 05 Sep 22:20
48fd640
  • Ruff's F401 (unused-import) rule is now enabled repo-wide: the lint set
    previously covered only the F63/F7/F82 subsets of pyflakes, so orphaned
    imports went unreported by ruff check, ruff format and ty — two left
    behind by the OC-152 fix below passed every gate and were caught only by an
    external linter. 22 dead imports were removed across tests, examples and
    notebooks; 4 intentional availability/side-effect probes (polars, h3,
    sentence_transformers) keep a per-site # noqa: F401 - <reason> waiver.
    Contributor-visible consequence: the pre-commit ruff hook runs with
    --fix, so an unused import added deliberately is now deleted at commit time
    unless it carries a waiver — and ruff honours # noqa only on the import
    statement line, not on a preceding comment. F841, the docstring rules and
    unused-args remain out of scope (OC-09).

  • A misspelled FASTAPI_ENV no longer silently boots the development security
    posture (OC-130):
    the value selected a settings profile through a lookup that
    fell back to DevelopmentSettings for anything unrecognized, so prod, prd,
    staging — or a trailing space picked up from a YAML/CI variable — started the
    server with DEBUG=True, CORS_ORIGINS=["*"], no security headers and the
    production SECRET_KEY check skipped. The wildcard origin is worse than it
    looks: the app also sets allow_credentials=True, and Starlette in that
    combination reflects the caller's origin instead of sending a literal *,
    so the browser's normal refusal never fires and any site can make credentialed
    requests. A second channel was dead as well — FASTAPI_ENV is not a Settings
    field and pydantic-settings never exports dotenv values into the process
    environment, so setting it in .env, the documented configuration file, had
    no effect at all and the server came up in development either way. Both now
    go through one resolver that reads the same channels as every other setting,
    normalizes case and surrounding whitespace, and raises at startup for any
    other value, naming the accepted ones (development, production, testing).
    An empty value raises too, since that is what an unset CI variable renders to.
    Behaviour change: a server with a typo'd or blank FASTAPI_ENV now refuses
    to start instead of starting insecurely. Fix the value rather than removing it
    — the absence of an error was the bug.

  • AWS credentials no longer reach the log through S3 errors (OC-150): both S3
    modules carried their own copy of a _sanitize_error helper that matched on
    credential key names, and in practice it did nothing at all — an S3 403
    response body, a presigned URL and an s3fs options dictionary all passed
    through byte-identical, exposing not just access key IDs and signatures but the
    secret access key itself. Presigned URLs are bearer credentials: anyone who
    obtains one can fetch the object, so logging one publishes it. The same helper
    was simultaneously too aggressive elsewhere, replacing an ordinary
    key=reports/2026/q3.csv not found message wholesale with redacted sensitive S3 error and destroying the only useful part of the diagnostic. Both copies
    are replaced by one redact_credentials() helper beside the existing log
    injection guard, which matches on value shape rather than setting name:
    20-character AWS access key IDs, name=value and name: value assignments for
    the known credential options, and the XML tags S3 uses in its 403 body. Only the
    secret is replaced, so the surrounding message survives intact, and redacting an
    already-redacted message changes nothing. The S3 connector's startup log line
    now also redacts the path it was given, since a caller may pass a presigned URL
    directly. Exception messages raised back to the caller are deliberately left
    readable — they carry your own input, and a redacted error is no use to you.

  • Removed four unused raw-SQL executor methods (OC-152):
    execute_query/execute_update on both the SQLite and PostgreSQL async
    connection managers accepted an arbitrary query string and passed it straight
    to the driver. Nothing in the codebase called them, but their presence offered
    the next contributor an unconstrained injection sink that bypasses every
    parameterisation convention the rest of the database layer follows. Use
    SQLAlchemy constructs, or text() with bound parameters, instead.

  • Deployed-model predictions no longer silently use misaligned or invented
    features (OC-154, OC-155):
    two serving paths degraded quietly instead of
    failing. The bundled path skipped its training-feature-order reindex exactly
    when alignment could not be confirmed — the one case the reindex existed
    for — and handed a positional model whatever columns the feature engineer
    happened to produce; reproduced returning 9921.0 from a frame whose
    columns did not match what the bundle was trained on, with no error and no
    warning. The legacy path imputed absent features with the literal constant
    0, which for income, age, price or any scaled feature is an extreme
    out-of-distribution input, returned the result as a normal prediction behind
    a server-side log line the caller never saw, and wrote the fabricated column
    into the caller's own DataFrame. Both now raise an error naming the offending
    columns, which the API surfaces as HTTP 400 with the column list and the
    canvas displays directly. Behaviour change: a deployment whose recorded
    feature columns no longer match what its own feature engineer emits, or a
    request that omits a feature the model was trained on, now fails loudly
    instead of returning a number. Such a bundle needs retraining and
    redeploying — the old response was wrong, not merely convenient.

  • Merging branches with different row counts no longer duplicates data
    silently (OC-153, OC-157):
    wiring two branches into a merge node expresses
    a feature union, but the engine picked its merge mode purely from row
    counts — so when either branch changed the row count (outlier removal,
    deduplication, any dropna) it silently switched to a row-wise concat and
    returned a taller frame of stacked rows instead of a wider one.
    Reproduced: a 5-row dataset merged with its own filtered branch produced 9
    rows containing 4 duplicates, and because the two branches had identical
    column sets the one condition that emitted a UI advisory was false — the
    canvas showed zero warnings and the only trace was a line in the job log.
    Duplicated rows silently reweight those observations during training and,
    when the merge feeds a split node, place identical rows on both sides of the
    train/test boundary. Row-wise stacking is still supported, since appending
    separate datasets is a legitimate use of a merge node, but it now always
    emits a row_count_mismatch advisory carrying the per-input row counts and
    rendered in the canvas merge banner with the remedy. Separately, the
    first_wins strategy reversed the output column order: it was
    implemented by iterating the inputs backwards, and the accumulator dict's
    insertion order is the merged frame's column order, so the same two
    branches came out ['a','b','c','d'] under last_wins and
    ['c','d','a','b'] under first_wins — contradicting its own docstring and
    handing positional consumers a different layout per configuration. Ownership
    is now resolved by declining to overwrite an already-claimed column while
    walking the inputs in their own order, so both strategies emit columns in
    input order with unchanged winners.

  • Recursive Feature Elimination now selects the number of features you asked
    for (OC-25, also closes OC-143):
    the RFE panel's "K (Number of Features)"
    field was ignored. The backend read n_features_to_select — a key nothing in
    the entire codebase ever wrote — so it was always unset and scikit-learn fell
    back to its own default of keeping half the candidate features. Setting
    K=2 over 6 features selected 3, with no error and no warning, while the node
    summary still read rfe · k=2. It hid because RFE's other field (step)
    is read correctly, so the panel looked fully wired up, and the test fixture
    exercised only the internal n_features_to_select spelling, leaving the k
    path the UI actually sends with no coverage at all. RFE now accepts k as an
    alias, with an explicit n_features_to_select taking precedence, so the
    canvas, direct API calls and notebooks all agree. Added 3 regression tests
    including an end-to-end case over 6 features.

v0.8.12

Choose a tag to compare

@github-actions github-actions released this 05 Sep 13:51
7399066
  • Binary pr_auc scored the wrong class on {1, n} labels (OC-146, also
    closes OC-37):
    _add_roc_pr_auc_metrics called average_precision_score
    without pos_label, and that function defaults to pos_label=1 — unlike
    roc_auc_score, which infers the positive class from the sorted uniques.
    With labels {1, 2} or {1, 5} (survey exports, Likert-derived targets,
    R-style factor codes, 1=no, 2=yes CSVs) the literal 1 is the negative
    class while proba[:, 1] is P(classes_[1]), so PR-AUC was computed for
    the inverted problem: 0.32 reported for a model whose true PR-AUC is
    0.97
    , with no warning. For every other non-{0,1} label set (strings,
    {2,3}, {0,2}, {10,20}) sklearn raised instead and the metric silently
    vanished from the report. The same report then contradicted itself on
    screen: the PR curve is built with pos_label=classes[1], so a chart
    hugging the top-right corner carried "0.32" beside it. The binary branch now
    resolves the positive class from model.classes_, the pattern
    _add_binary_unweighted_metrics and the curve builder already used.
    pr_auc is now identical across {0,1}, {1,2}, {1,5}, {-1,1} and
    {"no","yes"} encodings of the same data. Added 7 regression tests.

  • Threshold tuning picked a pathological cutoff on single-class validation
    (OC-36):
    _grid_search_binary broke score ties with a strict >, keeping
    whichever candidate the scan reached first. When the validation split held
    only one class, every candidate scored identically, so tuning returned the
    first grid point — {0: 0.990, 1: 0.0098} — which classified almost
    everything positive (measured: 49/50 rows at F1 0.0) and was persisted as a
    tuned threshold. Metrics like F1 are piecewise constant in the cutoff, so
    tied plateaus are routine even on healthy splits, and the same tie-break
    pinned whichever plateau edge came first. Ties now break toward the default
    0.5 cut; a single-class split warns and keeps {0.5, 0.5}; NaN scores
    from a caller-supplied metric are still skipped. _tune_decision_thresholds
    gains a matching gate so a degenerate split leaves
    decision_thresholds=None (predictions keep the model's default rule)
    instead of reporting a threshold nothing was tuned against. Documented in
    docs/user_guide/threshold_tuning.md. Added 4 regression tests.

  • optimize_thresholds' binary result bypassed its own documented decision
    rule (OC-147):
    apply_thresholds documents "predicts the positive class
    when y_proba[:, 1] >= threshold" and special-cased a bare float and a
    one-entry dict — but _grid_search_binary returns a two-entry dict,
    which fell through to the multiclass scaled argmax, where np.argmax breaks
    exact ties toward the first column. That silently turned >= into a strict
    >, so the search scored tied rows as positive while apply-time scoring
    counted them negative and the tuned score was not quite the score you get.
    Reachable in practice: the grid includes 0.5 and decision trees routinely
    emit p1 of exactly 0.5. The two-class case now compares the scaled
    scores directly, keeping >= authoritative — verified identical to argmax
    on every non-tied row for complementary and non-complementary threshold
    pairs, so a user-saved set still has both entries honored. Added 2
    regression tests.

  • The profiler reported NaN statistics — and a silently wrong median — for
    any float column containing a NaN (OC-39):
    polars keeps NaN distinct from
    null, so EDAAnalyzer's fill_null-based cleaning was a no-op on it.
    Aggregations then propagated the NaN instead of skipping it: std,
    skewness and kurtosis came back NaN, the histogram builder died with
    ComputeError: breaks cannot be NaN (its min_val == max_val guard cannot
    see that nan == nan is False), and worst of all the median was simply
    wrong
    [1, 2, nan, 4] reported 3.0 where pandas reports 2.0, with
    no error and no warning. Nothing caught this because tests build frames with
    pl.from_pandas, which converts NaN to null on the way in; the bug is only
    reachable from pl.read_csv (literal NaN tokens) or direct construction.
    Fixed once at the boundary rather than at six call sites: a _nan_to_null
    pass in EDAAnalyzer.__init__ rewrites NaN to null in every float column, so
    downstream type checks, aggregations and cut() all see the missing-value
    semantics the rest of the profiler already assumed. Added 6 regression tests.

  • Quartiles, skewness and kurtosis used polars' estimator defaults, not
    pandas' (OC-41, OC-42):
    three silent divergences in the same expression
    list. Expr.quantile() defaults to interpolation="nearest" where
    pandas/NumPy use linear, so Q1/Q3 were reported as actual observed data
    points rather than interpolated positions — and _compute_boxplot_stats
    disagreed with NumericStats about the same column's hinges. skew() and
    kurtosis() default to bias=True where pandas reports the bias-corrected
    estimator, and polars' kurtosis() is not Fisher excess kurtosis by default
    while pandas' .kurt() is. The skewness error was user-visible, not just
    numerical: [1, 2, 3, 4, 10] reports 1.30 biased against 1.70 unbiased,
    so the "High skewness — consider a transform" recommendation stayed silent on
    a column that clears the 1.5 threshold. Both quantile sites now pin
    interpolation="linear", and skew/kurtosis pin bias=False with
    fisher=True. Added 6 regression tests (shared with OC-39).

  • PCA and clustering "mean imputation" was actually zero-filling NaN (OC-40):
    _impute_matrix called fill_null(strategy="mean"), which — same root cause
    as OC-39 — is a no-op on NaN, so the values fell through to the
    np.nan_to_num(nan=0.0) guard. Components were fitted with 0.0 exactly
    where SimpleImputer would have put the column mean, which for a
    mean-centered feature is the most distorting value available. Its sibling
    _impute_matrix_drop_empty already normalized first; _impute_matrix now
    mirrors it. Added 2 regression tests.

  • Correlation dropped entire columns and returned nothing at all (OC-43):
    calculate_correlations used drop_nulls() — which keeps NaN rows — and
    then DataFrame.corr(), which is listwise: a single surviving null made
    the whole matrix NaN, the broad except swallowed it, and the profile
    silently lost its correlation section. Two fixes: NaN is normalized to null
    up front, and the matrix is now built pairwise with pl.corr (which
    already does pairwise deletion, matching pandas' .corr()), so a column with
    a few gaps correlates on its overlapping rows instead of taking the matrix
    down with it. Pairs with fewer than 3 overlapping observations are reported
    as 0.0 with a single aggregated warning, because at n=2 a Pearson r is
    always exactly ±1.0 — a schema limitation, since CorrelationMatrix.values
    is list[list[float]] and has no "unknown" cell. Added 4 regression tests.

  • Wasserstein drift reported a number its own threshold contradicted
    (OC-44):
    the drift decision has always been made on the distance normalized
    by the reference standard deviation, but value carried the raw distance in
    the column's own units. A large-scale column that barely moved therefore
    reported wasserstein_distance=50.0, threshold=0.1, has_drift=False — and
    every consumer that re-derives the verdict from value inherited the
    contradiction: the UI's threshold sliders flipped it to "drifted", the CSV
    export published 50.0, the alert modal showed it beside a 0.1 threshold, and
    _build_drift_column_summary persisted it into drift_check_results.summary
    where it is later compared against threshold_wasserstein. The published
    example notebook shows the failure outright — 784.7832 (Thresh: 0.1) [PASS]. Rather than teach each consumer a metric-specific exception, the
    invariant now lives in the schema: value is the number threshold applies
    to for every metric that decides its own verdict, and the untransformed
    distance moved to a new raw_value. A constant reference has no scale to
    normalize by and falls back to the raw distance rather than emitting inf.
    The one documented exception is ks_test_p_value, which is diagnostics only
    and borrows the KS statistic's threshold. No backend change was needed —
    EnrichedDriftReport.column_drifts is dict[str, Any]. Added 5 core, 1
    backend and 3 frontend regression tests.

  • Schema drift was never counted as drift (OC-45):
    drifted_columns_count was built from the per-column metric flags alone, so
    a feature that vanished between training and production left it at 0
    while _classify_drift_severity classified that same report "critical",
    and the drift-status dashboard counts drifted jobs by that field, so a
    critical job was reported as having no drift. The count now includes
    missing_columns and new_columns; the frontend's client-side
    re-evaluation adds them too, since rebuilding the count from metric flags
    alone dropped them the moment a slider moved. Added 2 core and 2 frontend
    regression tests.

  • A profile could serialize an invalid NaN token (OC-46):
    NumericStats accepted NaN and infinity into its float fields.
    backend/eda/tasks.py persists the profile via
    profile.model_dump(mode="json"), which retains a Python nan, and stdlib
    json.dumps then emits a bare NaN — not valid JSON, and rejected by the
    browser's JSON.parse, so the whole EDA result failed to load. The ten
    optional float fields now run through a FiniteFloat annotated type that maps
    any non-finite float to None before validation. BoxPlotStats was left
    alone: its fields are required, and making them optional would be a real
    contract change rather than a fix. Added 1 regression test (shared with
    OC-39).

  • **Every cross-origin error resp...

Read more

v0.8.11

Choose a tag to compare

@github-actions github-actions released this 04 Sep 15:55
a996a1e
  • topological_order no longer nondeterministic: the Kahn's-algorithm
    helpers in backend/ml_pipeline/_execution/graph_utils.py were seeded from
    a set of node ids, whose iteration order depends on insertion history —
    so topological_order(topological_order(nodes)) could return a different
    (still valid) order than topological_order(nodes), breaking idempotency
    and making execution order depend on how the node list happened to be
    built. The helpers now take a list[str] in deterministic order:
    topological_order passes input-list order (deduped via
    dict.fromkeys), and _collect_ancestors passes sorted() 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 in static/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 in sonar.exclusions in
    sonar-project.properties, so build output is no longer analyzed (the
    scanner job in .github/workflows/pr_check.yml reads 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". DropMissingRows now accepts a missing_threshold
    percentage param (pandas + polars parity, mirroring DropMissingColumns):
    rows with more than X% of the subset missing are dropped, and the
    converter maps the checkbox / null / 0% to how="any". The
    DropMissingRowsArtifact gains 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_estimator in
    preprocessing/imputation/_common.py matched only the exact documented
    strings (DecisionTree, ExtraTrees, KNeighbors) — so every canvas run
    silently fell back to BayesianRidge regardless 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 to BayesianRidge. 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_features branch now assembles the tuple keys from the
    scalar fields (with the same defaults); no backend change was needed.
    Added 4 vitest cases to pipelineConverter.test.ts.

  • Alias Replacement punctuation mode was a silent no-op (OC-19): the
    Standardize Values node's UI offers a punctuation mode ("Removes common
    punctuation characters from text"), but the backend applier in
    preprocessing/cleaning/alias.py only handled the alias-mapping modes —
    punctuation resolved to an empty mapping, so the node did nothing. Both
    engine paths now strip string.punctuation only (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 empty columns list — 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 empty columns + a mapping leaves data unchanged.

  • select_from_model's max_features was Python-only (OC-53): the
    backend reads config.get("max_features") and passes it to sklearn's
    SelectFromModel, but the Feature Selection node's UI rendered only a
    threshold field for the select_from_model method — 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's None default). No converter change: the feature_selection
    branch already forwards node.data unchanged. Added 2 vitest cases to
    pipelineConverter.test.ts.

  • Binning's "Precision (Decimals)" was UI-unreachable (OC-61): the
    backend (bucketing.py) reads config.get("precision", 3) and the canvas
    Binning node rendered a "Precision (Decimals)" input, but
    pipelineConverter.ts listed the GeneralBinning params explicitly and
    omitted precision, so the value was silently dropped. The converter now
    forwards precision (omitted when unset, matching the backend's default of
    3). Added 2 vitest cases to pipelineConverter.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's base_estimator selection
    was silently ignored whenever the node was tuned (it only took effect in
    plain fit). CalibratedClassifierCalculator now routes the selection
    through the structural-tuning hook (same mechanism as the ensemble
    calculators): prepare_tuning_params captures base_estimator (flat or
    nested params shape) and default_params resolves it via the
    BASE_ESTIMATORS factory, with a warn-and-fallback to
    logistic_regression for unknown keys. No backend change: both the
    fixed-run and tuned paths already call prepare_tuning_params and exclude
    STRUCTURAL_TUNING_KEYS from the search space. Added unit tests
    (capture, resolution, fallback) and an integration test asserting the tuned
    pipeline fits a RandomForestClassifier inside CalibratedClassifierCV.

  • KNN/Iterative imputers crashed on all-missing fitted columns (OC-16):
    sklearn's KNNImputer/IterativeImputer silently drop all-missing columns
    from transform() output when the column was all-missing at fit time,
    desyncing the artifact's columns list 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_columns helper in preprocessing/imputation/_common.py
    during fit: 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, and fit returns 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 a SimpleImputer with mean/median on an entirely-null column
    stored fill_values[col] = None in the polars path (polars mean()/
    median() over an all-null column returns None), and _apply_polars
    then called pl.col(col).fill_null(None) — which raises ValueError: must specify either a fill value or strategy. The pandas path already skipped
    None fills, so the engines diverged (pandas left the column all-null;
    polars crashed). _apply_polars now passes the column through unchanged
    when its fill value is None, mirroring _apply_pandas. Added 3 regression
    tests (polars no-crash for mean and median, plus a pandas/polars
    engine-parity test).

  • Engine trusted config.nodes list order without verifying topological
    sort (OC-69):
    both predict_schemas and the engine's node loop iterated
    config.nodes as-is, but validate_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 silent None schema
    degradation. A new public topological_order() in graph_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 in y_true instead 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 unweighted precision/recall/f1
    keys, lost a computable log_loss (sklearn raised "2 vs 3. Please provide
    labels"), and emitted ROC/PR curve points with null coordinates (NaN from
    roc_curve on the absent class's all-zero one-vs-rest target). The binary
    gate in _add_binary_unweighted_metrics now resolves model.classes_ and
    is binary iff len(classes_) == 2 (falling back to the unique-label count
    only when the model exposes no classes_); log_loss is now called with
    labels=classes (the full trained label set); and the per-class curve loop
    in evaluate_classification_model skips any class absent from the split.
    ...

Read more

v0.8.10

Choose a tag to compare

@github-actions github-actions released this 03 Sep 18:54
295ff14

🔒 Security — mistune 3.3.2 → 3.3.4 (dev dependency)

  • Bumped the transitive mistune dev dependency (pulled in via
    jupyternbconvert) from 3.3.2 to 3.3.4 via
    uv lock --upgrade-package mistune, resolving GHSA-6m44-fpc8-c3rq
    (HIGH). Dev-only; no runtime dependency change.

🐛 Bug Fixes — Core

  • X/y desync on duplicate index labels (OC-12): the pandas paths of
    DropMissingRows and Deduplicate selected y by label
    (y.loc[X_clean.index]); with duplicate index labels .loc returns all
    matching rows, so y came back longer than X with misaligned labels — a
    silent wrong-labels bug. Both paths now compute a positional keep mask
    (notna threshold / duplicated), select X.iloc[kept], and filter y
    positionally via the new _pandas_filter_y_by_kept_positions helper in
    preprocessing/drop_and_missing/_common.py, mirroring the already-correct
    polars paths. Added duplicate-index regression tests in
    tests/integration/test_drop_rows.py and
    tests/integration/test_drop_and_missing_gaps.py.

  • Numeric→boolean cast divergence on polars (OC-58): polars' default
    numeric→Boolean cast is C-style truthiness (x != 0) and never raises, so a
    value like 2.0 silently became True on polars while pandas
    (astype("boolean")) produced <NA> — and strict mode raised on pandas but
    not on polars. The polars path in preprocessing/casting.py now mirrors the
    pandas reference: only exact 0/1 values map to False/True, everything
    else (including non-integer floats) becomes null, and strict mode raises
    ValueError on those nulls. Added regression tests in
    tests/integration/test_casting.py (coerce, strict, pure 0/1, and
    engine-parity cases).

  • Non-reproducible fingerprint() for object-dtype arrays (OC-62):
    artifact_digest in pipeline/seal.py digested np.ndarray via
    arr.tobytes(); for dtype=object arrays that serialises raw PyObject*
    pointers, which are allocator/ASLR dependent — so the fingerprint of any
    artifact holding an object-dtype array (e.g. OneHotEncoder/LabelEncoder
    categories_) changed across processes and was useless for caching or
    comparison. The ndarray branch now digests the shape plus each element
    recursively, so the digest reflects values. Added regression tests in
    tests/unit/test_pipeline_coverage.py.

v0.8.9

Choose a tag to compare

@github-actions github-actions released this 02 Sep 15:15
09bc7f0

🔒 Security — tornado 6.5.7 → 6.5.8 (dev dependency)

  • Bumped the transitive tornado dev dependency (pulled in via the jupyter
    dev group) from 6.5.7 to 6.5.8 via uv lock --upgrade-package tornado,
    resolving GHSA-mpf4-983q-p7j4 (HIGH), GHSA-8423-8fgw-73vq (MED), and
    GHSA-wwv5-g3v4-889x (LOW). Dev-only; no runtime dependency change.

🧠 Core — F-08: strict DataFrame protocol split (finding closed)

  • Type-safety restored, zero runtime change: SkyulfDataFrame
    (skyulf/engines/protocol.py) no longer declares __getattr__ -> Any,
    which had silently disabled type checking on every
    pd.DataFrame | SkyulfDataFrame union. The protocol is now strict, and two
    @runtime_checkable sub-protocols carry the engine-specific surface:
    PandasBackedFrame (.loc, .iloc, .select_dtypes) and
    PolarsBackedFrame (.with_columns, .filter, .to_polars).
  • Call sites migrated: pandas/polars-specific attribute access now goes
    through cast(PandasBackedFrame, df) / cast(PolarsBackedFrame, df) (or a
    pd.DataFrame cast) so the checker sees the real attributes — applied in
    preprocessing/_helpers.py, modeling/base.py,
    modeling/sklearn_wrapper.py, modeling/_tuning/engine.py, and the
    integration tests. dtypes was added to the base protocol (both engines
    expose it).
  • Why it matters: the ~41 .iloc/.loc/.select_dtypes sites that were
    previously invisible to the type checker are now checked; a wrong attribute
    name on a frame is a compile-time error instead of a runtime AttributeError.
    Protocols are erased at runtime, so behavior is unchanged — verified by the
    full skyulf-core suite (3584 passed).

🧠 Core — F-09: engine-keyed dispatch mapping (finding closed)

  • Breaking (internal API): apply_dual_engine, fit_dual_engine, and
    fit_transform_train_dual_engine (skyulf/preprocessing/dispatcher.py)
    now take a single mapping keyed by engine name —
    {"polars": fn_pl, "pandas": fn_pd} — instead of two positional
    callables. All 63 node call sites migrated.
  • Loud failure replaces the silent pandas catch-all: an engine with no
    registered implementation raises NotImplementedError (naming the
    available keys) before any frame conversion, and an engine with an
    implementation but no input-preparation path raises as well — a third
    engine (Spark/Dask) can never again be silently collected to the driver.
    Adding a third engine is now an additive O(1) dispatcher change.
  • Guard test repaired: test_no_inline_engine_dispatch pointed at a
    nonexistent directory and passed vacuously; it now scans the real
    skyulf/preprocessing tree (87 files).
  • Behavior preserved: polars wrapper unwrap/re-wrap, pandas to_pandas()
    conversion, failure-log format, and mixed-engine (X, y) rejection.
    Deliberately untouched: vectorization/_common.py::apply_text_dual_engine
    (intentional pandas-first text path) and the non-dispatcher inline engine
    branches (F-08 territory).

v0.8.8 — Tuning Engine Split & Import Hygiene

Choose a tag to compare

@github-actions github-actions released this 30 Aug 13:17
93d7719

📦 Release

  • Version sync to 0.8.8 (app + frontend + core): root pyproject.toml,
    frontend/ml-canvas/package.json / package-lock.json, and
    skyulf-core/setup.py bumped to 0.8.8.

🧠 Core — F-18: _tuning/engine.py split (finding closed)

  • The 1,830-line skyulf/modeling/_tuning/engine.py (largest file in the
    library) is now a ~700-line orchestrator plus six leaf modules:
    params.py (search-space cleaning, param splitting, model instantiation,
    seed overlays), splitters.py (CV splitter builder family), metrics.py
    (metric validation/aliases, resolve_metric/resolve_scorer with
    pos_label pinning), grid_random.py (candidate generation + per-fold
    scoring), refit.py (best-model refit + decision-threshold tuning), and a
    strategies/ package (halving.py, optuna.py with the F-14 lazy
    loader, runner.py).
  • Pure code movement — the public TuningCalculator/TuningApplier surface
    is untouched, test-pinned private methods remain as one-line delegates,
    and stale test pins were retargeted to the new modules with the same
    coverage.

🧹 Core — F-11: import cycles & deferred imports (finding closed)

  • One real import cycle broken (modeling/base → _evaluation → sklearn_wrapper → base): the four _evaluation modules now import
    SklearnBridge from the leaf engines.sklearn_bridge, so base.py
    imports its evaluation/cross-validation dependencies at module level.
  • 173 function-level imports eliminated (ruff PLC0415): 144 hoisted to
    module level (internal, stdlib, hard dependencies incl. all 93 deferred
    polars imports), 29 genuinely optional extras (matplotlib, rich, shap,
    optuna, imblearn, sentence_transformers, vaderSentiment, causallearn, h3,
    scatter_matrix) kept deferred with documented per-site
    # noqa: PLC0415 - <reason> waivers.
  • Enforcement: PLC0415 is now in the ruff select set for
    skyulf-core/skyulf/ (backend/tests/entry points exempt), with the ruff
    pin bumped to >=0.15,<1.0 to match the pre-commit hook.
  • Monkeypatch-safe: third-party calls patched by tests keep
    module-attribute form (sklearn_metrics.*, scipy_stats.*,
    stattools.adfuller); degradation gates (SKLEARN_AVAILABLE etc.)
    untouched. Failures now surface at import time, and the real module graph
    is visible for the upcoming F-09/F-08 structural work.

🧾 Housekeeping

  • Findings tracker: with F-18 + F-11 closed, 3 findings remain open
    (F-30 deferred pending a compat call; structural F-09/F-08).
  • README refresh.

v0.8.7 — Pipeline Diagrams, Semantic Seal & Pipeline Package

Choose a tag to compare

@github-actions github-actions released this 30 Aug 08:01
31ccf5a

📦 Release

  • Version sync to 0.8.7 (app + frontend + core): root pyproject.toml,
    frontend/ml-canvas/package.json / package-lock.json, and
    skyulf-core/setup.py bumped to 0.8.7.

🗺️ New — Mermaid pipeline diagrams, end to end

  • Single source of truth in core: skyulf/pipeline/diagram.py renders a
    top-down flowchart TD (data -> preprocessing steps -> model) from
    config parts. Every label is double-quoted (mermaid rejects unquoted
    parentheses/brackets inside labels), with a human-readable head line and an
    optional second detail line — an explicit runtime summary or a compact
    digest of the step params (max 3 keys, _-prefixed/None/mapping values
    skipped, long values truncated). Algorithm names are humanized
    (random_forest_classifierRandom Forest Classifier); no internal
    node ids leak into labels.
  • Core API surface: SkyulfPipeline.to_mermaid() and
    to_mermaid_markdown() plus mermaid_markdown() for fenced export;
    export_model_card()["diagram"] carries the same diagram; the notebook
    export cell renders a ## Pipeline topology fenced block using node
    display names, not uuids.
  • Backend persistence: successful jobs now persist
    metrics.pipeline_diagram at completion, built from live node results —
    display names captured from the canvas (params._display_name
    metadata["display_name"]), runtime summaries preferred with a
    params-digest fallback for nodes without one, model node typed from the
    job's model_type.
  • Frontend: the Experiments "Pipeline Diagram" tab renders one card per
    selected run (legacy runs without a diagram are listed, not dropped), and
    each card has a Copy mermaid button that copies a ready-to-paste
    ```mermaid fenced block.
  • Render hardening: mermaid's htmlLabels output is HTML, not XML — the
    rendered SVG is now parsed via DOMParser('text/html') and mounted with
    replaceChildren, fixing the browser "Opening and ending tag mismatch:
    br and p" error page; regression tests pin both the parse and the mount.
  • Regression guards: core describe/humanize/fence tests, backend diagram
    adapter tests (display names, summaries, no-uuid assertions, notebook
    cell), and frontend real-mermaid parse tests plus copy-button tests.

🧠 Core — findings closeout

  • F-07: to_native() converts engine frames back to pandas for
    interchange consumers.
  • F-15: semantic fingerprint seals pipeline identity for reuse checks.
  • F-19/F-19b: pipeline split into the skyulf/pipeline/ package —
    diagram generation extracted from the fit path, describe()/to_mermaid()
    live on the pipeline object.

🔧 Fixes & hardening

  • Engine-agnostic node summaries: summary.py now handles both pandas
    and polars frames (shape, dtype breakdown, split renderings), pinned by
    tests/unit/test_summary_polars.py — previously polars-engine runs lost
    their diagram detail lines.
  • Docs: pipeline_quickstart.md gained an "Inspecting a pipeline" section
    covering the diagram labels and export paths.

v0.8.6 — Audit Closeout, Landing Redesign & SonarCloud CI

Choose a tag to compare

@github-actions github-actions released this 29 Aug 16:13
e69db6f

📦 Release

  • Version sync to 0.8.6 (app + frontend + core): root pyproject.toml,
    frontend/ml-canvas/package.json / package-lock.json, and
    skyulf-core/setup.py bumped to 0.8.6.

🧠 Core — findings closeout

  • F-21 single seed owner: one DEFAULT_RANDOM_STATE constant injected
    at fit resolution; seeds surfaced as non-tunable hyperparameter fields and
    canvas inputs (Random State / Fold Split Seed on training + ensemble
    nodes), with per-node seed docs.
  • F-13 decision-threshold tuning in TuningConfig end-to-end:
    tune_threshold grid-searches the binary cutoff after refit (gated on
    predict_proba + 2 classes + validation split), results ride TuningResult
    and are applied by TuningApplier; binary string-label tuning fixed by
    pinning pos_label in f1/precision/recall scorers. Backend forwards the
    flag in fixed/tuned modes and seeds the threshold store from training
    metrics (GET /thresholds exposes source); canvas gained the "Tune
    decision threshold" checkbox and seeded-at-training badges.
  • F-12/F-21/F-23 follow-ups: drift pages relabelled to the KS
    statistic (threshold input, table sort, alert evidence, CSV export —
    p-value kept as diagnostic only); legacy graphs omit the IterativeImputer
    seed so the core default stays the single owner; BLE001 staged rollout
    finished (per-file ignores dropped, 121 deliberate broad catches waived
    with per-site reasons).
  • F-31/F-14 closeout: lint hygiene (.values.to_numpy(), named
    statistical threshold constants, sorted __all__), backend compute and
    model-serializer seams moved to ContextVar with scoped context managers,
    optuna import cache onto a locked state object, and scanner re-export
    fixes (explicit lists replace star imports in the preprocessing shims).

🎨 Landing page — redesign, live stats & hero video

  • New landing page and theme/color redesign around the hero line "Stop
    trusting your pipeline. Verify it."; badge auto-updates from PyPI,
    example accordion open by default, gallery lightbox (prev/next, counter),
    hardened mobile menu and init script.
  • Hero badge images replaced by themed live stat pills — GitHub stars,
    downloads/month (pypistats) and total downloads (parsed from pepy's
    CORS-enabled badge SVG) — fed by keyless live APIs with static fallbacks;
    flaky shields.io badges swapped out of the README too.
  • Hero background is a muted looping video (static/video/wolf.mp4) over
    the .hero-bg fallback image, paused for reduced-motion users.
  • SEO foundations: root sitemap deployed via the docs workflow, robots.txt
    listing both sitemaps, FAQPage + sameAs structured data, website badge
    in the README.

🧪 Tests — six rounds of Codecov patch coverage

  • Six red-green rounds exercise every defensive except/fallback branch
    flagged by the Codecov patch reports: serializer capability probes,
    catalog/S3 cache fallbacks, degraded health/readiness endpoints,
    DataService polars→pandas fallbacks, isolated metric failures, tuning
    loader fallback chains and trial-error paths (incl. both directions of
    the len(fold_errors) > 1 branch), threshold-tuning gates, drift
    enrichment and ks_statistic plumbing, SHAP except path, and the
    balance-recommendation directions.

🚀 CI & quality — SonarCloud in PR Check

  • SonarCloud folded into pr_check.yml: a token-gated sonar-gate
    job detects SONAR_TOKEN presence (job-level if: cannot reference the
    secrets context — doing so fails the whole workflow file at validation)
    and drives two parallel coverage jobs (backend-coverage,
    skyulf-core-coverage) that upload Cobertura XML as artifacts;
    sonarcloud-scan downloads both and never re-runs the suites. Without
    the token everything skips silently; the dedicated test workflows stay
    the authoritative gates + Codecov source. Third-party scan action pinned
    to a full commit SHA; index.html excluded from analysis.
  • CodeQL fixes: readiness probe returns a generic error
    (py/stack-trace-exposure); pepy badge count parsed with a
    capture-group regex (js/incomplete-multi-character-sanitization).
  • ty 0.0.75: optional-import narrowing fixed with the
    TYPE_CHECKING-first idiom; CI pin raised to <0.0.76.

📚 Docs

  • Completed dual-engine-correctness initiative docs archived — all waves
    shipped (audit fixes, leakage enforcement, Polars migration, F-15
    per-fold refit, merged-branch refit, holdout/validation-split refit).
    Kept open: the fallback-shapes plan (Phase 0 telemetry done, Phases 1–4
    gated on demand) and the parked SplitDataset ownership design; the
    initiative README is rewritten around the archived/open split.

v0.8.5 — Fail-Fast Training Guard, Refit Telemetry & Canvas Help Guide

Choose a tag to compare

@github-actions github-actions released this 27 Aug 18:59
c69dc4c

📦 Release

  • Version sync to 0.8.5 (app + frontend + core): root pyproject.toml,
    frontend/ml-canvas/package.json / package-lock.json, and
    skyulf-core/setup.py bumped to 0.8.5.

🧠 Core — legible tuning failures

  • grid/random search no longer discards the original per-fold error when
    every candidate fails: the first trial error is threaded through the
    evaluation chain and appended to the All trials failed exception as
    First trial error: … — mirroring the detail optuna/halving already
    had, so a cryptic "All trials failed" now names the root cause (e.g. a
    non-numeric column reaching sklearn).

🔧 App — fail-fast guard & fold-refit telemetry

  • Non-numeric training-frame guard: before tuning starts, the training
    node preflights its frame and raises an actionable ValueError listing any
    leftover object/string/category/datetime columns — instead of dying deep
    inside sklearn with every fold error swallowed. The target column and the
    time-series CV time column are legitimately non-numeric and excluded. The
    message explains the most common cause: after a Split, merged branches
    resolve overlapping columns by pure merge order, so the last connected
    branch may have overwritten an earlier branch's encoding.
  • fold_refit_fallback metric: graphs that can't use per-fold refit and
    fall back to pre-transformed scoring now stamp a stable reason code into
    the node metrics (nested_merge, fork_not_splitter,
    learner_before_split, row_changing_branch_step, unsupported_graph,
    payload_reconstruction_failed) — demand telemetry for which unsupported
    shapes users actually hit.
  • fold_refit_audit metric: a new AuditedFoldPreprocessor
    (skyulf-core) records the input row count of every per-fold
    fit/transform; isolation_ok proves no preprocessing fit saw more rows
    than the train split, i.e. no held-out row leaked into a fit.

🎨 App — job details, help guide & canvas UX

  • Score Advisory amber tile + modal in Job Details: flags runs that fell
    back to pre-transformed scoring (optimistically biased scores) with a
    plain-language explanation mapped from the fold_refit_fallback code; a
    new Fold Refit Audit detail modal shows the per-fold isolation verdict.
    The placeholder Progress tile was removed.
  • In-app help guide: the navbar's round book button opens "How pipelines
    work" — ten sections covering linear chains, branches, merge ownership,
    post-split merge order, row alignment, Run Preview vs Run All Experiments,
    where results live, the Leakage Gate and Fold Refit Audit, the Score
    Advisory, and badge/edge-color semantics.
  • Canvas UX fixes: Preview Results gained an always-visible X close
    (dismissed until new results or validation issues arrive); Run Preview is
    always visible and explains its blockers on click; the legend button uses
    a tag icon with updated post-split merge-ownership copy and renders above
    the results panel; fixed stacking so canvas buttons no longer float over
    modals or the maximized panel, and the sidebar expand button no longer
    overlays everything; zoom controls lift above an expanded results panel.

📚 Docs

  • multi_path_pipelines.md: after a Split, merge ownership is inert —
    overlapping columns resolve by pure merge order (last connected branch
    wins every shared column); keep post-split branches disjoint or fully
    numeric.
  • troubleshooting.md: new entry for "All trials failed" /
    non-numeric-column errors after a Split; PropertiesPanel shows a matching
    post-split note under the merge-strategy select; leakage_proof_pandas.md
    cross-links to the guide.

v0.8.4 — Cyclic-Pipeline Guard & Live Tuning Charts

Choose a tag to compare

@github-actions github-actions released this 26 Aug 18:52
577078d

📦 Release

  • Version sync to 0.8.4 (app + frontend + core): root pyproject.toml,
    frontend/ml-canvas/package.json / package-lock.json, and
    skyulf-core/setup.py bumped to 0.8.4.

🧠 Core — leakage-free searcher tuning

  • optuna/halving_* tuning now refits preprocessing per fold for every
    chain — including the ones the old Pipeline wrap refused: resampling
    (SMOTE/over-/undersampling), row drops/outlier removal, and target
    re-encoding (LabelEncoder). The transformer-step wrap could only hand X
    forward, so such chains fell back to applying the chain once on the full
    training set — leaking synthetic rows / fitted target stats into the
    validation folds (measured: optuna 0.978 vs per-fold random 0.914 on the
    same SMOTE data).
  • New fit-time meta-estimator FoldAwareModelStep
    (skyulf/modeling/_tuning/fold_pipeline.py): the searcher wraps
    preprocessing + model in one estimator (fit runs the chain on the fold's
    training rows only, then fits the model), so the searcher's internal CV
    drives a true per-fold refit with no sklearn patching. When the chain
    re-encodes the target, predictions and classes_ are mapped back to the
    original label space (built from paired uniques over the whole fold — no
    sampling risk for rare classes), and predict_proba columns stay aligned
    for roc_auc-style scorers; regressors pass through untouched.
  • The runtime alignment probe and the one-shot "apply once to the full
    split" fallback are retired — the constraint they guarded no longer
    exists. The fallback narrows to frameless SDK calls (numpy-only, no named
    frames to run the chain on), which keep scoring the raw payload with an
    explicit log. Backend runs always have frames, so the app path is always
    per-fold now; grid/random (already per-fold) and fork-join
    merged-branch screening are unchanged.
  • Holdout tuning with a validation split is leakage-free now — the last
    remaining optimistic path. Before, the backend skipped per-fold refit
    entirely whenever data.validation existed and the engine refused
    preprocessing + validation_data together, so candidates trained on the
    already-preprocessed full train frame (SMOTE/WOE statistics fitted with
    held-out rows included). Now all five strategies refit the chain on the
    train rows only and score candidates against the untouched validation
    split: the pre-transform train and validation frames are concatenated into
    one search frame with a single PredefinedSplit fold (train masked -1,
    validation = the one scoring fold). The backend reconstructs the
    pre-transform validation payload alongside the train one and threads it
    through fit_predict(..., preprocessing_validation=...)
    TuningCalculator.fit/tune(..., validation_frames=...); post-tuning CV
    gets the adapter too instead of scoring raw train. Note: on
    leakage-dominated graphs (e.g. target-aware encoders) reported holdout
    best_scores will drop — same dynamic as the v0.8.2 note; the drop is the
    bias leaving. SDK callers without named validation frames keep today's
    logged raw-payload fallback.
  • Fold-aware wrap dtype fix: polars payloads handed to the searcher now
    convert through to_pandas() inside FoldAwareModelStep; the old
    np.asarray rebuild collapsed mixed-type frames to object dtype and
    silently disabled numeric steps (a SimpleImputer + WOE chain left NaNs
    untouched and crashed the candidate fits).
  • Search-space routing moved to model__estimator__<param>; best_params
    and trial params are stripped back to the caller's original keys.
  • Red-green coverage: meta-estimator unit tests (label-space round trip,
    SMOTE-style row shaping, deepcopy isolation for n_jobs > 1, regressor
    passthrough, array-input frame rebuild, f1/roc_auc through a real
    searcher), a fold-leakage proof (spy adapter asserts every
    fit_transform sees only its fold's training rows and every validation
    fold is scored through transform) for optuna and halving_random, and an
    end-to-end XGBoost + string-target optuna run matching the grid score on
    the same folds.

🔧 App — cyclic graphs fail fast at both layers

  • Connect-time checks already blocked cycles one edge at a time, but bulk
    graph loads (saved projects) applied nodes/edges verbatim and the backend
    never validated topology — a loop died late mid-run with a cryptic
    "Artifact not found". Both layers now reject whole-graph cycles up front:
    • Backend: _cycle_validation.py runs Kahn's algorithm over node inputs,
      prunes the stuck set down to the exact loop members (nodes merely
      downstream of the loop are not blamed), and raises PipelineCycleError
      naming the loop. Called in engine.run() before the leakage check,
      whose descendant-map build bails out on cycles.
    • Frontend: pipelineCycleValidation.ts mirrors the guard and feeds a new
      'cycle' category into collectGraphValidationIssues, so the validation
      panel flags loops from bulk-loaded graphs instantly and preview/training
      submission stays blocked until the loop is removed.
  • Red-green coverage: 8 backend unit tests, 1 engine integration test
    (nothing runs, no artifacts written), 8 frontend tests.

📈 App — live tuning trial chart

  • Tuning jobs now draw their trial progress while they run: the training
    runner publishes a structured trial job event per completed trial
    (grid/random and Optuna paths) over the existing /ws/jobs channel, and
    the job details view renders an updating per-trial score + best-so-far
    chart (useTuningTrials + TuningTrialsChart) with a live Trial x/y
    progress tile.
  • Completed jobs redraw the same chart from the already-persisted
    metrics.trials — no new persistence. Fixed runs (single trial) and
    callback-less strategies (halving) degrade honestly: no live curve,
    completed halving jobs still redraw from their recorded trials.
  • Privacy-safe by construction: the channel broadcasts to every client
    without auth, so trial events carry aggregate scalars only (trial index,
    total, score, metric) — never hyperparameters or data. Emission failures
    are swallowed; training is never impacted.
  • Late openers are backfilled: the WebSocket only reaches already-connected
    clients, so the runner also records each trial in a bounded in-memory
    per-job buffer (LRU-evicted, capped) served by
    GET /api/pipeline/jobs/{id}/trials; opening a running job fetches that
    snapshot and the curve starts at trial 1, with live events merged on top
    (deduped by trial number). Once a job turns terminal the complete
    persisted metrics.trials list always wins over the partial live tail,
    so watching a job to completion no longer leaves a truncated curve.

📈 App — live boosting iteration chart

  • Fixed boosting runs (XGBoost / LightGBM classifiers and regressors) now
    stream a per-iteration live chart exactly like tuning runs stream their
    trial curve: the calculators attach a native callback
    (XgboostIterationAdapter / LightGBMIterationAdapter in core) and an
    eval_set=[(X_train, X_train)] used purely for display — no early
    stopping is configured, and a test verifies predict_proba is identical
    with and without the callback. XGBoost 3.x dropped fit(callbacks=...),
    so the adapter is set as the estimator's callbacks attribute and
    detached again after fit so pickled artifacts never carry live closures.
  • Per explicit decision, the final refit pass of multi-trial tuning runs
    streams its iterations too, after the trial curve completes; candidate
    fits inside the search stay silent. Fixed runs route through the same
    engine (n_trials=1), so both paths share one code path.
  • New iteration job event carries iteration_direction
    ("minimize"/"maximize") because boosting scores are usually losses — the
    chart's best-so-far runs a running min for minimize metrics (LightGBM's
    is_higher_better is used verbatim; XGBoost metric names fall back to a
    lower-is-better heuristic).
  • The runner persists metrics["iterations"] (plus iteration_metric /
    iteration_direction) post-fit, mirroring metrics["trials"], so
    completed jobs redraw without new storage; the live backfill buffer and
    GET /jobs/{id}/trials snapshot grew a parallel iterations field.
  • Scope halved on purpose: XGBoost + LightGBM only. CatBoost is not
    registered anywhere and HistGradientBoosting has no callback API; both
    remain non-goals alongside DL epoch curves.

📈 App — Trials/Iterations tabs for dual-series jobs

  • A tuned boosting job runs two series — the search's trials, then the
    refit's boosting iterations — and used to collapse to one chart
    (iterations outranked trials). The job details view now keeps both:
    useTuningTrials returns two independent slices plus the actively
    streaming activeKind, and the view renders a small Trials / Iterations
    tab row whenever both series have chartable points.
  • The view auto-follows the streaming series (trials during the search,
    passing to iterations when the refit starts) until the user clicks a tab,
    which pins that series; the pin resets when switching jobs. The progress
    tile (Trial x/y vs Iteration x/y) tracks the visible series.
  • Single-series jobs are unchanged: no tabs, chart behaves as before
    (trials for any tuned job; iterations for fixed boosting runs).
  • Coverage: trials appear for every tuned job regardless of problem type
    or algorithm; iterations only for xgboost_* / lgbm_* (the only
    calculators with native callback APIs); segmentation never tunes, so it
    shows neither.

🧠 Core — console trial progress for SDK-only use

  • TuningConfig(progress=True) gives core-only users a tidy console read of
    a tuning run without hand-writing a progress_callback: on a TTY a single
    self-updating line (Tuning trial 12/60 | score 0.8530 | best 0.8710 (#8)),
    and on completion a compact summary (best score + params, top-...
Read more