Skip to content

P-037 A2.0: evidence harness hardening + terminal-green baseline (T, R) - #361

Merged
PhysShell merged 22 commits into
mainfrom
claude/p037-a2-baseline-4a8e658
Sep 19, 2026
Merged

PhysShell merged 22 commits into
mainfrom
claude/p037-a2-baseline-4a8e658

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Summary

  • Brings main through the P-037 A1.1/A2.0 hardening chain, ending at the
    terminal-green candidate T (4a8e6582e10222403cd40adc9e95db7e0228a1c2).
  • Adds R (5fd6bfa6abd4c2af7e53712af300cf66c97f2f50): the four baseline
    evidence records (MOS repo, MOS corpus, verdict/python, verdict/rust)
    taken at T in a pinned measurement environment, each independently
    verifyd. Evidence-only commit — no instrument/treatment change.
  • This is a prerequisite for the A2.1 evidence PR (dce26ed + 2cbd6c6,
    currently on claude/p037-a2.1-after-dce26ed), which was branched
    directly from T and needs T+R already in main's history so its own
    before/after comparison lands cleanly rather than referencing baseline
    files that don't yet exist in main.

Test plan

  • CI green on R (5fd6bfa): 30/30 checks
  • All four baseline records independently verify --against T and
    verify --against R
  • git diff from T to R touches only docs/evidence/**

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_011wKL9pEApQTK4dE3eXv8g8

🤖 Generated with Claude Code

claude and others added 22 commits September 18, 2026 10:59
…" is checkable

A1.1 changes the fact surface the Roslyn extractor emits BEFORE it changes any
semantics (#304, the a1/a2 sequence). Every one of those steps carries the same
obligation — richer facts, same verdicts — and an obligation nobody can check is
a wish. This is the checker. No production code, no verdict change: the tool
only runs the launcher and records what it already says.

Method is deliberately scripts/benchmark.py's: one own-check run per FILE, never
per directory. A directory run compiles a case's before.cs and after.cs into one
compilation where each resolves the other's symbols; the per-file runs are what
the corpus was labelled against, so a snapshot taken any other way would measure
a different program.

Captured at --severity note, which is strictly MORE information than the verdict
threshold: every finding is recorded with its level, so a comparison can be read
at verdict level (error/warning) or including advisories. That separation is
load-bearing for A1. An OWN051 appearing where a fabricated `release` used to sit
is the LEGACY_HONESTY class arriving, not a regression, and a snapshot that had
discarded advisories could not tell those two apart.

--engine is explicit and required (#262 Stage 3). A bare invocation resolves the
Rust candidate and exits 2 without one, and the A1.1 change is in the EXTRACTOR,
shared by both engines, so a snapshot that did not name its engine would not say
which half of the seam it measured. `compare` refuses two snapshots whose engines
differ: that comparison measures the engine, not the change.

Each snapshot records its source_commit and whether the tree was dirty, and a
dirty snapshot marks itself `is_evidence: false` in its own payload rather than
being quietly indistinguishable from a clean one. It is still written — the inner
loop needs it — it just cannot be cited.

A file whose SARIF fails to parse is recorded as parse_error with a stderr tail,
never as a file with no findings. The two are opposite facts and must not share
a representation.

  ruff check .                    All checks passed!
  mypy (repo config, --strict)    Success: no issues found in 55 source files
  tests/test_stage3_surfaces.py   rc=0 — 65 invocations, 25 explicit

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…not exist; fail closed on an unreadable run

The first version passed `--severity note`. The launcher takes `error|warning`
and nothing else, so every one of the 137 runs exited 2 with "unknown --severity
'note'" having analysed nothing, and the tool recorded a confident, entirely
empty baseline for both engines. Fixed to `--severity warning`, the most
inclusive threshold the flag actually offers, and the docstring no longer
describes a level the CLI does not have.

The wrong flag was one bug. Calling the result evidence was the worse one, and
it is the one fixed here: a snapshot with an unreadable run is not a snapshot
with fewer findings. `take` now clears `is_evidence`, records the unreadable
paths, prints the exit code and stderr of the first few, and returns 1;
`compare` refuses such a snapshot outright, because comparing it would report
the absence of those runs as agreement.

This is the same principle the P-022 merge gate already states about its own
controls — a gate whose controls are failing must not be the thing that says a
merge is safe. A baseline is a gate.

The `parse_error` field is what made the defect visible at all: had unreadable
runs been recorded as "no findings", the broken baseline would have been
indistinguishable from a clean one, and every "same verdicts" proof built on it
would have been worthless while looking rigorous.

Verified against independently measured facts before being trusted again
(corpus/p036-bakeoff, engine=rust, 12 files):

  3 findings total — the three G-V4 controls' false OWN003, and nothing else
  the four guarded-consume before.cs files: 0 findings — the F3 false negatives
  legacy-honesty-else-unresolved-forward: 0 findings
  tree correctly marked DIRTY (uncommitted fix in place at snapshot time)

each of which matches a measurement taken by hand earlier, through a different
path.

  ruff check .                    All checks passed!
  mypy (repo config, --strict)    Success: no issues found in 55 source files

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
The "richer facts, same verdicts" obligation needs something to be the same AS.
This is it, taken on a clean tree at 17e7285 (the snapshot tool's own fix), one
own-check run per file, engine named explicitly.

  137 files · 84 findings · 69 files with findings · 0 unreadable · is_evidence true
  OWN001 45 · OWN002 14 · OWN050 11 · OWN025 7 · OWN003 6 · DI001 1

The property worth recording, because A1.1-a1 changes the EXTRACTOR and the
extractor is shared: at this commit the two engines are identical on all 137
files — same exit codes, same findings, same lines, same levels, zero
differences. So any post-change divergence BETWEEN the engines is attributable
to the change rather than to pre-existing drift, and does not have to be
untangled from noise that was already there.

OWN050 appearing in the capture (11, note level) confirms the threshold does
what the tool claims: advisories ride through `--severity warning` and are
recorded with their level, so a comparison can be read at verdict level or over
everything. A1 needs that distinction — an OWN051 arriving where a fabricated
`release` used to sit is the LEGACY_HONESTY class, not a regression.

The seven A1 controls, in the baseline, as the acceptance matrix expects them
to stand TODAY:

  guarded-consume-early-return        exit 0, no findings   <- F3 false negative
  guarded-consume-flag-branch         exit 0, no findings   <- F3 false negative
  guarded-consume-negation-wrapper    exit 0, no findings   <- F3 false negative
  guarded-consume-wrapper-forward     exit 0, no findings   <- F3 false negative
  gv4-control-aliased-self-null       OWN003 warning @28    <- G-V4 false positive
  gv4-control-mutated-guard           OWN003 warning @38    <- G-V4 false positive
  gv4-control-ref-alias-guard         OWN003 warning @35    <- G-V4 false positive
  legacy-honesty-else-unresolved-forward  exit 0, no findings  <- class-3 pin

Four false negatives and three false positives from one mechanism seen from two
sides. None of them may move in a1 or a2: those steps change what the extractor
SAYS, not what anything concludes. They move only at the retirement step, and
then in the direction the matrix names.

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
…o semantic cut

The frontend now SAYS what a callee does with its owned parameters. Nothing
reads the new field to change a verdict, and `ConsumesParam` is untouched — the
semantic cut is a later step, deliberately, so that a verdict movement here is a
defect rather than a feature.

WHY THIS WAS NEEDED

`build_skeletons` (own-bridge/src/lower.rs and its Python twin) reads
`functions[].params` to derive each parameter's transfer. The C# frontend has
never emitted that field, so the MOS layer — solver, transfer lattice, the whole
INF rule set — has had no parameter input from C# at all, and "does this call
consume its argument?" was answered instead by `ConsumesParam`, syntactically
and flow-insensitively, inside the extractor.

That layer is not missing the ability, only the input. Fed the shapes by hand it
already discriminates `if (g) release p` -> may, `release p` -> must, `use p` ->
no, correctly and without flattening. It was simply never asked.

TWO CORRECTIONS TO THE DIAGNOSIS PUBLISHED ON #304

1. The gate is 6509, not 6621. There are two: `if (candidates.Count == 0)` at
   6509 and `if (tracked.Count == 0)` at 6621. A parameter-only method exits at
   the FIRST and never reaches the second, so a patch applied only at 6621 —
   the line the #304 comment names — builds, runs, and changes nothing. Found
   the way anyone following that citation would find it. Both gates now admit a
   method that has an ownership-relevant parameter; the #304 comment needs the
   correction once a1 is proven.

2. The predicate is `IsOwnedDisposableType`, not `ImplementsIDisposable`. The
   strict form (which `ConsumesParam` uses) demands a resolved symbol and
   silently answers false for a type the compilation cannot see — on a
   single-file run, most of the BCL. Locals use `IsOwnedDisposableType`, which
   falls back to the syntactic name heuristic exactly for that case. A frontend
   that tracks a `Stream` local but not a `Stream` parameter would be
   inconsistent about one type for no reason a reader could defend.

WHAT CHANGED

Owned parameters are computed once, before BOTH gates; by-value only, read off
the syntax so the test agrees with itself when the symbol does not resolve.
Neither gate was deleted — removing them would emit every method in the
compilation, most carrying nothing an ownership analysis can read, a five-figure
fact diff bought with one line that looked suspicious. A method earns a record by
having something to say: tracked locals, or a parameter whose ownership a caller
must reason about.

Lowering runs over locals AND owned parameters so `param_signals` /
`definite_release` / `forward_targets` have a body to read. No `acquire` is
emitted for a parameter: every acquire site is gated on a declarator and a
parameter has none — checked against the using-statement path, not assumed.

`params` rides only when non-empty, so a method with no owned parameter keeps
byte-for-byte the record shape it had before.

MEASURED

The callees now reach the facts:

  Guarded.Close         params=[{s,14}]  body=[if@16]         guarded -> may
  GuardedWrapper.Inner  params=[{s,9}]   body=[if@11]         guarded -> may
  GuardedWrapper.Outer  params=[{s,17}]  body=[release:s@19]

That third line is worth reading twice. `Outer` only calls `Inner(s, keep)`, but
`ConsumesParam` judges `Inner` a consumer, so a `release` is emitted where a
`call` belongs and `Outer` now enters the summary layer as an UNCONDITIONAL
consumer. The fabrication does not merely corrupt the caller's facts; now that
summaries exist it will corrupt those too. That is not a regression introduced
here — it is the reason retiring `ConsumesParam` is its own later step, and it
is now visible instead of implied.

  corpus/p036-bakeoff (12 files, engine=rust): 0 of 12 verdicts moved

The full-corpus proof over both engines is still measuring and lands in the next
commit; this message claims the subset only.

Refs #304.

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

CI run 2170 went red on six jobs. Root cause, one line of it:

  [OWN004] 'parg_59' is a borrow and cannot be returned
           (it would outlive the resource it borrows)

reproduced locally on samples/OverloadSigSample.cs:

  public static FileStream Open(FileStream existing, bool flush)
  {
      var probe = new MemoryStream();   // a tracked local, so this overload emits a record
      probe.Dispose();
      if (flush) existing.Flush();
      return existing;                  // returns a PARAMETER -> not fresh
  }

The rule at that site already said what it meant — "a tracked LOCAL returned
BARE is a fresh-factory transfer" — and it held for free only because a
parameter could never be in the set. a1 put owned parameters there, so
`return existing` started lowering as a fresh return, and the core read a fresh
return of something it had not seen acquired as an escaping borrow. The core is
right and the fact was wrong: the resource belongs to the CALLER and outlives
the call.

What that return actually is, is an ALIAS of the parameter, and OwnIR cannot say
so: `aliasOf`/`aliased` are reserved in the schema and the production skeleton
builder has never emitted them — own-bridge/src/mos.rs says exactly that in its
own header. So the frontend keeps the silence it has always kept here, a bare
CFG exit claiming no owned return, rather than asserting a kind that is false.
Teaching the IR alias returns is a semantic change and is not this step's
business; this is a gap now named rather than a behaviour now invented.

The parameter test is semantic on purpose: a parameter symbol resolves from
source even when its TYPE does not (a single-file run sees little of the BCL),
and a local never resolves to IParameterSymbol, so no local loses its fresh
return to this.

MEASURED — against origin/main's extractor, same inputs, same core

  the five --flow-locals samples   main 40 findings · a1 40 findings · IDENTICAL
  dogfood, frontend/roslyn/samples main 164 findings · a1 164 findings · IDENTICAL
                                   rc=1 with --fail-on-finding, OWN001 present

The first comparison in this session was run wrong and is worth recording: it
compared a1 against the previous COMMIT rather than against main, which already
carried the broken a1, so "before" was the internal error and scored 0 findings.
A diff against the wrong baseline is not a weaker result, it is a different
question answered confidently. Redone against origin/main.

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
The obligation a1 carries is "richer facts, same verdicts". Here is the same.

CORPUS — 137 files, both engines, both levels, clean trees

  rust    17e7285 -> 2de574a  verdict: UNCHANGED   all levels: UNCHANGED
  python  17e7285 -> 2de574a  verdict: UNCHANGED   all levels: UNCHANGED

"All levels" matters as much as the verdict line: it says no advisory moved
either, so nothing was traded quietly below the verdict threshold.

THE REPOSITORY'S OWN C# — 81 files, main's extractor vs a1's, same core

  frontend/ + audit/   rust   165 findings  IDENTICAL
                       python 165 findings  IDENTICAL
  frontend/roslyn/samples (the dogfood job's input)  164  IDENTICAL, rc=1
  the five --flow-locals samples                      40  IDENTICAL

WHY TWO SOURCES AND NOT ONE

The 137-file corpus said UNCHANGED for the FIRST version of a1 too — the one
that broke six CI jobs. The corpus contains no method that returns its own
disposable parameter; the repository's own tree does, and that shape is what
`[OWN004] 'parg_59' is a borrow and cannot be returned` was.

The corpus is labelled for VERDICTS. a1 changes the FACT SURFACE, and those are
not the same coverage question: a corpus can be complete for the bugs it names
and still miss the syntax that breaks a lowering. Treating a green corpus as a
green change is exactly how the first version of this step got pushed. So the
repo-tree comparison is not a nicety here, it is the source that has already
caught something the corpus could not, and it stays in the gate for a2.

Neither source is evidence on its own and both were run against origin/main's
extractor rather than against the previous commit — a mistake made once in this
session and not repeated: the previous commit already carried the broken a1, so
diffing against it answered a different question with great confidence.

Snapshots: docs/evidence/p037-a1-after-{rust,python}.json, taken clean at
2de574a, against docs/evidence/p037-a1-baseline-{rust,python}.json at 17e7285.

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Two gaps closed. The second one was embarrassing.

THE CENSUS — corpus/p037-shapes, 11 shapes, scripts/p037_fact_shapes.py

a1 taught this the hard way: the 137-file VERDICT corpus reported UNCHANGED for
a version of a1 that turned six CI jobs red. It contains no method returning its
own disposable parameter; the repository's tree does.

  verdict coverage != syntax / fact-shape coverage

Two hundred more random C# files would not have closed that gap. One fixture per
shape does, and it fails by NAME rather than as a number that moved somewhere.
Each case records today's emitted facts and verdict; `anchored` means the facts
must stay, `pending_a2` pins today's facts anyway so a2's diff is visible per
shape instead of aggregated away, and carries its contract in prose.

  return-disposable-parameter   ANCHORED — the OWN004 shape, nailed down so it
                                does not depend on OverloadSigSample.cs happening
                                to survive in the tree
  named-arguments-reordered     a2: resolve to declared parameter index
  extension-receiver            a2: receiver -> declared parameter 0
  guard-bool-const-true/false   a2: {kind: bool_const, value: …}
  guard-forward-bare/negated    a2: {kind: param, source_param, negated?}
  guard-mutated, guard-ref-alias  a2: NOT eligible, no guard metadata at all
  guard-opaque-expression       a2: {kind: opaque} and nothing else
  call-expression-statement     a2: emit a real `call` for statement-level calls

Two findings while recording them, both corrections to things stated earlier:

1. `CallReleasesReceiver` is reached ONLY from two sites gated on
   `ThisFieldName(...)`. It is part of the FIELD disposal detector and never runs
   on a local. The earlier claim — that fixing the seam only at
   `ConsumeReleaseArgs` would leave a guarded `this T` receiver fabricating
   call-site releases — was wrong in its location. Corrected inventory:
     A  ConsumeReleaseArgs @3762  flow-locals, ARGUMENT handoff  <- the seam
     B  recursion @4586           internal to ConsumesParam
     C  CallReleasesReceiver      FIELD channel only, @5785 @5801
     D  ConsumeReleaseArgs @6562  escape analysis, rides on A

2. Consequently `var s = ...; s.DisposeIt();` — a first-party extension that
   disposes its receiver UNCONDITIONALLY — is charged OWN001. Verified with an
   unguarded probe, so the guard is not what does it. A pre-existing false
   positive of exactly the family P-037 addresses, arriving through the RECEIVER
   rather than the argument. Not A1's to fix; recorded so it cannot later be
   mistaken for something A1 caused.

CI — neither P-037 checker was running anywhere

`scripts/p037_controls.py` shipped with the A1.0 bootstrap and nothing invoked
it. A conformance control that no job runs is a file, not a control. Both
censuses now run in the Rust-default dogfood job, the one place that already has
a .NET SDK, a built Rust candidate and a Python, on the Linux leg only: both are
about the extractor's output and the two engines' reading of it, neither of
which is platform-shaped, and the Windows leg exists for own-check.ps1.

The verdict snapshot is deliberately NOT wired: 137 files times two engines is
about fourteen minutes, and it is the per-STEP gate each A1.1 commit runs and
records under docs/evidence/. Said out loud in the workflow itself, because an
unstated omission is indistinguishable from an oversight.

  scripts/p037_controls.py --engine both     4/4 match
  scripts/p037_fact_shapes.py check          11/11 shapes, both engines
  ruff / mypy                                clean, 56 source files
  tests/test_stage3_surfaces.py              rc=0, 66 invocations, 26 explicit

Refs #304.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01376o4syzCem5aQ6NssxwX2
Replace the impossible source_commit==HEAD idea with a checkable freshness
predicate: clean evidence, source ancestry, unchanged measurement closure, and
an exact path/blob manifest for every C# input under the declared roots.

The implementation closure is one shared source for verdict and MOS snapshots.
A self-checkable runtime-path list makes an omitted dependency a red control
rather than an undocumented assumption.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Reuse the existing P-022 capture protocol on both engines: one extractor
capture, Python repro capture, and the existing own-shadow-engine adapter.
There is one summaries-layer reader for the common envelope and no second Rust
dump path.

Compare the whole summaries document, not only transfer, over the labelled
corpus and the independent repository C# tree. Fact bytes may move in A2;
Python MOS, Rust MOS, and after-state cross-engine parity may not.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Upgrade the verdict snapshot to the shared A2.0 provenance contract and add an
explicit verify mode. Pin the freshness predicate with live controls for
closure coverage, input-manifest tampering, and the evidence bit.

Wire those controls plus the whole-MOS reader selftest into the existing Linux
P-037 dogfood step. The expensive verdict and two-source MOS sweeps remain
per-step evidence, not per-push CI.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Use the provenance path/blob manifest as the MOS sweep denominator instead of
filesystem rglob. A prior dotnet build can leave generated obj/**/*.cs files in
the checkout; measuring those while the evidence manifest names only committed
blobs would make the run and its provenance describe different input sets.

Fail closed when a committed input named by git is absent from the working tree.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Address the first CI signal from the provenance module. Use collections.abc
for Iterable and the repository's preferred unpacking form.

The Stage-3 surface census also treats a literal own-check launcher name in
executable Python as a possible invocation. The provenance closure only stores
that path as data, so construct the same runtime string from two literals
instead of weakening the launcher census or adding a fake engine declaration.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Follow the repository's Ruff import ordering for the new level-2 tool. No
behaviour or evidence contract changes.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Keep the two level-2 evidence sources semantically distinct. Corpus files are
independent labelled programs and remain one document per file. The repository
source is one program, so all 81 tracked C# files under frontend/ and audit/
enter one Roslyn compilation and one summaries document.

This preserves inter-file summary edges and matches the repository-tree source
that caught A1's syntax-shape gap instead of silently replacing it with 81
different programs.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Strengthen the runtime-dependency self-check: every path declared as part of
the measurement must not only be covered by SUBJECT_PATHS, it must resolve to
an actual git object in HEAD. A matching typo in both lists can therefore no
longer protect an empty path.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
The repository discovers every test_*.py module through tests/run_tests.py and
requires a run() entry point. Keep one implementation of the control and expose
main() through that adapter so both direct dogfood invocation and the full
Python matrix execute the same checks.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
Make the A2 evidence tools fail closed before any heavy baseline is recorded.

- compare only honest evidence in ancestor order, with the same inputs,
  measurement closure and toolchain identity;
- drive verdict snapshots from the exact git path/blob denominator instead of
  filesystem rglob;
- build the Rust executable from HEAD with cargo --release --locked and record
  its digest plus Python/.NET/Rust toolchain identities;
- re-check tree cleanliness after measurement;
- put both new evidence tools under strict mypy and ignore .mypy_cache;
- restore the verdict snapshot executable bit and fix Ruff import ordering.

Keep the Stage-3 launcher census strict without a string-splitting escape hatch:
the literal own-check path is provenance data, declared after all subprocess
helpers so it cannot be mistaken for a nearby invocation.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
AI-Session: ChatGPT current conversation
Strict mypy now covers both A2 evidence tools and caught the same shape in both:
the input_manifest comprehension attempted iteration before its isinstance guard
could narrow the value. Reject a non-list manifest first, then build the path
set from the narrowed list.

No evidence has been recorded on the red head, so this moves no baseline.

Refs #304.

Co-Authored-By: ChatGPT <noreply@openai.com>
AI-Session: ChatGPT current conversation
The A2.1 claim is "richer facts, zero MOS/verdict movement". A differential can
carry it only when before and after measured ONE frozen population with ONE
measuring instrument in ONE qualified execution environment, and only the
treatment moved. This makes that contract executable and hostile-tested; it
takes no baseline and starts nothing of A2.1.

scripts/p037_evidence.py (the contract, shared by both snapshot tools)

  instrument vs treatment   INSTRUMENT_PATHS (engines, launcher, capture and
                            compare plumbing, these tools) may not move across
                            a before/after pair; TREATMENT_PATHS (extractor,
                            spec) may. Neither may move between a record's
                            source commit and the HEAD it is a claim about.
  snapshot validity         record_problems: self-consistent at its own commit.
  freshness                 provenance_problems: ...and fresh at a given HEAD.
  differential eligibility  comparison_problems: before self-valid (it predates
                            the treatment change on purpose), after fresh at
                            HEAD, ancestry, identical instrument, identical
                            frozen population and support closure, identical
                            execution profile.
  frozen population         population_commit; primary .cs blobs and their
                            semantic support closure (sibling .xaml of
                            *.xaml.cs, FodyWeavers.xml on the ancestor chain)
                            derived from git at that commit and materialized
                            with the same adjacency into the ignored,
                            deterministic .p037-population/<commit>/<digest>/
                            (one directory per population, so the corpus and
                            the repo populations of one commit never share a
                            tree), re-hashed through git, never read from the
                            working tree. cwd stays ROOT so the extractor's
                            Rel(path) is identical on both sides of a pair.
  population integrity      the extractor reads its side inputs LAZILY (the
                            weaver walk runs after parsing), so a population
                            rewritten under a run changes facts without failing
                            anything visible; found live, when two concurrent
                            takes sharing one directory produced a confident
                            is_evidence=true snapshot with one extra summary.
                            A take now holds a lease on its population and
                            re-hashes every materialized file after measuring;
                            a population that did not stay intact is never
                            evidence.
  external contamination    the extractor's FodyWeavers.xml walk continues above
                            the population to the filesystem root; every
                            ancestor there must prove the file absent
                            (present or uninspectable -> REFUSED), mirroring
                            File.GetAttributes semantics exactly.
  qualified artifacts       cargo build --locked --release --message-format=json;
                            the executable path comes from the compiler-artifact
                            message, its digest, size, toolchain, host, source
                            commit and Cargo.lock identity are recorded, and the
                            file is re-hashed right before it runs. own-cli is
                            handed to the launcher as OWEN_RUST_CORE explicitly;
                            own-shadow-engine's identity must equal the build's.
  execution profile         python -VV, .NET SDK + runtimes, rustc -vV, cargo,
                            host: an eligibility field, equal across a pair.
  reference environment     OWN_EXTRA_REF_DIRS removed from every child
                            environment AND the extractor's stderr read for the
                            "+N extra references" attestation: prevention plus
                            the observation that prevention held.
  cleanliness               clean before, clean after (post_run_dirty is never
                            evidence), and --out refused inside the checkout
                            unless git-ignored: baselines are written outside
                            the tree and enter docs/evidence by a record commit.

scripts/p037_{mos,verdict}_snapshot.py

  take --population-commit, compare/verify --against; measured files are the
  materialized population and must equal its manifest; schema bumped
  (mos/2, verdict/3); older snapshots are refused, they carry none of this.

tests/test_p037_evidence.py: 14 negative controls + 1 positive

  01-04 non-evidence / dirty on either side (+ post-run dirty)
  05    before not an ancestor of after
  06-07 manifest tampered on either side (+ digest)
  08    after stale at HEAD: instrument moved, treatment moved, verify alone
  09    frozen population drift, and drift is the ONLY refusal raised;
        population rewritten or a file vanished under the run; the lease is
        exclusive
  10    executable digest != qualified build; artifact from another commit or
        a dirty tree
  11    execution profile mismatch / missing
  12    support closure drift: blob changed, file dropped
  13    OWN_EXTRA_REF_DIRS removed from the child env; the attestation line is
        read; observed extra references and an inherited env are refused
  14    external ancestor FodyWeavers.xml present / uninspectable
  +     old before, current after, an intentional treatment change between,
        same instrument/population/closure/profile -> ADMITTED

  History-shaped controls (05, 08, 09, positive) run over real synthetic
  commits in a temporary depth-1 clone, never in the checkout's .git, and pass
  from a shallow checkout. Two fitness pins: every filesystem read site in the
  extractor is classified against the four explicit-file mechanisms (counting
  File.ReadAllText would guard spelling; this guards the contract), and the
  repo population's support closure is exactly the three files it is today,
  with the linked project's FodyWeavers.xml explicitly NOT among them
  (WeaverOwnedFiles is a .csproj-mode mechanism, explicit-file mode never
  runs it).

tests/test_stage3_surfaces.py

  The launcher literal in p037_evidence.py is data. Instead of a string split or
  a position dodge it now carries a narrow marker that the census accepts only
  for a bare assignment of that literal, in that file, on that line or the one
  beside it, exactly once; the marker's own self-test proves a subprocess call
  cannot wear it.

CI: the Linux dogfood step builds both qualified executables and derives,
materializes, re-hashes and removes the frozen repo population, where cargo,
git and dotnet are real. .gitignore: .p037-population/.

Refs #304.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfeUtLRm6KFRWcTGQZxQQb
… after the run

Hostile review of 1113188 found the executable had the same TOCTOU the
population had just been cured of. Both tools qualified target/release/<bin>
once, before the run, and then executed that shared, mutable path for the
whole measurement. A cargo build landing mid-run (anyone's, any crate that
rebuilds the bin) replaces the file under the launcher; the population stays
intact, the tree stays clean, the snapshot says is_evidence=true, and
artifacts.sha256 still names the build that did not run.

The fix is the same shape as the population's: prevention plus attestation.

  build_rust_artifact the package's own artifacts are cleaned first and the
                      compiler-artifact message must report the executable as
                      NOT fresh: cargo judges freshness by source fingerprints,
                      never by the output's bytes, so a target/ file rewritten
                      by anyone before the take would otherwise be qualified
                      as is. Only the package is recompiled and linked;
                      dependencies stay cached (both bins: ~20 s).
  seal_artifact       after qualification the executable is copied into a
                      private, unique take directory outside the checkout,
                      the copy is re-hashed against the qualified digest, and
                      the run executes ONLY the copy: own-shadow-engine through
                      engine_identity(sealed), own-cli as OWEN_RUST_CORE=sealed.
  finalize_run        the one post-run attestation both tools call before
                      writing: population intact, every sealed executable
                      still the qualified build, tree clean. Any failure is
                      not evidence and the reason is kept on the record.
  record_problems     every artifact must carry an executed attestation whose
                      digest equals the qualified digest and whose
                      post_run_intact is true.

Control 10d, in the real lifecycle rather than on a dict: a qualified build is
sealed; the shared target/ file is rewritten and the run is unaffected; the
sealed copy is rewritten and finalize_run drops is_evidence, marks the artifact
tampered, and record_problems refuses the snapshot. Records with an executed
digest other than the qualified one, without post_run_intact, or never sealed
are refused as well. The artifacts CLI seals and attests on the CI runner too.

Refs #304.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfeUtLRm6KFRWcTGQZxQQb
Four sequential baseline snapshots (MOS repo, MOS corpus, verdict/python,
verdict/rust) taken at 4a8e658 in a
pinned measurement environment (CPython 3.11.15, .NET SDK 8.0.425, Rust
1.94.1), each independently verified against T. Evidence-only: no
instrument, treatment, spec, or CI file changes.

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

coderabbitai Bot commented Sep 19, 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: Repository: PhysShell/Own.NET/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 93a89c50-0c9d-4fb0-94db-6c5362f1f26c


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.

@PhysShell
PhysShell merged commit 184dba5 into main Sep 19, 2026
75 checks passed
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.

2 participants