libs/exclusion-filter.lua

148 lines

1-- {{{ exclusion-filter.lua
2-- Issue 6-031: Configurable poem exclusion filter
3-- Issue 10-003: Now loads from config.lua excluded_poems section
4--
5-- Excluded poems leave gaps in the ID sequence (tombstoning) -
6-- IDs are assigned first, then exclusions are applied, preserving
7-- stable anchor links and external references.
8--
9-- Usage:
10-- local exclusion = require("exclusion-filter")
11-- local filter = exclusion.load_default(DIR)
12-- if filter:is_excluded("fediverse", "113847291038475") then
13-- -- skip this poem
14-- end
15-- }}}
16
17local M = {}
18
19-- {{{ local function load_exclusions_from_config
20-- Loads exclusions from unified config, converting arrays to sets
21-- Returns a table like: { fediverse = { ["123"] = true }, notes = { ["foo"] = true } }
22local function load_exclusions_from_config(project_dir)
23 local exclusions = {
24 fediverse = {},
25 notes = {},
26 messages = {},
27 bluesky = {}
28 }
29 local total_count = 0
30
31 -- Load config-loader
32 local ok, config_loader = pcall(require, "config-loader")
33 if not ok then
34 return exclusions, total_count
35 end
36
37 if project_dir then
38 config_loader.set_project_root(project_dir)
39 end
40
41 local config = config_loader.load()
42 if not config or not config.excluded_poems then
43 return exclusions, total_count
44 end
45
46 -- Convert arrays to sets
47 for category, ids in pairs(config.excluded_poems) do
48 if exclusions[category] and type(ids) == "table" then
49 for _, id in ipairs(ids) do
50 exclusions[category][tostring(id)] = true
51 total_count = total_count + 1
52 end
53 end
54 end
55
56 return exclusions, total_count
57end
58-- }}}
59
60-- {{{ ExclusionFilter class
61-- Object that holds loaded exclusions and provides query methods
62local ExclusionFilter = {}
63ExclusionFilter.__index = ExclusionFilter
64
65-- {{{ function ExclusionFilter:is_excluded
66-- Check if a poem should be excluded.
67-- category: string like "fediverse", "notes", "messages", "bluesky"
68-- id: the category-specific identifier (will be converted to string)
69-- Returns: true if excluded, false otherwise
70function ExclusionFilter:is_excluded(category, id)
71 if not self.exclusions[category] then
72 return false
73 end
74 return self.exclusions[category][tostring(id)] == true
75end
76-- }}}
77
78-- {{{ function ExclusionFilter:count
79-- Returns the count of exclusions for a category, or total if no category specified
80function ExclusionFilter:count(category)
81 if category then
82 local count = 0
83 for _ in pairs(self.exclusions[category] or {}) do
84 count = count + 1
85 end
86 return count
87 end
88 return self.total_count
89end
90-- }}}
91
92-- {{{ function ExclusionFilter:get_excluded_ids
93-- Returns a list of excluded IDs for a category (for logging/debugging)
94function ExclusionFilter:get_excluded_ids(category)
95 local ids = {}
96 for id, _ in pairs(self.exclusions[category] or {}) do
97 table.insert(ids, id)
98 end
99 table.sort(ids)
100 return ids
101end
102-- }}}
103
104-- {{{ function ExclusionFilter:summary
105-- Returns a summary string for logging
106function ExclusionFilter:summary()
107 local parts = {}
108 for _, cat in ipairs({"fediverse", "notes", "messages", "bluesky"}) do
109 local count = self:count(cat)
110 if count > 0 then
111 table.insert(parts, string.format("%s: %d", cat, count))
112 end
113 end
114 if #parts == 0 then
115 return "no exclusions configured"
116 end
117 return table.concat(parts, ", ")
118end
119-- }}}
120-- }}}
121
122-- {{{ function M.load_default
123-- Load exclusions from unified config (config.lua excluded_poems section)
124-- dir: project directory (optional, for config-loader)
125-- Returns: ExclusionFilter object
126function M.load_default(dir)
127 local exclusions, total_count = load_exclusions_from_config(dir)
128
129 local filter = setmetatable({
130 exclusions = exclusions,
131 total_count = total_count,
132 source = "config.lua"
133 }, ExclusionFilter)
134
135 return filter
136end
137-- }}}
138
139-- {{{ function M.load
140-- Backward compatibility wrapper - now just calls load_default
141-- file_path parameter is ignored (kept for API compatibility)
142function M.load(file_path, dir)
143 return M.load_default(dir)
144end
145-- }}}
146
147return M
148