Keep a name's frame with the read entering its category, refuse a binding in a choice's item, and read a name as a transform operand

This commit is contained in:
2026-10-03 02:05:02 +02:00
committed by Lillem4n
parent 40e7ab46f6
commit d0c477ec78
11 changed files with 198 additions and 130 deletions
+6 -5
View File
@@ -11,11 +11,12 @@ replacement, and each removed path, column or flag.
- A name keeps one pick: `{/misc.territory as t}` binds one row of `misc.territory` to `t`
and prints nothing, and `{t}`, `{t.capital}` and `{t.calling-code}` read that one row.
A name lives in the category binding it, so every field and record column of the
category reads one pick, and a name a `repeat` binds picks again on every iteration.
Refused at `New`: a name bound twice in one category, or inside a `repeat` and outside
it; a name that is a field or an option; a binding of anything but a reference; a
binding nothing reads; a path through a name that selects a row; and a binding in a
table. A field, column, category or folder name may not contain ` as `.
category reads one pick, a transform reads it as `{lowercase(t.name)}`, and a name a
`repeat` binds picks again on every iteration. Refused at `New`: a name bound twice in
one category, or inside a `repeat` and outside it; a name that is a field or an option;
a binding of anything but a reference; a binding in a choice's item; a binding nothing
reads; a path through a name that selects a row; and a binding in a table. A field,
column, category or folder name may not contain ` as `.
- 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,
+9 -6
View File
@@ -656,7 +656,7 @@ fail, except in a [typed column](#datatype), which must prove neither happens.
### Transforms
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
field, a path or a `..path` — and nest. `ascii` folds Latin letters (`Åsa Öberg`
field, a path, a `..path` or a [name](#names) — and nest. `ascii` folds Latin letters (`Åsa Öberg`
→ `Asa Oberg`) and drops any other non-ASCII rune. `x` is held
([One draw, one spelling](#one-draw-one-spelling)), so an email built from a name
matches the name beside it:
@@ -709,17 +709,20 @@ row:
```
Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. Everything under a name is drawn
once, so `{n}` and `{n.path}` agree, and a field `n` renders twice prints the same text twice.
once, so `{n}` and `{n.path}` agree, a [transform](#transforms) of `n.path` reads the same
value, and a field `n` renders twice prints the same text twice. A name is a pick of its own:
`{t.name}` and `{/misc.territory.name}` beside it are two draws.
A name is drawn on its first read, and lives in the category binding it: any field of the
category reads it, and a record's columns read one pick. Each render of the category picks
anew, and so does each bare reference to it. A name a `repeat` binds picks again on every
iteration, and one bound outside the repeat keeps its pick on every line.
Rejected at load: a name bound twice in one category, or both inside a `repeat` and outside
it; a name that is a field or an option; a binding of anything but a reference; a binding
nothing reads; a path through a name that selects a row, which the reference selects instead;
and a binding in a table's format or cell. A field, column, category or folder name may not
contain ` as `.
it; a name that is a field or an option; a binding of anything but a reference; a binding in
a choice's item, which every other item would leave unbound; a binding nothing reads; a path
through a name that selects a row, which the reference selects instead; and a binding in a
table's format or cell. A field, column, category or folder name may not contain ` as `.
### Draw group
+10 -7
View File
@@ -894,10 +894,13 @@ a struct's fields as one.
2026-10-03, larv-review. Goals 5.3 and 5.6.
`bindNames` scopes a name at compile, and a render keeps one `pickFrame` per scope. One
field binds a name where another reads it, `"first": "{/person as p}{p.first}"` beside
`"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category is the smallest unit holding every field and record column. The
scope is lexical: a category referencing another never sees its names, so what a category
renders never depends on who references it. A pick is drawn on its first read, so a record's
columns, rendered in name order, read one pick whichever binds it. A read entering a category
past its root, `Fake("cat.field")` or `{/cat.field}`, keeps the category's frame in the memo of
that read: per render and group, as the read's own draw is. Valid while a name is read only
inside the category binding it.
`"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category
is the smallest unit holding every field and record column. A choice's item binds no name,
since a read outside the item would see a binding another item leaves out. The scope is
lexical: a read entering a category, by a reference or through a name, sees none of the
frames rendering above it, so what a category renders never depends on who references it. A
pick is drawn on its first read, so a record's columns, rendered in name order, read one pick
whichever binds it. A read entering a category keeps the frame of each scope it enters in its
own memo, and the category's root renders that frame when the read lands on the root, so
`{n}` and `{n.path}` read one pick: per render and group for a reference path, per pick for a
path through a name, and per render for `Fake("cat.field")`.
+5 -1
View File
@@ -45,7 +45,11 @@ type renderScope struct {
draws *renderDraws
group string
row renderedRow
entry *drawMemo // the memo of the read that entered what renders
// entry is the memo of the read that entered the category rendering, nil where none did;
// base, the depth of the frame stack when it entered, below which no frame is the
// category's.
entry *drawMemo
base int
// pick is the named pick what renders is part of, at pickKey, its path from the name; nil
// outside one.
pick *namedPick
+13 -3
View File
@@ -247,7 +247,12 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
switch t.kind {
case builtinCall:
return nil, checkFunc(t, fields)
err := checkFunc(t, fields)
var miss *fieldMiss
if errors.As(err, &miss) {
return []unboundRead{{miss.head, err}}, nil
}
return nil, err
case nameBind:
return nil, checkBind(t)
case nameRead:
@@ -493,10 +498,15 @@ func (c *formatOps) holdName(a arm, label string) {
c.held[a.head] = h
}
func (c *formatOps) function(tok formatToken, refs map[string]refBinding) {
func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName func(string) bool) {
var operands []arm
for _, operand := range tok.names {
a := splitArm(operand, refs)
if isName(a.head) {
a.kind = namedRead
operands = append(operands, a)
continue
}
c.holdName(a, fmt.Sprintf("%s operand %q", tok.fn, operand))
operands = append(operands, a)
}
@@ -528,7 +538,7 @@ func compileFormat(toks []formatToken, refs map[string]refBinding, isName func(h
c.grow += len(tok.lit)
c.ops = append(c.ops, op{formatToken: tok})
case builtinCall:
c.function(tok, refs)
c.function(tok, refs, isName)
case nameRead:
c.field(tok, refs, isName)
}
+113 -73
View File
@@ -40,34 +40,38 @@ func (sc *nameScope) lookup(name string) *nameBinding {
}
// bindNames gives every template of a compiled category or inline template the scope its
// names live in, and refuses a name bound twice along one chain of scopes, a name a field
// spells too, a read no field or name answers, and a binding nothing reads.
// names live in, and refuses a name bound twice along one chain of scopes, a binding in a
// choice's item, a name a field spells too, a read no field or name answers, and a binding
// nothing reads.
func bindNames(root node) error {
var scopes []*nameScope
var gather func(n node, scope *nameScope, where string) error
gather = func(n node, scope *nameScope, where string) error {
top := &nameScope{owner: root}
scopes := []*nameScope{top}
var gather func(n node, scope *nameScope, inChoice bool, where string) error
gather = func(n node, scope *nameScope, inChoice bool, where string) error {
if t, isTemplate := n.(*template); isTemplate {
if t.repeat > 1 && n != root {
scope = &nameScope{up: scope, owner: t}
scopes = append(scopes, scope)
inChoice = false
}
t.scope = scope
for _, tok := range t.tokens {
if tok.kind != nameBind {
continue
}
if err := scope.bind(tok, t, where); err != nil {
return err
}
if err := scope.bindAll(t, inChoice, where); err != nil {
return err
}
}
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, where) })
_, isChoice := n.(*choice)
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) })
}
top := &nameScope{owner: root}
scopes = append(scopes, top)
if err := gather(root, top, ""); err != nil {
if err := gather(root, top, false, ""); err != nil {
return err
}
for _, sc := range scopes[1:] {
for _, b := range sc.order {
if outer := sc.up.lookup(b.name); outer != nil {
return fmt.Errorf("%stoken {%s}: name %q is bound outside this repeat already, by {%s}; rename one", b.where, b.body, b.name, outer.body)
}
}
}
if err := resolveReads(root, ""); err != nil {
return err
}
@@ -79,6 +83,37 @@ func bindNames(root node) error {
return nil
}
// bindAll binds every name t's tokens bind. A choice's item binds none: a read outside the item
// would find the name bound by an item that may never be drawn.
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
for _, tok := range t.tokens {
if tok.kind != nameBind {
continue
}
if inChoice {
return fmt.Errorf("%stoken {%s}: a choice's item binds no name, since every other item would leave it unbound; bind it outside the choice", where, tok.body)
}
if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body)
}
b := &nameBinding{name: tok.bound, ref: tok.names[0], body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t}
if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{}
}
sc.bindings[b.name] = b
sc.order = append(sc.order, b)
}
return nil
}
// spelled names the scope in an error.
func (sc *nameScope) spelled() string {
if sc.up == nil {
return "outside any repeat"
}
return "in one repeat"
}
// close refuses a binding nothing reads, and gives the owner of a scope binding names the scope.
func (sc *nameScope) close() error {
for _, b := range sc.order {
@@ -86,14 +121,8 @@ func (sc *nameScope) close() error {
return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name)
}
}
if len(sc.order) == 0 {
return nil
}
switch o := sc.owner.(type) {
case *template:
o.ownScope = sc
case *choice:
o.ownScope = sc
if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 {
t.ownScope = sc
}
return nil
}
@@ -113,22 +142,6 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
return nil
}
func (sc *nameScope) bind(tok formatToken, binder *template, where string) error {
if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice in one scope, by {%s} too; rename one", where, tok.body, tok.bound, b.body)
}
if b := sc.up.lookup(tok.bound); b != nil {
return fmt.Errorf("%stoken {%s}: name %q is bound outside this repeat already, by {%s}; rename one", where, tok.body, tok.bound, b.body)
}
b := &nameBinding{name: tok.bound, ref: tok.names[0], body: tok.body, where: where, scope: sc, index: len(sc.order), binder: binder}
if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{}
}
sc.bindings[b.name] = b
sc.order = append(sc.order, b)
return nil
}
// resolveReads answers each unbound read under n with a name its template sees, fields before the
// format as compile reports them, and refuses a field spelling a name.
func resolveReads(n node, where string) error {
@@ -141,7 +154,11 @@ func resolveReads(n node, where string) error {
}
for _, name := range sortedNames(t.fields) {
if b := t.scope.lookup(name); b != nil {
return fmt.Errorf("%stoken {%s}: name %q is a field too; options, fields and names share one namespace, so rename one", b.where, b.body, name)
holder := "the root template"
if where != "" {
holder = strings.TrimSuffix(where, ": ")
}
return fmt.Errorf("%stoken {%s}: name %q is a field of %s too; options, fields and names share one namespace, so rename one", b.where, b.body, name, holder)
}
}
for _, u := range t.unbound {
@@ -163,13 +180,15 @@ func (t *template) isName(head string) bool {
// every template is linked, so the binder has resolved that reference.
func linkNames(path string, t *template) error {
for i := range t.compiled.ops {
for j := range t.compiled.ops[i].arms {
a := &t.compiled.ops[i].arms[j]
if a.kind != namedRead {
continue
}
if err := linkName(t, a); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), a.spelling, err)
o := &t.compiled.ops[i]
for _, reads := range [][]arm{o.arms, o.operands} {
for j := range reads {
if reads[j].kind != namedRead {
continue
}
if err := linkName(t, &reads[j]); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
}
}
}
}
@@ -223,23 +242,58 @@ type frameStack struct {
frames []*pickFrame
}
// pushFrame opens a frame of scope until popFrame closes it, returning the mark popFrame takes.
func (d *renderDraws) pushFrame(scope *nameScope) int {
// pushFrame opens f until popFrame closes it, returning the mark popFrame takes.
func (d *renderDraws) pushFrame(f *pickFrame) int {
mark := d.depth()
if d.frames == nil {
d.frames = &frameStack{}
}
mark := len(d.frames.frames)
d.frames.frames = append(d.frames.frames, newPickFrame(scope))
d.frames.frames = append(d.frames.frames, f)
return mark
}
func (d *renderDraws) popFrame(mark int) { d.frames.frames = d.frames.frames[:mark] }
// pickOf is b's pick in the frame rendering its scope. A read entering the scope past its owner,
// as Fake("cat.field") or {/cat.field} do, keeps the frame in the memo of that read.
func (d *renderDraws) depth() int {
if d.frames == nil {
return 0
}
return len(d.frames.frames)
}
// entering is sc as a read entering another category sees it: none of the frames rendering, and
// memo, which keeps the frame of each scope the read enters past its owner.
func (sc renderScope) entering(memo *drawMemo) renderScope {
sc.base, sc.entry, sc.pick = sc.draws.depth(), memo, nil
return sc
}
// frameOf is the frame a render of scope's owner opens: the one the read entering the category
// keeps, so {n} and {n.path} read one frame, else a fresh one.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
if sc.entry == nil {
return newPickFrame(scope)
}
return sc.entry.frameOf(scope)
}
func (m *drawMemo) frameOf(scope *nameScope) *pickFrame {
f, ok := m.frames[scope]
if !ok {
f = newPickFrame(scope)
if m.frames == nil {
m.frames = map[*nameScope]*pickFrame{}
}
m.frames[scope] = f
}
return f
}
// pickOf is b's pick: in the frame rendering its scope since the read entering the category, else
// in the frame that read keeps, as for Fake("cat.field") or {/cat.field}.
func (sc renderScope) pickOf(b *nameBinding) *namedPick {
if stack := sc.draws.frames; stack != nil {
for i := len(stack.frames) - 1; i >= 0; i-- {
for i := len(stack.frames) - 1; i >= sc.base; i-- {
if f := stack.frames[i]; f.scope == b.scope {
return &f.picks[b.index]
}
@@ -249,19 +303,12 @@ func (sc renderScope) pickOf(b *nameBinding) *namedPick {
if memo == nil {
memo = &sc.groupDraws().memo
}
f, ok := memo.frames[b.scope]
if !ok {
f = newPickFrame(b.scope)
if memo.frames == nil {
memo.frames = map[*nameScope]*pickFrame{}
}
memo.frames[b.scope] = f
}
return &f.picks[b.index]
return &memo.frameOf(b.scope).picks[b.index]
}
func readName(s *generatorState, sc renderScope, a arm) readValue {
return readPicked(s, sc.pickOf(a.bind), a.bind.head, a.steps, a.levels, a.path, sc)
p := sc.pickOf(a.bind)
return readPicked(s, p, a.bind.head, a.steps, a.levels, a.path, sc.entering(&p.memo))
}
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
@@ -297,19 +344,12 @@ func readPicked(s *generatorState, p *namedPick, head node, steps []pathStep, le
return r
}
leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
mark := -1
if c, isChoice := leaf.(*choice); isChoice {
if c.ownScope != nil {
mark = sc.draws.pushFrame(c.ownScope)
}
leaf = p.memo.variantOf(s, c, key)
}
sc = sc.at(leaf, &p.pins)
sc.pick, sc.pickKey, sc.entry = p, key, &p.memo
sc.pick, sc.pickKey = p, key
r := renderLeaf(s, leaf, sc)
if mark >= 0 {
sc.draws.popFrame(mark)
}
if p.memo.value == nil {
p.memo.value = map[string]readValue{}
}
+3 -4
View File
@@ -25,10 +25,9 @@ func (*folder) isNode() {}
// relative dot paths every item can address, so carriedByAll and List both read the one
// answer to what a path may reach through this choice.
type choice struct {
items []node
cum []float64
shared map[string]bool
ownScope *nameScope // set on a category's root binding names, by bindNames
items []node
cum []float64
shared map[string]bool
}
func (*choice) isNode() {}
+1 -2
View File
@@ -264,8 +264,7 @@ func recordOf(n node) (*template, []Column, error) {
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t)
if t.ownScope != nil {
mark := sc.draws.pushFrame(t.ownScope)
defer sc.draws.popFrame(mark)
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(t.ownScope)))
}
if t.site.isFormat() {
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
+3 -1
View File
@@ -100,7 +100,9 @@ func compileArms(t *template) {
for i := range t.compiled.ops {
o := &t.compiled.ops[i]
for j := range o.operands {
compileArm(t, &o.operands[j])
if o.operands[j].kind != namedRead {
compileArm(t, &o.operands[j])
}
}
for j := range o.arms {
if o.arms[j].kind != namedRead {
+28 -26
View File
@@ -61,10 +61,6 @@ func renderOnce(s *generatorState, n node) string {
func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) {
case *choice:
if n.ownScope != nil {
mark := sc.draws.pushFrame(n.ownScope)
defer sc.draws.popFrame(mark)
}
return render(s, pick(s, n), sc)
case *nullItem:
return ""
@@ -83,38 +79,48 @@ func render(s *generatorState, n node, sc renderScope) string {
sc = sc.in(n)
if n.repeat == 1 {
if n.ownScope != nil {
mark := sc.draws.pushFrame(n.ownScope)
defer sc.draws.popFrame(mark)
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(n.ownScope)))
}
if lit, fixed := n.fixedText(); fixed {
return lit
}
return expand(s, n, sc)
}
var b strings.Builder
b.Grow(n.repeat * (n.compiled.grow + len(n.separator)))
for i := 0; i < n.repeat; i++ {
if i > 0 {
b.WriteString(n.separator)
}
b.WriteString(expandAnew(s, n, sc.draws.frames))
}
return b.String()
return renderRepeat(s, n, sc)
default:
panic(internalError("uncompiled node %T", n))
}
}
// renderRepeat renders each iteration of t as a render of its own, inside the name scopes
// rendering t, where a name bound outside t keeps its pick: a scope sc's category was entered
// past keeps its frame in that read's memo, else in sc's render's.
func renderRepeat(s *generatorState, t *template, sc renderScope) string {
entry := sc.entry
if entry == nil {
entry = &sc.groupDraws().memo
}
var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
for i := 0; i < t.repeat; i++ {
if i > 0 {
b.WriteString(t.separator)
}
b.WriteString(expandAnew(s, t, sc.draws.frames, sc.base, entry))
}
return b.String()
}
// expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the
// name scopes of frames. Inlined into render's loop, its draws would move to the heap.
// name scopes of frames from base. Inlined into render's loop, its draws would move to the heap.
//
//go:noinline
func expandAnew(s *generatorState, t *template, frames *frameStack) string {
func expandAnew(s *generatorState, t *template, frames *frameStack, base int, entry *drawMemo) string {
draws := renderDraws{frames: frames}
if t.ownScope != nil {
defer draws.popFrame(draws.pushFrame(t.ownScope))
defer draws.popFrame(draws.pushFrame(newPickFrame(t.ownScope)))
}
return expand(s, t, renderScope{draws: &draws})
return expand(s, t, renderScope{draws: &draws, base: base, entry: entry})
}
// pick selects one item. Uniform choices are O(1); weighted choices are an
@@ -180,8 +186,9 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
return readName(s, sc, a)
case sc.pick != nil && !isRef(a.head):
return readUnder(s, t, sc, a)
case a.kind == freshRead && isRef(a.head):
return readValue{text: render(s, t.head(a.head), sc.entering(nil))}
case a.kind == freshRead:
sc.pick = nil
return readValue{text: render(s, t.head(a.head), sc)}
}
return readMemo(s, t, hold, nil, sc, a)
@@ -203,9 +210,8 @@ func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc r
}
leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels)
if pins != nil {
sc = sc.at(leaf, pins)
sc = sc.at(leaf, pins).entering(memo)
}
sc.pick, sc.entry = nil, memo
r := renderLeaf(s, leaf, sc)
if memo.value == nil {
memo.value = map[string]readValue{}
@@ -228,10 +234,6 @@ func traceRead(trace renderTrace, t *template, sc renderScope, a arm) {
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
// reference alone whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
if c, isChoice := n.(*choice); isChoice && c.ownScope != nil {
mark := sc.draws.pushFrame(c.ownScope)
defer sc.draws.popFrame(mark)
}
n = resolveChoice(s, n)
switch leaf := n.(type) {
case *nullItem:
+7 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 71`
`Next ID = 72`
| Goal | W |
|---|---|
@@ -65,6 +65,7 @@
| 12 | 0.1.0 | defect | **Strip the whitespace `data-import/territory.py` copies from its register.** | 1 | 1 | 5 | 7 | 4 | 20.6 |
| 5 | 0.1.0 | | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 6 | 0.1.0 | question | **Decide how the comprehension gate treats panel noise: identical code scored 5.7 and 5.4.** | 1 | 1 | 3 | 8 | 3 | 19.1 |
| 71 | 0.1.0 | | **Accept a name as a `calc` operand, so `{calc(n * 2)}` computes from the pick `{n}` prints.** | 3 | 4 | 5 | 7 | 5.3 | 17.2 |
| 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 |
| 36 | 0.2.0 | | **Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states.** | 5 | 6 | 7 | 8 | 6 | 19.5 |
| 41 | 0.2.0 | | **Key every table by one key column, as the decision "A table selects by one key column, a code, never a free-form name" states.** | 6 | 7 | 7 | 8 | 5.7 | 19.1 |
@@ -194,7 +195,7 @@ Shape: T = table, t = template, c = choice.
### 4. Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly.
- Needs items 2 and 3. After it, `{/person} ({/person.first})` renders two people, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load.
- Needs items 2, 3 and 71. After it, `{/person} ({/person.first})` renders two people, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load.
- Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap.
- Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render.
- Rewrites the README's References, Draw group, Correlated fields, Linked tables and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides.
@@ -251,6 +252,10 @@ Two nine-seat runs on `5cb919e`, same briefs, same model, 2026-09-30:
Both runs named the same hardest units, `drawsApart` and `readFold.reads`. Run 1 is the recorded score; run 2 reran the same code to measure noise. A spread of 0.3 is close to the whole 5.7–6.1 range the recorded scores span since 2026-09-20, so `AGENTS.md`'s "no merge lowers the last score" can block or pass a chunk by chance. Until this item is decided, that rule compares against 5.7, from 2026-09-30.
### 71. Accept a name as a `calc` operand, so `{calc(n * 2)}` computes from the pick `{n}` prints.
`checkCalc` and the value proof read an operand as a sibling field, so `{calc(n * 2)}` is refused with `no field "n"` today. Item 4 stops an operand holding its field for the expansion, after which a name is the one way to show the operand a calc computes from, as the README's Computation example does.
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged.