v0.8.13
-
Ruff's
F401(unused-import) rule is now enabled repo-wide: the lint set
previously covered only theF63/F7/F82subsets of pyflakes, so orphaned
imports went unreported byruff check,ruff formatandty— 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# noqaonly 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_ENVno longer silently boots the development security
posture (OC-130): the value selected a settings profile through a lookup that
fell back toDevelopmentSettingsfor anything unrecognized, soprod,prd,
staging— or a trailing space picked up from a YAML/CI variable — started the
server withDEBUG=True,CORS_ORIGINS=["*"], no security headers and the
productionSECRET_KEYcheck skipped. The wildcard origin is worse than it
looks: the app also setsallow_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_ENVis not aSettings
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 blankFASTAPI_ENVnow 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_errorhelper 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 foundmessage wholesale withredacted sensitive S3 errorand destroying the only useful part of the diagnostic. Both copies
are replaced by oneredact_credentials()helper beside the existing log
injection guard, which matches on value shape rather than setting name:
20-character AWS access key IDs,name=valueandname: valueassignments 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_updateon 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, ortext()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, anydropna) 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 arow_count_mismatchadvisory carrying the per-input row counts and
rendered in the canvas merge banner with the remedy. Separately, the
first_winsstrategy 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']underlast_winsand
['c','d','a','b']underfirst_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 readn_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 readrfe · 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 internaln_features_to_selectspelling, leaving thek
path the UI actually sends with no coverage at all. RFE now acceptskas an
alias, with an explicitn_features_to_selecttaking precedence, so the
canvas, direct API calls and notebooks all agree. Added 3 regression tests
including an end-to-end case over 6 features.