The remaining P1 from the #939 review, plus the P2 it shares a mechanism with. Nothing carried a version, so two tabs editing one namespace silently overwrote each other — reproduced as tab B's `reasoning` lost to tab A's older draft. The seam's per-namespace write queue orders writes; it cannot tell a fresh writer from one replaying a snapshot a predecessor superseded. Each namespace now carries a monotonic `revision` over its RAW section. A write may send `expectedRevision`, checked at the FRONT of the queue (not at call time, which would race the very predecessor it guards against); a mismatch rejects with `SettingsConflictError` → `settings-conflict` on the wire, carrying both revisions. The editor captures the revision it opened at and, on conflict, asks the user to reopen rather than replaying its snapshot. The same counter fixes the missing broadcast. `settings/updated` is gated on the resolved value — correct for consumers, wrong for configuration surfaces: storing an override equal to the composition base leaves the resolved value alone while changing what the document says (the field is now overridden, not inherited) and moving every open editor's revision. `settings/document-updated (ns, revision)` fires on any raw-section change, in-process or external, and `host/settings-changed` now rides it. That event also closes the stale model picker: editing a provider's `models` changes no route, so `llm/adapters-updated` never fired and an open picker kept serving the old catalog. A change to an exposed provider namespace now emits `host/models-changed` too — that namespace holds the catalog. Docs: both sides of the five touched README pairs, a type-equiv block for `SettingsPathOp`, and an Agent Note recording what the plane exposes and who may overwrite what. The deferred wire-redaction gaps (secrets behind union/intersection/transform, `.default(...)` in the served envelope, schema text in rejection messages, `new Function` rehydration, pi-ai's `headers`) are recorded as TODO(settings-wire-redaction) and in Known Limitations rather than half-fixed.
2.9 KiB
@deepseek-ai/dsh-client-schema-form
English | 中文
面向 settings 编辑器的 schema/草稿模型层。wire 侧的 settings.describe 携带每个 namespace 的序列化 schemastery schema(schema.toJSON() 的 ref 信封);rehydrateSchema 用 new Schema(json) 将其还原(rehydrate)为活的校验器——在宿主上校验分节的那份 schema 对象,就是在浏览器里校验草稿的那份对象,因此客户端校验绝不会偏离 seam 侧的校验。编辑器各自渲染自己的控件(Models 页围绕它在此探测到的字段手写自己的卡片);该包(package)不含任何 React,也不做任何渲染。
契约
编辑的单元是用户分节草稿:一个以不可变方式编辑的普通对象(setPath 会物化中间对象,deletePath 即逐字段重置——去掉该键,解析值便回退到组合 base 与 schema 默认值)。字段只要出现在草稿中就被标记为已覆盖(hasPath)——判定采用存在性语义而非值比较,与 settings seam 的分层方式严格对应。nodeAtPath 解析可配置提供方目录 settingsPath 所寻址的 schema 节点(object 属性按名称解析,dict 条目经由 inner),编辑器因此可以在决定渲染什么之前,先探测某提供方的 profile 携带哪些字段(及其 meta.role);无法解析的路径返回 undefined,调用方因此会大声降级,而不是渲染出错误的子树。validateDraft(schema, draft) 运行还原出的校验器并返回其失败消息,页面因此可以在写入前拒绝无效草稿。
Model Experience
无。该包支撑的是浏览器配置编辑器;这里没有任何内容进入模型请求。
KV Cache effect
无;该包既不组装也不发送提供方请求。
Known Limitations and Deferred Work
- 重建 schema 会执行所收到的信封——
rehydrateSchema会重建一个活的 schemastery 校验器,而 schemastery 通过new Function复活序列化过的 callback,因此 schema 信封是可执行内容,而非惰性数据。这只有在信封来自提供该页面的同一 host 时才可接受;面向浏览器的 schema 协议应当传递客户端无法执行的描述,此项与 settings seam 的协议边界工作一并暂缓。 - 校验是草稿级的,而非逐字段——
validateDraft报告 schemastery 的第一条失败消息(其中会点名$.path);逐字段的报错映射延后到出现需要它的消费方再做。 - 没有通用渲染器——一个 schema 驱动的表单组件曾被构建出来,随后被手写的 Models 编辑器取代(Agent Note(agent 决策记录));若未来有页面需要编辑任意分节,起点是这些辅助函数,而不是复活后的通用渲染器——除非该 note 的权衡发生变化。