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
3131coverage: 9002 in-scope use(s) (100.0%), 0 out-of-scope (0.0%)
3232
3333KEYWORDS
34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4
35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2
34 SLUG 180 180 blog/2018-11-28-aes-encryption.org:4
35 TITLE 180 180 blog/2018-11-28-aes-encryption.org:2
36— LEDE 14 14 blog/2018-11-28-aes-encryption.org:3
3637...
3738#+END_EXAMPLE
3839
@@ -41,7 +42,12 @@ KEYWORDS
4142- The *coverage* line is the number to look at first.
4243- =???= marks a name orgo does not recognise at all. That is the blind-spot signal —
4344 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.
4551
4652Four censuses follow the construct table: every distinct =#+KEYWORD:=, block type,
4753drawer 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 {
7171 pub link_schemes: BTreeMap<String, Tally>,
7272}
7373
74/// Names the implementation understands, so the census can flag everything else. These
75/// are the *recognized* sets, not the supported ones: `INCLUDE` is recognized (it is
76/// deliberately inert) while an unlisted keyword is a genuine blind spot.
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 and
76/// deliberately inert.
7777const KNOWN_KEYWORDS: &[&str] = &[
7878 "TITLE", "AUTHOR", "DATE", "EMAIL", "LANGUAGE", "OPTIONS", "FILETAGS", "DESCRIPTION",
7979 "KEYWORDS", "CAPTION", "NAME", "ATTR_HTML", "RESULTS", "TBLFM", "INCLUDE", "TODO",
@@ -97,20 +97,36 @@ const KNOWN_SCHEMES: &[&str] = &[
9797 "relative",
9898];
9999
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
100106impl Audit {
101 /// Is this name one the implementation recognizes?
102 pub fn is_known(kind: Census, name: &str) -> bool {
103 let known = match kind {
104 Census::Keyword => KNOWN_KEYWORDS,
107 /// How the report marks a census name: the recognized set to look the name up in,
108 /// and what an unlisted name means for that census.
109 pub fn marker(kind: Census, name: &str) -> &'static str {
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),
105117 // Every block name renders, and renders as org renders it: the names in
106118 // `block_construct` through dedicated handling, every other name as a special
107119 // block — a div carrying the name, holding parsed org, which is exactly what
108120 // org's exporter emits. No block name is a blind spot.
109 Census::Block => return true,
110 Census::Drawer => KNOWN_DRAWERS,
111 Census::Scheme => KNOWN_SCHEMES,
121 Census::Block => return HANDLED,
122 Census::Drawer => (KNOWN_DRAWERS, UNKNOWN),
123 Census::Scheme => (KNOWN_SCHEMES, UNKNOWN),
112124 };
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 }
114130 }
115131}
116132
@@ -330,21 +346,27 @@ impl Audit {
330346 rest = &after[end..];
331347 }
332348
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) {
334356 self.count(Scope::In, "timestamp", at);
335357 }
336 if line.contains("{{{") {
358 if bare.contains("{{{") {
337359 self.count(Scope::Out, "macro call", at);
338360 }
339 if line.contains("<<<") {
361 if bare.contains("<<<") {
340362 self.count(Scope::Out, "radio target", at);
341 } else if line.contains("<<") && line.contains(">>") {
363 } else if bare.contains("<<") && bare.contains(">>") {
342364 self.count(Scope::Out, "internal target", at);
343365 }
344 if line.contains("\\begin{") || latex_inline(line) {
366 if bare.contains("\\begin{") || latex_inline(&bare) {
345367 self.count(Scope::Out, "LaTeX fragment", at);
346368 }
347 if entity_ref(line) {
369 if entity_ref(&bare) {
348370 // Rendered, and rendered as org renders it: `\alpha` becomes `&alpha;` in
349371 // both exporters. `fixtures/audit-entities.org` holds the oracle to that.
350372 self.count(Scope::In, "entity (\\name)", at);
@@ -510,11 +532,7 @@ fn latex_inline(line: &str) -> bool {
510532/// Only names org knows count, matching [`crate::parser`]'s rule for rendering one: a
511533/// Windows path (`C:\Users\me`) and a namespaced identifier (`Tumblr\API\Client`) are not
512534/// 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.
516535fn entity_ref(line: &str) -> bool {
517 let line = without_literal_spans(line);
518536 let chars: Vec<char> = line.chars().collect();
519537 for (i, c) in chars.iter().enumerate() {
520538 if *c != '\\' {
@@ -642,13 +660,9 @@ pub fn report(audit: &Audit) -> String {
642660 names.sort_by(|a, b| b.1.occurrences.cmp(&a.1.occurrences).then(a.0.cmp(b.0)));
643661 out.push_str(&format!("\n{title}\n"));
644662 for (name, tally) in names {
645 let flag = if Audit::is_known(kind, name) {
646 " "
647 } else {
648 "??? "
649 };
650663 out.push_str(&format!(
651 "{flag}{:<32} {:>8} {:>7} {}\n",
664 "{:<4}{:<32} {:>8} {:>7} {}\n",
665 Audit::marker(kind, name),
652666 name,
653667 tally.occurrences,
654668 tally.files,
@@ -656,6 +670,10 @@ pub fn report(audit: &Audit) -> String {
656670 ));
657671 }
658672 }
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 );
660678 out
661679}
tests/constructs.rs +32
@@ -844,3 +844,35 @@ fn audit_counts_special_blocks_as_in_scope() {
844844 "no block name is unrecognised — every one renders: {flagged:?}"
845845 );
846846}
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}