Skip to content

Expose the build model as JSON via --describe and --plan --json (#642) - #662

Open
phmatray wants to merge 9 commits into
Fallout-build:mainfrom
phmatray:feat/642-expose-the-build-model-as-data-fallout-d
Open

Expose the build model as JSON via --describe and --plan --json (#642)#662
phmatray wants to merge 9 commits into
Fallout-build:mainfrom
phmatray:feat/642-expose-the-build-model-as-data-fallout-d

Conversation

@phmatray

@phmatray phmatray commented Aug 27, 2026

Copy link
Copy Markdown

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:

  • BuildGraphUtility already projects targets and all four relation kinds into a schema-versioned document, so this extends that model additively rather than inventing a second one. SchemaVersion stays 1, which also delivers Emit declared build parameters into build-graph.json #499's parameters[].
  • BuildManager.Execute runs ToolRequirementService.EnsureToolRequirements (writes nuget.csproj, shells out to dotnet restore) before the IOnBuildInitialized hooks. 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 --describe and --plan --json, because CommandDispatcher routes bare arguments to the build and reserves :-prefixed tokens for CLI commands.

Plan

  • Task 1: Project tool requirements onto the target model
  • Task 2: Project declared parameters onto the model (also closes Emit declared build parameters into build-graph.json #499)
  • Task 3: --describe emits the model before tool requirements run
  • Task 4: --plan --json emits the resolved execution plan
  • Task 5: Machine-readable error envelope on failure
  • Task 6: Render --help from the shared model

--help is byte-identical to main

Verified by building main in a scratch worktree and diffing the output: the only difference is the version string. --help takes 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:

  • The plan contradicted the executor. BuildExecutor.MarkTargetSkipped only skips when !target.Invoked, so --skip never 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.
  • The gate was in the wrong place. It sat after the IOnBuildCreated extensions, so each misbehaving extension needed its own opt-out — and UpdateNotificationAttribute calls Console.ReadKey(), which would have deadlocked a piped --describe. The gate now fires before any extension runs, and the per-extension opt-out is gone. IsRequested reads each flag from the injected property or the raw arguments, because InjectParameterValuesAttribute is itself an IOnBuildCreated extension.
  • Build-level [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-level toolRequirements[].
  • Type.FullName leaked the runtime version into the contract for constructed generics (List\1[[System.String, …, Version=10.0.0.0, …]]`). Generic arguments are now rendered recursively.
  • Dropped a list field 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 --describe could not be piped into a parser. Three fixes:

  • Fallout.Cli compiled 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 and dotnet tool restore output to stdout. That is provisioning diagnostics, so it moves to standard error.
  • --help no longer aborts when stdout has no console. Console.BufferWidth throws IOException behind a pipe or on a console-less agent, which killed --help after 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 --help crashes with IOException when stdout has no console handle #616.

Also: the emitted documents are newline-terminated, matching SchemaUtility's JSON.

⚠️ ./build.ps1 --describe stays unparseable until a Fallout release carrying the CLI fix is installed, because the script delegates to whatever dotnet fallout is on the machine. Nothing in this PR can change an already-installed tool.

Follow-ups

🤖 Generated with Claude Code

@phmatray
phmatray marked this pull request as ready for review August 27, 2026 11:20
@phmatray
phmatray requested a review from a team as a code owner August 27, 2026 11:20

@ChrisonSimtian ChrisonSimtian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants