From bb9fb51f00efc4477586aea01c743d7a7e237722 Mon Sep 17 00:00:00 2001 From: bits-bytes-nn Date: Thu, 12 Nov 2026 22:01:39 +0900 Subject: [PATCH 1/2] feat(lint): a controlled topic tag only when one fits the post --- _plugins/related_posts.rb | 4 ++-- docs/writing.md | 9 +++++---- script/lint-posts.rb | 1 - test/test_lint_posts.rb | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/_plugins/related_posts.rb b/_plugins/related_posts.rb index 2fae0c4..b49fc9e 100644 --- a/_plugins/related_posts.rb +++ b/_plugins/related_posts.rb @@ -19,8 +19,8 @@ # better than a wrong suggestion. # # That strictness depends on tags that can meet. Per-paper contribution tags -# rarely do, so each post should also carry a controlled topic tag (the list is -# `tags.controlled` in _data/post_conventions.yml) — a looser rule here is not the fix for a post with no suggestions. +# rarely do, so a post should also carry a controlled topic tag that fits it +# (the list is `tags.controlled` in _data/post_conventions.yml) — a looser rule here is not the fix for a post with no suggestions. # # Jekyll's built-in site.related_posts is either "the 10 most recent posts" or # LSI, which needs the classifier gem and a slow indexing pass. This is cheaper diff --git a/docs/writing.md b/docs/writing.md index 74520d0..2630128 100644 --- a/docs/writing.md +++ b/docs/writing.md @@ -155,11 +155,12 @@ Tags are free-form and hyphenated, and come in two layers: - **Specific tags** name the post's own contribution (`Fine-Grained-Expert-Segmentation`). Precise, but they connect nothing. -- **At least one controlled topic tag** from `tags.controlled` in - `_data/post_conventions.yml`. "Related reading" needs a shared tag, so a post - with only specific tags ships without it. +- **A controlled topic tag** from `tags.controlled` in + `_data/post_conventions.yml`, when one fits. "Related reading" needs a shared + tag, so a post with only specific tags ships without it; that is better than + a topic tag the post is not about, which suggests unrelated posts. -Add the topic tag; don't swap it for the specific ones. Reuse an existing tag +Add the topic tag that fits; don't swap it for the specific ones. Reuse an existing tag before coining one: three names for one idea leave every post with a tag no other post shares. When two tags turn out to be one, keep one and move the other to `tags.aliases`; the lint then rejects the retired name. Renaming a tag diff --git a/script/lint-posts.rb b/script/lint-posts.rb index e7cc15c..3d4a3c0 100644 --- a/script/lint-posts.rb +++ b/script/lint-posts.rb @@ -185,7 +185,6 @@ def self.tags(post, spec) rules = spec["tags"] tags = Array(post.data["tags"]).map(&:to_s) found = [] - found << "1: no controlled topic tag (one of #{rules["controlled"].join(", ")})" if (tags & rules["controlled"]).empty? tags.each do |tag| if (replacement = rules["aliases"][tag]) found << "1: tag #{tag} is retired; use #{replacement}" diff --git a/test/test_lint_posts.rb b/test/test_lint_posts.rb index 3490def..efaad84 100644 --- a/test/test_lint_posts.rb +++ b/test/test_lint_posts.rb @@ -97,9 +97,8 @@ def test_rejects_an_unknown_topic_and_a_wrong_cover assert_finding(/cover for Paper Reviews/, lint(front: FRONT.sub("language-models.jpg", "default.jpg"))) end - def test_tags_need_a_controlled_tag_and_current_well_formed_names + def test_tags_need_current_well_formed_names found = lint(front: FRONT.sub('"Efficient-Attention"', '"Sparse-Mixture-of-Experts", "Public-Dataset-Only Training", "Community-Detection-For-RAG"')) - assert_finding(/no controlled topic tag/, found) assert_finding(/retired; use Mixture-of-Experts/, found) assert_finding(/joined by hyphens/, found) assert_finding(/should have for/, found) From f38ea6fc58ec9228b751e0c808333c2f13fe332f Mon Sep 17 00:00:00 2001 From: bits-bytes-nn Date: Thu, 12 Nov 2026 22:08:35 +0900 Subject: [PATCH 2/2] feat(lint): still require some tags; reword the controlled-tag note --- _data/post_conventions.yml | 5 +++-- script/lint-posts.rb | 1 + test/test_lint_posts.rb | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/_data/post_conventions.yml b/_data/post_conventions.yml index a92475b..a6d860f 100644 --- a/_data/post_conventions.yml +++ b/_data/post_conventions.yml @@ -48,8 +48,9 @@ math: defined_macros: [llbracket, rrbracket, textsc] tags: - # Every post carries at least one: it is what _plugins/related_posts.rb joins - # posts on. A tag only one paper can have connects nothing. + # A post carries one when one names its topic: it is what + # _plugins/related_posts.rb joins posts on. A tag only one paper can have + # connects nothing. controlled: - Agentic-AI - Alignment diff --git a/script/lint-posts.rb b/script/lint-posts.rb index 3d4a3c0..5eebc7f 100644 --- a/script/lint-posts.rb +++ b/script/lint-posts.rb @@ -185,6 +185,7 @@ def self.tags(post, spec) rules = spec["tags"] tags = Array(post.data["tags"]).map(&:to_s) found = [] + found << "1: no tags" if tags.empty? tags.each do |tag| if (replacement = rules["aliases"][tag]) found << "1: tag #{tag} is retired; use #{replacement}" diff --git a/test/test_lint_posts.rb b/test/test_lint_posts.rb index efaad84..14f91a3 100644 --- a/test/test_lint_posts.rb +++ b/test/test_lint_posts.rb @@ -97,6 +97,12 @@ def test_rejects_an_unknown_topic_and_a_wrong_cover assert_finding(/cover for Paper Reviews/, lint(front: FRONT.sub("language-models.jpg", "default.jpg"))) end + def test_a_post_needs_tags_but_not_a_controlled_one + assert_finding(/no tags/, lint(front: FRONT.sub(/^tags:.*$/, "tags: []"))) + found = lint(front: FRONT.sub('"Efficient-Attention"', '"Multi-Token-Prediction"')) + refute found.any? { |f| f.include?("tag") }, found.inspect + end + def test_tags_need_current_well_formed_names found = lint(front: FRONT.sub('"Efficient-Attention"', '"Sparse-Mixture-of-Experts", "Public-Dataset-Only Training", "Community-Detection-For-RAG"')) assert_finding(/retired; use Mixture-of-Experts/, found)