← [ BACK_TO_NOTES ]

BodySense StreamEvent Trust Boundary

解释 BodySense Consultation 如何把 SSE / durable replay 的网络 JSON 视为 unknown,并通过共享 parseStreamEvent runtime parser 升级为可信 StreamEvent 后再进入 reducer 和 UI。

#synthesis #growing #frontend #architecture #bodysense #typescript #javascript

[!info] related notes

BodySense StreamEvent Trust Boundary

一句话定义

StreamEvent Trust Boundary 是把来自 Live SSE / durable event API 的不可信 JSON,从 unknown 经过运行时解析与校验升级为可信 StreamEvent 的边界;只有越过这道边界的数据,才应该进入 reducer、selector 和 UI。

Network Bytes
→ Text Framing
→ JSON.parse
→ unknown
→ parseStreamEvent
→ trusted StreamEvent
→ domain projection

为什么这是架构边界,而不只是 TypeScript 最佳实践

一旦代码把网络对象叫作:

StreamEvent

下游就会开始相信:

type 合法
payload 与 type 匹配
seq 具有协议语义
ids 结构合法
control/safety event 不缺关键字段

因此 unknown → StreamEvent 实际上是一次信任升级。

如果只写:

const event = data as StreamEvent;

并没有产生 runtime evidence,只是要求 TypeScript 编译器相信开发者。

TypeScript Union 解决了什么

共享 StreamEvent discriminated union 对已经可信的数据非常有价值:

switch (event.type) {
  case "message.text.delta":
    // payload 可以按该 variant 收窄
}

它帮助:

exhaustiveness
payload narrowing
IDE / refactor safety
reducer contract

但 TypeScript 类型在编译后消失,所以:

Static type safety
≠ runtime input validation

当前 BodySense 已经闭合这条主要边界

旧实现曾经存在:

JSON.parse
→ as StreamEvent
→ reducer

这是一条真实历史缺口,但不再是当前实现状态。

现在 Live SSE 入口:

SSE data line
→ JSON.parse → unknown
→ parseStreamEvent(data)
→ trusted StreamEvent
→ handler/reducer

Durable Replay API 入口:

HTTP JSON
→ ids/payload decode
→ construct unknown event candidate
→ parseStreamEvent(...)
→ trusted StreamEvent
→ replay handler/reducer

对应代码:

packages/contracts/src/stream-event-parser.ts
packages/contracts/src/stream-events.ts
apps/web/src/features/consultation/hooks/useSSEProcessor.ts
apps/web/src/features/consultation/services/consultationService.ts

这使 Live 与 Replay 使用同一个当前 consumer contract,而不是“来自数据库就默认可信”。

Network Bytes 到 Domain Event 有多个 Boundary

完整路径:

Uint8Array
→ TextDecoder(stream=true)
→ text buffer
→ SSE framing
→ JSON.parse
→ unknown JSON value
→ StreamEvent runtime parser
→ trusted event

每一步都可能失败:

UTF-8 字符跨 chunk
一条 SSE frame 被拆成多个 chunk
一 chunk 包含多条 frame
JSON syntax invalid
JSON valid but event schema invalid
event/data type mismatch
unsupported/unknown protocol shape

所以:

ReadableStream chunk ≠ protocol event
JSON parse success ≠ valid StreamEvent

unknown 为什么优于 any

const data: unknown = JSON.parse(text);

unknown 会强迫调用方先 narrowing/validation 才能使用。

而 any 会让:

data.payload.foo.bar

直接穿过静态类型系统。

因此 unknown 是很好的“尚未获得信任”静态表示。

Runtime Parser 应验证什么

至少要覆盖:

envelope
├─ version
├─ seq
├─ channel
├─ type
├─ ids
└─ payload

然后按 discriminant 验证 variant-specific payload:

message.text.delta
→ delta/text contract

tool.result
→ tool_call identity + result contract

state.interaction.required
→ interaction/tool/question contract

safety.red_flag.detected
→ safety payload contract

如果某些字段之间存在关系,还需要 refinement/cross-field invariant,而不是只检查 primitive types。

event: 与 data.type 也应该一致

SSE 本身可能携带:

event: message.text.delta
data: { "type": "tool.call", ... }

即使 data 对象单独能通过某种 schema,这种 transport envelope 与 domain discriminant 不一致也属于 protocol error。

当前 useSSEProcessor.ts 会检查当前 event: 行和 parseStreamEvent 得到的 event.type 是否匹配。

Validation 失败后的原则

不要:

invalid event
→ unchecked reducer

也不要对 authority/safety relevant event:

malformed
→ silently treat as “nothing happened”

更可靠的是:

protocol validation fails
→ explicit error/diagnostic
→ stop or recover according to policy
→ never promote malformed payload into business state

尤其 safety/control event 更接近 fail-closed 边界。

Parser 不应该偷偷“修成合法”

例如:

missing seq → 0
unknown enum → normal
missing red flag → false

这些默认值可能把 UNKNOWN 变成错误业务事实。

对于 authority/safety/control 字段,通常宁可 explicit reject,也不要为了 parser 永不报错而创造语义。

Live 与 Replay 必须经过同一个 Trust Model

错误:

Live SSE → strict parser
Replay DB → unchecked cast

数据库可能包含:

旧 schema
migration 前 payload
历史 bug 生成的数据
corrupted row

所以 trust boundary 取决于当前 consumer contract,不是数据来自公网还是内网。

理想结构:

Live SSE ───┐
            ├→ parseStreamEvent → trusted union → reducer
Replay API ─┘

Trust Boundary 之后 Reducer 才能保持 Pure/Simple

如果 parser 已经建立可信边界,reducer 就不用每个 branch 都重新做:

if (!payload || typeof payload.delta !== 'string') ...

架构边界可以保持:

Boundary layer
= defensive validation

Domain reducer
= trusted deterministic projection

这对 replay correctness 很重要。

Protocol Version 与兼容性

如果 envelope 有:

version = 1

它应该参与 parser/compatibility policy,而不只是展示字段:

supported version → parse
legacy supported → explicit adapter
unknown critical version → explicit incompatibility

不要依赖历史数据“碰巧还能被 TypeScript cast”。

Contract Drift 怎么防

最危险的情况是:

TS union 一份
runtime parser 一份
Go DTO 一份

长期靠人工同步。

更稳的方向包括:

shared schema/source generation
or
cross-language contract fixtures/tests

至少应该证明:

Go-produced valid fixture
→ frontend parser accepts

malformed fixture
→ frontend parser rejects

与 Chapter 7 Streaming Recovery 的关系

只有 runtime trust boundary 正确,after_seq replay 才真正有意义。

否则:

Durable Event Log
→ replay malformed event
→ unchecked cast
→ deterministic reducer corrupts state

并不是“持久化了就可靠”。完整链路必须是:

Durable event
→ current runtime validation
→ run-aware ordering/dedup
→ pure projection

见 durable-sse-recovery-with-after-seq。

当前重点测试

应该覆盖:

valid variants accepted
unknown/malformed type rejected
wrong payload type rejected
invalid seq rejected
SSE event/data type mismatch rejected
Live and Replay both use parser
new/legacy version compatibility explicit
safety/control malformed input fails explicitly

代码审查问题

看到网络数据进入 domain 时问:

  1. JSON.parse 后变量是 unknown 还是直接 any/as T?
  2. 哪一行第一次有资格把它叫 StreamEvent?
  3. Live 与 Replay 是否走同一个 parser?
  4. Parser 是否验证 discriminant-specific payload?
  5. malformed safety/control event 会不会 fail open?
  6. Protocol version/schema drift 是否有自动化证据?

最终记忆

TypeScript protects code from developers;runtime validation protects the application from external data.

更适合 BodySense 的一句话:

unknown network data
→ runtime evidence
→ trusted StreamEvent
→ reducer

当前 BodySense 已经通过共享 parseStreamEvent 把这条主要信任边界真正落到了 Live SSE 与 durable replay 两个入口。

创建于 2026/8/22 更新于 2026/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本