pipeline:apply_object_schema/2 (+ stage_object_schema/1 factory) — the
object-schema stage between activity-type validation and the kernel
append (plans/fed-sx-host-types.md step 4). When an inbound activity's
:object declares a refinement type ({type, TypeName}), resolve it
(Cfg type_index: TypeName -> TypeCid; then peer_types:lookup_or_fetch/2,
a local hit or a wire fetch) and apply the record's refinement schema
to the object's :field_values, rejecting on schema-fail with
{error, {validation_failed, object_schema}}.
The schema is either a 1-arity Erlang predicate (substrate stand-in,
locally stored) or a term_codec-safe {required, [Field,...]} constraint
(so a wire-fetched record validates too). Default
strict_object_schema = false: an unresolvable type is let through (the
skip is where a validation_skipped log belongs); strict rejects.
Objects with no declared type, and names absent from the local index,
are skipped (open-world).
Test: next/tests/object_schema.sh (15) — local hit, wire fetch, fetch
failure strict/non-strict, no peer_types, untyped object, undeclared
name, fun + data schema forms, no-schema record, stage composition.
No regression: pipeline_signature, pipeline_driver green. Plan doc
steps 1-4 marked done.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.3 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 — 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
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).