; -*- mode: markdown -*-
# fed-sx host-type federation — substrate design + build log

How a host's typed-post graph (refinement types declared in
`lib/host`'s metamodel) flows across fed-sx nodes: a type is published
as a content-addressed `DefineType` activity, peers cache its record,
serve it over the wire, and validate inbound objects against the
declared refinement schema before appending them.

This document is both the design and the running build log for
`loops/fed-sx-types`. The companion build sheet is
`plans/agent-briefings/fed-sx-types-loop.md`.

## Vocabulary

- **Type record** — `{name, fields, refinement-schema, instance-type}`.
  The parsed `:object` payload of a `DefineType` activity. Immutable
  per CID: an updated type is a new CID (no in-place evolution).
- **Type CID** — content-address of the type record's wire form. The
  stable handle a `SubtypeOf` edge or an object's `{type, _}` field
  references.
- **Refinement schema** — a predicate over an object's field-values;
  the extra constraint a refinement type adds on top of its base
  `instance-type` (e.g. a `Post` is a `Note` whose `:title` is a
  non-empty string).

## Scope

Substrate side only — everything under `next/**`. The host-side
adapters (`lib/host/fed_sx_outbox.sx`, `lib/host/fed_sx_inbox.sx`)
are a deliberate follow-up that consumes this branch's public surface
(`DefineType` / `SubtypeOf` verbs, `peer_types`, the `/types/<cid>`
route) once `loops/host`'s metamodel settles. **This loop does not
touch `lib/host/`.**

## Steps

### Step 1 — `DefineType` + `SubtypeOf` genesis activity-types — DONE

New `DefineActivity`-form genesis files, parsed as data by
`bootstrap.erl` at startup (no kernel change yet):

- `next/genesis/activity-types/define_type.sx` — declares the
  `DefineType` verb. `:schema` accepts an activity whose `:object`
  carries a string `:name` and an optional list `:fields`.
- `next/genesis/activity-types/subtype_of.sx` — declares the
  `SubtypeOf` verb. `:schema` accepts an `:object` carrying both
  `:child-type-cid` and `:parent-type-cid` as strings.

Schema bodies are SX source written with nested `get` (not
keyword-threading) so they are directly evaluatable: keywords are not
callable getters in the kernel and `(-> d :k)` does not get. Both are
registered in `next/genesis/manifest.sx` (activity-types now 7) and the
bundle counts in the bootstrap suites were bumped accordingly.

Tests: `next/tests/define_type.sh`, `next/tests/subtype_of.sh` — parse
shape, schema accept/reject, and a `term_codec` envelope round-trip.

### Step 2 — `peer_types.erl` receiver-side cache — DONE

`next/kernel/peer_types.erl`, a mirror of `peer_actors.erl` keyed by
type CID. State `[{TypeCidBytes, TypeRecord}, ...]`. Pure API
(`new/2`-threaded `lookup`/`store`/`evict`/`types`/`lookup_or_fetch`)
plus a registered gen_server (`put`, `lookup`, `state_for`,
`known_types`, `lookup_or_fetch`). On a miss `lookup_or_fetch` pulls a
Cfg-supplied `type_fetch_fn :: fun ((TypeCid, Cfg) -> {ok, Bytes} |
{error, _})`, decodes the wire bytes via `term_codec`, and caches the
record. No fn → `{error, no_fetch_fn}`; fetch error or bad bytes do not
poison the cache. Test: `next/tests/peer_types.sh`.

### Step 3 — `/types/<cid>` route + `discovery_type_fetch.erl` — DONE

`http_server.erl` serves `GET /types/<cid>` with
`Accept: application/vnd.fed-sx.type-doc`: the cached TypeRecord
`term_codec`-encoded, 404 if not cached. `discovery_type_fetch.erl`
holds the live-HTTP closure that `peer_types:lookup_or_fetch` calls.
Tests: `next/tests/peer_types_route.sh`, `next/tests/discovery_type_fetch.sh`.

### Step 4 — object-schema validation stage in `pipeline.erl` — DONE

`pipeline:apply_object_schema/2` (+ `stage_object_schema/1` factory)
sits between activity-type validation and the kernel append. When an
inbound object carries `{type, TypeName}`, resolve the TypeRecord
(Cfg `type_index`: TypeName → TypeCid; then
`peer_types:lookup_or_fetch/2`) and apply its refinement schema to the
object's `:field_values`. The schema is either a 1-arity Erlang
predicate (the substrate stand-in, for locally-defined types) or a
term_codec-safe `{required, [Field, ...]}` data constraint (so a
wire-fetched record validates too). Default `strict_object_schema =
false`: an unresolvable type is let through (the non-strict skip is
where a `validation_skipped` log belongs); opt-in strict rejects.
Objects with no declared type, and type names absent from the local
index, are skipped (open-world). Test: `next/tests/object_schema.sh`.

## Out of scope (deliberately)

- Host-side outbox/inbox adapters (`lib/host/**`).
- Type evolution / version migration — schemas are immutable per CID;
  the "name → currently-valid CID" routing layer is a separate problem.
- Subtype-of unification / rendering across nodes — the graph data
  lands via `SubtypeOf` activities; dedup/display is a consumer concern.

## What the host-side adapter loop gets

Once all four steps land, the follow-up `loops/host` adapter work can
treat the following as stable public surface:

- `DefineType` / `SubtypeOf` activity verbs (publish a type, link two).
- `peer_types` gen_server (cache a peer's type, look it up).
- `GET /types/<cid>` (serve a type the node knows).
- `pipeline`'s object-schema stage (inbound objects validated against
  their declared refinement type when resolvable).
