docs(tp): separate property composition from CSS cascade

Define TP as the independent retained text-property runtime and reserve complete stylesheet winner semantics for ECSS.

Verified: make test EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs LOAD_EXTRA="-L ../twidget2/dependencies" (729 passed); git diff --check
This commit is contained in:
Kinneyzhang 2026-08-06 18:03:48 +08:00
parent 1195297011
commit 9d4047daf2
2 changed files with 32 additions and 22 deletions

View File

@ -10,6 +10,8 @@ TP 1.0 is an independently installable, testable, releasable, and usable retaine
TP does not depend on Ebox and contains no spatial concepts such as Box, Flex, Grid, padding, layout owners, or Ebox regions. Ebox, ETAF, dashboards, diagnostics, interactive lists, and ordinary buffer annotations can all consume the same public TP APIs.
TP is not a CSS engine either. Complete stylesheets, selectors, specificity, origin/importance, CSS cascade layers, CSS-wide values, and custom properties belong to the independent ECSS package. ECSS may hand final computed declarations to TP for publication, but TP neither requires, detects, nor names ECSS.
The boundary in one sentence is:
> TP owns objects, dependencies, positions, changes, and publication; an upper consumer owns business meaning and the desired display result.
@ -17,6 +19,7 @@ The boundary in one sentence is:
## 2. Design invariants
- TP source, tests, examples, and package metadata neither require, detect, nor name Ebox.
- TP owns no CSS stylesheet, selector matching, specificity, or cascade winner; those capabilities belong to the independent ECSS package.
- One property, binding, surface, mount, diff, and transaction core serves every live use; there is no embedded mode.
- TP is the sole final buffer committer. Producer and consumer prepare phases produce candidate values only.
- One live object has one TP identity. A consumer may store its opaque handle but cannot maintain a parallel identity requiring long-term synchronization.
@ -39,7 +42,7 @@ application state / theme / host edit
│ candidate objects/bindings/anchors
ordinary or client producer
│ object → schema/rules/cascade
│ object → direct property contributions
│ → generic keyed surface plan
reconcile + prepare diff
@ -53,19 +56,21 @@ application state / theme / host edit
Prepare creates candidate state without publication. A producer returns an ordinary surface plan plus optional opaque client state. TP never interprets client state but promotes it atomically with the plan, dependencies, mounts, and revision.
## 4. Properties, cascade, and value sources
## 4. Property contributions, composition, and value sources
### 4.1 Property schema
### 4.1 Property policies and contribution composition
`tp-define-property` registers initial value, inheritance, normalizer, validator, equality, merge, projector, and optional shorthand expansion for a namespaced property id. A schema may project a computed value to Emacs text properties or expose it only to a consumer.
TP maintains generic property policies for the Emacs text properties it ultimately writes: presence, normalization, validation, equality, merge, and projection. A policy answers only how TP contributions compose, whether a value truly changed, and how the result becomes an Emacs property; it defines no selector, stylesheet, or CSS winner.
TP provides a complete deterministic cascade over registered properties: origin/importance, layers, specificity, scope/source order, CSS-wide values, inheritance, custom properties/variables, shorthands, explicit nil versus absence, and schema-defined normalization/equality. TP does not claim browser CSS properties or spatial layout.
Every properties mount contributes only the properties it declares. TP composes the host baseline and all TP contributions per property in stable mount/contribution order, strictly distinguishes explicit nil from absence, and retains enough side state to remove one contribution, detect an external conflict, and roll back a transaction. This is text-property ownership and composition, not CSS cascade.
ECSS is an independent pure style-computation package: it consumes property schemas, a subject tree/adapter, and a stylesheet and returns computed declarations plus provenance. It neither mutates buffers nor owns markers and depends on neither TP nor Ebox. Ebox or an ordinary text UI that needs complete CSS runs ECSS first and then hands the final text/property plan to TP; callers that do not need CSS use TP directly.
### 4.2 Literal and computed values
Every ordinary Elisp value is literal, including function objects. TP never invokes a help-echo function, keymap command, or callback implicitly.
A declaration that must be evaluated uses `tp-computed` to wrap a compute function. It executes in the owning object's binding during prepare; `tp-signal-read` and `tp-binding-read` record dependencies automatically. The result then passes through normalization and validation and is treated as a literal, with no implicit second invocation. Errors abort the whole candidate transaction, and equal results produce no surface change.
A declaration that must be evaluated uses `tp-computed` to wrap a compute function. It executes in the owning object's binding during prepare; `tp-signal-read` and `tp-binding-read` record dependencies automatically. The result then passes through the property policy's normalization and validation and is treated as a literal, with no implicit second invocation. Errors abort the whole candidate transaction, and equal results produce no surface change.
An upper DSL may accept evaluable Elisp, but its constructor/compiler boundary must distinguish quoted lists, literal functions, and computed sources and compile reactive computation to `tp-computed` rather than creating a second watcher.
@ -123,7 +128,7 @@ A `content` mount owns text and properties in its span. It may insert, delete, m
A `properties` mount contributes declared properties without changing host text. `tp-range-anchor-create` constructs an opaque marker-backed anchor from buffer, start/end, and marker insertion policy. During prepare the producer calls `tp-object-attach-range` to attach an object to that anchor. The plan itself still contains no position.
For every anchor/property interval, TP side state records the host baseline, every TP contribution, and the last published value. Overlapping contributions from one surface compose through schema merge/cascade.
For every anchor/property interval, TP side state records the host baseline, every TP contribution, and the last published value. Overlapping contributions from one surface compose per property through its property policy.
If external code changes the same property to a value different from TP's last publication, the next prepare raises `tp-property-conflict` by default. TP neither overwrites the external value nor restores an old snapshot; the caller explicitly invokes `tp-range-rebase` or unmounts. Unmount removes only the TP contribution: it restores the baseline only while the current value still equals TP's last publication, otherwise preserving the external value and recording the conflict.
@ -148,7 +153,7 @@ If a buffer is killed during publication, kill-buffer teardown is authoritative
(tp-apply buffer start end declarations)
```
Both use the same schema/cascade/projector core without identity, bindings, or live mounts. Existing `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, lookup, and search APIs reuse the same lower semantics.
Both use the same direct-declaration validation/projection and mutation primitives without identity, bindings, or live mounts. Existing `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, lookup, and search APIs reuse the same lower semantics.
### 10.2 Reactive existing text
@ -177,7 +182,7 @@ With no Ebox on `load-path`, TP 1.0 continuously verifies:
1. static string/buffer properties: nested faces, keymaps, help-echo functions, and explicit nil;
2. reactive status: `tp-watch` and a retained content producer, conditional/binding dependencies, and batching;
3. retained dashboard: keyed add/remove/reorder, buttons, theme cascade, and rollback;
3. retained dashboard: keyed add/remove/reorder, buttons, signal-driven theme declarations, and rollback;
4. existing-text diagnostics: range anchors, host edits, overlap, external property conflict/rebase/unmount;
5. two-buffer producer: isolated identity/state plus global-signal multi-surface atomicity;
6. performance: sparse updates proportional to the actual dependency closure, zero buffer operations for equal values, and no marker/subscription/weak-reference leaks after unmount.
@ -186,7 +191,7 @@ With no Ebox on `load-path`, TP 1.0 continuously verifies:
TP 1.0 is a major-version transition. Stateless public APIs that map directly to the unified core remain. Managed behavior that requires the inline `tp-layers`/`tp-meta` database or scan renderer is explicitly removed or replaced rather than retained in a hidden compatibility engine.
An explicit one-shot legacy import may scan historical propertized text and construct a surface. Normal signal/style/update hot paths never invoke it automatically.
An explicit one-shot legacy import may scan historical propertized text and construct a surface. Normal signal/property/update hot paths never invoke it automatically.
The final system deletes the layer-to-buffer registry, scan-driven refresh hooks, old inline managed codec, duplicate transactions, and old batch renderer. TP 1.0 package, tests, examples, and this document all work without an Ebox repository.
@ -194,4 +199,4 @@ The final system deletes the layer-to-buffer registry, scan-driven refresh hooks
Executable contract tests must define prepare-context/object timing; binding identity and lifecycle; literal/computed values; range anchors and property conflicts; single/multi-surface rollback; the three convenience levels; error taxonomy; report shape; explicit nil versus absence; and read-only, undo, narrowing, indirect-buffer, and kill-buffer behavior before implementation begins.
If implementation requires an Ebox-specific branch, post-commit identity scan, raw position/closure in a plan, a second renderer, or cannot safely remove a properties contribution, integration stops for ownership review rather than adding an adapter mode.
If implementation requires an Ebox/ECSS-specific branch, a CSS selector/cascade winner, a post-commit identity scan, raw position/closure in a plan, a second renderer, or cannot safely remove a properties contribution, integration stops for ownership review rather than adding an adapter mode.

View File

@ -10,6 +10,8 @@ TP 1.0 是一个可独立安装、测试、发布和使用的 Emacs retained/rea
TP 不依赖 Ebox也不包含 Box、Flex、Grid、padding、layout owner 或 Ebox region 等空间布局概念。Ebox、ETAF、dashboard、diagnostics、交互列表和普通 buffer 标注都可以通过同一 TP public API 使用它。
TP 也不是 CSS 引擎。完整 stylesheet、selector、specificity、origin/importance、CSS cascade layer、CSS-wide value 和 custom property 由独立 ECSS 包负责ECSS 可以把最终 computed declarations 交给 TP 发布,但 TP 不 require、检测或命名 ECSS。
一句话边界是:
> TP 负责对象、依赖、位置、变化和提交;上层 consumer 负责这些对象的业务含义以及期望显示结果。
@ -17,6 +19,7 @@ TP 不依赖 Ebox也不包含 Box、Flex、Grid、padding、layout owner 或
## 2. 设计不变量
- TP source、tests、examples 和 package metadata 不 require、检测或命名 Ebox。
- TP 不拥有 CSS stylesheet、selector matching、specificity 或 cascade winner这些能力属于独立 ECSS 包。
- live runtime 只有一套 property、binding、surface、mount、diff 和 transaction core不增加 embedded mode。
- TP 是唯一最终 buffer committerproducer 和 consumer 的 prepare 阶段只能产生候选值。
- 一个 live object 只有一个 TP identityconsumer 可以保存 opaque handle但不能建立需要长期同步的平行 identity。
@ -39,7 +42,7 @@ application state / theme / host edit
│ candidate objects/bindings/anchors
ordinary or client producer
│ object → schema/rules/cascade
│ object → direct property contributions
│ → generic keyed surface plan
reconcile + prepare diff
@ -53,19 +56,21 @@ application state / theme / host edit
prepare 只产生 candidate state。producer 返回普通 surface plan 和可选 opaque client state。TP 不解释 client state但把它与 plan、dependencies、mounts 和 revision 一起原子晋升。
## 4. 属性、cascade 与 value source
## 4. 属性 contribution、合成与 value source
### 4.1 Property schema
### 4.1 Property policy 与 contribution composition
`tp-define-property` 为 namespaced property id 注册 initial value、inheritance、normalizer、validator、equality、merge、projector 和 optional shorthand expander。schema 可以把 computed value 投影为 Emacs text properties也可以只把值暴露给 consumer。
TP 为它最终写入的 Emacs text property 维护通用 property policypresence、normalizer、validator、equality、merge 和 projector。policy 只回答多个 TP contribution 如何合成、值是否真的变化以及怎样形成最终 Emacs property它不定义 selector、stylesheet 或 CSS winner。
TP 对已注册属性域提供完整、确定的 cascadeorigin/importance、layer、specificity、scope/source order、CSS-wide values、inheritance、custom properties/variables、shorthand、显式 nil 与 absent以及 schema-defined normalization/equality。TP 不声称实现浏览器 CSS 属性或空间布局。
每个 properties mount 只贡献它声明的 property。TP 按稳定的 mount/contribution 顺序逐属性合成 host baseline 与所有 TP contribution严格区分显式 nil 和 absent并保存足够的 side state 以便撤销单个 contribution、检测外部冲突和执行事务回滚。这个过程是文本属性 ownership/composition不是 CSS cascade。
ECSS 是独立的纯样式计算包:输入 property schemas、subject tree/adapter 与 stylesheet输出 computed declarations 和 provenance不读写 buffer、不持有 marker也不依赖 TP 或 Ebox。Ebox 或普通文本 UI 若需要完整 CSS 能力,先调用 ECSS再把最终文本/属性计划交给 TP不需要 CSS 的调用者直接使用 TP。
### 4.2 Literal 与 computed
普通 Elisp value 永远是 literal包括 function object。`help-echo` function、keymap command 和 callback 不会被 TP 隐式调用。
需要求值的声明必须使用 `tp-computed` 包装 compute function。compute 在所属 object binding 的 prepare context 中运行;`tp-signal-read` 和 `tp-binding-read` 自动登记依赖。返回值随后 normalize/validate并按 literal 处理,不隐式调用第二次。错误终止整个 candidate transaction相等结果不产生 surface change。
需要求值的声明必须使用 `tp-computed` 包装 compute function。compute 在所属 object binding 的 prepare context 中运行;`tp-signal-read` 和 `tp-binding-read` 自动登记依赖。返回值随后按 property policy normalize/validate并按 literal 处理,不隐式调用第二次。错误终止整个 candidate transaction相等结果不产生 surface change。
上层 DSL 可以接受可求值 Elisp但必须在 constructor/compiler 边界明确区分 quoted list、literal function 和 computed source并把响应式计算编译到 `tp-computed`,不能建立第二套 watcher。
@ -123,7 +128,7 @@ public constructor 对 caller-owned plist/string 做 defensive copy 或提供 im
`properties` mount 只能贡献声明的属性,不能修改 host text。`tp-range-anchor-create` 从 buffer、start/end 和 marker insertion policy 创建 opaque marker-backed anchorproducer 在 prepare 中调用 `tp-object-attach-range` 把 object 绑定到 anchor。plan 本身仍没有位置。
TP side state 为每个 anchor/property interval 保存 host baseline、各 TP contribution 和最后一次 published value。同一 surface 的重叠 contribution 通过 schema merge/cascade 合成。
TP side state 为每个 anchor/property interval 保存 host baseline、各 TP contribution 和最后一次 published value。同一 surface 的重叠 contribution 通过 property policy 逐属性合成。
外部代码把同一 property 改成不同于 TP 最后发布值时,下一次 prepare 默认报 `tp-property-conflict`。TP 不覆盖外部值,也不恢复旧 snapshot调用者显式 `tp-range-rebase` 或 unmount。unmount 只撤销 TP contribution当前值仍等于 TP 最后发布值时恢复 baseline否则保留外部值并在 report 中记录 conflict。
@ -148,7 +153,7 @@ transaction 顺序是:冻结 candidate signal writes建立 prepare contexts
(tp-apply buffer start end declarations)
```
二者使用相同 schema/cascade/projector core,不建立 identity、binding 或 live mount。现有 `tp-set`、`tp-reset`、`tp-add`、`tp-remove`、lookup/search APIs 继续复用这些底层语义。
二者使用相同的 direct-declaration validation/projection 和 mutation primitives,不建立 identity、binding 或 live mount。现有 `tp-set`、`tp-reset`、`tp-add`、`tp-remove`、lookup/search APIs 继续复用这些底层语义。
### 10.2 响应式已有文本
@ -177,7 +182,7 @@ TP 1.0 在没有 Ebox load-path 时必须长期通过:
1. static string/buffer propertiesnested face、keymap、help-echo function、explicit nil
2. reactive status`tp-watch` 与 retained content producer、conditional/binding dependency、batch
3. retained dashboardkeyed add/remove/reorder、button、theme cascade、rollback
3. retained dashboardkeyed add/remove/reorder、button、signal-driven theme declarations、rollback
4. existing-text diagnosticsrange anchor、host edit、overlap、external property conflict/rebase/unmount
5. two-buffer produceridentity/state 隔离与 global-signal multi-surface atomicity
6. performancesparse update 与实际 dependency closure 成正比equal value 零 buffer opunmount 后无 marker/subscription/weak-reference 泄漏。
@ -186,7 +191,7 @@ TP 1.0 在没有 Ebox load-path 时必须长期通过:
TP 1.0 是主版本切换。能直接映射到统一 core 的静态 public API 保留;依赖 inline `tp-layers`/`tp-meta` database 和扫描式 renderer 才能工作的 managed behavior 必须明确删除或替换,不保留 hidden compatibility engine。
允许显式 one-shot legacy import 扫描历史 propertized text 并建立 surfacenormal signal/style/update 热路径禁止自动调用它。
允许显式 one-shot legacy import 扫描历史 propertized text 并建立 surfacenormal signal/property/update 热路径禁止自动调用它。
最终删除 layer→buffer registry、scan-driven refresh hooks、旧 inline managed codec、重复 transaction 和旧 batch renderer。TP 1.0 package、tests、examples 和本文档都能在没有 Ebox repository 的环境中独立工作。
@ -194,4 +199,4 @@ TP 1.0 是主版本切换。能直接映射到统一 core 的静态 public API
实现开始前的 executable contract tests 必须确定prepare-context/object 时序binding identity/lifecycleliteral/computed valuerange anchor 和 property conflictsingle/multi-surface rollback三层便利 APIerror taxonomyreport shapeexplicit nil/absenceread-only、undo、narrowing、indirect-buffer 和 kill-buffer 行为。
任何实现若要求 Ebox-specific branch、post-commit identity scan、plan 中的 raw position/closure、第二套 renderer 或无法安全撤销 properties contribution应停止接入并重新评审 ownership model而不是增加 adapter mode。
任何实现若要求 Ebox/ECSS-specific branch、CSS selector/cascade winner、post-commit identity scan、plan 中的 raw position/closure、第二套 renderer 或无法安全撤销 properties contribution应停止接入并重新评审 ownership model而不是增加 adapter mode。