Skip to content

refactor(draft): the drafting core is a package, not a block of package main - #133

Merged
ShocOne merged 1 commit into
mainfrom
refactor/extract-drafting-core
Aug 7, 2026
Merged

refactor(draft): the drafting core is a package, not a block of package main#133
ShocOne merged 1 commit into
mainfrom
refactor/extract-drafting-core

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 7, 2026

Copy link
Copy Markdown
Member

Pure refactor. The blueprint-drafting core moves out of package main into internal/draft, so something other than the CLI can call it.

Why

We are migrating this repo's tests off committed blueprint artefacts and onto blueprints derived at test time. internal/generate's tests cannot call package main, and writing a second deriver for tests would be worse than useless — it would drift from the real pipeline, and the tests would then validate output the pipeline never produces.

What moved

inferAll (now draft.Run), pruneAgainstSDK, writeScenarioDraft, FilterCandidates, Matches. Flag parsing, path resolution and presentation stay in cmd/tfpfgen.

Three judgement calls worth a look:

  • printNotes stayed, as a hook. It cannot simply be hoisted to the caller: it fires between the writing loop and the summary line, so lifting it would print notes after "N blueprint(s) written" — a visible reordering. Options.Notes func([]openapi.Caveat) keeps the formatting in the CLI and the order exact.
  • errNothingToDo is the seam. internal/draft returns ErrNothingInferred; the command wraps it. Message and exit code verified identical: tfpfgen: nothing to do: nothing could be inferred.
  • Four bare return err sites keep //nolint:wrapcheck. .golangci.yml only lints internal/*.go, so moving the code brought previously-unlinted lines into scope. Wrapping them would change error text, which a refactor must not. The repo already uses that convention in 66 places.

The verification — and a correction to what I asked for

I specified "redraft all three committed blueprint sets, require zero diff". That premise was wrong for two of the three.

Only blueprints/jamfpro is a pure blueprint draft artefact. blueprints/thousandeyes and blueprints/thousandeyes-kiota are draft plus blueprint merge folding probe facts, plus curation. All 23 ThousandEyes blueprints carry behaviour blocks; they contain curation prose ("Required-by-API by curation: the sweeper's field, unprobeable by omission"), docRefUrl values inference never emits, and manual key renames a fresh draft still spells the old way. docs/generated-boundary.md:14 says so explicitly. There is no invocation that reproduces them, and there never was.

So a stronger gate was substituted: capture complete output of five drafting configurations before and after, requiring byte-identity of files, stderr, stdout and exit codes.

  • jamfpro (known invocation)
  • thousandeyes-kiota (kiota + prune)
  • thousandeyes (resty + prune — the exit-1 path)
  • thousandeyes no-prune with -scenario-drafts (the only run exercising writeScenarioDraft)
  • -tag tags,alerts against the older 7.0.97 snapshot (the selection path)

Result: all five trees, all five stderr logs (507/359/258/445/12 lines), all five exit codes, all stdout — identical.

Independently re-confirmed on this branch: redrafting jamfpro gives 120 blueprint(s) written and git status -- blueprints/ reports 0 changed files.

A pre-existing nondeterminism, found by the harness

Running the baseline twice gives 263 identical written files but differing pruned log lines: sdkbind.Prune reports whichever failing operation it reaches first, so the same removal is explained as binding.read.chain on one run and binding.update.chain on the next. The set, order and count are stable and it never reaches a written blueprint — so it is cosmetic, not a drift risk. Not introduced here, but worth knowing it exists.

Gates

go build · go vet · go test ./... (18 packages) · gofumpt -l . · golangci-lint run ./internal/... 0 issues · TestUnit_CLI_DocsMatchTheBinary PASS, so the CLI surface is unchanged.

Only TestUnit_CLI_Matches changed, to call draft.Matches.

🤖 Generated with Claude Code

…ge main

The tests in this repository are moving off committed blueprint artefacts and
onto blueprints derived at test time, and internal/generate cannot reach into
package main to derive one. The only two ways out were writing a second deriver
for the tests or moving the real one somewhere both can call. A second deriver
is worse than no test at all: it drifts from the pipeline silently, and the day
it does, the tests are asserting against output the generator never produces
while still reporting green. So the drafting core moves to internal/draft, and
the command keeps what a command owns.

The split is along the line between deciding what a blueprint contains and
presenting a run to a person. Inference, SDK pruning and the scenario worksheet
scaffold decide content and move; candidate selection moves with them because
choosing which families to draft is part of drafting, and a caller deriving a
single family in a test needs it as much as the CLI does. Flag parsing, snapshot
resolution and the candidate table stay behind, as does the notes report -- but
it is passed in as a hook rather than left where the loop could no longer reach
it, because where that report appears relative to the progress lines is itself
the reporting order, and moving it either side of the summary would have been a
visible change to what a run says.

The seven positional arguments become an Options struct. That is not tidying:
three of the seven were strings whose meaning came only from their position, and
the tests this refactor exists to enable will call it directly and often.

The progress lines are preserved verbatim, including their spacing, because they
are the run's only account of the families a document offered and this did not
take. The sentinel is the one deliberate seam: the package states the drafting
fact it knows -- nothing could be inferred -- and the command wraps it in the
"nothing to do" vocabulary every other subcommand shares, so both the exit code
and the message a user reads are unchanged.

Redrafting the Jamf Pro blueprints leaves the committed set byte-identical. Four
further drafting configurations were captured before the move and again after --
both kiota pilots, the resty dialect with and without pruning, and a tag-filtered
run against the older snapshot -- and all 263 written files, every progress line,
every exit code and the notes report match exactly. Two of those configurations
are diagnostic rather than reproductions: blueprints/thousandeyes and
blueprints/thousandeyes-kiota carry merged probe facts and curated renames, so no
bare draft run has ever reproduced them, and only the Jamf Pro set is a pure
drafting artefact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 8d25af0 into main Aug 7, 2026
10 of 12 checks passed
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.

1 participant