[PLAN-0061] Pre-1.0 Cleanup and Consolidation Gate¶
Status: Done
Implements: ADR-0041 Amendment 7, ADR-0033 Amendment 2
Goal¶
Close the concrete gaps found by the pre-1.0 repository-wide cleanup audit — a correctness bug in three framework integrations, unreachable docs-site navigation, a fragile CI package-list pattern, a permanent AOT regression gate, and zero sample coverage for three shipped packages — without manufacturing architectural work the audit didn't find evidence for. Done when: the three negative-seed guards are fixed and regression-tested; MSTest/NUnit are reachable from the docs nav; every publishable package has an obvious working sample; AOT smoke coverage runs permanently in CI with a precisely stated guarantee; the publishable-package list is declared once, not three times, via a mechanism that actually survives across separate CI steps; and full build/tests/package-validation/sample-validation/AOT-CI are green.
Revision note (2026-09-03): this plan originally also scoped landing ADR-0058 (generator-facing runtime hook EditorBrowsable policy) and its two companion ADR-0041/ADR-0055 amendments. Jonas Ha's PR #126 merged that entire slice directly to main (as ADR-0058, tracked to completion under its own PLAN-0060) before this plan was finalized. That work is removed from scope here — see "Revalidation against origin/main" below. This plan was originally drafted as PLAN-0060; it was renumbered to 0061 once main independently claimed 0060 for the now-superseded generator-hook-policy plan.
Revalidation against origin/main (2026-09-03)¶
Re-checked every audit finding below directly against main after merging PR #126 (commit 43a5e81):
- ADR-0058 generator-hook inventory /
EditorBrowsableannotations: fully landed andDone(PLAN-0060onmain). Removed from this plan's scope entirely — no remaining task. - Negative-seed guard state: unaffected.
Compono.XunitV3,Compono.MSTest,Compono.NUnit'sComposeAttribute{TProfile}.csall still reduce to a barebuilder.AddProfile<TProfile>()with no guard, confirmed by direct re-read post-merge. Still required. package-validation.yamlstructure: unaffected by PR #126. Re-inspected its actual step structure (not assumed) — the nuget.org-baseline lookup,pack_onecalls, and CS1591 enforcement are three separaterun:steps (lines ~47, ~82, ~111), each its own shell process. APACKAGES=(...)Bash array declared in one step does not survive to the next — this plan's original mechanism was wrong as written. Corrected below (Task 5).- AOT smoke-test projects / CI wiring: unaffected. Still eight
test/*.AotSmokeTestprojects, still zero references from any.github/workflows/*orscripts/*. Still required. - Sample structure / package coverage: unaffected.
samples/still has exactly the same three projects (Compono.Samples.AspNetApi,.AspNetApi.Tests,.BasicUsage);Compono.Http/Compono.DependencyInjection/Compono.Loggingstill have zero example-level coverage. Still required. mkdocs.ymlnavigation: unaffected. "Package Guides" nav still lists 9 of 11 packages, still missingCompono.MSTest/Compono.NUnit. Still required.- Generator helper duplication:
GeneratorVersionduplication across the five emitters is unaffected.StableHashduplication is now worse than originally audited — PR #126 did not introduce this, but a direct re-check found a third copy beyond the two the original audit found:TestDoubleOverloadIdentity.StableHash(src/Compono.Generators/Emitters/TestDoubleOverloadIdentity.cs:166), in addition toGeneratedFileNaming.StableHashandTestDoubleIdentifierNaming.StableHash. All three copies' own comments already cross-reference each other by name as "the same FNV-1a algorithm," so this was already self-documented drift risk, not a new finding — Task 6 below is corrected to cover three files, not two. docs.ymlstale package count: unaffected. Line ~106 still says "eight publishable packages" against a loop that builds 11. Still required.Compono.NSubstituteAOT-limitation doc gap: unaffected. Still required.
No other audit finding, ADR inventory, or assumption changed. This plan does not expand scope to cover anything from PR #126 beyond removing the work it already completed.
Scope¶
In scope, per the accepted audit findings and the product-owner scoping exchange that followed it:
- Negative-seed guard fix in
Compono.XunitV3/Compono.MSTest/Compono.NUnit - regression tests, using
Compono.TUnit's already-correct behavior as the contract. mkdocs.ymlnavigation fix forCompono.MSTest/Compono.NUnit(package guide + API reference).docs.yml's stale "eight publishable packages" text.Compono.NSubstitute's documented AOT limitation (ADR-0024) surfaced in its own package guide.- Publishable-package-list consolidation in
package-validation.yamlto a single authoritative declaration, using a mechanism that actually persists across that workflow's separaterun:steps (see Task 5). StableHash(three copies) /GeneratorVersion(five copies) generator-helper deduplication (validated by the audit as byte-for-byte identical, internal-only, zero AOT/annotation difference).- Permanent, CI-blocking AOT smoke validation per ADR-0041 Amendment 7, with conservative path-filtered triggering and a precisely scoped guarantee.
- Canonical sample coverage extension for
Compono.Http,Compono.DependencyInjection,Compono.Loggingper ADR-0033 Amendment 2. Compono.XunitV3.SampleTests' undocumented CI/filter requirement and its three staleRealRunnerTests.cscomment references (added 2026-09-03 during the audit-to-plan traceability reconciliation — see Phase 1 Task below; documentation/comment correctness only, not a rename or sample-architecture change).- The duplicated
"Compono.ComposableAttribute"metadata-name literal (WellKnownTypeData.cs/ComposableAttributeDiscovery.cs, added 2026-09-03 during the same reconciliation) — folded into the existing generator-helper consolidation task. TrackingNames' doc-comment/test-coverage gap (added 2026-09-03 during the same reconciliation) — see Phase 1 Task below; ADR-0005 actually requires this coverage (see "TrackingNames disposition" below), so this is a real missing-test gap, not a stale comment to merely correct.
Explicitly deferred (see the audit report for evidence):
- Framework-binder duplication across the four
[Compose]integration packages — moved to RESEARCH-0019, runs independently, non-blocking for 1.0. The research finding "keep the duplication" is a fully valid outcome, not merely a placeholder pending eventual consolidation. TestDoubleAnalyzer.Analyzedecomposition,CompositionBuilderExtensionssame-name-four-assemblies pattern,*SampleTestsrename,WellKnownTypesfive-class split,dogfood-validate.sh's cosmetic success-message text, missing AotSmokeTest coverage forCompono.NSubstitute(already explained by ADR-0024)/Compono.Bogus/Compono.DependencyInjection— all Tier 3/low-priority findings with no evidence of drift risk or correctness impact.- Four near-identical per-framework generator-registration blocks in
ComponoIncrementalGenerator.Initialize(added 2026-09-03 during the audit-to-plan traceability reconciliation). Explicitly deferred, not folded into theStableHash/GeneratorVersionhelper consolidation: the duplication here is structural (each block wires a distinct framework integration's own discovery path into the pipeline), not a duplicated constant or a byte-for-byte helper function — consolidating it would mean introducing an abstraction over generator registration/lifecycle behavior itself, a materially larger and riskier change than centralizing one semantic operation. No concrete drift bug or maintenance failure has ever been tied to these four blocks, and four visually-similar blocks reducing to one abstraction is not, by itself, evidence of better maintainability. Revisit if a real cross-framework registration drift bug appears (e.g. a fix applied to one framework's block and missed in another's). - ADR-0058's generator-hook
EditorBrowsablepolicy and its landing onmain— alreadyDoneviaPLAN-0060, no longer this plan's concern.
TrackingNames disposition (resolved 2026-09-03)¶
Evidence checked before adding this to Phase 1, per the instruction not to manufacture tests from an unverified comment alone: ADR-0005 itself requires .WithTrackingName(...) on every named incremental pipeline stage "so incrementality (cache-hit behavior) can be asserted in tests later." PLAN-0001 (line ~415) independently recorded, at the time tracking names were first added: "no incremental-caching test exists yet to consume them (that's still open work, not done in this pass)." Disposition: A — this is an actual intended invariant with a real, long-standing missing-test gap, not a stale or aspirational comment. Phase 1 adds the smallest test that proves the promised invariant, rather than correcting the comment.
Phases¶
Phase 1 — Product correctness and repository quality gate¶
Status: Done
Ships as its own PR.
- Add the negative-seed guard + try/catch (matching
Compono.TUnit'sComposeAttribute{TProfile}.cs) tosrc/Compono.XunitV3/ComposeAttribute{TProfile}.cs,src/Compono.MSTest/ComposeAttribute{TProfile}.cs,src/Compono.NUnit/ComposeAttribute{TProfile}.cs. - Port
Compono.TUnit.Tests/SeedObservabilityTests.cs's negative-seed test intoCompono.XunitV3.Tests,Compono.MSTest.Tests,Compono.NUnit.Testsso this gap can't silently recur. -
mkdocs.yml: addCompono.MSTest/Compono.NUnitrows under both "Package Guides" and "Reference → API Reference". -
docs/packages/compono-nsubstitute.md: add a concise paragraph stating the existing Native AOT/trimming limitation (ADR-0024), pointing to that ADR. -
docs.yml: fix line ~106's stale "eight publishable packages" text to match the loop's actual 11-package count (or de-numericize it). -
Compono.XunitV3.SampleTestsCI/filter documentation + stale-comment correction (repository/documentation correctness, not sample- architecture redesign — no rename, no relocation): document, inside the project itself (aREADME.mdin its folder, or a prominent header comment directly onFailingCompositionTests.cs/FailingConfigProfileTests.cs), the actual--filter-not-class "Compono.XunitV3.SampleTests.Failing*"requirementpackage-validation.yamlalready applies, so a baredotnet testno longer reads as "this project is broken." Correct the three staleRealRunnerTests.csreferences inCompono.XunitV3.SampleTests.csproj's comments (that file was removed per PLAN-0004's own history). Confirm afterward that the project's classification as a packaged-consumer/real-runner validation fixture — not a user-facing canonical sample — is unambiguous from its own README/comments to a new reader. -
test/Compono.Generators.Tests: add the smallest incremental-caching regression test ADR-0005/PLAN-0001 left as open work — assert at least one representativeTrackingNames-tagged pipeline stage (e.g.ComposableTypesorComposeMethodsAll) reports a cache hit (IncrementalStepRunReason.Cached/Unchanged) viaGeneratorDriverRunResult.Results[0].TrackedSteps[...]on a second driver run after an unrelated, non-invalidating source edit. This proves the invariant theTrackingNamesdoc comment already promises; it does not attempt full incremental-caching coverage of every stage. -
.github/workflows/package-validation.yamlpackage-list consolidation, corrected mechanism: the nuget.org-baseline lookup,pack_onecalls, and CS1591 enforcement are three independentrun:steps — a shell-local Bash array cannot cross that boundary. Declare the 11-package list once as a job-levelenv:string (PACKAGES: "Compono Compono.XunitV3 Compono.NSubstitute Compono.Bogus Compono.TUnit Compono.TestDoubles Compono.DependencyInjection Compono.Http Compono.Logging Compono.MSTest Compono.NUnit", alongside the existingBREAKING_CHANGE/PACK_OUTPUTjob-levelenv:entries) — GitHub Actions injects job-levelenv:into every step's process environment automatically, so this survives across steps with no extra plumbing. Each of the three steps loopsfor pkg in $PACKAGES; do ... doneinstead of repeating the literal list. No generalized manifest file, no dynamic project discovery, no cross-job output — the list stays a single, locally-readable line in the same workflow file. - Consolidate
StableHash— now duplicated three times (src/Compono.Generators/Emitters/GeneratedFileNaming.cs,Emitters/TestDoubleIdentifierNaming.cs,Emitters/TestDoubleOverloadIdentity.cs, confirmed byte-for-byte identical FNV-1a implementations whose own comments already cross-reference each other) into one shared internal helper all three call. - Consolidate the
GeneratorVersionfallback-chain logic (currently duplicated acrossCompositionPlanEmitter,CollectionPlanEmitter,TestDoubleEmitter,LoggingActivationEmitter,RowInvokerRegistrationEmitter) into one shared internal helper parameterized by the calling type/assembly. - Consolidate the duplicated
"Compono.ComposableAttribute"metadata-name literal:WellKnownTypeData.cs:22andDiscovery/ComposableAttributeDiscovery.cs:22both declare the exact same string for the same semantic identity (the[Composable]attribute's metadata name), consumed via two genuinely different paths —ComposableAttributeDiscovery.AttributeMetadataNamealready feedsForAttributeWithMetadataNameinComponoIncrementalGenerator.cs:60, whileWellKnownTypeData.cs's copy feeds its own symbol-cache lookup — but both have the same reason to change (the attribute's fully-qualified name). Reuse the existingComposableAttributeDiscovery.AttributeMetadataNameconstant fromWellKnownTypeData.csinstead of its own literal; no new helper type, no metadata-name registry. - New
aot-validation.yamlworkflow: one reusable script + matrix job driving the existing eight*.AotSmokeTestprojects' established pack → local-feed → publish-p:PublishAot=true→ run pattern. Handle the three structural outliers (Compono.Logging.AotSmokeTest'sverify-packaging.sh,Compono.Http.AotSmokeTest'sAnalyzerContract/, coreCompono.AotSmokeTest's lack of a framework integration to call through) via a per-entry optional hook, not bespoke per-project jobs. - No
paths:filter on the workflow'spull_requesttrigger — the workflow always starts, so its required check always has something to report (a workflow skipped entirely by trigger-levelpaths:leaves a required checkPendingand blocks the PR, per GitHub's required-check semantics — confirmed as the reasondocs.yml's own trigger-levelpaths:pattern must not be copied here). - A first, inexpensive job computes which of the eight legs are applicable from the PR's changed files (a small repository-owned
git diff --name-only-based script — no third-party changed-files action), publishing the result as a job output a dynamic matrix or per-legif:conditions consume. - That job marks all eight legs applicable on any change to
src/Compono/**,src/Compono.Generators/**,Directory.Packages.props,Directory.Build.props/.targetsaffecting packed output, any*.AotSmokeTestproject, or theaot-validation.yamlworkflow/script itself. - That job marks only that package's leg (plus core's own
Compono.AotSmokeTestleg, already covered under the point above for any core-affecting change) applicable on a change scoped to one integration package's ownsrc/Compono.<X>/**. - Each leg's actual publish-and-run job runs behind an
if:condition reading that output — an inapplicable leg reports a normal skipped conclusion, never a missing status. - A final
if: always()aggregation job depends on all eight leg jobs and is the one job named in branch protection/ruleset as the required check — fails if any applicable leg failed or was cancelled, succeeds if every applicable leg passed or none was applicable, and fails closed if the applicability computation itself didn't succeed (checked first, before the legs' own result, so achangesfailure/ cancellation can never be mistaken for "correctly found nothing to run" — PR #128 review finding, fixed post-merge-of-Phase-1-review). - No
merge_grouptrigger is added — this repository does not use GitHub merge queues today; revisit only if that changes. - The workflow's job/step names and any package-guide text referencing it state the exact guarantee from ADR-0041 Amendment 7 — "the packaged Compono package's exercised public API surface is callable from a Native-AOT-published, trimmed consumer application without runtime AOT/trimming failures" — covering core
Componoas well as the integration packages, making no claim of exhaustive public-API coverage beyond what each smoke consumer actually exercises, and explicitly not claiming the test framework's own runner/host is Native-AOT compatible. - Full
dotnet build/dotnet test Compono.slnx,package-validation.yaml, and the newaot-validation.yamlall green.
Phase 2 — Canonical samples¶
Status: Done
Ships as its own PR, after Phase 1 merges.
-
Compono.Samples.AspNetApi: add aCompono.Httpscenario (handler-based testing of an outbound HTTP-calling endpoint/service already present in the sample, or a small new one if none currently makes an outbound call) demonstrating realistic assertions a consumer would actually write, not merely a compiling reference. Added a newShippingClient(a typedHttpClientcalling an external carrier API for a shipping label) andShippingClientTests([Shared] TestHttpHandler, realisticRespondJson/Verify()and failure-status assertions). -
Compono.Samples.AspNetApi: add aCompono.DependencyInjectionscenario — a DI-composed row provider registered into the host'sIServiceCollection, exercised through a realistic test.DependencyInjectionTestscomposesIOrderRepositoryviarow.AsServiceProvider()and registers it into a realWebApplicationFactory<Program>host'sIServiceCollection. -
Compono.Samples.BasicUsage: add aCompono.Loggingscenario — compose anILogger<T>-dependent type viaUseLogging(), assert a captured log entry viaVerify(). AddedNotificationService,LoggingSampleProfile, andLoggingTests. -
docs/samples/*.mdoverview pages for both samples: add a mention of their new scenarios (no new per-scenario page, per ADR-0033 Amendment 2). -
docs/packages/compono-http.md,compono-dependencyinjection.md,compono-logging.md: link to their new sample scenario, if not already linked. -
README.md/docs/packages/index.md: confirm every publishable package's row links to a working example (sample or package guide), correcting any that don't. Confirmed:docs/packages/index.mdalready links every package's row to its guide, and each of the three new guides now links onward to its sample scenario.README.md's package table links no package to a guide (a pre-existing, uniform convention across all eleven rows, not a gap specific to these three) — left as is, correcting it would be an unrelated repository-wide change out of this phase's scope. - Full
dotnet build/dotnet test Compono.slnx(both samples build/run as part of the solution today; confirm the new scenarios do too) andpackage-validation.yamlgreen.
Critical Files¶
src/Compono.XunitV3/ComposeAttribute{TProfile}.cs,src/Compono.MSTest/ComposeAttribute{TProfile}.cs,src/Compono.NUnit/ComposeAttribute{TProfile}.cs— negative-seed guard fix.src/Compono.Generators/Emitters/GeneratedFileNaming.cs,Emitters/TestDoubleIdentifierNaming.cs,Emitters/TestDoubleOverloadIdentity.cs—StableHashconsolidation.src/Compono.Generators/Emitters/CompositionPlanEmitter.cs,CollectionPlanEmitter.cs,TestDoubleEmitter.cs,LoggingActivationEmitter.cs,RowInvokerRegistrationEmitter.cs—GeneratorVersionconsolidation.src/Compono.Generators/WellKnownTypes/WellKnownTypeData.cs,Discovery/ComposableAttributeDiscovery.cs—ComposableAttributemetadata-name literal consolidation.test/Compono.XunitV3.SampleTests/Compono.XunitV3.SampleTests.csproj, itsFailingCompositionTests.cs/FailingConfigProfileTests.cs, and a newREADME.mdin that project's folder — CI/filter documentation + stale-comment cleanup.test/Compono.Generators.Tests/— new incremental-caching regression test forTrackingNames.mkdocs.yml,.github/workflows/docs.yml,docs/packages/compono-nsubstitute.md— docs sync..github/workflows/package-validation.yaml— package-list consolidation via job-levelenv:..github/workflows/aot-validation.yaml(new) — permanent AOT CI gate.samples/Compono.Samples.AspNetApi/**,samples/Compono.Samples.AspNetApi.Tests/**,samples/Compono.Samples.BasicUsage/**— Phase 2 sample scenarios (ShippingClient/ShippingClientTests,DependencyInjectionTests,NotificationService/LoggingSampleProfile/LoggingTests).docs/samples/aspnet-api.md,docs/samples/basic-usage.md,docs/packages/compono-http.md,compono-dependencyinjection.md,compono-logging.md— Phase 2 documentation sync.
Test Plan¶
- New regression tests in
Compono.XunitV3.Tests,Compono.MSTest.Tests,Compono.NUnit.TestsmirroringCompono.TUnit.Tests/SeedObservabilityTests.cs's negative-seed-plus-throwing-profile case. - Existing full solution test suite stays green throughout — helper consolidation changes are internal-only and must not change any observable behavior.
- New incremental-caching regression test in
Compono.Generators.Testsproving at least oneTrackingNames-tagged stage reports a cache hit on an unrelated second edit (see "TrackingNamesdisposition" above). - After the
Compono.XunitV3.SampleTestsdocumentation fix, a fresh contributor following only the project's own README/comments (notpackage-validation.yaml) must be able to reproduce the correct--filter-not-classinvocation and get a clean 48/48 pass. aot-validation.yaml's own matrix run is the test plan for the AOT-CI gate itself — a deliberately-broken annotation on one leg (removed during local validation, not committed) should be used once to confirm the gate actually fails before merging it as passing.- Phase 2's new sample scenarios each need at least one real assertion exercised by
dotnet test Compono.slnx, not merely a compiling call.
Notes¶
The framework-binder duplication research (RESEARCH-0019) is intentionally absent from both phases' task lists — it runs independently and does not gate either PR.
Phase 1 validation (2026-09-03), PR #128:
dotnet build Compono.slnx -c Release— 0 errors.dotnet test Compono.slnx -c Release— 3478/3478 passed locally; CI's ownbuild/build / buildchecks green (one transient exit-143 flake on thePR Buildjob, unrelated to this PR's diff —test/Compono.MSTest.AotSmokeTestisn't part ofCompono.slnxat all, and the identical flake pattern independently appears on unrelated branches in this repo's own recent CI history; resolved by re-running the job, no code change).package-validation.yamlgreen — all 11 packages pack, CS1591-clean, nupkg contents inspected, all 5*SampleTestsprojects pass, NUnit compatibility matrix passes.aot-validation.yaml(new):changesjob correctly detected this PR's core/generator-touching diff and ran all eight legs; all eight passed, including the two outlier legs' extra steps (Compono.Logging.AotSmokeTest/verify-packaging.sh,Compono.Http.AotSmokeTest/AnalyzerContract/verify-analyzer-contract.sh);aot-gateresolved topass.- Deliberate-failure proof performed and reverted: a temporary commit made
Compono.MSTest's AOT leg throw unconditionally. Result: that one leg failed, the other seven passed independently (fail-fast: falseconfirmed working), andaot-gatecorrectly resolved tofail— proving the required check actually blocks on a real failure, not just reports green reflexively. The breaking commit was reverted in the next commit before this phase was considered done; the final push is all-green again. - Fixed one real bug found during this validation, not scoped by the original plan:
aot-validation.yaml'ssetup-dotnetstep only installed the10.0.xSDK, but this repo'sglobal.jsonpins a preview11.0.xSDK for the whole checkout —dotnetCLI resolution failed entirely (not just fornet11.0-specific work) until the workflow installed all four SDK majors, matchingpr-build.yaml/package-validation.yaml's own established convention. - PR title required a lowercase-subject fix (
amannn/action-semantic-pull-request's enforced convention) — mechanical, no scope impact.
Phase 2 validation (2026-09-03):
dotnet build Compono.slnx -c Release— 0 errors, 0 warnings.dotnet test Compono.slnx -c Release— 3482/3482 passed locally (3478 Phase-1 baseline + 4 new: 2ShippingClientTests, 1DependencyInjectionTests, 1LoggingTests).samples/Compono.Samples.AspNetApi.Testsalone: 9/9 passed (5 baseline + 4 new).samples/Compono.Samples.BasicUsagealone: 7/7 passed (6 baseline + 1 new).ComponoGeneratedLogginghad to be set explicitly (true, plus aCompilerVisibleProperty) inCompono.Samples.BasicUsage.csproj—Compono.Logging's packed-only default-true props asset doesn't reach aProjectReferenceconsumer, the identical situationCompono.Samples.AspNetApi.Tests.csprojalready solved forComponoGeneratedTestDoubles. Confirmed by a failing build first (LoggingProviderthrew the "no generated activation"InvalidOperationExceptionat test-run time without it), then fixed the same way.- All new documentation links (
docs/samples/aspnet-api.md→docs/packages/compono-http.md/compono-dependencyinjection.md,docs/samples/basic-usage.md→docs/packages/compono-logging.md, and each guide's reverse link back) manually verified to resolve to files that exist in this checkout;mkdocsitself isn't available in this environment to run a full nav build. - No public API changed —
Compono.Http,Compono.DependencyInjection,Compono.Loggingsource untouched; onlysamples/**anddocs/**. package-validation.yaml/aot-validation.yamlare unaffected by this phase (samples areIsPackable=false, not part of either workflow's package/AOT-smoke matrix) — Phase 1's own green run already covers them, confirmed no publishable-package source changed.