Changelog
All notable changes to this project are documented here. Format follows Keep a Changelog; versioning is semantic.
[6.0.0] - 2026-08-05
First stable release of the rewrite. No code changes since 6.0.0b1 —
this promotes the beta after review.
The alpha and beta series ran while aggregate_api adopted the engine; that
adoption is settled, so the surface is now fixed. From here 6.x is additive:
new TableSpec / HouseStyle fields and new renderers are minor bumps,
anything that changes an existing rendering is not.
Changed
Development Statusclassifier moved from4 - Betato5 - Production/Stable.Docs URL is greater-tables-project.readthedocs.io, not the
greater-tablesslug earlier entries promised. A Read the Docs slug is fixed when the project is created and cannot be renamed, so 6.0 publishes under the existing project. Moving it means a new RTD project and new release metadata, which is a job for the next version — see[rtd]indev/pending.md. Anygreater-tables.readthedocs.iolink in an entry below this one is dead; use the URL above.
The contract, stated once
ir_versionis 1 and stays 1 for all of 6.x. Readers must ignore unknown fields, so additive IR fields can only ever be advisory; anything content-bearing needs a version bump. The schema is committed atschema/ir-v1.jsonand diff-guarded by the test suite.greater_tables5.3 is frozen and stays on PyPI forever. Pingreater-tables<6to stay there; the import name is identical on both, sofrom greater_tables import GTworks either way.docs/migration.mdis the complete option-by-option map, including the three silent default changes (formattersints now group,show_indexauto-hides,header_rowdefaults off).Python 3.12+. Runtime dependencies are numpy, pandas, pydantic and PyYAML; the JS walker has none.
Known gaps, carried into 6.x
No CI yet. 428 tests pass locally and the schema and walker transcripts are guarded by the suite, but nothing runs on push. The 3.12 floor is therefore declared rather than matrix-verified. See
[ci]indev/pending.md.HouseStyle.vrule_widthsis a 3-tuple with only 2 usable slots, andhrule_widths/vrule_widthshave no Python route to the HTML weights — both indev/pending.mdunder[formats].Uppercase
G/Eformat sugar is still unparsed.
[6.0.0b1] - 2026-08-05
Release candidate in all but name: the last look before 6.0.0 goes public
and onto PyPI. No engine changes — this is the public face.
Changed
Python floor raised to 3.12.
requires-python = ">=3.12", classifiers now 3.12 / 3.13 / 3.14. The old 3.11 floor was declared but never tested, and 3.11 reaches end of life before this package’s first stable release would benefit from it. Note: 3.12 is likewise declared and not yet matrix-tested — that is what[ci]indev/pending.mdis for.transition.mdis gone, folded intodocs/migration.md. The complete 5.x → 6.0 option map — everyConfiguratorfield, the fullHouseStyletable, vertical rules, width machinery, porting recipes — was sitting in a root-level file that no page linked to and the sdist did not ship, whiledocs/migration.rstcovered the same ground more thinly. Now one document, in the docs, in the sdist. It is MyST markdown rather than reStructuredText because it is twelve wide reference tables and RST grid tables would be unmaintainable;myst_parserwas already enabled for the changelog.Installation text no longer says “6.0 is not on PyPI yet” (README and the docs landing page).
Fixed
docs/migrationsaid a bare int does not group. It does:formatters={'paid': 2}gives1,234.57. That is the one silent behavior change from 5.x and the page stated the opposite of the truth. Now a warning box, alongside the two other silent default changes (show_index,header_row).Two places claimed CI that does not exist —
README.mdanddocs/ir.rstboth said the schema and transcripts were guarded “in CI”. They are guarded by the test suite;design.rsthad it right.docs/ir.rstlisted theFormatKindvalues withoutgen(added in a11).docs/api.rstomittedload_styleand__version__from the documented exports, and never documentedrender.widths.README’s status block still read
6.0.0a1and “all seven design steps”, and its layout listing predated thetext-asciiandtikzgolden trees.“Licence” → “License” in README and the docs landing page (US spelling).
Added
The target-aware display bundle is now described where users will meet it:
README.md, the docs landing page,design.rst(with the measured reasoning behind the PDF path droppingtext/html), and migration’s “New in 6.0”.design.rstdocumentsrender/notebook.pyand the two-charset golden trees, and states plainly that the display-bundle probe is run by hand rather than in the unit suite.
Notes
JupyterLab verified by hand (2026-08-05) — a live notebook takes
text/html, as inferred. That was the one unmeasured consumer left over from the a9 probe, and it is now closed.CLAUDE.mdis untracked as of this release; it is a personal operating procedure, not project documentation.CHANGELOG entries before this one still reference
transition.md. They are a historical record of what was true at the time and have been left alone.
[6.0.0a11] - 2026-08-05
Item 1 of the [formats] batch in dev/pending.md, lowercase half.
Added
gformat sugar, in both dialects —'.3g'and'{x:.3g}', analogous to'.3f'. NewFormatKindmember'gen'.digitson agenspec counts significant figures, not decimal places, because that is what python’sgpresentation type means:'.3g'renders 1234.5 as1.23e+03and 0.0012345 as0.00123, where'.3f'would give three decimals in both cases. Bare'g'takes python’s own default precision of 6.groupstill applies, so',.9g'gives1,234,567— visible only whilegstays in its fixed-point form.The JS walker maps
gento.NgincgFormatSpec, keeping the two implementations in lockstep.
Changed
schema/ir-v1.json: theFormatKindenum gains"gen". Additive and advisory, soir_versionstays 1 — but note the asymmetry: documents written by an older greater_tables still validate here, while a document usinggenfails an older copy of the schema.
Notes
Uppercase
GandEremain unparsed, and stay indev/pending.md. They are the awkward half — either two more enum members or anupper: boolon everyFormatSpec.
[6.0.0a10] - 2026-08-05
dev/plan-display-bundle.md, work item [ascii-rules]. Additive: defaults
are untouched and every existing golden is byte-identical.
Added
HouseStyle.text_ascii_rules(default False) — draw the text rendering with ASCII only. Motivated by two measured problems: Pygments lexes U+2501 as an error token, so a Sphinx LaTeX build emitsLexing literal_block … resulted in an erroronce per table and styles the rule red in the PDF; and a cp1252 console cannot encode box drawing at all.render.text.Charset, withUNICODE_CHARS(default) andASCII_CHARS. Every non-ASCII character the renderer draws now lives in one frozen dataclass instead of eight literal sites.tests/golden/text-ascii/— a full parallel golden tree, one file per fixture, written bytests/make_goldens.pyalongside the existing ones.
Notes
The ASCII set is
=heavy,-light,|stub divider,<<flag>>row gutter. Heavy and light stay distinct: collapsing━and─both to-would make a frame rule and an internal group break identical, and which is which is information the conformance oracle should not lose.The row-flag delimiters (
«total») were the eighth site, and are easy to miss — they sit in a right gutter, not in the rules.Scope is what the renderer draws, not cell text. Values arrive already formatted, so
FormatSpec.null(an em dash) and the SI suffix ladder (µ) still come through and ASCII-mode output is not necessarilyisascii(). Laundering them here would make the oracle disagree with the document it is checking; change them on the spec side if a build must be pure ASCII end to end.tests/test_text.pyasserts this limit so it stays deliberate.
[6.0.0a9] - 2026-08-05
dev/plan-display-bundle.md, work items [plain-in-bundle],
[quarto-target] and [bundle-policy] — one coherent change to what GT
advertises to a display front end. Every decision here comes from a measured
probe of five consumers (Sphinx html/latex, Quarto html/latex/gfm), which
lives at dev/tools/output-modes/, not from reasoning about what ought to
happen.
The bundle rules are now:
target |
keys published |
|---|---|
Quarto PDF ( |
|
any other Quarto render, or none |
|
caller passed |
exactly what was asked for |
Added
text/plainin every mimebundle. It is the one type all five measured consumers render, so a table degrades to the monospace rendering rather than to silence. This alone fixes Sphinx HTML and Sphinx LaTeX, which prefertext/plainand previously fell through to the default repr — with no caller-side change. Measured identically under Sphinx 8.2.3 and 9.1.0.GT.__repr__returns the text table.__str__already did; without the pair, any consumer falling back torepr()showed<greater_tables.compat.GT at 0x…>. This is what makesGTusable from a plain script or REPL.render.notebook.quarto_target()→'pdf' | 'other' | None.'other'deliberately is not'html':QUARTO_FIG_FORMATispngfor both html and gfm, so the two are indistinguishable from the environment and the return type says so rather than hiding it.GT(..., mimes=...), threaded through tomimebundle.Nonedetects; an explicit tuple publishes exactly those types and suppresses detection. Members are validated at construction, so a typo raises instead of silently publishing a bundle nothing can display. It exists for the one target that cannot be auto-detected —quarto render --to gfm, where pandoc’s commonmark writer prefers html and leaks a raw<div class="gt">into the markdown. A whole-document policy, so the intended form isrender = partial(GT, mimes=('text/plain',)).
Changed
Both of these change output for anyone relying on the old bundle.
An HTML render no longer receives
text/latex.in_quarto()is true for anyQUARTO_*variable, soquarto render --to htmlwas computing and shipping a TikZ payload that was never used — the exact compute-TikZ-then-discard waste this module’s design was meant to have fixed, reintroduced one level up. Thetext/latexdecision now routes throughquarto_target().in_quarto()is unchanged and still exported. Net effect on an HTML or notebook render is a speed gain: the TikZ pass that disappears costs more than the text pass that arrives.A Quarto PDF render no longer receives
text/html. A{html, plain}bundle renders nothing at all under Quarto latex — pandoc picks the html and the LaTeX writer discards it, silently. Omitting html means a TikZ failure degrades to text rather than to an empty page.
Notes
mimebundle’s existingtikz=flag is untouched:Truestill means “also give me TikZ” and keepstext/html. It is a forcing knob, not a target declaration;mimes=is the precise control.render_textruns on every render now, and is bounded byTableSpec.max_rows(default 200) like every other renderer.Not measured: JupyterLab itself, which needs a live front end. It is expected to behave as Quarto HTML does and prefer
text/html, but that is an inference, not an observation. Worth one manual check before 6.0.0.
[6.0.0a8] - 2026-08-03
Steps 3 and 4 of dev/plan-column-widths.md. 5.x’s max_table_inch_width
finally has an heir, and it works in TikZ and HTML by the means each medium
actually has.
Added
HouseStyle.max_table_width_em(default None) — a total width to aim for. A target, not a guarantee, and the docstring leads with that: columns that cannot wrap hold their natural width whatever the budget says. What the budget can do is narrow the prose columns, and then:TikZ solves them with
render.widths— one shared quantile, so every prose column carries the same chance of wrapping — and fills or gives back the remainder proportionally so the budget is met, not merely respected. When even the floors do not fit,tikz_scalesteps down a coarse ladder (1.0, 0.95 … 0.6) picked with the measuredwidth(s) = s·T + Smodel, and says so throughlogging. Past 0.6 it warns, names the shortfall, and suggestssidewaysfigurerather than shrinking into illegibility. Verified by compiling: every fixture lands at or under its budget, within 0.1em wherever narrowing was possible.HTML emits
--gt-max-widthand lets the browser do it. No solver runs there and none should:gt-nowrapalready pins the columns that must not be narrowed, so the browser narrows exactly the prose ones — with the real font, not our estimate of it.
render.tikz.table_width— the emitted-width model in one place, shared by the autoscale arithmetic and the tests.
Changed
HouseStyle.tikz_prose_max_emis renamedmax_prose_width_em— it reads as a sibling ofmax_table_width_emnow, and it is no longer TikZ-only: HTML emits it as--gt-prose-max. One field, both renderers.--gt-prose-maxdefaults to 18em, was 30em, so the two renderers agree on one number. 18em is also the better number on its own merits — roughly 65 characters, the measure prose is comfortable at, where 30em is nearer 110. HTML tables with prose columns will render narrower. The markup is unchanged, so no DOM golden moved; put.gt { --gt-prose-max: 30em }in your stylesheet for the old look.A prose column’s floor is now its widest single word, and the cap is a hard ceiling that only an unhyphenatable word may beat.
Notes
Equal risk cannot narrow a column whose cells all overrun the prose cap: every quantile lands on the cap, so the bisection has nothing to bite on. Such a column is wrapping whatever happens and narrowing it only costs height, so the remaining slack is taken proportionally instead. Without this, a table of long prose ignored its budget entirely and went straight to scaling.
transition.md’s “Width machinery — all gone” section is now out of date —max_table_inch_widthandtable_width_modehave partial heirs. Left alone deliberately: that file has uncommitted edits.
[6.0.0a7] - 2026-08-03
Step 2 of dev/plan-column-widths.md: the allocator, plus the measurement
that unblocks the TikZ budget. No renderer consumes this yet — behavior
is unchanged in every output format.
Added
render/widths.py— column-width allocation under a total-width budget, ported from the csv-grid solver the JS walker already carries.solve_widths(samples, floors, budget, mode)returns anAllocation(widths plusnatural/solved/over). Two modes:equal-risk, which bisects for the one quantile every column is measured at so all columns carry the same overflow probability, andcoverage, which water-fills along each column’s concave envelope to fit the most cells outright. Integers in, integers out, unit-agnostic — milli-ems for TikZ, characters for the text renderer later. The budget is text width only; separations are the caller’s geometry.The port differs from csv-grid in one respect, and it is the caller’s world, not the math: csv-grid narrows a column by truncating, so its floor is a minimum readable width; a typeset table narrows by wrapping, so the floor is the widest unbreakable token. Same solver, different floors.
js/solver-check.mjsandtests/test_widths.py— the Python port and the walker’s copy must allocate identically, checked on 10 column shapes × both modes. Alongside the existing csv-grid drift check, the three copies of this algorithm are now pinned to each other.
Notes
tikz_scaledoes not scale a table uniformly, which the plan had assumed it might. It is a node option, so node contents scale while the matrixcolumn sepdoes not. Measured across six scales, width is exactly linear —width(s) = s·T + S, withT= Σ text widths + 2 × column sep (column 1’sinner xsep, which scales) andS= ncols × column sep. Predictions matched measurement to 0.1pt at every step, so an autoscale ladder can pick its step in advance; it just has to use this model rather than assumingwidth(s) = s · width(1).
[6.0.0a6] - 2026-08-03
Fallout from rendering 6.0.0a5 through Quarto rather than only through
Etcher: the estimate was right and the margin was not.
Fixed
Columns are no longer sized to the estimate exactly.
Sub 1measures 2.306em and got a 2.31em column — a 0.17% margin.Etchercleared it; the same table in a Quarto document, whose preamble brings microtype and friends, broke the line. Every measured requirement now carriesrender.tikz._WIDTH_PAD, 0.1em, so line breaking stops depending on the host document’s package list. This is what the stale+ source paddingcomment on that code had always claimed._BOLD_PERCENT115 → 118. Measured over the fixture corpus in newtx, bold/roman is 1.000 at the median but 1.130 at p99, so real header words (return,Incurred) could exceed the old factor and hyphenate. Digits are the same width in both weights, so the common numeric header is unaffected.
Notes
The width estimate is Times metrics, and that is now documented in
render/textlength.py. It is exact againstnewtxtext— whatEtchercompiles with — and roughly 11% tight against Latin Modern, the LaTeX default a plain Quarto PDF uses, where long headings will hyphenate. Load a Times-metric serif alongside the tables or accept slightly tight columns;dev/tools/measure_glyphs.pyre-measures any font. A per-font widening factor is the obvious answer and is deliberately deferred to the width budget work, where it has somewhere to live.\usepackage{newtxmath}collides with Quarto’s math setup (\arrowvert already defined) —newtxtextalone is what the width estimate needs.
[6.0.0a5] - 2026-08-03
Step 1 of dev/plan-column-widths.md: make the TikZ width estimate
trustworthy, which everything else in that plan rests on. No new API.
Fixed
TikZ used TikZ’s balanced-ragged alignment, which breaks lines that fit. Columns were emitted with
align=left/right/center, whose limited\rightskipstretch makes TeX wrap a cell set to exactly its own natural width — measured, a cell needed roughly 0.1em of slack per interword space, soSub 1in a 2.31em column came out asSub/1. The flush variants (align=flush left, …) are unlimited raggedness and honor the estimate exactly: across every multi-word string in the fixtures, required slack went from 0.10–0.20em to zero. This is also what the table-leveltext badly raggedwas already asking for.30 characters were missing from the TextLength glyph table and silently took the 600 milli-em default. The three that matter most to a real table were the most wrong:
—(the missing-value marker, actually 1000),%(every ratio column, 833) andµ(every SI-suffixed column, 500).-was also wrong, 500 against a measured 333, so date columns were over-reserved. Values measured against newtx; the rest of the table was already exact (median actual/estimate 1.000 over 596 fixture strings).
Changed
A column header no longer stretches its column. Header cells have a text width and wrap, so a heading now floors its column at its widest word rather than its full one-line width:
Cumulative Incurred Lossover six-em numbers took 12.0em and now takes 5.4em, wrapping instead. Header cells spanning one column are all measured this way, at any level.Spanning labels are measured at all. An overlaid group label or body
colspancell carries no text width and cannot wrap, so it now widens the last column it covers if the span — including the separations it crosses — cannot hold it. Previously spans contributed nothing to width and overflowed silently; the text renderer has always done this.TikZ width arithmetic is integer milli-ems throughout, divided only at emit time, extending the reproducibility discipline already documented in
render/textlength.pyto the column geometry itself.
Added
render.textlength.token_milli/token_display_len— width of a string’s widest unbreakable token. TeX will not hyphenate inside a word, so this is the floor for any wrapping column;$...$counts as one token.dev/tools/measure_glyphs.py— compiles a probe that measures every glyph in the table against real font metrics and reports what is missing or off. Reports only; corrections are pasted in deliberately so the committed table stays a reviewed artifact.
Notes
Every TikZ golden moved (alignment keyword on every column, plus the width corrections). IR, text, DOM transcripts and CSS are byte-identical.
[6.0.0a4] - 2026-08-01
Step 2 of two: house rules for formats. 5.x’s default_*_str settings come
back as real fields, so one YAML file can govern every table in a document
instead of a per-column dict in every call.
Changed
ratio_colsrenders as percent again —FormatSpec(kind='pct', digits=1), restoring 5.x’sdefault_ratio_str = '{x:.1%}'. GT2 had been producingdec/3, so0.4576showed as0.458instead of45.8%; bothtransition.mdanddocs/migration.rstalready claimed percent, so the docs were describing the intended behavior all along. To keep the old output:TableSpec(ratio_format='.3f')reproduces it exactly.TableSpec.formatsis renamedformatters— one name, matchingaligners, and the same name 5.x used on its constructor. Thecompat.GTtranslation layer is gone with it. One deliberate difference from 5.x: a bare int now groups (formatters={'paid': 2}→1,234.57), because GT2 groups whenever it picks the format for you. Write'.2f'for the ungrouped form — explicit sugar still means exactly what it means in Python and csv-grid, where the comma is the grouping flag.infer_numeric_formattakes the engineering digits and style, so the hard-codedsi/3 is nowTableSpec.eng_digits/eng_style.
Added
House defaults on
TableSpec:float_format,int_format,ratio_format,date_format. Each replaces its own kind’s built-in and loses to an explicitformattersentry. They apply to stub columns too, and they are YAML-expressible, which is the point:GT(df, config_path='house.yaml')gives every exhibit in a document the same decimals regardless of what its own data looks like. Settingfloat_formatturns float inference off entirely, including the automatic SI/engineering switch — a stated house rule beats a guess.table_float_format— the sledgehammer: every untagged float column, overriding even a per-column entry. Ratio, year and date columns have a kind, so it never reaches them (a ratio is an honorary non-float). Default None, and deliberately omitted fromwrite_template: it belongs at a conscious call site, not in a file where it would silently defeat everyformattersentry you write months later.eng_digits(3) andeng_style('si'suffixes or'exp'aligned exponents) — 5.x’spef_precision, plus the style choice it never had.parse_sugaraccepts a mapping, so YAML can express a fullFormatSpec:formatters: {paid: {kind: dec, digits: 1, negative: paren}}. Without it a config file could only say',.2f'— exactly what 5.x could already say — leaving paren negatives,scale,prefix/suffixand custom null/zero strings Python-only.config.spec_to_yaml(spec)andwrite_template(path, spec=None)— the template is now generated from the dataclass with a hint per field (it covered 12 of 22 and rotted), and passing a spec dumps its effective values uncommented. That is the round trip 5.x had viaGT().config.model_dump():write_template('house.yaml', gt.spec)→ edit →config_path='house.yaml'.tests/test_formats.py—parse_sugar,infer_numeric_formatandapply_formathad zero direct tests; everything rode on goldens. Plustests/test_house_rules.pyfor the ladder and the YAML round trip.
Fixed
load_specnow convertsnotesfrom a YAML list to the tuple the dataclass holds, so a loaded spec compares equal to a Python-built one and the dump/edit/reload round trip actually closes.
[6.0.0a3] - 2026-08-01
Step 1 of two: rules and the plumbing to reach them. Formats follow in a4.
Added
render/policy.py—rule_slot()andvrule_slot(), the single implementation of “which structural breaks get a rule, and with what weight”, called by the HTML, TikZ and text renderers and ported line for line into the walker. The comparison used to be written out four times.HouseStyle.max_rule_depthnow does something. It was declared, documented as the knob for this, and read by nothing.'auto'(default) is the previous behavior — every break except the innermost stub level, so no rules on a single-level index, depth 0 only on two levels, depths 0 and 1 on three. An int is literal:max_rule_depth=0on a single-level index rules every row whose stub changed, which was previously unsayable.Nonedraws no internal rules at all.max_vrule_depthis the sibling for column-group verticals.style=reaches the renderers.render_html,render_text,mimebundleandGT(...)all take aHouseStylenow; previously it was reachable only throughrender_tikz, so every field on it was dead on the HTML path. Passing aHouseStylefield name toGTas a bare keyword now warns with the fix (style=HouseStyle(tikz_scale=...)) instead of the generic “unknown option”.config.load_style(path)—HouseStylehad no YAML form, so half the house rules were file-able and half Python-only. A separate file fromload_spec’s, because semantics and renderer opinions are separate things.Walker:
opts.maxRuleDepth/opts.maxVruleDepth, plus exportedruleSlot/vruleSlot. Style is not in the document by design, so a server passes these alongside it, with the theme.--gt-vrule-0/1CSS custom properties. Column-group verticals borrowed--gt-rule-0/1, so softening or removing one changed the other; same default values, so nothing looks different.deep-stubfixture (5 stub levels) — the depth at which a fixed cap and then_stub_levels - 2policy disagree, and the fixture that would have caught the bug below.tests/test_rules.pychecks all four renderers agree with the policy across 1-5 stub levels × 5 policies, and compares Python against the walker live under non-default policies (the committed transcripts only ever cover the default).
Fixed
The text oracle drew rules the other three renderers suppressed.
render/text.pygated ondepth <= n_stub_levels - 2while HTML, TikZ and the walker usedmin(2, n_stub_levels - 2), so from five stub levels up the conformance oracle disagreed with everything it exists to check. Now impossible: there is one implementation.The first-row guard differed per renderer (text skipped row 0 per section, TikZ per body+foot concatenated, HTML and the walker not at all). No engine-built document can expose it —
break_depthsreturns None for row 0 — but a hand-authored or foreign one can, and conformance would not have caught it because HTML and the walker were wrong together.
Notes
Nothing changes at default settings: no golden IR, text, TikZ or transcript
file moved. The only regenerated artifacts are assets/gt.css (new
properties), the walker build, and the demo pages.
[6.0.0a2] - 2026-07-31
Changed
requires-pythondrops to>=3.11(was 3.13), with matching classifiers. Reported by aggregate_api, whose own floor the 3.13 pin had forced up from 3.11; 5.3 shipped>=3.10, so a 3.13 floor on a published 6.0 was pure adoption tax. Verified rather than assumed: the full suite runs green on 3.11, 3.12 and 3.13, with byte-identical golden files.include_rawon stub columns: closed as won’t-fix.include_rawmeans data columns in every form, and no form reaches the stub — an explicit list naming the index level does nothing (verified). A client that must sort on an index level shoulddf.reset_index()beforebuild, which is the honest shape anyway: a sorted grid has no index hierarchy left to respect.TableSpec.include_raw’s docstring now says so, which also settles the “reads broader than it behaves” complaint.dev/tidied further: the three working scripts moved todev/tools/(paths inside them and the one reference indocs/migration.rstupdated), the resolved aggregate_api report filed underdev/done, anddev/pending.mdrewritten to carry only what is actually open.
Fixed
TikZ widths are now identical across Python versions.
render/textlength.pyheld its glyph metrics as float ems and summed them withsum(). CPython 3.12 gavesum()Neumaier compensated summation, so'apples'totalled 2.5549999999999997 on 3.11 and 2.555 on 3.13 — one ULP apart, but on opposite sides of the 2 dp rounding the TikZ renderer applies, which moved a committed column width by 0.01em and made a golden.texfile fail on 3.11. Metrics are now integer milli-ems accumulated as integers and converted to ems once at the end, which is exact on every interpreter. Found only because the 3.11 floor change was verified by actually running the suite there. No golden file changed on 3.13; the goldens are now interpreter-independent.
[6.0.0a1] - 2026-07-31
First pre-release of the rewrite. The version line switches here from the
1.x development counter (1.0.0 → 1.11.0, one bump per completed step) to the
published series: this package ships as greater-tables 6.0 on the
existing PyPI project, where 5.3 stays frozen and pinnable (<6). Further
changes before the release are 6.0.0a2, a3, …
All seven design steps are complete — [ir-core], [engine], [walker],
[aggregate-api], [notebook-html], [tikz-blog], and the [backlog] adapter —
and every plan doc has moved to dev/done. Open items are in
dev/pending.md; the biggest is that Read the Docs hosting is not yet set up.
Changed
Version bumped to
6.0.0a1in_version.pyandGT_RENDER_VERSION.package.jsonspells the same version6.0.0-a.1, which is npm’s semver form of PEP 440’s6.0.0a1— the two differ in punctuation only.dev/tidied:spec-gt2-for-aggregate-api.mddeleted (superseded — the adoption landed in aggregate_api a34/a35, and their own notes carry the detail),plan-aggregate-api.mdmoved todev/done, anddesign.md’s sequencing section records that all steps are complete rather than describing them as upcoming.
[1.11.0] - 2026-07-31
Fixed
irToGridInputno longer throws on string data columns. It required raw values on every data column, butinclude_raw='data'only covers numeric, date and bool ones — so any frame with a string column outside the stub failed, built exactly as documented (3 of aggregate_api’s 12 frames; reported indev/note-from-aggregate-api-include-raw.md). A string’s formatted text is its value, sostring/objectcolumns now fall through as text and raw is required only where there is something to recover. The throw names the offending dtype and whatinclude_raw='data'actually covers, instead of advising what the caller already did. The stub half of that report — no raw on index columns, so a numeric index sorts lexically in csv-grid — is still open, seedev/pending.md.
Changed
Engine goldens no longer carry
generator, so a version bump stops rewriting all 22 of them and burying real IR changes in the churn (verified: a faked 9.9.9 bump now changes nothing undertests/golden,demo.html,demo2.html, orschema/).generatoris informative and already excluded from the content hash.tests/registry.golden_bytesis the one place that defines the golden byte form, used by bothmake_goldens.pyandtest_engine.py; the hand-authored exemplars keep their fixed test generator, so the field itself stays covered.
[1.10.0] - 2026-07-31
Added
LICENSE — MIT, the same text and holder as greater_tables 5.3, years extended to 2025-2026.
pyproject.tomldeclares it PEP 639 style (license = "MIT"+license-files), matching 5.3’s metadata.Sphinx documentation in
docs/, consistent with the 5.x docs (Read the Docs theme, autodoc, copy button) minus the extensions deprecated upstream (sphinx-panels,recommonmark,sphinx-rtd-dark-mode), plus napoleon for the numpy docstrings and intersphinx to python/pandas/numpy. Pages: quickstart, design (why the IR exists, engine, renderers, walker, verification), IR v1 reference, migration from 5.x (what carries over, what died and where it went), API reference, and the CHANGELOG included via myst. Builds clean under-W --keep-going.docs/_staticartwork is copied from 5.x as a placeholder pending new art..readthedocs.yamland adocsoptional-dependency extra (uv sync --extra docs). Creating the RTD project itself is pending — seedev/pending.md; the agreed slug isgreater-tables, so docs live at https://greater-tables.readthedocs.io (matching the PyPI name, not the repo codename).dev/pending.md— open items that are not step plans: RTD setup, artwork, the 6.0 publish sequence, therequires-pythonfloor, stub raw values, CI.
Changed
PyPI metadata completed to match 5.3’s shape: author email, keywords, classifiers, and
[project.urls](Homepage/Documentation/Source Code/Changelog/Issues) pointing atmynl/greatest_tablesand the new RTD slug. Explicit sdist include list. README rewritten as the PyPI long description: what the package is, how to install it, what 6.0 changes versus 5.x, the name story, and the pin (greater-tables<6) for anyone staying on the frozen engine.
Fixed
Leftovers from the 1.9.0 rename:
.gitignorestill un-ignoredsrc/greatest_tables/data/*.csv(so a new package data CSV would have been silently ignored),package.jsonexportspointed at the old asset paths, and README,dev/design.md,dev/plan-aggregate-api.md, and the aggregate_api handoff spec still told readers toimport greatest_tables. Repo, folder, and GitHub keep thegreatest-tablescodename by design; only import and distribution names moved.
[1.9.0] - 2026-07-31
Changed
Package renamed
greatest_tables→greater_tables(import and distribution name; the repo folder keeps thegreatest-tablescodename, so editable-path consumers and the V:\dev venv junction are untouched). Done now, before any adoption code exists, because the publish decision is: greater-tables 6.0 on the existing PyPI project — 5.3 stays pinnable (semver’s whole job), blog imports keep working verbatim via the facade, and a6.0.0rc1pre-release will unblock the server deploy (explicit pin only; plain installs still resolve to 5.3) once aggregate_api adoption settles. aggregate_api’s spec/brief updated +dev/note-gt2-rename.mddropped for their agent.dev/compare_gt5.pyreworked for the name collision this creates: 5.3 now runs in an isolateduv run --no-projectchild (_compare_gt5_old.py), DataFrames crossing as pickles with pandas/numpy pinned to the parent’s versions (parquet cannot carry MultiIndex columns). pyarrow added to the dev group.
[1.8.0] - 2026-07-31
Added
irToGridInput(doc)in gt-render.js (also on the iife global): flattens a table document into CsvGrid’s input shape —{columns, records, formats, align}. Stubs un-sparsify (rowspan values repeat down their run), multi-level names join with ‘ / ‘, data cells emit raw values (null → empty), IR FormatSpecs map to csv-grid format-spec strings, alignment rides as an ‘lrc’ string. Requiresinclude_raw='data'— throws with a clear message otherwise, because parsing formatted text back would be the re-inference the IR exists to kill. Hierarchy, flags, and foot rows are deliberately dropped: they cannot coexist with sort/filter. csv-grid itself is untouched. One IR fetch now feeds both the static walker and the interactive grid (the aggregate-api toggle).js/adapter-check.mjsharness + node round-trip tests (test_adapter.py).
[1.7.1] - 2026-07-31
Fixed
Spanner rules now run full width, edge to edge: HTML swaps the inset background-gradient for a plain
border-bottom(--gt-rule-spanner); TikZ extends each rule by the half column-gap on both sides. Adjacent groups’ rules meet exactly at the group vertical, which separates them — the cmidrule-style side trims from 1.7.0 read as gaps, per review.TikZ column-group verticals now run the full table height (top frame to bottom), matching the HTML
gt-vbreakborder which passes through the header rows; previously they started below the parent header level.
[1.7.0] - 2026-07-31
Added
Spanner rules in all three renderers: a trimmed rule under every labeled column-group header, spanning exactly its columns (the booktabs
\cmidrule(lr)pattern; 5.x never drew these in the live TikZ path). HTML/walker stampgt-spanner(underline via inset background gradient — a border would merge with adjacent groups and lose the span extent; size rides as--gt-spanner-size); TikZ draws trimmed\pathsegments (HouseStyle.spanner_rule, default 0.5); the text oracle draws─runs under each label with gaps between groups.
Fixed
Example qmd HTML: math cells rendered as literal
\(...\)because the document had no markdown math, so Quarto never loaded MathJax. The generator now includes inline math in the intro (and pinshtml-math-method: mathjax); the stale-file SyntaxWarnings were already fixed in 1.6.1 — regenerate withdev/make_example_qmd.py.
[1.6.1] - 2026-07-31
Added
dev/make_example_qmd.py: generates a Quarto example file (blog-pattern blocks ending inGT(df, ...)) with a self-contained TikZ preamble; renders to HTML and PDF (pdf-engine: tectonic). Verified end-to-end: 10 tables, both formats. RequiresQUARTO_PYTHONpointing at the project venv until the package is published; venv dev group gains ipykernel/nbformat/nbclient.
Fixed
Math auto-detection rejects
$...\$values whose inner text ends with a backslash — the “closing” delimiter is escaped, so treating them as math produced unterminated$in TeX and broken\(...\\)in MathJax. Found by a malformed Fabricator TeX entry during PDF rendering; now plain text.TikZ header width estimates apply a bold adjustment (×1.15) so long bold headers no longer collide across the column gap (the 5.x bold_adjustment, now applied where 5.x didn’t).
[1.6.0] - 2026-07-31
Added
[tikz-blog]
render/tikz.py: the single TikZ implementation (5.x carried three, two dead), rebuilt from the livemake_tikzmechanics against the IR: matrix-of-nodes with zero-height spacer row, bold header rows with bold-italic stub headers, phantom anchor column, rules as anchored\pathsegments shifted into the inter-cell gaps — row break rules start at the changed stub column, matching HTML/text. New capability: real colspans — spanning labels are overlaid centered on the span viacalcmidpoints (5.x blanked repeats and silently returned ‘’ on non-unique columns).render/textlength.py: the salvaged character-width oracle (glyph table + TeX math token estimation), scoped to the TikZ renderer only.render/etcher.py: Etcher salvaged and modernized — tectonic is the default engine (fast, self-caching; the 5.x.fmtprecompilation dance is deleted), pdflatex fallback kept; success judged by the PDF existing, not the exit code (MiKTeX exits 1 on recoverable issues). No hardcoded paths, no .bat side files, no os.startfile.HouseStylegains the TikZ geometry and rule-weight knobs (w2tikz scale).Golden
.texfiles per fixture (third renderer oracle); compile smoke test via tectonic (skips when unavailable);GT.tikznow renders;mimebundleaddstext/latexunder Quarto or on request.dev/compare_gt5.py: migration check — renders sample fixtures through greater_tables 5.x (in an ephemeraluv run --withenv, keeping its import side effects out of the project venv) and GT2 side by side intocompare-gt5.html(gitignored review artifact).156 tests.
[1.5.0] - 2026-07-30
Added
dev/spec-gt2-for-aggregate-api.md: the [aggregate-api] handoff spec (mirrored intoT:/worktrees/aggregate_api/dev/plan-gt2-ir-adoption.md) — the adoption work is implemented by the aggregate_api agent.
Changed
Engineering display defaults to SI suffixes (µ m k M G T, full 2022-extended ladder q..Q so a column never mixes suffix and exponent styles); the empty suffix slot is a no-break space so decimal points stay aligned in tabular-nums/monospace rendering. Exponent style remains the
'eng'sugar opt-in and now emits fixed-widthe+00/e-00exponents (sign always, two digits) so right-aligned columns line up exactly.
[1.4.0] - 2026-07-30
Added
demo2.html: curated showcase — TeX/math cells (page-level MathJax CDN typesets the walker’s\(...\)fallback), long-text wrapping from both Fabricator generators (‘s’ uniform low-variance, ‘w’ negative-binomial high-variance), wide-range floats in engineering format, and the csv-grid width solver demonstrated live in both modes (equal-risk and coverage) in a 560px-constrained container against the natural-layout reference.Four showcase fixtures (tex, wrap-low, wrap-high, wide-floats) join the registry and therefore all three golden suites (IR, text, DOM transcript); 125 tests.
Changed
Engineering-format inference gains csv-grid’s second trigger: a float column whose nonzero values span more than a 10^pef_upper magnitude ratio renders as engineering even when its mean sits inside the pef bounds (previously such columns got fixed decimals and tiny values collapsed to 0.00000).
[1.3.0] - 2026-07-30
Added
[notebook-html]
render/html.py: Python HTML fragment renderer mirroring the walker’s structure and class stamping exactly. Escaping by default with anallow_htmlgate (producer default on —html_colswas the opt-in; walker default stays off for foreign documents). Stylesheet policy:css='embed'(default, idempotent ~2 KB per fragment),'once'(per-process latch +reset_css_once()),'none';inline_css=Trueflattens the same RULES dict onto style attributes withvar()references resolved (HTMX/email path).Conformance proven: the Python renderer’s DOM transcript byte-matches the walker’s committed transcripts for all 20 goldens (
tests/test_conformance.pytests/transcribe.py, stdlib html.parser). The first run caught a real walker bug —col.role === 'data'guard broke on canonical JSON’s omitted defaults, silently droppinggt-vbreakclasses on body cells. Fixed.
render/notebook.py:mimebundle(text/html now; text/latex joins when the TikZ renderer lands) andin_quarto()env detection.compat.py: theGT(df, ...)facade — greater_tables constructor arguments, 5.x formatter shapes (callable /'{x:...}'string / int = digits without grouping), YAMLconfig_pathwith explicit-argument override, unknown options warn-and-ignore. Fully lazy cached: nothing computes at construction, TikZ is never touched on the HTML path..tikzraises NotImplementedError until [tikz-blog].config.py: YAML TableSpec loading + commented template writer.Engine: user formatters may be callables (facade path); callables format text but never enter the IR (
Column.formatstays null).demo.html: self-contained gallery at the repo root — gt.css + iife walker + all 20 golden IR documents, rendered client-side on open. Regenerated bytests/make_goldens.py(which now also rebuilds the walker dist).113 tests total.
[1.2.0] - 2026-07-30
Added
[walker]
render/css.py: the RULES dict — single source of truth for the shared class vocabulary — generating committedassets/gt.css..gt.gtselector doubling (beats JupyterLab/Quarto host rules without !important), per-table custom properties, currentColor-only palette, no widths/fonts. Row rules ride as border-top on the row’s own cells, so rowspan-covered stub columns are skipped and partial rules start exactly at the changed column.js/src/gt-render.js: zero-dependency ES walker.renderTable(doc, opts)→{el, doc, destroy(), toCSV()}instance handle; textContent by default with anallowHtmlgate (injection-safe); KaTeX injection or\(...\)fallback; delegatedgt:cellclickCustomEvent with the full row keyed by column name; optionalfitwidth pass (canvas measure + solver, forces single-line ellipsis). Solver functions copied verbatim from csv-gridutil.js(3.9.0, commit 01a9773) with a provenance header and a drift-guard test that re-diffs the copy against the csv-grid source when present.js/build.mjs(esbuild) → committedassets/gt-render.esm.js+.iife.js(globalGtRender); rootpackage.jsonfor the secondary npm-from-git path.DOM-transcript conformance harness (
js/transcript.mjs, node + linkedom): one JSON line per cell over every golden IR fixture → committedtests/golden/transcript/*.jsonl(20 files). The Python HTML renderer must byte-match these in [notebook-html]. pytest wrapper skips when node is absent.CSS contract guard tests (asset drift, vocabulary coverage, selector doubling, no-geometry/no-color policy).
Notes
IR goldens embed the generator version string, so
tests/make_goldens.pyruns once per release — the one-line-per-fixture diff doubles as re-validation.Version strings now live in
_version.py(hatchling-dynamic),package.json, andGT_RENDER_VERSION— keep all three in sync at each bump.
[1.1.0] - 2026-07-30
Added
[engine]
spec.py:TableSpec(semantics: caption/label/notes, semantic column tags as list|’all’|regex, per-column format sugar, aligners, row/cell flags, include_raw, max_rows, sparsify switches, pef bounds) andHouseStylestub.engine/dtypes.py: arrow-aware dtype classification (date vs datetime split on time-of-day), selector resolution, GT alignment ladder, wrap hints.engine/formats.py: format sugar parsing accepting both csv-grid spec strings (',.1f','year','eng', strftime) and GT f-strings ('{x:.1%}'); the salvaged column-statistics float inference; pureapply_formatincl. engineering/SI notation and paren-negative accounting style.engine/structure.py: the single implementation of hierarchy logic — row break depths, stub rowspan layout, header colspan groups, column-group break depths.engine/build.py:build(data, spec) -> TableDocfor DataFrame / Series / list / markdown pipe table (caption/{#tbl-label}/alignment extracted). Never mutates input. Auto neg flags,$...$math detection, html_cols, JSON-safe raw values (big ints as strings, ISO dates, NaN→null).render/text.py: the conformance oracle — box-drawing renderer reading only the TableDoc: spans, partial rules at break depths, flag gutter, math markers.Fixture registry (18 fixtures, all Fabricator-generated or tiny synthetics) with committed golden IR and golden text files; 72 tests total.
.gitattributesguards golden files from newline conversion; version now single- sourced from_version.py(hatchling dynamic).
Changed
Deliberate deviations from greater_tables 5.x behavior: engineering format is a column-level decision (5.x switched per cell, mixing formats within a column); missing values render as an em dash in formatted columns; the >50-row raise is replaced by
max_rowstruncation with an appended note;'all'selectors cover data columns only; format resolution returns data objects, structurally killing the 5.x late-binding closure bug.pytest cache moved to the V:\dev drive via junction (NAS path denies the cache dir creation rename).
[1.0.0] - 2026-07-30
Added
[ir-core] Repo bootstrap: hatchling/uv project,
src/greatest_tableslayout, venv on the V:\dev dev drive (junction), design and step plan docs underdev/.ir.py: frozen pydantic models for the table document IR,ir_version1 —TableDoc,Column,Row,Cell,HeadCell,FormatSpec. Write model rejects unknown fields; the read path (TableDoc.read()) ignores them so newer producers stay readable. Bare-string cell shorthand supported and canonicalized on dump.engine/hashing.py: canonical JSON (sorted keys, compact separators, NFC normalization, shortest round-trip floats) anddoc_hash— sha256 over the canonical bytes withhash/generatorexcluded. Deterministic: no time salt.fabricator.py: synthetic DataFrame generator ported from greater_tablesfabrications.py. Fixed in transit:oversamplerecursion passed arguments positionally-wrong;random()had a's' 'i'literal-concatenation typo; the documentedmissingparameter now exists and is wired to_insert_missing; scipy dependency replaced withrng.negative_binomial; datetime columns are deterministic under a seed (fixednowreference passed through, no wall-clock reads); deadrandom_date_within_last_n_years(referencedselffrom a staticmethod) removed.schema/ir-v1.json: committed JSON Schema export ofTableDoc, with a test that fails when the models drift from the committed schema.Test suite: exemplar golden IR documents (round-trip byte-equality), hash stability, read-path must-ignore, Fabricator determinism.
Notes
IR
ir_versionis decoupled from the package version and only bumps on breaking schema change.