From 9f00ad039e7bf1a9299bf252276570848075aa7f Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 24 Sep 2026 13:41:52 +0200 Subject: [PATCH] Move the decisions to docs/decisions.md, indexed in AGENTS.md and linked from the fences they back --- AGENTS.md | 91 ++++++- README.md | 485 +---------------------------------- docs/decisions.md | 640 ++++++++++++++++++++++++++++++++++++++++++++++ drawfence.go | 3 + family.go | 4 + fejkdata.go | 2 +- graph.go | 1 + holdfence.go | 3 + layout.go | 1 + node.go | 1 + reference.go | 4 +- table.go | 1 + todo.md | 13 +- value.go | 1 + 14 files changed, 759 insertions(+), 491 deletions(-) create mode 100644 docs/decisions.md diff --git a/AGENTS.md b/AGENTS.md index 6cbd2a3..6c05dd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,9 +14,98 @@ lowers the last score. - 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. -- 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 before paraphrasing it, and name the register you read before claiming none publishes a spelling. - 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. + +# 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 diff --git a/README.md b/README.md index c8d3bf5..08269f1 100644 --- a/README.md +++ b/README.md @@ -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 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 -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 | | |------|--| @@ -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 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 template nor a path, or an inline template that does not compile. From a checkout: `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 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 -[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 @@ -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 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 -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 `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 string can't — only a `format` — is rejected naming the string, as is a one-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 tests nothing; the caller who needs a value that can touch nothing asks for it 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 @@ -1357,7 +890,7 @@ data.go the load path: fs.FS folders/files -> merged namespace tree, and cmd/fejkdata/ the fejkdata CLI 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) -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 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 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 -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 and tests — so run it locally before pushing. The lowest Go builds the `portable` stage, all of that bar the format check: diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..5729990 --- /dev/null +++ b/docs/decisions.md @@ -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. diff --git a/drawfence.go b/drawfence.go index ecbffa4..1e30b7c 100644 --- a/drawfence.go +++ b/drawfence.go @@ -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 // 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 { 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 // 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 { t, ok := n.(*template) if !ok || !t.record { diff --git a/family.go b/family.go index fad404e..2bfd352 100644 --- a/family.go +++ b/family.go @@ -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, // 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. +// docs/decisions.md#every-reference-path-into-one-family-selects-the-same-rows-per-render-and-group func checkFamilies(reads []pathRead) error { for i, r := range reads { 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 // table one read draws that the other pins, since which token renders first would // 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 { for _, pair := range [][2]pathRead{{a, b}, {b, a}} { 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 // 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. +// docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route func checkOwnFamily(t *template) error { own := t.table 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 // 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 { found := false a.alt.each(func(t *table, r int) { diff --git a/fejkdata.go b/fejkdata.go index 3bcb894..ac2fbfa 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -21,7 +21,7 @@ var shippedFS embed.FS // any one path; the CLI's --repeat shares it. 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. var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS") diff --git a/graph.go b/graph.go index d0fcf79..c17f881 100644 --- a/graph.go +++ b/graph.go @@ -206,6 +206,7 @@ func (m reachMemo) of(n node) int { // repeatCheck bounds the renders a repeat multiplies to along any root-to-leaf // 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 { 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) diff --git a/holdfence.go b/holdfence.go index 562c915..cab1feb 100644 --- a/holdfence.go +++ b/holdfence.go @@ -9,6 +9,7 @@ import ( // 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 // 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 { t, ok := n.(*template) 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 // disagree. Reads are compared in sorted order, so which pair is reported does not // 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 { names := boundReaders(format, bound, refs) // 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 // {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The // 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 { count := map[string]int{} return eachToken(format, func(t ftoken) error { diff --git a/layout.go b/layout.go index a389bc8..69f26c1 100644 --- a/layout.go +++ b/layout.go @@ -34,6 +34,7 @@ func quotedLayout(a string) bool { } // 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) { if !quotedLayout(a) { bare := strings.Trim(a, `'"`) diff --git a/node.go b/node.go index cf3134b..4179d69 100644 --- a/node.go +++ b/node.go @@ -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 // 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 { var format string var fields []string diff --git a/reference.go b/reference.go index 7b6a510..92e8559 100644 --- a/reference.go +++ b/reference.go @@ -84,8 +84,8 @@ func linkRefs(root map[string]node) error { } // 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 -// than the fields beside it. +// category it sits in. +// 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 { names := refTokens(t.format) if len(names) == 0 { diff --git a/table.go b/table.go index 23a9081..f9c2431 100644 --- a/table.go +++ b/table.go @@ -358,6 +358,7 @@ func setTablePaths(root map[string]node) { // 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 // 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 { var walk func(dir string, children map[string]node) error walk = func(dir string, children map[string]node) error { diff --git a/todo.md b/todo.md index 40d5d2d..b1e9c44 100644 --- a/todo.md +++ b/todo.md @@ -19,17 +19,6 @@ Self-sufficiency 5.6), every seat capped by Locality or Self-sufficiency. 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 needs the `isRef` filter `named`, `paths`, `recordColumns` and `template.field` carry. - 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 +- 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 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. diff --git a/value.go b/value.go index c6f78ca..148e1b0 100644 --- a/value.go +++ b/value.go @@ -27,6 +27,7 @@ type valueProof struct { // 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. +// docs/decisions.md#a-column-of-one-reference-alone-is-the-column-it-reads func (p *valueProof) checkDatatype(path string, n node) error { t, ok := n.(*template) if !ok || t.datatype == DataTypeString {