Repo-side rename only (Phases 1-3 of the migration plan); the live box (~/.castle, systemd units, /data/castle, domains) is a separate cutover. - Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api), dist names, entry point `wildpc = wildpc_cli.main:main`. - Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*. - Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml -> wildpc.yaml, /data/castle -> /data/wildpc. - Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc. - Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend. - Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated; redundant nested uv.lock files dropped (workspace root lock is authoritative). Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds. Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
5.0 KiB
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 (
kindderived frommanager). - repo — a git working copy. Derived from
git rev-parse --show-toplevelon 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):
# deployments/<kind>/astro.yaml
requires:
- ref: astro-guru
- ref: supabase
- { ref: litellm, bind: LITELLM_URL } # bind: project the target's URL into env
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?— everyrequiresis 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.