Files
rose-ash/plans/agent-briefings/fed-sx-types-loop.md
giles 4c0a48834e preserve fed-sx-m1 loop briefings before pruning its worktree
4 untracked agent-briefing docs from the fed-sx-m1 worktree (merged branch loops/fed-sx-m2),
saved here so they survive the worktree cleanup.
2026-07-02 12:25:50 +00:00

8.6 KiB

; -- 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 scopenext/** 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 scopelib/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:

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

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/<cid>      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/<lang>/**.
  • 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.