From c3b462ec0a11c7a1630a63e6ff902246c92c0001 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 3 Oct 2026 17:31:39 +0200 Subject: [PATCH] Answer architecture round 1: one inline load-and-bind, go generate for the index, sorted unloaded names, item 79 named in the decision, and the audit's other findings filed --- AGENTS.md | 2 +- README.md | 10 +++++---- compose.yaml | 4 ++++ doc.go | 3 ++- docs/decisions.md | 10 +++++---- fejkdata.go | 4 ++-- inline.go | 15 +++++++------ shipped.go | 34 +++++++++++++++++++---------- shipped_test.go | 8 +++++-- struct.go | 5 +---- todo.md | 54 ++++++++++++++++++++++++++++++++++++----------- 11 files changed, 102 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bc4a4a9..60731c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,7 +94,7 @@ In [docs/decisions.md](docs/decisions.md): - Tables carrying token cells stay small - A path is walked once without drawing before it is walked for real - A country's postal codes and streets are siblings under its locality -- A locale's `address` reads its country's `geo` tree, so the shipped set loads whole +- A locale's `address` reads its country's `geo` tree, so a locale folder never loads alone - The default embed holds every Swedish postort the import can place and give a street-delivery code and a street, and the US places of 25,000 or more - A locale's `address` restates its country record's format - A postort's kommun comes from its name, its tätort or its codes, never from distance diff --git a/README.md b/README.md index 79365b0..7992aa8 100644 --- a/README.md +++ b/README.md @@ -988,8 +988,9 @@ docker compose run --rm dev # interactive shell Commands that rewrite source keep your file ownership when run with `--user`: ```sh -docker compose run --rm --user "$(id -u):$(id -g)" fmt # gofmt -w . -docker compose run --rm --user "$(id -u):$(id -g)" tidy # go mod tidy +docker compose run --rm --user "$(id -u):$(id -g)" fmt # gofmt -w . +docker compose run --rm --user "$(id -u):$(id -g)" generate # go generate: shippedindex.go from data/ +docker compose run --rm --user "$(id -u):$(id -g)" tidy # go mod tidy ``` A `--user` command answering `permission denied` on `/cache` has met root-owned @@ -1019,8 +1020,9 @@ Goal 3 is scored by a panel of simulated readers, and [`AGENTS.md`](AGENTS.md) s what the score asks of a pull request. Goal 5 is checked against simulated template writers from the Audience. -A change to the shipped data re-pins [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) -and regenerates [`shippedindex.go`](shippedindex.go) in its own commit: +A change to the shipped data regenerates [`shippedindex.go`](shippedindex.go) with +`generate` above, and re-pins [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) +in its own commit: ```sh REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test diff --git a/compose.yaml b/compose.yaml index 91aee47..e5682fc 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,6 +23,10 @@ services: <<: *go command: go test -run XXX -bench . -benchmem ./... + generate: + <<: *go + command: go generate ./... + vet: <<: *go command: go vet ./... diff --git a/doc.go b/doc.go index 1b608ed..0c9067e 100644 --- a/doc.go +++ b/doc.go @@ -43,5 +43,6 @@ package fejkdata // - whole — a read of a table with no selector and no descent, {/city}, landing on // the `table` itself, so it draws a row apart from every pin: `branches.wholePins`. // - fence — a load-time check, so rendering a compiled tree cannot fail: `New` -// runs them over the data set, `NewTemplate` and `FakeStruct` over what those +// runs them over the sources it loads, the first call reaching a shipped category +// over what that call loads, and `NewTemplate` and `FakeStruct` over what those // compile. diff --git a/docs/decisions.md b/docs/decisions.md index 07c17d6..56e5613 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -118,15 +118,17 @@ still lives in `data/` as JSON; `--data-path` layers over it. ## The shipped set alone loads a category on the first call reaching it; beside a `--data-path` every category loads in `New` -2026-10-03, approved by the maintainer as the restructure of `todo.md` item 76. Goal 12: a +2026-10-03, a restructure the maintainer approved. Goal 12: a `New` pays nothing for the categories a run never reaches, which matters once the shipped set grows to hundreds of MB. A category loads with every category it references and its whole table family, and is bound as a whole load binds, so it renders as it would from -`New`. `List` reads `shippedindex.go`, generated from `data/` with `REPIN=1`, and +`New`. `List` reads `shippedindex.go`, which `go generate` writes from `data/`, and `TestShippedIndexIsCurrent` fails while it is stale. CI loads the whole shipped set, and loads each shipped category alone into a fresh `New`, so none fails at first reach. Goal 6: beside a `--data-path` or a `WithDataFS`, every category loads in `New`, so each mistake in a source the user wrote is -`New`'s error. Valid while CI proves the shipped set whole and each category alone. +`New`'s error, though it parses the whole shipped set, against goal 12, until +`todo.md` item 79 loads only what the user's sources reach. Valid while CI proves the shipped set +whole and each category alone. ## A bare reference draws each time; a reference path is held @@ -650,7 +652,7 @@ its ZIPs would shape the two trees differently, so both draw inside the pinned l and an address agrees at that level. A street's own code is the exact pairing to add when a source carries it. -## A locale's `address` reads its country's `geo` tree, so the shipped set loads whole +## A locale's `address` reads its country's `geo` tree, so a locale folder never loads alone 2026-09-24, Lilleman auf Larv. diff --git a/fejkdata.go b/fejkdata.go index 8108780..d2e8887 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -144,8 +144,8 @@ func paths(n node) []string { out = append(out, join(name, p)) } } - for name, e := range n.unloaded { - for _, p := range e.paths { + for _, name := range sortedEntries(n.unloaded) { + for _, p := range n.unloaded[name].paths { out = append(out, join(name, p)) } } diff --git a/inline.go b/inline.go index f0adbd7..5e2a2a8 100644 --- a/inline.go +++ b/inline.go @@ -34,8 +34,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) { } f.mu.Lock() defer f.mu.Unlock() - loadShipped(&f.root, unloadedReads(&f.root, nil, inlineScope(n, "template"))) - if err := inlineBinding(n, "template", f.root.children).bind(); err != nil { + if err := bindInline(&f.root, n, "template", false); err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } return &Template{g: f, n: n}, nil @@ -149,18 +148,22 @@ func inputValue(input string) (any, error) { return raw, nil } -func inlineBinding(n node, label string, root map[string]node) binding { +// bindInline loads the shipped categories n reads, then binds n against root; typedByGo is +// binding's. +func bindInline(root *folder, n node, label string, typedByGo bool) error { scope := inlineScope(n, label) + loadShipped(root, unloadedReads(root, nil, scope)) return binding{ - scope: scope, - link: func() error { return linkNodeRefs(scope, root) }, + scope: scope, + link: func() error { return linkNodeRefs(scope, root.children) }, + typedByGo: typedByGo, scopeFence: func() error { if t, isTemplate := n.(*template); isTemplate && t.drawGroup != "" { return fmt.Errorf("%s: drawGroup %q names nothing, since nothing can reference an inline template; drop it", label, t.drawGroup) } return nil }, - } + }.bind() } // linkNodeRefs binds the references in an inline node's templates against the diff --git a/shipped.go b/shipped.go index a7c7525..5e6336d 100644 --- a/shipped.go +++ b/shipped.go @@ -9,6 +9,8 @@ import ( "strings" ) +//go:generate env REPIN=1 go test -run ^TestShippedIndexIsCurrent$ . + //go:embed data var shippedFS embed.FS @@ -43,6 +45,15 @@ func unloadedTree() folder { return root } +func sortedEntries(m map[string]shippedEntry) []string { + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + sort.Strings(names) + return names +} + // unloadedCategory is a shipped category not loaded yet: the folder holding it, that // folder's path, and its name there. type unloadedCategory struct { @@ -70,7 +81,8 @@ func unloadedAt(root *folder, segs []string) (unloadedCategory, bool) { } // loadShippedAt loads the shipped category a caller's path names or descends into, unless -// it is loaded already. +// it is loaded already. An entry point walking root calls it first: a walk that loaded +// would load mid-render. func (f *Generator) loadShippedAt(segs []string) { if u, unloaded := unloadedAt(&f.root, segs); unloaded { loadShipped(&f.root, []unloadedCategory{u}) @@ -100,8 +112,8 @@ func unloadedReads(root *folder, dir []string, scope nodeScope) []unloadedCatego return out } -// loadShipped loads the shipped categories wanted, with every category they read and the -// whole table family of each, then binds what it loaded, as New binds a whole load. +// loadShipped loads the shipped categories wanted, with every category they read and every +// table linked to one, then binds what it loaded, as New binds a whole load. // TestEveryShippedCategoryLoadsAlone proves each closure, so a failure is the index's. func loadShipped(root *folder, wanted []unloadedCategory) { var sites []categorySite @@ -113,18 +125,18 @@ func loadShipped(root *folder, wanted []unloadedCategory) { } site, err := u.load() if err != nil { - panic(internalError("shipped %s: %v", u.name, err)) + panic(internalError("shipped %s: %v; after a change under data/, regenerate shippedindex.go", join(strings.Join(u.dir, "."), u.name), err)) } sites = append(sites, site) queue = append(queue, unloadedReads(root, u.dir, sitesScope([]categorySite{site}))...) - queue = append(queue, u.family(e)...) + queue = append(queue, u.linkedTables(e)...) } if len(sites) == 0 { return } sort.Slice(sites, func(i, j int) bool { return sites[i].path < sites[j].path }) if err := categoryBinding(sites, root.children).bind(); err != nil { - panic(internalError("binding shipped categories: %v", err)) + panic(internalError("binding shipped categories: %v; after a change under data/, regenerate shippedindex.go", err)) } } @@ -144,15 +156,15 @@ func (u unloadedCategory) load() (categorySite, error) { return categorySite{dir: u.dir, path: join(strings.Join(u.dir, "."), u.name), in: u.in, n: u.in.children[u.name]}, nil } -// family is the tables beside u that a table family links it to: its parent, and every -// table whose parent it is. -func (u unloadedCategory) family(e shippedEntry) []unloadedCategory { +// linkedTables is the tables beside u that link to it or that it links to: its parent, and +// every table whose parent it is. +func (u unloadedCategory) linkedTables(e shippedEntry) []unloadedCategory { var out []unloadedCategory if e.parent != "" { out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: e.parent}) } - for name, sibling := range u.in.unloaded { - if sibling.parent == u.name { + for _, name := range sortedEntries(u.in.unloaded) { + if u.in.unloaded[name].parent == u.name { out = append(out, unloadedCategory{dir: u.dir, in: u.in, name: name}) } } diff --git a/shipped_test.go b/shipped_test.go index 84497ad..3c8819c 100644 --- a/shipped_test.go +++ b/shipped_test.go @@ -51,12 +51,16 @@ func TestShippedIndexIsCurrent(t *testing.T) { } return } - want, err := os.ReadFile(shippedIndexFile) + file, err := os.ReadFile(shippedIndexFile) + if err != nil { + t.Fatal(err) + } + want, err := format.Source(file) // docs/decisions.md#the-format-check-runs-on-the-latest-go-only if err != nil { t.Fatal(err) } if string(got) != string(want) { - t.Fatalf("%s is stale for data/; rewrite it with REPIN=1", shippedIndexFile) + t.Fatalf("%s is stale for data/; regenerate it with go generate", shippedIndexFile) } } diff --git a/struct.go b/struct.go index 0f74bf7..a27388c 100644 --- a/struct.go +++ b/struct.go @@ -258,10 +258,7 @@ func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, if err != nil { return fmt.Errorf("%s: %w", label, err) } - loadShipped(root, unloadedReads(root, nil, inlineScope(n, label))) - b := inlineBinding(n, label, root.children) - b.typedByGo = true - if err := b.bind(); err != nil { + if err := bindInline(root, n, label, true); err != nil { return err } record, columns, err := recordOf(n) diff --git a/todo.md b/todo.md index fa72a2d..d261058 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 79` +`Next ID = 86` | Goal | W | |---|---| @@ -52,7 +52,6 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| -| 76 | 0.1.0 | decision | **Generate the index of shipped categories at build time, and parse and link a shipped category only when a template or path first reaches it; a `--data-path` still loads whole.** | 4 | 8 | 10 | 10 | 2, 12 | 33.0 | | 77 | 0.1.0 | decision | **Split the shipped data into Go modules per locale and country, which a library imports by choice, and embed every one in the CLI.** | 5 | 8 | 10 | 10 | 2, 1.1 | 33.0 | | 78 | 0.1.0 | decision | **Ship 10% of every place's streets, at least 10, and 10% of all US places drawn by population, each place weighted by population.** | 6 | 7 | 10 | 10 | 2, 13.1 | 31.2 | | 61 | 0.1.0 | | **Add a locale, with its `geo/` tree, for each of the ten most-spoken languages and each Nordic country.** | 4 | 9 | 9 | 10 | 2.1 | 30.8 | @@ -68,7 +67,9 @@ | 12 | 0.1.0 | defect | **Strip the whitespace `data-import/territory.py` copies from its register.** | 1 | 1 | 5 | 7 | 4 | 20.6 | | 5 | 0.1.0 | | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 | | 71 | 0.1.0 | | **Accept a name as a `calc` operand, so `{calc(n * 2)}` computes from the pick `{n}` prints.** | 3 | 4 | 5 | 7 | 5.3 | 17.2 | +| 79 | 0.1.0 | decision | **Beside a `--data-path`, load only the shipped categories the user's sources override or reach, as the shipped set alone does.** | 6 | 6 | 7 | 8 | 12 | 13.7 | | 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 | +| 81 | 0.2.0 | question | **Refuse at load every `calc` divisor not proven nonzero, in a string column too.** | 4 | 4 | 6 | 8 | 4, 6 | 20.6 | | 36 | 0.2.0 | | **Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states.** | 5 | 6 | 7 | 8 | 6 | 19.5 | | 41 | 0.2.0 | | **Key every table by one key column, as the decision "A table selects by one key column, a code, never a free-form name" states.** | 6 | 7 | 7 | 8 | 5.7 | 19.1 | | 52 | 0.2.0 | | **Name the file a link error comes from beside its category path, `sub.x (/d/a/sub/x.json): …`.** | 1 | 2 | 5 | 7 | 6 | 19.0 | @@ -81,6 +82,7 @@ | 54 | 0.2.0 | defect | **Read the tag through `git/ref/tags/{tag}` in `publish_release.py`.** | 2 | 1 | 3 | 6 | 1.1 | 15.8 | | 31 | 0.2.0 | | **Rename `misc.territory.country` to `sovereign`.** | 3 | 2 | 4 | 7 | 5.8 | 15.7 | | 20 | 0.2.0 | defect | **Weight `sv_SE.sex` and `en_US.sex` by bearers, as the README's Data section says they are.** | 2 | 2 | 4 | 6 | 4 | 15.7 | +| 80 | 0.2.0 | defect | **Walk a path through one per-step function that both `pathCheck.walk` and `probePath` call.** | 4 | 4 | 4 | 7 | 3.2 | 15.6 | | 50 | 0.2.0 | | **Reword `prove`'s refusal of a typed column reading a row, `{/region}`.** | 1 | 2 | 4 | 6 | 6 | 15.5 | | 17 | 0.2.0 | | **Give every shipped category a column for each part its format composes, and one record shape across the national ids; re-pin the shape.** | 5 | 6 | 6 | 6 | 5 | 15.1 | | 48 | 0.2.0 | defect | **Refuse a `Fake` path into a level carrying a `repeat`, as the README's Correlated fields section states.** | 2 | 2 | 4 | 6 | 5 | 15.1 | @@ -90,6 +92,8 @@ | 32 | 0.2.0 | | **Add `misc.browser` as the parent of `misc.useragent`, so `misc.browser[Chrome].useragent` resolves.** | 3 | 3 | 4 | 6 | 4.1 | 14.4 | | 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 | | 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 9 | 14.0 | +| 85 | 0.2.0 | question | **Reword goal 9 to promise one output per seed and version, as the decision "Seeded output is promised within one version" reads it.** | 1 | 1 | 2 | 5 | 9 | 8.8 | +| 84 | 0.2.0 | question | **Reword goal 8 so a table's rows in a TSV beside its JSON meet it, or end the decision "Rows live in a TSV, the shape in JSON".** | 1 | 1 | 1 | 6 | 8 | 8.6 | | 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling.** | 3 | 3 | 4 | 6 | 5.7 | 13.9 | | 38 | 0.3.0 | defect | **Compare `calcParser.binary`'s operator as a rune.** | 1 | 1 | 3 | 6 | 6 | 13.8 | | 45 | 0.3.0 | | **Name the node a selector follows in `stepInto`'s refusal.** | 1 | 1 | 3 | 6 | 6 | 13.8 | @@ -101,12 +105,12 @@ | 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 3 | 5 | 4 | 13.0 | | 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 4 | 5 | 8 | 13.0 | | 21 | 0.3.0 | | **List `title` in both locales, and `sv_SE`'s `birth-number`, in the README's Data list of what each locale carries.** | 1 | 1 | 4 | 6 | 11 | 12.8 | -| 75 | 0.3.0 | | **Run every README `sh` example as a test, and compare each "Renders" line to a seeded render.** | 2 | 5 | 4 | 7 | 11.2 | 11.8 | | 23 | 0.3.0 | | **Give every other shipped category that could reach something real a path that never does.** | 3 | 6 | 6 | 7 | 15 | 11.7 | | 8 | 0.3.0 | | **Move `binding`, `bind` and `checkNodeFences` out of `data.go` into a file of their own.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 | | 9 | 0.3.0 | | **Move `table.route`, `selector`, `step` and `drawStep` from `path.go` to `table.go`.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 | | 30 | 0.3.0 | | **Draw `email.local` from `username`, so the two share one handle list.** | 2 | 2 | 3 | 5 | 5.7 | 11.5 | | 40 | 0.3.0 | | **Parse `defaultTable`'s path with the library's grammar.** | 2 | 2 | 3 | 5 | 5.7 | 11.5 | +| 75 | 0.3.0 | | **Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render.** | 2 | 6 | 4 | 7 | 11.2 | 11.5 | | 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 5 | 5 | 10.8 | | 47 | 0.3.0 | defect | **Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there.** | 1 | 1 | 2 | 5 | 6 | 10.2 | | 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 | @@ -114,7 +118,9 @@ | 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 13.1 | 9.8 | | 37 | 0.3.0 | | **Rename `newRand` to `newGeneratorState` and `Generator.rand` to `state`.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 | | 43 | 0.3.0 | | **Call `templateError` a compile failure in its doc.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 | +| 82 | 0.3.0 | principle | **Pass `newRand` its entropy source, so no test swaps the package's `randomBytes`.** | 1 | 1 | 1 | 5 | 3.2 | 9.8 | | 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 1 | 4 | 1 | 8.8 | +| 83 | 0.3.0 | principle | **Read a name binding's head and tail from its binder's link, and delete `linkBindings`.** | 4 | 3 | 1 | 6 | 3.2 | 8.1 | | 63 | 0.4.0 | | **Pair a street with its exact postnummer.** | 3 | 6 | 5 | 8 | 4.1 | 19.1 | | 62 | 0.4.0 | | **Add `{btc()}` and `{eth()}`.** | 2 | 4 | 4 | 6 | 4 | 15.2 | | 59 | 0.4.0 | | **Promise in `Generator`'s godoc that its renders run one at a time.** | 1 | 1 | 5 | 5 | 11 | 13.8 | @@ -128,13 +134,9 @@ ## Details -### 76. Generate the index of shipped categories at build time, and parse and link a shipped category only when a template or path first reaches it; a `--data-path` still loads whole. - -A restructure the maintainer approved on 2026-10-03, so it merges below goal 3's floor and goes first; the round's gates are adjusted to it afterwards. `go generate` writes an index of every shipped category, path and table, embedded beside the data. `New` reads the index; a template, `Fake` path or record compiles the categories it reaches, transitively, and keeps them. CI loads every shipped category eagerly, so shipped data never fails for a user. Revise the decisions this touches, "The shipped data is embedded, not discovered" among them, and the README's Performance section. - ### 77. Split the shipped data into Go modules per locale and country, which a library imports by choice, and embed every one in the CLI. -Needs item 76. The CLI binary carries every module and may grow to hundreds of MB; a library imports only what it uses, as it imports any dependency, and nothing is fetched at run time. Decide the module boundaries (a locale, a country's `geo/` tree, `misc`) and how an imported module registers with `New`. Goal 2 says what ships where since 2026-10-03. +The CLI binary carries every module and may grow to hundreds of MB; a library imports only what it uses, as it imports any dependency, and nothing is fetched at run time. Decide the module boundaries (a locale, a country's `geo/` tree, `misc`) and how an imported module registers with `New`. Goal 2 says what ships where since 2026-10-03. ### 78. Ship 10% of every place's streets, at least 10, and 10% of all US places drawn by population, each place weighted by population. @@ -253,10 +255,18 @@ Needs item 4. `checkCalc` and the value proof read an operand as a sibling field, so `{calc(n * 2)}` is refused with `no field "n"` today. Item 4 stops an operand holding its field for the expansion, after which a name is the one way to show the operand a calc computes from, as the README's Computation example does. +### 79. Beside a `--data-path`, load only the shipped categories the user's sources override or reach, as the shipped set alone does. + +The decision "The shipped set alone loads a category on the first call reaching it; beside a `--data-path` every category loads in `New`" parses the whole shipped set on every `--data-path` run, about 45 ms today and seconds once items 61 and 78 land. Goal 6 needs only the user's categories, the shipped ones they override, and the shipped ones whose reads reach those, so the index would carry each category's reads. + ### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go. GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged. +### 81. Refuse at load every `calc` divisor not proven nonzero, in a string column too. + +The decision "A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`" lets `{calc(a/(b*c))}` with `b` fixed at `"0"` write `Inf` into every row, against goals 4 and 6; `valueproof.go` already proves a typed column's divisor nonzero. Changing the decision is the maintainer's call. + ### 36. Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states. `New` and `NewTemplate` stop at the first, so data holding two mistakes takes two runs to fix. Split it into items before starting, one of them the shape a Go caller iterates and the CLI prints, and the order of an inline template's mistakes, which name no path; reword the README's "`New` refuses a mistake in the data" to every mistake. @@ -302,6 +312,10 @@ It holds a code, and `country` collides with the `misc.country` path it replaced Neither names a `weight`, so a bare `sex` draws evenly. +### 80. Walk a path through one per-step function that both `pathCheck.walk` and `probePath` call. + +The decision "A path draws through its compiled steps" keeps two loops over one rule for passing a choice, a table or a field, and they have drifted: `walk` refuses a `repeat` or `drawGroup` level through `enter`, `probePath` does not (item 48). Technical principle: DRY. + ### 50. Reword `prove`'s refusal of a typed column reading a row, `{/region}`. Say the format is the table's own (`region's format "{name}"`), name the read to write, `{/region.}` with its columns, and stop calling a one-column format, `{code}`, composed text. @@ -330,6 +344,14 @@ Require the path step to reach a sibling category. `readTableOptions` and `loadDir` return on the first in Go's map order. +### 85. Reword goal 9 to promise one output per seed and version, as the decision "Seeded output is promised within one version" reads it. + +Goal 9 says the same seed always gives the same output; the decision promises it within one version. + +### 84. Reword goal 8 so a table's rows in a TSV beside its JSON meet it, or end the decision "Rows live in a TSV, the shape in JSON". + +Goal 8 says data is JSON files, and the decision serves goal 12, which ranks below it. Proposed wording: "Data is JSON files, a table's rows a TSV beside them. Code is only for what JSON can't express." + ### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling. Both render the same run. @@ -366,10 +388,6 @@ Needs item 4; recount the switches then. Today ten, from `render` to `columnItem Nothing checks it, and a new country breaks it silently. -### 75. Run every README `sh` example as a test, and compare each "Renders" line to a seeded render. - -`readme_test.go` loads and renders each `json` block, but runs no `sh` example (CLI, Records, Table, Linked tables) and compares no "Renders e.g." output, so goal 11.2 is not met. - ### 23. Give every other shipped category that could reach something real a path that never does. `phone` draws live PTS and NANP ranges — PTS's five fiction series are the Swedish inert set, sourced in [research-sources-se.md](docs/research/research-sources-se.md), and the NANP one still wants a source — and `bankgiro`, `plusgiro` and `routing` draw live prefixes. @@ -386,6 +404,10 @@ The inherited architect's 3am trace crossed four files for one draw. The CLI counts brackets of its own, so a change to the selector grammar desyncs the default `--table` silently. +### 75. Run every README `sh`, `go` and `text` example as a test, and compare each "Renders" line to a seeded render. + +`readme_test.go` loads and renders each `json` block, but runs no `sh` example (CLI, Records, Table, Linked tables), no `go` example (Library) and no `text` block of error text, and compares no "Renders e.g." output, so goal 11.2 is not met. + ### 47. Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there. `loadDir` indexes hidden TSVs and skips them in two places, so one loads where its comment says a hidden file is never data. @@ -406,10 +428,18 @@ Both name half of a `generatorState`, hiding its `{seq()}` counters, and `New`'s Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTemplate` refuse. +### 82. Pass `newRand` its entropy source, so no test swaps the package's `randomBytes`. + +`fejkdata_test.go` swaps the global `randomBytes` to fail seeding, so every `New` in the package reads the failing source while that test runs. Technical principle: functional core, imperative shell. + ### 56. Build on a manual run of `test.yml`. `workflow_dispatch` leaves `github.event.before` empty, so the diff compares `HEAD` with itself and skips `docker build`. +### 83. Read a name binding's head and tail from its binder's link, and delete `linkBindings`. + +`linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. Technical principle: one owner per value. + ### 63. Pair a street with its exact postnummer. Needs an application to Lantmäteriet; today a street goes to the nearest postal code centroid. It rewrites shipped rows, so it is a major once 1.0 is cut and a minor before that.