Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0706103
test(cat): reject callback-bearing administration evidence
seonghobae Aug 24, 2026
8a564d0
fix(cat): seal administration evidence before NumPy protocols
seonghobae Aug 24, 2026
454ffaa
test(cat): preserve trusted administration compatibility
seonghobae Aug 24, 2026
995784e
docs(cat): record administration evidence boundary
seonghobae Aug 24, 2026
4308892
test(cat): reject impossible administrations before dense conversion
seonghobae Aug 24, 2026
f4ee035
fix(cat): preflight administration shape and length
seonghobae Aug 24, 2026
d1cac77
fix(cat): install administration resource preflight
seonghobae Aug 24, 2026
4ede92e
chore(cat): keep resource preflight lint-clean
seonghobae Aug 24, 2026
027c2cf
docs(cat): record bounded administration preflight
seonghobae Aug 24, 2026
a61ce4a
test(cat): bound standard-error administration before value scan
seonghobae Aug 24, 2026
cb1736b
fix(cat): bound standard-error administration before value scan
seonghobae Aug 24, 2026
145c0be
fix(cat): bind standard-error resource guard on public alias
seonghobae Aug 24, 2026
160cd49
docs(cat): record standard-error administration preflight
seonghobae Aug 24, 2026
a099969
test(cat): preserve standard-error deduplication semantics
seonghobae Aug 24, 2026
38e3f20
fix(cat): preserve standard-error deduplication semantics
seonghobae Aug 24, 2026
c8b613c
docs(cat): record standard-error mask compatibility
seonghobae Aug 24, 2026
90b7151
test(cat): bound standard-error mask preflight
seonghobae Aug 24, 2026
f7d4b86
fix(cat): bound standard-error mask evidence
seonghobae Aug 24, 2026
7bd842b
docs(cat): record standard-error mask bound
seonghobae Aug 24, 2026
1167074
Merge protected main into CAT evidence hardening
seonghobae Aug 24, 2026
b6e27da
test(cat): reject over-bank before response provider
seonghobae Aug 24, 2026
8385299
fix(cat): bound administration before response carrier
seonghobae Aug 24, 2026
ebcf17f
docs(cat): record response-independent administration bound
seonghobae Aug 24, 2026
2bc7ba2
merge: current main into cat-administration evidence lane
seonghobae Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/changelog.d/1344-cat-administration-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Seal dichotomous CAT administration evidence

## Fixed

- Reject callback-bearing top-level array providers, ndarray/container subclasses, and non-real storage for partial CAT administered-item and response evidence before NumPy materialization or Rust ability-estimation dispatch.
- Preserve exact NumPy and ordinary built-in list/tuple numeric evidence, including concrete NumPy scalar compatibility, while retaining lossless signed-64 item-index validation, item range/uniqueness rules, and the exact 0/1 response contract.
- Reject over-rank, length-mismatched, and structurally impossible partial administrations from inert container metadata before value-wise scans or dense `int64`/`float64` marshalling; a validated EAP/MLE administration cannot exceed the calibrated bank item count because administered identities must be unique.
- Apply the over-bank EAP/MLE administration bound before inspecting the response carrier, so an unsupported response provider cannot force dense validation of an already impossible administration.
- Preserve `ability_standard_error`'s historical set-valued mask semantics: duplicate-laden and multidimensional administered evidence is normalized losslessly and deduplicated with `np.unique`, so the uniqueness-specific raw-length/rank preflight is not applied to that surface.
- Bound `ability_standard_error` administered-mask evidence to 20,000,000 logical cells from inert exact-container metadata before signed-64 value scanning, dense conversion, or `np.unique`, without imposing EAP/MLE uniqueness or rank semantics on the set-valued mask.
- Keep CAT probability, likelihood, EAP/MLE posterior/scoring, Fisher-information selection, stopping, and uncertainty arithmetic Rust-owned; this change is Python validation, bounded materialization, and marshalling only.
6 changes: 6 additions & 0 deletions python/fast_mlsirm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from importlib.metadata import version as _distribution_version

from . import _legacy_init as _legacy_init
from . import cat as _cat
from . import cdm as _cdm
from . import exposure as _exposure
from . import inference as _inference
from . import reliability as _reliability
from . import scaling as _scaling
from . import serving as _serving
from . import validation as _validation
from ._cat_administration_resource_safety import install as _install_cat_administration_resource_safety
from ._cdm_response_safety import install as _install_cdm_response_safety
from ._exposure_array_safety import install as _install_exposure_array_safety
from ._exposure_flexilevel_safety import install as _install_exposure_flexilevel_safety
Expand All @@ -26,6 +28,7 @@
# Harden historical public adapters before copying legacy exports. These
# wrappers validate and normalize semantic controls only; result arithmetic
# remains in the existing Rust-backed implementations.
_install_cat_administration_resource_safety(_cat)
_install_exposure_array_safety(_exposure)
_install_exposure_flexilevel_safety(_exposure)
_install_cdm_response_safety(_cdm)
Expand All @@ -34,6 +37,7 @@
_install_scaling_control_safety(_scaling)
_install_fleiss_control_safety(_validation)
_install_serving_export_safety(_serving)
_legacy_init.ability_standard_error = _cat.ability_standard_error
_legacy_init.ccat_select = _exposure.ccat_select
_legacy_init.flexilevel_administer = _exposure.flexilevel_administer
_legacy_init.flexilevel_score_distribution = _exposure.flexilevel_score_distribution
Expand All @@ -48,8 +52,10 @@
_legacy_init.export_serving_bundle = _serving.export_serving_bundle

del (
_cat,
_exposure,
_inference,
_install_cat_administration_resource_safety,
_install_exposure_array_safety,
_install_exposure_flexilevel_safety,
_cdm,
Expand Down
121 changes: 121 additions & 0 deletions python/fast_mlsirm/_cat_administration_resource_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Resource preflight for dichotomous CAT administration evidence.

The CAT numerical owner remains Rust. This module rejects structurally
impossible EAP/MLE administrations and oversized standard-error mask evidence
from inert container metadata before existing CAT validators perform value-wise
scans or dense dtype marshalling.
"""

from __future__ import annotations

from types import ModuleType
from typing import Any

import numpy as np

_SHAPE_ERROR = "administered and responses must be 1D arrays of equal length"
_LENGTH_ERROR = "administration length cannot exceed item bank size"
_STANDARD_ERROR_RESOURCE_ERROR = (
"ability_standard_error administered evidence exceeds resource limit"
)
_MAX_STANDARD_ERROR_ADMINISTERED_CELLS = 20_000_000
_VALIDATE_MARKER = "__fast_mlsirm_cat_administration_resource_safe__"
_STANDARD_ERROR_MARKER = "__fast_mlsirm_cat_standard_error_resource_safe__"


def _vector_length(value: object) -> int | None:
"""Return inert one-dimensional length, or ``None`` for unsupported carriers."""
if type(value) is np.ndarray:
if value.ndim != 1:
raise ValueError(_SHAPE_ERROR)
return int(value.shape[0])
if type(value) in (list, tuple):
return len(value)
return None


def _standard_error_logical_cells(value: object) -> int | None:
"""Return inert mask size without imposing EAP/MLE rank or uniqueness rules."""
if type(value) is np.ndarray:
return int(value.size)
if type(value) in (list, tuple):
return len(value)
return None


def _reject_over_bank_administration(bank: Any, administered: object) -> None:
"""Reject exact-carrier administrations that cannot be unique in the bank."""
administered_length = _vector_length(administered)
if administered_length is None:
return
n_items = int(np.asarray(bank.b).shape[0])
if administered_length > n_items:
raise ValueError(_LENGTH_ERROR)


def install(cat_module: ModuleType) -> None:
"""Install fail-fast CAT resource preflights idempotently."""
original_validate = cat_module._validate_administration
if not bool(getattr(original_validate, _VALIDATE_MARKER, False)):

def safe_validate_administration(
bank: Any,
factor_id: np.ndarray,
administered: object,
responses: object,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
# Unsupported administered providers retain the original callback/type
# rejection contract. Once administered exposes inert exact-container
# metadata, reject structurally impossible unique administrations before
# inspecting the response carrier so an invalid/unsupported response
# cannot force an oversized administered vector through dense validation.
administered_length = _vector_length(administered)
if administered_length is None:
return original_validate(bank, factor_id, administered, responses)

_reject_over_bank_administration(bank, administered)

responses_length = _vector_length(responses)
if responses_length is None:
return original_validate(bank, factor_id, administered, responses)

if administered_length != responses_length:
raise ValueError(_SHAPE_ERROR)

return original_validate(bank, factor_id, administered, responses)
Comment thread
seonghobae marked this conversation as resolved.

setattr(safe_validate_administration, _VALIDATE_MARKER, True)
cat_module._validate_administration = safe_validate_administration

original_standard_error = cat_module.ability_standard_error
if not bool(getattr(original_standard_error, _STANDARD_ERROR_MARKER, False)):

def safe_ability_standard_error(
bank: Any,
factor_id: np.ndarray,
theta: np.ndarray,
*,
administered: object | None = None,
model: str = "MLS2PLM",
) -> np.ndarray:
# Standard-error administration is a set-valued mask rather than the
# unique 1-D EAP/MLE history. Preserve duplicate and multidimensional
# exact NumPy evidence, but bound its logical size before signed-int64
# value scans, dense conversion, and np.unique deduplication.
if administered is not None:
logical_cells = _standard_error_logical_cells(administered)
if (
logical_cells is not None
and logical_cells > _MAX_STANDARD_ERROR_ADMINISTERED_CELLS
):
raise ValueError(_STANDARD_ERROR_RESOURCE_ERROR)
return original_standard_error(
bank,
factor_id,
theta,
administered=administered,
model=model,
)

setattr(safe_ability_standard_error, _STANDARD_ERROR_MARKER, True)
cat_module.ability_standard_error = safe_ability_standard_error
Comment thread
seonghobae marked this conversation as resolved.
73 changes: 65 additions & 8 deletions python/fast_mlsirm/cat.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,67 @@ def _query_params(bank: MLSIRMParams, theta_rows: np.ndarray) -> MLSIRMParams:
)


def _trusted_numeric_vector(
values: object,
*,
error: str,
allow_bool: bool,
) -> np.ndarray:
"""Materialize a one-dimensional numeric vector only after inert admission.

Exact NumPy arrays are safe to inspect through dtype metadata without
invoking caller protocols. Exact built-in lists/tuples are traversed only
after their container identity is established; every leaf must be an exact
built-in numeric scalar or the canonical concrete NumPy scalar type for its
dtype. This keeps caller-defined ``__array__``/numeric conversion methods
outside the CAT evidence boundary.
"""

allowed_kinds = {"i", "u", "f"}
if allow_bool:
allowed_kinds.add("b")

if type(values) is np.ndarray:
raw = values
if raw.dtype.kind not in allowed_kinds:
raise ValueError(error)
return raw
if type(values) not in (list, tuple):
raise ValueError(error)
Comment thread
seonghobae marked this conversation as resolved.

for value in values:
value_type = type(value)
if value_type is bool:
kind = "b"
elif value_type is int:
kind = "i"
elif value_type is float:
kind = "f"
else:
try:
dtype = np.dtype(value_type)
except (TypeError, ValueError):
raise ValueError(error) from None
if dtype.type is not value_type:
raise ValueError(error)
kind = dtype.kind
if kind not in allowed_kinds:
raise ValueError(error)

try:
return np.asarray(values)
except (OverflowError, TypeError, ValueError) as exc:
raise ValueError(error) from exc


def _lossless_signed_int64_indices(values: np.ndarray) -> np.ndarray:
"""Normalize item indices without allowing signed-64 narrowing to wrap."""
"""Normalize item indices without allowing callbacks or signed-64 wrap."""

raw = np.asarray(values)
if np.iscomplexobj(raw):
raise ValueError("administered item indices must be integers")
raw = _trusted_numeric_vector(
values,
error="administered item indices must be integers",
allow_bool=False,
)
kind = raw.dtype.kind
if kind == "u":
if raw.size and np.any(raw > _INT64_MAX):
Expand All @@ -203,11 +258,13 @@ def _lossless_signed_int64_indices(values: np.ndarray) -> np.ndarray:


def _real_response_array(responses: np.ndarray) -> np.ndarray:
"""Normalize response data only after proving no imaginary component can be lost."""
"""Normalize response data after proving it is inert real-numeric evidence."""

raw = np.asarray(responses)
if np.iscomplexobj(raw):
raise ValueError("responses must be real-valued")
raw = _trusted_numeric_vector(
responses,
error="responses must be real-valued",
allow_bool=True,
)
try:
return np.asarray(raw, dtype=np.float64)
except (OverflowError, TypeError, ValueError) as exc:
Expand Down
Loading
Loading