Skip to content

feat(conformance): exploratory parity discovery (spec 025) - #378

Merged
pofallon merged 18 commits into
mainfrom
025-exploratory-parity-discovery
Jul 28, 2026
Merged

feat(conformance): exploratory parity discovery (spec 025)#378
pofallon merged 18 commits into
mainfrom
025-exploratory-parity-discovery

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

Implements 025-exploratory-parity-discovery in full — all 129 tasks, zero deferred.

The registry says whether each curated behavior is covered; the 024 coverage model says whether the scenario space is. This adds the third question: finding differences nobody curated, and handing each one over as a reviewable candidate that a human — never a program — may promote into the record.

What this adds

Two data roots, deliberately siblings. conformance/discovery/ (findings, campaigns, corpus) sits beside conformance/registry/, not inside it. No registry loader path reaches it, so nothing a campaign finds can influence validate or certify. The only permitted cross-root reference points out of the queue (promotedTo → a registry case, violation class D3).

Never gates. A discovery command's exit status reflects whether it ran, never what it found. A campaign surfacing forty differences exits 0. The sole exception, discovery-proof, asserts a property of the machinery rather than a finding — so non-zero there means the pipeline is broken, which is exactly what should fail a lane. A stochastic gate would make green non-reproducible; that is why this lane can be scheduled in CI at all.

Four campaign tiers: metamorphic (deacon-only — no oracle, Docker, or network), config-differential (nightly), container-differential (invoked-only), corpus (weekly, network-backed).

Promotion is a human act, structurally. There is no code path from a finding to a registry write — enforced behaviourally and by source scan. discovery scaffold prints to stdout with UNREVIEWED sentinels the loader rejects.

Violation classes D1–D5 over the discovery root, plus V31/V32 for the metamorphic relation catalogue.

Verification

  • make test-nextest full gate: 4324 passed, 0 failed, 38 skipped
  • Full discovery profile green with the pinned oracle 0.87.0: 18 campaign + 12 metamorphic
  • discovery-proof: 2 injections traverse all 6 stages, 0 failed, 0 inapplicable
  • Selection audit — zero live discovery tests in all six non-discovery profiles; the hermetic guard's 27 tests present in full/default/ci/dev-fast; the discovery profile selects exactly 30. The profile filter is an explicit allow-list, never a discovery_* glob, which would have silently dropped discovery_hermetic from the fast lane.
  • certify byte-identical with a 26-finding queue (one claiming promotion into a nonexistent case) and with an empty one
  • V30 clean; all 11 channels meet the three-covering-case floor
  • fmt + clippy --all-targets --all-features clean

Measured, not assumed

Three real campaigns against the verified oracle (seeds recorded in research.md so they reproduce):

Observed Threshold
SC-002 parse-stage failures 2.50% worst case ≤ 10%
SC-004 minimization 0.8438 pooled median (n=13) ≥ 0.80
SC-017 reviewer reproduction 1s < 10 min

Notes for review

  • The queue ships empty. Campaign findings were for measurement; the seeds reproduce them, and most of what they surfaced was the drop_absent_optional artifact described below rather than a triage backlog worth carrying.
  • SC-017's reproduction initially appeared to fail: a candidate claimed a ref-only difference while both CLIs produced identical raw output. Not a bug — drop_absent_optional applies to deacon's configuration block only, and cannot distinguish an authored "secrets": {} from an unset optional deacon serializes unconditionally. That is a normalizer-defect (a deliberate dead-end classification); the real fix is skip_serializing_if on deacon, tracked as 023 #T111. The walkthrough is now in the quickstart's Troubleshooting.
  • Quickstart drift corrected: the metamorphic example carried an invented clu- id that would have produced a V31 if copied, and the corpus example's id did not match the committed entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bp2tFKG5y9cnSUHcsngi33

pofallon and others added 17 commits July 27, 2026 13:43
Adds the hermetic/live module skeletons, the conformance/discovery/ data
root, DiscoveryError/HarnessError variants, the discovery nextest profile
with all-profile exclusions, and the foundational spine: deterministic
PRNG (SplitMix64->xoshiro256**), grammar loader over the constraint
inventory, normalized-diff-derived signature classifier, and the
findings-queue model/loader/atomic-writer with D1/D5 validation. Wires
the `discovery` command group (check/report/triage/split/scaffold) into
the conformance CLI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Adds the MetamorphicRelation model, seven mandated mrl- relations each
grounded in a resolvable clu-/bhv- id, V31 (missing/unresolvable ground)
and V32 (mandated family with no record) validation, deacon-only relation
evaluation (no oracle/Docker/network required), and the live
discovery_metamorphic test binary selected only by [profile.discovery].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Registers discovery test binaries in the parity registry with a
role-aware (live-campaign vs guard) reading, extends
parity_registry_check with discovery-lane wiring + no-consumer-surface
assertions, proves the hermetic discovery surface makes zero network
calls, proves discovery report/check never gate on findings volume, and
adds a nightly + workflow_dispatch discovery.yml CI lane.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Adds grammar-driven constrained generation, the eleven-category
mutation catalogue, the campaign driver (seed/tier/budget/timeout,
admission cap, unsafe/unpinned-image guards), the differential
comparison against the verified pinned oracle (reusing exec/oracle/
prereq/normalize, no new process-execution path), already-characterized
suppression, and the discovery-campaign bin with --seed required and
never defaulted. Verified against the live pinned oracle: a 44-candidate
campaign reaches a 2.3% trivial-failure ratio, exercises all eleven
mutation categories, and the admission cap correctly suppresses excess
distinct signatures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…rity-discovery

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…US1 merge

US1 added a required `profile` field and a derived-id check to `Campaign`
(data-model.md §1: seed ‖ pinnedInputSet ‖ lane ‖ profile ‖ tier). US3's
synthetic_queue() test helper predated that change and built a campaign
record with a hand-picked id and no profile, so it failed the strict
loader post-merge. Compute the pinned input set and derive the campaign
id the same way the real pipeline does instead of hand-picking either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Phase 6 of 025-exploratory-parity-discovery: place every finding in one of
six causes and collapse repeats, so the queue reflects distinct problems
rather than campaign volume.

Violation class D2 (T068) — classification arity and promotability, over
four shapes that are one defect: the queue asserting a judgement nobody
made, or holding one that cannot lead anywhere. "More than one
classification" is unrepresentable rather than unchecked (`Finding` carries
`Option<Classification>` and the strict loader rejects an array), so the
arity rule reduces to its only reachable violation.

State machine (T069) — `FindingState::may_transition_to` encodes exactly
data-model.md's five arrows, plus `Finding::{triage,promote,
mark_no_longer_reproducing}` and a typed `TransitionError`. Three
plausible-looking arrows are absent on purpose: `untriaged ->
no-longer-reproducing` (that state requires a classification, so the arrow
would manufacture a D2 out of a campaign merely not re-observing something
nobody had looked at), `no-longer-reproducing -> promoted` (promotion needs
current evidence), and `untriaged -> split` (a split separates a judgement,
so there must be one to find inadequate).

Split lineage (T070) — `split_finding` produces one child per witness, each
keyed by `parent || its witness ids`, because a child carries its parent's
signature verbatim and so cannot use the signature derivation. Deduplication
now refuses the whole lineage, including when the ancestor record is gone and
only children remain: without that clause the id lookup misses and a fresh
merged record resurrects under the ancestor's id, silently reverting the
reviewer's decision.

Admission cap (T071) — the cap was measured against every finding a campaign
touched, including ones the standing queue already carried and the run merely
re-witnessed. A queue grown past the cap would therefore reach it on
re-witnesses alone and freeze out every genuinely new signature forever. It
now counts only newly distinct signatures, per FR-034b.

CLI (T072) — `triage` and `split` delegate to the state machine rather than
re-deciding the lifecycle, and `split` authors the children it previously
deferred.

Report (T073) — adds the FR-031 grouping view (keyed by a reviewed behavior
where a promoted finding's case resolves, else by observable path; grouping
is a view, never a merge) and the queue-level suppression total, which is
what tells a reviewer the queue is a sample rather than everything.

Guard (T125) — no discovery source authors or extends an allowed-difference
entry. Reading stays permitted, since FR-017 requires it, and the guard
asserts the read still happens so a scan matching nothing cannot pass by
checking nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ce (025 T096/T059)

Two deliberately-deferred integrations from US6 and US3, both blocked until now
only because each needed a file the other track had not yet created.

T096 — the `metamorphic` tier in the campaign driver

`campaign::run` now dispatches to a dedicated `run_metamorphic` BEFORE the
prerequisite step, because this tier has no prerequisite to acquire (research
D12): it compares deacon against deacon over a declared transformation, so there
is no oracle to verify, no Docker to probe, no network to reach. Previously the
tier reached the differential loop with `oracle == None` and failed as
`OracleUnverified` — the fail-loud guard doing its job on a tier that was never
routed anywhere.

The tier evaluates the committed `registry.metamorphic` catalogue with
`Sabotage::None` under the same wall-clock and per-candidate bounds the
differential uses; a relation that exceeds its bound is discarded AND counted,
never left to hang the run. Each violated relation's residual signatures are
admitted through the shared queue, keyed on `chan-structured-output` so a
metamorphic residual and a differential divergence at the same path deduplicate
against each other — they are the same defect observed two ways.

The plan IS the catalogue. `spaceCoveredFraction` is denominated by
`registry.metamorphic.len()`, not `planned_candidates`: there is nothing to
generate for this tier, so honoring the request's figure would report a
seven-relation run as 3.5% of a two-hundred-candidate plan it never had.
`mutationApplications` is all-zero with all eleven keys present (FR-010) and
`parseStageFailures` is 0 — the fixtures are authored, not drawn. An EMPTY
catalogue is an error, not a zero-relation run: V32 already forbids a mandated
family with no record, and a run over nothing reports exactly what a run in
which everything held reports.

Two limits are recorded in the code rather than left silent:

- `Characterization` (the FR-017 already-characterized suppression) is NOT wired
  into this tier. That index is keyed to differential-style observable paths from
  `cases/<area>.json` and the waivers, all of which describe a deacon-vs-reference
  difference; a metamorphic residual is deacon-vs-deacon, so consulting it would
  either suppress nothing or suppress by path collision. Left out visibly rather
  than approximated.
- A violated SENSITIVITY relation yields no signature by construction (its failure
  is the ABSENCE of a difference, and the catalogue deliberately refuses to key one
  on the touched site, which would merge two unrelated defects). It cannot enter
  the queue, so it is logged at `warn` naming the relation rather than dropped.

Extracted from the differential path, shared by both drivers rather than
duplicated: `AdmissionQueue` (dedup against the standing queue, the admission cap,
and FR-034b's "suppression is counted, never silent"), `pinned_input_set`, and
`complete` (exhaustion semantics, the append-only campaign history, the report).
US1's cap semantics are preserved verbatim — including that `admitted` counts
re-witnesses, so a campaign that re-witnesses `admission_cap` standing findings
has no budget left for a new one. That is a decision about the cap's meaning, not
a refactor, and is documented rather than changed.

T059 — evidence for the bin's exit-status contract

US1's `main()` already implemented the contract correctly; what was missing was
evidence. `discovery_campaign` now drives the COMPILED bin as a subprocess and
asserts all three statuses: `0` for a `--tier metamorphic --dry-run` run that
reached the whole catalogue, `2` for six malformed invocations, `1` for an
unverifiable oracle. T055 covers the library level; only a subprocess can observe
`main`'s `Result` -> `ExitCode` translation, which is where the contract is
actually enforced and where a doc comment is not evidence.

`env!("CARGO_BIN_EXE_discovery-campaign")` does not exist in this test — the bin
belongs to `parity-harness`, the test to `deacon` — so `prereq::cargo_bin`
generalizes `prereq::deacon_binary`'s build-and-read-cargo's-artifact rule instead
of guessing a `target/{debug,release}` path, which is the 023 T115 defect.

Verified: fmt --check clean; clippy --workspace --all-targets --all-features -D
warnings clean; `--profile default` 4233/4233; `--profile dev-fast` 3934/3934;
`--profile discovery` 24/24.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…to 025-exploratory-parity-discovery

Resolves a conflict in campaign.rs between US4's admission-cap fix
(measure the cap against newly-distinct signatures, not every id a
campaign touched) and T096's AdmissionQueue extraction (which had
preserved the pre-fix semantics verbatim, per its own report). Folds
the newly_admitted fix into AdmissionQueue::offer so both changes are
preserved: the cap logic now lives in one shared place and behaves
correctly for both the differential and metamorphic tiers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
The merge conflict resolution moved finding_id into admitted.push()
before its later use in newly_admitted.insert(), which doesn't compile.
Clone at the first use site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…s (025 US2)

Implements Phase 4 (US2) of 025-exploratory-parity-discovery: reduce each
finding's input while preserving its normalized signature, then hand the
reviewer a complete, self-contained candidate.

Reduction strategy (T047, hermetic, `deacon_conformance::discovery::shrink`)

The seven-step catalogue of data-model.md §6 becomes executable, taking its
reproduction predicate as a PARAMETER (`ReproductionProbe`) so the strategy is
unit-testable against a synthetic predicate with no oracle, no Docker and no
network (research D4/D5).

The predicate has three answers, not a boolean: FR-023 needs the third, because
a `bool` would discard a drifted signature at the moment it was observed.

Termination is a property rather than a hope. Every accepted step strictly
decreases a lexicographic `(mutations remaining, node count, non-minimal
scalars)` triple; each component is reduced by exactly one step and increased by
none that does not reduce an earlier one. Corruptions must lead, because
`un-apply-mutation` can GROW the document — a size-only objective would let the
shrinker oscillate between emptying and un-applying forever, spending the run's
most expensive step on a ring it never leaves.

`isMinimal` is true only when a complete pass over all seven steps accepted
nothing, which is FR-021's claim exactly: minimal with respect to the DECLARED
catalogue, finite and checkable — never the unfalsifiable claim that no smaller
input exists. `first_further_reduction` re-establishes it independently, since a
property that rests on how a loop exits is worth checking with a function whose
only job is to look.

`un-apply-mutation` is an exact reversal, not a category-directed guess:
`mutate.rs` now records a `Reversal` per application, derived by diffing the
operator's pre- and post-images at the document root so it cannot disagree with
what the operator did. It is a LOCAL edit rather than a pre-image document —
by the time reduction reaches it, earlier steps have already removed keys, and
restoring a whole pre-image would silently undo all of them.

No async runtime was added to `deacon-conformance`, deliberately. The first
attempt took `tokio` as a dev-dependency to drive the async `reduce`, and
`discovery_hermetic`'s SC-013 guard correctly rejected it: its claim is that the
network capability is ABSENT, not merely unused, and a dev-dependency still puts
a runtime in this crate's test binary. The tests drive the future with a
five-line `block_on` over `Waker::noop()` instead — the synthetic predicate
never suspends, so there is nothing to schedule.

Live predicate (T048) and candidate assembly (T049–T051, `parity-harness`)

`minimize::DifferentialProbe` re-runs `differential::compare` — no new execution
path, no second normalization, no second opinion on what differs. Two things are
held constant across every probe: the workspace shape (a probe tree differing
from the candidate tree would measure the scaffold) and `deliberately_invalid`
(it describes the candidate's provenance, and recomputing it as mutations are
un-applied would move the tolerance boundary mid-reduction).

`candidate::write` emits the six parts of data-model.md §9 under
`target/discovery/candidates/<fnd-id>/`, with `raw.json` and `normalized.json`
kept SEPARATE (FR-014): conflating them is what makes a `normalizer-defect`
unreachable for a reviewer.

`mapping.json` suggests a behavior only when a committed case already declares a
document-shaped assertion on the same channel at the same observable path.
FR-025 is held structurally — every proposal is filtered against
`registry.behaviors`, so an id that does not resolve cannot escape the function.
Ambiguity reports `match: none` with the candidates listed, because choosing one
would be a coin flip wearing a suggestion's clothes.

Campaign wiring (T052)

Added to the existing differential loop; the tier dispatch and `AdmissionQueue`
are unchanged apart from two read-only queries. Two gates on paying for a
reduction — the signature must be new to this campaign, and the wall clock must
remain — and neither may present the result as minimal: both route through
`Reduction::not_attempted`, which carries `isMinimal: false` AND the reason
(FR-022). Drifted signatures are admitted as separate findings whose witness
names the REJECTED PROPOSAL they were seen on, not the reduction's own result,
which does not reproduce them.

A finding admitted from a drift gets no six-part candidate — it has no two-sided
evidence set behind it — and the campaign reports which findings it did package
rather than papering over the difference.

Tests

T041–T045 in `shrink.rs` (synthetic predicate): preservation, minimality
(independently re-checked), determinism over the probe SEQUENCE and not merely
the answer, budget exhaustion, and drift capture.

T046/T124 live, verified against the pinned oracle 0.87.0. T046 goes past file
existence: each candidate's fixture is copied to a fresh workspace and re-run
against both implementations, and the claimed signature must still appear — the
failure mode minimization introduces is precisely a fixture that no longer
reproduces. T124 asserts the tier split as a PAIR, since `config-differential`
running without Docker proves nothing unless `container-differential` refuses in
the same environment.

Verification: fmt clean, clippy `-D warnings` clean workspace-wide,
`--profile discovery` 27/27 passed live, `--profile dev-fast` 3979/3979 passed.
`--profile default` had two Docker timeouts (`test_alpine_container_starts`,
`test_secret_masking_in_lifecycle_logs`) under 105 concurrent containers from
other sessions; both pass in isolation in 13s and 57s.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
Phase 9 of 025-exploratory-parity-discovery: the real-world corpus becomes an
ecological canary with recorded provenance, verified content, and a schedule of
its own.

T105/T106 — `CorpusEntry` + violation class D4, and the 33 pinned entries moved
out of the Python fetcher into the Rust-owned strict-JSON
`conformance/discovery/corpus.json`. D4 has two clauses answered in two places
because they are answerable in two places: a non-immutable reference is a
property of the manifest alone, so `corpus::check` rejects a branch/tag/HEAD/
`latest`/abbreviated SHA hermetically on every PR (research D8 — a validation
that only runs when the network is up is a validation that does not run); a
digest recorded and then removed is a property of a *change*, so
`corpus::check_drift` answers it against an explicit baseline. `DiscoveryData`
now loads all three collection files, so D4 runs wherever `discovery check` runs.

T107 — `corpus_fetch`: a blob-filtered partial clone plus a sparse checkout,
one round trip per entry, no API token and no rate limit (`microsoft/vscode`
costs the same as `vscode-remote-try-node`). The digest is recorded at first
materialization and verified thereafter; a mismatch is reported for that entry
and the entry is NOT compared, because attributing a change in the upstream
workspace to a difference between the implementations is a wrong conclusion
arrived at confidently. A mismatch also never re-baselines the manifest — a
self-healing check reports a problem once and then never again. `EntryStatus`
has no "failed" catch-all: unreachable and digest-mismatch stay distinct facts.

T108 — the `corpus` tier in the campaign driver, a third dispatch branch beside
`run_metamorphic`, gated on the verified oracle AND the network
(`HarnessError::NetworkUnavailable`). It reuses `differential::compare`, the
tolerance index, `AdmissionQueue`, and `complete`, so FR-054's "same pipeline"
holds by construction rather than by parallel implementation. Its plan is the
manifest's own size; its witnesses name upstream provenance (repository, commit,
path, verified digest) because corpus content is never vendored and a witness
whose input nobody can retrieve names nothing.

T128 — a weekly `schedule` for a dedicated `corpus` job in discovery.yml,
separate from the nightly campaign. A canary that runs only on request cannot
warn anyone; weekly rather than nightly because the corpus moves only when
someone re-pins it (research D10).

T109 — `fetch_realworld_corpus.py` is RETIRED. Two copies of one manifest drift,
its documented workflow named runners 023 deleted, and the immutable-reference
rule could not run from a Python tuple. `baseline.rs` now reads the entry names
from the JSON manifest — same 33 names, same order, so the frozen baseline still
regenerates byte-for-byte — and the surface is registered in
`parity_registry_check`'s removed-surface guards.

Tests: T099 (mutable reference is D4 with no network, SC-012), T100 (full
provenance per entry), T104 (no corpus entry is a mutation seed, FR-008a/054a)
in discovery_hermetic; T101 (digest mismatch), T102 (unreachable vs ran-and-
found-nothing), T103 (pipeline parity + provenance + cross-campaign dedup) in
discovery_campaign.

Verified against the live pinned oracle and real network: 33/33 entries fetched
and compared, 10 findings admitted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…ty-discovery

Resolves conflicts in campaign.rs (CampaignRun struct-field union: US2's
shrink_probes/candidates_root/candidates alongside US7's corpus_statuses,
plus a merged module doc comment covering both minimization and the
corpus tier) and in discovery_campaign.rs's test imports (union of both
new imports).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…corpus tier

The merge conflict resolution left run_corpus's Completion literal
missing the candidates field US2 added, which doesn't compile. Corpus
findings deliberately get no packaged reviewable candidate (Vec::new()):
the existing candidate/minimize machinery was built around a single
generated JSON document a campaign owns and can freely re-synthesize,
but a corpus entry's workspace is a real third-party directory that may
reference other files by relative path (Dockerfile, compose, local
features) that this tier does not own and must not vendor (FR-053).
Reducing just the JSON portion risks silently breaking those references.
The witness still names full upstream provenance per FR-054; packaging
a six-part candidate for real-world content is a separate design
question, documented in place rather than papered over.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFsUBzDguiWtpaXXP74ogf
…25 US5)

Make promotion of a discovery finding a human act with a stable behavior
identity and a disposition, and make automatic promotion structurally
impossible.

- T079/T126 `discovery scaffold [--tolerate]`: emits skeleton behavior/
  case/fixture records — or a scoped `wvr-` waiver plus its
  `allowedDifferences` entry — to **stdout only**, with `UNREVIEWED`
  sentinels the loader rejects. A blanket or unscoped tolerance is
  refused rather than emitted (FR-041).
- T080 violation class **D3**: `promotedTo` must be present in state
  `promoted`, must resolve to a real registry case, and must be absent
  in every other state.
- T081/T082 pipeline proof + `discovery-proof` bin: an injected
  difference must traverse generation → comparison → minimization →
  candidate → classification → promotable. Injection goes through the
  sealed `EvidenceSource` boundary, so perturbing an observer's return
  value does not compile (research D7). An injection that never lands is
  a fail-loud `inapplicable`, never "found nothing" (SC-016).
- T074/T083 no-write-path guards: no discovery program can write a
  behavior, case, waiver, tolerated difference, disposition, or snapshot
  (SC-008), enforced both behaviourally and by source scan.
- T075/T076/T077 promotion validation and certify isolation: a promoted
  finding validates as an ordinary case including `scenarioContext`
  (SC-009); a promotion missing an identity or disposition names what is
  missing (FR-038); `certify` is byte-identical with a queue full of
  unreviewed findings and with an empty one (SC-018).

Verified: `cargo fmt --all -- --check`, `cargo clippy --all-targets
--all-features -- -D warnings`, `discovery_hermetic` 27/27,
`deacon-conformance` 431 unit + all integration, `parity-harness` 204
unit + all integration, `parity_registry_check` 15/15, `conformance
validate` and `conformance discovery check` both clean, and
`discovery-proof` traversing 2 injections with 0 failed / 0 inapplicable.

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

Completes the final phase of 025-exploratory-parity-discovery (T110-T120,
T129). No production code changed — this is documentation, measurement,
and verification.

Documentation:
- CLAUDE.md gains an "Exploratory Parity Discovery" section (T110): the
  two sibling data roots, the hermetic/live split, the D1-D5 vs V-class
  distinction and their different consequences, and the never-gates rule.
- CLAUDE.md's gate section becomes "Three lanes, two gates" with a
  summary table (T111), making explicit that discovery gates nothing.
- conformance/RULES.md gains "Finding vs gap (do not conflate)" (T112):
  a finding is a candidate for an assertion backed by a reproducing
  witness and never blocks; a gap admits missing coverage and always
  blocks. The arrow between the roots points one way only.

Quickstart drift corrected (T113):
- The metamorphic example carried `"ground": "clu-7a2b91ce"`, an invented
  short id. Real `clu-` grounds are substance-anchored slugs, so copying
  the example produced a V31. Replaced with the committed record and a
  note showing a real clause id.
- The corpus example's id and notes did not match the committed
  `images-python` entry; corrected, plus a note that corpus ids are
  derived, never hand-authored (a made-up one is D4).

Measured thresholds (T114) — three real config-differential campaigns
against the verified pinned oracle 0.87.0, recorded in research.md:
- SC-002: worst observed parse-stage failure rate 2.50% against a 10%
  ceiling (seeds 0xa1b2c3d4 / 0x5eed0002 / 0x0badc0de).
- SC-004: pooled median reduction 0.8438 over 13 independent
  minimizations, clearing the 0.80 floor.
- Recorded why n is small: a candidate re-emitted for a signature the
  queue already carries deliberately does not re-minimize, so the metric
  must be recomputed from candidate provenance. Noted as a possible
  follow-up that the campaign outcome could carry the aggregate.

SC-017 reviewer reproduction (T115) — reproduced a candidate from its
fixture and named pins alone in 1s against a 600s budget. The one step
that was not self-evident is now in the quickstart's Troubleshooting: the
signature is defined on NORMALIZED output, and `drop_absent_optional`
applies to deacon's `configuration` block only, so a fixture authoring an
empty value for an ABSENT_OPTIONAL_KEYS key yields a `ref-only` signature
with identical raw output on both sides. That is a `normalizer-defect`,
and the underlying fix is `skip_serializing_if` on deacon (023 #T111).

Verification (T116-T120, T129):
- V30 clean: all 11 channels have a `reg-` record, no orphans, and every
  channel meets the SC-005 three-covering-case floor (minimum 3).
- fmt + clippy --all-targets --all-features clean.
- `make test-nextest` full gate: 4324 passed, 0 failed, 38 skipped.
- Live discovery selection audit: zero live discovery tests in all six
  non-discovery profiles; the hermetic guard's 27 tests present in
  full/default/ci/dev-fast; the discovery profile selects exactly 30.
- The whole discovery profile run green for the first time with the
  oracle installed: 18 campaign + 12 metamorphic tests.
- certify's verdict is byte-identical with a 26-finding queue (including
  one claiming promotion into a nonexistent case) and with an empty one.
- SC-015 budgets confirmed: 1800s wall clock, 60s hermetic and 300s
  container per-candidate, with timeouts discarded and counted into
  `candidatesDiscardedUnsafe` at all three sites rather than hanging.

Campaign findings were deliberately NOT committed: the runs were for
measurement, the seeds are recorded so they reproduce, and most of what
they surfaced is the `drop_absent_optional` artifact above rather than a
triage backlog worth carrying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp2tFKG5y9cnSUHcsngi33
@github-actions github-actions Bot added feature New feature (alias) ci CI/CD changes build Build system changes labels Jul 28, 2026
The Windows `Test (MVP fast)` lane failed three tests introduced by this
branch. Two causes, both environmental — the production code is correct
on every platform.

**`conformance/discovery/**` was never pinned `-text`.** Every other
byte-compared artifact tree in this repository is (schemas, inventory,
spec prose, fixtures, snapshots, migration, obligations), and the
discovery data root has exactly the same discipline: the campaign driver
writes it through the canonical renderer, and both `discovery check` and
the hermetic guard byte-compare the committed files against a fresh
rendering. A Windows autocrlf checkout rewrote LF to CRLF, so
`raw.ends_with("}\n")` saw `}\r\n` and the comparison failed on bytes git
introduced — pointing the reader at a renderer that was working
correctly. Fixed by adding the pin, matching the seven that precede it.

Fixes:
- deacon::discovery_hermetic the_discovery_data_root_exists_and_is_canonically_rendered
- deacon-conformance discovery::queue::tests::the_rendered_empty_file_matches_the_committed_data_root

**`an_unreadable_file_is_not_mistaken_for_an_absent_one` simulates the
stat failure with `ENOTDIR`, which Windows does not have.** Windows maps
a stat under a non-directory to `ErrorKind::NotFound`, so the probe took
`load_file`'s legitimate "the file is absent" branch and `load` returned
`Ok` — the test could not reach the error path it exists to assert. The
guarantee itself is platform-independent (`load_file` treats only
`NotFound` as absent and turns every other stat error into a
`SchemaError`); it is the way of PROVOKING a non-`NotFound` stat error
that is Unix-specific. Gated `#[cfg(unix)]` with that reason, per the
convention CLAUDE.md records for genuinely Unix-only tests.

Verified: `git check-attr text` reports `unset` for all three data-root
files, the committed files carry zero CR bytes, fmt + clippy
--all-targets --all-features clean, discovery_hermetic 27/27 and the
deacon-conformance queue tests 50/50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp2tFKG5y9cnSUHcsngi33
@pofallon
pofallon merged commit e6a9cc3 into main Jul 28, 2026
14 of 15 checks passed
@pofallon
pofallon deleted the 025-exploratory-parity-discovery branch July 28, 2026 00:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build system changes ci CI/CD changes feature New feature (alias)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants