refactor: publish framework changes through structured TP participants
Some checks are pending
CI / test (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-09-05 05:25:29 +08:00
parent f1f91468aa
commit 32b4834322
15 changed files with 422 additions and 251 deletions

View File

@ -2,7 +2,7 @@
All notable changes to Ebox are documented here.
## 2.0.1 (Unreleased)
## 3.0.0 (Unreleased)
### Added
@ -11,15 +11,19 @@ All notable changes to Ebox are documented here.
- Surface-scoped region handles through `ebox-region-resolve`, retained TP publication through `ebox-render-to-buffer` and `ebox-display-buffer`, and atomic handle/selector/viewport/scroll updates.
- Typography longhands and inherited foreground projection that keep text readable when child boxes override background colors.
- A public, nestable framework render-burst boundary that covers pre-commit allocation, shares interactive deferred GC ownership with Ebox commits, and restores batch GC settings without collecting at the boundary.
- A supported `ebox-transaction-participant-route` switch: v2 structured TP
registration is the default, while the complete v1 adapter remains available
explicitly and as the compatibility fallback for older TP manifests.
- Structured TP participant registration for every retained publication.
### Changed
- Package metadata now distinguishes builds with the public unmount/revision
boundary and SPI v2 compatibility surface from older 2.0.0 development
boundary and SPI v2 compatibility surface from older 2.x development
snapshots.
- TP 1.0.1 is the minimum consumer-first dependency. Ebox accepts its structured
transition protocol and TP 2.0.0's final v2-only protocol, while always using
the public structured participant API.
- SPI initial publication owns widening and an outer editor change group. A
failed stage or nonlocal rollback exit restores text, markers, overlays,
point, mark, narrowing, undo history, and modified state before it escapes.
- ECSS is the only selector matcher and cascade winner. Ebox adapts node trees to ECSS subjects, interprets computed values for measurement/layout/paint, and classifies their layout impact.
- TP is the only live buffer writer. Ebox produces retained surface candidates and semantic dirty-owner plans; TP owns diff execution, marker-backed mounts, revisions, transactions, and rollback.
- Flex and Grid container/item declarations enter the same ECSS pipeline as ordinary boxes instead of maintaining a parallel style path.
@ -35,10 +39,15 @@ All notable changes to Ebox are documented here.
- `ebox-pop-to-buffer`, `ebox-switch-to-buffer`, and the private erase-and-evaluate buffer writer they exposed.
- Ebox's duplicate selector parser/matcher and duplicate cascade winner.
- Bundled playground fixtures. Executable `.ebox` references live in the independent `ebox-playground` package.
- The Ebox transaction route switch, v1 TP participant adapter call, and
missing-capability fallback.
### Migration
- Install ECSS 0.1.0 and TP 1.0.1, in either order, before Ebox 2.0.1.
- Install ECSS 0.1.0 and TP 1.0.1 or newer, in either order, before Ebox 3.0.0.
- Remove any customization of `ebox-transaction-participant-route`; Ebox 3.0.0
requires TP's structured participant capability and fails closed when it is
missing or malformed.
- Replace numeric region-id mutation with `ebox-region-resolve` followed by `ebox-region-update` on the returned handle.
- Replace `ebox-pop-to-buffer` or `ebox-switch-to-buffer` with `ebox-display-buffer`, or call `ebox-render-to-buffer` when display is controlled by the caller.
- Ebox does not scan previously rendered text to reconstruct runtime identity. Remount the declarative source through Ebox 2.0.
- Ebox does not scan previously rendered text to reconstruct runtime identity. Remount the declarative source through Ebox 3.0.

View File

@ -2,7 +2,7 @@
Ebox 的重要变化记录在这里。
## 2.0.1(未发布)
## 3.0.0(未发布)
### 新增
@ -11,14 +11,17 @@ Ebox 的重要变化记录在这里。
- 通过 `ebox-region-resolve` 提供 surface-scoped region handle`ebox-render-to-buffer` 与 `ebox-display-buffer` 统一使用 retained TP publicationhandle、selector、viewport 与 scroll 更新加入同一原子事务。
- 增加 typography longhand 与继承 foreground 的投影;子盒子覆盖背景色时仍能保持文字可读。
- 增加公开、可嵌套的 framework render-burst 边界,覆盖 commit 前分配,与 Ebox commit 共用交互 deferred GC ownership并在 batch 边界不主动 GC 的情况下恢复原设置。
- 增加受支持的 `ebox-transaction-participant-route` 开关:默认使用 v2
structured TP registration显式旧 route 与旧 TP manifest 的完整 v1 adapter
fallback 继续保留。
- 所有 retained publication 统一使用 structured TP participant registration。
### 变更
- 包 metadata 现在会区分具备 public unmount/revision 边界与 SPI v2
compatibility surface 的构建,旧的 2.0.0 development snapshot 不会再被误接受。
compatibility surface 的构建,旧的 2.x development snapshot 不会再被误接受。
- TP 1.0.1 是 consumer-first 迁移的最低依赖。Ebox 接受其 structured 过渡协议与
TP 2.0.0 的最终 v2-only 协议,但始终只调用公开 structured participant API。
- SPI initial publication 自己负责 widening 与外层 editor change group。stage
失败或 rollback 非局部退出向外传播前,会恢复 text、marker、overlay、point、
mark、narrowing、undo history 与 modified state。
- ECSS 是唯一 selector matcher 与 cascade winner。Ebox 只把节点树适配为 ECSS subject解释 computed value 的测量、布局与绘制后果,并分类 layout impact。
- TP 是唯一 live buffer writer。Ebox 只生成 retained surface candidate 与语义 dirty-owner plandiff execution、marker-backed mount、revision、transaction 与 rollback 都属于 TP。
- Flex/Grid container 与 item declaration 和普通 box 一样进入同一条 ECSS pipeline不再维护平行 style path。
@ -33,10 +36,15 @@ Ebox 的重要变化记录在这里。
- `ebox-pop-to-buffer`、`ebox-switch-to-buffer`,以及它们暴露的私有 erase-and-evaluate buffer writer。
- Ebox 内重复的 selector parser/matcher 与 cascade winner。
- 仓库内置 playground fixture可执行 `.ebox` reference 已迁到独立 `ebox-playground` 包。
- Ebox transaction route 开关、v1 TP participant adapter 调用,以及 capability
缺失时的 fallback。
### 迁移
- ECSS 0.1.0 与 TP 1.0.1 互相独立,安装顺序任意;两者都安装后再安装 Ebox 2.0.1。
- ECSS 0.1.0 与 TP 1.0.1 或更高版本互相独立,安装顺序任意;两者都安装后再安装
Ebox 3.0.0。
- 删除 `ebox-transaction-participant-route` 的自定义设置Ebox 3.0.0 要求 TP
提供 structured participant capability缺失或格式错误时会 fail closed。
- 数字 region id 更新改为先调用 `ebox-region-resolve`,再把返回的 handle 交给 `ebox-region-update`
- `ebox-pop-to-buffer``ebox-switch-to-buffer` 改为 `ebox-display-buffer`;如果显示行为由调用方控制,则使用 `ebox-render-to-buffer`
- Ebox 不扫描历史渲染文本来重建 runtime identity请通过 Ebox 2.0 重新 mount 声明式 source。
- Ebox 不扫描历史渲染文本来重建 runtime identity请通过 Ebox 3.0 重新 mount 声明式 source。

View File

@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests m0a-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance style-schema-tests style-schema-performance spi-tests spi-performance c1b-contract-tests docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
all: check
@ -90,6 +90,8 @@ style-schema-performance:
spi-tests:
$(EMACS_TEST) -l tests/ebox-spi-tests.el -f ert-run-tests-batch-and-exit
c1b-contract-tests: ebox-commit-tests spi-tests package-tests docs-contract-tests
spi-performance:
$(EMACS_TEST) -l tests/ebox-spi-tests.el --eval "(ert-run-tests-batch-and-exit 'ebox-spi-performance-builds-bounded-provider-snapshots)"

View File

@ -7,7 +7,8 @@ application also needs Components, reactive state, behaviors, or lifecycle.
## Install
Ebox requires Emacs 29.1 or newer, ECSS, and TP. A package manager should
Ebox requires Emacs 29.1 or newer, ECSS 0.1.0 or newer, and TP 1.0.1 or newer.
A package manager should
install the declared dependencies. For sibling source checkouts, add the three
directories to `load-path`, then load Ebox:
@ -67,11 +68,11 @@ do not require a wrapper node.
Ebox copies canonical input before assigning runtime identity, so one built
value may be mounted in multiple buffers without sharing live ownership.
Ebox registers its rollback-capable TP state through the public structured v2
participant API by default. Set `ebox-transaction-participant-route` to `v1`
before the next operation for an immediate old-route rollback. If the loaded TP
manifest predates the structured API, the default v2 policy selects the same
complete v1 adapter automatically; the two registrations are never run together.
Ebox registers rollback-capable state only through TP's public structured
participant API. TP 1.0.1 supports the consumer-first migration protocol;
TP 2.0.0 publishes the final v2-only protocol. Missing or malformed structured
participant capability stops Ebox loading instead of selecting a compatibility
writer.
## Optional native module
@ -89,6 +90,7 @@ exact Elisp fallback. Ebox never builds it while loading.
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make c1b-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make native-rust-tests
```

View File

@ -6,8 +6,9 @@ Component、响应式状态、behavior 或生命周期时,使用同级 ETAF
## 安装
Ebox 需要 Emacs 29.1 或更高版本,并依赖 ECSS 与 TP。包管理器应自动安装声明的
依赖。使用同级源码 checkout 时,把三个目录加入 `load-path` 后加载 Ebox
Ebox 需要 Emacs 29.1 或更高版本、ECSS 0.1.0 或更高版本,以及 TP 1.0.1 或
更高版本。包管理器应自动安装声明的依赖。使用同级源码 checkout 时,把三个目录
加入 `load-path` 后加载 Ebox
```elisp
(add-to-list 'load-path "/path/to/ecss")
@ -62,10 +63,9 @@ Flex/Grid participation property 直接属于子 `box`,不需要额外 wrapper
Ebox 会在分配 runtime identity 前复制 canonical input因此同一个 built value 可以
挂载到多个 buffer而不会共享 live ownership。
Ebox 默认通过 TP 的公开 structured v2 participant API 注册可回滚状态。下一次
operation 前把 `ebox-transaction-participant-route` 设为 `v1`,即可立即回到旧
route。若加载的 TP manifest 尚未提供 structured API默认 v2 policy 会自动选择
同一条完整 v1 adapter两种 registration 绝不会同时运行。
Ebox 只通过 TP 的公开 structured participant API 注册可回滚状态。TP 1.0.1
支持 consumer-first 迁移协议TP 2.0.0 发布最终的 v2-only 协议。structured
participant capability 缺失或格式错误时Ebox 会停止加载,不会选择兼容 writer。
## 可选 native 模块
@ -83,6 +83,7 @@ fallback。Ebox 加载时不会自动构建它。
make load EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make compile EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make docs-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make c1b-contract-tests EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make check EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs
make native-rust-tests
```

View File

@ -86,17 +86,20 @@ Caller-owned Source Tree
- Keys are local to siblings; visible strings are never used as identity.
- Ebox owns logical node-to-subject adaptation and id/class/type candidate indexes; ECSS's public structured matcher is the only selector truth source. Subjects expose built-in id/key plus explicit `:selector-attributes`, never visible content, layout state, or runtime containers.
- `ebox-style--property-definitions` is the single Ebox author-property source. Load time derives Ebox's read-only lookup index and one immutable ECSS package schema from it; the surface schema is composed once, and node construction or updates never re-register or copy the whole schema domain.
- E1 freezes the closed M2a target classification for retained state: immutable generation facts; generation-bound scroll/native authority; opaque TP client-state custody; one-way compatibility mirrors; and disposable caches. Every inventory row separately records the current v1 storage shape plus its target owner, mutation API, generation binding, rollback, rebuild proof, and cleanup. Current v1 TP client state still holds the whole Ebox state plist until a later checkpoint changes that storage shape. The E1 mirror probes only rebuild and compare projections; they never mutate live state.
- E1 freezes the closed M2a target classification for retained state: immutable generation facts; generation-bound scroll/native authority; opaque TP client-state custody; one-way compatibility mirrors; and disposable caches. Every inventory row separately records the M2a-era storage shape plus its target owner, mutation API, generation binding, rollback, rebuild proof, and cleanup. TP client state holds the whole Ebox state plist. The E1 mirror probes only rebuild and compare projections; they never mutate live state.
- `ebox-layout.el` has no load or call edge to `ebox-surface.el` or TP. Candidate isolation, inherited-cascade detection, and one-shot materialization enter layout only through the validated port owned by `ebox-render-context.el`; `ebox.el` wires the surface implementation after both sides load.
- `ebox-incremental.el` likewise contains no `ebox-surface` symbol or publication call. `ebox-surface.el` installs one immutable read-only context port for live client state, region mounts, cascade proof, and inline-style preparation; batch observation remains in the higher `ebox.el` orchestrator.
- `ebox-patch-plan.el` consumes only tentative operation plists plus the immutable generation parent table and returns a deterministic plan artifact. The incremental adapter owns all live-fact preparation. E3 defaults to the pure route while retaining explicit `legacy` and equality-checking `shadow` routes; none of the three routes publishes.
- `ebox-style--property-definitions` is also the single used-value projection truth. Non-default engine lowering lives in each property's `:engine-projection`; ECSS metadata exposes the derived `:engine-targets`, and only non-public engine fields may use the small internal exception table.
- `ebox-framework-spi-capabilities` returns a fresh immutable v2 provider record declaring paired initial/update stage+rollback, combined participant ordering, same-object legacy reports, initial observation replay, and `tp-transaction-protocol-v1+v2`. Initial TP/Ebox timing is measured during publication, stored as defensive snapshots in the completed report, and replayed without mutation authority; no synthetic zero-duration timing is emitted. Ebox provides no selected port or consumer bootstrap; the existing v1 callback APIs remain available.
- Ebox's TP adapter defaults to the manifest-advertised public
`tp-transaction-participate-v2` API. `ebox-transaction-participant-route=v1`
is the immediate kill switch, and a TP manifest without the structured API
selects the same v1 adapter as a compatibility fallback. One helper owns this
selection and registers exactly one participant per transaction.
- `ebox-framework-spi-capabilities` returns a fresh immutable SPI v2 provider record declaring paired initial/update stage+rollback, combined participant ordering, same-object reports, initial observation replay, and the actual accepted TP protocol. `ebox-framework-spi-required-tp-version` exposes the minimum TP 1.0.1 dependency, while `ebox-framework-spi-supported-tp-protocols` lists the consumer-first and final v2 protocol identities. Initial TP/Ebox timing is measured during publication, stored as defensive snapshots in the completed report, and replayed without mutation authority; no synthetic zero-duration timing is emitted. Ebox provides no selected port or consumer bootstrap.
- Ebox registers every rollback-capable runtime mirror through TP's public
`tp-transaction-participate-v2` API. The loaded manifest must advertise that
callable API with an accepted structured protocol; missing or malformed
capability fails closed during load.
- SPI initial publication widens under provider ownership and wraps the TP
transaction in an editor change group. On any unsuccessful or nonlocal exit,
it restores original overlay identities and bounds together with buffer text,
marker identities, point, mark, narrowing, undo history, and modified state.
- Framework report finalization is postaccept and no-throw. Initial, full-update, and scoped-update completion faults are retained in the same legacy report as `framework-report-finalization` diagnostics; they cannot invoke framework rollback or escape into an ETAF semantic rollback window.
- `owner-rerender` is broader than `span-patch`, which is broader than `paint-patch`.
- Buffer coordinates belong to the generation that produced them and must be refreshed after mutation.
@ -144,6 +147,7 @@ make style-schema-tests
make style-schema-performance
make spi-tests
make spi-performance
make c1b-contract-tests
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

View File

@ -86,16 +86,19 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- Key 只在兄弟节点中有效;不能用可见字符串作为 identity。
- Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。
- `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。
- E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录当前 v1 存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。在后续 checkpoint 改变存储形态前,当前 v1 TP client state 保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。
- E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录 M2a 阶段的存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。TP client state 保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。
- `ebox-layout.el` 不再 load 或调用 `ebox-surface.el`/TP。candidate isolation、继承 cascade 判定和一次性 materialization 只能通过 `ebox-render-context.el` 拥有的已校验 port 进入 layout`ebox.el` 在两侧加载后接线 surface 实现。
- `ebox-incremental.el` 同样不再包含任何 `ebox-surface` symbol 或 publication call。`ebox-surface.el` 只安装一个不可变只读 context port提供 live client state、region mount、cascade proof 与 inline-style preparationbatch observation 留在更高层的 `ebox.el` orchestrator。
- `ebox-patch-plan.el` 只消费 tentative operation plist 与不可变 generation parent table并返回 deterministic plan artifact所有 live fact 准备仍由 incremental adapter 拥有。E3 默认使用 pure route同时保留显式 `legacy` 与执行等价检查的 `shadow` route三条 route 都不能发布。
- `ebox-style--property-definitions` 同时也是 used-value projection 的唯一真相源。非默认 engine lowering 写在每个 property 的 `:engine-projection`ECSS metadata 暴露派生的 `:engine-targets`,只有非公共 engine 字段可以进入小型 internal exception table。
- `ebox-framework-spi-capabilities` 返回 fresh immutable v2 provider record声明 paired initial/update stage+rollback、combined participant order、same-object legacy report、initial observation replay 以及 `tp-transaction-protocol-v1+v2`。initial TP/Ebox timing 在 publication 期间真实测量,以 defensive snapshot 写入 completed report 后再 replay不生成虚假的零耗时数据也不携带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap现有 v1 callback API 保留。
- Ebox 的 TP adapter 默认使用 manifest 声明的公开
`tp-transaction-participate-v2` API`ebox-transaction-participant-route=v1`
是立即 kill switch。TP manifest 未提供 structured API 时,同一个 selection
owner 会选择完整 v1 adapter每个 transaction 只注册一个 participant。
- `ebox-framework-spi-capabilities` 返回 fresh immutable SPI v2 provider record声明 paired initial/update stage+rollback、combined participant order、same-object report、initial observation replay以及实际接受的 TP protocol。`ebox-framework-spi-required-tp-version` 公开最低 TP 1.0.1 依赖,`ebox-framework-spi-supported-tp-protocols` 列出 consumer-first 与最终 v2 protocol identity。initial TP/Ebox timing 在 publication 期间真实测量,以 defensive snapshot 写入 completed report 后再 replay不生成虚假的零耗时数据也不携带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap。
- Ebox 始终通过 TP 的公开 `tp-transaction-participate-v2` API 注册可 rollback 的
runtime mirror。加载的 manifest 必须用 accepted structured protocol 声明该可调用
APIcapability 缺失或格式错误会在加载时 fail closed。
- SPI initial publication 由 provider 负责 widen并在 TP transaction 外包一层
editor change group。任何失败或非局部退出都会恢复原 overlay identity/bounds
以及 buffer text、marker identity、point、mark、narrowing、undo history 与
modified state。
- Framework report finalization 属于 postaccept 且保证不向外抛出。initial、full update 与 scoped update 的 completion fault 都以 `framework-report-finalization` diagnostics 留在同一个 legacy report 中,不能触发 framework rollback也不能逃逸进 ETAF semantic rollback window。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。
@ -140,6 +143,7 @@ make style-schema-tests
make style-schema-performance
make spi-tests
make spi-performance
make c1b-contract-tests
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

View File

@ -4,21 +4,24 @@
;;; Commentary:
;; Additive provider-side Ebox framework SPI v2. This module publishes an
;; Provider-side Ebox framework SPI v2. This module publishes an
;; immutable capability snapshot and delegates initial/update execution to the
;; existing combined Ebox participant. It does not select a consumer port,
;; probe a framework package, or remove the v1 callback surface.
;; or probe a framework package.
;;; Code:
(require 'cl-lib)
(require 'ebox-canonical)
(require 'ebox-surface)
(defvar tp-transaction-protocol)
(require 'tp-transaction)
(defconst ebox-framework-spi--tp-v2-available-p
(require 'tp-transaction nil t)
"Non-nil when TP exposes the additive transaction protocol module.")
(defconst ebox-framework-spi-required-tp-version "1.0.1"
"Minimum TP package version required by Ebox framework SPI v2.")
(defconst ebox-framework-spi-supported-tp-protocols
'(tp-transaction-protocol-v1+v2 tp-transaction-protocol-v2)
"TP protocols that provide the structured participant API Ebox requires.")
(declare-function ebox--render-to-buffer-internal
"ebox" (buffer-or-name input options &optional participant))
@ -144,6 +147,58 @@
(signal 'wrong-type-argument (list 'functionp framework-rollback)))
t)
(defun ebox-framework-spi--buffer-overlays (buffer)
"Return every overlay currently attached to BUFFER."
(with-current-buffer buffer
(let ((lists (overlay-lists)))
(delete-dups (append (car lists) (cdr lists))))))
(defun ebox-framework-spi--snapshot-editor-custody (buffer)
"Capture BUFFER state that an unsuccessful initial mount must preserve."
(with-current-buffer buffer
(let ((restriction-start (point-min))
(restriction-end (point-max)))
(save-restriction
(widen)
(list
:point (point)
:mark-marker (mark-marker)
:mark-position (mark t)
:mark-insertion-type (marker-insertion-type (mark-marker))
:mark-active mark-active
:restriction-start restriction-start
:restriction-end restriction-end
:overlays
(mapcar (lambda (overlay)
(list overlay (overlay-start overlay) (overlay-end overlay)))
(ebox-framework-spi--buffer-overlays buffer))
:undo-list (copy-tree buffer-undo-list)
:modified-p (buffer-modified-p))))))
(defun ebox-framework-spi--restore-editor-custody (buffer snapshot)
"Restore BUFFER editor state from SNAPSHOT after mount rollback."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(widen)
(let* ((overlay-snapshots (plist-get snapshot :overlays))
(original-overlays (mapcar #'car overlay-snapshots)))
(dolist (overlay (ebox-framework-spi--buffer-overlays buffer))
(unless (memq overlay original-overlays)
(delete-overlay overlay)))
(dolist (entry overlay-snapshots)
(when (overlayp (car entry))
(move-overlay (car entry) (nth 1 entry) (nth 2 entry) buffer))))
(let ((mark-marker (plist-get snapshot :mark-marker)))
(set-marker mark-marker (plist-get snapshot :mark-position) buffer)
(set-marker-insertion-type
mark-marker (plist-get snapshot :mark-insertion-type)))
(goto-char (plist-get snapshot :point))
(setq mark-active (plist-get snapshot :mark-active)
buffer-undo-list (copy-tree (plist-get snapshot :undo-list)))
(narrow-to-region (plist-get snapshot :restriction-start)
(plist-get snapshot :restriction-end))
(set-buffer-modified-p (plist-get snapshot :modified-p)))))
(defun ebox-framework-spi-initial
(buffer-or-name input framework-stage framework-rollback)
"Initially mount INPUT and run the paired framework callbacks.
@ -160,10 +215,10 @@ transaction failure. Return that same report object after completion."
(let ((buffer (get-buffer-create buffer-or-name)))
(when (ebox-surface-buffer-mounted-p buffer)
(error "Ebox SPI initial operation requires an unmounted buffer"))
(let* ((existing-bridge
(let* ((custody (ebox-framework-spi--snapshot-editor-custody buffer))
(change-group (with-current-buffer buffer (prepare-change-group)))
(existing-bridge
(with-current-buffer buffer ebox-surface--tp-observer))
(_bridge (or existing-bridge
(ebox-surface--ensure-tp-observer buffer)))
(started (float-time))
(participant
(ebox-surface--make-framework-participant
@ -177,11 +232,29 @@ transaction failure. Return that same report object after completion."
(cl-remf report :framework-initial-observation-started)
(funcall framework-rollback report))
:state 'unpublished
:diagnostics nil)))
:diagnostics nil))
activated success)
(unwind-protect
(ebox--render-to-buffer-internal buffer input nil participant)
(unless existing-bridge
(ebox-surface-cleanup-buffer-observer buffer))))))
(progn
(unless existing-bridge
(ebox-surface--ensure-tp-observer buffer))
(with-current-buffer buffer (activate-change-group change-group))
(setq activated t)
(prog1 (with-current-buffer buffer
(save-restriction
(widen)
(ebox--render-to-buffer-internal
buffer input nil participant)))
(with-current-buffer buffer (accept-change-group change-group))
(setq success t)))
(unwind-protect
(unless success
(when (and activated (buffer-live-p buffer))
(with-current-buffer buffer
(cancel-change-group change-group))
(ebox-framework-spi--restore-editor-custody buffer custody)))
(unless existing-bridge
(ebox-surface-cleanup-buffer-observer buffer)))))))
(defun ebox-framework-spi-update
(buffer-or-name input framework-stage framework-rollback)
@ -264,8 +337,8 @@ or promotion capability."
ebox-framework-spi-schema-version)
(equal (ebox-framework-spi--provider-capabilities provider)
ebox-framework-spi--required-capabilities)
(eq (ebox-framework-spi--provider-tp-protocol provider)
tp-transaction-protocol)
(memq (ebox-framework-spi--provider-tp-protocol provider)
ebox-framework-spi-supported-tp-protocols)
(equal (ebox-framework-spi--provider-stage-order provider)
ebox-framework-spi--stage-order)
(equal (ebox-framework-spi--provider-rollback-order provider)
@ -308,31 +381,7 @@ or promotion capability."
(ebox-framework-spi-validate-provider provider)
provider))
(if ebox-framework-spi--tp-v2-available-p
(provide 'ebox-framework-spi-v2)
(dolist (function
'(ebox-framework-spi-operation-kind
ebox-framework-spi-operation-function
ebox-framework-spi-operation-argument-schema
ebox-framework-spi-operation-result-schema
ebox-framework-spi-operation-paired-stage-rollback-p
ebox-framework-spi-provider-spi-version
ebox-framework-spi-provider-schema-version
ebox-framework-spi-provider-capabilities
ebox-framework-spi-provider-tp-protocol
ebox-framework-spi-provider-stage-order
ebox-framework-spi-provider-rollback-order
ebox-framework-spi-provider-report-semantics
ebox-framework-spi-provider-initial-operation
ebox-framework-spi-provider-update-operation
ebox-framework-spi-initial
ebox-framework-spi-update
ebox-framework-spi-initial-observation-reports
ebox-framework-spi-validate-provider
ebox-framework-spi-capabilities))
(when (symbol-function function)
(fmakunbound function)))
(setq features (delq 'ebox-framework-spi-v2 features)))
(provide 'ebox-framework-spi-v2)
(provide 'ebox-spi)
;;; ebox-spi.el ends here

View File

@ -47,50 +47,36 @@
(defvar ebox--render-display-signature)
(defvar ebox-incremental--allocated-slot-proof-cache)
(defvar ebox-incremental--buffer-render-state-override)
(defvar ebox-transaction-participant-route 'v2
"Early default for the Ebox-owned TP participant route.")
(declare-function tp-runtime-manifest "tp" ())
(declare-function tp-transaction-participate-v2 "tp-reactive" (&rest args))
(define-error 'ebox-surface-participant-route-error
"Invalid Ebox TP participant route")
(define-error 'ebox-surface-tp-protocol-error
"Incompatible TP transaction protocol")
(defun ebox-surface--tp-participant-v2-capability-p (manifest)
"Return non-nil when MANIFEST provides the valid public TP v2 API.
An absent capability denotes an older compatible TP and returns nil. A
present but malformed capability fails closed instead of masking corruption."
(let ((api (plist-get manifest :structured-participant-api)))
(cond
((null api) nil)
((and (eq (plist-get manifest :transaction-protocol)
'tp-transaction-protocol-v1+v2)
(eq api 'tp-transaction-participate-v2)
(fboundp api))
t)
(t
(signal 'ebox-surface-participant-route-error
(list :malformed-tp-structured-participant manifest))))))
(defun ebox-surface--validate-tp-v2-capability (manifest)
"Validate MANIFEST as the required public TP v2 participant capability."
(let ((api (plist-get manifest :structured-participant-api))
(protocol (plist-get manifest :transaction-protocol)))
(unless (and (memq protocol
'(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2))
(eq api 'tp-transaction-participate-v2)
(fboundp api))
(signal 'ebox-surface-tp-protocol-error
(list :accepted-protocols
'(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2)
:manifest manifest)))
t))
(defconst ebox-surface--tp-participant-v2-available-p
(ebox-surface--tp-participant-v2-capability-p (tp-runtime-manifest))
"Non-nil when the loaded TP manifest advertises its public v2 API.")
(defun ebox-surface--effective-participant-route ()
"Return the selected TP participant route for the next registration."
(pcase ebox-transaction-participant-route
('v2 (if ebox-surface--tp-participant-v2-available-p 'v2 'v1))
('v1 'v1)
(_ (signal 'ebox-surface-participant-route-error
(list :participant-route ebox-transaction-participant-route)))))
(defconst ebox-surface--tp-v2-capability-valid-p
(ebox-surface--validate-tp-v2-capability (tp-runtime-manifest))
"Non-nil after the loaded TP manifest passes the required v2 contract.")
(defun ebox-surface--transaction-participate (key stage rollback)
"Register KEY with paired STAGE and ROLLBACK through one selected TP route."
(pcase (ebox-surface--effective-participant-route)
('v2
(tp-transaction-participate-v2
:key key :stage stage :rollback rollback))
('v1
(tp-transaction-participate key stage rollback))))
"Register KEY with paired STAGE and ROLLBACK through TP v2."
(tp-transaction-participate-v2
:key key :stage stage :rollback rollback))
(defvar ebox-surface--inline-style-value-cache nil
"Per-projection cache for selector-free computed inline styles.")

11
ebox.el
View File

@ -1,7 +1,7 @@
;;; ebox.el --- Pixel-perfect box model renderer -*- lexical-binding: t -*-
;; Author: ebox contributors
;; Version: 2.0.1
;; Version: 3.0.0
;; Package-Requires: ((emacs "29.1") (ecss "0.1.0") (tp "1.0.1"))
;; Keywords: ui, graphics, convenience
;; URL: https://github.com/ginqi7/ebox
@ -100,15 +100,6 @@
:group 'applications
:prefix "ebox-")
(defcustom ebox-transaction-participant-route 'v2
"TP participant registration route used by new Ebox transactions.
`v2' uses TP's public structured participant capability when advertised and
falls back to the complete v1 adapter when that capability is absent. `v1'
is the immediate rollback switch and always uses the retained adapter."
:type '(choice (const :tag "Structured v2 participant" v2)
(const :tag "Legacy v1 adapter" v1))
:group 'ebox)
;;;###autoload
(defun ebox-native-build (&optional clean)
"Build and install Ebox's optional native module asynchronously.

View File

@ -74,29 +74,16 @@
:scroll-region-ids (copy-sequence
(plist-get state :scroll-region-ids)))))
(defvar ebox-surface--tp-participant-v2-available-p)
(defun ebox-commit-test--participant-route-counts
(ebox-route tp-route v2-available-p)
"Return v2/v1 registration counts for EBOX-ROUTE and TP-ROUTE.
V2-AVAILABLE-P controls the frozen TP manifest capability used by the fixture."
(let ((buffer (generate-new-buffer " *ebox-participant-route*"))
(ebox-transaction-participant-route ebox-route)
(tp-transaction-execution-route tp-route)
(ebox-surface--tp-participant-v2-available-p v2-available-p)
(defun ebox-commit-test--participant-v2-count ()
"Return the structured participant registration count for mount and commit."
(let ((buffer (generate-new-buffer " *ebox-participant-v2*"))
(v2-register (symbol-function 'tp-transaction-participate-v2))
(v1-register (symbol-function 'tp-transaction-participate))
(v2-count 0)
(v1-count 0))
(v2-count 0))
(unwind-protect
(cl-letf (((symbol-function 'tp-transaction-participate-v2)
(lambda (&rest arguments)
(cl-incf v2-count)
(apply v2-register arguments)))
((symbol-function 'tp-transaction-participate)
(lambda (&rest arguments)
(cl-incf v1-count)
(apply v1-register arguments))))
(apply v2-register arguments))))
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "old")
:width '(80)))
@ -105,7 +92,7 @@ V2-AVAILABLE-P controls the frozen TP manifest capability used by the fixture."
:width '(80)))
(with-current-buffer buffer
(should (string-match-p "new" (buffer-string))))
(list :v2 v2-count :v1 v1-count))
v2-count)
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-source-index-promotes-and-rolls-back-atomically ()
@ -954,59 +941,36 @@ V2-AVAILABLE-P controls the frozen TP manifest capability used by the fixture."
1))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-transaction-participant-route-defaults-v2-across-tp-routes ()
"Ebox uses only public v2 registration under either TP live writer."
(should
(eq (eval (car (get 'ebox-transaction-participant-route 'standard-value)) t)
'v2))
(let ((ebox-transaction-participant-route 'v2))
(should (eq (ebox-surface--effective-participant-route) 'v2)))
(dolist (tp-route '(structured v1))
(let ((counts
(ebox-commit-test--participant-route-counts 'v2 tp-route t)))
(should (= (plist-get counts :v2) 2))
(should (= (plist-get counts :v1) 0)))))
(ert-deftest ebox-transaction-participant-is-v2-only ()
"Mount and commit register only through TP's structured participant API."
(should (= (ebox-commit-test--participant-v2-count) 2)))
(ert-deftest ebox-transaction-participant-capability-is-absent-or-valid ()
"Old TP manifests fall back, while advertised corruption fails closed."
(should-not
(ebox-surface--tp-participant-v2-capability-p
'(:transaction-protocol tp-transaction-protocol-v1+v2)))
(should
(ebox-surface--tp-participant-v2-capability-p (tp-runtime-manifest)))
(ert-deftest ebox-transaction-participant-accepts-structured-protocols ()
"Both consumer-first and final TP manifests satisfy Ebox's v2 contract."
(dolist (protocol '(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2))
(should
(ebox-surface--validate-tp-v2-capability
(list :transaction-protocol protocol
:structured-participant-api
'tp-transaction-participate-v2)))))
(ert-deftest ebox-transaction-participant-rejects-missing-or-malformed-v2 ()
"Missing or incompatible structured capabilities fail closed."
(should-error
(ebox-surface--tp-participant-v2-capability-p
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol tp-transaction-protocol-v1+v2))
:type 'ebox-surface-tp-protocol-error)
(should-error
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol tp-transaction-protocol-v1+v2
:structured-participant-api ignore))
:type 'ebox-surface-participant-route-error)
:type 'ebox-surface-tp-protocol-error)
(should-error
(ebox-surface--tp-participant-v2-capability-p
(ebox-surface--validate-tp-v2-capability
'(:transaction-protocol incompatible
:structured-participant-api tp-transaction-participate-v2))
:type 'ebox-surface-participant-route-error))
(ert-deftest ebox-transaction-participant-route-retains-v1-fallback ()
"Missing TP v2 capability and the kill switch each select only v1."
(dolist (case '((v2 structured nil) (v1 structured t) (v1 v1 t)))
(let ((counts
(ebox-commit-test--participant-route-counts
(nth 0 case) (nth 1 case) (nth 2 case))))
(should (= (plist-get counts :v2) 0))
(should (= (plist-get counts :v1) 2)))))
(ert-deftest ebox-transaction-participant-route-rejects-invalid-before-live-write ()
"An invalid Ebox route fails inside TP and leaves buffer/runtime unpublished."
(let ((buffer (generate-new-buffer " *ebox-invalid-participant-route*"))
(ebox-transaction-participant-route 'invalid))
(unwind-protect
(progn
(should-error
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "rejected")))
:type 'ebox-surface-participant-route-error)
(should-not (ebox--buffer-render-state buffer))
(with-current-buffer buffer (should (equal (buffer-string) ""))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
:type 'ebox-surface-tp-protocol-error))
(ert-deftest ebox-commit-framework-publish-failure-rolls-back-full-and-scoped ()
"A framework publish failure invokes its pair once on both commit paths."

View File

@ -56,7 +56,7 @@
"layout-boundary-tests" "layout-boundary-performance"
"patch-plan-tests" "patch-plan-performance"
"style-schema-tests" "style-schema-performance"
"spi-tests" "spi-performance"
"spi-tests" "spi-performance" "c1b-contract-tests"
"docs-contract-tests" "ci-contract-tests" "performance-evaluator"
"visual-check"
"native-rust-tests" "native-build" "package-lint" "diff-check")

View File

@ -30,6 +30,30 @@
(insert "\n"))
(buffer-string)))
(defun ebox-test--production-lisp-text ()
"Return all package-root Ebox Lisp production source as text."
(with-temp-buffer
(dolist (file (directory-files ebox-test--root t "\\`ebox.*\\.el\\'"))
(insert-file-contents file)
(insert "\n"))
(buffer-string)))
(ert-deftest ebox-package-c1b-is-v2-only ()
"Ebox 3 requires structured TP and contains no executable v1 bridge."
(let ((facade
(with-temp-buffer
(insert-file-contents (expand-file-name "ebox.el" ebox-test--root))
(buffer-string)))
(source (ebox-test--production-lisp-text)))
(should (string-match-p "Version: 3.0.0" facade))
(should (string-match-p (regexp-quote "(tp \"1.0.1\")") facade))
(should-not (string-match-p "ebox-transaction-participant-route" source))
(should-not (string-match-p "(tp-transaction-participate\\_>" source))
(should (string-match-p "(tp-transaction-participate-v2\\_>" source))
(should (equal ebox-framework-spi-required-tp-version "1.0.1"))
(should (memq tp-transaction-protocol
ebox-framework-spi-supported-tp-protocols))))
(ert-deftest ebox-package-defines-custom-group-and-scroll-option ()
"The package exposes a real customization group and default scroll option."
(should (get 'ebox 'custom-group))

View File

@ -38,10 +38,14 @@
(should (ebox-framework-spi-validate-provider first))
(should-not (eq first second))
(should (= (ebox-framework-spi-provider-spi-version first) 2))
(should (equal ebox-framework-spi-required-tp-version "1.0.1"))
(should (equal ebox-framework-spi-supported-tp-protocols
'(tp-transaction-protocol-v1+v2
tp-transaction-protocol-v2)))
(should (eq (ebox-framework-spi-provider-schema-version first)
'ebox-framework-spi-schema/v2))
(should (eq (ebox-framework-spi-provider-tp-protocol first)
'tp-transaction-protocol-v1+v2))
tp-transaction-protocol))
(should
(equal (ebox-framework-spi-provider-capabilities first)
'(initial-paired-stage-rollback
@ -108,63 +112,19 @@
(ebox-framework-spi-provider-initial-operation fresh)))
'buffer)))))
(ert-deftest ebox-spi-provider-is-additive-and-does-not-bootstrap-consumers ()
"E5 publishes no selected port, consumer probe, or v1 removal."
(ert-deftest ebox-spi-provider-does-not-bootstrap-consumers ()
"The provider publishes no selected port or consumer probe."
(let ((source (ebox-spi-test--source "ebox-spi.el"))
(facade (ebox-spi-test--source "ebox.el")))
(dolist (pattern '("featurep" "fboundp" "selected-port"
"etaf-renderer-port" "etaf-spi-bootstrap"))
(should-not (string-match-p (regexp-quote pattern) source)))
(should (string-match-p "Version: 2.0.1" facade))
(should (string-match-p "Version: 3.0.0" facade))
(should (string-match-p "(tp \"1.0.1\")" facade))
(should (functionp #'ebox-render-to-buffer))
(should (functionp #'ebox-commit))
(should (functionp #'ebox-framework-spi-capabilities))))
(ert-deftest ebox-spi-absent-tp-v2-keeps-the-v1-facade-loadable ()
"An absent TP transaction feature leaves the optional v2 provider absent."
(let* ((emacs (expand-file-name invocation-name invocation-directory))
(parent (file-name-directory
(directory-file-name ebox-spi-test--root)))
(ecss (expand-file-name "ecss" parent))
(tp (expand-file-name "tp" parent))
(provider-source
(expand-file-name "ebox-spi.el" ebox-spi-test--root))
(probe
(prin1-to-string
`(progn
(require 'cl-lib)
(require 'ebox-canonical)
(require 'ebox-surface)
(let ((original-require (symbol-function 'require)))
(cl-letf (((symbol-function 'require)
(lambda (feature &optional filename noerror)
(if (eq feature 'tp-transaction)
nil
(funcall original-require feature filename
noerror)))))
(load ,provider-source nil t)))
(require 'ebox)
(unless (featurep 'ebox-spi)
(error "SPI loader feature is absent"))
(when (featurep 'ebox-framework-spi-v2)
(error "v2 provider feature leaked without TP v2"))
(when (fboundp 'ebox-framework-spi-capabilities)
(error "v2 provider predicate leaked without TP v2"))
(unless (and (fboundp 'ebox-render-to-buffer)
(fboundp 'ebox-commit))
(error "v1 Ebox facade failed to load"))))))
(with-temp-buffer
(let ((status
(process-file
emacs nil (current-buffer) nil
"-Q" "--batch"
"-L" ebox-spi-test--root "-L" ecss "-L" tp
"--eval" "(setq load-prefer-newer t)"
"--eval" probe)))
(unless (zerop status)
(ert-fail (buffer-string)))))))
(ert-deftest ebox-spi-initial-and-update-share-their-legacy-report ()
"Both operation descriptors preserve paired callbacks and report identity."
(ebox-spi-test--reset-runtime-state)
@ -447,6 +407,161 @@
(should (equal (with-current-buffer buffer (buffer-string)) "")))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-initial-activation-failure-cleans-only-owned-observer ()
"Activation errors and throws preserve text and pre-existing observers."
(dolist (failure '(error throw))
(dolist (existing '(nil t))
(ert-info ((format "failure=%s existing-observer=%s" failure existing))
(let ((buffer (generate-new-buffer " *ebox-spi-activation-fault*"))
(input (ebox-test-box :key 'root (ebox-test-text "new")))
prior-observer cancelled result)
(unwind-protect
(progn
(with-current-buffer buffer
(insert (propertize "sentinel" 'face 'bold)))
(when existing
(setq prior-observer
(ebox-surface--ensure-tp-observer buffer)))
(cl-letf (((symbol-function 'activate-change-group)
(lambda (_group)
(if (eq failure 'throw)
(throw 'ebox-spi-activation 'activation-throw)
(error "activation-error"))))
((symbol-function 'cancel-change-group)
(lambda (_group) (setq cancelled t))))
(setq result
(catch 'ebox-spi-activation
(condition-case condition
(ebox-framework-spi-initial
buffer input #'ignore #'ignore)
(error condition)))))
(should (equal result
(if (eq failure 'throw) 'activation-throw
'(error "activation-error"))))
(should-not cancelled)
(should-not (ebox-surface-buffer-mounted-p buffer))
(should-not (gethash buffer ebox--buffer-render-state-table))
(with-current-buffer buffer
(should (eq ebox-surface--tp-observer prior-observer))
(should (equal-including-properties
(buffer-string)
(propertize "sentinel" 'face 'bold)))))
(when (buffer-live-p buffer) (kill-buffer buffer))))))))
(ert-deftest ebox-spi-initial-failure-restores-exact-editor-custody ()
"A failed initial mount preserves editor identities, bounds, and history."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-spi-editor-custody*"))
(input (ebox-test-box :key 'root (ebox-test-text "committed")))
overlay left-marker right-marker snapshot captured)
(unwind-protect
(progn
(with-current-buffer buffer
(buffer-enable-undo)
(insert (propertize "sentinel" 'face 'bold))
(undo-boundary)
(goto-char 4)
(set-mark 2)
(setq mark-active t
overlay (make-overlay 2 6 buffer t t)
left-marker (copy-marker 3 nil)
right-marker (copy-marker 5 t))
(overlay-put overlay 'ebox-test-property '(owned value))
(narrow-to-region 2 7)
(set-buffer-modified-p nil)
(setq snapshot
(list
:contents
(save-restriction
(widen)
(buffer-substring (point-min) (point-max)))
:point (point)
:mark-marker (mark-marker)
:mark-position (mark t)
:mark-insertion-type
(marker-insertion-type (mark-marker))
:mark-active mark-active
:narrow-start (point-min)
:narrow-end (point-max)
:overlay-start (overlay-start overlay)
:overlay-end (overlay-end overlay)
:overlay-properties (overlay-properties overlay)
:left-position (marker-position left-marker)
:left-insertion-type (marker-insertion-type left-marker)
:right-position (marker-position right-marker)
:right-insertion-type (marker-insertion-type right-marker)
:undo-list (copy-tree buffer-undo-list)
:modified-p (buffer-modified-p))))
(condition-case condition
(ebox-framework-spi-initial
buffer input
(lambda (_report) (error "editor custody primary"))
#'ignore)
(error (setq captured condition)))
(should (equal captured '(error "editor custody primary")))
(should-not (ebox-surface-buffer-mounted-p buffer))
(should-not (ebox-surface-buffer-observer buffer))
(with-current-buffer buffer
(should
(equal (save-restriction
(widen)
(buffer-substring (point-min) (point-max)))
(plist-get snapshot :contents)))
(should (= (point) (plist-get snapshot :point)))
(should (eq (mark-marker) (plist-get snapshot :mark-marker)))
(should (= (mark t) (plist-get snapshot :mark-position)))
(should (eq (marker-insertion-type (mark-marker))
(plist-get snapshot :mark-insertion-type)))
(should (eq mark-active (plist-get snapshot :mark-active)))
(should (= (point-min) (plist-get snapshot :narrow-start)))
(should (= (point-max) (plist-get snapshot :narrow-end)))
(should (eq (overlay-buffer overlay) buffer))
(should (= (overlay-start overlay)
(plist-get snapshot :overlay-start)))
(should (= (overlay-end overlay)
(plist-get snapshot :overlay-end)))
(should (equal (overlay-properties overlay)
(plist-get snapshot :overlay-properties)))
(should (= (marker-position left-marker)
(plist-get snapshot :left-position)))
(should (eq (marker-insertion-type left-marker)
(plist-get snapshot :left-insertion-type)))
(should (= (marker-position right-marker)
(plist-get snapshot :right-position)))
(should (eq (marker-insertion-type right-marker)
(plist-get snapshot :right-insertion-type)))
(should (equal buffer-undo-list (plist-get snapshot :undo-list)))
(should (eq (buffer-modified-p)
(plist-get snapshot :modified-p)))))
(when (overlayp overlay) (delete-overlay overlay))
(when (markerp left-marker) (set-marker left-marker nil))
(when (markerp right-marker) (set-marker right-marker nil))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-rollback-throw-cannot-stop-initial-cleanup ()
"A framework rollback throw escapes only after editor cleanup completes."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-spi-rollback-throw*"))
(input (ebox-test-box :key 'root (ebox-test-text "committed"))))
(unwind-protect
(progn
(with-current-buffer buffer (insert "before"))
(should
(eq
(catch 'ebox-spi-rollback-escape
(ebox-framework-spi-initial
buffer input
(lambda (_report) (error "primary before rollback throw"))
(lambda (_report)
(throw 'ebox-spi-rollback-escape 'rollback-escaped)))
'not-escaped)
'rollback-escaped))
(should-not (ebox-surface-buffer-mounted-p buffer))
(should-not (ebox-surface-buffer-observer buffer))
(should (equal "before"
(with-current-buffer buffer (buffer-string)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-update-rolls-the-same-report-back ()
"A later TP failure restores update state through the v2 operation."
(ebox-spi-test--reset-runtime-state)

View File

@ -16,6 +16,13 @@
(require 'tp-surface)
(require 'ebox-native-reflow)
(defmacro ebox-surface-test--with-elisp-backend (&rest body)
"Run BODY with the explicit Elisp rendering backend."
(declare (indent 0) (debug t))
`(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
(lambda () nil)))
,@body))
(defun ebox-surface-test--reset-render-state ()
"Reset render identities and side tables used by projection tests."
(setq ebox--region-id-counter 0
@ -1265,7 +1272,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-viewport-reflow-reuses-retained-node-subtree ()
"A safe viewport reflow should reuse the retained TP node topology."
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-surface-viewport-reflow*"))
(ebox-surface-test--with-elisp-backend
(let ((buffer (generate-new-buffer " *ebox-surface-viewport-reflow*"))
(ensured-node-count 0)
(runtime-index-count 0)
(original-ensure (symbol-function 'ebox-surface--ensure-node-tree))
@ -1332,7 +1340,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
:surface-object)))
(should (equal-including-properties actual expected)))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(kill-buffer buffer))))))
(ert-deftest ebox-viewport-resize-retains-native-frame-continuity ()
"A topology-stable resize should keep the committed native frame session."
@ -1814,7 +1822,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-viewport-reflow-reuses-stable-active-stylesheet ()
"A viewport change should retain styles only while their signature is stable."
(ebox-surface-test--reset-render-state)
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
(ebox-surface-test--with-elisp-backend
(let ((ebox-style-stylesheet (ecss-stylesheet-create))
(buffer (generate-new-buffer " *ebox-viewport-cascade*"))
(ensured-node-count 0)
(original-ensure (symbol-function 'ebox-surface--ensure-node-tree)))
@ -1855,12 +1864,13 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should-not (plist-get report :projection-kind))
(should (plist-get report :runtime-published))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(kill-buffer buffer))))))
(ert-deftest ebox-viewport-reflow-retains-stable-inline-inheritance ()
"Stable inherited Text style should retain its viewport projection exactly."
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-viewport-inheritance*"))
(ebox-surface-test--with-elisp-backend
(let ((buffer (generate-new-buffer " *ebox-viewport-inheritance*"))
(ensured-node-count 0)
(original-ensure (symbol-function 'ebox-surface--ensure-node-tree)))
(unwind-protect
@ -1894,7 +1904,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should (plist-get state :cascade-required-p))
(should (equal-including-properties actual expected))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(kill-buffer buffer))))))
(ert-deftest ebox-text-style-owns-local-cascade-state ()
"Persist an independent style owner state for canonical Text."
@ -1955,7 +1965,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-viewport-reflow-falls-back-for-scroll-and-visible-overflow ()
"Nested scroll state and visible overflow must not enter retained reflow."
(dolist (fixture
(ebox-surface-test--with-elisp-backend
(dolist (fixture
(list
(cons 'nested-scroll
(lambda ()
@ -1992,7 +2003,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should-not (plist-get report :projection-kind))
(should (plist-get report :runtime-published))))
(when (buffer-live-p buffer)
(kill-buffer buffer))))))
(kill-buffer buffer)))))))
(ert-deftest ebox-viewport-reflow-retains-viewport-dependent-root-scroll ()
"A sole root scroll owner may reflow its own viewport-dependent content."
@ -2037,7 +2048,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(ert-deftest ebox-viewport-reflow-supports-height-and-both-axis-resize ()
"Retained viewport reflow should cover height-only and two-axis changes."
(dolist (case
(ebox-surface-test--with-elisp-backend
(dolist (case
'((height 120 3 120 5 height)
(both 160 3 240 5 both)))
(pcase-let ((`(,name ,old-width ,old-height ,new-width ,new-height ,axes)
@ -2077,7 +2089,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(line-end-position)))
new-width)))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))))
(kill-buffer buffer))))))))
(ert-deftest ebox-viewport-reflow-rolls-back-after-publication-failure ()
"A failed retained viewport publication must restore its old generation."