Compare commits

...

6 Commits

Author SHA1 Message Date
Axel Meyer
00edd432da docs: CR-6 — README auf Core-Tier-Template (Badges/Topology/API/References)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:02:11 +00:00
Axel Meyer
e919cb082b 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>
2026-05-17 01:29:01 +02:00
Axel Meyer
97df886e0f feat(api-discovery): add tier=engine for engine-repo lints
The engine repo has neither init.lua nor manifest.lib — it's a C
codebase, not a Lua lib. Previously running --lint on it would fail
with read errors.

Add tier=engine: skips parse_lua_surface, parse_readme_api, and
topology generation, runs only validate_readme_structure for
structural compliance. Tier accepts a more flexible Badges pattern
since engine uses fields like Version/License instead of Lib-ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:45:02 +02:00
Axel Meyer
a8830c0680 chore: add Tier-1 LICENSE file
Adds the proprietary all-rights-reserved license that applies to this
library. The text is identical to the engine LICENSE — this lib is
Tier-1 official platform content, distributed under the same terms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:04:14 +02:00
Axel Meyer
a169c6c74b fix(parse_readme_api): support namespace-less function names
The previous single-pattern approach used a greedy `[^.\x60]*` segment
that mis-captured H3 headers without a namespace prefix (e.g. test-lib
entrypoints documented as `### \`run_tests(ctx)\``).

Replace with a two-pass match: pattern 1 captures `### \`ns.func(...)\``
(namespaced) and pattern 2 captures `### \`func(...)\`` (no namespace).
A seen-set prevents the namespace-less pass from re-matching names
already collected by the namespaced pass.

Enables test-libs to declare a public surface in their READMEs and
satisfy --lint's missing-docs check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:51:27 +02:00
Axel Meyer
78cf0eb980 fix(parse_lua_surface): strip Lua line-comments before matching
Previous implementation matched function-declaration patterns inside
Lua line-comments, causing false-positive missing_docs warnings for
commented-out forward-compat stubs (e.g. DEPRECATED-MVP placeholders
in camera and render libs, 5 each).

Pre-process source with gsub to strip "--" through end-of-line before
running the two gmatch passes. Block comments (--[[...]]) are not
handled; not used in this project's Lua sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:51:10 +02:00
3 changed files with 265 additions and 20 deletions

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
Copyright (c) 2026 Calic. All rights reserved.
This software is part of the Sporel platform — **Tier 1 (Official /
Proprietary)** content per the Three-Tier Licensing Model documented in
`meta/docs/archive/design/vision.md §Licensing Model` (current source;
migration to `meta/docs/architecture/licensing-model.md` pending).
⚠ **WIP — Legal review required before public launch.** The terms below
reflect design intent only; the formalized license framework will be
finalized through legal counsel before the first public release. Until
then, this notice serves as a placeholder defending the platform owner's
rights against unintentional re-licensing.
No license is granted to copy, modify, distribute, sublicense, or otherwise
use this software in any form without prior written permission from the
copyright holder.
References:
- Tier 1 (this file): all rights reserved, proprietary, sold/distributed
via official channels (Steam, etc.)
- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue-
share — see vision.md §Licensing Model
- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies
to community-uploaded libs/modules/assets, not this repo

161
README.md
View File

@@ -1,5 +1,162 @@
# lib-core.api-discovery
Pure-Lua-Library for surface-discovery (extract `M.*` from Lua source) and README-parsing (extract documented API from markdown). Sandbox-safe — no FS, no side-effects. Used by Sporel's `--lint` CLI mode.
Pure-Lua-Library für Surface-Discovery (extrahiert `M.*` aus Lua-Quelltext)
und README-Parsing (extrahiert die dokumentierte API aus Markdown). Außerdem
ein README-Struktur-Linter, der die API-Doc-Convention (ADR-0038) gegen die
MUST-Sections eines Tiers prüft. Sandbox-safe — kein FS-Zugriff, keine
Seiteneffekte. Verwendet vom Sporel-`--lint`-CLI-Modus.
Version: 0.1.0
**Version:** 0.1.0
**Lib-ID:** lib-core.api-discovery
**Requires:** (none)
**Tags:** api-discovery, lint, readme, surface, topology, tooling
## Topology
<!-- topology:start (auto-generated; do not edit) -->
```mermaid
graph LR
this["lib-core.api-discovery"]
```
<!-- topology:end -->
Reine Lua-Lib ohne Dependencies und ohne `engine.*`-Aufrufe — daher kein
`engine`-Knoten im Topology-Graphen (sandbox-safe per Design, damit der
Linter im `--lint`-CLI-Modus ohne Engine-Kontext laufen kann).
## API
### `api-discovery.parse_lua_surface(source_string)`
**Syntax:** `parse_lua_surface(source_string: string) -> {public: string[], private: string[]}`
**Example:**
```lua
local ad = require("lib-core.api-discovery")
local src = io.read_file("init.lua")
local s = ad.parse_lua_surface(src)
-- s.public == { "load", "create", ... }
-- s.private == { "_normalize", ... }
```
**Description:** Extrahiert die öffentliche und private Surface aus
Lua-Quelltext. Konvention: `M.<name>` ist öffentlich, `M._<name>` privat.
Erkennt beide Schreibweisen — explizite Zuweisung (`M.foo = function`) und
Syntax-Sugar (`function M.foo`). Lua-Zeilenkommentare (`--` bis Zeilenende)
werden vor dem Pattern-Matching entfernt, damit auskommentierte
Forward-Compat-Stubs (z. B. `DEPRECATED-MVP`-Platzhalter) keine
False-Positives erzeugen. Block-Kommentare (`--[[ ... ]]`) werden nicht
behandelt (in Sporel-Lib-Code nicht verwendet). Dedupliziert über alle
Namen. Liefert `{ public = [...], private = [...] }`.
### `api-discovery.parse_readme_api(markdown_string)`
**Syntax:** `parse_readme_api(markdown_string: string) -> {documented: string[]}`
**Example:**
```lua
local r = ad.parse_readme_api(readme_text)
-- r.documented == { "bind", "unbind", ... }
```
**Description:** Extrahiert die dokumentierten Funktionsnamen aus dem
`## API`-Abschnitt eines READMEs. Sucht den Abschnitt zwischen `## API` und
der nächsten H2-Überschrift und parst die H3-Header darin. Konvention: ein
H3-Header öffnet mit einem Backtick, der Funktionsname folgt nach einem
optionalen Namespace-Punkt — sowohl die namespaced-Form `ns.func(...)`
(Capture `func`) als auch die namespace-lose Form `func(...)` werden
erkannt. Dedupliziert; Pattern 2 matcht
keine Namen, die Pattern 1 bereits erfasst hat. Fehlt der `## API`-Abschnitt,
ist `documented` leer.
### `api-discovery.diff_surface(surface, readme)`
**Syntax:** `diff_surface(surface: {public: string[]}, readme: {documented: string[]}) -> {missing_docs: string[], stale_docs: string[]}`
**Example:**
```lua
local s = ad.parse_lua_surface(src)
local r = ad.parse_readme_api(readme_text)
local diff = ad.diff_surface(s, r)
-- diff.missing_docs == öffentliche Funktionen ohne README-Eintrag
-- diff.stale_docs == README-Einträge ohne öffentliche Funktion
```
**Description:** Vergleicht die Code-Surface (`surface.public`) gegen die
README-Dokumentation (`readme.documented`). `missing_docs` sind öffentliche
Funktionen ohne dokumentierten Eintrag; `stale_docs` sind dokumentierte
Einträge ohne korrespondierende öffentliche Funktion. Beide Listen sind
alphabetisch sortiert.
### `api-discovery.grep_engine_calls(source_string)`
**Syntax:** `grep_engine_calls(source_string: string) -> string[]`
**Example:**
```lua
local calls = ad.grep_engine_calls(src)
-- calls == { "engine.render.draw_rect", "engine.window.size", ... }
```
**Description:** Extrahiert alle eindeutigen `engine.<namespace>.<func>`-Aufrufe
aus Lua-Quelltext. Liefert ein dedupliziertes, alphabetisch sortiertes Array.
Wird von `generate_topology_block` genutzt, um zu entscheiden, ob ein
`engine.*`-Knoten in den Topology-Graphen aufgenommen wird.
### `api-discovery.generate_topology_block(manifest, engine_calls)`
**Syntax:** `generate_topology_block(manifest: {id: string, deps?: {id: string}[]}, engine_calls: string[]) -> string`
**Example:**
```lua
local manifest = { id = "lib-core.maps", deps = {} }
local calls = ad.grep_engine_calls(src)
local mermaid = ad.generate_topology_block(manifest, calls)
-- mermaid == "graph LR\n this[\"lib-core.maps\"]\n engine[\"engine.*\"]\n this --> engine"
```
**Description:** Generiert den Mermaid-Topology-Block aus Manifest +
`engine_calls`. Der `this`-Knoten trägt die `manifest.id`. Für jede
Dependency in `manifest.deps` wird ein Knoten plus `this --> dep`-Kante
ausgegeben (die Dep-ID wird für den Mermaid-Variablennamen entschärft: `-`
und `.``_`). Enthält `engine_calls` mindestens einen Eintrag, kommt ein
`engine["engine.*"]`-Knoten mit `this --> engine`-Kante hinzu. Liefert den
reinen Mermaid-Quelltext ohne Marker — der Aufrufer wrappt ihn in die
`<!-- topology:start -->` / `<!-- topology:end -->`-Marker.
### `api-discovery.validate_readme_structure(markdown_string, tier)`
**Syntax:** `validate_readme_structure(markdown_string: string, tier: "core"|"engine"|"module"|"community") -> {missing_sections: string[], section_order_ok: boolean}`
**Example:**
```lua
local res = ad.validate_readme_structure(readme_text, "core")
if #res.missing_sections > 0 or not res.section_order_ok then
-- README verletzt die API-Doc-Convention
end
```
**Description:** Prüft die README-Struktur gegen die MUST-Sections des
angegebenen Tiers.
- **`core`** verlangt: `H1`, `Badges` (ein `**Lib-ID:** lib-…`-Block),
`Topology` (`## Topology`), `Topology-Block` (`<!-- topology:start`-Marker),
`API` (`## API`) und `References` (`## References`).
- **`engine`** verlangt dieselben Sections, lockert aber den Badges-Check auf
eine beliebige fettgedruckte `**Key:**`-Zeile (die Engine ist keine Lib und
trägt kein `Lib-ID:`, sondern `Version`/`License`).
- **`module`** ersetzt `## API` durch `## Controls` + `## Demonstrates` und
identifiziert über `**Module-ID:**` statt `**Lib-ID:**` (Module haben keine
konsumierbare Public-API).
- **`community`** erzwingt nichts (liefert ein leeres Ergebnis).
`section_order_ok` ist `true`, wenn `## API` vor `## References` steht
(bzw. `## Demonstrates` vor `## References` im Module-Tier) — oder wenn
keine der beiden Sections vorhanden ist. Liefert
`{ missing_sections = [...], section_order_ok = bool }`.
## References
- API-Doc-Convention Spec: `meta/docs/superpowers/specs/2026-05-16-api-doc-convention-design.md`
- ADR-0038 (API-Doc-Convention)
- ADR-0001 (engine knows verbs, libs bring nouns)

100
init.lua
View File

@@ -8,6 +8,15 @@ local M = {}
-- (`function M.foo`) — Lua treats them semantically identical.
-- Returns: { public = ["foo","bar",...], private = ["_baz",...] }
function M.parse_lua_surface(source_string)
-- Strip Lua line-comments (-- to end-of-line) before pattern matching.
-- This prevents false-positive matches inside commented-out forward-compat
-- stubs (e.g. DEPRECATED-MVP placeholders).
-- Block comments (--[[...]]) are not handled; not used in Sporel-lib code.
-- Caveat: a literal "--" inside a string would also be stripped; acceptable
-- for the lint-tool's purpose (false-negatives in pathological string cases
-- are preferable to false-positives on commented stubs).
local stripped = string.gsub(source_string, "%-%-[^\n]*", "")
local public = {}
local private = {}
local seen = {}
@@ -23,12 +32,12 @@ function M.parse_lua_surface(source_string)
end
-- Form 1: M.foo = function(...)
for name in string.gmatch(source_string, "M%.([_%w]+)%s*=%s*function") do
for name in string.gmatch(stripped, "M%.([_%w]+)%s*=%s*function") do
classify(name)
end
-- Form 2: function M.foo(...)
for name in string.gmatch(source_string, "function%s+M%.([_%w]+)") do
for name in string.gmatch(stripped, "function%s+M%.([_%w]+)") do
classify(name)
end
@@ -41,6 +50,8 @@ end
-- Returns: { documented = ["bind","unbind",...] }
function M.parse_readme_api(markdown_string)
local documented = {}
local seen = {}
-- Find "## API" section start (allow trailing whitespace/content)
local api_start = string.find(markdown_string, "\n## API[%s\n]")
if not api_start then
@@ -50,10 +61,24 @@ function M.parse_readme_api(markdown_string)
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)
-- Two-pass matching to handle both namespace-prefixed and namespace-less
-- function-names in H3 headers.
-- Pattern 1: namespaced — "### `ns.func(...)`" -> capture "func"
for name in string.gmatch(section, "###%s+`[%w_]+%.([_%w]+)") do
if not seen[name] then
seen[name] = true
table.insert(documented, name)
end
end
-- Pattern 2: namespace-less — "### `func(...)`" -> capture "func"
-- The seen-set prevents re-matching names already captured by pattern 1.
for name in string.gmatch(section, "###%s+`([_%w]+)%s*[%(`]") do
if not seen[name] then
seen[name] = true
table.insert(documented, name)
end
end
return { documented = documented }
end
@@ -117,32 +142,71 @@ end
-- Validates README structure against MUST-sections for the given tier.
-- Tier "core" enforces: H1, Abstract, Badges, Topology, Topology-Block, API, References.
-- 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).
-- 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).
-- Returns: { missing_sections = [...], section_order_ok = bool }
function M.validate_readme_structure(markdown_string, tier)
if tier ~= "core" then
if tier == "community" then
return { missing_sections = {}, section_order_ok = true }
end
local missing = {}
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]" },
}
local checks
if tier == "module" then
-- Module-tier: no ## API (engine-hooks aren't a consumable surface);
-- Badges identifies the module via **Module-ID:**; Controls + Demonstrates
-- 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
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)
-- Order check: for module tier Demonstrates must precede References
-- (no API exists to check); for non-module tiers API must precede References.
local order_ok
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 }
end