Answer architecture round 7: reads and its note move to item 196, a mounted module's errors read sv_SE (address.json), internal/datafiles handles namespace folders in one form, and a folder outside the mount is a broken invariant

This commit is contained in:
2026-10-11 13:23:30 +02:00
committed by Lillem4n
parent 6a62840373
commit cc1c3bbc7d
12 changed files with 76 additions and 111 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ In [docs/decisions.md](docs/decisions.md):
- A cell may read a reference, and each row its own - A cell may read a reference, and each row its own
- A path is walked once without drawing before it is walked for real - 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 country's postal codes and streets are siblings under its locality
- A locale's `address` reads its country's `geo` tree, and the locale module's manifest reads its country's geo module - A locale's `address` reads its country's `geo` tree, whose module a program loads beside it
- 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
- A locale's `address` binds its country's record once and prints it whole - A locale's `address` binds its country's record once and prints it whole
- A postort's kommun comes from its name, its tätort or its codes, never from distance - A postort's kommun comes from its name, its tätort or its codes, never from distance
+1 -4
View File
@@ -130,10 +130,7 @@ replacement, and each removed path, column or flag.
`…/geous`. `data.Modules()`, from `github.com/larvit/fejkdata/data`, lists every one, `…/geous`. `data.Modules()`, from `github.com/larvit/fejkdata/data`, lists every one,
and the CLI carries them all. `New` loads the modules `WithDataFS(modules...)` names and and the CLI carries them all. `New` loads the modules `WithDataFS(modules...)` names and
the folders `WithDataPath` names, and fails, naming `WithDataFS`, where no such option the folders `WithDataPath` names, and fails, naming `WithDataFS`, where no such option
is given. A manifest's `namespace` mounts its source under a dot path, and its `reads` lists the is given. A manifest's `namespace` mounts its source under a dot path.
namespaces of the modules its source reads by default; a reference to a path nothing
provides fails to load, asking whether a module is missing and naming each namespace
read that nothing loaded provides, or saying no loaded manifest names a module to add.
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each - A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
category on the first call reaching it, as each shipped module does. Data failing to load, category on the first call reaching it, as each shipped module does. Data failing to load,
in `New` or on that first call, gives an error matching `ErrLoad`, and the CLI exits 1. in `New` or on that first call, gives an error matching `ErrLoad`, and the CLI exits 1.
+6 -14
View File
@@ -185,7 +185,7 @@ The index maps each category's dot path to an entry: `parent`, the table it link
which needs its own entry in the same folder; and `paths`, every path `List` advertises which needs its own entry in the same folder; and `paths`, every path `List` advertises
below the category, `""` for the category itself. Each shipped module's manifest, such as below the category, `""` for the category itself. Each shipped module's manifest, such as
[`data/svse/.fejkdata.json`](data/svse/.fejkdata.json), carries one, so in a copy you [`data/svse/.fejkdata.json`](data/svse/.fejkdata.json), carries one, so in a copy you
edit, delete its `index` and keep its `namespace` and `reads`, or keep the index current. edit, delete its `index` and keep its `namespace`, or keep the index current.
A table whose parent column differs from its entry's `parent` fails at first reach, and a A table whose parent column differs from its entry's `parent` fails at first reach, and a
table linking under another source's table needs a source without an index. `//go:embed` table linking under another source's table needs a source without an index. `//go:embed`
of a directory leaves the manifest out, and the source then loads whole in `New` and at of a directory leaves the manifest out, and the source then loads whole in `New` and at
@@ -193,16 +193,8 @@ the root, with no `namespace`; name the manifest in the pattern too.
A manifest's `namespace` is the dot path its source's root sits under: a folder holding A manifest's `namespace` is the dot path its source's root sits under: a folder holding
`person.json` with `"namespace": "sv_SE"` provides `sv_SE.person`, its subfolders adding `person.json` with `"namespace": "sv_SE"` provides `sv_SE.person`, its subfolders adding
segments below that. Each shipped module carries one, so `data/svse/person.json` is segments below that. It needs no index. Each shipped module carries one, so
`sv_SE.person`. `data/svse/person.json` is `sv_SE.person`.
A manifest's `reads` lists the namespace of every module the source's references read
unless another source provides the same paths, directly or through another module.
Nothing loads these modules: pass them yourself. `svse`'s manifest reads `geo.SE`. Data
whose reference names a path nothing provides fails to load; where a namespace the loaded
manifests read is one nothing loaded provides, the error asks whether a module is missing
and names it, and otherwise it says no loaded manifest names a module to add.
`namespace` and `reads` need no index.
Each locale carries `address`, `color`, `company`, `date`, `email`, `first-name`, Each locale carries `address`, `color`, `company`, `date`, `email`, `first-name`,
`ip`, `last-name`, `person`, `phone`, `price`, `sentence`, `sex`, `time`, `url`, `ip`, `last-name`, `person`, `phone`, `price`, `sentence`, `sex`, `time`, `url`,
@@ -366,7 +358,7 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el
| `WithDataFS(modules...)` | layer modules in order, each an `fs.FS`: a shipped package's `FS`, or your own `embed.FS`; `WithDataFS()` loads none | | `WithDataFS(modules...)` | layer modules in order, each an `fs.FS`: a shipped package's `FS`, or your own `embed.FS`; `WithDataFS()` loads none |
`data.Modules()` is every shipped module, and links all of them into your program. To link `data.Modules()` is every shipped module, and links all of them into your program. To link
only the ones you read, pass each package's `FS` with the modules its manifest reads: only the ones you read, pass each locale's `FS` with its country's places:
```go ```go
import ( import (
@@ -378,7 +370,7 @@ import (
f, err := fejkdata.New(fejkdata.WithDataFS(svse.FS, geose.FS)) f, err := fejkdata.New(fejkdata.WithDataFS(svse.FS, geose.FS))
``` ```
`enus` reads `geous`; `misc` and the geo packages read none. `New` fails where no `WithDataFS` or `enus` goes with `geous`; `misc` and the geo packages stand alone. `New` fails where no `WithDataFS` or
`WithDataPath` names the data to load. `WithDataPath` names the data to load.
`New` refuses data that breaks the grammar, could mean two things or cannot render a valid `New` refuses data that breaks the grammar, could mean two things or cannot render a valid
@@ -915,7 +907,7 @@ and regenerate seeded fixtures and expected values when you raise that version.
| Data format | a fence: a spelling a load rejects that it accepted, in a category or a manifest; a template option, since it reserves a field name | a builtin | | Data format | a fence: a spelling a load rejects that it accepted, in a category or a manifest; a template option, since it reserves a field name | a builtin |
| CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format | | CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format |
| Library | change or remove an exported name; raise the lowest supported Go | an exported name, a `With…` option | | Library | change or remove an exported name; raise the lowest supported Go | an exported name, a `With…` option |
| Data packages | rename or remove a package; move a category to another package; add a module to a package's `reads` | a package | | Data packages | rename or remove a package; move a category to another package; make a package read another it did not | a package |
A patch changes no row of this table: performance, docs, or a fix inside a promised A patch changes no row of this table: performance, docs, or a fix inside a promised
behaviour that adds or removes no value and changes no path, format or spelling. behaviour that adds or removes no value and changes no path, format or spelling.
+5 -14
View File
@@ -1,7 +1,6 @@
package fejkdata package fejkdata
import ( import (
"slices"
"strings" "strings"
"github.com/larvit/fejkdata/internal/datafiles" "github.com/larvit/fejkdata/internal/datafiles"
@@ -9,28 +8,20 @@ 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. It also returns, sorted, every namespace the sources' // the unloaded categories it needs.
// manifests read.
// 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 // 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, []string, error) { func loadSources(sources []datafiles.Source) (folder, error) {
root := folder{children: map[string]node{}} root := folder{children: map[string]node{}}
var reads []string
for _, src := range sources { for _, src := range sources {
m, err := src.Manifest() m, err := src.Manifest()
if err != nil { if err != nil {
return folder{}, nil, err return folder{}, err
} }
if err := addSource(&root, src, m); err != nil { if err := addSource(&root, src, m); err != nil {
return folder{}, nil, err return folder{}, err
}
for _, namespace := range m.Reads {
if !slices.Contains(reads, namespace) {
reads = append(reads, namespace)
}
} }
} }
slices.Sort(reads) return root, loadReached(&root, categorySites(&root), nil)
return root, reads, loadReached(&root, reads, categorySites(&root), nil)
} }
func addSource(root *folder, src datafiles.Source, m datafiles.Manifest) error { func addSource(root *folder, src datafiles.Source, m datafiles.Manifest) error {
+1 -2
View File
@@ -23,6 +23,5 @@
"en_US.version": {"paths":["","n","pre","suffix"]}, "en_US.version": {"paths":["","n","pre","suffix"]},
"en_US.word": {"paths":[""]} "en_US.word": {"paths":[""]}
}, },
"namespace": "en_US", "namespace": "en_US"
"reads": ["geo.US"]
} }
+1 -2
View File
@@ -23,6 +23,5 @@
"sv_SE.version": {"paths":["","n","pre","suffix"]}, "sv_SE.version": {"paths":["","n","pre","suffix"]},
"sv_SE.word": {"paths":[""]} "sv_SE.word": {"paths":[""]}
}, },
"namespace": "sv_SE", "namespace": "sv_SE"
"reads": ["geo.SE"]
} }
+25
View File
@@ -392,3 +392,28 @@ Inherited architect, decided by:
- Shape: `data/geo/SE/fs.go:2` `SE`: a package named SE reads as Sweden beside sv_SE, and "module" means a Go package here - Shape: `data/geo/SE/fs.go:2` `SE`: a package named SE reads as Sweden beside sv_SE, and "module" means a Go package here
- Shape: `reference.go:126` `withDefaultModules`: one concept spelled reads, defaultModules and "read by default", the last defined only in the README - Shape: `reference.go:126` `withDefaultModules`: one concept spelled reads, defaultModules and "read by default", the last defined only in the README
- Locality: `fejkdata.go:114` `New`: four call sites apply loadError, withDefaultModules and noDataNote in a fixed order, offset by the hidden shipped source being gone - Locality: `fejkdata.go:114` `New`: four call sites apply loadError, withDefaultModules and noDataNote in a fixed order, offset by the hidden shipped source being gone
## 2026-10-11T10:44:13Z, PR #203 at 36b5062, against bf1becb
Ruling: worse
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---|
| Mid A | worse | worse | worse | same | worse |
| Inherited architect | worse | worse | same | worse | worse |
Mid A, decided by:
- Locality, Shape, Overall: `index.go:110` `loadReached`: a reads parameter runs through four layers only to add a note to an error, and reads already means a template's reads
- Navigation: `internal/datafiles/datafiles.go:36` `Source.Mounted`: a shipped module's broken file is labelled geo/SE/address.json, a path that no longer exists
- Locality: `internal/datafiles/datafiles.go:77` `Source.Walk`: Walk adds the mount and Load removes it, two forms of a folder path in one type
- Locality: `fejkdata.go:68` `WithDataFS`: the shipped data no longer loads silently, a partial offset
- Shape: `reference.go:127` `withMissingModules`: a typo in manifest-less data still gets a note about modules
Inherited architect, decided by:
- Locality, Overall: `index.go:110` `loadReached`: reads threaded through six signatures for one error note
- Navigation: `internal/datafiles/datafiles.go:36` `Source.Mounted`: errors name sv_SE/address.json, a path that does not exist, where the base named the real data/sv_SE/address.json
- Navigation, Self-sufficiency: `data/svse/.fejkdata.json:26` `namespace`: a namespace no longer mirrors its folder, so the path alone no longer finds the file
- Self-sufficiency: `internal/datafiles/manifest.go:23` `Manifest.Reads`: its comment and withMissingModules' only make sense after the README
- Locality: `internal/datafiles/datafiles.go:77` `Source.Walk`: the mount is translated in opposite directions by Walk and Load
+7 -12
View File
@@ -143,7 +143,7 @@ A directory a machine happens to have would make `--seed 42` machine-dependent;
- 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. It is an exported `var FS embed.FS`, the shape `//go:embed` gives; a function returning it would only stop a program reassigning the FS variable of a package it imports. 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 only stop a program reassigning the FS variable of a package it imports. 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; `todo.md` item 196 carries this out. 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.
- The README asks for functions with no side effects of their own. - The README asks for functions with no side effects of their own.
@@ -159,11 +159,8 @@ custom-CLI builder. Serves goals 1 and 3.3. `sv_SE` ships as `data/svse`, `en_US
`data/enus`, `geo.SE` as `data/geose`, `geo.US` as `data/geous`, and `misc` as `data/misc`: `data/enus`, `geo.SE` as `data/geose`, `geo.US` as `data/geous`, and `misc` as `data/misc`:
Go linters flag a capital or an underscore in a package name, and `SE.FS` read as a type. Go linters flag a capital or an underscore in a package name, and `SE.FS` read as a type.
Each package holds its files at its root, and its manifest's `namespace` puts them under Each package holds its files at its root, and its manifest's `namespace` puts them under
the namespace, so no path repeats it. The bench also chose `reads` for what a module reads the namespace, so no path repeats it. Valid until a locale's lowered name clashes with
by default and the wording of the errors naming it. A `reads` entry is a namespace, so the another's.
error names only what nothing loaded provides, and with every module loaded a typo names
none. Valid until a locale's lowered name
clashes with another's.
## A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads ## A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads
@@ -590,14 +587,12 @@ 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 and an address agrees at that level. A street's own code is the exact pairing to add
when a source carries it. when a source carries it.
## A locale's `address` reads its country's `geo` tree, and the locale module's manifest reads its country's geo module ## A locale's `address` reads its country's `geo` tree, whose module a program loads beside 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. `svse.FS` loads alone, and `sv_SE.address` then fails at first reach, Serves goal 9.2. `svse.FS` loads alone, and `sv_SE.address` then fails at first reach with
naming `geo.SE`, which `svse`'s manifest reads and nothing loaded provides. `no entry "geo"`, so anything providing `geo.SE`'s paths can stand in for `geose`. Until
`New` does not check that each module a manifest reads was given. A check by module name `todo.md` item 196 names the missing module in that error, it works against goal 6.2.
would refuse a stand-in providing the same paths (goal 9.2). A check by path needs every
reference, and only loading each indexed category finds them, which goal 13 rules out.
## 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
+2 -4
View File
@@ -31,8 +31,6 @@ type Generator struct {
drawState *drawstate.State drawState *drawstate.State
root folder // the categories as the node a path walks from, owned here so a walk allocates none root folder // the categories as the node a path walks from, owned here so a walk allocates none
structs map[reflect.Type]structResult structs map[reflect.Type]structResult
// manifestReads is every namespace the loaded sources' manifests read.
manifestReads []string
} }
type config struct { type config struct {
@@ -109,7 +107,7 @@ func New(opts ...Option) (*Generator, error) {
if c.nilModules != nil { if c.nilModules != nil {
return nil, c.nilModules return nil, c.nilModules
} }
root, reads, err := loadSources(c.sources) root, err := loadSources(c.sources)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", loadError{err}) return nil, fmt.Errorf("fejkdata: %w", loadError{err})
} }
@@ -117,7 +115,7 @@ func New(opts ...Option) (*Generator, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
return &Generator{drawState: drawstate.New(seed), root: root, manifestReads: reads}, nil return &Generator{drawState: drawstate.New(seed), root: root}, nil
} }
// List returns the sorted dotted paths Fake renders: each category and every field, // List returns the sorted dotted paths Fake renders: each category and every field,
+20 -23
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
"github.com/larvit/fejkdata/internal/jsonvalue" "github.com/larvit/fejkdata/internal/jsonvalue"
) )
@@ -31,15 +32,10 @@ func Dir(path string) Source {
// FS is the tree in fsys. // FS is the tree in fsys.
func FS(fsys fs.FS) Source { return Source{fsys: fsys} } func FS(fsys fs.FS) Source { return Source{fsys: fsys} }
// Mounted is s with its root under the dot path namespace, "" for the namespace's root. A // Mounted is s with its root under the dot path namespace, "" for the namespace's root.
// source with no label of its own names its files in errors by the namespace's folders.
func (s Source) Mounted(namespace string) Source { func (s Source) Mounted(namespace string) Source {
if namespace == "" { if namespace != "" {
return s s.mount = strings.Split(namespace, ".")
}
s.mount = strings.Split(namespace, ".")
if s.label == "" {
s.label = path.Join(s.mount...)
} }
return s return s
} }
@@ -56,16 +52,24 @@ type Category struct {
// ReadRows reads a rows file beside the category. // ReadRows reads a rows file beside the category.
func (c Category) ReadRows(name string) (string, error) { return c.rows.read(name) } func (c Category) ReadRows(name string) (string, error) { return c.rows.read(name) }
// labelled is the file p of fsys as errors name it: under the source's label, or, for a
// mounted source with none, beside its namespace, "sv_SE (address.json)".
func (s Source) labelled(p string) string { func (s Source) labelled(p string) string {
if s.label == "" { switch {
return p case s.label != "":
return path.Join(s.label, p)
case s.mount != nil:
return fmt.Sprintf("%s (%s)", strings.Join(s.mount, "."), p)
} }
return path.Join(s.label, p) return p
} }
// dirPath is the folder that dir names, as fsys spells it. // dirPath is the folder that the namespace folders dir name, as fsys spells it.
func (s Source) dirPath(dir []string) string { func (s Source) dirPath(dir []string) string {
if p := path.Join(dir...); p != "" { if len(dir) < len(s.mount) || !slices.Equal(dir[:len(s.mount)], s.mount) {
panic(invariant.Broken("folder %s lies outside the mount %s", strings.Join(dir, "."), strings.Join(s.mount, ".")))
}
if p := path.Join(dir[len(s.mount):]...); p != "" {
return p return p
} }
return "." return "."
@@ -78,10 +82,7 @@ func (s Source) Walk(compile func(Category) error) error {
if err := s.check(); err != nil { if err := s.check(); err != nil {
return err return err
} }
_, err := s.walkDir(nil, func(c Category) error { _, err := s.walkDir(slices.Clip(s.mount), compile)
c.Folders = append(s.mount[:len(s.mount):len(s.mount)], c.Folders...)
return compile(c)
})
return err return err
} }
@@ -142,13 +143,9 @@ func (s Source) walkFolder(dir []string, compile func(Category) error) (bool, er
return true, nil return true, nil
} }
// Load hands compile the category file name.json in the folder dir of the namespace, which // Load hands compile the category file name.json in the namespace folder dir.
// lies under the source's mount.
func (s Source) Load(dir []string, name string, compile func(Category) error) error { func (s Source) Load(dir []string, name string, compile func(Category) error) error {
if len(dir) < len(s.mount) || !slices.Equal(dir[:len(s.mount)], s.mount) { full := s.dirPath(dir)
return fmt.Errorf("%s.%s lies outside the namespace %s", strings.Join(dir, "."), name, strings.Join(s.mount, "."))
}
full := s.dirPath(dir[len(s.mount):])
entries, err := fs.ReadDir(s.fsys, full) entries, err := fs.ReadDir(s.fsys, full)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", s.labelled(full), err) return fmt.Errorf("%s: %w", s.labelled(full), err)
+1 -34
View File
@@ -20,9 +20,6 @@ type Manifest struct {
Index map[string]IndexEntry Index map[string]IndexEntry
// Namespace is the dot path the source's root sits under, "" for the namespace's root. // Namespace is the dot path the source's root sits under, "" for the namespace's root.
Namespace string Namespace string
// Reads is the namespace of every module the source reads unless another source
// provides the same paths, directly or through another; nothing loads them.
Reads []string
} }
// IndexEntry is what an index says of a category: its parent table, "" for none, and the // IndexEntry is what an index says of a category: its parent table, "" for none, and the
@@ -78,12 +75,8 @@ func decodeManifest(b []byte) (Manifest, error) {
var err error var err error
m.Namespace, err = decodeNamespace(top[k]) m.Namespace, err = decodeNamespace(top[k])
errs = append(errs, err) errs = append(errs, err)
case "reads":
var err error
m.Reads, err = decodeReads(top[k])
errs = append(errs, err)
default: default:
errs = append(errs, fmt.Errorf("unknown key %q; a manifest holds index, namespace and reads", k)) errs = append(errs, fmt.Errorf("unknown key %q; a manifest holds index and namespace", k))
} }
} }
for _, p := range sortedKeys(m.Index) { for _, p := range sortedKeys(m.Index) {
@@ -108,32 +101,6 @@ func decodeNamespace(v any) (string, error) {
return namespace, nil return namespace, nil
} }
// decodeReads decodes the namespaces of the modules a source reads.
func decodeReads(v any) ([]string, error) {
items, isList := v.([]any)
if !isList {
return nil, fmt.Errorf("reads must be a list, not %s", jsonvalue.Kind(v))
}
out := make([]string, 0, len(items))
seen := map[string]bool{}
var errs []error
for i, item := range items {
namespace, isString := item.(string)
switch {
case !isString:
errs = append(errs, fmt.Errorf("reads item %d must be a string, not %s", i+1, jsonvalue.Kind(item)))
case checkCategoryPath(namespace) != nil:
errs = append(errs, fmt.Errorf("reads item %d: %w", i+1, checkCategoryPath(namespace)))
case seen[namespace]:
errs = append(errs, fmt.Errorf("reads item %d repeats %q", i+1, namespace))
default:
seen[namespace] = true
out = append(out, namespace)
}
}
return out, errors.Join(errs...)
}
func decodeIndex(v any) (map[string]IndexEntry, []error) { func decodeIndex(v any) (map[string]IndexEntry, []error) {
entries, isObject := v.(map[string]any) entries, isObject := v.(map[string]any)
if !isObject { if !isObject {
+6 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 196` `Next ID = 197`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -76,6 +76,7 @@
| 129 | 0.1.0 | decision | **Take a template wherever the library renders, so `fake:"{/sv_SE.person.first}"` and `Fake("{/sv_SE.person}")` render, and text with no `{…}` prints as written, as the CLI already does.** | 5 | 8 | 8 | 10 | 5.9, 7 | 25.0 | | 129 | 0.1.0 | decision | **Take a template wherever the library renders, so `fake:"{/sv_SE.person.first}"` and `Fake("{/sv_SE.person}")` render, and text with no `{…}` prints as written, as the CLI already does.** | 5 | 8 | 8 | 10 | 5.9, 7 | 25.0 |
| 163 | 0.1.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 | | 163 | 0.1.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 |
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 | | 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 196 | 0.1.0 | decision | **Name, in the error for a reference nothing loaded provides, the module the reader's manifest reads by default that would provide it, or say none does.** | 3 | 4 | 6 | 8 | 6.2 | 20.0 |
| 164 | 0.1.0 | | **Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.** | 2 | 2 | 5 | 7 | 4.1, 5.3 | 19.4 | | 164 | 0.1.0 | | **Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.** | 2 | 2 | 5 | 7 | 4.1, 5.3 | 19.4 |
| 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 | | 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 |
| 181 | 0.1.0 | decision | **Fail a load where two sources define one category, unless the library's option or the CLI's flag says the later replaces the earlier, and then make every read reach the replacement.** | 5 | 4 | 6 | 9 | 9.3 | 16.8 | | 181 | 0.1.0 | decision | **Fail a load where two sources define one category, unless the library's option or the CLI's flag says the later replaces the earlier, and then make every read reach the replacement.** | 5 | 4 | 6 | 9 | 9.3 | 16.8 |
@@ -279,6 +280,10 @@ The README's Audience names a bulk fixture author writing millions of rows, and
Nothing has shipped, so "no longer paths", "where it used to fail" and "where it used to be an empty string" describe versions no reader can have installed. Nothing has shipped, so "no longer paths", "where it used to fail" and "where it used to be an empty string" describe versions no reader can have installed.
### 196. Name, in the error for a reference nothing loaded provides, the module the reader's manifest reads by default that would provide it, or say none does.
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", and ends the locale-address decision's conflict with goal 6.2. Carved out of item 182 on 2026-10-11, after six rounds of the comprehension panel ruled each attempt worse. Tried and ruled worse: a module list carried per category from each source's manifest (two routes, `walked` and `indexed`); the union of every manifest's list on the `Generator`, noted at four entry points; and the same noted inside `loadReached`, its list threaded through `loadInline` and `structCompile`. Each named every module read, even loaded ones, until the list held namespaces and the note named only those nothing in the tree provides, which the product-owner and goals reviewers accepted. Bench the manifest key and the note's wording with the data author, since entries changed from import paths to namespaces after the 2026-10-11 bench.
### 164. Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw. ### 164. Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.
The maintainer ruled so on 2026-10-09: a name is a lexical closure, as the decision "A name lives in the category binding it, or in the repeat or choice item binding it, and is drawn on its first read" now states. Today `table.go` refuses `{/sv_SE.person as p}{p.first} {p.last}` in a cell with `a table binds no name`. A cell reads no name its table's format binds. Revise that decision's title and the README's Names list with it. The maintainer ruled so on 2026-10-09: a name is a lexical closure, as the decision "A name lives in the category binding it, or in the repeat or choice item binding it, and is drawn on its first read" now states. Today `table.go` refuses `{/sv_SE.person as p}{p.first} {p.last}` in a cell with `a table binds no name`. A cell reads no name its table's format binds. Revise that decision's title and the README's Names list with it.