Hide module engine plumbing from public API - #3835
Conversation
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Review
Solid, well-scoped refactor — IModule now exposes only ResultType/Configuration, the engine plumbing (ResultTask, TrySetDistributedResult) moved to an internal IInternalModule, and the dead ModuleRunType enum was replaced by a plain Configuration.AlwaysRun check everywhere it was used. I diffed every remaining reference to ResultTask/TrySetDistributedResult/ModuleRunType across the whole repo (not just the touched files) and confirmed the migration is complete — no stragglers in the untouched tool packages. The new ModuleApiSurfaceTests is a good regression guard for the public surface itself.
One architectural concern worth addressing before merge:
The public registration API still advertises a contract this change breaks
IModule is not just an internal marker — it's a real, reachable public extension point:
PipelineBuilderExtensions.AddModule<TModule>()is constrained towhere TModule : class, IModule(notModule<T>), src/ModularPipelines/Extensions/PipelineBuilderExtensions.cs:29.- Assembly auto-discovery (
ServiceCollectionExtensions.AddModulesFromAssembly) andModuleAutoRegistrar.IsValidModuleTypeboth register any concrete, non-abstract type that isIsAssignableTo(typeof(IModule))— again, notModule<T>specifically.
Before this PR, a type that implemented IModule directly (skipping Module<T>) had to provide real ResultTask/TrySetDistributedResult members itself, which the engine could call polymorphically through the IModule interface — awkward, but functional. After this PR, those members live only on internal interface IInternalModule, and every engine touchpoint (ModuleCacheResultRepository.cs:23, DistributedModuleExecutor.cs:49, WorkerModuleExecutor.cs:84, ModuleCompletionSourceApplicator.cs:72) reaches them via a hard (IInternalModule) module cast. Since IInternalModule is internal, no type outside this repo's InternalsVisibleTo list can ever implement it — so any consumer who (legally, per the public generic constraints) implements IModule directly instead of deriving from Module<T>/SyncModule<T> will now get an InvalidCastException the instant the engine touches their module, rather than a clear error. It's a real regression in that specific (if rare) path: previously-possible custom IModule implementations are now guaranteed to crash, and there's nothing in the public API or docs (docs/docs/how-to/defining-modules.md only documents Module<T>/SyncModule<T>) that signals this.
Two ways to close the gap, either is better than leaving a silent InvalidCastException landmine:
- Tighten the public contract to match reality. Change the generic constraints on
AddModule<TModule>(),DependsOnAttribute,IModuleRegistrationContext, etc. fromIModuletoModule<T>/a shared non-genericModuleBasethat itself implementsIInternalModule. This makes the compiler enforce what's already true at runtime, instead of the type system promising something the engine can't deliver. - If arbitrary
IModuleimplementations must stay supported, validate at registration time (AddModulesFromAssembly/ModuleAutoRegistrar/AddModule<TModule>()) that the type also satisfiesIInternalModule, and throw a descriptiveInvalidOperationException("modules must derive from Module") instead of deferring to a cryptic cast failure deep inside distributed/caching code.
Given Module<T>/SyncModule<T> are the only base types the docs ever mention, option 1 is probably the cleaner fix and is a natural extension of what this PR is already trying to do (make the real contract explicit).
Minor
(IInternalModule) x casts are duplicated across 5 files (ModuleCacheResultRepository, DistributedModuleExecutor, WorkerModuleExecutor, ModuleCompletionSourceApplicator, ModuleTester). Not blocking, but a small internal static IInternalModule AsInternal(this IModule module) extension would remove the repeated cast syntax and give you one place to add the guard from option 2 above if you go that route.
No prior reviewer comments to reconcile against (the earlier @codex review request hit a usage-limit error with no actual feedback).
|
Addressed the architectural review concern in c8472cf: direct IModule implementations now fail immediately with descriptive Module/SyncModule guidance across generic, instance, factory, runtime-type, assembly-scan, and auto-registration paths. Internal execution access also uses one guarded contract instead of raw casts. Validation: ModuleApiSurfaceTests 3/3, PipelineBuilderRegistrationTests 14/14, ModuleCompletionSourceApplicatorTests 2/2; core and ModularPipelines.Testing Release builds both clean with 0 warnings/errors. |
ReviewFollow-up on the earlier review comment: the architectural gap I flagged — direct What changed and why it closes the gap:
One small gap, not blocking: Everything else from the original review still holds — the Note: CodeFactor is currently reporting 7 issues on this PR (link in the checks tab) — worth a quick look before merge, though that's a separate lint pass and out of scope for this architectural review. |
|
Cleared CodeFactor’s seven SA1508 findings in d2f5c76; these were stray blank lines left in three direct-IModule test fakes after removing engine members. Targeted formatter passes; change is whitespace-only. |
Review (follow-up)Re-checked the PR after the latest commit (d2f5c76). Since my last review, that commit only removes stray blank lines in three test files (SA1508 fixes) — whitespace-only, confirmed via the commit diff, no code paths touched. The architectural gap I flagged earlier (direct Re-verified the full diff end-to-end (all 3 commits) with fresh eyes:
One architectural point worth a quick gut-check (not blocking): The non-blocking test-coverage suggestion from my last comment (assembly-scan path not exercised by Nothing else to add — the rest of the plumbing move (public API narrowing, |
|
Addressed the final review gut-check in f0def1c: bulk assembly scans are deliberately all-or-nothing for unsupported direct IModule implementations, the public scan APIs now document that exception contract, and the API regression exercises the scan path. Post-main validation: focused test 1/1; core and Testing Release solutions both 0 warnings/errors. |
There was a problem hiding this comment.
Review (re-check after latest commits)
Re-reviewed the full current state of the PR (through feb086e, the merge of main). This is a solid, well-scoped refactor: IModule now only exposes ResultType/Configuration, engine plumbing lives on internal IInternalModule, and ModuleRunType is gone in favor of Configuration.AlwaysRun everywhere.
Both previously-flagged issues are resolved:
- Silent
InvalidCastExceptionfor directIModuleimplementations (flagged in the first review pass) — fixed viaModuleExecutionContract(src/ModularPipelines/Modules/ModuleExecutionContract.cs).Validate(Type)now runs at every registration entry point I could find: all fourAddModuleoverloads and the factory/instance variants inServiceCollectionExtensions.cs,AddModulesFromAssembly's reflection-fallback loop, andModuleAutoRegistrar.IsValidModuleType. Every former raw(IModule)/(IInternalModule)cast is replaced by the singleAsInternal()extension (ModuleTester.cs,ModuleCacheResultRepository.cs,DistributedModuleExecutor.cs,WorkerModuleExecutor.cs,ModuleCompletionSourceApplicator.cs), so there's exactly one place that owns the guard now instead of five duplicated casts. Good fix — turns a deferred runtime crash into an immediate, descriptiveInvalidOperationExceptionat the point of registration. - Bulk assembly-scan blast radius (the "all-or-nothing" gut-check from the second review pass) — now explicitly documented via
<exception>XML doc tags onAddModulesFromAssembly/AddModulesFromAssemblyContainingTypein bothPipelineBuilderExtensions.csandModuleRegistration.cs, and exercised directly by the newModuleApiSurfaceTests.DirectIModuleImplementationsFailAtRegistrationWithGuidancetest, which asserts the guard fires from the generic, instance, factory, runtime-type, and assembly-scan registration paths, plusAsInternal()itself — six call sites in one test. That closes the "nice-to-have" test-coverage gap noted previously.
I also swept the whole repo (not just the touched files) for stray ResultTask/TrySetDistributedResult/ModuleRunType references outside this diff and found none — the migration is complete, and the only remaining ModuleRunType mentions are in versioned migration docs describing prior releases, which is correct as historical content.
Nothing further to add — this is ready from my side. Nice iteration across the review rounds.
|
@claude review |
Review (re-check after merge refresh)Re-checked at current head Since the PR's actual code is unchanged since my last pass, my prior assessment stands:
No new issues to flag. This is ready from my side. |
|
Required pipeline failed from core test timing/process instability; rerunning failed jobs once on the unchanged head. |
Closes #3793
Summary
IModuletoResultTypeandConfigurationIInternalModuleModuleRunTypechecks withModuleConfiguration.AlwaysRunand remove the dead enumValidation
ModularPipelines.slnxRelease build (0 warnings, 0 errors)ModularPipelines.Testing.slnxRelease build (0 warnings, 0 errors)