Skip to content

Commit dde2cd9

Browse files
donislawdevclaude
andauthored
format: hold the text and picture families to the declaration they share (#94)
* format: an xlsx sheet can be wider than a spreadsheet will open xlsx columns stopped at 64, with the reason "the width a person would actually look at". That describes a document somebody reads, and this tool writes fixtures somebody tests with - a ceiling belongs to the reader under test. At 64 there was no way to build a sheet that asks Excel about its own limit at all, while csv answered the same question about the same reader with 32768 and nothing compared the two. Measured 2026-09-08 with LibreOffice Calc 26.2.5.2 headless, on workbooks built outside this tool because this tool could not build them: 16384 columns come back whole, 16385 come back as 16384 with the last column dropped, exit 0 and not one word on either stream. After the change our own 16385 column workbook behaves identically, and hits the requested size to the byte. The encoder needed no change - column() counts letters in base 26, so it walks past XFD on its own. rows times columns still cannot pass 2 million cells, MinBytes is computed from a one by one sheet so it does not move, and a sheet of 64 columns or fewer is byte for byte what it was. The site is regenerated for the two lines that name the range. The guard asks every registered format that declares columns, so a third tabular format is covered on the day it arrives. It asks about the OFFER rather than building the file, because building one costs twelve megabytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * format: hold the text and picture families to the declaration they share textenc.Properties() becomes textenc.Axes(names...) alongside textenc.Names(), the shape the archive package has had since the containers were unified. All three text formats take both axes and call it with both, so the narrowing has no dead call. It is there because the fourth format is already named and cannot take both: an HTML document has to be UTF-8 by its own specification, so the only encoding axis it could carry is the mark. Two guards close a gap that was measured rather than suspected. Given md a declaration of its own that agreed on kind, unit and default and offered one encoding fewer, eleven guards stayed green while md went on writing utf-16be and stopped offering it. Given bmp a width starting at zero, ten stayed green while tfg formats printed "whole number of pixels from 0 to 20000" and --set width=0 was refused with "it has to be between 1 and 20000" - a declaration contradicting the program, caught only by the backstop in imagedim.Value, which picturesides_test.go says a person cannot reach. A person reaches it exactly when a declaration drifts. The picture guard rebuilds each declaration from the parts a format supplies rather than comparing outright, because the largest side, the default and the sentence are the format's own and are supposed to differ - four of the ten sentences are correctly different. Rebuilding covers a field nobody has thought of yet, which a list of field comparisons would not. The narrowing is by name rather than by value. No text format wants a narrower set of encodings, and all ten picture formats call Width and Height in pairs, so narrowing a value is what imagedim.Side already does and it was built after ten different ceilings had been measured, not before. Declarations are byte for byte identical: tfg formats --json hashes to 0b1361a7294ab5b71b6acd39f13c9c74effa0a6df215f241fc3989636836dfc7 before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 1dad937 commit dde2cd9

6 files changed

Lines changed: 219 additions & 19 deletions

File tree

‎internal/format/md/md.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func init() {
6464
// Heading depth, table size and which elements appear come later.
6565
// Declaring only what is here is what makes a recipe asking for them
6666
// fail loudly rather than quietly producing something else.
67-
Properties: textenc.Properties(),
67+
Properties: textenc.Axes(textenc.Setting, textenc.SettingBOM),
6868
GeneratorVersion: generatorVersion,
6969
Generator: generator{},
7070
})

‎internal/format/textenc/textenc.go‎

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Package textenc is the character encoding the text formats share.
22
//
3-
// It lives here rather than inside one of them because TXT and MD ask the same
4-
// question and have to answer it in the same words. A copy in each is how
3+
// It lives here rather than inside one of them because TXT, MD and XML ask the
4+
// same question and have to answer it in the same words. A copy in each is how
55
// thirteen packages ended up carrying four different versions of one filler
66
// loop, and the same rule applies to a setting a person reads.
77
//
8+
// Sharing the declaration is not the same as being held to it, and until
9+
// 2026-09-09 only the first was true here. A format that simply did not call
10+
// this package could declare the same setting any way it liked, which is the
11+
// gap Axes and TestEveryTextFormatDeclaresTheEncodingSettingsAsTheyAreDeclared
12+
// Once close between them.
13+
//
814
// What it holds is arithmetic as much as bytes. A file written in UTF-16 is a
915
// whole number of sixteen bit units, so its length is always even - which
1016
// means half of all sizes stop being reachable, and the exact size promise
@@ -18,6 +24,7 @@ package textenc
1824
import (
1925
"fmt"
2026
"io"
27+
"sort"
2128
"unicode/utf16"
2229
"unicode/utf8"
2330

@@ -253,19 +260,67 @@ func appendUnit(dst []byte, u uint16, big bool) []byte {
253260
return append(dst, byte(u), byte(u>>8))
254261
}
255262

256-
// Properties is the declaration both text formats hand to the registry, so
257-
// the two cannot describe the same setting differently.
258-
func Properties() []format.Property {
259-
return []format.Property{
260-
{
261-
Name: Setting, Kind: format.PropertyChoice,
262-
Choices: []string{UTF16BE, UTF16LE, UTF8}, Default: UTF8,
263-
Detail: "Which encoding the characters are written in. UTF-16 stores two bytes per character, so a file in it always has an even number of bytes and an odd size is refused.",
264-
},
265-
{
266-
Name: SettingBOM, Kind: format.PropertyBool,
267-
Default: "false",
268-
Detail: "Whether the file opens with a byte order mark. A reader that has to guess the encoding needs one, and a reader that does not expect it shows it as stray characters at the start of the file.",
269-
},
263+
// axes is the declaration of every setting a text format may take, by key.
264+
//
265+
// One copy, so two text formats cannot offer the same setting with a different
266+
// set of values, a different default or a different sentence beside it. That
267+
// is not tidiness waiting for a problem: measured on 2026-09-09 by giving md a
268+
// declaration of its own that agreed on kind, unit and default and offered one
269+
// encoding fewer, eleven guards stayed green while txt and xml wrote utf-16be
270+
// and md refused it.
271+
//
272+
// The default is the one field here that is untouchable rule 3 rather than
273+
// consistency. Every one of these formats has always written UTF-8 with no
274+
// mark, so a format defaulting to anything else moves its own bytes.
275+
var axes = map[string]format.Property{
276+
Setting: {
277+
Name: Setting, Kind: format.PropertyChoice,
278+
Choices: []string{UTF16BE, UTF16LE, UTF8}, Default: UTF8,
279+
Detail: "Which encoding the characters are written in. UTF-16 stores two bytes per character, so a file in it always has an even number of bytes and an odd size is refused.",
280+
},
281+
SettingBOM: {
282+
Name: SettingBOM, Kind: format.PropertyBool,
283+
Default: "false",
284+
Detail: "Whether the file opens with a byte order mark. A reader that has to guess the encoding needs one, and a reader that does not expect it shows it as stray characters at the start of the file.",
285+
},
286+
}
287+
288+
// Names is every text encoding setting this build declares, in a stable order.
289+
//
290+
// It exists for the guard that compares what a text format offers against what
291+
// this package says the setting is, so an axis added tomorrow is covered
292+
// without a line changing in that guard.
293+
func Names() []string {
294+
out := make([]string, 0, len(axes))
295+
for n := range axes {
296+
out = append(out, n)
297+
}
298+
sort.Strings(out)
299+
return out
300+
}
301+
302+
// Axes is the declarations for the settings named, in the order given.
303+
//
304+
// A format lists what it takes rather than receiving all of it, and the three
305+
// that exist today all take both. The narrowing is here because the fourth is
306+
// already named and cannot: an HTML document has to be UTF-8 by its own
307+
// specification, so the only encoding axis it could carry is the mark - and
308+
// without this it would have to write that declaration out by hand, which is
309+
// exactly the drift the guard above exists to refuse. The choice is by NAME
310+
// rather than by value, because no text format wants a narrower set of
311+
// encodings and building for one that might is guessing at a shape.
312+
//
313+
// An unknown name panics rather than being skipped, for the reason archive
314+
// gives: this is called from init, the caller is a programmer, and a silently
315+
// dropped axis is a setting that vanishes from both surfaces with nothing said.
316+
func Axes(names ...string) []format.Property {
317+
out := make([]format.Property, 0, len(names))
318+
for _, n := range names {
319+
p, ok := axes[n]
320+
if !ok {
321+
panic(fmt.Sprintf("textenc: %q is not a text encoding setting this build declares", n))
322+
}
323+
out = append(out, p)
270324
}
325+
return out
271326
}

‎internal/format/txt/txt.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func init() {
5757
// Line endings and line length come later. Until they do, declaring
5858
// only what is here is what makes a recipe asking for them fail loudly
5959
// instead of quietly producing something else.
60-
Properties: textenc.Properties(),
60+
Properties: textenc.Axes(textenc.Setting, textenc.SettingBOM),
6161
GeneratorVersion: generatorVersion,
6262
Generator: generator{},
6363
})

‎internal/format/xmlfile/xml.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func init() {
109109
// Depth, element counts, namespaces, CDATA and an internal DTD come
110110
// later. Declaring only what is here is what makes a recipe asking for
111111
// them fail loudly instead of quietly producing something else.
112-
Properties: textenc.Properties(),
112+
Properties: textenc.Axes(textenc.Setting, textenc.SettingBOM),
113113
GeneratorVersion: generatorVersion,
114114
Generator: generator{},
115115
})

‎internal/guard/familyaxes_test.go‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package guard
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/donislawdev/TestingFilesGenerator/internal/format"
8+
_ "github.com/donislawdev/TestingFilesGenerator/internal/format/all"
9+
"github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim"
10+
"github.com/donislawdev/TestingFilesGenerator/internal/format/textenc"
11+
)
12+
13+
// Two families of formats share a declaration through a package, and this is
14+
// what stops a member writing its own copy instead.
15+
//
16+
// The containers have had this since 2026-09-01 - archiveaxes_test.go compares
17+
// every axis a container offers against the one archive declares. The other two
18+
// families had nothing, and the gap was measured rather than suspected. On
19+
// 2026-09-09 md was given a declaration of its own that agreed on kind, unit
20+
// and default and offered one encoding fewer, and bmp was given one whose width
21+
// started at zero. ELEVEN guards stayed green on the first and TEN on the
22+
// second, including the ones that look closest: TestOneSettingNameMeansOneKind
23+
// OfSetting asks only about Kind and Unit, and the README and format-document
24+
// guards compare the prose against whatever the registry happens to say.
25+
//
26+
// What each of the two ran into is different, and both are worth naming:
27+
//
28+
// - md kept writing utf-16be perfectly well and stopped OFFERING it, so txt
29+
// and xml answered one question and md answered another.
30+
// - bmp printed "whole number of pixels from 0 to 20000" and refused width=0
31+
// with "it has to be between 1 and 20000". The declaration contradicted the
32+
// program, and what caught it was the backstop in imagedim.Value - the one
33+
// picturesides_test.go says a person cannot reach. A person reaches it
34+
// exactly when a declaration drifts, which is the case with no guard.
35+
//
36+
// Both guards skip a name listed in deliberateHomonyms, so the one list this
37+
// package already keeps stays the one place a shared name can be excused. A
38+
// second list of excuses beside it would be a second place to look away from.
39+
//
40+
// The cost of sharing that list is worth stating rather than leaving to be
41+
// found: an entry there turns the name off for EVERY format, not only for the
42+
// one it excuses, so writing "encoding" into it would silence half of the first
43+
// guard. That is a deliberate act and a visible one - the entry is in the diff,
44+
// and TestEveryDeliberateHomonymStillNamesTwoMeanings makes it prove that two
45+
// formats really do mean different things by the name - but it is a door, and
46+
// archiveaxes_test.go has none because it keeps no list at all. Narrowing the
47+
// skip to the one format that earned it would mean changing the shape of
48+
// deliberateHomonyms, which is another guard's, so it is written down here
49+
// instead of done quietly.
50+
51+
// Every text format declares the encoding settings the way textenc declares
52+
// them.
53+
//
54+
// Asked by name against textenc.Names(), so a third axis added to that package
55+
// is covered on the day it arrives rather than the day somebody remembers this
56+
// file. A format is free not to carry an axis at all - that is what
57+
// textenc.Axes narrows, and HTML is the named case, since its specification
58+
// leaves it no encoding to choose and only the mark to declare. What is
59+
// refused is carrying one and describing it differently.
60+
func TestEveryTextFormatDeclaresTheEncodingSettingsAsTheyAreDeclaredOnce(t *testing.T) {
61+
compared := 0
62+
for _, d := range format.All() {
63+
declared := byName(d.Properties)
64+
for _, axis := range textenc.Names() {
65+
if _, excused := deliberateHomonyms[axis]; excused {
66+
continue
67+
}
68+
p, offered := declared[axis]
69+
if !offered {
70+
continue
71+
}
72+
compared++
73+
want := textenc.Axes(axis)[0]
74+
if !reflect.DeepEqual(p, want) {
75+
t.Errorf("%s declares %q its own way rather than the way textenc declares it\n"+
76+
" format: %+v\n"+
77+
" shared: %+v",
78+
d.ID, axis, p, want)
79+
}
80+
}
81+
}
82+
83+
if compared == 0 {
84+
t.Fatal("no format declares a text encoding setting, so this proved nothing")
85+
}
86+
t.Logf("%d text encoding declaration(s) compared against the one definition", compared)
87+
}
88+
89+
// Every picture format declares its sides the way imagedim would build them.
90+
//
91+
// This one cannot be a straight comparison, because three of the fields are the
92+
// format's own and are SUPPOSED to differ: the largest side is the ceiling of
93+
// the thing itself, the default is declared by SVG alone, and the sentence is
94+
// per format on purpose - four of the ten are correctly different, and a shared
95+
// one would have made three of them wrong.
96+
//
97+
// So it asks the question the other way round: rebuild the declaration from the
98+
// parts a format supplies and require the result to be what is registered.
99+
// Whatever the format did NOT supply - the name, that it is a whole number,
100+
// that the smallest side is one pixel, that the number counts pixels - has to
101+
// come out of the package, and anything a format added by hand shows up as a
102+
// difference. That covers a field nobody has thought of yet, which a list of
103+
// four field comparisons would not.
104+
func TestEveryPictureFormatDeclaresItsSidesAsTheImageDimensionPackageWould(t *testing.T) {
105+
rebuild := map[string]func(imagedim.Side) format.Property{
106+
imagedim.SettingWidth: imagedim.Width,
107+
imagedim.SettingHeight: imagedim.Height,
108+
}
109+
110+
compared := 0
111+
for _, d := range format.All() {
112+
for _, p := range d.Properties {
113+
build, isSide := rebuild[p.Name]
114+
if !isSide {
115+
continue
116+
}
117+
if _, excused := deliberateHomonyms[p.Name]; excused {
118+
continue
119+
}
120+
compared++
121+
want := build(imagedim.Side{Largest: p.Max, Default: p.Default, Detail: p.Detail})
122+
if !reflect.DeepEqual(p, want) {
123+
t.Errorf("%s declares %q its own way rather than through imagedim\n"+
124+
" format: %+v\n"+
125+
" the package: %+v",
126+
d.ID, p.Name, p, want)
127+
}
128+
}
129+
}
130+
131+
if compared == 0 {
132+
t.Fatal("no format declares a picture side, so this proved nothing")
133+
}
134+
t.Logf("%d picture side declaration(s) rebuilt from what the format supplies", compared)
135+
}

‎internal/guard/picturesides_test.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ import (
2929
//
3030
// What it does NOT claim is that a person can reach these. A person cannot, and
3131
// the guards on the declaration are what prove the sentence they do get.
32+
//
33+
// That sentence was measured on 2026-09-09 and it holds, but only just, and the
34+
// reason is worth carrying: a person reaches this backstop the moment a
35+
// declaration DRIFTS from what imagedim would build. Given a width starting at
36+
// zero, bmp printed "whole number of pixels from 0 to 20000" and this function
37+
// is what refused width=0, with wording contradicting the print - and ten
38+
// guards stayed green. The door was closed the same day by
39+
// TestEveryPictureFormatDeclaresItsSidesAsTheImageDimensionPackageWould, so
40+
// "a person cannot" is true because something now holds it true rather than
41+
// because nothing had tried.
3242
func TestThePictureSideBackstopRefusesWhatTheDeclarationWould(t *testing.T) {
3343
const largest = 256
3444

0 commit comments

Comments
 (0)