From 8d827595202a84f273197a0408f09640f44724eb Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 09:54:38 +0200 Subject: [PATCH] Answer stability round 1 and product-owner round 1: refuse a field binding read once from above its binder, word the self-read refusal plainly, say how a bound field's column keeps the pick, and ask item 96 --- CHANGELOG.md | 4 +--- README.md | 7 ++++--- docs/decisions.md | 4 +++- namefence.go | 32 +++++++++++++++++++++++++++----- pick_test.go | 1 + todo.md | 7 ++++++- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ad6b8..3f52532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,7 @@ replacement, and each removed path, column or flag. `{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}` binds a sibling field as well as a reference, and a struct's tags share its names, - `{/sv_SE.person as p}{p.first}` beside `{p.sex}`. `drawGroup` is no longer an option, and `New` no longer - refuses a read beside a path into what it reads, or reads of one table family that - could disagree. + `{/sv_SE.person as p}{p.first}` beside `{p.sex}`. - 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 `key`, `name`, `weight` and `parent`; a path selects a row by key or name, diff --git a/README.md b/README.md index 612a022..6c5f681 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,9 @@ it as nothing. 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 -currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. +currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. A +field a name binds draws afresh as a column of its own, so write the column through the name, +`"where": "{p}"` beside `{place as p}`, for it to agree with the columns reading `p`. 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 @@ -754,8 +756,7 @@ Refused at `New`, each error naming what to write instead: and outside it; - 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 read of a name inside the field it binds, which would render itself: read it outside the - field; +- a read of a name inside the field it binds: read it outside the field; - 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, which the spelling it binds draws the same way: `{/word as w}{w}` is `{/word}`, and diff --git a/docs/decisions.md b/docs/decisions.md index b3dc118..a98d6f0 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -299,7 +299,9 @@ 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 `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), -while a path tag draws afresh as `{/path}` does (goal 5.2); a tagged field that another field hides is refused, not dropped. A named +while a path tag draws afresh as `{/path}` does (goal 5.2). 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 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 diff --git a/namefence.go b/namefence.go index 004d853..7dae4d3 100644 --- a/namefence.go +++ b/namefence.go @@ -19,7 +19,7 @@ func checkNameReads(path string, t *template) error { } return namedReads(t, func(o *op, a *arm) error { 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, which would render itself; 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 it binds; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref) } for _, leaf := range a.leaves { if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil { @@ -58,15 +58,22 @@ func rendersInside(nodes []node, t *template) bool { } // 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 only beside its binding. 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. +// draws the same way without the name. A field stands where the reading template 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. func (b *nameBinding) checkUses() error { r := b.uses[0] - if len(b.uses) > 1 || r.nested || b.bindsField() && r.in != b.binder { + if len(b.uses) > 1 || r.nested { return nil } spelling := b.ref + if b.bindsField() { + down, reaches := fieldPathTo(r.in, b.binder) + if !reaches { + return nil + } + spelling = joinSegments(append(down, b.ref)) + } switch { case strings.HasPrefix(r.tail, ".."): spelling += r.tail @@ -82,6 +89,21 @@ func (b *nameBinding) checkUses() error { return fmt.Errorf("name %q is read once, so it keeps no pick for another read; write %s where it is read, and drop the token", b.name, spelling) } +// fieldPathTo is the fields leading from t down to the template target, if t reaches it so. +func fieldPathTo(t, target *template) ([]string, bool) { + if t == target { + return nil, true + } + for _, name := range sortedNames(t.fields) { + if sub, isTemplate := t.fields[name].(*template); isTemplate { + if rest, reaches := fieldPathTo(sub, target); reaches { + return append([]string{name}, rest...), true + } + } + } + return nil, false +} + // calcReads reports whether a calc reads spelling as one operand. func calcReads(spelling string) bool { n, err := parseCalc(spelling) diff --git a/pick_test.go b/pick_test.go index f7ac197..b918105 100644 --- a/pick_test.go +++ b/pick_test.go @@ -120,6 +120,7 @@ func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) { `{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`, `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `write {place.x} where it is read`, `{"format":"{place as p}{p}","place":["x","y"]}`: `write {place} where it is read`, + `{"format":"{n}|{x}","x":{"format":"{a as n}{a}","a":["1","2"]}}`: `write {x.a} where it is read`, `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`, `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, } { diff --git a/todo.md b/todo.md index 6aacf6f..b85d3a1 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 96` +`Next ID = 97` | Goal | W | |---|---| @@ -72,6 +72,7 @@ | 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 | | 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 | | 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 | | 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 | @@ -233,6 +234,10 @@ 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. +### 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. + +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. + ### 5. Run the nine-seat comprehension panel after the draw restructure, 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.