krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

809ac9d720951c3f1ded537571e832c7fb349960

signed_unknown_key

author: Christian Cleberg <hello@cleberg.net> · 2026-08-23T03:52:32Z
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
 docs/guide/05-org-support.org |  2 +-
 docs/guide/09-auditing.org    | 28 ++++++++++++++++------------
 fixtures/tblfm.org            | 10 ++++++++++
 src/audit.rs                  |  6 +++++-
 tests/constructs.rs           | 16 ++++++++++++++++
 tests/oracle.rs               |  1 +
 6 files changed, 49 insertions(+), 14 deletions(-)

diff --git a/docs/guide/05-org-support.org b/docs/guide/05-org-support.org
index dde4d6e..70213a7 100644
--- a/docs/guide/05-org-support.org
+++ b/docs/guide/05-org-support.org
@@ -167,6 +167,7 @@ broken.
 | =#+TEMPLATE:= | The layout this page renders through. |
 | =#+OPTIONS:= | Per-file export switches. |
 | =#+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:=. |
+| =#+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. |
 
 Every other =#+KEYWORD:= is available to templates as
 ={{ page.keywords.that_keyword }}=, so metadata orgo has never heard of still reaches
@@ -180,7 +181,6 @@ a build.
 | Construct | What happens |
 |-----------+--------------|
 | Babel execution, =:results= | The source block renders as code. A checked-in =#+RESULTS:= block is *dropped*. |
-| =#+TBLFM:= | Inert. The table renders with the values as written. |
 | =#+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. |
 | LaTeX, MathJax | Survives as the literal text you typed. |
 | Macros ={{{name}}}=, radio targets | Literal text. |
diff --git a/docs/guide/09-auditing.org b/docs/guide/09-auditing.org
index 0c870d1..eab289f 100644
--- a/docs/guide/09-auditing.org
+++ b/docs/guide/09-auditing.org
@@ -17,31 +17,35 @@ The audit answers two questions about a body of org files:
 * Reading the output
 
 #+BEGIN_EXAMPLE
-corpus: 179 file(s), 29258 line(s)
+corpus: 180 file(s), 29742 line(s)
 
 CONSTRUCTS (by frequency)
      construct                            uses   files  first seen
-IN   list item                            1282     109  blog/2018-11-28-aes-encryption.org:53
-IN   heading                              1128     174  blog/2018-11-28-aes-encryption.org:7
-IN   source block                          932     121  blog/2018-11-28-cpp-compiler.org:17
+IN   list item                            1309     111  blog/2018-11-28-aes-encryption.org:53
+IN   heading                              1148     176  blog/2018-11-28-aes-encryption.org:7
+IN   verbatim                             1048     130  blog/2018-11-28-aes-encryption.org:79
 ...
-OUT  table formula (#+TBLFM:)                4       1  blog/2024-08-11-org-mode-features.org:191
-OUT  entity (\name)                          3       3  blog/2024-04-06-convert-onenote.org:37
+IN   table formula (#+TBLFM:)                4       1  blog/2024-08-11-org-mode-features.org:191
+OUT  unmodelled block type                   1       1  blog/2026-03-03-auditing-aws-s3.org:50
 
-coverage: 8854 in-scope use(s) (99.9%), 8 out-of-scope (0.1%)
+coverage: 9001 in-scope use(s) (100.0%), 1 out-of-scope (0.0%)
 
 KEYWORDS
-   TITLE                                 179     179  blog/2018-11-28-aes-encryption.org:2
-??? SLUG                                  178     178  blog/2018-11-28-aes-encryption.org:4
-   DESCRIPTION                           176     176  blog/2018-11-28-aes-encryption.org:3
+   SLUG                                  180     180  blog/2018-11-28-aes-encryption.org:4
+   TITLE                                 180     180  blog/2018-11-28-aes-encryption.org:2
 ...
+
+BLOCK TYPES
+   SRC                                   941     123  blog/2018-11-28-cpp-compiler.org:17
+   QUOTE                                 115      48  blog/2018-11-28-aes-encryption.org:21
+??? NOTE                                    1       1  blog/2026-03-03-auditing-aws-s3.org:50
 #+END_EXAMPLE
 
 - =IN= is supported; =OUT= is excluded by design and degrades as described in
   [[file:05-org-support.org][Org support]].
 - The *coverage* line is the number to look at first.
-- =???= marks a name orgo does not recognise at all — in this example =#+SLUG:=, from
-  a run made before it was supported.
+- =???= marks a name orgo does not recognise at all — here =#+begin_note=, which one
+  post uses once. That is the blind-spot signal: not "known unsupported", but unknown.
 
 Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
 drawer name and link scheme in the corpus. A =???= in any of them is worth a look.
diff --git a/fixtures/tblfm.org b/fixtures/tblfm.org
new file mode 100644
index 0000000..5c29c69
--- /dev/null
+++ b/fixtures/tblfm.org
@@ -0,0 +1,10 @@
+#+TITLE: Table Formula
+
+The cells deliberately contradict the formula: org's HTML exporter does not
+recalculate on export, so both exporters must emit the values as written.
+
+| N | N^2 |
+|---+-----|
+| 2 | 999 |
+| 3 | 888 |
+#+TBLFM: $2=$1^2
diff --git a/src/audit.rs b/src/audit.rs
index 7bd4538..db6202b 100644
--- a/src/audit.rs
+++ b/src/audit.rs
@@ -258,7 +258,11 @@ impl Audit {
                         "CAPTION" | "NAME" | "ATTR_HTML" => {
                             self.count(Scope::In, "affiliated keyword", at)
                         }
-                        "TBLFM" => self.count(Scope::Out, "table formula (#+TBLFM:)", at),
+                        // Not a gap: org's HTML exporter does not recalculate `#+TBLFM:`
+                        // either, so the cells as written are what both exporters emit.
+                        // `fixtures/tblfm.org` holds the oracle to that (`tests/oracle.rs`).
+                        // Unlike `#+INCLUDE:`, nothing is lost by leaving it inert.
+                        "TBLFM" => self.count(Scope::In, "table formula (#+TBLFM:)", at),
                         "INCLUDE" => self.count(Scope::Out, "#+INCLUDE:", at),
                         "RESULTS" => self.count(Scope::Out, "babel results block", at),
                         "TODO" => self.count(Scope::Out, "#+TODO: keyword sequence", at),
diff --git a/tests/constructs.rs b/tests/constructs.rs
index 147bc19..5a1e421 100644
--- a/tests/constructs.rs
+++ b/tests/constructs.rs
@@ -387,6 +387,7 @@ fn well_formed_fixtures_produce_no_diagnostics() {
         "blocks.org",
         "timestamps.org",
         "images.org",
+        "tblfm.org",
     ] {
         let document = parse_fixture(name);
         assert!(
@@ -800,3 +801,18 @@ fn audit_counts_real_entities() {
         "a real entity was not counted:\n{report}"
     );
 }
+
+/// `#+TBLFM:` is in scope, not a gap: org's exporter does not recalculate it either, so
+/// the page orgo produces is the page Emacs produces. `fixtures/tblfm.org` is the oracle.
+#[test]
+fn audit_counts_table_formulas_as_in_scope() {
+    let report = audit_fixture("tblfm.org");
+    let line = report
+        .lines()
+        .find(|l| l.contains("table formula"))
+        .expect("the formula is counted at all");
+    assert!(
+        line.starts_with("IN "),
+        "a #+TBLFM: table orgo renders exactly as Emacs does must not read as a gap:\n{line}"
+    );
+}
diff --git a/tests/oracle.rs b/tests/oracle.rs
index ba8a5fc..fc0fbe6 100644
--- a/tests/oracle.rs
+++ b/tests/oracle.rs
@@ -628,6 +628,7 @@ fn every_divergence_from_emacs_is_deliberate() {
         "timestamps.org",
         "images.org",
         "elements.org",
+        "tblfm.org",
     ];
     let mut offenders = Vec::new();
     for fixture in fixtures {