Status: RESOLVED. The owner approved OwnIR v2 and T0 Amendment 2 for H1; H1
landed as h1-proven-call.md. Kept as the record of why the
version had to move. (Original status: STOP: needs an owner ruling.)
Base: main = 1c70e867eb408d931ae3dfffd8c0b351190f6547, the H0 landing point
(heap-effect-summaries.md).
H1 would let a provably harmless direct source call stand inside a
[ProtocolRegion] instead of being refused. The kill-first question was whether
a fail-loud transport for that decision exists inside the current contracts.
It does not. The only compliant transport bumps OWNIR_VERSION, and a bump
moves the frozen T0 harness identity, which only an owner amendment may do.
ProtocolLoweringrefuses in the extractor, before any fact exists.- The H0 summary is solved in the core.
- The frontend may not solve summaries (IR6, and the rule that frontends emit facts only).
So the facts must carry an obligation: "this call is admitted only if the core proves Harmless(...)". A core that does not understand the obligation must not read it as clean.
| Candidate transport | Old core (v1) reads it as | Verdict |
|---|---|---|
Optional field on call (requires: harmless) |
a call to an extern with no tracked argument, i.e. nothing, i.e. clean | fail-open: rejected |
| Omit the admitted call from facts and put the obligation in a separate file or section | a valid v1 document without the call, i.e. clean | fail-open: rejected |
| Extractor calls the core solver before writing facts | n/a | frontend depends on and decides with the core at runtime: an architectural decision no spec makes; STOP condition |
| New flow op without a bump | OwnIRError (IR4: unknown op) |
fail-loud, but violates IR3 ("add a flow op ⇒ bump"); a contract violation is not a transport |
| New resource kind without a bump | OwnIRError at load |
same: IR3 requires a bump |
New flow op + OWNIR_VERSION 1 → 2 |
OwnIRError twice: IR1 (version) and IR4 (op) |
the only compliant fail-loud transport |
This is by design, not an accident: in OwnIR, "an old core fails loud" and "the vocabulary changed" are the same event (spec/OwnIR.md §2, IR1–IR4). No mandatory-capability or feature-gating mechanism exists. fix_candidates_version and the H0 heap_effects_version are additive and are ignored by the core.
scripts/perf_baseline.py's facts calibration generator stamps "ownir_version": 1. Under a v2 core:
- The literal must move, or
perf-calibration-facts-currentgoes red: the generator would write facts both engines refuse. This is exactly the defect Amendment 1 found. - Moving it moves the T0 harness identity (
measurement_harness_digest, currently1a26aa63fd5f…), becauseperf_baseline.pyis in the harness source set. - The P-022 merge gate (
scripts/step7/mergegate.py,p022-merge-gate.yml) recomputes the live digest of the merged commit and requires it to equal the one T0-1 names, so the PR cannot merge green. - T0 rules it out unilaterally. "the next move is a new amendment or it is a defect" (
p022-263-t0-protocol-freeze.md, Amendment 1). The owner also "ruled out a compatibility shim on either side", so a v2 core that still accepts v1 is not an option either.
The P-022 freeze on verdict-changing inference itself is not the blocker: it lifted at the Stage 3 cutover (P-022-rust-core-migration.md, "With the cutover complete the P-022 feature freeze on verdict-changing inference lifts"). The blocker is the T0 harness binding that a version bump drags along.
Precedent: Amendment 1 is this exact situation (0 → 1 for borrow_mut/move), and the owner resolved it with one literal plus a re-binding of steps 4, 5 and 6.
Amendment 2: OWNIR_VERSION 1 → 2 for the proven_call op. Mirroring Amendment 1:
| Item | What moves |
|---|---|
| Instrument | one literal in perf_baseline.py's facts generator, 1 → 2; nothing else in the harness source set |
| Re-bound | T0 steps 4, 5, 6 (with design_constants_blob_sha1), to the new harness identity |
| Re-run | the instrumentation controls Amendment 1 re-ran |
| Unchanged | rules, budgets, populations, statistic, roll-ups, host predicate, retry budget |
| Not rewritten | committed evidence of past runs |
Mechanical churn of the bump itself: three producer stamps (ownlang/ownir.py, own-ir/src/lib.rs, Program.cs) and 244 committed JSON fixtures carrying "ownir_version": 1:
| Fixture family | Files |
|---|---|
| summaries | 82 |
| lowered | 64 |
| verdicts | 38 |
| ownir | 22 |
| repro | 19 |
| verdict_renders | 9 |
| cli_ownir | 4 |
corpus/ownership-lab/h29 |
4 |
| others | 2 |
All of them are regenerated by their own --write tooling, never by hand.
- Extractor (
ProtocolLowering). Only one site changes: an invocation inside a region that touches no entity and no token. Today it reachesOpaqueCodeand is refused.- If its dispatch is
direct, decided by the same classifier H0 uses (shared, not copied), it lowers to{"op": "proven_call", "site": <id>, "line": n}. - Every other call is refused exactly as today, with the same text: extern, virtual, delegate, local function, constructors, getters, operators.
- An entity-touching call keeps today's
use+ core verdict.
- If its dispatch is
- Site evidence. Each site is a synthetic H0 record walked by the H0 producer over the call expression alone. Outer variables (the enclosing method's locals and parameters, the token) read as
heap, so any mutation through them is a heap write, never silence. - Section. The facts carry a
heap_effectssection (H0 vocabulary v1): the site records plus the records reachable from them bydirectedges. Selecting the records is closure only, not solving; a missing record solves to Unknown. - Core (Python reference + Rust port, byte-parity).
- The section is required whenever a
proven_callexists. Its absence, a missing site record, or a malformed section is anOwnIRError(refusal, exit 2). - The H0 solver solves the section. Each site is admitted iff its solved summary satisfies the predicate below; otherwise
OwnIRErrornaming the site. - An admitted
proven_calllowers to nothing: it touches no tracked resource.
- The section is required whenever a
- Predicate v1 (strict) on the site summary, and so transitively on the callee:
- no Unknown anywhere;
- every parameter and the receiver at most
borrow; writes.instance,writes.staticandwrites.indirectallnone;returnsis[](inert, or no alias to a parameter, the receiver or the heap).
- Negative controls.
- A v1 core on v2 facts gives
OwnIRError, on both engines. - A v2 core with the section removed gives
OwnIRError. - A v2 core with a site record removed gives
OwnIRError. - A mutant that reads Unknown as harmless must turn the suite red.
- Python and Rust refusal texts are identical.
- A v1 core on v2 facts gives
- Kill fixtures.
Twiceis admitted.Mutates(order)andEscapes(order)touch the entity, so they keepuse+ verdict.TouchesGlobalState(): refusal (static write).Console.WriteLine: refusal in the extractor, unchanged text.- A → B pure: admitted. A → B mutating, escaping or global: refusal.
- A pure SCC is admitted; an SCC with mutation, escape or Unknown is refused as a whole.
- Regression. Protocol documents without a harmless call change only their version stamp. Their verdicts are unchanged on both CLIs.