feat: compile-check learns which names a public API change touches
Merged
HA
3d ago+1773+1773+0123456789012345678901234567890123456789-129-129-012345678901234567890123456789
Summary
- When an assembly's public API changes, compile-check now learns which names the change touches (type and member names), or that no name can tell it. This is the groundwork for compiling only the dependents that write one of those names.
- No scheduling change; the next PR consults the names. Which dependents compile is the same as before.
User Impact
None yet. The VibeLog (ULOOP_DEBUG) surface_checks entries now show changed_name_count, the first 10 changed_names, and unnamed (why no name tells the change), so the next PR's effect can be measured on a real project.
Changes
- Worker (C#):
- Every row of a surface description now carries the type and member it belongs to and a row kind. The same / different answer still comes from comparing the texts in order, so it does not change.
- On "different", the worker collects names from the rows whose text one side holds more often than the other (rows that only moved are not a named change).
- A change a dependent can feel without writing any name is unnamed and carries no names:
- a manifest row;
- for a type both sides have: its flags or attributes, base type, interfaces, type parameters, an instance field of a value type, a parameterless constructor, an indexer;
- an abstract member added;
- an operator, a compiler-generated member, or a member C# binds by pattern (
GetEnumerator,GetAwaiter,Add,Deconstruct,SetResult, …); - every row of an interpolated string handler or collection builder type;
- every row of a type in
System,System.*,Microsoft.CodeAnalysisorMicrosoft.CodeAnalysis.*, on one side or both. The compiler looks these up by name (IsExternalInitforinit,Indexfor^1), so a dependent uses them without writing their names. Only polyfills live there in a project. A project that puts its own extension classes inSystem.*gets every dependent compiled when one of their members changes; the docs and the ADR in the follow-up PRs say so; - more than 2000 names.
- A type added or removed is named by its type name (and its members). Namespace segments are named only when one side lacks that namespace.
- A member whose name starts with an accessor prefix names itself and the name without the prefix (
get_PandP, also for an explicit implementationN.IFoo.get_P). Only its metadata tells an accessor from a method declared asget_Value, which a dependent writes with the prefix. - The base type moves from the
typerow to a row of its own, so the description version goes fromsv1tosv2. No other row's text changes.
- Go:
- The response, the surface record (format version 1 → 2, so an old record is asked again once), and
SurfaceVerdictcarryChangedNames/UnnamedReason. - A different answer that names nothing reads as unnamed, both in the client and where an answer becomes a verdict.
- Only a verdict that is not kept, has names, and has no unnamed reason is a named change. Every other verdict, including the zero verdict of a failed unit, still affects every dependent.
- The response, the surface record (format version 1 → 2, so an old record is asked again once), and
docs/vibe-logs.mddocuments the newsurface_checksfields.
Verification
-
C# (
tests/CompileCheckWorker.UnitTests): 113 tests pass.AssertDifferentnow also checks the answer against the dependent it compiles: the change is unnamed, or the dependent writes one of its names. All existing different-surface cases pass this check.- New tests use dependents that never write the changed type's name (foreach, await, operators,
new()andunmanagedconstraints, casts, boxing, a renamed indexer, an extensionGetAwaiter). - One limitation: adding an interface member with a default body cannot be emitted against the fixture's .NET Standard 2.0 (CS8701). That rule is tested by building the description rows directly.
-
Mutations, each tried on the worker and then restored:
Mutation Fails Empty the pattern-bound member list foreach / await on an unnamed type, extension GetAwaiter,GetEnumeratoradded,SetResultchangedTreat type-shape rows as members base type changed, interface added Treat every type as one-sided 21 tests, including abstract / sealed / ref struct Always name namespace segments nested generic member, new namespace Value-type instance field as member struct field added Parameterless constructor as member parameterless constructor removed Indexer as member indexer added, renamed indexer changed Drop the well-known namespace check well-known type removed ( init), well-known type member changed (^1)Do not keep the name before the accessor prefix method named like an accessor, explicit accessor implementation, and 4 tests whose expected names include accessors Go: a different answer without names reads as named TestVerdictFromWorkerTreatsADifferenceWithoutNamesAsUnnamedGo: record format version stays 1 TestReusableSurfaceVerdictRejectsAVersionOneRecord -
In
cli/dispatcher:go test -race -count=1 ./internal/compilecheck/... ./internal/dispatcher/...passes.
-
In each of the four Go modules:
gofmt -lis empty;go vetpasses;golangci-lint runreports 0 issues.
-
go test ./...passes incli/dispatcherandcli/release-automation, including./internal/architecture/.... Incli/commonandcli/project-runner, one test each fails only because the local sandbox refuses a directory under/tmpand a Unix socket bind; neither module is touched here. -
scripts/check-code-complexity.shandscripts/check-file-length.shpass.