diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d41128..2a5cbe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 620a67c..fd91e7b 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -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 publication;handle、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 plan;diff 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。 diff --git a/Makefile b/Makefile index e36b8f0..a60f46d 100644 --- a/Makefile +++ b/Makefile @@ -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)" diff --git a/README.md b/README.md index e747c41..644860e 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/README.zh-CN.md b/README.zh-CN.md index becbafc..f2e3a58 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 ``` diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index 0839d32..1385621 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -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 diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index 2076b6e..da48302 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -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 schema;surface 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 preparation;batch 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 声明该可调用 + API;capability 缺失或格式错误会在加载时 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 diff --git a/ebox-spi.el b/ebox-spi.el index 50e066c..699dec5 100644 --- a/ebox-spi.el +++ b/ebox-spi.el @@ -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 diff --git a/ebox-surface.el b/ebox-surface.el index f23c4fb..0da7d3f 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -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.") diff --git a/ebox.el b/ebox.el index b531325..a0eff1f 100644 --- a/ebox.el +++ b/ebox.el @@ -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. diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index f89473c..73f8891 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -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." diff --git a/tests/ebox-docs-contract-tests.el b/tests/ebox-docs-contract-tests.el index e0b8588..1b90c38 100644 --- a/tests/ebox-docs-contract-tests.el +++ b/tests/ebox-docs-contract-tests.el @@ -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") diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index f9af626..016c30b 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -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)) diff --git a/tests/ebox-spi-tests.el b/tests/ebox-spi-tests.el index cc26598..b48e5f1 100644 --- a/tests/ebox-spi-tests.el +++ b/tests/ebox-spi-tests.el @@ -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) diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index 0991ba2..7b918d8 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -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."