From 83b4b0bdf0e8568c0788a186cee5bbbc6da173e2 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 7 Oct 2026 16:53:41 +0200 Subject: [PATCH] Answer architecture round 1: stdin drops one newline, \n or \r\n, and reaches the library unchanged, inputError names the input's fault, two decisions and item 110 stop naming a CLI argument, the gate exemption moves to the floor entry, and item 156 asks about the JSON string --- AGENTS.md | 6 ++-- CHANGELOG.md | 7 ++-- README.md | 13 +++---- cmd/fejkdata/main.go | 71 +++++++++++++++++---------------------- cmd/fejkdata/main_test.go | 58 +++++++++++++------------------- docs/decisions.md | 34 +++++++++---------- namefence.go | 4 +-- todo.md | 20 +++++------ 8 files changed, 95 insertions(+), 118 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2a00f8b..4764b07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ wait for one. - 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 - dependency bump, a repair of behaviour the README documents, 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 + dependency bump, a repair of behaviour the README documents, and the infrastructure + 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 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 @@ -41,7 +41,7 @@ In [docs/decisions.md](docs/decisions.md): - 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 - 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 - 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` diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe42a1..8845a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,9 @@ replacement, and each removed path, column or flag. under [Names](README.md#names). - 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' | - fejkdata`, and takes no path or template as an argument. One newline ending stdin is - dropped, and so is the whitespace around a JSON template. A template of one reference - alone, `{/users}`, is the record `users` under `--format`. An argument, or nothing - piped in, exits 2. + fejkdata`, and takes no path or template as an argument. One newline ending stdin, + `\n` or `\r\n`, is dropped. A template of one reference alone, `{/users}`, is the + record `users` under `--format`. An argument, or nothing piped in, exits 2. - 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, `misc.territory[SE]`, and descends to a linked table by name, one path drawing each diff --git a/README.md b/README.md index 1d94c1d..67c64de 100644 --- a/README.md +++ b/README.md @@ -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 these paths; a template reads one as `{/path}`. -One newline ending stdin is dropped, so `echo` and `echo -n`, a file and a heredoc -render alike; end stdin with two newlines to print one. Whitespace around a JSON -template is dropped too. Which spellings a template rejects, and what each names -instead, is under +One newline ending stdin, `\n` or `\r\n`, is dropped, so `echo` and `echo -n`, a file +and a heredoc render alike; end stdin with two newlines to print one. Some shells' `echo` +reads a backslash as an escape, so pipe a template holding one with `printf '%s'` or from +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). | Flag | | @@ -56,7 +57,7 @@ instead, is under `--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, 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: `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/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/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/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 diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index bde72ab..03b6c9b 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -9,7 +9,6 @@ package main import ( "bufio" - "encoding/json" "errors" "fmt" "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 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 -two to print one. Whitespace around a JSON template is dropped too. +One newline ending stdin, \n or \r\n, is dropped, so echo and echo -n render +alike; end stdin with two to print one. 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} — @@ -60,13 +59,13 @@ A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3) ` type invocation struct { + args []string dirs []string format string formatSet bool help bool list bool noShipped bool - args []string repeat int repeatSet bool seed uint64 @@ -267,7 +266,6 @@ func (in invocation) checkFlags() error { 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" // 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) if err != nil { - return nil, templateError{err} + return nil, inputError{err} } 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 == "" { t, err := f.NewRecordTemplate(src.template) if err != nil { - return nil, "", templateError{err} + return nil, "", inputError{err} } 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 // segment, or "records" for a template that reads no path alone. -func defaultTable(arg string) string { - if arg == "" { +func defaultTable(path string) string { + if path == "" { return "records" } 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 { - case arg[i] == '[': + case path[i] == '[': depth++ - case arg[i] == ']': + case path[i] == ']': depth-- case depth == 0: - names.WriteByte(arg[i]) + names.WriteByte(path[i]) } } segments := strings.Split(names.String(), ".") return segments[len(segments)-1] } -// templateError marks a failure that is the input's own fault — no template, or one -// that does not compile. run reports it as misuse (exit 2, with a pointer to --help), -// unlike an unknown path, which is a runtime error (exit 1). -type templateError struct{ error } +// inputError marks a failure that is the input's own fault — no template, or one that +// does not compile. run reports it as misuse (exit 2, with a pointer to --help), unlike +// an unknown path, which is a runtime error (exit 1). +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 -// the library takes it. +// input is what stdin names: the path a lone reference reads, or else the template. type input struct { path string template string } -// readInput reads stdin less one newline ending it. A JSON object, array or string -// sheds the whitespace around it; any other text becomes a JSON string, so the library -// reads it as a format string even where it is JSON of another kind, such as 42. -func readInput(raw string) (input, error) { - text := strings.TrimSuffix(raw, "\n") +// parseInput reads stdin's text less one newline ending it. +func parseInput(raw string) (input, error) { + text, found := strings.CutSuffix(raw, "\n") + if found { + text = strings.TrimSuffix(text, "\r") + } if text == "" { return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint) } if path, lone := loneReference(text); lone { return input{path: path}, nil } - if trimmed := strings.TrimSpace(text); trimmed != "" && strings.ContainsRune(`{["`, rune(trimmed[0])) && json.Valid([]byte(trimmed)) { - return input{template: trimmed}, nil - } - quoted, err := json.Marshal(text) - if err != nil { - return input{}, err - } - return input{template: string(quoted)}, nil + return input{template: text}, nil } // 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 if !in.list { - if src, err = source(stdin); err != nil { + if src, err = readStdin(stdin); err != nil { return fail(stderr, err) } } @@ -515,25 +506,23 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 0 } -// source reads the input from stdin. -func source(stdin io.Reader) (input, error) { +func readStdin(stdin io.Reader) (input, error) { 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) if err != nil { return input{}, err } - src, err := readInput(string(raw)) + src, err := parseInput(string(raw)) if err != nil { - return input{}, templateError{err} + return input{}, inputError{err} } 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 { - var te templateError + var te inputError if errors.As(err, &te) { return misuse(stderr, te.error) } diff --git a/cmd/fejkdata/main_test.go b/cmd/fejkdata/main_test.go index ec58949..0767ca5 100644 --- a/cmd/fejkdata/main_test.go +++ b/cmd/fejkdata/main_test.go @@ -242,8 +242,8 @@ func TestRunMisuse(t *testing.T) { } } -// TestRunTakesNoArgument pins the way from the old spelling, a template or a path as -// an argument, to the one that runs: the template on stdin. +// TestRunTakesNoArgument pins the error for a path or a template given as an argument: +// it shows the template piped to stdin. func TestRunTakesNoArgument(t *testing.T) { for _, args := range [][]string{{"sv_SE.person"}, {"{/sv_SE.person}"}, {"name:", "{/sv_SE.person.last}"}} { code, out, errb := runOut("", args...) @@ -267,47 +267,32 @@ func TestRunTerminalStdinIsMisuse(t *testing.T) { func TestRunDropsOneTrailingNewline(t *testing.T) { for stdin, want := range map[string]string{ - "hihi": "hihi\n", - "hihi\n": "hihi\n", - "hihi\n\n": "hihi\n\n", - "a\nb\n": "a\nb\n", - " hihi \n": " hihi \n", - "hihi\r\n": "hihi\r\n", - "{{x}}\n": "{x}\n", - "x: {/sv_SE.person.last}\n\n": "", + "hihi": "hihi\n", + "hihi\n": "hihi\n", + "hihi\r\n": "hihi\n", + "hihi\n\n": "hihi\n\n", + "hihi\r\n\r\n": "hihi\r\n\n", + "a\nb\n": "a\nb\n", + " hihi \n": " hihi \n", + "{{x}}\n": "{x}\n", + "\"a\"\n": "a\n", } { 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 { t.Errorf("run(%q) = %d, %q, %q; want %q", stdin, code, out, errb, want) } } -} - -func TestRunTextPrintsAsWritten(t *testing.T) { - 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) - } + 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 { + t.Errorf("a template ending in a newline = %d, %q, %q; want its own newline, then the line's", code, out, errb) } } -func TestRunTrimsWhitespaceAroundJSON(t *testing.T) { - for _, stdin := range []string{ - "{\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n", - " {\"format\":\"{x}\",\"x\":[\"a\",\"b\"]}\n\n", - "\t[\"a\",\"b\"]\r\n", - " \"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) +func TestRunTextPrintsAsWritten(t *testing.T) { + for _, stdin := range []string{"sv_SE.person", "[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) } } } @@ -464,6 +449,9 @@ func TestRunTemplateMisuse(t *testing.T) { `{"format":"x"}`: "is a string", "name: {/no.such.path}": "no entry", "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") 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) { - 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 { t.Fatalf("run = %d, stderr=%q", code, errb) } diff --git a/docs/decisions.md b/docs/decisions.md index ca6109a..e512aeb 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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 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 -where it is printed. An argument or struct tag of one reference alone, `{/users}`, is -refused naming 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 +where it is printed. A struct tag of one reference alone, `{/users}`, is refused naming +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 inline template sits in no folder. `IsTemplate` exports the rule, so struct tags and any 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 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 -`echo`, a file and a heredoc render as `echo -n` does; only one, so a template that -should end in a newline ends stdin with two. Any text that is no JSON object, array or -string, `42` and `null` included, is a format string and prints as written. One -reference from the root and nothing else, `{/users}`, reads the path it names: it renders -the same text, and under `--format` it is the record `users`, with `users` the default -`--table`. `--list` still prints paths, each read as `{/path}`. Valid while the CLI reads -one template per run. +show `echo -n '{/sv_SE.person}' | fejkdata`. One newline ending stdin, `\n` or `\r\n`, +is dropped, so `echo`, a file and a heredoc render as `echo -n` does; only one, so a +template that should end in a newline ends stdin with two. The rest of stdin reaches +`NewTemplate` unchanged, so a padded JSON template is refused there, as goal 5.7 asks. +One reference from the root and nothing else, `{/users}`, reads the path it names: it +renders the same text, and under `--format` it is the record `users`, with `users` the +default `--table`; one to nothing exits 1, as an unknown path does. That reading sits in +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 @@ -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 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 -drops it — so `NewTemplate` names the spelling that renders rather than silently -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. +drops it — so the spelling that renders is named rather than silently chosen. ## `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. +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. ## The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/` diff --git a/namefence.go b/namefence.go index 3b502d0..88e7973 100644 --- a/namefence.go +++ b/namefence.go @@ -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 // 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. -// Where the spelling would be a whole CLI argument or a whole struct tag, that entry point refuses -// it and names the bare path. +// Where the spelling would be a whole struct tag, that entry point refuses it and names the bare +// path. func (b *nameBinding) refuseSingleRead(uses []nameUse) error { if len(uses) == 0 { panic(invariant.Broken("name %q has no read at resolve, though its compile found one", b.name)) diff --git a/todo.md b/todo.md index 6935b66..a38d812 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 156` +`Next ID = 157` | 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 | | 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 | -| 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 | | 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 | | 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 | | 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 | | 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 | @@ -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 | | 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 | -| 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 | | 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 | @@ -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. -### 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. @@ -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. -### 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. `structShape.fieldIndexes` pairs with its record template's `columns` by index alone, against the technical principle "one owner per value".