Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -11,8 +11,8 @@
|
|||||||
wait for one.
|
wait for one.
|
||||||
- README goal 3 is gated at 7.0 on the `comprehension-panel` skill, and below it
|
- README goal 3 is gated at 7.0 on the `comprehension-panel` skill, and below it
|
||||||
nothing else merges — no feature, no category, no data — bar a security fix, a
|
nothing else merges — no feature, no category, no data — bar a security fix, a
|
||||||
dependency bump, a repair of behaviour the README documents, the infrastructure
|
dependency bump, a repair of behaviour the README documents, and the infrastructure
|
||||||
the round itself runs on, and the CLI reading its template from stdin. `todo.md` carries the round: a PR per item it plans, the nine-seat
|
the round itself runs on. `todo.md` carries the round: a PR per item it plans, the nine-seat
|
||||||
findings at depth 1 or a restructure the maintainer approved, then the nine-seat run
|
findings at depth 1 or a restructure the maintainer approved, then the nine-seat run
|
||||||
again, until the score passes. At or above 7.0
|
again, until the score passes. At or above 7.0
|
||||||
every pull request scores with the four-seat run and answers it in one run. No merge
|
every pull request scores with the four-seat run and answers it in one run. No merge
|
||||||
@@ -41,7 +41,7 @@ In [docs/decisions.md](docs/decisions.md):
|
|||||||
- An argument is a template by its shape, not by a flag
|
- An argument is a template by its shape, not by a flag
|
||||||
- The CLI renders the template on its stdin, less one newline ending it
|
- The CLI renders the template on its stdin, less one newline ending it
|
||||||
- An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
- An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
||||||
- The library rejects a padded JSON template, and the CLI trims the whitespace around one on stdin
|
- A padded JSON argument is rejected, not trimmed
|
||||||
- `FakeTemplate` and `NewTemplate` both stay
|
- `FakeTemplate` and `NewTemplate` both stay
|
||||||
- The shipped data is embedded, not discovered
|
- The shipped data is embedded, not discovered
|
||||||
- With only the shipped set, a category loads on the first call reaching it; beside a `--data-path`, every category loads in `New`
|
- With only the shipped set, a category loads on the first call reaching it; beside a `--data-path`, every category loads in `New`
|
||||||
|
|||||||
+3
-4
@@ -28,10 +28,9 @@ replacement, and each removed path, column or flag.
|
|||||||
under [Names](README.md#names).
|
under [Names](README.md#names).
|
||||||
- First release: the CLI, the library and the shipped data set.
|
- First release: the CLI, the library and the shipped data set.
|
||||||
- The CLI renders the template piped to its stdin, `echo -n '{/sv_SE.person} hihi' |
|
- The CLI renders the template piped to its stdin, `echo -n '{/sv_SE.person} hihi' |
|
||||||
fejkdata`, and takes no path or template as an argument. One newline ending stdin is
|
fejkdata`, and takes no path or template as an argument. One newline ending stdin,
|
||||||
dropped, and so is the whitespace around a JSON template. A template of one reference
|
`\n` or `\r\n`, is dropped. A template of one reference alone, `{/users}`, is the
|
||||||
alone, `{/users}`, is the record `users` under `--format`. An argument, or nothing
|
record `users` under `--format`. An argument, or nothing piped in, exits 2.
|
||||||
piped in, exits 2.
|
|
||||||
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
- Table categories: a category JSON naming a `rows` TSV beside it, with the options
|
||||||
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
|
`key`, `name`, `weight` and `parent`; a path selects a row by key or name,
|
||||||
`misc.territory[SE]`, and descends to a linked table by name, one path drawing each
|
`misc.territory[SE]`, and descends to a linked table by name, one path drawing each
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ inside one: each dot segment descends one level — folders, then the category (
|
|||||||
file), then fields — and `[SE]` after a [table](#table) selects its row. `--list` prints
|
file), then fields — and `[SE]` after a [table](#table) selects its row. `--list` prints
|
||||||
these paths; a template reads one as `{/path}`.
|
these paths; a template reads one as `{/path}`.
|
||||||
|
|
||||||
One newline ending stdin is dropped, so `echo` and `echo -n`, a file and a heredoc
|
One newline ending stdin, `\n` or `\r\n`, is dropped, so `echo` and `echo -n`, a file
|
||||||
render alike; end stdin with two newlines to print one. Whitespace around a JSON
|
and a heredoc render alike; end stdin with two newlines to print one. Some shells' `echo`
|
||||||
template is dropped too. Which spellings a template rejects, and what each names
|
reads a backslash as an escape, so pipe a template holding one with `printf '%s'` or from
|
||||||
instead, is under
|
a file. One reference alone, `{/users}`, is the record `users` under `--format`; the
|
||||||
|
rest of stdin's rules are under
|
||||||
[Decisions](docs/decisions.md#the-cli-renders-the-template-on-its-stdin-less-one-newline-ending-it).
|
[Decisions](docs/decisions.md#the-cli-renders-the-template-on-its-stdin-less-one-newline-ending-it).
|
||||||
|
|
||||||
| Flag | |
|
| Flag | |
|
||||||
@@ -56,7 +57,7 @@ instead, is under
|
|||||||
`--name value` and `--name=value` both work, a short flag's value attaches or
|
`--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
|
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
|
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
||||||
dir, unknown path), `2` misuse — a bad flag, an argument, nothing on stdin, or a
|
dir, a lone reference to nothing), `2` misuse — a bad flag, an argument, nothing on stdin, or a
|
||||||
template that does not compile. From a checkout:
|
template that does not compile. From a checkout:
|
||||||
`go run ./cmd/fejkdata …`.
|
`go run ./cmd/fejkdata …`.
|
||||||
|
|
||||||
@@ -992,7 +993,7 @@ internal/builtinfunc/ the {name()} functions: their checks and draws, checksums,
|
|||||||
internal/datafiles/ the walk of a data tree: its folders, its category files and the rows files beside them
|
internal/datafiles/ the walk of a data tree: its folders, its category files and the rows files beside them
|
||||||
internal/datatype/ the datatype a record column holds; the root's DataType is an alias of it
|
internal/datatype/ the datatype a record column holds; the root's DataType is an alias of it
|
||||||
internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through
|
internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through
|
||||||
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, identifiers, calc syntax, and whether an argument is a template or a path
|
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, identifiers, calc syntax, and whether a struct tag is a template or a path
|
||||||
internal/invariant/ the one phrase every package panics with when an invariant breaks
|
internal/invariant/ the one phrase every package panics with when an invariant breaks
|
||||||
internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands
|
internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands
|
||||||
internal/rows/ a table's rows: the TSV, the options proved over it, the links between tables, row selection and draws, and the pin set one path or one named pick fixes
|
internal/rows/ a table's rows: the TSV, the options proved over it, the links between tables, row selection and draws, and the pin set one path or one named pick fixes
|
||||||
|
|||||||
+30
-41
@@ -9,7 +9,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -36,8 +35,8 @@ A reference names a category or a dotted path into one ({/sv_SE.person.last}), a
|
|||||||
table's row by key or name ({/misc.territory[SE]}), and .. steps up from a row to
|
table's row by key or name ({/misc.territory[SE]}), and .. steps up from a row to
|
||||||
the row it links to ({/geo.SE.locality..municipality.name}).
|
the row it links to ({/geo.SE.locality..municipality.name}).
|
||||||
|
|
||||||
One newline ending stdin is dropped, so echo and echo -n render alike; end it with
|
One newline ending stdin, \n or \r\n, is dropped, so echo and echo -n render
|
||||||
two to print one. Whitespace around a JSON template is dropped too.
|
alike; end stdin with two to print one.
|
||||||
|
|
||||||
With --format json, ndjson, csv or sql the template must name a record — a
|
With --format json, ndjson, csv or sql the template must name a record — a
|
||||||
template whose fields are its columns, or one reference alone, {/sv_SE.person} —
|
template whose fields are its columns, or one reference alone, {/sv_SE.person} —
|
||||||
@@ -60,13 +59,13 @@ A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3)
|
|||||||
`
|
`
|
||||||
|
|
||||||
type invocation struct {
|
type invocation struct {
|
||||||
|
args []string
|
||||||
dirs []string
|
dirs []string
|
||||||
format string
|
format string
|
||||||
formatSet bool
|
formatSet bool
|
||||||
help bool
|
help bool
|
||||||
list bool
|
list bool
|
||||||
noShipped bool
|
noShipped bool
|
||||||
args []string
|
|
||||||
repeat int
|
repeat int
|
||||||
repeatSet bool
|
repeatSet bool
|
||||||
seed uint64
|
seed uint64
|
||||||
@@ -267,7 +266,6 @@ func (in invocation) checkFlags() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pipeHint is how a template reaches fejkdata, for the errors of a run that has none.
|
|
||||||
const pipeHint = "echo -n '{/sv_SE.person}' | fejkdata"
|
const pipeHint = "echo -n '{/sv_SE.person}' | fejkdata"
|
||||||
|
|
||||||
// check rejects a flag combination or an argument that cannot run, before any data is
|
// check rejects a flag combination or an argument that cannot run, before any data is
|
||||||
@@ -333,7 +331,7 @@ func textDraw(f *fejkdata.Generator, src input) (func() (string, error), error)
|
|||||||
}
|
}
|
||||||
t, err := f.NewTemplate(src.template)
|
t, err := f.NewTemplate(src.template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, templateError{err}
|
return nil, inputError{err}
|
||||||
}
|
}
|
||||||
return func() (string, error) { return t.Fake(), nil }, nil
|
return func() (string, error) { return t.Fake(), nil }, nil
|
||||||
}
|
}
|
||||||
@@ -345,7 +343,7 @@ func (in invocation) recordStream(f *fejkdata.Generator, src input) (func() (*fe
|
|||||||
if src.path == "" {
|
if src.path == "" {
|
||||||
t, err := f.NewRecordTemplate(src.template)
|
t, err := f.NewRecordTemplate(src.template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", templateError{err}
|
return nil, "", inputError{err}
|
||||||
}
|
}
|
||||||
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
||||||
}
|
}
|
||||||
@@ -393,58 +391,51 @@ func (in invocation) writeRecords(f *fejkdata.Generator, src input, w io.Writer)
|
|||||||
|
|
||||||
// defaultTable names the INSERT target when --table is absent: the path's last
|
// defaultTable names the INSERT target when --table is absent: the path's last
|
||||||
// segment, or "records" for a template that reads no path alone.
|
// segment, or "records" for a template that reads no path alone.
|
||||||
func defaultTable(arg string) string {
|
func defaultTable(path string) string {
|
||||||
if arg == "" {
|
if path == "" {
|
||||||
return "records"
|
return "records"
|
||||||
}
|
}
|
||||||
var names strings.Builder // the path with its [selectors] cut out
|
var names strings.Builder // the path with its [selectors] cut out
|
||||||
for depth, i := 0, 0; i < len(arg); i++ {
|
for depth, i := 0, 0; i < len(path); i++ {
|
||||||
switch {
|
switch {
|
||||||
case arg[i] == '[':
|
case path[i] == '[':
|
||||||
depth++
|
depth++
|
||||||
case arg[i] == ']':
|
case path[i] == ']':
|
||||||
depth--
|
depth--
|
||||||
case depth == 0:
|
case depth == 0:
|
||||||
names.WriteByte(arg[i])
|
names.WriteByte(path[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
segments := strings.Split(names.String(), ".")
|
segments := strings.Split(names.String(), ".")
|
||||||
return segments[len(segments)-1]
|
return segments[len(segments)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// templateError marks a failure that is the input's own fault — no template, or one
|
// inputError marks a failure that is the input's own fault — no template, or one that
|
||||||
// that does not compile. run reports it as misuse (exit 2, with a pointer to --help),
|
// does not compile. run reports it as misuse (exit 2, with a pointer to --help), unlike
|
||||||
// unlike an unknown path, which is a runtime error (exit 1).
|
// an unknown path, which is a runtime error (exit 1).
|
||||||
type templateError struct{ error }
|
type inputError struct{ error }
|
||||||
|
|
||||||
func (e templateError) Unwrap() error { return e.error }
|
func (e inputError) Unwrap() error { return e.error }
|
||||||
|
|
||||||
// input is what stdin names: the path a lone reference reads, or else the template as
|
// input is what stdin names: the path a lone reference reads, or else the template.
|
||||||
// the library takes it.
|
|
||||||
type input struct {
|
type input struct {
|
||||||
path string
|
path string
|
||||||
template string
|
template string
|
||||||
}
|
}
|
||||||
|
|
||||||
// readInput reads stdin less one newline ending it. A JSON object, array or string
|
// parseInput reads stdin's text less one newline ending it.
|
||||||
// sheds the whitespace around it; any other text becomes a JSON string, so the library
|
func parseInput(raw string) (input, error) {
|
||||||
// reads it as a format string even where it is JSON of another kind, such as 42.
|
text, found := strings.CutSuffix(raw, "\n")
|
||||||
func readInput(raw string) (input, error) {
|
if found {
|
||||||
text := strings.TrimSuffix(raw, "\n")
|
text = strings.TrimSuffix(text, "\r")
|
||||||
|
}
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
|
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
|
||||||
}
|
}
|
||||||
if path, lone := loneReference(text); lone {
|
if path, lone := loneReference(text); lone {
|
||||||
return input{path: path}, nil
|
return input{path: path}, nil
|
||||||
}
|
}
|
||||||
if trimmed := strings.TrimSpace(text); trimmed != "" && strings.ContainsRune(`{["`, rune(trimmed[0])) && json.Valid([]byte(trimmed)) {
|
return input{template: text}, nil
|
||||||
return input{template: trimmed}, nil
|
|
||||||
}
|
|
||||||
quoted, err := json.Marshal(text)
|
|
||||||
if err != nil {
|
|
||||||
return input{}, err
|
|
||||||
}
|
|
||||||
return input{template: string(quoted)}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loneReference is the path that text, one reference from the root and nothing else,
|
// loneReference is the path that text, one reference from the root and nothing else,
|
||||||
@@ -491,7 +482,7 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
var src input
|
var src input
|
||||||
if !in.list {
|
if !in.list {
|
||||||
if src, err = source(stdin); err != nil {
|
if src, err = readStdin(stdin); err != nil {
|
||||||
return fail(stderr, err)
|
return fail(stderr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,25 +506,23 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// source reads the input from stdin.
|
func readStdin(stdin io.Reader) (input, error) {
|
||||||
func source(stdin io.Reader) (input, error) {
|
|
||||||
if stdin == nil {
|
if stdin == nil {
|
||||||
return input{}, templateError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint)}
|
return input{}, inputError{fmt.Errorf("the template comes from stdin, and nothing is piped in: %s", pipeHint)}
|
||||||
}
|
}
|
||||||
raw, err := io.ReadAll(stdin)
|
raw, err := io.ReadAll(stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return input{}, err
|
return input{}, err
|
||||||
}
|
}
|
||||||
src, err := readInput(string(raw))
|
src, err := parseInput(string(raw))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return input{}, templateError{err}
|
return input{}, inputError{err}
|
||||||
}
|
}
|
||||||
return src, nil
|
return src, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fail reports err and returns its exit code: 2 for the input's own fault, else 1.
|
|
||||||
func fail(stderr io.Writer, err error) int {
|
func fail(stderr io.Writer, err error) int {
|
||||||
var te templateError
|
var te inputError
|
||||||
if errors.As(err, &te) {
|
if errors.As(err, &te) {
|
||||||
return misuse(stderr, te.error)
|
return misuse(stderr, te.error)
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-35
@@ -242,8 +242,8 @@ func TestRunMisuse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRunTakesNoArgument pins the way from the old spelling, a template or a path as
|
// TestRunTakesNoArgument pins the error for a path or a template given as an argument:
|
||||||
// an argument, to the one that runs: the template on stdin.
|
// it shows the template piped to stdin.
|
||||||
func TestRunTakesNoArgument(t *testing.T) {
|
func TestRunTakesNoArgument(t *testing.T) {
|
||||||
for _, args := range [][]string{{"sv_SE.person"}, {"{/sv_SE.person}"}, {"name:", "{/sv_SE.person.last}"}} {
|
for _, args := range [][]string{{"sv_SE.person"}, {"{/sv_SE.person}"}, {"name:", "{/sv_SE.person.last}"}} {
|
||||||
code, out, errb := runOut("", args...)
|
code, out, errb := runOut("", args...)
|
||||||
@@ -267,47 +267,32 @@ func TestRunTerminalStdinIsMisuse(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunDropsOneTrailingNewline(t *testing.T) {
|
func TestRunDropsOneTrailingNewline(t *testing.T) {
|
||||||
for stdin, want := range map[string]string{
|
for stdin, want := range map[string]string{
|
||||||
"hihi": "hihi\n",
|
"hihi": "hihi\n",
|
||||||
"hihi\n": "hihi\n",
|
"hihi\n": "hihi\n",
|
||||||
"hihi\n\n": "hihi\n\n",
|
"hihi\r\n": "hihi\n",
|
||||||
"a\nb\n": "a\nb\n",
|
"hihi\n\n": "hihi\n\n",
|
||||||
" hihi \n": " hihi \n",
|
"hihi\r\n\r\n": "hihi\r\n\n",
|
||||||
"hihi\r\n": "hihi\r\n",
|
"a\nb\n": "a\nb\n",
|
||||||
"{{x}}\n": "{x}\n",
|
" hihi \n": " hihi \n",
|
||||||
"x: {/sv_SE.person.last}\n\n": "",
|
"{{x}}\n": "{x}\n",
|
||||||
|
"\"a\"\n": "a\n",
|
||||||
} {
|
} {
|
||||||
code, out, errb := runOut(stdin, "--seed", "1")
|
code, out, errb := runOut(stdin, "--seed", "1")
|
||||||
if want == "" {
|
|
||||||
if code != 0 || !strings.HasPrefix(out, "x: ") || !strings.HasSuffix(out, "\n\n") || strings.Count(out, "\n") != 2 {
|
|
||||||
t.Errorf("run(%q) = %d, %q, %q; want the template's own newline, then the line's", stdin, code, out, errb)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if code != 0 || out != want {
|
if code != 0 || out != want {
|
||||||
t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want)
|
t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
code, out, errb := runOut("x: {/sv_SE.person.last}\n\n", "--seed", "1")
|
||||||
|
if code != 0 || !strings.HasPrefix(out, "x: ") || !strings.HasSuffix(out, "\n\n") || strings.Count(out, "\n") != 2 {
|
||||||
func TestRunTextPrintsAsWritten(t *testing.T) {
|
t.Errorf("a template ending in a newline = %d, %q, %q; want its own newline, then the line's", code, out, errb)
|
||||||
for _, stdin := range []string{"sv_SE.person", "42", "null", "true", "[Skåne län]", "x[1]y", `say "hi"`} {
|
|
||||||
code, out, errb := runOut(stdin)
|
|
||||||
if code != 0 || out != stdin+"\n" {
|
|
||||||
t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunTrimsWhitespaceAroundJSON(t *testing.T) {
|
func TestRunTextPrintsAsWritten(t *testing.T) {
|
||||||
for _, stdin := range []string{
|
for _, stdin := range []string{"sv_SE.person", "[Skåne län]", "x[1]y", `say "hi"`} {
|
||||||
"{\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n",
|
code, out, errb := runOut(stdin)
|
||||||
" {\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n\n",
|
if code != 0 || out != stdin+"\n" {
|
||||||
"\t[\"a\",\"b\"]\r\n",
|
t.Errorf("run(%q) = %d, %q, %q; want the text as written", stdin, code, out, errb)
|
||||||
" \"a\" \n",
|
|
||||||
} {
|
|
||||||
code, out, errb := runOut(stdin, "--seed", "1")
|
|
||||||
if code != 0 || (out != "a\n" && out != "b\n") {
|
|
||||||
t.Errorf("run(%q) = %d, %q, %q; want a or b", stdin, code, out, errb)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,6 +449,9 @@ func TestRunTemplateMisuse(t *testing.T) {
|
|||||||
`{"format":"x"}`: "is a string",
|
`{"format":"x"}`: "is a string",
|
||||||
"name: {/no.such.path}": "no entry",
|
"name: {/no.such.path}": "no entry",
|
||||||
"a } b": "}}",
|
"a } b": "}}",
|
||||||
|
" [\"a\",\"b\"]": "may not be padded",
|
||||||
|
"[\"a\",\"b\"]\n\n": "may not be padded",
|
||||||
|
"42": "number",
|
||||||
} {
|
} {
|
||||||
code, out, errb := runOut(stdin, "--seed", "1")
|
code, out, errb := runOut(stdin, "--seed", "1")
|
||||||
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
||||||
@@ -579,7 +567,7 @@ func TestRunRecordNDJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunRecordCSVRoundTrips(t *testing.T) {
|
func TestRunRecordCSVRoundTrips(t *testing.T) {
|
||||||
code, out, errb := runOut("{/users}\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t))
|
code, out, errb := runOut("{/users}\r\n", "--seed", "1", "--format", "csv", "--repeat", "3", "--data-path", recordDir(t))
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("run = %d, stderr=%q", code, errb)
|
t.Fatalf("run = %d, stderr=%q", code, errb)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -67,9 +67,8 @@ says so. No `--template` flag is needed. Reserving the characters whole — thou
|
|||||||
leading one could collide — keeps one simple name rule instead of a leading-position
|
leading one could collide — keeps one simple name rule instead of a leading-position
|
||||||
special case. The JSON string is what makes the library's own advice reachable: the
|
special case. The JSON string is what makes the library's own advice reachable: the
|
||||||
error for an object holding only a format names `"…"`, and that spelling has to work
|
error for an object holding only a format names `"…"`, and that spelling has to work
|
||||||
where it is printed. An argument or struct tag of one reference alone, `{/users}`, is
|
where it is printed. A struct tag of one reference alone, `{/users}`, is refused naming
|
||||||
refused naming the path `users`: both render the same text, and only the path names a
|
the path `users`: both render the same text, and only the path names a record. A folder-relative `{.name}` or `{..name}` is refused naming `{/name}`, since an
|
||||||
record. A folder-relative `{.name}` or `{..name}` is refused naming `{/name}`, since an
|
|
||||||
inline template sits in no folder. `IsTemplate` exports the rule, so struct tags and any
|
inline template sits in no folder. `IsTemplate` exports the rule, so struct tags and any
|
||||||
other caller read one.
|
other caller read one.
|
||||||
|
|
||||||
@@ -79,14 +78,16 @@ other caller read one.
|
|||||||
|
|
||||||
The CLI takes no path or template as an argument: an argument is misuse, and so is a
|
The CLI takes no path or template as an argument: an argument is misuse, and so is a
|
||||||
terminal with nothing piped in, which would otherwise wait for typed input. Both errors
|
terminal with nothing piped in, which would otherwise wait for typed input. Both errors
|
||||||
show `echo -n '{/sv_SE.person}' | fejkdata`. One newline ending stdin is dropped, so
|
show `echo -n '{/sv_SE.person}' | fejkdata`. One newline ending stdin, `\n` or `\r\n`,
|
||||||
`echo`, a file and a heredoc render as `echo -n` does; only one, so a template that
|
is dropped, so `echo`, a file and a heredoc render as `echo -n` does; only one, so a
|
||||||
should end in a newline ends stdin with two. Any text that is no JSON object, array or
|
template that should end in a newline ends stdin with two. The rest of stdin reaches
|
||||||
string, `42` and `null` included, is a format string and prints as written. One
|
`NewTemplate` unchanged, so a padded JSON template is refused there, as goal 5.7 asks.
|
||||||
reference from the root and nothing else, `{/users}`, reads the path it names: it renders
|
One reference from the root and nothing else, `{/users}`, reads the path it names: it
|
||||||
the same text, and under `--format` it is the record `users`, with `users` the default
|
renders the same text, and under `--format` it is the record `users`, with `users` the
|
||||||
`--table`. `--list` still prints paths, each read as `{/path}`. Valid while the CLI reads
|
default `--table`; one to nothing exits 1, as an unknown path does. That reading sits in
|
||||||
one template per run.
|
the CLI against goal 3.2 until `todo.md` item 129 moves it into the library. `--list`
|
||||||
|
still prints paths, each read as `{/path}`. Valid while the CLI reads one template per
|
||||||
|
run.
|
||||||
|
|
||||||
## An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
## An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
||||||
|
|
||||||
@@ -96,15 +97,12 @@ The whole template is the spelling under test, and `NewTemplate` compiles, links
|
|||||||
validates as one step. An unknown *path* stays a runtime error (exit 1): there the
|
validates as one step. An unknown *path* stays a runtime error (exit 1): there the
|
||||||
template is well-formed and only the data is absent.
|
template is well-formed and only the data is absent.
|
||||||
|
|
||||||
## The library rejects a padded JSON template, and the CLI trims the whitespace around one on stdin
|
## A padded JSON argument is rejected, not trimmed
|
||||||
|
|
||||||
2026-09-03, Lilleman auf Larv; the CLI's trim 2026-10-07, Lilleman auf Larv. Serves goal 7.2.
|
2026-09-03, Lilleman auf Larv.
|
||||||
|
|
||||||
Padding is the one place the two readings disagree — a format string renders it, JSON
|
Padding is the one place the two readings disagree — a format string renders it, JSON
|
||||||
drops it — so `NewTemplate` names the spelling that renders rather than silently
|
drops it — so the spelling that renders is named rather than silently chosen.
|
||||||
choosing. On stdin, `echo` and a file end in a newline nobody meant to render, so the
|
|
||||||
maintainer ruled that the whitespace around a JSON object, array or string is dropped
|
|
||||||
before the library reads it. Valid while a JSON template on stdin is one value.
|
|
||||||
|
|
||||||
## `FakeTemplate` and `NewTemplate` both stay
|
## `FakeTemplate` and `NewTemplate` both stay
|
||||||
|
|
||||||
@@ -852,6 +850,8 @@ Valid while names are read only inside the category binding them.
|
|||||||
|
|
||||||
2026-10-04, Lilleman auf Larv. Serves goal 3 and applies KISS.
|
2026-10-04, Lilleman auf Larv. Serves goal 3 and applies KISS.
|
||||||
|
|
||||||
|
Shipped under the floor by the maintainer's decision of 2026-10-07: the CLI reading its template from stdin, with the scoring run and its ratchet waived for that chunk.
|
||||||
|
|
||||||
Last scoring run, 2026-10-07, on commit ad59967: Navigation 7.00, Locality 5.88, Shape 6.50, Self-sufficiency 6.13, overall 6.38. The nine-seat panel at depth 1, run beside it: Navigation 7.17, Locality 6.06, Shape 6.67, Self-sufficiency 6.56, overall 6.56. Against the runs on ac6158e, the scoring run's overall rose from 6.25 and its Shape from 6.38, while its Self-sufficiency fell from 6.38; the panel's overall held, and its Locality and Self-sufficiency fell by 0.05 each. Locality is lowest again, as at every panel since 2026-09-22. All 13 seats named the named-pick read path hardest: `readField`, `readName`, `readUnder` and `keeps`, with the pick keys `levelKeys` and `addressedKeys` build at load; twelve kept `doc.go`'s vocabulary open beside it. What the two runs found is filed in `todo.md`, and `todo.md` item 153 runs both again. Valid while `AGENTS.md` gates goal 3 at 7.0.
|
Last scoring run, 2026-10-07, on commit ad59967: Navigation 7.00, Locality 5.88, Shape 6.50, Self-sufficiency 6.13, overall 6.38. The nine-seat panel at depth 1, run beside it: Navigation 7.17, Locality 6.06, Shape 6.67, Self-sufficiency 6.56, overall 6.56. Against the runs on ac6158e, the scoring run's overall rose from 6.25 and its Shape from 6.38, while its Self-sufficiency fell from 6.38; the panel's overall held, and its Locality and Self-sufficiency fell by 0.05 each. Locality is lowest again, as at every panel since 2026-09-22. All 13 seats named the named-pick read path hardest: `readField`, `readName`, `readUnder` and `keeps`, with the pick keys `levelKeys` and `addressedKeys` build at load; twelve kept `doc.go`'s vocabulary open beside it. What the two runs found is filed in `todo.md`, and `todo.md` item 153 runs both again. Valid while `AGENTS.md` gates goal 3 at 7.0.
|
||||||
|
|
||||||
## The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
|
## The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
|
||||||
|
|||||||
+2
-2
@@ -68,8 +68,8 @@ func rendersInside(nodes []node, t *template) bool {
|
|||||||
// template reaches the binder through fields. A single read as a calc operand stands where a calc
|
// template reaches the binder through fields. A single read as a calc operand stands where a calc
|
||||||
// could not read the spelling, a reference or a path. So {/misc.coordinate.lat as lat}{calc(lat * 60)}
|
// could not read the spelling, a reference or a path. So {/misc.coordinate.lat as lat}{calc(lat * 60)}
|
||||||
// stands, since {calc(/misc.coordinate.lat * 60)} does not compile.
|
// stands, since {calc(/misc.coordinate.lat * 60)} does not compile.
|
||||||
// Where the spelling would be a whole CLI argument or a whole struct tag, that entry point refuses
|
// Where the spelling would be a whole struct tag, that entry point refuses it and names the bare
|
||||||
// it and names the bare path.
|
// path.
|
||||||
func (b *nameBinding) refuseSingleRead(uses []nameUse) error {
|
func (b *nameBinding) refuseSingleRead(uses []nameUse) error {
|
||||||
if len(uses) == 0 {
|
if len(uses) == 0 {
|
||||||
panic(invariant.Broken("name %q has no read at resolve, though its compile found one", b.name))
|
panic(invariant.Broken("name %q has no read at resolve, though its compile found one", b.name))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
`Bar = 9`
|
`Bar = 9`
|
||||||
|
|
||||||
`Next ID = 156`
|
`Next ID = 157`
|
||||||
|
|
||||||
| Goal | W |
|
| Goal | W |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -70,14 +70,15 @@
|
|||||||
| 61 | 0.1.0 | | **Add a locale, with its `geo/` tree, for each of the ten most-spoken languages and each Nordic country.** | 4 | 9 | 9 | 10 | 2.1 | 30.8 |
|
| 61 | 0.1.0 | | **Add a locale, with its `geo/` tree, for each of the ten most-spoken languages and each Nordic country.** | 4 | 9 | 9 | 10 | 2.1 | 30.8 |
|
||||||
| 16 | 0.1.0 | | **Add locale categories: company, phone, finance, vehicle, words.** | 3 | 8 | 8 | 9 | 2.2, 15 | 28.1 |
|
| 16 | 0.1.0 | | **Add locale categories: company, phone, finance, vehicle, words.** | 3 | 8 | 8 | 9 | 2.2, 15 | 28.1 |
|
||||||
| 15 | 0.1.0 | | **Add `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 8 | 8 | 2, 15, 14 | 25.9 |
|
| 15 | 0.1.0 | | **Add `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 8 | 8 | 2, 15, 14 | 25.9 |
|
||||||
| 129 | 0.1.0 | | **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, 5.7, 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, 5.7, 7 | 25.0 |
|
||||||
| 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 |
|
||||||
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
|
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
|
||||||
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
|
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
|
||||||
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
|
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
|
||||||
| 145 | 0.1.0 | | **Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.** | 2 | 3 | 1 | 9 | 3.4, 3.2 | 15.4 |
|
| 145 | 0.1.0 | | **Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.** | 2 | 3 | 1 | 9 | 3.4, 3.2 | 15.4 |
|
||||||
| 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 |
|
| 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 |
|
||||||
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
|
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
|
||||||
|
| 156 | 0.1.0 | question | **Refuse a JSON string whose text stands alone as a format string, `"x {/a}"`, naming the bare text, or keep it as the decision "An argument is a template by its shape, not by a flag" does.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 |
|
||||||
| 10 | 0.1.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 |
|
| 10 | 0.1.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 |
|
||||||
| 148 | 0.1.0 | | **State in `geo.py` the locality row each country's script hands it, and meet the fence that every parent row has a child row in one shared step.** | 2 | 3 | 2 | 6 | 2.1, 3.2 | 12.6 |
|
| 148 | 0.1.0 | | **State in `geo.py` the locality row each country's script hands it, and meet the fence that every parent row has a child row in one shared step.** | 2 | 3 | 2 | 6 | 2.1, 3.2 | 12.6 |
|
||||||
| 146 | 0.1.0 | | **Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.** | 1 | 2 | 1 | 6 | 3.1, 3.2 | 11.3 |
|
| 146 | 0.1.0 | | **Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.** | 1 | 2 | 1 | 6 | 3.1, 3.2 | 11.3 |
|
||||||
@@ -141,7 +142,6 @@
|
|||||||
| 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 5 | 5 | 10.8 |
|
| 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 5 | 5 | 10.8 |
|
||||||
| 47 | 0.3.0 | defect | **Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there.** | 1 | 1 | 2 | 5 | 6 | 10.2 |
|
| 47 | 0.3.0 | defect | **Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there.** | 1 | 1 | 2 | 5 | 6 | 10.2 |
|
||||||
| 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 |
|
| 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 |
|
||||||
| 43 | 0.3.0 | | **Call `templateError` a compile failure in its doc.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 |
|
|
||||||
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
|
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
|
||||||
| 116 | 0.3.0 | | **Turn a row index into its rows file's line in one function.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
|
| 116 | 0.3.0 | | **Turn a row index into its rows file's line in one function.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
|
||||||
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14 | 9.2 |
|
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14 | 9.2 |
|
||||||
@@ -280,9 +280,13 @@ All 13 seats of the 2026-10-07 panel and scoring run on ad59967 again named the
|
|||||||
|
|
||||||
`docs/research/research-geo-se.md` reads delivery digit 0 or 1 as boxes, 2–4 and 6–7 as street, 5 as rural, 8 as reply mail and 9 as competitions or temporary, with exceptions in big cities. `street_delivery` drops 0, 1 and 8 and keeps 9. The panel of 2026-10-05 found it; whether a shipped street carries a 9 code is unchecked.
|
`docs/research/research-geo-se.md` reads delivery digit 0 or 1 as boxes, 2–4 and 6–7 as street, 5 as rural, 8 as reply mail and 9 as competitions or temporary, with exceptions in big cities. `street_delivery` drops 0, 1 and 8 and keeps 9. The panel of 2026-10-05 found it; whether a shipped street carries a 9 code is unchecked.
|
||||||
|
|
||||||
### 110. Refuse an empty segment in a path `Fake`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.
|
### 110. Refuse an empty segment in a path `Fake` and `FakeRecord` take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.
|
||||||
|
|
||||||
Today `fejkdata 'sv_SE.person.'` reports `no field ""`, while `{/sv_SE.person.}` reports `path has an empty segment`. `grammar.CheckSegments` is the check a reference, a sibling path and a step up `..` run.
|
Today `Fake("sv_SE.person.")` reports `no field ""`, while `{/sv_SE.person.}` reports `path has an empty segment`. `grammar.CheckSegments` is the check a reference, a sibling path and a step up `..` run.
|
||||||
|
|
||||||
|
### 156. Refuse a JSON string whose text stands alone as a format string, `"x {/a}"`, naming the bare text, or keep it as the decision "An argument is a template by its shape, not by a flag" does.
|
||||||
|
|
||||||
|
Goal 5.7 refuses a wrapper around what could stand alone. That decision keeps the JSON string because, as a CLI argument, bare text read as a path; stdin reads bare text as a format string, so `"x {/a}"` and `x {/a}` render alike there and in `NewTemplate`. A JSON string still earns its place where its escapes carry what bare text cannot. The goals audit of the stdin change found it.
|
||||||
|
|
||||||
### 10. Test that every node kind reaches each switch over node kinds.
|
### 10. Test that every node kind reaches each switch over node kinds.
|
||||||
|
|
||||||
@@ -517,10 +521,6 @@ Both phone formats carry the same 21-code `area` list, so an edit to one copy ca
|
|||||||
|
|
||||||
`technical-principles.md` holds that test coverage should not decline. `docker compose run --rm cover` prints the figure, and neither the `Dockerfile` gate nor `.github/workflows/test.yml` checks it.
|
`technical-principles.md` holds that test coverage should not decline. `docker compose run --rm cover` prints the figure, and neither the `Dockerfile` gate nor `.github/workflows/test.yml` checks it.
|
||||||
|
|
||||||
### 43. Call `templateError` a compile failure in its doc.
|
|
||||||
|
|
||||||
Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTemplate` refuse.
|
|
||||||
|
|
||||||
### 95. Hold a struct's columns and the field each fills in one slice.
|
### 95. Hold a struct's columns and the field each fills in one slice.
|
||||||
|
|
||||||
`structShape.fieldIndexes` pairs with its record template's `columns` by index alone, against the technical principle "one owner per value".
|
`structShape.fieldIndexes` pairs with its record template's `columns` by index alone, against the technical principle "one owner per value".
|
||||||
|
|||||||
Reference in New Issue
Block a user