# Architecture Notes

> Architect's design rationale for the typescript-federation package.
> Documents accommodation of Matrix-3 integration requirements while preserving federation invariants.

---

## Design Philosophy

This package implements a classic **Anti-Corruption Layer** (Fowler) sitting between:

* **Federation / Edge** (MXENV/MXURI semantics + mailbox transport constraints), and
* **Local component runtime** (your existing component system / handler contracts).

The trick is: **make the integration seam tolerant and observable**, while keeping the core routing/security invariants rigid.

This is implemented as a "strangler fig" compatibility layer, not a rewrite.

---

## Invariants (MUST NOT VIOLATE)

The invariants live in the **federation boundary**, not in the developer ergonomics:

| Invariant | Description |
|-----------|-------------|
| **Mailbox pattern (no wildcards)** | EdgeGateway subscribes to *exact* ingress topics (`public.ingress.<realm>` and/or `mx/in/1/<realm>/00`) |
| **Semantic URI ≠ delivery topic** | MXURI parsing canonicalizes to `toRealm` + `toPath`; delivery goes to mailbox topic |
| **Anti-relay** | Block any attempt to turn EdgeGateway into an open relay; enforce `replyTo.realm == from.realm` |
| **Replay / rate limit / policy** | Centralized at ingress boundary, not on local bus |

**Core principle:** Strict at trust boundaries, tolerant inside the trust domain.

---

## Accommodations Implemented

### P0 - Integration Seam (Must-Have)

#### #7 Internal Request Format

**Invariant pressure:** None (intra-realm only, local bus).

**Pattern:** Bounded context with stable contract + extension points.

**Implementation:**

1. Documented internal wire contract:

```typescript
interface InternalRequestWire {
  correlationId: string;
  replyTo: string;          // internal topic (string)
  params: unknown;          // decoded body
  _mx?: {                   // optional (config)
    fromRealm: string;
    incomingProfile: 'matrix3' | 'mxenv1';
    msgId?: string;
    principalKey?: string;
    traceId?: string;
  };
  envelope?: unknown;       // optional (config)
}
```

2. Config switches on EdgeGateway:

| Option | Default | Purpose |
|--------|---------|---------|
| `includeInternalMeta` | `true` | Set `false` if handlers are strict-schema and choke on extra fields |
| `includeInboundEnvelope` | `false` | Set `true` if handlers want full inbound envelope visibility |

This is the **Strategy/Policy** pattern at the seam without infecting the core.

#### #8 Lenient Internal Replies

**Invariant pressure:** None (local bus only).

**Pattern:** Tolerant Reader (Postel's Law) + consumer-driven contract.

**Implementation:** `normalizeInternalReply(rawPayload, fallbackCorrelationId)`

EdgeGateway accepts **any** of these reply shapes:

| Shape | Interpretation |
|-------|----------------|
| `{ correlationId, ok, body, error? }` | Full strict reply |
| `{ ok: true, body: ... }` | Semi-strict (no correlationId) |
| `{ ok: false, error: { code, message } }` | Error reply |
| `{ result: ... }` | Inferred `ok=true`, `body=result` |
| Raw JSON value (`"hello"`, `123`, `{...}`) | Inferred `ok=true`, `body=<value>` |
| Invalid JSON string | Inferred `ok=true`, `body=<raw string>` |

This allows existing handlers to work without forced schema migration.

#### #5 Matrix-3 Compatibility Tests

**Invariant pressure:** None (proof harnessing).

**Implementation:** `tests/integration/matrix3_compat.test.ts`

Covers:
- Legacy request envelope with `ttlMs`
- Flat string `replyTo`
- Legacy replies with `ok:true/false` detection
- Legacy URI parsing (no `/-/` delimiter)
- Lenient internal reply `{ result: ... }` end-to-end

Executable evidence, not hand-wavy claims.

---

### P1 - Observability + Typing

#### #3 EdgeGateway Events

**Invariant pressure:** None (pure side-effects).

**Pattern:** Observer (GoF) / structured tracing hooks.

**Implementation:**

```typescript
interface EdgeGatewayOptions {
  onInboundRequest?: (envelope: TaggedEnvelope, internalTopic: string) => void;
  onInboundReply?: (envelope: TaggedEnvelope) => void;
  onOutboundReply?: (replyEnvelope, ingressTopic: string) => void;
  onTimeout?: (correlationId: string, toPath: string) => void;
  onDrop?: (reason: string, topic: string, rawPayload: string) => void;
  onSecurityBlock?: (reason: SecurityBlockReason, envelope: TaggedEnvelope) => void;
}
```

Makes integration debugging feasible (especially with `nats sub` and correlated logs).

#### #6 Export Envelope Type Aliases

```typescript
export type {
  CrossRealmEnvelopeLegacy as CrossRealmEnvelope,
  ReplyEnvelopeLegacy as ReplyEnvelope,
} from './envelope';
```

Low-friction TypeScript ergonomics: import from package and keep type compatibility.

#### #9 TTL Clarification

| Field | Semantics |
|-------|-----------|
| `MxEnvelopeV1.ttl` | Hop limit (routing TTL) |
| Legacy `ttlMs` | Expiry duration (milliseconds) |

The compat layer **does not coerce** `ttlMs` to `ttl` (different units, different semantics).

---

### P2 - Transport Ergonomics

#### #2 Transport Interface Meta

```typescript
interface Transport {
  publish(topic: string, payload: string, meta?: TransportMeta): void;
  subscribe(topic: string, handler: (topic, payload, meta?) => void): () => void;
}

interface TransportMeta {
  qos?: 0 | 1 | 2;
  retain?: boolean;
  // extension keys allowed
}
```

**Pattern:** Ports-and-adapters. Core stays oblivious, adapters preserve delivery guarantees.

### Metrics + Config Validation

**Implementation:**

```typescript
// Fail-fast validation
const validation = EdgeGateway.validateOptions(opts);
if (!validation.valid) throw new Error(validation.errors.join('; '));

// Runtime metrics
const metrics = edge.getMetrics();
// { inboundRequests, inboundReplies, outboundReplies, timeouts, securityBlocks, pendingRequests }

edge.resetMetrics();
```

---

## What Was NOT Implemented (And Why)

### File Naming (PascalCase)

Safe but cosmetic. Cross-platform casing hazards (macOS vs Linux CI).
Do as a single mechanical commit with consistent import updates if desired.

### Raw Mode

With lenient reply normalization + `includeInboundEnvelope`, raw mode is largely redundant.
If still needed, formalize an `InternalCodec` Strategy - but that's a bigger API commitment.

### Graceful Shutdown (Drain Pending)

Lifecycle semantic decision: fail fast (send negative replies) or wait?
Should be pinned to product requirements before implementing.

---

## Integration Recipe

Minimal "no churn" bridge wiring:

```typescript
import {
  EdgeGateway,
  WIRE_PROFILES,
} from '@open-matrix/federation';

const edge = new EdgeGateway({
  localRealm: 'matrix-3',
  profile: WIRE_PROFILES.matrix3,            // legacy mailbox + legacy envelope
  backbone: backboneTransport,               // NatsTransport instance
  localBus: localTransport,                  // NatsTransport or InMemoryBroker

  // Integration seam knobs:
  includeInternalMeta: false,                // if handlers are strict schema
  includeInboundEnvelope: true,              // if handlers want full envelope visibility

  // Observability:
  onInboundRequest: (env, topic) => console.log('IN', env.profile, topic),
  onOutboundReply: (rep, ingressTopic) => console.log('OUT', ingressTopic),
  onTimeout: (cid, toPath) => console.warn('TIMEOUT', cid, toPath),
  onSecurityBlock: (reason) => console.warn('SECURITY', reason),
});

edge.start();
```

On the internal side, your component runtime just needs to:

1. Subscribe to internal topic `mx/1/matrix-3/<toPath>`
2. Read `replyTo` from the request payload
3. Publish *any* reply shape to `replyTo` (raw value is fine)

No forced schema migration.

---

## Future Enhancements (Invariant-Safe)

The next "developer happiness" increment is to formalize an `InternalCodec` Strategy (encode/decode) to plug existing internal envelope formats 1:1. But with the changes above, this is likely unnecessary - integration without churn is already achieved while keeping the federation boundary strict.
