Skip to content

Commit d092f3e

Browse files
donislawdevclaude
andcommitted
format: make the minimum a number this tool will take, for every seed
Two findings, one behind the other, both found by asking for exactly what the tool printed. THE NUMBER ON SCREEN WAS ONE THE TOOL REFUSED Measured on 2026-08-03, with nothing but the defaults: pdf printed 3265 refused it, said 3286 wav printed 44 refused it, said 98 zip printed 156 refused it, said 4285 The registry holds the structural floor of a format with no label and every setting at its default, which is a real thing to know and not what a column headed MINIMUM says to a reader. The label is on unless it is turned off, so for three formats out of twelve the advertised number was one an ordinary invocation would never take. Same class as the PNG dimensions closed earlier today: the tool advertising something it then refuses. The number is asked of the generator now rather than declared beside it. The generator already works it out - it has to, in order to refuse - and carries it in the refusal, so the two cannot disagree. It asks repeatedly rather than once, because the answer is a fixed point: the label states the byte count, so a longer number is a longer label is a larger minimum. Asking once gave pdf 3334 while 3286 was accepted, and wav 96 while 96 was refused. min_bytes keeps its name and meaning in --json, because a key in machine output is a public name. smallest_accepted sits beside it. AND THE ANSWER DEPENDED ON THE SEED Found while fixing the first one, and worse than it. The text on a PDF page is drawn - eight to thirteen words from a vocabulary of different lengths, over twenty four lines - so the smallest document one seed can produce is not the smallest another can. --size 3300 accepted for 6 seeds out of 10 the floor over 200 seeds 3090 B to 3499 B An error that appears and disappears when the seed changes is the one thing a tool whose whole promise is "the same seed gives the same run" cannot have. The engine says exactly that, in as many words, about a size drawn from a range - and nobody had applied it to the minimum itself. The floor is now worked out from the longest text a page can ever hold, so it is one number for everybody. Sizes at or above it are produced exactly as before: the pinned values did not move, because the boundary moved and the content did not. The cost is named rather than hidden. The floor is the theoretical worst case, 4495 B, while a given seed may really need only 3090 - so sizes some seeds could have produced are refused now. Buying that back needs a fixed line length, which shifts the bytes of every PDF this tool has ever made, and that is a major bump and the owner's call. Recorded as O52. Two guards, both seen red first, both proven by mutation, and one of them is the opposite direction - a number well above the real floor would satisfy "the minimum is accepted" while telling somebody they cannot ask for a file they can have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4a2d377 commit d092f3e

5 files changed

Lines changed: 285 additions & 19 deletions

File tree

‎CHANGELOG.md‎

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

8989
### Changed
9090

91+
- **The minimum `tfg formats` prints is a size it will take.** For pdf, wav
92+
and zip it was not: the number came from the registry, which holds the
93+
structural floor of a format with no label, and the label is on unless you
94+
turn it off. Asking for exactly what was printed was refused. The column now
95+
shows what an ordinary run accepts, and `--json` carries both - `min_bytes`
96+
unchanged, `smallest_accepted` beside it.
97+
- **Whether a PDF size is accepted no longer depends on the seed.** The text on
98+
a page is drawn, so the smallest document one seed could produce was not the
99+
smallest another could: `--size 3300` was accepted for six seeds out of ten,
100+
and across two hundred seeds the floor moved between 3090 and 3499 bytes. An
101+
error that comes and goes when you change the seed is the one thing a tool
102+
built on repeatability cannot have. The floor is now worked out from the
103+
longest text a page can ever hold, so it is the same for everybody. Every
104+
document at an accepted size is unchanged, byte for byte - the boundary
105+
moved, not the content. The cost is that the floor is the worst case, so
106+
sizes some seeds could have produced are now refused.
91107
- **The smallest SVG this tool makes now draws something.** At exactly its old
92108
minimum of 193 bytes the document held one empty text element and no shapes:
93109
valid SVG, exactly the size ordered, repeatable, and a blank canvas when

‎internal/cli/formats.go‎

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@ import (
1212
)
1313

1414
type formatEntry struct {
15-
ID string `json:"id"`
16-
Extension string `json:"extension"`
17-
Fidelity string `json:"fidelity"`
18-
Determinism string `json:"determinism"`
19-
MinBytes int64 `json:"min_bytes"`
20-
Padding string `json:"padding_channel"`
21-
PaddingCap int64 `json:"padding_capacity,omitempty"`
22-
Label string `json:"label_carrier"`
23-
Properties []propertyEntry `json:"properties,omitempty"`
24-
Oracle string `json:"oracle"`
25-
Version string `json:"generator_version"`
15+
ID string `json:"id"`
16+
Extension string `json:"extension"`
17+
Fidelity string `json:"fidelity"`
18+
Determinism string `json:"determinism"`
19+
// MinBytes is the structural floor of the format: its skeleton, with no
20+
// label and every setting at its default. Kept under the name it has
21+
// always had, because a key in machine output is a public name.
22+
MinBytes int64 `json:"min_bytes"`
23+
// SmallestAccepted is the smallest --size this build will actually take
24+
// for an ordinary run, with the label on. For three formats out of twelve
25+
// that is not MinBytes, and asking for MinBytes was refused.
26+
SmallestAccepted int64 `json:"smallest_accepted"`
27+
Padding string `json:"padding_channel"`
28+
PaddingCap int64 `json:"padding_capacity,omitempty"`
29+
Label string `json:"label_carrier"`
30+
Properties []propertyEntry `json:"properties,omitempty"`
31+
Oracle string `json:"oracle"`
32+
Version string `json:"generator_version"`
2633
}
2734

2835
// propertyEntry is one setting as a script sees it. Everything a window would
@@ -39,6 +46,17 @@ type propertyEntry struct {
3946
Detail string `json:"detail,omitempty"`
4047
}
4148

49+
// smallestAccepted is the number this command prints as the minimum.
50+
//
51+
// It is what an ordinary run will take, so the label is on and every property
52+
// is at its default - which is what somebody reading that column is about to
53+
// type. The registry's own MinBytes is the structural floor with no label, and
54+
// for pdf, wav and zip the two are not the same number: asking for the floor
55+
// was refused, measured on 2026-08-03.
56+
func smallestAccepted(d format.Descriptor) int64 {
57+
return d.SmallestAccepted(format.Request{Label: true})
58+
}
59+
4260
func entryFor(d format.Descriptor) formatEntry {
4361
props := make([]propertyEntry, 0, len(d.Properties))
4462
for _, p := range d.Properties {
@@ -50,7 +68,8 @@ func entryFor(d format.Descriptor) formatEntry {
5068
return formatEntry{
5169
ID: d.ID, Extension: d.Extension,
5270
Fidelity: string(d.Fidelity), Determinism: string(d.Determinism),
53-
MinBytes: d.MinBytes, Padding: d.Padding.Name, PaddingCap: d.Padding.Capacity,
71+
MinBytes: d.MinBytes, SmallestAccepted: smallestAccepted(d),
72+
Padding: d.Padding.Name, PaddingCap: d.Padding.Capacity,
5473
Label: string(d.Label), Properties: props,
5574
Oracle: d.Oracle, Version: d.GeneratorVersion,
5675
}
@@ -63,7 +82,7 @@ func entryFor(d format.Descriptor) formatEntry {
6382
// a format accepts, and the silence looked like an answer.
6483
func describeOne(d format.Descriptor, out io.Writer) {
6584
fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %d B\n",
66-
d.ID, d.Fidelity, d.Determinism, d.MinBytes)
85+
d.ID, d.Fidelity, d.Determinism, smallestAccepted(d))
6786
fmt.Fprintf(out, " extension %s\n", d.Extension)
6887
fmt.Fprintf(out, " padding %s\n", d.Padding.Name)
6988
fmt.Fprintf(out, " label %s\n", d.Label)
@@ -171,7 +190,7 @@ Flags:
171190
fmt.Fprintf(out, "%-8s %-10s %-12s %-10s %s\n", "FORMAT", "FIDELITY", "DETERMINISM", "MINIMUM", "PADDING CHANNEL")
172191
for _, d := range format.All() {
173192
fmt.Fprintf(out, "%-8s %-10s %-12s %-10d %s\n",
174-
d.ID, d.Fidelity, d.Determinism, d.MinBytes, d.Padding.Name)
193+
d.ID, d.Fidelity, d.Determinism, smallestAccepted(d), d.Padding.Name)
175194
}
176195
fmt.Fprint(out, "\nRun \"tfg formats <id>\" for what one format accepts.\n")
177196
return ExitOK

‎internal/format/format.go‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package format
77

88
import (
99
"context"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"sort"
@@ -328,6 +329,71 @@ func (p Property) unitSuffix() string {
328329
return " of " + p.Unit
329330
}
330331

332+
// SmallestAccepted is the smallest size this format will actually produce for
333+
// a request shaped like r.
334+
//
335+
// MinBytes beside it is the structural floor: the skeleton of the format with
336+
// no label and nothing else. That is a real thing to know and it is not what a
337+
// person reading a column headed MINIMUM takes it to mean, because the label is
338+
// on unless it is turned off and some formats size themselves from their
339+
// settings. Measured on 2026-08-03, asking for exactly what the tool printed:
340+
//
341+
// pdf printed 3265 refused it, said 3286
342+
// wav printed 44 refused it, said 98
343+
// zip printed 156 refused it, said 4285
344+
//
345+
// The generator is asked rather than a second number being declared beside the
346+
// first. A declaration would be one more thing to keep in step, and the
347+
// generator already works this out - it has to, in order to refuse - and
348+
// carries it in the refusal. So the answer here and the answer somebody gets
349+
// when they ask for one byte less cannot disagree.
350+
// It asks repeatedly rather than once, and that is not caution - it is
351+
// arithmetic. The minimum a format reports depends on the size being asked
352+
// for, because the self describing label states the byte count and a longer
353+
// number is a longer label. So the answer is a fixed point: keep asking until
354+
// a size is accepted, moving to whatever the refusal names next.
355+
//
356+
// Measured on 2026-08-03, which is how this was found rather than reasoned
357+
// about. Asking once at nought gave pdf 3334 while 3286 was accepted, and wav
358+
// 96 while 96 was refused and 98 was not. One question gives a number that is
359+
// wrong in either direction.
360+
//
361+
// A format may also refuse a band above a size it accepts - PNG takes 73 and
362+
// refuses 74 through 84, because the smallest chunk that could make up the
363+
// difference costs twelve on its own - so this steps by one when a refusal
364+
// names nothing higher, rather than assuming the answer only ever grows.
365+
func (d Descriptor) SmallestAccepted(r Request) int64 {
366+
r.SizeFromContents = false
367+
368+
// Sixty four rounds is far more than any format needs: each round either
369+
// settles or jumps to a number the format itself named. It is here so that
370+
// a generator whose refusals ever cycle costs a wrong number rather than a
371+
// run that never ends.
372+
size := int64(0)
373+
for round := 0; round < 64; round++ {
374+
r.Bytes = size
375+
if _, err := d.Generator.Plan(r); err == nil {
376+
return size
377+
} else {
378+
var below *BelowMinimumError
379+
if !errors.As(err, &below) {
380+
// Refused for a reason that is not about size - a property this
381+
// request cannot have, say. The structural floor is the honest
382+
// answer left.
383+
return d.MinBytes
384+
}
385+
if below.Minimum > size {
386+
size = below.Minimum
387+
continue
388+
}
389+
// The refusal names nothing above where we already are, so this is
390+
// a size inside a band the format cannot reach. Step past it.
391+
size++
392+
}
393+
}
394+
return d.MinBytes
395+
}
396+
331397
// PropertyNames is the declared keys, in the order the format listed them.
332398
func (d Descriptor) PropertyNames() []string {
333399
out := make([]string, 0, len(d.Properties))

‎internal/format/pdf/pdf.go‎

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ type memo struct {
102102
// cross reference table, which sits before the padding.
103103
suffix []byte
104104
padLen int64
105+
// widestContent asks document() to lay out the longest content this format
106+
// can ever draw, so that the smallest size it accepts is the same number for
107+
// every seed. It is only ever set while measuring.
108+
widestContent bool
105109
}
106110

107111
type pageSize struct {
@@ -135,8 +139,18 @@ func (generator) Plan(r format.Request) (format.Plan, error) {
135139
m := memo{pages: pages, pageSize: size, seed: r.Seed, label: label}
136140
m.prefix, m.suffix = document(m)
137141

142+
// What this seed happens to come to, which decides how much padding the
143+
// file needs.
138144
bare := int64(len(m.prefix) + len(m.suffix))
139145

146+
// What any seed could come to, which decides what is refused. The two are
147+
// different numbers and using the first for both is what made the same
148+
// --size succeed on one seed and fail on another - see longestSentence.
149+
widestM := m
150+
widestM.widestContent = true
151+
wp, ws := document(widestM)
152+
floor := int64(len(wp) + len(ws))
153+
140154
p := format.Plan{
141155
Bytes: r.Bytes,
142156
Exact: true,
@@ -155,14 +169,14 @@ func (generator) Plan(r format.Request) (format.Plan, error) {
155169
switch {
156170
case r.Bytes == bare:
157171
m.padLen = 0
158-
case r.Bytes < bare:
172+
case r.Bytes < floor:
159173
return format.Plan{}, &format.BelowMinimumError{
160174
Format: "PDF",
161175
Requested: r.Bytes,
162-
Minimum: bare,
176+
Minimum: floor,
163177
Reason: fmt.Sprintf("a %d page %s document%s already needs that much before any padding",
164178
pages, size.name, labelCost(r.Label)),
165-
Hint: fmt.Sprintf("Ask for %d B or more%s.", bare, cleanHint(r.Label)),
179+
Hint: fmt.Sprintf("Ask for %d B or more%s.", floor, cleanHint(r.Label)),
166180
}
167181
case r.Bytes < bare+minComment:
168182
// A comment is a per cent sign and a newline at the very least, so
@@ -361,7 +375,13 @@ func pageContent(m memo, index int) string {
361375
rng := core.NewRand(core.FileSeed(m.seed, index))
362376
y := top - 36
363377
for line := 0; line < 24 && y > 108; line++ {
364-
fmt.Fprintf(&b, "1 0 0 1 72 %d Tm\n(%s) Tj\n", y, escapeString(sentence(rng)))
378+
text := sentence(rng)
379+
if m.widestContent {
380+
// Measuring the worst case rather than producing a file. See
381+
// longestSentence for why the floor may not depend on the seed.
382+
text = longestSentence
383+
}
384+
fmt.Fprintf(&b, "1 0 0 1 72 %d Tm\n(%s) Tj\n", y, escapeString(text))
365385
y -= 16
366386
}
367387
b.WriteString("ET\n")
@@ -385,6 +405,44 @@ func sentence(rng interface{ IntN(int) int }) string {
385405
return strings.Join(parts, " ")
386406
}
387407

408+
// longestSentence is the most a line of this page can ever come to.
409+
//
410+
// It exists so that the size this format refuses below does not depend on the
411+
// seed. The content of a page is drawn - eight to thirteen words, each from a
412+
// vocabulary of different lengths - so the smallest document a given seed can
413+
// produce is not the smallest document some other seed can produce.
414+
//
415+
// Measured on 2026-08-03, asking every seed for the bare minimum: across two
416+
// hundred seeds the answer ranged from 3090 B to 3499 B. The visible cost was
417+
// worse than the number suggests - "tfg generate --format pdf --size 3300"
418+
// was accepted for six seeds out of ten and refused for four. An error that
419+
// appears and disappears depending on the seed is the one thing a tool built
420+
// on "the same seed gives the same run" cannot have, and the engine already
421+
// says so in as many words about a size drawn from a range.
422+
//
423+
// So the floor is the worst case rather than this seed's case, and it is the
424+
// same number for everybody. Sizes at or above it are produced exactly as
425+
// before, byte for byte - only the boundary moved.
426+
var longestSentence = func() string {
427+
longest := ""
428+
for _, w := range words {
429+
if len(w) > len(longest) {
430+
longest = w
431+
}
432+
}
433+
const most = 8 + 5 // the largest n that sentence can draw
434+
parts := make([]string, most)
435+
for i := range parts {
436+
parts[i] = longest
437+
}
438+
return strings.Join(parts, " ")
439+
}()
440+
441+
// widest is a stand in for a draw, handing back the longest of everything.
442+
type widest struct{}
443+
444+
func (widest) IntN(int) int { return 0 }
445+
388446
// escapeString protects the three characters that end or nest a PDF string.
389447
// Without this a label containing a bracket would produce a file no reader
390448
// can parse.
@@ -439,7 +497,7 @@ func sorted(in []string) []string {
439497
// minimumBytes is the smallest document this generator can produce: one A4
440498
// page with no label. Measured at start up rather than guessed.
441499
func minimumBytes() int64 {
442-
m := memo{pages: 1, pageSize: pageSizes["a4"]}
500+
m := memo{pages: 1, pageSize: pageSizes["a4"], widestContent: true}
443501
prefix, suffix := document(m)
444502
return int64(len(prefix) + len(suffix))
445503
}

0 commit comments

Comments
 (0)