Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Compile a bounded experiment from a hypothesis

Use std/eval/hypothesis when an agent or product needs to turn a question into an experiment without letting model output become executable authority. The planner produces typed data. The deterministic compiler accepts only registered adapters, validates trusted host risk, capability, placement, citation, and resource ceilings, and lowers an accepted design into Harn's existing experiment registration contract.

This guide shows the control flow. The complete executable fixture is eval_hypothesis_compiler.harn, with its catalog and intent in hypothesis_fixture_lib.harn.

Register what the experiment may do#

Build an ExperimentCompileContext at the host boundary. Its catalog contains the only intervention, outcome, and population adapters the compiler may reference. Its budget ceiling is authority; a requested budget above any ceiling is refused.

import { compile_experiment_intent } from "std/eval/hypothesis"

const context = {
  owner: "eval-team",
  seed: "release-42",
  catalog: {
    schema: "harn.experiment.adapter_catalog.v1",
    interventions: [baseline_adapter, candidate_adapter],
    outcomes: [bounded_success_outcome],
    populations: [frozen_fixture_population],
  },
  trusted_citations: researched_sources,
  risk_floor: host_classified_risk,
  capability_ceiling: host_capability_ceiling,
  supported_blocking_factors: ["host", "time_slot"],
  host_identity_available: true,
  budget_request: requested_budget,
  budget_ceiling: approved_ceiling,
  approval_id: nil,
  provenance: {
    source: "release-eval",
    actor: "automation",
    created_at: "2026-08-08T00:00:00Z",
  },
}

const receipt = compile_experiment_intent(intent, context)
if !receipt.ok {
  for diagnostic in receipt.failure?.diagnostics ?? [] {
    harness.stdio.eprintln(diagnostic.code + ": " + diagnostic.message)
    if diagnostic.repair != nil {
      harness.stdio.eprintln(
        "repair " + diagnostic.repair.owner
          + " " + diagnostic.repair.operation
          + " " + diagnostic.repair.path,
      )
    }
  }
  return
}

The current authored contract is harn.experiment.intent.v2. It carries typed alternative hypotheses and a typed decision scope. The compiler also accepts v1 input and normalizes it before fingerprinting, so equivalent v1 and v2 intents produce one plan identity. Future versions fail with experiment.intent_schema_version; structural failures use experiment.intent_schema. Both can carry a typed repair owner, operation, path, and expected shape.

Select quasi_experimental only with an explicit quasi_experiment contract. Name the non-random assignment mechanism and identifying assumptions. A matched comparison must also name its matching method and pre-treatment covariates. The current compiler emits an observe_only plan with an associational claim ceiling; it does not reinterpret matching as randomized causal evidence.

Use immutable adapter IDs and variants. Put credentials in host-managed secret references, never in the catalog or intent. The capability manifest declares filesystem roots, process commands, network domains, providers, connectors, database scopes, customer-state access, model routing, privacy class and retention, mutation reversibility, and approval requirements. Missing customer-state, model, and privacy fields in a legacy manifest normalize to no authority. New compiled plans carry them explicitly. Executable adapters may also declare one typed placement requirement with mode, platform, requires_gpu, and resource_class. Baseline and candidate requirements must be identical, and the compiler refuses a host ceiling that cannot enforce them.

Hand the registration to an enforcing host adapter#

An accepted receipt contains a stable intent fingerprint and plan fingerprint. If receipt.plan.kind == "registered_experiment", the plan contains the canonical ExperimentManifest and ExperimentRegistration. It deliberately does not contain an executable workflow. Only a registered host adapter that enforces the plan's capabilities, placement, approval requirement, and remaining resource ceilings may schedule it; that adapter must use Harn's canonical assignment, observation, and decision APIs rather than reconstructing their rules.

if receipt.plan.kind == "observe_only" {
  for question in receipt.plan.instrumentation_questions {
    harness.stdio.println("instrumentation needed: " + question)
  }
  return
}

const plan = receipt.plan
if plan.design.approval_required {
  request_native_approval(plan.design.approval_id, plan.fingerprint)
  return
}

require plan.execution_status == "requires_registered_host_adapter",
  "compiled registrations are not self-executing"
registered_experiment_adapter.schedule(plan.registration, {
  capabilities: plan.design.capabilities,
  resource_ceiling: plan.design.budget,
  plan_fingerprint: plan.fingerprint,
})

observe_only is a successful, non-executable result. It preserves the question and names missing instrumentation instead of manufacturing a causal test. A high-risk randomized design carries a stable approval requirement; compilation does not pretend approval already happened, and an adapter must not execute it without the corresponding native approval. Record that approval as an approval_recorded event bound to the exact approval ID and plan fingerprint, then mint its opaque native_approval proof from the registered approval adapter. The ledger refuses scheduling before the matching approval. The event payload is an audit record, not evidence that the native approval UI ran.

Record lifecycle facts once#

Create typed events with hypothesis_event, append them with hypothesis_ledger_append, and derive current state with the single-pass hypothesis_ledger_snapshot. The ledger is a typed projection over Harn's event log, so it inherits global ordering, integrity hashes, and SQLite, file, or memory persistence. The topic is reserved: generic event-log writes fail, and the specialized append requires a non-serializable authority proof minted by a registered native adapter for that exact event.

import {
  hypothesis_event,
  hypothesis_ledger_append,
  hypothesis_ledger_snapshot,
} from "std/eval/hypothesis"

const event = hypothesis_event({
  schema: "harn.hypothesis.event.v1",
  schema_version: 1,
  event_id: "plan-registered",
  hypothesis_id: hypothesis_id,
  plan_id: plan.plan_id,
  run_id: nil,
  predecessor_fingerprint: nil,
  occurred_at: "2026-08-08T00:01:00Z",
  actor: "automation",
  source: "release-eval",
  payload: {kind: "plan_registered", plan: plan},
})

// The native adapter owns this receipt
// and returns a tagged success only after
// it verifies that the corresponding plan-admission operation completed.
const proof = harness.obs.hypothesis_event_authority_request(
  "plan_admission",
  event.fingerprint,
  plan.fingerprint,
  hypothesis_id,
  "plan-admission-receipt-01",
  nil,
)
const first = hypothesis_ledger_append(harness.obs, event, proof)
const replay = hypothesis_ledger_append(harness.obs, event, proof)
require first.cursor == replay.cursor && !replay.inserted,
  "a retry must return the original durable event"

const read = hypothesis_ledger_snapshot(harness.obs, hypothesis_id)
require read.integrity.scope == "retained_topic_chain"
  && read.integrity.verified,
  "the retained topic chain must verify before projection"
const snapshot = read.snapshot

The example assumes the host registered hypothesis.attest_event and gave this pipeline the narrow authority.write@plan_admission effect grant. Harn sends the exact authority kind, fingerprints, IDs, and operation receipt over the host bridge. The adapter must return a JSON-RPC error for a missing, stale, mismatched, reused-with-different-bindings, or denied receipt. It may return the same success for an exact retry. An accepted response is the exact tagged result documented in Bridge protocol. Harn consumes that document inside the scoped builtin and returns only a non-serializable VM resource. An ordinary host_call sees the same document as plain data and cannot turn it into authority.

In-process Rust embedders may instead create a native attestation with harn_vm::stdlib::mint_hypothesis_native_attestation and pass it to hypothesis_event_authority_mint. That constructor is not a wire format.

Do not grant hypothesis-event authority writes to model-authored code. Give approval, execution, and lifecycle adapters only their corresponding authority.write@native_approval, authority.write@native_observation, or authority.write@lifecycle_audit scope. Mint native_approval only after native approval, native_observation only after an assigned execution produces its measurement, and lifecycle_audit only from the adapter that owns the transition or decision. Every proof is bound to the event fingerprint, plan fingerprint, hypothesis, and optional run; copying its serialized audit headers cannot authorize another append.

Each later event names the preceding aggregate fingerprint. Reusing the same hypothesis and event IDs with different content, breaking that predecessor chain, recording an unassigned observation, exceeding the plan budget, or submitting a decision that differs from Harn's canonical recomputation fails closed. Record realized paired observations, execution drift, decisions, invalidations, regressions, and follow-up relationships as events; do not update a parallel JSON document. Reports and host dashboards should project the ledger snapshot rather than becoming independent sources of truth.

A completed run transition carries typed completion evidence. Use {kind: "statistical"} only when decide_experiment is already non-RUNNING with budget_spent: false. Use {kind: "max_trials"} only after every frozen candidate, case, and trial cell has an observation. Native budget and wall-clock completion carry a non-empty receipt ID in the fingerprinted transition and in its receipt_ids; the native lifecycle attestation binds that exact event. The ledger validates the event and its attestation. It does not query or validate the host's external receipt store.

Use hypothesis_workflow(harness.obs, {kind: "inspect", hypothesis_id: id}) for the same typed read and report projection. start, pause, resume, advance, and stand_down are state-checked requests. They call the registered hypothesis.operation native adapter; when none is present, they return kind: "adapter_unavailable" before appending a lifecycle event.

advance executes one balanced case/trial block. The caller supplies concrete blocking values, Harn freezes and randomizes the arm order with plan_assignments, and the adapter returns measurements in exactly that order. Harn realizes the assignments, validates the full block against the resource ceilings, appends only missing cells on retry, and runs decide_experiment. The adapter never chooses the assignment or supplies the verdict.

const advanced = hypothesis_workflow(
  harness.obs,
  {
    kind: "advance",
    hypothesis_id: plan.hypothesis_id,
    blocking_values: {host: host_id, time_slot: frozen_slot},
  },
)

Call design_hypothesis separately when natural-language design is needed.

Use the same boundary from CLI or MCP#

The hypothesis control-plane example exports the planner, compiler, workflow, ledger inspection, and report as typed functions. Run one function through harn run, or expose the same functions as structured tools with harn serve mcp. The projection adds no scheduler, manifest, decision rule, or write authority. The compile tool's generated schema accepts the same v1/v2 intent union, including the v2 quasi-experiment and decision-scope fields.

Verify the boundary you claim#

For deterministic compiler changes, run the exact conformance fixture:

harn test conformance tests/stdlib/eval_hypothesis_compiler.harn --verbose

For an experiment claim, also prove through the native adapter that the registered intervention fired, the realized assignment matched the randomized plan, outcomes came from the declared population and grader, the stopping decision used the frozen experiment registration, and resource totals remained below every ceiling. Passing compiler tests alone does not prove a live intervention worked.