; -*- mode: markdown -*- # loops/fed-sx-types — fed-sx-side prep for host-type federation Scoped briefing for a focused iteration loop that lands the substrate-side pieces needed for host's typed-post graph to flow across fed-sx nodes. Companion to `plans/fed-sx-host-types.md` (full design + context); this is the build sheet. ``` description: loops/fed-sx-types — federation of typed posts (fed-sx side) subagent_type: general-purpose run_in_background: true isolation: worktree # worktree at /root/rose-ash-loops/fed-sx-types ``` ## Scope **In scope** — `next/**` only. Four pieces, in dependency order: 1. `DefineType` + `SubtypeOf` genesis activity-types 2. `peer_types.erl` receiver-side cache (mirror of `peer_actors.erl`) 3. `http_server.erl` route for serving type-docs over the wire 4. Object-schema validation stage in `pipeline.erl` **Out of scope** — `lib/host/**`. The host-side adapters (`lib/host/fed_sx_outbox.sx`, `lib/host/fed_sx_inbox.sx`) are a follow-up that needs `loops/host`'s metamodel to settle first. Touching `lib/host/**` here would conflict with `loops/host`'s in-flight slices. **Hard line: do not edit `lib/host/`.** **Substrate** — the fed-sx kernel modules (`nx_kernel.erl`, `http_server.erl`, `pipeline.erl`, `outbox.erl`, etc.) are fine to edit — they're `next/kernel/` scope, this loop owns them. ## Branch base Start from `origin/architecture` (currently has m2 fully merged locally as `ef3d2df4`; once that's pushed it'll be available on origin too). Tip has: - All of fed-sx-m2 (multi-actor, signature pipeline, peer_actors, delivery_worker with 8b-timer, two-instance smoke test, etc.) - Native `http-listen` and `http-request` primitives - `erlang:send_after` / `cancel_timer` / `monotonic_time` substrate - The Erlang substrate `:pending-args` scheduler fix that makes kernel-aware routes work over real HTTP If `origin/architecture` is still missing m2 at start time (pending push), rebase against `origin/loops/fed-sx-m2` instead. ## Phase 1 — `DefineType` + `SubtypeOf` genesis activity-types New files in `next/genesis/activity-types/`: - `define_type.sx` — the verb declaration. Schema accepts `{name, fields, refinement-schema, instance-type}`. Semantics registers the type into a kernel-side type registry. - `subtype_of.sx` — relation verb. `{child-type-cid, parent-type-cid}`. Schema validates both are previously-defined types. Semantics adds the edge to the registry's hierarchy index. These are data-shaped (`DefineActivity` form), parsed by `bootstrap.erl` at startup. No kernel code change at this phase — the registry surface needed to consume them lands in Phase 2. Sketch: ```sx (DefineType :name "Type" ;; the verb-name (will be the activity :type) :doc "..." :schema (fn (act) (and (string? (-> act :object :name)) (or (nil? (-> act :object :fields)) (and (list? (-> act :object :fields)) (every? type-field-shape? (-> act :object :fields)))))) :semantics (fn (state act) state)) ``` Tests in `next/tests/define_type.sh` and `next/tests/subtype_of.sh` — round-trip through `term_codec`, schema accept/reject cases. Phase 1 acceptance: 6 tests (3 per verb) + envelope shape round-trip. ## Phase 2 — `peer_types.erl` receiver-side cache Mirror `next/kernel/peer_actors.erl`. State shape: `[{TypeCidBytes, TypeRecord}]` where `TypeRecord` is the parsed `DefineType` envelope's `:object` payload — name, fields, refinement-schema, instance-type-binding. Public API (mirrors `peer_actors`): ```erlang peer_types:start_link(_) -> Pid peer_types:put(TypeCid, TypeRecord) -> ok peer_types:lookup(TypeCid) -> {ok, TR} | not_found peer_types:lookup_or_fetch(TypeCid, Cfg) -> {ok, TR} | {error, _} peer_types:state_for(TypeCid) -> {ok, TR} | not_found peer_types:known_types() -> [TypeCid] ``` The `lookup_or_fetch` path delegates to a `Cfg`-supplied `type_fetch_fn :: fun((TypeCid, Cfg) -> {ok, Bytes} | {error, _})` to keep transport pluggable (same pattern as `discovery_fetch:make_fetch_fn`). Tests in `next/tests/peer_types.sh` — put/lookup, miss → fetch via mocked Cfg.fn, cache populated after fetch, lookup_or_fetch with no Cfg.fn → `{error, no_fetch_fn}`. Phase 2 acceptance: 8-10 tests covering the cache surface. ## Phase 3 — `http_server.erl` type-doc route Add a route mirroring `actor_doc_response_for/3`: ``` GET /types/ with Accept: application/vnd.fed-sx.type-doc ``` Encodes the cached TypeRecord via `term_codec:encode/1` and serves the bytes. 404 if not in cache. Wire format matches the actor-doc shape (per `plans/fed-sx-design.md` §10). Also add a small `discovery_type_fetch.erl` (sibling to `discovery_fetch.erl`) holding the closure that `peer_types`'s `lookup_or_fetch` calls — keeps the live HTTP path testable end-to-end. Tests: - `next/tests/peer_types_route.sh` (server-side, port + actor-style curl) — known type-cid → 200 + decoded body, unknown → 404 - Extend `next/tests/discovery_type_fetch.sh` to cover the client-side closure round-trip with a python http.server stub Phase 3 acceptance: 8 server-side tests + 6 closure tests. ## Phase 4 — Object-schema validation stage in `pipeline.erl` The pipeline currently validates: envelope shape, signature against peer-AS, activity-type schema (the activity's outer `:schema` fn). **It does not validate the inner object against its declared type schema.** This phase plugs that in. When an activity arrives whose `{type, _}` is a known refinement- typed activity AND whose `:object` carries a `{type, TypeName}` field, look up the TypeRecord (local Define-name index → TypeCid → `peer_types:lookup_or_fetch`) and apply its refinement-schema to the object's `:field-values`. Reject the activity on schema-fail. New `apply_object_schema/2` between activity-type validation and the kernel append. Default behaviour when the type isn't in the local Define-name index: try `peer_types:lookup_or_fetch`; if that errors and Cfg has `{strict_object_schema, true}` reject the activity, otherwise let it through with a `validation_skipped` log entry. Default `strict_object_schema = false` — opt-in for nodes that want airtight validation. Tests in `next/tests/object_schema.sh`: - Activity with type that matches local registry + valid object → accepted, appended - Same activity with refinement-failing object → rejected with `{validation_failed, object_schema}` - Activity with type unknown to local registry, peer_types fetch succeeds → validates against fetched TypeRecord - Activity with type unknown, fetch fails, strict_object_schema not set → accepted with skip log - Activity with type unknown, fetch fails, strict set → rejected - Activity without inner `{type, _}` → skipped (no schema applies) Phase 4 acceptance: 10-12 tests covering the matrix. ## Out-of-scope deliberately - **host-side adapters** (`lib/host/fed_sx_outbox.sx`, `lib/host/fed_sx_inbox.sx`). Belongs to whatever loop wires `loops/host` to fed-sx. That loop can read this branch and consume `peer_types`/`DefineType` as public surface. - **Type evolution / version migration**. Refinement schemas are immutable per CID; an updated TypeRecord is a new CID. The "name → currently-valid CID" routing layer is a separate problem. - **Subtype-of unification across nodes**. The graph data lands via `SubtypeOf` activities; rendering / display / dedup is a consumer concern. ## Tests discipline - `bash lib/erlang/conformance.sh` green before AND after every commit (currently 771/771 on `origin/architecture` after the m2 merge). - Commits scoped to `next/**` and `plans/fed-sx-host-types.md` (the design plan; update it as you ratify decisions). No edits to `lib/erlang/**`, `lib/host/**`, `bin/sx_server.ml`, or other `lib//**`. - One commit per phase as outlined; smaller intermediate commits fine as long as each leaves both gates green. - Push to `origin/loops/fed-sx-types` after each commit. ## Done when - Phases 1-4 land with their acceptance tests green. - `plans/fed-sx-host-types.md` updated to mark steps 1-4 done and describe what the host-side adapter loop now has to work with. - Conformance + every `next/tests/*.sh` green at finish. When this loop closes, the substrate side of host-type federation is complete. The remaining `loops/host`-side work (outbox/inbox adapters) becomes a small, well-scoped follow-up. ## Parallel-safety with loops/host `loops/host` is iterating heavily on `lib/host/blog.sx` and the metamodel slices. This loop **does not touch any `lib/host/` file** — scope is `next/**` only. No conflict surface. Both loops run their own worktrees on separate branches; they meet only at the eventual architecture merge.