Answer product-owner round 1: the parent error and the README name the way to link under another source's table, a missing indexed file names the manifest, the README warns a copied data/ carries its index, and the docs say what a load failure and a replacement do

This commit is contained in:
2026-10-10 11:14:19 +02:00
committed by Lillem4n
parent 15477e989f
commit 13e3de39fe
8 changed files with 64 additions and 46 deletions
+4 -3
View File
@@ -127,6 +127,7 @@ replacement, and each removed path, column or flag.
that gives it one.
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
category on the first call reaching it, as the shipped set does. Data failing to load,
in `New` or there, gives an error matching `ErrLoad`, and the CLI exits 1. A source without
one loads in `New`, with the shipped categories it reads; the rest of the shipped set,
a reader of a category it replaces too, loads on first reach.
in `New` or on that first call, gives an error matching `ErrLoad`, and the CLI exits 1.
A source without an index loads in `New`, with the shipped categories it reads. Every
other shipped category loads on the first call reaching it, so one your replacement
breaks fails there.
+30 -24
View File
@@ -63,8 +63,8 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules
`--name value` and `--name=value` both work, a short flag's value attaches or
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
dir, data that fails to load, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (data
that fails to load, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument
other than a flag, an unreadable stdin, nothing piped in, or a
template that does not compile. From a checkout:
`go run ./cmd/fejkdata …`.
@@ -173,15 +173,20 @@ entry is never a category or a folder, so a data directory can also be a checkou
a category may name a hidden rows file beside it.
A source whose root holds `.fejkdata.json` with an `index` loads each category on the
first call reaching it, and its author proves each loads. Any other source loads in
`New`, with the indexed categories it reads. A shipped category reading one that another
source replaced loads on its own first reach, and fails there if the replacement breaks
it. The index names every category of its source, so a category file it leaves out never
loads. The shipped set's [`data/.fejkdata.json`](data/.fejkdata.json) shows the shape:
each category's dot path, its `parent` table, an entry of the same index, and every path
`List` advertises below it, `""` for the category itself. A table whose parent column
differs from its entry's `parent` fails at first reach.
`//go:embed` of a directory leaves the manifest out; name it in the pattern too.
first call reaching it, so `New` does not check them: load each category once in a test,
as CI does for the shipped set. Any other source loads in `New`, with the indexed
categories it reads. An indexed category reading one that another source replaced loads
on its own first reach, and fails there if the replacement breaks it. The index names
every category of its source, so a category file it leaves out never loads.
The index maps each category's dot path to an entry: `parent`, the table it links to,
which needs its own entry in the same folder; and `paths`, every path `List` advertises
below the category, `""` for the category itself. The shipped set's
[`data/.fejkdata.json`](data/.fejkdata.json) is one, so delete it from a copy of `data/`
you edit, or keep its index current. 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` of a directory leaves the manifest out, and
the source then loads whole in `New`; name it in the pattern too.
Each locale carries `address`, `color`, `company`, `date`, `email`, `first-name`,
`ip`, `last-name`, `person`, `phone`, `price`, `sentence`, `sex`, `time`, `url`,
@@ -342,13 +347,14 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el
`New` refuses data that breaks the grammar, could mean two things or cannot render a valid
value, and `NewTemplate` refuses such a template. A category of an indexed source is
checked on the first call reaching it instead. Data that fails to load, in `New` or
there, gives an error matching `ErrLoad`, the same every call. On a loaded generator:
checked on the first call reaching it instead. Data that fails to load, in `New` or on
that first call, gives an error matching `ErrLoad`. On a loaded generator:
- `Fake` fails only for a path that names nothing, could name two things, or reads one
draw of a level carrying a `repeat`, with the same error every call, or with `ErrLoad`.
- `FakeStruct` fails only for a non-struct argument or a type its tags do not describe,
with the same error every call, or with `ErrLoad`.
- `Fake` fails only for a path that names nothing, could name two things or reads one
draw of a level carrying a `repeat`, or for data that fails to load (`ErrLoad`), with
the same error every call.
- `FakeStruct` fails only for a non-struct argument, a type its tags do not describe, or
data that fails to load (`ErrLoad`), with the same error every call.
- `Template.Fake` cannot fail.
A path, wherever a call takes one, may start with one `/`, as a reference does.
@@ -848,12 +854,12 @@ The target is about a microsecond per value (goal 13).
A source with an [index](#data), as the shipped set has, loads nothing in `New`: a
category loads on the first `Fake`, `FakeRecord`, `FakeStruct` or template that reads
it, with every category it reads and its whole table family. Each file is parsed, validated and weight-indexed once, bar a
table's name and parent columns, which are mapped on the first `Fake` that selects by
name or descends through it. A `Fake` call then costs about what its output
costs: an unweighted pick is O(1) whatever the list's length, a weighted one
O(log n), and long formats, deep nesting and many tokens add cost in proportion to
the output.
it, with every category it reads and its whole table family. Each file is parsed,
validated and weight-indexed once, bar a table's name and parent columns, which are
mapped on the first `Fake` that selects by name or descends through it. A `Fake` call then
costs about what its output costs: an unweighted pick is O(1) whatever the list's length,
a weighted one O(log n), and long formats, deep nesting and many tokens add cost in
proportion to the output.
## Versioning
@@ -870,7 +876,7 @@ and regenerate seeded fixtures and expected values when you raise that version.
|---------|-------|-------|
| Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another; change a table's key, name, weight or parent column, or remove a row | a path outside a record's columns, a locale, a value in a list, a row |
| Records (a table is one) | remove, rename, retype or add a column; let a column be null | a record, as a new category |
| Data format | a fence: a spelling `New` 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 |
| Library | change or remove an exported name; raise the lowest supported Go | an exported name, a `With…` option |
+6 -7
View File
@@ -156,8 +156,7 @@ Valid while the shipped data stays under 100 MiB, which a gate test holds (`todo
## A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads
2026-10-03, a restructure the maintainer approved; widened to any source on 2026-10-09,
and narrowed by the maintainer on 2026-10-10.
2026-10-03, a restructure the maintainer approved; widened to any source on 2026-10-09.
Goal 13: `New` pays nothing for a category a run never reaches. A manifest,
`.fejkdata.json` at a source's root, may carry an index: each category's parent table and
@@ -171,11 +170,11 @@ at first reach.
Goal 6: a source without an index loads whole in `New`, with every indexed category it
reads, so each mistake in it is `New`'s error. Goal 6.3: an indexed source from anyone
else may fail at first reach; the call then returns the load's error, matching
`ErrLoad`, the same every call. So does an indexed category reading one that another
source replaced, where the replacement breaks it. The maintainer chose that on
2026-10-10 over loading in `New` every indexed category depending on a replaced one:
readers of three rounds ruled that rule harder to follow (goal 3), and it needed every
entry to list what its templates read.
`ErrLoad`, the same every call. An indexed category reading one that another source
replaced fails the same way, at its first reach, when the replacement breaks it. The
maintainer chose that on 2026-10-10 over loading in `New` every indexed category
depending on a replaced one: three rounds of the comprehension panel ruled that rule
harder to follow (goal 3), and it needed every entry to list what its templates read.
A first reach checks only a table's `parent` against its entry, so a table loads with its
parent and a failed load puts back whole. Proving the rest of an index is its author's,
+2 -2
View File
@@ -84,8 +84,8 @@ func (loadError) Is(target error) bool { return target == ErrLoad }
// namespace segment. It errors on a missing directory, invalid JSON or invalid data; a
// generator with no data at all renders templates that read none. A source whose
// .fejkdata.json carries an index, as the shipped set's does, loads each category on the
// first call reaching it. Data failing to load, here or there, gives an error matching
// [ErrLoad].
// first call reaching it. Data failing to load, in New or on that first call, gives an
// error matching [ErrLoad].
func New(opts ...Option) (*Generator, error) {
c := config{shipped: true}
for _, opt := range opts {
+18 -6
View File
@@ -1,7 +1,9 @@
package fejkdata
import (
"errors"
"fmt"
"io/fs"
"slices"
"strings"
@@ -15,8 +17,8 @@ type indexed struct {
src datafiles.Source
}
// placeIndex sets every category index names unloaded in root, in place of what root holds
// at its path.
// placeIndex sets every category the index names as unloaded in root, in place of what
// root holds at its path.
func placeIndex(root *folder, src datafiles.Source, index map[string]datafiles.IndexEntry) {
for _, p := range sortedNames(index) {
segs := strings.Split(p, ".")
@@ -128,9 +130,9 @@ type loaded struct {
was *indexed
}
// load loads each unloaded category in queue and what it needs. A table whose parent
// column disagrees with its entry stops it, so a table loads with its parent and no table
// loaded before links to one putBack removes.
// load loads each unloaded category in queue and what it needs. It stops at a table whose
// parent column disagrees with its entry. Otherwise a table could load without its parent,
// or an earlier table could link to one that putBack then removes.
func (b *batch) load(root *folder, queue []categoryAt) error {
for ; len(queue) > 0; queue = queue[1:] {
c := queue[0]
@@ -139,12 +141,15 @@ func (b *batch) load(root *folder, queue []categoryAt) error {
continue
}
if err := x.src.Load(c.dir, c.name, compileInto(func([]string) *folder { return c.in })); err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = fmt.Errorf("%s indexes %s: %w", x.src.ManifestPath(), categoryPath(c.dir, c.name), err)
}
return err
}
s := siteIn(c.dir, c.in, c.name)
*b = append(*b, loaded{site: s, was: x})
if parent := parentOf(s.n); parent != x.entry.Parent {
return fmt.Errorf("%s: the index entry for %s names parent %q, and its table's parent column is %q", x.src.ManifestPath(), s.path, x.entry.Parent, parent)
return fmt.Errorf("%s: the index entry for %s %s, and its table %s", x.src.ManifestPath(), s.path, namesParent(x.entry.Parent), namesParent(parent))
}
queue = append(queue, needs(root, s)...)
}
@@ -168,6 +173,13 @@ func (b batch) putBack() {
}
}
func namesParent(parent string) string {
if parent == "" {
return "names no parent"
}
return fmt.Sprintf("names parent %q", parent)
}
func parentOf(n node) string {
if t, isTable := n.(*table); isTable {
return t.rows.Options().Parent
+1 -1
View File
@@ -176,7 +176,7 @@ func checkParent(index map[string]IndexEntry, p string) error {
sibling = p[:dot+1] + parent
}
if _, ok := index[sibling]; !ok {
return fmt.Errorf("parent %q names no entry beside it in this index", parent)
return fmt.Errorf("parent %q names no entry in this entry's folder; a table in an indexed source links only to a parent in the same index, so to link under another source's table, drop this index and the source loads in New", parent)
}
return nil
}
+1 -1
View File
@@ -23,7 +23,7 @@ type node interface{ isNode() }
// A name stands in children or in unloaded, never both; put and putUnloaded keep it so.
type folder struct {
children map[string]node
unloaded map[string]*indexed // the categories here an index names and no call has reached yet
unloaded map[string]*indexed
}
func (*folder) isNode() {}
+2 -2
View File
@@ -192,7 +192,7 @@ Carries out the decision "The shipped data is Go packages a library imports by c
### 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. An entry lists every path below its category, its linked tables' too, so `geo.*.region` repeats its whole family; an entry holding only its own paths, `List` composing a family from the entries naming it as `parent`, would shrink the manifest that decoding makes most of `New`'s cost (about 0.55 ms and 2449 allocations on 2026-10-10). 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). 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.
@@ -425,7 +425,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 a test of fejkdata's own writes and checks an index (`TestShippedManifestIsCurrent` over `indexEntry`). A category file the index leaves out never loads, silently. The principles audit of item 180's chunk found it.
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.
### 53. Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.