Files
rose-ash/plans/fed-sx-host-types.md
giles 89dd23c287
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
fed-sx-types Phase 4: object-schema validation stage in pipeline
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>
2026-06-30 15:50:45 +00:00

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 :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).