Add Cargo/pyproject/tag version-consistency check - #63
Merged
Conversation
Today's rc6 publish partially failed: crates.io published difi-rs v2.0.0-rc6 fine, but PyPI 400-rejected the upload because pyproject.toml was still pinned at "2.0.0rc5" (from the prior release). maturin built wheels named difi-2.0.0rc5-*.whl against an rc6 Cargo.toml, PyPI said "File already exists", and the Publish workflow failed. The same class of drift also explains why v2.0.0rc4 never got a tag (publish failed mid-way and was abandoned). This adds a shared check and wires it into both workflows so the drift cannot reach a release again. - `scripts/check_versions.sh` — single source of truth. Compares Cargo.toml (SemVer, e.g. `2.0.0-rc6`) against pyproject.toml (PEP 440, e.g. `2.0.0rc6`) on the PEP 440 normal form. Optionally takes a tag argument and verifies it matches both. Emits `::error::` annotations for GitHub Actions and a plain ✓ line for local use. - `rust.yml` — runs the check on every PR inside the existing build-lint job, immediately after checkout. Drift is caught at PR time, not release time. - `publish.yml` — new `preflight` job runs the check against the pushed tag before any artifact-producing job starts. publish-crate, build-wheels, and build-sdist all `needs: preflight`, so crates.io/PyPI cannot receive a build from an inconsistent tree. Also bumps `pyproject.toml` from rc5 → rc6 so the check passes on this PR and pyproject matches the rc6 state of Cargo.toml on main. This does not re-publish rc6 (rc6 is live on crates.io and the PyPI slot is blocked by rc5); the next release tag — rc7, when partitions lands — will be the first to exercise the preflight end-to-end. Verified locally: - `./scripts/check_versions.sh` → pass - `./scripts/check_versions.sh v2.0.0rc6` → pass - `./scripts/check_versions.sh v2.0.0rc7` → fail (exit 1, two error lines) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
moeyensj
added a commit
that referenced
this pull request
Apr 22, 2026
The CLI's analyze-linkages was single-partition only because the library's update_all_objects collapsed AllObjects rows by object_id, overwriting rows when the same object appeared across partitions. This lifts that restriction, adds CIFI-output reuse, and — because difi is designed to validate science algorithms — makes the "phantom classification" failure mode explicit rather than silent. Library (breaking changes to public signatures): - src/difi.rs: update_all_objects takes &PartitionSummary and filters obj_to_idx to rows matching the partition; multi-partition AllObjects is now safe to loop over. - src/difi.rs: classify_linkages and analyze_linkages return (AllLinkages, IgnoredLinkages). Linkages with zero observations inside the target partition are excluded from AllLinkages and reported in IgnoredLinkages with reason NoObservationsInPartition, instead of being classified as phantom pure/contaminated/mixed rows with num_obs_outside_partition == num_obs. Callers loop and aggregate. - src/types.rs: new IgnoredLinkage, IgnoredLinkageReason, IgnoredLinkages types, symmetric with AllLinkages. - src/io.rs: new readers read_all_objects / read_partition_summaries / read_findable_observations (for reuse); new writer write_ignored_linkages. Readers threading interned strings take &mut StringInterner; doc comments spell out the observations-first ordering contract. - src/python/mod.rs: destructures the new tuple; returns num_ignored_linkages in the result dict. Python parity tests stay bit-for-bit green (single-partition → ignored table is empty). CLI: - analyze-linkages exposes PartitionArgs and loops DIFI over each partition; all_linkages.parquet is flat with a partition_id column. - --cifi-output-dir reuses a prior CIFI snapshot (skips the CIFI phase, loads all_objects / partition_summaries / findable_observations, runs DIFI per partition). Mutually exclusive with partition flags; SHA-256 prefix fingerprint check against the reused manifest aborts on mismatch. Manifest gains a reused_cifi provenance field. - When any linkages are ignored, the CLI writes ignored_linkages.parquet and emits a structured warning to stderr (and NDJSON under --progress-json). The manifest's new warnings section records ignored_linkage_rows and orphan_linkages counts — orphan linkages (never classified in any partition) are a strong signal of a mismatched --linkages file for the partition scheme. - Conflict rule: --cifi-output-dir + any non-default partition flag is rejected up front. - README documents multi-partition analyze-linkages, the reuse flow, the ignored_linkages semantics, the output layout, and the ./scripts/check_versions.sh developer invocation. Tests (57 pass, +11 new over v1 CLI): - Library units: multi-partition update_all_objects isolation; per-partition sequential accumulation; classify_linkages excludes wholly-external linkages and surfaces them in IgnoredLinkages. - IO roundtrip: read_all_objects / read_partition_summaries / read_findable_observations round-trip (with interner re-alignment). - CLI: sliding-partition analyze-linkages produces multi-partition output; --cifi-output-dir succeeds and records provenance; ignored_linkages.parquet + manifest warnings populated for sliding partitions; fingerprint mismatch and partition-flag conflicts rejected. - Python parity tests remain byte-for-byte green. Version: Cargo.toml 2.0.0-rc6 -> 2.0.0-rc7, pyproject.toml 2.0.0rc6 -> 2.0.0rc7 (kept in lockstep by the preflight check from PR #63). Verified: - cargo build --no-default-features - cargo build --features cli - cargo fmt -- --check - cargo clippy --all-targets --all-features -- -D warnings - cargo test --features cli # 57 passed - ./scripts/check_versions.sh # versions consistent - ./scripts/check_versions.sh v2.0.0rc7 # tag + versions consistent Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks
moeyensj
added a commit
that referenced
this pull request
Apr 22, 2026
The CLI's analyze-linkages was single-partition only because the library's update_all_objects collapsed AllObjects rows by object_id, overwriting rows when the same object appeared across partitions. This lifts that restriction, adds CIFI-output reuse, and — because difi is designed to validate science algorithms — makes the "phantom classification" failure mode explicit rather than silent. Library (breaking changes to public signatures): - src/difi.rs: update_all_objects takes &PartitionSummary and filters obj_to_idx to rows matching the partition; multi-partition AllObjects is now safe to loop over. - src/difi.rs: classify_linkages and analyze_linkages return (AllLinkages, IgnoredLinkages). Linkages with zero observations inside the target partition are excluded from AllLinkages and reported in IgnoredLinkages with reason NoObservationsInPartition, instead of being classified as phantom pure/contaminated/mixed rows with num_obs_outside_partition == num_obs. Callers loop and aggregate. - src/difi.rs: "found" semantics tightened to in-partition counts. found_pure = pure && obs_inside_partition >= min_obs (was total_obs), found_contaminated = contaminated && dominant_count_in_partition >= min_obs (was whole-linkage dominant_count). analyze_linkages's found_objects set uses found_pure rather than pure. Prevents cross-boundary linkages with enough total obs but few inside any one partition from inflating partition_summary.completeness above 100%. Single-partition runs are unaffected (obs_inside_partition == total_obs there); Python parity tests remain bit-for-bit green. - src/types.rs: new IgnoredLinkage, IgnoredLinkageReason, IgnoredLinkages types, symmetric with AllLinkages. - src/io.rs: new readers read_all_objects / read_partition_summaries / read_findable_observations (for reuse); new writer write_ignored_linkages. Readers threading interned strings take &mut StringInterner; doc comments spell out the observations-first ordering contract. - src/python/mod.rs: destructures the new tuple; returns num_ignored_linkages in the result dict. Python parity tests stay bit-for-bit green (single-partition → ignored table is empty). CLI: - analyze-linkages exposes PartitionArgs and loops DIFI over each partition; all_linkages.parquet is flat with a partition_id column. - --cifi-output-dir reuses a prior CIFI snapshot (skips the CIFI phase, loads all_objects / partition_summaries / findable_observations, runs DIFI per partition). Mutually exclusive with partition flags; SHA-256 prefix fingerprint check against the reused manifest aborts on mismatch. Manifest gains a reused_cifi provenance field. In reuse mode the manifest's scenarios[0].metric and scenarios[0].partitions now reflect the reused snapshot's scheme rather than the (defaulted) CLI args, so reproducibility metadata is accurate. - When any linkages are ignored, the CLI writes ignored_linkages.parquet and emits a structured warning to stderr (and NDJSON under --progress-json). The manifest's new warnings section records ignored_linkage_rows and orphan_linkages counts — orphan linkages (never classified in any partition) are a strong signal of a mismatched --linkages file for the partition scheme. - Conflict rule: --cifi-output-dir + any non-default partition flag is rejected up front. - README documents multi-partition analyze-linkages, the reuse flow, the ignored_linkages semantics, the output layout, the updated in-partition "found" definition of completeness, and the ./scripts/check_versions.sh developer invocation. Tests (58 pass, +12 new over v1 CLI): - Library units: multi-partition update_all_objects isolation; per-partition sequential accumulation; classify_linkages excludes wholly-external linkages and surfaces them in IgnoredLinkages. - IO roundtrip: read_all_objects / read_partition_summaries / read_findable_observations round-trip (with interner re-alignment). - CLI: sliding-partition analyze-linkages produces multi-partition output; --cifi-output-dir succeeds and records provenance; reuse manifest captures the snapshot's partition scheme (regression test against the reuse-defaults bug found during empirical validation); ignored_linkages.parquet + manifest warnings populated for sliding partitions; fingerprint mismatch and partition-flag conflicts rejected. - Python parity tests remain byte-for-byte green. Version: Cargo.toml 2.0.0-rc6 -> 2.0.0-rc7, pyproject.toml 2.0.0rc6 -> 2.0.0rc7 (kept in lockstep by the preflight check from PR #63). Verified: - cargo build --no-default-features - cargo build --features cli - cargo fmt -- --check - cargo clippy --all-targets --all-features -- -D warnings - cargo test --features cli # 58 passed - ./scripts/check_versions.sh # versions consistent - ./scripts/check_versions.sh v2.0.0rc7 # tag + versions consistent - Empirical validation on 166M-row neomod survey with 12k synthetic linkages Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
moeyensj
added a commit
that referenced
this pull request
Apr 23, 2026
The CLI's analyze-linkages was single-partition only because the library's update_all_objects collapsed AllObjects rows by object_id, overwriting rows when the same object appeared across partitions. This lifts that restriction, adds CIFI-output reuse, and — because difi is designed to validate science algorithms — makes the "phantom classification" failure mode explicit rather than silent. Library (breaking changes to public signatures): - src/difi.rs: update_all_objects takes &PartitionSummary and filters obj_to_idx to rows matching the partition; multi-partition AllObjects is now safe to loop over. - src/difi.rs: classify_linkages and analyze_linkages return (AllLinkages, IgnoredLinkages). Linkages with zero observations inside the target partition are excluded from AllLinkages and reported in IgnoredLinkages with reason NoObservationsInPartition, instead of being classified as phantom pure/contaminated/mixed rows with num_obs_outside_partition == num_obs. Callers loop and aggregate. - src/difi.rs: "found" semantics tightened to in-partition counts. found_pure = pure && obs_inside_partition >= min_obs (was total_obs), found_contaminated = contaminated && dominant_count_in_partition >= min_obs (was whole-linkage dominant_count). analyze_linkages's found_objects set uses found_pure rather than pure. Prevents cross-boundary linkages with enough total obs but few inside any one partition from inflating partition_summary.completeness above 100%. Single-partition runs are unaffected (obs_inside_partition == total_obs there); Python parity tests remain bit-for-bit green. - src/types.rs: new IgnoredLinkage, IgnoredLinkageReason, IgnoredLinkages types, symmetric with AllLinkages. - src/io.rs: new readers read_all_objects / read_partition_summaries / read_findable_observations (for reuse); new writer write_ignored_linkages. Readers threading interned strings take &mut StringInterner; doc comments spell out the observations-first ordering contract. - src/python/mod.rs: destructures the new tuple; returns num_ignored_linkages in the result dict. Python parity tests stay bit-for-bit green (single-partition → ignored table is empty). CLI: - analyze-linkages exposes PartitionArgs and loops DIFI over each partition; all_linkages.parquet is flat with a partition_id column. - --cifi-output-dir reuses a prior CIFI snapshot (skips the CIFI phase, loads all_objects / partition_summaries / findable_observations, runs DIFI per partition). Mutually exclusive with partition flags; SHA-256 prefix fingerprint check against the reused manifest aborts on mismatch. Manifest gains a reused_cifi provenance field. In reuse mode the manifest's scenarios[0].metric and scenarios[0].partitions now reflect the reused snapshot's scheme rather than the (defaulted) CLI args, so reproducibility metadata is accurate. - When any linkages are ignored, the CLI writes ignored_linkages.parquet and emits a structured warning to stderr (and NDJSON under --progress-json). The manifest's new warnings section records ignored_linkage_rows and orphan_linkages counts — orphan linkages (never classified in any partition) are a strong signal of a mismatched --linkages file for the partition scheme. - Conflict rule: --cifi-output-dir + any non-default partition flag is rejected up front. - README documents multi-partition analyze-linkages, the reuse flow, the ignored_linkages semantics, the output layout, the updated in-partition "found" definition of completeness, and the ./scripts/check_versions.sh developer invocation. Tests (58 pass, +12 new over v1 CLI): - Library units: multi-partition update_all_objects isolation; per-partition sequential accumulation; classify_linkages excludes wholly-external linkages and surfaces them in IgnoredLinkages. - IO roundtrip: read_all_objects / read_partition_summaries / read_findable_observations round-trip (with interner re-alignment). - CLI: sliding-partition analyze-linkages produces multi-partition output; --cifi-output-dir succeeds and records provenance; reuse manifest captures the snapshot's partition scheme (regression test against the reuse-defaults bug found during empirical validation); ignored_linkages.parquet + manifest warnings populated for sliding partitions; fingerprint mismatch and partition-flag conflicts rejected. - Python parity tests remain byte-for-byte green. Version: Cargo.toml 2.0.0-rc6 -> 2.0.0-rc7, pyproject.toml 2.0.0rc6 -> 2.0.0rc7 (kept in lockstep by the preflight check from PR #63). Verified: - cargo build --no-default-features - cargo build --features cli - cargo fmt -- --check - cargo clippy --all-targets --all-features -- -D warnings - cargo test --features cli # 58 passed - ./scripts/check_versions.sh # versions consistent - ./scripts/check_versions.sh v2.0.0rc7 # tag + versions consistent - Empirical validation on 166M-row neomod survey with 12k synthetic linkages Co-Authored-By: Claude Opus 4.7 (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.
Summary
Preventing a recurrence of today's
v2.0.0rc6publish failure:crates.iogot the right version but PyPI 400-rejected the upload becausepyproject.tomlwas still pinned at"2.0.0rc5"(maturin builtdifi-2.0.0rc5-*.whlwheels against anrc6Cargo.toml). Same class of drift explains whyv2.0.0rc4never got tagged — silent config mismatch, failed publish, move on.This wires a single-source-of-truth check into both the PR gate and the publish pre-flight so Cargo ↔ pyproject ↔ tag drift can never reach a release again.
Changes
scripts/check_versions.sh— comparesCargo.toml(SemVer,2.0.0-rc6) topyproject.toml(PEP 440,2.0.0rc6) on the PEP 440 normal form. Optional tag arg verifies tag-vs-both. Emits::error::annotations in CI and a plain ✓ line locally.rust.yml— Build & Lint: runs the check right after checkout, no tag arg. Drift caught at PR time.publish.yml— newpreflightjob: runs the check with${GITHUB_REF_NAME}as the tag, gatespublish-crate/build-wheels/build-sdistvianeeds: preflight. Inconsistent tree → no crates.io or PyPI upload.pyproject.toml2.0.0rc5 → 2.0.0rc6: pyproject catches up to main'sCargo.tomlso this PR lands green. Does not re-publish (rc6 is live on crates.io; the PyPI slot for rc6 stays skipped since rc5 occupies the only path to it). The next tag —rc7, landing with PR Add multi-partition DIFI, CIFI-output reuse, and IgnoredLinkages #62 — is the first to exercise the preflight end-to-end.Test plan
./scripts/check_versions.sh→ ✓ (versions match after pyproject bump)./scripts/check_versions.sh v2.0.0rc6→ ✓ (tag matches)./scripts/check_versions.sh v2.0.0rc7→ exit 1, two::error::annotations (tag doesn't match either file)needs: preflightis sufficient to block artifact upload — matrix jobbuild-wheelsinherits it for both OS variants./scripts/check_versions.shin a pre-commit / pre-push hook? Low-priority follow-up🤖 Generated with Claude Code