ci: unified tag-triggered release pipeline with SLSA provenance on the Release (Scorecard Signed-Releases → 10)#91
Conversation
RankQuant.search_asymmetric_subset and Bitmap.body_overlap_scores_subset declared their candidate/doc-id arrays as PyReadonlyArray1<u32>, which rust-numpy matches strictly. The int64 arrays NumPy produces by default (np.arange, np.where()[0], np.array([...]), fancy indexing, np.argpartition) therefore raised an opaque "ndarray cannot be cast as ndarray" TypeError. ordvec's own top_m_candidates* emit uint32, so the happy path worked and the suite never exercised a user-built candidate set. Accept any integer dtype and convert to the core's u32 with checked bounds: negatives and values >= 2^32 raise a clear ValueError rather than silently wrapping (np.asarray(-1, uint32) -> 4294967295, 2**32 -> 0, which would score the wrong document). Already-uint32 contiguous arrays are still borrowed zero-copy; other dtypes are copied once. The >= n bound check and the body_overlap sorted-ids policy are unchanged. Flips the two red-team tests that asserted the old strict-uint32 rejection to the new contract (integer dtypes converted by value, non-integer dtypes still TypeError) and adds dtype-matrix + fail-loud regression tests. 390 pytest pass. Reported via external review: candidate ids naturally arrive as int64. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…d contract
Every embedding parameter was declared PyReadonlyArray{1,2}<f32>, which rust-numpy
matches strictly — so float64 (NumPy's default for np.array([...]) and most API
embeddings) and float16 raised an opaque TypeError. The premise of ordvec is
'float vector in -> rank/sign transform', so float32 is the internal working dtype,
not a contract the caller must pre-satisfy.
Add two choke-point helpers, as_f32_1d / as_f32_2d, that every embedding entry point
(18 methods + free functions) routes through, so dtype/layout/finite policy is
defined once:
- coerce float16/float32/float64 -> float32. The rank/sign transforms are
order/sign-only and f64->f32 rounding is monotonic, so coercion is faithful; the
asymmetric LUT scores against f32-quantised docs, so sub-f32 query precision is
meaningless there too.
- REJECT bool + all integer dtypes (TypeError): a {0,1}/narrow-int vector
rank-transforms to a degenerate index-tie artefact (silent retrieval garbage) — a
deliberate usage-error guard, not an ergonomic gap.
- reject complex/object/string (complex would silently drop the imaginary part).
- reject wrong ndim (TypeError).
- reject non-C-contiguous input (ValueError) BEFORE coercion, so a transposed
float64 is never silently laundered into a contiguous float32 (a hidden copy can
dominate runtime / poison benchmarks — the copy decision stays with the caller).
- all-finite check AFTER coercion (f64 > f32::MAX rounds to +inf — caught here).
Candidate IDs are a different boundary (labels, not measurements), so they keep the
permissive contract: rename coerce_candidate_ids -> as_u32_ids_1d, accept any
range-safe integer dtype (int64 included), reject bool/float/negative/overflow with
sharper messages. Coherent split: vectors = float-only/C-contiguous/finite;
candidate IDs = integer labels range-safe-coerced to u32.
Core, persistence (.tvr/.tvrq/.tvbm/.tvsb store no floats), and the integer
primitives are untouched. Inverts the 5 tests that asserted the old strict-f32
rejection (now coerced+faithful); adds test_input_dtype.py covering the full
accept/reject matrix across all four index types. 483 pytest pass; fmt + clippy
-D warnings clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
The choke-point helpers coerced non-float32 input to float32 (a full copy) before the caller's width check ran, so a wrong-width float64 array was fully converted only to be rejected — wasteful, and a potential OOM on a large misshapen input. Move the width check into as_f32_1d / as_f32_2d via a cheap shape-metadata read (axis_len), so dtype -> ndim -> width -> contiguity are all validated on the ORIGINAL array before the ascontiguousarray copy. as_f32_2d takes the expected dim; as_f32_1d takes Option<usize> (rank_transform has no width constraint). Adds a wrong-width-float64 regression across all four index types. 491 pytest pass; fmt + clippy -D warnings clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…e Release Replaces changelog.yml + release-crate.yml + release-python.yml with one tag-triggered release.yml. Cutting a stable vX.Y.Z tag now fully automates build (crate + wheels + sdist) -> GitHub artifact attestation -> SLSA Build-L3 provenance -> attach ALL assets (incl. multiple.intoto.jsonl + a .sigstore.json bundle) to the GitHub Release -> un-draft. Only the crates.io + PyPI publishes are gated (Environments with Required Reviewers). Full OIDC, no stored tokens. Fixes OpenSSF Scorecard Signed-Releases = 0: v0.2.0's assets were attached by hand and the attestations dropped. Scorecard's probes read ONLY GitHub release-asset filenames (.intoto.jsonl -> 10, .sigstore.json -> 8; registry/PEP740 ignored by design). The slsa-github-generator multiple.intoto.jsonl gives the provenance probe its 10; the attest-build-provenance .sigstore.json is a backup signing-probe asset + powers 'gh attestation verify'. A single release-asset writer (release-assets) + explicit needs: edges remove the cross-workflow coordination that forced the manual attach. Fail-closed: publishes need attest + provenance. REQUIRES before the first real tag (both fail CLOSED at the gate until done): (1) crates.io + PyPI Trusted-Publisher configs re-pointed to workflow=release.yml (env stays crates-io / pypi); (2) a fork dry-run to validate end-to-end — a fork cannot publish (Trusted Publishing OIDC is bound to this repo). actionlint clean; NOT yet run against a real tag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Review Summary by QodoNormalize vector/id input dtypes at FFI boundary; unified release pipeline with SLSA provenance
WalkthroughsDescription• Accept any integer dtype for candidate/doc-id arrays, converting to u32 with checked bounds - Eliminates opaque TypeError when passing NumPy's default int64 index arrays - Negative ids and values >= 2^32 raise clear ValueError; already-uint32 arrays borrowed zero-copy • Normalize float vector input to f32 at FFI boundary, accepting float16/float32/float64 - Coerces NumPy's default float64 to float32; f64→f32 rounding is monotonic and lossless for rank/sign transforms - Rejects bool/integer/complex/object/string arrays deliberately (degenerate index artifacts) • Add comprehensive dtype/layout boundary tests covering acceptance, rejection, and coercion • Unified tag-triggered release pipeline with SLSA Build-L3 provenance and GitHub attestations - Replaces three independent workflows (changelog/release-crate/release-python) with single release.yml - Coordinates build → attest → provenance → Release assets → un-draft → gated publishes Diagramflowchart LR
A["NumPy arrays<br/>any float/int dtype"] -->|as_f32_1d/2d| B["float32<br/>coercion"]
A -->|as_u32_ids_1d| C["u32 ids<br/>checked bounds"]
B -->|finite check| D["Index methods<br/>Rank/RankQuant/Bitmap/SignBitmap"]
C -->|range check| D
E["Tag vX.Y.Z"] -->|guard| F["SemVer + CI green"]
F -->|build| G["crate + wheels + sdist"]
G -->|attest| H["GitHub attestations<br/>+ .sigstore.json"]
G -->|provenance| I["SLSA .intoto.jsonl"]
H -->|release-assets| J["GitHub Release<br/>un-draft"]
I -->|release-assets| J
J -->|gated| K["publish-crate<br/>publish-pypi"]
File Changes1. ordvec-python/src/lib.rs
|
Code Review by Qodo
1.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Code Review
This pull request updates the Python FFI boundary for the ordvec library to automatically coerce and validate input NumPy arrays. Specifically, it introduces helper functions to coerce float16, float32, and float64 arrays to C-contiguous float32 arrays, and to safely convert any integer dtype candidate or document ID array to u32 with checked bounds. It also adds robust error handling to raise appropriate Python exceptions (such as TypeError, ValueError, and IndexError) for invalid inputs, and updates the index classes (Rank, RankQuant, Bitmap, and SignBitmap) to use these new helpers. Comprehensive unit and integration tests have been added to verify these coercion and validation behaviors. I have no feedback to provide as there are no review comments.
…zizmor cache-poisoning Addresses all three failing checks on PR #91 + all three qodo bugs: * actionlint (SC2035, x3): sha256sum *.crate *.whl *.tar.gz could be tricked by a hostile filename starting with '-'. Use ./*.glob form. * zizmor (HIGH, cache-poisoning): sccache: 'true' on maturin-action in a release workflow allows a poisoned cache to inject code into the shipped wheel. Disable sccache on the release path (python.yml keeps it for the PR/main cadence). * tests/release_publish_invariants.sh: was coupled to the deleted release-python.yml and the explicit '*.cdx.json' delete pattern. Re-point at release.yml, the publish-pypi job, and accept the new keep-only-wheels/tar.gz cleanup form (qodo bug 1). * release.yml concurrency group: 'release-${{ github.ref }}' was per-tag and so allowed multiple tag pipelines to run concurrently — contrary to the 'serialize releases' comment. Use a constant 'release' group for true global serialization (qodo bug 2). * Docs / comments referring to deleted workflows: RELEASING.md (significantly rewritten for the tag-triggered + gated-publish model), CONTRIBUTING.md, THREAT_MODEL.md, cliff.toml, .github/workflows/python.yml. python.yml's stale 'Intel wheel is still built + shipped' claim corrected (no Intel wheel ships — issue #29) (qodo bug 3). Locally: actionlint clean, zizmor clean ('No findings to report'), tests/release_publish_invariants.sh OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…codex stop-gate)
Codex flagged that the env-policy I'd documented ("Deployment branches and
tags = main-only") was inherited from the old workflow_dispatch model and
would now DEADLOCK the new tag-triggered publish: the workflow runs on
`refs/tags/vX.Y.Z`, not `refs/heads/main`, so a branch-only allowlist refuses
every tag-triggered run at the environment gate.
Update RELEASING.md, THREAT_MODEL.md, and the release.yml header comment to
specify the correct env policy: "Selected branches and tags" with a single
TAG pattern `v[0-9]*.[0-9]*.[0-9]*`. The "tag must come from main" guarantee
is preserved by `require-ci-green` (queries `?branch=main&status=success` for
the SHA, which only returns a hit if the SHA was pushed to main) plus branch
protection on `main`. An optional tag ruleset can be added as defence in
depth.
Doc-only change; actionlint still clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…codex stop-gate)
Two stale spots survived the env-policy fix:
* THREAT_MODEL.md THREAT-SUPPLY-001 still described the env policy as
"restrict deployment to the main branch only" — the branch-only policy that
would deadlock the new tag-triggered workflow. Rewritten to specify the tag
pattern and the require-ci-green + branch-protection chain that preserves
the "must come from main" guarantee. Also adjusted the social-engineering
residual ("dispatcher and approver" -> "cuts the release tag and approves
both publishes") to match the new model.
* .github/workflows/ci.yml release-guard preamble still called the release
workflows "workflow_dispatch-only." Updated to describe release.yml's
tag-triggered + Environment-gated model.
Doc-only / comments-only; no YAML semantic change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
There was a problem hiding this comment.
Pull request overview
This PR replaces three independent release workflows (changelog.yml, release-crate.yml, release-python.yml) with a single tag-triggered release.yml pipeline that automates build → attest → SLSA provenance → Release-asset attach → un-draft, with only the two registry publishes (crates.io, PyPI) gated behind GitHub Environments with required reviewers. It also broadens the Python FFI binding to accept float16/float32/float64 embeddings (coerced to f32 at the boundary) and any integer dtype for candidate/doc IDs (checked-conversion to u32), with comprehensive new tests covering the updated dtype/layout contract.
Changes:
- Add unified
release.yml(tag-triggered, SemVer-guarded, withactions/attest-build-provenance+slsa-github-generatorproviding.intoto.jsonland.sigstore.jsonRelease assets) and delete the three old workflows. - Broaden Python FFI: new helpers
as_f32_1d/as_f32_2d(coerce f16/f64→f32, enforce C-contiguity before coercion, finite-check after) andas_u32_ids_1d/check_ids_in_range(accept any integer dtype with checked conversion). - Update
tests/release_publish_invariants.shand supporting docs (RELEASING.md,THREAT_MODEL.md,CONTRIBUTING.md,cliff.toml) to match the new tag-triggered model, and replace*_rejectedfloat64 tests with*_coercedequivalents.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/release.yml |
New unified tag-triggered release pipeline with SLSA L3 provenance and gated env publishes. |
.github/workflows/release-crate.yml, release-python.yml, changelog.yml |
Deleted; functionality folded into release.yml. |
.github/workflows/ci.yml, python.yml |
Comment updates pointing to the new workflow filename. |
tests/release_publish_invariants.sh |
Retargeted to release.yml/publish-pypi job and accepts a keep-only-wheels cleanup form. |
ordvec-python/src/lib.rs |
New as_f32_1d/as_f32_2d/as_u32_ids_1d boundary helpers; all embedding entry points and ID-array params switched to &Bound<PyAny> and route through them. |
ordvec-python/tests/test_input_dtype.py |
New module documenting the f16/f32/f64-accepted, non-float-rejected, contiguity-before-coercion, finite-after-coercion contract. |
ordvec-python/tests/test_input_guards.py |
New broad integer-dtype coverage for candidate/doc IDs, including overflow/negative/out-of-corpus rejection. |
ordvec-python/tests/test_redteam_fuzz.py |
Remove f16/f32/f64 from rejected-dtype list; add _NON_INTEGER_ID_DTYPES and int-dtype-by-value tests. |
ordvec-python/tests/test_rank.py, test_rank_quant.py, test_bitmap.py, test_sign_bitmap.py |
Replace test_add_float64_is_rejected with test_add_float64_is_coerced. |
RELEASING.md, THREAT_MODEL.md, CONTRIBUTING.md, cliff.toml |
Update release docs to describe the tag-triggered unified pipeline and tag-pattern environment policies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ate publish (grumpy blockers) Addresses both grumpy blockers on PR #91: BLOCKER 1: the GitHub Release was un-drafted as part of the asset-attach step, BEFORE the registry publishes. A failed crates.io / PyPI publish would leave a public GitHub Release pointing at a version the registries refused — exactly the half-published "coordinated release" mode RELEASING.md says to avoid. Split `release-assets` into: * `release-assets-draft` — uploads to the DRAFT release; DOES NOT un-draft. * `publish-github-release` — `needs: [publish-crate, publish-pypi]`; the SOLE un-draft point. If either registry publish fails, the Release stays DRAFT until the failure is investigated / re-run. publish-crate and publish-pypi now `needs: release-assets-draft` (the draft- assets edge transitively carries the attest + provenance fail-closed gate). BLOCKER 2: publish-crate ran `cargo publish` on a fresh checkout — the .crate it uploaded was not proven to match the SLSA-attested artifact `build-crate` produced (toolchain drift / non-determinism could quietly diverge). Add a byte-identity gate to publish-crate: 1. Download the attested `dist-crate` artifact. 2. Re-package with `cargo package -p ordvec --locked`. 3. sha256-compare the repackaged .crate to the attested .crate. 4. Only then mint the crates.io OIDC token and `cargo publish`. A mismatch fails closed BEFORE the token is minted — nothing reaches crates.io. publish-pypi already uploads the exact built wheels/sdist via pypa/gh-action-pypi-publish, so it has byte-identity by construction. NEW: tests/release_signed_release_invariants.sh — Grumpy's "anti-Claude regression guard." Structural lint over release.yml asserting the signed- release graph: release-assets-draft needs attest+provenance+require-ci-green, uploads .crate/.whl/.tar.gz/.sigstore.json/.intoto.jsonl, does NOT un-draft; SLSA generator tag-pinned with upload-assets:false and `*.intoto.jsonl` provenance-name; attest grants id-token+attestations:write; publish-* grant id-token:write and need the draft assets; publish-crate does the byte-identity check (download-artifact dist-crate + cargo package + sha256sum); publish- github-release needs BOTH publishes and is the sole un-draft point. Wired into ci.yml's release-guard so a future commit can't silently dismantle the chain. RELEASING.md flow description updated for the new sequence (stage on DRAFT, gated publishes, un-draft only after both succeed) + the byte-identity check. Locally: actionlint clean, zizmor clean ("No findings to report"), both invariant scripts OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…ed (codex stop-gate) Codex correctly flagged that the previous byte-identity check only proved `cargo package`'s output matches the attested .crate — but `cargo publish` runs its OWN internal packaging step before uploading, which the pre-publish gate cannot inspect. Determinism makes those bytes equal in practice, but "must be" is not "is." Add the empirical post-publish proof to publish-crate: after `cargo publish` succeeds, curl the just-published .crate from `https://crates.io/api/v1/crates/ordvec/<v>/download` (with a 60s retry window for CDN propagation) and sha256-compare to the attested artifact. * If the bytes crates.io serves equal the SLSA-attested bytes -> the version on crates.io IS the artifact the provenance covers (the byte-identity claim is empirically verified, not just assumed). * If they differ -> publish-crate fails closed. The version is on crates.io (yank-only) but publish-github-release will NEVER un-draft the Release, and the mismatch is loudly audit-logged. The pre-publish gate stays as a fast-fail before the OIDC token is even minted; the post-publish step is the actual proof. Together they cover both sides of `cargo publish`'s internal packaging. Invariants script (release_signed_release_invariants.sh) updated to require the post-publish curl + sha256 step exists in publish-crate; RELEASING.md describes both sides of the proof. release.yml header expanded accordingly. Locally: actionlint clean, zizmor clean, both invariant scripts OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
…d-2) Grumpy's round-2 nits flagged two workflow comments still referencing the old (pre-split) `release-assets` job name + un-draft timing: * `notes` job: "draft so artifacts get assembled before release-assets un-drafts" -> rewritten to describe the new sequence (release-assets-draft stages, publishes run gated, publish-github-release un-drafts only after both succeed). * `provenance` job: "release-assets is the single owner of all Release uploads" -> renamed to release-assets-draft. Pure comment cleanup; no YAML semantic change. actionlint clean, zizmor clean, both invariant scripts OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
Summary
Surfaced by @Signal-Ridge-SysAdmin's external review of the published wheel: the bindings declared NumPy parameters with strict dtypes, so the dtypes NumPy produces by default raised opaque
TypeErrors. That reported papercut generalised into two coherent boundary contracts and a unified, tag-triggered release pipeline that automates the build/attest/attach steps v0.2.0 had to do by hand (which is how its attestations got dropped → ScorecardSigned-Releases = 0):The split is deliberate: vector values define ordinal structure (bool/narrow-int must be rejected — they'd rank-transform to garbage); candidate IDs are labels (so
int64must be accepted — rejecting it is hostile). And the release flow refuses to make a public GitHub Release for a version the registries refused.Commit 1 — candidate IDs (
as_u32_ids_1d)int64candidate arrays (NumPy's default fornp.arange,np.where()[0], fancy-indexing,np.argpartition) used to bounce off withTypeError: 'ndarray' object cannot be cast as 'ndarray'. Now: accept any range-safe integer dtype (negatives,>= 2**32, non-integer → fail-loudValueError/TypeError); uint32 still borrowed zero-copy.Commit 2 — embeddings (
as_f32_1d/as_f32_2d)Every
f32embedding param was strict, sofloat64(NumPy's default) andfloat16raisedTypeError. ordvec's premise is float vector in → rank/sign transform; f32 is the internal working dtype, not a caller contract.float16/float32/float64 → float32(rank/sign transforms are order-only;f64→f32rounding is monotonic; the asym LUT scores against f32-quantised docs).bool+ integer dtypes (silent degenerate-rank artefact); reject complex/object/string and wrong ndim.f64 > f32::MAX→+inf).Commits 3+ — unified release pipeline (
release.yml)Replaces
changelog.yml+release-crate.yml+release-python.ymlwith one tag-triggered workflow whose job graph forces the assemble-then-publish coordination the old split-workflow design left to a manual step.Provenance / attestation, soup to nuts (all genuine, nothing faked):
slsa-github-generator@v2.1.0,upload-assets: false(a workflow-artifact*.intoto.jsonlis the SOLE provenance file, attached via the single release-assets writer)multiple.intoto.jsonlon the Releaseactions/attest-build-provenance@v4.1.0*.sigstore.jsonon Releasegh attestation verify, signing-probe backuppypa/gh-action-pypi-publish@v1.14.0(attestations default ON)rust-lang/crates-io-auth-action@v1.0.4(Trusted Publishing OIDC)Job graph (on tag-push of a strict
vX.Y.Z):What this PR fixes about how v0.2.0 broke
release-assets-draftis a single coordinated writer with explicitneeds:edges; no human in the asset-attach loop.Signed-Releases = 0(no.intoto.jsonlon the Release) → SLSA generator's signedmultiple.intoto.jsonlis attached automatically. Scorecard floor-averages 5 releases, so the score lifts at the next stable tag.cargo publishcould upload a different.cratethan the SLSA-attested one →publish-crateproves byte-identity twice: a pre-publishcargo packagesha256-compare (fast-fail before OIDC token mint), and a post-publishcurl crates.io/.../downloadsha256-compare (empirical proof the bytes crates.io actually serves equal the attested bytes; mismatch fails the job → no un-draft).Full OIDC — no stored tokens
Already the case and carried over: crates.io via
rust-lang/crates-io-auth-action(ephemeral token), PyPI via Trusted Publishing. There is noCARGO_REGISTRY_TOKENsecret anywhere.Anti-regression —
tests/release_signed_release_invariants.shStructural lint over
release.yml, wired intoci.yml'srelease-guard(runs every push/PR). Asserts the whole signed-release graph:release-assets-draftneeds attest+provenance+CI, uploads every required suffix, does NOT un-draft; SLSA generator tag-pinned withupload-assets: falseand a*.intoto.jsonlprovenance-name; attest grants id-token+attestations:write; both publishes grant id-token:write and need the draft assets;publish-crateactually performs the pre-publish AND post-publish byte-identity checks;publish-github-releaseneeds both publishes and is the only un-draft point.A future PR that silently weakens any of these edges fails CI here, not at the next release.
workflow = release.ymlworkflow = release.ymlv[0-9]*.[0-9]*.[0-9]*crates-ioandpypi(the oldmain-branch-only setting from the dispatch model would now deadlock publishing — the workflow runs onrefs/tags/..., never onrefs/heads/main)Signed-Releasescheck against the forkFieldnote-Echo/ordvec)Test plan
actionlintclean; YAML parses;zizmorclean (No findings to report).tests/release_publish_invariants.shOK(SBOM cleanup ordering preserved).tests/release_signed_release_invariants.shOK(signed-release graph pinned).pytest ordvec-python/tests— 491 passed (the dtype-boundary commits' coverage).Signed-Releaseshits 10 against the fork's Release.Notes
@v2.1.0), not SHA-pinned — mandatory for its self-verification trust model; carries# zizmor: ignore[unpinned-uses].v[0-9]*.[0-9]*.[0-9]*); theguardjob enforces strict SemVer.harden-runnerblock mode (audit → block once an allowlist is observed), strict "tag SHA ==mainHEAD" (todayrequire-ci-greenenforces "SHA must be onmainwith green push-event CI"), and promoting the optional tag ruleset to mandatory.Credit
Reported by @Signal-Ridge-SysAdmin, who reviewed the published wheel and caught the candidate-ID dtype papercut (and the broader "should this keep the floats at all?" question that motivated the embedding contract). Release-pipeline rigour (byte-identity, fail-closed un-draft, anti-regression invariants) shaped by grumpy's adversarial review.
🤖 Generated with Claude Code