# tp API 语义规范 本文档是 tp 核心 API 的当前行为契约。代码、测试、README 与 docstring 若与本文冲突,应以经过测试的代码为准并同步修正文档。 tp 当前定位是 **Emacs 文本属性的高层操作工具箱,以及一套受管理的命名层、层栈和响应式渲染模型**。它尚不是全部原生文本/字符属性语义的等价替代品;完整范围与路线见 [REPOSITORY-AUDIT.md](REPOSITORY-AUDIT.md)。 TP 1.0 迁移的第一层纯计算合同已经落地在 `tp-style.el`:namespaced property schema、结构化 selector、确定性 cascade、逐属性继承、custom property、显式 `tp-computed` 和 Emacs 属性投影已经是当前 public behavior;retained object/surface/mount/transaction 尚未切换,仍以目标架构文档描述为未来合同。 Stage 2 canonical façade 已完成为内部模型:`tp--native-range`、`tp--presence`、`tp--request`、`tp--match`、`tp--result` 是模块间传递的规范记录。公开入口和历史返回值保持兼容,不因内部模型收敛而改变。 Stage 3 text-only 原生语义 façade 已完成:direct/effective/source-aware lookup、property change/any/not-all 与三种 mutation policy 组合均有明确公开边界。 Stage 4 managed lifecycle 已完成:managed metadata、attach/detach、diagnostics、transaction、参数化 mounted layer args/version refresh 与 theme generation diagnostics 均有公开边界。 Stage 5 overlay-aware 字符属性查询已完成:`tp-lookup :char` / `:char-source` 报告 Emacs 选出的字符属性值、overlay 来源和获胜 overlay 身份。overlay 创建、移动、删除、priority 管理和生命周期不属于 tp 契约。 ## 1. 对象、范围与坐标 - 所有范围均采用半开区间 `[START, END)`。 - 字符串使用 Emacs 原生的 0-based 坐标,合法边界为 `0` 到 `(length STRING)`。 - 缓冲区使用 Emacs 原生位置,通常从 `point-min` 开始;显式 BUFFER 和 nil(当前缓冲区)具有相同语义。 - 范围结果原则上使用目标对象的原生坐标。当前公开兼容例外是 `tp-intervals` / `tp-intervals-map`:缓冲区默认返回相对 START 的 offset,传入 `ABSOLUTE` 才返回可直接回传给写 API 的原生坐标。内部 canonical range 使用原生坐标,但该 relative 默认作为历史兼容例外保留。 ## 2. 修改策略 | 调用族 | 字符串 | 缓冲区 | | --- | --- | --- | | `tp-set` / `tp-reset` / `tp-add` / `tp-remove` 整串形式 | 复制后返回新字符串 | 不适用 | | 上述函数的 START/END 形式 | 原地修改显式字符串 OBJECT | 原地修改 | | `tp-match-*` / `tp-regexp-*` | 返回新字符串 | 原地修改,返回匹配范围 | | 层栈修改函数 | 原地修改字符串 | 原地修改 | | `tp-forward-do` / `tp-backward-do` / `tp-search-map` | 原地修改;替换文本必须等长 | 原地修改;替换文本可增减长度 | 普通缓冲区写入使用 Emacs 的属性修改原语,因此遵循 Emacs 的 modified、undo 和 read-only 行为;tp 不把所有修改默认包装为 silent modification。响应式重渲染和 `tp-text` 文本替换为了恢复受管理输出会在内部允许修改 read-only 文本,这是当前实现边界,不代表公开 API 已提供统一的 read-only 策略。相同值刷新会尽量避免无意义地翻转 buffer-modified 状态。 ## 3. presence、nil 与 wildcard 以下三种状态必须区分: 1. 属性键不存在; 2. 属性键存在,值为 nil; 3. 属性键存在,值为非 nil。 `tp-member` 用于判断直接属性键是否存在;`tp-at` / `plist-get` 单独使用时不能区分前两种状态。删除最后一个子属性会移除空的父属性键,不会偶然留下 `(PROPERTY nil)`。 搜索 API 的规则是: - 省略 VALUE:匹配该直接属性的任意已存在值; - 显式传入 nil:只匹配“键存在且值为 nil”; - 需要继续提供 OBJECT、次数或范围等后续位置参数时,传入公共唯一哨兵 `tp-any-value` 表示任意值; - 自定义 PREDICATE 总是优先执行,并接收请求 VALUE(可能是 `tp-any-value`)和实际属性值。 属性缺失的范围不属于搜索结果,即使搜索值是 nil。 ## 4. 搜索结果 - `tp-search` 对字符串和缓冲区范围都返回 `(START END VALUE)` 列表。 - `tp-forward` / `tp-backward` 的字符串路径返回前/后 N 个 `(START END VALUE)`;缓冲区路径移动 point,并返回第 N 次搜索的 `prop-match`。 - `tp-forward-do` / `tp-backward-do` 只在第 N 个匹配上调用函数;匹配不足 N 个时不调用函数,返回实际找到的数量。 - `tp-search-map` 处理全部匹配并返回处理数量。 `tp-forward` / `tp-backward` 的对象相关结果差异是现存公开兼容契约。内部搜索路径已收敛到 canonical match/result 记录;公开返回结构暂不改变。 ## 5. 原生文本查询与修改策略 Stage 3/5 查询 façade 已完成:text modes 保持 text-only,char modes 委托 Emacs 的 overlay-aware 字符属性查询。 `tp-lookup` 返回 `tp-lookup-result` 记录,字段为 property、value、present-p、source、mode、object、position、overlay。text modes 中 overlay 始终为 nil;`:char` / `:char-source` 中 overlay 为 Emacs 选出的获胜 overlay,若文本 fallback 获胜则为 nil。 | MODE | 语义 | | --- | --- | | `:text-direct` | 只读取直接文本属性;显式 nil 与缺失通过 present-p/source 区分 | | `:text-effective` | 值使用 `get-text-property`;source 使用 text-only 来源解释 | | `:text-source` | 返回 direct/category/alias/default/absent 来源和值,不查看 overlay | | `:char` | 使用 `get-char-property-and-overlay` 的值,按 Emacs overlay/text 优先级解析 | | `:char-source` | 同 `:char`,并在 overlay 获胜时把 source 设为 `:overlay`、overlay 设为获胜 overlay | source 取值为 `:text-direct`、`:category`、`:alias`、`:default`、`:overlay` 或 `:absent`。direct 显式 nil 的结果是 present-p 为 t、value 为 nil、source 为 `:text-direct`;alias nil 与 Emacs 原生语义一致,会继续寻找后续 alias/default;缺失属性的结果是 present-p 为 nil、value 为 nil、source 为 `:absent`。 `tp-property-change` 是 Emacs property change 原语的显式封装:`:direction :next` / `:previous` 选择 next/previous,传入 `:property` 时使用 single-property change,省略时使用 all-property change。 `tp-property-any` / `tp-property-not-all` 是 `text-property-any` / `text-property-not-all` 的薄封装,保留 Emacs 对显式 nil、边界和对象的行为。 `tp-with-mutation-policy` 只接受三种有效组合: | POLICY | 语义 | | --- | --- | | `(:modified :ordinary :read-only :respect)` | 普通修改,尊重 read-only | | `(:modified :ordinary :read-only :inhibit)` | 绑定 `inhibit-read-only`,普通 modified/undo 行为 | | `(:modified :silent :read-only :inhibit)` | 绑定 `inhibit-read-only` 并使用 `with-silent-modifications` | `(:modified :silent :read-only :respect)` 明确拒绝,因为 silent modification 与尊重 read-only 不能同时满足。 insert、copy、yank、stickiness、narrowing 和 indirect buffer 行为直接委托 Emacs;tp 不为这些原生操作提供 wrapper。 ## 6. 直接模板展开与 managed mount 层名有两种不同用途: ### 6.1 直接模板展开 把非响应式层名传给 `tp-set`、`tp-reset`、`tp-add`、`tp-match-*` 或 `tp-regexp-*` 时,层定义展开为普通属性,通常不保留 `tp-name`。结果不能依赖层名进行后续移动、隐藏、按名删除或静态重定义刷新。 匿名响应式属性和响应式层需要保留 `tp-name` 才能登记与刷新,这是直接路径中的受管理例外。 ### 6.2 managed mount `tp-push-layer` / `tp-put-layer` 明确保留 `tp-name` 和必要的 `tp-layers` 状态。mounted layer 可以被查询、移动、隐藏、显示、删除和响应式刷新。 非参数化层重新定义后,已挂载区域按 old/new 所有权协调: - 新定义写入其拥有的键; - 旧定义拥有、但新定义不再拥有的键,仅在当前值仍等于旧值时移除; - 外部已经改写的值不会被当作旧层残留删除。 所有 managed mount 都携带 lifecycle metadata。即使只有单个 managed layer,只要存在 `tp-meta`,权威存储也使用 `tp-layers`;直接渲染属性和 `tp-layer-stack-at` 等 public stack query 不暴露 `tp-meta`。 参数化层的已挂载 entry 保存调用实参、形参表和 definition version;重新定义参数化层后,既有 managed entry 会按保存的 args 刷新。历史无 metadata 的 entry 按 legacy entry 保守处理。 `tp-attach-managed-layers` 扫描已经进入缓冲区的 managed storage,补齐/规范化 metadata,登记发现的层,并返回层名列表。`tp-detach-managed-layers` 移除 managed storage;KEEP-RENDERED 非 nil 时保留当前可见渲染属性为普通文本属性。 `tp-managed-layer-diagnostics`、`tp-managed-buffer-diagnostics` 和 `tp-managed-diagnostics` 是只读诊断入口,报告 entries、args、registry、errors 与 theme diagnostics。`tp-managed-diagnostics` 的 theme 部分报告 generation、last hook source、refresh mode、refreshed ranges 和 errors;这是 lifecycle 诊断,不是性能基准。 `tp-layer-transaction` 在给定范围内执行 managed stack 修改。成功返回结构化 plist,其中 `:status` 为 `ok`、`:ok` 为 t,`:result` 保留 FUNCTION 的返回值;失败时恢复事务前文本/属性快照并默认发出 `tp-layer-transaction-error`,NOERROR 非 nil 时返回结构化失败 plist。 ## 7. `tp-text` 替换 - 初次应用和响应式更新都按内嵌字符串的真实属性 interval 处理,不从位置 0 采样后扩散到整段。 - 调用者显式属性覆盖内嵌属性;显式 nil 也是有效覆盖值。 - 未被调用者覆盖的内嵌属性按各自 interval 保留。 - 每个 interval 算出待写属性后,`tp-set` 只覆盖这些键并保留其他目标属性,`tp-reset` 替换目标的完整属性集合,`tp-add` 则对目标已有的 face-family 与嵌套 plist 继续合并;文本内容相同和发生替换时遵守同一规则。 - 字符串和缓冲区路径遵守相同的 per-interval 属性计算;对象的复制/原地策略仍按第 2 节执行。 ## 8. 错误边界 - 未定义或无法解析的层使用 `tp-unresolved-layer` 表达。 - 栈 API 的 NOERROR 只抑制 `tp-unresolved-layer`;参数化层 body、计算、属性结构和其他内部错误必须传播。 - 隐藏层存储发生所有权冲突时使用 `tp-layer-conflict`。 - transform 失败或返回非字符串、compute 失败都属于业务输出失败,必须传播。 - watcher 属于 observer:单个 watcher 失败不会阻断 managed update;失败会记录到 `tp-reactive-observer-errors`(newest first)并输出消息。 - 公开边界不应把内部失败转换为默认值后继续写入。 ## 9. 隐藏层与外部直接修改 存在隐藏层时,`tp-layers` 保存完整 managed stack,其他直接属性是第一个可见层的渲染缓存。两类调用采用不同但明确的策略: - definition/reactive refresh 知道正在刷新哪个定义,也知道直接属性对应哪个可见 entry;它先把原生直接编辑协调进该可见 entry,再执行 old/new 所有权刷新,因此外部改写值不会被当作旧定义残留删除; - 普通 stack decode/write 缺少这次 definition refresh 的所有权上下文,缓存不一致时会在写入前发出 `tp-layer-conflict`,且不修改层栈; - 所有层都隐藏时没有可接收直接属性的可见 entry,此时出现直接属性始终发出 `tp-layer-conflict`。 tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。 ## 10. 返回值现状 核心写 API 的返回值仍保留历史差异: - buffer/region `tp-set` / `tp-reset` / `tp-add`:`(START . END)`; - 整串复制式写入:新字符串; - buffer `tp-remove` / `tp-clear`:nil; - stack mutator:修改的 property-run 数量; - `tp-put-layer` / `tp-push-layer`:显式 OBJECT 或 `(START . END)`。 这些返回值是当前兼容契约。property-run 数量会受无关 interval 边界影响,不应被当成稳定业务标识。内部 request/result 模型已建立,但不会改变这些历史 public returns。 ## 11. 明确不在当前完整契约内 以下能力仍需设计或补齐,不能据现有 API 推断: - overlay lifecycle(创建、移动、删除、priority 管理); - insert/copy/yank/stickiness 的 tp wrapper 或 managed workflow; - mutation policy 三种组合之外的统一 read-only、silent modification、undo 策略; - 字符串/缓冲区完全一致的搜索结果结构; - observer 错误的清理、重试与汇总策略。 ## 12. Schema-driven cascade - `tp-define-property` 只接受带 namespace 的 symbol id,例如 `text/face` 或 `ebox/width`;schema replacement 在完整验证后一次写入,失败不会破坏旧 definition。 - structured selector 原生支持 type/id/class/attribute/state、compound、descendant/child/adjacent/general sibling,以及 `:is`、`:where` 和 `:not`;`tp-selector-specificity` 与 rule matching 使用同一 AST。 - cascade 顺序固定为 importance、origin、layer、specificity、scope proximity、source order;normal 与 important declaration 的 layer 顺序按 CSS 规则相反,unlayered normal 高于 layered normal。 - `initial`、`inherit`、`unset`、`revert` 和 `revert-layer` 必须由 `tp-wide-value` 显式构造,普通同名 Elisp symbol 保持 literal。 - 普通 function value 永远不执行;只有 `tp-computed` 包装的 function 在计算时执行一次,其返回值不二次调用。 - `--name` custom property 默认继承;`tp-var` 支持 fallback 和 cycle invalidation。属性 schema 的 normalizer/validator 在变量与 wide value 求值后执行。 - `tp-compute-style` 返回 `tp-computed-style`,保存 canonical values、resolved custom properties 和可选 provenance;`tp-project-style` 是把 schema projector 汇总为最终 Emacs text properties 的唯一纯投影入口。 - 静态 `define-tp` 和静态 `define-tps` 生成层会同步编译为同名 canonical style;参数化或旧 `$var` 响应式定义不会冻结当前值为 style,待新的 signal/binding runtime 接管其动态 source。 ## 13. Signals、bindings 与 transaction - `tp-signal-create` 创建 global 或 buffer-scoped source;`tp-signal-read` 仅在 binding compute context 中登记依赖,`tp-signal-peek` 永不登记依赖。buffer kill 会 dispose scoped signal;global signal 可用 `tp-signal-dispose` 显式结束 lifecycle,两者都会移除 subscriptions。 - `tp-bind` 的 identity 是 owner object identity 加 caller-namespaced key;重复安装复用同一 binding,compute definition 变化才使它 dirty。`tp-binding-read` 读取 memoized value 并建立 binding→binding edge。 - 每次成功 compute 以本次实际读取的依赖替换旧依赖;条件分支切换后旧 signal 不再触发。binding value 经其 equality comparator 判等,相等结果不 invalidates downstream。 - signal write 先写入 transaction-local candidate state。最外层 `tp-with-transaction` 只遍历 exact dirty closure,去重并按 binding dependency 拓扑惰性求值;compute 内嵌 write 排队稳定,不递归执行。 - 任一 compute 或 cycle 失败会恢复 committed signal values、last successful binding values、dependencies、dirty state、owner registry 和 scheduler counters。cycle condition 携带 namespaced binding-key path。 - `tp-variable-signal` 是 global/buffer-local Elisp variable(包括后续 `$var` compiler)的 source adapter;它只转发精确 scope 的 write,不调用 legacy layer renderer。 - `tp-reactive-counters` 只公开 invalidated、recomputed、skipped、subscription-added、subscription-removed 五个工作量计数,不暴露 internal hash shape。 ## 14. Retained surface、range ownership 与 publication - `tp-surface-plan-create` 只接受 key/kind/text/props/children/tags/capability 纯数据;text 与 children 互斥,同一 parent 的显式 key 不可重复,constructor 与每次 prepare 都做 defensive copy。普通 function property value 保持 literal identity,plan 不携带 marker、position、binding 或 producer closure。 - producer 在 active prepare context 中先用 `tp-object-ensure` 取得 candidate handle,再返回 plan 或 `tp-surface-result-create`。成功 publication 才使新 handle live;失败、materialize 返回或 orphan/cross-surface validation 失败都会释放 candidate bindings、anchors 与 subscriptions。`tp-object-resolve` 只读 live key path,不创建 identity。 - `content` capability 拥有一个 disjoint text span,使用 character common-prefix/suffix 与 direct-property run diff;外部字符编辑使其 stale。`properties` capability 不能携带 text,只能通过 `tp-object-attach-range` 写 attached anchor。host edit 位于 anchor 前方时 marker 正常移动;跨入 owned range 时按 boundary policy shorten/remove/stale。 - properties ledger 保存 baseline、last-published value 与 contribution anchors。prepare compare-before-write;外部 property override 触发 `tp-property-conflict` 且保持旧 revision。`tp-range-rebase` 显式接受当前 host runs 为新 baseline;unmount 只恢复仍等于 TP last-published value 的子区间,冲突子区间保持外部值并进入 report。 - 同一 surface 的 overlapping properties contributions 按 plan order 和 native property schema merge;独立 surfaces 当前不得重叠字符 ownership。这个限制把 journal owner 保持为唯一 surface,避免两套 baseline 静默覆盖。 - 最外层 transaction 先完成所有 producer/plan/conflict validation,再按 surface id publish。multi-buffer change group 负责 text rollback,TP 的精确 property journal 覆盖 `with-silent-modifications`;失败恢复 buffer direct properties、markers/index、objects、plan、client state、revision、bindings 与 signal values。publish 中 killed buffer 不复活,其他 surfaces 与 sources 回滚。 - `tp-surface-report` 的字段只使用 transaction/surface/source/binding/object/text/property/conflict/observer/timing 通用词汇。observer 在成功 commit 且 publishing transaction 的动态范围退出后执行,因此 observer 中的 signal write 会开启新 transaction;observer error 只写入 report,不回滚。 ## 15. 简单与响应式便利入口 - `tp-propertize STRING DECLARATIONS` 接受 Emacs 原生 property plist,把它转换为 canonical `text/` declarations,经 schema/cascade/projector 后应用到 STRING 的防御性副本;它不建立 object、binding、anchor 或 surface。 - `tp-apply BUFFER START END DECLARATIONS` 使用同一 projection 与现有 range mutation primitive,只修改声明过的 direct properties,保留文本和未声明 property,并返回 `(START . END)`。 - `tp-watch BUFFER START END COMPUTE` 创建一个 properties-only surface。COMPUTE 是返回原生 property plist 的零参数函数;其 signal/binding dependencies 由 exact graph 收集。返回值就是可传给 `tp-surface-inspect`、`tp-surface-report` 和 `tp-surface-unmount` 的 opaque surface。首次 publication 失败会释放 convenience 层创建的 anchor。