Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -14,9 +14,98 @@
|
|||||||
lowers the last score.
|
lowers the last score.
|
||||||
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
- Hard tabs. No comment by default; delete a restatement, a rationale, history, or a file preamble.
|
||||||
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
- One spelling per result: reject the other at `New`, and let the error name the spelling to use.
|
||||||
- A standing choice a reader would relitigate goes under Decisions in the README, not in a comment.
|
|
||||||
- Prose naming a source states what that source states: read the register's own field
|
- Prose naming a source states what that source states: read the register's own field
|
||||||
before paraphrasing it, and name the register you read before claiming none
|
before paraphrasing it, and name the register you read before claiming none
|
||||||
publishes a spelling.
|
publishes a spelling.
|
||||||
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
- A README example is a `json` block that loads and renders as a category; `readme_test.go` runs every one.
|
||||||
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`linkParent`, `compileTemplate`, `renderEdges`) sit at it and stay whole; a function that would pass it is decomposed.
|
- Cyclomatic complexity is gated at 14: the table-shaped dispatches (`linkParent`, `compileTemplate`, `renderEdges`) sit at it and stay whole; a function that would pass it is decomposed.
|
||||||
|
|
||||||
|
# Decisions
|
||||||
|
|
||||||
|
In [docs/decisions.md](docs/decisions.md):
|
||||||
|
|
||||||
|
- GitHub is canonical, and the module path names it
|
||||||
|
- The vocabulary sits below `doc.go`'s package clause, not in the package doc
|
||||||
|
- Options and fields share one namespace
|
||||||
|
- `{a|b}` stays beside nested choices
|
||||||
|
- Flags follow getopt_long
|
||||||
|
- An argument is a template by its shape, not by a flag
|
||||||
|
- An inline template skips the cycle fence
|
||||||
|
- An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
||||||
|
- A padded JSON argument is rejected, not trimmed
|
||||||
|
- `FakeTemplate` and `NewTemplate` both stay
|
||||||
|
- The shipped data is embedded, not discovered
|
||||||
|
- A bare reference draws each time; a reference path is held
|
||||||
|
- Reference sigils follow the filesystem
|
||||||
|
- A change to what exists is a major; a minor only adds
|
||||||
|
- Seeded output is promised within one version
|
||||||
|
- An error is a contract by what it names, not its bytes
|
||||||
|
- Raising the lowest supported Go is a major
|
||||||
|
- The format check runs on the latest Go only
|
||||||
|
- The changelog heading is the one spelling of a release; CI cuts the tag
|
||||||
|
- A `--data-path` override rebinds every reference to the category it replaces
|
||||||
|
- The repeat cap bounds renders, not bytes
|
||||||
|
- 64-bit targets only
|
||||||
|
- A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`
|
||||||
|
- In data, a default written out and a constant spelled as a sample are load errors
|
||||||
|
- Samples say what they emit, transforms what they do
|
||||||
|
- A record is a template seen as columns; a Go struct is the one second schema
|
||||||
|
- A struct's records follow Go's field access, and compile on first use
|
||||||
|
- A render shares one reference draw per category, per group
|
||||||
|
- A draw group name is local to its category
|
||||||
|
- The expansion hold and the render's draws are two fences
|
||||||
|
- A record makes its draw maps up front, a `Fake` on its first read
|
||||||
|
- A category never references itself, and a record's fences run at load
|
||||||
|
- A record's column set is fixed before the first draw
|
||||||
|
- Null is a `null` item, not a rate
|
||||||
|
- A typed column holds one value, not composed text
|
||||||
|
- A column of one reference alone is the column it reads
|
||||||
|
- A typed column's calc is refused unless proven
|
||||||
|
- `Column` carries text, not a Go value
|
||||||
|
- `hold` names what a draw is kept in, `draw` the draw itself
|
||||||
|
- A function may not spell a method; two types may
|
||||||
|
- The package stays flat
|
||||||
|
- The performance gate asserts allocations, not wall-clock time
|
||||||
|
- Rows live in a TSV, the shape in JSON
|
||||||
|
- A selector is bracketed, and a dot inside it is literal
|
||||||
|
- `parent` names the link column and the table alike
|
||||||
|
- After a row, a path names a column or a linked table
|
||||||
|
- A table read into is pinned; a table read whole draws afresh
|
||||||
|
- Every reference path into one family selects the same rows, per render and group
|
||||||
|
- 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
|
||||||
|
- A table is a record of string columns
|
||||||
|
- The key index is built at load, the rest on first draw
|
||||||
|
- Two categories may name one TSV
|
||||||
|
- The rows of a table are alternatives
|
||||||
|
- A table never reaches its own family, by any route
|
||||||
|
- Tables carrying token cells stay small
|
||||||
|
- A path is walked once without drawing before it is walked for real
|
||||||
|
- A country's postal codes and streets are siblings under its locality
|
||||||
|
- A locale's `address` reads its country's `geo` tree, so the shipped set loads whole
|
||||||
|
- The default embed holds every Swedish postort the import can place and give a street-delivery code and a street, and the US places of 25,000 or more
|
||||||
|
- A locale's `address` restates its country record's format
|
||||||
|
- A postort's kommun comes from its name, its tätort or its codes, never from distance
|
||||||
|
- A highway designation is not a street, and a US postal code belongs to the place holding most of its land inside places
|
||||||
|
- `--list` stays a plain list of paths
|
||||||
|
- A layout is always quoted
|
||||||
|
- A title is a table under `sex`
|
||||||
|
- A table owns the spelling of a selector on it
|
||||||
|
- No builtin reads the clock, so a date is bounded by days, never by an age
|
||||||
|
- A name column without a key resolves inside its parent
|
||||||
|
- `misc` is what every locale shares
|
||||||
|
- A register's canonical spelling loses to the one its domain writes
|
||||||
|
- `misc.territory` is the spine, and a `misc` table naming a territory links to it
|
||||||
|
- A table whose register publishes no frequency draws evenly
|
||||||
|
- `misc.timezone` weighs a zone by the people living in it
|
||||||
|
- `misc.port` selects by number, and carries no `name`
|
||||||
|
- `misc.car` is one flat table, not a make linked to its models
|
||||||
|
- `misc.territory` names its sovereign in a column, and there is no `misc.country` table
|
||||||
|
- `misc.loglevel` is a table keyed by the code, rendering POSIX's keyword
|
||||||
|
- `misc.tld` keys carry the leading dot, where other tables key on a bare code
|
||||||
|
- `misc.tld` is a table of its own, and `misc.territory.tld` stays a column
|
||||||
|
- `misc.territory` carries a currency code, it does not link to `misc.currency`
|
||||||
|
- An extension may name two media types
|
||||||
|
- The Swedish ids draw Skatteverket's test series
|
||||||
|
- The US given names come from a mirror of the SSA file
|
||||||
|
- `List` advertises direct descents only
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ spot. Its tokens reach the data by reference from the root —
|
|||||||
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
||||||
available. A path never contains a brace or a quote, and a bracket only as a selector
|
available. A path never contains a brace or a quote, and a bracket only as a selector
|
||||||
after a name, so the two spellings cannot collide; which spellings an inline template
|
after a name, so the two spellings cannot collide; which spellings an inline template
|
||||||
rejects, and what each names instead, is under [Decisions](#decisions).
|
rejects, and what each names instead, is under
|
||||||
|
[Decisions](docs/decisions.md#an-argument-is-a-template-by-its-shape-not-by-a-flag).
|
||||||
|
|
||||||
| Flag | |
|
| Flag | |
|
||||||
|------|--|
|
|------|--|
|
||||||
@@ -51,7 +52,7 @@ rejects, and what each names instead, is under [Decisions](#decisions).
|
|||||||
|
|
||||||
`--name value` and `--name=value` both work, a short flag's value attaches or
|
`--name value` and `--name=value` both work, a short flag's value attaches or
|
||||||
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
||||||
[Decisions](#decisions); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
||||||
dir, unknown path), `2` misuse — a bad flag, an argument that names neither a
|
dir, unknown path), `2` misuse — a bad flag, an argument that names neither a
|
||||||
template nor a path, or an inline template that does not compile. From a checkout:
|
template nor a path, or an inline template that does not compile. From a checkout:
|
||||||
`go run ./cmd/fejkdata …`.
|
`go run ./cmd/fejkdata …`.
|
||||||
@@ -144,7 +145,8 @@ category never references itself — `{/users.first}` or a bare `{/users}` insid
|
|||||||
describes a draw other than the fields beside it — so read a sibling as a path, and put
|
describes a draw other than the fields beside it — so read a sibling as a path, and put
|
||||||
a value two fields share in its own category and reference that. A field hold, transform or
|
a value two fields share in its own category and reference that. A field hold, transform or
|
||||||
operand ties fields together within one column as always (see
|
operand ties fields together within one column as always (see
|
||||||
[Correlated fields](#correlated-fields) and [Decisions](#decisions)).
|
[Correlated fields](#correlated-fields) and
|
||||||
|
[Decisions](docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load)).
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
@@ -547,12 +549,12 @@ it, and a cell or format of a table that references a table of its own family, t
|
|||||||
any template, a `repeat` or a `drawGroup` included, since a row rendered whole would
|
any template, a `repeat` or a `drawGroup` included, since a row rendered whole would
|
||||||
draw the family apart from itself: read the family from a template beside it, or add
|
draw the family apart from itself: read the family from a template beside it, or add
|
||||||
the value as a column. A cell's reads are the render's too; which pairs are weighed
|
the value as a column. A cell's reads are the render's too; which pairs are weighed
|
||||||
against each other is under [Decisions](#decisions).
|
against each other is under [Decisions](docs/decisions.md#the-rows-of-a-table-are-alternatives).
|
||||||
|
|
||||||
### Options and fields
|
### Options and fields
|
||||||
|
|
||||||
`format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are the only options;
|
`format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are the only options;
|
||||||
**any other key is a field** (see [Decisions](#decisions)), and `rows` makes a category
|
**any other key is a field** (see [Decisions](docs/decisions.md#options-and-fields-share-one-namespace)), and `rows` makes a category
|
||||||
a [table](#table), so no template carries a field of that name. An object that does nothing a
|
a [table](#table), so no template carries a field of that name. An object that does nothing a
|
||||||
string can't — only a `format` — is rejected naming the string, as is a one-item
|
string can't — only a `format` — is rejected naming the string, as is a one-item
|
||||||
choice naming its item.
|
choice naming its item.
|
||||||
@@ -855,475 +857,6 @@ the Development section below, and who ships a register the four above then draw
|
|||||||
nothing, each on its own path. A fixture that looks nothing like production
|
nothing, each on its own path. A fixture that looks nothing like production
|
||||||
tests nothing; the caller who needs a value that can touch nothing asks for it
|
tests nothing; the caller who needs a value that can touch nothing asks for it
|
||||||
by name.
|
by name.
|
||||||
## Decisions
|
|
||||||
|
|
||||||
- **GitHub is canonical, and the module path names it.** Goal 1 wants the usage the
|
|
||||||
tool earns and goal 2 expects extenders who did not write it; both need a stranger
|
|
||||||
to file an issue and open a pull request. Gitea has no anonymous issue, and no
|
|
||||||
cross-host pull request at all, so a contributor would need an account and a fork on
|
|
||||||
a personal instance. Valid while the project wants contribution from outside: the
|
|
||||||
workflow decided nothing, being portable already — `github.api_url` and
|
|
||||||
`secrets.GITHUB_TOKEN` resolve on either host, so only the names moved.
|
|
||||||
- **The vocabulary sits below `doc.go`'s package clause, not in the package doc.**
|
|
||||||
Goal 1 is what a developer choosing a library reads first, and the package doc is
|
|
||||||
that page: the glossary would fill two thirds of it with the render path's units,
|
|
||||||
mostly unexported, and Go doc comments have no code markup, so every name would
|
|
||||||
render its backticks literally. Below the clause it reaches goal 2's reader — who
|
|
||||||
opens the file — and nothing else. Valid while the glossary's subject is the render
|
|
||||||
path; one written to teach the exported API belongs above the clause.
|
|
||||||
- **Options and fields share one namespace.** `format`, `weight`, `repeat`,
|
|
||||||
`separator`, `datatype` and `drawGroup` are reserved; every other key is a field. Nesting fields under a
|
|
||||||
key, or prefixing options, would tax every template to guard against a
|
|
||||||
misspelt option.
|
|
||||||
- **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
|
|
||||||
its arms are anonymous; `{female|male}` keeps `person.female` addressable.
|
|
||||||
- **Flags follow getopt_long.** `--name value` and `--name=value` both work; a
|
|
||||||
short flag's value attaches or follows (`-s42`, `-s 42`) and short flags bundle
|
|
||||||
(`-hn 3`), as every shell user expects. A single-dash long flag is rejected
|
|
||||||
naming the double-dash spelling, and `-s=42` is rejected naming both short
|
|
||||||
spellings: `=` belongs to the long form, and reading `=42` as the value would
|
|
||||||
make `-d=./x` a directory named `=./x`.
|
|
||||||
- **An argument is a template by its shape, not by a flag.** A JSON object, array
|
|
||||||
or string, or a string carrying a `{` token, is an inline template; anything else
|
|
||||||
is a path. A name may not contain a brace, a bracket or a quote, so a path can
|
|
||||||
never collide with any of those spellings, and the leading `[` or `"` is gated on
|
|
||||||
valid JSON so a stray copied bracket never swallows an argument — it names
|
|
||||||
nothing, and says so. No `--template` flag is needed. Reserving the characters
|
|
||||||
whole — though only a leading one could collide — keeps one simple name rule
|
|
||||||
instead of a leading-position special case. The JSON string is what makes the
|
|
||||||
library's own advice reachable: the error for an object holding only a format
|
|
||||||
names `"…"`, and that spelling has to work where it is printed. An argument or struct
|
|
||||||
tag of one reference alone, `{/users}`, is refused naming the path `users`: both
|
|
||||||
render the same text, and only the path names a record. A folder-relative `{.name}`
|
|
||||||
or `{..name}` is refused naming `{/name}`, since an inline template sits in no
|
|
||||||
folder. `IsTemplate` exports the
|
|
||||||
rule, so the CLI, struct tags and any other caller read one.
|
|
||||||
- **An inline template skips the cycle fence.** `New` proves the loaded tree
|
|
||||||
acyclic, an inline node is a finite tree of its own, and nothing in the tree can
|
|
||||||
reference it, so no render of it reaches itself. Every other fence runs over both,
|
|
||||||
from one `checkScope`, except that struct tags leave column agreement to their Go
|
|
||||||
types.
|
|
||||||
- **An inline template that does not compile is misuse (exit 2), including a
|
|
||||||
reference that resolves to nothing** — the whole argument is the spelling under
|
|
||||||
test, and `NewTemplate` compiles, links and validates as one step. An unknown
|
|
||||||
*path* stays a runtime error (exit 1): there the argument is well-formed and only
|
|
||||||
the data is absent.
|
|
||||||
- **A padded JSON argument is rejected, not trimmed.** Padding is the one place the
|
|
||||||
two readings disagree — a format string renders it, JSON drops it — so the
|
|
||||||
spelling that renders is named rather than silently chosen.
|
|
||||||
- **`FakeTemplate` and `NewTemplate` both stay.** They reach the same value but not
|
|
||||||
at the same cost: `NewTemplate` pays the compile and validation once and renders
|
|
||||||
many times, `FakeTemplate` is the one-shot call, and `--repeat` is exactly the
|
|
||||||
case that needs the first. The pair is `regexp.MustCompile` and `regexp.Match`,
|
|
||||||
not two spellings of one result.
|
|
||||||
- **The shipped data is embedded, not discovered.** A directory a machine happens
|
|
||||||
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
|
|
||||||
as JSON; `--data-path` layers over it.
|
|
||||||
- **A bare reference draws each time; a reference path is held.** `{/p} {/p}`
|
|
||||||
is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads
|
|
||||||
one draw, and a bare `{/p}` beside them is a load error: a bare token
|
|
||||||
is by contract an independent draw, a path pins its level, and a fresh draw of a
|
|
||||||
pinned level could show another row. A builtin's operand holds what it reads for
|
|
||||||
its expansion, references included, so `{uppercase(/p)} {/p}` is one draw — the
|
|
||||||
rule every operand follows.
|
|
||||||
- **Reference sigils follow the filesystem.** `/` is the root, `.` this file's
|
|
||||||
folder, `..` the folder above — what those spellings already mean to anyone who
|
|
||||||
has typed a path. A locale's files reach each other without naming the locale,
|
|
||||||
so a folder renames and copies without editing its references.
|
|
||||||
- **A change to what exists is a major; a minor only adds.** Data files, the CLI
|
|
||||||
and the Go API are the public API, and a consumer must be able to take a minor
|
|
||||||
without an edit — so an added column is a major, since it changes the CSV header
|
|
||||||
and the `INSERT` column list, as is a removed value, which changes what a fixture
|
|
||||||
holds, and a new option, which reserves a field name. One spelling per result
|
|
||||||
grows by tightening, so every fence invalidates some file. Each such release
|
|
||||||
names the rejected spelling and its replacement in the changelog and in the load
|
|
||||||
error, and that is the whole migration: a fence rejects one spelling with one
|
|
||||||
replacement, so the fix is local to each site. A fence that would need a
|
|
||||||
non-local rewrite ships a converter with its release instead. Before `v1.0.0` a
|
|
||||||
minor carries what a major would.
|
|
||||||
- **Seeded output is promised within one version.** Any edit to a category shifts
|
|
||||||
its stream and everything drawn after it, so a promise across versions would
|
|
||||||
freeze every shipped list; a fixture is re-pinned on a bump, as this repo's own are.
|
|
||||||
- **An error is a contract by what it names, not its bytes.** A script branches on
|
|
||||||
the exit code and reads the named path or spelling, so those hold; wording improves
|
|
||||||
in a minor.
|
|
||||||
- **Raising the lowest supported Go is a major.** A consumer building on it breaks,
|
|
||||||
which is the one test every rule above applies; Go's convention of a minor is not
|
|
||||||
followed.
|
|
||||||
- **The format check runs on the latest Go only.** `gofmt`'s output is the
|
|
||||||
toolchain's, not the code's: 1.27 stopped padding a map literal's values out to a
|
|
||||||
lone long key, so no source satisfies both it and 1.22's. A consumer on the lowest
|
|
||||||
supported Go depends on the code compiling and its tests passing there, which is
|
|
||||||
the `portable` stage, and never runs `gofmt` over this source, so the latest
|
|
||||||
toolchain alone defines the one canonical form goal 2 asks a reader to meet. Valid
|
|
||||||
while the lowest supported Go is not the latest.
|
|
||||||
- **The changelog heading is the one spelling of a release; CI cuts the tag.** A
|
|
||||||
tag pushed by hand is served by `go get` at once, so a tag whose commit lacks its
|
|
||||||
heading is burnt, not fixed. The heading on a gate-passed `main` commit is the
|
|
||||||
trigger instead: the tag can land only there, and the GitHub release the same job
|
|
||||||
publishes keeps one text as its body and is where prebuilt binaries will attach.
|
|
||||||
- **A `--data-path` override rebinds every reference to the category it
|
|
||||||
replaces.** References bind against the merged tree, so once shipped data uses
|
|
||||||
`{.person}`, a consumer's `sv_SE/person.json` is what every shipped reference
|
|
||||||
into `person` reads, and `New` fails on shipped data the consumer never wrote
|
|
||||||
when that file lacks a field those references read. Accepted: overriding is the
|
|
||||||
point of layering, the error names the reference and the field, and the fix is
|
|
||||||
the consumer's file carrying the fields the shipped tree reads.
|
|
||||||
- **The repeat cap bounds renders, not bytes.** A repeat, alone or nested, may
|
|
||||||
ask for at most 1 048 576 renders; how large each render is stays what the data
|
|
||||||
asked for, so `{hex(1048576)}` repeated to the cap is a terabyte, loaded without
|
|
||||||
complaint. A byte estimate would need every builtin to declare a width to fence
|
|
||||||
a shape no data comes near, and the harm lands on the author who wrote it.
|
|
||||||
- **64-bit targets only.** The gate builds amd64, and the buffer sizing a render
|
|
||||||
pre-computes (renders × bytes) assumes a 64-bit int; on a 32-bit target it could
|
|
||||||
overflow and panic.
|
|
||||||
- **A constant zero divisor is a load error; in a string column a divisor that is not
|
|
||||||
constant prints `Inf`.** `1/0` and a fixed `"0"` field are decidable, so they join
|
|
||||||
the never-numeric operand as a load error; the fold stops where an operand varies,
|
|
||||||
so `a/(b*c)` with `b` fixed at `0` and `c` varying loads and prints `Inf` every
|
|
||||||
draw — catching it needs zero-absorbing algebra for a shape nobody writes. A
|
|
||||||
[typed column](#datatype) bounds its operands instead and refuses a divisor it
|
|
||||||
cannot keep from zero.
|
|
||||||
- **In data, a default written out and a constant spelled as a sample are load
|
|
||||||
errors.** `weight: 1`, `repeat: 1`, `separator: ""`, `datatype: "string"`,
|
|
||||||
`int(5,5)`, `float(1,1,2)`,
|
|
||||||
`+5` and `05` each spell what a shorter form already spells, so each is rejected
|
|
||||||
naming that form. The CLI's numbers follow the shell instead: `--seed 007` and
|
|
||||||
`--repeat +3` are 7 and 3, as every command line reads them.
|
|
||||||
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
|
||||||
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
|
||||||
whether the argument looks like a number.
|
|
||||||
- **A record is a template seen as columns; a Go struct is the one second schema.** A
|
|
||||||
template's `format` composes its fields into one string; `FakeRecord` and
|
|
||||||
`--format` project the same fields as columns. Two views of one dataset, so a
|
|
||||||
record author writes the same JSON they already know, and a column is the same
|
|
||||||
field `Fake` renders by dotted path. The `format` is inert to a record — a
|
|
||||||
record-only template writes `"format": ""` — but it is compiled and fenced, so
|
|
||||||
a template that loads renders as whichever shape is asked for. `FakeStruct` takes
|
|
||||||
its columns from a struct instead, because a Go caller has already written that
|
|
||||||
schema: the fields name the columns and their types are the datatypes, so a tag
|
|
||||||
says only what to draw, and a `datatype` in it would be a second spelling of the
|
|
||||||
type. The Go type is a struct column's one datatype, so its items need not agree on
|
|
||||||
one among themselves; each must only hold that type.
|
|
||||||
- **A struct's records follow Go's field access, and compile on first use.** 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 draws; 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 caller's types, so the first `FakeStruct` for a type compiles its tags and the
|
|
||||||
answer, error included, is kept per type: a test's first call is its load, and no
|
|
||||||
`NewStruct` handle is needed, as the cache already compiles once.
|
|
||||||
- **A render shares one reference draw per category, per group.** Every reference
|
|
||||||
path into a category in one `Fake`, or one record, reads one draw of it, so a
|
|
||||||
value's facts agree across its fields, nested templates and columns alike —
|
|
||||||
`{/currency.code}` in one field and `{/currency.symbol}` in another name one
|
|
||||||
currency, whichever view renders them. A `repeat` iteration is a render of its
|
|
||||||
own, since repeating asks for another entity, and a [draw group](#draw-group) names further
|
|
||||||
entities within one render, so a payer and a payee are two groups over one
|
|
||||||
`person` rather than two copies of it. Only references share: a sibling field is
|
|
||||||
local to its own expansion, so a `first` column does not silently bind to a
|
|
||||||
`first` in the column next to it.
|
|
||||||
- **A draw group name is local to its category.** A category's groups are its own
|
|
||||||
entities, so a caller naming a group the same way never joins them by accident,
|
|
||||||
and renaming a group inside one file changes no render elsewhere. The unnamed
|
|
||||||
group still spans categories, since facts that belong together across categories
|
|
||||||
must agree.
|
|
||||||
- **The expansion hold and the render's draws are two fences.** One proves a sibling
|
|
||||||
path or an operand is reached only by its readers within an expansion, the other
|
|
||||||
does the same for reference paths across a render and its draw groups. They pin
|
|
||||||
different things — an operand pins the value its own render produced and stops at a
|
|
||||||
reference, a path pins every level it passes through — so one walk would carry both
|
|
||||||
rules and both scopes anyway, and tell them apart at every step.
|
|
||||||
- **A record makes its draw maps up front, a `Fake` on its first read.** A record's
|
|
||||||
columns always read through the render's draws, so making the maps where the set is
|
|
||||||
declared keeps them on that frame's stack. A `Fake` often reads no reference path at
|
|
||||||
all, and making them anyway cost about a fifth of the cheapest render, so it makes
|
|
||||||
them on the first read instead, at two heap allocations for a render that does share
|
|
||||||
a draw. The allocation gate over a repeat of a reference path and over a named draw
|
|
||||||
group prices that, and pins the two measures that keep a hold set off the heap.
|
|
||||||
- **A category never references itself, and a record's fences run at load.** 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 path stays
|
|
||||||
the one spelling for a field of one's own. A value two fields share goes in its own
|
|
||||||
category, which both reference. That settled, a record's column fences run at `New`
|
|
||||||
too, so a category that loads renders as whichever shape is asked for, and a reference
|
|
||||||
reaching back into a category through another one is refused there as the overlap it
|
|
||||||
is. Which reads those fences weigh differs on purpose: a record-only template's inert
|
|
||||||
format renders nothing, so a `drawGroup` on it can never matter and is refused, while
|
|
||||||
one on a rendering format can matter to a caller that bare-references it.
|
|
||||||
- **A record's column set is fixed before the first draw.** Only a category-level
|
|
||||||
template is a record: a path descending into a field, or naming a folder or a
|
|
||||||
choice, errors. A tail may pass through a choice whose variants carry different
|
|
||||||
fields, so the columns — and with them the CSV header written once ahead of every
|
|
||||||
row — would vary per draw. A fixed column set is what the CSV and `INSERT`
|
|
||||||
contracts rest on, so the restriction holds even where a particular choice would
|
|
||||||
happen to agree.
|
|
||||||
- **Null is a `null` item, not a rate.** A null is one more outcome of a column's
|
|
||||||
draw, so a choice's weights skew it like any other; a null-rate option would be a
|
|
||||||
second way to state odds.
|
|
||||||
- **A typed column holds one value, not composed text.** Its bounds come from a
|
|
||||||
literal or a call's arguments, so a load error names a real value, a range check is
|
|
||||||
one comparison, and `1{digits(2)}` is a second spelling of `{int(100,199)}`.
|
|
||||||
- **A column of one reference alone is the column it reads.** `{/src.score}` renders
|
|
||||||
exactly what `src.score` draws, so it takes that column's datatype and null rather
|
|
||||||
than restating them, and a `datatype` restating the one it takes is a second
|
|
||||||
spelling. Any other `datatype` still types the values — the one way to type a column
|
|
||||||
someone else wrote.
|
|
||||||
- **A typed column's calc is refused unless proven.** Operand bounds must keep each
|
|
||||||
divisor from zero and the result finite; what they cannot show is refused rather
|
|
||||||
than trusted, since a bare `NaN` breaks the JSON and SQL it lands in.
|
|
||||||
- **`Column` carries text, not a Go value.** `Value` is the rendered string beside
|
|
||||||
`DataType` and `Null`, which each serializer writes as the load check proved it; a
|
|
||||||
`Value any` would hand every caller a type switch.
|
|
||||||
- **`hold` names what a draw is kept in, `draw` the draw itself.** Goal 2 wants a name
|
|
||||||
to reach one unit, so a unit takes the stem of what it is, and a file the stem of the
|
|
||||||
units it holds.
|
|
||||||
- **A function may not spell a method; two types may.** A call writes a method with its
|
|
||||||
receiver and a function bare, so one name on both greps as one unit, which is the
|
|
||||||
reaching cost goal 2 counts, while the receiver before a shared method name says
|
|
||||||
which type answers. `TestNoFunctionSpellsAMethod` holds it. A type is outside the
|
|
||||||
rule: `renderScope.hold` answers with a `*hold`, so the two spell one unit, which no
|
|
||||||
test can judge.
|
|
||||||
- **The package stays flat.** Go ties a package to one directory, so folders would
|
|
||||||
split the API into packages.
|
|
||||||
- **The performance gate asserts allocations, not wall-clock time.** `AllocsPerRun`
|
|
||||||
is deterministic across machines, so a ±10% ceiling does not flake under CI load,
|
|
||||||
while time varies with the machine and its neighbours. A rendering slowdown
|
|
||||||
almost always costs an allocation too (a lost pre-size, a per-item map, an extra
|
|
||||||
copy). The benchmark suite (see Development) reports time for a human, not as a
|
|
||||||
pass/fail gate.
|
|
||||||
- **Rows live in a TSV, the shape in JSON.** `New` allocates once per node, so a
|
|
||||||
register of thirty thousand rows written as JSON objects would cost it a second;
|
|
||||||
a TSV is one allocation whose cells are substrings, and the JSON says only how a
|
|
||||||
row is composed. The TSV sits beside its category file, named by `rows`, so a
|
|
||||||
data directory stays a directory of categories, and one nothing names is a
|
|
||||||
load error rather than a file silently ignored.
|
|
||||||
- **A selector is bracketed, and a dot inside it is literal.** `municipality[0180]`
|
|
||||||
reads as selection to anyone who has indexed an array, and `[St. Louis]` keeps a
|
|
||||||
name whole where a colon or a dot-separated spelling could not; zsh needs the
|
|
||||||
brackets quoted, which the README's examples show. A name that spells another
|
|
||||||
row's key is refused at load rather than shadowed: a key names one row by
|
|
||||||
contract, so the name could never select its own, and the check is one lookup per
|
|
||||||
row against the key index that already exists.
|
|
||||||
- **`parent` names the link column and the table alike.** One word says both, so a
|
|
||||||
child table sits in its parent's folder and links on a column of the parent's
|
|
||||||
name; the geo plan wants exactly that, and a table needing another folder or
|
|
||||||
another column name would be asking for a second spelling.
|
|
||||||
- **After a row, a path names a column or a linked table.** `locality[Lund].address`,
|
|
||||||
a template beside the family under a selected row, is refused today; admitting it
|
|
||||||
later is additive, since a refused spelling gains a meaning and no accepted one
|
|
||||||
changes, so the door stays open for the address records the plan describes.
|
|
||||||
- **A table read into is pinned; a table read whole draws afresh.** A path into a
|
|
||||||
table pins its row for the render and group, as a
|
|
||||||
reference path pins its level, and a bare `{/city}` draws a row each time and pins no
|
|
||||||
row of its family, as a bare reference does; so a bare table beside a path into its
|
|
||||||
family is refused like a bare reference beside a path into it. A bare table reference
|
|
||||||
still counts as a read for a `drawGroup`, since the group is what draws it apart from
|
|
||||||
the family's pins.
|
|
||||||
- **Every reference path into one family selects the same rows, per render and
|
|
||||||
group.** A selector pins rows for the render, and a read that draws freely before it
|
|
||||||
could pin a row the selector contradicts, so accepting both would make the result
|
|
||||||
depend on which token rendered first. Requiring one selection per family per group
|
|
||||||
is checkable at load with no data lookup beyond the selectors themselves, and the
|
|
||||||
error names the rewrite. Two selectors naming one row by key and by name compare
|
|
||||||
equal, since the load check resolves them.
|
|
||||||
- **A parent row with no child row is a load error.** A descendant is drawn inside the
|
|
||||||
nearest pinned ancestor, so every ancestor row must lead to a row at every level
|
|
||||||
below it, or a render could find nothing to draw. The import script drops or fills
|
|
||||||
such rows; the alternative, falling back to a free draw, would break the consistency
|
|
||||||
the link exists for without saying so.
|
|
||||||
- **The choice-of-rows fence guards a data file's root, and requires string fields.**
|
|
||||||
A table is a category with a TSV beside its file, so only a root choice has the
|
|
||||||
spelling the fence names; a nested choice of same-shaped templates and an inline
|
|
||||||
one keep loading. Fields must all be strings because a cell is a string node: a
|
|
||||||
choice whose items carry a nested choice is not one table but two linked ones.
|
|
||||||
- **A table is a record of string columns.** Its columns are the CSV header and the
|
|
||||||
`INSERT` column list, fixed by the TSV header, so a table is a record by
|
|
||||||
construction; every column is a string until a typed column option earns its place.
|
|
||||||
- **The key index is built at load, the rest on first draw.** A link is proved
|
|
||||||
against the parent's keys and a key's uniqueness is a data mistake, so both are
|
|
||||||
load-time; the name index and the per-parent child lists serve only a draw or a
|
|
||||||
selection, so they wait for the first one, keeping `New` linear in the bytes read.
|
|
||||||
- **Two categories may name one TSV.** Each is a view of the file with its own
|
|
||||||
format and options, at the cost of holding the rows twice, which is what a
|
|
||||||
category over a register with two natural formats asks for; a TSV nothing names
|
|
||||||
stays a load error, since that one is a file forgotten rather than shared.
|
|
||||||
- **The rows of a table are alternatives.** Only one row renders per read, so within one
|
|
||||||
read a cell in one row and a cell in another never meet, and each may select its own
|
|
||||||
row of another table; the cells of one row, and whatever they reach, do meet, and so
|
|
||||||
does the format beside them. The rule the fence applies is that two reads meet unless
|
|
||||||
they sit in rows that never render together: rows falling under different rows of a
|
|
||||||
table a path pinned, two rows of that table itself included. A choice's items get no
|
|
||||||
such treatment yet: two items selecting different rows are still refused.
|
|
||||||
- **A table never reaches its own family, by any route.** A `repeat` iteration and a
|
|
||||||
`drawGroup` each draw apart on purpose, but a row that lists three localities from
|
|
||||||
other regions is the output the family exists to prevent, so the own-family fence
|
|
||||||
walks through both rather than stopping where the draw fences do.
|
|
||||||
- **Tables carrying token cells stay small.** The family fence compares the reads of
|
|
||||||
every pair of rows that can render together, so a table whose every row's cell
|
|
||||||
selects a row of another table loads in time quadratic in its rows: about a
|
|
||||||
second at four thousand rows. No shipped table carries such cells, and a register
|
|
||||||
is a column set rather than a set of references, so the fence is left as it is
|
|
||||||
until a real data set needs the indexed form.
|
|
||||||
- **A path is walked once without drawing before it is walked for real.** A path
|
|
||||||
that fails below its first level then moves no seeded stream, at the cost of one
|
|
||||||
draw-free walk per call, which allocates nothing.
|
|
||||||
- **A country's postal codes and streets are siblings under its locality.** No open
|
|
||||||
source pairs a Swedish street with its postnummer, and pairing the US through its
|
|
||||||
ZIPs would shape the two trees differently, so both draw inside the pinned
|
|
||||||
locality and an address agrees at that level. A street's own code is the exact
|
|
||||||
pairing to add when a source carries it.
|
|
||||||
- **A locale's `address` reads its country's `geo` tree, so the shipped set loads
|
|
||||||
whole.** `data/sv_SE` alone no longer loads: a test loads `data` and prefixes
|
|
||||||
the locale, and `--no-shipped-data -d` takes the whole `data` folder or a set of
|
|
||||||
one's own.
|
|
||||||
- **The default embed holds every Swedish postort the import can place and give a
|
|
||||||
street-delivery code and a street, and the US places of 25,000 or more.** Sweden
|
|
||||||
fits whole in 700 KB; every US place of 10,000 would pass a
|
|
||||||
megabyte and fetch 1,200 counties of TIGER files, so the threshold sits where the
|
|
||||||
two countries match in size, and `--min-population` and
|
|
||||||
`--streets-per-locality` on the import scripts build a fuller set. The two trees
|
|
||||||
add about 20 ms to `New`, which loads the shipped set in about 45 ms.
|
|
||||||
- **A locale's `address` restates its country record's format.** A record cannot
|
|
||||||
read another whole and keep its columns, so `sv_SE.address` names the same four
|
|
||||||
columns as `geo.SE.address`, each a reference into it, and the format appears
|
|
||||||
twice; a column is spelled the same in both, `street-number`, so the two never
|
|
||||||
disagree on a name.
|
|
||||||
- **A postort's kommun comes from its name, its tätort or its codes, never from
|
|
||||||
distance.** GeoNames leaves a fifth of Sweden's codes without a kommun and
|
|
||||||
carries stale spellings; the nearest code across a border named the wrong kommun
|
|
||||||
half the time it was tried, so a postort none of the three rules place is
|
|
||||||
dropped, as is one not cased like a place name.
|
|
||||||
- **A highway designation is not a street, and a US postal code belongs to the place
|
|
||||||
holding most of its land inside places.** `I- 55 Bus` and `US Hwy 1` carry the
|
|
||||||
most address ranges in many places and would head every address, so the import
|
|
||||||
drops names spelled as a route. A ZCTA goes to the place its largest in-place part
|
|
||||||
lies in, census-designated places left out since they never ship, and ships only
|
|
||||||
when that place does, so a few dozen places whose every code lies mostly in a
|
|
||||||
bigger neighbour ship no address; counting the land outside every place too would
|
|
||||||
drop a quarter of the places, whose codes straddle unincorporated land, for a
|
|
||||||
postal city the USPS mostly names the same way.
|
|
||||||
- **`--list` stays a plain list of paths.** It is what a script reads, so every line
|
|
||||||
has to be a path that `Fake` takes; a marker for the tables a `[selector]` follows,
|
|
||||||
or a legend above them, would make the output something to parse before use.
|
|
||||||
`--help` names the selector spelling instead, and the Table section teaches it.
|
|
||||||
- **A layout is always quoted.** A layout may carry the comma that separates
|
|
||||||
arguments, `'January 2, 2006'`, and one spelling for every layout beats a rule
|
|
||||||
about which ones need the quotes, so the bare spelling is refused naming the
|
|
||||||
quoted one. The layout is Go's reference time because the library renders with
|
|
||||||
it and a Go caller already knows it; its names are English, and a locale's own
|
|
||||||
month and weekday names are data.
|
|
||||||
- **A title is a table under `sex`.** A prefix drawn apart would put `Mr` on a record
|
|
||||||
whose `sex` column says `female`, which is the disagreement the record exists to
|
|
||||||
prevent; the tables this set already has are what a title needs, so `en_US.title`
|
|
||||||
links to `sex` as `first-name` does. Swedish has no everyday sexed honorific, so
|
|
||||||
`sv_SE.title` is a table as well but carries no `parent`. Its weight column is
|
|
||||||
`share`, not the `count` a name table carries, because the values are a curated
|
|
||||||
proportion rather than bearers anyone counted.
|
|
||||||
- **A table owns the spelling of a selector on it.** A reference reaches a table by a
|
|
||||||
path that carries no selector — `sv_SE.person.first` reads `first-name` through
|
|
||||||
`sex` — so the walk that resolved a name cannot say where a reader would type one.
|
|
||||||
The table's own location can, which is why it keeps its path, and why an ambiguity
|
|
||||||
error names `sv_SE.sex[f].first-name[Kim]` rather than the table's own name.
|
|
||||||
- **No builtin reads the clock, so a date is bounded by days, never by an age.**
|
|
||||||
An `age(min,max)` would make a seeded fixture change with the day it runs on,
|
|
||||||
which is what a seed exists to prevent; a birthdate for someone 20 to 60 is
|
|
||||||
`date(1966-01-01,2006-12-31,…)`, re-pinned as any fixture is.
|
|
||||||
- **A name column without a key resolves inside its parent.** A given name both
|
|
||||||
sexes carry is a row under each, so `name` cannot be the key; the parent's row
|
|
||||||
tells the two apart, `sex[f].first-name[Kim]`, the ambiguity error spells each
|
|
||||||
row inside its parent, and a name repeating inside one parent row is refused at
|
|
||||||
load, since nothing could then select it.
|
|
||||||
- **`misc` is what every locale shares.** A category whose facts differ by country
|
|
||||||
belongs in that country's locale, read from the register that country's own
|
|
||||||
records use; `misc` takes only sources that are international. NHTSA vPIC and
|
|
||||||
Mobility Sweden's registrations are national, so they are for `en_US.car` and
|
|
||||||
`sv_SE.car`, and `misc.car` waits for an international source.
|
|
||||||
- **A register's canonical spelling loses to the one its domain writes.** Where a
|
|
||||||
source offers several spellings of one fact, the shipped one is what records in
|
|
||||||
that domain carry. `misc.timezone` reads `zone.tab` and not the `zone1970.tab`
|
|
||||||
that supersedes it, because the latter keeps one zone per set of countries that
|
|
||||||
have agreed since 1970: it spells Sweden `Europe/Berlin`, and no Swedish system
|
|
||||||
writes that. For the same reason `misc.language` takes the ISO 639-2 register's
|
|
||||||
first synonym over CLDR, which says "Chinese, Mandarin" for `zh`.
|
|
||||||
- **`misc.territory` is the spine, and a `misc` table naming a territory links to
|
|
||||||
it.** Where every territory row has a child the column is a `parent`, and the
|
|
||||||
import drops the child rows whose territory the set does not ship — 17 of
|
|
||||||
`misc.timezone`'s, Antarctica's ten among them. Agreement across a record is
|
|
||||||
worth more than the last rows of a table. Where no such link can hold the fact
|
|
||||||
stays a column. Layer your own `misc.territory` over the shipped one and you
|
|
||||||
must layer `misc.timezone` too, or the link fails at load naming the row.
|
|
||||||
- **A table whose register publishes no frequency draws evenly.** `misc.httpmethod`,
|
|
||||||
`misc.port`, `misc.httpstatus`, `misc.mimetype` and `misc.tld` weigh every row alike,
|
|
||||||
so GET is a ninth of the methods drawn. Goal 10 keeps an authored fact out of a
|
|
||||||
sourced table, and no register publishes how often a method, a port or a TLD is used,
|
|
||||||
so a weight here would be invented. Where one exists it is read, as `misc.timezone`
|
|
||||||
reads GeoNames populations and `sv_SE.first-name` SCB bearers.
|
|
||||||
- **`misc.port` selects by number, and carries no `name`.** 29 of its services sit on
|
|
||||||
more than one port, `http-alt` on three, so `misc.port[http-alt]` could name no one
|
|
||||||
row. The number is what a port field holds anyway, and `misc.port[443].service`
|
|
||||||
reads the other way.
|
|
||||||
- **`misc.car` is one flat table, not a make linked to its models.** A row is a make
|
|
||||||
and a model drawn together, so no render pairs a Volvo with a RAV4. Two linked
|
|
||||||
tables would reach the same pairs and add a selector nothing asks for; a make alone
|
|
||||||
is `misc.car.make`.
|
|
||||||
- **`misc.territory` names its sovereign in a column, and there is no `misc.country`
|
|
||||||
table.** ISO 3166-1 codes territories, so `territory` is the honest name, and
|
|
||||||
`is_independent` in the register gives each one's state. A second table of the 195
|
|
||||||
sovereigns would hold a `DK` row beside the territory `DK` row, both carrying
|
|
||||||
Denmark's capital, currency, flag and TLD — two owners for one fact, drifting at
|
|
||||||
the next import. Splitting the columns to avoid that is worse: put `capital` on
|
|
||||||
the territory alone and a country row can no longer name Copenhagen. The column
|
|
||||||
cannot be a `parent`, since a table never reaches its own family; a test proves
|
|
||||||
every value names a row instead. A territory the register records no state for
|
|
||||||
stands alone, which is `EH` alone, and naming one for it would be a claim
|
|
||||||
fejkdata has no business making.
|
|
||||||
- **`misc.loglevel` is a table keyed by the code, rendering POSIX's keyword.** A flat
|
|
||||||
list of names carries neither the code a PRI encodes nor a selector reaching it, and
|
|
||||||
goal 3 draws the two as one fact. The canonical spelling losing to the one its domain
|
|
||||||
writes, above, settles the rest: a configuration writes `info`, so RFC 5424's
|
|
||||||
`Informational` stays the `severity` column, and the code and the keyword hold the
|
|
||||||
two selector slots.
|
|
||||||
- **`misc.tld` keys carry the leading dot, where other tables key on a bare code.**
|
|
||||||
The register spells a TLD `.se` and `misc.territory.tld` already ships it so, which a
|
|
||||||
bare key would make two spellings of one fact; `{/misc.tld}` also composes onto a
|
|
||||||
host with no separator. `misc.tld[se]` misses for it, which `todo.md` carries.
|
|
||||||
- **`misc.tld` is a table of its own, and `misc.territory.tld` stays a column.** A
|
|
||||||
`parent` demands a child for every parent row, so linking them would drop every root
|
|
||||||
zone row naming no territory, which is most of them, and goal 12 holds a sourced
|
|
||||||
table whole. The loader refuses the link outright anyway: `tld` is a column of
|
|
||||||
`misc.territory`, and a table may not be named like a column of its ancestor.
|
|
||||||
- **`misc.territory` carries a currency code, it does not link to `misc.currency`.**
|
|
||||||
A `parent` demands a child for every parent row, and ISO 4217 registers codes no
|
|
||||||
country's row can name: the funds codes (Mvdol, WIR Euro, US Dollar (Next day)),
|
|
||||||
and VED beside VES, both Venezuela's, of which a country row names one. Linking
|
|
||||||
would trade the register for the link, and `misc.territory.currency` already pairs a
|
|
||||||
country with its currency in one draw.
|
|
||||||
- **An extension may name two media types.** `.xml`, `.rtf`, `.sub`, `.mpp` and `.ac`
|
|
||||||
each name two rows of `misc.mimetype`. Separating them would mean dropping a
|
|
||||||
registered type, or naming one by an extension that is not its own — `.mpt` is
|
|
||||||
Project's template, not its document. Selecting such a name is an error listing
|
|
||||||
both keys; select by type instead.
|
|
||||||
- **The Swedish ids draw Skatteverket's test series.** A Luhn-valid personnummer
|
|
||||||
over a random birth number may be a living person's; 238 and 239 after any date
|
|
||||||
are blocked from assignment, so the shipped `personnummer` and
|
|
||||||
`samordningsnummer` use those. They sit in a `birth-number` table under `sex`
|
|
||||||
rather than as a column of it: the render's shared draw of the family is what
|
|
||||||
makes the number and the name agree on sex, and `sex` stays one shape across
|
|
||||||
locales instead of collecting every sex-keyed id fact. A samordningsnummer's
|
|
||||||
day, the birthday plus 60, is drawn from 61 to 88, valid in every month, rather
|
|
||||||
than computed from the date drawn.
|
|
||||||
- **The US given names come from a mirror of the SSA file.** ssa.gov refuses a
|
|
||||||
client outside the US, so `names-us.py` reads a GitHub copy that ends at 2020,
|
|
||||||
which a count over the births since 1930 barely feels; `--names` takes the
|
|
||||||
official zip. The SSA's placeholder rows are top-1000 entries that name nobody, so
|
|
||||||
the import drops them by name rather than by a rank a regeneration would move.
|
|
||||||
- **`List` advertises direct descents only.** `region.municipality.locality` is
|
|
||||||
listed, and `region.locality` resolves too but is not: the set of every descent
|
|
||||||
through a chain of five tables is every subsequence of it, and the direct chain is
|
|
||||||
the one a reader can predict from the tables' parents.
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -1357,7 +890,7 @@ data.go the load path: fs.FS folders/files -> merged namespace tree, and
|
|||||||
cmd/fejkdata/ the fejkdata CLI
|
cmd/fejkdata/ the fejkdata CLI
|
||||||
data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc
|
data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc
|
||||||
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md)
|
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md)
|
||||||
docs/ the register research behind the shipped data, and the survey of other libraries
|
docs/ the decision log, the register research behind the shipped data, and the survey of other libraries
|
||||||
release-tooling/ the release CI publishes from the changelog heading
|
release-tooling/ the release CI publishes from the changelog heading
|
||||||
testdata/ the pinned shipped shape (see Versioning)
|
testdata/ the pinned shipped shape (see Versioning)
|
||||||
```
|
```
|
||||||
@@ -1396,7 +929,7 @@ docker compose run --rm --user root --entrypoint chown test -R "$(id -u):$(id -g
|
|||||||
Every pull request runs `docker build .` against both the latest and the lowest
|
Every pull request runs `docker build .` against both the latest and the lowest
|
||||||
supported Go, and must pass before it can be merged — unless it changes none of
|
supported Go, and must pass before it can be merged — unless it changes none of
|
||||||
the files the build and its tests read, nor the workflow itself, in which case
|
the files the build and its tests read, nor the workflow itself, in which case
|
||||||
it's skipped (see [Decisions](#decisions)). That build is the whole gate but the
|
it's skipped. That build is the whole gate but the
|
||||||
changelog check, which CI runs against the PR base — vet, complexity, format check
|
changelog check, which CI runs against the PR base — vet, complexity, format check
|
||||||
and tests — so run it locally before pushing. The lowest Go builds the `portable`
|
and tests — so run it locally before pushing. The lowest Go builds the `portable`
|
||||||
stage, all of that bar the format check:
|
stage, all of that bar the format check:
|
||||||
|
|||||||
@@ -0,0 +1,640 @@
|
|||||||
|
# Decisions
|
||||||
|
|
||||||
|
## GitHub is canonical, and the module path names it
|
||||||
|
|
||||||
|
Goal 1 wants the usage the tool earns and goal 2 expects extenders who did not write it;
|
||||||
|
both need a stranger to file an issue and open a pull request. Gitea has no anonymous
|
||||||
|
issue, and no cross-host pull request at all, so a contributor would need an account and
|
||||||
|
a fork on a personal instance. Valid while the project wants contribution from outside:
|
||||||
|
the workflow decided nothing, being portable already — `github.api_url` and
|
||||||
|
`secrets.GITHUB_TOKEN` resolve on either host, so only the names moved.
|
||||||
|
|
||||||
|
## The vocabulary sits below `doc.go`'s package clause, not in the package doc
|
||||||
|
|
||||||
|
Goal 1 is what a developer choosing a library reads first, and the package doc is that
|
||||||
|
page: the glossary would fill two thirds of it with the render path's units, mostly
|
||||||
|
unexported, and Go doc comments have no code markup, so every name would render its
|
||||||
|
backticks literally. Below the clause it reaches goal 2's reader — who opens the file —
|
||||||
|
and nothing else. Valid while the glossary's subject is the render path; one written to
|
||||||
|
teach the exported API belongs above the clause.
|
||||||
|
|
||||||
|
## Options and fields share one namespace
|
||||||
|
|
||||||
|
`format`, `weight`, `repeat`, `separator`, `datatype` and `drawGroup` are reserved;
|
||||||
|
every other key is a field. Nesting fields under a key, or prefixing options, would tax
|
||||||
|
every template to guard against a misspelt option.
|
||||||
|
|
||||||
|
## `{a|b}` stays beside nested choices
|
||||||
|
|
||||||
|
`[[…], […]]` picks the same way, but its arms are anonymous; `{female|male}` keeps
|
||||||
|
`person.female` addressable.
|
||||||
|
|
||||||
|
## Flags follow getopt_long
|
||||||
|
|
||||||
|
`--name value` and `--name=value` both work; a short flag's value attaches or follows
|
||||||
|
(`-s42`, `-s 42`) and short flags bundle (`-hn 3`), as every shell user expects. A
|
||||||
|
single-dash long flag is rejected naming the double-dash spelling, and `-s=42` is
|
||||||
|
rejected naming both short spellings: `=` belongs to the long form, and reading `=42` as
|
||||||
|
the value would make `-d=./x` a directory named `=./x`.
|
||||||
|
|
||||||
|
## An argument is a template by its shape, not by a flag
|
||||||
|
|
||||||
|
A JSON object, array or string, or a string carrying a `{` token, is an inline template;
|
||||||
|
anything else is a path. A name may not contain a brace, a bracket or a quote, so a path
|
||||||
|
can never collide with any of those spellings, and the leading `[` or `"` is gated on
|
||||||
|
valid JSON so a stray copied bracket never swallows an argument — it names nothing, and
|
||||||
|
says so. No `--template` flag is needed. Reserving the characters whole — though only a
|
||||||
|
leading one could collide — keeps one simple name rule instead of a leading-position
|
||||||
|
special case. The JSON string is what makes the library's own advice reachable: the
|
||||||
|
error for an object holding only a format names `"…"`, and that spelling has to work
|
||||||
|
where it is printed. An argument or struct tag of one reference alone, `{/users}`, is
|
||||||
|
refused naming the path `users`: both render the same text, and only the path names a
|
||||||
|
record. A folder-relative `{.name}` or `{..name}` is refused naming `{/name}`, since an
|
||||||
|
inline template sits in no folder. `IsTemplate` exports the rule, so the CLI, struct
|
||||||
|
tags and any other caller read one.
|
||||||
|
|
||||||
|
## An inline template skips the cycle fence
|
||||||
|
|
||||||
|
`New` proves the loaded tree acyclic, an inline node is a finite tree of its own, and
|
||||||
|
nothing in the tree can reference it, so no render of it reaches itself. Every other
|
||||||
|
fence runs over both, from one `checkScope`, except that struct tags leave column
|
||||||
|
agreement to their Go types.
|
||||||
|
|
||||||
|
## An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
|
||||||
|
|
||||||
|
The whole argument is the spelling under test, and `NewTemplate` compiles, links and
|
||||||
|
validates as one step. An unknown *path* stays a runtime error (exit 1): there the
|
||||||
|
argument is well-formed and only the data is absent.
|
||||||
|
|
||||||
|
## A padded JSON argument is rejected, not trimmed
|
||||||
|
|
||||||
|
Padding is the one place the two readings disagree — a format string renders it, JSON
|
||||||
|
drops it — so the spelling that renders is named rather than silently chosen.
|
||||||
|
|
||||||
|
## `FakeTemplate` and `NewTemplate` both stay
|
||||||
|
|
||||||
|
They reach the same value but not at the same cost: `NewTemplate` pays the compile and
|
||||||
|
validation once and renders many times, `FakeTemplate` is the one-shot call, and
|
||||||
|
`--repeat` is exactly the case that needs the first. The pair is `regexp.MustCompile`
|
||||||
|
and `regexp.Match`, not two spellings of one result.
|
||||||
|
|
||||||
|
## The shipped data is embedded, not discovered
|
||||||
|
|
||||||
|
A directory a machine happens to have would make `--seed 42` machine-dependent. Data
|
||||||
|
still lives in `data/` as JSON; `--data-path` layers over it.
|
||||||
|
|
||||||
|
## A bare reference draws each time; a reference path is held
|
||||||
|
|
||||||
|
`{/p} {/p}` is two draws, as `{word} {word}` is, while every `{/p.first}` in one render
|
||||||
|
reads one draw, and a bare `{/p}` beside them is a load error: a bare token is by
|
||||||
|
contract an independent draw, a path pins its level, and a fresh draw of a pinned level
|
||||||
|
could show another row. A builtin's operand holds what it reads for its expansion,
|
||||||
|
references included, so `{uppercase(/p)} {/p}` is one draw — the rule every operand
|
||||||
|
follows.
|
||||||
|
|
||||||
|
## Reference sigils follow the filesystem
|
||||||
|
|
||||||
|
`/` is the root, `.` this file's folder, `..` the folder above — what those spellings
|
||||||
|
already mean to anyone who has typed a path. A locale's files reach each other without
|
||||||
|
naming the locale, so a folder renames and copies without editing its references.
|
||||||
|
|
||||||
|
## A change to what exists is a major; a minor only adds
|
||||||
|
|
||||||
|
Data files, the CLI and the Go API are the public API, and a consumer must be able to
|
||||||
|
take a minor without an edit — so an added column is a major, since it changes the CSV
|
||||||
|
header and the `INSERT` column list, as is a removed value, which changes what a fixture
|
||||||
|
holds, and a new option, which reserves a field name. One spelling per result grows by
|
||||||
|
tightening, so every fence invalidates some file. Each such release names the rejected
|
||||||
|
spelling and its replacement in the changelog and in the load error, and that is the
|
||||||
|
whole migration: a fence rejects one spelling with one replacement, so the fix is local
|
||||||
|
to each site. A fence that would need a non-local rewrite ships a converter with its
|
||||||
|
release instead. Before `v1.0.0` a minor carries what a major would.
|
||||||
|
|
||||||
|
## Seeded output is promised within one version
|
||||||
|
|
||||||
|
Any edit to a category shifts its stream and everything drawn after it, so a promise
|
||||||
|
across versions would freeze every shipped list; a fixture is re-pinned on a bump, as
|
||||||
|
this repo's own are.
|
||||||
|
|
||||||
|
## An error is a contract by what it names, not its bytes
|
||||||
|
|
||||||
|
A script branches on the exit code and reads the named path or spelling, so those hold;
|
||||||
|
wording improves in a minor.
|
||||||
|
|
||||||
|
## Raising the lowest supported Go is a major
|
||||||
|
|
||||||
|
A consumer building on it breaks, which is the one test every rule above applies; Go's
|
||||||
|
convention of a minor is not followed.
|
||||||
|
|
||||||
|
## The format check runs on the latest Go only
|
||||||
|
|
||||||
|
`gofmt`'s output is the toolchain's, not the code's: 1.27 stopped padding a map
|
||||||
|
literal's values out to a lone long key, so no source satisfies both it and 1.22's. A
|
||||||
|
consumer on the lowest supported Go depends on the code compiling and its tests passing
|
||||||
|
there, which is the `portable` stage, and never runs `gofmt` over this source, so the
|
||||||
|
latest toolchain alone defines the one canonical form goal 2 asks a reader to meet.
|
||||||
|
Valid while the lowest supported Go is not the latest.
|
||||||
|
|
||||||
|
## The changelog heading is the one spelling of a release; CI cuts the tag
|
||||||
|
|
||||||
|
A tag pushed by hand is served by `go get` at once, so a tag whose commit lacks its
|
||||||
|
heading is burnt, not fixed. The heading on a gate-passed `main` commit is the trigger
|
||||||
|
instead: the tag can land only there, and the GitHub release the same job publishes
|
||||||
|
keeps one text as its body and is where prebuilt binaries will attach.
|
||||||
|
|
||||||
|
## A `--data-path` override rebinds every reference to the category it replaces
|
||||||
|
|
||||||
|
References bind against the merged tree, so once shipped data uses `{.person}`, a
|
||||||
|
consumer's `sv_SE/person.json` is what every shipped reference into `person` reads, and
|
||||||
|
`New` fails on shipped data the consumer never wrote when that file lacks a field those
|
||||||
|
references read. Accepted: overriding is the point of layering, the error names the
|
||||||
|
reference and the field, and the fix is the consumer's file carrying the fields the
|
||||||
|
shipped tree reads.
|
||||||
|
|
||||||
|
## The repeat cap bounds renders, not bytes
|
||||||
|
|
||||||
|
A repeat, alone or nested, may ask for at most 1 048 576 renders; how large each render
|
||||||
|
is stays what the data asked for, so `{hex(1048576)}` repeated to the cap is a terabyte,
|
||||||
|
loaded without complaint. A byte estimate would need every builtin to declare a width to
|
||||||
|
fence a shape no data comes near, and the harm lands on the author who wrote it.
|
||||||
|
|
||||||
|
## 64-bit targets only
|
||||||
|
|
||||||
|
The gate builds amd64, and the buffer sizing a render pre-computes (renders × bytes)
|
||||||
|
assumes a 64-bit int; on a 32-bit target it could overflow and panic.
|
||||||
|
|
||||||
|
## A constant zero divisor is a load error; in a string column a divisor that is not constant prints `Inf`
|
||||||
|
|
||||||
|
`1/0` and a fixed `"0"` field are decidable, so they join the never-numeric operand as a
|
||||||
|
load error; the fold stops where an operand varies, so `a/(b*c)` with `b` fixed at `0`
|
||||||
|
and `c` varying loads and prints `Inf` every draw — catching it needs zero-absorbing
|
||||||
|
algebra for a shape nobody writes. A [typed column](../README.md#datatype) bounds its operands
|
||||||
|
instead and refuses a divisor it cannot keep from zero.
|
||||||
|
|
||||||
|
## In data, a default written out and a constant spelled as a sample are load errors
|
||||||
|
|
||||||
|
`weight: 1`, `repeat: 1`, `separator: ""`, `datatype: "string"`, `int(5,5)`,
|
||||||
|
`float(1,1,2)`, `+5` and `05` each spell what a shorter form already spells, so each is
|
||||||
|
rejected naming that form. The CLI's numbers follow the shell instead: `--seed 007` and
|
||||||
|
`--repeat +3` are 7 and 3, as every command line reads them.
|
||||||
|
|
||||||
|
## Samples say what they emit, transforms what they do
|
||||||
|
|
||||||
|
`{upper(2)}` is two letters, `{uppercase(x)}` is `x` upper-cased; one name for both
|
||||||
|
would turn on whether the argument looks like a number.
|
||||||
|
|
||||||
|
## A record is a template seen as columns; a Go struct is the one second schema
|
||||||
|
|
||||||
|
A template's `format` composes its fields into one string; `FakeRecord` and `--format`
|
||||||
|
project the same fields as columns. Two views of one dataset, so a record author writes
|
||||||
|
the same JSON they already know, and a column is the same field `Fake` renders by dotted
|
||||||
|
path. The `format` is inert to a record — a record-only template writes `"format": ""` —
|
||||||
|
but it is compiled and fenced, so a template that loads renders as whichever shape is
|
||||||
|
asked for. `FakeStruct` takes its columns from a struct instead, because a Go caller has
|
||||||
|
already written that schema: the fields name the columns and their types are the
|
||||||
|
datatypes, so a tag says only what to draw, and a `datatype` in it would be a second
|
||||||
|
spelling of the type. The Go type is a struct column's one datatype, so its items need
|
||||||
|
not agree on one among themselves; each must only hold that type.
|
||||||
|
|
||||||
|
## A struct's records follow Go's field access, and compile on first use
|
||||||
|
|
||||||
|
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 draws; 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
|
||||||
|
caller's types, so the first `FakeStruct` for a type compiles its tags and the answer,
|
||||||
|
error included, is kept per type: a test's first call is its load, and no `NewStruct`
|
||||||
|
handle is needed, as the cache already compiles once.
|
||||||
|
|
||||||
|
## A render shares one reference draw per category, per group
|
||||||
|
|
||||||
|
Every reference path into a category in one `Fake`, or one record, reads one draw of it,
|
||||||
|
so a value's facts agree across its fields, nested templates and columns alike —
|
||||||
|
`{/currency.code}` in one field and `{/currency.symbol}` in another name one currency,
|
||||||
|
whichever view renders them. A `repeat` iteration is a render of its own, since
|
||||||
|
repeating asks for another entity, and a [draw group](../README.md#draw-group) names further
|
||||||
|
entities within one render, so a payer and a payee are two groups over one `person`
|
||||||
|
rather than two copies of it. Only references share: a sibling field is local to its own
|
||||||
|
expansion, so a `first` column does not silently bind to a `first` in the column next to
|
||||||
|
it.
|
||||||
|
|
||||||
|
## A draw group name is local to its category
|
||||||
|
|
||||||
|
A category's groups are its own entities, so a caller naming a group the same way never
|
||||||
|
joins them by accident, and renaming a group inside one file changes no render
|
||||||
|
elsewhere. The unnamed group still spans categories, since facts that belong together
|
||||||
|
across categories must agree.
|
||||||
|
|
||||||
|
## The expansion hold and the render's draws are two fences
|
||||||
|
|
||||||
|
One proves a sibling path or an operand is reached only by its readers within an
|
||||||
|
expansion, the other does the same for reference paths across a render and its draw
|
||||||
|
groups. They pin different things — an operand pins the value its own render produced
|
||||||
|
and stops at a reference, a path pins every level it passes through — so one walk would
|
||||||
|
carry both rules and both scopes anyway, and tell them apart at every step.
|
||||||
|
|
||||||
|
## A record makes its draw maps up front, a `Fake` on its first read
|
||||||
|
|
||||||
|
A record's columns always read through the render's draws, so making the maps where the
|
||||||
|
set is declared keeps them on that frame's stack. A `Fake` often reads no reference path
|
||||||
|
at all, and making them anyway cost about a fifth of the cheapest render, so it makes
|
||||||
|
them on the first read instead, at two heap allocations for a render that does share a
|
||||||
|
draw. The allocation gate over a repeat of a reference path and over a named draw group
|
||||||
|
prices that, and pins the two measures that keep a hold set off the heap.
|
||||||
|
|
||||||
|
## A category never references itself, and a record's fences run at load
|
||||||
|
|
||||||
|
A category is one unit: a reference back into it — `{/users.first}` inside `users` —
|
||||||
|
describes a draw other than the fields beside it, where goal 3 asks facts that belong
|
||||||
|
together to come from one draw, so `New` refuses it and the sibling path stays the one
|
||||||
|
spelling for a field of one's own. A value two fields share goes in its own category,
|
||||||
|
which both reference. That settled, a record's column fences run at `New` too, so a
|
||||||
|
category that loads renders as whichever shape is asked for, and a reference reaching
|
||||||
|
back into a category through another one is refused there as the overlap it is. Which
|
||||||
|
reads those fences weigh differs on purpose: a record-only template's inert format
|
||||||
|
renders nothing, so a `drawGroup` on it can never matter and is refused, while one on a
|
||||||
|
rendering format can matter to a caller that bare-references it.
|
||||||
|
|
||||||
|
## A record's column set is fixed before the first draw
|
||||||
|
|
||||||
|
Only a category-level template is a record: a path descending into a field, or naming a
|
||||||
|
folder or a choice, errors. A tail may pass through a choice whose variants carry
|
||||||
|
different fields, so the columns — and with them the CSV header written once ahead of
|
||||||
|
every row — would vary per draw. A fixed column set is what the CSV and `INSERT`
|
||||||
|
contracts rest on, so the restriction holds even where a particular choice would happen
|
||||||
|
to agree.
|
||||||
|
|
||||||
|
## Null is a `null` item, not a rate
|
||||||
|
|
||||||
|
A null is one more outcome of a column's draw, so a choice's weights skew it like any
|
||||||
|
other; a null-rate option would be a second way to state odds.
|
||||||
|
|
||||||
|
## A typed column holds one value, not composed text
|
||||||
|
|
||||||
|
Its bounds come from a literal or a call's arguments, so a load error names a real
|
||||||
|
value, a range check is one comparison, and `1{digits(2)}` is a second spelling of
|
||||||
|
`{int(100,199)}`.
|
||||||
|
|
||||||
|
## A column of one reference alone is the column it reads
|
||||||
|
|
||||||
|
`{/src.score}` renders exactly what `src.score` draws, so it takes that column's
|
||||||
|
datatype and null rather than restating them, and a `datatype` restating the one it
|
||||||
|
takes is a second spelling. Any other `datatype` still types the values — the one way to
|
||||||
|
type a column someone else wrote.
|
||||||
|
|
||||||
|
## A typed column's calc is refused unless proven
|
||||||
|
|
||||||
|
Operand bounds must keep each divisor from zero and the result finite; what they cannot
|
||||||
|
show is refused rather than trusted, since a bare `NaN` breaks the JSON and SQL it lands
|
||||||
|
in.
|
||||||
|
|
||||||
|
## `Column` carries text, not a Go value
|
||||||
|
|
||||||
|
`Value` is the rendered string beside `DataType` and `Null`, which each serializer
|
||||||
|
writes as the load check proved it; a `Value any` would hand every caller a type switch.
|
||||||
|
|
||||||
|
## `hold` names what a draw is kept in, `draw` the draw itself
|
||||||
|
|
||||||
|
Goal 2 wants a name to reach one unit, so a unit takes the stem of what it is, and a
|
||||||
|
file the stem of the units it holds.
|
||||||
|
|
||||||
|
## A function may not spell a method; two types may
|
||||||
|
|
||||||
|
A call writes a method with its receiver and a function bare, so one name on both greps
|
||||||
|
as one unit, which is the reaching cost goal 2 counts, while the receiver before a
|
||||||
|
shared method name says which type answers. `TestNoFunctionSpellsAMethod` holds it. A
|
||||||
|
type is outside the rule: `renderScope.hold` answers with a `*hold`, so the two spell
|
||||||
|
one unit, which no test can judge.
|
||||||
|
|
||||||
|
## The package stays flat
|
||||||
|
|
||||||
|
Go ties a package to one directory, so folders would split the API into packages.
|
||||||
|
|
||||||
|
## The performance gate asserts allocations, not wall-clock time
|
||||||
|
|
||||||
|
`AllocsPerRun` is deterministic across machines, so a ±10% ceiling does not flake under
|
||||||
|
CI load, while time varies with the machine and its neighbours. A rendering slowdown
|
||||||
|
almost always costs an allocation too (a lost pre-size, a per-item map, an extra copy).
|
||||||
|
The benchmark suite (the README's Development) reports time for a human, not as a pass/fail gate.
|
||||||
|
|
||||||
|
## Rows live in a TSV, the shape in JSON
|
||||||
|
|
||||||
|
`New` allocates once per node, so a register of thirty thousand rows written as JSON
|
||||||
|
objects would cost it a second; a TSV is one allocation whose cells are substrings, and
|
||||||
|
the JSON says only how a row is composed. The TSV sits beside its category file, named
|
||||||
|
by `rows`, so a data directory stays a directory of categories, and one nothing names is
|
||||||
|
a load error rather than a file silently ignored.
|
||||||
|
|
||||||
|
## A selector is bracketed, and a dot inside it is literal
|
||||||
|
|
||||||
|
`municipality[0180]` reads as selection to anyone who has indexed an array, and `[St.
|
||||||
|
Louis]` keeps a name whole where a colon or a dot-separated spelling could not; zsh
|
||||||
|
needs the brackets quoted, which the README's examples show. A name that spells another
|
||||||
|
row's key is refused at load rather than shadowed: a key names one row by contract, so
|
||||||
|
the name could never select its own, and the check is one lookup per row against the key
|
||||||
|
index that already exists.
|
||||||
|
|
||||||
|
## `parent` names the link column and the table alike
|
||||||
|
|
||||||
|
One word says both, so a child table sits in its parent's folder and links on a column
|
||||||
|
of the parent's name; the geo plan wants exactly that, and a table needing another
|
||||||
|
folder or another column name would be asking for a second spelling.
|
||||||
|
|
||||||
|
## After a row, a path names a column or a linked table
|
||||||
|
|
||||||
|
`locality[Lund].address`, a template beside the family under a selected row, is refused
|
||||||
|
today; admitting it later is additive, since a refused spelling gains a meaning and no
|
||||||
|
accepted one changes, so the door stays open for the address records the plan describes.
|
||||||
|
|
||||||
|
## A table read into is pinned; a table read whole draws afresh
|
||||||
|
|
||||||
|
A path into a table pins its row for the render and group, as a reference path pins its
|
||||||
|
level, and a bare `{/city}` draws a row each time and pins no row of its family, as a
|
||||||
|
bare reference does; so a bare table beside a path into its family is refused like a
|
||||||
|
bare reference beside a path into it. A bare table reference still counts as a read for
|
||||||
|
a `drawGroup`, since the group is what draws it apart from the family's pins.
|
||||||
|
|
||||||
|
## Every reference path into one family selects the same rows, per render and group
|
||||||
|
|
||||||
|
A selector pins rows for the render, and a read that draws freely before it could pin a
|
||||||
|
row the selector contradicts, so accepting both would make the result depend on which
|
||||||
|
token rendered first. Requiring one selection per family per group is checkable at load
|
||||||
|
with no data lookup beyond the selectors themselves, and the error names the rewrite.
|
||||||
|
Two selectors naming one row by key and by name compare equal, since the load check
|
||||||
|
resolves them.
|
||||||
|
|
||||||
|
## A parent row with no child row is a load error
|
||||||
|
|
||||||
|
A descendant is drawn inside the nearest pinned ancestor, so every ancestor row must
|
||||||
|
lead to a row at every level below it, or a render could find nothing to draw. The
|
||||||
|
import script drops or fills such rows; the alternative, falling back to a free draw,
|
||||||
|
would break the consistency the link exists for without saying so.
|
||||||
|
|
||||||
|
## The choice-of-rows fence guards a data file's root, and requires string fields
|
||||||
|
|
||||||
|
A table is a category with a TSV beside its file, so only a root choice has the spelling
|
||||||
|
the fence names; a nested choice of same-shaped templates and an inline one keep
|
||||||
|
loading. Fields must all be strings because a cell is a string node: a choice whose
|
||||||
|
items carry a nested choice is not one table but two linked ones.
|
||||||
|
|
||||||
|
## A table is a record of string columns
|
||||||
|
|
||||||
|
Its columns are the CSV header and the `INSERT` column list, fixed by the TSV header, so
|
||||||
|
a table is a record by construction; every column is a string until a typed column
|
||||||
|
option earns its place.
|
||||||
|
|
||||||
|
## The key index is built at load, the rest on first draw
|
||||||
|
|
||||||
|
A link is proved against the parent's keys and a key's uniqueness is a data mistake, so
|
||||||
|
both are load-time; the name index and the per-parent child lists serve only a draw or a
|
||||||
|
selection, so they wait for the first one, keeping `New` linear in the bytes read.
|
||||||
|
|
||||||
|
## Two categories may name one TSV
|
||||||
|
|
||||||
|
Each is a view of the file with its own format and options, at the cost of holding the
|
||||||
|
rows twice, which is what a category over a register with two natural formats asks for;
|
||||||
|
a TSV nothing names stays a load error, since that one is a file forgotten rather than
|
||||||
|
shared.
|
||||||
|
|
||||||
|
## The rows of a table are alternatives
|
||||||
|
|
||||||
|
Only one row renders per read, so within one read a cell in one row and a cell in
|
||||||
|
another never meet, and each may select its own row of another table; the cells of one
|
||||||
|
row, and whatever they reach, do meet, and so does the format beside them. The rule the
|
||||||
|
fence applies is that two reads meet unless they sit in rows that never render together:
|
||||||
|
rows falling under different rows of a table a path pinned, two rows of that table
|
||||||
|
itself included. A choice's items get no such treatment yet: two items selecting
|
||||||
|
different rows are still refused.
|
||||||
|
|
||||||
|
## A table never reaches its own family, by any route
|
||||||
|
|
||||||
|
A `repeat` iteration and a `drawGroup` each draw apart on purpose, but a row that lists
|
||||||
|
three localities from other regions is the output the family exists to prevent, so the
|
||||||
|
own-family fence walks through both rather than stopping where the draw fences do.
|
||||||
|
|
||||||
|
## Tables carrying token cells stay small
|
||||||
|
|
||||||
|
The family fence compares the reads of every pair of rows that can render together, so a
|
||||||
|
table whose every row's cell selects a row of another table loads in time quadratic in
|
||||||
|
its rows: about a second at four thousand rows. No shipped table carries such cells, and
|
||||||
|
a register is a column set rather than a set of references, so the fence is left as it
|
||||||
|
is until a real data set needs the indexed form.
|
||||||
|
|
||||||
|
## A path is walked once without drawing before it is walked for real
|
||||||
|
|
||||||
|
A path that fails below its first level then moves no seeded stream, at the cost of one
|
||||||
|
draw-free walk per call, which allocates nothing.
|
||||||
|
|
||||||
|
## A country's postal codes and streets are siblings under its locality
|
||||||
|
|
||||||
|
No open source pairs a Swedish street with its postnummer, and pairing the US through
|
||||||
|
its ZIPs would shape the two trees differently, so both draw inside the pinned locality
|
||||||
|
and an address agrees at that level. A street's own code is the exact pairing to add
|
||||||
|
when a source carries it.
|
||||||
|
|
||||||
|
## A locale's `address` reads its country's `geo` tree, so the shipped set loads whole
|
||||||
|
|
||||||
|
`data/sv_SE` alone no longer loads: a test loads `data` and prefixes the locale, and
|
||||||
|
`--no-shipped-data -d` takes the whole `data` folder or a set of one's own.
|
||||||
|
|
||||||
|
## The default embed holds every Swedish postort the import can place and give a street-delivery code and a street, and the US places of 25,000 or more
|
||||||
|
|
||||||
|
Sweden fits whole in 700 KB; every US place of 10,000 would pass a megabyte and fetch
|
||||||
|
1,200 counties of TIGER files, so the threshold sits where the two countries match in
|
||||||
|
size, and `--min-population` and `--streets-per-locality` on the import scripts build a
|
||||||
|
fuller set. The two trees add about 20 ms to `New`, which loads the shipped set in about
|
||||||
|
45 ms.
|
||||||
|
|
||||||
|
## A locale's `address` restates its country record's format
|
||||||
|
|
||||||
|
A record cannot read another whole and keep its columns, so `sv_SE.address` names the
|
||||||
|
same four columns as `geo.SE.address`, each a reference into it, and the format appears
|
||||||
|
twice; a column is spelled the same in both, `street-number`, so the two never disagree
|
||||||
|
on a name.
|
||||||
|
|
||||||
|
## A postort's kommun comes from its name, its tätort or its codes, never from distance
|
||||||
|
|
||||||
|
GeoNames leaves a fifth of Sweden's codes without a kommun and carries stale spellings;
|
||||||
|
the nearest code across a border named the wrong kommun half the time it was tried, so a
|
||||||
|
postort none of the three rules place is dropped, as is one not cased like a place name.
|
||||||
|
|
||||||
|
## A highway designation is not a street, and a US postal code belongs to the place holding most of its land inside places
|
||||||
|
|
||||||
|
`I- 55 Bus` and `US Hwy 1` carry the most address ranges in many places and would head
|
||||||
|
every address, so the import drops names spelled as a route. A ZCTA goes to the place
|
||||||
|
its largest in-place part lies in, census-designated places left out since they never
|
||||||
|
ship, and ships only when that place does, so a few dozen places whose every code lies
|
||||||
|
mostly in a bigger neighbour ship no address; counting the land outside every place too
|
||||||
|
would drop a quarter of the places, whose codes straddle unincorporated land, for a
|
||||||
|
postal city the USPS mostly names the same way.
|
||||||
|
|
||||||
|
## `--list` stays a plain list of paths
|
||||||
|
|
||||||
|
It is what a script reads, so every line has to be a path that `Fake` takes; a marker
|
||||||
|
for the tables a `[selector]` follows, or a legend above them, would make the output
|
||||||
|
something to parse before use. `--help` names the selector spelling instead, and the
|
||||||
|
Table section teaches it.
|
||||||
|
|
||||||
|
## A layout is always quoted
|
||||||
|
|
||||||
|
A layout may carry the comma that separates arguments, `'January 2, 2006'`, and one
|
||||||
|
spelling for every layout beats a rule about which ones need the quotes, so the bare
|
||||||
|
spelling is refused naming the quoted one. The layout is Go's reference time because the
|
||||||
|
library renders with it and a Go caller already knows it; its names are English, and a
|
||||||
|
locale's own month and weekday names are data.
|
||||||
|
|
||||||
|
## A title is a table under `sex`
|
||||||
|
|
||||||
|
A prefix drawn apart would put `Mr` on a record whose `sex` column says `female`, which
|
||||||
|
is the disagreement the record exists to prevent; the tables this set already has are
|
||||||
|
what a title needs, so `en_US.title` links to `sex` as `first-name` does. Swedish has no
|
||||||
|
everyday sexed honorific, so `sv_SE.title` is a table as well but carries no `parent`.
|
||||||
|
Its weight column is `share`, not the `count` a name table carries, because the values
|
||||||
|
are a curated proportion rather than bearers anyone counted.
|
||||||
|
|
||||||
|
## A table owns the spelling of a selector on it
|
||||||
|
|
||||||
|
A reference reaches a table by a path that carries no selector — `sv_SE.person.first`
|
||||||
|
reads `first-name` through `sex` — so the walk that resolved a name cannot say where a
|
||||||
|
reader would type one. The table's own location can, which is why it keeps its path, and
|
||||||
|
why an ambiguity error names `sv_SE.sex[f].first-name[Kim]` rather than the table's own
|
||||||
|
name.
|
||||||
|
|
||||||
|
## No builtin reads the clock, so a date is bounded by days, never by an age
|
||||||
|
|
||||||
|
An `age(min,max)` would make a seeded fixture change with the day it runs on, which is
|
||||||
|
what a seed exists to prevent; a birthdate for someone 20 to 60 is
|
||||||
|
`date(1966-01-01,2006-12-31,…)`, re-pinned as any fixture is.
|
||||||
|
|
||||||
|
## A name column without a key resolves inside its parent
|
||||||
|
|
||||||
|
A given name both sexes carry is a row under each, so `name` cannot be the key; the
|
||||||
|
parent's row tells the two apart, `sex[f].first-name[Kim]`, the ambiguity error spells
|
||||||
|
each row inside its parent, and a name repeating inside one parent row is refused at
|
||||||
|
load, since nothing could then select it.
|
||||||
|
|
||||||
|
## `misc` is what every locale shares
|
||||||
|
|
||||||
|
A category whose facts differ by country belongs in that country's locale, read from the
|
||||||
|
register that country's own records use; `misc` takes only sources that are
|
||||||
|
international. NHTSA vPIC and Mobility Sweden's registrations are national, so they are
|
||||||
|
for `en_US.car` and `sv_SE.car`, and `misc.car` waits for an international source.
|
||||||
|
|
||||||
|
## A register's canonical spelling loses to the one its domain writes
|
||||||
|
|
||||||
|
Where a source offers several spellings of one fact, the shipped one is what records in
|
||||||
|
that domain carry. `misc.timezone` reads `zone.tab` and not the `zone1970.tab` that
|
||||||
|
supersedes it, because the latter keeps one zone per set of countries that have agreed
|
||||||
|
since 1970: it spells Sweden `Europe/Berlin`, and no Swedish system writes that. For the
|
||||||
|
same reason `misc.language` takes the ISO 639-2 register's first synonym over CLDR,
|
||||||
|
which says "Chinese, Mandarin" for `zh`.
|
||||||
|
|
||||||
|
## `misc.territory` is the spine, and a `misc` table naming a territory links to it
|
||||||
|
|
||||||
|
Where every territory row has a child the column is a `parent`, and the import drops the
|
||||||
|
child rows whose territory the set does not ship — 17 of `misc.timezone`'s, Antarctica's
|
||||||
|
ten among them. Agreement across a record is worth more than the last rows of a table.
|
||||||
|
Where no such link can hold the fact stays a column. Layer your own `misc.territory`
|
||||||
|
over the shipped one and you must layer `misc.timezone` too, or the link fails at load
|
||||||
|
naming the row.
|
||||||
|
|
||||||
|
## A table whose register publishes no frequency draws evenly
|
||||||
|
|
||||||
|
`misc.httpmethod`, `misc.port`, `misc.httpstatus`, `misc.mimetype` and `misc.tld` weigh
|
||||||
|
every row alike, so GET is a ninth of the methods drawn. Goal 12 keeps an authored fact
|
||||||
|
out of a sourced table, and no register publishes how often a method, a port or a TLD is
|
||||||
|
used, so a weight here would be invented. Where one exists it is read, as
|
||||||
|
`misc.timezone` reads GeoNames populations and `sv_SE.first-name` SCB bearers.
|
||||||
|
|
||||||
|
## `misc.timezone` weighs a zone by the people living in it
|
||||||
|
|
||||||
|
The weight is the population GeoNames records in the zone's cities of 15,000 or more,
|
||||||
|
floored at 15,000, which goal 14 asks for: over 300 seeded draws of
|
||||||
|
`misc.territory[US].timezone`, the four zones most Americans live in took 278 where an
|
||||||
|
even weight gave them 44, and `America/Indiana/Petersburg`, a town of 2,400, fell from 17
|
||||||
|
to 0. Valid while a draw weighted this way lands where people live.
|
||||||
|
|
||||||
|
## `misc.port` selects by number, and carries no `name`
|
||||||
|
|
||||||
|
29 of its services sit on more than one port, `http-alt` on three, so
|
||||||
|
`misc.port[http-alt]` could name no one row. The number is what a port field holds
|
||||||
|
anyway, and `misc.port[443].service` reads the other way.
|
||||||
|
|
||||||
|
## `misc.car` is one flat table, not a make linked to its models
|
||||||
|
|
||||||
|
A row is a make and a model drawn together, so no render pairs a Volvo with a RAV4. Two
|
||||||
|
linked tables would reach the same pairs and add a selector nothing asks for; a make
|
||||||
|
alone is `misc.car.make`.
|
||||||
|
|
||||||
|
## `misc.territory` names its sovereign in a column, and there is no `misc.country` table
|
||||||
|
|
||||||
|
ISO 3166-1 codes territories, so `territory` is the honest name, and `is_independent` in
|
||||||
|
the register gives each one's state. A second table of the 195 sovereigns would hold a
|
||||||
|
`DK` row beside the territory `DK` row, both carrying Denmark's capital, currency, flag
|
||||||
|
and TLD — two owners for one fact, drifting at the next import. Splitting the columns to
|
||||||
|
avoid that is worse: put `capital` on the territory alone and a country row can no
|
||||||
|
longer name Copenhagen. The column cannot be a `parent`, since a table never reaches its
|
||||||
|
own family; a test proves every value names a row instead. A territory the register
|
||||||
|
records no state for stands alone, which is `EH` alone, and naming one for it would be a
|
||||||
|
claim fejkdata has no business making.
|
||||||
|
|
||||||
|
## `misc.loglevel` is a table keyed by the code, rendering POSIX's keyword
|
||||||
|
|
||||||
|
A flat list of names carries neither the code a PRI encodes nor a selector reaching it,
|
||||||
|
and goal 3 draws the two as one fact. The canonical spelling losing to the one its
|
||||||
|
domain writes, above, settles the rest: a configuration writes `info`, so RFC 5424's
|
||||||
|
`Informational` stays the `severity` column, and the code and the keyword hold the two
|
||||||
|
selector slots.
|
||||||
|
|
||||||
|
## `misc.tld` keys carry the leading dot, where other tables key on a bare code
|
||||||
|
|
||||||
|
The register spells a TLD `.se` and `misc.territory.tld` already ships it so, which a
|
||||||
|
bare key would make two spellings of one fact; `{/misc.tld}` also composes onto a host
|
||||||
|
with no separator. `misc.tld[se]` misses for it, which `todo.md` carries.
|
||||||
|
|
||||||
|
## `misc.tld` is a table of its own, and `misc.territory.tld` stays a column
|
||||||
|
|
||||||
|
A `parent` demands a child for every parent row, so linking them would drop every root
|
||||||
|
zone row naming no territory, which is most of them, and goal 12 holds a sourced table
|
||||||
|
whole. The loader refuses the link outright anyway: `tld` is a column of
|
||||||
|
`misc.territory`, and a table may not be named like a column of its ancestor.
|
||||||
|
|
||||||
|
## `misc.territory` carries a currency code, it does not link to `misc.currency`
|
||||||
|
|
||||||
|
A `parent` demands a child for every parent row, and ISO 4217 registers codes no
|
||||||
|
country's row can name: the funds codes (Mvdol, WIR Euro, US Dollar (Next day)), and VED
|
||||||
|
beside VES, both Venezuela's, of which a country row names one. Linking would trade the
|
||||||
|
register for the link, and `misc.territory.currency` already pairs a country with its
|
||||||
|
currency in one draw.
|
||||||
|
|
||||||
|
## An extension may name two media types
|
||||||
|
|
||||||
|
`.xml`, `.rtf`, `.sub`, `.mpp` and `.ac` each name two rows of `misc.mimetype`.
|
||||||
|
Separating them would mean dropping a registered type, or naming one by an extension
|
||||||
|
that is not its own — `.mpt` is Project's template, not its document. Selecting such a
|
||||||
|
name is an error listing both keys; select by type instead.
|
||||||
|
|
||||||
|
## The Swedish ids draw Skatteverket's test series
|
||||||
|
|
||||||
|
A Luhn-valid personnummer over a random birth number may be a living person's; 238 and
|
||||||
|
239 after any date are blocked from assignment, so the shipped `personnummer` and
|
||||||
|
`samordningsnummer` use those. They sit in a `birth-number` table under `sex` rather
|
||||||
|
than as a column of it: the render's shared draw of the family is what makes the number
|
||||||
|
and the name agree on sex, and `sex` stays one shape across locales instead of
|
||||||
|
collecting every sex-keyed id fact. A samordningsnummer's day, the birthday plus 60, is
|
||||||
|
drawn from 61 to 88, valid in every month, rather than computed from the date drawn.
|
||||||
|
|
||||||
|
## The US given names come from a mirror of the SSA file
|
||||||
|
|
||||||
|
ssa.gov refuses a client outside the US, so `names-us.py` reads a GitHub copy that ends
|
||||||
|
at 2020, which a count over the births since 1930 barely feels; `--names` takes the
|
||||||
|
official zip. The SSA's placeholder rows are top-1000 entries that name nobody, so the
|
||||||
|
import drops them by name rather than by a rank a regeneration would move.
|
||||||
|
|
||||||
|
## `List` advertises direct descents only
|
||||||
|
|
||||||
|
`region.municipality.locality` is listed, and `region.locality` resolves too but is not:
|
||||||
|
the set of every descent through a chain of five tables is every subsequence of it, and
|
||||||
|
the direct chain is the one a reader can predict from the tables' parents.
|
||||||
@@ -38,6 +38,8 @@ func checkNestedDrawGroup(fields map[string]node, group string) error {
|
|||||||
|
|
||||||
// drawCheck fences each template of a scope as a render of its own, remembering which nodes read a
|
// drawCheck fences each template of a scope as a render of its own, remembering which nodes read a
|
||||||
// reference path.
|
// reference path.
|
||||||
|
// docs/decisions.md#a-render-shares-one-reference-draw-per-category-per-group
|
||||||
|
// docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences
|
||||||
type drawCheck struct {
|
type drawCheck struct {
|
||||||
memo map[readsMemo]bool
|
memo map[readsMemo]bool
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,7 @@ func (c *drawCheck) checkDraws(path string, n node) error {
|
|||||||
|
|
||||||
// checkRecordDraws fences the columns of a record — a template compiled at the top without a
|
// checkRecordDraws fences the columns of a record — a template compiled at the top without a
|
||||||
// repeat — so a load proves the record view of it as well as the string view.
|
// repeat — so a load proves the record view of it as well as the string view.
|
||||||
|
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
|
||||||
func (c *drawCheck) checkRecordDraws(path string, n node) error {
|
func (c *drawCheck) checkRecordDraws(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok || !t.record {
|
if !ok || !t.record {
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ func (r *tableRead) selected(t *table) (tableSel, bool) {
|
|||||||
// read one consistent draw: a table rendered whole beside a path into the family,
|
// read one consistent draw: a table rendered whole beside a path into the family,
|
||||||
// a table one read draws that another pins, and two reads pinning different rows.
|
// a table one read draws that another pins, and two reads pinning different rows.
|
||||||
// The reads come sorted by group and path, so which pair is reported does not vary.
|
// The reads come sorted by group and path, so which pair is reported does not vary.
|
||||||
|
// docs/decisions.md#every-reference-path-into-one-family-selects-the-same-rows-per-render-and-group
|
||||||
func checkFamilies(reads []pathRead) error {
|
func checkFamilies(reads []pathRead) error {
|
||||||
for i, r := range reads {
|
for i, r := range reads {
|
||||||
if r.tr == nil {
|
if r.tr == nil {
|
||||||
@@ -155,6 +156,7 @@ func (r *tableRead) replay(d *pinSet) error {
|
|||||||
// checkFamilyPair refuses a table read whole beside a path into its family, and a
|
// checkFamilyPair refuses a table read whole beside a path into its family, and a
|
||||||
// table one read draws that the other pins, since which token renders first would
|
// table one read draws that the other pins, since which token renders first would
|
||||||
// then decide the row.
|
// then decide the row.
|
||||||
|
// docs/decisions.md#a-table-read-into-is-pinned-a-table-read-whole-draws-afresh
|
||||||
func checkFamilyPair(a, b pathRead) error {
|
func checkFamilyPair(a, b pathRead) error {
|
||||||
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
|
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
|
||||||
x, y := pair[0], pair[1]
|
x, y := pair[0], pair[1]
|
||||||
@@ -192,6 +194,7 @@ func (r *tableRead) drawnOf(pins *pinSet) *table {
|
|||||||
// away and through a repeat or a draw group too, a table of its own family: a row
|
// away and through a repeat or a draw group too, a table of its own family: a row
|
||||||
// rendered whole draws its row without pinning it, so the family would draw apart
|
// rendered whole draws its row without pinning it, so the family would draw apart
|
||||||
// from the row being rendered, whichever draws the reaching template holds.
|
// from the row being rendered, whichever draws the reaching template holds.
|
||||||
|
// docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route
|
||||||
func checkOwnFamily(t *template) error {
|
func checkOwnFamily(t *template) error {
|
||||||
own := t.table
|
own := t.table
|
||||||
if own == nil {
|
if own == nil {
|
||||||
@@ -227,6 +230,7 @@ func checkOwnFamily(t *template) error {
|
|||||||
|
|
||||||
// alternatives reports whether two reads sit in different rows of one table. Not clash: a row drawn
|
// alternatives reports whether two reads sit in different rows of one table. Not clash: a row drawn
|
||||||
// whole is held without its ancestors, since the render draws it over the whole table.
|
// whole is held without its ancestors, since the render draws it over the whole table.
|
||||||
|
// docs/decisions.md#the-rows-of-a-table-are-alternatives
|
||||||
func alternatives(a, b drawAt) bool {
|
func alternatives(a, b drawAt) bool {
|
||||||
found := false
|
found := false
|
||||||
a.alt.each(func(t *table, r int) {
|
a.alt.each(func(t *table, r int) {
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ var shippedFS embed.FS
|
|||||||
// 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{} // 64-bit only, per the README's Decisions
|
var _ [^uint(0)>>63 - 1]struct{} // docs/decisions.md#64-bit-targets-only
|
||||||
|
|
||||||
// ErrNoData is returned by New when no source is loaded at all.
|
// ErrNoData is returned by New when no source is loaded at all.
|
||||||
var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")
|
var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ func (m reachMemo) of(n node) int {
|
|||||||
|
|
||||||
// repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf
|
// repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf
|
||||||
// path, so nested repeats cannot build what one repeat may not.
|
// path, so nested repeats cannot build what one repeat may not.
|
||||||
|
// docs/decisions.md#the-repeat-cap-bounds-renders-not-bytes
|
||||||
func repeatCheck(path string, n node, mem reachMemo) error {
|
func repeatCheck(path string, n node, mem reachMemo) error {
|
||||||
if t, ok := n.(*template); ok && t.repeat > 1 && mem.of(n) > MaxRepeat {
|
if t, ok := n.(*template); ok && t.repeat > 1 && mem.of(n) > MaxRepeat {
|
||||||
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, mem.of(n), MaxRepeat)
|
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, mem.of(n), MaxRepeat)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
// heldCheck rejects every route to a held sibling name except the ones that read its
|
// heldCheck rejects every route to a held sibling name except the ones that read its
|
||||||
// draw. An expansion holds one draw of that name; anything else that renders it draws
|
// draw. An expansion holds one draw of that name; anything else that renders it draws
|
||||||
// again, and the two disagree.
|
// again, and the two disagree.
|
||||||
|
// docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences
|
||||||
func heldCheck(path string, n node) error {
|
func heldCheck(path string, n node) error {
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok || len(t.held) == 0 {
|
if !ok || len(t.held) == 0 {
|
||||||
@@ -181,6 +182,7 @@ func renders(n node, want, seen map[node]bool) bool {
|
|||||||
// level's held draw while rendering the level expands it afresh, so their values would
|
// level's held draw while rendering the level expands it afresh, so their values would
|
||||||
// disagree. Reads are compared in sorted order, so which pair is reported does not
|
// disagree. Reads are compared in sorted order, so which pair is reported does not
|
||||||
// depend on where the tokens sit.
|
// depend on where the tokens sit.
|
||||||
|
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
|
||||||
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
||||||
names := boundReaders(format, bound, refs)
|
names := boundReaders(format, bound, refs)
|
||||||
// Stable over one format-order scan, so two readers of one name (a token and a
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
||||||
@@ -231,6 +233,7 @@ func boundReaders(format string, bound map[string]string, refs map[string]refBin
|
|||||||
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
||||||
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
||||||
// error names the single-token spelling.
|
// error names the single-token spelling.
|
||||||
|
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
|
||||||
func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error {
|
func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error {
|
||||||
count := map[string]int{}
|
count := map[string]int{}
|
||||||
return eachToken(format, func(t ftoken) error {
|
return eachToken(format, func(t ftoken) error {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ func quotedLayout(a string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
// layoutArg is the Go layout a quoted arg holds, refused when unquoted or constant.
|
||||||
|
// docs/decisions.md#a-layout-is-always-quoted
|
||||||
func layoutArg(a string) (string, error) {
|
func layoutArg(a string) (string, error) {
|
||||||
if !quotedLayout(a) {
|
if !quotedLayout(a) {
|
||||||
bare := strings.Trim(a, `'"`)
|
bare := strings.Trim(a, `'"`)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func compileCategory(v any, name string, files *categoryFiles) (node, error) {
|
|||||||
|
|
||||||
// checkNotRows refuses a choice of templates sharing one format and one set of
|
// checkNotRows refuses a choice of templates sharing one format and one set of
|
||||||
// string fields: each item is a row, and the rows file is the spelling for that.
|
// string fields: each item is a row, and the rows file is the spelling for that.
|
||||||
|
// docs/decisions.md#the-choice-of-rows-fence-guards-a-data-files-root-and-requires-string-fields
|
||||||
func checkNotRows(items []any, name string) error {
|
func checkNotRows(items []any, name string) error {
|
||||||
var format string
|
var format string
|
||||||
var fields []string
|
var fields []string
|
||||||
|
|||||||
+2
-2
@@ -84,8 +84,8 @@ func linkRefs(root map[string]node) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// linkTemplateRefs binds one template's references against root, refusing one that names the
|
// linkTemplateRefs binds one template's references against root, refusing one that names the
|
||||||
// category it sits in: a category is a unit, and a reference back into it describes a draw other
|
// category it sits in.
|
||||||
// than the fields beside it.
|
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
|
||||||
func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error {
|
func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error {
|
||||||
names := refTokens(t.format)
|
names := refTokens(t.format)
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ func setTablePaths(root map[string]node) {
|
|||||||
// linkTables binds every table's parent to the table beside it, and proves the
|
// linkTables binds every table's parent to the table beside it, and proves the
|
||||||
// links: a parent has a key, every link cell is one, every parent row is linked
|
// links: a parent has a key, every link cell is one, every parent row is linked
|
||||||
// to, no chain of parents closes, and no child is named like a parent's column.
|
// to, no chain of parents closes, and no child is named like a parent's column.
|
||||||
|
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
|
||||||
func linkTables(root map[string]node) error {
|
func linkTables(root map[string]node) error {
|
||||||
var walk func(dir string, children map[string]node) error
|
var walk func(dir string, children map[string]node) error
|
||||||
walk = func(dir string, children map[string]node) error {
|
walk = func(dir string, children map[string]node) error {
|
||||||
|
|||||||
@@ -19,17 +19,6 @@ Self-sufficiency 5.6), every seat capped by Locality or Self-sufficiency.
|
|||||||
|
|
||||||
This round, in order:
|
This round, in order:
|
||||||
|
|
||||||
- Move the README's Decisions section to `docs/decisions.md`, leaving a one-line index
|
|
||||||
of the titles in `AGENTS.md`, and drop the `AGENTS.md` line pointing decisions at the
|
|
||||||
README. Give each fence a bare link to the entry it enforces: every seat needed
|
|
||||||
"The rows of a table are alternatives", "The expansion hold and the render's draws
|
|
||||||
are two fences" or "A table read into is pinned" and found it by grepping 470 lines.
|
|
||||||
Carry the timezone weight's premise in with it: weighting by GeoNames city
|
|
||||||
population moved 300 seeded draws of `misc.territory[US].timezone` from 44 landing on
|
|
||||||
the four zones most Americans live in to 278, and dropped `America/Indiana/Petersburg`
|
|
||||||
(pop. 2,400) from 17 draws to 0. Carry in the decision `linkTemplateRefs`'s doc holds
|
|
||||||
today, which no entry records: a category is a unit, so a reference back into it
|
|
||||||
describes a draw other than the fields beside it, which is what goal 3 asks for.
|
|
||||||
- Keep reference bindings out of `template.fields`, so no walk over a template's fields
|
- Keep reference bindings out of `template.fields`, so no walk over a template's fields
|
||||||
needs the `isRef` filter `named`, `paths`, `recordColumns` and `template.field` carry.
|
needs the `isRef` filter `named`, `paths`, `recordColumns` and `template.field` carry.
|
||||||
- Compile a template's format once: `compileFormat` runs again from `linkTemplateRefs`,
|
- Compile a template's format once: `compileFormat` runs again from `linkTemplateRefs`,
|
||||||
@@ -285,6 +274,8 @@ because pairing a street with its exact postnummer rewrites shipped rows.
|
|||||||
|
|
||||||
## Not release-bound
|
## Not release-bound
|
||||||
|
|
||||||
|
- Give every entry in `docs/decisions.md` the goal it serves, its date, who made it and
|
||||||
|
the premise it rests on; propose a goal for an entry that serves none.
|
||||||
- Hold every change the charter says owes a `CHANGELOG.md` entry to one in CI: the
|
- Hold every change the charter says owes a `CHANGELOG.md` entry to one in CI: the
|
||||||
check covers `data` and `testdata/shipped_shape.txt` alone, where `AGENTS.md` adds a
|
check covers `data` and `testdata/shipped_shape.txt` alone, where `AGENTS.md` adds a
|
||||||
flag, an exit code, an exported name, a fence, a builtin and the lowest Go.
|
flag, an exit code, an exported name, a fence, a builtin and the lowest Go.
|
||||||
|
|||||||
@@ -27,6 +27,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
|
||||||
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user