Compare commits
6 Commits
2e72de2ea6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00edd432da | ||
|
|
e919cb082b | ||
|
|
97df886e0f | ||
|
|
a8830c0680 | ||
|
|
a169c6c74b | ||
|
|
78cf0eb980 |
24
LICENSE
Normal file
24
LICENSE
Normal 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
161
README.md
@@ -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
100
init.lua
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user