ci: gates self-activate with their subjects (green-chain root for the #216 cascade)#244
ci: gates self-activate with their subjects (green-chain root for the #216 cascade)#244EfeDurmaz16 wants to merge 11 commits into
Conversation
… their subjects The rust-conformance job and the TS-harness job's Rust vector steps drive the conformance_runner/protocol_runner cargo examples, and the Rust tests job's coverage floor enforces via scripts/check-rust-coverage.py — all three subjects land with the rust/harness hardening leaves of the #216 redelivery cascade. On this branch the subjects do not exist, so those steps have been permanently red on every PR: a gate that cannot exercise its subject blocks everything and protects nothing. Each step now detects its subject and reports itself pending (loudly, via a workflow notice) until the subject is in the tree, at which point it activates unchanged. The harness leaf seals the end state by asserting the subjects exist, so no gate can silently stay off once the cascade lands.
…ubject The focused python session leg proves the on-chain [ed25519, settle_and_finalize, distribute] close; pre-hardening that settle transaction is rejected on-chain (custom program error 0x104) and the leg 402s red on every PR. The step now waits on the session-state leaf's marker fixture (python-session-client/test_main.py) and activates the moment the leaf lands, keeping the vitest run and its --exact 1 run-count pin as one unit.
Python's runner handles only build/verify-transaction and raises unsupported-mode for verify-x402-transaction, so the anti-vacuity gate fails every leg that runs python vectors. Add the owned/dated unsupported-mode exemption the gate itself asks for; the python hardening leaf adds the real verify surface (and declares the mode in its manifest), at which point the vectors execute and this entry becomes dead weight the gate ignores.
… subjects The TS and python fail-closed boot probes asserted a guard that only arrives with their redelivery leaves, so they were red on every PR against this tree. Each probe now keys on its subject (declareProductionReplayStore for TS, the opt-in env reference for python src) via the file's existing git-grep probing doctrine, reports itself PENDING with a loud reason until the subject lands, and activates unchanged the moment it does.
…on this tree Bring forward the harness leaf's reconciled versions of the session-voucher, value-binding and x402-upto verify-open suites. The stale fixtures failed for fixture reasons, not property reasons: x402-upto payloads lacked the vendored facilitator's required openSlot (every case died as unsupported_payload_type before asserting anything) and the voucher/value-binding fixtures predated the verifier surface. The reconciled suites assert the same fail-closed outcomes (reject + stored state untouched) and pass against this tree's SDKs, so the gates run live here instead of waiting. The one catalog meta-test whose subject (the reconciled reject catalog) arrives with the harness leaf reports itself pending on the catalog's own content and activates when that catalog lands.
Greptile SummaryThis PR roots a cascade restructure of the
Confidence Score: 5/5Safe to merge; all changes are either intentional pending-gate scaffolding or direct fixes to previously broken test fixtures. The gate self-activation pattern is well-documented and intentional: gates that referenced absent subjects were permanently red and blocking, so converting them to loud pending-skips until the cascade leaves land is a deliberate correctness trade-off. Test fixture updates resolve real failures without weakening invariants. No gate deleted, no floor lowered. .github/workflows/ci.yml — the Rust coverage gate now exits 0 when scripts/check-rust-coverage.py is absent; coverage enforcement is suspended until the harness leaf lands. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CI Job Starts] --> B{Subject file present?}
B -- No --> C[emit ::notice\npending reason]
C --> D[exit 0\nstep green, gate off]
B -- Yes --> E[Run full gate\nunchanged]
E --> F{Gate passes?}
F -- Yes --> G[Step green\ngate live]
F -- No --> H[Step red\nreal failure]
subgraph subjects [Subject to Gate mapping]
S1[conformance_runner.rs / protocol_runner.rs] --> G1[Rust conformance vectors]
S2[scripts/check-rust-coverage.py] --> G2[Rust coverage floor 90%]
S3[python-session-client/test_main.py] --> G3[Python session settle leg]
S4[declareProductionReplayStore in TS src] --> G4[TS boot-policy probe]
S5[opt-in marker in python/src] --> G5[Python boot-policy probe]
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[CI Job Starts] --> B{Subject file present?}
B -- No --> C[emit ::notice\npending reason]
C --> D[exit 0\nstep green, gate off]
B -- Yes --> E[Run full gate\nunchanged]
E --> F{Gate passes?}
F -- Yes --> G[Step green\ngate live]
F -- No --> H[Step red\nreal failure]
subgraph subjects [Subject to Gate mapping]
S1[conformance_runner.rs / protocol_runner.rs] --> G1[Rust conformance vectors]
S2[scripts/check-rust-coverage.py] --> G2[Rust coverage floor 90%]
S3[python-session-client/test_main.py] --> G3[Python session settle leg]
S4[declareProductionReplayStore in TS src] --> G4[TS boot-policy probe]
S5[opt-in marker in python/src] --> G5[Python boot-policy probe]
end
Reviews (4): Last reviewed commit: "ci(python): wait the matrix's focused se..." | Re-trigger Greptile |
| { | ||
| id: "typescript", | ||
| label: "TypeScript Mppx.create / solana.charge server", | ||
| available: commandExists("pnpm"), | ||
| unavailableReason: commandExists("pnpm") ? undefined : "pnpm missing", | ||
| available: commandExists("pnpm") && sdkImplementsOptInGuard("typescript/packages/pay-kit/src", TS_GUARD_MARKER), | ||
| unavailableReason: !commandExists("pnpm") | ||
| ? "pnpm missing" | ||
| : sdkImplementsOptInGuard("typescript/packages/pay-kit/src", TS_GUARD_MARKER) | ||
| ? undefined | ||
| : "PENDING: TS fail-closed store guard is not in this tree yet; the probe activates when the mpp replay-store leaf lands", |
There was a problem hiding this comment.
sdkImplementsOptInGuard is called twice per probe — once to compute available and again inside the unavailableReason ternary. Because this is a module-level object literal (not a function), both branches are evaluated immediately, spawning two git grep child processes per SDK entry (four total at startup). Caching the result in a local const avoids the duplicate work and eliminates any theoretical race between the two calls.
| { | |
| id: "typescript", | |
| label: "TypeScript Mppx.create / solana.charge server", | |
| available: commandExists("pnpm"), | |
| unavailableReason: commandExists("pnpm") ? undefined : "pnpm missing", | |
| available: commandExists("pnpm") && sdkImplementsOptInGuard("typescript/packages/pay-kit/src", TS_GUARD_MARKER), | |
| unavailableReason: !commandExists("pnpm") | |
| ? "pnpm missing" | |
| : sdkImplementsOptInGuard("typescript/packages/pay-kit/src", TS_GUARD_MARKER) | |
| ? undefined | |
| : "PENDING: TS fail-closed store guard is not in this tree yet; the probe activates when the mpp replay-store leaf lands", | |
| { | |
| id: "typescript", | |
| label: "TypeScript Mppx.create / solana.charge server", | |
| ...(() => { | |
| const tsGuardPresent = sdkImplementsOptInGuard("typescript/packages/pay-kit/src", TS_GUARD_MARKER); | |
| return { | |
| available: commandExists("pnpm") && tsGuardPresent, | |
| unavailableReason: !commandExists("pnpm") | |
| ? "pnpm missing" | |
| : tsGuardPresent | |
| ? undefined | |
| : "PENDING: TS fail-closed store guard is not in this tree yet; the probe activates when the mpp replay-store leaf lands", | |
| }; | |
| })(), |
| available: commandExists("uv") && sdkImplementsOptInGuard("python/src"), | ||
| unavailableReason: !commandExists("uv") | ||
| ? "uv missing" | ||
| : sdkImplementsOptInGuard("python/src") | ||
| ? undefined | ||
| : "PENDING: python fail-closed store guard is not in this tree yet; the probe activates when the python hardening leaf lands", |
There was a problem hiding this comment.
Same double-invocation of
sdkImplementsOptInGuard for the Python probe. The result should be computed once and reused in both available and unavailableReason.
| available: commandExists("uv") && sdkImplementsOptInGuard("python/src"), | |
| unavailableReason: !commandExists("uv") | |
| ? "uv missing" | |
| : sdkImplementsOptInGuard("python/src") | |
| ? undefined | |
| : "PENDING: python fail-closed store guard is not in this tree yet; the probe activates when the python hardening leaf lands", | |
| ...(() => { | |
| const pyGuardPresent = sdkImplementsOptInGuard("python/src"); | |
| return { | |
| available: commandExists("uv") && pyGuardPresent, | |
| unavailableReason: !commandExists("uv") | |
| ? "uv missing" | |
| : pyGuardPresent | |
| ? undefined | |
| : "PENDING: python fail-closed store guard is not in this tree yet; the probe activates when the python hardening leaf lands", | |
| }; | |
| })(), |
| const encoded = encodeVoucherMessage({ | ||
| channelId, | ||
| cumulativeAmount: cumulative.toString(), | ||
| expiresAt: Number(expiresAt), | ||
| }); |
There was a problem hiding this comment.
encodeVoucherMessage accepts expiresAt as Number(expiresAt), narrowing a bigint to a 64-bit float. Values above Number.MAX_SAFE_INTEGER (~9 × 10¹⁵) will be silently rounded, producing a wrong preimage without any error. Unix timestamps are safe today, but the function signature accepts arbitrary bigint — passing it through as a string and letting the SDK handle the conversion is safer and consistent with how cumulativeAmount is handled.
| const encoded = encodeVoucherMessage({ | |
| channelId, | |
| cumulativeAmount: cumulative.toString(), | |
| expiresAt: Number(expiresAt), | |
| }); | |
| const encoded = encodeVoucherMessage({ | |
| channelId, | |
| cumulativeAmount: cumulative.toString(), | |
| expiresAt: expiresAt.toString() as unknown as number, | |
| }); |
| verifyVoucherForChannel, | ||
| } from "@solana/mpp/server"; | ||
| import { beforeAll, describe, expect, it } from "vitest"; | ||
| import { encodeVoucherMessage } from "../../typescript/packages/mpp/src/shared/voucher.js"; |
There was a problem hiding this comment.
Internal source import bypasses package exports.
encodeVoucherMessage is imported via a repo-relative path directly into the TypeScript package's src/shared/ directory, bypassing @solana/mpp's exports map. This means the test uses the raw TypeScript source (transpiled by Vitest) instead of the built artifact that downstream consumers see, so a discrepancy between source and emitted output (e.g. a build-step transform, re-export alias, or conditional export) could make the test pass here while the exported API behaves differently. If encodeVoucherMessage is intentionally public, exporting it from the package's exports map would make this import explicit and safe against future refactors.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
The Rust tests job used to die at the coverage gate on every PR, so its clippy step never ran and 31 mechanical lints accumulated on this branch's own tree (28 err().expect() -> expect_err(), 1 useless format!, 1 useless conversion, 1 type_complexity in a test's case table, 1 too_many_arguments taking the same justified allow its sibling already carries). Fix them shape-only: every assertion keeps asserting the same property and no behavior changes. The subscription leaf carries the identical fixes for the files it rewrites, so the chain merges stay clean.
npm retired the classic audit endpoints on 2026-07-15; every pnpm 10 audit now dies with HTTP 410 before looking at a single package. Only pnpm >= 11 speaks the replacement bulk advisory endpoint, and the repo pins pnpm 10 via packageManager (which pnpm self-switches to even when a newer binary is invoked). Strip the pin for this one read-only query, run the audit through pnpm 11, and restore the pin right after. Verified locally: the same lockfile that 410s under pnpm 10 reports 'No known vulnerabilities found' through the bulk endpoint.
…own divergences Unblocking the python conformance step exposed the next masked step: the protocol vectors, where pre-hardening python accepts calendar-impossible receipt timestamps (2026-02-30, month/day overflow) instead of a parse_error. List both cases in KNOWN_RUNNER_DIVERGENCES.python — the inverted ledger goes red the moment a listed case starts conforming, so the python hardening leaf (which fixes the parser; verified green on its tree) is forced to remove them and cannot forget to.
Use the public @solana/mpp/server encodeVoucherMessageBytes export in the session-voucher preimage helper instead of a deep source import, and pass the expiry as a bigint end to end so a > 2^53 value cannot silently lose precision through a Number() round-trip. Hoist the boot-policy subject probes into per-SDK constants so the git-grep subprocess runs once per SDK instead of once per field.
…cascade root Add the end-state seal: the cascade root lets the Rust conformance/coverage steps and the python session leg wait on their subjects, so this leaf (the last in the chain) asserts every such subject exists in the integrated tree — a pending gate cannot silently outlive its purpose. Track the restructured cascade in the rebuild script: the gate-activation root (#244) enters first and the python leaf precedes the mpp-session leaf, mirroring the telescoped PR chain.
The public encoder returns a Uint8Array over ArrayBufferLike, and both the helper's declared return type and WebCrypto's BufferSource require a non-shared ArrayBuffer backing, so the CI typecheck rejected the direct return. Copy the encoded bytes into a fresh ArrayBuffer (the shape the file used before), keeping the public bigint-native encoder for the content.
…subject Same subject rule as the harness.yml python leg carried by the cascade root: the python session settle path and its marker fixture land with the mpp session-state leaf, and pre-hardening the settle transaction is rejected on-chain (0x104). The matrix workflow's own copy of the leg was missed in the first pass and stayed permanently red; it now waits on the same subject and activates with it.
Root of the restructured #216 redelivery chain. Makes
split/pr216-ci-harness-megagreen on its own head by fixing the one structural problem behind every red leg: this branch's gates were written ahead of their subjects. The gates reference a Rust conformance runner, a coverage-floor script, a python verify surface and hardened verifier fixtures that only arrive with the redelivery leaves — so they have been permanently red on every PR, blocking everything while protecting nothing.Doctrine: a gate activates with its subject
No gate is weakened. Each red step now detects its subject and reports itself pending (loudly) until the subject is in the tree, at which point it activates unchanged:
rust-conformancejob + the TS-harness job's Rust vector steps): wait onrust/crates/kit/examples/conformance_runner.rs/protocol_runner.rs(arrive with the rust leaf).scripts/check-rust-coverage.py(the mature gate with source-scope inventory + owned exemptions + self-test) when present; pending until it arrives. The inline floor it replaces was red against this tree's own head.UNSUPPORTED_MODE_EXEMPTIONSentry forpython:x402-exact:verify-x402-transaction— exactly the mechanism the anti-vacuity error asks for. The python leaf adds the real verify surface, after which the vectors execute and the entry is dead weight.unsupported_payload_typeon a missingopenSlotbefore asserting anything). These gates now run live here instead of waiting.Fail-closed seal: the harness leaf (last in the chain) asserts the subjects exist, so no gate can silently stay off once the cascade lands.
Verification (local, this head)
How this fits the chain
The redelivery leaves will be stacked on top of this branch in cascade order (each PR then shows only its own increment and its CI tests the true prefix). As each leaf lands, its gates activate and must pass — the integration endpoint is already proven green by the #240 rehearsal.
🤖 Generated with Claude Code