; -*- 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/<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.
