API reference¶
Rendered from source. assaylab keeps a light base wheel; heavy surfaces
(LLM providers, REST) are lazy-imported behind extras.
Models¶
models ¶
assaylab domain models, built on the agentsensory contract.
The test domain has no pixel (BBox) or time (Span) grounding, so each
issue grounds itself in detail (a JSON-safe dict) carrying the signature id,
affected tests, and file loci. Report / Issue subclass the agentsensory
base types so every result speaks the shared pass/warn/fail + Handoff
language.
Outcome ¶
Bases: str, Enum
Normalized outcome of a single test execution.
IssueKind ¶
Bases: str, Enum
assaylab issue kinds (the domain vocabulary carried in Issue.kind).
IssueSource ¶
Bases: str, Enum
Which analysis produced an issue.
TestRecord ¶
Bases: BaseModel
One test execution, normalized across ingestion formats.
This is the universal internal schema every backend maps onto:
(project, commit, test_id, run_id, outcome, duration, message, stacktrace).
FailureSignature ¶
Bases: BaseModel
A cluster of failing executions that share a normalized fingerprint.
The fingerprint is a stable hash of the templated (variable-stripped) error type + message + top stack frames — so runs that differ only in addresses, line numbers, temp paths or timestamps collapse together.
Issue ¶
Bases: IssueBase
assaylab issue — an agentsensory.IssueBase with test-domain grounding in detail.
Report ¶
Bases: ReportBase
assaylab report — an agentsensory.ReportBase (verdict + issues + handoff).
Core sense¶
analyze ¶
Grade test records into an agentsensory :class:Report.
P1 verdict rule (deterministic):
- no failing records -> PASS
- failing records present -> FAIL, one issue per failure signature
- a signature absent from a supplied baseline is marked
NEW_FAILURE(higher risk) rather than a plainFAILURE_SIGNATURE.
Flaky-vs-real downgrading to WARN arrives in P2; the hook (baseline diff) is already here.
grade_records ¶
grade_records(
records: list[TestRecord],
*,
baseline_ids: set[str] | None = None,
backend: str = "unknown",
) -> Report
Cluster failures and return a graded :class:Report.
analyze
async
¶
analyze(
source: str,
*,
backend: str | None = None,
baseline: str | None = None,
settings: Settings | None = None,
) -> Report
Ingest source (and optional baseline) and grade it.
signature ¶
Failure-signature normalization and clustering.
Given failing :class:TestRecord executions, collapse the ones that differ only
in incidental detail — memory addresses, object hashes, line numbers, temp
paths, timestamps, UUIDs, numbers — into a single :class:FailureSignature
identified by a stable fingerprint. This is the evidence layer everything else
(RCA, flaky classification, confidence bounds) grounds on.
Pure-stdlib and deterministic: the same corpus always yields the same signature ids, so verdicts are reproducible.
normalize ¶
Strip incidental variation, returning a stable template string.
fingerprint ¶
Return (signature_id, template, exception_type, files) for a failing record.
The template combines the normalized message with normalized top frames, so two failures with the same root but different incidental text collapse.
cluster ¶
Cluster failing records into signatures, ordered by descending run count.
Passing/skipped records are ignored (only failures carry a signature).
Root-cause analysis¶
rootcause ¶
Root-cause categorization.
A transparent, always-available baseline: map a failure signature to a
root-cause category with a confidence and the evidence that fired. This is the
interpretable floor the learned model (model.py) is measured against, and
what grounds every RCA verdict so a human can audit why a cause was assigned.
categorize ¶
Assign a root-cause category to a signature with confidence + evidence.
flaky ¶
Flaky-vs-real classification.
A failure signature is flaky-leaning when its affected tests behave non-deterministically — the strongest evidence being a single commit that shows both a pass and a failure (order-agnostic flakiness, à la iDFlakies), backed by a high flip-rate across runs. Real failures are consistent at a given commit.
The heuristic here is always available; :func:flaky_probability optionally
defers to a learned model when one is supplied.
flaky_probability ¶
flaky_probability(
features: dict[str, float],
tests: list[str],
stats: dict[str, TestStats],
model: object | None = None,
) -> FlakyVerdict
Learned probability when model is given, else the heuristic.
risk ¶
Per-test risk scoring and next-run failure forecasting.
Risk blends recency-weighted failure rate with instability (flip-rate): a test
that fails often and recently is high risk; a rarely-failing stable test is
low risk. forecast is the model's estimate that the test fails on its next
run, used later (P3) to bound the confidence lost when a test is skipped.
rank_risk ¶
All tests ranked by descending risk.
model ¶
A small, self-contained logistic-regression model for flaky prediction.
Pure Python (batch gradient descent) — no numpy/scikit-learn in the base wheel, and the trained model serializes to JSON, never pickle. That matters: a pickled model is arbitrary-code-execution on load, so a model pulled from a registry or CI cache would be a supply-chain hole. JSON weights load inertly.
Features are addressed by name (a dict), so callers never depend on column order. Unknown/missing features default to 0.0. Inputs are standardized using means/stds captured at fit time.
For heavier models, train externally and export coefficients into this JSON shape; prediction stays dependency-free.
train ¶
train(
samples: list[tuple[dict[str, float], int]],
*,
feature_names: list[str] | None = None,
epochs: int = 400,
lr: float = 0.3,
l2: float = 0.001,
) -> LogisticModel
Fit a logistic model on (features, label) pairs (label in {0,1}).
Deterministic: no randomness, weights start at zero, so the same data always yields the same model.
Attested selection¶
engine ¶
Test-selection engine with a verifiable confidence bound.
Each candidate test t has a detection probability q_t — the modelled
chance it fails (catches a regression) on this run, estimated from history
(P2 forecast). If we run a subset S and skip U = All \ S, the
probability that at least one skipped test would have failed is
epsilon = 1 - prod_{t in U} (1 - q_t)
under an independence assumption. epsilon is the confidence lost by
skipping — an upper-bound proxy for the chance we miss a regression the suite
would have caught. Selection keeps the highest value-density tests
(q_t per second) until either epsilon <= target_epsilon or the
time_budget_s is exhausted; the achieved epsilon is always reported.
Honest limits (stated, not hidden): independence of failures, q_t
stationarity, and coverage only of regression classes seen historically. These
are the receipt's residual assumptions, not guarantees.
select ¶
select(
candidates: list[Candidate],
*,
target_epsilon: float | None = None,
time_budget_s: float | None = None,
) -> Selection
Choose a subset to run under a confidence target and/or a time budget.
Exactly one of target_epsilon / time_budget_s should be the primary
driver; if both are given, selection stops as soon as either is satisfied
(target met) or violated (budget spent).
receipt ¶
The attested receipt: a signed commitment to a test-selection outcome.
The signature (HMAC-SHA256) covers the result — the inputs hash, the selected
and skipped sets (by hash + count), and the computed confidence bound epsilon
— so it binds what actually happened, not merely that a run occurred. Verifying
is constant-time and, given the candidate inputs, re-derivable: a consumer can
recompute epsilon from the committed inputs and check it matches.
Receipt ¶
keys ¶
Signing-key resolution — env, else a persisted per-installation random key.
Never a hardcoded/default secret (a shipped default key lets anyone forge a receipt). Resolution order:
ASSAYLAB_SIGNING_KEY— an explicitly encoded key:hex:<hex>,base64:<b64>, orraw:<utf8>(or a bare value, treated as raw). The encoding is never guessed, so one string can't resolve to two different keys. Must carry >= 16 bytes and not be a degenerate (single-byte-repeated) value.- A per-installation key persisted at
<user_config>/assaylab/signing.key, created with :func:secrets.token_bytesand0600perms on first use.
Both paths refuse to follow symlinks and reject files not owned by the current user, closing the symlink/TOCTOU forgery vector. A malformed env key fails closed (raises) rather than silently falling back.
Test generation & self-healing¶
models ¶
The Proposal: an LLM artifact assaylab never executes or applies.
Proposal ¶
Bases: BaseModel
A dry-run suggestion + its provenance and acceptance criterion.
applied is always False: assaylab produces the proposal and defines how
to verify it, but a human/CI applies and runs it in their own sandbox. The
verdict layer decides acceptance (see :mod:assaylab.llm.gate).
signed_body ¶
Canonical bytes covered by the signature (everything but the signature).
generate ¶
Propose a regression test from a failure signature (dry-run).
gate ¶
Gate a proposal against a REAL run — acceptance flows through the verdict layer.
assaylab never runs the proposed code. Instead the user applies + runs it in their own sandbox and feeds the resulting test output back here; the proposal is accepted only if that graded run meets its acceptance criterion. This is what keeps LLM output honest: a generated test counts only if it actually reproduces the bug; a heal counts only if the flaky signature stops failing.
evaluate_proposal ¶
evaluate_proposal(
proposal: Proposal,
result_source: str,
*,
backend: str | None = None,
settings: Settings | None = None,
) -> Evaluation
Grade the returned run and decide whether the proposal is accepted.
The proposal is treated as UNTRUSTED: its signature must verify (it was signed at generation by the tool that derived the criteria from a real signature), or we refuse to grade it. This stops a hand-crafted proposal JSON from weakening its own acceptance criteria.