feat(data): aprender-contrastive-data — deterministic, leakage-safe pair construction - #2702
Merged
Merged
Conversation
Schema before implementation, as the repo does for phase contracts. Defines the deterministic contrastive data protocol: class buckets, balanced few-shot selection, bounded pair sampling, typed split roles, dataset fingerprints and the cross-split leakage checks. `contracts/aprender/binding.yaml` gains the 24 entries that bind its equations to the modules landing in the next commit. pv validate contracts/contrastive-pair-protocol-v1.yaml 0 errors, 0 warnings Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…air construction
Contrastive/Siamese DATA CONSTRUCTION as a general capability: class buckets,
balanced few-shot selection, bounded positive/negative pair sampling, typed
split roles, dataset fingerprints, and the cross-split leakage checks that make
those trustworthy. SetFit is its first consumer, not its owner.
Deliberately a separate crate rather than part of aprender-data: that crate
carries arrow/parquet/zstd/s3/streaming/federated weight which must not flow
into a training crate or a serverless consumer.
BYTES BOUNDARY. The public API is bytes-in / bytes-out and typed values. No
filesystem access, no sockets, no path-shaped parameters — not even in tests;
apr-cli owns every filesystem adapter. The destination for these artifacts is
object storage, where a manifest is an S3 object, not a file: an API that speaks
`&Path` makes such a consumer a rewrite instead of a wrapper. The boundary is
ENFORCED, not asserted — `make contrastive-data-boundary` checks the resolved
dependency closure against a positive allowlist and bans std::fs/net/path
symbols under src/. Wired into tier3, because a target outside the tiers is a
target that stops being run.
DETERMINISM. Every random decision is a pure function of its draw ordinal, from
the counter-based Philox generator in aprender-rand. Worker-count independence
is structural rather than tested-for: draw i cannot depend on how many draws
preceded it, because nothing precedes it.
Five typestate misuses are proven unrepresentable with trybuild compile-fail
cases and committed .stderr snapshots, rather than asserted in prose.
ONE FIX TO THE GATE ITSELF. allowed-deps.txt listed `provable-contracts-macros`
— the workspace dependency KEY. The crate is `aprender-contracts-macros` (the
key carries `package = "..."`), and the crate name is what `cargo tree` prints,
so the entry could never match and the gate was RED from the commit that
scaffolded it. Corrected to the real package name. This is a name fix, not a
widening: the reviewed decision is unchanged, and the gate proved non-vacuous by
catching it.
Verified (rc captured directly, never through a pipe):
cargo check -p aprender-contrastive-data --all-targets rc=0
cargo test -p aprender-contrastive-data 249 passed, 0 failed
cargo clippy -p aprender-contrastive-data --all-targets
-- -D warnings rc=0
make contrastive-data-boundary rc=0 (deps subset + symbol ban)
cargo fmt --all -- --check rc=0
pv validate contracts/contrastive-pair-protocol-v1.yaml 0 errors, 0 warnings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
I will look tomorrow and merge |
…osed Merges 4aff459 (paiml#2618, same author) into this branch. paiml#2618 landed first and appended 23 binding entries to the same region of contracts/aprender/binding.yaml, so the merge needed resolving. Resolving it surfaced three further defects that were NOT conflicts, plus one this PR would have taken red into CI. 1. contracts/aprender/binding.yaml — the 24 entries bound NOTHING, on this branch, before any merge. They were appended at end-of-file, below `critical_path:` (a Vec<String>), so every one parsed as a map inside that string list and the WHOLE file failed to load: pv audit contracts/contrastive-pair-protocol-v1.yaml \ --binding contracts/aprender/binding.yaml rc=1 error: Failed to parse YAML: critical_path[41]: invalid type: map, expected a string at line 896 column 3 This is trap 3 verbatim from paiml#2618's own comment block in this same file ("Entries must be inserted BEFORE critical_path:. Appending at end-of-file lands inside that list"). `pv validate` cannot see it — it reads the contract, not the binding — which is why the PR's verification block reported green. Both sides' entries now sit inside `bindings:`, ahead of `critical_path:`: pv audit contrastive rc=0 24/24 bound, 24 implemented, no gaps pv audit setfit-apr rc=0 15/15 bound, no gaps Note the resolution is NOT "keep both": keep-both leaves this branch's block below `critical_path:` and reproduces the same rc=1. 2. Cargo.toml — a SILENT duplicate key, worse than a conflict. Both branches add aprender-rand to [workspace.dependencies] at different textual positions, so git auto-merged BOTH and reported no conflict: cargo metadata rc=101 error: duplicate key `aprender-rand` ... Cargo.toml:305 Dropped this branch's copy; paiml#2618's entry on main carries the fuller comment. 3. scripts/cascade-publish.sh — required CI check, red. aprender-contrastive-data is publishable (no `publish = false`) but was absent from TIERS[]. check_cascade_covers_all_crates.sh runs in ci.yml and is green on main, red with this branch. Its own message states why this matters: FINAL VERIFICATION iterates the same table, so an absent crate reads as success. Added to TIERS[2] — after its only two workspace deps, both TIER 1. 4. README.md — required CI check, red. check_readme_claims.sh counts crates and contracts against the tree. This branch adds one of each: 78 -> 79, 1796 -> 1797. Verified on the merged tree, rc captured directly, never through a pipe: cargo metadata --no-deps rc=0 (79 members) cargo metadata --locked rc=0 cargo check --workspace rc=0 cargo test -p aprender-contrastive-data --all-targets rc=0 (242 passed, 0 failed) cargo clippy -p aprender-contrastive-data --all-targets -- -D warnings rc=0 cargo fmt --all -- --check rc=0 pv validate contrastive-pair-protocol-v1 rc=0 pv audit contrastive / setfit --binding rc=0 / rc=0 make contrastive-data-boundary rc=0 scripts/check_cascade_covers_all_crates.sh rc=0 scripts/check_readme_claims.sh rc=0 scripts/check_assertions_exclude.sh rc=0 (delta -10) 77 of 79 ci.yml shell gates green before this commit; these two were the only failures, and both are green on main. Also drops a stray duplicated "# Tier 4: CI/CD" comment this branch introduced at Makefile:378/380. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
…ing its own rows
D1. CanonicalDeclarations carries FOUR independent label maps — one per split, plus a
shared one — and nothing compared them.
`from_labeled_rows` validates each split's rows against that split's own `label_names`,
then passes the UNRELATED `decls.label_names` into the dataset fingerprint, the stored
map, and `SelectionPayload::label_names`. Declare the split maps ["none","against",
"favor"] and the shared map ["favor","against","none"] and every row validates, the
counts agree, `select` returns Ok, and `Selection::replay` PASSES — because replay
compares the payload against the same reversed map the fingerprint committed. Downstream
reads its class names from that payload, so every class-0 example is labelled "favor"
while the rows say "none". Nothing anywhere goes red.
No existing test could see it: `test_corpus::declarations` builds all four maps from one
`label_names()` helper, so the four can never disagree in the fixture.
The shorter-map variant is a broken round-trip rather than a relabelling: ClassBuckets
sizes from the train map while check_class_balance sizes from the payload map, so the
crate emits a manifest it cannot replay.
`check_label_maps_agree()` on both declaration types, called at the top of both ingest
doors BEFORE any row is read, naming the first divergent split in declaration order and
carrying both maps. The attested-bytes door already threads a single map and is untouched.
MUTATION-VERIFIED, not asserted. With the two calls replaced by a comment:
cargo test -p aprender-contrastive-data --lib label_map
gate armed 5 passed, 0 failed
gate disarmed 2 passed, 4 FAILED
The 2 that survive are the control, `prepared_accepts_declarations_whose_label_maps_agree`,
which asserts the honest corpus STILL BUILDS. A gate is not proven by a test that reds
when it is removed; it is proven by that test plus one that would red if the gate started
refusing everything.
D3. The source half of `make contrastive-data-boundary` passed real filesystem writes and
TcpStream egress.
The ban was three literal substrings and two whole words. `use std::{fs, net::TcpStream};`
contains NONE of them — the text is `std::{fs` — and binds `fs` and `TcpStream` into scope
identically. This is not an exotic spelling: rustfmt.toml asks for
`imports_granularity = "Crate"`, so it is the spelling rustfmt PRODUCES here.
Verified against a compiled mutation, rc captured directly, never through a pipe: a module
doing fs::write and TcpStream::connect compiled rc=0 and the gate passed it rc=0.
scripts/lib/d04_grouped_std_import.awk accumulates each `use` statement to its terminating
`;`, so a multi-line group is tested as one string, then requires both a `std::` prefix and
an fs/net/path path SEGMENT — segment-anchored so `collections`, `fmt`, `Cow` and an
identifier like `pathological_case` cannot trip it. The same mutation is now rc=2.
The patterns ship a case table, because every miss this gate has had was found by a case
and none by review: 6 must-match and 6 must-not-match under tests/gate_cases/, run by the
gate itself on every invocation so a detector that silently stops matching fails loudly
instead of passing violations through. Both of the table's own failure modes induced:
detector emptied rc=2, "3 of 12 case(s) WRONG", naming each unflagged file
case dir removed rc=2, "would report PASS while checking nothing"
The must-not-match half is not symmetry for its own sake — it is the only thing that can
tell a working detector from one that flags everything.
D2. `cargo tree ... 2>&1` merged cargo's stderr into the file parsed as the dependency
closure.
cargo writes progress to stderr: `Blocking waiting for file lock on package cache`,
`Updating crates.io index`. `awk 'NF { print $1 }'` turns each into a phantom package name
and `comm -23` reports it as an unlisted dependency. This fired twice unprompted during
review — rc=2, offender `Blocking` — on a tree that was clean seconds earlier, so any
concurrent cargo reds the gate at random. The FAIL text says do not widen the allowlist,
which leaves a developer no lever except to add `Blocking` to allowed-deps.txt forever.
stderr now has its own sink; both files are shown when cargo tree actually fails.
Verified on this tree, rc captured directly:
cargo test -p aprender-contrastive-data --all-targets rc=0 247 passed, 0 failed
cargo clippy -p aprender-contrastive-data --all-targets
-- -D warnings rc=0
cargo fmt --all -- --check rc=0
make contrastive-data-boundary rc=0 (incl. cases 12/12)
pv audit contrastive --binding rc=0 24/24 bound
all 79 ci.yml shell gates rc=0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift
enabled auto-merge
August 27, 2026 07:59
…id so FALSIFY-README-CRATE-001 (crates/aprender-core/tests/readme_contract.rs:254) asserts every directory under crates/ carrying a Cargo.toml also carries a README.md. The new crate carried neither, so the required workspace-test job failed: Integration tests ... failure FALSIFY-README-CRATE-001: Crates missing README.md: ["aprender-contrastive-data"] test result: FAILED. 14 passed; 1 failed Note WHERE this was caught, because it is the interesting part. The 79 ci.yml shell gates were all green on this tree, and so was `cargo test -p aprender-contrastive-data --all-targets`. Neither could see it: the check lives in aprender-core's integration target, reachable only from the hand-maintained `&&` chain at ci.yml:358. Sweeping the shell gates and calling that "CI-equivalent" is the gap; this commit was verified by extracting that chain verbatim from the workflow file and running it: bash <ci.yml:358 chain, extracted verbatim> rc=0 33/33 suites ok The README also satisfies FALSIFY-README-CRATE-002 (must contain `paiml/aprender`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift
pushed a commit
that referenced
this pull request
Aug 27, 2026
…ld not see it Build Book failed on this PR: FAIL: book/src/lib/setfit.md does not exist (aprender::setfit has no chapter) Coverage: 71/72 aprender-core public modules have a chapter (1 missing) FALSIFY-BOOK-LIB-PARITY-001: FAIL The module came from #2618, not from this branch. It failed HERE because this PR touches book/, which is the only thing that runs the gate. THE PATH FILTER IS THE REAL DEFECT, and book.yml already documents it — about the gate one line up: # FALSIFY-BOOK-CLI-PARITY-001 asserts every `apr` subcommand has a chapter. # Filtering on book/** alone meant it only ran when the BOOK changed, never # when the CLI gained a command — so `apr beat-run` shipped in #1995 with no # chapter and the gate stayed green for months, until an unrelated batch that # happened to touch book/ woke it. A parity gate must run when EITHER side # moves; watching only one of them is how it comes to certify nothing. That fix added crates/apr-cli/src/commands_enum.rs to the paths. It was never applied to FALSIFY-BOOK-LIB-PARITY-001, whose other side is aprender-core — which appeared in this file only inside a comment. So the identical failure recurred one gate down: a module shipped without a chapter, the gate stayed green, and an unrelated batch touching book/ woke it. Same shape, same workflow, same paragraph. Fixed both halves: - book/src/lib/setfit.md written, listed in SUMMARY.md between serialization and showcase. It records that pair construction lives in aprender-contrastive-data (#2702), so the two crates' boundary is stated where a reader of either will find it. - crates/aprender-core/src/lib.rs added to the paths filter, in BOTH the push and pull_request blocks. The gate now runs when either side moves, which is what its own comment already said it must do. MUTATION: remove book/src/lib/setfit.md rc=1 RED restored rc=0 GREEN, 72/72 modules book.yml parses (yaml.safe_load) OK cli_parity / example_block / linkcheck / readme_claims / claim_literals rc=0 The last one matters: a new book page is a user-facing surface, so it goes through the claim-literal guard this PR widened. It states no throughput number and no comparator ratio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
aprender-contrastive-data— deterministic, leakage-safe pair constructionContrastive/Siamese data construction as a general capability: class buckets,
balanced few-shot selection, bounded positive/negative pair sampling, typed split
roles, dataset fingerprints, and the cross-split leakage checks that make those
trustworthy.
Independent of #2618. This does not depend on
aprender-coreat all — itswhole dependency closure is 28 packages. Review and merge in either order.
Why a separate crate
Not folded into
aprender-data: that crate carries arrow/parquet/zstd/s3/streaming/federated weight which must not flow into a training crate or a
serverless consumer. SetFit is this crate's first consumer, not its owner.
The bytes boundary
The public API is bytes-in / bytes-out and typed values. No filesystem access, no
sockets, no path-shaped parameters — not even in tests.
apr-cliowns everyfilesystem adapter.
That is not stylistic. The destination for these artifacts is object storage,
where a manifest is an S3 object rather than a file; an API that speaks
&Pathmakes such a consumer a rewrite instead of a wrapper.
It is enforced, not asserted:
make contrastive-data-boundarychecks theresolved dependency closure against a positive allowlist and bans
std::fs/std::net/std::path/Path/PathBufundersrc/. Wired into tier3,because a target outside the tiers is a target that stops being run.
Determinism
Every random decision is a pure function of its draw ordinal, from the
counter-based Philox generator in
aprender-rand. Worker-count independence isstructural rather than tested-for: draw i cannot depend on how many draws
preceded it, because nothing precedes it.
Five typestate misuses are proven unrepresentable with
trybuildcompile-failcases and committed
.stderrsnapshots, rather than asserted in prose.One fix to the gate itself
allowed-deps.txtlistedprovable-contracts-macros— the workspace dependencykey. The crate is
aprender-contracts-macros(the key carriespackage = "..."), and the crate name is whatcargo treeprints, so the entrycould never match. The gate was RED from the commit that scaffolded it.
Corrected to the real package name. This is a name fix, not a widening — the
reviewed decision is unchanged, and the gate proved non-vacuous by catching it.
Verification
rc captured directly, never through a pipe:
Note for whoever merges second
#2618 and this PR both add
aprender-randto[workspace.dependencies]. Thatone line will conflict textually; everything else is disjoint. Whichever lands
second needs a one-line rebase, and I'm happy to do it.
🤖 Generated with Claude Code