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.
92 lines
3.4 KiB
Markdown
92 lines
3.4 KiB
Markdown
# 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
|
|
|
|
```lua
|
|
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`
|
|
|
|
```lua
|
|
{
|
|
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.
|