refactor: add pure patch planner for M2a E3
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
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:
parent
12ce14d43a
commit
a19672a25e
@ -22,7 +22,7 @@ node tree
|
||||
-> pure TP surface plan
|
||||
```
|
||||
|
||||
The owners are `ebox-child-range.el` for immutable child-sequence and key indexes, `ebox-tree.el` for Ebox identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-render-context.el` for the candidate/materialization input port, `ebox-surface.el` for pure TP plan projection and retained publication, and `ebox-incremental.el` for dirty-owner planning. `ebox-layout.el` never loads or calls the surface/TP layer; the facade wires surface operations into its validated render-context port. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary.
|
||||
The owners are `ebox-child-range.el` for immutable child-sequence and key indexes, `ebox-tree.el` for Ebox identity and traversal, `ebox-style.el` for style semantics, `ebox-measure.el` for display-sensitive measurement, `ebox-layout.el`/`ebox-flex.el`/`ebox-grid.el` for geometry, `ebox-fragment.el` for fragment facts, `ebox-render-context.el` for the candidate/materialization input port, `ebox-patch-plan.el` for pure operation-antichain artifacts, `ebox-surface.el` for pure TP plan projection and retained publication, and `ebox-incremental.el` for dirty classification and live-fact adaptation. `ebox-layout.el` never loads or calls the surface/TP layer; the facade wires surface operations into its validated render-context port. The patch planner never reads a buffer or surface; its incremental adapter supplies immutable parent facts and tentative operations. `ebox-buffer-backend.el` only builds and reshapes propertized render strings; all public live buffer publication routes through the TP surface boundary.
|
||||
|
||||
## Public boundary
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
|
||||
-> 纯 TP surface plan
|
||||
```
|
||||
|
||||
各层 owner 是:`ebox-child-range.el` 负责不可变 child sequence 与 key index,`ebox-tree.el` 负责 Ebox identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-render-context.el` 负责 candidate/materialization 输入 port,`ebox-surface.el` 负责纯 TP plan 投影与 retained 发布,`ebox-incremental.el` 负责 dirty owner 规划。`ebox-layout.el` 不再 load 或调用 surface/TP 层;门面把 surface operation 接入已校验的 render-context port。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string;所有公共 live buffer 发布路径都经过 TP surface 边界。
|
||||
各层 owner 是:`ebox-child-range.el` 负责不可变 child sequence 与 key index,`ebox-tree.el` 负责 Ebox identity 与遍历,`ebox-style.el` 负责样式语义,`ebox-measure.el` 负责 display 敏感测量,`ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` 负责几何,`ebox-fragment.el` 负责 fragment 事实,`ebox-render-context.el` 负责 candidate/materialization 输入 port,`ebox-patch-plan.el` 负责纯 operation-antichain artifact,`ebox-surface.el` 负责纯 TP plan 投影与 retained 发布,`ebox-incremental.el` 负责 dirty 分类与 live fact 适配。`ebox-layout.el` 不再 load 或调用 surface/TP 层;门面把 surface operation 接入已校验的 render-context port。patch planner 不读取 buffer 或 surface;incremental adapter 只传入不可变 parent fact 与 tentative operation。`ebox-buffer-backend.el` 只构造和整形带文本属性的 render string;所有公共 live buffer 发布路径都经过 TP surface 边界。
|
||||
|
||||
## 公共边界
|
||||
|
||||
|
||||
10
Makefile
10
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 state-contract-tests layout-boundary-tests layout-boundary-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 state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance 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
|
||||
|
||||
@ -25,7 +25,7 @@ compile:
|
||||
rm -f *.elc tests/*.elc scripts/*.elc
|
||||
$(EMACS_BATCH) --eval '(setq byte-compile-error-on-warn t byte-compile-warnings (quote (not obsolete)))' -l ebox.el --eval '(ebox-byte-compile)'
|
||||
|
||||
test: 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 state-contract-tests layout-boundary-tests docs-contract-tests ci-contract-tests
|
||||
test: 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 state-contract-tests layout-boundary-tests patch-plan-tests docs-contract-tests ci-contract-tests
|
||||
|
||||
source-tests:
|
||||
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
|
||||
@ -72,6 +72,12 @@ layout-boundary-tests:
|
||||
layout-boundary-performance:
|
||||
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-layout-boundary-batch
|
||||
|
||||
patch-plan-tests:
|
||||
$(EMACS_TEST) -l tests/ebox-patch-plan-tests.el -f ert-run-tests-batch-and-exit
|
||||
|
||||
patch-plan-performance:
|
||||
$(EMACS_TEST) -l tests/ebox-patch-plan-tests.el --eval "(ert-run-tests-batch-and-exit 'ebox-patch-plan-performance-bounds-parent-walks)"
|
||||
|
||||
performance-evaluator: surface-tests ebox-commit-tests
|
||||
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
|
||||
| `ebox-grid.el` | Tracks, implicit tracks, fractions, minmax/repeat, gap, placement, span, and alignment. |
|
||||
| `ebox-surface.el` | Candidate identity, projection to TP surface plans, retained mount/update, and the rollback-capable Ebox runtime-state participant. |
|
||||
| `ebox-buffer-backend.el` | Propertized render-string construction, display spaces/borders, and existing-slot shaping. |
|
||||
| `ebox-patch-plan.el` | Pure tentative-operation antichain planning from immutable parent facts. |
|
||||
| `ebox-incremental.el` | Runtime state, snapshots, dirty planning, owner escalation, pure commit preparation, and reports. |
|
||||
| `ebox-dsl.el` | Data-oriented `.ebox` forms and lowering to public nodes. |
|
||||
| `ebox-selector.el` | CSS-like string parsing to ECSS structured selector ASTs, indexed candidate lookup, and tree/runtime query handles. |
|
||||
@ -38,7 +39,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
|
||||
|
||||
The package intentionally does not include application Components, UI controls, reactive data, or a playground implementation. Those are sibling-package responsibilities.
|
||||
|
||||
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-tests.el`, `tests/ebox-layout-boundary-tests.el`, `tests/ebox-child-range-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, `scripts/ebox-visual-check.el`, and `scripts/ebox-performance-evaluator.el`.
|
||||
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-tests.el`, `tests/ebox-layout-boundary-tests.el`, `tests/ebox-patch-plan-tests.el`, `tests/ebox-child-range-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, `scripts/ebox-visual-check.el`, and `scripts/ebox-performance-evaluator.el`.
|
||||
|
||||
## Runtime model
|
||||
|
||||
@ -69,6 +70,7 @@ Caller-owned Source Tree
|
||||
| Fragment/Snapshot | `ebox-fragment.el`, `ebox-incremental.el` | Source parsing or identity allocation. |
|
||||
| Surface Projection and Runtime Publication | `ebox-surface.el` plus public TP surface APIs | Ebox layout decisions or generic diff execution. |
|
||||
| Dirty/Patch Semantics | `ebox-incremental.el` | Raw measurement or TP buffer writes. |
|
||||
| Pure Patch Artifact Plan | `ebox-patch-plan.el` | Buffer/surface reads or publication. |
|
||||
| Generic Surface Diff/Commit | TP | Ebox geometry, dirty policy, or application state. |
|
||||
| Render-String Backend | `ebox-buffer-backend.el` | Live buffer writes, retained markers, style semantics, or application state. |
|
||||
|
||||
@ -85,6 +87,7 @@ Caller-owned Source Tree
|
||||
- `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.
|
||||
- `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-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.
|
||||
- `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.
|
||||
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
|
||||
@ -119,6 +122,8 @@ make flex-tests
|
||||
make state-contract-tests
|
||||
make layout-boundary-tests
|
||||
make layout-boundary-performance
|
||||
make patch-plan-tests
|
||||
make patch-plan-performance
|
||||
make docs-contract-tests
|
||||
make ci-contract-tests
|
||||
make performance-evaluator
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
| `ebox-grid.el` | 轨道、隐式轨道、分数、minmax/repeat、gap、placement、span 和对齐。 |
|
||||
| `ebox-surface.el` | 候选 identity、向 TP surface plan 的投影、retained mount/update,以及支持 rollback 的 Ebox runtime-state participant。 |
|
||||
| `ebox-buffer-backend.el` | 带文本属性的渲染字符串构造、display space/border 与既有 slot 整形。 |
|
||||
| `ebox-patch-plan.el` | 从不可变 parent fact 生成 tentative operation antichain 的纯规划。 |
|
||||
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、纯 commit 准备和报告。 |
|
||||
| `ebox-dsl.el` | 数据型 `.ebox` form,以及向公共节点的 lowering。 |
|
||||
| `ebox-selector.el` | 把 CSS-like 字符串解析为 ECSS structured selector AST,利用索引缩小候选,并返回 tree/runtime query handle。 |
|
||||
@ -38,7 +39,7 @@
|
||||
|
||||
本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界;Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。
|
||||
|
||||
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-tests.el`、`tests/ebox-layout-boundary-tests.el`、`tests/ebox-child-range-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el`、`scripts/ebox-visual-check.el` 和 `scripts/ebox-performance-evaluator.el`。
|
||||
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-tests.el`、`tests/ebox-layout-boundary-tests.el`、`tests/ebox-patch-plan-tests.el`、`tests/ebox-child-range-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el`、`scripts/ebox-visual-check.el` 和 `scripts/ebox-performance-evaluator.el`。
|
||||
|
||||
## 运行时模型
|
||||
|
||||
@ -69,6 +70,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
||||
| Fragment/Snapshot | `ebox-fragment.el`、`ebox-incremental.el` | Source parsing 或 identity 分配。 |
|
||||
| Surface 投影与 Runtime 发布 | `ebox-surface.el` 加公共 TP surface API | Ebox 布局决策或通用 diff 执行。 |
|
||||
| Dirty/Patch 语义 | `ebox-incremental.el` | 原始测量或 TP buffer 写入。 |
|
||||
| 纯 Patch Artifact Plan | `ebox-patch-plan.el` | Buffer/surface 读取或发布。 |
|
||||
| 通用 Surface Diff/Commit | TP | Ebox 几何、dirty 策略或应用状态。 |
|
||||
| 渲染字符串 Backend | `ebox-buffer-backend.el` | Live buffer 写入、retained marker、样式语义或应用状态。 |
|
||||
|
||||
@ -85,6 +87,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
|
||||
- `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。
|
||||
- `ebox-layout.el` 不再 load 或调用 `ebox-surface.el`/TP。candidate isolation、继承 cascade 判定和一次性 materialization 只能通过 `ebox-render-context.el` 拥有的已校验 port 进入 layout;`ebox.el` 在两侧加载后接线 surface 实现。
|
||||
- `ebox-patch-plan.el` 只消费 tentative operation plist 与不可变 generation parent table,并返回 deterministic plan artifact;所有 live fact 准备仍由 incremental adapter 拥有。E3 默认使用 pure route,同时保留显式 `legacy` 与执行等价检查的 `shadow` route;三条 route 都不能发布。
|
||||
- `owner-rerender` 范围大于 `span-patch`,`span-patch` 大于 `paint-patch`。
|
||||
- Buffer 坐标属于生成它的 generation,变更后必须重新获取。
|
||||
- Grid 使用普通测量与渲染流水线;native reflow 可以拒绝不适合的树并回退到 Elisp,正确性不变。
|
||||
@ -117,6 +120,8 @@ make flex-tests
|
||||
make state-contract-tests
|
||||
make layout-boundary-tests
|
||||
make layout-boundary-performance
|
||||
make patch-plan-tests
|
||||
make patch-plan-performance
|
||||
make docs-contract-tests
|
||||
make ci-contract-tests
|
||||
make performance-evaluator
|
||||
|
||||
@ -7,7 +7,7 @@ This document defines the low-level publication contract. It is independent of E
|
||||
1. The caller owns the fresh source tree.
|
||||
2. `ebox-tree.el` owns identity, keys, traversal, and snapshots.
|
||||
3. `ebox-style.el`, `ebox-measure.el`, and the layout modules own normalization, measurement, and geometry.
|
||||
4. `ebox-incremental.el` owns dirty classification, patch planning, pure declarative commit preparation, and Ebox report semantics.
|
||||
4. `ebox-incremental.el` owns dirty classification, live-fact adaptation, pure declarative commit preparation, and Ebox report semantics; `ebox-patch-plan.el` reduces tentative operations using only immutable parent facts.
|
||||
5. `ebox-surface.el` projects retained candidates and joins Ebox runtime-state publication to the TP transaction.
|
||||
6. TP owns generic retained-surface reconciliation, mount/index state, all live buffer diff execution, revision changes, and rollback for initial mount, declarative commit, handle update, viewport/theme update, and scroll update.
|
||||
7. `ebox-buffer-backend.el` only builds and reshapes propertized render strings. It does not own live markers or a buffer mutation executor.
|
||||
@ -33,7 +33,7 @@ The candidate is either fully published or discarded. A failed render, TP write,
|
||||
|
||||
## Patch order
|
||||
|
||||
The planner prefers `paint-patch`, then `span-patch`, then `owner-rerender`, and finally `root-rerender` when geometry or identity makes a smaller operation unsafe. These names describe Ebox semantic owner scope; TP alone computes and executes the physical text/property diff. A patch must not silently widen its semantic scope. Reports preserve the Ebox strategy and planned scope while separately recording TP's actual surface operations and revision.
|
||||
The planner prefers `paint-patch`, then `span-patch`, then `owner-rerender`, and finally `root-rerender` when geometry or identity makes a smaller operation unsafe. The incremental adapter derives tentative owners from live generation facts; the pure patch port then receives only those artifacts and the immutable parent table. These names describe Ebox semantic owner scope; TP alone computes and executes the physical text/property diff. A patch must not silently widen its semantic scope. Reports preserve the Ebox strategy and planned scope while separately recording TP's actual surface operations and revision.
|
||||
|
||||
## Strict retained viewport reflow
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
1. 调用者拥有新的 source tree。
|
||||
2. `ebox-tree.el` 拥有 identity、key、遍历和 snapshot。
|
||||
3. `ebox-style.el`、`ebox-measure.el` 与布局模块拥有归一化、测量和几何。
|
||||
4. `ebox-incremental.el` 拥有 dirty 分类、patch 规划、纯声明式 commit 准备和 Ebox 报告语义。
|
||||
4. `ebox-incremental.el` 拥有 dirty 分类、live fact 适配、纯声明式 commit 准备和 Ebox 报告语义;`ebox-patch-plan.el` 只用不可变 parent fact 归并 tentative operation。
|
||||
5. `ebox-surface.el` 投影 retained candidate,并让 Ebox runtime-state 发布加入 TP transaction。
|
||||
6. TP 拥有首次 mount、声明式 commit、handle 更新、viewport/theme 更新和 scroll 更新的通用 retained-surface reconciliation、mount/index 状态、全部 live buffer diff 执行、revision 变更和 rollback。
|
||||
7. `ebox-buffer-backend.el` 只构造和整形带文本属性的渲染字符串,不拥有 live marker 或 buffer mutation executor。
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
## Patch 顺序
|
||||
|
||||
规划器优先使用 `paint-patch`,再使用 `span-patch`、`owner-rerender`,最后在几何或 identity 使小操作不安全时使用 `root-rerender`。这些名称描述的是 Ebox 的语义 owner 范围;只有 TP 负责计算并执行物理文本/属性 diff。Patch 不得静默扩大语义范围。报告保留 Ebox strategy 与 planned scope,同时单独记录 TP 的实际 surface operation 和 revision。
|
||||
规划器优先使用 `paint-patch`,再使用 `span-patch`、`owner-rerender`,最后在几何或 identity 使小操作不安全时使用 `root-rerender`。incremental adapter 从 live generation fact 推导 tentative owner;pure patch port 随后只接收这些 artifact 与不可变 parent table。这些名称描述的是 Ebox 的语义 owner 范围;只有 TP 负责计算并执行物理文本/属性 diff。Patch 不得静默扩大语义范围。报告保留 Ebox strategy 与 planned scope,同时单独记录 TP 的实际 surface operation 和 revision。
|
||||
|
||||
## 严格 retained viewport reflow
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
(require 'ebox-tree)
|
||||
(require 'ebox-buffer-backend)
|
||||
(require 'ebox-render-context)
|
||||
(require 'ebox-patch-plan)
|
||||
|
||||
(defvar ebox-region-types)
|
||||
(declare-function ebox--scroll-state-region-ids-containing-node-ids
|
||||
@ -8168,9 +8169,12 @@ rendered into its retained slot and must pass the complete footprint proof."
|
||||
buffer (plist-get entry :node-id) entry)))
|
||||
render-dirty-set))
|
||||
|
||||
(defun ebox-incremental--declarative-tentative-patch-set
|
||||
(defvar ebox-incremental--patch-planner-route 'pure
|
||||
"Internal M2a patch planner route: `legacy', `pure', or `shadow'.")
|
||||
|
||||
(defun ebox-incremental--legacy-tentative-patch-set
|
||||
(buffer render-dirty-set)
|
||||
"Return merged smallest-owner candidates for RENDER-DIRTY-SET."
|
||||
"Return legacy merged owner candidates for RENDER-DIRTY-SET in BUFFER."
|
||||
(let ((ebox--patchable-owner-cache (make-hash-table :test 'equal))
|
||||
(ebox--runtime-ancestor-cache (make-hash-table :test 'equal))
|
||||
(ebox--flex-slot-safety-cache
|
||||
@ -8199,6 +8203,69 @@ rendered into its retained slot and must pass the complete footprint proof."
|
||||
buffer (ebox--patch-op-merge-index-result index)))))
|
||||
(ebox--dedupe-patch-owners buffer ops)))
|
||||
|
||||
(defun ebox-incremental--pure-tentative-patch-set
|
||||
(buffer render-dirty-set)
|
||||
"Return pure-port merged owner candidates for RENDER-DIRTY-SET in BUFFER."
|
||||
(let ((ebox--patchable-owner-cache (make-hash-table :test 'equal))
|
||||
(ebox--runtime-ancestor-cache (make-hash-table :test 'equal))
|
||||
(ebox--flex-slot-safety-cache
|
||||
(or ebox--flex-slot-safety-cache
|
||||
(make-hash-table :test 'equal)))
|
||||
(parent-table
|
||||
(plist-get (ebox--buffer-render-state buffer) :parent-table))
|
||||
ops)
|
||||
(unless (hash-table-p parent-table)
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :missing-parent-table buffer)))
|
||||
(ebox--with-layout-snapshot-detail-context buffer
|
||||
(if-let* ((coalesced-op
|
||||
(and (not
|
||||
(ebox-incremental--dirty-set-has-child-splice-p
|
||||
buffer render-dirty-set))
|
||||
(or (ebox-incremental--broad-dirty-op
|
||||
buffer render-dirty-set)
|
||||
(ebox-incremental--fixed-basis-flex-local-op
|
||||
buffer render-dirty-set)
|
||||
(ebox-incremental--local-reflow-op
|
||||
buffer render-dirty-set)))))
|
||||
(setq ops (list coalesced-op))
|
||||
(setq ops
|
||||
(mapcar
|
||||
(lambda (entry)
|
||||
(ebox-incremental--declarative-tentative-op buffer entry))
|
||||
render-dirty-set))
|
||||
(setq ops (ebox-patch-plan-merge-ops parent-table ops))
|
||||
(setq ops
|
||||
(ebox-incremental--coalesce-sibling-line-span-ops
|
||||
buffer ops))))
|
||||
(ebox-patch-plan-merge-ops parent-table ops)))
|
||||
|
||||
(defun ebox-incremental--declarative-tentative-patch-set
|
||||
(buffer render-dirty-set)
|
||||
"Return route-selected smallest-owner candidates for RENDER-DIRTY-SET."
|
||||
(pcase ebox-incremental--patch-planner-route
|
||||
('legacy
|
||||
(ebox-incremental--legacy-tentative-patch-set
|
||||
buffer render-dirty-set))
|
||||
('pure
|
||||
(ebox-incremental--pure-tentative-patch-set
|
||||
buffer render-dirty-set))
|
||||
('shadow
|
||||
(let ((legacy
|
||||
(ebox-incremental--legacy-tentative-patch-set
|
||||
buffer render-dirty-set))
|
||||
(pure
|
||||
(ebox-incremental--pure-tentative-patch-set
|
||||
buffer render-dirty-set)))
|
||||
(unless (equal legacy pure)
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :shadow-artifact-mismatch
|
||||
:legacy legacy :pure pure)))
|
||||
pure))
|
||||
(_
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :unknown-route ebox-incremental--patch-planner-route)))))
|
||||
|
||||
(defun ebox-incremental--declarative-next-owner-id
|
||||
(buffer owner-id dirty)
|
||||
"Return the next ancestor of OWNER-ID worth proving for DIRTY."
|
||||
|
||||
232
ebox-patch-plan.el
Normal file
232
ebox-patch-plan.el
Normal file
@ -0,0 +1,232 @@
|
||||
;;; ebox-patch-plan.el --- Pure Ebox patch artifact planning -*- lexical-binding: t; -*-
|
||||
|
||||
;; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Reduces tentative patch operations into one deterministic antichain. The
|
||||
;; planner consumes only operation plists and an immutable node parent table;
|
||||
;; it does not read a buffer, call a surface, or publish an artifact.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
|
||||
(define-error 'ebox-patch-plan-error "Invalid Ebox patch plan input")
|
||||
|
||||
(defconst ebox-patch-plan--dirty-kind-order
|
||||
'(paint span geometry placement structure)
|
||||
"Dirty kinds ordered from cheapest to most disruptive.")
|
||||
|
||||
(defun ebox-patch-plan--operation-strength (strategy)
|
||||
"Return structural dominance strength for patch STRATEGY."
|
||||
(pcase strategy
|
||||
('native-frame 4)
|
||||
('owner-rerender 3)
|
||||
('span-patch 2)
|
||||
('paint-patch 1)
|
||||
((or 'child-splice 'child-reorder) 0)
|
||||
(_ 0)))
|
||||
|
||||
(defun ebox-patch-plan--dirty-kind-strength (kind)
|
||||
"Return relative disruption strength for dirty KIND."
|
||||
(or (cl-position kind ebox-patch-plan--dirty-kind-order) -1))
|
||||
|
||||
(defun ebox-patch-plan--dirty-node-ids (dirty)
|
||||
"Return distinct source node ids recorded by DIRTY."
|
||||
(delete-dups
|
||||
(append (when-let* ((node-id (plist-get dirty :node-id)))
|
||||
(list node-id))
|
||||
(copy-sequence (or (plist-get dirty :node-ids) nil)))))
|
||||
|
||||
(defun ebox-patch-plan--merge-dirty (primary secondary)
|
||||
"Merge SECONDARY dirty provenance into PRIMARY without mutating either."
|
||||
(cond
|
||||
((null primary) (copy-sequence secondary))
|
||||
((null secondary) (copy-sequence primary))
|
||||
(t
|
||||
(let ((merged (copy-sequence primary)))
|
||||
(setq merged
|
||||
(plist-put
|
||||
merged :node-ids
|
||||
(delete-dups
|
||||
(append (ebox-patch-plan--dirty-node-ids primary)
|
||||
(ebox-patch-plan--dirty-node-ids secondary)))))
|
||||
(dolist (key '(:changed-keys :region-ids :old-region-ids
|
||||
:new-region-ids :impact-vector))
|
||||
(setq merged
|
||||
(plist-put
|
||||
merged key
|
||||
(delete-dups
|
||||
(append (copy-sequence (or (plist-get primary key) nil))
|
||||
(copy-sequence
|
||||
(or (plist-get secondary key) nil)))))))
|
||||
(when (or (plist-get primary :requires-owned-overflow-coverage)
|
||||
(plist-get secondary :requires-owned-overflow-coverage))
|
||||
(setq merged
|
||||
(plist-put merged :requires-owned-overflow-coverage t)))
|
||||
(when (> (ebox-patch-plan--dirty-kind-strength
|
||||
(plist-get secondary :dirty-kind))
|
||||
(ebox-patch-plan--dirty-kind-strength
|
||||
(plist-get primary :dirty-kind)))
|
||||
(setq merged
|
||||
(plist-put merged :dirty-kind
|
||||
(plist-get secondary :dirty-kind))))
|
||||
merged))))
|
||||
|
||||
(defun ebox-patch-plan--merge-op-dirty (op absorbed)
|
||||
"Return OP with ABSORBED's dirty provenance merged into a fresh plist."
|
||||
(let ((merged (copy-sequence op)))
|
||||
(plist-put merged :dirty
|
||||
(ebox-patch-plan--merge-dirty
|
||||
(plist-get op :dirty)
|
||||
(plist-get absorbed :dirty)))))
|
||||
|
||||
(defun ebox-patch-plan--merge-same-owner (left right)
|
||||
"Return the stronger same-owner op with merged dirty provenance."
|
||||
(let* ((left-strength
|
||||
(ebox-patch-plan--operation-strength (plist-get left :op)))
|
||||
(right-strength
|
||||
(ebox-patch-plan--operation-strength (plist-get right :op)))
|
||||
(left-dirty-strength
|
||||
(ebox-patch-plan--dirty-kind-strength
|
||||
(plist-get (plist-get left :dirty) :dirty-kind)))
|
||||
(right-dirty-strength
|
||||
(ebox-patch-plan--dirty-kind-strength
|
||||
(plist-get (plist-get right :dirty) :dirty-kind)))
|
||||
(left-wins
|
||||
(or (> left-strength right-strength)
|
||||
(and (= left-strength right-strength)
|
||||
(>= left-dirty-strength right-dirty-strength))))
|
||||
(winner (if left-wins left right))
|
||||
(loser (if left-wins right left)))
|
||||
(ebox-patch-plan--merge-op-dirty winner loser)))
|
||||
|
||||
(defun ebox-patch-plan--validate-op (op)
|
||||
"Signal unless OP is one complete tentative patch artifact."
|
||||
(unless (and (proper-list-p op)
|
||||
(plist-member op :op)
|
||||
(plist-member op :owner-id))
|
||||
(signal 'ebox-patch-plan-error (list :malformed-operation op)))
|
||||
op)
|
||||
|
||||
(defun ebox-patch-plan--ancestor-set (parent-table owner-id)
|
||||
"Return OWNER-ID's strict ancestors from immutable PARENT-TABLE."
|
||||
(let ((ancestors (make-hash-table :test #'equal))
|
||||
(seen (make-hash-table :test #'equal))
|
||||
(walk (gethash owner-id parent-table)))
|
||||
(while walk
|
||||
(when (gethash walk seen)
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :cyclic-parent-table :node-id walk)))
|
||||
(puthash walk t seen)
|
||||
(puthash walk t ancestors)
|
||||
(setq walk (gethash walk parent-table)))
|
||||
ancestors))
|
||||
|
||||
(defun ebox-patch-plan--make-index ()
|
||||
"Return fresh planner-local antichain merge state."
|
||||
(list :owner-table (make-hash-table :test #'equal)
|
||||
:ancestor-table (make-hash-table :test #'equal)
|
||||
:serial 0))
|
||||
|
||||
(defun ebox-patch-plan--index-insert (index entry)
|
||||
"Register planner ENTRY in INDEX."
|
||||
(puthash (plist-get (aref entry 0) :owner-id)
|
||||
entry (plist-get index :owner-table))
|
||||
(let ((ancestor-table (plist-get index :ancestor-table)))
|
||||
(maphash
|
||||
(lambda (ancestor-id _present)
|
||||
(push entry (gethash ancestor-id ancestor-table)))
|
||||
(aref entry 2))))
|
||||
|
||||
(defun ebox-patch-plan--index-remove (index entry)
|
||||
"Remove planner ENTRY from INDEX."
|
||||
(remhash (plist-get (aref entry 0) :owner-id)
|
||||
(plist-get index :owner-table))
|
||||
(let ((ancestor-table (plist-get index :ancestor-table)))
|
||||
(maphash
|
||||
(lambda (ancestor-id _present)
|
||||
(puthash ancestor-id
|
||||
(delq entry (gethash ancestor-id ancestor-table))
|
||||
ancestor-table))
|
||||
(aref entry 2))))
|
||||
|
||||
(defun ebox-patch-plan--index-add (parent-table index candidate)
|
||||
"Merge CANDIDATE into INDEX using immutable PARENT-TABLE ancestry."
|
||||
(setq candidate (ebox-patch-plan--validate-op candidate))
|
||||
(let* ((owner-table (plist-get index :owner-table))
|
||||
(owner-id (plist-get candidate :owner-id))
|
||||
(same-owner (gethash owner-id owner-table)))
|
||||
(when same-owner
|
||||
(setq candidate
|
||||
(ebox-patch-plan--merge-same-owner
|
||||
(aref same-owner 0) candidate))
|
||||
(ebox-patch-plan--index-remove index same-owner))
|
||||
(let* ((ancestors
|
||||
(ebox-patch-plan--ancestor-set parent-table owner-id))
|
||||
dominator)
|
||||
(maphash
|
||||
(lambda (ancestor-id _present)
|
||||
(when-let* ((entry (gethash ancestor-id owner-table)))
|
||||
(when (and (>= (ebox-patch-plan--operation-strength
|
||||
(plist-get (aref entry 0) :op))
|
||||
(ebox-patch-plan--operation-strength 'span-patch))
|
||||
(or (null dominator)
|
||||
(< (aref entry 1) (aref dominator 1))))
|
||||
(setq dominator entry))))
|
||||
ancestors)
|
||||
(if dominator
|
||||
(aset dominator 0
|
||||
(ebox-patch-plan--merge-op-dirty
|
||||
(aref dominator 0) candidate))
|
||||
(when (>= (ebox-patch-plan--operation-strength
|
||||
(plist-get candidate :op))
|
||||
(ebox-patch-plan--operation-strength 'span-patch))
|
||||
(dolist (entry
|
||||
(sort
|
||||
(copy-sequence
|
||||
(gethash owner-id (plist-get index :ancestor-table)))
|
||||
(lambda (left right)
|
||||
(< (aref left 1) (aref right 1)))))
|
||||
(setq candidate
|
||||
(ebox-patch-plan--merge-op-dirty
|
||||
candidate (aref entry 0)))
|
||||
(ebox-patch-plan--index-remove index entry)))
|
||||
(let ((entry (vector candidate
|
||||
(plist-get index :serial)
|
||||
ancestors)))
|
||||
(plist-put index :serial (1+ (plist-get index :serial)))
|
||||
(ebox-patch-plan--index-insert index entry))))))
|
||||
|
||||
(defun ebox-patch-plan--index-result (index)
|
||||
"Return INDEX's surviving operations in stable insertion order."
|
||||
(let (entries)
|
||||
(maphash (lambda (_owner-id entry) (push entry entries))
|
||||
(plist-get index :owner-table))
|
||||
(mapcar (lambda (entry) (aref entry 0))
|
||||
(sort entries
|
||||
(lambda (left right)
|
||||
(< (aref left 1) (aref right 1)))))))
|
||||
|
||||
(defun ebox-patch-plan-merge-ops (parent-table operations)
|
||||
"Return the pure deterministic antichain for tentative OPERATIONS.
|
||||
|
||||
PARENT-TABLE maps a node id to its parent id and is treated as immutable.
|
||||
OPERATIONS are patch artifact plists. This function mutates neither input and
|
||||
has no publication capability."
|
||||
(unless (hash-table-p parent-table)
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :malformed-parent-table parent-table)))
|
||||
(unless (proper-list-p operations)
|
||||
(signal 'ebox-patch-plan-error
|
||||
(list :malformed-operations operations)))
|
||||
(let ((index (ebox-patch-plan--make-index)))
|
||||
(dolist (operation operations)
|
||||
(ebox-patch-plan--index-add parent-table index operation))
|
||||
(ebox-patch-plan--index-result index)))
|
||||
|
||||
(provide 'ebox-patch-plan)
|
||||
|
||||
;;; ebox-patch-plan.el ends here
|
||||
3
ebox.el
3
ebox.el
@ -30,7 +30,7 @@
|
||||
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
|
||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
||||
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
|
||||
"ebox-buffer-backend.el" "ebox-incremental.el"
|
||||
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
|
||||
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
|
||||
"ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||
"ebox-native-reflow.el")
|
||||
@ -59,6 +59,7 @@
|
||||
(require 'ebox-grid)
|
||||
(require 'ebox-canonical)
|
||||
(require 'ebox-buffer-backend)
|
||||
(require 'ebox-patch-plan)
|
||||
(require 'ebox-incremental)
|
||||
(require 'ebox-surface)
|
||||
(require 'ebox-viewport)
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"state-contract-tests:"
|
||||
"layout-boundary-tests:"
|
||||
"layout-boundary-performance:"
|
||||
"patch-plan-tests:"
|
||||
"patch-plan-performance:"
|
||||
"docs-contract-tests:"
|
||||
"ci-contract-tests:" "native-rust-tests:"
|
||||
"native-build:" "performance-evaluator:" "diff-check:"))
|
||||
@ -35,6 +37,7 @@
|
||||
(should (string-match-p "byte-compile-error-on-warn" source))
|
||||
(should (string-match-p "ebox-byte-compile" source))
|
||||
(should (string-match-p "ebox-native-reflow.el" ebox))
|
||||
(should (string-match-p "ebox-patch-plan.el" ebox))
|
||||
(should (string-match-p "ebox-child-range.el" ebox))
|
||||
(should-not (string-match-p "ebox-playground.el" ebox))))
|
||||
|
||||
|
||||
@ -28,10 +28,12 @@
|
||||
"ebox-child-range.el" "ebox-tree.el"
|
||||
"ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el"
|
||||
"ebox-layout.el" "ebox-flex.el" "ebox-grid.el"
|
||||
"ebox-buffer-backend.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el"
|
||||
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
|
||||
"ebox-surface.el" "ebox-dsl.el"
|
||||
"ebox-selector.el" "ebox-native-reflow.el"
|
||||
"tests/ebox-core-render-tests.el" "tests/ebox-state-contract-tests.el"
|
||||
"tests/ebox-layout-boundary-tests.el"
|
||||
"tests/ebox-patch-plan-tests.el"
|
||||
"tests/ebox-child-range-tests.el" "tests/ebox-grid-tests.el"
|
||||
"tests/ebox-commit-tests.el" "tests/ebox-surface-tests.el"
|
||||
"tests/ebox-dsl-tests.el" "tests/ebox-flex-tests.el"
|
||||
@ -50,6 +52,7 @@
|
||||
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
|
||||
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"
|
||||
"layout-boundary-tests" "layout-boundary-performance"
|
||||
"patch-plan-tests" "patch-plan-performance"
|
||||
"docs-contract-tests" "ci-contract-tests" "performance-evaluator"
|
||||
"visual-check"
|
||||
"native-rust-tests" "native-build" "package-lint" "diff-check")
|
||||
|
||||
@ -56,7 +56,8 @@
|
||||
ebox-node-factory
|
||||
ebox-fragment ebox-layout ebox-flex ebox-grid
|
||||
ebox-canonical
|
||||
ebox-buffer-backend ebox-incremental
|
||||
ebox-buffer-backend ebox-patch-plan
|
||||
ebox-incremental
|
||||
ebox-surface ebox-viewport ebox-dsl
|
||||
ebox-selector))
|
||||
(should (featurep feature))))
|
||||
@ -138,7 +139,8 @@
|
||||
"ebox-tree.el" "ebox-measure.el"
|
||||
"ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el"
|
||||
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
|
||||
"ebox-buffer-backend.el" "ebox-incremental.el"
|
||||
"ebox-buffer-backend.el" "ebox-patch-plan.el"
|
||||
"ebox-incremental.el"
|
||||
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
|
||||
"ebox-dsl.el" "ebox-selector.el" "ebox.el"
|
||||
"ebox-native-reflow.el"))))
|
||||
|
||||
260
tests/ebox-patch-plan-tests.el
Normal file
260
tests/ebox-patch-plan-tests.el
Normal file
@ -0,0 +1,260 @@
|
||||
;;; ebox-patch-plan-tests.el --- M2a pure patch planner gates -*- lexical-binding: t; -*-
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
(require 'benchmark)
|
||||
(require 'ebox)
|
||||
(require 'ebox-patch-plan)
|
||||
(require 'ebox-fixtures)
|
||||
|
||||
(defconst ebox-patch-plan-test--root
|
||||
(expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name)))
|
||||
"Repository root used by patch planner tests.")
|
||||
|
||||
(defun ebox-patch-plan-test--read-forms (file)
|
||||
"Read and return every top-level form in repository FILE."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents (expand-file-name file ebox-patch-plan-test--root))
|
||||
(let (forms form)
|
||||
(condition-case nil
|
||||
(while t
|
||||
(setq form (read (current-buffer)))
|
||||
(push form forms))
|
||||
(end-of-file nil))
|
||||
(nreverse forms))))
|
||||
|
||||
(defun ebox-patch-plan-test--walk-symbols (form function)
|
||||
"Call FUNCTION for every symbol contained by FORM."
|
||||
(cond
|
||||
((symbolp form) (funcall function form))
|
||||
((consp form)
|
||||
(ebox-patch-plan-test--walk-symbols (car form) function)
|
||||
(ebox-patch-plan-test--walk-symbols (cdr form) function))
|
||||
((vectorp form)
|
||||
(mapc (lambda (item)
|
||||
(ebox-patch-plan-test--walk-symbols item function))
|
||||
form))))
|
||||
|
||||
(defun ebox-patch-plan-test--dirty (node-id kind keys)
|
||||
"Return one planner fixture for NODE-ID, KIND, and changed KEYS."
|
||||
(list :node-id node-id :dirty-kind kind :changed-keys keys))
|
||||
|
||||
(defun ebox-patch-plan-test--reset-runtime-state ()
|
||||
"Reset planner-visible global identities and compatibility projections."
|
||||
(setq ebox--region-id-counter 0
|
||||
ebox--runtime-node-id-counter 0)
|
||||
(dolist (table (list ebox--region-box-table ebox--scroll-global-state))
|
||||
(when (hash-table-p table) (clrhash table))))
|
||||
|
||||
(defun ebox-patch-plan-test--op (operation owner-id dirty)
|
||||
"Return one tentative OPERATION for OWNER-ID and DIRTY."
|
||||
(list :op operation :owner-id owner-id :dirty dirty))
|
||||
|
||||
(ert-deftest ebox-patch-plan-module-has-no-surface-or-publication-capability ()
|
||||
"The pure planner cannot inspect buffers or call publication APIs."
|
||||
(let (forbidden)
|
||||
(dolist (form (ebox-patch-plan-test--read-forms "ebox-patch-plan.el"))
|
||||
(ebox-patch-plan-test--walk-symbols
|
||||
form
|
||||
(lambda (symbol)
|
||||
(let ((name (symbol-name symbol)))
|
||||
(when (or (string-prefix-p "ebox-surface-" name)
|
||||
(string-prefix-p "tp-surface-" name)
|
||||
(string-prefix-p "tp-object-" name)
|
||||
(memq symbol '(with-current-buffer set-buffer
|
||||
insert delete-region
|
||||
put-text-property
|
||||
ebox--buffer-render-state)))
|
||||
(push symbol forbidden))))))
|
||||
(should-not (delete-dups forbidden))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-merges-one-deterministic-antichain ()
|
||||
"A stronger ancestor absorbs descendant dirt without input mutation."
|
||||
(let ((parents (make-hash-table :test #'equal))
|
||||
operations)
|
||||
(puthash 'branch 'root parents)
|
||||
(puthash 'leaf 'branch parents)
|
||||
(setq operations
|
||||
(list
|
||||
(ebox-patch-plan-test--op
|
||||
'paint-patch 'leaf
|
||||
(ebox-patch-plan-test--dirty 'leaf 'paint '(:color)))
|
||||
(ebox-patch-plan-test--op
|
||||
'span-patch 'branch
|
||||
(ebox-patch-plan-test--dirty 'branch 'geometry '(:content)))))
|
||||
(let ((before (copy-tree operations))
|
||||
(result (ebox-patch-plan-merge-ops parents operations)))
|
||||
(should (equal operations before))
|
||||
(should (= (length result) 1))
|
||||
(should (eq (plist-get (car result) :op) 'span-patch))
|
||||
(should (eq (plist-get (car result) :owner-id) 'branch))
|
||||
(should
|
||||
(equal (sort (copy-sequence
|
||||
(plist-get (plist-get (car result) :dirty) :node-ids))
|
||||
(lambda (left right)
|
||||
(string< (symbol-name left) (symbol-name right))))
|
||||
'(branch leaf)))
|
||||
(should
|
||||
(equal (sort (copy-sequence
|
||||
(plist-get (plist-get (car result) :dirty)
|
||||
:changed-keys))
|
||||
(lambda (left right)
|
||||
(string< (symbol-name left) (symbol-name right))))
|
||||
'(:color :content))))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-rejects-malformed-and-cyclic-input ()
|
||||
"Invalid artifacts fail before any downstream publication can exist."
|
||||
(should-error (ebox-patch-plan-merge-ops nil nil)
|
||||
:type 'ebox-patch-plan-error)
|
||||
(let ((parents (make-hash-table :test #'equal)))
|
||||
(should-error (ebox-patch-plan-merge-ops parents '((:op paint-patch)))
|
||||
:type 'ebox-patch-plan-error)
|
||||
(puthash 'first 'second parents)
|
||||
(puthash 'second 'first parents)
|
||||
(should-error
|
||||
(ebox-patch-plan-merge-ops
|
||||
parents
|
||||
(list
|
||||
(ebox-patch-plan-test--op
|
||||
'paint-patch 'first
|
||||
(ebox-patch-plan-test--dirty 'first 'paint '(:color)))))
|
||||
:type 'ebox-patch-plan-error)))
|
||||
|
||||
(defun ebox-patch-plan-test--integration-fixture ()
|
||||
"Return a retained layout covering paint, geometry, and broad planning."
|
||||
(apply
|
||||
#'ebox-test-column
|
||||
:width '(360)
|
||||
(cl-loop for index below 12
|
||||
collect
|
||||
(ebox-test-box
|
||||
:key (intern (format "patch-plan-%d" index))
|
||||
(ebox-test-text (format "Item %02d" index))
|
||||
:width '(160) :height 1))))
|
||||
|
||||
(defun ebox-patch-plan-test--route-artifact (buffer dirty route)
|
||||
"Return BUFFER's planner artifact for DIRTY through ROUTE."
|
||||
(let ((ebox-incremental--patch-planner-route route))
|
||||
(ebox-incremental--declarative-tentative-patch-set
|
||||
buffer (copy-tree dirty))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-pure-route-matches-legacy-artifacts ()
|
||||
"Paint, geometry, and broad viewport inputs match the old planner exactly."
|
||||
(ebox-patch-plan-test--reset-runtime-state)
|
||||
(let ((buffer (generate-new-buffer " *ebox-m2a-e3-artifacts*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(let ((ebox-viewport-width 360))
|
||||
(ebox-render-to-buffer
|
||||
buffer (ebox-patch-plan-test--integration-fixture)))
|
||||
(let* ((root (ebox--buffer-root-node buffer))
|
||||
(children (ebox-tree-node-children root))
|
||||
(first-id (plist-get (nth 0 children) :node-id))
|
||||
(second-id (plist-get (nth 1 children) :node-id))
|
||||
(paint
|
||||
(list
|
||||
(ebox-patch-plan-test--dirty
|
||||
first-id 'paint '(:color))))
|
||||
(geometry
|
||||
(list
|
||||
(append
|
||||
(ebox-patch-plan-test--dirty
|
||||
second-id 'geometry '(:content))
|
||||
'(:old-signature (:content "Item 01" :width 160)
|
||||
:new-signature (:content "Item 01 changed" :width 160)))))
|
||||
(broad (ebox--viewport-dirty-set root)))
|
||||
(dolist (dirty (list paint geometry broad))
|
||||
(let ((legacy
|
||||
(ebox-patch-plan-test--route-artifact
|
||||
buffer dirty 'legacy))
|
||||
(pure
|
||||
(ebox-patch-plan-test--route-artifact
|
||||
buffer dirty 'pure)))
|
||||
(should (equal pure legacy))
|
||||
(should
|
||||
(equal
|
||||
(ebox-patch-plan-test--route-artifact buffer dirty 'shadow)
|
||||
legacy))))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-fault-cannot-publish-live-state ()
|
||||
"A pure planner fault leaves buffer text, revision, and client state intact."
|
||||
(ebox-patch-plan-test--reset-runtime-state)
|
||||
(let ((buffer (generate-new-buffer " *ebox-m2a-e3-fault*")))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
buffer (ebox-patch-plan-test--integration-fixture))
|
||||
(let* ((surface (ebox-surface--live-buffer-surface buffer))
|
||||
(state (tp-surface-client-state surface))
|
||||
(revision (tp-surface-revision surface))
|
||||
(contents
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max))))
|
||||
(root (plist-get state :root-node))
|
||||
(dirty (ebox--viewport-dirty-set root))
|
||||
(ebox-incremental--patch-planner-route 'pure))
|
||||
(cl-letf (((symbol-function 'ebox-patch-plan-merge-ops)
|
||||
(lambda (&rest _arguments)
|
||||
(error "Injected E3 planner fault"))))
|
||||
(should-error
|
||||
(ebox-incremental--declarative-tentative-patch-set
|
||||
buffer dirty)))
|
||||
(should (eq (tp-surface-client-state surface) state))
|
||||
(should (= (tp-surface-revision surface) revision))
|
||||
(should
|
||||
(equal-including-properties
|
||||
(with-current-buffer buffer
|
||||
(buffer-substring (point-min) (point-max)))
|
||||
contents))))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-feature-route-defaults-pure-and-keeps-legacy ()
|
||||
"E3 enables the pure route while retaining explicit rollback selection."
|
||||
(should (eq (default-value 'ebox-incremental--patch-planner-route) 'pure))
|
||||
(dolist (route '(legacy pure shadow))
|
||||
(should (memq route '(legacy pure shadow))))
|
||||
(let ((ebox-incremental--patch-planner-route 'unknown)
|
||||
(buffer (generate-new-buffer " *ebox-m2a-e3-route*")))
|
||||
(unwind-protect
|
||||
(should-error
|
||||
(ebox-incremental--declarative-tentative-patch-set
|
||||
buffer '((:node-id node :dirty-kind paint
|
||||
:changed-keys (:color))))
|
||||
:type 'ebox-patch-plan-error)
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest ebox-patch-plan-performance-bounds-parent-walks ()
|
||||
"A wide plan performs one bounded parent walk per tentative artifact."
|
||||
(let ((parents (make-hash-table :test #'equal))
|
||||
operations)
|
||||
(dotimes (index 512)
|
||||
(let ((node-id (intern (format "ebox-patch-perf-%d" index))))
|
||||
(puthash node-id 'root parents)
|
||||
(push
|
||||
(ebox-patch-plan-test--op
|
||||
'paint-patch node-id
|
||||
(ebox-patch-plan-test--dirty node-id 'paint '(:color)))
|
||||
operations)))
|
||||
(setq operations (nreverse operations))
|
||||
(let ((walks 0)
|
||||
(original (symbol-function 'ebox-patch-plan--ancestor-set))
|
||||
plan benchmark)
|
||||
(cl-letf (((symbol-function 'ebox-patch-plan--ancestor-set)
|
||||
(lambda (&rest arguments)
|
||||
(cl-incf walks)
|
||||
(apply original arguments))))
|
||||
(setq benchmark
|
||||
(benchmark-run
|
||||
1
|
||||
(setq plan
|
||||
(ebox-patch-plan-merge-ops parents operations)))))
|
||||
(should (= (length plan) 512))
|
||||
(should (= walks 512))
|
||||
(should (< (car benchmark) 2.0)))))
|
||||
|
||||
(provide 'ebox-patch-plan-tests)
|
||||
|
||||
;;; ebox-patch-plan-tests.el ends here
|
||||
Loading…
Reference in New Issue
Block a user