Expose the build model as JSON via --describe and --plan --json (#642) - #662
Expose the build model as JSON via --describe and --plan --json (#642)#662phmatray wants to merge 10 commits into
Conversation
ChrisonSimtian
left a comment
There was a problem hiding this comment.
a lot of this code is static methods and static classes. I think this one needs a bit more hands on from a human, the code quality unfortunately trips AI to think all-static is a good and preferred pattern.
might be worth to nudge your agent into the right direction and have it have another go 😊
662c6f8 to
46c4456
Compare
Addresses the review: the introspection code was a static class over ambient state, which the surrounding statics made easy to reach for but which cost correctness and coverage. BuildIntrospectionService is now a sealed class holding the resolved request. BuildManager builds one with `For(build)` and reuses it at the gate, on the failure path and before the outcome tables. Those three sites each used to re-read the flags out of ParameterService, so a single run answered "is this introspection?" three separate times from process-global state that nothing kept in agreement. The invoked targets are read once now too, instead of once for the planner and again inside GetDocument. That also removes a test-only overload, and with it a real gap. Production described a build through GetJsonString(build, targets), which projects the class-level [Requires<T>] requirements; the specs called an overload taking an explicit version, and that one passed no requirements at all. The specs were asserting a document the product never emits, and the projection was uncovered. BuildGraphUtility now takes the version on the same path production uses, and a new spec pins a build-level requirement reaching the document. The argument-based IsRequested stays static: the CLI asks it before a build exists, and it is a pure function of its arguments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VyD69qYha8hBMuja7opyj2
|
Fair call, and it turned out to be hiding a real bug rather than just style. Pushed in e6c358d.
The part worth flagging: dropping the static shape removed a test-only overload, and that overload was papering over a coverage gap. Production described a build through One thing I deliberately left static: I did not touch Full suite green: 849 passing. |
Implements #642.
Closes #642.
Executing the implementation plan task-by-task; the checklist below — and the plan on the issue — are ticked as each task lands. Opened as a draft — will be marked ready after the final task and a code-review pass.
Two constraints shaped the design, both verified in the tree:
BuildGraphUtilityalready projects targets and all four relation kinds into a schema-versioned document, so this extends that model additively rather than inventing a second one.SchemaVersionstays1, which also delivers Emit declared build parameters into build-graph.json #499'sparameters[].BuildManager.ExecuterunsToolRequirementService.EnsureToolRequirements(writesnuget.csproj, shells out todotnet restore) before theIOnBuildInitializedhooks. So the "runs no external tool" criterion rules out another extension; the emission short-circuits above that line.The commands land as the build-side parameters
--describeand--plan --json, becauseCommandDispatcherroutes bare arguments to the build and reserves:-prefixed tokens for CLI commands.Plan
--describeemits the model before tool requirements run--plan --jsonemits the resolved execution plan--helpfrom the shared model--helpis byte-identical tomainVerified by building
mainin a scratch worktree and diffing the output: the only difference is the version string.--helptakes its displayed fields from the shared model (so the two views cannot drift) but keeps ordering — of targets, of each dependency line, and of parameters — from the declarations, because declaration order carries the pipeline reading a human wants.Code review
A review of the branch surfaced 15 findings; all are addressed. The ones worth calling out:
BuildExecutor.MarkTargetSkippedonly skips when!target.Invoked, so--skipnever stops an explicitly invoked target. The projection had no such guard and reported"skip": "via parameter"for it — a consumer driving CI off the plan would conclude nothing runs. Fixed, with a spec that pins the parity.IOnBuildCreatedextensions, so each misbehaving extension needed its own opt-out — andUpdateNotificationAttributecallsConsole.ReadKey(), which would have deadlocked a piped--describe. The gate now fires before any extension runs, and the per-extension opt-out is gone.IsRequestedreads each flag from the injected property or the raw arguments, becauseInjectParameterValuesAttributeis itself anIOnBuildCreatedextension.[Requires<T>]was invisible. That attribute targets Class/Interface only, so class-level requirements could never appear on a target and were missing entirely. They now project to a root-leveltoolRequirements[].Type.FullNameleaked the runtime version into the contract for constructed generics (List\1[[System.String, …, Version=10.0.0.0, …]]`). Generic arguments are now rendered recursively.listfield that could never be false, gave the plan and error documents their own schema-version constants, and de-duplicated the serializer options, version lookup, and skip-reason string that had been copy-pasted.Making standard output actually parseable
Running the feature revealed that the document was correct but unreachable through any shipped entry point — everything upstream of the build wrote to stdout too, so
--describecould not be piped into a parser. Three fixes:Fallout.Clicompiled the build project with its output inherited on stdout. For an introspection request that step now goes to standard error. Measured through the locally built CLI: 12 123 bytes of JSON on stdout, 8 582 bytes of build noise on stderr. Ordinary runs are unchanged — build output stays on stdout.build.ps1/build.sh(and the shipped templates, kept in sync) printed their banner anddotnet tool restoreoutput to stdout. That is provisioning diagnostics, so it moves to standard error.--helpno longer aborts when stdout has no console.Console.BufferWidththrowsIOExceptionbehind a pipe or on a console-less agent, which killed--helpafter the target list and before the parameters. The wrap width is cosmetic, so it now falls back to the existing 90-column cap. This closes--helpcrashes with IOException when stdout has no console handle #616.Also: the emitted documents are newline-terminated, matching
SchemaUtility's JSON../build.ps1 --describestays unparseable until a Fallout release carrying the CLI fix is installed, because the script delegates to whateverdotnet falloutis on the machine. Nothing in this PR can change an already-installed tool.Follow-ups
[Requires]resolves its version over the network insideNuGetPackageRequirement's constructor, before any short-circuit. Pre-existing, affects--helptoo; "runs no external tool" is scoped to this code path.IOnBuildPlannedextension hook this deliberately avoided is worth revisiting once FT-7 ([Foundation] FT-7: Formalize the build extension pipeline (deterministic ordering + typed phases) #312) formalizes the extension pipeline; Declare target effects and gate them at plan time #644 would use it too.🤖 Generated with Claude Code