Skip to content

Hold a deploy suite key to an alphabet the key list reads back as - #219

Merged
thedavidmeister merged 7 commits into
mainfrom
fix-203
Sep 16, 2026
Merged

thedavidmeister merged 7 commits into
mainfrom
fix-203

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #203.

The rule

A deploy suite key is a NAME, or a name and a release TAG joined by an at sign:
the name lowercase letters and hyphens, the tag those plus digits and
underscores, neither half empty and at most one at sign.

Digits and underscores are legal ONLY after the at sign. That is what the keys
this repo generates need and all they need: LibRainDeploySnapshot emits a
released entry as the candidate key it was cut from, the at sign, and the record
directory's tag, which isTag already holds to X_Y_Z — so
address-registry@0_1_10 passes as it stands and the hand written
address-registry it was cut from passes as it stands.

The rule is held against the WHOLE key rather than against a candidate key with
the released form derived from it, because releasedSuites() declares finished
keys: allSuites() is the one place every key a repo declares is read, and a
rule that only knew about the candidate half could not be asked about a released
entry at all.

The test fixtures spelled their released key address-registry-0-0-1, a shape
LibRainDeploySnapshot has never emitted and the rule now refuses, so they move
to address-registry@0_0_1 — the generated shape. That is most of the diff's
line count and none of its behaviour.

What was wrong

UnknownDeploymentSuite carries the declared keys "because the whole point of a
registry is that the answer is not one hardcoded string the caller has to
already know". suiteNames() builds that list by joining the registry on ", "
and nothing constrained what a key may contain, so the rendering was not
injective: the TWO suite registry keyed a,b and c renders a,b, c, which is
exactly what the THREE suite registry keyed a, b and c reports. A reader is
told a different number of suites exist than do, and b — declared nowhere — is
handed to them as a valid key.

Code or docs

The code. The NatSpec is the intent oracle and it is right: a list a caller
cannot read back as the set of keys it names IS the hardcoded string the registry
exists to replace, just spelled differently.

The refusal lives in allSuites(), in the pairwise pass that already refuses a
duplicate: both the deploy side and the verify side read the registry through
it, so every reader refuses at once and suiteByName can never answer a failed
lookup with an ambiguous list.

Reconciled with the empty-key rule, not stacked on it

#231 (issue #202) landed EmptyDeploySuiteKey(uint256 index) in this same pass
while this PR was open. An alphabet that admits no zero-byte key already refuses
the empty key, so this does not add a second rule beside it: the two are one
InvalidDeploySuiteKey(uint256 index, string suite), and the empty-key
reasoning moves into its NatSpec unchanged — the empty string is the value
RainDeployBroadcast.run() substitutes for an absent DEPLOYMENT_SUITE, so it
is the one refusal here with a broadcast behind it.

The error keeps #231's index, for #231's reason: an empty key names nothing, so
the position in allSuites() is the only thing that can. It gains the key
itself, which is what a reader needs for every refusal that is not the empty
one. testEmptySuiteKeyReverts is kept, asserting the same index at the same
reader set, under the error that now refuses it.

Migration

Every key a consumer declares is now held to the alphabet, on every reader —
the deploy script, the verification tests and the chain matrix alike — with
InvalidDeploySuiteKey(<index>, <key>) at declaration read time, which cannot
reach a broadcast.

Measured rather than assumed: the 40 distinct keys declared across every repo in
the org that declares one — rain.extrospection.deploy, rain.factory.deploy,
rain.math.float.deploy, rain.metadata.deploy, rain.tofu.erc20-decimals.deploy,
rainlang.deploy and raindex — were extracted from a fresh clone of each and
held against the rule. Zero are rejected. So are this repo's own: the six
<name>@<tag> released keys under src/lib/Lib*Released.sol and the two
candidate keys in RegistryDeploySuites.

Suite

nix develop -c forge test -j 2 on the merged branch: 535 passed, 2 failed
(537 total). Both failures are testFindDeployBlockZoltuFactory and
testIsStartBlockAtDeployBlock in test/src/lib/LibRainDeploy.t.sol, and both
are the environmental Base archive read — HTTP 403 ... Archive requests require a personal token from the allowed provider, before any assertion. They fail the
same way on unmodified main.

QA

  • Discriminating tests: testSuiteKeyAlphabetRefuses, testSeparatorKeyIsRefused,
    testEmptySuiteKeyReverts - each fails on base, verified two ways. (1) The base
    VALUES were pinned against unmodified main (feefc95) in a separate fresh
    clone: with the a,b / c fixture, externalAllSuites().length == 2,
    externalSuiteNames() == "a,b, c" and externalSuiteByName("b") reverting
    UnknownDeploymentSuite("b", "a,b, c") — a phantom key answered as valid — all
    green there; throwaway test deleted afterwards. (2) Base behaviour was then
    reproduced in place as mutant M12 below, which makes checkSuiteKey accept
    every key: all three tests fail under it. testSuiteKeyAlphabetAccepts is
    deliberately NOT in this list — it is the half that pins which keys the rule
    must not refuse, so it passes on base by construction and is here to stop the
    rule being narrowed into refusing the repo's own generated release keys.
  • Mutations applied: 12 mutants over src/abstract/RainDeploySuitesBase.sol, all
    KILLED (mutation-probe, baseline green at 16 passed).
    if (i == 0 || tagStart != 0) -> drop the i == 0 half (empty name admitted)
    -> testSuiteKeyAlphabetRefuses; same line -> drop the tagStart != 0 half
    (two at signs admitted) -> testSuiteKeyAlphabetRefuses;
    nameAlphabet -> hyphen dropped -> testEverySuiteIsSelectableByKey,
    testEmptySuiteKeyReverts, testSameLengthKeysSelectApart and two more;
    !(nameAlphabet || (tagStart != 0 && tagAlphabet)) -> !(nameAlphabet || tagAlphabet)
    (tag alphabet read over the whole key) -> testSuiteKeyAlphabetRefuses;
    if (tagStart == key.length) -> if (false) (empty-half guard removed) ->
    testEmptySuiteKeyReverts, testSuiteKeyAlphabetRefuses; same line ->
    if (key.length == 0) (narrowed to An unset DEPLOYMENT_SUITE can select and broadcast a suite, because the empty string is a declarable suite key #202's check, trailing at sign admitted) ->
    testSuiteKeyAlphabetRefuses; revert InvalidDeploySuiteKey(index, suite) ->
    (0, suite) on the alphabet path -> testSuiteKeyAlphabetRefuses, and on the
    empty-half path -> testEmptySuiteKeyReverts, testSuiteKeyAlphabetRefuses;
    if (char == "@") -> if (false) (at sign not recognised as the separator) ->
    testSuiteKeyAlphabetAccepts, testEverySuiteIsSelectableByKey and three more;
    checkSuiteKey(i, suites[i].suite) in allSuites() -> deleted ->
    testSeparatorKeyIsRefused, testEmptySuiteKeyReverts; same line -> guarded to
    i < released.length (released half only) -> testEmptySuiteKeyReverts; M12,
    checkSuiteKey body -> return; (base behaviour, every key accepted) ->
    testSeparatorKeyIsRefused, testEmptySuiteKeyReverts,
    testSuiteKeyAlphabetRefuses.
  • Oracle: UnknownDeploymentSuite's own NatSpec — the list exists so a caller who
    does NOT already know the valid keys is told them — which fixes the expected
    behaviour as "the rendering must be injective", independent of any
    implementation. The accepting half of the table is oracled by what the
    declarations in this repo and its consumers actually spell: the emission shape
    in LibRainDeploySnapshot (<candidate key> + at sign + tagForRecordPath,
    already held to X_Y_Z by isTag) and the 40 consumer keys in Migration above,
    neither derived from the check under test.
  • Category check: the issue asks for (A) the non-injective rendering, (B) the
    reader being told a different number of suites than exist, and (C) a phantom key
    handed back as valid. All three are one observable of one declaration and are
    covered together by testSeparatorKeyIsRefused, which asserts the refusal at
    externalAllSuites, externalSuiteNames, externalSuiteByName("c") (the key
    that IS declared) and externalSuiteByName("b") (the phantom the rendering
    invented) — the base values for all of which were pinned on main as above. The
    issue's own triage question, code or docs, is answered under "Code or docs".

🤖 Generated with Claude Code

https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN

`UnknownDeploymentSuite` carries the declared keys so a caller who does not
already know them is told what they are. `suiteNames()` joins them on ", " and
nothing constrained key contents, so the two suite registry keyed `a,b` and `c`
rendered `a,b, c` — the same list a three suite registry keyed `a`, `b` and `c`
reports. The reader was told a different number of suites exist than do and was
sent after `b`, which is declared nowhere.

`allSuites()` now refuses a key carrying any character of the separator, in the
same pass that refuses a duplicate, so every reader refuses it at once and no
lookup can answer with a list that reads back as a set the registry does not
hold. The join reads the separator from the constant the check does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
@coderabbitai

coderabbitai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 47 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: a85faf85-e1c2-4e4e-8993-c163a802f690

📥 Commits

Reviewing files that changed from the base of the PR and between ad3851a and a335031.

📒 Files selected for processing (12)
  • README.md
  • src/abstract/RainDeploySuitesBase.sol
  • test/abstract/ExampleDeploySuites.sol
  • test/abstract/ExternalDeploySuites.sol
  • test/concrete/EmptyKeyDeploySuites.sol
  • test/concrete/NoCandidateDeploySuites.sol
  • test/concrete/SeparatorKeyDeploySuites.sol
  • test/src/abstract/RainDeployBroadcast.t.sol
  • test/src/abstract/RainDeploySuitesBase.t.sol
  • test/src/abstract/RainDeployVerifyChain.t.sol
  • test/src/abstract/RainDeployVerifyChainCandidate.t.sol
  • test/src/abstract/RainDeployVerifySnapshotBase.t.sol

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

baku-ccron and others added 3 commits September 15, 2026 20:01
# Conflicts:
#	README.md
#	src/abstract/RainDeploySuitesBase.sol
#	test/src/abstract/RainDeploySuitesBase.t.sol
`suiteNames()` joins the declared keys on `", "` and nothing constrained what a
key may contain, so `UnknownDeploymentSuite` rendered a two suite registry keyed
`a,b` and `c` as `a,b, c` — indistinguishable from a three suite registry keyed
`a`, `b` and `c`, and `b` is declared nowhere. The error exists so a caller who
does not already know the valid keys is told them; a list that cannot be read
back as the set it names is the hardcoded string the registry replaces.

Keys are now held to a name of lowercase letters and hyphens, optionally an at
sign and a tag of those plus digits and underscores, neither half empty and at
most one at sign. That is what a repo declares by hand and what
`LibRainDeploySnapshot` generates, so no declaration needs an exemption.

The empty key falls to the same rule rather than to a second one beside it:
`EmptyDeploySuiteKey(uint256 index)` from #202 becomes
`InvalidDeploySuiteKey(uint256 index, string suite)`, keeping the index and
gaining the key every refusal that is not the empty one needs.

Closes #203.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
@thedavidmeister thedavidmeister changed the title Refuse a deploy suite key that spells the key list separator Hold a deploy suite key to an alphabet the key list reads back as Sep 15, 2026
baku-ccron and others added 2 commits September 15, 2026 22:42
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
c3bef6b cut the whole of `InvalidDeploySuiteKey`'s NatSpec down to a one
sentence summary of the alphabet. What went with it, off a published abstract
that deriving repos read: that a key carries at most one at sign and digits and
underscores only after it; that the released keys `LibRainDeploySnapshot`
generates satisfy the rule without an exemption; WHY an alphabet exists at all,
which is that `suiteNames()` joins the keys on `", "` and a list that reads
back as a different set is the hardcoded string this registry replaces; that
the empty key is refused by the same rule because it is what
`RainDeployBroadcast.run()` substitutes for an absent `DEPLOYMENT_SUITE`, where
`CREATE2` under a zero salt makes the mistake permanent; and why the rule is
held on the declaration rather than beside the substitution.

The tests lost the same arguments where they are pinned: why there is no length
floor, why the index is asserted with every refusal, and what the `a,b` fixture
demonstrates — that `externalSuiteByName("b")` asks after a key the rendering
invents, which is not a typo.

Left cut: the `@param` lines on the test wrapper, the inline comment that spells
the two array indices below it back, and the `SeparatorKeyDeploySuites` title
block, whose argument now stands once on the test that makes it rather than
twice. `checkSuiteKey` keeps c3bef6b's shorter form of the empty-half comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ViHcKLVk2YoS2joH4HdN
@thedavidmeister

Copy link
Copy Markdown
Contributor Author

rainix / static / static is pre-existing, not this PR

Reproduced on a clean main. Tip feefc95, untouched working tree, in the shell CI pins:

$ nix develop github:rainlanguage/rainix/8657b83b68f41957ab85da91132c3f652c1f32c0#sol-shell -c forge lint -D warnings
Error: Compiler run failed:
Warning (2018): Function state mutability can be restricted to pure
   --> test/src/lib/LibRainDeploySnapshot.t.sol:667:5
Warning (2018): Function state mutability can be restricted to pure
   --> test/src/lib/LibRainDeploySnapshot.t.sol:679:5

These are solc diagnostics, not forge-lint rules. -D is a compiler flag, so
forge lint -D warnings denies solc's own warnings and aborts at compile before
reaching a single lint rule.

Why it reads as new. main's last run (35015487248, feefc95,
2026-09-15T19:45:08Z) is green and its static job has no forge lint step at
all — it ran soldeer install, slither ., forge fmt --check,
rainix-sol-single-contract. The step arrived upstream in
rainlanguage/rainix@55c8198e ("Gate every sol consumer on forge lint and the
pre-commit hook bundle") at 2026-09-15T20:59:35Z, 74 minutes after that run.
.github/workflows/rainix.yaml consumes rainix-sol.yaml@main, floating, so
every push from then on picks the gate up. main has not been pushed since, so
main is stale-green over red code.

This branch. merge-base feefc95f5f9bbf7bf14e179461bfc4aa7869f71e. LibRainDeploySnapshot.t.sol is not in this PR's diff at all; the two functions sit at 667 and 679 on both sides, byte-identical. Both functions came in with
a201ec8 (2026-09-14), which is on main.

Clearing the two warnings will not be enough. With both flipped to pure the
compile succeeds and forge lint reports four findings CI has never printed,
every one on code no restore branch touches:

  • missing-zero-check x2 — test/concrete/MockChainDependentOwner.sol:26, both constructor address params
  • boolean-cst — the trailing : false in the semver precedes ternary, test/src/lib/LibRainDeploySnapshot.t.sol:1601
  • block-timestamp — test/src/concrete/MigrationRegistryApplyMigration.t.sol:924

Error: aborting due to 4 linter warning(s). Behind forge lint, the same
upstream commit added a pre-commit run --all-files step that has never executed
on this repo because lint fails first, so what that step does here is unknown.

All six restore branches (#219, #226, #228, #229, #232, #234) fail identically on
code none of them touches. The fix belongs on main once, not six times.

@thedavidmeister
thedavidmeister merged commit d7ac706 into main Sep 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The valid-key list in UnknownDeploymentSuite cannot be read back as the set of keys it reports

1 participant