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.
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 scope — next/** only. Four pieces, in dependency order:
DefineType+SubtypeOfgenesis activity-typespeer_types.erlreceiver-side cache (mirror ofpeer_actors.erl)http_server.erlroute for serving type-docs over the wire- 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-listenandhttp-requestprimitives erlang:send_after/cancel_timer/monotonic_timesubstrate- The Erlang substrate
:pending-argsscheduler 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.shto 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 wiresloops/hostto fed-sx. That loop can read this branch and consumepeer_types/DefineTypeas 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
SubtypeOfactivities; rendering / display / dedup is a consumer concern.
Tests discipline
bash lib/erlang/conformance.shgreen before AND after every commit (currently 771/771 onorigin/architectureafter the m2 merge).- Commits scoped to
next/**andplans/fed-sx-host-types.md(the design plan; update it as you ratify decisions). No edits tolib/erlang/**,lib/host/**,bin/sx_server.ml, or otherlib/<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-typesafter each commit.
Done when
- Phases 1-4 land with their acceptance tests green.
plans/fed-sx-host-types.mdupdated to mark steps 1-4 done and describe what the host-side adapter loop now has to work with.- Conformance + every
next/tests/*.shgreen 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.