# API 语义收敛阶段 1:属性所有权必须先于便利重载 ## 背景 仓库审计发现的错误并非彼此独立的条件遗漏。`tp-text` 属性扩散、层重定义残留、显式 nil 丢失、搜索 nil 分裂、隐藏层覆盖外部编辑,都来自同一个更深的问题: > 调用者、内嵌字符串、普通 Emacs 属性、层定义和 managed stack 对同一属性键的所有权没有统一表达。 继续为每个入口增加特殊分支会扩大语义分裂,因此本阶段先冻结 `docs/API-SEMANTICS.md`,再在拥有状态的层修复。 ## 决策 ### 1. presence 是数据,不是真值 属性缺失与 present-nil 必须使用 `plist-member` 一类 presence-aware 判断区分。搜索中 VALUE 省略不能再借用 nil 表达,因而增加唯一哨兵 `tp-any-value`。选择唯一对象而不是某个普通符号,是为了避免与合法属性值冲突。 ### 2. `tp-text` 的属性所有者是 interval 内嵌字符串的位置 0 不能代表整串。初次应用与响应式更新都按每个真实 property interval 计算,外部 props 在每段上覆盖内嵌 props,显式 nil 也参与覆盖。 逐段算出待写属性后仍必须执行调用者选择的操作语义:`tp-set` 覆盖指定键,`tp-reset` 替换完整属性集合,`tp-add` 合并 face-family 和嵌套 plist。实现中一度让“属性已逐段应用”的返回标志绕过了这一步,导致文本内容相同时 reset 留下旧键、add 覆盖而不合并。最终把 operation 传到唯一的逐段写入点,并为字符串/缓冲区和 `tp-text nil` 初始化路径加入回归断言。 ### 3. 层重定义必须拿到 old 与 new 只传 layer name 会让渲染器无法判断哪些键应删除。定义入口在替换注册表前保存 old props,渲染器随后执行: 1. 仅删除仍等于旧值的旧层键; 2. 保留无关键和已经被外部改写的值; 3. 写入新定义; 4. 同步 direct render 与 buried/hidden stack entry。 这比完整 `set-text-properties` 更安全,因为后者会错误取得整个区间的所有权。 ### 4. 隐藏层冲突默认失败 隐藏层存在时,直接属性只是 managed stack 的渲染缓存。外部直接写入无法可靠归属到某一层。三个候选方案中: - 静默覆盖会丢用户数据; - 自动收编为匿名层会凭空改变 stack 结构; - 明确失败能保留双方状态并暴露所有权冲突。 因此选择 `tp-layer-conflict`,并保证在 managed write 前检查。 ### 5. NOERROR 只处理可预期的解析失败 增加 `tp-unresolved-layer`,NOERROR 仅捕获这一类型。层 body、compute、transform 和内部不变量错误不属于“未找到”,必须传播。 ### 6. 业务计算与 observer 分开 transform/compute 决定渲染结果,失败后继续会产生貌似有效的陈旧输出,因此错误传播。watcher 是副作用观察者,单个失败不应阻断 managed update;错误以结构化 plist 记录在 `tp-reactive-observer-errors`,同时保留用户可见消息。 ## 有意延期 参数化 mounted layer entry 没有保存调用实参,重定义时无法重新求值。为避免猜测参数或引入兼容包装,本阶段明确不自动刷新参数化实例。后续应让 managed entry 保存参数和定义版本,再设计迁移。 字符串/缓冲区搜索返回结构、复制/原地修改策略和 stack mutator 的 run-count 返回值也仍有历史差异;它们属于 canonical façade 阶段,不能混入正确性补丁。 ## 验证标准 本阶段以以下证据作为停止条件: - 每个确认问题先有失败回归测试; - 聚焦测试覆盖字符串/缓冲区、present-nil、静态/响应式重定义和 hidden conflict; - warning-as-error 字节编译通过; - 全量 ERT、固定种子乱序 ERT 与 README doctest 通过; - 中英文 README、docstring、架构文档和 changelog 与规范同步。