Skip to content

Org-wide repo refactor: split deploy from library, then absorb interfaces into cores #296

Description

@thedavidmeister

Ask

Track the org-wide repository refactor to completion. Two phases, in this order:

  1. Deploy-repo / library-repo split — every repo is strictly one or the other, never both.
  2. Interface absorption — once a core repo is a pure library, fold its *.interface repo back into it.

Phase 1 for a given core repo is a hard prerequisite of phase 2 for that repo. See the phase 2 section for why the order cannot be reversed.

Phase 1: the split

  • Library repo — autopublishes to Soldeer on merge via rainix-autopublish (next-version lifecycle). No concrete deployables, no deploy pins, no frozen snapshots.
  • Deploy repo — releases only on a manual sol-v* tag via rainix-tag-release. Carries the concrete contracts, the deployed address/codehash pins, and the frozen per-tag src/generated/<tag>/ snapshots.

rain.factory was the practise run (rainlanguage/rain.factory#46). This issue is the org-wide rollout it was a rehearsal for.

Why: the two lifecycles are mutually exclusive

A library bumps [package].version on every content change. A deploy record's tag may only move when a real deploy happens. Hold both in one repo and the next-version bump on every merge desyncs [package].version from the frozen deploy tag — permanently-red drift.

This is no longer theoretical. Measured across the org on 2026-08-13:

Release lanes currently wedged ([package].version equals the published revision, so rainix-autopublish's next-version invariant rejects every run):

repo slot = published deploy artifacts / concrete
raindex 0.1.13 78 / 8
rainlang 0.1.8 9 / 7
rain.math.float 0.1.7 3 / 1
rain.flare 0.1.2 2 / 1
rain.pyth 0.1.0 2 / 1
rain.vats 0.2.0 0 / 15

Every repo carrying neither a deploy artifact nor a concrete contract publishes cleanly — five consecutive successes each: rain.solmem, rain.string, rain.lib.hash, rain.lib.memkv, rain.lib.typecast, rain.math.fixedpoint, rain.math.binary, rain.math.saturating, rain.sol.binmaskflag, rain.intorastring, rain.datacontract, rainlang.interface, raindex.interface.

Two honest qualifiers:

  • Carrying both halves does not guarantee a wedge. rain.extrospection has 1 deploy artifact and 1 concrete contract and publishes fine. The conflict creates the possibility; a half-completed publish realises it.
  • rain.vats is wedged with zero deploy artifacts (15 concrete contracts, no Deploy.sol, no src/generated/). Different shape — needs its own diagnosis, not a split.

Not the same problem, don't conflate

  • startup_failure class — diagnosed, unrelated to the split. rain.flare, rain.tofu.erc20-decimals and rain.verify.interface never create a single job. rainix-autopublish's release job declares permissions: {contents: write, id-token: write}, a called workflow may only narrow the token it is handed, and these repos have default_workflow_permissions: read with no caller permissions: block — so GitHub rejects the run before creating any job:

    The nested job 'release' is requesting 'contents: write, id-token: write',
    but is only allowed 'contents: read, id-token: none'.
    

    Exception-free correlation across all 30 org repos calling rainix-autopublish: 3/3 with read fail at startup, 27/27 with write start normally. Fix is an explicit permissions: block on the calling job — fix(ci): grant Package Release the token permissions its callee needs rain.flare#206 and fix(ci): grant Package Release the token permissions its callee needs rain.tofu.erc20-decimals#28. rain.verify.interface still needs the same one-line fix — Package Release never starts: caller declares no permissions block rain.verify.interface#14. Systemic follow-up: rainix#297.

    Systemic fragility worth fixing generally: none of the 30 consumers declares a caller permissions: block, so the 27 that work do so only by accident of a permissive repo setting. Flipping any repo to read breaks its release lane instantly, with a failure mode that produces no logs, no jobs and no annotations.

  • Silent missed releases — rain.metadata has published 0.1.0 against a 0.1.3 slot: three releases that did not happen.

  • rainix#273 — decoupling snapshot-cutting and deploying from publish. Mechanism-level, and complementary to this; the split does not remove the need for it.

State

Done:

  • rainix#280 — rainix-tag-release (tag-triggered deploy-repo release) merged, workflow live
  • rain.factory.deploy created with the deploy half
  • rain.factory#47 — library slimmed to interfaces-only, merged 2026-08-13 (8a5c603a)

rain.factory, remaining:

  • rain.factory.deploy#5 (rolling-candidate snapshot model) — green, ready. Must land before the first tag: it changes what the tag does
  • rain.factory.deploy#6 (inherited audit provenance) — green, ready, independent
  • rain.factory.deploy#4 — close as superseded by debug deploy #5 (conflicts textually and semantically; debug deploy #5 makes DEPLOY_TAG the constant "candidate" while 2024 01 09 tauri #4 asserts DEPLOY_TAG == [package].version)
  • rain.factory.deploy#7 — close as superseded by Address registry: interface, concrete, reader lib and post-deploy cross-network verification rain.deploy#26
  • rain.factory#51 — re-scope: ICloneableFactoryV4 stays in rain.factory; the open-salt implementation, its snapshot and tests belong in rain.factory.deploy
  • HUMAN — fix the fork-RPC secrets, dispatch the on-chain deploy, then cut a fresh sol-v* tag. rain-factory-deploy has never published; sol-v0.1.6 is spent (points five commits behind main, and its run died on the <NETWORK>_RPC_URL naming mismatch since fixed upstream). Version 0.1.6 is still free on the registry
  • Consumer migration — scope is S01-Issuer/st0x.deploy alone, two files. rainix is not a consumer: the LibCloneFactoryDeploy hits in rainix-static/src/frozen_snapshots.rs are unit-test fixture strings

Next candidates, factory-shaped (one concrete contract, 1–3 deploy artifacts) and independent of each other:

Then the large ones, which should follow the small proofs rather than lead them:

Separately:

Phase 2: interface absorption

With deployment out and everything on Soldeer rather than submodules, the separate *.interface repos stop earning their keep. Fold each back into its core repo.

Why

Absorption shortens the republish cascade. A leaf-library bump today needs rain-interpreter-interface republished, then rainlang, before any leaf repo can move — this is exactly what blocked the rain-math-binary 0.1.1 → 0.1.3 campaign on 2026-08-13, parking six repos behind two lockstep republishes. Merged, that is one republish, and every future leaf bump gets one link shorter.

Compile cost of a fatter package is near zero: Foundry compiles only what src/test/script transitively import, so unreached source is never parsed. Verified in that same campaign — rain.verify bumped green while siblings failed, precisely because it never reaches the offending file.

Cost, stated honestly

Under the versioned-import convention, absorption couples interface-only consumers to the implementation's release cadence: they re-pin and rewrite import prefixes on every implementation release, not just on interface changes. Interfaces are stable and implementations churn, so this is a real cost. It lands on four repos — raindex.governance, raindex.interface, rain.vats, rain.vats.flare — rather than org-wide.

Ordering is a hard dependency, not a preference

The interface split is currently protecting the interfaces from their cores' wedged lanes: rainlang.interface publishes cleanly precisely because it carries no deploy half, while rainlang is wedged at 0.1.8 and raindex at 0.1.13. Folding an interface into a wedged core propagates the failure instead of escaping it. For each repo: complete phase 1, confirm the lane publishes, then absorb.

Related

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions