Skip to content

Retain observed execution footprints for falsification - #716

Draft
flyingrobots wants to merge 8 commits into
mainfrom
feat/falsification
Draft

Retain observed execution footprints for falsification#716
flyingrobots wants to merge 8 commits into
mainfrom
feat/falsification

Conversation

@flyingrobots

Copy link
Copy Markdown
Owner

Goal

Establish the scheduler-side evidence foundation for first-class falsification witnesses: retain a canonical per-Action actual read/write footprint without weakening the ordinary enforcement panic or widening FootprintGuard.

Plan

  • Define actual-footprint values, posture, and deterministic subset evaluation.
  • Add an observed executor capability while preserving provider-v1 as explicit legacy compatibility infrastructure.
  • Retain canonically ordered per-Action evidence across worker execution and runtime worldline state.
  • Prove record-before-panic behavior, write/guard agreement, legacy posture, generated-consumer compatibility, and worker-count invariance.
  • Correct the pre-existing barren-writer-epoch LSN hole that blocked the workspace test gate.
  • Update the canonical falsification roadmap, authorship invariant, WAL semantics, and changelog.

Current status

Stages 1 and 2 of the 14-stage falsification roadmap are implemented. Stage 2 is complete locally: native and generated rules use the observed ABI, provider-v1 remains legacy, serial execution retains no evidence, and the scheduler stores canonical per-Action evidence before propagating ordinary panics.

Validation is green for the full workspace tests, all-target Clippy with warnings denied, formatting, documentation lint/dead-link checks, release builds with and without footprint_enforce_release, the unsafe_graph build, and release enforcement footprint tests.

Stage 3—delivering these records to the read-only property evaluator—is intentionally outside this PR's implemented boundary.

Open questions

  • ADR 0027 remains Proposed pending owner acceptance. Does this implementation establish the intended trust boundary and legacy/provider posture strongly enough to accept it?

Add docs/topics/FalsificationWitnesses.md as the design doc and roadmap
for first-class falsification artifacts: GeneratedPropertyV1,
PropertyInstanceV1, CounterexampleProposalV1, and
AdmittedFalsificationWitnessV1.

Anyone may propose a counterexample; only Echo may admit that it
falsifies an exact property instance. Discovery stays outside the
admission trust boundary, admission requires fresh-host exact replay,
reduction must preserve a typed violation class, and the target
worldline is never rewritten — witnesses append to a separate evidence
worldline.

Every claim is anchored to source at c354d53. Verification against the
tree corrected four errors in the source draft and added two roadmap
stages that it omitted:

- FootprintViolation is a std::panic::panic_any payload, not a returned
  value, so a property evaluator cannot receive it today. Stage 2
  (guard reification) is new.
- There is no ExecutableActionEvidence observation projection; the only
  projections are Head, Snapshot, TruthChannels, and Query, and
  RecordedTruth admits only TruthChannels. Stage 3 (footprint evidence
  projection) is new.
- WalAppendAuthority::AdmissionKernel already exists and is used by
  CausalAnchorAdmission; only the transaction kind (next stable code 13)
  and record kinds (next stable code 32) are new.
- The guard reports nine distinct ViolationKind variants, three of which
  are not read/write footprint violations and must not be merged into
  one violation class.

Also records two coupled edits the roadmap will need: adding ADR 0027
requires updating tests/docs/test_adr_namespace.sh, and wiring the
footprint_enforce_release lane into CI requires changing the literal
sentence in GeneratedRules.md that test_generated_rule_truth.sh asserts.

Index the topic in docs/topics/README.md.
Add docs/adr/0027-first-class-falsification-witnesses.md fixing the
boundaries the falsification schemas must respect: the discovery/admission
trust boundary, the closed property-outcome sum, fresh-host replay as the
admission boundary, typed-violation-preserving reduction, always-qualified
minimality, dual identities, the never-rewritten target worldline,
durability before publication, and enforcement posture as evidence.
Records twelve rejected alternatives.

Respecify the footprint blocker in the topic doc after reading
FootprintGuard rather than only its violation payload. The guard holds
exactly six declared BTreeSets and has no accumulator; every check_*
method takes &self, compares one access, and panics on a miss. Nothing in
Echo records what an Action actually touched, so the subset relation
Actual ⊆ Declared cannot be evaluated at all — catching the panic yields
one violating access, not a footprint.

Consequences:

- Stage 2 becomes accumulation, not unwind-catching. The sink must record
  every checked access, additively, leaving the panic path unchanged.
- Stage 3 becomes execution-evidence delivery rather than a new
  observation projection. The evaluator already receives execution
  evidence on a channel separate from the reading, so routing footprints
  there avoids widening the bound aperture — which the design forbids.
- Open question 4 (per-Action vs per-Tick granularity) is closed. The
  guard is constructed once per rule execution and pre-filtered to one
  warp, so an accumulator hung off that instance is per-Action by
  construction.
- Two open questions replace it: the pub(crate) guard visibility seam,
  and the cost of an accumulator on a guard that is live in every debug
  build.

Add a Status column to the roadmap table. Index ADR 0027 in
docs/adr/README.md and register it in tests/docs/test_adr_namespace.sh
(current_adrs plus current_adr_last=27) as the coupled edit the roadmap
predicted. Update CHANGELOG.

All four doc-truth tests pass; markdownlint clean.
Add ActualFootprint: an accumulated record of the graph resources an
execution actually touched, comparable against a declared Footprint.

FootprintGuard holds only the six declared BTreeSets. Every check_*
method takes &self, compares one access, and panics on a miss — it
answers "was this access declared?" and immediately forgets. Nothing
recorded what an Action actually touched, so the soundness relation

    ActualRead(a)  ⊆ DeclaredRead(a)
    ActualWrite(a) ⊆ DeclaredWrite(a)

had no left-hand side and could only be observed as a panic. A read-only
property evaluator cannot refute a footprint claim that way.

soundness_violations returns the existing ViolationKind vocabulary in a
fixed axis order — node reads, edge reads, attachment reads, then the
three write axes — canonically within each axis, so the same execution
yields the same violation sequence on every host.

from_ops derives the write axis from an emitted op sequence via
op_write_targets, the same extraction enforcement uses, so a recorded
write set and an enforced write check cannot disagree about what an op
mutates. Cross-warp and instance-level concerns are excluded on purpose:
CrossWarpEmission and UnauthorizedInstanceOp are scope and authority
questions the guard already reports, and recording them as local writes
would misreport them as footprint-subset failures.

The read axis is not derivable from ops and is left unwired. Reads reach
the guard through GraphView, whose guards live in WorkUnit and are shared
across worker threads, so an accumulator inside the guard would make
WorkUnit !Sync and break parallel execution. That fork is documented for
review rather than decided here.

Enforcement behaviour is unchanged. Recording is additive and never
panics; the guard's panic remains the correct response to an undeclared
access during ordinary execution.

14 unit tests cover subset semantics, superset soundness, axis
separation, per-warp scoping, canonical ordering, idempotence,
order-independence, op-derived writes, cross-warp exclusion, and the
edge-writes-imply-from-node adjacency rule.

Also add #[allow(clippy::panic)] to the external_action test module,
matching the convention in other in-src test modules. That failure was
pre-existing and blocked `cargo clippy -p warp-core --all-targets`.
ActualFootprint landed the write axis. The read axis is blocked on a
genuine architecture decision rather than on effort.

Reads reach the guard through GraphView, and guards live in
WorkUnit.guards, which worker threads share by reference. A RefCell
accumulator inside FootprintGuard would make WorkUnit !Sync and break
parallel execution. Three ways out, each with a real cost: a Mutex in the
guard (lock per guarded read in every debug build), a sink threaded
through GraphView (no locks, guard stays pure, but GraphView carries an
explicit DO-NOT-add-interior-mutability prohibition and check_*
signatures change), or a thread-local accumulator (rejected outright —
global mutable state on a deterministic path, forbidden by ADR 0004 and
enforced by CI).

Recorded for review rather than settled unilaterally: this is a hot path
and one option brushes an explicit architectural prohibition.
The previous revision offered a Mutex-in-the-guard option and framed the
read axis as needing synchronization. That was wrong.

execute_work_queue claims unit indices with next_unit.fetch_add, so each
unit goes to exactly one worker, and items within a unit execute
serially. A guard is never touched concurrently. The scheduler already
provides the exclusivity a lock would buy; adding one would
re-implement a guarantee Echo already makes.

The real constraint is type-level, not runtime: s.spawn over &[WorkUnit]
requires WorkUnit: Sync because the borrow checker cannot see the
atomic-claim protocol. A RefCell inside FootprintGuard fails to compile
for that reason alone.

So the resolution is to keep mutable state out of the shared structure:
an accumulator owned by the worker's frame in execute_item_enforced is
already exclusive, never crosses a thread, and leaves WorkUnit: Sync
untouched. Only one question remains, and it is a judgment call rather
than a constraint — whether a worker-local accumulator reference in
GraphView is inside the intent of its DO-NOT-add-interior-mutability
prohibition, whose listed items all concern mutable access to graph
state.

Also record a finding that surfaced while checking this: execute_serial
constructs no guard at all, and execute_item_enforced has exactly one
call site. A serial verification replay runs with enforcement inert and
would report an empty actual footprint for an execution that touched
everything — the false negative acceptance criterion 21 exists to
prevent.
An LSN names a WAL frame. Acquiring a writer epoch persists ledger
evidence and emits no frame, so an epoch that committed nothing spent
nothing. `acquire_fresh_writer_epoch` nonetheless applied `checked_next`
to the predecessor's `started_at_lsn` when that predecessor had no final
committed LSN, and `validate_writer_epoch_request` enforced the fiction
by rejecting equality against an empty predecessor's start.

Every open-and-close therefore minted a phantom coordinate. A host that
reopened a filesystem WAL only to inspect it left a permanent hole; the
next writer's frames landed past the gap, and `recover_from_frames_and_
commits` failed closed with `LsnContinuityMismatch` for every reader
thereafter. `inverse_intent_resolves_one_admitted_transition_after_
restart` reproduced exactly this: two inspect-only hosts, two holes, and
recovery refusing the WAL at LSN 18.

An epoch's start LSN is the next unallocated frame coordinate. It is
non-regressing, not universally strictly increasing:

    previous epoch committed frames:
        successor.start = previous.final_lsn + 1

    previous epoch committed no frames:
        successor.start = previous.start

Epoch-chain advancement stays strict, carried by epoch identity,
ordinal, fencing token, and lease evidence — none of which changed.

`filesystem_writer_lease_refuses_overlap_before_takeover` had grown a
second policy as a barnacle: it asserted the start LSN advanced past an
empty predecessor. That assertion is dropped; the test's subject is
lease overlap and takeover linkage. Empty-epoch cursor semantics move to
three named tests that also witness the distinction an empty closure
hides — a predecessor with committed frames, and a predecessor whose
frames were written but never committed.

Also declares `required-features` for eight test targets whose contents
sit behind an inner `#![cfg(feature = ...)]`. Without it `cargo test -p
warp-core` compiled an empty crate and printed "running 0 tests ... test
result: ok", so the failure above was invisible to a per-crate run.
`ActualFootprint` landed the write axis. The read axis was blocked on a
real constraint: reads reach the guard through `GraphView`, whose
accessors take `&self`, so recording would need to mutate through a
shared reference. Interior mutability is what that costs, and `GraphView`
forbids it by contract and would lose `Sync` — which `s.spawn` over
`&[WorkUnit]` requires, since the borrow checker cannot see the atomic
unit-claim protocol that already makes access exclusive.

No lock is warranted. The scheduler hands each unit to exactly one
worker and runs items within a unit serially, so guards are never touched
concurrently; a `Mutex` would re-implement a guarantee that already
holds, and `UnsafeCell` plus a manual `Sync` would turn an observation
feature into a scheduler-soundness theorem every future refactor must
preserve.

The shape adopted moves the mutable execution frame — not the declared
guard — into exclusive worker ownership:

    Shared prepared work   WorkUnit { items, guards }     immutable
    Worker-local frame     ActualFootprint, TickDelta     exclusive
    Executor capability    ExecutionGraphView             borrows both

`ExecutionGraphView` is not `Copy` and not `Clone`, and its accessors
take `&mut self`. That is inherited mutability expressing exactly what
the scheduler already guarantees: one executor owns one observation
session, and it cannot be casually shared. `GraphView` keeps its
contract, its `Copy`, and its `Sync`; nothing in `WorkUnit` changes.

Accessors record *before* consulting the guard. The guard panics, so
checking first would unwind before the violating coordinate entered the
record, leaving Echo holding a purported actual footprint that omits the
very access that falsifies it. The recorded axis mirrors enforcement
rather than inventing a finer one — `edges_from` records a node read,
because a node in `n_read` grants its outbound adjacency — and an absent
resource is still a recorded coordinate, so a rule cannot probe
undeclared coordinates for free by choosing empty ones.

`ActualFootprintPosture` makes lane provenance a value. An empty
violation set means the declaration covered the execution only when the
lane recorded *and* enforced; from an unobserved lane it means only that
nothing was compared. `read_axis_is_complete` keeps an empty read axis
readable as unknown rather than as "read nothing", which is the false
negative acceptance criterion 21 exists to block, and
`build_footprint_posture` caps every lane by the enforcement the binary
compiled.

`serial_execution_is_an_unobserved_lane` pins the serial replay hole by
contrast: one executor reading one undeclared node runs through
`execute_serial` without panicking and without leaving a trace, while the
identical read through `ExecutionGraphView` is recorded and reported.
Two lanes, identical behaviour, different evidence.

Differential tests run the recorded write set and the enforced write
check over the same ops and declaration, closing an assumption the design
had only asserted, and pin the two deliberate disagreements: cross-warp
emission and unauthorized instance ops stay scope and authority classes
rather than collapsing into footprint-subset failures.

No production executor reaches the new capability yet; `ExecuteFn` still
passes `GraphView` by value. That migration is the next unit of work.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cb69434a-3e80-4770-b35e-4a1534592a6e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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.

1 participant