feat(api-discovery): add tier=module for module-repo lints

Modules differ from libs structurally: they have no consumable public
API (their hooks like M.update are engine-contract, documented in the
engine spec, not in the module). The README convention for modules
replaces the ## API section with ## Controls + ## Demonstrates.

This adds tier=module validation in validate_readme_structure with
MUST-sections H1, Abstract via Module-ID badge, Topology + Topology-
Block markers, Controls, Demonstrates, References. Order-check
ensures Demonstrates precedes References (no API exists to check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-05-17 01:29:01 +02:00
parent 97df886e0f
commit e919cb082b

View File

@@ -144,6 +144,10 @@ end
-- Tier "core" enforces: H1, Abstract, Badges, Topology, Topology-Block, API, References. -- Tier "core" enforces: H1, Abstract, Badges, Topology, Topology-Block, API, References.
-- Tier "engine" enforces the same MUST-sections, but the Badges check accepts -- Tier "engine" enforces the same MUST-sections, but the Badges check accepts
-- any bold key:value line (engine README has no Lib-ID; instead Version/License). -- any bold key:value line (engine README has no Lib-ID; instead Version/License).
-- Tier "module" enforces: H1, Abstract via Module-ID badge, Topology + Topology-Block,
-- Controls, Demonstrates, References. Modules have no consumable public API
-- (engine-hooks are documented in the engine spec), so ## API is replaced by
-- ## Controls + ## Demonstrates.
-- Tier "community" enforces nothing (returns empty result). -- Tier "community" enforces nothing (returns empty result).
-- Returns: { missing_sections = [...], section_order_ok = bool } -- Returns: { missing_sections = [...], section_order_ok = bool }
function M.validate_readme_structure(markdown_string, tier) function M.validate_readme_structure(markdown_string, tier)
@@ -151,22 +155,38 @@ function M.validate_readme_structure(markdown_string, tier)
return { missing_sections = {}, section_order_ok = true } return { missing_sections = {}, section_order_ok = true }
end end
-- Both "core" and "engine" tiers use the same MUST-sections list.
-- Difference is handled upstream (engine tier skips parse_lua_surface etc.).
local missing = {} local missing = {}
local checks = { local checks
{ name = "H1", pattern = "^#%s+%S" },
{ name = "Badges", pattern = "\n%*%*Lib%-ID:%*%*%s*lib%-" },
{ name = "Topology", pattern = "\n##%s+Topology[%s\n]" },
{ name = "Topology-Block", pattern = "<!%-%-%s*topology:start" },
{ name = "API", pattern = "\n##%s+API[%s\n]" },
{ name = "References", pattern = "\n##%s+References[%s\n]" },
}
-- Engine-tier: relax the Badges check (engine uses "Version", "License" etc. if tier == "module" then
-- instead of a "Lib-ID:" prefix — engine is not a lib). -- Module-tier: no ## API (engine-hooks aren't a consumable surface);
if tier == "engine" then -- Badges identifies the module via **Module-ID:**; Controls + Demonstrates
checks[2] = { name = "Badges", pattern = "\n%*%*[%w%-]+:%*%*" } -- are the module-specific README sections.
checks = {
{ name = "H1", pattern = "^#%s+%S" },
{ name = "Badges", pattern = "\n%*%*Module%-ID:%*%*%s*" },
{ name = "Topology", pattern = "\n##%s+Topology[%s\n]" },
{ name = "Topology-Block", pattern = "<!%-%-%s*topology:start" },
{ name = "Controls", pattern = "\n##%s+Controls[%s\n]" },
{ name = "Demonstrates", pattern = "\n##%s+Demonstrates[%s\n]" },
{ name = "References", pattern = "\n##%s+References[%s\n]" },
}
else
-- Both "core" and "engine" tiers use the same MUST-sections list.
-- Difference is handled upstream (engine tier skips parse_lua_surface etc.).
checks = {
{ name = "H1", pattern = "^#%s+%S" },
{ name = "Badges", pattern = "\n%*%*Lib%-ID:%*%*%s*lib%-" },
{ name = "Topology", pattern = "\n##%s+Topology[%s\n]" },
{ name = "Topology-Block", pattern = "<!%-%-%s*topology:start" },
{ name = "API", pattern = "\n##%s+API[%s\n]" },
{ name = "References", pattern = "\n##%s+References[%s\n]" },
}
-- Engine-tier: relax the Badges check (engine uses "Version", "License" etc.
-- instead of a "Lib-ID:" prefix — engine is not a lib).
if tier == "engine" then
checks[2] = { name = "Badges", pattern = "\n%*%*[%w%-]+:%*%*" }
end
end end
for _, c in ipairs(checks) do for _, c in ipairs(checks) do
@@ -175,10 +195,18 @@ function M.validate_readme_structure(markdown_string, tier)
end end
end end
-- Order check: API must appear before References in source order -- Order check: for module tier Demonstrates must precede References
local api_pos = string.find(markdown_string, "\n##%s+API[%s\n]") -- (no API exists to check); for non-module tiers API must precede References.
local ref_pos = string.find(markdown_string, "\n##%s+References[%s\n]") local order_ok
local order_ok = (api_pos and ref_pos and api_pos < ref_pos) or (not api_pos and not ref_pos) if tier == "module" then
local dem_pos = string.find(markdown_string, "\n##%s+Demonstrates[%s\n]")
local ref_pos = string.find(markdown_string, "\n##%s+References[%s\n]")
order_ok = (dem_pos and ref_pos and dem_pos < ref_pos) or (not dem_pos and not ref_pos)
else
local api_pos = string.find(markdown_string, "\n##%s+API[%s\n]")
local ref_pos = string.find(markdown_string, "\n##%s+References[%s\n]")
order_ok = (api_pos and ref_pos and api_pos < ref_pos) or (not api_pos and not ref_pos)
end
return { missing_sections = missing, section_order_ok = order_ok } return { missing_sections = missing, section_order_ok = order_ok }
end end