refactor(routing): introduce authoritative provider routing plan - #229
Open
GionaGranchelli wants to merge 6 commits into
Open
refactor(routing): introduce authoritative provider routing plan#229GionaGranchelli wants to merge 6 commits into
GionaGranchelli wants to merge 6 commits into
Conversation
Introduce ProviderRoutingPlan as the single immutable snapshot of configured provider routing (providers, routes, defaultProvider) with typed ProviderId/ ModelId value classes and fail-fast build-time validation. Duplicate provider registration now fails with ConfigurationException instead of silently replacing the earlier registration; blank IDs, unknown primary/fallback providers, duplicate fallback routes, and unknown defaults are rejected at construction. ProviderRegistry remains a public compatibility facade over the plan with unchanged API and resolution order (ProviderRoute/ResolvedProviderRoute JVM shapes unchanged). The engine freezes the plan into EngineComponents. ProviderComponents; standalone composes through the plan builder; SovereignTramai.Builder deletes its shadow routing state and validates the shared plan via SovereignRoutingValidationPolicy; Spring resolves property-provider vs bean precedence into one unique provider set before the plan builder (explicit beans still override property-backed providers; genuine duplicate user beans fail deterministically). Routing-related sovereign evidence and artifact-verification targets derive from the same frozen plan. Epic 2.2 complete.
There was a problem hiding this comment.
Pull request overview
This PR implements Epic 2.2 by introducing ProviderRoutingPlan (in tramai-core) as the single immutable, authoritative representation of provider routing, and refactors standalone composition, engine execution, sovereign validation, and Spring auto-configuration to derive routing behavior from that frozen plan (with stricter fail-fast validation via ConfigurationException).
Changes:
- Added
ProviderRoutingPlanwith typed IDs (ProviderId/ModelId) and build-time validation, and rewroteProviderRegistryas a compatibility facade over the plan. - Updated engine, standalone, sovereign, and Spring wiring to freeze/consume the plan as the single routing source of truth.
- Added/updated tests and docs to reflect the new validation and routing-plan model.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tramai-standalone/src/test/kotlin/dev/tramai/standalone/TramaiComponentCompositionTest.kt | Adds build-time failure assertions for invalid routing; aligns provider IDs and model mapping in composition tests. |
| tramai-standalone/src/main/kotlin/dev/tramai/standalone/Tramai.kt | Switches standalone builder to build a ProviderRoutingPlan, then wraps it via ProviderRegistry.from(...). |
| tramai-spring/src/test/kotlin/dev/tramai/spring/TramaiAutoConfigurationTest.kt | Adds Spring tests for provider override precedence and fail-fast duplicate/invalid fallback routing. |
| tramai-spring/src/main/kotlin/dev/tramai/spring/TramaiAutoConfiguration.kt | Merges property-backed providers and bean providers before registering into the canonical plan builder. |
| tramai-sovereign/src/test/kotlin/dev/tramai/sovereign/SovereignTramaiTest.kt | Updates sovereign tests to expect ConfigurationException for invalid routing configs. |
| tramai-sovereign/src/test/kotlin/dev/tramai/sovereign/SovereignRoutingValidationPolicyTest.kt | New tests ensuring sovereign validation reads providers/routes from the authoritative plan. |
| tramai-sovereign/src/main/kotlin/dev/tramai/sovereign/SovereignTramai.kt | Removes sovereign shadow routing state; validates and derives verification targets from the plan. |
| tramai-sovereign/src/main/kotlin/dev/tramai/sovereign/SovereignRoutingValidationPolicy.kt | Introduces plan-based sovereign routing validation and verificationTargets() extension. |
| tramai-engine/src/test/kotlin/dev/tramai/engine/EngineComponentsTest.kt | Adds assertions that engine freezes and preserves routing plan ordering and explicit provider resolution. |
| tramai-engine/src/main/kotlin/dev/tramai/engine/TramaiEngine.kt | Switches invocation routing from ProviderRegistry to ProviderRoutingPlan.resolveCandidates(...). |
| tramai-engine/src/main/kotlin/dev/tramai/engine/components/EngineComponents.kt | Changes ProviderComponents to carry the frozen ProviderRoutingPlan. |
| tramai-engine/src/main/kotlin/dev/tramai/engine/components/EngineComponentFactory.kt | Freezes the routing plan into ProviderComponents at component construction. |
| tramai-core/src/test/kotlin/dev/tramai/core/provider/ProviderRoutingPlanTest.kt | New unit tests for plan ordering, validation, immutability snapshot behavior, and identity preservation. |
| tramai-core/src/test/kotlin/dev/tramai/core/provider/ProviderRegistryCompatibilityTest.kt | New tests proving legacy ProviderRegistry API preserves routing semantics while backed by the plan. |
| tramai-core/src/main/kotlin/dev/tramai/core/provider/ProviderRoutingPlan.kt | New authoritative routing plan model + resolution helpers. |
| tramai-core/src/main/kotlin/dev/tramai/core/provider/ProviderRegistry.kt | Rewritten as compatibility facade over ProviderRoutingPlan; exposes routingPlan. |
| tramai-core/api/tramai-core.api | API dump updates for new routing plan types and ProviderRegistry.from(...) / routingPlan. |
| docs/ROADMAP-0.6.0.md | Marks Epic 2.2 complete and documents the implemented routing plan model. |
| docs/modules/tramai-spring.md | Updates Spring module flow to describe provider merge/override behavior before plan build. |
| docs/modules/tramai-sovereign.md | Updates sovereign build-time validation description to validate the immutable plan. |
| CHANGELOG.md | Adds changelog entry describing authoritative routing plan and validation behavior changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+56
to
+58
| val requestedModelId = ModelId(requestedModelName) | ||
| routes[requestedModelId] = routes.getOrPut(requestedModelId) { emptyList() } + | ||
| PlannedProviderRoute(ProviderId(providerName), ModelId(fallbackModelName)) |
Comment on lines
+108
to
+112
| private fun validateModelId(modelId: ModelId) { | ||
| if (modelId.value.isBlank()) throw ConfigurationException("Model name must not be blank") | ||
| } | ||
| } | ||
| } |
Comment on lines
+184
to
+188
| val providersById = propertyProviders.toMap() + uniqueBeanProviders.associate { it.providerId() to it } | ||
| providersById.forEach { (providerId, provider) -> | ||
| builder.provider(provider, name = providerId) | ||
| } | ||
| duplicateBeanProviders.forEach { provider -> builder.provider(provider, name = provider.providerId()) } |
Comment on lines
+379
to
+381
| @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") | ||
| val plan = tramai.providerRegistry.routingPlan | ||
| SovereignRoutingValidationPolicy.validate(plan, profile) |
…ity, ABI bridge - ProviderRoutingPlan: unmodifiableMap/unmodifiableList defensive copies; explicit primary+fallback split (fallback-only rejected, duplicate primary rejected, fallback==primary rejected, order-independent fallback registration); model whitespace validation mirrors provider validation. - ProviderRegistry: legacy (Map,Map,String) primary ctor preserves the 0.5.0 synthetic DefaultConstructorMarker descriptor; plan-backed secondary restores the exact frozen plan instance (identity, not a reconstructed copy). - Tramai.Builder.buildRoutingPlan() freezes the authoritative plan once; build() reuses the same instance. providerRegistry back to private. - SovereignTramai: validates the frozen plan BEFORE constructing the runtime (no abandoned instance); @Suppress INVISIBLE_MEMBER reach-through removed. - SovereignRoutingValidationPolicy: primary effective model must be allowed. - Spring: property-vs-property duplicates no longer collapse; they reach the plan builder and fail deterministically. Bean-over-property precedence intact. - Tests: immutability mutation, fallback-only, duplicate primary, fallback==primary, whitespace, fallback-before-primary, Spring collision, sovereign rejection.
…anonical baseline The DefaultConstructorMarker descriptor for ProviderRegistry's private constructor is non-contractual (marker cannot be instantiated by consumers; binary-compat fixture does not exercise the class). The bridge introduced a mutable plan field flagged as global state; removing it eliminates the scanner findings and the maintenance cost. - ProviderRegistry: single plan-backed private ctor; no legacy bridge. - config/quality/0.6.0-baseline.json: restored from origin/master (immutable v0.5.0 canonical measurement; earlier session had wrongly regenerated it). - maintainability-deviations.yml MQ-0004: allowed 3->4 for the two new ProviderRoutingPlan builder mutable collections (requestedModelId, seenFallbacks), resolved in the 0.7.0 registry migration.
…talog, module-dependency-graph) from PR #229 These files were rewritten by the earlier session's generateMaintainabilityBaseline run, not by the routing work. They drift from the canonical origin/master copies.
…ation P2: Tramai.Builder cached its first builtRoutingPlan forever; routing mutations after the first build()/buildRoutingPlan() were silently ignored by later builds. Every routing mutator now calls invalidateRoutingPlan() so a reusable builder sees new routing state while already-built runtimes keep their immutable plan. Sovereign validation-failure retry no longer validates a stale cached plan. P3: MQ-0004 rationale now describes the actual builder/validation mutable collections (not local variables); roadmap API wording changed to 'existing public API preserved; additive routing-plan APIs introduced'; journal dropped the volatile head commit; SovereignTramai.Builder.provider() KDoc reflects build()-time ConfigurationException validation. Tests: standalone builder A->mutate->B sees new routing; sovereign validation failure -> add missing route -> retry succeeds.
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
Implements Epic 2.2: there is now exactly one immutable representation of configured provider routing —
ProviderRoutingPlanintramai-core. Every execution, validation, framework-composition, sovereign-restriction, and routing-evidence path works from that plan rather than maintaining its own copy.Change class: runtime-behaviour. Intentional behaviour changes: duplicate providers and structurally invalid routing configurations that were previously accepted now fail fast at construction with
ConfigurationException.What
tramai-core
ProviderRoutingPlan(new): immutable snapshot ofproviders,routes,defaultProviderwith typed@JvmInlineProviderId/ModelIdvalue classes. Builder enforces fail-fast validation: blank/whitespace IDs (providers and models), duplicate provider IDs (no more silent replacement), unknown primary/fallback providers, duplicate primaries, duplicate identical fallback routes, fallback-identical-to-primary, fallback-without-primary, unknown default provider, degenerate route structures. Defensive copies wrapped inCollections.unmodifiableMap/unmodifiableList— mutation after build throws.build(), so fallback-before-primary registration order is preserved (the old index-0 inference could drop a fallback registered before its primary).ProviderRegistry(rewritten): compatibility façade over the plan. Public API unchanged (builder(),singleProvider,provider,model,fallbackModel,fallbackProvider,defaultProvider,resolve,resolveCandidates). No duplicate backing maps — state exists only in the plan.ProviderRoute/ResolvedProviderRouteJVM shapes unchanged.fallbackProvider(model, provider)deliberately keeps the same effective model on another provider — not classified as a self-loop. No recursive fallback routing introduced.tramai-engine
EngineComponents.ProviderComponentsnow freezesProviderRoutingPlan(not a second registry representation).TramaiInvocationHandlerresolves candidates from the plan. PublishedTramaiEngine(providerRegistry=...)constructors untouched.tramai-standalone
Tramai.Builder.buildRoutingPlan()freezes the authoritative plan once;build()reuses the same instance (identity, not a reconstructed copy). Post-build builder mutations have no effect on the built runtime.providerRegistryback to private.tramai-sovereign
registeredProviders,primaryModelRoutes,fallbackRoutes,defaultProviderName,FallbackRoute.SovereignRoutingValidationPolicyvalidates the same frozen plan (allowedProviders, providerZones, allowedModels incl. primary effective models, allowedFallbackProviders, default-provider checks, offline-mode LOCAL constraints). Validation runs before runtime construction — an invalid routing config fails at build without leaving a partially-built instance. Artifact-verification targets derive from the plan. No@Suppress("INVISIBLE_MEMBER")reach-through.tramai-spring
ModelProviderbeans merge into one unique set (LinkedHashMap, unique bean overrides same-id property provider, deterministic order). Property-vs-property duplicates and genuine duplicate user beans pass through to the plan builder and fail deterministically. No Spring-side route validator.Docs
docs/ROADMAP-0.6.0.md(Epic 2.2 ✅ Complete),CHANGELOG.md,docs/modules/tramai-spring.md,docs/modules/tramai-sovereign.md.Fix Round 1 (review findings addressed)
Collections.unmodifiableMap/unmodifiableList; regression test assertsUnsupportedOperationExceptiononas MutableMap/as MutableListmutationDuplicate primary route for model 'X')duplicates its primary route)Duplicate provider 'openai'; test addedvalidateModelIdmirrors provider validation (value == value.trim())@SuppressTramai.Builder.buildRoutingPlan()public freeze; sovereign validates same instance before runtime build; suppression removedABI note (reviewed, deliberate)
The api dump no longer records the pre-0.6.0 synthetic
(Map, Map, String, DefaultConstructorMarker)constructor ofProviderRegistry. That descriptor is aDefaultConstructorMarkermarker for a private constructor —DefaultConstructorMarkeritself cannot be instantiated by consumers, and the committed binary-compatibility fixture does not exerciseProviderRegistry(verified: the fixture jar only touchesBinaryCompatFixtureKtandStructuredOutputBinaryCompatFixture). The public surface — companion factories, builder,resolve/resolveCandidates, and both DTO shapes — is byte-compatible with 0.5.0. The alternative (an ABI bridge ctor) introduced a mutable field flagged as global state by the maintainability scanner; dropping the bridge removes both the scanner findings and the maintenance cost. The api dump diff is otherwise additive-only.Verification
./gradlew :tramai-core:test :tramai-engine:test :tramai-standalone:test :tramai-sovereign:test :tramai-spring:test --rerun-tasks— all green, 0 failures./gradlew apiCheck(all tramai modules) — PASSED. Note:examples:governed-workflow:apiCheckfails on a pre-existing master drift (buildGovernedNetworkPolicyWorkflowin source, never dumped); this PR touches zero example files./gradlew verifyPr -PchangeClass=runtime-behaviour— PASSED (268 tasks: maintainability baseline, change policy, build-logic tests, all subproject tests)./gradlew verifyCancellationSafety— PASSED (no new findings)Fix Round 2 (review findings addressed)
Tramai.Builderpermanently cached the first routing plan; routing mutations after the firstbuild()/buildRoutingPlan()were silently ignored by later builds (and sovereign retry validated a stale plan)provider,model,fallbackModel,fallbackProvider,defaultProvider) now callsinvalidateRoutingPlan(). Already-built runtimes keep their immutable plan; the next build sees the new routing state. Sovereign validates and installs the same frozen instance because no mutation occurs between freeze and buildSovereignTramai.Builder.provider()KDoc now documents build-timeConfigurationExceptionRegression tests added: standalone build A → mutate routing → build B (A stays frozen, B sees mutation, call counts 1/1); sovereign validation failure → add missing route → retry succeeds.
Non-claims
ProviderRoute/ResolvedProviderRoutepublic JVM shapes.TramaiPropertiesschema or property semantics.config/quality/0.6.0-baseline.jsonis byte-identical to origin/master (the immutable v0.5.0 canonical measurement). MQ-0004 deviationallowedraised 3→4 for the two newProviderRoutingPlanbuilder mutable collections, resolved in the 0.7.0 registry migration.This PR needs review before merge.