local M = {}
local FAKE_LABELS = {
["Usage"] = true,
["Args"] = true,
["Arguments"] = true,
["Parameters"] = true,
["Params"] = true,
["Returns"] = true,
["Return"] = true,
["Example"] = true,
["Examples"] = true,
["Notes"] = true,
["Note"] = true,
["See"] = true,
["See also"] = true,
}
local function split_lines(s)
local lines = {}
local i = 1
local len = #s
while i <= len do
local j = s:find("\n", i, true)
if not j then
lines[#lines + 1] = s:sub(i)
break
end
lines[#lines + 1] = s:sub(i, j - 1)
i = j + 1
end
return lines
end
local function has_parameters_section(sections)
for i = 1, #sections do
if sections[i].heading == "Parameters" then return true end
end
return false
end
local function has_result_section(sections)
for i = 1, #sections do
if sections[i].heading == "Result" then return true end
end
return false
end
function M.check(pkg_info, docstring, pkg_dir)
local out = {}
local id = pkg_info.identity
local nar = pkg_info.narrative
local shp = pkg_info.shape
if not id.name or id.name == "" then
out[#out + 1] = {
severity = "error", code = "E_META_MISSING_NAME",
msg = "meta.name is required" }
end
if not id.version or id.version == "" then
out[#out + 1] = {
severity = "error", code = "E_META_MISSING_VERSION",
msg = "meta.version is required" }
end
if not id.description or id.description == "" then
out[#out + 1] = {
severity = "error", code = "E_META_MISSING_DESCRIPTION",
msg = "meta.description is required" }
end
if not id.category or id.category == "" then
out[#out + 1] = {
severity = "error", code = "E_META_MISSING_CATEGORY",
msg = "meta.category is required" }
end
if pkg_dir and id.name and id.name ~= "" and id.name ~= pkg_dir then
out[#out + 1] = {
severity = "error", code = "E_NAME_MISMATCH",
msg = string.format(
"meta.name='%s' does not match pkg directory '%s'",
id.name, pkg_dir) }
end
if id.description and id.description:find("\n", 1, true) then
out[#out + 1] = {
severity = "warning", code = "W_DESCRIPTION_MULTILINE",
msg = "meta.description contains a newline (keep it single-line)" }
end
local in_fence = false
local raw_lines = split_lines(docstring or "")
for i = 1, #raw_lines do
local line = raw_lines[i]
if line:match("^```") then
in_fence = not in_fence
elseif not in_fence then
if line:sub(1, 2) == "# " then
out[#out + 1] = {
severity = "error", code = "E_H1_IN_DOCSTRING",
msg = "H1 is reserved for the generator; drop `# ` prefix",
line = i }
end
local label = line:match("^([%w][%w ]*):%s*$")
if label and FAKE_LABELS[label] then
out[#out + 1] = {
severity = "warning", code = "W_FAKE_LABEL",
msg = string.format(
"line looks like a section label; promote to '## %s'", label),
line = i }
end
end
end
if (nar.summary == nil or nar.summary == "") and #nar.sections == 0 then
out[#out + 1] = {
severity = "warning", code = "W_EMPTY_NARRATIVE",
msg = "docstring has no summary and no sections" }
end
if shp.input ~= nil and has_parameters_section(nar.sections) then
out[#out + 1] = {
severity = "error", code = "E_PARAMETERS_CONFLICT",
msg = "spec.entries.run.input is declared AND docstring has a ## " ..
"Parameters section; remove the docstring section (shape is the SSoT)" }
end
if shp.result ~= nil and has_result_section(nar.sections) then
out[#out + 1] = {
severity = "error", code = "E_RESULT_CONFLICT",
msg = "spec.entries.run.result is declared AND docstring has a ## " ..
"Result section; remove the docstring section (shape is the SSoT)" }
end
return { violations = out }
end
function M.format(pkg_name, violations)
if #violations == 0 then return "" end
local parts = { string.format("# %s", pkg_name) }
for i = 1, #violations do
local v = violations[i]
local tag = v.severity == "error" and "ERROR" or "warn "
local loc = v.line and string.format(" (line %d)", v.line) or ""
parts[#parts + 1] = string.format(
" %s %s%s: %s", tag, v.code, loc, v.msg)
end
return table.concat(parts, "\n")
end
function M.errors(violations)
local out = {}
for i = 1, #violations do
if violations[i].severity == "error" then
out[#out + 1] = violations[i]
end
end
return out
end
M._internal = {
FAKE_LABELS = FAKE_LABELS,
split_lines = split_lines,
}
return M