audit: stop reporting gaps orgo's own docs do not have !2

merged merged by cmc on 2026-08-30 06:55 UTC · krz/orgo:audit-overstated-gaps into main

5 files changed, +96 −31

Layout: unified · split

docs/guide/09-auditing.org +9 −3
@@ -31,8 +31,9 @@ IN special block 1 1 blog/2026-03-03-auditing
31coverage: 9002 in-scope use(s) (100.0%), 0 out-of-scope (0.0%) 31coverage: 9002 in-scope use(s) (100.0%), 0 out-of-scope (0.0%)
32 32
33KEYWORDS 33KEYWORDS
34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4 34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4
35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2 35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2
36— LEDE 14 14 blog/2018-11-28-aes-encryption.org:3
36... 37...
37#+END_EXAMPLE 38#+END_EXAMPLE
38 39
@@ -41,7 +42,12 @@ KEYWORDS
41- The *coverage* line is the number to look at first. 42- The *coverage* line is the number to look at first.
42- =???= marks a name orgo does not recognise at all. That is the blind-spot signal — 43- =???= marks a name orgo does not recognise at all. That is the blind-spot signal —
43 not "known unsupported", but unknown — and this corpus has none. Block names never 44 not "known unsupported", but unknown — and this corpus has none. Block names never
44 carry it: an unrecognised one is still a special block, and still renders. 45 carry it: an unrecognised one is still a special block, and still renders. Keyword
46 names never carry it either — see below.
47- =—= marks a keyword with no dedicated handling. It is not a gap: the keyword reaches
48 your layout as ={{ page.keywords.<name> }}=, which is the designed behaviour, so the
49 marker tells you which of your keywords orgo reads by name and which rely on that
50 pass-through.
45 51
46Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type, 52Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
47drawer name and link scheme in the corpus. A =???= in any of them is worth a look. 53drawer name and link scheme in the corpus. A =???= in any of them is worth a look.
fixtures/audit-keywords.org added +5
@@ -0,0 +1,5 @@
1#+TITLE: Keyword census
2#+LEDE: A keyword the shipped docs template reads.
3#+PROJECT_STATUS: A keyword nothing reads by name.
4
5Body text.
fixtures/audit-verbatim-constructs.org added +4
@@ -0,0 +1,4 @@
1#+TITLE: Verbatim mentions
2
3A doc explaining syntax, where every mention is inside verbatim:
4=${x^2}$=, ={{{name}}}=, =<<<radio>>>=, =[1/3]=, =CLOCK: [2024-01-01]=.
src/audit.rs +46 −28
@@ -71,9 +71,9 @@ pub struct Audit {
71 pub link_schemes: BTreeMap<String, Tally>, 71 pub link_schemes: BTreeMap<String, Tally>,
72} 72}
73 73
74/// Names the implementation understands, so the census can flag everything else. These 74/// Names the implementation reads by name, so the census can mark everything else. These
75/// are the *recognized* sets, not the supported ones: `INCLUDE` is recognized (it is 75/// are the *recognized* sets, not the supported ones: `INCLUDE` is recognized and
76/// deliberately inert) while an unlisted keyword is a genuine blind spot. 76/// deliberately inert.
77const KNOWN_KEYWORDS: &[&str] = &[ 77const KNOWN_KEYWORDS: &[&str] = &[
78 "TITLE", "AUTHOR", "DATE", "EMAIL", "LANGUAGE", "OPTIONS", "FILETAGS", "DESCRIPTION", 78 "TITLE", "AUTHOR", "DATE", "EMAIL", "LANGUAGE", "OPTIONS", "FILETAGS", "DESCRIPTION",
79 "KEYWORDS", "CAPTION", "NAME", "ATTR_HTML", "RESULTS", "TBLFM", "INCLUDE", "TODO", 79 "KEYWORDS", "CAPTION", "NAME", "ATTR_HTML", "RESULTS", "TBLFM", "INCLUDE", "TODO",
@@ -97,20 +97,36 @@ const KNOWN_SCHEMES: &[&str] = &[
97 "relative", 97 "relative",
98]; 98];
99 99
100/// The census markers. A name with dedicated handling carries none; `PASS_THROUGH` says
101/// nothing reads the name but it works anyway; `UNKNOWN` is the blind-spot signal.
102const HANDLED: &str = "";
103const PASS_THROUGH: &str = "\u{2014}";
104const UNKNOWN: &str = "???";
105
100impl Audit { 106impl Audit {
101 /// Is this name one the implementation recognizes? 107 /// How the report marks a census name: the recognized set to look the name up in,
102 pub fn is_known(kind: Census, name: &str) -> bool { 108 /// and what an unlisted name means for that census.
103 let known = match kind { 109 pub fn marker(kind: Census, name: &str) -> &'static str {
104 Census::Keyword => KNOWN_KEYWORDS, 110 let (known, unlisted) = match kind {
111 // An unlisted keyword is not a blind spot. Reaching templates as
112 // `page.keywords.<name>` is the designed behaviour, so every keyword name
113 // works; what the census reports is which ones have dedicated handling.
114 // `LEDE`, read by the shipped docs theme's template, was the case that made
115 // this concrete: `???` on it was a claim about orgo's source, not a gap.
116 Census::Keyword => (KNOWN_KEYWORDS, PASS_THROUGH),
105 // Every block name renders, and renders as org renders it: the names in 117 // Every block name renders, and renders as org renders it: the names in
106 // `block_construct` through dedicated handling, every other name as a special 118 // `block_construct` through dedicated handling, every other name as a special
107 // block — a div carrying the name, holding parsed org, which is exactly what 119 // block — a div carrying the name, holding parsed org, which is exactly what
108 // org's exporter emits. No block name is a blind spot. 120 // org's exporter emits. No block name is a blind spot.
109 Census::Block => return true, 121 Census::Block => return HANDLED,
110 Census::Drawer => KNOWN_DRAWERS, 122 Census::Drawer => (KNOWN_DRAWERS, UNKNOWN),
111 Census::Scheme => KNOWN_SCHEMES, 123 Census::Scheme => (KNOWN_SCHEMES, UNKNOWN),
112 }; 124 };
113 known.iter().any(|k| k.eq_ignore_ascii_case(name)) 125 if known.iter().any(|k| k.eq_ignore_ascii_case(name)) {
126 HANDLED
127 } else {
128 unlisted
129 }
114 } 130 }
115} 131}
116 132
@@ -330,21 +346,27 @@ impl Audit {
330 rest = &after[end..]; 346 rest = &after[end..];
331 } 347 }
332 348
333 if has_timestamp(line) { 349 // The rest read a line that has had its `=verbatim=` and `~code~` spans blanked:
350 // a construct shown inside verbatim is displayed rather than rendered, so a page
351 // documenting org syntax is not a use of the syntax it names. Emphasis pairs
352 // below deliberately still see the raw line — `=` is one of the markers scanned.
353 let bare = without_literal_spans(line);
354
355 if has_timestamp(&bare) {
334 self.count(Scope::In, "timestamp", at); 356 self.count(Scope::In, "timestamp", at);
335 } 357 }
336 if line.contains("{{{") { 358 if bare.contains("{{{") {
337 self.count(Scope::Out, "macro call", at); 359 self.count(Scope::Out, "macro call", at);
338 } 360 }
339 if line.contains("<<<") { 361 if bare.contains("<<<") {
340 self.count(Scope::Out, "radio target", at); 362 self.count(Scope::Out, "radio target", at);
341 } else if line.contains("<<") && line.contains(">>") { 363 } else if bare.contains("<<") && bare.contains(">>") {
342 self.count(Scope::Out, "internal target", at); 364 self.count(Scope::Out, "internal target", at);
343 } 365 }
344 if line.contains("\\begin{") || latex_inline(line) { 366 if bare.contains("\\begin{") || latex_inline(&bare) {
345 self.count(Scope::Out, "LaTeX fragment", at); 367 self.count(Scope::Out, "LaTeX fragment", at);
346 } 368 }
347 if entity_ref(line) { 369 if entity_ref(&bare) {
348 // Rendered, and rendered as org renders it: `\alpha` becomes `&alpha;` in 370 // Rendered, and rendered as org renders it: `\alpha` becomes `&alpha;` in
349 // both exporters. `fixtures/audit-entities.org` holds the oracle to that. 371 // both exporters. `fixtures/audit-entities.org` holds the oracle to that.
350 self.count(Scope::In, "entity (\\name)", at); 372 self.count(Scope::In, "entity (\\name)", at);
@@ -510,11 +532,7 @@ fn latex_inline(line: &str) -> bool {
510/// Only names org knows count, matching [`crate::parser`]'s rule for rendering one: a 532/// Only names org knows count, matching [`crate::parser`]'s rule for rendering one: a
511/// Windows path (`C:\Users\me`) and a namespaced identifier (`Tumblr\API\Client`) are not 533/// Windows path (`C:\Users\me`) and a namespaced identifier (`Tumblr\API\Client`) are not
512/// entity references. 534/// entity references.
513///
514/// Verbatim and code spans are skipped: `=\alpha=` shows the name rather than rendering
515/// the character, so it is not a use of the feature.
516fn entity_ref(line: &str) -> bool { 535fn entity_ref(line: &str) -> bool {
517 let line = without_literal_spans(line);
518 let chars: Vec<char> = line.chars().collect(); 536 let chars: Vec<char> = line.chars().collect();
519 for (i, c) in chars.iter().enumerate() { 537 for (i, c) in chars.iter().enumerate() {
520 if *c != '\\' { 538 if *c != '\\' {
@@ -642,13 +660,9 @@ pub fn report(audit: &Audit) -> String {
642 names.sort_by(|a, b| b.1.occurrences.cmp(&a.1.occurrences).then(a.0.cmp(b.0))); 660 names.sort_by(|a, b| b.1.occurrences.cmp(&a.1.occurrences).then(a.0.cmp(b.0)));
643 out.push_str(&format!("\n{title}\n")); 661 out.push_str(&format!("\n{title}\n"));
644 for (name, tally) in names { 662 for (name, tally) in names {
645 let flag = if Audit::is_known(kind, name) {
646 " "
647 } else {
648 "??? "
649 };
650 out.push_str(&format!( 663 out.push_str(&format!(
651 "{flag}{:<32} {:>8} {:>7} {}\n", 664 "{:<4}{:<32} {:>8} {:>7} {}\n",
665 Audit::marker(kind, name),
652 name, 666 name,
653 tally.occurrences, 667 tally.occurrences,
654 tally.files, 668 tally.files,
@@ -656,6 +670,10 @@ pub fn report(audit: &Audit) -> String {
656 )); 670 ));
657 } 671 }
658 } 672 }
659 out.push_str("\n`???` marks a name the implementation does not recognize at all.\n"); 673 out.push_str(
674 "\n`???` marks a name the implementation does not recognize at all.\n\
675 `\u{2014}` marks a keyword with no dedicated handling; it reaches templates \
676 as `page.keywords.<name>`.\n",
677 );
660 out 678 out
661} 679}
tests/constructs.rs +32
@@ -844,3 +844,35 @@ fn audit_counts_special_blocks_as_in_scope() {
844 "no block name is unrecognised — every one renders: {flagged:?}" 844 "no block name is unrecognised — every one renders: {flagged:?}"
845 ); 845 );
846} 846}
847
848/// A page that documents org syntax names constructs inside `=verbatim=`; the names are
849/// displayed, not rendered, so none of them is a use of the construct. Counting them
850/// reported orgo's own guide as using macros it does not support.
851#[test]
852fn audit_ignores_constructs_inside_verbatim() {
853 let report = audit_fixture("audit-verbatim-constructs.org");
854 for construct in ["macro call", "radio target", "internal target", "timestamp", "LaTeX fragment"]
855 {
856 assert!(
857 !report.contains(construct),
858 "{construct} was counted from a verbatim mention:\n{report}"
859 );
860 }
861}
862
863/// An unlisted keyword is not a blind spot: reaching templates as `page.keywords.<name>`
864/// is the designed behaviour, so it is marked as pass-through rather than `???`, which
865/// claims orgo does not recognise the name at all.
866#[test]
867fn audit_marks_unhandled_keywords_as_pass_through() {
868 let report = audit_fixture("audit-keywords.org");
869 for name in ["LEDE", "PROJECT_STATUS"] {
870 let line = report.lines().find(|l| l.contains(name)).expect("the keyword is censused");
871 assert!(
872 line.starts_with('\u{2014}'),
873 "a keyword that reaches templates must not read as a blind spot:\n{line}"
874 );
875 }
876 let flagged: Vec<&str> = report.lines().filter(|l| l.starts_with("???")).collect();
877 assert!(flagged.is_empty(), "no keyword name is unrecognised: {flagged:?}");
878}