[PLAN-0008] Milestone 8: Public Preview¶
Status: Done
Implements: ADR-0030 (including Amendments 1-2), ADR-0031 (release/versioning policy, package-readiness bar), ADR-0032 (API reference toolchain), ADR-0033 (samples strategy) — a docs/mvp.md roadmap milestone drawing on four Accepted ADRs, per docs/plans/README.md's multi-ADR plan convention.
Goal¶
An external developer can discover, understand, install, use successfully, troubleshoot, evaluate, and safely contribute to Compono using only public artifacts (nuget.org packages, the deployed docs site, the GitHub repository) — verified by a clean-room acceptance pass (Phase 8) that follows only public instructions and consumes only published/local-feed packages. See "Exit criteria" below for the full, checkable bar.
Scope¶
Per docs/mvp.md's Milestone 8 section and the four ADRs above. This plan supersedes the earlier flat, unphased PLAN-0008 draft (recorded in Notes below) — same total scope, now split into independently shippable phases, each its own PR, per design-decisions.md's "each phase ships as its own PR" rule.
Explicitly deferred (unchanged from the original draft): any change to the documentation architecture itself (ADR-0030's hierarchy, section purposes, audiences) — friction discovered while executing this plan routes to a new tasks/design.md pass (likely an ADR-0030 Amendment), not a silent deviation absorbed here. Any post-MVP product capability discovered while writing docs or running the acceptance test is recorded in docs/roadmap/post-mvp.md, not built in this milestone — per the milestone brief's own "don't introduce unrelated post-MVP features" constraint. A blocking bug found along the way may be fixed in its own scoped PR, same precedent as ADR-0029's "Bug handling."
Phase ordering rationale¶
The milestone brief's suggested shape (decisions → toolchain → package hardening → core docs → package-specific docs → samples/cookbook → contributor readiness → release pipeline → acceptance → closeout) is adopted with two changes, both driven by real dependencies this design pass surfaced:
- Release pipeline moves out of its own phase. The user's own decision (this plan's design conversation) settled that the pipeline itself needs no redesign — only the
alpha→previewidentifier rename (ADR-0031). That change is mechanically trivial and has no dependency on anything else in this plan, so it's folded into Phase 0 rather than getting a whole phase to itself. It belongs in Phase 0, not a later checkpoint — every versionpublish-preview.yamlproduces is a SemVer prerelease regardless of the identifier string (-alpha.Nand-preview.Nare both still prereleases, both still excluded from a plaindotnet add packageinstall), so renaming it carries no risk of a still-in-progress milestone looking publicly "done." The actual "does this look done" gate is Phase 8's manual GitHub Release publish (publish-release.yaml), which this rename has no effect on either way — see ADR-0031's "Preview publishing identifier" Decision Outcome for the full mechanics. - Package-readiness hardening moves before documentation writing, not after. Package Guides, Samples, and the acceptance test all need real, installable packages to write accurate content against and verify examples with — writing Package Guides against packages that haven't yet been hardened (symbols, validation, packed-consumer verification) risks documenting a shape that changes underneath the docs before launch.
Phase 0: Package readiness hardening¶
Status: Done
Checkpoint: Package Quality Complete — every package meets ADR-0031's readiness bar and can be safely built against by every later phase.
Executes ADR-0031's package-readiness bar against all five packages — four independently published (Compono, Compono.XunitV3, Compono.NSubstitute, Compono.Bogus), plus Compono.Generators, which never gets its own .nupkg (IsPackable=false, embedded inside Compono.nupkg's analyzers/dotnet/cs, per ADR-0003) and is verified by inspecting that embedded content, not by packing it independently. No documentation content depends on this phase's outcome being novel — it hardens what already ships — but Phases ¾/8 depend on it being done before writing package-specific content or running the acceptance test.
- Rename
publish-preview.yaml'sprereleaseIdentifierinput fromalphatopreview(ADR-0031) —mainstarts publishing0.x.y-preview.Ninstead of0.x.y-alpha.N. Still a real SemVer prerelease either way, so this carries no sequencing risk and belongs here, not at a later checkpoint. - Per ADR-0031 Amendment 1, give
Directory.Packages.props'sPackageVersionentries for the three dependencies that flow into a publishable package's own.nuspeca deliberate tested range instead of a bare (unbounded floor) or exact-pin version:NSubstitute→[6.0.0, 7.0.0),Bogus→[35.6.5, 36.0.0),xunit.v3.extensibility.core→[3.2.2, 4.0.0). Everything else in the file (Roslyn/Scriban/ polyfill build-time-only deps that never flow to a consumer's.nuspec, and test-only tooling versions) stays as originally declared — no blanket pin or range policy applies to those. - Fix the same gap for the internal dependency:
Compono.XunitV3/Compono.NSubstitute/Compono.Boguseach referenceComponovia a plain<ProjectReference>(PrivateAssets="none"), whichdotnet packconverts into a bare-version (minimum-inclusive) dependency onCompono's current version, not an exact match — a consumer could install e.g.Compono.XunitV3 0.3.0alongside a newerCompono 0.5.0and have it restore successfully, exactly the cross-package version mismatch ADR-0031 declares unsupported. Keep<ProjectReference>— it stays the mechanism for local development (fast inner loop, no local-feed round-trip needed to iterate on an integration package against core changes in the same repo); this task only overrides the NuGet dependency version range thatdotnet packwrites into the.nuspecat pack time ([$(Version)]instead of the default bare version), it does not switch toPackageReference. Verify by inspecting the packed.nuspec's<dependencies>entry in the package-contents-inspection CI job below, not just its file listing. - Add
PackageTagsandPackageReleaseNotestoDirectory.Build.props, not per-project — one shared, uniform value for all five packages (PackageTags:testing;test-data;source-generator;dotnet;PackageReleaseNotes:$(PackageProjectUrl)/releases, the repo's stable releases index rather than a specific version tag — a per-version.../releases/tag/v$(Version)link would 404 for every preview buildpublish-preview.yamlpushes on a plainmainpush, since no GitHub Release or tag exists for those, only for versions published throughpublish-release.yaml). Matches howPackageLicenseExpression/RepositoryUrl/PackageIcon/PackageReadmeFileare already centralized — the five packages distribute as one coherent set, so their discovery metadata comes from one place, not five copies that can drift. No per-package tag differentiation (e.g.xunit/nsubstitute/bogus) — the package name itself already carries that distinction. - Add a
<Title>to each of the four publishable packages'.csprojfiles (none currently set one —Directory.Build.propsonly centralizes what's genuinely uniform; a human-friendly title is per-package, same asDescriptionalready is). A short, human-friendly name distinct from the raw package ID (e.g. "Compono — Core Composition Engine," not just "Compono") — ADR-0031's discovery-metadata bar names title alongside tags/description, and nothing before this task actually added one. Verify via the package-contents-inspection CI job's manifest check, not just file listing. - Add
Microsoft.DotNet.PackageValidation(EnablePackageValidation=true) toDirectory.Build.props's packablePropertyGroup, with no staticPackageValidationBaselineVersionvalue. Do not try to inject this intopublish-preview.yaml/publish-release.yaml— both are a single job each callinguses: LayeredCraft/devops-templates/.github/workflows/publish-*.yml, with no step-level hook inside a reusable-workflow job to insert a "compute baseline, then pack" sequence into, and extending the shareddevops-templatesworkflow with a new input is a cross-repo change out of scope here. Instead, fold this into the locally-controlled pack/contents-inspection CI job below (this repo fully controls it): query nuget.org for each package's currently-latest published version, then rundotnet pack -p:PackageValidationBaselineVersion=<prior-version>for each of the four publishable packages, as a pre-merge PR gate — catching an accidental break before it reachesmainis strictly better than catching it after either publish workflow has already run. Skip this gate on any PR carrying thebreaking-changelabel — that label already means the break is deliberate and permitted by ADR-0031's own0.X+1.0policy, so failing the same PR on the incompatibility the label declares would be a self-contradiction. The very first-ever publish has nothing to query yet, so validation is inert for exactly that one case. This new job's own trigger must includelabeled/unlabeledPR activity types, not justpr-build.yaml's defaultopened/synchronize/reopened—release-drafter.yamlapplies thebreaking-changeautolabel in its own, separately-triggered workflow run, so a gate that only reads labels at the PR's initial push can run before the label exists (blocking a real, legitimate break) or stay green after the label is later removed (silently missing an unjustified break on the same commit). Read the current label state at the gate's own run time, never a cached value from an earlier trigger. - Reconfigure
.github/release-drafter.yml'sversion-resolverso thebreaking-changelabel maps tominor, not the file's currentmajor— as configured today, a labeled breaking-change PR resolves the next version as1.0.0, silently exiting the0.xpreview line the first time anyone uses the label, rather than the deliberate0.X+1.0minor bump ADR-0031's compatibility policy requires. Leavebreaking-changeincategoriesunchanged — only itsversion-resolverbucket moves. - Add a new, locally-controlled CI job (a real job in this repo's own workflow, distinct from
publish-preview.yaml/publish-release.yaml's opaqueuses:calls) that packs the four publishable packages and: asserts each.nupkg's file listing matches the expected shape per TFM (lib, README, icon, no stray build artifacts;analyzers/dotnet/csforComponospecifically, containingCompono.Generators.dll— this is also whereCompono.Generatorsitself gets verified, by content inspection rather than an independent pack); runs the API-compatibility baseline check from the task above (-p:PackageValidationBaselineVersion=<prior-version>, skipped onbreaking-change-labeled PRs); and runsdotnet build -p:WarningsAsErrors=CS1591for the four publishable packages so a missing public-member doc comment actually fails CI —Directory.Build.props' existingGenerateDocumentationFilesetting deliberately leavesCS1591a warning for ordinary builds, which alone never failsdotnet build. All three checks run as a pre-merge PR gate, on the same trigger (see the trigger note above — must includelabeled/unlabeled, not justpr-build.yaml's default activity types). - Extend the local-feed packed-consumer pattern (already used by
test/Compono.XunitV3.SampleTests) to restore and smoke-test the four publishable packages together from one local feed, as a standing CI gate — not ad hoc per milestone. ReusesCompono.XunitV3.SampleTests' ownPackToLocalFeedrestore directly (--filter-not-classon the one deliberately-failing class, MTP's actual filter syntax —dotnet test's VSTest-style--filterproduced zero matched tests against an MTP host and was corrected during verification) rather than inventing a second project; the newpackage-validation.yamlCI job runs it as a pre-merge gate. Verified locally: 16/16 tests pass against packages restored from.local-nuget-feed. - Verify (not redesign)
PrivateAssets/analyzer-transitivity holds for every package, not justCompono/Compono.Generators. Proven by the local-feed smoke test above, not just static inspection: all 16 passing tests compose real generated plans reached only throughCompono.XunitV3/Compono.NSubstitute/Compono.Bogus'sPackageReferences (never aProjectReferencetoComponoorCompono.GeneratorsinCompono.XunitV3.SampleTests), soCompono.Generators' analyzer packaging demonstrably flows transitively through every integration package's ownPrivateAssets="none"reference toCompono. - Spot-check of
Directory.Packages.props's current dependency licenses — self-contained to this phase, not dependent oncontributing.md(which doesn't exist until Phase 6; see Phase 6's own Tasks for the standing review-habit note this spot-check feeds into). Findings: every dependency in the file is MIT, BSD-⅔-Clause, or Apache-2.0 (xUnit v3 family, NSubstitute, AwesomeAssertions, Bogus, Scriban, Meziantou.Polyfill, Microsoft.CodeAnalysis., Microsoft.SourceLink.GitHub, BenchmarkDotNet, AutoFixture, Basic.Reference.Assemblies, Verify.) — no copyleft (GPL/AGPL/LGPL) dependency, nothing incompatible with Compono's own MIT license.
Phase 1: API reference toolchain evaluation and wiring¶
Status: Done
Executes ADR-0032. Depends on Phase 0 only loosely (needs real packages to generate against, but can run against main's current build) — sequenced early because reference/api/ is a dependency for later cross-links (Concepts, Package Guides, Cookbook) that are expected to point into it.
- Time-boxed bake-off:
DefaultDocumentationvs.xmldocmd(plus any other maintained candidate surfaced) against a representative slice of Compono's real public API, scored against ADR-0032's evaluation criteria (generics, overloads, inheritance, extension methods, attributes, nullable signatures,<see>/cross-package refs, doc-tag coverage, stable filenames/anchors, MkDocs Material readability, deterministic output, maintenance/TFM compatibility). Record the result in this plan's Notes. - Wire the winning tool into CI: generates
docs/reference/api/Markdown from the four publishable packages' (Compono,Compono.XunitV3,Compono.NSubstitute,Compono.Bogus) compiled DLL + XML doc file — notCompono.Generators, whichIsPackable=falseand is an internal analyzer implementation embedded inCompono.nupkg, not a consumer-referenceable library; generating areference/api/Compono.Generatorssection for it would be empty or misleading.Compono.Generatorsis verified as package content (Phase 0's.nuspecinspection), not documented as public API here. - Add the drift-detection CI gate (regeneration produces no uncommitted diff) and the missing-XML-doc-comment gate where the tool supports it.
-
reference/index.mdstates the "supplements, never replaces" philosophy (already drafted per ADR-0030 Amendment 1's framing).
Phase 2: Core documentation and README¶
Status: Done
Checkpoint: Documentation Foundation Complete — a newcomer has a complete, real (non-stub) linear path from zero knowledge to productive use; every later section can safely link back into this one.
The primary learning path a newcomer needs before anything package-specific or task-specific makes sense — sequenced before Phases 3-5 since How-to Guides/Package Guides/Cookbook/Best Practices all assume Concepts, and Samples/Migration Guide pages link back into Getting Started.
-
docs/index.md— fix theCompono.Create(builder => ...)example to the realComposer.Create(builder => ...)API (src/Compono/Composer.cs). - Repository-root
README.md— review and update (distinct artifact fromdocs/getting-started/*, perdocs/mvp.md's Milestone 8 scope). Apply ADR-0030 Amendment 2's benchmark-claims policy: no comparative AutoFixture performance claims. Fixed the same staleCompono.Create(...)example asdocs/index.md; added a "Getting Started" section linking the docs site's Getting Started and AutoFixture migration guide (noCONTRIBUTING.mdlink yet — that file doesn't exist until Phase 6, which owns adding it here per that phase's own Tasks). -
getting-started/index.md,installation.md,first-test.md,learning-paths.md,next-steps.md. -
concepts/index.md,composition-model.md,profiles.md,registrations-and-rules.md,shared-values.md,providers.md,determinism-and-seeding.md,collections.md. -
how-to/index.md,create-an-object.md,write-a-composed-theory.md,customize-a-member.md,register-a-type.md,use-profiles.md,share-a-value-across-a-test.md.
Phase 3: Package guides, migration guide, troubleshooting, reference¶
Status: Done
Content with the most existing raw material to draw from (Milestone 7's research/migration-guide evidence, real diagnostics already shipped) — sequenced after Phase 2 (assumes Concepts) and Phase 0 (packages must be final-shaped to describe accurately) and Phase 1 (reference/diagnostics.md/ glossary.md complete the Reference section Phase 1 started).
-
packages/index.md(ecosystem map table),packages/compono.md,packages/compono-xunitv3.md,packages/compono-nsubstitute.md,packages/compono-bogus.md. - Polish
docs/migrating-from-autofixture.mdto publication-ready. Superseded the original plan's "content refinement only, no structural change" framing mid-phase, on direct user feedback: the first pass (content-stable, per ADR-0030) left the doc reading as an internal Milestone 7 research artifact — acosmere-tracker-named status block citing PLAN-0007 phase counts/classification totals, a repository-specific package-referencing/CI section, and "Finding N"/"gap N"/"Phase N" narration threaded through every section — not a public migration guide. Rewritten around the reader's migration journey instead: a task-oriented structure (who it's for, migration mindset, install, a quick concept map, per-concept migration sections, a specimen-builder decision table, a known-limitations section linking out to Troubleshooting/Reference for full mechanics rather than re-deriving them inline, a migration checklist, and a closing evidence-trail section), with every before/after example re-expressed in generic domain terms (Order/Customer/IOrderRepository, consistent with the rest of the site's Concepts examples) instead of the source repository's own type names — real evidence, credible patterns, no repository name in the published page.docs/research/0001-autofixture-comparison.md(the internal research dossier, not a public docs-site page) is unaffected and keeps its fullcosmere-tracker-specific detail; this guide links out to it for readers who want the complete evidence record instead of duplicating it. -
troubleshooting/index.md,troubleshooting/common-errors.md(start from the realCMP0001finding),troubleshooting/faq.md(start from the real gap-3 "why fail-fast" finding). -
reference/diagnostics.md(everyCMPcode),reference/glossary.md. - Explicit known-limitations content, surfaced from Getting Started, Package Guides, Troubleshooting, and release notes (not one obscure page) — sourced from
docs/research/0001-autofixture-comparison.md's recorded findings (CMP0001, fail-fast recursion vs. omission, the Compose-family stacking constraint,Compono.Bogus's exact member-name-matching limits) plusdocs/mvp.md's Non-goals list. Each Package Guide carries its own "What it deliberately doesn't do" section;troubleshooting/index.mdaggregates a "Known limitations" pointer to all four plusmvp.md's Non-goals; Getting Started'snext-steps.mdalready linked to Troubleshooting, so no separate edit was needed there to make the content reachable from that entry point too.
Phase 4: Samples, cookbook, best practices¶
Status: Done
Checkpoint: Public Documentation Feature Complete — every docs/documentation-architecture.md section that depends on real, runnable code (Samples, Cookbook) now has it; only architecture consolidation, contributor readiness, and final hardening remain.
Executes ADR-0033 for samples. Sequenced after Phase 2 (Concepts/Package Guides content to link back to) and Phase 0 (packed-package verification needs hardened packages).
-
samples/Compono.Samples.BasicUsage/— real project, added toCompono.slnx, plainProjectReferences (see ADR-0033 Amendment 1 for why the packed-verification mode this task originally scoped was dropped).docs/samples/basic-usage.mdoverview page. DemonstratesComposer.Create()/Create<T>()/CreateMany<T>(), a reusable profile (registration + member rule),[Compose<TProfile>], and both seed-reproduction paths.Compono.Generatorsis referenced as a separate Analyzer-onlyProjectReference(matching howCompono.csprojitself references it) — a plainProjectReferencechain doesn't flow an upstream project's own analyzer-only reference transitively, only a packed.nupkg'sanalyzers/dotnet/csfolder does. 6/6 tests pass. -
samples/Compono.Samples.AspNetApi/— real project, added toCompono.slnx.docs/samples/aspnet-api.mdoverview page. Implemented as two projects (the minimal-API app itself, andCompono.Samples.AspNetApi.Testsfor the composed test suite) — a top-level-statements minimal API can't also host the MTP test runner's own generated entry point in the same project, and a real ASP.NET Core sample conventionally ships its tests separately anyway. DemonstratesUseNSubstitute()/UseBogus()together, a[Shared]substitute with explicitReturns(...)setup verified viaReceived(...), aCompono.Bogus-generatedCustomer, inline- composed theory values, and one real
WebApplicationFactory<Program>endpoint integration test. 5/5 tests pass.
- composed theory values, and one real
-
docs/samples/index.mdoverview; records the five deferred candidates (CQRS, Clean Architecture, Minimal APIs, MediatR, EF Core) as future candidates, not silently dropped. - Removed, not just left unbuilt: deleted the five deferred samples' Phase 5 (PLAN-0007) stub pages (
docs/samples/{cqrs,clean-architecture,minimal-apis,mediatr,ef-core}.md) and their fivemkdocs.ymlnav entries. - Dropped, not shipped: a local-feed packed-package verification job for both samples. Both samples' own
nuget.configwould need to list the local feed as an unconditional package source (NuGet has no way to condition a source the way anItemGroupcan be conditioned), which madedotnet restore Compono.slnxhard-fail withNU1301the moment that git-ignored directory was simply absent — true on every fresh checkout, including this repo's own CI, and it broke restore for the entire solution, not just the two samples. See ADR-0033 Amendment 1 for the full account and why the packed-artifact-divergence risk this task was meant to cover is still handled centrally by Phase 0's existingCompono.XunitV3.SampleTestspacked-consumer smoke test. -
cookbook/index.md(flat, alphabetical, per ADR-0030 Amendment 2's deferred-navigation decision — recipes deliberately have nomkdocs.ymlnav entries) plus a first batch of five recipes: generate a realistic email, freeze a sharedHttpMessageHandler, override one field only for one test, seed a specific failing case for reproduction, compose a substitute with one method stubbed. Each recipe carries the stable front matter (title, description,packages,concepts) ADR-0030 Amendment 2 requires. Every code sample verified to actually compile and pass against real Compono source before publishing (the freeze-a-shared-handler recipe's first draft composedHttpMessageHandler/HttpResponseMessagedirectly, which turned out to be an ambiguous-constructor compile error —CMP0001— for both types; corrected to a single-constructorStubHttpMessageHandlerwrapper). -
best-practices/index.md,organizing-profiles.md,large-test-suites.md,naming-conventions.md,reusing-configuration.md,performance-recommendations.md,deterministic-and-non-brittle-tests.md.
Phase 5: Architecture consolidation, legacy retirement, and benchmark suite redesign¶
Status: Done
Executes ADR-0030 Amendment 2's "one canonical home" principle for the architecture/roadmap documentation, and — added after this phase's doc-consolidation work was already done, per direct review of the existing benchmark suite — ADR-0034's full benchmark-suite redesign. Sequenced after Phase 2 (Concepts must exist for Architecture pages to cross-link back to) — the last phase touching docs/architecture.md/docs/performance.md/ docs/design-principles.md/docs/manifesto.md/docs/public-api.md's real pre-existing content, so it can safely consume and then retire them.
Part A: Architecture and roadmap documentation (done)¶
-
architecture/index.md,architecture/design-principles.md(absorbsdocs/design-principles.md/docs/manifesto.md's content). -
architecture/current/source-generation.md,generated-plans-and-discovery.md,provider-pipeline.md,deterministic-seeding.md(real content, migrated fromdocs/architecture.md). -
architecture/decision-log.md(public-facing index intodocs/adr/, including ADR-0034). - Retire all five pre-existing legacy pages this phase consolidates —
docs/public-api.md,docs/manifesto.md,docs/architecture.md,docs/design-principles.md, anddocs/performance.md— from navigation and canonical-content ownership once every cross-reference has a new home inarchitecture//reference/, and delete all fivemkdocs.yml"(legacy)" nav entries. Do not delete any of the five files themselves. 24+AcceptedADRs link to one or more of them by path, and this repo's own ADR-immutability rule means none of that historical text can be rewritten to point elsewhere. Replace each file's content with a short redirect/tombstone stub ("this content moved toarchitecture/..., see there") instead — satisfies "retired, not part of the public nav, one canonical home" while keeping every existing ADR link resolvable, which Phase 7's site-wide broken-link check would otherwise fail against files this plan can't touch. (An earlier version of this task only tombstoned the first two — the same problem applies identically to the other three, since they're excluded from the canonical tree the same way.) -
roadmap/index.md,roadmap/proposed-adrs.md,roadmap/future-packages.md(roadmap/post-mvp.mdalready real content from PLAN-0007 Phase 3 — just needs its nav confirmed).
Part B: Benchmark suite redesign (per ADR-0034 — not started)¶
architecture/current/performance.md currently still documents the old benchmark suite (migrated as-is from docs/performance.md during Part A, before the redesign decision below was made) — it gets overwritten by this Part's last task, not before. Sequencing matters: design and implement the new suite first, produce real results from it, then write the page — this phase does not narrate a redesign still in progress.
- Design the benchmark strategy from first principles: the question set, audiences, and category taxonomy — done as ADR-0034 (
Accepted). - Define and implement the reused
Models/set (SimplePoco,MediumAggregate/Address,DeepGraph(DeepLevel2-8),LargeCollection,SharedValueGraph(SharedContext/ConsumerOne/ConsumerTwo),ProviderBackedModel(IClock/FixedClock)) andBaselines/(AutoFixtureComposer) — replacedBenchmarkTypes.cs,ResolutionBenchmarkTypes.cs,ReflectionComposer.cs,AutoFixtureComposer.cs. -
Implement— implemented (ImplementationStrategies/SimplePocoConstructionBenchmarks.cs/MediumAggregateConstructionBenchmarks.cs, handwritten-as-ceiling/ generated/cached-reflection/uncached-reflection), then removed per ADR-0034's Amendment: the category compared different systems doing different amounts of work (bare construction vs. a full resolution pipeline), not one isolated variable, so a result from it couldn't be attributed to a specific cause or guide an optimization decision. ItsBaselines/(HandwrittenComposer,CachedReflectionComposer,UncachedReflectionComposer) were deleted alongside it — no other category referenced them.ArchitectureBenchmarks.cs/ResolutionArchitectureBenchmarks.cs(the old files this category was meant to replace) are still correctly deleted below; their question just doesn't get a new home. - Implement
ConsumerScenarios/—RepresentativeModelBenchmarks.cs(simple POCO, medium aggregate, deep graph, large collection),SharedValueBenchmarks.cs(CreateRow/ResolveShared),ProviderEnabledBenchmarks.cs(Bogus-enabled, NSubstitute-enabled) — new; no prior equivalent existed. - Implement
ExternalComparison/— split intoSimplePocoComparisonBenchmarks.cs/MediumAggregateComparisonBenchmarks.cs(one file per model, so each class's[Benchmark(Baseline = true)]ratio stays meaningful — a single class covering both models would compute every ratio against one model's baseline) — replacesEcosystemBenchmarks.csandResolutionEcosystemBenchmarks.cs. - Implement
FeatureOverhead/— diverged from the plan's literal additive chain (a singleComposercan't coherently stack aCreateRow-based sharing step onto aCreate<T>()-based rule step):ConfigurationOverheadBenchmarks.cs(generated-only vs. +member-rule vs. +type-rule vs. +custom-provider, all onMediumAggregate),SharingOverheadBenchmarks.cs(row without sharing vs. with sharing, both viaCreateRow),BogusOverheadBenchmarks.cs/NSubstituteOverheadBenchmarks.cs(pairwise: the cheapest alternative mechanism vs. the package provider, isolating exactly one member's resolution) — new; no prior equivalent existed. - Implement
Scalability/—BatchScalingBenchmarks.cs(CreateManyat 1/10/100/1000),GraphDepthScalingBenchmarks.cs(shallowMediumAggregatevs. deepDeepGraph),CollectionSizeScalingBenchmarks.cs(3/10/50/200-element collections) — replacesResolutionBenchmarks.csandDeepGraphBenchmarks.cs's construction, generalizing the latter's one-off trace-buffer-resize question into a real shallow-vs-deep comparison. - Implement
SourceGeneration/—GeneratorDriverBenchmarks.cs(in-processGeneratorDriver, clean vs. incremental generation across a 1/10/50 composable-type-count matrix) — new; no prior equivalent existed. The incremental compilation is derived from the clean one viaCompilation.ReplaceSyntaxTree(not built as an independently-parsedCSharpCompilation), since Roslyn's incremental-generator cache is keyed on compilation/tree identity — two separately-constructed compilations look entirely unrelated to the driver even with near-identical source text, which would silently defeat the whole point of the incremental-vs-clean comparison.Compono.Generatorschanged from analyzer-only (ReferenceOutputAssembly="false") to also a normal compile reference on the sameProjectReferenceitem (not a second, separate reference to the same project), plusInternalsVisibleTo="Compono.Benchmarks"added toCompono.Generators.csprojso this class can constructComponoIncrementalGeneratordirectly, matchingCompono.Generators.Tests' own pattern. - Add
Compono.NSubstitute/Compono.BogusProjectReferences toCompono.Benchmarks.csproj(needed byConsumerScenarios/andFeatureOverhead/'s provider-enabled cases; not needed previously since no prior benchmark exercised either package). Also addedBasic.Reference.Assemblies.Net100/Net110(conditional on$(TargetFramework)), needed to build a real reference-assembly set forSourceGeneration/'s in-process compilations, matchingCompono.Generators.Tests' own pattern. - Delete
ArchitectureBenchmarks.cs,EcosystemBenchmarks.cs,ResolutionArchitectureBenchmarks.cs,ResolutionEcosystemBenchmarks.cs,ResolutionBenchmarks.cs,DeepGraphBenchmarks.cs,BenchmarkTypes.cs,ResolutionBenchmarkTypes.csonce every question they answered has a home in the categories above — no benchmark class straddles the old and new structure once this task is done. - Build and smoke-test the redesigned suite:
dotnet build Compono.slnx -c Release(0 warnings, 0 errors) anddotnet run -c Release --project benchmarks/Compono.Benchmarks -f net10.0 -- --job Dry --filter '*'(all 49 benchmarks, across the 15 classes that existed at that point in this task list — before Implementation Strategies was implemented-then-removed later in this same Part, which brought the suite down to 41 benchmarks across 13 classes — executed without throwing) — confirms the suite is structurally correct. This is not the statistically real run the next task produces;--job Dryis one cold iteration per benchmark, fast enough to smoke-test but not meant to be reported as a real result. - Run the full redesigned suite (
dotnet run -c Release --project benchmarks/Compono.Benchmarks -f net10.0 -- --filter '*'),DefaultJob(not--job Dry), and record real results — all 15 classes (at the time; 13 after Implementation Strategies' later removal) produced real Mean/Error/StdDev/Allocated/Gen0/Gen1results (Apple M3 Max, macOS Tahoe 26.6, .NET 10.0.3 arm64 RyuJIT,BenchmarkDotNetv0.15.8; total run ~15 minutes). One real, published finding the run surfaced:UseBogus()costs ~865x a plain member rule (291.5 μs vs. 337 ns, isolated) becauseBogusMemberNameProviderconstructs a newBogus.Fakerper resolution — root-caused, not investigated further or fixed here, per ADR-0034's "publish an unfavorable result, don't hide it" rule. - Rewrite
architecture/current/performance.mdas capability- oriented public documentation per ADR-0034's public-documentation direction: what Compono optimizes for, what the benchmarks measure (and don't), methodology, representative Consumer Scenario/External Comparison results, theUseBogus()finding above, scaling behavior, reproducibility, and how to get the full per-category artifacts — not a historical narrative of Milestone ½/PR-review optimization work. Implementation Strategies/Feature Overhead/ Source Generation results are summarized (not every parameter value reproduced) per ADR-0034's "mostly engineering documentation rather than front-page marketing" direction for implementation- strategy benchmarks. Also fixedbest-practices/performance-recommendations.md's two links, which pointed at the now-tombstoneddocs/performance.mdinstead of this page. - Removed the Implementation Strategies category entirely, per ADR-0034's Amendment — deleted
ImplementationStrategies/and its dedicatedBaselines/classes (HandwrittenComposer,CachedReflectionComposer,UncachedReflectionComposer), removed the "Implementation strategies" section fromarchitecture/current/performance.md, and updated its category-count/overview text (six categories → five). The five remaining categories, the reused model set, and every fair-comparison/reporting rule not specific to Implementation Strategies are unchanged. - Fixed the
UseBogus()finding above, per ADR-0027's Amendment:BogusMemberNameProvidernow caches oneFakerper thread (ThreadLocal<Faker>,trackAllValues: false) instead of constructing one per request, reseedingRandomimmediately before every use — safe under concurrent access because a thread-local instance is never touched by more than one thread, distinct from the shared-Faker<T>alternative ADR-0027 already considered and rejected. New regression coverage: a concurrency test (200 concurrent resolutions on one shared provider instance, each checked against an independent single-threaded reference value) and a "convention throws mid-generate" test. Re-ran the full suite afterward (--filter '*',DefaultJob, same environment) and rewrotearchitecture/current/performance.mdfrom that single fresh run —UseBogus()'s isolated overhead dropped from ~865× to ~6.31×, and the full-profile Consumer Scenario cost from 903.4 μs / 2,229.31 KB to 5.481 μs / 7.04 KB. The page also picked up a clarity pass: explicit AutoFixture-relative-to-Compono ratio direction, preciseDefaultJobmethodology wording (pilot/warmup/ measured iterations, not "statistically significant"), an explicit "equivalent work" definition, a note connecting the Consumer Scenario and Feature OverheadUseBogus()numbers' different scope, and consistent×formatting throughout. - Addressed a real adversarial PR review of this phase's work (PR #53):
BogusMemberNameProvider's thread-local reuse was real but incomplete — a customAddConventiondelegate could mutateFakerstate (DateTimeReference, a sub-generator, any ofFaker's ~20 other public settable properties) that would then leak into a later, unrelated built-in-convention request on the same thread; reseedingRandomalone didn't restore isolation. Fixed by reusing the per-threadFakeronly for built-in/alias conventions (BogusConventions.IsBuiltIn, a reference-equality check against the ten built-in delegates) and giving every customAddConventiondelegate its own single-useFaker, with a new regression test mutatingDateTimeReferencein a custom convention and proving it doesn't perturb a later built-in request. The concurrency test didn't force genuine thread overlap —Parallel.ForEachAsyncover a fully-synchronous body could pass serially by scheduler luck; replaced with realThread+Barrierso all workers release simultaneously.GeneratorDriverBenchmarks' incremental tree was still built via a freshParseTextcall (given a different constructor argument than the base tree, but not derived from it) rather thanSyntaxTree.WithChangedText, so it measured a wholesale reparse under an "incremental" label; fixed to derive the touched tree via an append-onlyWithChangedTextedit so unaffected nodes keep their base-tree identity.GraphDepthScalingBenchmarksdidn't isolate depth — itsMediumAggregateshallow arm resolved seven strings and a collection againstDeepGraph's single string, conflating depth with total value-generation work; fixed to compareDeepLevel8(depth 1, one string) againstDeepGraph(depth 8, same one-string leaf shape) — the real, isolated result is 4.47× the mean and 2.65× the allocation for 8× the depth, a much more meaningful number than the original 1.03×/1.23×.architecture/current/performance.mdviolated ADR-0034's own Reporting Rules by omitting Error/StdDev/Gen0/Gen1 from most tables despite the page's own Methodology section claiming full columns; rewritten with the complete mandatory column set on every table. Also fixed: several broken/stale doc links and anchors (aremoved--itdouble-hyphen anchor MkDocs never generates;docs/plans/0002-...'s link to a heading removed from the rewritten performance page;docs/index.md/docs/getting-started/learning-paths.md/docs/concepts/providers.md/docs/concepts/composition-model.md/docs/concepts/index.md/docs/concepts/determinism-and-seeding.md/docs/concepts/registrations-and-rules.md/docs/concepts/collections.md/docs/how-to/register-a-type.mdstill linking to tombstoned legacy pages instead of their canonical replacements, includingdocs/index.md's stale "~6.1× faster" claim tied to a benchmark class that no longer exists); a stale "49 benchmarks/15 classes" verification-record bullet earlier in this list, now annotated with the post-removal counts; anddocs/roadmap/future-packages.mdwording that impliedCompono.Generatorswas a fifth installable package rather than an embedded analyzer. One reviewer claim (returningGeneratorDriverfrom a benchmark method breaks standardnet10dry runs via missingCompono.Generatorsrestore assets) was investigated directly — reproduced from a fully cleanbin/objstate,--job DryandDefaultJob, both TypeCount matrices — and did not reproduce; left as-is, noted in the PR reply.
Phase 6: Contributor and repository readiness¶
Status: Done
Executes ADR-0030 Amendment 2's governance-scope decision. Independent of the documentation content phases above — could run in parallel with Phases 2-5 in principle, sequenced here mainly for reviewability (one focused PR, not interleaved with doc-content PRs).
- Two files, not one.
docs/contributing.md— the full docs-site page: build/test/PR expectations, cross-linking this skill's public-facing equivalents, plus the license-review note Phase 0's dependency spot-check feeds into (any PR adding or bumping a dependency version — Dependabot-authored or not — gets its target package's license checked as part of normal review, per ADR-0031: Dependabot's own flow catches vulnerabilities, not licenses, so this is an ongoing review habit, not a one-time task that was already "done" after Phase 0). Separately, a root-levelCONTRIBUTING.md— this repo's owncontributing.mdreference (.claude/skills/engineering-workflow/references/contributing.md) already states the requirement: "if this project opens to outside contributors, split this section out into a realCONTRIBUTING.mdat repo root and link it fromREADME.md" — exactly what this milestone does.CONTRIBUTING.mdat repo root is what GitHub actually surfaces (the "Contributing" prompt on a new issue/PR, the community-standards checklist) — a docs-site page alone is invisible to that flow. Keep it short: a few sentences plus a link todocs/contributing.mdfor the full detail, not a duplicate. Also add the link from repository-rootREADME.md(a small addition to Phase 2's README review/update, done here sinceCONTRIBUTING.mddoesn't exist until this phase). Added tomkdocs.yml's nav (a top-level "Contributing" entry, after Roadmap) anddocs/documentation-architecture.md's tree updated from "not yet created" to real content.LayeredCraft/.githubalready supplies an org-wideCONTRIBUTING.mdvia GitHub's native community-health-file inheritance, but it's generic across the whole org (.NET 8/9, no mention of Compono's MTP runner or CI gates); kept as a deliberate repo-local override (per direct user decision) since it links straight to Compono-specific detail the org default can't have. -
SECURITY.md/CODE_OF_CONDUCT.md— not added to this repo, after a reversal. A first pass added repo-local copies (GitHub Advisory-only reporting; standard, unmodified Contributor Covenant v2.1) as deliberate overrides ofLayeredCraft/.github's weaker org defaults (a published email address; a short custom paragraph instead of the standard Covenant text). Per direct user request, the repo-local copies were removed on the expectation that LayeredCraft/.github#4 would update those two org-level files in place instead. Amendment (Milestone 8 closeout, 2026-08-07): that PR was still open at the time this task's own text above was written, verified directly against the liveLayeredCraft/.githubrepo — the org defaults were still serving the original weaker content (the published email address; the short custom paragraph) even though this task claimed the fix already landed. Found during Milestone 8's own closeout review; the user merged LayeredCraft/.github#4 the same day (2026-08-07), and the org defaults now genuinely serve the corrected content (GitHub's private Security Advisory flow; the full, unmodified Contributor Covenant) — re-verified directly against the live files post-merge.README.md/docs/contributing.md's links toLayeredCraft/.github's copies are accurate as of now. - GitHub issue templates — not added to this repo. A first pass added
.github/ISSUE_TEMPLATE/{bug_report,feature_request}.mddirectly, before discovering (via direct user feedback) thatLayeredCraft/.githubalready providesbug_report.yml/feature_request.yml/config.yml(GitHub issue-forms, a nicer format than the.mdtemplates first drafted) that apply to every org repo automatically via GitHub's native community-health-file inheritance — any repo-localISSUE_TEMPLATE/directory shadows the org's entirely (all-or-nothing per directory, not merged), so adding repo-local.mdtemplates here would have actively replaced the org's better ones with worse ones for no compono- specific benefit. Reverted;docs/contributing.md's "Before you start" and "Good first issue candidates" sections link to.../compono/issues/new/choosegenerically instead of naming a specific template file, since the actual templates now live and evolve inLayeredCraft/.github, not here. - PR template — not added to this repo. A first pass added
.github/PULL_REQUEST_TEMPLATE.mdas a repo-local override (a checklist naming compono's specific CI gates: Conventional-Commit title,CS1591doc-comment gate, package-validation, dependency- license check) sinceLayeredCraft/.github's generic template doesn't cover those. Per direct user decision, reverted in favor of the org's existing template via inheritance — one fewer file to keep in sync, anddocs/contributing.md's "Pull request conventions" section already spells out the same CI-gate detail in prose, so the checklist duplication wasn't pulling its weight. - "Good first issue" candidates identified from the Cookbook recipe backlog: six candidate recipes not yet written (collection with a fixed size, sharing a value without
[Shared]on the type, a customCompono.Bogusnaming convention, verifying NSubstitute call arguments, reproducing aCreateMany<T>()failure from a seed, a required-member record with one member overridden), listed indocs/contributing.md's "Good first issue candidates" section rather than as actual open GitHub issues — filing the issues themselves is a repository-administration action, not a docs/code change this plan's Tasks scope covers; the candidate list itself is the artifact this task produces.
Phase 7: Final navigation, link, and snippet validation pass¶
Status: Done
A dedicated hardening phase before publication — every prior phase wrote content against its own section; this phase verifies the whole site holds together, not just each page in isolation.
-
mkdocs.ymlfinal nav pass: every "(legacy)" entry retired or resolved (Phase 5), nav matchesdocs/documentation-architecture.md's tree exactly. Verified programmatically: every file the nav references exists on disk, nav order matches the documentation tree's section order, and no "(legacy)" string remains anywhere inmkdocs.yml. - Site-wide broken-link check (internal cross-links, per "every page leads somewhere"). Ran
mkdocs build --strict(already this repo's CI build command) against a local build — it validates every internal Markdown link and#anchor, but anchor mismatches were only logged atINFOlevel, so a strict build never actually failed on one. Addedvalidation.links.anchors: warntomkdocs.ymlso--strict(already run in.github/workflows/docs.yml) now fails the build on a broken internal anchor going forward, not just logs it. That change surfaced two real, pre-existing broken links, both fixed:docs/plans/0008-...md's reference to0007-milestone-7-dogfooding.md#phase-5-2026-08-03(no such anchor exists — that text is bold prose, not a heading — corrected to point at the actual## Notessection containing it), and two self-referencing TOC links indocs/research/0001-autofixture-comparison.mdusing a double-hyphen anchor MkDocs' slugifier never generates for an em-dash in a heading (the same bug class PR #53 already fixed once inarchitecture/current/performance.md— this phase found the two remaining instances). Re-ranmkdocs build --strictafterward: 0 warnings, build succeeds. - Code-snippet compilation check where practical (snippets drawn from real sample/test code, per
documentation.md's "prefer real examples" quality bar — verify they still compile against currentmain, not just that they did when written).dotnet build Compono.slnx -c Release— 0 warnings, 0 errors, including both sample projects (Compono.Samples.BasicUsage,Compono.Samples.AspNetApi+ its test project) that public docs pages draw real snippets from. Cross-checked every distinct Compono API symbol referenced across the 79 C# code fences on the public docs pages (Concepts, How-to, Getting Started, Cookbook, Package Guides, Best Practices, Samples, migration guide) against currentsrc/— every symbol (Composer.Create,CreateMany,CompositionBuilder,ICompositionProfile,ICompositionValueProvider,Register<T>,For<T>(),[Shared],UseBogus,UseNSubstitute,UseServiceProvider,WithCollectionSize,WithSeed,AddProfile,ComposeAttribute,SharedAttribute) still exists on the current public API — no stale example found. - Spelling/style pass across the full site. Ran
codespellacrossdocs/(excludingreference/api/— generated content — anddocs/adr/— immutable onceAccepted). Zero issues on every public/hand-authored page. The only two hits before excludingdocs/adr/were both "implementors" indocs/adr/0024-...md, a legitimate alternate spelling of "implementers" that also can't be edited post-Acceptedeither way. Grepped for stray TODO/TBD/FIXME/lorem-ipsum/placeholder markers across public pages: none found (the twoplaceholderhits indocs/packages/compono-bogus.mdare legitimate prose about AutoFixture-style anonymous placeholder values, not stub markers).
Phase 8: Clean-room public-preview acceptance test and first publication¶
Status: Done
Checkpoint: Release Candidate — every prior phase is done; this phase either confirms the milestone is genuinely ready to publish or sends findings back to an earlier phase before anything ships.
The milestone's actual proof point — depends on every content and package-readiness phase above being done.
- Clean-room acceptance test: a fresh project, following only the public docs site and consuming only published (or local-feed, for pre-publish verification) packages — the five-minute Getting Started path, one How-to Guide task, one Cookbook recipe, one Package Guide's "when to install" decision, one Troubleshooting lookup, all followed literally as written, no ADR/internal-repo knowledge assumed. See "Public-preview acceptance checklist" below for the full list. Run against the real, currently-published nuget.org preview packages (
0.1.0-preview.45, frompublish-preview.yaml's continuous main-branch publishing) rather than a local feed — more faithful to what an actual stranger gets today. Every checklist item passed; two real bugs were found and sent to their own scoped fix, not absorbed into this phase's own diff (design-decisions.md's "each phase ships as its own PR," and this is itself a "blocking bug found along the way," same precedent as Phase 5/7's own mid-phase bug fixes): PR #58 fixedCMP0001's message (the only one of the 12CMPdiagnostics pointing at an internaldocs/adr/...path a consumer has no way to resolve, and it didn't even match whatdocs/reference/diagnostics.mdalready documented as the message) and three flagship types' (Composer/CompositionDiagnostic/CompositionException) XML doc comments still citingdocs/public-api.md, a page Phase 5 already retired to a redirect-only tombstone — repointed to the live docs site instead, now real hyperlinks in the regeneratedreference/api/rather than inert text. Not fixed there and deliberately deferred: ~85 furtherdocs/architecture.md/docs/performance.mdreferences across nearly every file in the core package's XML doc comments (also stale since Phase 5, confirmed to render as inert code-styled text rather than actual broken links, so lower-severity than the two bugs above) — each needs its own semantic mapping to a real replacement page rather than a blind find/replace, too large a sweep to safely fold into a scoped fix; flagged for its own follow-up rather than silently dropped. Also spot-checked the live public docs site itself (not just a local build) — every page touched during the walkthrough returns200, and the deployed nav reflects both post-Phase-7 fixes (#56/#57). - User's own action, not automatable — done. The user reviewed and published the existing
v0.1.0draft GitHub Release (not draft) on 2026-08-07 — this is the actual "does this look done" gate (publish-release.yaml, triggered byrelease: types: [published]), completely independent ofpublish-preview.yaml'spreviewidentifier (renamed in Phase 0, unrelated to this step). The workflow ran clean (resolve→publish→push, all green). - Verified all four publishable packages installable from nuget.org post-publish (not just the local-feed pre-check), in a fresh project with no
nuget.config, each pinned to the real, stable0.1.0(not a-preview.Nprerelease):dotnet add package Compono/Compono.XunitV3/Compono.NSubstitute/Compono.Bogus --version 0.1.0all restored successfully. Ran a real composition smoke test against the installed packages (plainCreate<T>(),UseBogus(),UseNSubstitute(), and a[Compose]xUnit v3 theory row) — 5/5 passed, confirmingCompono.Generatorsis genuinely running (a generated composition plan resolvedCustomer), not just that the packages restored. Verified directly:~/.nuget/packages/compono/0.1.0/analyzers/dotnet/cs/Compono.Generators.dllis present in the installed package. (Along the way, found and cleaned up a local-machine-only artifact: an earlier Phase 8 acceptance-test session had packed scratch test packages to a local feed at a fabricated1.0.0version, which NuGet's global package cache — shared across all restores on this machine — had retained; it briefly caused a real restore to prefer that phantom1.0.0over the real published0.1.0viaNU1605version-downgrade protection. Confirmed via nuget.org directly that no1.0.0was ever actually published, removed the four stray cache entries, and the real install then succeeded cleanly. Not a nuget.org or release issue — a side effect of this plan's own earlier local testing on this specific machine.) - Verified the documentation site is live at its public URL with the final nav:
https://layeredcraft.github.io/compono/returns200, and the deployed nav still reflects both post-Phase-7 fixes (collapsible tree, no horizontal tabs — #56/#57).
Phase 9: Final MVP documentation and closeout¶
Status: Done
Checkpoint: Milestone 8 / MVP Complete — docs/mvp.md reflects the real, final outcome and every MVP success criterion has an honest verdict.
-
docs/mvp.md's Milestone 8 section: outcome, links to all four ADRs and this plan, exit-criteria results. Added a full Outcome subsection recording the real0.1.0release, every scoped bug-fix PR found along the way (#56-#58, #61), the deliberately deferred doc-comment sweep, and theLayeredCraft/.github#4gap found and resolved during this same closeout. - Final MVP success-criteria review (
docs/mvp.md's "Success Criteria" list) — each marked met/partially met/unmet, honestly, against real evidence from this milestone and Milestone 7's. All seven marked Met, each with its own real evidence citation, not a blanket claim. -
docs/adr/README.md/docs/plans/README.md— confirm all rows accurate.docs/adr/README.mdwas already accurate (ADR-0030-0034 allAccepted).docs/plans/README.mdwas stale — PLAN-0008 still showedNot Starteddespite beingIn Progress/Donethroughout this entire milestone; fixed toDone. design pass).
Package-readiness checklist¶
The concrete, executable form of ADR-0031's package-readiness bar — that ADR states the long-lived policy (what must be true of a package before release), this plan owns how it gets verified. Applied to all five packages — four independently published, plus Compono.Generators, verified by content inspection inside Compono.nupkg rather than an independent pack (it's IsPackable=false, per ADR-0003). Phase 0's Tasks above are this checklist:
-
publish-preview.yaml'sprereleaseIdentifierrenamed fromalphatopreview. -
Directory.Packages.props'sPackageVersionentries forNSubstitute/Bogus/xunit.v3.extensibility.core(the three third-party dependencies that flow into a publishable package's own.nuspec) declare a deliberate tested range (tested minimum, exclusive next-untested-major upper bound), per ADR-0031 Amendment 1 — not a bare unbounded floor and not a blanket exact pin. - Each integration package's generated
Componodependency (from its own<ProjectReference>) is exact-pinned in the packed.nuspec, not left at the same bare-version minimum floor. -
PackageTags/PackageReleaseNotesset once inDirectory.Build.props(one uniform value for all five packages, not per-project) —PackageReleaseNotespoints at the repo's stable releases index ($(PackageProjectUrl)/releases), not a per-version tag URL, since most published preview versions have no matching GitHub Release/tag to link to (see Phase 0's Tasks above). - Each of the four publishable packages has a per-package
<Title>(human-friendly, distinct from the raw package ID) — not centralized, since it's genuinely per-package likeDescription. -
Microsoft.DotNet.PackageValidationenabled (EnablePackageValidation=true), with no static baseline value — instead, a locally-controlled CI job in this repo (not insidepublish-preview.yaml/publish-release.yaml, which are opaqueuses:calls to the shareddevops-templatesworkflow with no hook point for this) queries nuget.org for each package's latest published version and runsdotnet pack -p:PackageValidationBaselineVersion=<prior-version>as a pre-merge PR gate, skipped onbreaking-change-labeled PRs. -
.github/release-drafter.yml'sversion-resolverremapsbreaking-changefrommajortominor(itscategoriesentry is unchanged) — otherwise a labeled breaking-change PR would silently resolve to1.0.0, exiting0.xby accident. - The same locally-controlled CI job asserts each publishable
.nupkg's file listing matches the expected per-TFM shape (lib, README, icon, no stray build artifacts;analyzers/dotnet/csforComponospecifically, containingCompono.Generators.dll— this isCompono.Generators' own verification, not a separate pack of it). - Local-feed packed-consumer smoke test covers the four publishable packages together, as a standing CI gate.
-
PrivateAssets/analyzer transitivity verified for every package. - Dependency license spot-check against
Directory.Packages.props's current set (Phase 0) — the standing review-habit note for future dependency changes lives in Phase 6'scontributing.md, not here.
Release-readiness checklist¶
-
publish-preview.yaml's identifier renamed fromalphatopreview(Phase 0) — the actual "does this look done" gate is the manually-published GitHub Release below, not this rename. - All four publishable packages passed the locally-controlled
Microsoft.DotNet.PackageValidationPR gate (Phase 0) before this version merged tomain— verified against a real CI run immediately beforev0.1.0was cut:package-validation.yaml's "Resolve nuget.org baseline versions" step resolved a real prior version (0.1.0-preview.45) for all four packages, not a first-ever/inert<none, first publish>run. -
.github/release-drafter.yml'sbreaking-changelabel resolves to a minor bump (Phase 0's static config change) — not yet confirmed against a real labeled PR: no PR in this repo's history has ever carried thebreaking-changelabel (gh pr list --state all --label breaking-changereturns none), so this specific mechanic was never actually exercised end-to-end beforev0.1.0shipped (which itself carried no breaking-change PR). Recorded honestly as an unexercised gate, not silently checked off — it'll get its first real test whenever the first genuine breaking-change PR lands, per ADR-0031's0.X+1.0policy. - Local-feed packed-consumer smoke test passes for the four publishable packages together (Phase 0) — confirmed passing on the same pre-release CI run referenced above.
- Package-contents inspection CI step passes for the four publishable packages, and separately confirms
Compono.Generators.dllis present insideCompono.nupkg'sanalyzers/dotnet/cs(Phase 0) — same pre-release CI run; also independently re-verified post-publish in Phase 8's own task by inspecting the actual installed package from nuget.org. - Every public member has an XML doc comment — actually enforced as a build failure (Phase 0's new CI job runs
dotnet build -p:WarningsAsErrors=CS1591for the four publishable packages specifically), not merely assumed fromGenerateDocumentationFile=true. That existingDirectory.Build.propssetting deliberately leavesCS1591a warning, not an error, for normal local/CI builds (see its own comment there) — a warning alone doesn't faildotnet build, so nothing before Phase 0 was an actual enforcement gate on its own. Phase 1's reference-generation gate is a second, tool-dependent check, not a substitute for this one. Confirmed passing on the same pre-release CI run. -
docs/roadmap/index.md's compatibility framing and every affected Package Guide are current with the version about to publish —docs/roadmap/index.mddoesn't hardcode a version number (states "Compono's shipped package set" generically), so nothing there goes stale per release. - If this release includes a breaking-change-labeled PR, the generated release notes carry the "⚠️ Breaking Changes" section (release-drafter's
categoriesgrouping renders it automatically — nothing to check if no such PR is included this time; the absence of the section is itself the "nothing broke" signal, per ADR-0031). Confirmed:v0.1.0's actual published release notes have no such section, correctly, since no breaking-change PR was included. - Documentation site deploys successfully from the same
maincommit being released (verified viadocs.yml, not a separate manual check) — confirmed:docs.ymlran and succeeded on commit821527d(themaincommitv0.1.0was tagged from, right after PR #59 merged), before the release was published.
Public-preview acceptance checklist¶
Run during Phase 8, using only public artifacts (no internal-repo knowledge, no ADR references, no local source checkout beyond what's needed to author the fresh test project):
- A stranger can find Compono via GitHub search or nuget.org search (package tags/description, Phase 0) and land on a README that states what it is, why it exists, and what to do next within seconds of scrolling. Nuget.org's own search index confirmed returning
Componofor a plaincomponoquery, title "Compono — Core Composition Engine," tagstesting/test-data/source-generator/dotnet. One real gap found and fixed separately: the search result's own description text cited two internal-only paths (docs/manifesto.md, retired to a tombstone in Phase 5;design-decisions.md, an internal engineering-workflow skill file never public at all) — the literal first thing a stranger sees before ever reaching the README. Fixed in PR #61 for all four packages'Descriptionfields, same bug class PR #58 already fixed once elsewhere. Ships in the next published version, notv0.1.0itself. -
dotnet add package Compono/Compono.XunitV3(the common-case pair, per Getting Started) succeeds from a clean project against published nuget.org packages — verified twice: once pre-publish against0.1.0-preview.45, once post-publish against the real stable0.1.0. - The five-minute Getting Started path succeeds verbatim, start to finish, with no undocumented step — including the "remove
[Shared]and rerun" step, which correctly produces an ordinary xUnit assertion failure, not a composition crash, exactly as documented. - At least one How-to Guide task, followed literally, succeeds —
register-a-type.md's three examples (direct registration, nested dependency resolution, profile-wired registration), 3/3 passed. - At least one Cookbook recipe, copy-pasted, works without modification —
generate-a-realistic-email.md, verbatim. - The relevant Package Guide's "when to install" section is sufficient to decide whether to add
Compono.NSubstitute/Compono.Boguswithout reading any other page — confirmed againstpackages/compono-bogus.md, which states the decision criterion and exact install command with nothing else to look up. - A deliberately-triggered compile-time failure (e.g. an ambiguous-constructor type composed directly,
CMP0001) produces aCMP-coded build error, and Troubleshooting'scommon-errors.mdresolves it by that code — reproduced withHttpClientcomposed directly, exactly the doc's own example. One real bug found and fixed separately: the actual shipped message carried a trailing internal-path clause not present in whatreference/diagnostics.mddocuments; the checklist item itself still passed (the code lookup resolves it regardless), the message defect was fixed in PR #58. - A deliberately-triggered runtime composition failure (e.g. a genuine construction cycle) produces a readable, path-annotated error and a reproducible seed via
CompositionDiagnostic(src/Compono/CompositionDiagnostic.cs— no diagnostic-code field, unlike the compile-time case above), and Troubleshooting'scommon-errors.md/faq.mdresolves it by symptom, not by code — these are two different failure modes with two different resolution paths, not one combined check. Reproduced with a missingIRuleProviderregistration:Console.WriteLine(exception.Diagnostic)rendered the exact tree-shaped formatcommon-errors.mddocuments, including a workingSeed:line. - The API reference (
reference/api/) answers at least one "what does this method do / what does it throw" question the guides don't already answer inline —CompositionException/CompositionDiagnostic's reference pages document the precise constructor/exception contract at a level of detail no Concepts or How-to page repeats. - Every link followed during the acceptance pass resolves (no 404s, no dead cross-references) — every page touched (Getting Started, the How-to page, the Cookbook recipe, the Package Guide, Troubleshooting) spot-checked live at
https://layeredcraft.github.io/compono/, all200. - Nothing in the acceptance pass required reading
docs/adr/,docs/plans/, ordocs/research/— confirmed; the only place ADR/ internal-repo knowledge entered this milestone's work was investigating theCMP0001/public-api.md/package-Descriptionbugs above, which is root-cause debugging of a defect, not a step the acceptance walkthrough itself required.
Exit criteria¶
Milestone 8 — and the entire MVP — is complete only when every item below is true, checked honestly (met / partially met / unmet is an acceptable outcome for the final MVP review in Phase 9, but every item here must be individually resolved, not left ambiguous):
- All four publishable
0.xpackages are available on nuget.org and installable in a clean project;Compono.Generatorsis present and running inside the installedComponopackage. Verified post-publish against real0.1.0, including a real composition smoke test (Phase 8). - Packed-package consumer verification passes (Phase 0's local-feed gate, plus Phase 8's post-publish nuget.org verification). Both confirmed.
- The documentation site is publicly deployed and live at its stated URL.
https://layeredcraft.github.io/compono/returns200, current nav confirmed. - The root
README.mdaccurately directs each audience (newcomer, AutoFixture migrator, contributor) to its right next step (Phase 2 README review, Phase 6'sCONTRIBUTING.mdlink addition). - The five-minute Getting Started flow succeeds from a clean project (Phase 8's acceptance test) — verified verbatim, including the "break it" step.
- All public APIs have useful XML documentation (Phase 0/1's gates —
CS1591-as-error enforced in CI). Two real content-quality gaps found and fixed, not just presence-checked:CMP0001's message and three flagship types' doc comments citing an internal, retired page (PR #58). A further ~85 staledocs/architecture.md/docs/performance.mdreferences remain across the core package's doc comments — inert code-styled text, not broken links, deliberately deferred (see Phase 8's own notes) rather than silently left unrecorded. - API reference (
reference/api/) and diagnostics reference (reference/diagnostics.md) are published (Phase ⅓). - Both required samples (Basic Usage, ASP.NET API) build in CI (Phase 4).
- Benchmark methodology and results are published, without unsupported comparative claims (Phase 5, per ADR-0030 Amendment 2).
- Known limitations and the
0.xcompatibility policy are explicit and discoverable from multiple entry points, not one obscure page (Phase 3, per ADR-0031). - Contribution, security, and release guidance exist — both root
CONTRIBUTING.md(linked fromREADME.md) anddocs/contributing.md, plusSECURITY.md,CODE_OF_CONDUCT.md, issue/PR templates (Phase 6, viaLayeredCraft/.githubinheritance for the latter three). Found genuinely unmet during this milestone's own closeout review — Phase 6's record claimed the org-levelSECURITY.md/CODE_OF_CONDUCT.mdfix (LayeredCraft/.github#4) had already landed; it was still open, so the live inherited files were still the original weaker versions. The user merged that PR during this closeout (2026-08-07); re-verified directly against the live org repo afterward — genuinely met now. - The AutoFixture migration guide is publication-ready (Phase 3).
- Every package and documentation link is valid (Phase 7's
mkdocs build --strictgate, now enforced going forward viavalidation.links.anchors: warn). - The public-preview acceptance test (above) passes end to end using only public-facing instructions (Phase 8) — every item passed; two real bugs found along the way were fixed in their own scoped PRs (#58, #61) rather than silently absorbed.
-
docs/mvp.md's MVP-wide Success Criteria are reviewed one final time and each honestly marked met, partially met, or unmet (Phase 9) — seedocs/mvp.md's Success Criteria section and Milestone 8's own Outcome subsection.
Critical Files¶
- Every path listed in each phase's Tasks above, under
docs/— most already exist as Phase 5 (PLAN-0007) stubs; this plan replaces stub content with real content, not new files, except:docs/index.mdand repository-rootREADME.md(pre-existing real pages this plan corrects);samples/*'s real runnable projects (new, Phase 4); rootCONTRIBUTING.md/docs/contributing.md/SECURITY.md/CODE_OF_CONDUCT.md/issue-template/PR-template content (new, Phase 6);reference/api/(new, Phase 1, generated);docs/reference/diagnostics.md/glossary.mdcontent (Phase 3). mkdocs.yml— nav updated per phase as content lands; final pass in Phase 7. Phase 1 adds the "API Reference" sub-section under "Reference" (one entry per package'sindex.mdlanding page; the ~150 generated member/type pages per package are reachable through cross-links, not individually listed in nav)..config/dotnet-tools.json— new local tool manifest (Phase 1), pinningdefaultdocumentation.console1.2.5..github/scripts/generate-api-reference.sh— new (Phase 1): regeneratesdocs/reference/api/<package>/for the four publishable packages from their compiled net10.0 assembly + XML doc file, core-first so the three integration packages' cross-package<see cref>s resolve locally, plus the#ctor-filename post-processing fix described in this phase's Notes..github/workflows/docs.yml— the drift-detection gate was initially a separateapi-reference.yamlworkflow, deleted during PR #47 review at the user's direction: with noneeds/workflow_runlink between two independently-triggered workflows, a failing drift check could never actually stopdocs.ymlfrom deploying stale/incorrect content. The regenerate-and-diff-check steps now run asdocs.yml's own first real steps, sequentially beforemkdocs build— the site only ever builds fromdocs/reference/apicontent already confirmed fresh in the same job.docs.yml's trigger paths expanded to include the four publishable packages'src/paths (previouslyapi-reference.yaml-only) so a source-only PR still runs the check..github/workflows/publish-preview.yaml—prereleaseIdentifierrenamed fromalphatopreview(Phase 0). No other change to either publish workflow — the API-compatibility baseline check lives in a new, separate, locally-controlled CI job instead (below), not inside eitheruses:-based publish workflow.- A new CI workflow/job (this repo's own, not
devops-templates), triggered onpull_requestincludinglabeled/unlabeled(not just the default activity types) — packs the four publishable packages, runs the nuget.org baseline lookup andMicrosoft.DotNet.PackageValidationcheck (skipped onbreaking-change-labeled PRs, evaluated against the label state current at its own run), asserts.nupkgcontents, and runsdotnet build -p:WarningsAsErrors=CS1591for the four publishable packages, as a pre-merge PR gate (Phase 0). .github/release-drafter.yml—breaking-changeremapped frommajortominorinversion-resolver(Phase 0).Directory.Packages.props—PackageVersionentries converted to exact-pin bracket syntax (Phase 0).src/Compono.XunitV3/Compono.XunitV3.csproj,src/Compono.NSubstitute/Compono.NSubstitute.csproj,src/Compono.Bogus/Compono.Bogus.csproj— each package's generatedComponodependency overridden to exact-pin syntax at pack time (Phase 0).Directory.Build.props— package-validation and tags/release-notes properties added, shared across all five packages (Phase 0).Compono.slnx— both new sample projects added (Phase 4).docs/adr/0034-benchmark-suite-strategy-and-redesign.md— new (Phase 5 Part B): the benchmark-suite redesign decision.benchmarks/Compono.Benchmarks/— fully restructured (Phase 5 Part B, done) intoModels/,Baselines/,ConsumerScenarios/,ExternalComparison/,FeatureOverhead/,Scalability/,SourceGeneration/(five categories —ImplementationStrategies/was implemented, then removed per ADR-0034's Amendment), replacing all 8 existing benchmark files per ADR-0034.Compono.Benchmarks.csprojgainsProjectReferences toCompono.NSubstitute/Compono.Bogus, its existingCompono.Generatorsreference is now also a normal compile reference (not analyzer-only), and it gains conditionalBasic.Reference.Assemblies.Net100/Net110package references.src/Compono.Generators/Compono.Generators.csproj— gainsInternalsVisibleTo="Compono.Benchmarks"(Phase 5 Part B, done), soSourceGeneration/GeneratorDriverBenchmarks.cscan constructComponoIncrementalGeneratordirectly.docs/documentation-architecture.md— Open Items section already updated to reflect all six resolutions as part of this design pass; further updated in place as content lands and stub statuses flip to real.docs/mvp.md— Milestone 8 section (Phase 9).
Test Plan¶
Documentation content itself has no automated test suite beyond the link/snippet-validation gates in Phase 1 (API reference drift) and Phase 7 (site-wide link/snippet check). Both samples (Phase 4) get real, buildable projects with, where practical, their own tests demonstrating the pattern they showcase, matching testing.md's bar for any real code this plan produces. Package-readiness changes (Phase 0) are verified by the new CI gates themselves (package validation, contents inspection, local-feed restore) rather than a separate hand-run test plan. The redesigned benchmark suite (Phase 5 Part B) is verified by actually running it (dotnet run -c Release) and confirming every category produces real, sane results before architecture/current/performance.md is rewritten from them — a BenchmarkDotNet project has no pass/fail test suite of its own; its correctness is verified by inspection of its results, not by testing.md's xUnit-based test conventions, which don't apply to benchmarks/. Phase 8's acceptance checklist is this plan's actual end-to-end verification.
Notes¶
The original PLAN-0008 draft (produced by PLAN-0007 Phase 5) was a flat, unphased backlog, deliberately left unphased pending this design pass — see that phase's own Notes in PLAN-0007 for why. This rewrite is that design pass: same total scope (every page, every package-readiness item, every repository-process artifact the draft listed), now split into ten independently-shippable phases (0-9), with four new/amended ADRs (ADR-0031, ADR-0032, ADR-0033, and ADR-0030 Amendment 2) settling every decision the draft's Tasks section left implicit. Anything discovered while actually executing a phase that suggests the architecture (not just this plan's task list) needs to change is recorded here, then routed to tasks/design.md for an ADR-0030 Amendment — per this plan's own Scope section above.
Phase 0 (2026-08-04)¶
Mid-implementation correction: ADR-0031's original "Exact, tested dependency pins during 0.x" bullet applied one blanket exact-pin rule to both the internal Compono-family lockstep dependency and Compono's external third-party dependencies (Bogus, NSubstitute, xUnit). Caught before either shipped to a real consumer — the internal case (lockstep Compono pin inside each integration package's .nuspec) is correct and unchanged; the external case was wrong and is corrected in ADR-0031 Amendment 1: third-party dependencies that flow into a publishable package's own .nuspec now get a deliberate tested range (tested minimum, exclusive next-untested-major upper bound) instead of a blanket exact pin or an unbounded bare-version floor. Directory.Packages.props and this phase's Tasks/Package-readiness checklist above reflect the corrected policy, not the original text.
Also caught during verification: dotnet test's VSTest-style --filter flag produces zero matched tests against an MTP v2 host (exit code 5, "Zero tests ran") — MTP's actual simple-filter syntax is --filter-not-class/--filter-class/etc., passed after a -- separator. The local-feed packed-consumer smoke test task above (and package-validation.yaml) uses -- --filter-not-class "Compono.XunitV3.SampleTests.FailingCompositionTests", verified locally (16/16 tests pass).
Phase 2 (2026-08-04)¶
All 19 skeleton pages across Getting Started, Concepts, and How-to Guides replaced with real content, verified against the actual public API (Composer, CompositionBuilder, ICompositionProfile, ICompositionValueProvider, [Shared], [Compose]/[Compose<TProfile>]) rather than the design-target examples in docs/public-api.md alone — code snippets are drawn from or cross-checked against real test files (test/Compono.Tests/*, test/Compono.XunitV3.SampleTests/*). docs/index.md's and README.md's stale Compono.Create(builder => ...) example both fixed to the real Composer.Create(builder => ...) API. README.md also gets a new "Getting Started" section (links to the docs site's Getting Started and the AutoFixture migration guide) — no CONTRIBUTING.md link added yet, since that file doesn't exist until Phase 6, which owns adding this link per its own Tasks list. README.md's existing Performance section already complied with ADR-0030 Amendment 2's benchmark-claims policy (no comparative "Nx faster than AutoFixture" phrasing) — left as-is.
Verified with a real uv run mkdocs build --clean --strict: exits 0, no new warnings introduced by this phase's pages (one dangling internal link this phase itself introduced — ../reference/api/index.md in concepts/profiles.md — was caught by this same build and corrected to ../reference/api/Compono/index.md, the real landing page Phase 1 generated). The handful of remaining INFO-level anchor mismatches in migrating-from-autofixture.md/research/0001-autofixture-comparison.md/ plans/0007-milestone-7-dogfooding.md predate this phase and are out of scope here.
Phase 1 (2026-08-04)¶
Bake-off result: DefaultDocumentation (DefaultDocumentation.Console 1.2.5) wins, run against a representative slice — all four publishable packages' real net10.0 assemblies + XML doc files, not a synthetic sample — scored against every ADR-0032 criterion:
xmldocmd(2.9.0) eliminated outright on maintenance/TFM compatibility, the first criterion it failed: its own package ships host builds only fornet6.0/net7.0, and running it (viadotnet tool run, any host) againstnet10.0-targeted assemblies throwsFileNotFoundException: Could not load file or assembly 'System.Runtime, Version=10.0.0.0...'— a hard failure, not a degraded-output case. No amount of further evaluation on the other criteria was relevant once this failed.DefaultDocumentationpassed every other criterion against the real API surface: generics (CollectionPlanCache<T>,ICompositionPlan<T>), overloads (Register<T>'s two overloads got distinct, correctly cross-linked pages), inheritance (ComposableAttribute : Attributerendered with the full chain), attributes, nullable signatures (Nullabilityenum's doc came through verbatim),<exception>/<returns>/<remarks>/<typeparam>all rendered correctly (verified againstComposer.CreateMany<T>(int)andCompositionBuilder's real doc comments). Ships anet10.0host build already (current, not lagging the repo's own TFMs) and is under active release (39 published versions, 1.2.5 current). Deterministic: two consecutive runs against the same input produced byte-identical output, verified both for a single package and for the full four-package generation run.- Nine real defects found and fixed during wiring, not left as accepted gaps — the first two caught before the PR opened, the other seven by PR #47's automated review (
chatgpt-codex-connector, across five review passes), addressed in the same PR rather than deferred: - Cross-package
<see cref>resolution. Generating each package standalone, a<see cref="Compono.Composer"/>inCompono.XunitV3's XML docs fell back toDotnetApiFactory, which treats any type it doesn't recognize as a BCL type and links to a fabricatedlearn.microsoft.com/en-us/dotnet/api/compono.composerURL (404). Fixed by generatingComponofirst with--LinksOutputFilePath/--LinksBaseUrl, then feeding that links file to each integration package's--ExternLinksFilePaths— verified: the rendered link becomes[Composer](../Compono/Compono.Composer.md ...), a real local page. #in generated filenames.DefaultDocumentationnames a parameterless constructor's page after the raw CLR metadata name (Compono.ComposableAttribute.#ctor.md) —#is the URL fragment delimiter, so a realmkdocs build(not just eyeballing the Markdown) parsed links into that filename as truncated-path-plus-fragment and reported it as a broken link..github/scripts/generate-api-reference.shrenames every such file to.ctorpost-generation and rewrites the handful of other generated pages that link to it, confined toComponocore in practice (the only package with a documented parameterless constructor).- Bogus links for internal Compono types (PR #47 review). The same
DotnetApiFactoryfallback as (1) fires for any<see cref>on a public member's XML docs that names an internal Compono type (e.g.CompositionBuilder's docs mentionCompositionConfiguration) — no local page exists for it (Public-only generation) and it's never in--ExternLinksFilePathseither, so it falls to the same fabricated, deadlearn.microsoft.com/en-us/dotnet/api/compono.*URL as (1). 38 such dead links across 29 files, not the single isolated instance (SeedAsNullable) originally noticed and wrongly framed as a hover-only cosmetic quirk during initial verification — the scale only became clear from PR review's exhaustive scan. Fixed by post-processing every generated page: a link whose target matches that URL pattern is rewritten to plain inline code of its (unescaped) type name instead of a dead link, since there is no real page to point it at without publishing internal implementation types, which would contradict generating only the public surface in the first place. - Stale in-page anchor names on renamed
#ctorpages (PR #47 review). Fix (2)'s filename rename corrected every href pointing at a constructor-overload page, butDefaultDocumentation's own same-page anchornamevalues on that page are built as "the part of the filename after its own last#,#, member id" (name='ctor.md#Compono.ComposableAttribute.ComposableAttribute()') — internally consistent only on the assumption the filename still contains a literal#acting as the real fragment delimiter. Once that#is renamed away, the anchor's stalector.md#prefix no longer matches the (already-correct) href fragment, so a deep link into a specific overload landed at the top of the page instead of that overload's section. Fixed by stripping the stale prefix from the renamed page's own anchors in the same post-processing pass. <paramref>/<typeparamref>self-references target the wrong page (PR #47 second review pass). A member's own parameter/type-parameter doc (e.g. a constructor's "diagnosticis null" exception doc, orRegister<T>'s ownTtypeparam doc) always links back to its containing type's page, but the anchor lives whereverOverloadsGeneratoractually placed that specific overload once a member has more than one (its own dedicated page, not the type's). Flagged on theCompono.CompositionExceptionconstructor case specifically; verified to be the generalOverloadsGeneratorinteraction, not constructor-specific — 92 mismatched fragment links across all four packages (69 inComponoalone, e.g.Compono.CompositionBuilder.Register's ownT/factoryparameter docs). Fixed generally, not with another special case: the generation script now builds an anchor-id → actual-file map per package directory (from every<a name='...'>in that directory) and rewrites any same-package link whose target file doesn't actually contain the anchor it points at. 0 mismatches remain after the fix, confirmed by the same scan that found the original 92.- Bogus links for third-party dependency types (PR #47 third review pass). The same
DotnetApiFactoryfallback as (3), but for types fromBogus/NSubstitute/xUnit.v3(e.g.Bogus.Faker,NSubstitute.Substitute.For,Xunit.v3.IDataAttribute) — fix (3)'s pattern only matchedcompono.*, so it left every non-Compono fallback link untouched. Generalized fix (3)'s pattern from acompono.*blocklist to asystem.*/microsoft.*allowlist — the only namespaceslearn.microsoft.com/en-us/dotnet/api/ever actually resolves — so it now catches every non-BCL fallback regardless of which package the referenced type belongs to, current or future. - The generalized fix (6) itself had a link-text parsing bug, caught before pushing (own verification, not another review round):
[^\]]+for the link-text capture group stops at the first literal], but a signature likeNSubstitute.Substitute.For's array parameters renders its display text with escaped brackets (...\[\],System\.Object\[\]\)) — the regex silently failed to match at all rather than matching wrong, so the fix from (6) missed exactly the two links it was written to catch until this was found. Fixed by changing the text-group pattern to(?:[^\]\\]|\\.)+(an unescaped non-]character, or a backslash-escaped pair), which treats\]as a literal character rather than a false terminator. - CI architecture changed on user direction, mid-review: the drift-detection gate was originally a separate, independently-triggered
api-reference.yamlworkflow with noneeds/workflow_runlink todocs.yml— meaning a failing drift check could never actually stopdocs.ymlfrom building and deploying stale/incorrect content, since the two workflows had no ordering relationship at all. Per the user's explicit preference (sequential dependency over two disconnected workflows — the local-generation-only alternative was rejected since it would reverse ADR-0032's explicit "CI must catch drift" Decision Outcome without an ADR amendment),api-reference.yamlis deleted and its regenerate-and-diff-check steps moved intodocs.yml's ownbuildjob, as its first real steps, sequentially beforemkdocs build.docs.yml's trigger paths expanded to include the four packages'src/paths so a source-only PR (nodocs/change) still runs the check. mkdocs buildnever actually enforced ADR-0032's broken-link requirement (PR #47 fourth review pass).docs.ymlranmkdocs build --cleanwith no--strict, andmkdocs.ymldoesn't enable strict validation either, so ADR-0032's "CI fails the build when... broken internal links" bullet was unenforced by anything — a warning never fails a plainmkdocs build. Enabling--strictsurfaced exactly 4 pre-existingWARNING-level broken links (the.claude/skills//.agents/skills/cross-references from ADR-0014/ 0015/0016/0022, already visible as noise in every earlier verification pass in this Notes section) — not a path-depth bug:.claude/skills/is outsidedocs_direntirely, so no relative-path correction could ever make these resolve inside the built site. Per the user's explicit direction (weighed against deferring to Phase 7, which already owns a "site-wide broken-link check" as its own task — enabling--strictnow doesn't preclude that later, more comprehensive pass), fixed both at once:docs.ymlnow runsmkdocs build --clean --strict, and the 4 ADR cross-references were converted from a dead hyperlink to plain, unlinked text (the engineering-workflow skill's `design-decisions.md` reference) — a mechanical fix to link syntax only, the Decision/Rationale/Consequences prose itself is untouched, consistent withdesign-decisions.md's own ADR-immutability rule. Verified:mkdocs build --clean --strictnow exits 0 (previously aborted with exactly those 4 warnings).docs.yml's trigger paths missedDirectory.Build.targets(PR #47 fifth review pass).Directory.Build.targetsis auto-imported into every project the same wayDirectory.Build.propsis (just after the project body instead of before) — including the four publishable packages this workflow's own drift-check step builds — but onlyDirectory.Build.propswas in the triggerpathslists. A PR touching onlyDirectory.Build.targets(e.g. changing the test-projectCS1591NoWarnscoping, or a future rule that does affect the publishable packages) would silently skip both the drift check and the site rebuild. AddedDirectory.Build.targetsto bothpull_requestandpushpath lists, alongsideDirectory.Build.props.- One cosmetic, accepted gap: link
titleattributes (hover tooltips) carryDefaultDocumentation's markdown-escaped\</\>verbatim, since MkDocs/python-markdown doesn't re-process escapes inside a link's title string — visible only on hover, never in link text or navigation, and consistent with ADR-0032's already-accepted "less polished... in some edge cases" Negative Consequence. Not fixed; recorded here rather than silently absorbed. - Verified with a real
mkdocs build, not just inspecting generated Markdown (documentation.md's "do real manual verification" bar, applied here even though this isn't source-generator-facing — same principle, generated content the tests don't otherwise exercise): initially verified non-strict (uv run mkdocs build --clean) against the full site including all four packages' generatedreference/api/content, which built clean apart from four pre-existing, unrelated broken links to.claude/skills//.agents/skills/paths from ADR pages. Defect 8 below then enabled--strictand fixed those same four links — the current, final state isuv run mkdocs build --clean --strictexiting 0 with zeroWARNINGs, not the earlier non-strict result. - Missing-XML-doc-comment gate:
DefaultDocumentationhas no independent detection of its own (--IncludeUndocumentedItems=Falsejust silently omits an undocumented public member from output, it doesn't fail). The actual enforcement is Phase 0's pre-existingdotnet build -p:WarningsAsErrors=CS1591gate inpackage-validation.yaml, which already blocks a missing doc comment on any public member before this workflow's regeneration step ever runs — satisfies ADR-0032's "where the tool supports it" qualifier rather than leaving a real gap. - Net10.0 build output only (not net11.0) — the two TFMs share the same public API surface, and generating from both would either double the work for no additional coverage or require picking one to diff against anyway.
Phase 3 (2026-08-05)¶
Package Guides written directly against each package's real public API (.csproj Description, NSubstituteOptions, BogusConventions' ten-name allowlist) and cross-linked into the existing Concepts pages rather than re-explaining them. troubleshooting/common-errors.md splits into two index dimensions per its own skeleton brief: by CMP diagnostic code (compile-time, linking into reference/diagnostics.md) and by symptom (runtime CompositionException/CompositionConfigurationException, which carry no diagnostic code at all — only a path-annotated message and a reproducible seed via CompositionDiagnostic). reference/diagnostics.md covers all twelve CMP0001-CMP0012 codes from src/Compono.Generators/Diagnostics/DiagnosticDescriptors.cs directly (message, cause, fix each), not a partial set.
Verified with a real uv run mkdocs build --clean --strict (this phase isn't source-generator-facing, but the same "do real manual verification" bar applies to generated/cross-linked documentation content per documentation.md): exit 0, zero new broken-link warnings introduced by this phase's own new pages. Caught and fixed one real defect during this verification: troubleshooting/faq.md's new link into reference/diagnostics.md#cmp0001-... used a double hyphen where the source heading's em dash (CMP0001 — Ambiguous construction path) needed to collapse to a single hyphen — confirmed against the actual generated id attribute in the built HTML, not guessed. The same double-hyphen mistake turned out to already exist in docs/migrating-from-autofixture.md from before this phase (three links into research/0001-autofixture-comparison.md's Finding 4/7/9 anchors) — fixed in the same pass as this phase's "polish the migration guide" task, since it's a real dead link the same verification step surfaced, not a structural change to the guide's content. Three pre-existing, unrelated anchor mismatches remain (a plans/0008 self-link into plans/0007's Phase 5 section, and two research-doc-internal Finding cross-references) — all INFO-level, not WARNING, so --strict doesn't fail the build on them; left for Phase 7's dedicated site-wide link pass rather than fixed here, since none are in a file this phase's Tasks own.
Migration guide rewrite, same day, on direct user feedback. The verification pass above shipped a docs/migrating-from-autofixture.md that was still, structurally, an internal Milestone 7 artifact wearing a public-docs skin — accurate per the original "content-stable" framing (no prose was wrong), but the wrong bar for a public-facing migration guide. User feedback, in two rounds, redirected this task's own scope: "shouldn't mention [the source repository] by name at all... should read like the rest of the completed docs... pure documentation on how to do something with good examples" first, then a full structural spec (title, section ordering, a migration-mindset section, a quick concept map, a specimen-builder decision table, a migration checklist, and where to move versus summarize the heaviest implementation detail). Rewritten per that spec — see the Phase 3 Tasks entry above for the shape. Verified again with a fresh uv run mkdocs build --clean --strict: exit 0, and the one new anchor this rewrite introduced (reference/diagnostics.md#cmp0001-...) had the same double-hyphen mistake as before, caught the same way and fixed before commit. Confirmed no literal mention of the source repository's name survives anywhere in the published page (grep -i across the file); docs/research/0001-autofixture-comparison.md is explicitly out of scope for this anonymization — it's the internal research dossier this guide now links out to for readers who want the full evidence trail, and keeping its detail complete there is what makes summarizing safe here.
Phase 4 (2026-08-05)¶
Both samples designed and built directly against ADR-0033's Decision Outcome. One real gap surfaced during implementation, not anticipated by the ADR's "ProjectReference during authoring" build story: a plain ProjectReference chain (sample → Compono.XunitV3 → Compono) does not flow Compono's own Analyzer-only ProjectReference to Compono.Generators transitively — only a packed .nupkg's analyzers/dotnet/cs folder does that. Without a fix, every composed type in both samples failed at test-run time with CompositionException: No ... generated plan could satisfy '<Type>', even though the build itself succeeded silently (the generator just never ran). Fixed by adding a second, separate Analyzer-only ProjectReference to Compono.Generators in each sample's dev-mode ItemGroup — the same mechanism Compono.csproj already uses for its own compilation. This is now the documented pattern for any future project that references Compono packages via ProjectReference rather than PackageReference.
Compono.Samples.AspNetApi is two projects, not one — see the Tasks entry above for why (an MTP test host's own generated entry point and a top-level-statements minimal API's Main can't coexist in the same project). WebApplicationFactory<Program> needed Program exposed via public partial class Program; after the top-level statements, the standard ASP.NET Core minimal-API testing pattern.
Verified for real, not just "tests pass": dotnet build Compono.slnx and dotnet test Compono.slnx both run clean — 865 tests total, 0 failures, 0 warnings, across every project including both new samples. The ASP.NET API sample's endpoint test was watched running a real HTTP request through real minimal-API routing (visible Microsoft.AspNetCore.Hosting.Diagnostics/EndpointMiddleware log output in the test run), not just asserted against.
Post-PR correction, same day (PR #52 CI): the original implementation gave each sample a packed-.nupkg verification mode (-p:UsePackedPackages=true, switching its ProjectReferences to the four publishable packages over to PackageReference from a local feed, per ADR-0033's originally-chosen build story) — this passed every local check, including a manual rm -rf .local-nuget-feed re-test, because the directory had already been created by an earlier local run and every local verification pass silently reused it. CI's genuinely fresh checkout had no such directory, and both samples' nuget.config files listed the local feed as an unconditional package source — NuGet validates that a configured source directory exists at restore time before resolving any package, regardless of whether anything actually needs it, so this failed dotnet restore Compono.slnx with NU1301 for the entire solution, not just the two samples (build, build / build, and package-validation all failed identically). A first fix (always MakeDir the feed directory before restore, gate only the actual pack-and-populate step behind UsePackedPackages) resolved the immediate CI failure and was verified against a genuinely clean tree (rm -rf .local-nuget-feed samples/*/obj samples/*/bin before restoring) — but on direct user feedback ("simplify this and just reference the projects directly"), replaced entirely instead: both samples now use a plain, unconditional ProjectReference with no packed-verification mode of their own, no nuget.config, and no PackToLocalFeed target — see ADR-0033 Amendment 1 for the full account of what changed and why. Re-verified clean on a fresh tree after the simplification: dotnet build/dotnet test Compono.slnx both pass (865/865), and package-validation.yaml's new "Local-feed packed-consumer smoke test - samples" step was removed entirely rather than left referencing a build property that no longer exists.
Every Cookbook code sample was compile-and-run verified against real Compono source before publishing, not merely reviewed for plausibility — documentation.md's "prefer real examples" bar. This caught a real defect in the Freeze a Shared HttpMessageHandler recipe's first draft: composing HttpMessageHandler/HttpResponseMessage directly both hit CMP0001 (each has more than one accessible constructor) — the recipe was rewritten around a single-constructor StubHttpMessageHandler wrapper instead, which is also the more broadly correct pattern (an ambiguous BCL type should be wrapped, not composed directly) rather than a workaround specific to this one recipe.
uv run mkdocs build --clean --strict: exit 0 after every new page (3 samples pages, 5 cookbook recipes + index, 7 best-practices pages) — zero new broken-link/anchor warnings. The five deferred sample stub pages' removal and their mkdocs.yml nav-entry removal were confirmed not to orphan any existing cross-reference into them.