diff --git a/README.md b/README.md index 047325e..2c5fd45 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,8 @@ it to a name and read the name The shipped data comes in modules, each a Go package under [`data/`](data) that embeds its files, so nothing is read from disk: one per locale (`en_US`, `sv_SE`), one per country's tree of places (`geo/SE`, `geo/US`), and the locale-neutral `misc`. The CLI -carries every module, and a library loads those it imports ([Library](#library)). A +carries every module, and a library loads those it passes to `WithDataFS` +([Library](#library)). A directory is a namespace: each JSON file is a category named after the file, each subdirectory a dot-path segment, so `mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one. A module's tree spells its namespace the same way, so diff --git a/data.go b/data.go index 68dbc53..c542424 100644 --- a/data.go +++ b/data.go @@ -8,30 +8,36 @@ import ( // loadSources merges sources in order, the last winning a clash. A source whose manifest // carries an index leaves its categories unloaded; every other source loads here, with -// the unloaded categories it needs. +// the unloaded categories it needs: docs/decisions.md#a-source-whose-manifest-carries-an-index-loads-each-category-on-first-reach-any-other-loads-in-new-with-what-it-reads func loadSources(sources []datafiles.Source) (folder, error) { root := folder{children: map[string]node{}} + defaultModules := map[string][]string{} for _, src := range sources { - if err := addSource(&root, src); err != nil { + if err := addSource(&root, src, defaultModules); err != nil { return folder{}, err } } - return root, loadReached(&root, categorySites(&root), nil) + return root, loadReached(&root, categorySites(&root), nil, defaultModules) } -func addSource(root *folder, src datafiles.Source) error { +// addSource places src's categories in root, and sets in defaultModules, for each category +// it loads, the modules src reads by default. +func addSource(root *folder, src datafiles.Source, defaultModules map[string][]string) error { m, err := src.Manifest() if err != nil { return err } if m.Index != nil { - placeIndex(root, src, m.Index) + placeIndex(root, src, m) return nil } g := &folder{children: map[string]node{}} if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil { return err } + for _, s := range categorySites(g) { + defaultModules[s.path] = m.Reads + } mergeFolder(root, g) return nil } diff --git a/data/modules.go b/data/modules.go index b8ee847..931f3ae 100644 --- a/data/modules.go +++ b/data/modules.go @@ -1,4 +1,5 @@ -// Package data lists every module fejkdata ships. +// Package data lists every module fejkdata ships. A module's tree spells its namespace: +// data/geo/SE embeds geo/SE/…, whose categories are geo.SE.*. package data import ( diff --git a/data_test.go b/data_test.go index c736447..c9e61ba 100644 --- a/data_test.go +++ b/data_test.go @@ -63,7 +63,7 @@ func TestShippedDataCategories(t *testing.T) { } } -// TestShippedMiscCategories covers the locale-neutral data/misc folder: a proper +// TestShippedMiscCategories covers the locale-neutral misc module: a proper // v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers // whose trailing {luhn()} check passes. func TestShippedMiscCategories(t *testing.T) { @@ -467,8 +467,8 @@ func TestShippedTableCellsAreTrimmed(t *testing.T) { } } -// TestShippedNamespacedTree loads the whole data/ tree (not a single locale) and -// reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person. +// TestShippedNamespacedTree loads every shipped module and reaches each locale through its +// folder segment: data/sv_SE/sv_SE/person.json -> sv_SE.person. func TestShippedNamespacedTree(t *testing.T) { f := shipped(t, WithSeed(1)) for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} { diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md index 1db3a88..7d7be04 100644 --- a/docs/comprehension-history.md +++ b/docs/comprehension-history.md @@ -304,3 +304,29 @@ Junior A, decided by: - Locality, Shape: `node.go:23` `folder`: `put` and `putUnloaded` keep a name in one map, where `compileInto` and `unloadedCategory.load` kept it as a pair - Locality, Shape: `data.go:12` `loadSources`: one load path, where `config.load` forked and `loadShipped` was a second, panic-only pipeline - Shape: `data.go:81` `categorySite`: embeds `categoryAt`, where `unloadedCategory` repeated its fields + +## 2026-10-10T11:11:16Z, PR #203 at 421bb49, against bf1becb + +Ruling: worse + +| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | +|---|---|---|---|---|---| +| Mid A | same | worse | worse | same | worse | +| Inherited architect | same | worse | worse | same | worse | + +Mid A, decided by: + +- Locality, Overall: `node.go:28` `folder.reads`: a third per-name map beside children and unloaded with no invariant tying it to them; every placement site must remember a separate setReads call (`data.go:36` addSource, `data.go:76` mergeFolder, `index.go:27` placeIndex) +- Locality, Shape: `resolve.go:96` `withReads`: the manifest's reads travels through folder, categorySite.reads and templateSite.category to feed one error clause, four files to hold for one sentence of error text +- Shape: `internal/datafiles/manifest.go:22` `Manifest.Reads`: reads as a load dependency the loader acts on, though it only feeds error text, and "reads" already means a template reading a name or column +- Shape: `data/geo/SE/fs.go:2` `SE`: package names SE, US, en_US, sv_SE; in Modules SE.FS sits beside sv_SE.FS with nothing saying which is geography +- Locality: `fejkdata.go:94` `New`: loads exactly what the options name, easier than the hidden config{shipped: true} default, though config.given brings back a smaller flag + +Inherited architect, decided by: + +- Locality, Shape, Overall: `node.go:27` `folder.reads`: a third per-name map that put and putUnloaded leave alone, so addSource, mergeFolder and placeIndex must each call setReads, and batch.load relies on placeIndex having done it; action at a distance for one error suffix +- Locality: `resolve.go:94` `templateSite.withReads`: reads travels folder, categorySite.reads, a *categorySite per template, then withReads, four hops across three files +- Shape: `data/geo/SE/fs.go:2` `SE`: bare package names SE and US do not say geography at WithDataFS(sv_SE.FS, SE.FS); the namespace-doubling rule lives only in README and test code +- Shape, Self-sufficiency: `internal/datafiles/manifest.go:22` `Manifest.Reads`: suggests it drives loading though it only feeds withReads; "by default" needs README goal 9.2 +- Locality, Navigation: `data/modules.go:17` `Modules`: the hidden shippedFS default is gone; Modules lists the modules, errNoData names the fix, and the CLI loads data.Modules() in plain sight +- Locality: `fejkdata.go:40` `config.given`: WithDataFS() with no arguments differs from no option, learnt only from its comment diff --git a/docs/decisions.md b/docs/decisions.md index 9abdb7d..2ecf559 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -143,7 +143,7 @@ package under `data/` embeds its own files, and a library imports the packages i - One Go package per locale, one per country's `geo/` tree, and one for `misc`, all in the core's Go module. A package registers nothing when imported. - A bare `New()` loads no data and fails, naming the option to add. `WithoutShippedData` goes. -- A data package's `FS` is an `fs.FS` passed to `WithDataFS`, as anyone's data is, and `New` takes several. A module carrying functions loads through an option of its own, so a data module that starts carrying functions breaks its users. Such a function reaches the randomness through an interface the root declares; `internal/drawstate`'s type stays internal. +- A data package's `FS` is an `fs.FS` passed to `WithDataFS`, as anyone's data is, and `New` takes several. It is an exported `var FS embed.FS`, the shape `//go:embed` gives; a function returning it would guard only against a program reassigning a dependency of its own. A module carrying functions loads through an option of its own, so a data module that starts carrying functions breaks its users. Such a function reaches the randomness through an interface the root declares; `internal/drawstate`'s type stays internal. - A module names, in a manifest, every module it reads by default, directly or through another, so the first error names the whole set to import. A manifest is optional: a `--data-path` folder without one is a module. Nothing loads a default on its own. - A manifest names no fejkdata version, and `New` checks none: the shipped packages ship in the core's Go module, and a module from anyone else keeps loading across a minor or a patch, as the README's Versioning promises. The maintainer chose so on 2026-10-09. - In the CLI, a flag of its own says a `--data-path` replaces what it clashes with, so flags still go anywhere on the line. Data authors and hand fixture authors bench it. @@ -582,7 +582,9 @@ when a source carries it. 2026-09-24, Lilleman auf Larv; revised 2026-10-10, when the shipped data became Go packages. Serves goal 6.2. `sv_SE.FS` loads alone, and `sv_SE.address` then fails at first reach, -naming `github.com/larvit/fejkdata/data/geo/SE`, which `sv_SE`'s manifest reads. +naming `github.com/larvit/fejkdata/data/geo/SE`, which `sv_SE`'s manifest reads. `New` +checks no `reads`: a stand-in providing the same paths under another name would fail it +(goal 9.2). `--no-shipped-data -d` takes a locale's folder with its country's, or a set of one's own. ## 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 diff --git a/fejkdata.go b/fejkdata.go index 54a5eaf..0504b51 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -65,7 +65,7 @@ func WithDataPath(dir string) Option { func WithDataFS(modules ...fs.FS) Option { return func(c *config) { for _, fsys := range modules { - c.sources = append(c.sources, datafiles.FS(fsys, "")) + c.sources = append(c.sources, datafiles.FS(fsys)) } c.given = true } diff --git a/index.go b/index.go index 4f1ee78..25ce958 100644 --- a/index.go +++ b/index.go @@ -11,10 +11,12 @@ import ( "github.com/larvit/fejkdata/internal/grammar" ) -// indexed is a category an index names and no call has reached yet, and the source holding it. +// indexed is a category an index names and no call has reached yet, the source holding it, +// and the modules that source reads by default. type indexed struct { - entry datafiles.IndexEntry - src datafiles.Source + entry datafiles.IndexEntry + src datafiles.Source + defaultModules []string } // placeIndex sets every category m's index names as unloaded in root, in place of what @@ -22,9 +24,7 @@ type indexed struct { func placeIndex(root *folder, src datafiles.Source, m datafiles.Manifest) { for _, p := range sortedNames(m.Index) { segs := strings.Split(p, ".") - g, name := madeFolder(root, segs[:len(segs)-1]), segs[len(segs)-1] - g.putUnloaded(name, &indexed{entry: m.Index[p], src: src}) - g.setReads(name, m.Reads) + madeFolder(root, segs[:len(segs)-1]).putUnloaded(segs[len(segs)-1], &indexed{entry: m.Index[p], src: src, defaultModules: m.Reads}) } } @@ -60,7 +60,7 @@ func (f *Generator) loadCallerPath(path string) (string, []string, error) { return "", nil, fmt.Errorf("fejkdata: %w", err) } if c, ok := categoryOn(&f.root, segs); ok && c.unloaded() { - if err := loadReached(&f.root, nil, []categoryAt{c}); err != nil { + if err := loadReached(&f.root, nil, []categoryAt{c}, nil); err != nil { return "", nil, fmt.Errorf("fejkdata: %w", loadError{err}) } } @@ -107,8 +107,9 @@ func needs(root *folder, s categorySite) []categoryAt { // loadReached loads the unloaded categories among wanted and every unloaded category they // or sites need, then runs sites and all it loaded through a whole load's pipeline. On an -// error it puts back what it loaded, so the next call fails the same way. -func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error { +// error it puts back what it loaded, so the next call fails the same way. defaultModules +// holds, by path, the modules the source of each of sites reads by default. +func loadReached(root *folder, sites []categorySite, wanted []categoryAt, defaultModules map[string][]string) error { queue := wanted for _, s := range sites { queue = append(queue, needs(root, s)...) @@ -119,6 +120,14 @@ func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error err = b.run(root, sites) } if err != nil { + err = withDefaultModules(err, func(category string) []string { + for _, l := range b { + if l.site.path == category { + return l.was.defaultModules + } + } + return defaultModules[category] + }) b.putBack() } return err @@ -169,6 +178,19 @@ func (b batch) run(root *folder, sites []categorySite) error { return categoryPipeline(sites, root.children).run() } +// withDefaultModules adds to err, where a reference of a category names nothing, the modules +// the category's source reads by default, or that it names none. +func withDefaultModules(err error, of func(category string) []string) error { + var u unresolvedRead + if !errors.As(err, &u) { + return err + } + if modules := of(u.category); len(modules) > 0 { + return fmt.Errorf("%w; the module holding %s reads by default %s", err, u.category, strings.Join(modules, ", ")) + } + return fmt.Errorf("%w, and the module holding %s names no module it reads by default", err, u.category) +} + func (b batch) putBack() { for _, l := range b { l.site.in.putUnloaded(l.site.name, l.was) diff --git a/inline.go b/inline.go index a9e15b3..660b765 100644 --- a/inline.go +++ b/inline.go @@ -90,7 +90,7 @@ func loadInline(root *folder, n node, label string, typedByGo bool) error { if err := refuseFolderRefs(nodes); err != nil { return err } - if err := loadReached(root, nil, referenced(root, nil, nodes)); err != nil { + if err := loadReached(root, nil, referenced(root, nil, nodes), nil); err != nil { return loadError{err} } return pipeline{ diff --git a/internal/datafiles/datafiles.go b/internal/datafiles/datafiles.go index 5739bc7..b57c63e 100644 --- a/internal/datafiles/datafiles.go +++ b/internal/datafiles/datafiles.go @@ -12,13 +12,12 @@ import ( "github.com/larvit/fejkdata/internal/jsonvalue" ) -// Source is one tree to load: an fs.FS and the directory in it to start from. label -// prefixes file names in errors; onDisk marks label as a directory that must exist. +// Source is one tree to load, from the root of an fs.FS. label prefixes file names in +// errors; onDisk marks label as a directory that must exist. type Source struct { fsys fs.FS label string onDisk bool - base string } // Dir is the tree in the directory path, its errors labelled with it. @@ -26,10 +25,10 @@ func Dir(path string) Source { return Source{fsys: os.DirFS(path), label: path, onDisk: true} } -// FS is the tree in fsys below base, "" for its root. -func FS(fsys fs.FS, base string) Source { return Source{fsys: fsys, base: base} } +// FS is the tree in fsys. +func FS(fsys fs.FS) Source { return Source{fsys: fsys} } -// Category is one category file: the folders above it from the source's base, its name, +// Category is one category file: the folders above it from the source's root, its name, // and its parsed JSON. type Category struct { Folders []string @@ -50,7 +49,7 @@ func (s Source) labelled(p string) string { // dirPath is the folder that dir names, as fsys spells it. func (s Source) dirPath(dir []string) string { - if p := path.Join(append([]string{s.base}, dir...)...); p != "" { + if p := path.Join(dir...); p != "" { return p } return "." diff --git a/internal/datafiles/manifest.go b/internal/datafiles/manifest.go index fd22908..e7a8c5a 100644 --- a/internal/datafiles/manifest.go +++ b/internal/datafiles/manifest.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io/fs" - "path" "sort" "strings" @@ -19,7 +18,8 @@ const ManifestFile = ".fejkdata.json" type Manifest struct { // Index is every category of the source by dot path; nil where the manifest has none. Index map[string]IndexEntry `json:"index"` - // Reads is every module the source reads by default, directly or through another. + // Reads is every module the source reads by default, directly or through another; the + // error for a reference naming nothing names them, and nothing loads them. Reads []string `json:"reads"` } @@ -35,7 +35,7 @@ func (s Source) Manifest() (Manifest, error) { if err := s.check(); err != nil { return Manifest{}, err } - b, err := fs.ReadFile(s.fsys, path.Join(s.base, ManifestFile)) + b, err := fs.ReadFile(s.fsys, ManifestFile) if errors.Is(err, fs.ErrNotExist) { return Manifest{}, nil } @@ -50,7 +50,7 @@ func (s Source) Manifest() (Manifest, error) { } // ManifestPath is the manifest's path as errors name it. -func (s Source) ManifestPath() string { return s.labelled(path.Join(s.base, ManifestFile)) } +func (s Source) ManifestPath() string { return s.labelled(ManifestFile) } // decodeManifest decodes a manifest, refusing any key or shape it does not define, and // reports every mistake: first each entry's own, in path order, then each parent naming diff --git a/pick.go b/pick.go index b79fbf3..47ebf04 100644 --- a/pick.go +++ b/pick.go @@ -19,7 +19,7 @@ import ( // the read's first-level key, put under the rendering template's key, is one of the name's // addressed keys. keptInPick decides this, and readUnder reads it. // -// In data/geo/SE/address.json, {.locality as l} binds l, and {l.street.name} passes the levels +// In geo.SE.address, {.locality as l} binds l, and {l.street.name} passes the levels // "", "street" and "street.name", the last of which is its key. With {l.name} and {l.postal-code.code}, // l's addressed keys are those three, "name", "postal-code" and "postal-code.code". Where p // binds a category whose format reads {first}, and both {p} and {p.first} are read, the {first} @@ -174,8 +174,8 @@ func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame { // When the pick renders another category, enter takes that category's frames from the pick's // memo, and renderFrame finds them and opens no new one. // So every read through one pick sees one pick of each name the other category binds: -// {a.street} and {a.postal-code} in data/sv_SE/address.json read one pick of the name l -// that data/geo/SE/address.json binds. +// {a.street} and {a.postal-code} in sv_SE.address read one pick of the name l +// that geo.SE.address binds. func readName(s *drawstate.State, env renderEnv, a arm) readValue { f := env.frameOf(a.named.scope) if f == nil { diff --git a/resolve.go b/resolve.go index ba82a17..01f8295 100644 --- a/resolve.go +++ b/resolve.go @@ -3,18 +3,26 @@ package fejkdata import ( "errors" "fmt" - "strings" ) -// templateSite is a template to resolve, with its folder and category, both empty for an inline -// template, and eachNode's label for it. +// templateSite is a template to resolve, with its folder and its category's path, both empty for an +// inline template, and eachNode's label for it. type templateSite struct { t *template folder []string label string - category *categorySite + category string } +// unresolvedRead is a reference of a category's template naming nothing. +type unresolvedRead struct { + category string + err error +} + +func (u unresolvedRead) Error() string { return u.err.Error() } +func (u unresolvedRead) Unwrap() error { return u.err } + // resolveCategoryTemplates resolves every template of the categories, once all data is merged, so a // reference sees the override-resolved tree. func resolveCategoryTemplates(sites []categorySite, root map[string]node) error { @@ -22,7 +30,7 @@ func resolveCategoryTemplates(sites []categorySite, root map[string]node) error for _, s := range sites { if err := eachNode(s.n, s.path, func(label string, n node) error { if t, isTemplate := n.(*template); isTemplate { - ts = append(ts, templateSite{t: t, folder: s.dir, label: label, category: &s}) + ts = append(ts, templateSite{t: t, folder: s.dir, label: label, category: s.path}) } return nil }); err != nil { @@ -58,8 +66,12 @@ type resolvedNames struct { func resolveTemplates(ts []templateSite, root map[string]node) error { for _, s := range ts { refs, err := s.t.resolveRefs(s.folder, s.label, root) + var ne noEntry + if err != nil && s.category != "" && errors.As(err, &ne) { + return unresolvedRead{category: s.category, err: err} + } if err != nil { - return s.withReads(err) + return err } s.t.refs = refs } @@ -90,16 +102,3 @@ func resolveTemplates(ts []templateSite, root map[string]node) error { } return nil } - -// withReads adds to err, where a reference of a category names nothing, the modules the -// category's manifest names it reading by default, or that it names none. -func (s templateSite) withReads(err error) error { - var ne noEntry - if s.category == nil || !errors.As(err, &ne) { - return err - } - if len(s.category.reads) == 0 { - return fmt.Errorf("%w, and the module holding %s names no module it reads by default", err, s.category.path) - } - return fmt.Errorf("%w; the module holding %s reads by default %s", err, s.category.path, strings.Join(s.category.reads, ", ")) -} diff --git a/todo.md b/todo.md index ed9aad7..4de0b65 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 195` +`Next ID = 196` | Goal | W | |---|---| @@ -128,6 +128,7 @@ | 168 | 0.2.0 | principle | **Test the shared steps of `data-import/` in the merge gate, starting with `geo.fence`.** | 2 | 4 | 2 | 6 | 3 | 11.8 | | 161 | 0.2.0 | | **Let a field whose name starts or ends with whitespace be bound to a name, or refuse such an identifier, so `{ x as p}` means one thing.** | 2 | 2 | 2 | 6 | 5.6, 4.1 | 11.7 | | 175 | 0.2.0 | | **Rewrite the comments the 2026-10-09 panel needed several reads of, and delete those it read as restating their code.** | 1 | 2 | 1 | 6 | 3.4 | 11.3 | +| 195 | 0.2.0 | principle | **Parse each builtin argument once, in `checkArgs`, and hand `prep` and `proveNumber` the parsed value.** | 3 | 3 | 2 | 6 | 3 | 11.1 | | 112 | 0.2.0 | | **Check a calc's field operands where its template resolves, beside its name operands, once item 36 reports every mistake at once.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 | | 172 | 0.2.0 | | **Pass `drawSteps` the kind of read it draws as an argument of its own, so no caller signals a sibling path by a nil `pins` or a fresh read by a nil `memo`.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 | | 173 | 0.2.0 | | **Call what a token reads its spelling, never a name, in `format.go`, `arm.go` and `reference.go`, so a name in the engine is only a bound name.** | 2 | 3 | 1 | 6 | 3.2 | 10.1 | @@ -181,7 +182,7 @@ Pick the ten from a published ranking of languages by total speakers, such as Et ### 185. Seat a panel of the README's audience personas to pull data in through the module API and build a custom CLI, and fix what it names. -Needs item 184; 0.1.0 is not cut before it. Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Seats: the Go test author, the bulk fixture author, a custom CLI's builder, and a data author with a `-d` folder and the replace flag, each in a scratch Go module against the checkout or a pre-release tag. Bench the geo packages' names, `data.Modules()`, the error naming the import set, and the `.fejkdata.json` manifest a data author writes by hand. Decoding the manifest is most of `New`'s cost (about 0.55 ms and 2449 allocations on 2026-10-10). An entry lists every path below its category, its linked tables' paths too, so `geo.*.region` repeats its whole family. An entry holding only its own paths, with `List` composing a family from the entries naming it as `parent`, would shrink it. Record the ruling in `docs/`. +Needs item 184; 0.1.0 is not cut before it. Carries out the decision "The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0". Seats: the Go test author, the bulk fixture author, a custom CLI's builder, and a data author with a `-d` folder and the replace flag, each in a scratch Go module against the checkout or a pre-release tag. Bench the geo packages' names, `data.Modules()`, the error naming the import set, and the `.fejkdata.json` manifest a data author writes by hand. Decoding the manifest is most of `New`'s cost (about 0.55 ms and 2449 allocations on 2026-10-10, 2864 allocations once each module carried its own). An entry lists every path below its category, its linked tables' paths too, so `geo.*.region` repeats its whole family. An entry holding only its own paths, with `List` composing a family from the entries naming it as `parent`, would shrink it. Record the ruling in `docs/`. ### 16. Add locale categories: company, phone, finance, vehicle, words. @@ -414,7 +415,7 @@ A hyphen is always subtraction in a calc, and the shipped data has hyphenated fi ### 194. Let a data author write and check their own `.fejkdata.json` index with the library or the CLI, and fail a load whose index leaves out a category file. -Goal 6.3 has an author prove their module, and goal 9.1 has fejkdata's data load as anyone's; today only fejkdata's own test, `TestShippedManifestIsCurrent` over `indexEntry`, writes and checks an index. A category file the index leaves out never loads, silently. +Goal 6.3 has an author prove their module, and goal 9.1 has fejkdata's data load as anyone's; today only fejkdata's own test, `TestShippedManifestsAreCurrent` over `indexEntry`, writes and checks an index. A category file the index leaves out never loads, silently. ### 53. Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean. @@ -488,6 +489,10 @@ Against "Most, if not all, code should have automated tests" in `technical-princ Reread: `readField`'s note on clearing the pick (`render.go`), `pickKey.under`, which three seats read backwards, `scopeAround`'s "short of the frames n renders itself", `enter` returning `env.base` as the pop mark, and add a line at `walkEvery`'s three-index slice, which three seats found unmarked. Restating: `compileCells`, `columnOrLinked`, `render`'s first line, `categorySites`, `madeFolder`, `categoryOf`, `rendersInside`, `templateSite`, `templateOptions`, `compileFrom`, `flagArg`, `drawSeed`, the `calc` parser's precedence line in `internal/grammar`, and `compileChoice`'s note that a uniform choice draws in O(1), which `choice`'s doc already says. A doc comment `go doc` shows on an exported name stays. +### 195. Parse each builtin argument once, in `checkArgs`, and hand `prep` and `proveNumber` the parsed value. + +Applies "Validate data and build DTOs as early as possible" in `~/.claude/principles/technical-principles.md`. Today `checkArgs` (`internal/builtinfunc/builtins.go`) checks an argument, then `prep` and `proveNumber` parse it again through `atoi`/`atof`, which panic, as do `layout.go`, `transform.go` and `calc.go`; two parsers can drift, and a drift panics at compile instead of failing the load. + ### 112. Check a calc's field operands where its template resolves, beside its name operands, once item 36 reports every mistake at once. Needs item 36. `checkCalcFields` runs at compile and `checkCalcNames` in `resolveTemplates`, two passes over one rule. Moving the field check there before item 36 would change which of two mistakes in two category files a load reports, since every category compiles before any resolves.