# Acquisition lifecycle

This is the coordinated **pre-v1 breaking contract** tracked in #136. It does
not announce a v1 product release. New native results use schema 4 and new
frozen plans / compiled builds use format 2. Earlier formats are unsupported;
create a new plan, build and capture. There is no archival reader or conversion
fallback, and no existing files are deleted.

All three runtimes accept the same typed policy:

```yaml
defaults:
  fvp:
    enabled: true
    acquisition:
      state: carry
      warmup_invokes: 2
      measured_invokes: 3
```

`carry` keeps one initialized instance across warmups and measurement. Resource
variables persist. Inputs are filled with zeros by default, or with the fixed
bytes in `tensor_io`, before every invoke; filling inputs is not resetting state. `single` permits one invoke on an initialized
instance, with no warmup. An explicit warmup count greater than zero is rejected
with single state, as is a measured count other than one.

For LiteRT and heliaRT, AllocateTensors runs before invokes, while CALL_ONCE
initialization runs inside the first invoke when present. Automatic warmup is
one for a runner model containing CALL_ONCE and zero otherwise. An explicit
zero measures the first invoke, including its CALL_ONCE work.

For AOT, model_init runs once before invokes and outside PMU capture. Automatic
warmup is zero. Explicit warmups run model_run on the same context; they do not
repeat model_init. An AOT receipt makes no CALL_ONCE claim about the source model.

PMU capture stays disabled through warmups, and counters reset before the
each measured inference. The reported scope is NPU callbacks within that inference,
not application/CPU wall time. The AOT CPU wall timer starts after warmups.
The default remains one measured inference. Carry supports a positive measured
count on one initialized instance. Each sample has independently reset counters;
warmups and output serialization are excluded. A complete ordered sequence is
required before a result is published.
Explicit policies with bus tracing or per-layer fencing remain unsupported.

## Current evidence and reader

Every executed runner must emit one current `helx.acquisition` receipt:

- `version`: protocol version 1, with the required schema identifier.
- `state`, `warmup_invokes`, `measured_invokes`: actual lifecycle/counts.
- `initialization`: `interpreter_allocate_and_first_invoke` or `before_measurement`.
- `call_once_present`: an observed runner-model boolean for interpreters, null
  for generated AOT code. This is not proof of all possible initialization work.

The bundle retains it as `artifacts/acquisition.json`, covered by its manifest.
Missing, duplicate, unsupported or contradictory receipts fail execution before
publication. A mismatch against the native UART warmup count also fails.

```python
from helia_estimator.api import acquisition_facts, load_result, run_identity

result = load_result("results/my-run")
facts = acquisition_facts(result)  # None only when the FVP stage did not execute
if facts is not None:
    print(facts.state, facts.warmup_invokes, facts.initialization, facts.timing_scope)
identity = run_identity(result)
```

The reader verifies the receipt's manifest bytes, completed stage, runtime,
native warmup count and UART receipt when retained. An executed bundle without
lifecycle facts is invalid. Build-only and Vela-only results legitimately have
no execution facts. A new result may still have unavailable provider versions
or other unestablished facts; those are never fabricated from the current host.

## Saved execution

Policy is compiled into the runner and captured by freeze/build. Every current
compiled build binds its CMake acquisition controls to the captured request.
Changing the policy requires a new build. Run-build uses the retained executable
without recompiling and checks its actual receipt before publication. Current
plans and builds remain portable and integrity checked; earlier saved formats
are rejected explicitly.

## Mapping to HPX and BENCH

The portable view retains verified lifecycle facts under
`extensions["helia-estimator"].acquisition`, with a manifest-bound receipt
reference in `evidence`. Non-executed results have a null acquisition value.

| Comparison fact | HELX meaning |
|---|---|
| `first_invoke_initializer` | False for AOT's `before_measurement` initialization; otherwise the observed `call_once_present` boolean |
| `first_invoke_window` | `warmup` when warmups are nonzero; otherwise `measured` |
| `instance_boundary` | `boot`: one runner instance per FVP execution |
| `counter_scope` | `npu_callbacks`: NPU counter coverage within the measured invocation |
| `cpu_timing_scope` | `invoke_only`: when enabled, the runner CPU timer covers the invocation, excluding input refill and warmups |

AOT's null `call_once_present` means the operator fact is not applicable to the
generated runtime. Its initialization boundary is known: `model_init()` runs
before the first `model_run()`. It must not be mapped to an unknown first-invoke
initializer. The interpreter boolean describes the runner model's CALL_ONCE
operator, not every possible source of lazy initialization.

Counter coverage and CPU timing scope are distinct comparison dimensions.
`npu_callbacks` must not be compared directly with HPX's `invoke_only` or
`restore_and_invoke` CPU timing labels.

## Samples and statistics

Native inference fields live in `measurements.samples[i]`: `cycles`,
`traffic_beats`, `latency_s`, `port_utilization`, `utilization_fraction`,
`energy`, `sanity`, and output signatures. Index zero is the first *measured*
invoke. A signature is an output-byte fingerprint, not a numerical accuracy verdict.
The reader requires the count to agree with the acquisition receipt and verifies
that stored statistics reproduce the samples.

`measurements.statistics` includes counter count/sum/min/max, exact mean and
population-variance numerator/denominator pairs, and approximate population
standard deviation. Latency and total modeled energy have floating-point
population statistics. No missing sample is dropped: a missing metric makes
its statistic unavailable. For one sample the deviation is zero. Utilization
and port occupancy remain per-sample ratios; no batch ratio is implied.

```python
from helia_estimator.api import load_result, measurement_samples, measurement_statistics

result = load_result("results/my-run")
for sample in measurement_samples(result):
    print(sample.index, sample.cycles, sample.energy)
stats = measurement_statistics(result)  # None for an unexecuted result
if stats is not None:
    total = stats.counters["cycles.total"]
    if total is not None:
        print(total.count, total.mean.numerator, total.mean.denominator)
```

`helx show` labels every measured sample. The native `run_summary.csv` and
`helx export` emit one indexed row per sample. Explicit aggregate export is:

```bash
helx export results/my-run --fields statistics --format json
```

The portable draft emits one observation per sample with `sample_count: 1`,
its native sample index, and a source pointer into the sample. It retains the
full native statistics in the HELX extension rather than inventing shared
aggregate fields. Multi-inference measured per-layer/viewer attribution is
omitted with a reason until an explicit sample selection is supported;
single-inference tracing/fencing is unchanged.

See [Tensor inputs and numerical verification](https://ambiqai.github.io/helia-estimator/guides/tensor-verification/) for fixed input bytes,
per-sample output artifacts and explicit reference/tolerance policies.
