Skip to content

Commit d297357

Browse files
donislawdevclaude
andcommitted
gui: a preset screen, and a guard that compares the two surfaces by their bytes
A second screen that starts from a question rather than from numbers. It shows the question a preset closes and what it typically finds, and its parameters are drawn by the code that draws a format's settings - because a preset parameter is a format.Property. Nothing in the screen knows what size-boundaries takes. Expanding a preset moved out of internal/cli into internal/preset, so the window, "generate --preset" and "preset eject" all call one Expand. The manifest record stays a small literal in each surface on purpose: putting it in internal/preset would make an input concept import the output contract. The run half of a screen is now shared. A runner owns preview, generate, progress, cancel, the manifest save and the close intercept, and a screen supplies only what to produce. Written when the second screen arrived rather than after the two had drifted, because the parts that drift here are the ones nobody compares: whether closing waits, whether a preview writes. The guard for all this does not compare two lists of fields. It runs one preset from both surfaces and compares seven files byte for byte, the preset block of the manifest and the recipe hash - because turning a parsed recipe into engine targets is written twice and cannot be shared, and that exact conversion drifted before, when validate lost BoundaryLimit and started refusing recipes generate accepted. It found two real differences on its first run. A field filled in with its declared default makes "I did not state this" impossible to express, so the window would never have marked a parameter as defaulted - untouchable rule 5 broken by a form convenience. No field arrives filled in now, and what a setting falls back to is shown as a hint instead. And the window had nowhere to put the sentences the command line prints as "note:", so it stayed silent about building a set around a limit nobody gave it. A set built around a number we invented carries expectations that read exactly like a set built around the real one, so saying so is not decoration. The close intercept moved to Open and asks every screen, because a run keeps going while somebody moves to another screen and the busy one is not necessarily the visible one. D1 parity 36 of 60 to 39 of 60. Seven mutation patterns repointed after the refactor moved their code, which the staleness check caught rather than a twelve minute run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 07d850b commit d297357

11 files changed

Lines changed: 858 additions & 277 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ because it turns other people's test suites red.
1616

1717
### Added
1818

19+
- **Presets in the window.** A second screen that starts from a question rather
20+
than from numbers: pick what you are testing, and the set is worked out from
21+
the answer. It shows the question the preset closes and the mistakes it
22+
typically finds, and its settings are drawn from the preset itself, the same
23+
way a format's settings are.
24+
It produces exactly what `tfg generate --preset` produces - the same files,
25+
byte for byte, and the same record of the run. When a number was not given and
26+
ours stood in, the window says so out loud, because a set built around a limit
27+
we invented carries expectations that read exactly like a set built around
28+
yours.
29+
No field arrives filled in any more. What a setting falls back to is shown in
30+
the box as a hint instead, so leaving a field alone means "I did not state
31+
this" and the manifest can say which numbers were ours.
32+
1933
- **The window generates files.** Pick a format, say how big and how many, and
2034
press Generate. It writes the same bytes the command line writes from the same
2135
settings, because it is the same engine underneath rather than a second one.

‎internal/cli/preset.go‎

Lines changed: 21 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,70 +15,26 @@ import (
1515
"github.com/donislawdev/TestingFilesGenerator/internal/recipe"
1616
)
1717

18-
// expandedPreset is one preset settled on its parameters and turned into the
19-
// recipe it stands for.
20-
//
21-
// The source is what a run consumes and what eject prints, because it is the
22-
// same bytes - PR5 in docs/PRESETS.md. Every command here goes through this one
23-
// function, so none of them can be the one that expands differently.
24-
type expandedPreset struct {
25-
Preset preset.Preset
26-
Settled preset.Args
27-
// Defaulted names the parameters nobody gave, whose declared default stood
28-
// in. Sorted, so two runs of one preset produce identical records.
29-
Defaulted []string
30-
Source []byte
31-
}
32-
33-
func expandPreset(id string, given preset.Args) (*expandedPreset, error) {
34-
p, err := preset.Get(id)
35-
if err != nil {
36-
return nil, err
37-
}
38-
settled, err := p.Settle(given)
39-
if err != nil {
40-
return nil, err
41-
}
42-
43-
var defaulted []string
44-
for name := range p.Defaults() {
45-
if given[name] == "" {
46-
defaulted = append(defaulted, name)
47-
}
48-
}
49-
sort.Strings(defaulted)
50-
51-
src, err := p.Expand(settled)
52-
if err != nil {
53-
return nil, err
54-
}
55-
return &expandedPreset{Preset: p, Settled: settled, Defaulted: defaulted, Source: src}, nil
56-
}
18+
// Expanding a preset lives in internal/preset, because the window expands them
19+
// too and the two surfaces cannot import each other. What stays here is the one
20+
// thing that is about output rather than about presets: turning an expansion
21+
// into the manifest record.
5722

5823
// record is what goes into the manifest.
59-
func (e *expandedPreset) record() *manifest.Preset {
24+
//
25+
// A three field literal rather than a shared constructor, and deliberately so:
26+
// putting it in internal/preset would make that package import the manifest,
27+
// which is the output contract and no business of an input concept. The drift
28+
// this invites is watched behaviourally instead - a guard runs the same preset
29+
// from both surfaces and compares the records they produce.
30+
func record(e *preset.Expansion) *manifest.Preset {
6031
return &manifest.Preset{
6132
ID: e.Preset.ID,
6233
Parameters: map[string]string(e.Settled),
6334
Defaulted: e.Defaulted,
6435
}
6536
}
6637

67-
// notes is what to say out loud about a value nobody gave us.
68-
//
69-
// Some defaults describe our own file and some describe somebody else's system.
70-
// A set built around a limit we invented carries expectations that read exactly
71-
// like a set built around the real one, so the run says which number it made up.
72-
func (e *expandedPreset) notes() []string {
73-
var out []string
74-
for _, name := range e.Defaulted {
75-
if said := e.Preset.SaidWhenDefaulted[name]; said != "" {
76-
out = append(out, said)
77-
}
78-
}
79-
return out
80-
}
81-
8238
// budget is what a preset would produce, counted by the planner.
8339
//
8440
// Not a declared number beside the code. The one that used to sit in
@@ -92,7 +48,7 @@ type budget struct {
9248
Formats []string `json:"formats"`
9349
}
9450

95-
func budgetOf(e *expandedPreset) (budget, error) {
51+
func budgetOf(e *preset.Expansion) (budget, error) {
9652
rec, err := recipe.Parse(e.Source, e.Preset.ID)
9753
if err != nil {
9854
return budget{}, err
@@ -358,7 +314,7 @@ func targetsFromPreset(fs *flag.FlagSet, g *generateOpts, given map[string]bool,
358314
return nil, ExitUsage
359315
}
360316

361-
expanded, err := expandPreset(g.presetID, givenPresetArgs(fs, p))
317+
expanded, err := preset.Expand(g.presetID, givenPresetArgs(fs, p))
362318
if err != nil {
363319
fmt.Fprintf(errOut, "tfg: %s\n", describeError(err))
364320
return nil, classify(err)
@@ -374,10 +330,10 @@ func targetsFromPreset(fs *flag.FlagSet, g *generateOpts, given map[string]bool,
374330
return nil, classify(err)
375331
}
376332

377-
for _, note := range expanded.notes() {
333+
for _, note := range expanded.Notes() {
378334
fmt.Fprintf(errOut, "note: %s\n", note)
379335
}
380-
opt.Preset = expanded.record()
336+
opt.Preset = record(expanded)
381337
return targetsFromParsedRecipe(rec, hash, g, given, opt), ExitOK
382338
}
383339

@@ -431,7 +387,7 @@ Run "tfg generate --preset <id>" to produce the files.
431387
// nil for the one that does not - a recipe is already machine readable, and a
432388
// second encoding of it would be a second thing to keep in step.
433389
func presetFlagSet(name string, args []string, out, errOut io.Writer, usage func(io.Writer), asJSON *bool) (
434-
*expandedPreset, int) {
390+
*preset.Expansion, int) {
435391

436392
fs := flag.NewFlagSet("preset "+name, flag.ContinueOnError)
437393
fs.SetOutput(errOut)
@@ -470,7 +426,7 @@ func presetFlagSet(name string, args []string, out, errOut io.Writer, usage func
470426
return nil, ExitUsage
471427
}
472428

473-
expanded, err := expandPreset(id, givenPresetArgs(fs, p))
429+
expanded, err := preset.Expand(id, givenPresetArgs(fs, p))
474430
if err != nil {
475431
fmt.Fprintf(errOut, "tfg: %s\n", describeError(err))
476432
return nil, classify(err)
@@ -555,14 +511,14 @@ Usage:
555511
entry := presetEntryFor(expanded.Preset)
556512
entry.Budget = &b
557513
entry.Defaulted = expanded.Defaulted
558-
entry.Notes = expanded.notes()
514+
entry.Notes = expanded.Notes()
559515
return renderJSON(entry, out, errOut)
560516
}
561517
describePreset(expanded, b, out)
562518
return ExitOK
563519
}
564520

565-
func describePreset(e *expandedPreset, b budget, out io.Writer) {
521+
func describePreset(e *preset.Expansion, b budget, out io.Writer) {
566522
p := e.Preset
567523
fmt.Fprintf(out, "%s - %s\n%s\n", p.ID, p.Title, p.Question)
568524

@@ -581,7 +537,7 @@ func describePreset(e *expandedPreset, b budget, out io.Writer) {
581537

582538
fmt.Fprintf(out, "\nbudget at these values:\n %d target(s), %d file(s), %d B total, format %s\n",
583539
b.Targets, b.Files, b.Bytes, strings.Join(b.Formats, ", "))
584-
for _, note := range e.notes() {
540+
for _, note := range e.Notes() {
585541
fmt.Fprintf(out, "\nnote: %s\n", note)
586542
}
587543

@@ -618,7 +574,7 @@ Usage:
618574
// The note goes to the error channel. The recipe is the data here, and a
619575
// sentence about a number we chose has no business inside a file somebody
620576
// is about to commit.
621-
for _, note := range expanded.notes() {
577+
for _, note := range expanded.Notes() {
622578
fmt.Fprintf(errOut, "note: %s\n", note)
623579
}
624580
if _, err := out.Write(expanded.Source); err != nil {

‎internal/guard/parity_test.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ import (
4848
// the guard was armed on 2026-08-03, which was the point: the distance started
4949
// as the whole list rather than as whatever was left after nobody was watching.
5050
var reachableFromTheWindow = []string{
51+
// A named question, its parameters drawn by the same code that draws a
52+
// format's settings - because a preset parameter IS a format.Property.
53+
// TestTheWindowOffersEveryPresetThereIs and the two beside it, one of which
54+
// runs the same preset from both surfaces and compares the bytes.
55+
"preset:size-boundaries",
56+
"preset:size-boundaries.limit",
57+
"preset:size-boundaries.spread",
58+
5159
// Every format the registry holds, taken from the registry itself rather
5260
// than listed in the window. TestTheWindowOffersEveryFormatTheRegistryHas.
5361
"format:csv",
@@ -111,9 +119,6 @@ var reachableFromTheWindow = []string{
111119
// key the window cannot produce, and separating the two reasons would be a
112120
// second list to keep in step for no gain.
113121
var notYetReachable = []string{
114-
"preset:size-boundaries",
115-
"preset:size-boundaries.limit",
116-
"preset:size-boundaries.spread",
117122
"recipe:allow_nondeterministic",
118123
"recipe:targets.group",
119124
"recipe:defaults",

0 commit comments

Comments
 (0)