StackReader
Every backing service the booted plugins declared they need, with what can be observed about it. Discovery is `instanceof StackProviderInterface` over `Kernel::plugins()` — the same pattern the routes table uses. For each declaration the snapshot carries the declaration (image, ports, env, volumes, command, summary, the declaring plugin), the env with its values RESOLVED — a secret has no value here and none downstream; the glyph a human sees is the renderer's —, the service's compose fragment, and its state: `up` when the probe port accepts TCP on the probe's host — and, when the declaration names a {@see ServiceSignature}, answers it as declared —, `occupied` when it accepts but answers otherwise (something else took the port), `down` when it refuses, `unknown` when the service publishes no port to probe, `conflict` when another plugin declared the same name. A collision drops nothing: every colliding row is kept and names the others, because a compose file keys services by name and two declarations under one key would silently overwrite each other. Nothing here starts or stops anything (greenhouse decisions/0201). Reading what was declared is not running it: that decision is about the VERB, and this class has none. 🚨 IT LIVES NEXT TO THE CONTRACT BECAUSE IT HAS TWO CONSUMERS NOW, and it used to have one. It was `Milpa\Admin\Data\StackSource`, written for the panel's Stack section — and `milpa/admin` is opt-in, so a fresh app could not see the services its own plugins declared. Measured on cattle: a Mercure hub was running on 127.0.0.1:3000, the exact port `AgentWorkspacePlugin` declares, and the app answered `GET /desktop/hub` with `{}` and swallowed the one sentence that said where to go, because that sentence names a section that ships elsewhere (greenhouse decisions/0282). The alternative was a second reader inside the operation. This family has paid for that shape often enough to name it: one question, one implementation, and the surfaces are callers (greenhouse decisions/0266).
StackReader::__construct()
public function __construct(Milpa\Interfaces\Di\DIContainerInterface $container, Milpa\Runtime\Stack\ReachabilityProbe $probe, Milpa\Runtime\Stack\ComposeProjection $projection, ?object $fallbackProvider = null, ?Milpa\Runtime\Stack\SignatureProbe $signatures = null):Parameters
| Name | Type | Description |
|---|---|---|
| $container | Milpa\Interfaces\Di\DIContainerInterface | |
| $probe | Milpa\Runtime\Stack\ReachabilityProbe | |
| $projection | Milpa\Runtime\Stack\ComposeProjection | |
| $fallbackProvider | (object | null) | a provider to read when no kernel is in the container (tests) |
| $signatures | (SignatureProbe | null) | asks a declared signature; defaults to one HTTP request to the reachability probe's host |
StackReader::snapshot()
public function snapshot(): arrayThe services table, sorted by name, each with its resolved env, compose fragment and probed state.
StackReader::declarations()
public function declarations(): arrayThe declarations themselves, sorted by name — what the compose route projects. Colliding declarations are in here too: refusing them is the caller's decision ({@see self::conflicts()}).
StackReader::conflicts()
public function conflicts(): arrayThe service names more than one plugin declared, each with every plugin that declared it — empty when every name is declared once.
StackReader::config()
public function config(): ?Milpa\Runtime\ConfigThe app's config bag when the container carries one — where `configKey` values are read from.