Commit 809ac9d720
809ac9d720951c3f1ded537571e832c7fb349960
parent: 82639e554b
Unregistered key
cmc <hello@cleberg.net> · 2026-08-23 03:52 UTC
committer: <noreply@github.com>
audit: #+TBLFM: is in scope, not a gap (#27)
The audit counted `#+TBLFM:` as out-of-scope, which read as a gap in coverage.
It is not one: org's HTML exporter does not recalculate table formulas on
export either, so both orgo and Emacs emit the cells exactly as written. The
parser has always swallowed the line, and `docs/guide/05-org-support.org`
already recorded that as the decision — only the audit disagreed.
`fixtures/tblfm.org` is a table whose cells deliberately contradict its
formula, wired into the Emacs oracle so the claim is checked rather than
asserted: 0 unexplained divergences.
Moves the doc row from "Not supported" to the supported keyword table with the
reason, and refreshes the sample output in the auditing guide, which still
showed the pre-#26 entity line and a `??? SLUG` that has since been recognised.
cleberg.net's corpus now audits at 100.0% in-scope coverage, 1 out-of-scope
(the `#+begin_note` of #25).
Closes #24
Layout: unified · split
docs/guide/05-org-support.org
+1 −1
| @@ -167,6 +167,7 @@ broken. |
| 167 | 167 | | =#+TEMPLATE:= | The layout this page renders through. | |
| 168 | 168 | | =#+OPTIONS:= | Per-file export switches. | |
| 169 | 169 | | =#+CAPTION:=, =#+ATTR_HTML:= | Attach to the image *directly* below them — a blank line in between attaches to nothing, as in org. A captioned image is numbered =Figure N:=. | |
| 170 | | =#+TBLFM:= | Kept inert, and that matches org: the HTML exporter does not recalculate formulas either, so both emit the cells as written. Recalculate in Emacs (=C-c C-c=) to change them. | |
| 170 | 171 | |
| 171 | 172 | Every other =#+KEYWORD:= is available to templates as |
| 172 | 173 | ={{ page.keywords.that_keyword }}=, so metadata orgo has never heard of still reaches |
| @@ -180,7 +181,6 @@ a build. |
| 180 | 181 | | Construct | What happens | |
| 181 | 182 | |-----------+--------------| |
| 182 | 183 | | Babel execution, =:results= | The source block renders as code. A checked-in =#+RESULTS:= block is *dropped*. | |
| 183 | | | =#+TBLFM:= | Inert. The table renders with the values as written. | |
| 184 | 184 | | =#+INCLUDE:= | Never expanded, and *reported*: the build prints =file:line: `#+INCLUDE: …` is not expanded=, so a page is never quietly missing content. =--strict= makes it a failure. | |
| 185 | 185 | | LaTeX, MathJax | Survives as the literal text you typed. | |
| 186 | 186 | | Macros ={{{name}}}=, radio targets | Literal text. | |
docs/guide/09-auditing.org
+16 −12
| @@ -17,31 +17,35 @@ The audit answers two questions about a body of org files: |
| 17 | 17 | * Reading the output |
| 18 | 18 | |
| 19 | 19 | #+BEGIN_EXAMPLE |
| 20 | | corpus: 179 file(s), 29258 line(s) |
| 20 | corpus: 180 file(s), 29742 line(s) |
| 21 | 21 | |
| 22 | 22 | CONSTRUCTS (by frequency) |
| 23 | 23 | construct uses files first seen |
| 24 | | IN list item 1282 109 blog/2018-11-28-aes-encryption.org:53 |
| 25 | | IN heading 1128 174 blog/2018-11-28-aes-encryption.org:7 |
| 26 | | IN source block 932 121 blog/2018-11-28-cpp-compiler.org:17 |
| 24 | IN list item 1309 111 blog/2018-11-28-aes-encryption.org:53 |
| 25 | IN heading 1148 176 blog/2018-11-28-aes-encryption.org:7 |
| 26 | IN verbatim 1048 130 blog/2018-11-28-aes-encryption.org:79 |
| 27 | 27 | ... |
| 28 | | OUT table formula (#+TBLFM:) 4 1 blog/2024-08-11-org-mode-features.org:191 |
| 29 | | OUT entity (\name) 3 3 blog/2024-04-06-convert-onenote.org:37 |
| 28 | IN table formula (#+TBLFM:) 4 1 blog/2024-08-11-org-mode-features.org:191 |
| 29 | OUT unmodelled block type 1 1 blog/2026-03-03-auditing-aws-s3.org:50 |
| 30 | 30 | |
| 31 | | coverage: 8854 in-scope use(s) (99.9%), 8 out-of-scope (0.1%) |
| 31 | coverage: 9001 in-scope use(s) (100.0%), 1 out-of-scope (0.0%) |
| 32 | 32 | |
| 33 | 33 | KEYWORDS |
| 34 | | TITLE 179 179 blog/2018-11-28-aes-encryption.org:2 |
| 35 | | ??? SLUG 178 178 blog/2018-11-28-aes-encryption.org:4 |
| 36 | | DESCRIPTION 176 176 blog/2018-11-28-aes-encryption.org:3 |
| 34 | SLUG 180 180 blog/2018-11-28-aes-encryption.org:4 |
| 35 | TITLE 180 180 blog/2018-11-28-aes-encryption.org:2 |
| 37 | 36 | ... |
| 37 | |
| 38 | BLOCK TYPES |
| 39 | SRC 941 123 blog/2018-11-28-cpp-compiler.org:17 |
| 40 | QUOTE 115 48 blog/2018-11-28-aes-encryption.org:21 |
| 41 | ??? NOTE 1 1 blog/2026-03-03-auditing-aws-s3.org:50 |
| 38 | 42 | #+END_EXAMPLE |
| 39 | 43 | |
| 40 | 44 | - =IN= is supported; =OUT= is excluded by design and degrades as described in |
| 41 | 45 | [[file:05-org-support.org][Org support]]. |
| 42 | 46 | - The *coverage* line is the number to look at first. |
| 43 | | - =???= marks a name orgo does not recognise at all — in this example =#+SLUG:=, from |
| 44 | | a run made before it was supported. |
| 47 | - =???= marks a name orgo does not recognise at all — here =#+begin_note=, which one |
| 48 | post uses once. That is the blind-spot signal: not "known unsupported", but unknown. |
| 45 | 49 | |
| 46 | 50 | Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type, |
| 47 | 51 | drawer name and link scheme in the corpus. A =???= in any of them is worth a look. |
fixtures/tblfm.org
added
+10
| @@ -0,0 +1,10 @@ |
| 1 | #+TITLE: Table Formula |
| 2 | |
| 3 | The cells deliberately contradict the formula: org's HTML exporter does not |
| 4 | recalculate on export, so both exporters must emit the values as written. |
| 5 | |
| 6 | | N | N^2 | |
| 7 | |---+-----| |
| 8 | | 2 | 999 | |
| 9 | | 3 | 888 | |
| 10 | #+TBLFM: $2=$1^2 |
src/audit.rs
+5 −1
| @@ -258,7 +258,11 @@ impl Audit { |
| 258 | 258 | "CAPTION" | "NAME" | "ATTR_HTML" => { |
| 259 | 259 | self.count(Scope::In, "affiliated keyword", at) |
| 260 | 260 | } |
| 261 | | "TBLFM" => self.count(Scope::Out, "table formula (#+TBLFM:)", at), |
| 261 | // Not a gap: org's HTML exporter does not recalculate `#+TBLFM:` |
| 262 | // either, so the cells as written are what both exporters emit. |
| 263 | // `fixtures/tblfm.org` holds the oracle to that (`tests/oracle.rs`). |
| 264 | // Unlike `#+INCLUDE:`, nothing is lost by leaving it inert. |
| 265 | "TBLFM" => self.count(Scope::In, "table formula (#+TBLFM:)", at), |
| 262 | 266 | "INCLUDE" => self.count(Scope::Out, "#+INCLUDE:", at), |
| 263 | 267 | "RESULTS" => self.count(Scope::Out, "babel results block", at), |
| 264 | 268 | "TODO" => self.count(Scope::Out, "#+TODO: keyword sequence", at), |
tests/constructs.rs
+16
| @@ -387,6 +387,7 @@ fn well_formed_fixtures_produce_no_diagnostics() { |
| 387 | 387 | "blocks.org", |
| 388 | 388 | "timestamps.org", |
| 389 | 389 | "images.org", |
| 390 | "tblfm.org", |
| 390 | 391 | ] { |
| 391 | 392 | let document = parse_fixture(name); |
| 392 | 393 | assert!( |
| @@ -800,3 +801,18 @@ fn audit_counts_real_entities() { |
| 800 | 801 | "a real entity was not counted:\n{report}" |
| 801 | 802 | ); |
| 802 | 803 | } |
| 804 | |
| 805 | /// `#+TBLFM:` is in scope, not a gap: org's exporter does not recalculate it either, so |
| 806 | /// the page orgo produces is the page Emacs produces. `fixtures/tblfm.org` is the oracle. |
| 807 | #[test] |
| 808 | fn audit_counts_table_formulas_as_in_scope() { |
| 809 | let report = audit_fixture("tblfm.org"); |
| 810 | let line = report |
| 811 | .lines() |
| 812 | .find(|l| l.contains("table formula")) |
| 813 | .expect("the formula is counted at all"); |
| 814 | assert!( |
| 815 | line.starts_with("IN "), |
| 816 | "a #+TBLFM: table orgo renders exactly as Emacs does must not read as a gap:\n{line}" |
| 817 | ); |
| 818 | } |
tests/oracle.rs
+1
| @@ -628,6 +628,7 @@ fn every_divergence_from_emacs_is_deliberate() { |
| 628 | 628 | "timestamps.org", |
| 629 | 629 | "images.org", |
| 630 | 630 | "elements.org", |
| 631 | "tblfm.org", |
| 631 | 632 | ]; |
| 632 | 633 | let mut offenders = Vec::new(); |
| 633 | 634 | for fixture in fixtures { |