Skip to content

[ds-spec 2.4 review] 重构落地前应补齐的确定性、安全与扩展契约 #2

Description

@yqzhishen

审查范围

审查基线:docs/ds-spec-2.4.md @ 2a5a6e3

本 issue 只讨论 spec 自身的矛盾、歧义和缺失的协议边界refactor 是待重构的旧版本;旧代码尚未实现 2.4 不构成这里的风险,也不以旧实现作为规范真值。

2.4 把 interfacevariant、开放贡献类别和统一引用拆开,方向比 2.3 清楚很多。但目前仍有若干地方会让两个“严格按文档实现”的 Installer / Loader / Executor 得到不同结果。建议在新架构冻结前至少解决下列 P0 项。

P0:建议在 2.4 冻结前解决

1. required: false 与“所有依赖都必须可加载”直接矛盾

描述文件定义了 dependencies[].required,默认 true,加载规则却规定 “当且仅当所有依赖项都能被加载”时 Package 才能加载

最小反例:

{
  "dependencies": [
    { "id": "gpu-accel", "version": "1.0", "required": false }
  ]
}

机器未安装 gpu-accel 时,一个 Loader 可以依据 required:false 继续,另一个可以依据“所有依赖”拒绝,二者都符合原文。若模块又无条件 ref 到该依赖,跳过依赖后仍会在 Ready 阶段失败。

建议:

  • missingpresent but incompatibleload/init failed 三种情况分别规定 required/optional 的结果;
  • 明确 optional 依赖是否参与版本冲突求解;
  • 为 import 增加 optional / feature gate / fallback,或明确禁止对 optional 依赖作无条件引用;
  • 将“所有依赖”改为“所有 required 依赖”,并说明 optional 依赖失败是否只产生诊断。

2. 版本文法、依赖求解和引用绑定没有确定算法

当前字段说明只给出 x.y[.z.w],引用 ABNF 又使用了一个未定义的 version production。因此至少这些问题没有唯一答案:

  • 是否允许一段或三段版本;1.01.0.01.0.0.0 是否相等;
  • 是否允许前导零,各分量上限和溢出行为是什么;
  • compatVersion <= version 是否是合法清单必须满足的不变量;
  • dependencies[].version 是最低兼容基线,而 ref 中的 =version 是精确安装版本、兼容基线,还是二次求解条件;
  • dependency 写 bar >= 1.0、ref 写 bar=2.0:inference/x 时应报冲突、提升约束,还是加载两个实例;
  • 多个搜索根或同一根中有多个兼容版本时,先按路径优先还是先取最高版本;
  • diamond dependency、重复依赖、自依赖、环和同 ID 多版本是否允许并存。

“简单方案”只说依次遍历路径并取找到的兼容依赖,仍不足以让结果可复现。

建议给出完整 ABNF、规范化与比较规则,并提供一段规范性 resolver 伪代码。ref 应绑定本次依赖求解得到的实例,而不是再次做全局搜索;求解结果最好能输出包含实际版本、路径/来源和内容摘要的 resolution graph/lock。

3. compatVersion 没有定义“兼容”必须保持什么

当前算法只用 version >= requested && compatVersion <= requested 判断候选是否兼容,却没有定义 Package 的兼容性承诺覆盖哪些公开表面。

例如 bar@1.1 可以声称兼容 1.0,同时把 inference/pitchinterface 或 Level 换掉、删除某个 export,甚至复用同一模块 ID 表示另一件事。依赖方仍会被允许绑定到它,但原有 options 将按另一套契约解释。

建议明确兼容区间内至少要保持:公开 category/module ID、模块的 interface/Level 兼容关系、exports/options schema,以及可观察的运行时契约。Import 也应能声明 expected interface/level/capabilities,并在 Ready 前验证;不满足这些不变量的版本必须提升 compatVersion

4. .dspk 的安全解包与路径封闭完全未规定

规范目前只说 .dspk 是 ZIP,声明路径相对声明文件解析,安装则只是“在某个目录中解压”。这会把安全关键行为全部留给实现猜测:

  • ZIP entry 的 ..、绝对路径、盘符/UNC、反斜杠;
  • symlink/junction 及“先建链接、再写链接下文件”的逃逸;
  • 重复 desc.json、大小写或 Unicode 归一化后碰撞;
  • 特殊文件、超长路径、zip bomb、伪造 size header、过多文件;
  • 模块声明里的 ../、绝对路径,以及 ONNX external data 等二级引用逃出 Package root。

建议增加 normative DSPK profile:只允许 package-root 内的 regular files;拒绝绝对/父级/链接/特殊条目和归一化后冲突;根部恰有一份 desc.json;限制单文件/总解压字节、文件数、压缩比、路径长度、JSON 大小/深度;流式解压时再次计数;先写同文件系统 staging,完整验证后原子提交,失败整包清理。所有直接和模型格式内部的间接路径都必须在 canonicalize 后仍位于 package root。

另外应明确信任模型:若 id + version 被当作依赖身份,建议至少支持内容摘要锁定;签名由本规范承担还是由单独 distribution profile 承担,也应明确。

5. 推理任务只有方法名,没有可验证的生命周期与并发契约

initialize/start/startAsync/stop/state/result 没有定义:

  • 合法状态与状态转移,初始化/失败/取消后能否复用;
  • 同一实例是 single-flight、多并发还是有界排队,队列满时如何背压;
  • startAsync 的 callback 线程、是否恰好一次、是否允许重入、对象销毁时如何处理;
  • start 提交失败与任务运行失败的错误通道及优先级;
  • result 何时可读、由谁持有、下一次 start 是否覆盖;
  • stop 与自然完成竞态时的 terminal state,以及 deadline/timeout。

“立即停止(同步)”对不可抢占的 GPU kernel 也不是可移植的保证。建议把它改成语义明确的 cooperative cancel request,并另行定义 wait until quiescent(deadline);给出完整状态机、每个方法的前/后置条件、幂等性、线程安全级别和资源预算/背压规则。模块加载时的 Initialized/Ready 也应与每次 inference task 的 initialize 明确分开。

6. “variant 可互换”与“exports 可以不同”互相冲突

规范先要求同一契约下各 variant “输入与输出完全一致,对导入方而言可互换”,随后又允许一个 variant 支持某项功能、另一个不支持

最小反例:同一 (interface, level) 下 A export energy,B 不 export;Singer 的 options 要求 energy。A 替换为 B 后立即失效,因此不是无条件可互换。

建议把承诺改为“共享同一 schema/调用契约”,并把实际替换判据定义成“候选 exports 满足 importer 的 capability constraints”;Ready/Validate 阶段必须执行该检查。若真正要求无条件可替换,则同一 interface/level 的 exports 必须一致。

此外,variant 私有字段与 contract 公共字段目前共处一个 configuration 对象,未来可能键冲突;可拆成 configuration.contract / configuration.variant,或规定保留命名空间和 schema composition 规则。

7. interface 契约、解释器发现和 variant 演进仍不足以互操作

文档说 (interface, level) 定义 exports/options 和运行时输入输出,但没有在本规范中定义,也没有规范性链接到不可变的 interface-level contract。仅有三元组名称,无法判定两个实现对 tensor dtype/shape、单位、错误、所有权和取消语义是否真的一致。

同时,Loader 要靠三元组选择解释器,而插件章节又说三项属性在解释器实例化前就必须可读,却只描述了由解释器“返回”这些属性,没有定义静态 descriptor/factory。相同 triple 有两个 provider 时,也没有冲突或选择规则。

最后,规范明确不为 variant 设版本,允许它在 configuration 内自行版本化。但 Loader 必须在解析 variant configuration 前按 triple 选择 provider;两个不兼容配置格式仍使用同一 triple 时,选择阶段无法协商。

建议:

  • 为每个官方 (interface, level) 发布不可变、可定位的 contract/schema,覆盖 exports/options/common configuration、运行时 I/O、单位、错误和任务语义;第三方 interface 同样必须有稳定的 schema identity;
  • 定义实例化前可读的 registration descriptor + factory,至少含 provider/plugin ID、宿主 ABI、支持的 triple/config-format range;
  • 同 triple 多 provider 默认报歧义,或只允许显式、稳定的用户策略选择,禁止依赖扫描顺序;
  • 为 variant 增加标准 configFormat,或规定任何不兼容格式变化必须更换 variant ID;
  • 明确 Level 历史支持方式:一个 descriptor 可广告 level 集合,或同一 provider 分别注册多个 level,避免“精确匹配”与旧模块免重发之间没有衔接。

P1:会破坏扩展性、确定性或跨实现一致性

8. 开放贡献类别目前仍是闭世界,而且内联表示没有统一 envelope

贡献类别被定义为开放集合,但未知类别会让 Loader 拒绝整包。一个包只要增加无关的 UI metadata 类别,旧的 headless Executor 就可能连原有 inference/singer 都无法使用。

类别名又只是裸 segment,没有命名空间、schema version 或注册冲突规则;第三方都注册 language 时无法判断语义。类别自定义的 desc entry 也没有版本轴。

此外,贡献条目可以“完全内联而不指向文件”,但后文又说所有类别模块都有由框架统一解析的 interface/level/variant 公共字段。内联条目把三元组放在哪里、是否属于“模块”,没有唯一表示。

建议采用 namespaced + versioned category descriptor,并给类别/条目 requiredness:未知 required 才失败,未知 optional 可保留并忽略。统一 contribution envelope,例如 {id, path}{id, descriptor} 二选一;若允许非模块贡献,应从类型系统和术语上明确区分。

9. 省略 category 的持久引用不是演进稳定的,PackageReference 与 ModuleReference 也未分型

引用允许省略 category 并搜索所有类别:main 在只有 inference/main 时有效;Package 后来增加 singer/main,同一份旧 manifest 未改动却突然变成歧义。

通用 reference 还能只指向 Package,但 importsoptions 只能由“被引用模块”的 interface/level 解释,因此 package-only ref 在 imports 中没有语义。显式写当前 Package ID 是否触发“必须在 dependencies 声明”并形成自依赖,也不清楚。

建议持久化 manifest 中的 ModuleReference 强制写 category,省略只留给交互式查询;将 PackageReference 与 ModuleReference 分型,imports 只接受后者;明确 self-reference 的例外与自依赖禁令。Imports 还应由 importer contract 定义本地 slot/alias、cardinality、重复 ref 和数组顺序语义。

10. 两阶段初始化与 rollback 不是闭合的事务模型

Initialized/Ready/反序 rollback 没有说明:

  • “所有模块”是当前 Package 还是整个 dependency closure;
  • dependency cycle 是拒绝、SCC 两阶段,还是别的规则;
  • phase 内的确定顺序是什么(contributes 是 JSON object,本身没有语义顺序);
  • 并发加载同一 Package 如何去重,何时对读者原子可见;
  • A/B 共用 C 时 A 失败,C 的引用和资源是否能回滚;
  • 当前失败模块的半初始化资源、rollback 自己失败、重试与 unload 如何处理。

“按相反顺序”只有在先定义唯一前向顺序并记录实际完成日志后才有意义。建议给 Package 与 Module 分别定义状态机和事务边界;先做无副作用 Resolve/Parse/Validate,再 Acquire,所有 Ready 后原子 Commit;rollback 只撤销本事务新增的引用/资源,要求幂等,并保留 primary error、附加 cleanup errors。若只支持一层跨模块引用,也应说明如何检测和诊断更深的 Ready 依赖,而不是让结果取决于遍历顺序。

11. 缺少统一 JSON、标识符和 conformance profile

规范没有明确 JSON 编码、BOM/注释、重复 key、unknown key、null 与 absent、整数范围、字符串/数组/递归上限。{"$version":"1.0","$version":"999"} 在 first-wins 与 last-wins parser 中甚至会绕过不同的版本门。

建议规范性采用 RFC 8259:UTF-8,重复 object member 一律拒绝,并明确 BOM/注释、未知字段与资源上限。标识符还需规定大小写、canonical serialization、最大长度和 collision 规则;例如 Foo/foo 在不同文件系统上不应悄悄绑定到不同对象。

同时建议:

  • 定义 RFC 2119/8174 风格的 MUST/SHOULD/MAY;
  • 分别列出 Installer、Loader、Executor 的 conformance 要求和验证顺序;
  • 发布 desc.json、公共 module envelope 及官方 category/interface 的机器可读 schema;
  • 提供正反 test corpus:版本/引用、依赖图、重复键/深度、恶意 ZIP、生命周期/取消竞态、大小写/Unicode/跨平台路径。

12. RFC 4647 Lookup 的转述不完整,路径型多语言字段也没有落到具体字段

多语言章节只描述单个偏好标签逐段右删。但 RFC 4647 Lookup 的输入通常是有序 language priority list,并且遇到 singleton extension/private-use 时要连同相邻尾段处理,不能产生 de-DE-x 这类中间候选。*、非法/废弃 tag 和 canonicalization 也未说明。

匹配又声明大小写不敏感,因此 JSON 同时出现 zh-CNZH-cn 时两键语义相同,当前没有拒绝或胜者规则。

建议直接规范性引用 RFC 4647 §3.4 的完整算法:按顺序处理 preference list,整张列表失败后才取 _;加载时校验 BCP 47 tag,并拒绝 case-fold/canonicalize 后的重复键;发布含 extension/private-use/重复大小写键的 golden vectors。

另外,“多语言字段的值是文件路径时” 目前没有对应到任何明确声明为“多语言路径”的标准字段。请逐字段说明 readmeavatarbackgrounddemoAudio 等究竟是普通路径还是本地化路径,并让每个本地化路径都服从第 4 条的 package-root confinement。

示例与编辑性问题

  • Singer 示例引用的是 bar/pitch=1.0.0.0:inference/pitch,而 desc 示例依赖的是 bar;如果它们意在组成一份连续示例,则违反“外部 Package ID 必须在 dependencies 声明”。建议提供一套可整体校验的 end-to-end fixture。
  • Variance 示例中的 breathness 很可能是 breathiness 的拼写错误;duration 是否属于该 interface 也无法由本规范判定,恰好说明官方 interface-level schema 不能缺席。
  • 文中称本规范定义的 variant “当前只有 onnx”,Singer 示例却使用裸词 diffsinger。如果 diffsinger 也是本规范拥有的 Singer variant,应列入;否则应使用其拥有者的命名空间。
  • interfacevariant 本身没有 ABNF、大小写和命名空间所有权规则;“第三方实现使用自己的 interface 命名空间”与“第三方为他人的契约提供 namespaced variant”容易被读成两种相反做法。应明确 interface 属于契约所有者,而不是实现提供者。

建议的完成条件

  • 解决 optional dependency、variant 可替换性两处直接矛盾。
  • 给出版本/引用 ABNF 和确定性 dependency resolver;明确 compat 不变量。
  • 增加 DSPK/JSON 安全 profile、资源上限和原子安装规则。
  • 发布 Package/Module 加载事务状态机与 Inference Task 状态机。
  • 发布 interface-level contract registry 以及实例化前可读的 plugin descriptor。
  • 给开放 category 增加命名空间、版本、requiredness 和统一 inline envelope。
  • 收紧持久化 ModuleReference,并定义 import capability validation。
  • 发布机器可读 schema 与跨实现 golden/negative test corpus。

这些问题多数现在修文档成本很低;等新架构、Package 和插件生态按各自理解落地后,再统一会变成格式兼容和 ABI 迁移问题。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions