Files
sporel-lib-core.api-discovery/init.lua
Axel Meyer 4ab4bda0e7 feat: add validate_readme_structure (S2 Phase 1)
Validates MUST-Sections for tier=core (H1, Badges-Table, Topology-Block,
API, References) + section-order (API before References). Tier=community
returns empty (recommendation-only per spec section 3.1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:37:35 +02:00

133 lines
5.1 KiB
Lua

-- lib-core.api-discovery — Pure-Lua surface-discovery + README-parsing.
-- See: meta/docs/superpowers/specs/2026-05-16-api-doc-convention-design.md
local M = {}
-- Extracts public/private surface from Lua source code.
-- Convention: `M.<name> = function(...)` is public; `M._<name> = ...` is private.
-- Returns: { public = ["foo","bar",...], private = ["_baz",...] }
function M.parse_lua_surface(source_string)
local public = {}
local private = {}
for name in string.gmatch(source_string, "M%.([_%w]+)%s*=%s*function") do
if string.sub(name, 1, 1) == "_" then
table.insert(private, name)
else
table.insert(public, name)
end
end
return { public = public, private = private }
end
-- Extracts documented function-names from README's "## API" section.
-- Parses H3-Headers like "### `input.bind(action_name, keys)`" → "bind".
-- Convention: H3 header opens with backtick, function-name follows after optional namespace-dot.
-- Returns: { documented = ["bind","unbind",...] }
function M.parse_readme_api(markdown_string)
local documented = {}
-- Find "## API" section start (allow trailing whitespace/content)
local api_start = string.find(markdown_string, "\n## API[%s\n]")
if not api_start then
return { documented = documented }
end
-- Find next H2 (terminate API section)
local api_end = string.find(markdown_string, "\n## ", api_start + 5)
local section = string.sub(markdown_string, api_start, api_end or #markdown_string)
-- Match H3 headers: "### `[namespace.]name(...)`" — capture name portion
for line in string.gmatch(section, "###%s+`[^.`]*%.?([_%w]+)%s*[%(`]") do
table.insert(documented, line)
end
return { documented = documented }
end
-- Diffs code-surface vs README-doc.
-- Compares surface.public (set) vs readme.documented (set).
-- Returns: { missing_docs = [...], stale_docs = [...] }
function M.diff_surface(surface, readme)
local public_set = {}
for _, name in ipairs(surface.public) do public_set[name] = true end
local doc_set = {}
for _, name in ipairs(readme.documented) do doc_set[name] = true end
local missing = {}
for name in pairs(public_set) do
if not doc_set[name] then table.insert(missing, name) end
end
local stale = {}
for name in pairs(doc_set) do
if not public_set[name] then table.insert(stale, name) end
end
table.sort(missing); table.sort(stale)
return { missing_docs = missing, stale_docs = stale }
end
-- Extracts unique engine.<namespace>.<func> calls from Lua source.
-- Returns: array of unique strings, sorted.
function M.grep_engine_calls(source_string)
local seen = {}
for call in string.gmatch(source_string, "(engine%.[_%w]+%.[_%w]+)") do
seen[call] = true
end
local out = {}
for call in pairs(seen) do table.insert(out, call) end
table.sort(out)
return out
end
-- Generates mermaid topology-block from manifest + engine_calls.
-- engine_calls: array from grep_engine_calls (presence determines engine-node).
-- Returns: mermaid-source-string (no markers - caller wraps).
function M.generate_topology_block(manifest, engine_calls)
local lines = {"graph LR"}
local self_node = string.format(' this["%s"]', manifest.id)
table.insert(lines, self_node)
if manifest.deps then
for _, dep in ipairs(manifest.deps) do
local dep_var = string.gsub(dep.id, "[%-%.]", "_")
table.insert(lines, string.format(' %s["%s"]', dep_var, dep.id))
table.insert(lines, string.format(' this --> %s', dep_var))
end
end
if engine_calls and #engine_calls > 0 then
table.insert(lines, ' engine["engine.*"]')
table.insert(lines, ' this --> engine')
end
return table.concat(lines, "\n")
end
-- Validates README structure against MUST-sections for the given tier.
-- Tier "core" enforces: H1, Abstract, Badges-Table, Topology-Block, API, References.
-- Tier "community" enforces nothing (returns empty result).
-- Returns: { missing_sections = [...], section_order_ok = bool }
function M.validate_readme_structure(markdown_string, tier)
if tier ~= "core" then
return { missing_sections = {}, section_order_ok = true }
end
local missing = {}
local checks = {
{ name = "H1", pattern = "^#%s+%S" },
{ name = "Badges-Table", pattern = "\n|%s*Field%s*|" },
{ name = "Topology-Block", pattern = "<!%-%-%s*topology:start" },
{ name = "API", pattern = "\n##%s+API[%s\n]" },
{ name = "References", pattern = "\n##%s+References[%s\n]" },
}
for _, c in ipairs(checks) do
if not string.find(markdown_string, c.pattern) then
table.insert(missing, c.name)
end
end
-- Order check: API must appear before References in source order
local api_pos = string.find(markdown_string, "\n##%s+API[%s\n]")
local ref_pos = string.find(markdown_string, "\n##%s+References[%s\n]")
local order_ok = (api_pos and ref_pos and api_pos < ref_pos) or (not api_pos and not ref_pos)
return { missing_sections = missing, section_order_ok = order_ok }
end
return M