Skip to content

feat(language): a lookup says whether a key with no row was meant - #443

Closed
FBumann wants to merge 8 commits into
claude/lookup-relations-vhvfjdfrom
claude/math-spec-pr-437-concept-b1rtku
Closed

FBumann wants to merge 8 commits into
claude/lookup-relations-vhvfjdfrom
claude/math-spec-pr-437-concept-b1rtku

Conversation

@FBumann

@FBumann FBumann commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Doesn't a period to snapshot lookup need one to one?" — "But it might be a valuable one…? Related to #245?" — "How would the coverage look like on lookups? Per column, per key? Per value?" — "Do this as a stacked draft pr?"

Note

The following content was generated by AI. Stacked on #437; the base branch is its head. The lookup half of #245, restated for a relation with a key. The parameter half stays in #245.

What this changes

A keyed lookup takes coverage: total | masked, default total: every key tuple has a row, and the consumer that binds the table refuses one short of a key. masked says the gap is meant. A bare relation declares neither, and a bare where: on a total lookup is refused, since it masks nothing.

lookups:
  gen_bus: { over: [generator, bus], key: generator } # total: every generator is on a bus
  line_to: { over: [line, bus], key: line, coverage: masked } # an open end is meant
  zone_of: { over: [generator, period, zone], key: [generator, period] } # total over generator × period
  connection: { over: [generator, bus] } # no key, so no coverage: the rows it has

Per key, not per column or per value. The key is the one column set where "every tuple of the product has a row" is the claim anyone wants: it is the parameter rule one axis over, so total means the same on both blocks. Per column is weaker on a composite key, and per value column ("every period has a snapshot") is a claim about the other dimension's labels rather than about the table.

Two refusals, both decided by the file alone.

Lookup 'connection' declares no key, so nothing is there for 'coverage:' to be total over — a bare relation is the rows it has. Drop the coverage line, or declare key: for the columns each row is identified by.
Constraint 'wired': 'gen_bus' is total, so a row exists at every ['generator'] and the mask has no effect. Remove it, or declare coverage: masked on the lookup if a key may have no row.

The second follows the precedent of a bare dimension name in a where, which is refused for the same reason. It is the one decision here that could be dropped without touching the rest; say so and it goes.

What consumers see. LookupDeclaration.coverage is Coverage | None: total or masked for a keyed table, None for a bare relation, which answers for no coverage the way #245's block-owned parameters do. Namespace carries it too, since the where refusal reads it. coverage is not typeset, the precedent dtype sets, and the golden .out files are byte-identical.

Coverage moved. Four fixtures test the bare where: name form on a keyed lookup and now declare masked: the golden model's zone_of, the dimension tests' gen_bus and gen_zone, and the lowering test's zone_of. No example in examples/ uses a bare lookup name in a where, so none changed.

Every guard deleted in turn

Each on a clean tree at 31a6595, restored with git checkout -- and __pycache__ dropped on both sides; the tree came back clean and the suite at 1211 passed.

Mutation Result
the bare-relation refusal never fires 2 failed, 1209 passed
the bare-where-on-total refusal never fires 1 failed, 1210 passed
the unwritten default flips to masked 9 failed, 1202 passed
lowering drops the coverage 2 failed, 1209 passed
a bare relation answers total instead of None 16 failed, 1195 passed
Verified

uv venv on Python 3.13 with ruff==0.16.1 and pyrefly==1.2.0; pixi is blocked here. On 31a6595 over the #437 head: pytest -q -n auto 1211 passed, 6 skipped (the two TeX compilers). ruff check, ruff format --check, reuse lint, prettier --list-different on every docs page: clean. mkdocs build --strict passes with the python.org inventory removed, which the proxy answers 403. Every generator re-run (tools.schema, tests.typesetting.golden, tools.notation, tools.gallery, tools.spec_math, tools.home_math); the schema and notation.md moved, nothing else. The two messages quoted above are the loader's own output on a model built to trigger them.

Not run: compile-tex; pixi run ci itself. pyrefly check reports 11 errors in this venv, the same 11 on the base branch (unused ignores and no yaml stubs), so they are the environment's. typos flags typ in five places, none of them in a file this PR touches.

Why

key: says at most one row per key tuple, never at least one. A snapshot with no period, a generator on no bus, are rows lost in preparation, and today the language reads each as "belongs to no group" and drops its terms from every sum silently. That is the data-safety hole cardinality does not close; totality does, and the two compose: a key plus total is a total function.

Stacked on #437 because that PR changes what "total" is over: a composite key is total over the product of its dimensions, and a bare relation has nothing to be total over, which is the refusal above.

🤖 Generated with Claude Code

https://claude.ai/code/session_016LDbfiWuoU2U8g8vV6iWq5


Generated by Claude Code

Superseeds #438 #430 #256

…the direction each call names

`over:` lists the columns, `key:` is the claim that makes the table a
map, and `from=`/`to=` on the call say which column an operator
consumes and which it produces; the other key columns are joined on.
A one-key, one-value table still reads `sum(p, by=gen_bus)` and
`at(x, by=gen_bus)` unchanged. Without a key the table is a bare
relation: `sum` walks it with both ends named, a bare `where` tests it,
and `at`, `shift` and `position` refuse it. Two columns over one
dimension are named by role, which is how a self-map is declared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…oduct or is read at two columns at once

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…the row, and the golden model renders it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…ion walks the one key column over its dimension

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…so one calendar table serves every granularity

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…a key nor a column name claims a dimension it is not over

A partition lands nothing, so its group columns are not checked as
dimensions the call produces. A key names one column per dimension,
since a frame carries each once. A column named like a dimension is
over that dimension. GroupSum and At hold their walks alone and read
over, coordinate and into off them; a Walk holds its LookupDeclaration,
which is the one home of a lookup's roles, values and column dims.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
…and each cardinality names its declaration

The lookups reference says what `key:` means in uniqueness terms, that a
composite key leaves each column non-unique on its own, and which
declaration says many-to-one, one-to-many and many-to-many. One-to-one is
named as a claim the language does not have.

Page measure after the change (docs-writing script): 78 sentences, median 25
words, 38 over 25; the two new paragraphs add sentences of 7 to 25 words.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016LDbfiWuoU2U8g8vV6iWq5
…ator lost in preparation is not read as one on no bus

`coverage: total | masked` on a keyed lookup, default `total`: every key
tuple has a row, over the product of the key columns' dimensions, and the
consumer that binds the table refuses one short of a key. `masked` says the
gap is meant. A bare relation has no key to be total over and declaring
`coverage:` on it is refused at load, as is the bare `where: name` on a
`total` lookup, where every key has a row and the mask selects nothing.

The program carries it on `LookupDeclaration.coverage`, `None` for a bare
relation. `coverage` is not typeset, the precedent `dtype` sets.

Coverage moved: the golden model's `zone_of`, the dimension tests' `gen_bus`
and `gen_zone`, and the lowering test's `zone_of` declare `masked`, since
each tests the bare where form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016LDbfiWuoU2U8g8vV6iWq5
@FBumann
FBumann added this pull request to stack #439 September 9, 2026 20:40
@FBumann
FBumann removed this pull request from stack #439 September 10, 2026 06:46
@FBumann
FBumann added this pull request to stack #448 September 10, 2026 06:46
@FBumann
FBumann force-pushed the claude/lookup-relations-vhvfjd branch from 2546a8a to 75840fd Compare September 10, 2026 08:50
@FBumann FBumann closed this Sep 11, 2026
@FBumann
FBumann removed this pull request from stack #448 September 11, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants