Skip to content

Protected evaluation for CI decisions

Existing COLEMAN analysis reports summarize native TCPCI output. A ranking or an average over repeated seeds is descriptive; it does not establish independent scientific evidence. This additive library API separates chronological eligibility, frozen candidate/protocol declarations, recorded holdout access and paired inference. It uses the versioned result contract, existing native runner extensions, sinks, artifacts and manifests. No scheduling algorithm or legacy result meaning is replaced.

Temporal eligibility

from coleman.evaluation.protected import TemporalObservation, TemporalPlan

plan = TemporalPlan.model_validate({
    "train_end": "2026-01-02T23:59:59Z",
    "validation_end": "2026-01-04T23:59:59Z",
    "test_end": "2026-01-06T23:59:59Z",
})
late = TemporalObservation.model_validate({
    "observation_id": "repository-a/build-1",
    "group_id": "repository-a",
    "event_at": "2026-01-02T12:00:00Z",
    "available_at": "2026-01-03T12:00:00Z",
})
assert not plan.training_eligible(late)
assert plan.decide(late).reason == "outcome_not_available"

Train is (-infinity, train_end], validation (train_end, validation_end], and test (validation_end, test_end]. Cutoffs must increase strictly. Zoned ISO timestamps are normalized to UTC; naive/date-only times and numeric epochs/strings fail. Event time fixes the nominal partition: late labels never move training events into validation/test. Both label availability and the entire closed target interval must be within that partition's cutoff. This conservative purge excludes a training target extending into validation or test, even if an early outcome is already known.

TemporalPlan.split(tuple_of_metadata) returns sorted typed decisions with an eligibility flag and reason (eligible, outside_window, outcome_not_available, overlapping_target). It rejects groups spanning nominal partitions, including late rows, duplicate revision identities, and revisions changing event/group identity. Post-cutoff revisions are retained as excluded decisions rather than overwriting legitimate historical observations. It does not choose or aggregate competing revisions; a producer must predeclare its point-in-time revision selection.

plan.observation(mapping) is a dataset-boundary adapter. Configure event_field, availability_field, group_field, observation_field, revision_field and target_end_field for an existing dataset. Required metadata missing from a mapping raises KeyError; invalid values raise Pydantic ValidationError. Optional target_end defaults to a point interval at the event, and revision_id defaults to original. Other dataset columns stay outside the metadata contract.

Freeze before protected outcomes

FrozenCandidate.from_configuration accepts a complete JSON configuration at the boundary and stores canonical JSON bytes in an immutable typed object. Include all hyperparameters, preprocessing/feature definitions, learned-state fingerprints and external dependency versions used by that candidate. A hash cannot freeze an undeclared dependency or prove that an external fit did not inspect future labels.

freeze takes typed candidates, the canonical dataset fingerprint, the temporal plan, an EvaluationProtocol and a tuple of ComparisonHypothesis declarations. The result is a FrozenEvaluation; persist it with EvaluationAudit.save_freeze before running or evaluating the candidates. Changing data, candidates, thresholds, units, assumptions, plan or family changes its SHA-256 fingerprint. Candidate and hypothesis declaration order is canonicalized; duplicate/self/reversed/unfrozen comparisons are rejected. Never add a favorable hypothesis after reading outcomes.

The protocol declares:

  • metric_name, unit, meaning, and objective direction;
  • independent_unit and dependence (independent, serial, clustered);
  • explicit normal_differences_assumed, required by the v1 inference method;
  • nonnegative practical_effect in the metric's own unit, family-wise alpha, and minimum_units (default 20, always at least 2);
  • a stable holdout_id, retained across candidates, protocol revisions and dataset revisions exposing the same protected observations.

For NAPFD/APFDc, larger is better and the unit is dimensionless 1. Native cost is historically named but contains APFDc, not monetary expense. Actual execution cost needs its own measured unit/meaning and minimization objective.

named_seed(root_seed, candidate_id, purpose) hashes canonical named input into 64-bit entropy compatible with NumPy, Polars and Python RNGs. Candidate/worker ordering does not affect the stream. Use distinct purposes for training, selection, evaluation or dataset-specific execution. It does not modify existing run IDs, sweep semantics or legacy native RNG derivation, and does not make repeated seeds independent evidence. Truncated seeds are reproducibility tools, not unique IDs; content identities continue to use full SHA-256 fingerprints.

Access history and interrupted work

EvaluationAudit(artifact_root) stores immutable frozen contracts, access receipts and completed reports through the existing ArtifactWriter. evaluate reserves a test/confirmatory access before invoking the outcome loader. Every invocation counts, including attempts whose loader/validation/inference later fails. A retry after publication cannot claim a previously untouched holdout. Calling audit.access also records train/development, validation/tuning and test/verification accesses. Any test exposure, including tuning or verification, counts toward reuse.

Keep a shared durable audit root and stable holdout ID for every workflow using the same protected observations. Changing candidates, protocol or dataset fingerprint does not reset a holdout ID's exposure count. Creating a different root or disguising the holdout with another ID cannot establish fresh evidence. This is an audit of calls through these APIs; it is not access control over files, proof of undisclosed external access, or a cryptographically authenticated tamper seal.

Reservations form a validated ordinal/hash chain. Concurrent processes compete on atomic create-if-absent publication and retry with a fresh chain snapshot; no lock file survives interruption. One contender records the first test access. Temporary files are ignored. Missing internal entries, changed predecessors, corrupt content and unknown versions fail closed. External removal of an entire history cannot be detected by that history alone; protect/retain the audit root as part of the research record. Local hard-link/directory-fsync requirements and power-loss limitations are the same as domain artifacts.

A reservation is an attempted access, not a completed evaluation. save_report requires a matching persisted receipt and frozen contract, then atomically publishes validated report content. Point estimates must be present for uncertainty or confirmatory claims, follow the protocol-defined sign (relative tolerance 1e-12, zero absolute tolerance) and lie within their reported interval. Report validation recomputes Holm corrections from the complete frozen raw p-value family (unavailable tests count as p=1); stored adjusted values must match that deterministic calculation exactly. Every report embeds its versioned raw records and verifies their logical keys and content digests. Reading or persisting a report recomputes the complete frozen method from those records. Scalar agreement permits relative 1e-12 and absolute 1e-15 numerical variation across compatible libraries; counts, reasons and conclusions must agree exactly, so a threshold crossing fails closed. Failed evaluation leaves the attempt and creates no completed report. Replaying the same report/sink publication is idempotent.

flowchart TD
    Metadata[Point-in-time metadata] --> Split[Typed split and exclusion decisions]
    Candidates[Complete candidate configurations] --> Freeze[Immutable frozen protocol and family]
    Split --> Freeze
    Freeze --> Runner[Existing native runner extension]
    Runner --> Records[Versioned raw CI measurements]
    Freeze --> Reserve[Durable access receipt before outcome loading]
    Reserve --> Load[Typed protected outcome loader]
    Records --> Load
    Load --> Check[Unit, provenance, pairing and temporal validation]
    Check --> Compare[Paired effects, Holm p-values, simultaneous intervals]
    Compare --> Report[Versioned report with embedded raw records, keys and digests]
    Report --> Artifacts[Existing ArtifactWriter, local sinks and manifest]

Paired method and conservative conclusions

PairedObservation links a candidate ID, TemporalObservation and DomainRecord. The metadata's group is the independent unit. Exactly one result per candidate/unit is accepted, with the same metadata across the pair. Repeated seeds/revisions on one unit raise ValueError. Raw record logical keys must also be globally unique across every candidate and unit in the report, even if candidate names, metadata labels, provenance, values or content digests differ; use a predeclared upstream aggregation if replicates are required. The evaluator verifies frozen dataset/configuration/context fingerprints, observed availability and metric units/meaning/objective before calculating a comparison. Unpaired units and incompatible measurements fail.

Version 1 provides paired Student-t inference for a declared normal population of independent pair differences. Serial/clustered units or an undeclared normality assumption raise NotImplementedError before loading outcomes. Adjacent builds from one real repository generally need a justified dependence model; labeling them with different seeds does not supply one. No automatic normality or independence detector is provided. Normality/independence declarations are assumptions to justify in a study, not certifications inferred by the library.

The implementation applies a one-sample t procedure to paired differences, equivalent to SciPy's paired t procedure. Holm's step-down Bonferroni adjustment controls family-wise p-value error across the entire frozen family; unavailable comparisons remain in that family as p=1. See the reference Holm implementation. Intervals use the conservative simultaneous Bonferroni level 1 - alpha/m, where m is the full family size. They are not unadjusted individual intervals or Holm intervals. Cross-hypothesis independence is not required by these corrections.

raw_mean_difference is candidate minus baseline, in the original metric unit. improvement reverses its sign for minimization. A confirmatory benefit/harm needs both a Holm-adjusted p-value within alpha and a simultaneous interval entirely beyond the signed practical-effect threshold. Otherwise the result is inconclusive. There is no automatic policy promotion. Confidence intervals are conditional on the declared sampling assumptions; p-values are not calibrated failure probabilities or learning rewards.

Too few units, incomplete outcomes, identical pair differences or nonfinite uncertainty yield insufficient_evidence, with raw signed means retained where well-defined and unavailable uncertainty represented as null. Unknown outcomes are not silently removed for a complete-case test. A negative result remains visible. On a reused holdout, numerical estimates remain available, but a confirmatory benefit/harm conclusion is suppressed; inspect confirmatory_eligible and the receipt's prior_test_accesses. First recorded access means first within the complete declared audit history, not proof that nobody ever saw those data.

Reports contain the complete freeze, access receipt, independent unit IDs, estimates, conclusions and each embedded raw record's logical key/content digest plus temporal metadata. The required UTC completed_at records when calculation and source reference assembly finish, after the protected loader and inference. It cannot predate the access receipt or consumed outcomes. Exported comparison records use this completion time as available_at, never the earlier access request time. Use the embedded records to verify results against immutable sink records; reserve a test/verification access before inspecting protected raw outcomes again. FamilyReport.domain_records(run_id) exports typed #251 measurements with explicit units/signs/unavailable states and no synthetic learning reward. The full report retains statistical conclusions and access context that a scalar alone cannot convey.

Runnable installed-package workflow

Install the reviewed wheel, then copy and run ci.py from a directory outside the repository:

pip install path/to/coleman.whl
python ci.py ./protected-ci-output

The standalone script freezes Greedy/Random RunSpecs before invoking run_with_extension. Four synthetic repository units exercise the real native runner, RNFail reward, NAPFD metric and Parquet sink. It traces candidate fingerprints to saved specs, publishes a queryable report and #251 records, and generates a manifest. It intentionally returns insufficient_evidence for four units under a 20-unit minimum. A repeated access is recorded as reuse before the final manifest is generated, so both receipts appear in its inventory. These synthetic repositories validate plumbing, not production independence, normality, effectiveness or ROI.

Compatibility and limits

Frozen/access/report artifacts require integer schema_version=1. Duplicate JSON keys, unknown fields and missing/older/future versions are rejected. No automatic migration invents missing provenance, units or access history. Legacy native results, RunSpec IDs, sweep APIs and descriptive reports retain their existing readers and behavior. Existing domain records can be evaluated only if they already carry the matching frozen context and candidate configuration; never relabel old artifacts silently to claim protected evidence.

Version 1 intentionally excludes serial/block/cluster inference, robust/nonparametric intervals, sequential testing/alpha spending, unknown external state verification, remote artifact transactions and policy activation. Access-history scanning is linear in scope history, and contention retries are bounded at 1000 publications. Reports retain the complete consumed record payload and recompute estimates on validation; their size and verification cost grow with the paired input. Record hashes and recomputation establish internal consistency, not authenticity of an externally supplied dataset or proof of independent sampling. Keep confirmatory access scarce and predeclared. Larger benchmark/corpus methodology and mathematical/performance validation remain separate roadmap work.

coleman.evaluation.protected

Library-first protected temporal evaluation and versioned paired reports.

AccessReceipt

Bases: EvaluationContract

One durable attempted access, including failures after reservation.

Sequence counts all train/validation/test accesses within the same explicit holdout scope. prior_test_accesses counts every previous test access, including tuning or failed evaluations, so reuse cannot masquerade as virgin. Changing candidates/protocols/data does not reset a stable holdout_id. Timestamp is audit request time (not outcome availability); access_id names a unique API invocation, not independent scientific evidence or an RNG seed.

ComparisonHypothesis

Bases: BaseModel

Predeclared candidate/baseline pair with a unique auditable hypothesis ID.

EvaluationAudit

Persist frozen contracts and serialized access reservations without a new runner.

Immutable ordinal files use atomic create-if-absent publication. Concurrent contenders reread the chain before retrying; no stale lock survives a crash. A reservation precedes outcome loading, and is counted even if loading or inference later fails. This is an audit of this API, not access control over externally readable files or proof that a caller did not inspect data elsewhere. Holdout IDs must remain stable across experiments on the same observations. Chain validation detects gaps/corruption; it is not an authenticated tamper seal.

__init__

__init__(artifact_root)

Configure paths only; filesystem side effects begin with save/access.

access

access(frozen, *, partition, purpose, actor)

Reserve an access before any outcome loader runs, returning a durable receipt.

Every invocation is a new access, including retries after interruption; no caller-supplied idempotency key can hide repeated confirmatory reads. Test tuning counts as test exposure. Native training/validation history is recorded without incrementing protected exposure. At most 1000 publication conflicts are retried, then RuntimeError reports contention. Other validation/filesystem errors propagate immediately.

history

history(holdout_id)

Read and validate the complete immutable scope chain, ignoring .tmp files.

Snapshot iteration is not a global transaction. Missing/corrupt/unknown entries fail closed; no repair invents past access events. External deletion is unsupported and detected by subsequent chain mismatches.

load_freeze

load_freeze(digest)

Load a v1 contract by identified fingerprint, failing on mismatched content.

save_freeze

save_freeze(frozen)

Publish a revalidated frozen contract before protected access.

Identical publication is idempotent. Conflict, unknown versions or I/O failures propagate; the writer's local-filesystem durability limits apply.

save_report

save_report(report)

Publish a versioned report only when its access receipt is in this audit.

Identical report publication is idempotent. Unreserved/mismatched receipts fail before file writes; interrupted reservations remain attempts, not completed reports. Local writer errors propagate to the caller.

EvaluationProtocol

Bases: BaseModel

Frozen metric, estimand, independence, family error rate and practical threshold.

practical_effect is a nonnegative improvement in the metric's own unit. Positive improvement favors the candidate (sign reverses for minimization). Alpha controls family-wise error; paired Student-t tests assume independent units and normal differences. These assumptions must be explicitly declared, not inferred from seeds or successful normality tests. Serial/clustered units are representable but explicitly unsupported by this v1 inference method. holdout_id must persist across candidates/protocol revisions using the same observations; changing it to disguise reuse violates the contract.

FamilyReport

Bases: EvaluationContract

Versioned report tied to an immutable freeze and pre-load access receipt.

Receipt history determines confirmatory eligibility, independently of point estimates. This report never activates a policy. The frozen protocol declares the metric units, practical threshold, independent-unit meaning and assumptions. Digest-checked raw records are embedded and statistics recomputed on validation. completed_at is UTC calculation/source-assembly completion, after access and input availability; derived records retain this availability timestamp.

confirmatory_eligible property

confirmatory_eligible

Return whether this API recorded the first protected confirmatory access.

domain_records

domain_records(
    run_id, *, namespace="ci.protected_comparisons"
)

Convert estimates to #251 records, retaining nulls, signs, units and provenance.

Each access/hypothesis has a distinct logical decision identity; repeating serialization or sink publication is idempotent. Unknown uncertainty is explicitly unavailable. Access IDs/sequences never count as independent evidence. No transformed learning reward is created from inference.

FrozenCandidate

Bases: BaseModel

Named candidate with its entire resolved configuration frozen as canonical JSON.

Include hyperparameters, learned-state fingerprints, preprocessing and feature definitions in this object before protected evaluation. Only JSON objects are accepted; use from_configuration at the input boundary. Opaque config bytes cannot be mutated through nested dictionaries. A fingerprint is not evidence that an undeclared external dependency was frozen.

configuration_fingerprint property

configuration_fingerprint

Return the canonical configuration digest used by domain result provenance.

from_configuration classmethod

from_configuration(candidate_id, configuration)

Freeze a complete JSON object supplied at the configuration boundary.

FrozenEvaluation

Bases: EvaluationContract

Pre-outcome snapshot including full candidates, chronology, metrics and family.

Candidate and hypothesis ordering is canonicalized, never used as RNG entropy. This contract does not execute/tune candidates or replace the native runner. All preprocessing/hyperparameters belong in frozen candidate configurations; exclusion intervals and grouped point-in-time semantics belong in the plan.

candidate

candidate(candidate_id)

Query a frozen candidate; unknown identity raises KeyError.

ObservationReference

Bases: BaseModel

Embedded immutable raw result with verified join/digest and temporal metadata.

PairedObservation

Bases: BaseModel

One candidate's raw result for a declared independent group/unit.

metadata.group_id is the independent unit, not a seed/worker identity. Exactly one observation per candidate/unit is accepted; repeated seed rows require a predeclared upstream aggregation and never count as new units. Outcome availability must agree with the versioned result record.

SplitDecision

Bases: BaseModel

Auditable inclusion/exclusion; excluded rows retain their nominal partition.

TemporalObservation

Bases: BaseModel

Metadata for one revision of an observation; no outcomes are inferred.

All times are explicitly zoned and normalized to UTC. Outcome availability cannot precede the event. Target intervals are closed, start at event_at and optionally end at target_end (otherwise the event itself). A revision retains its event/group identity but has its own availability and revision ID.

TemporalPlan

Bases: BaseModel

Chronological closed-right splits with conservative point-in-time purging.

Train is (-infinity, train_end], validation is (train_end, validation_end], test is (validation_end, test_end]. Event time fixes the nominal partition; late availability never moves a training row into validation/test. Availability and the entire target interval must be within that partition's cutoff. Exact endpoints are inclusive. Missing metadata fails; ineligible rows are excluded with reasons. Groups spanning any two nominal partitions fail even when one of their observations would otherwise be excluded for late availability.

decide

decide(observation)

Validate one metadata object and record eligibility without reading values.

nominal_partition

nominal_partition(observation)

Assign event time only, using closed-right cutoff inclusivity.

observation

observation(values)

Convert a dataset mapping once at the boundary; missing required keys fail.

Unused dataset columns remain outside the typed metadata contract. Optional revision/interval columns may be omitted; explicit null revision is invalid. No fallback timestamp, group or observation identity is invented.

split

split(observations)

Validate revision identity/group separation, returning deterministic decisions.

Duplicate (observation_id, revision_id) or an observation ID changing its event/group identity fails. This excludes post-cutoff revisions without overwriting legitimate historical observations. No source data is mutated.

training_eligible

training_eligible(observation)

Return train eligibility for typed metadata; no automatic fold reassignment.

evaluate

evaluate(frozen, audit, load_observations, *, actor)

Reserve protected access, load typed records and evaluate the full frozen family.

Paired t inference requires declared independent units and normal differences; serial/clustered observations and undeclared normality assumptions raise NotImplementedError before outcome loading. Complete pairing is required; repeated seed rows are rejected. Missing outcomes produce insufficient evidence rather than selective complete-case tests. Holm p correction and Bonferroni simultaneous intervals use the entire predeclared family. Reuse is durably recorded and prevents a confirmatory conclusion, even after candidate changes. Loader/validation failures leave access reservations visible for honest retries.

freeze

freeze(
    candidates,
    data_fingerprint,
    plan,
    protocol,
    comparison_family,
)

Validate and freeze the complete protocol before reading protected outcomes.

Pure operation; changes to candidate, data, splits, assumptions, practical threshold, metric or comparison family change the canonical fingerprint. The caller owns pre-outcome timing and must persist the contract before access.

holm_adjust

holm_adjust(p_values)

Return Holm-Bonferroni adjusted p-values controlling family-wise error.

Uses all predeclared hypotheses, with unavailable tests supplied as p=1 by the evaluator. Finite values in [0,1] are required. Sorting is internal and original ordering is preserved; empty input returns an empty tuple. No RNG, independence-between-hypotheses or additional optional dependency is needed.

named_seed

named_seed(root_seed, candidate_id, purpose)

Derive 64-bit NumPy/Polars/Python seed entropy from stable, purpose-separated names.

Reordering candidates/workers has no effect. Nonnegative integer roots and nonempty names are required; no global RNG state is modified. Repeated seeds on the same observations never increase the independent-unit count.