Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
14 changed files with 120 additions and 59 deletions
Showing only changes of commit 50e1a9fb22 - Show all commits
+2 -1
View File
@@ -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 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 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 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 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 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 replaces the shipped one. A module's tree spells its namespace the same way, so
+11 -5
View File
@@ -8,30 +8,36 @@ import (
// loadSources merges sources in order, the last winning a clash. A source whose manifest // 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 // 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) { func loadSources(sources []datafiles.Source) (folder, error) {
root := folder{children: map[string]node{}} root := folder{children: map[string]node{}}
defaultModules := map[string][]string{}
for _, src := range sources { for _, src := range sources {
if err := addSource(&root, src); err != nil { if err := addSource(&root, src, defaultModules); err != nil {
return folder{}, err 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() m, err := src.Manifest()
if err != nil { if err != nil {
return err return err
} }
if m.Index != nil { if m.Index != nil {
placeIndex(root, src, m.Index) placeIndex(root, src, m)
return nil return nil
} }
g := &folder{children: map[string]node{}} g := &folder{children: map[string]node{}}
if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil { if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil {
return err return err
} }
for _, s := range categorySites(g) {
defaultModules[s.path] = m.Reads
}
mergeFolder(root, g) mergeFolder(root, g)
return nil return nil
} }
+2 -1
View File
@@ -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 package data
import ( import (
+3 -3
View File
@@ -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 // v4 UUID (version/variant nibbles fixed), a MAC address, and credit-card numbers
// whose trailing {luhn()} check passes. // whose trailing {luhn()} check passes.
func TestShippedMiscCategories(t *testing.T) { 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 // TestShippedNamespacedTree loads every shipped module and reaches each locale through its
// reaches each locale through its folder segment: data/sv_SE/person -> sv_SE.person. // folder segment: data/sv_SE/sv_SE/person.json -> sv_SE.person.
func TestShippedNamespacedTree(t *testing.T) { func TestShippedNamespacedTree(t *testing.T) {
f := shipped(t, WithSeed(1)) f := shipped(t, WithSeed(1))
for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} { for _, path := range []string{"sv_SE.person", "en_US.person", "sv_SE.address.locality"} {
+26
View File
@@ -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: `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 - 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 - 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
+4 -2
View File
@@ -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. - 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 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 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. - 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. - 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. 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, 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. `--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 ## 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
+1 -1
View File
@@ -65,7 +65,7 @@ func WithDataPath(dir string) Option {
func WithDataFS(modules ...fs.FS) Option { func WithDataFS(modules ...fs.FS) Option {
return func(c *config) { return func(c *config) {
for _, fsys := range modules { for _, fsys := range modules {
c.sources = append(c.sources, datafiles.FS(fsys, "")) c.sources = append(c.sources, datafiles.FS(fsys))
} }
c.given = true c.given = true
} }
+29 -7
View File
@@ -11,10 +11,12 @@ import (
"github.com/larvit/fejkdata/internal/grammar" "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 { type indexed struct {
entry datafiles.IndexEntry entry datafiles.IndexEntry
src datafiles.Source src datafiles.Source
defaultModules []string
} }
// placeIndex sets every category m's index names as unloaded in root, in place of what // 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) { func placeIndex(root *folder, src datafiles.Source, m datafiles.Manifest) {
for _, p := range sortedNames(m.Index) { for _, p := range sortedNames(m.Index) {
segs := strings.Split(p, ".") segs := strings.Split(p, ".")
g, name := madeFolder(root, segs[:len(segs)-1]), segs[len(segs)-1] madeFolder(root, segs[:len(segs)-1]).putUnloaded(segs[len(segs)-1], &indexed{entry: m.Index[p], src: src, defaultModules: m.Reads})
g.putUnloaded(name, &indexed{entry: m.Index[p], src: src})
g.setReads(name, m.Reads)
} }
} }
@@ -60,7 +60,7 @@ func (f *Generator) loadCallerPath(path string) (string, []string, error) {
return "", nil, fmt.Errorf("fejkdata: %w", err) return "", nil, fmt.Errorf("fejkdata: %w", err)
} }
if c, ok := categoryOn(&f.root, segs); ok && c.unloaded() { 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}) 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 // 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 // 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. // error it puts back what it loaded, so the next call fails the same way. defaultModules
func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error { // 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 queue := wanted
for _, s := range sites { for _, s := range sites {
queue = append(queue, needs(root, s)...) queue = append(queue, needs(root, s)...)
@@ -119,6 +120,14 @@ func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error
err = b.run(root, sites) err = b.run(root, sites)
} }
if err != nil { 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() b.putBack()
} }
return err return err
@@ -169,6 +178,19 @@ func (b batch) run(root *folder, sites []categorySite) error {
return categoryPipeline(sites, root.children).run() 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() { func (b batch) putBack() {
for _, l := range b { for _, l := range b {
l.site.in.putUnloaded(l.site.name, l.was) l.site.in.putUnloaded(l.site.name, l.was)
+1 -1
View File
@@ -90,7 +90,7 @@ func loadInline(root *folder, n node, label string, typedByGo bool) error {
if err := refuseFolderRefs(nodes); err != nil { if err := refuseFolderRefs(nodes); err != nil {
return err 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 loadError{err}
} }
return pipeline{ return pipeline{
+6 -7
View File
@@ -12,13 +12,12 @@ import (
"github.com/larvit/fejkdata/internal/jsonvalue" "github.com/larvit/fejkdata/internal/jsonvalue"
) )
// Source is one tree to load: an fs.FS and the directory in it to start from. label // Source is one tree to load, from the root of an fs.FS. label prefixes file names in
// prefixes file names in errors; onDisk marks label as a directory that must exist. // errors; onDisk marks label as a directory that must exist.
type Source struct { type Source struct {
fsys fs.FS fsys fs.FS
label string label string
onDisk bool onDisk bool
base string
} }
// Dir is the tree in the directory path, its errors labelled with it. // 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} return Source{fsys: os.DirFS(path), label: path, onDisk: true}
} }
// FS is the tree in fsys below base, "" for its root. // FS is the tree in fsys.
func FS(fsys fs.FS, base string) Source { return Source{fsys: fsys, base: base} } 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. // and its parsed JSON.
type Category struct { type Category struct {
Folders []string Folders []string
@@ -50,7 +49,7 @@ func (s Source) labelled(p string) string {
// dirPath is the folder that dir names, as fsys spells it. // dirPath is the folder that dir names, as fsys spells it.
func (s Source) dirPath(dir []string) string { 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 p
} }
return "." return "."
+4 -4
View File
@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"path"
"sort" "sort"
"strings" "strings"
@@ -19,7 +18,8 @@ const ManifestFile = ".fejkdata.json"
type Manifest struct { type Manifest struct {
// Index is every category of the source by dot path; nil where the manifest has none. // Index is every category of the source by dot path; nil where the manifest has none.
Index map[string]IndexEntry `json:"index"` 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"` Reads []string `json:"reads"`
} }
@@ -35,7 +35,7 @@ func (s Source) Manifest() (Manifest, error) {
if err := s.check(); err != nil { if err := s.check(); err != nil {
return Manifest{}, err 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) { if errors.Is(err, fs.ErrNotExist) {
return Manifest{}, nil return Manifest{}, nil
} }
@@ -50,7 +50,7 @@ func (s Source) Manifest() (Manifest, error) {
} }
// ManifestPath is the manifest's path as errors name it. // 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 // 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 // reports every mistake: first each entry's own, in path order, then each parent naming
+3 -3
View File
@@ -19,7 +19,7 @@ import (
// the read's first-level key, put under the rendering template's key, is one of the name's // 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. // 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}, // "", "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 // 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} // 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 // 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. // 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: // 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 // {a.street} and {a.postal-code} in sv_SE.address read one pick of the name l
// that data/geo/SE/address.json binds. // that geo.SE.address binds.
func readName(s *drawstate.State, env renderEnv, a arm) readValue { func readName(s *drawstate.State, env renderEnv, a arm) readValue {
f := env.frameOf(a.named.scope) f := env.frameOf(a.named.scope)
if f == nil { if f == nil {
+18 -19
View File
@@ -3,18 +3,26 @@ package fejkdata
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
) )
// templateSite is a template to resolve, with its folder and category, both empty for an inline // templateSite is a template to resolve, with its folder and its category's path, both empty for an
// template, and eachNode's label for it. // inline template, and eachNode's label for it.
type templateSite struct { type templateSite struct {
t *template t *template
folder []string folder []string
label 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 // resolveCategoryTemplates resolves every template of the categories, once all data is merged, so a
// reference sees the override-resolved tree. // reference sees the override-resolved tree.
func resolveCategoryTemplates(sites []categorySite, root map[string]node) error { 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 { for _, s := range sites {
if err := eachNode(s.n, s.path, func(label string, n node) error { if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate { 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 return nil
}); err != nil { }); err != nil {
@@ -58,8 +66,12 @@ type resolvedNames struct {
func resolveTemplates(ts []templateSite, root map[string]node) error { func resolveTemplates(ts []templateSite, root map[string]node) error {
for _, s := range ts { for _, s := range ts {
refs, err := s.t.resolveRefs(s.folder, s.label, root) 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 { if err != nil {
return s.withReads(err) return err
} }
s.t.refs = refs s.t.refs = refs
} }
@@ -90,16 +102,3 @@ func resolveTemplates(ts []templateSite, root map[string]node) error {
} }
return nil 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, ", "))
}
+8 -3
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 195` `Next ID = 196`
| Goal | W | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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. ### 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. ### 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. ### 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. ### 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. 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. ### 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. 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.