Two new DefineActivity-form genesis activity-types for host-type federation (plans/fed-sx-host-types.md step 1): - next/genesis/activity-types/define_type.sx — DefineType verb; schema accepts an :object with a string :name and optional list :fields. - next/genesis/activity-types/subtype_of.sx — SubtypeOf verb; schema accepts an :object carrying string :child-type-cid + :parent-type-cid. Schema bodies use nested `get` (not keyword-threading) so they are directly evaluatable — keywords are not callable getters in the kernel. Both registered in manifest.sx (activity-types now 7); the four bootstrap suites' bundle counts bumped (5->7, total 36->38). Tests: next/tests/define_type.sh (7), subtype_of.sh (6) — parse shape, schema accept/reject, term_codec envelope round-trip. Also load follower_graph + delivery in bootstrap_start.sh: its check-26 publish path exercises outbox:compute_delivery_set/3 (follower_graph:new + delivery:delivery_set), which an m2 substrate change had left unloaded in that suite — a pre-existing red unrelated to the count bump. Conformance 771/771; all touched next/tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
; -- 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:objectpayload of aDefineTypeactivity. 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
SubtypeOfedge 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. aPostis aNotewhose:titleis 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 theDefineTypeverb.:schemaaccepts an activity whose:objectcarries a string:nameand an optional list:fields.next/genesis/activity-types/subtype_of.sx— declares theSubtypeOfverb.:schemaaccepts an:objectcarrying both:child-type-cidand:parent-type-cidas 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 — TODO
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 — TODO
A new apply_object_schema/2 stage between activity-type validation
and the kernel append. When an inbound object carries {type, TypeName},
resolve the TypeRecord (local Define-name index → CID →
peer_types:lookup_or_fetch) and apply its refinement schema to the
object's field-values. Default strict_object_schema = false: an
unresolvable type is let through with a validation_skipped log;
opt-in strict mode rejects. 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
SubtypeOfactivities; 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/SubtypeOfactivity verbs (publish a type, link two).peer_typesgen_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).