Files
Axel Meyer 1cab8610b1 lib-management.dep-fetcher v0.1.0: closure walk + conflict + state-check
Pure-Lua manifest-walker for the dep-fetcher slice 5. Reads a module's
manifest.module, walks the transitive lib-dep tree, detects pin
conflicts (hard-fail), and uses lib-core.git to ensure every lib in
the closure is at the pinned v<X.Y.Z> tag.

State-check covers cases A-E from the dep-fetcher architecture spec:
missing (clone), clean (no-op), dirty (warn), wrong-tag (silent
checkout), broken (error).

Ships an inline JSON decoder (~80 lines, deterministic, no deps)
because the engine has no generic engine.json.decode binding yet —
engine.asset.load_json is sandboxed to the asset-tree and cannot read
fixture/install paths.

Stop-gaps documented in the lib (resolved by slice 6):
- opts.install_root substitutes for engine.install_root() binding
- gitea_base hardcoded; slice 6 sources from engine.json via
  engine.config(key)

Gitea-slug derivation handles the .git-suffix exception: lib-core.git
maps to sporel-lib-core.git-lib because Gitea reserves the *.git
suffix pattern. Encoded as generic predicate so future .git-suffixed
libs work without code change.
2026-05-31 01:46:49 +02:00

3.4 KiB

lib-management.dep-fetcher

Pure-Lua manifest-walker + dep-closure resolver. Reads a module's manifest.module, walks the transitive lib-dep tree, detects pin conflicts, then uses lib-core.git to ensure every lib in the closure is at the pinned v<X.Y.Z> tag locally — cloning missing libs, checking out wrong-tag libs, surfacing dirty libs as warnings.

Slice 5 of the dep-fetcher plan (sporel-meta/docs/superpowers/plans/2026-05-30-dep-fetcher-implementation.md).

API

local depf = require("lib-management.dep-fetcher")

-- Primary entry point
local result = depf.ensure_for_module_at(manifest_path, opts)

-- Convenience wrapper for install-layout resolution (slice 6: switches
-- to engine.install_root() once that binding lands).
local result = depf.ensure_for_module(module_id, opts)

opts

Field Type Notes
install_root string Root of <staged>/libs/. Falls back to env SPOREL_INSTALL_ROOT. Slice 6 will add engine.install_root() and drop this stop-gap.
check_only bool Skip all git operations. Closure walk + conflict detection only — used by unit tests and by the launcher's dep-diagnose surface.
gitea_base string Override base URL (default https://git.davoryn.de/sporel). Slice 6 will source from engine.json via engine.config(key).

result

{
  ok        : bool,                                 -- false if conflict or error
  conflicts : { {lib_id, pins=[{source,version}]}, ... },
  warnings  : { {lib_id, kind="dirty", describe}, ... },
  errors    : { {lib_id, kind, message}, ... },
  closure   : { {source, lib_id, version}, ... },
}

errors[].kind ∈ { "config", "manifest-read", "net-fail", "describe-fail", "checkout-fail" }.

State-check cases

Per spec sporel-meta/docs/superpowers/specs/2026-05-30-dep-fetcher-architecture.md §5:

Case Condition Action
A is_repo == false git.clone(url, dir, v<X.Y.Z>)
B describe == v<X.Y.Z> No-op (clean)
C describe matches v<X.Y.Z>-N-g… or ends -dirty Warning, no action
D describe is some other tag Silent git.checkout(dir, v<X.Y.Z>)
E describe returns nil/err Error

Gitea-slug derivation

Default: sporel-<lib-id>. Exception for libs whose ID ends in .git (currently only lib-core.git): the Gitea repo name pattern *.git is reserved by the protocol, so the slug appends -lib:

lib-core.maps         → sporel-lib-core.maps
lib-management.input  → sporel-lib-management.input
lib-core.git          → sporel-lib-core.git-lib    (exception)

This rule is encoded as a generic predicate (_gitea_slug_for) so any future .git-suffixed library uses the same translation without code change.

Visibility model (slice 5 status)

Only the Vagrant transitive closure is public. Everything else is private (paid content, Steam-backed Gitea-SSO planned). Slice 5 intentionally does not plumb credentials; if a private repo is in the closure and not locally present, the clone returns errors[] with kind="net-fail". That is the documented limit of this slice.

JSON parser

Bundled inline (see init.lua::json_decode). The engine has no generic engine.json.decode binding yet — engine.asset.load_json is sandboxed to the asset-tree and cannot read fixture/install paths. A future slice may extract this parser into a shared utility lib; until then it lives here.