SDK/Foundation 2.0 冻结前全面审查清单
2.0 发布后 TuneLab.SDK 与 TuneLab.Foundation 即冻结 ABI,本 issue 记录发布前全量审查(96 个源文件逐一通读)发现的问题,按优先级逐项清账。
总评:整体接近可冻结状态。快照体(sealed + required init 纯加性)、线程纪律/坐标系头注释、config 工厂+With 范式、ImageResource 封闭层次、agent 会话 DIM 默认实现、会话/快照/声明面三分——均为成熟的冻结面设计,不动。
一、高优先级(冻结前必须处理,冻结后无机会)
二、中优先级(改动小、值得顺手做)
三、低优先级(记录在案,接受现状也合理)
四、冻结纪律(流程层,比单点修改更重要)
明确不动的(审查确认为正确设计):三大合成域会话/快照/声明面三分与镜像对称;required init 快照纯加性形态;PhonemeLayout 只冻 I/O 形状、算法留宿主侧演进;PropertyValue 单 box 无装箱联合 + 新增类型臂检查单注释;voice/instrument/effect 刻意平行不抽基类;IsContinuation 无默认体强制显式表态。
五、收尾统一修(issue 全清后一次性 sweep)
清完所有单点项后做一次全量测试工程 sweep:build + run 每一个测试工程(非仅 tests/TuneLab.Tests)+ build 全部样例插件,一次性修净编译遗漏与爆红,确保无工程带病发布。已知遗漏:
✅ 全清(2026-07-21):31 项 + 收尾 sweep 全部了结。最终全量验证:solution build 0 error、TuneLab.Tests 149/149、Compat.Legacy.Tests 10/10、22 个样例插件全部 build 通过。SDK/Foundation 冻结面此后由 PublicAPI 门禁(每次编译机械校验)+ AssemblyVersion 钉死 + sdk-version 门三道防线持续守护;演进纪律见 docs/sdk-api-evolution.md。
六、第二次全面复审(投影 API 下沉,2026-07-21)
在三道防线就位后,对全量 SDK public 面做第二次复审,判据:grep 消费方,全在宿主(含 GUI/测试)+ SDK 内部即候选下沉;插件/legacy 读则留(沿用 item 22 「投影是消费侧私事、SDK 面只承载契约」先例)。第一批 5 处已了结:
✅ 一并提交 12cade0(PublicAPI.Shipped.txt -7 行)。验证:solution build 0 error、TuneLab.Tests 149/149、Compat.Legacy.Tests 10/10、样例插件 V1.Voice 0 error。
保留(复审确认不动):PhonemeSlots.UnionSlots/PhonemeAt(插件面,V1.Voice+docs 使用)、PhonemeLayout.Resolve(宿主+legacy 依赖)、PhonemeTiming.Duration/Deconstruct/ToString(Resolve 返回类型的自然值类型 API)。
待续:第二次复审仅完成上述第一批;其余 SDK 面(Foundation 响应式组合子扩展等)尚未逐一过 grep,留后续批次。
六.1 穷举 sweep + 冻结契约回归覆盖(2026-07-22)
对全部 SDK public 成员逐一 grep 消费方(分 voice / instrument+effect+shared / DTO / config+杂项 四路)。结论:第一批已把派生投影成员清光,全域再无新的派生投影。各路初报的"宿主独用候选"经"派生投影 vs 正交状态 vs 对称设计"复核后几乎全为误报——它们不是可下沉的投影,而是:① DTO 正交序列化状态(Effects/PiecewiseAutomations/Bitrate 等,删=格式插件丢数据);② 跨域对称声明面(三域平行设计);③ 正交快照/上下文状态;④ 半接线配置能力(活 getter 的声明半边)。判定:SDK 契约面在"去投影"目标上已收口,其余"当前宿主独用"成员均为合法契约,全留。
沿途查出若干已冻结契约路径零回归保护,补测试(提交 4d2fdda):PhonemeSlotsTests(PhonemeAt/UnionSlots 定位与边界 + 多选声明合并范式)、VoiceSynthesisSnapshotPropertiesTests(合成期 NoteSnapshot/PhonemeSnapshot.Properties 读回)。主测试 149→161 全绿。
方法论纠正(重要):sweep 的 grep 只扫了仓内 tests/plugins/V1.* 夹具,漏了外部真实 voice 引擎插件(跨仓 ProjectReference 直引 2.0 SDK 源,是权威消费方)。以其为准复核后:
- 第一批 5 处删除对真插件零破坏——真插件本就自己
[.. Leading, .. Body] 拼接、从不读被删的 IVoiceSynthesisNoteView.Phonemes,反证该删除正确。
- 大量被 sweep 判"存疑/宿主独用/零消费死代码"的成员,真插件实际在用:
VoiceSynthesisSnapshot.Automations、context.Automations、NoteView.{Lyric,BodyOffset,LeadingPhonemes,BodyPhonemes}、PhonemeView.{Symbol,Properties}、WithRange、DraggableNumberBoxConfig.Integer、AutomationConfig.WithRandomizable、GetSnapshot、Previous、PartProperties+Merge、IsContinuation、PhonemeLayout.Resolve。"全留"决定获强验证。
- 今后消费方判据:外部真实插件为第一权威,V1.* 夹具只是补充;仅凭仓内 grep 会严重低估插件面。
第二次复审收口:SDK 契约面去投影已完成;design-decision 项(EffectSynthesisSnapshot、VoiceSourceInfo.Description、NormalizedScale.Ceil/Floor/Rounded 等)经决定全留不动;Foundation 通用工具库经复审亦全留(见记忆)。
SDK/Foundation 2.0 冻结前全面审查清单
2.0 发布后
TuneLab.SDK与TuneLab.Foundation即冻结 ABI,本 issue 记录发布前全量审查(96 个源文件逐一通读)发现的问题,按优先级逐项清账。总评:整体接近可冻结状态。快照体(sealed + required init 纯加性)、线程纪律/坐标系头注释、config 工厂+With 范式、ImageResource 封闭层次、agent 会话 DIM 默认实现、会话/快照/声明面三分——均为成熟的冻结面设计,不动。
一、高优先级(冻结前必须处理,冻结后无机会)
Last一词两义:IReadOnlyNotifiableLinkedList<T>.Last(TuneLab.Foundation/Event/IReadOnlyNotifiableList.cs)是末元素;IVoiceSynthesisNote.Last/IInstrumentSynthesisNote.Last是前一个邻居。插件作者两个面都会碰(context.Notes.Lastvsnote.Last),同名反义。建议 note 邻居改Previous(对齐 .NETLinkedListNode.Next/Previous)。 ✅e2363bf(取Previous全词;整个链表家族对齐:ILinkedNode 根→SDK note→数据层 Note/Part/Vibrato→合成 context 代理→调用点→legacy compat→测试/样例插件→5 文档;集合 First/Last 末元素访问器保持不动)IValueConfig注释与AutomationConfig现实矛盾:IControllerConfig.cs注释称"是否实现本接口在类型上区分有无默认基线(连续型 AutomationConfig 实现、分段型不实现)",但AutomationConfig是单一类、恒实现IValueConfig<double>,分段型只是运行时DefaultValue = NaN。按is IValueConfig做"重置为默认值"的通用逻辑会把分段轨当"默认值 NaN 的有基线配置"。要么拆两个类型兑现注释,要么改注释并钉死"消费方必须先判IsPiecewise"的契约。 ✅dc71879(取方向 B:保留单类统一形态。查明IValueConfig真实语义 = 默认值递归GetDefaultValue的「叶子」基例,与「有无基线」无关;改注释如实描述叶子 vs 复合,钉死 NaN 哨兵契约,AutomationConfig显式实现处就地点明。纯注释、无行为改动)VoiceSourceInfo/InstrumentSourceInfo是可变 struct + public 字段:声库目录元数据必然增长(作者/版本/语种/标签/试听……),public 字段冻结后永不能改属性、struct 永不能改 class(均为二进制破坏)。不在热路径,建议改与快照家族同款sealed class + required init,加字段纯加性。 ✅6b0cfd1(附带:TryGet* 的 out 参数改[MaybeNullWhen(false)] out …?,编译期强制 false 分支不解引用)DraggableNumberBoxConfig/TextBoxConfig的 With 手抄字段:SliderConfig.cs明文承诺"With 走 Clone,加字段只改声明处",但DraggableNumberBoxConfig7 个 With 与TextBoxConfig.WithPassword全是逐字段手抄对象初始化器——将来加字段漏抄一个 With = 静默丢配置,编译器不报。统一 Clone 模式(private set+MemberwiseClone)。 ✅60153d9(其余 config 已确认无此问题:AutomationConfig/ComboBoxConfig 已 Clone、复合 config 与 CheckBoxConfig 只有 Create 无 With)TuneLabContext.Globalpublic setter 可被插件劫持:SDK 是所有插件 ALC 共享的一份,任一插件TuneLabContext.Global = 自己的实现即可替换全体插件的语言/日志器(绕过"日志前缀由宿主判定、插件无法伪造"的设计)。setter 改 internal(宿主经既有 InternalsVisibleTo 注入)或注入一次后再赋值抛异常。 ✅6469b28(取 internal set 方案,getter 仍 public)SoundSourceInfo.ID命名不一致:全 SDK 皆Id(EffectInfo.Id、PropertyKey.Id、AgentToolCall.Id),唯此处ID。改名一次到位。 ✅753f9cc(JSON/CBOR key 是小写字面量"id"、与属性名解耦,不动已序列化数据)Map<K,V>.Empty共享单例可被下转型改写:声明为IReadOnlyMap但实例是可变Map,((Map<K,V>)Map<K,V>.Empty).Add(...)污染全进程(含PropertyObject.Empty底层 map);IReadOnlyOrderedMapBuilder的 EmptyOrderedMap 同理。给真不可变空实现(或 Add 抛异常的私有子类)。 ✅c9e2288(新增EmptyMap/EmptyOrderedMap直接实现只读接口而非子类化,下转型抛 InvalidCastException;空 PropertyObject 底层也改指向不可变单例)OrderedMap.Add与Map.Add重复键语义分叉:前者 = 删旧 + 挪到末尾(静默重排),后者 = 抛异常(Dictionary 语义)。且OrderedMap.Insert(index, key, value)在键已存在且旧位置 < index 时实际落点前移一位、语义未定义。统一(倾向 Add 恒抛、替换走索引器)或把现状写成明文契约。 ✅a5c257a(取统一方案:Add/Insert 恒抛、替换走索引器;全量审计调用方无一依赖旧覆盖/挪位;新增/改测试锁契约)EffectSynthesisSnapshot漏 sealed:Voice/Instrument 快照均 sealed,唯它是裸public class。不 seal = 把"可被插件继承"冻进 ABI。 ✅91f4892Serialize形态:(分三提交0dc9590/abc24e8/7fd7615)IExportFormat.Serialize(ProjectInfo) → Stream迫使插件全量缓冲进 MemoryStream,且流所有权/Position 无契约;void Serialize(Stream output, ProjectInfo info)更常规省内存——现在是唯一能改的窗口。 ✅0dc9590(改 void + 宿主给流;FormatsManager 内部缓冲保留原子写;TLP/CBOR/compat/样例/测试/文档同步)NoteInfo.Pos/Dur、TempoInfo.Pos、PartInfo.Pos是 tick(PPQ 多少?)、Gain是 dB 还是线性、AudioPartInfo.Path绝对还是相对——外部格式作者只能猜。按 Voice 面注释水准补齐。 ✅abc24e8(逐处查实:tick=PPQ 480、Pos 全局 vs part 相对、秒 vs tick 混用点明、Gain=dB、Pan∈[-1,1]、Path 相对可多层上级..等)NoteInfo.Phonemes是 get-only 计算属性(每次 LINQ 拼接):DTO 域里反射式序列化器(插件拿 System.Text.Json 序列化 NoteInfo 很常见)会把派生属性当数据写出。改方法GetPhonemes()。 ✅7fd7615(in-repo 零消费,直接删除而非改方法——SDK 面最小;消费方自行 Concat)二、中优先级(改动小、值得顺手做)
PropertyValue.ToInt截断(int)d,PropertyObject.GetInt四舍五入Math.Round——同一值体系对 2.7 一个给 2 一个给 3。统一成 Round。 ✅58e0f02ArrayConfig/ListConfig/ObjectConfig/ExtensibleObjectConfig/ComboBoxConfig.Create直接持有调用方 list/map 引用,事后改动穿透进已构造 config——与PropertyObject/PropertyArray"构造即拷贝、值语义由构造保证"的纪律相悖。要么拷贝、要么把"传入后不得改动"写成明文契约。 ✅a206216(取拷贝方案:5 处 Create 一律[.. x]构造即拷入自持副本;map 经 CollectionBuilder 物化、空态落真不可变单例;元素/项均不可变 → 浅拷封死整树。命名保留 CLR 类型名共识不变)ComboBoxItempublic set:全 SDK 唯一可变公开配置件(Value/DisplayText/SubItems均set),改init(对象初始化器写法不受影响)。 ✅aaa9ea4(IsSeparator 亦 private init)Merge/MergedEvent持惰性 enumerable:Subscribe/Unsubscribe 各枚举一次sources,传 LINQ 查询且两次结果不同时退订失配泄漏。构造时 ToArray 一次。 ✅f2398a8(取固化方案:MergedEvent 与孪生 Merged 均构造时 ToArray 锁死同一批源;明文契约 Merge=固定源快照、动态成员走 WhenAny/WhenAnyItem。核实无行为变化——消费方源本就稳定)IAutomationEvaluator.Evaluate(times)是否要求升序未定契约:宿主实现大概率按升序游标优化,乱序传入是静默错值还是合法?一句注释钉死。 ✅ eb820bcIInstrumentSynthesisSession头注释有"新增成员一律 DIM 兜底"约定,但同为插件实现面的IVoiceSynthesisSession、IEffectSynthesisSession、三个 Engine、IImportFormat/IExportFormat、IExtensionSettings都没写。每个插件实现面接口统一声明。 ✅ef5ec4b(8 个接口补齐;IVoiceSynthesisSession 注明 IsContinuation 刻意无默认为例外)IVoiceContext、IVoiceEngine(IVoiceSynthesisSession.cs)、IInstrumentContext(IInstrumentSynthesisSession.cs)、engine 注释里IVoicePart*/IInstrumentPart*;IExtensionSettings.cs有"Init/CreateSession/CreateSession"重复词。冻结前统一 grep 注释里的类型名。 ✅ef5ec4b(另扫掉 item 1 漏网的 3 处 note.Next/Last→Next/Previous)DisposableManager.DisposeAll用 HashSet 无序销毁:退订常有"后建先拆"期望。改 List + 逆序;顺便考虑实现IDisposable支持using。 ✅3be2612(List 保序 + 逆序 Dispose + 实现 IDisposable)IExport/ImportFormat的模板期无用 using(System.Threading.Tasks/System.Text/自引用using TuneLab.SDK;);IReadOnlyNotifiable藏在 IReadOnlyNotifiableProperty.cs、IReadOnlyNotifiableLinkedList藏在 IReadOnlyNotifiableList.cs。冻结版源码是外部阅读基准,清一次。 ✅ 1d48ac0(清冗余 using + IReadOnlyNotifiable / IReadOnlyNotifiableLinkedList 移到同名文件)IReadOnlyNotifiablePropertyObjectExtension有 Number/String/Bool/ValueField 但缺IntField/EnumField(PropertyObject有 GetInt/GetEnum 对应物)。事后补纯加性、不急,想对称现在补最干净。 ⛔ won't-do(ba1d914):结论相反——int/enum 非PropertyType存储类型(To<int>恒 false、无Create(int)),*Field只绑存储类型,不该补 IntField/EnumField。反而顺势删除了三个零调用的派生转换器PropertyValue.ToInt/PropertyObject.GetInt/GetEnum(ToInt 是 IsInteger 时代宿主转 int 契约的遗留,换 INormalizedScale 后取整规则已下放插件),并统一取值面命名到 CLR 类型名(Bool→Boolean、NumberField→DoubleField)。三、低优先级(记录在案,接受现状也合理)
SynthesizedPhoneme/SynthesisStatusSegment(可变 struct+public 字段)vsVoiceSynthesisPhonemeSnapshot(readonly struct+ctor)vsPhonemeLayoutNote/SynthesizedSyllable(readonly struct+init)。字段冻结后永不能改属性,明文选定政策。 ✅84873d4(取 readonly struct + init:淘汰可变 struct+public 字段;判据=值/引用按数据本质选、struct 内成员选属性非裸字段(field↔property 二进制破坏)、readonly 除坑;未加 required 留与 default 安全议题并定。成文政策并入 item 31)SynthesizedSyllable.Phonemes/VoiceSynthesisNoteSnapshot.Phonemes每次 get LINQ 拼接分配;default(SynthesizedSyllable).PhonemesNRE。 ✅eca6612(取「移出 SDK」而非修补:删两类型 Phonemes;SynthesizedSyllable 宿主独用→宿主扩展 PhonemeCount/AllPhonemes(值+nullable 双重载,吸 default NRE);VoiceSynthesisNoteSnapshot 插件面→守 SDK 最小不补便利,消费方从 Leading/Body 自数;compat/样例已迁。分配+NRE 结构性消除)PropertyKey为 default 时GetHashCodeNRE(Id=null);Point缺IEquatable<Point>(Equals 装箱)、NaN 点不自反。 ✅d52349e(PropertyKey.GetHashCode 改 Id?.GetHashCode() ?? 0 除 default NRE;Point 实现 IEquatable + Equals 用 double.Equals 自反(NaN 可被集合找回、免装箱),== 保留 IEEE 刻意分叉,对齐 BCL)AnyItemEvent(WhenAnyItem)同一 handler 重复订阅时mWires账本被覆盖、退订失配(边角)。:5aaf696(mWires 账本 (item,handler)->值改 Stack<转发器列表>,对齐 mDownstreams 容忍重复;订 N 次触发 N 次、退 N 次净,单订阅不变。覆盖式 bug 只毁拆除路径-退订后悬挂转发器泄漏+幽灵回调-订阅期间无碍。补重复订阅测试用例,堵 a22c6bb 盲区)OrderedMap.Values每次 get 新建 ValueCollection、map 枚举逐项分配ReadOnlyKeyValuePair、ComboBoxConfig.AppendO(n²)——均在低频域。 ✅ 记录接受(won't-do,无代码改动):三点均冷路径且成本被夸大——Values是 ValueCollection wrapper(持 map 引用按需 ValueAt,非复制值;Keys 更直接返回底层 List);KVP 逐项分配是唯一真分配但去不掉——Enumerator.Current 类型是接口 IReadOnlyKeyValuePair,改 struct 装箱回接口一样分配,且零分配路径已在(热循环用 KeyAt/ValueAt 索引,只有 foreach 付 KVP);ComboBoxConfig.Append O(n²) 低频(config 建一次、n 小)且有批量重载 Append(IEnumerable) 作逃生口。按“SDK 干净=API 面最小”取向,冷路径微分配不属 API 面问题,不 churn 冻结内部。GetPhonemePropertyConfigs扁平索引对齐契约脆弱(长度须精确等于扁平音素总数)——注释已钉死口径,接受。 ✅406dd53(升格为重设计而非接受:审出真缺陷=契约粒度(逐音素 schema)与 UI 多选合并粒度(按核相对 slot)错位——宿主取首成员 config 当「代表」实质是宿主合并 schema,违反「宿主只并值」原则。改按核相对 slot 键控IReadOnlyMap<int, ObjectConfig>(键=角色:0=核/<0=引导/>0=核后),多选合并归引擎(值依赖时对同 slot 成员值三态 Merge,同 GetNotePropertyConfig 契约);键控自描述、扁平漂移契约消失;SDK 增共享口径 PhonemeSlots(PhonemeAt/UnionSlots)。备选已论证否决:返回合并结果=逼插件复刻宿主对齐规则;逐 note 嵌套=值依赖 schema 下宿主拿到互异 schema 无从合并)IAgentModelSessionSendAsync 三连 DIM 重载:将来 tool-call 流式增量时考虑 callbacks 聚合类新重载(纯加性),现不动。 ✅2832b90(升格为发布范围决策:agent-model 整面收回宿主内部模块(TuneLab/Agent/Contracts,命名空间 TuneLab.Agent),不随 2.0 SDK 冻结;新模型适配走 PR(贡献指南 docs/agent-model-adapters.md),外部扫描分支摘除报自解释错误,现无外部实现者、零破坏。理由:LLM 协议面迭代速度与冻结错配 + owner 未 review 的契约不冻 + 适配器少/小/无资产更适 PR。原 SendAsync 重载问题随之作废。将来若开放为插件点的先决整改 checklist(亦钉 IAgentModelEngine 头注释):①SendAsync 重载阶梯收敛为 callbacks 聚合对象 ②AgentTokenUsage int→long ③枚举(Role/Modality/ContentKind)补未知值容忍契约 ④两接口补 DIM 加性政策头 ⑤AgentMessage Content/Parts 双真相源契约钉紧、角色限定措辞改「当前仅」 ⑥AgentModality 与 AgentContentKind 对应关系注释交叉引用)NumberFormat.Decimals用 CurrentCulture:不同区域设置下滑条文本不同(小数点/逗号),知晓即可。 ✅bf7a8bd(升格为修复而非知晓:NumberFormat.Decimals 的 Format/Parse 改 InvariantCulture——值本质是数据(持久层句点存),CurrentCulture 会在逗号区把句点当千位分隔误读("3.14"→314);本地化数字走 Custom 逃生口。附 1.0 血证 c764721「尝试修复部分地区svg图标不显示」——rgba alpha 经 CurrentCulture 出 "0,5" 多逗号致分量错乱,当年用 zh-CN 碰巧句点绕过;本次 SvgIcon 一并改 InvariantCulture 为正解)四、冻结纪律(流程层,比单点修改更重要)
Microsoft.CodeAnalysis.PublicApiAnalyzers(PublicAPI.Shipped.txt 进仓库)或 CI 跑 ApiCompat——冻结从此是每个 PR 的机械校验,不是一次性审查。 ✅e5f7a13(选 PublicApiAnalyzers 而非 ApiCompat:编译期即时+零 CI 依赖+文本基线可 diff,贴合本地 build 工作流且是 AI 执笔的硬护栏。SDK 673 + Foundation 303 条 public 签名(含 nullability)进 PublicAPI.Shipped.txt=2.0 基线;RS0016/17/24/25 提 error;运营约定钉 csproj 注释(加性走 Unshipped、发布时挪 Shipped+提 SdkVersion、breaking 用 REMOVED 留痕)。已验证:改名 public 成员触发 RS0017+RS0016 双 error 编译失败)<AssemblyVersion>2.0.0.0</AssemblyVersion>永不再动,真实版本走 FileVersion/InformationalVersion——否则未来版本跳动使已编译插件绑定失败。 ✅e8f3a81(两 csproj 显式 2.0.0.0,切断 Version 派生链、2.x 永不再动;防「作者升级快×用户升级慢」组合的无辜绑定失败(与是否用新 API 无关);合法拒绝归 manifest sdk-version 门(另一版本轴,勿对齐)+PEReader 直扫兜底。已验证:DLL 版本 2.0.0.0、V1.Voice 重编译引用记录烧 2.0.0.0)1de1a02(docs/sdk-api-evolution.md:61 接口三分类全列(A 插件实现面=DIM 必带+语义责任 / B 宿主实现面=自由加、插件自实现不受保护 / C 开放数据结构=按 A 对待 / D config 封闭族)+ item 21 DTO 三形态政策并入 + 枚举未知值容忍 + 变更操作速查表 + 三道防线与发布收口总览。另写 AI 记忆 project_sdk_public_api_gate:遇 RS0016/17 的正确处置(breaking 先呈报、禁机械改清单绕过))明确不动的(审查确认为正确设计):三大合成域会话/快照/声明面三分与镜像对称;
required init快照纯加性形态;PhonemeLayout只冻 I/O 形状、算法留宿主侧演进;PropertyValue单 box 无装箱联合 + 新增类型臂检查单注释;voice/instrument/effect 刻意平行不抽基类;IsContinuation无默认体强制显式表态。五、收尾统一修(issue 全清后一次性 sweep)
清完所有单点项后做一次全量测试工程 sweep:build + run 每一个测试工程(非仅 tests/TuneLab.Tests)+ build 全部样例插件,一次性修净编译遗漏与爆红,确保无工程带病发布。已知遗漏:
FakeNote仍声明旧名Last(item 1Last->Previous改名未同步到 TuneLab.Hosting.Compat.Legacy.Tests,自那时起编译不过):NoteOverlapClampTests.cs / LeadingIntoUnpinnedNeighborTests.cs 各一处改名。 ✅a07f4d3(两处改名,compat 测试复活 10/10 通过)✅ 全清(2026-07-21):31 项 + 收尾 sweep 全部了结。最终全量验证:solution build 0 error、TuneLab.Tests 149/149、Compat.Legacy.Tests 10/10、22 个样例插件全部 build 通过。SDK/Foundation 冻结面此后由 PublicAPI 门禁(每次编译机械校验)+ AssemblyVersion 钉死 + sdk-version 门三道防线持续守护;演进纪律见 docs/sdk-api-evolution.md。
六、第二次全面复审(投影 API 下沉,2026-07-21)
在三道防线就位后,对全量 SDK public 面做第二次复审,判据:grep 消费方,全在宿主(含 GUI/测试)+ SDK 内部即候选下沉;插件/legacy 读则留(沿用 item 22 「投影是消费侧私事、SDK 面只承载契约」先例)。第一批 5 处已了结:
NoteInfoExtension.StartPos/EndPos(整类):StartPos 是 Pos 恒等别名、EndPos=Pos+Dur,仅 Midi.cs 两处宿主消费。整类删除、就地内联。ComboBoxItem.ShowText():纯渲染投影,仅宿主 ComboBoxController 三处。移宿主扩展 ComboBoxItemExtensions(TuneLab.GUI)。ComboBoxItem.IsGroup:宿主独用且可派生,仅 ComboBoxController 两处。内联为SubItems is not null。PhonemeLayoutNote.PhonemeCount:零外部读者,仅 PhonemeLayout.Resolve 内部用。降为 PhonemeLayout 私有静态助手、移出 public 面。IVoiceSynthesisNoteView.Phonemes:Leading++Body 的派生投影(唯一读者是 SDK 内 PhonemeSlots,宿主只 implement),移出接口、拼接折进 PhonemeSlots(跨两列表就地索引、省一次物化);删宿主实现,docs 四篇同步。✅ 一并提交
12cade0(PublicAPI.Shipped.txt -7 行)。验证:solution build 0 error、TuneLab.Tests 149/149、Compat.Legacy.Tests 10/10、样例插件 V1.Voice 0 error。保留(复审确认不动):
PhonemeSlots.UnionSlots/PhonemeAt(插件面,V1.Voice+docs 使用)、PhonemeLayout.Resolve(宿主+legacy 依赖)、PhonemeTiming.Duration/Deconstruct/ToString(Resolve 返回类型的自然值类型 API)。待续:第二次复审仅完成上述第一批;其余 SDK 面(Foundation 响应式组合子扩展等)尚未逐一过 grep,留后续批次。
六.1 穷举 sweep + 冻结契约回归覆盖(2026-07-22)
对全部 SDK public 成员逐一 grep 消费方(分 voice / instrument+effect+shared / DTO / config+杂项 四路)。结论:第一批已把派生投影成员清光,全域再无新的派生投影。各路初报的"宿主独用候选"经"派生投影 vs 正交状态 vs 对称设计"复核后几乎全为误报——它们不是可下沉的投影,而是:① DTO 正交序列化状态(Effects/PiecewiseAutomations/Bitrate 等,删=格式插件丢数据);② 跨域对称声明面(三域平行设计);③ 正交快照/上下文状态;④ 半接线配置能力(活 getter 的声明半边)。判定:SDK 契约面在"去投影"目标上已收口,其余"当前宿主独用"成员均为合法契约,全留。
沿途查出若干已冻结契约路径零回归保护,补测试(提交
4d2fdda):PhonemeSlotsTests(PhonemeAt/UnionSlots 定位与边界 + 多选声明合并范式)、VoiceSynthesisSnapshotPropertiesTests(合成期 NoteSnapshot/PhonemeSnapshot.Properties 读回)。主测试 149→161 全绿。方法论纠正(重要):sweep 的 grep 只扫了仓内
tests/plugins/V1.*夹具,漏了外部真实 voice 引擎插件(跨仓 ProjectReference 直引 2.0 SDK 源,是权威消费方)。以其为准复核后:[.. Leading, .. Body]拼接、从不读被删的IVoiceSynthesisNoteView.Phonemes,反证该删除正确。VoiceSynthesisSnapshot.Automations、context.Automations、NoteView.{Lyric,BodyOffset,LeadingPhonemes,BodyPhonemes}、PhonemeView.{Symbol,Properties}、WithRange、DraggableNumberBoxConfig.Integer、AutomationConfig.WithRandomizable、GetSnapshot、Previous、PartProperties+Merge、IsContinuation、PhonemeLayout.Resolve。"全留"决定获强验证。第二次复审收口:SDK 契约面去投影已完成;design-decision 项(EffectSynthesisSnapshot、VoiceSourceInfo.Description、NormalizedScale.Ceil/Floor/Rounded 等)经决定全留不动;Foundation 通用工具库经复审亦全留(见记忆)。