Skip to content

Point-in-time CI observations and decision signals

COLEMAN 4.2 implements #258. CI durations/outcomes retain their original availability and revision lineage, so later corrections do not rewrite the information used by an earlier decision. The base package needs no provider SDK, credential, hosted service or optional MCP dependency.

Install the wheel attached to COLEMAN 4.2, or build a development wheel from reviewed source. Run examples outside the checkout. ADR 001 records the module/dependency/extension decisions.

Observation and revision contract

ObservationRevision schema v1 contains protected TemporalObservation metadata, a logical entity ID, first local received_at, explicit predecessor supersedes and a typed #251 Measurement. Event, source availability and first receipt are separate instants. Effective availability is their later availability/receipt; receipt cannot precede declared source availability. Source/local clocks must be comparable; clock disagreement is rejected, never silently adjusted.

ObservationHistory holds at most 4,096 supplied revisions per snapshot. Providers must preserve all build/job/stage/test/variant dimensions needed to prevent logical collisions. Measurement names/units/meaning, entity, event/group and target interval stay stable across a lineage. Revisions can change values and missingness, with explicit availability. Each lineage has one root and one successor per revision; missing parents, cycles, forks and conflicting duplicates fail. Identical retries are idempotent, including their unchanged first receipt time. Duplicate payloads are compared by canonical serialized content. COLEMAN JSON v1 preserves signed float zero: 0.0 and -0.0 for one revision key are conflicting representations, not an input-order-dependent choice. Existing domain JSON semantics are unchanged.

history.as_of(cutoff) selects one latest eligible member per logical observation. Cutoffs are inclusive. Explicit lineage orders ties; revision names are never lexically ranked. A newer revision can arrive before its predecessor; a late older delivery cannot replace it. Unknown/unavailable values retain their states rather than reviving an obsolete earlier value.

For the issue's CI fixture, event time is 10:00:00Z, r1 becomes available at 10:00:02Z, and r2 at 10:05:00Z:

Query cutoff Visible revision
10:00:01Z None
10:00:03Z r1
10:05:00Z r2

A receipt later than source availability delays visibility further. Adding or changing a valid future revision cannot change the earlier query or its fingerprint. Whole-history hashes intentionally change on append; selected-view hashes identify only the declared source/version, cutoff and selected eligible content.

Providers attest completeness and source identity. An isolated serialized ObservationSnapshot checks one-row-per-observation and availability; it cannot prove that an upstream provider supplied every revision. as_of establishes selection for the complete bounded history actually supplied. No stream database, tombstone protocol, multi-parent merge or implicit provider import is introduced.

All instants reuse the existing aware UTC time contract and Python datetime's microsecond resolution. Providers must declare rounding/rejection for finer source clocks before normalization. No local timezone, numeric epoch unit or host clock is inferred. Context/variant observations sharing executions are not independent data.

Frozen training and honest signals

prepare_training queries at min(information cutoff, declared train_end), selects the named measurement, and reuses TemporalPlan eligibility/purging. Target intervals must also end by this actual information cutoff, even if the predeclared training window ends later. Serialized training artifacts recheck the same effective boundary. The model records temporal exclusions without retaining excluded labels. An eligible but unknown/unavailable training value fails; no mean-zero imputation or silent complete-case fit occurs. Later validation/test labels and post-training corrections cannot refit the frozen model.

Fitting only inspects the available past view. The complete later evaluation must still apply the existing protected cross-fold group gate; future group membership cannot rewrite a past feature. Seeds and repeated synthetic builds never become independent observations.

fit_quantity_mean implements a simple baseline for nonnegative physical quantities, not a claim of superior forecasting. Inputs must be finite, between 0 and 10^12, in exactly the declared units/meaning. It sums exact rational forms of the #251 binary64 inputs and rounds the mean up to 10^-9 of that unit, independently of Decimal context. For example, (0, 1, 0) gives 0.333333334. Binary64 0.1 is slightly above mathematical 0.1 and therefore rounds up to 0.100000001; this rule does not recover an unknown original decimal spelling. Exact rounded Quantity values remain in the model; signal values declare their binary64 projection. There is no implicit seconds/milliseconds conversion.

A QuantityMeanModel retains eligible training evidence, temporal protocol, freeze time, means, logical observation counts and latest event times. Reads rederive those values and reject tampering. Counts describe observations; they are not confidence estimates or proof of independence.

A DecisionSignal carries request/target identity, decision time, information cutoff, closed future target interval, interpreted measurement, explicit availability, producer/model versions, eligible-source/model fingerprints and the freshness rule when used. Its cutoff cannot exceed decision time, its target cannot precede the decision and its model must already be frozen. Units and meanings remain explicit for null values too.

Availability Measurement status Value Meaning
available observed 0 A real available estimate/score of zero
missing unknown null Insufficient eligible history
stale unavailable null Source event is too old at the decision
unavailable unavailable null Producer declares unavailable information
not_applicable not_applicable null Explicitly inapplicable signal

The mean helper emits available, missing or stale signals. Age is decision time minus latest event time; a newly received correction does not make an old event fresh. Maximum age uses EVENT_AGE_SECONDS, with equality still fresh. The rule is stored in the signal and enters specification/logical identity. The resolved cutoff/horizon also enter specification identity, so changing context cannot be silently treated as an identical domain-record replay. A null generic freshness rule means unspecified by that producer, not guaranteed fresh forever.

The generic signal supports signed priority scores when their declared lower bound permits them. A negative score remains negative. A priority score is not a calibrated probability, reward, causal effect or hypothesis-test result. Generic readers validate schema/chronology/metadata without executing arbitrary producer code. For a serialized mean signal, call model.verify_signal(signal) to check the complete canonical value, status, parameters and provenance against its frozen training evidence before use, including signed-zero representation.

Complete installed library example

This repository-owned synthetic fixture uses test occupancy seconds. The future correction cannot influence the earlier query or frozen duration estimate.

from datetime import datetime
from decimal import Decimal

from coleman.artifacts import ArtifactWriter
from coleman.evaluation.protected import TemporalPlan
from coleman.observations import (
    EVENT_AGE_SECONDS, DecisionSignal, ObservationHistory, ObservationRevision,
    SignalRequest, fit_quantity_mean, prepare_training, publish_model, publish_signal,
)
from coleman.selection import Quantity, UnitSemantics

seconds = UnitSemantics(unit="s", meaning="test-runner-occupancy-v1")
r1 = ObservationRevision.model_validate({
    "schema_version": 1,
    "metadata": {
        "observation_id": "build-2:test-a:duration", "revision_id": "r1",
        "group_id": "build-2", "event_at": "2026-01-02T10:00:00Z",
        "available_at": "2026-01-02T10:00:02Z",
    },
    "entity_id": "test-a", "received_at": "2026-01-02T10:00:02Z", "supersedes": None,
    "measurement": {
        "name": "ci_duration", "value": 3.0, "unit": "s", "meaning": seconds.meaning,
        "direction": "none", "status": "observed", "minimum": 0.0,
    },
})
r2 = ObservationRevision.model_validate({
    **r1.model_dump(), "supersedes": "r1", "received_at": "2026-01-02T10:05:00Z",
    "metadata": {**r1.metadata.model_dump(), "revision_id": "r2", "available_at": "2026-01-02T10:05:00Z"},
    "measurement": {**r1.measurement.model_dump(), "value": 9.0},
})
history = ObservationHistory(schema_version=1, source_id="owned-ci-demo", source_version="1", revisions=(r1, r2))
assert history.as_of(datetime.fromisoformat("2026-01-02T10:00:01Z")).observations == ()
assert history.as_of(datetime.fromisoformat("2026-01-02T10:00:03Z")).observations == (r1,)
plan = TemporalPlan.model_validate({
    "train_end": "2026-01-02T10:00:03Z", "validation_end": "2026-01-02T11:00:00Z",
    "test_end": "2026-01-04T00:00:00Z",
})
training = prepare_training(
    history, plan, information_cutoff=plan.test_end,
    measurement_name="ci_duration", input_semantics=seconds,
)
model = fit_quantity_mean(training, frozen_at=datetime.fromisoformat("2026-01-02T10:00:04Z"))
request = SignalRequest.model_validate({
    "schema_version": 1, "decision_id": "build-3", "entity_id": "test-a",
    "decided_at": "2026-01-03T10:00:00Z", "information_cutoff": "2026-01-03T10:00:00Z",
    "target_start": "2026-01-03T10:00:00Z", "target_end": "2026-01-03T10:01:00Z",
})
signal = model.signal(request, maximum_event_age=Quantity(value=Decimal(86400), semantics=EVENT_AGE_SECONDS))
assert signal.measurement.value == 3.0
assert signal.availability == "available"
writer = ArtifactWriter("point-in-time-library-artifacts")
publish_model(model, writer)
path = publish_signal(signal, writer)
assert model.verify_signal(DecisionSignal.from_json(path.read_bytes())) == signal

To add an authorized provider, implement ObservationProvider.load_history() in an external adapter package and return typed ObservationHistory. Establish source-specific identities, units, clock resolution and immutable first receipts; retain complete bounded lineage and bound raw payloads before decoding. The core never loads a provider from a name, URI or credential. OfflineObservationProvider revalidates an already supplied owned history without I/O.

Real native TCPCI and recovery

Copy ci.py outside the checkout and run python ci.py native-observation-demo from the installed 4.6.0 wheel or source build. It fits owned historical quantities at a frozen training cutoff before decisions, then runs three constructed native CI copies using run_with_extension, native Parquet results, RNFail and existing checkpoints.

The native first-build shuffle remains a warmup. On the next two builds, fresh estimates produce t2/t3/t1; stale estimates retain the full base order t1/t2/t3. The policy reads logical test names and frozen historical signals, without using current realized durations/verdicts as predecision estimates. The historical and current durations intentionally differ. No required test is removed by fallback. This is functional offline replay, with no prospective execution, savings, calibration or superiority claim.

Models/signals use existing immutable ArtifactWriter publication in observation-models/ and decision-signals/. Matching named domain sidecars enter the existing sink before checkpoint advancement. Native manifests discover those families. Signal paths use the stable logical ID: identical retries succeed and changed content for that ID conflicts. Models use their content fingerprints. Logical identity derives from dataset/policy/reward/experiment, excluding native runtime attempt suffixes, retry clocks and worker IDs. Content digests are deterministic. A changed freshness policy changes specification identity. Domain sinks retain their existing conflicting-logical-record checks.

The same owned output can resume with python ci.py native-observation-demo. Immutable input/configuration/actual RunSpec proofs must match before loading a checkpoint. Checkpoints live in the existing native run directory; only trusted local checkpoints created by the example may be loaded. Unknown output roots, changed inputs/parameters/specs and pre-existing symlinks fail before native use. Local path checks assume no hostile concurrent filesystem mutation, not an OS sandbox. Concurrent native invocations must use separate output roots: the existing checkpoint store does not coordinate simultaneous writers of one logical execution. Existing native pickle/checkpoint durability limits remain unchanged.

Regression tests interrupt after signal publication, after a synchronous domain record commit, and after checkpoint commit, then resume through the real store. Accepted signals remain complete; retry reuses the same logical signal path/content digest and domain records are not duplicated. Reopened/concurrent Parquet writers now use distinct per-flush filenames even within one PID/second, preserving previously committed native rows. Legacy Parquet filenames were already operational/non-deterministic; deterministic mathematical identities belong to versioned models/signals/domain records.

Unknown/future/missing versions and duplicate JSON keys fail closed. Existing native artifacts keep their readers/meaning; there is no automatic migration or silent conversion to observation histories. Same-content publication is idempotent; conflicts/I/O errors propagate and incomplete temporary files never become completed artifacts. There is no additional checkpoint or stream store.

flowchart LR
  P[Authorized provider or owned offline fixture] --> H[Typed bounded revision history]
  H --> A[As-of source availability and first receipt]
  A --> T[Existing protected training eligibility]
  T --> M[Frozen simple quantity model]
  M --> S[Versioned estimate or explicit missing/stale signal]
  S --> N[Existing native extension policy and conservative fallback]
  S --> D[Existing domain sidecars and immutable artifacts]
  N --> C[Existing native checkpoint]
  D --> C