A `{ref, bind}` to a TCP-exposed deployment (e.g. postgres) projected
nothing, so consumers hardcoded the docker bridge IP. Project the address
it's already documented as reachable at.
107 lines
5.3 KiB
Markdown
107 lines
5.3 KiB
Markdown
# Relationships: requires, repos, and derived predicates
|
|
|
|
How wildpc models the relationships between **programs**, **deployments**, and
|
|
**repos** — and answers questions like *"is this functional?"*, *"is it fresh?"*,
|
|
*"is it deployed?"* — with the smallest possible amount of stored state.
|
|
|
|
## The governing principle
|
|
|
|
> **Predicates are always derived. Encode only what is not derivable.**
|
|
|
|
A *predicate* is a question we ask about a program or deployment: `functional?`,
|
|
`fresh?`, `deployed?`. None of these are ever stored — each is a **function** over
|
|
data wildpc already has (git, config, the registry). When a predicate can't be
|
|
answered from derived data, find the one missing datum and ask: is it about the
|
|
**thing** (a node property) or about a **relationship** (an edge property)? Encode
|
|
*only* that datum. Everything else stays computed.
|
|
|
|
This is the same instinct as `kind` (derived from `manager`) — we don't store what
|
|
we can compute, and a relationship that proves real and stable in the derived graph
|
|
is a candidate to *promote* into a first-class concept. Diagnostic → evidence →
|
|
abstraction, in that order.
|
|
|
|
## Entities
|
|
|
|
- **program** — the software catalog entry.
|
|
- **deployment** — a program realized on this node (`kind` derived from `manager`).
|
|
- **repo** — a git working copy. **Derived** from `git rev-parse --show-toplevel`
|
|
on each program's source; several programs sharing one toplevel is a *monorepo*.
|
|
Never stored.
|
|
|
|
## Encoded preconditions: `requires` + `system_dependencies`
|
|
|
|
Everything we were calling "substrate", "wiring", or "dependency" reduces to
|
|
preconditions ("A must have B to be functional"), encoded on the layer each belongs
|
|
to. The relationship model unifies them into one requirement set with a typed target;
|
|
the **kind fixes the meaning and the check**:
|
|
|
|
| kind | source (where encoded) | means | checked by |
|
|
|------|------------------------|-------|-----------|
|
|
| `deployment` | the **deployment**'s `requires` | another deployment must **exist** | registry / config |
|
|
| `system` | the **program**'s `system_dependencies` | the host package/binary must be **installed** | `which` / `dpkg` |
|
|
|
|
A **deployment** declares the deployments it depends on. `kind` defaults to
|
|
`deployment`, so an entry is just a `ref` (+ optional `bind`):
|
|
|
|
```yaml
|
|
# deployments/<kind>/astro.yaml
|
|
requires:
|
|
- ref: astro-guru
|
|
- ref: supabase
|
|
- { ref: litellm, bind: LITELLM_URL } # bind: project the target's URL into env
|
|
```
|
|
|
|
`path` is appended to the bound URL, for a consumer that wants a sub-path as its
|
|
base (e.g. `{ ref: vllm, bind: VLLM_API_BASE, path: /v1 }`).
|
|
A raw-TCP target (`expose.tcp`, e.g. postgres) has no URL scheme, so it binds as
|
|
`<name>.<domain>:<port>`.
|
|
|
|
A **program**'s host-package preconditions stay on the program as
|
|
`system_dependencies` (a plain list of package names); the model synthesizes the
|
|
`{kind: system}` requirements from it for the `functional?` check. This split keeps
|
|
each precondition on its natural layer — a deployment-ref is node-level wiring
|
|
(belongs on the deployment), a host package is intrinsic to the software (belongs on
|
|
the program). There is no `requires` on the program, and no `kind: system` written
|
|
into a deployment's `requires`.
|
|
|
|
Only encode a `requires` edge that is **not derivable** and that **wildpc itself
|
|
must traverse** for an operation (status, bring-up order, group ops). Do **not**
|
|
duplicate what another layer already owns — systemd `Requires=`/`After=` for unit
|
|
ordering, uv/pnpm for build graphs. This is *wildpc's* slice, uncoupled from any
|
|
one package ecosystem.
|
|
|
|
### Env is derived *from* `requires`, never scraped *into* it
|
|
|
|
Reading dependencies out of env strings is unstable (formats vary; a static
|
|
frontend's API URL is baked into its bundle and invisible). The stable direction is
|
|
the reverse: from an encoded `{ref, bind}` deployment requirement wildpc **generates**
|
|
the wiring env — it knows the target's address (`<ref>.<domain>` / its port) and
|
|
projects it into the consumer's env, optionally under the var named by `bind`. Same
|
|
move as `${public_url}`, one step further. Dependency → env, never env → dependency.
|
|
|
|
## Derived predicates
|
|
|
|
Computed on demand from encoded `requires` + git + registry; nothing stored:
|
|
|
|
- **`functional?`** — every `requires` is satisfied (system installed, deployment
|
|
exists). The unmet ones *are* the node's status (`doctor`/`status`).
|
|
- **`fresh?`** — the program's repo is at latest and clean (git status).
|
|
- **`deployed?`** — the deployment is active in the registry.
|
|
|
|
New predicates are just new functions over the same preconditions — there is
|
|
nothing to "unify" in storage.
|
|
|
|
## What's encoded vs derived (the whole surface)
|
|
|
|
| datum | source | stored? |
|
|
|-------|--------|---------|
|
|
| repo / monorepo | git toplevel | derived |
|
|
| `fresh?` / `deployed?` / `functional?` | git / registry / requires | derived |
|
|
| env wiring for a dependency | the `requires` edge + target address | derived |
|
|
| fan-in ("widely depended-on") | count of requires | derived |
|
|
| a **non-derivable** requirement (frontend→backend, host package) | — | **encoded** (`requires`) |
|
|
| the env var to bind a dep's URL to, when non-conventional | — | **encoded** (`requires[].bind`) |
|
|
|
|
Every irreducible found so far is an **edge** (a relationship); no new **node**
|
|
property has been needed yet — a sign the encoded surface stays tiny.
|