Skip to content

feat(bridge): #259 cp5 — the full Finding, the refusal text and the rendered surfaces, proven - #339

Merged
PhysShell merged 17 commits into
mainfrom
claude/p022-cp5-bridge-verdict-7i0968
Sep 6, 2026
Merged

feat(bridge): #259 cp5 — the full Finding, the refusal text and the rendered surfaces, proven#339
PhysShell merged 17 commits into
mainfrom
claude/p022-cp5-bridge-verdict-7i0968

Conversation

@PhysShell

@PhysShell PhysShell commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Checkpoint 4 left three members of ownir.Finding carried by the Layer 3 goldens but not compared — message, related, flow — one normalization on every refusal, and the rendered surfaces with no golden at all. This PR closes all of it across checkpoints 5.0–5.4: the inventory, the message matrix and evidence slices, the refusal text, a new byte-exact fixture family for BR-V9, and the status surfaces brought level with what the tree proves. No existing golden was regenerated or edited.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — без изменения поведения

Как проверено

  • python tests/run_tests.py
  • ruff check . и mypy
  • cd rust && cargo fmt --check, cargo clippy --workspace --all-targets (zero warnings), cargo test --workspace --no-fail-fast
  • every mutation campaign this branch touches, re-run on a clean tree at its head (cp5.1, cp5.2, cp5.3, plus cp4 and shadow-cp2, both re-anchored where cp5 moved the code they mutate)

Связанные issue

Refs #259 (checkpoints 5.0–5.4). Refs #250. Does not close #259: row 4b (the obligation-protocol analysis) and the coordinate-domain decision are both outstanding.

Чеклист

  • изменение покрыто тестом/селфтестом (или объяснено, почему нет)
  • README/docs обновлены при необходимости
  • коммиты в conventional-commit стиле

Checkpoint report

Every count in this report lives in a generated fragment and is linked, never typed — the one exception being the differential counters the checkpoint report itself requires.

Scope

what landed
5.0 tests/verdict_surface_inventory.py — a completeness ledger: every BR-V4 wording branch with who owns the string, every BR-V5 evidence family and degradation rule, every BR-V9 rule, each matched against the committed goldens so the coverage column is computed from the tree. Self-policing: a finding matching zero or two branches, or a slice matching zero or two families, fails the gate.
5.1 own_bridge::Finding grows message, related, flow; the BR-V4 matrix and BR-V5 slice builders are ported; the replay compares every member.
5.2 own_cfg::Diag carries the reference's message for the resolver text BR-V3's map-or-raise line interpolates; the message= cut is removed and refusals compare in full.
5.3 ownlang/renders.py + tests/fixtures/verdict_renders/ + its harness + a Rust replay comparing the bytes — the surface that had no golden of any kind.
5.4 The census, the campaigns, spec/Bridge.md §6, spec/BridgeBehaviorMatrix.md, the P-022 row and the proposals index.

Explicit non-goals

  • The obligation-protocol family (OBL001–005)P-022 step 6b: implement Rust own-bridge with layered OwnIR parity #259 row 4b. The bridge still refuses a protocol-bearing document; the reference documents stay in rust_replay_excluded.
  • The u32 coordinate domain — its boundary controls stay excluded; the contract decision is still owed, and the stated direction is a Python-first tightening.
  • OD-1 door controls — measured, unchanged.
  • own-shadow is not extended to Layer 3. The reducer still refuses the verdict layer and records the refusal in every reduction. One thing there did change and is explained below: the engine's declaration of what it puts in the envelope, which had become false.
  • The ledger rust_replay_excluded neither grew nor shrank.

Python source of truth

ownlang/ownir.py::check_facts (the BR-V4 matrix, BR-V5 slice construction, _di_findings, _effect_findings, _unresolved_findings, the OWN051 mint), ownlang/di.py, ownlang/effects.py, ownlang/evidence.py::di_path_steps, and ownir.render_finding / ownir.build_sarif for BR-V9. ownlang/cfg.py::lookup for the one core message the refusal text quotes.

Frozen fixtures and their regeneration commands

  • tests/fixtures/verdicts/*.verdicts.jsonpython tests/test_verdict_fixtures.py --write
  • tests/fixtures/verdict_renders/*.renders.jsonpython tests/test_verdict_render_fixtures.py --write (new family)
  • tests/fixtures/unreachable_branches.jsonpython tests/test_unreachable_branch_probe.py --write (the oracle probe, below)

No pre-existing golden was regenerated or edited, verified against the branch base: the only pre-existing file touched under the four frozen corpora is manifest.json, and its diff is additions only — zero deleted lines. Cases were added to both families, and the shadow slice's digest ledger gained one record per added document with existing-record churn measured at zero: P-022 discipline rule 4, on both of its lines.

Steady-state test commands without Python

Run from rust/: cargo test -p own-bridge --test verdicts and cargo test -p own-bridge --test renders.

Production dependency changes

own-bridge gains serde as a normal dependency (it was a dev-dependency): the BR-V9 SARIF log is typed structs whose field order is the surface, so the shape is checked at compile time rather than assembled as a free-form value. No change to the crate DAG; own-diagnostics/tests/dag.rs still asserts no core crate depends on the bridge.

Behavior changes

In Python: none. The only file added under ownlang/ is renders.py, an observer in the same shape as verdicts.py and lowered.py — it calls the production renderers and records what they returned, and nothing in the production path imports it. No existing ownlang/ module was touched. The OwnIR schema is unchanged.

On the Rust side, additive core data where the reference keeps it (BR-B1 — the analysis owns its verdict, message included):

  • own_analysis::di::Service regains the ctor metadata P-022 step 4: port own-analysis (worklist solver + ownership/lifetime/effect/DI) with layered diagnostic parity #214 dropped as "presentation-only"; DiFinding carries its message, the registration (file, line) its DI004/DI005 related needs beside the call/store-site primary, and the raw site_line that says whether the primary came from a site at all — which line alone cannot, once the fallback has been applied;
  • own_analysis::effect::EffectStorm carries its message, origin_kind, decl_line and reference chain;
  • own_cfg::Diag gains message: Option<String> and own-analysis reads it. The Option is the design: an unported message renders as the code's title, and because refusals now compare byte-exact, the first golden that refuses on such a code goes red demanding the message rather than agreeing with a title — a tripwire, not a blind spot.

check_di, effect_diagnostics and own-diagnostics are untouched, so no existing core surface changed shape.

Counters over the measured set

count
Python-only 0
Rust-only 0
Changed 0
Ordering-only 0
Unexplained 0

Asserted, not tallied: each replay collects every divergence without fail-fast and fails if one exists, so a green run is 0/0/0/0/0 by construction. That the comparisons bite was checked rather than inferred — breaking a single wording turns a large block of verdict cases red, and breaking a single escaping rule turns a render case red.

The measured set, and what it is not — generated, never typed: p022-cp4-census.md (the Layer 3 census, both families, describing the cp5 surface) and p022-cp5-inventory.md (which BR-V4 wording, BR-V5 slice family and BR-V9 rule the corpus reaches, and the recorded disposition of every one it does not). Unmeasured, named: protocol documents (row 4b), coordinate-domain controls (decision owed), OD-1 door controls.

The oracle probe: where the unreachable branches' expected text comes from

Some BR-V4 wordings are unreachable end to end — the routing table never mints a handle that would take them, or the analysis never reports a verdict that would select them. Their controls have to get their expected text from somewhere, and "the reference's own output" is only worth saying if someone else can re-run it.

tests/test_unreachable_branch_probe.py runs check_facts with its lowering and core substituted, which is the only way to ask the oracle about a state its own inputs cannot construct, and records what it answered into tests/fixtures/unreachable_branches.json. The Rust controls (own-bridge's verdict::tests, own-analysis's effect::tests) include_str! that record and read the wording out of it rather than carrying their own copy — one source of truth, and the two sides cannot drift into agreeing with each other instead of with Python.

What the probe does not prove is stated beside it: the substitution removes reachability, ordering and the pipeline around the branch, so it can never stand in for a golden anywhere a golden is possible. It is used only where one is not.

Two mutations in the cp5.1 campaign attack the wiring rather than the port: corrupt a recorded wording, and the corresponding control must fail. A control that survives a drifting oracle record is carrying a literal and only appears to read the reference.

Mutation campaigns

All re-run on a clean tree at this branch's head, rendered into p022-cp5-mutations.md and p022-cp4-mutations.md by the one interpreter every campaign in the tree shares. Counts live only there; the suite gate fails while any is stale, was taken on a dirty tree, missed a required catcher, or names a commit this tree does not descend from.

Divergences found, and how each was classified

Between Python and Rust, on the verdict list: none. The first full-equality run over the cases cp4 replayed was green.

Which is exactly why the rest of the work exists — a green replay over a corpus that never reaches a branch proves nothing about it.

  1. Reachable branches the inventory named → synthetic cases under the frozen ledgers: the OWN009 and pooled flow-local wordings with their slices, the DI-scoped and both inline-lambda OWN014 wordings, the injected-source lambda note, the dropped escape slice, the bare consuming-constructor tail, the one-step DI retention path, the dropped effect slice, the line-guard degradations, present-but-degenerate metadata, and the dedup pairs below.
  2. Branches no facts document can reach → controls driven through map_core, in cp4's M19 shape, reading their expected text out of the recorded probe above.
  3. A port bug, found by removing a comparison boundary. cp5.2 removed the message= cut and the hoist_neg_* refusals went red immediately — on py_repr. CPython's repr switches from ' to " when a string contains a single quote and no double quote, and every core message naming an identifier does; cp4's placeholder quoted unconditionally. Fixed in Rust, pinned against CPython's own repr() output across the quote switch, both escape directions, the backslash, the ASCII control range and None. This is the argument for removing a boundary instead of living with it: it was hiding a formatting defect in code that was already ported, not text that was not.
  4. A regression the campaign found and a green suite would not have. Re-running cp4's campaign against the cp5.1 tree turned its BR-V7 dedup key drops <member> mutations from caught to survived. Not a weakening: putting message in the key made several of its members unobservable at the output surface — every wording that varies with event, kind or severity interpolates it. The recoverable ones are now a golden (verdict_dedup_key_members: the disposable-field wording names no handler, the flow-local wording names no component); the rest drive dedup, extracted from check_facts so the control runs the production function rather than a copy, and the cp4 definition names that control instead of the replay. Worth stating because it will recur: a comparison surface that gains a member can lose controls for the members it subsumes.
  5. A false declaration in a committed artifact. own-shadow's capture declared its verdict layer a partial projection whose stated reason was that BR-V4 and the evidence slices "are checkpoint 5 and are not ported". After 5.1 that was false. The layer now emits every member and declares full; artifacts and traces were regenerated. The reducer still refuses the verdict layerP-022 step 7a: add dual-engine shadow mode and zero-diff reproduction artifacts #260's boundary, unmoved. Promoting it opened a gap and the gap is closed rather than noted: the projection check only validated partial claims, so a full declared over a short document — the over-claim that became reachable the moment nothing was partial — was unchecked. It validates both now, and the shadow cp2 campaign's M35 was re-anchored from the lie that is no longer possible to the one that is.

Two properties of the surface, recorded rather than papered over

  • The pooled "rented but never returned" sentence is emitted by two branches of the BR-V4 matrix, on the same kind, with the same empty handler. Nothing in a serialized Finding separates them, so the inventory carries them as one row and says why. A port reaching the sentence by the other branch would produce a byte-identical golden — that is what parity on this surface means.
  • BR-V9 codeFlows reuses own_diagnostics::code_flow verbatim; relatedLocations deliberately does not. The core's builder drops a step whose file is empty — the "unprocessable log" invariant evidence.py names — while ownir.py's inline comprehension filters on the line alone and emits the empty uri. Python is the oracle, so the port reproduces that, and render_evidence_slices is the golden that goes red if someone simplifies the two into one. own-diagnostics behaviour on the core path is unchanged.

The checkpoint-4 subject tail, closed

cp4 established that no Rust output surface serializes a diagnostic's subject and left the re-check to cp5, once the bridge grew render and SARIF paths. Both harnesses now scan the rendered bytes for a subject key. ownir.Finding has no such member, so a bridge surface emitting one would be inventing a field.

Wording

Layer 3 parity over the measured set at the full Finding and the rendered surfaces; unmeasured set: protocol documents (4b), coordinate-domain controls (decision owed), OD-1 door controls.

Not "verdict parity complete", not "#259 complete", not "shadow mode", not "P-022 done".

CI

GitHub Actions runs on this head are linked from the checks section of this PR.

Not for merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb

Checkpoint 4 left three Finding members carried but not compared (message,
related, flow) and the rendered surfaces with no golden at all. Before porting
any of it, establish what the frozen goldens actually reach.

tests/verdict_surface_inventory.py is the ledger: every BR-V4 wording branch
read off ownlang/ownir.py with WHO OWNS THE STRING (the bridge's matrix, the
DI/effect finders' own `message`, or the core diagnostic's), every BR-V5
evidence family and degradation rule, and every BR-V9 rendered-surface rule —
each matched against the committed goldens so the coverage column is computed
from the tree. It is self-policing: a finding matching zero or two branches, or
a slice matching zero or two families, is a ledger problem that fails the gate
rather than a count rounded down.

scripts/render_checkpoint_status.py renders it into
docs/generated/p022-cp5-inventory.md, and the existing suite gate holds it in
sync, so no cp5 number is ever typed into a status surface.

Two readings the inventory forced, both recorded in the note:

* the core-diagnostic message layer does not exist on the Rust side — every
  core diagnostic is built with its TITLE as the message, and own-cfg's Diag
  carries no message field at all, which is why the three map-or-raise refusals
  are still compared up to their `message=` member;
* the pooled "never returned" sentence is emitted by two branches of the matrix
  and is byte-identical on a byte-identical `kind`, so the two are one row here
  — a property of the surface, stated rather than papered over.

No production code changed; no golden was regenerated or edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…cp5.1)

own_bridge::Finding grows `message`, `related` and `flow`; the BR-V4 wording
matrix and the BR-V5 slice builders are ported; own-bridge/tests/verdicts.rs
compares EVERY member of the reference's Finding instead of cp4's identity /
anchor / kind / tiering projection.

No golden was regenerated or edited to get there. The Layer 3 family has
carried those three members since cp4 precisely so this checkpoint could
tighten the comparison against the files as committed, and it went green
against them. That the comparison bites was checked, not inferred: breaking one
wording turns fourteen cases red.

Three additive pieces land in own-analysis, because the reference keeps them
there and BR-B1 says the analysis owns its verdict, message included:

* di::Service regains the ctor metadata #214 dropped as "presentation-only",
  and DiFinding carries its message, the registration (file, line) its
  DI004/DI005 `related` needs beside the call/store-site primary, and the raw
  site line that says whether the primary came from a site at all — which
  `line` alone cannot, once the registration fallback has been applied;
* effect::EffectStorm carries its message plus the origin kind, mint line and
  reference chain it is built from; the lattice already computed the chain and
  the port was discarding it;
* nothing else moves — check_di and effect_diagnostics keep their (line, code)
  projections, so no existing core surface changes shape.

A green replay over a corpus that never reaches a branch proves nothing about
it, so the cp5.0 inventory's named gaps are closed rather than noted. Six
synthetic cases cover every reachable one (the OWN009 and pooled flow-local
wordings with their slices, the DI-scoped and both lambda-note OWN014
wordings, the injected-source lambda note, the dropped escape slice, the bare
consuming-constructor tail, the one-step DI path, the dropped effect slice);
adding them rewrote zero existing records in the verdict ledger and zero in
the shadow digest ledger, which is discipline rule 4 measured rather than
asserted. Five branches no facts document can reach are pinned by controls
driven through map_core, in cp4's M19 shape — and their expected text is the
reference's own output, taken by substituting the lowering under check_facts,
not a reading of ownir.py.

The cp4 campaign definition is re-anchored where cp5.1 moved the code it
mutates; its recorded run is superseded and re-run in the following commit,
which is what the anchor gate exists to demand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…laration

The cp5.1 campaign's first run caught 31 of 40. Eight survivors and one
compile error are each a statement about the corpus, so each gets an answer
rather than a footnote.

Three synthetic cases, added under the frozen ledger (insertion-stable: zero
existing records rewritten in the verdict ledger or the shadow digest ledger):

* `verdict_unknown_line_guards` — one record per slice builder that IS a
  verdict but cannot anchor its slice: an acquire with no line, a proven
  captive whose subscribe line is unknown, an effect whose minting binding has
  none. All three must emit their verdict and no slice; a step at line 0 is a
  well-formed coordinate pointing nowhere;
* `verdict_degenerate_metadata` — metadata present and degenerate, which is not
  absent: an empty `type` (the parenthetical is a truthiness test) and a `"?"`
  ctor type (the tail and the related label drop to the bare wording rather
  than quoting a placeholder as a type name);
* `verdict_dedup_needs_the_message` — two findings equal on every other member
  of the BR-V7 key and differing only in the `type` that rides into the
  message. cp4 argued the message member was redundant on the reference's own
  outputs; this is the document where it is not, and without it a leak is lost.

One survivor is unreachable end to end and says so itself: the effect message's
`via` clause is guarded on a chain longer than one hop, and a storm only words
"derives from" when it walked at least one reference — so the guard is pinned
by a unit control on a hand-built storm, on the same reasoning the reference's
own `len(path) > 1` rests on.

M18 did not compile (an unused format argument) and was rewritten to attack the
same rule with valid Rust; a compile error is reported as such and never as
"caught".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
Forty mutations over the production surfaces cp5.1 added: every branch of the
BR-V4 wording matrix (both OWN001 splits, the pooled twins, the OWN025 view
sentence, both OWN014 families with their lifetime phrases and the inline-lambda
note in both directions, every token kind, the three advisories), the
analysis-owned DI and effect messages with their two conditional tails, every
BR-V5 slice family with its line guard and its short-slice degradation, and the
BR-V7 dedup key's new message member.

Every one is caught, and by the layer that should catch it: the goldens catch
the reachable branches through the replay, and the four rules a facts document
cannot reach are caught by the unit controls that exist because it cannot.

The cp4 campaign's recorded run is still the superseded one — it re-runs next,
against the re-anchored definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…rd why

Re-running the cp4 campaign against the cp5.1 tree turned five
"dedup key drops <member>" mutations from caught into survived. Putting
`message` in the key made three of its members unobservable at the output
surface: every wording that varies with `event`, `kind` or `severity`
interpolates it, so no facts document can produce two findings equal on the
message and differing on one of the three.

Two of the five were recoverable and are now a golden. The disposable-field
wording names no handler, so one field disposed by two methods is kept apart by
`handler` alone; the flow-local wording names no component, so the same local
leaking in two methods of one file is kept apart by `component` alone.

The other three now drive `dedup`, extracted from `check_facts` so the control
runs the production function instead of a copy of it, and the cp4 definition
names that control instead of the replay — the replay genuinely cannot catch
them any more, and leaving it listed would have been a coverage claim the tree
does not support.

Worth stating because it will recur: a comparison surface that gains a member
can lose controls for the members it subsumes. The campaign surfaced it; a
green suite would not have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…ition

30/30 caught on the cp5.1 tree, with the three unobservable dedup members now
caught by the control that can see them. The previously recorded run measured
a tree cp5.1 reshaped, which is what the anchor gate exists to say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
Both campaigns now name the same commit, so neither describes a tree the other
has since reshaped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: a424e54b-49b4-4690-afd1-4d096091bb4a


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… cp5.2)

The `message=` normalization is gone from both sides: the three `hoist_neg_*`
map-or-raise refusals now compare byte for byte, and no comparison boundary is
left on a refusal.

`own_cfg::Diag` carries the reference's message where this core has it, and the
resolver's `undefined name '<name>'` is what it has — measured, not assumed:
driving every facts document in the four shared corpora through to_module +
check_module and keeping the diagnostics that fail to map yields exactly one
message, and that is it. `own-analysis` reads it and falls back to the code's
title where own-cfg has none.

The remainder being an Option is the point. A code whose text is not ported
still renders as its title, and because the refusal comparison is now
byte-exact, the first golden that refuses on such a code goes RED demanding the
message rather than agreeing with a title. Porting only what the surface
consumes is safe because the boundary became a tripwire.

Removing the cut turned the three refusals red at once, on a member cp4 could
not see. The reference formats the interpolated message with CPython's repr,
which switches from ' to " when the string holds a single quote and no double
quote — and every core message naming an identifier does. cp4's placeholder
quoted unconditionally. Classified as a port bug, fixed in Rust, and pinned
against CPython's own repr() output across the quote switch, both escape
directions, the backslash, the ASCII control range and None. That is the case
for removing a comparison boundary instead of living with it: it was hiding a
formatting defect in code that was already ported, not text that was not.

One consequence in own-shadow. Its capture declared the verdict layer a partial
projection whose stated reason was that BR-V4 and the evidence slices are not
ported — false since cp5.1, and a committed artifact carrying a false
declaration is worse than none. The layer now emits every member and declares
full; artifacts and traces are regenerated. The reducer still REFUSES the
verdict layer and records the refusal in every reduction: that is #260's
boundary and it has not moved.

Promoting it opened a gap, closed here rather than noted: the projection check
only validated partial claims, so a full declared over a short document — the
over-claim that became reachable once nothing was partial — was unchecked. It
now validates both, and the shadow cp2 campaign's M35 is re-anchored from the
lie that is no longer possible to the one that is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
The cp5.2 campaign runs in workspace mode, where a catcher is labelled by
package and test target, and was written with the shadow slice's layer labels;
the shadow cp2 definition still named the projection test under the name it had
before cp5.2 widened it. Both are re-anchored so a required catcher that did
fire is recorded as having fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
Ten mutations over what cp5.2 built: the resolver message own-cfg now carries,
the seam own-analysis reads it through, every rule of the CPython repr the
BR-V3 refusal quotes it with (the quote switch in both directions, the
backslash, the ASCII control range, None), and the shadow engine's verdict
capture, which stopped being a partial projection here.

Three of the ten are caught only by the py_repr control, and that is the honest
split: the corpus's messages contain no backslash and no control character, so
those rules have no reachable golden and the unit test exists for exactly that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…d M35/M36

cp5.2 made the over-claim M35 attacked impossible (nothing declares a partial
projection any more) and widened the test M36 names. Both are re-anchored and
the campaign re-run: 11/11 caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…yte (#259 cp5.3)

BR-V9 had no golden of any kind: Layer 3 froze what the bridge concludes, never
what a consumer sees. `render_finding` and `build_sarif` were pinned only by a
handful of hand-written strings in test_ownir.py, which is not the same as
freezing the bytes a run emits — and the cp5.0 inventory reported every BR-V9
row as having no control.

A new fixture family, built to the Layer 3 pattern:

* ownlang/renders.py — an observer beside verdicts.py, imported by nothing in
  the production path. Every format at both host severities, plus one format
  render_finding does not know, so the fallback is RENDERED rather than
  asserted equal to the human line;
* tests/fixtures/verdict_renders/ — seven targeted cases under a frozen
  manifest ledger, each naming the BR-V9 rows it is the control for. Listed,
  never swept: rendering 79 documents at two severities would freeze megabytes
  to prove less than seven documents chosen for the rules;
* tests/test_verdict_render_fixtures.py — verify/--write, stale/missing/
  orphaned each red;
* own-bridge/tests/renders.rs — zero Python, compared BYTE for byte. Not a
  value comparison: SARIF key order is part of this surface, so the port
  reconstructs the document through typed structs whose field order is the
  emitter's key order, rather than a map type that sorts and would have quietly
  produced a different document.

Reuse where the format is shared, and not where it is not. codeFlows is
own_diagnostics::code_flow verbatim. relatedLocations is deliberately the
bridge's own: the core's builder drops a step whose file is empty — the
"unprocessable log" invariant evidence.py names — while ownir.py's inline
comprehension filters on the line alone and emits the empty uri. Python is the
oracle, so the port reproduces that, and render_evidence_slices is the golden
that goes red if someone simplifies the two into one. own-diagnostics is
unchanged on the core path.

The checkpoint-4 subject tail is closed rather than restated: both harnesses
scan the rendered bytes for a `subject` key. ownir.Finding has no such member,
so a bridge surface emitting one would be inventing a field.

Every BR-V9 ledger row is now pinned, and the join is computed: the row ledger
is in the inventory, the pins are in the fixture manifest, a row nobody pins
reads GAP, and a case pinning a row the ledger does not know is a hard problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
The first cp5.3 run caught 25 of 29. Each of the four is answered.

`render_rule_catalogue` is a document where the SARIF log's two orderings
disagree: the advisory sits on the first line and the leaks after it, so the
results run OWN050, OWN001, OWN001 in the bridge's own order while the
catalogue must run OWN001, OWN050 — sorted and deduplicated. Until it existed,
a log that sorted its results, left its catalogue in encounter order, or listed
OWN001 twice all matched the goldens; three mutations survived on that alone.

The fourth is defensive and says so: `relatedLocations` drops a step whose line
is unknown, and no facts document can build one — every `related` builder
requires its line to be `>= 1` before it emits a step at all. It is the
reference's rule either way, and dropping it would emit `"startLine": 0`, which
is not a coordinate, so it is pinned by a unit control on the production
function instead of left unproven.

M28 did not compile and was rewritten to attack the same rule with valid Rust.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
Twenty-nine mutations over BR-V9: the three line-per-finding formats and their
host-severity pass-through, every workflow-command escaping rule in both
directions and in the order they compose, and the bridge's SARIF log — the
level mapping's three branches, the sorted and deduplicated catalogue with its
titles, the ownirSchemaVersion stamp, region omission and startColumn,
backslash normalisation, conditional properties, both evidence projections
(including the one place reusing the core's builder would be a bug),
suppressions, and the result order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…cp5.4)

The census fragment now describes the checkpoint-5 comparison surface — every
`Finding` member and every refusal in full — and counts the rendered-surface
family beside the verdict ledger through tests/verdict_render_census.py, the
same interpretation the render harness and the inventory use. Its filename
stays checkpoint 4's, because that is where it was introduced and two notes
link it; what it describes it now says in its own first paragraph, which keeps
one census for one ledger instead of two that can disagree.

The cp5.1/5.2/5.3 campaigns are registered like cp4's and the shadow slice's
and rendered by the same single interpreter, so the existing gate holds them:
a result that no longer matches its definition, was taken on a dirty tree,
missed a required catcher, or names a commit this tree does not descend from is
a red build.

spec/Bridge.md §6 and spec/BridgeBehaviorMatrix.md move the BR-V4 wording rows
and the BR-V9 rendering rows from "carried by the goldens, compared at cp5" to
compared, marked L3 ✅ with a legend saying what the mark means. The P-022
status row, the proposals index and the cp4 note's forward pointer move with
them. The cp4 note keeps its own numbers as history and says so.

No count is typed into any status surface: every number lives in a generated
fragment and the prose links. The wording these surfaces now carry is "Layer 3
parity over the measured set at the full Finding and the rendered surfaces",
with the unmeasured set named — protocol documents (row 4b), the
coordinate-domain controls (decision owed), the OD-1 door controls. Not
"verdict parity complete", not "#259 complete", not "shadow mode".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
@PhysShell PhysShell changed the title feat(bridge): #259 cp5.0–5.1 — the message matrix and the evidence slices, proven feat(bridge): #259 cp5 — the full Finding, the refusal text and the rendered surfaces, proven Sep 6, 2026
Three review findings, each taken as stated.

**The probe is in the tree now.** The controls for branches no facts document
can reach claimed their expected text was "the reference's own output, taken by
substituting the lowering" — but the substitution existed only in a shell and a
paragraph of prose. Nobody could re-run it, which made those strings typed
numbers wearing a sentence's clothes.

tests/test_unreachable_branch_probe.py performs the substitution and records
what the reference answered into tests/fixtures/unreachable_branches.json, with
verify/--write like every other family. The Rust controls (own-bridge's
verdict::tests, own-analysis's effect::tests) now `include_str!` that record and
read the wording out of it instead of carrying their own copy — so there is one
source of truth, the two sides cannot drift into agreeing with each other
instead of with Python, and a changed reference wording turns both red.

The probe's own limits are written down beside it: the substitution removes
reachability, ordering and the pipeline around the branch, so it can never
stand in for a golden anywhere a golden is possible, and it is used only where
one is not.

Two mutations attack the wiring rather than the port: corrupt a recorded
wording, and the corresponding control must fail. A control that survives a
drifting oracle record is carrying a literal and only appears to be reading the
reference.

**The queue no longer contradicts the row above it.** P-022's preferred queue
still read "#259 cp5 → 4b … 4b does not block cp5" while row 5 in the same
table read complete. It now reads 4b → the coordinate-domain decision → #259
final acceptance → #260 acceptance / #261, and the sentence about not blocking
cp5 goes with cp5.

**The proposals index stops being a database.** The P-022 row carried typed
counts from the step-7a slice; they are replaced by what the slice established
plus a pointer to the two generated fragments that count it. The cp1 numbers
beside them stay: they predate the generator and have no fragment to point at,
so removing them would delete information rather than relocate it.

One thing found on the way: the capture-branch disposition had a test name
wrapped mid-identifier, shipping a reference nobody could follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
…mutations

42/42 caught. The two new ones corrupt a recorded oracle wording and require
the Rust control that reads it to fail — so "these controls read the
reference's answer, not a literal" is now a recorded fact rather than something
demonstrated once in a shell and described afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ471fbXuoL2Poo4Ap3Kfb
@PhysShell
PhysShell merged commit 6d2d8ea into main Sep 6, 2026
46 checks passed
PhysShell pushed a commit that referenced this pull request Sep 6, 2026
…259 cp4b.2)

`refuse_protocols` is gone. `own_bridge::check_facts` now runs the obligation
family in its BR-V1 place — after effects, before OWN050 — through the shared
grammar and `own_analysis`'s walk, and does the half the bridge owns: the
(kind, definite) -> OBL001-004 table, the four deliberately line-free
wordings with both exit_desc branches, the component/handler derivations, the
anchorless advisory OBL005 with its sorted CPython-repr scope list, and the
opened -> barrier (or exit) -> late-close slice with its line guard.

The two reference documents leave `rust_replay_excluded` and the replay's
pinned exclusion set drops from eight names to six. That is the contract
change this checkpoint exists to make. Neither golden was regenerated: the
port converges on `protocol_isloaded_violation` and its silent twin exactly as
committed, on every `Finding` member.

Seven synthetic Layer 3 cases close every row the corpus could not reach —
OBL002/003/004/005, all four wordings, the two-, one- and zero-step slices,
the identity derivations including the awkward splits, the tolerant door's
skip-whole and first-wins rules, and the family composing with DI, effects and
OWN050. Insertion-stable: zero existing records rewritten. An eighth case
freezes the rendered surfaces — OD-6's literal `?` URI with the region omitted
at level note, and a `codeFlows` built from a protocol slice.

Two things measured rather than assumed, and recorded instead of faked into
controls: the BR-V5 "a slice shorter than two steps is dropped" rule is NOT
applied by the protocol path (a leak off the end carries a one-step slice, and
one whose open has no line carries none), and the family's append POSITION is
unobservable end to end because the sort key's code component decides before
insertion order can.

The surface inventory follows: the `.*` placeholder row becomes five wordings
and two exit_desc tails, the two protocol slice families become five precise
shapes plus an empty-slice degradation, and every one of them is now counted
over the REPLAYED set instead of carrying a "row 4b" disposition.

Shadow: the Rust engine's `verdicts` layer for the protocol document moves
from `refused` to `produced`, so its artifact and trace regenerate — the only
goldens this checkpoint rewrites, for the same reason #339 recorded when the
capture moved partial -> full. The reducer's scope is untouched and still
refuses Layer 3: that line is #260's.

One gate is knowingly red until the next commit: cp4's mutation M14 attacked
the refusal this checkpoint removes, so its definition is re-anchored on the
rule as it is now stated (the family must not go silently missing) and the
recorded result no longer matches its definition. The re-run follows
immediately, exactly as d80359e did for the previous re-anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbgYFv2UW3iRJ3L33naVC3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P-022 step 6b: implement Rust own-bridge with layered OwnIR parity

2 participants