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
20 changed files with 202 additions and 227 deletions
Showing only changes of commit 0002efb9d4 - Show all commits
-1
View File
@@ -3,7 +3,6 @@ FROM golang:${GO_VERSION} AS portable
WORKDIR /app WORKDIR /app
# Module layer cached separately from source.
COPY go.mod ./ COPY go.mod ./
RUN go mod download RUN go mod download
+5 -5
View File
@@ -987,11 +987,11 @@ shipped.go the embedded data set, and loading a shipped category on the fir
shippedindex.go the index of shipped categories, generated from data/ shippedindex.go the index of shipped categories, generated from data/
node.go the node model and JSON -> node compilation node.go the node model and JSON -> node compilation
table.go tables: their options, format and cells, the link to the parent beside them, and how a path passes and draws a table table.go tables: their options, format and cells, the link to the parent beside them, and how a path passes and draws a table
name.go names: {x as n} bindings, the scopes they live in, the reads they answer, and each binding's target and the keys its reads address name.go names: {x as n} bindings, the scopes they live in, the reads they answer, and each binding's target
namefence.go the load fences over names namefence.go the load fences over names
pick.go a render's named picks: the frame of each name scope rendering, the memo a pick keeps, and the reads kept under it pick.go a render's named picks: the key of each level a pick keeps a draw at, built at load and read at render, the frame of each name scope rendering, the memo a pick keeps, and the reads kept under it
env.go renderEnv: what a render reads names and rows through, the stack of frames it renders in, and the frames a read opens and finds env.go renderEnv: what a render reads names and rows through, the stack of frames it renders in, and the frames a read opens and finds
path.go the dotted-path walks, and proving a path reaches a node path.go the dotted-path walks, proving a path reaches a node, and the sub-paths every variant of a choice carries
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype
@@ -1001,9 +1001,9 @@ arm.go the compiled read of one {a|b} alternative or operand: its head,
reference.go one reference's resolution: its sigil, its category, its tail reference.go one reference's resolution: its sigil, its category, its tail
resolve.go resolving templates against the assembled tree in steps, each over every template before the next resolve.go resolving templates against the assembled tree in steps, each over every template before the next
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
sorted.go sortedNames: a map's keys, sorted sorted.go sortedNames, a map's keys sorted, and join, two dotted paths joined
calc.go the {calc()} operand checks: never a number, and a constant zero divisor calc.go the {calc()} operand checks: never a number, and a constant zero divisor
datatype.go column datatypes: DataType, a template's declared datatype, a column's datatype datatype.go column datatypes: DataType, a template's declared datatype, a column's datatype, and a format that only reads a column
valueproof.go the value proof: what a typed column, calc operand or struct field holds, checked at load valueproof.go the value proof: what a typed column, calc operand or struct field holds, checked at load
data.go the load path: the categories internal/datafiles hands over, compiled into one merged tree, and the load pipeline every entry point runs data.go the load path: the categories internal/datafiles hands over, compiled into one merged tree, and the load pipeline every entry point runs
cmd/fejkdata/ the fejkdata CLI cmd/fejkdata/ the fejkdata CLI
-9
View File
@@ -54,15 +54,6 @@ func pathArm(name, head string, segs []string) arm {
return arm{spelling: name, head: head, tail: segs, levels: levelKeys(append([]string{head}, segs...), 1)} return arm{spelling: name, head: head, tail: segs, levels: levelKeys(append([]string{head}, segs...), 1)}
} }
// levelKeys is the key of each prefix of path holding at least from segments, shortest first.
func levelKeys(path []string, from int) []pickKey {
levels := make([]pickKey, len(path)-from+1)
for i := range levels {
levels[i] = pickKey(grammar.JoinSegments(path[:from+i]))
}
return levels
}
// key is the key of the leaf a lands on, the one key every way of writing this read shares. // key is the key of the leaf a lands on, the one key every way of writing this read shares.
func (a arm) key() pickKey { return a.levels[len(a.levels)-1] } func (a arm) key() pickKey { return a.levels[len(a.levels)-1] }
-1
View File
@@ -47,7 +47,6 @@ services:
<<: *go <<: *go
command: go mod tidy command: go mod tidy
# Interactive shell for ad-hoc work: docker compose run --rm dev
dev: dev:
<<: *go <<: *go
command: sh command: sh
+42
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/larvit/fejkdata/internal/datatype" "github.com/larvit/fejkdata/internal/datatype"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant" "github.com/larvit/fejkdata/internal/invariant"
) )
@@ -212,3 +213,44 @@ func typedAs(t *template, d DataType) string {
} }
return fmt.Sprintf(`give %q "datatype": %q`, t.format, d) return fmt.Sprintf(`give %q "datatype": %q`, t.format, d)
} }
// columnRead is a record's column read by a format that only reads one reference or name,
// 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 {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not or t declares a string, which reads the column as text.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.datatype != nil && *t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
var start node
var category string
var tail []string
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
// {f as n}{n}: column f of the record binding n, in t's own category.
start, tail = b.binder, append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
// {/c as n}{n.x} or {/c.x as n}{n}: column x of category c.
start, category = targets[b].start, categoryOf(b.binder.refs.byName[b.ref].head)
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case grammar.IsRef(a.head):
// {/c.x}: column x of category c.
start, category, tail = t.startOf(a.head), categoryOf(a.head), a.tail
default:
return nil
}
if target, isTemplate := start.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+3 -14
View File
@@ -29,20 +29,9 @@ package fejkdata
// `namedPick`, `pickFrame`. // `namedPick`, `pickFrame`.
// - memo — what a named pick keeps its draws in, the variant drawn at each level // - memo — what a named pick keeps its draws in, the variant drawn at each level
// and the value each path read: `drawMemo`. // and the value each path read: `drawMemo`.
// - level — a prefix of a read's path, from where it starts to its leaf; a choice and the // - level — a prefix of a read's path, from where it starts to its leaf: `pickKey`.
// variant drawn from it, or a table and its row, share one. `arm.levels` holds one pick // - pick key — the key a memo keeps a level's draw under: `pickKey`.
// key per level. // - addressed key — a pick key some read of a name lands on or passes: `pickKey`.
// - pick key — the key a memo keeps a level's draw under, the level's path of segments:
// `pickKey`, `levelKeys`.
// - addressed key — a pick key some read of a name lands on or passes, from the level the
// name's binding lands on onward: `addressedKeys`. A fresh read rendering in a pick is
// kept in it only where its first level is addressed: `renderEnv.keeps`, `readUnder`.
// In data/geo/SE/address.json, {.locality as l} binds l, and {l.street.name} passes the
// levels "", "street" and "street.name", the last its key. With {l.name} and
// {l.postal-code.code}, l's addressed keys are those three, "name", "postal-code" and
// "postal-code.code". Where p binds a category whose format reads {first}, and both {p}
// and {p.first} are read, the {first} that {p} renders has the key "first" under {p}'s
// key "". {p.first} addresses "first", so both read one draw.
// - pin — fixing which row of a table one path, or one named pick, uses: // - pin — fixing which row of a table one path, or one named pick, uses:
// `pinSet`, `namedPick.pins`, `table.followStep`. // `pinSet`, `namedPick.pins`, `table.followStep`.
// - link — a table's tie to its parent table, each row to its parent row: // - link — a table's tie to its parent table, each row to its parent row:
-6
View File
@@ -111,9 +111,3 @@ func (env renderEnv) drawRowOf(s *drawstate.State, t *table) int {
} }
return t.rows.Draw(s) return t.rows.Draw(s)
} }
// keeps reports whether a read of env's name addresses the level a starts at.
func (env renderEnv) keeps(a arm) bool {
_, kept := env.pick.named.addressed[env.pickAt.under(a.levels[0])]
return kept
}
+4 -2
View File
@@ -17,7 +17,8 @@ import (
// any one path; the CLI's --repeat shares it. // any one path; the CLI's --repeat shares it.
const MaxRepeat = 1 << 20 const MaxRepeat = 1 << 20
var _ [^uint(0)>>63 - 1]struct{} // docs/decisions.md#64-bit-targets-only // Fails to compile where uint is narrower than 64 bits: docs/decisions.md#64-bit-targets-only
var _ [^uint(0)>>63 - 1]struct{}
// Generator generates fake data from a namespace tree. Create one with [New]. // Generator generates fake data from a namespace tree. Create one with [New].
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn // It is safe for concurrent use; a seeded sequence is reproducible only when drawn
@@ -88,7 +89,8 @@ func New(opts ...Option) (*Generator, error) {
return &Generator{drawState: drawstate.New(seed), root: root}, nil return &Generator{drawState: drawstate.New(seed), root: root}, nil
} }
// docs/decisions.md#with-only-the-shipped-set-a-category-loads-on-the-first-call-reaching-it-beside-a---data-path-every-category-loads-in-new // load builds the tree New starts from: with only the shipped set, every category unloaded; else
// every source loaded and merged. docs/decisions.md#with-only-the-shipped-set-a-category-loads-on-the-first-call-reaching-it-beside-a---data-path-every-category-loads-in-new
func (c config) load() (folder, error) { func (c config) load() (folder, error) {
if c.shipped && len(c.sources) == 0 { if c.shipped && len(c.sources) == 0 {
return unloadedTree(), nil return unloadedTree(), nil
+15 -16
View File
@@ -32,43 +32,43 @@ func eachNode(n node, label string, fn func(label string, n node) error) error {
return visit(label, n) return visit(label, n)
} }
// namedNode is a contained child and the segment reaching it; a choice's items carry // containedNode is a contained child and the segment reaching it; a choice's items carry
// no segment, matching how a dot path steps over a choice. // no segment, matching how a dot path steps over a choice.
type namedNode struct { type containedNode struct {
name string name string
node node node node
line int // a table cell's line in its rows file, else 0 line int // a table cell's line in its rows file, else 0
} }
func (c namedNode) labelIn(label string) string { func (c containedNode) labelIn(label string) string {
if c.line > 0 { if c.line > 0 {
return fmt.Sprintf("%s, line %d", label, c.line) return fmt.Sprintf("%s, line %d", label, c.line)
} }
return join(label, c.name) return join(label, c.name)
} }
func contained(n node) []namedNode { func contained(n node) []containedNode {
switch n := n.(type) { switch n := n.(type) {
case *folder: case *folder:
return namedNodes(n.children) return containedByName(n.children)
case *choice: case *choice:
out := make([]namedNode, len(n.items)) out := make([]containedNode, len(n.items))
for i, it := range n.items { for i, it := range n.items {
out[i] = namedNode{node: it} out[i] = containedNode{node: it}
} }
return out return out
case *template: case *template:
return namedNodes(n.fields) return containedByName(n.fields)
case *table: case *table:
return append([]namedNode{{node: n.formatTemplate}}, namedNodes(n.formatTemplate.fields)...) return append([]containedNode{{node: n.formatTemplate}}, containedByName(n.formatTemplate.fields)...)
case *tableColumn: case *tableColumn:
if len(n.t.cellTemplates) == 0 { if len(n.t.cellTemplates) == 0 {
return nil return nil
} }
var out []namedNode var out []containedNode
for r := 0; r < n.t.rows.Len(); r++ { for r := 0; r < n.t.rows.Len(); r++ {
if cell := n.t.cellTemplate(r, n.i); cell != nil { if cell := n.t.cellTemplate(r, n.i); cell != nil {
out = append(out, namedNode{node: cell, line: r + 2}) out = append(out, containedNode{node: cell, line: r + 2})
} }
} }
return out return out
@@ -79,10 +79,10 @@ func contained(n node) []namedNode {
} }
} }
func namedNodes(m map[string]node) []namedNode { func containedByName(m map[string]node) []containedNode {
out := make([]namedNode, 0, len(m)) out := make([]containedNode, 0, len(m))
for _, name := range sortedNames(m) { for _, name := range sortedNames(m) {
out = append(out, namedNode{name: name, node: m[name]}) out = append(out, containedNode{name: name, node: m[name]})
} }
return out return out
} }
@@ -95,8 +95,7 @@ type renderEdge struct {
} }
// renderEdges lists the children rendering n recurses into, mirroring render: a // renderEdges lists the children rendering n recurses into, mirroring render: a
// choice's items, and a template's field/reference tokens plus its operands. A // choice's items, and a template's field/reference tokens plus its operands.
// folder renders nothing, so it has no edges.
func renderEdges(n node) []renderEdge { func renderEdges(n node) []renderEdge {
switch n := n.(type) { switch n := n.(type) {
case *choice: case *choice:
-1
View File
@@ -330,7 +330,6 @@ func (t *Table[O]) checkAncestors(p *Table[O], sibling func(name string) *Table[
return nil return nil
} }
// Children is the tables linked to t, sorted by segment.
func (t *Table[O]) Children() []*Table[O] { func (t *Table[O]) Children() []*Table[O] {
out := make([]*Table[O], 0, len(t.children)) out := make([]*Table[O], 0, len(t.children))
for _, c := range t.children { for _, c := range t.children {
-40
View File
@@ -62,43 +62,3 @@ func tablePaths(t *table, intoRepeats bool) []string {
} }
return out return out
} }
// sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] {
if len(shared) == 0 {
return nil
}
next := subPaths(it, intoRepeats)
for p := range shared {
if !next[p] {
delete(shared, p)
}
}
}
return shared
}
// subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{}
for _, p := range paths(n, intoRepeats) {
if p != "" {
out[p] = true
}
}
return out
}
func join(prefix, name string) string {
switch {
case prefix == "":
return name
case name == "":
return prefix
}
return prefix + "." + name
}
-21
View File
@@ -109,7 +109,6 @@ func (sc *nameScope) bindAll(t *template, where string) error {
return nil return nil
} }
// spelled names the scope in an error.
func (sc *nameScope) spelled() string { func (sc *nameScope) spelled() string {
if k := sc.kind(); k != scopeCategory { if k := sc.kind(); k != scopeCategory {
return "in one " + k.String() return "in one " + k.String()
@@ -269,23 +268,3 @@ func nameTargets(ts []templateSite) map[*nameBinding]nameTarget {
} }
return targets return targets
} }
// addressedKeys is every key the reads of each name in ts land on or pass, from the name's own
// level, with the spelling of the first read reaching it.
func addressedKeys(ts []templateSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[pickKey]string {
keys := map[*nameBinding]map[pickKey]string{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
if keys[b] == nil {
keys[b] = map[pickKey]string{}
}
for _, key := range r.a.levels[len(targets[b].tail):] {
if _, seen := keys[b][key]; !seen {
keys[b][key] = r.a.spelling
}
}
}
}
return keys
}
+5 -3
View File
@@ -65,9 +65,11 @@ type template struct {
nameScope *nameScope // where its tokens look a name up nameScope *nameScope // where its tokens look a name up
// Filled by `resolveTemplates`, from the assembled tree: // Filled by `resolveTemplates`, from the assembled tree:
refs templateRefs refs templateRefs
compiled formatOps compiled formatOps
readsColumn *columnRead // set when the format only reads one reference or name, and that read is a record's column // readsColumn is set when the format only reads one reference or name, and that read is a
// record's column. renderLeaf, proveColumnItem and itemDatatype each read it: change them together.
readsColumn *columnRead
// Filled by `settleRecords`, once the load refused every cycle: // Filled by `settleRecords`, once the load refused every cycle:
columns []recordColumn // a record's, where it has fields, in name order columns []recordColumn // a record's, where it has fields, in name order
+38 -8
View File
@@ -68,10 +68,10 @@ const (
// compiledPath walks a path and proves it reaches a node whichever way the draws go: every // compiledPath walks a path and proves it reaches a node whichever way the draws go: every
// variant of a choice carries the rest of it, and is walked, a selector names a row inside the // variant of a choice carries the rest of it, and is walked, a selector names a row inside the
// rows selected before it, and no level read carries a repeat. Once run, it holds the steps a // rows selected before it, and no level read carries a repeat. Once run, it holds the steps a
// draw takes, and every leaf the path may render; level names the head in its errors. // draw takes, and every leaf the path may render; label names the head in its errors.
type compiledPath struct { type compiledPath struct {
pins pinSet pins pinSet
level string label string
tail []string tail []string
steps []pathStep steps []pathStep
leaves []node leaves []node
@@ -88,7 +88,7 @@ func (w *compiledPath) walk(n node, tail []string) (node, error) {
return w.walkEvery(c, at.tail) return w.walkEvery(c, at.tail)
} }
var err error var err error
if w.steps, err = compileStep(&at, w.tail, w.level, w.steps, &w.pins); err != nil { if w.steps, err = compileStep(&at, w.tail, w.label, w.steps, &w.pins); err != nil {
return nil, err return nil, err
} }
} }
@@ -112,8 +112,8 @@ func (p pathPos) more() bool { return len(p.tail) > 0 || p.fromRow }
// compileStep takes the first step of at.tail from at.n, which must not be a choice, moves at past // compileStep takes the first step of at.tail from at.n, which must not be a choice, moves at past
// it, and appends the step to steps: a field, or the route through a table. whole is the full // it, and appends the step to steps: a field, or the route through a table. whole is the full
// path; level is the prefix an error puts before it. // path; label is the prefix an error puts before it.
func compileStep(at *pathPos, whole []string, level string, steps []pathStep, pins *pinSet) ([]pathStep, error) { func compileStep(at *pathPos, whole []string, label string, steps []pathStep, pins *pinSet) ([]pathStep, error) {
i := len(whole) - len(at.tail) i := len(whole) - len(at.tail)
switch x := at.n.(type) { switch x := at.n.(type) {
case *table: case *table:
@@ -125,7 +125,7 @@ func compileStep(at *pathPos, whole []string, level string, steps []pathStep, pi
return routeSteps(steps, pins, x, r, i) return routeSteps(steps, pins, x, r, i)
case *template: case *template:
if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) { if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) {
return steps, repeatLevelError(join(level, strings.Join(whole[:i], "."))) return steps, repeatLevelError(join(label, strings.Join(whole[:i], ".")))
} }
} }
next, err := childNamed(at.n, at.tail[0]) next, err := childNamed(at.n, at.tail[0])
@@ -222,6 +222,36 @@ func drawVariant(s *drawstate.State, c *choice, memo *drawMemo, levels []pickKey
return memo.variantOf(s, c, levels[at]) return memo.variantOf(s, c, levels[at])
} }
// sharedPaths is the sub-paths every item carries — the only ones a path may step
// through a choice to reach. It intersects, bailing as soon as the set
// is empty, which is immediate for a choice of plain strings.
func sharedPaths(items []node, intoRepeats bool) map[string]bool {
shared := subPaths(items[0], intoRepeats)
for _, it := range items[1:] {
if len(shared) == 0 {
return nil
}
next := subPaths(it, intoRepeats)
for p := range shared {
if !next[p] {
delete(shared, p)
}
}
}
return shared
}
// subPaths is paths(n, intoRepeats) as a set, without the empty path that means n itself.
func subPaths(n node, intoRepeats bool) map[string]bool {
out := map[string]bool{}
for _, p := range paths(n, intoRepeats) {
if p != "" {
out[p] = true
}
}
return out
}
// carriedByAll is the choice rule a path that must reach a node on every call obeys: // carriedByAll is the choice rule a path that must reach a node on every call obeys:
// the rest of the tail must be one every variant carries. // the rest of the tail must be one every variant carries.
func carriedByAll(c *choice, rest []string) error { func carriedByAll(c *choice, rest []string) error {
@@ -241,8 +271,8 @@ func unreachableInChoice(c *choice, want string) error {
return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered) return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered)
} }
func provePath(n node, tail []string, level string) error { func provePath(n node, tail []string, label string) error {
_, err := (&compiledPath{level: level, tail: tail}).run(n) _, err := (&compiledPath{label: label, tail: tail}).run(n)
return err return err
} }
+72 -19
View File
@@ -2,12 +2,28 @@ package fejkdata
import ( import (
"github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant" "github.com/larvit/fejkdata/internal/invariant"
) )
// pickKey is a level's path of segments from a root keyed "": for a named read, the node its // pickKey is the key a named pick's memo keeps one level's draw under.
// name's target starts at; for a fresh read, the template it sits in. under moves a fresh read's //
// key into the pick it renders in. // A level is a prefix of a read's path, from where it starts to its leaf; a choice and the
// variant drawn from it, or a table and its row, share one. A level's key is its path of
// segments from a root keyed "": for a named read, the node its name's target starts at; for a
// fresh read, the template it sits in. Load gives each read one key per level, in levelKeys.
//
// A name's addressed keys are the keys some read of it lands on or passes, from the level its
// binding lands on onward; load gathers them in addressedKeys. A fresh read rendering in a pick
// is kept in it only where its first level, moved under the key of what renders, is addressed:
// keptInPick, then readUnder.
//
// In data/geo/SE/address.json, {.locality as l} binds l, and {l.street.name} passes the levels
// "", "street" and "street.name", the last its key. With {l.name} and {l.postal-code.code},
// l's addressed keys are those three, "name", "postal-code" and "postal-code.code". Where p
// binds a category whose format reads {first}, and both {p} and {p.first} are read, the {first}
// that {p} renders has the key "first" under {p}'s key "". {p.first} addresses "first", so both
// read one draw.
type pickKey string type pickKey string
// under is rel, a key from a fresh read's levels, moved under k, the key of what renders. It never // under is rel, a key from a fresh read's levels, moved under k, the key of what renders. It never
@@ -19,6 +35,59 @@ func (k pickKey) under(rel pickKey) pickKey {
return k + "." + rel return k + "." + rel
} }
// levelKeys is the key of each prefix of path holding at least from segments, shortest first.
func levelKeys(path []string, from int) []pickKey {
levels := make([]pickKey, len(path)-from+1)
for i := range levels {
levels[i] = pickKey(grammar.JoinSegments(path[:from+i]))
}
return levels
}
// addressedKeys is every key the reads of each name in ts land on or pass, from the name's own
// level, with the spelling of the first read reaching it.
func addressedKeys(ts []templateSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[pickKey]string {
keys := map[*nameBinding]map[pickKey]string{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
if keys[b] == nil {
keys[b] = map[pickKey]string{}
}
for _, key := range r.a.levels[len(targets[b].tail):] {
if _, seen := keys[b][key]; !seen {
keys[b][key] = r.a.spelling
}
}
}
}
return keys
}
// keptInPick reports whether a, a fresh read rendering in env.pick, is kept in it: a reads a
// field, and a read of the pick's name addresses the level a starts at.
func keptInPick(env renderEnv, a arm) bool {
if env.pick == nil || grammar.IsRef(a.head) {
return false
}
_, kept := env.pick.named.addressed[env.pickAt.under(a.levels[0])]
return kept
}
// readUnder reads a, an arm of t kept in env.pick, once per pick, keyed by a's key under env.pickAt.
func readUnder(s *drawstate.State, t *template, env renderEnv, a arm) readValue {
p, key := env.pick, env.pickAt.under(a.key())
if r, done := p.memo.value[key]; done {
return r
}
levels := make([]pickKey, len(a.levels))
for i, l := range a.levels {
levels[i] = env.pickAt.under(l)
}
leaf, pins := p.draw(s, t.startOf(a.head), a.steps, levels)
return p.renderAt(s, leaf, pins, key, env)
}
// namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the // namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the
// value each read there produced, by its pickKey, and the table rows they pinned. // value each read there produced, by its pickKey, and the table rows they pinned.
type namedPick struct { type namedPick struct {
@@ -124,22 +193,6 @@ func readName(s *drawstate.State, env renderEnv, a arm) readValue {
return r return r
} }
// readUnder reads a, an arm of t, once per pick: t renders in env.pick at env.pickAt, and a read of
// the name addresses the level a starts at. It keys the value by a's key under env.pickAt.
func readUnder(s *drawstate.State, t *template, env renderEnv, a arm) readValue {
p, key := env.pick, env.pickAt.under(a.key())
if r, done := p.memo.value[key]; done {
return r
}
levels := make([]pickKey, len(a.levels))
for i, l := range a.levels[:len(levels)-1] {
levels[i] = env.pickAt.under(l)
}
levels[len(levels)-1] = key
leaf, pins := p.draw(s, t.startOf(a.head), a.steps, levels)
return p.renderAt(s, leaf, pins, key, env)
}
// draw draws the path from start under p, keeping the variant drawn at each of levels, whose last // draw draws the path from start under p, keeping the variant drawn at each of levels, whose last
// is the leaf's key, and returns the leaf and the pins its row is in. // is the leaf's key, and returns the leaf and the pins its row is in.
func (p *namedPick) draw(s *drawstate.State, start node, steps []pathStep, levels []pickKey) (node, *pinSet) { func (p *namedPick) draw(s *drawstate.State, start node, steps []pathStep, levels []pickKey) (node, *pinSet) {
+1 -1
View File
@@ -152,7 +152,7 @@ func readField(s *drawstate.State, t *template, env renderEnv, a arm) readValue
switch { switch {
case a.kind == namedRead: case a.kind == namedRead:
return readName(s, env, a) return readName(s, env, a)
case env.pick != nil && !grammar.IsRef(a.head) && env.keeps(a): case keptInPick(env, a):
return readUnder(s, t, env, a) return readUnder(s, t, env, a)
} }
env.pick = nil env.pick = nil
-43
View File
@@ -2,8 +2,6 @@ package fejkdata
import ( import (
"fmt" "fmt"
"github.com/larvit/fejkdata/internal/grammar"
) )
// templateSite is a template to resolve, with its folder and category, both empty for an inline // templateSite is a template to resolve, with its folder and category, both empty for an inline
@@ -89,44 +87,3 @@ func resolveTemplates(ts []templateSite, root map[string]node) error {
} }
return nil return nil
} }
// columnRead is a record's column read by a format that only reads one reference or name,
// 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 {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not or t declares a string, which reads the column as text.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.datatype != nil && *t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
var start node
var category string
var tail []string
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
// {f as n}{n}: column f of the record binding n, in t's own category.
start, tail = b.binder, append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
// {/c as n}{n.x} or {/c.x as n}{n}: column x of category c.
start, category = targets[b].start, categoryOf(b.binder.refs.byName[b.ref].head)
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case grammar.IsRef(a.head):
// {/c.x}: column x of category c.
start, category, tail = t.startOf(a.head), categoryOf(a.head), a.tail
default:
return nil
}
if target, isTemplate := start.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+10
View File
@@ -10,3 +10,13 @@ func sortedNames[V any](m map[string]V) []string {
sort.Strings(names) sort.Strings(names)
return names return names
} }
func join(prefix, name string) string {
switch {
case prefix == "":
return name
case name == "":
return prefix
}
return prefix + "." + name
}
+3 -3
View File
@@ -74,7 +74,7 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil
if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.options); err != nil { if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.options); err != nil {
return nil, err return nil, err
} }
if err := t.checkCells(); err != nil { if err := t.compileCells(); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err) return nil, fmt.Errorf("%s: %w", o.rows, err)
} }
if err := t.compileRowFormat(o.format); err != nil { if err := t.compileRowFormat(o.format); err != nil {
@@ -118,8 +118,8 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) {
return o, nil return o, nil
} }
// checkCells compiles every cell carrying a token. // compileCells compiles every cell carrying a token into cellTemplates.
func (t *table) checkCells() error { func (t *table) compileCells() error {
header := t.rows.Header() header := t.rows.Header()
for row := 0; row < t.rows.Len(); row++ { for row := 0; row < t.rows.Len(); row++ {
for col := range header { for col := range header {
+4 -34
View File
@@ -73,23 +73,17 @@
| 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, 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, 7 | 25.0 |
| 163 | 0.1.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 | | 163 | 0.1.0 | | **Lift the CLI's `--repeat` cap, since the CLI streams each render, and give the builtins' count and decimal caps a technical reason in the README.** | 2 | 2 | 7 | 7 | 2, 5.7 | 24.8 |
| 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 |
| 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 145 to 152 and item 10 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 | | 153 | 0.1.0 | decision | **Run the nine-seat comprehension panel and the scoring run again once items 10, 148 and 151 have merged, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 164 | 0.1.0 | | **Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.** | 2 | 2 | 5 | 7 | 4.1, 5.3 | 19.4 | | 164 | 0.1.0 | | **Let a table's format or a cell bind a name, which lives in that format or cell and keeps one pick through the draw.** | 2 | 2 | 5 | 7 | 4.1, 5.3 | 19.4 |
| 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 | | 157 | 0.1.0 | | **Show in the README's CLI section how PowerShell and cmd.exe pipe a template, one line per shell family.** | 1 | 1 | 5 | 6 | 7.2 | 17.1 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 | | 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 145 | 0.1.0 | | **Move `doc.go`'s definitions of a level, a pick key and an addressed key, with their worked example, to `pickKey` in `pick.go`, and gather `levelKeys`, `addressedKeys`, `renderEnv.keeps` and `readField`'s kept read beside `readUnder`, so a reader follows a named read in one file.** | 2 | 3 | 1 | 9 | 3.4, 3.2 | 15.4 |
| 165 | 0.1.0 | | **Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 | | 165 | 0.1.0 | | **Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 |
| 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 | | 127 | 0.1.0 | | **Give a Swedish street only postal codes whose delivery digit means street or rural delivery, dropping codes whose delivery digit is 9, or record why they stay.** | 3 | 3 | 4 | 6 | 4 | 14.4 |
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake` and `FakeRecord` 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 |
| 166 | 0.1.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 | | 166 | 0.1.0 | | **Let a key or name cell hold a bracket, a brace, a double quote or a pipe, so the table loads, its row draws at random, and only a selector cannot reach it.** | 2 | 2 | 3 | 6 | 5.7 | 13.1 |
| 10 | 0.1.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 | | 10 | 0.1.0 | | **Test that every node kind reaches each switch over node kinds.** | 1 | 3 | 2 | 6 | 3.2 | 13.1 |
| 148 | 0.1.0 | | **State in `geo.py` the locality row each country's script hands it, and meet the fence that every parent row has a child row in one shared step.** | 2 | 3 | 2 | 6 | 2.1, 3.2 | 12.6 | | 148 | 0.1.0 | | **State in `geo.py` the locality row each country's script hands it, and meet the fence that every parent row has a child row in one shared step.** | 2 | 3 | 2 | 6 | 2.1, 3.2 | 12.6 |
| 146 | 0.1.0 | | **Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.** | 1 | 2 | 1 | 6 | 3.1, 3.2 | 11.3 |
| 147 | 0.1.0 | | **Rename `namedNode`, `compiledPath.level` and `checkCells`, so "named" means bound to a name, "level" means only a prefix of a read's path, and `checkCells`' new name says it compiles.** | 1 | 2 | 1 | 6 | 3.3 | 11.3 |
| 151 | 0.1.0 | | **Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 | | 151 | 0.1.0 | | **Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded.** | 2 | 2 | 1 | 6 | 3.2 | 10.3 |
| 149 | 0.1.0 | | **Say in one line what `config.load` does and what the 64-bit assertion asserts, beside their decision links.** | 1 | 1 | 1 | 5 | 3.4 | 9.8 |
| 150 | 0.1.0 | | **Move `columnRead` and `columnReadOf` from `resolve.go` into `datatype.go`, and state at `template.readsColumn` that `renderLeaf`, `proveColumnItem` and `itemDatatype` change with it.** | 1 | 2 | 1 | 5 | 3.1, 3.2 | 9.5 |
| 152 | 0.1.0 | | **Delete the doc lines that restate their code.** | 1 | 2 | 1 | 5 | 3.4 | 9.5 |
| 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 | | 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 |
| 113 | 0.2.0 | defect | **Draw `{iban(CC)}` with its country's national check digits, and SE's with a real bank's id, so a validator checking more than mod 97 accepts it.** | 3 | 5 | 7 | 9 | 4 | 25.0 | | 113 | 0.2.0 | defect | **Draw `{iban(CC)}` with its country's national check digits, and SE's with a real bank's id, so a validator checking more than mod 97 accepts it.** | 3 | 5 | 7 | 9 | 4 | 25.0 |
| 154 | 0.2.0 | | **Add `en_US.routing`, an ABA routing number over the Fed's prefix ranges with its check digit drawn by `{aba()}`.** | 2 | 2 | 6 | 7 | 2.2, 15 | 22.8 | | 154 | 0.2.0 | | **Add `en_US.routing`, an ABA routing number over the Fed's prefix ranges with its check digit drawn by `{aba()}`.** | 2 | 2 | 6 | 7 | 2.2, 15 | 22.8 |
@@ -270,7 +264,7 @@ The README's Audience names a bulk fixture author writing millions of rows, and
Nothing has shipped, so "no longer paths", "where it used to fail" and "where it used to be an empty string" describe versions no reader can have installed. Nothing has shipped, so "no longer paths", "where it used to fail" and "where it used to be an empty string" describe versions no reader can have installed.
### 153. 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. ### 153. Run the nine-seat comprehension panel and the scoring run again once items 10, 148 and 151 have merged, and file what the panel names.
The decision "comprehension floor: every dimension and the overall at 7.0 or above" holds the scores this run is measured against. The decision "comprehension floor: every dimension and the overall at 7.0 or above" holds the scores this run is measured against.
@@ -286,10 +280,6 @@ Split from item 129. cmd.exe's `echo` keeps its quotes and a trailing space, so
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs. Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs.
### 145. 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.
All 13 seats of the 2026-10-07 panel and scoring run on ad59967 again named the named-pick read path hardest, and twelve kept `doc.go`'s vocabulary open beside `pick.go`, `env.go` and `namefence.go` to follow it. Load builds a pick key in `levelKeys` (`arm.go`) and `addressedKeys` (`name.go`), and render builds the same key in `keeps` (`env.go`) and `readUnder` (`pick.go`); five seats named the two sides agreeing by hand as the unit they least want to change. `readUnder`'s loop stops one level short and its next line builds that level's key the same way, which one seat searched for a difference. Update the README's Layout block, and leave `doc.go` a one-line entry per term naming `pickKey`.
### 165. Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several. ### 165. Let a table carry `name` with neither `key` nor `parent`, and list the rows a name matches by their line where it matches several.
The maintainer ruled so on 2026-10-09. Today `bindOptions` in `internal/rows` refuses such a table, since `ambiguous`, which reports a name matching several rows, lists those rows by key. Afterwards `ambiguous` lists them by line, the header being line 1, and ends with "make the names unique, or add a key column". The maintainer ruled so on 2026-10-09. Today `bindOptions` in `internal/rows` refuses such a table, since `ambiguous`, which reports a name matching several rows, lists those rows by key. Afterwards `ambiguous` lists them by line, the header being line 1, and ends with "make the names unique, or add a key column".
@@ -314,30 +304,10 @@ Today nine, from `render` to `columnItems`, must agree, and only `render` and `r
Needed by item 61. The extender seat of the 2026-10-07 panel on ad59967, reading for item 61, rebuilt the row's keys, `name`, `municipality`, `population`, `lat`, `lon` and `codes`, from `geo-se.py` and `geo-us.py` side by side. It found the fence met two ways: `geo-se.py`'s `main` exits on a kommun with no locality, while `geo-us.py`'s `main` prunes counties and regions left with none. Needed by item 61. The extender seat of the 2026-10-07 panel on ad59967, reading for item 61, rebuilt the row's keys, `name`, `municipality`, `population`, `lat`, `lon` and `codes`, from `geo-se.py` and `geo-us.py` side by side. It found the fence met two ways: `geo-se.py`'s `main` exits on a kommun with no locality, while `geo-us.py`'s `main` prunes counties and regions left with none.
### 146. Move `sharedPaths` and `subPaths` from `list.go` beside `carriedByAll` in `path.go`, and `join` beside `sortedNames` in `sorted.go`.
Three architect seats of the 2026-10-07 panel and scoring run on ad59967 found that `sharedPaths` fills `choice.shared` in `node.go`, which `carriedByAll` reads to accept or refuse every path through a choice, so `List`'s file decides which paths compile; `join` serves `list.go`, `data.go`, `graph.go` and `path.go`.
### 147. Rename `namedNode`, `compiledPath.level` and `checkCells`, so "named" means bound to a name, "level" means only a prefix of a read's path, and `checkCells`' new name says it compiles.
`namedNode` in `graph.go` is a child and its segment, beside `namedRead` and `namedPick`, which go through a bound name; `compiledPath.level` is an error's label, where a level is a prefix of a read's path; `checkCells` compiles each cell and stores it in `cellTemplates`. Three seats of the 2026-10-07 panel and scoring run on ad59967 named the first two, and four named `checkCells`.
### 151. Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded. ### 151. Load a caller's path and walk it in one `Generator` method, so no entry point walks the root before the shipped category the path names has loaded.
`Fake` and `FakeRecord` each call `loadShippedAt` before they walk, and its doc asks the next entry point to do the same. The greenfield architect of the 2026-10-07 panel on ad59967 named it the one corner a new senior would break without warning. A test passing `WithDataPath` would not catch it, because then `New` loads every category. `Fake` and `FakeRecord` each call `loadShippedAt` before they walk, and its doc asks the next entry point to do the same. The greenfield architect of the 2026-10-07 panel on ad59967 named it the one corner a new senior would break without warning. A test passing `WithDataPath` would not catch it, because then `New` loads every category.
### 149. Say in one line what `config.load` does and what the 64-bit assertion asserts, beside their decision links.
Five seats of the 2026-10-07 panel and scoring run on ad59967 met a bare decision link where a definition belonged: `config.load` in `fejkdata.go` has only the link as its doc, and `var _ [^uint(0)>>63 - 1]struct{}` only a link beside it.
### 150. Move `columnRead` and `columnReadOf` from `resolve.go` into `datatype.go`, and state at `template.readsColumn` that `renderLeaf`, `proveColumnItem` and `itemDatatype` change with it.
Two seats of the 2026-10-07 scoring run on ad59967 found `readsColumn` changing what `renderLeaf`, `proveColumnItem` and `itemDatatype` do, and found that by grep; `columnReadOf`, which sets it, sits in `resolve.go`, apart from all three.
### 152. Delete the doc lines that restate their code.
Named by seats of the 2026-10-07 panel and scoring run on ad59967: `Children` in `internal/rows/rows.go`, `spelled` in `name.go`, the line in `graph.go` saying a folder has no edges, the Dockerfile's "Module layer cached separately from source." and `compose.yaml`'s "Interactive shell for ad-hoc work".
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go. ### 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. 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.
@@ -509,7 +479,7 @@ In `internal/rows`, `Pins.PinRow` names `t.path` and `Pins.MustRow` panics with
### 115. Spell a rows file's line one way in every error, naming the file. ### 115. Spell a rows file's line one way in every error, naming the file.
Today a cell reads `g.t.b, line 3` (`namedNode.labelIn`), a cell check `t.tsv: line 3, b` (`checkCells`) and a link `t.tsv line 3` (`rows.Table.Link`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name. Today a cell reads `g.t.b, line 3` (`containedNode.labelIn`), a cell check `t.tsv: line 3, b` (`compileCells`) and a link `t.tsv line 3` (`rows.Table.Link`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name.
### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`. ### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`.
@@ -545,7 +515,7 @@ Both phone formats carry the same 21-code `area` list, so an edit to one copy ca
### 116. Turn a row index into its rows file's line in one function. ### 116. Turn a row index into its rows file's line in one function.
Today `namedNode`'s cell line in `graph.go`, `table.checkCells` and each site in `internal/rows` add the header line and the one-based count. Today `containedNode`'s cell line in `graph.go`, `table.compileCells` and each site in `internal/rows` add the header line and the one-based count.
### 56. Build on a manual run of `test.yml`. ### 56. Build on a manual run of `test.yml`.