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

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
17 changed files with 121 additions and 85 deletions
Showing only changes of commit f89ffd1b73 - Show all commits
+3 -3
View File
@@ -63,7 +63,7 @@ In [docs/decisions.md](docs/decisions.md):
- A record's column set is fixed before the first draw - A record's column set is fixed before the first draw
- Null is a `null` item, not a rate - Null is a `null` item, not a rate
- A typed column holds one value, not composed text - A typed column holds one value, not composed text
- A column of one reference or name read alone is the column it reads - A column that only reads one reference or name is the column it reads
- A typed column's calc is refused unless proven - A typed column's calc is refused unless proven
- `Column` carries text, not a Go value - `Column` carries text, not a Go value
- A unit takes the stem of what it is, and a file the stem of the units it holds - A unit takes the stem of what it is, and a file the stem of the units it holds
@@ -74,7 +74,7 @@ In [docs/decisions.md](docs/decisions.md):
- `parent` names the link column and the table alike - `parent` names the link column and the table alike
- After a row, a path names a column or a linked table - After a row, a path names a column or a linked table
- A selected row is fixed, not drawn - A selected row is fixed, not drawn
- `..` steps up to the parent row, and a step down after it draws afresh, or once per pick - `..` steps up to the parent row, and a step down after it draws afresh, or once per name's pick
- A table selects by one key column, a code, never a free-form name - A table selects by one key column, a code, never a free-form name
- A parent row with no child row is a load error - A parent row with no child row is a load error
- The choice-of-rows fence guards a data file's root, and requires string fields - The choice-of-rows fence guards a data file's root, and requires string fields
@@ -111,4 +111,4 @@ In [docs/decisions.md](docs/decisions.md):
- `List` advertises direct descents only - `List` advertises direct descents only
- A path draws through its compiled steps - A path draws through its compiled steps
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read - A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
- comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them - comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them
+8 -8
View File
@@ -12,16 +12,16 @@ replacement, and each removed path, column or flag.
`{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again, `{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again,
drawing afresh. `New` refuses a `..` naming anything but the parent table, and a drawing afresh. `New` refuses a `..` naming anything but the parent table, and a
selector after a `..`. selector after a `..`.
- Names: `{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints
nothing; `{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name lives
in the category binding it, and a `repeat` binding it picks again on every iteration.
A name is a `{calc()}` operand: `{/misc.coordinate.lat as lat}{calc(lat * 60)}`. What
`New` refuses is listed under [Names](README.md#names).
- Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`, - Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`,
`{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which `{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which
`{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one. `{x as n}` `{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one.
binds a sibling field as well as a reference, and a struct's tags share its names, `{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints nothing;
`{/sv_SE.person as p}{p.first}` beside `{p.sex}`. `{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name binds a
reference, a sibling field or a path into one, lives in the category binding it, and
picks again on every iteration of a `repeat` binding it. A struct's tags share its
names, `{/sv_SE.person as p}{p.first}` beside `{p.sex}`. A name is a `{calc()}`
operand: `{/misc.coordinate.lat as lat}{calc(lat * 60)}`. What `New` refuses is listed
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.
- 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,
+22 -21
View File
@@ -137,12 +137,14 @@ it as nothing.
A record is one render, so its columns read one pick of each [name](#names) its category A record is one render, so its columns read one pick of each [name](#names) its category
binds: `"code": "{/currency as c}{c.code}"` beside `"symbol": "{c.symbol}"` is one binds: `"code": "{/currency as c}{c.code}"` beside `"symbol": "{c.symbol}"` is one
currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. A currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. A
field the format binds whole, `{place as p}`, is a column holding that pick, so it agrees with column the format binds to a name, as `{place as p}` binds `place`, holds that same pick, so it
the columns reading `p`. agrees with the columns reading `p`; a column bound only in part, `{place.town as t}`, draws
afresh, so bind it whole and read `{p.town}`.
A category never references itself: `{/users.first}` or a bare `{/users}` inside `users` A category never references itself: `{/users.first}` or a bare `{/users}` inside `users`
describes a draw other than the fields beside it. Read a sibling as a field, and put a describes a draw other than the fields beside it. Read a sibling as a field, and put a
value two fields share in its own category, which each binds to a name value two fields share in its own category, bind it to a name in one field, and read the
name in both
([Decisions](docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load)). ([Decisions](docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load)).
## Data ## Data
@@ -342,9 +344,8 @@ naming a kind that holds it. The tags are a record's columns, so they share its
[names](#names): `First` tagged `{/sv_SE.person as p}{p.first}` and `Sex` tagged `{p.sex}` [names](#names): `First` tagged `{/sv_SE.person as p}{p.first}` and `Sex` tagged `{p.sex}`
describe one person, while two path tags into `sv_SE.person` are two draws. The fields an describe one person, while two path tags into `sv_SE.person` are two draws. The fields an
embedded struct promotes are columns of the same record; a named struct field, or a embedded struct promotes are columns of the same record; a named struct field, or a
pointer to one, fills from its own tags as a record of its own, so it picks apart from its pointer to one, fills from its own tags as a record of its own, so its names and picks are
parent. `fake:"-"` leaves a its own. `fake:"-"` leaves a struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
their values, and so does a pointer back to a struct already being filled; a type their values, and so does a pointer back to a struct already being filled; a type
whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The
first call for a type compiles its tags and reports what they get wrong, with the same first call for a type compiles its tags and reports what they get wrong, with the same
@@ -372,15 +373,15 @@ Every character is literal except a `{…}` token:
| Token | Renders | | Token | Renders |
|-------|---------| |-------|---------|
| `{name}` | the sibling field `name`, or the pick bound to the name ([Names](#names)) | | `{name}` | the sibling field `name`, or the pick bound to the name ([Names](#names)) |
| `{x as name}` | nothing: binds one pick of `x`, a reference or a path into a sibling field, to `name` ([Names](#names)) | | `{x as name}` | nothing: binds one pick of `x`, a reference, a sibling field or a path into one, to `name` ([Names](#names)) |
| `{name.field}` | `field` of a fresh draw of the sibling `name`, or of the pick bound to it ([Correlated fields](#correlated-fields)) | | `{name.field}` | `field` of a fresh draw of the sibling `name`, or of the pick bound to it ([Correlated fields](#correlated-fields)) |
| `{a\|b}` | one of the named fields, even odds | | `{a\|b}` | one of the named fields, even odds |
| `{fn(args)}` | a builtin ([Functions](#functions)) | | `{fn(args)}` | a builtin ([Functions](#functions)) |
| `{/path}`, `{.path}`, `{..path}` | a node reached from the data root, this file's folder, or the folder above ([References](#references)) | | `{/path}`, `{.path}`, `{..path}` | a node reached from the data root, this file's folder, or the folder above ([References](#references)) |
| `{{`, `}}` | a literal `{` or `}` | | `{{`, `}}` | a literal `{` or `}` |
Each `{…}` makes a new pick, so `{w} {w}`, `{p.a} {p.b}` and `{/person.first} Each `{…}` draws afresh, so `{w} {w}`, `{p.a} {p.b}` and `{/person.first} {/person.last}`
{/person.last}` are two draws each. A [name](#names) keeps one pick for every read of it. are two draws each. Only a [name](#names) keeps one pick for every read of it.
```json ```json
"100 Main St, Apt {int(1,9)}{upper(1)} — tel {digits(3)}-{digits(4)}" "100 Main St, Apt {int(1,9)}{upper(1)} — tel {digits(3)}-{digits(4)}"
@@ -448,7 +449,7 @@ order.id: datatype integer: {digits(3)} prints text, not an integer
order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {float()}, {int()}, {seq()} or {calc()}, or read one order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {float()}, {int()}, {seq()} or {calc()}, or read one
``` ```
A column of one reference or name read alone to another record's column — A column whose format is one read of another record's column and nothing else —
`"score": "{/src.score}"`, or `"score": "{s.score}"` with `s` bound to `/src` — is that column: it takes the column's datatype and is null where the column is, and `"score": "{/src.score}"`, or `"score": "{s.score}"` with `s` bound to `/src` — is that column: it takes the column's datatype and is null where the column is, and
a struct field tagged `src.score` is nil there. A `datatype` of its own types the a struct field tagged `src.score` is nil there. A `datatype` of its own types the
column's values where they prove it, and one restating the datatype it takes is refused. column's values where they prove it, and one restating the datatype it takes is refused.
@@ -569,7 +570,7 @@ country by its weights, then a city inside it by theirs. Two paths are two draws
[name](#names) agree instead, and the table it binds decides whose weights govern: [name](#names) agree instead, and the table it binds decides whose weights govern:
`{/city as c}` draws a city by city weight and `{c..country.name}` is its country, while `{/city as c}` draws a city by city weight and `{c..country.name}` is its country, while
`{/country as k}` draws a country by country weight and `{k.city.name}` a city inside it. `{/country as k}` draws a country by country weight and `{k.city.name}` a city inside it.
A name keeps every row its reads pin, so `{k.city.name}` and `{k.city.population}` describe A name keeps every row its reads draw, so `{k.city.name}` and `{k.city.population}` describe
one city, and `{k.name}` its country. one city, and `{k.name}` its country.
`New` refuses a link cell that is no key of the parent, a parent row no child links to, a `New` refuses a link cell that is no key of the parent, a parent row no child links to, a
@@ -717,8 +718,8 @@ without naming `sv_SE`:
"Hej, {/en_US.person}!" "Hej, {/en_US.person}!"
``` ```
Renders e.g. `Hej, Pat Smith!`. `{.person.first} {.person.last}` may name two people, as Renders e.g. `Hej, Pat Smith!`. Each reference draws afresh ([Format string](#format-string)),
`{/misc.uuid} {/misc.uuid}` is two draws ([Format string](#format-string)). Bind the person to a [name](#names) to read one: `{.person as p}{p.first} so `{.person.first} {.person.last}` may name two people. Bind the person to a [name](#names) to read one: `{.person as p}{p.first}
{p.last}`. Rejected at `New`: a path that is {p.last}`. Rejected at `New`: a path that is
unknown, names a folder, has no folder above, reads a field not every variant unknown, names a folder, has no folder above, reads a field not every variant
of a choice carries, or names the category the reference sits in, and a reference of a choice carries, or names the category the reference sits in, and a reference
@@ -726,8 +727,8 @@ that leads back to its own value, directly, mutually or through a chain.
### Names ### Names
`{x as n}` binds one pick of `x`, a reference or a path into a sibling field, to the name `n` `{x as n}` binds one pick of `x`, a reference, a sibling field or a path into one, to the name
and prints nothing. `{n}` then `n` and prints nothing. `{n}` then
prints that pick, and `{n.path}` reads a path through it, so every read of `n` describes one prints that pick, and `{n.path}` reads a path through it, so every read of `n` describes one
row: row:
@@ -739,7 +740,7 @@ Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. A name keeps wha
lands on and every level it passes, so `{n}` and `{n.path}` agree, and so does a lands on and every level it passes, so `{n}` and `{n.path}` agree, and so does a
[transform](#transforms) of `n.path`. Everything else under the name draws as it would [transform](#transforms) of `n.path`. Everything else under the name draws as it would
anywhere: if `n`'s category has the format `{w}-{w}`, it draws `w` twice, and `{n.w}` beside anywhere: if `n`'s category has the format `{w}-{w}`, it draws `w` twice, and `{n.w}` beside
`{n}` is refused, since it could read either draw. A field binds the same way, so `{n}` is refused, since it could read either draw. A sibling field is bound the same way, so
`{place as p}{p.postal-code} {p.locality}` reads one place ([Correlated fields](#correlated-fields)). `{place as p}{p.postal-code} {p.locality}` reads one place ([Correlated fields](#correlated-fields)).
A name is a pick of its own: `{t.name}` and `{/misc.territory.name}` beside it are two draws, A name is a pick of its own: `{t.name}` and `{/misc.territory.name}` beside it are two draws,
@@ -756,12 +757,12 @@ Refused at `New`, each error naming what to write instead:
and outside it; and outside it;
- a name that is a field or an option; - a name that is a field or an option;
- a binding of anything but a reference or a path into a field of the template binding it; - a binding of anything but a reference or a path into a field of the template binding it;
- a read of a name inside the field it binds: read it outside the field; - a read of a name inside the field bound to it: read it outside that field;
- a binding in a choice's item, which every other item would leave unbound; - a binding in a choice's item, which every other item would leave unbound;
- a binding nothing reads, or one read once outside a `repeat` nested where it is bound, - a binding nothing reads, or one read only once, unless that read sits in a `repeat`
which the spelling it binds draws the same way: `{/word as w}{w}` is `{/word}`, and nested where it is bound: write what it binds there instead, `{/word}` for
`{/region as r}{r.name}` is `{/region.name}`; a calc reads neither a reference nor a `{/word as w}{w}` and `{/region.name}` for `{/region as r}{r.name}`; a calc cannot read a
path, so there one read is not refused; reference or a path, so a calc's one read of a name bound to either is not refused;
- a path through a name that selects a row; - a path through a name that selects a row;
- `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or - `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or
`{w.a} {w.b}`; `{w.a} {w.b}`;
+1 -1
View File
@@ -76,7 +76,7 @@ func checkColumns(s nodeScope) error {
checked[column] = true checked[column] = true
items, _ := columnItems(column) items, _ := columnItems(column)
for _, it := range items { for _, it := range items {
if r := it.link.readsColumn; r != nil { if r := it.link.readsColumn; r != nil && r.category != "" {
if err := check(r.category, r.field, r.column); err != nil { if err := check(r.category, r.field, r.column); err != nil {
return err return err
} }
+18 -17
View File
@@ -128,19 +128,18 @@ Works against goal 2.3 until `todo.md` item 77 ends it.
## Every `{…}` draws afresh, and only a name keeps a pick ## Every `{…}` draws afresh, and only a name keeps a pick
2026-10-05, larv-review on the draw restructure the maintainer planned on 2026-10-03. 2026-10-05, larv-review on the change the maintainer planned on 2026-10-03.
Serves goals 5.2 and 4.1: each `{…}` makes a new pick, and values that belong together come Serves goals 5.2 and 4.1: each `{…}` makes a new pick, and values that belong together come
from the one pick a name keeps. from the one pick a name keeps.
- `{p.a} {p.b}`, `{w} {uppercase(w)}`, `{/p.first} {/p.last}` and `{/p} {/p}` are two draws - `{p.a} {p.b}`, `{w} {uppercase(w)}`, `{/p.first} {/p.last}` and `{/p} {/p}` are two draws
each, and `{p as q}{q.a} {q.b}` is one. Nothing is held per format or per render, so no each, and `{p as q}{q.a} {q.b}` is one. Nothing is kept per format or per render, so the
fence guards a hold: the load checks on draws are names, paths through names, cycles and only load checks on draws are names, paths through names, cycles and the repeat cap.
the repeat cap. - A name binds a reference, or a field of the template binding it or a path into one, `{place as
- A name binds a reference or a path into a field of the template binding it, `{place as
p}`: an inline template, and a choice of rows inside one category, have no reference to p}`: an inline template, and a choice of rows inside one category, have no reference to
bind, and goal 4.1 holds there too. bind, and goal 4.1 holds there too.
- A name read once is refused where the spelling it binds stands in, and the error names - A name read once is refused where writing what it binds in its place draws the same, and
that spelling (goal 5.7): `{/x as n}{n.y}` is `{/x.y}`. A calc reads no reference or the error names that spelling (goal 5.7): `{/x as n}{n.y}` is `{/x.y}`. A calc reads no reference or
path, and a read inside a nested repeat keeps one pick on every line, so neither is path, and a read inside a nested repeat keeps one pick on every line, so neither is
refused. refused.
@@ -299,9 +298,11 @@ not agree on one among themselves; each must only hold that type.
The fields an embedded struct promotes are the struct's own — `e.First`, as The fields an embedded struct promotes are the struct's own — `e.First`, as
`encoding/json` and SQL mappers read them — so they are columns of its record and share `encoding/json` and SQL mappers read them — so they are columns of its record and share
its names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}` (goal 4.1), its names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}` (goal 4.1),
while a path tag draws afresh as `{/path}` does (goal 5.2). A tag binding a name only an while a path tag draws afresh as `{/path}` does (goal 5.2).
embedding type reads is refused on the embedded type alone, as any name read once is, so the
embedder binds it in a tag of its own; a tagged field that another field hides is refused, not dropped. A named A tag binding a name only an embedding type reads is refused on the embedded type alone, as
any name read once is, so the embedder binds it in a tag of its own. A tagged field that
another field hides is refused, not dropped. A named
struct field is another entity and a record of its own. `fake:"-"` leaves a struct struct field is another entity and a record of its own. `fake:"-"` leaves a struct
field, embedded or named, unfilled, so no name may be `-`; a pointer back to a struct field, embedded or named, unfilled, so no name may be `-`; a pointer back to a struct
already being filled is left alone, since filling it would never end. `New` cannot see a already being filled is left alone, since filling it would never end. `New` cannot see a
@@ -316,7 +317,7 @@ handle is needed, as the cache already compiles once.
A category is one unit: a reference back into it — `{/users.first}` inside `users` — A category is one unit: a reference back into it — `{/users.first}` inside `users` —
describes a draw other than the fields beside it, so `New` refuses it and the sibling describes a draw other than the fields beside it, so `New` refuses it and the sibling
field stays the one spelling for a field of one's own. A value two fields share goes in its field stays the one spelling for a field of one's own. A value two fields share goes in its
own category, which each binds to a name (goal 4.1). That settled, a record's column fences own category, which one field binds to a name and both read (goal 4.1). That settled, a record's column fences
run at `New` too, so a category that loads renders as a value and as a record alike. run at `New` too, so a category that loads renders as a value and as a record alike.
## A record's column set is fixed before the first draw ## A record's column set is fixed before the first draw
@@ -345,7 +346,7 @@ Its bounds come from a literal or a call's arguments, so a load error names a re
value, a range check is one comparison, and `1{digits(2)}` is a second spelling of value, a range check is one comparison, and `1{digits(2)}` is a second spelling of
`{int(100,199)}`. `{int(100,199)}`.
## A column of one reference or name read alone is the column it reads ## A column that only reads one reference or name is the column it reads
2026-09-15, Lilleman auf Larv. 2026-09-15, Lilleman auf Larv.
@@ -435,10 +436,10 @@ accepted one changes, so the door stays open for the address records the plan de
Decided 2026-09-27 by the maintainer, for goals 4 and 5; valid while a selector names Decided 2026-09-27 by the maintainer, for goals 4 and 5; valid while a selector names
exactly one row. exactly one row.
Selected rows never conflict: `{/misc.territory[SE].capital} / {/misc.territory[FI].capital}` Each selector reads the row it names: `{/misc.territory[SE].capital} / {/misc.territory[FI].capital}`
renders `Stockholm / Helsinki`, and two items of one choice may select different rows. renders `Stockholm / Helsinki`, and two items of one choice may select different rows.
## `..` steps up to the parent row, and a step down after it draws afresh, or once per pick ## `..` steps up to the parent row, and a step down after it draws afresh, or once per name's pick
2026-10-05, larv-review in PR #158. Serves goal 5.4, which has `..` go up one level, and goal 2026-10-05, larv-review in PR #158. Serves goal 5.4, which has `..` go up one level, and goal
5.2, which has each `{…}` make a new pick. 5.2, which has each `{…}` make a new pick.
@@ -810,7 +811,7 @@ it.
The scope is lexical. A read entering a category, by a reference, through a name or from The scope is lexical. A read entering a category, by a reference, through a name or from
`Fake`, sees no frame (one render of a scope's picks) opened above it, so a category renders `Fake`, sees no frame (one render of a scope's picks) opened above it, so a category renders
the same whoever references it. It opens the frames around where it lands: a read through a the same whoever references it. Such a read opens the frames around where it lands: a read through a
name from its pick's memo, so `{n}` and `{n.path}` read one pick of each name inside, and a name from its pick's memo, so `{n}` and `{n.path}` read one pick of each name inside, and a
reference or `Fake` fresh ones. reference or `Fake` fresh ones.
@@ -820,6 +821,6 @@ category's frames, since the field sits inside it.
Valid while names are read only inside the category binding them. Valid while names are read only inside the category binding them.
## comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them ## comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat, and the hardest units were the draw fences the restructure deleted. The draw restructure, shipped 2026-10-05, and `todo.md` item 90 delete or move what the earlier chunks of the below-floor round touched, so a scoring run before item 90 merges would score code about to go; the calc change accepting a name as an operand shipped unscored on 2026-10-05 for that reason. The two finish the round `AGENTS.md`'s comprehension rule lets merge below the floor, a restructure the maintainer approved. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs. 2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat, and the hardest units were the draw fences that change deleted. The change making every `{…}` draw afresh and `todo.md` item 90 delete or move what the earlier chunks of the below-floor round touched, so a scoring run before item 90 merges would score code about to go; the calc change accepting a name as an operand shipped unscored on 2026-10-05 for that reason. The two finish the round `AGENTS.md`'s comprehension rule lets merge below the floor, a restructure the maintainer approved. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
+1 -1
View File
@@ -316,7 +316,7 @@ func checkBound(ref string, fields map[string]node) error {
} }
missing, err := checkArm(ref, fields, false) missing, err := checkArm(ref, fields, false)
if missing { if missing {
return fmt.Errorf("%w; a name binds a pick of a field, or of a reference, which starts with /, . or ..", err) return fmt.Errorf("%w; a binding names a field, or a reference, which starts with /, . or ..", err)
} }
return err return err
} }
+3 -3
View File
@@ -222,7 +222,7 @@ func (t *template) isName(head string) bool {
return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
} }
// linkBindings gives each binding t's tokens make the head and tail what it binds resolved to. // linkBindings resolves what each of t's bindings binds to a head node and a tail path.
func linkBindings(t *template) { func linkBindings(t *template) {
for _, tok := range t.tokens { for _, tok := range t.tokens {
if tok.kind != nameBind { if tok.kind != nameBind {
@@ -255,8 +255,8 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
// starts: a pass reads what the one before filled in. // starts: a pass reads what the one before filled in.
var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames} var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames}
// linkNames compiles t's reads of a name as paths from the head its binding's reference names, once // linkNames compiles t's reads of a name as paths from the head its binding resolved to, once every
// every template is linked, so each binder has resolved that reference. // template is linked, so each binder has resolved what it binds.
func linkNames(path string, t *template) error { func linkNames(path string, t *template) error {
return namedReads(t, func(o *op, a *arm) error { return namedReads(t, func(o *op, a *arm) error {
if err := linkName(t, a); err != nil { if err := linkName(t, a); err != nil {
+3 -3
View File
@@ -185,7 +185,7 @@ func TestNameErrors(t *testing.T) {
{"an option", map[string]string{"word": `["a","b"]`, "card": `"{/word as format}{format}"`}, {"an option", map[string]string{"word": `["a","b"]`, "card": `"{/word as format}{format}"`},
`"format" is an option and can never be a name`}, `"format" is an option and can never be a name`},
{"no reference or field", map[string]string{"card": `{"format":"{word as w}{w}{w}","words":["a","b"]}`}, {"no reference or field", map[string]string{"card": `{"format":"{word as w}{w}{w}","words":["a","b"]}`},
`no field "word"; a name binds a pick of a field, or of a reference`}, `no field "word"; a binding names a field, or a reference`},
{"read once through a field", map[string]string{"card": `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`}, {"read once through a field", map[string]string{"card": `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`},
`name "p" is read once, so it keeps no pick for another read; write {place.x} where it is read`}, `name "p" is read once, so it keeps no pick for another read; write {place.x} where it is read`},
{"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`}, {"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`},
@@ -380,8 +380,8 @@ func TestOneRenderReadsOneNameAcrossNestedTemplates(t *testing.T) {
func TestAFieldBindingReadInsideItsFieldIsRefused(t *testing.T) { func TestAFieldBindingReadInsideItsFieldIsRefused(t *testing.T) {
f := engine(1) f := engine(1)
for src, want := range map[string]string{ for src, want := range map[string]string{
`{"format":"{x as n}{n}{n}","x":"{n}"}`: `name "n" is read inside "x", the field it binds`, `{"format":"{x as n}{n}{n}","x":"{n}"}`: `name "n" is read inside "x", the field bound to it`,
`{"format":"{x as n}{n.a}{n.a}","x":{"format":"{a}","a":"{n}"}}`: `name "n" is read inside "x", the field it binds`, `{"format":"{x as n}{n.a}{n.a}","x":{"format":"{a}","a":"{n}"}}`: `name "n" is read inside "x", the field bound to it`,
`{"format":"{x as n}{y as m}{n}{n}{m}{m}","x":"{m}","y":"{n}"}`: "cycle", `{"format":"{x as n}{y as m}{n}{n}{m}{m}","x":"{m}","y":"{n}"}`: "cycle",
} { } {
if _, err := f.NewTemplate(src); err == nil || !strings.Contains(err.Error(), want) { if _, err := f.NewTemplate(src); err == nil || !strings.Contains(err.Error(), want) {
+6 -5
View File
@@ -6,8 +6,9 @@ import (
"strings" "strings"
) )
// checkNameReads refuses each binding of t that checkUses refuses, and a read such as {n} whose // checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
// pick renders twice a field another read, {n.w}, reads once: the pick keeps one draw of it. // field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one
// draw of w.
func checkNameReads(path string, t *template) error { func checkNameReads(path string, t *template) error {
for _, tok := range t.tokens { for _, tok := range t.tokens {
if tok.kind != nameBind { if tok.kind != nameBind {
@@ -19,7 +20,7 @@ func checkNameReads(path string, t *template) error {
} }
return namedReads(t, func(o *op, a *arm) error { return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) { if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field it binds; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref) return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
} }
for _, leaf := range a.leaves { for _, leaf := range a.leaves {
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil { if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
@@ -58,8 +59,8 @@ func rendersInside(nodes []node, t *template) bool {
} }
// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling // checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling
// draws the same way without the name. A field stands where the reading template reaches the // draws the same way without the name. A bound field can stand in only where the reading template
// binder through fields. Where the spelling would be a CLI argument or tag of one reference alone, // reaches the binder through fields. Where the spelling would be a CLI argument or tag of one reference alone,
// that entry point's own refusal then names the bare path. // that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses() error { func (b *nameBinding) checkUses() error {
r := b.uses[0] r := b.uses[0]
+2 -2
View File
@@ -183,8 +183,8 @@ func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue
return p.renderAt(s, leaf, pins, key, sc) return p.renderAt(s, leaf, pins, key, sc)
} }
// underKey is the key of path under the pick key prefix. It never returns prefix itself, which a // underKey is the key of path under the pick key prefix. It never returns prefix itself: prefix is
// memo would keep: anything a renderScope holds reaching the heap moves every render's scope there. // sc.pickKey, and a memo keeping it would move every render's scope to the heap.
func underKey(prefix, path string) string { func underKey(prefix, path string) string {
if prefix == "" { if prefix == "" {
return path return path
+25
View File
@@ -307,3 +307,28 @@ func TestAFieldBoundWholeIsItsPickAsAColumn(t *testing.T) {
t.Errorf("place drew only %v in 100 records, want both", seen) t.Errorf("place drew only %v in 100 records, want both", seen)
} }
} }
func TestAColumnReadingABoundColumnIsThatColumn(t *testing.T) {
f := newGenerator(t, writeData(t, map[string]string{
"row": `{"format":"{place as p}","place":[null,{"format":"{int(1,9)}","datatype":"integer"}],"a":"{p}","z":"{p}"}`,
}), WithSeed(1))
nulls := 0
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
c := r.Columns()
for _, col := range c {
if col.DataType != DataTypeInteger || col.Null != c[0].Null || col.Value != c[0].Value {
t.Fatalf("%s: want a, place and z one integer pick, null alike", r.JSON())
}
}
if c[0].Null {
nulls++
}
}
if nulls == 0 || nulls == 100 {
t.Errorf("the pick was null %d times in 100 records, want both outcomes", nulls)
}
}
+4 -1
View File
@@ -269,7 +269,7 @@ func renderRecord(s *generatorState, t *template, columns []Column, sc renderSco
field := t.fields[r.columns[i].Name] field := t.fields[r.columns[i].Name]
var column readValue var column readValue
if b := bindingOfWhole(t, field); b != nil { if b := bindingOfWhole(t, field); b != nil {
column = readName(s, sc, arm{kind: namedRead, named: b, levels: []string{""}}) column = readName(s, sc, arm{kind: namedRead, named: b, levels: wholeLevels})
} else { } else {
column = renderLeaf(s, field, sc) column = renderLeaf(s, field, sc)
} }
@@ -278,6 +278,9 @@ func renderRecord(s *generatorState, t *template, columns []Column, sc renderSco
return r return r
} }
// wholeLevels is the levels of a read of a whole name, as linkName compiles {n}.
var wholeLevels = []string{""}
// bindingOfWhole is the name t binds to the whole of field, whose pick the field's column renders // bindingOfWhole is the name t binds to the whole of field, whose pick the field's column renders
// so the row agrees with the columns reading the name; nil where none does. // so the row agrees with the columns reading the name; nil where none does.
func bindingOfWhole(t *template, field node) *nameBinding { func bindingOfWhole(t *template, field node) *nameBinding {
+13 -8
View File
@@ -155,7 +155,9 @@ func (t *template) resolveLink(folder []string, path, category string, root map[
} }
// columnRead is a record's column read by a format of that one reference or name read alone, // columnRead is a record's column read by a format of that one reference or name read alone,
// which is the column: it takes the column's datatype and null. category and field name it. // which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct { type columnRead struct {
a arm a arm
category, field string category, field string
@@ -170,18 +172,21 @@ func linkColumnRead(_ string, t *template) error {
return nil return nil
} }
a := ops[0].arms[0] a := ops[0].arms[0]
head, category, tail := t.head(a.head), a.head, a.tail head, category, tail := t.head(a.head), a.head[min(1, len(a.head)):], a.tail
switch { switch b := a.named; {
case a.kind == namedRead && !a.named.bindsField(): case a.kind == namedRead && b.bindsField():
ref := a.named.binder.link.refs[a.named.ref] head, category = b.binder, ""
head, category = a.named.head, ref.head tail = append(append([]string{splitArm(b.ref, nil).head}, b.tail...), a.tail...)
case a.kind == namedRead:
ref := b.binder.link.refs[b.ref]
head, category = b.head, ref.head[1:]
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...) tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
case a.kind == namedRead || !isRef(a.head): case !isRef(a.head):
return nil return nil
} }
target, isTemplate := head.(*template) target, isTemplate := head.(*template)
if isTemplate && target.isRecord && len(tail) == 1 { if isTemplate && target.isRecord && len(tail) == 1 {
t.link.readsColumn = &columnRead{a: a, category: category[1:], field: tail[0], column: target.fields[tail[0]]} t.link.readsColumn = &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
} }
return nil return nil
} }
+2 -2
View File
@@ -168,8 +168,8 @@ func readField(s *generatorState, t *template, sc renderScope, a arm) readValue
return r return r
} }
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one // renderLeaf draws and renders what a read lands on: null on a null item, or on a column that only
// reference alone whose read drew null. // reads one reference or name whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue { func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
n = resolveChoice(s, n) n = resolveChoice(s, n)
switch leaf := n.(type) { switch leaf := n.(type) {
+2 -2
View File
@@ -27,8 +27,8 @@ type renderedRow struct {
index int index int
} }
// at is the scope n, the leaf of a path that pinned its rows in pins, renders in: where n is a // at returns the scope n renders in, n being the leaf of a path whose rows sit in pins; where n
// table's row or column, at the row pins holds for that table. // is a table's row or column, the scope carries the row pins holds for that table.
func (sc renderScope) at(n node, pins *pinSet) renderScope { func (sc renderScope) at(n node, pins *pinSet) renderScope {
switch n := n.(type) { switch n := n.(type) {
case *tableRow: case *tableRow:
+7 -7
View File
@@ -72,8 +72,8 @@
| 15 | 0.1.0 | | **Add the remaining `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 the remaining `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 8 | 8 | 2, 15, 14 | 25.9 |
| 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 |
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 | | 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
| 96 | 0.1.0 | question | **Refuse two reads of one sibling, one category or one struct category by different spellings, `{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 | question | **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 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 | | 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel after every `{…}` draws afresh and the code is restructured, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 | | 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
| 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 |
| 90 | 0.1.0 | decision | **Restructure the code into folders named for what they hold: the public API stays in the root package, and the implementation moves into `internal/` packages.** | 6 | 9 | 3 | 9 | 3.2, 3.4 | 13.9 | | 90 | 0.1.0 | decision | **Restructure the code into folders named for what they hold: the public API stays in the root package, and the implementation moves into `internal/` packages.** | 6 | 9 | 3 | 9 | 3.2, 3.4 | 13.9 |
@@ -234,13 +234,13 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
`isin`: Luhn over letters expanded to digits. `aba`: 3-7-1 weights. `vin`: position 9 over the whole; a sample taking the WMI, since the check sits mid-string. `isin`: Luhn over letters expanded to digits. `aba`: 3-7-1 weights. `vin`: position 9 over the whole; a sample taking the WMI, since the check sits mid-string.
### 96. Refuse two reads of one sibling, one category or one struct category by different spellings, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two. ### 96. 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.
Asked of the maintainer by the product-owner pass of item 4's chunk. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. Item 4 planned the draw checks as names, paths through names, cycles and the repeat cap, so the fence would revise the decision "Every `{…}` draws afresh, and only a name keeps a pick". `{w} {w}`, one spelling twice, stays two draws. Open: whether reads across categories, `{/city.name}, {/country.name}`, count. Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. Open: whether reads across categories, `{/city.name}, {/country.name}`, count.
### 5. Run the nine-seat comprehension panel after the draw restructure, and file what it names. ### 5. Run the nine-seat comprehension panel after every `{…}` draws afresh and the code is restructured, and file what it names.
Needs item 90. The decision "comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then. Needs item 90. The decision "comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then.
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints. ### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
@@ -301,7 +301,7 @@ With several `--data-path` layers the author has to work out which directory won
### 28. Accept a middle name, and draw a `personnummer` whose sex matches a sex selected through a named pick. ### 28. Accept a middle name, and draw a `personnummer` whose sex matches a sex selected through a named pick.
Today `personnummer` draws its own sex, so a person bound to `{/sv_SE.sex[f] as s}` needs the template the README's Data section shows to agree with a number. Today `personnummer` draws its own sex, so a name and a number agree only through the README Data section's template, which reads both through one bound `sex`.
### 24. Stop `misc.territory[EH].tld` rendering `.eh`, the one shipped TLD `misc.tld` does not hold. ### 24. Stop `misc.territory[EH].tld` rendering `.eh`, the one shipped TLD `misc.tld` does not hold.
+1 -1
View File
@@ -29,7 +29,7 @@ type valueProof struct {
// checkDatatype rejects a typed column item some render of which is not text of its datatype, // checkDatatype rejects a typed column item some render of which is not text of its datatype,
// and one restating the datatype of the column it is. // and one restating the datatype of the column it is.
// docs/decisions.md#a-column-of-one-reference-alone-is-the-column-it-reads // docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads
func (p *valueProof) checkDatatype(path string, n node) error { func (p *valueProof) checkDatatype(path string, n node) error {
t, ok := n.(*template) t, ok := n.(*template)
if !ok || t.datatype == DataTypeString { if !ok || t.datatype == DataTypeString {