chore: freeze verified ebox baseline before C1b
Some checks are pending
CI / test (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run

This commit is contained in:
Kinneyzhang 2026-09-05 05:07:37 +08:00
parent 5236100add
commit f1f91468aa
30 changed files with 3145 additions and 421 deletions

View File

@ -2,22 +2,33 @@
All notable changes to Ebox are documented here.
## 2.0.0 (Unreleased)
## 2.0.1 (Unreleased)
### Added
- Direct dependencies on the independent ECSS 0.1.0 cascade engine and TP 1.0.0 retained/reactive text runtime.
- Direct dependencies on the independent ECSS 0.1.0 cascade engine and TP 1.0.1 retained/reactive text runtime.
- ECSS-backed property schemas, selectors, stylesheets, inheritance, cascade layers, custom properties, computed values, and provenance for Box, Flex, and Grid nodes.
- 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.
### Changed
- Package metadata now distinguishes builds with the public unmount/revision
boundary and SPI v2 compatibility surface from older 2.0.0 development
snapshots.
- 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.
- Region updates use opaque surface-scoped handles. The same declarative source may be mounted in multiple buffers without sharing live identity.
- A full declarative update that continues a committed native frame now computes
a complete selector-local style certificate before stable native eligibility.
Other full/local paths retain their existing O(changed) style work. Native
updates no longer commit replacement runtime nodes without computed styles,
and structural full-frame bootstrap remains available.
### Removed
@ -27,7 +38,7 @@ All notable changes to Ebox are documented here.
### Migration
- Install ECSS 0.1.0 and TP 1.0.0, in either order, before Ebox 2.0.0.
- Install ECSS 0.1.0 and TP 1.0.1, in either order, before Ebox 2.0.1.
- 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.

View File

@ -2,22 +2,31 @@
Ebox 的重要变化记录在这里。
## 2.0.0(未发布)
## 2.0.1(未发布)
### 新增
- 直接依赖独立的 ECSS 0.1.0 cascade engine 与 TP 1.0.0 retained/reactive text runtime。
- 直接依赖独立的 ECSS 0.1.0 cascade engine 与 TP 1.0.1 retained/reactive text runtime。
- Box、Flex 与 Grid 节点统一使用 ECSS property schema、selector、stylesheet、inheritance、cascade layer、custom property、computed value 与 provenance。
- 通过 `ebox-region-resolve` 提供 surface-scoped region handle`ebox-render-to-buffer` 与 `ebox-display-buffer` 统一使用 retained TP publicationhandle、selector、viewport 与 scroll 更新加入同一原子事务。
- 增加 typography longhand 与继承 foreground 的投影;子盒子覆盖背景色时仍能保持文字可读。
- 增加公开、可嵌套的 framework render-burst 边界,覆盖 commit 前分配,与 Ebox commit 共用交互 deferred GC ownership并在 batch 边界不主动 GC 的情况下恢复原设置。
- 增加受支持的 `ebox-transaction-participant-route` 开关:默认使用 v2
structured TP registration显式旧 route 与旧 TP manifest 的完整 v1 adapter
fallback 继续保留。
### 变更
- 包 metadata 现在会区分具备 public unmount/revision 边界与 SPI v2
compatibility surface 的构建,旧的 2.0.0 development snapshot 不会再被误接受。
- ECSS 是唯一 selector matcher 与 cascade winner。Ebox 只把节点树适配为 ECSS subject解释 computed value 的测量、布局与绘制后果,并分类 layout impact。
- TP 是唯一 live buffer writer。Ebox 只生成 retained surface candidate 与语义 dirty-owner plandiff execution、marker-backed mount、revision、transaction 与 rollback 都属于 TP。
- Flex/Grid container 与 item declaration 和普通 box 一样进入同一条 ECSS pipeline不再维护平行 style path。
- Region 更新改用 opaque、surface-scoped handle同一份声明式 source 可以挂载到多个 buffer而不共享 live identity。
- 只有继续已提交 native frame 的 full declarative update 才会在 stable native
eligibility 前计算完整 selector-local style certificate其他 full/local 路径
保持原有 O(changed) style work。native update 不再提交缺失 computed style 的
replacement runtime结构性 full-frame bootstrap 继续保留。
### 删除
@ -27,7 +36,7 @@ Ebox 的重要变化记录在这里。
### 迁移
- ECSS 0.1.0 与 TP 1.0.0 互相独立,安装顺序任意;两者都安装后再安装 Ebox 2.0.0
- ECSS 0.1.0 与 TP 1.0.1 互相独立,安装顺序任意;两者都安装后再安装 Ebox 2.0.1
- 数字 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。

View File

@ -10,7 +10,7 @@ NATIVE_MANIFEST = native/Cargo.toml
NATIVE_TARGET ?= $(shell $(EMACS_BATCH) -l ebox-native-reflow.el --eval '(princ (ebox-native-reflow--rust-target))')
NATIVE_RELEASE_DIR = native/target/$(NATIVE_TARGET)/release
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests 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 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 patch-plan-tests style-schema-tests spi-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 m0a-tests state-contract-tests layout-boundary-tests patch-plan-tests style-schema-tests spi-tests docs-contract-tests ci-contract-tests
source-tests:
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
@ -63,6 +63,9 @@ dsl-tests:
flex-tests:
$(EMACS_TEST) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
m0a-tests:
$(EMACS_TEST) -l tests/ebox-m0a-inventory-fixture.el -l tests/ebox-m0a-inventory-tests.el -l tests/ebox-m0a-characterization-tests.el -f ert-run-tests-batch-and-exit
state-contract-tests:
$(EMACS_TEST) -l tests/ebox-state-contract-tests.el -f ert-run-tests-batch-and-exit

View File

@ -67,6 +67,12 @@ 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.
## Optional native module
The Rust module accelerates eligible reflow work. It is optional and has an

View File

@ -62,6 +62,11 @@ 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 绝不会同时运行。
## 可选 native 模块
Rust 模块只加速符合条件的 reflow它不是正确性的依赖并有完全等价的 Elisp

View File

@ -88,12 +88,25 @@ 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-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`. Replay derives defensive TP/Ebox provider reports from a completed initial report and carries no mutation authority. Ebox provides no selected port or consumer bootstrap; the existing v1 callback APIs remain available.
- `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.
- 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.
- Grid uses the normal measurement and rendering pipeline. Native reflow may reject an ineligible tree and must fall back to Elisp without changing correctness.
- A full declarative update continuing a committed native frame computes
selector-local styles for every candidate node before stable native
eligibility. The `:styles-prepared-p` certificate prevents replacement
runtime nodes from losing computed styles while ordinary full/local updates
retain O(changed) style work. Topology-changing full-frame bootstrap remains
eligible.
- Loading Ebox never builds or installs the optional Rust module.
- Phase 9 is complete: first mount, declarative commit, handle/selector update, viewport/theme update, batch flush, and scroll update all publish through TP surfaces. Phase 10 has unified selector parsing, matching, and cascade under ECSS; Ebox computes dirty/layout ownership and joins its opaque runtime state to the TP rollback-capable transaction, with no second live buffer executor.
- Successful reports preserve the Ebox semantic strategy and planned publication scope, then add `:publication-scope tp-surface`, TP physical operation counts, surface revision, scoped/full-root facts, and retained-object reconciliation counts.

View File

@ -88,12 +88,23 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。
- E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录当前 v1 存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。在后续 checkpoint 改变存储形态前,当前 v1 TP client state 仍保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。
- `ebox-layout.el` 不再 load 或调用 `ebox-surface.el`/TP。candidate isolation、继承 cascade 判定和一次性 materialization 只能通过 `ebox-render-context.el` 拥有的已校验 port 进入 layout`ebox.el` 在两侧加载后接线 surface 实现。
- `ebox-incremental.el` 同样不再包含任何 `ebox-surface` symbol 或 publication call。`ebox-surface.el` 只安装一个不可变只读 context port提供 live client state、region mount、cascade proof 与 inline-style preparationbatch observation 留在更高层的 `ebox.el` orchestrator。
- `ebox-patch-plan.el` 只消费 tentative operation plist 与不可变 generation parent table并返回 deterministic plan artifact所有 live fact 准备仍由 incremental adapter 拥有。E3 默认使用 pure route同时保留显式 `legacy` 与执行等价检查的 `shadow` route三条 route 都不能发布。
- `ebox-style--property-definitions` 同时也是 used-value projection 的唯一真相源。非默认 engine lowering 写在每个 property 的 `:engine-projection`ECSS metadata 暴露派生的 `:engine-targets`,只有非公共 engine 字段可以进入小型 internal exception table。
- `ebox-framework-spi-capabilities` 返回 fresh immutable v2 provider record声明 paired initial/update stage+rollback、combined participant order、same-object legacy report、initial observation replay 以及 `tp-transaction-protocol-v1+v2`。replay 只从 completed initial report 构造 defensive TP/Ebox provider reports不带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap现有 v1 callback API 保留。
- `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。
- 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变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。
- 只有继续已提交 native frame 的 full declarative update才会在 stable native
eligibility 前为全部 candidate node 计算 selector-local style生成的
`:styles-prepared-p` certificate 防止 replacement runtime 丢失 computed style
普通 full/local update 仍保持 O(changed)。topology-changing full-frame bootstrap
继续可用。
- 加载 Ebox 不会构建或安装可选 Rust 模块。
- Phase 9 已完成:首次 mount、声明式 commit、handle/selector 更新、viewport/theme 更新、batch flush 与 scroll 更新全部通过 TP surface 发布。Phase 10 已把 selector 解析、匹配与 cascade 统一到 ECSSEbox 只计算 dirty/layout owner并让不透明 runtime state 参与 TP 的可 rollback transaction不存在第二个 live buffer executor。
- 成功报告会保留 Ebox 语义 strategy 与 planned publication scope再加入 `:publication-scope tp-surface`、TP 物理 operation 数、surface revision、scoped/full-root 事实和 retained-object reconciliation 统计。

View File

@ -280,11 +280,14 @@ each segment so structural copies never recover keys from source metadata."
sequence (list (cons ref items)) key-function))
(defun ebox-child-range--replace-many
(sequence replacements key-function &optional reuse-maps)
(sequence replacements key-function &optional reuse-maps
retain-item-identities-p)
"Replace RANGE REPLACEMENTS in SEQUENCE with one two-phase transaction.
REUSE-MAPS is an alist from Range ref to proven `(NEW . OLD)' item indexes;
those NEW items already name canonical published objects and are not copied.
KEY-FUNCTION returns the source-owned key for every replacement item."
KEY-FUNCTION returns the source-owned key for every replacement item.
When RETAIN-ITEM-IDENTITIES-P is non-nil, non-reused items are already owned by
the candidate transaction and are retained without another node copy."
(let ((metrics (ebox-child-range--make-metrics))
(missing (make-symbol "missing-range-ref"))
(seen (make-hash-table :test #'equal)) records)
@ -310,7 +313,8 @@ KEY-FUNCTION returns the source-owned key for every replacement item."
collect
(if (assq offset reuse-map)
item
(ebox-child-range--item-copy item metrics))))
(ebox-child-range--item-copy
item metrics retain-item-identities-p))))
(vconcat
(mapcar (lambda (item)
(and key-function (funcall key-function item)))

View File

@ -69,6 +69,8 @@
"ebox-buffer-backend" (string property region-id))
(declare-function ebox--propertize-content-line
"ebox-buffer-backend" (line region-id idx fallback))
(declare-function ebox--add-content-owner
"ebox-buffer-backend" (string region-id))
(defvar ebox--propertize-private-content-line-p)
(defvar ebox--flex-content-min-width-table)
@ -892,7 +894,7 @@ metrics, and render intrinsically only when layout semantics require it."
(ebox--flex-unrendered-measurement source props axis)
(ebox--flex-measure-source source props axis viewport probe))))
(defun ebox--flex-pad-width (string width &optional align)
(defun ebox--flex-pad-width (string width &optional align owner-id)
"Pad every line of STRING to WIDTH pixels."
(let ((align (or align 'flex-start)))
(ebox--maplines
@ -903,19 +905,27 @@ metrics, and render intrinsically only when layout semantics require it."
((or 'flex-end 'end 'right 'self-end) extra)
('center (/ extra 2))
(_ 0))))
(concat (ebox-pixel-space left)
line
(ebox-pixel-space (- extra left)))))
(let ((left-space (ebox-pixel-space left))
(right-space (ebox-pixel-space (- extra left))))
(when owner-id
(setq left-space
(ebox--add-content-owner left-space owner-id))
(setq right-space
(ebox--add-content-owner right-space owner-id)))
(concat left-space line right-space))))
string)))
(defun ebox--flex-pad-height (string height offset width)
(defun ebox--flex-pad-height (string height offset width &optional owner-id)
"Pad STRING vertically to HEIGHT using blank lines of WIDTH pixels."
(let* ((lines (ebox-string-lines string))
(current (length lines))
(extra (max 0 (- height current)))
(top (max 0 (min extra offset)))
(bottom (- extra top))
(blank (ebox-pixel-space width)))
(blank (ebox-pixel-space width))
(blank (if owner-id
(ebox--add-content-owner blank owner-id)
blank)))
(ebox-lines-join
(append (make-list top blank)
lines
@ -1009,6 +1019,8 @@ Each item in PARTS is a cons cell (RENDERED . WIDTH)."
Return a sized render entry containing the rendered string and dimensions."
(let* ((source (plist-get item :source))
(source-box-p (ebox--flex-box-source-p source))
(allocation-owner
(and source-box-p (ebox--ensure-region-id source)))
(align (ebox--flex-resolved-align item container-align))
(stretch (memq align '(stretch normal)))
(render-main (plist-get item :render-main))
@ -1143,7 +1155,9 @@ Return a sized render entry containing the rendered string and dimensions."
main
(not (and can-use-measured (= main render-main))))
(if (eq axis 'row)
(setq rendered (ebox--flex-pad-width rendered main))
(setq rendered
(ebox--flex-pad-width
rendered main nil allocation-owner))
(setq rendered
(ebox--flex-pad-height
rendered main 0 (ebox--flex-string-width rendered)))))
@ -1158,8 +1172,12 @@ Return a sized render entry containing the rendered string and dimensions."
(width (max (or main rendered-width) rendered-width))
(extra (max 0 (- cross (ebox-string-height rendered))))
(offset (ebox--flex-cross-offset align extra)))
(setq rendered (ebox--flex-pad-height rendered cross offset width)))
(setq rendered (ebox--flex-pad-width rendered cross align))))
(setq rendered
(ebox--flex-pad-height
rendered cross offset width allocation-owner)))
(setq rendered
(ebox--flex-pad-width
rendered cross align allocation-owner))))
(let ((entry
(ebox--flex-sized-entry
rendered

View File

@ -44,16 +44,8 @@
"ebox-tree" (old new))
(declare-function ebox-tree-runtime-identity-snapshot
"ebox-tree" (root))
(declare-function ebox-surface-region-mounts
"ebox-surface" (buffer region-id &optional roles))
(declare-function ebox-surface-region-bounds
"ebox-surface" (buffer region-id &optional roles))
(declare-function tp-object-mounts
"tp-surface" (object))
(declare-function ebox-surface--cascade-local-owner-proof-p
"ebox-surface" (state candidate-state dirty-set))
(declare-function ebox-surface-prepare-inline-candidate-styles
"ebox-surface" (old-state candidate-index node-ids))
(declare-function ebox-string-lines "ebox" (string))
(declare-function ebox-get "ebox" (box key))
(declare-function ebox--ensure-node-id "ebox" (node))
@ -90,8 +82,6 @@
"ebox-cache" (buffer))
(declare-function ebox-cache-restore-report
"ebox-cache" (buffer snapshot))
(declare-function ebox-surface-call-with-observation
"ebox-surface" (buffer stage function))
(declare-function ebox-style-cascade-active-p
"ebox-style" ())
(declare-function ebox-native-commit-plan-eligible-p
@ -105,6 +95,67 @@
(declare-function ebox-native-reflow-release-session
"ebox-native-reflow" (session))
(cl-defstruct
(ebox-incremental-context-port
(:constructor ebox-incremental-context-port-create))
"Read-only host queries supplied by the Ebox orchestrator."
(buffer-state nil :read-only t)
(region-mounts nil :read-only t)
(cascade-owner-proof-p nil :read-only t)
(prepare-inline-styles nil :read-only t))
(defvar ebox-incremental--context-port nil
"Immutable host-query port installed by the Ebox orchestrator.")
(defun ebox-incremental-install-context-port (port)
"Install validated immutable incremental context PORT."
(unless (and (ebox-incremental-context-port-p port)
(cl-every
#'functionp
(list
(ebox-incremental-context-port-buffer-state port)
(ebox-incremental-context-port-region-mounts port)
(ebox-incremental-context-port-cascade-owner-proof-p port)
(ebox-incremental-context-port-prepare-inline-styles port))))
(signal 'wrong-type-argument
(list 'ebox-incremental-context-port-p port)))
(setq ebox-incremental--context-port port))
(defun ebox-incremental--require-context-port ()
"Return the installed incremental context port or fail closed."
(or ebox-incremental--context-port
(error "Ebox incremental context port is not installed")))
(defun ebox-incremental--buffer-state (buffer)
"Return BUFFER's live client state through the context port."
(funcall
(ebox-incremental-context-port-buffer-state
(ebox-incremental--require-context-port))
buffer))
(defun ebox-incremental--region-mounts (buffer region-id &optional roles)
"Return BUFFER mounts for REGION-ID and optional ROLES through the port."
(funcall
(ebox-incremental-context-port-region-mounts
(ebox-incremental--require-context-port))
buffer region-id roles))
(defun ebox-incremental--cascade-local-owner-proof-p
(state candidate-state dirty-set)
"Return the port proof for STATE, CANDIDATE-STATE, and DIRTY-SET."
(funcall
(ebox-incremental-context-port-cascade-owner-proof-p
(ebox-incremental--require-context-port))
state candidate-state dirty-set))
(defun ebox-incremental--prepare-inline-candidate-styles
(old-state candidate-index node-ids)
"Prepare NODE-IDS in CANDIDATE-INDEX using OLD-STATE through the port."
(funcall
(ebox-incremental-context-port-prepare-inline-styles
(ebox-incremental--require-context-port))
old-state candidate-index node-ids))
(defvar ebox-viewport-width)
(defvar ebox-viewport-height)
(defvar ebox--region-id-counter)
@ -168,6 +219,46 @@ arguments.")
(defvar ebox-incremental--candidate-full-source-base-index nil
"Transaction-local fact index for one broad candidate rebuild.")
(defun ebox-incremental--candidate-range-items-retainable-p
(candidate items source-index reuse-map)
"Return non-nil when ITEMS satisfy the candidate-owned retention proof.
The proof is deliberately narrow: every non-reused node must belong to the
incoming canonical source generation and carry no Ebox runtime identity.
Published reuse slots are skipped because they are replaced by their exact old
objects during canonical reuse. Canonical input construction has already
checked node/handle uniqueness; this check adds the ownership boundary against
the published candidate base without scanning that base's full tree."
(let* ((base-state (ebox-candidate--base-state candidate))
(base-source-index (plist-get base-state :source-index))
(seen (make-hash-table :test #'eq)))
(and (ebox-source-index-p source-index)
(not (eq source-index base-source-index))
(cl-labels
((visit
(node)
(and (listp node)
(not (stringp node))
(not (ebox-child-range--descriptor-p node))
(not (gethash node seen))
(let ((handle (ebox-tree-node-source-handle node)))
(and (ebox-source-index-handle-member-p
source-index handle)
(not (and base-source-index
(ebox-source-index-handle-member-p
base-source-index handle)))
(null (plist-get node :node-id))
(null (plist-get node :region-id))
(null (plist-get node :surface-object))
(null (plist-get node :ebox-sequence-location))
(progn
(puthash node t seen)
(cl-every #'visit
(ebox-tree--children-raw node))))))))
(cl-loop for item in items for index from 0
unless (assq index reuse-map)
always (visit item))))))
(cl-defstruct
(ebox-candidate
(:constructor ebox-incremental--make-candidate)
@ -200,7 +291,7 @@ arguments.")
(:constructor ebox-incremental--make-candidate-range-replacement)
(:conc-name ebox-incremental--candidate-range-replacement-))
"One base-addressed child Range payload replacement."
ref items reuse-map source-indexes)
ref items reuse-map source-indexes retain-item-identities-p)
(cl-defstruct
(ebox-incremental--candidate-paint-patch
@ -269,7 +360,6 @@ user code or signal after mutation.")
'ebox-incremental-before-runtime-mutation-hook buffer kind)))
(defvar ebox--buffer-render-state-table)
(defvar ebox-surface--buffer-surface)
(declare-function tp-surface-client-state "tp-surface" (surface))
(declare-function tp-surface-live-p "tp-surface" (surface))
@ -998,14 +1088,9 @@ Keys are region-id, values are plists with :scroll-offset, :content-lines, etc."
(eq buffer
(car ebox-incremental--buffer-render-state-override)))
(cdr ebox-incremental--buffer-render-state-override)
(let ((surface
(and (buffer-live-p buffer)
(boundp 'ebox-surface--buffer-surface)
(with-current-buffer buffer
ebox-surface--buffer-surface))))
(if (and surface (tp-surface-live-p surface))
(tp-surface-client-state surface)
(gethash buffer ebox--buffer-render-state-table)))))
(or (and (buffer-live-p buffer)
(ebox-incremental--buffer-state buffer))
(gethash buffer ebox--buffer-render-state-table))))
(defun ebox--buffer-update-report (buffer)
"Return the update report stored in BUFFER's render state."
@ -1835,7 +1920,7 @@ therefore unsafe."
spans)
(dolist (region-id region-ids)
(dolist (mount
(ebox-surface-region-mounts
(ebox-incremental--region-mounts
(current-buffer) region-id (mapcar #'car ebox-region-types)))
(ebox--add-role-span-line-ranges
line-ranges
@ -1981,7 +2066,7 @@ If a line span would cross a foreign ebox region, return nil."
(buffer (current-buffer)))
(dolist (region-id region-ids)
(dolist (mount
(ebox-surface-region-mounts buffer region-id roles))
(ebox-incremental--region-mounts buffer region-id roles))
(dolist (role (plist-get (plist-get mount :tags) :ebox/roles))
(puthash role t role-set)))))
(maphash (lambda (role _) (push role roles)) role-set)
@ -4057,12 +4142,6 @@ during `ebox-incremental-flush'."
buffer pending report))
(ebox--buffer-update-report buffer)))))
(defun ebox-incremental-flush (buffer)
"Flush BUFFER's explicit incremental update batch as one operation."
(ebox-surface-call-with-observation
buffer 'batch
(lambda () (ebox-incremental--flush buffer))))
;;; Declarative Root Commit
(defun ebox-incremental-candidate-begin (buffer)
@ -4086,7 +4165,7 @@ during `ebox-incremental-flush'."
:sealed-p nil)))
(defun ebox-incremental-candidate-replace-range-ref
(candidate ref next-items &optional reuse-map)
(candidate ref next-items &optional reuse-map retain-item-identities-p)
"Record a Range REF replacement using proven slot REUSE-MAP."
(unless (ebox-candidate-p candidate)
(error "Ebox Range replacement requires an Ebox candidate"))
@ -4100,7 +4179,12 @@ during `ebox-incremental-flush'."
(record (gethash ref (plist-get base-state :range-ref-table))))
(unless record
(error "Ebox candidate Range ref does not exist in its base: %S" ref))
(let* ((parent (gethash (plist-get record :parent-node-id)
(let* ((retain-item-identities-p
(and retain-item-identities-p
(ebox-incremental--candidate-range-items-retainable-p
candidate next-items ebox-incremental--source-base-index
reuse-map)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get base-state :node-table)))
(sequence (and parent (plist-get parent :ebox-child-sequence)))
(segment (and sequence (ebox-child-range--lookup-ref sequence ref)))
@ -4146,10 +4230,13 @@ during `ebox-incremental-flush'."
(reject-range child)))))
(reject-range item))
(let ((copy
(if retain-item-identities-p
item
(ebox-tree-clear-runtime-identities
(ebox-tree-copy-node-structure item))))
(ebox-tree-copy-node-structure item)))))
(unless retain-item-identities-p
(ebox-tree-source-index
copy nil nil ebox-incremental--source-base-index t)
copy nil nil ebox-incremental--source-base-index t))
copy)))
(cl-loop
for item in next-items
@ -4189,9 +4276,9 @@ during `ebox-incremental-flush'."
(list
(ebox-incremental--make-candidate-range-replacement
:ref ref :items owned :reuse-map (copy-tree reuse-map)
:source-indexes source-indexes))))))
)
candidate)
:source-indexes source-indexes
:retain-item-identities-p retain-item-identities-p))))))
candidate))
(defun ebox-incremental--candidate-ancestor-p
(candidate ancestor-id descendant-id)
@ -5325,6 +5412,9 @@ but must re-enter the candidate indexes as a new object."
entry))
(reuse-map
(ebox-incremental--candidate-range-replacement-reuse-map
entry))
(retain-item-identities-p
(ebox-incremental--candidate-range-replacement-retain-item-identities-p
entry)))
(let ((segment-index
(plist-get (gethash ref base-ranges)
@ -5352,7 +5442,8 @@ but must re-enter the candidate indexes as a new object."
location)
do (plist-put item :ebox-sequence-location
location))
(push (list ref items reuse-map source-indexes)
(push (list ref items reuse-map source-indexes
retain-item-identities-p)
replacements)))))
(when replacements
(let* ((ordered-replacements (nreverse replacements))
@ -5392,6 +5483,10 @@ but must re-enter the candidate indexes as a new object."
(lambda (replacement)
(cons (nth 0 replacement)
(nth 2 replacement)))
ordered-replacements)
(cl-every
(lambda (replacement)
(nth 4 replacement))
ordered-replacements)))
(new-payloads
(mapcar
@ -6976,8 +7071,11 @@ derive render changes from the computed fragment style signatures."
(defun ebox-incremental--prepare-declarative-runtime
(buffer old-state candidate-root &optional logical-candidate-p
local-preparation scroll-patch-p source-index)
"Prepare CANDIDATE-ROOT for BUFFER without mutating its published runtime."
local-preparation scroll-patch-p source-index prepare-all-styles-p)
"Prepare CANDIDATE-ROOT for BUFFER without mutating its published runtime.
When PREPARE-ALL-STYLES-P is non-nil, compute selector-local styles for every
candidate node so a topology-stable native publication has a complete style
certificate."
(let* ((old-root (plist-get old-state :root-node))
(render-cache
(or (plist-get old-state :render-cache)
@ -7007,11 +7105,15 @@ derive render changes from the computed fragment style signatures."
candidate-source-index 'type))))
(candidate-region-box-table
(plist-get candidate-index :region-box-table))
(style-node-ids
(or (plist-get local-preparation :touched-node-ids)
(and prepare-all-styles-p
(ebox-incremental--hash-keys
(plist-get candidate-index :node-table)))))
(candidate-subject-table
(and local-preparation
(ebox-surface-prepare-inline-candidate-styles
old-state candidate-index
(plist-get local-preparation :touched-node-ids))))
(and style-node-ids
(ebox-incremental--prepare-inline-candidate-styles
old-state candidate-index style-node-ids)))
(styles-prepared-p (hash-table-p candidate-subject-table))
(source-dirty-set
(if local-preparation
@ -7020,7 +7122,7 @@ derive render changes from the computed fragment style signatures."
old-state candidate-root candidate-index)))
(dirty-set source-dirty-set)
(current-display-signature (ebox--current-display-signature)))
(when local-preparation
(when (or local-preparation prepare-all-styles-p)
;; Participation depends on the final parent relation and, when the
;; local Style Adapter succeeded, on computed values. Validate that
;; final contract once here; Surface consumes the resulting
@ -7028,7 +7130,8 @@ derive render changes from the computed fragment style signatures."
(ebox-tree-validate-indexed-participation
(plist-get candidate-index :node-table)
(plist-get candidate-index :parent-table)
(plist-get local-preparation :participation-node-ids)
(or (plist-get local-preparation :participation-node-ids)
style-node-ids)
(if styles-prepared-p 'computed 'author)
candidate-source-index))
(when styles-prepared-p
@ -7094,7 +7197,8 @@ derive render changes from the computed fragment style signatures."
:runtime-index-complete-p
(plist-get local-preparation :runtime-index-complete-p)
:computed-participation-validated-p
(and local-preparation styles-prepared-p)
(and (or local-preparation prepare-all-styles-p)
styles-prepared-p)
:styles-prepared-p styles-prepared-p
:index candidate-index
:dirty-set dirty-set
@ -7135,7 +7239,8 @@ derive render changes from the computed fragment style signatures."
old-root (plist-get old-state :source-index)
candidate-root candidate-source-index)
(ebox-incremental--prepare-declarative-runtime
buffer old-state candidate-root nil nil nil candidate-source-index)))
buffer old-state candidate-root nil nil nil candidate-source-index
(eq (plist-get old-state :projection-kind) 'native-frame))))
(defun ebox-incremental--candidate-source-input-map (candidate)
"Return exact source-handle inputs carried by CANDIDATE operations."
@ -7400,7 +7505,7 @@ line count; its outside siblings are shifted by the one scoped replacement."
(null (plist-get old-state :scroll-region-ids))
(not (plist-get old-state :cascade-active-p))
(or (not (plist-get old-state :cascade-required-p))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))
(cl-every
(lambda (entry)
@ -7786,7 +7891,7 @@ suppresses the ordinary fallback when any fact is missing."
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))))
(when (and geometry-span-proof
(not (plist-get geometry-span-proof :owner-proofs)))
@ -7907,7 +8012,7 @@ because it could address a run whose topology the closure just replaced."
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set)))
(while (and candidate-id path-valid-p (not proof)
(not (equal candidate-id root-id)))
@ -8100,41 +8205,18 @@ because it could address a run whose topology the closure just replaced."
(defun ebox-incremental--fixed-basis-flex-local-op
(buffer render-dirty-set)
"Return one local Flex-item op for related geometry RENDER-DIRTY-SET.
"Return one tentative Flex-item op for related geometry RENDER-DIRTY-SET.
Multiple descendant content changes can share a fixed-basis, zero-minimum Flex
item without reflowing the outer Flex line. The candidate item is still
rendered into its retained slot and must pass the complete footprint proof."
item without changing its main-axis allocation. This planner only selects the
shared owner; grouped proof verifies topology and the retained ancestor slot,
then Surface performs the one actual candidate render and complete footprint,
role, overflow, and outside-complement validation."
(when (and (> (length render-dirty-set) 1)
(cl-every
(lambda (entry)
(and (eq (plist-get entry :dirty-kind) 'geometry)
(not (plist-get entry :children))
;; Typography can change intrinsic allocation inside the
;; candidate Flex item. Those owners already have exact
;; local span proofs; speculatively rendering their whole
;; common item here duplicates that work and commonly
;; rejects after the expensive render.
(null (cl-intersection
(plist-get entry :changed-keys)
ebox--typography-style-signature-keys))
;; A whole-owner footprint proof cannot retain the same
;; intrinsic content runs when a changed text run already
;; has a different measured width. Reject that group
;; before rendering it; the per-owner slot proofs can
;; still publish each changed run precisely.
(let ((old-content
(plist-get (plist-get entry :old-signature)
:content))
(new-content
(plist-get (plist-get entry :new-signature)
:content)))
(or (not (memq :content
(plist-get entry :changed-keys)))
(not (and (stringp old-content)
(stringp new-content)))
(= (ebox--string-pixel-width old-content)
(ebox--string-pixel-width new-content))))))
(not (plist-get entry :children))))
render-dirty-set))
(let* ((owner-ids
(delete-dups
@ -8143,17 +8225,9 @@ rendered into its retained slot and must pass the complete footprint proof."
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
buffer (plist-get entry :node-id)))
render-dirty-set)))
(owner-id (and (= (length owner-ids) 1) (car owner-ids)))
(changed-keys
(delete-dups
(cl-mapcan
(lambda (entry)
(copy-sequence (plist-get entry :changed-keys)))
render-dirty-set))))
(owner-id (and (= (length owner-ids) 1) (car owner-ids))))
(when (and owner-id
(not (equal owner-id (ebox--buffer-root-node-id buffer)))
(ebox--cached-flex-item-slot-footprint-safe-p
buffer owner-id changed-keys))
(not (equal owner-id (ebox--buffer-root-node-id buffer))))
(ebox--patch-op
'owner-rerender owner-id
:dirty (ebox--merge-dirty-provenance-list render-dirty-set))))))
@ -8355,7 +8429,7 @@ closure may still plan one for a later owner-scoped surface projection."
dirty-set)
(or missing-structure-style-p
(not
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set)))))))
(defun ebox-incremental--root-owner-plan (buffer dirty-set)
@ -9335,7 +9409,7 @@ metadata; this predicate only authorizes the local attempt."
(not (ebox-style-cascade-active-p)))
(and (or (memq :content changed-keys)
local-typography-p)
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty))))
(null (plist-get old-state :scroll-region-ids))
(or (null (plist-get candidate-state :scroll-region-ids))
@ -9413,7 +9487,7 @@ metadata; this predicate only authorizes the local attempt."
(and (or (memq :content changed-keys) local-typography-p)
(or (plist-get old-state :cascade-active-p)
(plist-get old-state :cascade-required-p))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty))))))))
(defun ebox-incremental--owner-proofs-disjoint-p (buffer proofs)
@ -9478,27 +9552,30 @@ role, and outside-complement compatibility before publication."
(old-node (gethash owner-id (plist-get old-state :node-table)))
(new-node (gethash owner-id
(plist-get candidate-state :node-table)))
(fixed-flex-slot-p
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer owner-id))
(snapshot
(and old-node
(if fixed-flex-slot-p
(ebox-incremental--cached-allocation-snapshot
buffer owner-id)
(ebox-incremental--cached-layout-snapshot-details
buffer owner-id)))
buffer owner-id))))
(changed-keys
(delete-dups
(cl-mapcan
(lambda (entry)
(copy-sequence (plist-get entry :changed-keys)))
dirty-set)))
(fixed-flex-slot-p
(ebox--flex-item-fixed-basis-content-allocation-stable-p
buffer owner-id))
(owner-slot-proof
(ebox-incremental--owner-slot-stable-p
buffer old-state candidate-state owner-id))
(fixed-flex-ancestor-proof
(and fixed-flex-slot-p
(not owner-slot-proof)
(ebox-incremental--ancestor-slot-signature
buffer old-state candidate-state owner-id)))
(owner-slot-proof
(and (not fixed-flex-ancestor-proof)
(ebox-incremental--owner-slot-stable-p
buffer old-state candidate-state owner-id)))
;; A grouped patch publishes the owner as one unit. Its retained
;; external allocation is therefore the canonical certificate;
;; probing each changed leaf repeats the same ancestor walk and
@ -9551,7 +9628,7 @@ role, and outside-complement compatibility before publication."
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state dirty-set))))
(let ((ebox-incremental--buffer-render-state-override
(cons buffer candidate-state))
@ -9572,6 +9649,8 @@ role, and outside-complement compatibility before publication."
(or (eq key :content)
(eq key :surface-properties)
(memq key ebox--paint-style-signature-keys)
(memq key
ebox--typography-style-signature-keys)
(memq key '(:font-family :font-size
:font-weight :font-style))))
(plist-get entry :changed-keys))))
@ -9660,7 +9739,7 @@ role, and outside-complement compatibility before publication."
(or (and (not (plist-get old-state :cascade-active-p))
(not (plist-get old-state :cascade-required-p))
(not (ebox-style-cascade-active-p)))
(ebox-surface--cascade-local-owner-proof-p
(ebox-incremental--cascade-local-owner-proof-p
old-state candidate-state (list dirty)))))
(when (and delta old-node new-node snapshot spans ancestor-slot-signature
(eq (plist-get operation :op) 'span-patch)
@ -10510,6 +10589,10 @@ When RETAIN-NATIVE-P is non-nil, preserve its committed native frame bases."
(append
(list :viewport-width (plist-get state :viewport-width)
:viewport-height (plist-get state :viewport-height)
;; Candidate runtime revisions are the TP base revision for the
;; upcoming viewport transaction; the published TP revision is
;; advanced by the transaction and recorded after publication.
:runtime-revision (plist-get state :runtime-revision)
:display-signature (plist-get state :display-signature)
:render-cache (plist-get state :render-cache)
:layout-fragments (plist-get state :layout-fragments)

View File

@ -100,6 +100,9 @@ containing block merely because the row itself is being rendered in it.")
(defvar ebox--box-content-cache-purpose nil
"Dynamic discriminator for distinct box content materializations.")
(defvar ebox--containing-wrap-mode nil
"Box-owned soft-wrap policy inherited by canonical Text descendants.")
(defvar ebox--rendered-uniform-width-table
(make-hash-table :test 'eq :weakness 'key)
"Weak map from rendered strings to their proven uniform pixel width.")
@ -926,6 +929,7 @@ FALLBACK is used for nil, auto, or unavailable viewport-height values."
ebox-viewport-height
ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p
ebox--containing-wrap-mode
ebox--box-content-cache-purpose
ebox--render-region-id))
@ -1354,7 +1358,8 @@ property parsers."
(defun ebox--call-with-box-content-context (props function)
"Call FUNCTION in the content viewport established by box PROPS."
(let ((content-viewport (ebox--wrapper-content-viewport-pixel props)))
(let ((content-viewport (ebox--wrapper-content-viewport-pixel props))
(ebox--containing-wrap-mode (ebox-get props :wrap-mode)))
(cond
((eq (ebox-get props :width) 'max-content)
(let ((ebox-viewport-width nil)
@ -3470,9 +3475,23 @@ ambient horizontal viewport."
('center 'center)
('end 'right)
(_ 'left)))
(content-width
(and (not ebox--intrinsic-layout-measurement)
(not ebox--inline-auto-width-intrinsic-p)
(ebox--viewport-pixel-width nil)))
(rendered-items
(mapcar (lambda (leaf)
(let ((rendered (ebox--render-with-cache leaf)))
(let* ((rendered (ebox--render-with-cache leaf))
(wrap-mode
(and (eq (plist-get leaf :ebox-kind) 'text)
(or ebox--containing-wrap-mode
(ebox-get leaf :wrap-mode))))
(rendered
(if (and content-width (> content-width 0)
(ebox-style-soft-wrap-p wrap-mode))
(ebox--wrap-text
rendered content-width wrap-mode)
rendered)))
(cons rendered
(ebox--string-max-pixel-width rendered))))
children))
@ -3482,7 +3501,7 @@ ambient horizontal viewport."
(target-w (if (or ebox--intrinsic-layout-measurement
ebox--inline-auto-width-intrinsic-p)
max-w
(or (ebox--viewport-pixel-width nil) max-w))))
(or content-width max-w))))
(let (pieces)
(cl-loop for item in rendered-items
for tail on rendered-items

View File

@ -265,8 +265,7 @@ the native continuation contract: a live session, a confirmable prior frame,
the committed text/fragment bases, compiler postorder, and projection support."
(and (eq (plist-get previous-state :projection-kind) 'native-frame)
(plist-get previous-state :native-sync-session)
(or (plist-get previous-state :native-sync-pending)
(plist-get previous-state :native-sync-confirmed-p))
(plist-get previous-state :native-sync-confirmed-p)
(stringp (plist-get previous-state :native-committed-rendered))
(vectorp
(plist-get previous-state :native-committed-fragment-template))
@ -282,20 +281,51 @@ the committed text/fragment bases, compiler postorder, and projection support."
(ebox-native-commit-projection-eligible-p
previous-state state nil))))
(defun ebox-native-commit--create-private-session (&optional session)
"Create a private native session, forking SESSION when it is present."
(if session
(ebox-native-reflow-fork-session session)
(ebox-native-reflow-create-session
:workers 1 :max-jobs 4 :max-results 4)))
(defun ebox-native-commit--isolate-session
(previous-state candidate-state)
"Give CANDIDATE-STATE a private child of PREVIOUS-STATE's native session."
(when-let* ((session (plist-get previous-state :native-sync-session))
(fork
(condition-case nil
(ebox-native-reflow-fork-session session)
(error nil))))
(plist-put candidate-state :native-sync-session fork)
;; A pending/confirmed frame is local to the previous Rust handle. The
;; fork retains compiler facts but begins its own publication sequence.
"Give CANDIDATE-STATE a private native session.
Fork PREVIOUS-STATE's committed session when present; otherwise bootstrap a
new session with the same bounded configuration used by retained frames."
(let ((source (plist-get previous-state :native-sync-session)))
(cl-remf candidate-state :native-session-setup-failure)
(condition-case condition
(when-let* ((private
(ebox-native-commit--create-private-session source)))
(plist-put candidate-state :native-sync-session private)
(plist-put candidate-state :native-sync-pending nil)
(plist-put candidate-state :native-sync-confirmed-p nil)
t))
;; A strict native fork shares the source's immutable confirmed
;; baseline. Bootstrap sessions deliberately begin unconfirmed.
(plist-put candidate-state :native-sync-confirmed-p
(not (null source)))
t)
(error
;; The ordinary renderer remains the semantic fallback, but the failed
;; native attempt must stay observable in the committed report.
(plist-put candidate-state :native-session-setup-failure
(list :phase (if source 'fork 'create)
:condition (copy-tree condition)))
nil))))
(defun ebox-native-commit-attach-confirmed-base (previous-state state)
"Attach PREVIOUS-STATE's confirmed geometry identity to candidate STATE."
(when (and previous-state (plist-get state :native-sync-confirmed-p))
(let* ((root (plist-get previous-state :root-node))
(box (ebox-native-commit--root-box root)))
(plist-put state :native-base-viewport-width
(plist-get previous-state :viewport-width))
(plist-put state :native-base-viewport-height
(plist-get previous-state :viewport-height))
(plist-put state :native-base-root-width
(or (and box (ebox--literal-root-pixel-width box))
(plist-get previous-state :viewport-width)))))
state)
(defun ebox-native-commit-prepare-viewport-continuity
(previous-state candidate-state)
@ -330,6 +360,12 @@ the committed text/fragment bases, compiler postorder, and projection support."
(plist-put state :native-touched-node-ids layout-touched)
(plist-put state :native-topology-stable-p stable)
(when (and (or stable layout-touched)
;; A stable native patch reuses prior TP objects and skips a
;; full Surface style projection. Its replacement nodes must
;; therefore already carry the computed-style certificate.
;; A topology-changing full bootstrap projects every node and
;; does not need this local-style shortcut certificate.
(or (not stable) (plist-get prepared :styles-prepared-p))
(not (ebox-style-cascade-active-p))
(ebox-native-commit-projection-eligible-p
old-state state nil)
@ -429,17 +465,26 @@ the committed text/fragment bases, compiler postorder, and projection support."
:viewport-height viewport-height
:root-width root-width
:runtime-revision
;; Ebox's internal runtime revision names the TP base revision
;; for this candidate. The published TP revision is confirmed
;; separately after the transaction commits.
(or (plist-get state :runtime-revision) 0)
:context-hash
(sxhash-equal
(list viewport-width viewport-height root-width
(plist-get state :display-signature)))
;; Viewport and root geometry are matched by their typed fields
;; below. Keep this hash for the measurement/display context so
;; content changes can still use the confirmed frame as a patch
;; base while font/display changes invalidate it.
(sxhash-equal (plist-get state :display-signature))
:complete t
:root-metadata nil)
(when (plist-get state :native-sync-confirmed-p)
(list :base-viewport-width viewport-width
:base-viewport-height viewport-height
:base-root-width root-width))))))
(when (and (plist-get state :native-sync-confirmed-p)
(plist-get state :native-topology-stable-p))
(list :base-viewport-width
(plist-get state :native-base-viewport-width)
:base-viewport-height
(plist-get state :native-base-viewport-height)
:base-root-width
(plist-get state :native-base-root-width)))))))
(defun ebox-native-commit--apply-patches (source patches)
"Return SOURCE with ordered native PATCHES applied."
@ -472,25 +517,32 @@ the committed text/fragment bases, compiler postorder, and projection support."
(aset target index record))))
target))
(defun ebox-native-commit--promote-pending-frame (state session)
"Promote STATE's previously published pending frame in SESSION."
(defun ebox-native-commit-confirm-published-frame (state published-revision)
"Confirm STATE's pending native frame at its exact published revision.
This function must run after TP installed STATE as client state and while the
publication transaction can still roll back."
(when-let* ((pending (plist-get state :native-sync-pending)))
(let ((revision published-revision))
(unless (and (integerp revision) (>= revision 0)
(= revision (plist-get pending :confirmed-revision)))
(error "Native published revision %S does not match pending revision %S"
revision (plist-get pending :confirmed-revision)))
(ebox-native-reflow-confirm-native-frame
session (plist-get pending :generation) (plist-get pending :key)
(plist-get pending :confirmed-revision))
(plist-get state :native-sync-session)
(plist-get pending :generation)
(plist-get pending :key)
revision)
(plist-put state :native-sync-confirmed-p t)
(cl-remf state :native-sync-pending)))
state)
(defun ebox-native-commit--retained-frame (state node frame-spec)
"Return NODE's full or patch-derived retained native frame for STATE."
(let ((session
(or (plist-get state :native-sync-session)
(let ((created
(ebox-native-reflow-create-session
:workers 1 :max-jobs 4 :max-results 4)))
(let ((created (ebox-native-commit--create-private-session)))
(plist-put state :native-sync-session created)
created))))
(ebox-native-commit--promote-pending-frame state session)
(let* ((result
(ebox-native-reflow-execute-session-sync
session node frame-spec nil state))
@ -560,9 +612,44 @@ the committed text/fragment bases, compiler postorder, and projection support."
(if (plist-get frame :native-patch) 'patch 'full))
frame)))
(defconst ebox-native-commit--failed-render-state-keys
'(:native-sync-session
:native-sync-pending
:native-sync-confirmed-p
:native-render-p
:native-render-output
:native-render-fragments
:native-render-frame
:native-render-fragment-template
:native-render-fallback
:native-frame-kind
:native-committed-rendered
:native-committed-fragment-template
:native-committed-owned-ranges
:native-coordinate-patches
:native-reuse-mount-projection-p
:native-reuse-ownership-p
:native-inherited-dirty-node-ids
:native-topology-stable-p
:native-touched-node-ids
:native-removed-node-ids)
"Candidate fields that are invalid after native rendering fails.")
(defun ebox-native-commit--downgrade-candidate (state reason)
"Downgrade STATE after native rendering fails for diagnostic REASON."
(dolist (key ebox-native-commit--failed-render-state-keys)
(cl-remf state key))
(plist-put state :projection-kind nil)
;; Surface consumes this once into the transaction report, then removes it
;; from runtime state. It never participates in projection eligibility.
(plist-put state :native-render-fallback reason)
nil)
(defun ebox-native-commit-render (state node)
"Return NODE's native rendered string after committing effects into STATE.
Return nil without mutating STATE when the native capability proof is absent."
Return nil without mutating STATE when the native capability proof is absent.
Once native execution begins, failure downgrades STATE to an ordinary
projection so the caller may publish its existing Elisp fallback truthfully."
(when (and (require 'ebox-native-reflow nil t)
(ebox-native-reflow-layout-ready-p)
(eq (plist-get state :projection-kind) 'native-frame)
@ -577,7 +664,8 @@ Return nil without mutating STATE when the native capability proof is absent."
(fragment-template
(and (not (plist-get effects :scroll-window-p))
(plist-get effects :fragment-span-template))))
(when (and (stringp rendered) (vectorp fragment-template))
(if (and (stringp rendered) (vectorp fragment-template))
(progn
(ebox-native-commit--install-region-boxes state)
(plist-put state :render-owned-text-values
(make-hash-table :test #'eq))
@ -586,11 +674,12 @@ Return nil without mutating STATE when the native capability proof is absent."
(plist-put state :native-render-fragment-template
fragment-template)
(plist-put state :native-render-p t)
rendered))
rendered)
(ebox-native-commit--downgrade-candidate
state "Native rendering returned an incomplete frame")))
(error
(plist-put state :native-render-fallback
(error-message-string err))
nil)))))
(ebox-native-commit--downgrade-candidate
state (error-message-string err)))))))
(provide 'ebox-native-commit)
;;; ebox-native-commit.el ends here

View File

@ -20,6 +20,8 @@
(declare-function ebox-native--module-layout-ready-p "ebox_native_reflow" ())
(declare-function ebox-native--module-create-session "ebox_native_reflow"
(workers max-jobs max-results max-result-bytes))
(declare-function ebox-native--module-fork-confirmed "ebox_native_reflow"
(session))
(declare-function ebox-native--module-submit "ebox_native_reflow"
(session generation payload))
(declare-function ebox-native--module-ready-p "ebox_native_reflow"
@ -93,7 +95,7 @@
(defvar ebox--flex-content-min-width-table)
(defconst ebox-native-reflow-abi-version "9:7:12"
(defconst ebox-native-reflow-abi-version "10:7:12"
"Version tuple shared by the native module, layout IR, and render tape.")
(defconst ebox-native-reflow--minimum-rust-version "1.82.0"
@ -1321,18 +1323,20 @@ Ebox nodes, identities, text, properties, or publication state."
:layout-fragment-cache (make-hash-table :test 'equal)
:layout-fragment-revision 0)))
(cl-defun ebox-native-reflow-fork-session
(session &key (workers 1) (max-jobs 4) (max-results 4)
(max-result-bytes ebox-native-reflow-max-result-bytes))
(defun ebox-native-reflow-fork-session (session)
"Return an unpublished native child of committed SESSION.
The child owns a fresh Rust handle, while immutable compiled layout facts and
a private copy of the fragment index seed its first frame. Pending/confirmed
frame state deliberately stays with SESSION because it names that handle."
(ebox-native-reflow--live-handle session)
(let ((fork
(ebox-native-reflow-create-session
:workers workers :max-jobs max-jobs :max-results max-results
:max-result-bytes max-result-bytes)))
The Rust child owns fresh queues, workers, generation and pending state, and
shares only SESSION's immutable confirmed baseline. Elisp compiler facts are
copied separately; no pending frame or registered Rust document is inherited."
(let* ((source-handle (ebox-native-reflow--live-handle session))
(fork
(ebox-native-reflow--make-session
:handle (ebox-native--module-fork-confirmed source-handle)
:generation 0
:layout-fragment-cache (make-hash-table :test 'equal)
:layout-fragment-revision 0)))
(condition-case err
(progn
(setf (ebox-native-reflow-session-styles fork)
(copy-sequence (ebox-native-reflow-session-styles session))
(ebox-native-reflow-session-layout-package fork)
@ -1342,7 +1346,10 @@ frame state deliberately stays with SESSION because it names that handle."
(ebox-native-reflow-session-layout-fragment-cache session))
(ebox-native-reflow-session-layout-fragment-revision fork)
(ebox-native-reflow-session-layout-fragment-revision session))
fork))
fork)
(error
(ebox-native-reflow-release-session fork)
(signal (car err) (cdr err))))))
(defun ebox-native-reflow--live-handle (session)
"Return SESSION's live module handle or signal an error."
@ -2469,9 +2476,15 @@ per call, and no call recursively visits the captured Ebox tree."
(inherited-dirty-ids
(plist-get state :native-inherited-dirty-node-ids))
(touched-set
(and (plist-get state :native-topology-stable-p)
(plist-member state :native-touched-node-ids)
(and (plist-member state :native-touched-node-ids)
(proper-list-p touched-ids)
;; A structural native object delta still identifies the
;; exact changed node partition. Retain the compiled
;; fragments for its untouched complement; only stable
;; topology is required to reuse TP objects, not to reuse
;; pure layout fragments.
(or (plist-get state :native-topology-stable-p)
(consp touched-ids))
(make-hash-table :test 'equal)))
(inherited-touched-set
(and touched-set inherited-dirty-ids
@ -2484,6 +2497,12 @@ per call, and no call recursively visits the captured Ebox tree."
(if retained-fast-p
(copy-hash-table old-cache)
(make-hash-table :test 'equal)))
(postorder-id-set
(let ((set (make-hash-table :test 'equal)))
(dotimes (index (length postorder))
(puthash (plist-get (aref postorder index) :node-id)
t set))
set))
(ebox-native-reflow--compile-styles
(append (ebox-native-reflow-session-styles session) nil))
(ebox-native-reflow--compile-property-templates
@ -2501,6 +2520,16 @@ per call, and no call recursively visits the captured Ebox tree."
(make-hash-table :test 'equal))
(ebox--render-string-max-pixel-width-cache
(make-hash-table :test 'eq)))
;; Structural Range updates replace a bounded item partition. The
;; untouched shell remains cache-valid, while departed item ids must
;; not accumulate and make the next update miss the retained-cache
;; cardinality proof.
(when retained-fast-p
(maphash
(lambda (node-id _entry)
(unless (gethash node-id postorder-id-set)
(remhash node-id new-cache)))
new-cache))
(dolist (node-id touched-ids)
(when touched-set (puthash node-id t touched-set)))
(when (and touched-set inherited-dirty-ids)

View File

@ -45,7 +45,7 @@
"Bounded persistent document-order handle delta."
base replacements removed appended local-handles handles depth)
(defconst ebox-source--persistent-max-depth 32
(defconst ebox-source--persistent-max-depth 4
"Maximum SourceIndex overlay depth before exact compaction.")
(defconst ebox-source--memo-missing (make-symbol "ebox-source-memo-missing")
@ -54,6 +54,18 @@
(defconst ebox-source--memo-unset (make-symbol "ebox-source-memo-unset")
"Private marker for a persistent-table key not present in its memo.")
(defconst ebox-source--lookup-missing
(make-symbol "ebox-source-lookup-missing")
"Private marker for an absent persistent-table lookup.")
(defconst ebox-source--local-missing
(make-symbol "ebox-source-local-missing")
"Private marker for an absent binding in one table overlay.")
(defconst ebox-source--order-missing
(make-symbol "ebox-source-order-missing")
"Private marker for a handle without an order replacement.")
(defun ebox-source--flat-table (hash test)
"Seal HASH as a flat immutable source table using TEST."
(ebox-source--make-table
@ -72,9 +84,9 @@
"Return KEY's value from persistent or flat TABLE, or nil."
(cond
((ebox-source-table-p table)
(let ((missing (make-symbol "ebox-source-table-missing")))
(let ((value (ebox-source--table-get table key missing)))
(unless (eq value missing) value))))
(let ((value
(ebox-source--table-get table key ebox-source--lookup-missing)))
(unless (eq value ebox-source--lookup-missing) value)))
((hash-table-p table) (gethash key table))
((null table) nil)
(t (signal 'wrong-type-argument (list 'hash-table-p table)))))
@ -94,12 +106,12 @@
(and memo (gethash key memo ebox-source--memo-unset))))
(if (and memo (not (eq memoized ebox-source--memo-unset)))
(if (eq memoized ebox-source--memo-missing) missing memoized)
(let* ((local-missing (make-symbol "ebox-source-local-missing"))
(local
(gethash key (ebox-source-table-added table) local-missing))
(let* ((local
(gethash key (ebox-source-table-added table)
ebox-source--local-missing))
(resolved
(cond
((not (eq local local-missing)) local)
((not (eq local ebox-source--local-missing)) local)
((gethash key (ebox-source-table-removed table))
ebox-source--memo-missing)
((ebox-source-table-base table)
@ -126,8 +138,8 @@
(defun ebox-source--table-member-p (table key)
"Return non-nil when persistent TABLE contains KEY."
(let ((missing (make-symbol "ebox-source-missing")))
(not (eq (ebox-source--table-get table key missing) missing))))
(not (eq (ebox-source--table-get table key ebox-source--lookup-missing)
ebox-source--lookup-missing)))
(defun ebox-source--table-overlay (base added removed count)
"Return bounded persistent BASE overlaid by ADDED and REMOVED with COUNT."
@ -146,11 +158,13 @@
(defun ebox-source--order-apply (handles replacements removed appended)
"Apply order delta to HANDLES and return a fresh exact order."
(let ((missing (make-symbol "ebox-source-order-missing")) result)
(let (result)
(dolist (handle handles)
(let ((replacement (gethash handle replacements missing)))
(let ((replacement
(gethash handle replacements ebox-source--order-missing)))
(cond
((not (eq replacement missing)) (push replacement result))
((not (eq replacement ebox-source--order-missing))
(push replacement result))
((gethash handle removed) nil)
(t (push handle result)))))
(append (nreverse result) (copy-sequence appended))))

View File

@ -14,7 +14,11 @@
(require 'cl-lib)
(require 'ebox-canonical)
(require 'ebox-surface)
(require 'tp-transaction)
(defvar tp-transaction-protocol)
(defconst ebox-framework-spi--tp-v2-available-p
(require 'tp-transaction nil t)
"Non-nil when TP exposes the additive transaction protocol module.")
(declare-function ebox--render-to-buffer-internal
"ebox" (buffer-or-name input options &optional participant))
@ -156,13 +160,28 @@ 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 ((participant
(let* ((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
:publish framework-stage
:rollback framework-rollback
:publish
(lambda (report)
(plist-put report :framework-initial-observation-started
started)
(funcall framework-stage report))
:rollback
(lambda (report)
(cl-remf report :framework-initial-observation-started)
(funcall framework-rollback report))
:state 'unpublished
:diagnostics nil)))
(ebox--render-to-buffer-internal buffer input nil participant))))
(unwind-protect
(ebox--render-to-buffer-internal buffer input nil participant)
(unless existing-bridge
(ebox-surface-cleanup-buffer-observer buffer))))))
(defun ebox-framework-spi-update
(buffer-or-name input framework-stage framework-rollback)
@ -173,7 +192,7 @@ Return the same completed legacy report object observed by FRAMEWORK-STAGE."
(ebox-commit buffer-or-name input framework-stage framework-rollback))
(defun ebox-framework-spi-initial-observation-reports (report)
"Return TP/Ebox provider reports reconstructed from completed initial REPORT.
"Return TP/Ebox provider snapshots captured by completed initial REPORT.
The reports are defensive observational snapshots; they expose no publication
or promotion capability."
(unless (and (proper-list-p report)
@ -181,27 +200,31 @@ or promotion capability."
(integerp (plist-get report :tp-transaction-id)))
(signal 'ebox-framework-spi-provider-error
(list :invalid-initial-report report)))
(let* ((transaction-id (plist-get report :tp-transaction-id))
(tp-report
(list :provider 'tp
:stage 'publication
:status 'success
:duration-ms 0.0
:correlation-id transaction-id
:transaction-id transaction-id
:surface-revision (plist-get report :surface-revision)
:text-operations (plist-get report :tp-text-operations)
:property-operations
(plist-get report :tp-property-operations)
:operation-count (plist-get report :tp-operation-count)))
(ebox-report (copy-tree report)))
(dolist (entry `((:provider . ebox)
(:stage . mount)
(:status . success)
(:duration-ms . 0.0)
(:correlation-id . ,transaction-id)))
(setq ebox-report (plist-put ebox-report (car entry) (cdr entry))))
(list tp-report ebox-report)))
(let ((reports
(plist-get report :framework-initial-observation-reports)))
(cond
((and (proper-list-p reports)
(= (length reports) 2)
(equal (mapcar (lambda (item)
(plist-get item :provider))
reports)
'(tp ebox))
(cl-every
(lambda (item)
(and (numberp (plist-get item :duration-ms))
(>= (plist-get item :duration-ms) 0.0)))
reports))
(copy-tree reports))
((cl-find 'framework-report-finalization
(plist-get report :framework-participant-diagnostics)
:key (lambda (entry) (plist-get entry :phase)))
;; The accepted publication remains authoritative. Its completed
;; report carries the failure; observer replay is skipped, never
;; reclassified as a rollback-capable provider error.
nil)
(t
(signal 'ebox-framework-spi-provider-error
(list :missing-initial-observation-reports report))))))
(defun ebox-framework-spi--operation
(kind function argument-schema)
@ -285,7 +308,31 @@ 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-spi)
;;; ebox-spi.el ends here

View File

@ -20,6 +20,7 @@
(require 'ebox-incremental)
(require 'tp-reactive)
(require 'tp-surface)
(require 'tp)
(defvar ebox-region-types)
(defvar ebox--region-box-table)
@ -46,6 +47,50 @@
(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")
(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))))))
(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)))))
(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))))
(defvar ebox-surface--inline-style-value-cache nil
"Per-projection cache for selector-free computed inline styles.")
@ -67,6 +112,10 @@
(declare-function ebox--ensure-node-id "ebox" (node))
(declare-function ebox-native-commit-render
"ebox-native-commit" (state node))
(declare-function ebox-native-commit-confirm-published-frame
"ebox-native-commit" (state published-revision))
(declare-function ebox-native-commit-attach-confirmed-base
"ebox-native-commit" (previous-state state))
(declare-function ebox-native-reflow-frame-fragments
"ebox-native-reflow" (frame))
(declare-function ebox-native-reflow-layout-ready-p
@ -94,15 +143,29 @@ the winning client state; otherwise only the candidate is disposable."
(committed-session
(and committed-p surface
(plist-get (tp-surface-client-state surface)
:native-sync-session))))
:native-sync-session)))
diagnostics)
(cl-labels
((release-loser
(session)
(let ((inhibit-quit t) (quit-flag nil))
(condition-case condition
(ebox-surface--release-native-session session)
((error quit)
(push (list :phase 'native-session-retirement
:session-id (sxhash-eq session)
:condition (copy-tree condition))
diagnostics))))))
(if committed-p
(dolist (session
(delete-dups (delq nil (list old-session candidate-session))))
(delete-dups
(delq nil (list old-session candidate-session))))
(unless (eq session committed-session)
(ebox-surface--release-native-session session)))
(release-loser session)))
(when (and candidate-session
(not (eq candidate-session old-session)))
(ebox-surface--release-native-session candidate-session)))))
(release-loser candidate-session))))
(nreverse diagnostics)))
(declare-function tp-object-reuse "tp-surface" (context object))
(declare-function tp-object-ensure-at
"tp-surface" (context parent key kind position))
@ -207,6 +270,22 @@ the winning client state; otherwise only the candidate is disposable."
publish rollback state report diagnostics runtime-state scroll-keys
scroll-diagnostics)
(defun ebox-surface--record-native-retirement-diagnostics
(surface participant diagnostics)
"Record native retirement DIAGNOSTICS for SURFACE and PARTICIPANT."
(let ((state (tp-surface-client-state surface)))
(if diagnostics
(progn
(plist-put state :native-retirement-diagnostics
(copy-tree diagnostics))
(when participant
(setf (ebox-surface--framework-participant-diagnostics participant)
(append
(ebox-surface--framework-participant-diagnostics participant)
(copy-tree diagnostics)))))
(cl-remf state :native-retirement-diagnostics)))
diagnostics)
(defvar-local ebox-surface--context-signals nil
"Buffer-scoped TP signals consumed by the mounted Ebox producer.")
@ -519,8 +598,7 @@ Scoped rules remain tree-dependent because scope distance reads ancestors."
(defun ebox-surface--style-consuming-node-p (node)
"Return non-nil when NODE owns a paintable style surface."
(and (eq (plist-get node :ebox-type) 'box)
(not (eq (plist-get node :ebox-kind) 'text))))
(eq (plist-get node :ebox-type) 'box))
(defun ebox-surface--inherited-style-consumer-p (node)
"Return non-nil when NODE materializes inherited text style.
@ -712,10 +790,21 @@ Headless/test windows retain the body width fallback."
(with-current-buffer buffer ebox-surface--buffer-surface)))
(and (tp-surface-live-p surface) surface))))
(defun ebox-surface--buffer-client-state (buffer)
"Return BUFFER's live TP client state for incremental context reads."
(when-let* ((surface (ebox-surface--live-buffer-surface buffer)))
(tp-surface-client-state surface)))
(defun ebox-surface-buffer-mounted-p (buffer)
"Return non-nil when BUFFER has a live Ebox TP surface."
(not (null (ebox-surface--live-buffer-surface buffer))))
(defun ebox-surface-buffer-revision (buffer)
"Return BUFFER's committed TP surface revision or fail when unmounted."
(if-let* ((surface (ebox-surface--live-buffer-surface buffer)))
(tp-surface-revision surface)
(error "Ebox buffer has no committed surface revision: %S" buffer)))
(defun ebox-surface-region-mounts (buffer region-id &optional roles)
"Return REGION-ID's TP-owned output mounts in BUFFER.
When ROLES is non-nil, keep only direct mounts owning a listed Ebox role.
@ -763,12 +852,16 @@ ROLES has the same filtering meaning as in `ebox-surface-region-mounts'."
(or (plist-get tp-report :property-operations) 0))
(patch-count (+ text-operations property-operations))
(report (copy-sequence report-base))
(native-fallback-reason
(or (plist-get state :native-render-fallback)
(plist-get state :native-session-setup-failure)))
(scoped-p (and (not (plist-get tp-report :full-root))
(> (or (plist-get tp-report :scope-count) 0) 0)))
(planned-publication-scope (plist-get report :publication-scope)))
(dolist (entry
`((:runtime-published . t)
(:runtime-revision . ,(plist-get state :runtime-revision))
(:projection-kind . ,(plist-get state :projection-kind))
(:native-frame-kind . ,(plist-get state :native-frame-kind))
(:surface-revision . ,(tp-surface-revision surface))
(:publication-scope . tp-surface)
@ -790,6 +883,27 @@ ROLES has the same filtering meaning as in `ebox-surface-region-mounts'."
(:removed-objects . ,(plist-get tp-report :removed-objects))
(:moved-objects . ,(plist-get tp-report :moved-objects))))
(setq report (plist-put report (car entry) (cdr entry))))
(when native-fallback-reason
;; Planner fields describe the rejected native attempt. Replace them
;; with the ordinary full-surface publication that actually committed;
;; preserve the attempt only as report diagnostics.
(dolist (key '(:owner-ids :owner-type
:constraint-owner-id :constraint-owner-type))
(cl-remf report key))
(setq report (plist-put report :strategy 'ordinary-fallback))
(setq report
(plist-put report :patch-ops
(and (> patch-count 0) '(tp-surface))))
(setq report (plist-put report :patch-count patch-count))
(setq report (plist-put report :render-scope 'surface))
(setq report (plist-put report :publication-scope 'root))
(setq report (plist-put report :root-rerender t))
(setq report (plist-put report :native-attempt 'failed))
(setq report
(plist-put report :native-fallback-reason
native-fallback-reason))
(cl-remf state :native-render-fallback)
(cl-remf state :native-session-setup-failure))
(unless (plist-member report :strategy)
(setq report
(plist-put report :strategy
@ -871,12 +985,87 @@ ROLES has the same filtering meaning as in `ebox-surface-region-mounts'."
diagnostics)
(let* ((state
(ebox-surface--framework-participant-runtime-state participant))
(report (ebox-surface--framework-participant-report participant)))
(report (ebox-surface--framework-participant-report participant))
(started
(plist-get report :framework-initial-observation-started)))
(when started
(let* ((surface (plist-get state :surface))
(tp-report (tp-surface-report surface))
(transaction-id (plist-get tp-report :transaction-id))
(ebox-report (copy-tree report)))
(cl-remf report :framework-initial-observation-started)
(cl-remf ebox-report :framework-initial-observation-started)
(dolist (entry `((:provider . tp)
(:stage . publication)
(:status . success)
(:correlation-id . ,transaction-id)))
(setq tp-report (plist-put tp-report (car entry) (cdr entry))))
(dolist (entry `((:provider . ebox)
(:stage . mount)
(:status . success)
(:duration-ms
. ,(* 1000.0 (- (float-time) started)))
(:correlation-id . ,transaction-id)
(:framework-participant-state . completed)))
(setq ebox-report (plist-put ebox-report (car entry) (cdr entry))))
(setq report
(plist-put
report :framework-initial-observation-reports
(list tp-report ebox-report)))))
(plist-put state :last-update-report
(ebox-surface--participant-report
participant report 'completed)))
(setf (ebox-surface--framework-participant-state participant) 'completed)))
(defun ebox-surface--participant-complete-postaccept (participant diagnostics)
"Complete PARTICIPANT with DIAGNOSTICS after accept without throwing."
(when participant
(let ((inhibit-quit t) (quit-flag nil) completion-condition)
(condition-case condition
(ebox-surface--participant-complete participant diagnostics)
((error quit) (setq completion-condition condition)))
(when completion-condition
(let* ((diagnostic
(list :phase 'framework-report-finalization
:condition (copy-tree completion-condition)))
(state
(ebox-surface--framework-participant-runtime-state participant))
(report
(ebox-surface--framework-participant-report participant)))
(push diagnostic
(ebox-surface--framework-participant-diagnostics participant))
(setf (ebox-surface--framework-participant-scroll-diagnostics
participant)
diagnostics)
(unwind-protect
(condition-case secondary
(progn
(cl-remf report :framework-initial-observation-started)
(setq report
(plist-put report :framework-participant-state
'completed)
report
(plist-put
report :framework-participant-diagnostics
(copy-tree
(ebox-surface--framework-participant-diagnostics
participant)))
report
(plist-put report :scroll-finalization-diagnostics
(copy-tree diagnostics)))
(setf (ebox-surface--framework-participant-report participant)
report)
(when state
(plist-put state :last-update-report report)))
((error quit)
(push (list :phase 'framework-report-finalization-diagnostic
:condition (copy-tree secondary))
(ebox-surface--framework-participant-diagnostics
participant))))
(setf (ebox-surface--framework-participant-state participant)
'completed)))))
participant))
(defun ebox-surface--publish-runtime-state
(buffer surface old-state report-base after-publication
&optional framework-participant observation-context)
@ -886,7 +1075,7 @@ runs after TP and Ebox point at the same candidate generation.
FRAMEWORK-PARTICIPANT owns paired framework publication when non-nil."
(let (new-state region-snapshot scroll-snapshot
old-surface old-mirror region-keys scroll-keys)
(tp-transaction-participate
(ebox-surface--transaction-participate
(list 'ebox/runtime buffer)
(lambda ()
(setq new-state (tp-surface-client-state surface)
@ -928,6 +1117,13 @@ FRAMEWORK-PARTICIPANT owns paired framework publication when non-nil."
(plist-put new-state :surface surface)
(plist-put new-state :runtime-revision
(1- (tp-surface-revision surface)))
;; TP now exposes this exact candidate revision, but the surrounding
;; transaction can still restore both TP and Ebox state. Confirm the
;; private Rust frame here so a failure aborts publication and the
;; old confirmed session remains untouched.
(when (plist-get new-state :native-sync-pending)
(ebox-native-commit-confirm-published-frame
new-state (tp-surface-revision surface)))
;; A fixed-basis role-owned patch rendered against the previous
;; allocation. Its installed details describe that proof render,
;; not the now-committed TP mounts. Drop only those owners' derived
@ -1027,7 +1223,7 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(producer
(ebox-surface-producer
source old-state preserve-identities-p state-overrides signals))
success)
success native-settled-p)
(unwind-protect
(progn
(let ((ebox-surface--font-window
@ -1050,23 +1246,34 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
buffer surface old-state report-base after-publication
framework-participant observation-context)))
(setq success t)
(let ((scroll-keys
(let* ((scroll-keys
(if framework-participant
(ebox-surface--framework-participant-scroll-keys
framework-participant)
(let ((new-state (tp-surface-client-state surface)))
(delete-dups
(append
(copy-sequence (plist-get old-state :scroll-region-ids))
(copy-sequence
(plist-get new-state :scroll-region-ids))))))))
(ebox-surface--participant-complete
framework-participant
(plist-get old-state :scroll-region-ids))
(copy-sequence
(plist-get new-state :scroll-region-ids)))))))
(scroll-diagnostics
(ebox-incremental--finalize-declarative-scroll-publication
scroll-keys)))
surface)
scroll-keys))
(native-diagnostics
(ebox-surface--settle-native-session
old-state state-overrides surface success)
old-state state-overrides surface t)))
(setq native-settled-p t)
(ebox-surface--record-native-retirement-diagnostics
surface framework-participant native-diagnostics)
(ebox-surface--participant-complete-postaccept
framework-participant scroll-diagnostics))
surface)
(unless native-settled-p
;; Candidate cleanup is post-transaction and must not hide the primary
;; publication condition. The helper contains every release fault.
(ebox-surface--settle-native-session
old-state state-overrides surface success))
(when (and signals-created-p (not success))
(ebox-surface--dispose-signals signals)))))
@ -1103,7 +1310,7 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(error "Ebox mounted surface has no live context signals"))
(tp-with-transaction
(ebox-surface--stage-signal-values signals values)
(tp-transaction-participate
(ebox-surface--transaction-participate
(list 'ebox/context-no-op buffer)
(lambda ()
(unless (eq state (tp-surface-client-state surface))
@ -1478,7 +1685,10 @@ the projection roots a second time."
(plist-get state :scroll-fast-window-p)))
(native-retained-p
(and (eq projection-kind 'native-frame)
(plist-get state :native-topology-stable-p)))
(plist-get state :native-topology-stable-p)
(plist-get state :native-render-frame)
(plist-get (plist-get state :native-render-frame)
:native-patch)))
(full-surface-p
(or (and scroll-fast-p
(plist-get state :retained-scroll-content-p)
@ -1841,6 +2051,13 @@ may be adopted without another structural copy or identity reconciliation.
When SOURCE-PATH-COPIED-P is non-nil, SOURCE shares untouched published nodes;
the surface must preserve those shared nodes while consuming the candidate.
FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
;; Ebox owns the complete mounted TP surface. Caller narrowing is an editor
;; view and must neither constrain candidate coordinates nor become an
;; accidental publication scope. Isolate it while every full-buffer fact
;; and TP operation runs against the complete target.
(with-current-buffer buffer
(save-restriction
(widen)
(let* ((observation-context ebox-surface--observation-context)
(surface (ebox-surface--live-buffer-surface buffer))
(old-state (and surface (tp-surface-client-state surface))))
@ -1858,7 +2075,8 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
after-publication t state-overrides
framework-participant)
(let* ((context-values
(ebox-surface--context-values buffer old-state state-overrides))
(ebox-surface--context-values
buffer old-state state-overrides))
(signals-result
(ebox-surface--ensure-signals buffer context-values))
(signals (car signals-result))
@ -1868,10 +2086,11 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
source old-state t state-overrides signals projection-kind
source-isolated-p
(plist-get state-overrides :source-path-copied-p)))
success)
success native-settled-p)
(when (and (null on-mismatch)
(or (plist-get state-overrides :owner-scoped-proofs)
(plist-get state-overrides :scroll-state-transaction)
(plist-get state-overrides
:scroll-state-transaction)
(> (length scope-node-ids) 1)))
(setq on-mismatch 'root))
(unwind-protect
@ -1879,7 +2098,8 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
(let ((ebox-surface--font-window
(plist-get context-values :display-window)))
(tp-with-transaction
(ebox-surface--stage-signal-values signals context-values)
(ebox-surface--stage-signal-values
signals context-values)
(tp-surface-update-scoped
surface objects producer
(list :on-mismatch on-mismatch :return-report nil))
@ -1887,26 +2107,36 @@ FRAMEWORK-PARTICIPANT retains post-TP publication facts when non-nil."
buffer surface old-state report-base after-publication
framework-participant observation-context)))
(setq success t)
(let ((scroll-keys
(let* ((scroll-keys
(if framework-participant
(ebox-surface--framework-participant-scroll-keys
framework-participant)
(let ((new-state (tp-surface-client-state surface)))
(let ((new-state
(tp-surface-client-state surface)))
(delete-dups
(append
(copy-sequence
(plist-get old-state :scroll-region-ids))
(copy-sequence
(plist-get new-state :scroll-region-ids))))))))
(ebox-surface--participant-complete
framework-participant
(plist-get new-state
:scroll-region-ids)))))))
(scroll-diagnostics
(ebox-incremental--finalize-declarative-scroll-publication
scroll-keys scroll-prefetch-delay)))
surface)
scroll-keys scroll-prefetch-delay))
(native-diagnostics
(ebox-surface--settle-native-session
old-state state-overrides surface success)
old-state state-overrides surface t)))
(setq native-settled-p t)
(ebox-surface--record-native-retirement-diagnostics
surface framework-participant native-diagnostics)
(ebox-surface--participant-complete-postaccept
framework-participant scroll-diagnostics))
surface)
(unless native-settled-p
(ebox-surface--settle-native-session
old-state state-overrides surface success))
(when (and signals-created-p (not success))
(ebox-surface--dispose-signals signals))))))))
(ebox-surface--dispose-signals signals))))))))))
(defun ebox-surface--clear-runtime-attachments (root)
"Clear TP handles and render-cache attachments below ROOT."
@ -2479,16 +2709,9 @@ dependencies, or any malformed index conservatively preserves root planning."
(ecss-computed-style-p
(plist-get candidate-node :ebox-computed-style))
(ebox-style--closed-declarations-p
candidate-declarations))))
(and style-state candidate-node subject
(or locally-computed-p
(and
(or (ebox-style-declarations-equal-p
candidate-declarations
(plist-get style-state :declarations))
(ebox-style--paint-declarations-equivalent-p
(plist-get style-state :declarations)
candidate-declarations))
candidate-declarations)))
(selector-stable-p
(and candidate-node subject
(equal (ebox-tree-node-id
candidate-source-index candidate-node)
(ecss-subject-id subject))
@ -2501,15 +2724,27 @@ dependencies, or any malformed index conservatively preserves root planning."
(equal
(mapcar
(lambda (attribute)
(cons
(substring (symbol-name (car attribute)) 1)
(cons (substring (symbol-name (car attribute)) 1)
(cdr attribute)))
(ebox-tree-node-attributes
candidate-source-index candidate-node))
(ecss-subject-attributes subject))
(equal (ebox-tree-node-state
candidate-source-index candidate-node)
(ecss-subject-states subject))))
(ecss-subject-states subject)))))
(and style-state candidate-node subject
(or selector-stable-p
(and locally-computed-p
(null (car stylesheet-signature))))
(or locally-computed-p
(and
(or (ebox-style-declarations-equal-p
candidate-declarations
(plist-get style-state :declarations))
(ebox-style--paint-declarations-equivalent-p
(plist-get style-state :declarations)
candidate-declarations))
selector-stable-p))
(equal stylesheet-signature
(plist-get style-state :stylesheet-signature))
(ebox-surface--static-style-state-p style-state)))))
@ -3624,7 +3859,7 @@ causes the caller to use the ordinary full projection."
(if (not affected-p)
(progn
(cl-remf copy :text)
(plist-put copy :text-source-p t)
(setq copy (plist-put copy :text-source-p t))
(push copy pieces))
(when (plist-get old-fragment :face-baseline-known-p)
(plist-put copy :face-baseline
@ -4931,6 +5166,8 @@ but cannot introduce or remove a viewport expression or node identity."
(ebox--new-buffer-render-state root))
(copy-sequence state-overrides))))
(plist-put state :root-node root)
(when (plist-get state :native-sync-confirmed-p)
(ebox-native-commit-attach-confirmed-base previous-state state))
(plist-put state :projection-kind projection-kind)
(plist-put state :cascade-active-p stylesheet-active-p)
(plist-put state :cascade-required-p cascade-required-p)
@ -5269,6 +5506,13 @@ published runtime and must be consumed without clearing those shared nodes."
(ebox-surface--render-candidate state)))
(t
(ebox-surface--render-candidate state)))))
(when (and (eq projection-kind 'native-frame)
(not (eq (plist-get state :projection-kind)
'native-frame)))
;; Native delta projection already owns a complete object map.
;; The existing Elisp renderer produced RENDERED exactly once;
;; only its final publication semantics need to become ordinary.
(setq projection-kind nil))
(when (and initial-native-p
(not (plist-get state :native-render-p)))
(plist-put state :projection-kind nil))
@ -5334,6 +5578,15 @@ also bypass candidate copying and reconciliation without clearing those nodes."
source nil t
(and source-index (list :source-base-index source-index))))))
;; Install the only upward-facing read port after every callback is defined.
;; Incremental planning owns no Surface symbol or publication capability.
(ebox-incremental-install-context-port
(ebox-incremental-context-port-create
:buffer-state #'ebox-surface--buffer-client-state
:region-mounts #'ebox-surface-region-mounts
:cascade-owner-proof-p #'ebox-surface--cascade-local-owner-proof-p
:prepare-inline-styles #'ebox-surface-prepare-inline-candidate-styles))
(provide 'ebox-surface)
;;; ebox-surface.el ends here

27
ebox.el
View File

@ -1,8 +1,8 @@
;;; ebox.el --- Pixel-perfect box model renderer -*- lexical-binding: t -*-
;; Author: ebox contributors
;; Version: 2.0.0
;; Package-Requires: ((emacs "29.1") (ecss "0.1.0") (tp "1.0.0"))
;; Version: 2.0.1
;; Package-Requires: ((emacs "29.1") (ecss "0.1.0") (tp "1.0.1"))
;; Keywords: ui, graphics, convenience
;; URL: https://github.com/ginqi7/ebox
@ -76,6 +76,12 @@
(setq ebox--layout-context-port ebox--default-layout-context-port)
(defun ebox-incremental-flush (buffer)
"Flush BUFFER's explicit incremental batch through Surface observation."
(ebox-surface-call-with-observation
buffer 'batch
(lambda () (ebox-incremental--flush buffer))))
(declare-function ebox--buffer-update-report "ebox-incremental" (buffer))
(declare-function ebox--set-buffer-update-report
"ebox-incremental" (buffer report))
@ -94,6 +100,15 @@
: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.
@ -4622,8 +4637,10 @@ replacement."
;;;###autoload
(defun ebox-candidate-replace-range-ref
(candidate range-ref input &optional reuse-map)
"Replace RANGE-REF from canonical INPUT, retaining proven REUSE-MAP slots."
(candidate range-ref input &optional reuse-map retain-item-identities-p)
"Replace RANGE-REF from canonical INPUT, retaining proven REUSE-MAP slots.
When RETAIN-ITEM-IDENTITIES-P is non-nil, Ebox retains candidate-owned items
only after its strict ownership proof; otherwise it uses the normal copy path."
(unless (ebox-canonical-input-p input)
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
(let ((ebox-incremental--source-base-index
@ -4631,7 +4648,7 @@ replacement."
(ebox-incremental-candidate-replace-range-ref
candidate range-ref
(copy-sequence (ebox-canonical-input--nodes input))
reuse-map)))
reuse-map retain-item-identities-p)))
;;;###autoload
(defun ebox-candidate-replace-root (candidate input)

View File

@ -38,6 +38,8 @@ extern void *ebox_native_session_create(uint32_t workers,
uint32_t max_results,
uint64_t max_result_bytes,
ebox_native_bytes *error);
extern void *ebox_native_session_fork_confirmed(void *source,
ebox_native_bytes *error);
extern int64_t ebox_native_session_submit(void *session,
uint64_t generation,
const unsigned char *payload,
@ -162,7 +164,7 @@ ebox_module_version(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
(void) nargs;
(void) args;
(void) data;
static const char version[] = "9:7:12";
static const char version[] = "10:7:12";
return env->make_string(env, version, (ptrdiff_t) (sizeof version - 1));
}
@ -241,6 +243,24 @@ ebox_module_create_session(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
return env->make_user_ptr(env, ebox_session_finalizer, session);
}
static emacs_value
ebox_module_fork_confirmed(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void) nargs;
(void) data;
void *source = ebox_session_pointer(env, args[0]);
if (source == NULL ||
env->non_local_exit_check(env) != emacs_funcall_exit_return)
return ebox_nil(env);
ebox_native_bytes error = {0};
void *session = ebox_native_session_fork_confirmed(source, &error);
if (session == NULL)
return ebox_signal_native_error(env, error,
"Native confirmed session fork failed");
return env->make_user_ptr(env, ebox_session_finalizer, session);
}
static emacs_value
ebox_module_submit(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
void *data)
@ -922,6 +942,9 @@ ebox_module_init_impl(struct emacs_runtime *runtime)
ebox_bind_function(env, "ebox-native--module-create-session", 4, 4,
ebox_module_create_session,
"Create one bounded private native session.") ||
ebox_bind_function(env, "ebox-native--module-fork-confirmed", 1, 1,
ebox_module_fork_confirmed,
"Fork one confirmed private native session.") ||
ebox_bind_function(env, "ebox-native--module-submit", 3, 3,
ebox_module_submit,
"Submit one private native control batch.") ||

View File

@ -450,7 +450,7 @@ struct RuntimeState {
baseline_misses: u64,
base_renders: u64,
target_renders: u64,
confirmed_baseline: Option<ConfirmedBaseline>,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
}
#[derive(Clone, Copy, Debug)]
@ -528,6 +528,16 @@ impl Session {
max_jobs: usize,
max_results: usize,
max_result_bytes: usize,
) -> Result<Box<Self>, String> {
Self::new_with_confirmed_baseline(workers, max_jobs, max_results, max_result_bytes, None)
}
fn new_with_confirmed_baseline(
workers: usize,
max_jobs: usize,
max_results: usize,
max_result_bytes: usize,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
) -> Result<Box<Self>, String> {
if workers == 0 || max_jobs == 0 || max_results == 0 || max_result_bytes == 0 {
return Err("Native reflow capacities must be positive".to_owned());
@ -552,7 +562,7 @@ impl Session {
baseline_misses: 0,
base_renders: 0,
target_renders: 0,
confirmed_baseline: None,
confirmed_baseline,
}),
readiness_channel: Mutex::new(None),
job_available: Condvar::new(),
@ -584,6 +594,27 @@ impl Session {
}))
}
fn fork_confirmed(&self) -> Result<Box<Self>, String> {
if !self.shared.alive.load(Ordering::Acquire) {
return Err("Native reflow source session is closed".to_owned());
}
let confirmed_baseline = self
.shared
.state
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.confirmed_baseline
.clone()
.ok_or_else(|| "Native reflow source has no confirmed baseline".to_owned())?;
Self::new_with_confirmed_baseline(
self.worker_count,
self.shared.max_jobs,
self.shared.max_results,
self.shared.max_result_bytes,
Some(confirmed_baseline),
)
}
fn submit(&self, generation: u64, payload: &[u8]) -> Result<usize, String> {
let batch = parse_control_batch(payload)?;
if !self.shared.alive.load(Ordering::Acquire) {
@ -819,16 +850,16 @@ impl Session {
.as_ref()
.is_some_and(|baseline| baseline.identity.runtime_revision == confirmed_revision));
};
if confirmed_revision <= pending.confirmed_identity.runtime_revision {
if pending.confirmed_identity.runtime_revision.checked_add(1) != Some(confirmed_revision) {
return Err("Native confirmed frame revision mismatch".to_owned());
}
let mut identity = pending.confirmed_identity;
identity.runtime_revision = confirmed_revision;
state.confirmed_baseline = Some(ConfirmedBaseline {
state.confirmed_baseline = Some(Arc::new(ConfirmedBaseline {
identity,
tape: pending.tape,
styles: pending.styles,
});
}));
Ok(true)
}
@ -1122,7 +1153,7 @@ fn render_layout_payload(
generation: u64,
key: i64,
max_result_bytes: usize,
confirmed_baseline: Option<ConfirmedBaseline>,
confirmed_baseline: Option<Arc<ConfirmedBaseline>>,
require_confirmed_patch_base: bool,
) -> RenderedJob {
match payload {
@ -1204,7 +1235,7 @@ fn render_layout_payload(
return Ok((bytes, target, false, 0, 1));
}
let (old, baseline_hit, base_renders) = if let Some(baseline) = base_hit {
(baseline.tape, true, 0)
(baseline.tape.clone(), true, 0)
} else {
(
document.layout_tape(
@ -1499,6 +1530,28 @@ pub extern "C" fn ebox_native_session_create(
}
}
#[no_mangle]
pub extern "C" fn ebox_native_session_fork_confirmed(
source: *mut c_void,
error: *mut NativeBytes,
) -> *mut c_void {
match catch_unwind(AssertUnwindSafe(|| {
let source =
session_ref(source).ok_or_else(|| "Native reflow source session is null".to_owned())?;
source.fork_confirmed()
})) {
Ok(Ok(session)) => Box::into_raw(session) as *mut c_void,
Ok(Err(message)) => {
set_error(error, message);
ptr::null_mut()
}
Err(_) => {
set_error(error, "Native reflow confirmed fork panicked");
ptr::null_mut()
}
}
}
#[no_mangle]
/// Copy and submit one versioned control payload to a live native session.
///
@ -1810,6 +1863,10 @@ mod tests {
u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
}
fn tape_patch_p(bytes: &[u8]) -> bool {
u16::from_le_bytes(bytes[6..8].try_into().unwrap()) & (1 << 2) != 0
}
fn full_tape_character_count(bytes: &[u8]) -> u64 {
read_le_u64(bytes, 92)
}
@ -2005,6 +2062,76 @@ mod tests {
session.stop(true);
}
#[test]
fn confirmed_fork_shares_only_immutable_baseline() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
assert!(parent.fork_confirmed().is_err());
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"root-width":120,"runtime-revision":0,"context-hash":77}]"#,
);
let first_tape = parent.render_sync(1, &first).unwrap();
assert!(!tape_patch_p(&first_tape));
assert!(parent.confirm(1, 1, 1).unwrap());
let parent_before = parent.stats();
let child = parent.fork_confirmed().unwrap();
let child_initial = child.stats();
assert_ne!(child_initial.session_id, parent_before.session_id);
assert_eq!(child_initial.generation, 0);
assert_eq!(child_initial.queued_jobs, 0);
assert_eq!(child_initial.ready_results, 0);
assert_eq!(child_initial.pending_baselines, 0);
assert!(child_initial.confirmed_baseline);
assert!(!child_initial.layout_registered);
parent.stop(true);
let second = proof_layout_payload(
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":1,"context-hash":77}]"#,
);
let second_tape = child.render_sync(1, &second).unwrap();
assert!(tape_patch_p(&second_tape));
let child_rendered = child.stats();
assert_eq!(child_rendered.baseline_hits, 1);
assert_eq!(child_rendered.base_renders, 0);
assert_eq!(child_rendered.target_renders, 1);
assert_eq!(child_rendered.pending_baselines, 1);
assert!(child.confirm(1, 2, 2).unwrap());
assert!(child.stats().confirmed_baseline);
let parent_after = parent.stats();
assert_eq!(parent_after.baseline_hits, parent_before.baseline_hits);
assert_eq!(parent_after.base_renders, parent_before.base_renders);
assert_eq!(parent_after.target_renders, parent_before.target_renders);
assert_eq!(
parent_after.pending_baselines,
parent_before.pending_baselines
);
child.stop(true);
}
#[test]
fn confirmed_fork_falls_back_to_full_frame_on_identity_mismatch() {
let parent = Session::new(1, 4, 4, 64 * 1024).unwrap();
let first = proof_layout_payload(
r#"[{"key":1,"viewport-width":120,"viewport-height":10,"root-width":120,"runtime-revision":0,"context-hash":77}]"#,
);
assert!(!parent.render_sync(1, &first).unwrap().is_empty());
assert!(parent.confirm(1, 1, 1).unwrap());
let child = parent.fork_confirmed().unwrap();
let mismatched = proof_layout_payload(
r#"[{"key":2,"viewport-width":80,"viewport-height":10,"root-width":80,"patch":true,"base-viewport-width":120,"base-viewport-height":10,"base-root-width":120,"runtime-revision":2,"context-hash":77}]"#,
);
let tape = child.render_sync(1, &mismatched).unwrap();
assert!(!tape_patch_p(&tape));
assert_eq!(child.stats().baseline_hits, 0);
assert_eq!(child.stats().base_renders, 0);
assert_eq!(child.stats().target_renders, 1);
parent.stop(true);
child.stop(true);
}
#[test]
fn synchronous_proof_render_returns_full_tape_without_session() {
let payload =

View File

@ -638,6 +638,120 @@
:range-ref-table))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-child-range-candidate-retains-proven-owned-items ()
"A fresh candidate-owned Range item is not copied twice before commit."
(let* ((buffer (generate-new-buffer " *ebox-range-retained-items*"))
(input
(ebox-child-range-test--input
(list (ebox-test-box :key 'new (ebox-test-text "new")))))
(item (ebox-test-root input)))
(unwind-protect
(progn
(ebox-render-to-buffer
buffer
(ebox-test-column
(ebox-test-box :key 'static (ebox-test-text "static"))
(ebox-test-child-range
'items (ebox-test-box :key 'old (ebox-test-text "old")))))
(let* ((candidate (ebox-candidate-begin buffer))
entry)
(ebox-candidate-replace-range-ref
candidate 'items input nil t)
(setq entry (car (ebox-candidate--range-replacements candidate)))
(should
(ebox-incremental--candidate-range-replacement-retain-item-identities-p
entry))
(should (eq item
(car
(ebox-incremental--candidate-range-replacement-items
entry))))
(ebox-commit buffer candidate))
(let* ((state (ebox--buffer-render-state buffer))
(record (gethash 'items (plist-get state :range-ref-table)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get state :node-table)))
(segment
(ebox-child-range--lookup-ref
(plist-get parent :ebox-child-sequence) 'items))
(published (aref (ebox-child-range--segment-payload segment) 0)))
(should (eq published item))
(should (plist-get published :node-id))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-child-range-candidate-retention-falls-back-on-runtime-identity ()
"A retention proof miss uses the ordinary identity-clearing copy path."
(let* ((buffer (generate-new-buffer " *ebox-range-retention-fallback*"))
(input
(ebox-child-range-test--input
(list (ebox-test-box :key 'new (ebox-test-text "new")))))
(item (ebox-test-root input)))
(plist-put item :node-id 999)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer
(ebox-test-column
(ebox-test-child-range
'items (ebox-test-box :key 'old (ebox-test-text "old")))))
(let* ((candidate (ebox-candidate-begin buffer))
entry retained)
(ebox-candidate-replace-range-ref
candidate 'items input nil t)
(setq entry (car (ebox-candidate--range-replacements candidate))
retained
(ebox-incremental--candidate-range-replacement-items entry))
(should-not
(ebox-incremental--candidate-range-replacement-retain-item-identities-p
entry))
(should-not (eq item (car retained)))
(should-not (plist-get (car retained) :node-id))
(ebox-commit buffer candidate))
(should (string-match-p "new" (with-current-buffer buffer
(buffer-string)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-child-range-candidate-retains-fresh-slots-around-reuse ()
"Mixed Range updates retain fresh slots while preserving reused slots."
(let* ((buffer (generate-new-buffer " *ebox-range-mixed-retention*"))
(input
(ebox-child-range-test--input
(list (ebox-test-box :key 'old-one (ebox-test-text "old-one"))
(ebox-test-box :key 'new-two (ebox-test-text "new-two")))))
(fresh (nth 1 (ebox-test-forest input))))
(unwind-protect
(progn
(ebox-render-to-buffer
buffer
(ebox-test-column
(ebox-test-child-range
'items
(ebox-test-box :key 'old-one (ebox-test-text "old-one"))
(ebox-test-box :key 'old-two (ebox-test-text "old-two")))))
(let* ((candidate (ebox-candidate-begin buffer))
entry)
(ebox-candidate-replace-range-ref
candidate 'items input '((0 . 0)) t)
(setq entry (car (ebox-candidate--range-replacements candidate)))
(should
(ebox-incremental--candidate-range-replacement-retain-item-identities-p
entry))
(should (eq fresh
(nth 1
(ebox-incremental--candidate-range-replacement-items
entry))))
(ebox-commit buffer candidate))
(let* ((state (ebox--buffer-render-state buffer))
(record (gethash 'items (plist-get state :range-ref-table)))
(parent (gethash (plist-get record :parent-node-id)
(plist-get state :node-table)))
(segment
(ebox-child-range--lookup-ref
(plist-get parent :ebox-child-sequence) 'items))
(payload (ebox-child-range--segment-payload segment)))
(should (eq fresh (aref payload 1)))
(should (plist-get (aref payload 1) :node-id))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-child-range-stable-keyed-refresh-is-not-structural ()
"Stable keyed Range refreshes must not become root structure updates."
(let ((buffer (generate-new-buffer " *ebox-stable-range-dirty*")))
@ -1404,7 +1518,8 @@
candidate 'items
(ebox-child-range-test--input
(list (ebox-test-box :key 'new :source-identity 'new
(ebox-test-text "new")))))
(ebox-test-text "new"))))
nil t)
(cl-letf (((symbol-function 'accept-change-group)
(lambda (_group) (error "reject final accept"))))
(should-error

View File

@ -74,6 +74,40 @@
: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)
(v2-register (symbol-function 'tp-transaction-participate-v2))
(v1-register (symbol-function 'tp-transaction-participate))
(v2-count 0)
(v1-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))))
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "old")
:width '(80)))
(ebox-commit
buffer (ebox-test-box :key 'root (ebox-test-text "new")
:width '(80)))
(with-current-buffer buffer
(should (string-match-p "new" (buffer-string))))
(list :v2 v2-count :v1 v1-count))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-source-index-promotes-and-rolls-back-atomically ()
"Candidate source indexes promote once and never corrupt retained state."
(let ((buffer (generate-new-buffer " *ebox-source-index-lifecycle*")))
@ -481,6 +515,133 @@
(should (equal (aref (aref target 1) 7) '(3 5)))
(should (equal (aref second 7) '(2)))))
(ert-deftest ebox-native-session-isolation-bootstraps-or-forks-privately ()
"Session isolation creates the first candidate and forks later candidates."
(let ((created (list 'created-session))
(forked (list 'forked-session))
create-arguments fork-argument)
(cl-letf (((symbol-function 'ebox-native-reflow-create-session)
(lambda (&rest arguments)
(setq create-arguments arguments)
created))
((symbol-function 'ebox-native-reflow-fork-session)
(lambda (session &rest _options)
(setq fork-argument session)
forked)))
(let ((candidate (list :native-sync-pending 'stale
:native-sync-confirmed-p t)))
(should (ebox-native-commit--isolate-session nil candidate))
(should (eq (plist-get candidate :native-sync-session) created))
(should (equal create-arguments
'(:workers 1 :max-jobs 4 :max-results 4)))
(should-not fork-argument)
(should-not (plist-get candidate :native-sync-pending))
(should-not (plist-get candidate :native-sync-confirmed-p)))
(let* ((committed (list 'committed-session))
(candidate (list :native-sync-pending 'stale
:native-sync-confirmed-p t)))
(setq create-arguments nil)
(should
(ebox-native-commit--isolate-session
(list :native-sync-session committed) candidate))
(should (eq fork-argument committed))
(should (eq (plist-get candidate :native-sync-session) forked))
(should-not create-arguments)
(should-not (plist-get candidate :native-sync-pending))
(should (plist-get candidate :native-sync-confirmed-p))))))
(ert-deftest ebox-native-published-frame-confirms-exact-runtime-revision ()
"A pending frame is confirmed only at the installed runtime revision."
(let ((session (list 'candidate-session))
confirmed)
(cl-letf (((symbol-function 'ebox-native-reflow-confirm-native-frame)
(lambda (&rest arguments)
(setq confirmed arguments)
t)))
(let ((state (list :native-sync-session session
:native-sync-pending
'(:generation 3 :key 7 :confirmed-revision 11)
:native-sync-confirmed-p nil
:runtime-revision 11)))
(should (eq (ebox-native-commit-confirm-published-frame state 11)
state))
(should (equal confirmed (list session 3 7 11)))
(should (plist-get state :native-sync-confirmed-p))
(should-not (plist-member state :native-sync-pending)))
(let ((state (list :native-sync-session session
:native-sync-pending
'(:generation 3 :key 7 :confirmed-revision 11)
:runtime-revision 10)))
(should-error (ebox-native-commit-confirm-published-frame state 10))
(should (plist-member state :native-sync-pending))))))
(ert-deftest ebox-native-session-isolation-rejects-create-and-fork-errors ()
"A failed private-session operation preserves state and its diagnosis."
(dolist (previous
(list nil (list :native-sync-session (list 'committed-session))))
(let ((candidate (list :native-sync-pending 'unchanged
:native-sync-confirmed-p t)))
(cl-letf (((symbol-function 'ebox-native-reflow-create-session)
(lambda (&rest _) (error "create failed")))
((symbol-function 'ebox-native-reflow-fork-session)
(lambda (&rest _) (error "fork failed"))))
(should-not
(ebox-native-commit--isolate-session
previous candidate))
(should-not (plist-member candidate :native-sync-session))
(should (eq (plist-get candidate :native-sync-pending) 'unchanged))
(should (plist-get candidate :native-sync-confirmed-p))
(let ((failure (plist-get candidate :native-session-setup-failure)))
(should (eq (plist-get failure :phase)
(if previous 'fork 'create)))
(should (eq (car (plist-get failure :condition)) 'error)))))))
(ert-deftest ebox-native-session-setup-failure-enters-public-report ()
"Ordinary fallback reports and consumes a native setup failure."
(let* ((failure '(:phase create :condition (error "setup failed")))
(state (list :runtime-revision 2
:native-session-setup-failure failure))
report)
(cl-letf (((symbol-function 'tp-surface-report-summary)
(lambda (_surface)
'(:transaction-id 7 :text-operations 1
:property-operations 0 :full-root t :scope-count 0)))
((symbol-function 'tp-surface-revision)
(lambda (_surface) 3)))
(setq report
(ebox-surface--commit-report
'surface state
'(:strategy native-frame :publication-scope layout-owners))))
(should (eq (plist-get report :strategy) 'ordinary-fallback))
(should (equal (plist-get report :native-fallback-reason) failure))
(should-not (plist-member state :native-session-setup-failure))))
(ert-deftest ebox-native-session-retirement-contains-release-failures ()
"Losing native sessions all retire and return diagnostics after commit."
(let ((old (list 'old-session))
(candidate (list 'candidate-session))
(committed (list 'committed-session))
released)
(cl-letf (((symbol-function 'tp-surface-client-state)
(lambda (_surface)
(list :native-sync-session committed)))
((symbol-function 'ebox-surface--release-native-session)
(lambda (session)
(push session released)
(when (eq session old)
(error "release failed")))))
(let ((diagnostics
(ebox-surface--settle-native-session
(list :native-sync-session old)
(list :native-sync-session candidate)
'surface t)))
(should (equal (nreverse released) (list old candidate)))
(should (= 1 (length diagnostics)))
(should (eq (plist-get (car diagnostics) :phase)
'native-session-retirement))
(should (eq (car (plist-get (car diagnostics) :condition))
'error))))))
(ert-deftest ebox-native-object-delta-orders-moved-and-new-nodes ()
"A topology delta names parents before moved and introduced children."
(let* ((old-input
@ -573,6 +734,27 @@
:removed-node-ids
(ebox--runtime-node-ids removed-node))))))))
(ert-deftest ebox-native-frame-spec-patches-only-stable-topology ()
"Only a topology-stable candidate may request a confirmed native patch."
(let* ((node
(ebox-test-box :key 'root (ebox-test-text "Frame") :width '(100)))
(state
(list :viewport-width 120 :viewport-height 10
:runtime-revision 3 :display-signature '(display)
:native-sync-confirmed-p t
:native-base-viewport-width 80
:native-base-viewport-height 10
:native-base-root-width 80
:native-topology-stable-p nil))
(full (ebox-native-commit--frame-spec state node)))
(should full)
(should-not (plist-member full :base-viewport-width))
(plist-put state :native-topology-stable-p t)
(let ((patch (ebox-native-commit--frame-spec state node)))
(should (= (plist-get patch :base-viewport-width) 80))
(should (= (plist-get patch :base-viewport-height) 10))
(should (= (plist-get patch :base-root-width) 80)))))
(ert-deftest ebox-style-schema-composition-is-not-per-node-work ()
"Repeated node construction must not rebuild the immutable schema domain."
(let ((package-calls 0)
@ -626,6 +808,34 @@
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-commit-ignores-caller-narrowing ()
"A full Ebox surface commit must not inherit caller narrowing."
(let ((buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-commit-narrowing*")
(ebox-test-box :key 'root (ebox-test-text "Before")
:width '(80))))
report)
(unwind-protect
(progn
(with-current-buffer buffer
(goto-char (1+ (point-min)))
(narrow-to-region (point) (point-max))
(setq report
(ebox-commit
buffer
(ebox-test-box :key 'root (ebox-test-text "After!")
:width '(80)))))
(should (plist-get report :runtime-published))
(should
(string-prefix-p
"After!"
(string-trim-right
(substring-no-properties
(ebox-commit-test--buffer-string buffer))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-commit-preserves-keyed-sibling-identity ()
"Keyed siblings should remain addressable after a reorder commit."
(let* ((buffer (ebox-render-to-buffer
@ -744,6 +954,60 @@
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-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)))
(should-error
(ebox-surface--tp-participant-v2-capability-p
'(:transaction-protocol tp-transaction-protocol-v1+v2
:structured-participant-api ignore))
:type 'ebox-surface-participant-route-error)
(should-error
(ebox-surface--tp-participant-v2-capability-p
'(: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)))))
(ert-deftest ebox-commit-framework-publish-failure-rolls-back-full-and-scoped ()
"A framework publish failure invokes its pair once on both commit paths."
(dolist (mode '(full scoped))
@ -1750,9 +2014,12 @@ remain retained identities."
(ert-deftest ebox-commit-fixed-basis-selection-round-trip-stays-local ()
"Continuous row-1 -> row-2 -> row-1 publication keeps local TP scope."
(let ((slot-proof-count 0)
(original-slot-proof
(let ((planner-render-count 0)
(surface-render-count 0)
(original-planner-render
(symbol-function 'ebox--flex-item-slot-footprint-safe-p))
(original-surface-render
(symbol-function 'ebox-surface--render-candidate-node))
(buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-fixed-basis-round-trip* ")
@ -1766,10 +2033,15 @@ remain retained identities."
(setq panel-id
(ebox-incremental--nearest-fixed-basis-flex-item-owner-id
buffer panel-id))
(cl-letf (((symbol-function 'ebox--flex-item-slot-footprint-safe-p)
(cl-letf
(((symbol-function 'ebox--flex-item-slot-footprint-safe-p)
(lambda (&rest arguments)
(cl-incf slot-proof-count)
(apply original-slot-proof arguments))))
(cl-incf planner-render-count)
(apply original-planner-render arguments)))
((symbol-function 'ebox-surface--render-candidate-node)
(lambda (&rest arguments)
(cl-incf surface-render-count)
(apply original-surface-render arguments))))
(dolist (contents '(("[ ] row 1" "[x] row 2")
("[x] row 1" "[ ] row 2")))
(let* ((report
@ -1777,16 +2049,16 @@ remain retained identities."
buffer
(ebox-commit-test--fixed-basis-selection-candidate
buffer (car contents) (cadr contents))))
(surface (with-current-buffer
buffer ebox-surface--buffer-surface))
(surface
(with-current-buffer buffer ebox-surface--buffer-surface))
(tp-report (tp-surface-report surface))
(object-count
(plist-get (tp-surface-inspect surface) :object-count))
(snapshots
(plist-get (ebox--buffer-render-state buffer)
:layout-snapshots))
(panel-snapshot (and snapshots
(gethash panel-id snapshots))))
(panel-snapshot
(and snapshots (gethash panel-id snapshots))))
(push report reports)
(should (memq (plist-get report :projection-kind)
'(span-patch owner-scoped)))
@ -1810,7 +2082,8 @@ remain retained identities."
(plist-get state :root-node)
(plist-get state :source-index))))))))))
(should (= (length reports) 2))
(should (= slot-proof-count 2)))
(should (zerop planner-render-count))
(should (= surface-render-count 2)))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(defun ebox-commit-test--mixed-owner-candidate
@ -2032,6 +2305,9 @@ remain retained identities."
(ert-deftest ebox-commit-allocation-closure-proof-misses-fallback ()
"Topology, selector, cascade, and role misses reject allocation closure."
(dolist (kind '(topology selector cascade))
(ebox-style-reset-rules)
(when (eq kind 'selector)
(ebox-style-add-rule "box:has(.changed)" '(:color "#EF4444")))
(let ((buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-allocation-proof-miss* ")
@ -2064,9 +2340,11 @@ remain retained identities."
(lambda (&rest _) nil)))
(ebox-commit buffer candidate))
(ebox-commit buffer candidate))))
(ert-info ((format "proof miss kind: %S" kind))
(should-not (eq (plist-get report :projection-kind)
'mixed-owner-reflow))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
'mixed-owner-reflow)))))
(when (buffer-live-p buffer) (kill-buffer buffer))
(ebox-style-reset-rules))))
(let* ((buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-allocation-role-miss* ")

View File

@ -12188,6 +12188,23 @@ explicit visible-window handoff may admit its full-content publication."
(should (string-match-p word plain)))
(should (> (ebox-string-height rendered) 1))))
(ert-deftest ebox-column-wraps-direct-text-in-containing-block ()
"A direct Text child inherits the containing Box wrap policy."
(ebox-test--reset-runtime-state)
(let* ((ebox-viewport-width 1000)
(node
(ebox-test-column
:width '(20) :wrap-mode 'word
(ebox-test-text
"alpha beta gamma delta epsilon zeta eta theta iota kappa")))
(rendered (ebox-render node))
(widths
(mapcar #'ebox--string-pixel-width (ebox-string-lines rendered))))
(should (> (length widths) 1))
(should (cl-every (lambda (width) (<= width 20)) widths))
(should (string-match-p "alpha" (substring-no-properties rendered)))
(should (string-match-p "kappa" (substring-no-properties rendered)))))
(ert-deftest ebox-wrapper-child-layout-keeps-pixel-width-with-wide-font ()
"Engine pixel widths must not be reparsed as public character widths."
(ebox-test--reset-runtime-state)
@ -13132,6 +13149,71 @@ face patch must match that exactly (issue014)."
(ebox-test--buffer-propertized-matches-runtime-render-p)))))
(when (buffer-live-p buffer) (kill-buffer buffer))))))
(ert-deftest ebox-native-stable-full-commit-prepares-style-certificate ()
"A stable full-source commit certifies styles before native eligibility."
(ebox-test--reset-runtime-state)
(let* ((buffer (generate-new-buffer " *ebox-native-style-certificate*"))
(before
(ebox-build
'(column :key root
(box :key target :id target :width (52) :padding (0 1)
:color "#0F172A" :bgcolor "#F8FAFC" "Before")
(box :key peer :id peer :width (52) :padding (0 1)
:color "#0F172A" :bgcolor "#F8FAFC" "Peer"))))
(after
(ebox-build
'(column :key root
(box :key target :id target :width (52) :padding (0 1)
:color "#0F172A" :bgcolor "#F8FAFC" "UPDATED\ndetail")
(box :key peer :id peer :width (52) :padding (0 1)
:color "#0F172A" :bgcolor "#F8FAFC" "Peer")))))
(unwind-protect
(progn
(ebox-render-to-buffer buffer before)
;; The core suite disables the optional module. Mark the otherwise
;; ordinary committed fixture as a prior confirmed native frame so
;; this test deterministically exercises the update eligibility
;; boundary without depending on a local dylib.
(plist-put (ebox--buffer-render-state buffer)
:projection-kind 'native-frame)
(let* ((peer-before
(car (ebox-selector-query-buffer buffer "#peer")))
(peer-node-id (plist-get peer-before :node-id))
(peer-region-id (plist-get peer-before :region-id))
(style-before
(plist-get (ebox--buffer-runtime-node buffer peer-node-id)
:ebox-computed-style))
(plan (symbol-function 'ebox-native-commit-plan-eligible-p))
prepared-seen report
(_commit
(cl-letf
(((symbol-function 'ebox-native-commit-plan-eligible-p)
(lambda (old-state candidate-state prepared)
(setq prepared-seen prepared)
(funcall plan old-state candidate-state prepared))))
(setq report (ebox-commit buffer after))))
(peer-after (car (ebox-selector-query-buffer buffer "#peer")))
(style-after
(plist-get (ebox--buffer-runtime-node buffer peer-node-id)
:ebox-computed-style))
(values (and style-after
(ecss-computed-style-values style-after))))
(should style-before)
(should (plist-get prepared-seen :styles-prepared-p))
(should (plist-get prepared-seen
:computed-participation-validated-p))
(should (eq (plist-get report :strategy) 'span-patch))
(should (= (plist-get peer-after :node-id) peer-node-id))
(should (= (plist-get peer-after :region-id) peer-region-id))
(should style-after)
(should (equal (plist-get values 'ebox/color) "#0F172A"))
(should (equal (plist-get values 'ebox/background-color)
"#F8FAFC"))
(with-current-buffer buffer
(should
(ebox-test--buffer-propertized-matches-runtime-render-p)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-typed-commit-preserves-unaffected-source-range ()
"A keyed peer keeps identity and bounds across a sibling-only commit."
(let* ((buffer (generate-new-buffer " *ebox-typed-peer-range*"))

View File

@ -682,6 +682,26 @@
(nth 1 lines) region-id)
120))))
(ert-deftest ebox-flex-stretch-padding-retains-child-allocation-owner ()
"Cross-axis blank lines remain part of the stretched child fragment."
(let* ((child (ebox-test-box (ebox-test-text "A") :width '(80)))
(region-id (car (ebox-region-ids (ebox-test-root child))))
(layout
(ebox-test-flex :width '(120) :height 3 :align-items 'stretch
child))
(lines (ebox-string-lines (ebox-render layout))))
(should (= (length lines) 3))
(dolist (line lines)
(should
(text-property-not-all
0 (length line) 'ebox-content-owners nil line))
(should
(cl-some
(lambda (position)
(memq region-id
(get-text-property position 'ebox-content-owners line)))
(number-sequence 0 (1- (length line))))))))
(ert-deftest ebox-flex-natural-reuse-preserves-stretched-scroll-state ()
"Stretch sizing should publish the final box geometry to scroll state."
(let* ((ebox-viewport-width 120)

View File

@ -97,6 +97,17 @@
(should (string-match-p
"ebox-render-context-materialize-string" source))))
(ert-deftest ebox-incremental-boundary-uses-injected-context-port ()
"Incremental planning has no upward Surface symbol or publication edge."
(let ((source (ebox-layout-boundary-test--source "ebox-incremental.el")))
(should-not (string-match-p "ebox-surface" source))
(should (ebox-incremental-context-port-p
ebox-incremental--context-port))
(should
(string-match-p
"(ebox-incremental-install-context-port"
(ebox-layout-boundary-test--source "ebox-surface.el")))))
(ert-deftest ebox-layout-boundary-preserves-pre-e2-layout-artifacts ()
"The injected route preserves every old character and property interval."
(dolist (input (ebox-layout-boundary-test--fixtures))

View File

@ -0,0 +1,76 @@
;;; ebox-m0a-characterization-tests.el --- M0a commit characterization -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'ebox-fixtures)
(define-error 'ebox-m0a-test-condition "M0a sentinel condition")
(ert-deftest ebox-m0a-success-return-preserves-framework-report-identity ()
"One report moves from provisional publish to completed return in place."
(let ((buffer (generate-new-buffer " *ebox-m0a-report-identity*"))
seen provisional-state)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "before")))
(let ((returned
(ebox-commit
buffer
(ebox-test-box :key 'root (ebox-test-text "after"))
(lambda (report)
(setq seen report
provisional-state
(plist-get report :framework-participant-state))))))
(should (eq provisional-state 'published))
(should (eq seen returned))
(should (eq (plist-get returned :framework-participant-state)
'completed))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-m0a-framework-failure-resignals-raw-condition-symbol-and-data ()
"Framework failure keeps its original condition symbol and data payload."
(let ((buffer (generate-new-buffer " *ebox-m0a-raw-condition*")) caught)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "before")))
(setq caught
(condition-case condition
(ebox-commit
buffer
(ebox-test-box :key 'root (ebox-test-text "after"))
(lambda (_report)
(signal 'ebox-m0a-test-condition '(alpha 17))))
(ebox-m0a-test-condition condition)))
(should (eq (car caught) 'ebox-m0a-test-condition))
(should (equal (cdr caught) '(alpha 17))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-m0a-framework-rollback-receives-publish-report-object ()
"Framework rollback receives the exact report passed to failed publish."
(let ((buffer (generate-new-buffer " *ebox-m0a-rollback-identity*"))
published rolled-back)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "before")))
(condition-case nil
(ebox-commit
buffer
(ebox-test-box :key 'root (ebox-test-text "after"))
(lambda (report)
(setq published report)
(error "reject"))
(lambda (report) (setq rolled-back report)))
(error nil))
(should published)
(should (eq published rolled-back))
(should (eq (plist-get rolled-back :framework-participant-state)
'rolled-back)))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(provide 'ebox-m0a-characterization-tests)
;;; ebox-m0a-characterization-tests.el ends here

View File

@ -0,0 +1,533 @@
;;; ebox-m0a-inventory-fixture.el --- M0a architecture inventory -*- lexical-binding: t; -*-
;;; Commentary:
;; Machine-readable characterization of the current Ebox ownership model.
;; This is an observed-v1 inventory, not a declaration that the future v2 SPI
;; or state split already exists.
;;; Code:
(defconst ebox-m0a-state-table-required-fields
'(:id :storage :owner :authority :current-mutation :aliasing :lifetime
:rollback :rebuild :cleanup)
"Fields required on every M0a state-table inventory record.")
(defconst ebox-m0a-non-store-exclusions
'((:storage (:global ebox--defer-scroll-content-index)
:why dynamic-scalar-not-a-store :owner ebox-layout
:evidence defvar-is-a-dynamic-boolean-or-index-value)
(:storage (:global ebox--string-pixel-width-cache-ring-index)
:why eviction-cursor-not-a-store :owner ebox-measure
:evidence defvar-holds-a-numeric-ring-cursor)
(:storage (:state-key :content-index)
:why address-scalar-not-a-store :owner ebox-surface
:evidence plist-key-holds-a-numeric-content-address)
(:storage (:state-key :max-jobs)
:why native-limit-not-a-store :owner ebox-native-reflow
:evidence plist-key-holds-a-numeric-capacity)
(:storage (:state-key :next-index)
:why traversal-cursor-not-a-store :owner ebox-layout
:evidence plist-key-holds-a-numeric-traversal-cursor)
(:storage (:state-key :line-index)
:why prewarm-cursor-not-a-store :owner ebox
:evidence plist-key-is-incremented-as-a-numeric-task-cursor)
(:storage (:state-key :measure-index)
:why flex-prewarm-cursor-not-a-store :owner ebox-flex
:evidence plist-key-is-incremented-as-a-numeric-measure-cursor)
(:storage (:state-key :partial-index)
:why native-result-scalar-not-a-store :owner ebox-native-reflow
:evidence plist-key-holds-a-numeric-native-result-index)
(:storage (:state-key :render-index)
:why flex-prewarm-cursor-not-a-store :owner ebox-flex
:evidence plist-key-is-incremented-as-a-numeric-render-cursor)
(:storage (:state-key :segment-index)
:why range-address-scalar-not-a-store :owner ebox-child-range
:evidence plist-key-holds-a-numeric-range-address)
(:storage (:state-key :buffer-table)
:why fresh-projection-result-not-retained :owner ebox-state-contract
:evidence plist-key-exists-only-in-a-detached-rebuild-return-value)
(:storage (:state-key :region-table)
:why fresh-projection-result-not-retained :owner ebox-state-contract
:evidence plist-key-exists-only-in-a-detached-rebuild-return-value)
(:storage (:state-key :cyclic-parent-table)
:why condition-data-key-not-a-store :owner ebox-patch-plan
:evidence keyword-labels-a-validation-error-payload)
(:storage (:state-key :malformed-parent-table)
:why condition-data-key-not-a-store :owner ebox-patch-plan
:evidence keyword-labels-a-validation-error-payload)
(:storage (:state-key :missing-parent-table)
:why condition-data-key-not-a-store :owner ebox-incremental
:evidence keyword-labels-a-validation-error-payload)
(:storage (:state-key :malformed-region-index)
:why condition-data-key-not-a-store :owner ebox-state-contract
:evidence keyword-labels-a-validation-error-payload)
(:storage (:state-key :public-rule-in-internal-table)
:why condition-data-key-not-a-store :owner ebox-style
:evidence keyword-labels-a-validation-error-payload))
"Scanner-name matches proven by source usage not to be stores.")
(defconst ebox-m0a-observed-unclassified-stores nil
"Real store candidates lacking classification; M0a requires this to be nil.")
(defun ebox-m0a--storage-module-owner (storage)
"Return the current implementation module responsible for STORAGE."
(let ((kind (car storage))
(container (cadr storage))
(name (symbol-name (car (last storage)))))
(cond
((and (eq kind :struct-slot)
(string-prefix-p "ebox-source" (symbol-name container)))
'ebox-source)
((and (eq kind :struct-slot)
(string-prefix-p "ebox-native-reflow" (symbol-name container)))
'ebox-native-reflow)
((and (eq kind :struct-slot)
(eq container 'ebox-incremental--detached-history))
'ebox-incremental)
((string-match-p "cache--" name) 'ebox-cache)
((string-match-p "canonical" name) 'ebox-canonical)
((string-match-p "render-burst" name) 'ebox-buffer-backend)
((string-match-p "render-owned-text-values" name) 'ebox-render-context)
((string-match-p "font\\|char-width\\|pixel-width\\|face-height\\|space-pixel\\|display-signature" name)
'ebox-measure)
((string-match-p "style" name) 'ebox-style)
((string-match-p "surface\\|paint-node\\|inline-style" name) 'ebox-surface)
((string-match-p "native" name) 'ebox-native-reflow)
((string-match-p "source\\|host-ref" name) 'ebox-source)
((string-match-p "child-range\\|ref-index" name) 'ebox-child-range)
((string-match-p "flex" name) 'ebox-flex)
((string-match-p "layout-fragment" name) 'ebox-fragment)
((string-match-p "node-region" name) 'ebox-tree)
((string-match-p "subject-table" name) 'ebox-tree)
((string-match-p "frame-cache\\|key-table" name) 'ebox-native-reflow)
((string-match-p "region-line\\|owner-table" name) 'ebox-incremental)
((string-match-p "render-output-provenance" name) 'ebox-render-context)
((string-match-p "uniform-width\\|scroll-table" name) 'ebox-layout)
((string-match-p "deferred-render-gc" name) 'ebox-buffer-backend)
((string-match-p "window-line\\|box-content\\|region-box\\|scroll-window" name)
'ebox-layout)
((string-match-p "incremental\\|render-cache\\|viewport\\|detached\\|ancestor\\|proof\\|patchable" name)
'ebox-incremental)
((string-match-p "selector" name) 'ebox-tree)
(t 'ebox))))
(defun ebox-m0a--supplemental-record
(storage proposed-category owner lifecycle)
"Classify STORAGE by PROPOSED-CATEGORY, OWNER, and LIFECYCLE."
(list :id (list 'supplemental storage) :storage storage
:proposed-category proposed-category
:owner (if (eq owner 'ebox)
(ebox-m0a--storage-module-owner storage)
owner)
:authority 'ebox
:lifetime lifecycle :rollback 'candidate-discard-or-not-authoritative
:rebuild 'lazy-or-generation-rebuild
:cleanup 'scope-retirement-or-cache-clear))
(defconst ebox-m0a--disposable-global-cache-symbols
'(ebox--box-content-render-cache ebox--char-width-cache
ebox--display-signature-cache ebox--face-height-width-cache
ebox--flex-content-min-width-table ebox--flex-sized-render-observation-table
ebox--flex-slot-safety-cache ebox--layout-fragments-table
ebox--node-region-ids-cache ebox--patchable-owner-cache
ebox--render-cache-scroll-state-retained-cost-cache
ebox--render-cache-signature-cache ebox--render-cache-table
ebox--render-output-provenance-table ebox--render-recached-source-node-cache
ebox--render-string-max-pixel-width-cache
ebox--render-string-pixel-width-cache ebox--rendered-uniform-width-table
ebox--runtime-ancestor-cache ebox--space-pixel-cache
ebox--string-pixel-width-cache ebox--viewport-dependent-node-ids-cache
ebox--viewport-dependent-subtree-cache
ebox--viewport-height-dependent-subtree-cache
ebox--window-line-prewarmer-table ebox--window-line-renderer-table
ebox-cache--buffer-report-table ebox-cache--registry
ebox-canonical--declaration-fact-cache
ebox-incremental--allocated-slot-proof-cache
ebox-incremental--candidate-path-copy-origin-table
ebox-incremental--candidate-proof-node-table
ebox-style--box-engine-longhand-cache ebox-style--closed-computed-cache
ebox-style--closed-inheritance-cache ebox-style--computed-snapshot-cache
ebox-style--declaration-cache ebox-style--text-engine-longhand-cache
ebox-surface--inline-style-value-cache ebox-surface--paint-node-chain-cache
ebox-surface--paint-node-depth-cache ebox-surface--scroll-line-fragment-cache)
"Process or dynamically scoped disposable cache/table variables.")
(defconst ebox-m0a--generation-bound-global-store-symbols
'(ebox--deferred-render-gc-timer ebox--reflow-cache-prewarm-timers
ebox--render-burst-records ebox--render-owned-text-values
ebox--region-box-table
ebox--region-line-index ebox--render-source-index
ebox--runtime-prewarm-jobs ebox--runtime-prewarm-timers
ebox--scroll-idle-prefetch-inhibited-buffers
ebox--scroll-idle-prefetch-timers
ebox-native-reflow--compile-property-template-ids
ebox-incremental--candidate-full-source-base-index
ebox-incremental--previous-source-base-index
ebox-incremental--source-base-index ebox-style--property-index)
"Generation facts and mutable scheduling stores outside the state plist.")
(defconst ebox-m0a--nested-generation-store-identities
'((:state-key :ancestor-table) (:state-key :candidate-subject-table)
(:state-key :host-ref-table)
(:state-key :node-subject-table) (:state-key :owner-table)
(:state-key :ref-index) (:state-key :region-line-bounds-index)
(:state-key :region-line-span-index)
(:state-key :rendered-region-line-span-index) (:state-key :source-base-index)
(:state-key :source-index) (:state-key :surface-source-base-index)
(:struct-slot ebox-canonical-input source-index)
(:struct-slot ebox-child-range--sequence ref-index)
(:struct-slot ebox-incremental--candidate-paint-patch source-index)
(:struct-slot ebox-incremental--candidate-replacement source-index)
(:struct-slot ebox-source-index records)
(:struct-slot ebox-source-index handle-records)
(:struct-slot ebox-source-index subjects)
(:struct-slot ebox-source-index node-subjects)
(:struct-slot ebox-source-builder records)
(:struct-slot ebox-source-builder handle-records)
(:struct-slot ebox-source-table added)
(:struct-slot ebox-source-table removed)
(:struct-slot ebox-source-order replacements)
(:struct-slot ebox-source-order removed)
(:struct-slot ebox-source-index host-ref-table)
(:struct-slot ebox-source-index selector-class-table)
(:struct-slot ebox-source-index selector-id-table)
(:struct-slot ebox-source-index selector-type-table))
"Nested immutable or generation-derived index/table stores.")
(defconst ebox-m0a--nested-mutable-store-identities
'((:state-key :detached-identity-history) (:state-key :native-session)
(:state-key :scroll-table)
(:struct-slot ebox-incremental--detached-history table)
(:struct-slot ebox-native-reflow-preparation ready-timer)
(:struct-slot ebox-native-reflow-preparation native-session))
"Nested generation-bound mutable authority stores.")
(defconst ebox-m0a--nested-cache-store-identities
'((:state-key :frame-cache) (:state-key :key-table)
(:state-key :layout-fragment-cache)
(:state-key :native-fragment-owner-cache)
(:state-key :node-region-ids-cache)
(:state-key :string-max-pixel-width-cache)
(:state-key :string-pixel-width-cache)
(:state-key :viewport-dependent-node-ids-cache)
(:state-key :viewport-dependent-subtree-cache)
(:struct-slot ebox-native-reflow-layout-builder string-max-pixel-width-cache)
(:struct-slot ebox-native-reflow-layout-builder string-pixel-width-cache)
(:struct-slot ebox-source-table memo)
(:struct-slot ebox-native-reflow-preparation frame-cache)
(:struct-slot ebox-native-reflow-preparation key-table)
(:struct-slot ebox-native-reflow-session layout-fragment-cache))
"Nested disposable native, viewport, render, and measurement caches.")
(defconst ebox-m0a-supplemental-state-table-inventory
(append
(mapcar (lambda (symbol)
(ebox-m0a--supplemental-record
(list :global symbol) 'disposable-cache 'ebox 'dynamic-or-process))
ebox-m0a--disposable-global-cache-symbols)
(mapcar (lambda (symbol)
(ebox-m0a--supplemental-record
(list :global symbol) 'generation-bound-mutable
'ebox 'dynamic-or-generation))
ebox-m0a--generation-bound-global-store-symbols)
(mapcar (lambda (storage)
(ebox-m0a--supplemental-record
storage 'generation-fact 'ebox 'candidate-or-generation))
ebox-m0a--nested-generation-store-identities)
(mapcar (lambda (storage)
(ebox-m0a--supplemental-record
storage 'generation-bound-mutable 'ebox 'candidate-or-runtime))
ebox-m0a--nested-mutable-store-identities)
(mapcar (lambda (storage)
(ebox-m0a--supplemental-record
storage 'disposable-cache 'ebox 'candidate-or-dynamic))
ebox-m0a--nested-cache-store-identities))
"Stores found by static schema scan beyond the primary retained-state list.")
(defun ebox-m0a--current-store-facts (storage)
"Return conservative current mutation and aliasing facts for STORAGE."
(cond
((equal storage '(:state-key :layout-snapshots))
'(:current-mutation in-place-puthash-on-committed-state
:aliasing committed-state-table-retained-and-mutated))
((memq (car storage) '(:global :struct-slot))
'(:current-mutation mutable-container-or-whole-value-replacement
:aliasing dynamic-or-enclosing-object-aliasing-not-proven-absent))
((memq (cadr storage)
'(:native-sync-session :native-sync-pending :native-session
:scroll-state-table :scroll-table :detached-identity-history))
'(:current-mutation in-place-mutation-and-whole-value-replacement
:aliasing candidate-and-committed-state-may-share-owned-handles))
(t
'(:current-mutation in-place-mutation-or-whole-table-replacement
:aliasing candidate-and-committed-aliasing-not-proven-absent))))
(defun ebox-m0a--normalize-current-record (record)
"Return RECORD augmented with current observable M0a facts."
(let* ((copy (copy-sequence record))
(facts (ebox-m0a--current-store-facts (plist-get copy :storage))))
(setq copy (plist-put copy :current-mutation
(plist-get facts :current-mutation)))
(setq copy (plist-put copy :aliasing (plist-get facts :aliasing)))
(when (plist-get copy :proposed-category)
(setq copy (plist-put copy :proposal-status 'not-active)))
copy))
(defconst ebox-m0a-state-table-inventory
'((:id buffer-runtime/state-store
:storage (:global ebox--buffer-render-state-table)
:proposed-category generation-bound-mutable :owner ebox-incremental
:authority ebox :lifetime process-by-buffer
:rollback tp-client-state-or-snapshot :rebuild render-or-commit
:cleanup buffer-kill-and-unmount)
(:id buffer-runtime/node-table
:storage (:state-key :node-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/parent-table
:storage (:state-key :parent-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/runtime-type-count-table
:storage (:state-key :runtime-type-count-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/range-ref-table
:storage (:state-key :range-ref-table)
:proposed-category generation-fact :owner ebox-child-range :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/region-id-set
:storage (:state-key :region-id-set)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/region-node-table
:storage (:state-key :region-node-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/region-box-count-table
:storage (:state-key :region-box-count-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild runtime-index :cleanup generation-replacement)
(:id buffer-runtime/region-box-table
:storage (:state-key :region-box-table)
:proposed-category generation-bound-mutable :owner ebox-layout :authority ebox
:lifetime committed-generation :rollback candidate-snapshot
:rebuild layout-materialization :cleanup generation-replacement)
(:id buffer-runtime/selector-id-table
:storage (:state-key :selector-id-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild selector-index :cleanup generation-replacement)
(:id buffer-runtime/selector-class-table
:storage (:state-key :selector-class-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild selector-index :cleanup generation-replacement)
(:id buffer-runtime/selector-type-table
:storage (:state-key :selector-type-table)
:proposed-category generation-fact :owner ebox-tree :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild selector-index :cleanup generation-replacement)
(:id buffer-runtime/layout-snapshots
:storage (:state-key :layout-snapshots)
:proposed-category generation-fact :owner ebox-fragment :authority ebox
:lifetime committed-generation :rollback candidate-discard
:rebuild layout-snapshot-pass :cleanup generation-replacement)
(:id buffer-runtime/layout-fragments
:storage (:state-key :layout-fragments)
:proposed-category disposable-cache :owner ebox-fragment :authority ebox
:lifetime buffer-runtime :rollback copy-on-candidate
:rebuild lazy-layout :cleanup clear-cache-or-buffer-kill)
(:id buffer-runtime/render-cache
:storage (:state-key :render-cache)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime buffer-runtime :rollback copy-on-candidate
:rebuild lazy-render :cleanup clear-cache-or-buffer-kill)
(:id buffer-runtime/render-signature-cache
:storage (:state-key :render-signature-cache)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime buffer-runtime :rollback copy-on-candidate
:rebuild lazy-render :cleanup clear-cache-or-buffer-kill)
(:id buffer-runtime/flex-content-min-widths
:storage (:state-key :flex-content-min-widths)
:proposed-category disposable-cache :owner ebox-flex :authority ebox
:lifetime buffer-runtime :rollback candidate-discard
:rebuild lazy-measure :cleanup generation-replacement)
(:id buffer-runtime/viewport-height-dependent-subtree-cache
:storage (:state-key :viewport-height-dependent-subtree-cache)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime buffer-runtime :rollback copy-on-candidate
:rebuild viewport-proof :cleanup clear-cache-or-buffer-kill)
(:id buffer-runtime/render-owned-text-values
:storage (:state-key :render-owned-text-values)
:proposed-category generation-bound-mutable :owner ebox-surface :authority ebox
:lifetime committed-generation :rollback tp-transaction
:rebuild surface-projection :cleanup generation-replacement)
(:id buffer-runtime/style-binding-states
:storage (:state-key :style-binding-states)
:proposed-category generation-bound-mutable :owner ebox-surface :authority tp
:lifetime mounted-surface :rollback tp-transaction
:rebuild surface-projection :cleanup surface-unmount)
(:id buffer-runtime/surface-node-object-table
:storage (:state-key :surface-node-object-table)
:proposed-category disposable-mirror :owner ebox-surface :authority tp
:lifetime mounted-surface :rollback tp-transaction
:rebuild surface-projection :cleanup surface-unmount)
(:id buffer-runtime/region-surface-object-table
:storage (:state-key :region-surface-object-table)
:proposed-category disposable-mirror :owner ebox-surface :authority tp
:lifetime mounted-surface :rollback tp-transaction
:rebuild surface-projection :cleanup surface-unmount)
(:id buffer-runtime/surface-object-region-table
:storage (:state-key :surface-object-region-table)
:proposed-category disposable-mirror :owner ebox-surface :authority tp
:lifetime mounted-surface :rollback tp-transaction
:rebuild surface-projection :cleanup surface-unmount)
(:id buffer-runtime/logical-id-region-table
:storage (:state-key :logical-id-region-table)
:proposed-category disposable-mirror :owner ebox-surface :authority ebox
:lifetime committed-generation :rollback tp-transaction
:rebuild surface-projection :cleanup generation-replacement)
(:id buffer-runtime/scroll-state-table
:storage (:state-key :scroll-state-table)
:proposed-category generation-bound-mutable :owner ebox-layout :authority ebox
:lifetime buffer-runtime :rollback scroll-state-snapshot
:rebuild layout-or-scroll-reconciliation :cleanup buffer-kill)
(:id buffer-runtime/native-sync-session
:storage (:state-key :native-sync-session)
:proposed-category generation-bound-mutable :owner ebox-native-commit
:authority ebox :lifetime buffer-runtime :rollback private-session-discard
:rebuild create-or-fork-session :cleanup buffer-kill)
(:id buffer-runtime/native-sync-pending
:storage (:state-key :native-sync-pending)
:proposed-category generation-bound-mutable :owner ebox-native-commit
:authority ebox :lifetime candidate-until-final-accept
:rollback private-session-discard :rebuild native-frame-stage
:cleanup confirm-or-rollback)
(:id process/scroll-global-state
:storage (:global ebox--scroll-global-state)
:proposed-category generation-bound-mutable :owner ebox-incremental
:authority ebox :lifetime process-by-region :rollback explicit-snapshot
:rebuild render-or-scroll :cleanup region-retirement)
(:id process/smooth-scroll-state-table
:storage (:global ebox--smooth-scroll-state-table)
:proposed-category generation-bound-mutable :owner ebox :authority ebox
:lifetime active-scroll :rollback stop-and-restore
:rebuild scroll-input :cleanup scroll-stop-or-buffer-kill)
(:id process/incremental-batch-table
:storage (:global ebox-incremental--batch-table)
:proposed-category generation-bound-mutable :owner ebox-incremental
:authority ebox :lifetime explicit-batch :rollback discard-pending
:rebuild batch-begin :cleanup batch-end-or-buffer-kill)
(:id process/render-cache-ring-table
:storage (:global ebox--render-cache-ring-table)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime process-by-render-cache :rollback not-authoritative
:rebuild lazy-eviction-index :cleanup cache-retirement)
(:id process/render-root-cache-table-table
:storage (:global ebox--render-root-cache-table-table)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime process-by-render-cache :rollback not-authoritative
:rebuild lazy-root-cache :cleanup cache-retirement)
(:id process/render-cache-entry-side-effects-table
:storage (:global ebox--render-cache-entry-side-effects-table)
:proposed-category disposable-cache :owner ebox-incremental :authority ebox
:lifetime process-by-render-cache :rollback not-authoritative
:rebuild lazy-render :cleanup cache-retirement)
(:id process/font-cache
:storage (:global ebox-font--cache)
:proposed-category disposable-cache :owner ebox-font :authority ebox
:lifetime process :rollback not-authoritative :rebuild lazy-measure
:cleanup ebox-font-clear-cache)
(:id process/native-source-cluster-cache
:storage (:global ebox-native-reflow--source-cluster-cache)
:proposed-category disposable-cache :owner ebox-native-reflow :authority ebox
:lifetime process :rollback not-authoritative :rebuild lazy-native-compile
:cleanup bounded-eviction-or-clear))
"Observed current Ebox runtime, region, scroll, native, and cache stores.")
(defun ebox-m0a-current-state-inventory ()
"Return all stores with observable facts and inactive M2a proposals."
(mapcar #'ebox-m0a--normalize-current-record
(append ebox-m0a-state-table-inventory
ebox-m0a-supplemental-state-table-inventory)))
(defconst ebox-m0a-spi-inventory
'((:id ebox-to-tp/publication :provider tp :consumer ebox-surface
:version unversioned-v1 :discovery required-feature
:entry-points (tp-with-transaction tp-surface-mount tp-surface-update
tp-surface-update-scoped tp-transaction-participate)
:rollback tp-transaction :future-v2-status not-required-in-m0a)
(:id ebox-to-ecss/cascade :provider ecss :consumer ebox-style
:version unversioned-v1 :discovery required-feature
:entry-points (ecss-cascade-compute ecss-stylesheet-create)
:rollback prepare-only :future-v2-status not-required-in-m0a)
(:id ebox-to-ekp/wrap-backend :provider ekp :consumer ebox-layout
:version optional-string-v1 :discovery dynamic-require-and-fboundp
:entry-points (ekp-pixel-justify)
:rollback word-wrap-fallback :future-v2-status not-required-in-m0a)
(:id etaf-to-ebox/lowering :provider ebox :consumer etaf-renderer
:version unversioned-v1 :discovery required-feature
:entry-points (ebox-source-builder-create ebox-source-builder-bind
ebox-text-create ebox-box-create ebox-canonical-input-create)
:rollback candidate-discard :future-v2-status not-required-in-m0a)
(:id etaf-to-ebox/commit :provider ebox :consumer etaf-runtime
:version callback-pair-v1 :discovery required-feature
:entry-points (ebox-candidate-begin ebox-candidate-replace-host-ref
ebox-candidate-replace-range-ref ebox-commit)
:rollback framework-callback-pair :future-v2-status not-required-in-m0a)
(:id etaf-to-ebox/event-burst :provider ebox :consumer etaf-events
:version capability-probe-v1 :discovery fboundp
:entry-points (ebox-call-with-render-burst ebox-host-ref-bounds
ebox-host-ref-position)
:rollback unwind-protect :future-v2-status not-required-in-m0a))
"Observed cross-package call groups; no v2 capability object exists yet.")
(defconst ebox-m0a-public-load-inventory
'((:feature ebox :file "ebox.el"
:public-entry-points (ebox-render ebox-render-to-buffer ebox-commit
ebox-buffer-update-report ebox-candidate-begin
ebox-call-with-render-burst))
(:feature ebox-dsl :file "ebox-dsl.el"
:public-entry-points (ebox-build))
(:feature ebox-selector :file "ebox-selector.el"
:public-entry-points (ebox-selector-query-buffer
ebox-selector-update-buffer)))
"Current public feature/load and representative entry-point inventory.")
(defconst ebox-m0a-characterization-inventory
'((:id framework/same-report-success :status current-green
:file "tests/ebox-m0a-characterization-tests.el"
:test ebox-m0a-success-return-preserves-framework-report-identity)
(:id framework/same-report-rollback :status current-green
:file "tests/ebox-m0a-characterization-tests.el"
:test ebox-m0a-framework-rollback-receives-publish-report-object)
(:id condition/raw-symbol-and-data :status current-green
:file "tests/ebox-m0a-characterization-tests.el"
:test ebox-m0a-framework-failure-resignals-raw-condition-symbol-and-data)
(:id framework/full-and-scoped-rollback :status current-green
:file "tests/ebox-commit-tests.el"
:test ebox-commit-framework-publish-failure-rolls-back-full-and-scoped)
(:id framework/final-accept-rollback :status current-green
:file "tests/ebox-commit-tests.el"
:test ebox-commit-final-accept-failure-rolls-framework-back)
(:id native/private-session-failure-isolation :status current-green
:file "tests/ebox-commit-tests.el"
:test ebox-native-session-isolation-rejects-create-and-fork-errors)
(:id paint/atomic-rollback :status current-green
:file "tests/ebox-core-render-tests.el"
:test ebox-region-update-valid-paint-and-invalid-edge-is-atomic)
(:id future/versioned-spi-v2 :status future-not-required
:file nil :test nil))
"Executable current gates and explicitly non-green future targets for M0a.")
(provide 'ebox-m0a-inventory-fixture)
;;; ebox-m0a-inventory-fixture.el ends here

View File

@ -0,0 +1,358 @@
;;; ebox-m0a-inventory-tests.el --- M0a inventory contracts -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'ebox)
(require 'ebox-m0a-inventory-fixture)
(require 'ebox-fixtures)
(defun ebox-m0a-test--all-state-inventory ()
"Return primary and supplemental classified state inventories."
(ebox-m0a-current-state-inventory))
(defun ebox-m0a-test--state-key-records ()
"Return inventory records stored as retained state keys."
(cl-remove-if-not
(lambda (record) (eq (car (plist-get record :storage)) :state-key))
(ebox-m0a-test--all-state-inventory)))
(defun ebox-m0a-test--store-name-p (symbol)
"Return non-nil when SYMBOL names a table-like state store."
(and (symbolp symbol)
(string-match-p
"\\(?:-table\\|-cache\\|-index\\|-registry\\|-history\\|-session\\|-jobs\\|-timer\\|-timers\\)\\'"
(symbol-name symbol))))
(defun ebox-m0a-test--keyword-store-name-p (symbol)
"Return non-nil when keyword SYMBOL names a nested state store."
(and (keywordp symbol) (ebox-m0a-test--store-name-p symbol)))
(defun ebox-m0a-test--hash-constructor-p (form)
"Return non-nil when FORM directly materializes a hash table."
(eq (car-safe form) 'make-hash-table))
(defun ebox-m0a-test--declared-globals (forms)
"Return variables declared by top-level FORMS."
(let (globals)
(dolist (form forms)
(when (and (memq (car-safe form) '(defvar defconst))
(symbolp (cadr form)))
(push (cadr form) globals)))
(delete-dups globals)))
(defun ebox-m0a-test--materialized-global-bindings (form declared-globals)
"Return declared globals directly bound to hashes inside FORM.
DECLARED-GLOBALS distinguishes dynamic package stores from ordinary lexical
scratch tables, which are intentionally outside the retained-store inventory."
(let (globals)
(ebox-m0a-test--walk-form
form
(lambda (nested)
(pcase (car-safe nested)
((or 'let 'let*)
(dolist (binding (cadr nested))
(when (and (consp binding)
(memq (car binding) declared-globals)
(ebox-m0a-test--hash-constructor-p (cadr binding)))
(push (car binding) globals))))
((or 'setq 'setq-default)
(let ((pairs (cdr nested)))
(while pairs
(when (and (memq (car pairs) declared-globals)
(ebox-m0a-test--hash-constructor-p (cadr pairs)))
(push (car pairs) globals))
(setq pairs (cddr pairs))))))))
(delete-dups globals)))
(defun ebox-m0a-test--read-forms (file)
"Read and return every top-level Lisp form in FILE."
(with-temp-buffer
(insert-file-contents file)
(let (forms form)
(condition-case nil
(while t
(setq form (read (current-buffer)))
(push form forms))
(end-of-file nil))
(nreverse forms))))
(defun ebox-m0a-test--walk-form (form function)
"Call FUNCTION for FORM and every nested cons or vector element."
(funcall function form)
(cond
((consp form)
(ebox-m0a-test--walk-form (car form) function)
(ebox-m0a-test--walk-form (cdr form) function))
((vectorp form)
(mapc (lambda (item) (ebox-m0a-test--walk-form item function)) form))))
(defun ebox-m0a-test--struct-name (definition)
"Return the structure name declared by `cl-defstruct' DEFINITION."
(let ((head (cadr definition)))
(if (symbolp head) head (car head))))
(defun ebox-m0a-test--struct-slots (definition)
"Return plain slot names declared by `cl-defstruct' DEFINITION."
(let ((tail (cddr definition)) slots)
(while (and tail (stringp (car tail))) (pop tail))
(dolist (slot tail)
(let ((name (if (symbolp slot) slot (car-safe slot))))
(when (symbolp name) (push name slots))))
(nreverse slots)))
(defun ebox-m0a-test--schema-store-slot-p (structure slot)
"Return non-nil when STRUCTURE's SLOT is a known nested store schema."
(or (ebox-m0a-test--store-name-p slot)
(member
(cons structure slot)
'((ebox-source-index . records)
(ebox-source-index . handle-records)
(ebox-source-index . subjects)
(ebox-source-index . node-subjects)
(ebox-source-builder . records)
(ebox-source-builder . handle-records)
(ebox-source-table . added)
(ebox-source-table . removed)
(ebox-source-table . memo)
(ebox-source-order . replacements)
(ebox-source-order . removed)
(ebox-incremental--detached-history . table)))))
(defun ebox-m0a-test--static-store-candidates ()
"Return located store candidates found in active Ebox source forms."
(let* ((sources (delete-dups (copy-sequence ebox--compile-sources)))
(source-forms
(mapcar (lambda (source)
(cons source (ebox-m0a-test--read-forms source)))
sources))
(declared-globals
(delete-dups
(apply #'append
(mapcar (lambda (entry)
(ebox-m0a-test--declared-globals (cdr entry)))
source-forms))))
candidates)
(dolist (entry source-forms)
(let* ((source (car entry))
(module (intern (file-name-base source)))
(forms (cdr entry)))
(dolist (form forms)
(when (and (memq (car-safe form) '(defvar defconst))
(or (ebox-m0a-test--store-name-p (cadr form))
(ebox-m0a-test--hash-constructor-p (caddr form))))
(push (list :storage (list :global (cadr form))
:defined-in source :declaring-module module)
candidates))
(dolist (symbol
(ebox-m0a-test--materialized-global-bindings
form declared-globals))
(push (list :storage (list :global symbol)
:defined-in source :declaring-module module)
candidates))
(when (eq (car-safe form) 'cl-defstruct)
(let ((structure (ebox-m0a-test--struct-name form)))
(dolist (slot (ebox-m0a-test--struct-slots form))
(when (ebox-m0a-test--schema-store-slot-p structure slot)
(push (list :storage (list :struct-slot structure slot)
:defined-in source :declaring-module module)
candidates)))))
(ebox-m0a-test--walk-form
form
(lambda (nested)
(when (ebox-m0a-test--keyword-store-name-p nested)
(push (list :storage (list :state-key nested)
:defined-in source :declaring-module module)
candidates)))))))
(sort (delete-dups candidates)
(lambda (left right)
(string< (prin1-to-string left) (prin1-to-string right))))))
(defun ebox-m0a-test--inventory-storage-identities ()
"Return all classified and explicitly blocked storage identities."
(append
(mapcar (lambda (record) (plist-get record :storage))
(ebox-m0a-test--all-state-inventory))
(mapcar (lambda (record) (plist-get record :storage))
ebox-m0a-non-store-exclusions)))
(ert-deftest ebox-m0a-inventory-records-have-complete-lifecycle-fields ()
"Every state inventory record names its current lifecycle contract."
(dolist (record (ebox-m0a-test--all-state-inventory))
(dolist (field ebox-m0a-state-table-required-fields)
(should (plist-member record field))
(should (plist-get record field)))))
(ert-deftest ebox-m0a-non-store-exclusions-carry-source-evidence ()
"Every scanner exclusion records why its matching name is not a store."
(dolist (record ebox-m0a-non-store-exclusions)
(should (plist-get record :storage))
(should (plist-get record :why))
(should (plist-get record :owner))
(should (plist-get record :evidence))))
(ert-deftest ebox-m0a-has-no-observed-unclassified-stores ()
"M0a cannot pass while a real discovered store remains unclassified."
(should-not ebox-m0a-observed-unclassified-stores))
(ert-deftest ebox-m0a-static-store-scan-is-fail-closed ()
"Every located static candidate is classified or proven not to be a store."
(let ((known (ebox-m0a-test--inventory-storage-identities)))
(dolist (candidate (ebox-m0a-test--static-store-candidates))
(should (plist-get candidate :defined-in))
(should (plist-get candidate :declaring-module))
(should (member (plist-get candidate :storage) known)))))
(ert-deftest ebox-m0a-materialized-global-scan-excludes-lexical-scratch-tables ()
"Dynamic package stores are candidates; ordinary lexical tables are not."
(should
(equal
(ebox-m0a-test--materialized-global-bindings
'(let ((scratch (make-hash-table :test #'eq))
(ebox-m0a-test--dynamic-store (make-hash-table :test #'equal)))
(puthash 'key 'value scratch))
'(ebox-m0a-test--dynamic-store))
'(ebox-m0a-test--dynamic-store))))
(ert-deftest ebox-m0a-lazy-and-nested-stores-are-classified-not-blocked ()
"Known lazy native/viewport/scroll and nested stores have classifications."
(let ((classified
(mapcar (lambda (record) (plist-get record :storage))
(ebox-m0a-test--all-state-inventory)))
(critical
'((:global ebox-cache--registry)
(:global ebox-cache--buffer-report-table)
(:global ebox--char-width-cache)
(:global ebox--render-burst-records)
(:global ebox--render-owned-text-values)
(:global ebox--string-pixel-width-cache)
(:global ebox--scroll-idle-prefetch-inhibited-buffers)
(:global ebox--scroll-idle-prefetch-timers)
(:global ebox--runtime-prewarm-jobs)
(:global ebox--runtime-prewarm-timers)
(:global ebox--window-line-renderer-table)
(:global ebox--window-line-prewarmer-table)
(:global ebox-native-reflow--compile-property-template-ids)
(:state-key :source-index)
(:state-key :detached-identity-history)
(:state-key :viewport-dependent-node-ids-cache)
(:state-key :native-session)
(:struct-slot ebox-source-index host-ref-table)
(:struct-slot ebox-source-table added)
(:struct-slot ebox-source-table removed)
(:struct-slot ebox-source-table memo)
(:struct-slot ebox-incremental--detached-history table)
(:struct-slot ebox-native-reflow-preparation frame-cache)
(:struct-slot ebox-native-reflow-preparation ready-timer)
(:struct-slot ebox-native-reflow-session layout-fragment-cache))))
(dolist (storage critical)
(should (member storage classified)))))
(ert-deftest ebox-m0a-supplemental-stores-name-concrete-module-owners ()
"Representative supplemental stores resolve to their implementation modules."
(dolist (expected
'(((:global ebox-cache--registry) . ebox-cache)
((:global ebox--char-width-cache) . ebox-measure)
((:global ebox--render-burst-records) . ebox-buffer-backend)
((:global ebox--render-owned-text-values) . ebox-render-context)
((:state-key :owner-table) . ebox-incremental)
((:struct-slot ebox-source-index host-ref-table) . ebox-source)
((:struct-slot ebox-native-reflow-preparation frame-cache)
. ebox-native-reflow)))
(let ((record
(cl-find (car expected) (ebox-m0a-test--all-state-inventory)
:key (lambda (item) (plist-get item :storage))
:test #'equal)))
(should (eq (plist-get record :owner) (cdr expected))))))
(ert-deftest ebox-m0a-inventory-record-identities-are-unique ()
"Every inventory record has one stable machine-readable identity."
(let ((ids (mapcar (lambda (record) (plist-get record :id))
(ebox-m0a-test--all-state-inventory))))
(should (= (length ids) (length (delete-dups (copy-sequence ids)))))))
(ert-deftest ebox-m0a-proposed-state-categories-remain-inactive ()
"M0a records proposed M2a categories without asserting their activation."
(dolist (record (ebox-m0a-test--all-state-inventory))
(when (plist-get record :proposed-category)
(should (eq (plist-get record :proposal-status) 'not-active)))))
(ert-deftest ebox-m0a-layout-snapshots-record-current-in-place-aliasing ()
"Layout snapshots expose current committed-state mutation, not target purity."
(let ((record
(cl-find '(:state-key :layout-snapshots)
(ebox-m0a-test--all-state-inventory)
:key (lambda (item) (plist-get item :storage))
:test #'equal)))
(should (eq (plist-get record :current-mutation)
'in-place-puthash-on-committed-state))
(should (eq (plist-get record :aliasing)
'committed-state-table-retained-and-mutated))))
(ert-deftest ebox-m0a-global-store-inventory-resolves-current-bindings ()
"Every inventoried process store names a currently bound variable."
(dolist (record (ebox-m0a-test--all-state-inventory))
(when (eq (car (plist-get record :storage)) :global)
(should (boundp (cadr (plist-get record :storage)))))))
(ert-deftest ebox-m0a-inventory-covers-all-materialized-runtime-hash-tables ()
"A normal committed runtime exposes no unclassified retained hash table."
(let ((buffer (generate-new-buffer " *ebox-m0a-runtime-inventory*")))
(unwind-protect
(progn
(ebox-render-to-buffer
buffer
(ebox-test-column
(ebox-test-box :key 'first (ebox-test-text "first"))
(ebox-test-box :key 'second (ebox-test-text "second"))))
(let ((state (ebox--buffer-render-state buffer))
actual expected)
(while state
(when (hash-table-p (cadr state))
(push (car state) actual))
(setq state (cddr state)))
(dolist (record (ebox-m0a-test--state-key-records))
(let ((key (cadr (plist-get record :storage))))
(when (hash-table-p
(plist-get (ebox--buffer-render-state buffer) key))
(push key expected))))
(should (equal (sort actual #'string-lessp)
(sort expected #'string-lessp)))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-m0a-current-spi-entry-points-are-symbol-lists ()
"Every observed SPI group names a non-empty symbolic call surface."
(dolist (record ebox-m0a-spi-inventory)
(let ((entry-points (plist-get record :entry-points)))
(should entry-points)
(should (cl-every #'symbolp entry-points))
(should (eq (plist-get record :future-v2-status)
'not-required-in-m0a)))))
(ert-deftest ebox-m0a-public-load-inventory-resolves-current-entry-points ()
"Every inventoried public feature and representative function currently loads."
(dolist (record ebox-m0a-public-load-inventory)
(should (require (plist-get record :feature) nil t))
(dolist (entry-point (plist-get record :public-entry-points))
(should (fboundp entry-point)))))
(ert-deftest ebox-m0a-current-characterization-references-existing-tests ()
"Every current-green behavior gate points at an existing ERT definition."
(dolist (record ebox-m0a-characterization-inventory)
(when (eq (plist-get record :status) 'current-green)
(let* ((file (plist-get record :file))
(test (plist-get record :test))
(source
(with-temp-buffer
(insert-file-contents file)
(buffer-string))))
(should
(string-match-p
(format "(ert-deftest[[:space:]\n]+%s\\_>"
(regexp-quote (symbol-name test)))
source))))))
(provide 'ebox-m0a-inventory-tests)
;;; ebox-m0a-inventory-tests.el ends here

View File

@ -81,6 +81,7 @@
(ert-deftest ebox-source-rebind-keeps-order-and-bounded-persistence ()
"Long-lived local updates must preserve order without growing recursion."
(should (= ebox-source--persistent-max-depth 4))
(let* ((builder (ebox-source-builder-create))
(first (ebox-source-builder-bind builder :identity 'a))
(middle (ebox-source-builder-bind builder :identity 'b))
@ -124,7 +125,7 @@
(let* ((base-values (make-hash-table :test #'equal))
(_ (puthash 'retained 'value base-values))
(table (ebox-source--flat-table base-values #'equal)))
(dotimes (revision 12)
(dotimes (revision (1- ebox-source--persistent-max-depth))
(let ((added (make-hash-table :test #'equal))
(removed (make-hash-table :test #'equal)))
(puthash (list 'revision revision) revision added)
@ -140,6 +141,34 @@
(should (eq ebox-source--memo-missing
(gethash 'absent (ebox-source-table-memo table))))))
(ert-deftest ebox-source-hot-lookups-reuse-private-absence-markers ()
"Persistent lookup and order reads must not allocate absence symbols."
(let* ((opaque-value (make-symbol "opaque-source-value"))
(base-values (make-hash-table :test #'eq))
(_ (puthash 'present opaque-value base-values))
(base (ebox-source--flat-table base-values #'eq))
(added (make-hash-table :test #'eq))
(removed (make-hash-table :test #'eq))
(table (ebox-source--table-overlay base added removed 1))
(replacements (make-hash-table :test #'eq))
(order-removed (make-hash-table :test #'eq))
(original-make-symbol (symbol-function 'make-symbol))
(symbol-creations 0))
(puthash 'old 'new replacements)
(cl-letf (((symbol-function 'make-symbol)
(lambda (name)
(cl-incf symbol-creations)
(funcall original-make-symbol name))))
(should (eq opaque-value
(ebox-source--table-value table 'present)))
(should (ebox-source--table-member-p table 'present))
(should-not (ebox-source--table-member-p table 'absent))
(should-not (ebox-source--table-value table 'absent))
(should (equal '(new kept tail)
(ebox-source--order-apply
'(old kept) replacements order-removed '(tail)))))
(should (zerop symbol-creations))))
(ert-deftest ebox-source-delta-resolves-each-node-record-once ()
"Node-local source diff should materialize one record view per generation."
(let* ((old-input (ebox-build '(box :id "before" "Same")))

View File

@ -115,11 +115,56 @@
(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 "(tp \"1.0.0\")" facade))
(should (string-match-p "Version: 2.0.1" 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)
@ -206,11 +251,108 @@
(plist-get item :correlation-id))
reports)))
(dolist (item reports)
(should (numberp (plist-get item :duration-ms))))
(should (numberp (plist-get item :duration-ms)))
(should (> (plist-get item :duration-ms) 0.0)))
(should
(equal reports
(plist-get report
:framework-initial-observation-reports)))
(plist-put (car reports) :provider 'mutated)
(should-not (plist-member report :provider)))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-native-retirement-failure-is-postaccept-diagnostic ()
"A contained native retirement failure cannot escape after final accept."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-native-retirement*"))
(diagnostic
'(:phase native-session-retirement :session-id 7
:condition (error "release failed"))))
(unwind-protect
(cl-letf (((symbol-function 'ebox-surface--settle-native-session)
(lambda (&rest _arguments) (list diagnostic))))
(let ((report
(ebox-framework-spi-initial
buffer
(ebox-test-box :key 'root (ebox-test-text "committed"))
#'ignore #'ignore)))
(should (eq (plist-get report :framework-participant-state)
'completed))
(should
(equal (plist-get report :framework-participant-diagnostics)
(list diagnostic)))
(should (ebox-surface-buffer-mounted-p buffer))
(should (equal "committed"
(with-current-buffer buffer
(buffer-substring-no-properties
(point-min) (point-max)))))
(let ((update-report
(ebox-framework-spi-update
buffer
(ebox-test-box :key 'root (ebox-test-text "updated"))
#'ignore #'ignore)))
(should (eq (plist-get update-report
:framework-participant-state)
'completed))
(should
(equal
(plist-get update-report :framework-participant-diagnostics)
(list diagnostic)))
(should (equal "updated"
(with-current-buffer buffer
(buffer-substring-no-properties
(point-min) (point-max))))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-postaccept-report-failure-never-enters-rollback ()
"Initial, scoped, and full report-finalization faults stay postaccept."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-report-finalization*"))
(rollback-count 0))
(unwind-protect
(cl-letf (((symbol-function 'ebox-surface--participant-complete)
(lambda (&rest _arguments)
(error "injected report finalization failure"))))
(cl-labels
((assert-completed
(report)
(should (eq (plist-get report :framework-participant-state)
'completed))
(should
(eq (plist-get
(car (plist-get report
:framework-participant-diagnostics))
:phase)
'framework-report-finalization))))
(let ((initial-report
(ebox-framework-spi-initial
buffer
(ebox-test-box :key 'root (ebox-test-text "initial"))
#'ignore (lambda (_report) (cl-incf rollback-count)))))
(assert-completed initial-report)
(should-not
(ebox-framework-spi-initial-observation-reports
initial-report)))
(assert-completed
(ebox-framework-spi-update
buffer (ebox-test-box :key 'root (ebox-test-text "scoped"))
#'ignore (lambda (_report) (cl-incf rollback-count))))
(let ((candidate (ebox-candidate-begin buffer)))
(ebox-candidate-replace-root
candidate
(ebox-test-box :key 'root (ebox-test-text "full")))
(assert-completed
(ebox-framework-spi-update
buffer candidate #'ignore
(lambda (_report) (cl-incf rollback-count)))))
(should (zerop rollback-count))
(should (string-match-p
"full"
(with-current-buffer buffer
(buffer-substring-no-properties
(point-min) (point-max)))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-public-unmount-allows-a-fresh-initial-generation ()
"Public Ebox teardown removes retained authority before a later initial."
(ebox-spi-test--reset-runtime-state)

View File

@ -711,8 +711,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(let* ((ebox-viewport-width 80)
(ebox-viewport-height 20)
(content (propertize "native" 'face 'italic 'help-echo "source"))
(node (ebox-test-box (ebox-test-text content) :width 20
:color "#f0f0f0" :bgcolor "#101010"))
(node (ebox-test-root
(ebox-test-box (ebox-test-text content) :width 20
:color "#f0f0f0" :bgcolor "#101010")))
(package (ebox-native-reflow--compile-layout-package node))
(normal
(let ((ebox--surface-materialization-active t)
@ -1350,8 +1351,6 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(let* ((state (ebox--buffer-render-state buffer))
(session (list 'native-session))
(candidate-session (list 'candidate-native-session))
(pending (list :generation 1 :key 1
:confirmed-revision 1))
(rendered
(with-current-buffer buffer
(buffer-substring (point-min) (point-max))))
@ -1360,8 +1359,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(postorder (vector (plist-get state :root-node))))
(plist-put state :projection-kind 'native-frame)
(plist-put state :native-sync-session session)
(plist-put state :native-sync-pending pending)
(plist-put state :native-sync-confirmed-p nil)
(plist-put state :native-sync-pending nil)
(plist-put state :native-sync-confirmed-p t)
(plist-put state :native-committed-rendered rendered)
(plist-put state :native-committed-fragment-template
fragment-template)
@ -1389,8 +1388,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should-not (eq (plist-get overrides :native-sync-session)
session))
(should-not (plist-get overrides :native-sync-pending))
(should-not
(plist-get overrides :native-sync-confirmed-p))
(should (plist-get overrides :native-sync-confirmed-p))
(should (eq (plist-get overrides
:native-committed-rendered)
rendered))
@ -1413,7 +1411,7 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(skip-unless (ebox-native-reflow-layout-ready-p))
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-native-viewport-rollback*"))
candidate-session committed-session)
candidate-session confirmation-rejected-session committed-session)
(unwind-protect
(let ((ebox-viewport-width 180)
(ebox-viewport-height 6)
@ -1465,17 +1463,39 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should (equal-including-properties
(buffer-substring (point-min) (point-max))
contents)))
(cl-letf
(((symbol-function 'ebox-native-reflow-fork-session)
(lambda (&rest arguments)
(setq confirmation-rejected-session
(apply original-fork arguments))))
((symbol-function
'ebox-native-reflow-confirm-native-frame)
(lambda (&rest _)
(error "Reject native confirmation"))))
(should-error
(ebox-rerender-buffer-with-context buffer 320 6)))
(should confirmation-rejected-session)
(should (ebox-native-reflow-session-released-p
confirmation-rejected-session))
(should-not (ebox-native-reflow-session-released-p session))
(should (= (tp-surface-revision surface) revision))
(should (eq (tp-surface-client-state surface) state))
(let ((report
(ebox-rerender-buffer-with-context buffer 320 6)))
(setq committed-session
(plist-get (ebox--buffer-render-state buffer)
:native-sync-session))
(should (eq (plist-get report :projection-kind) 'native-frame))
(should (eq (plist-get report :native-frame-kind) 'patch))
(should committed-session)
(should-not (eq committed-session session))
(should (ebox-native-reflow-session-released-p session))
(should-not (ebox-native-reflow-session-released-p
committed-session))
(should (plist-get (ebox--buffer-render-state buffer)
:native-sync-confirmed-p))
(should-not (plist-get (ebox--buffer-render-state buffer)
:native-sync-pending))
(should (eq (plist-get
(plist-get (ebox--buffer-render-state buffer)
:root-node)
@ -1487,6 +1507,151 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(should (ebox-native-reflow-session-released-p
committed-session))))))
(ert-deftest ebox-native-full-frame-bootstraps-from-ordinary-surface ()
"Native bootstrap falls back truthfully and remains retryable."
(skip-unless (ebox-native-reflow-layout-ready-p))
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-native-bootstrap*"))
rejected-session winning-session next-session)
(unwind-protect
(let* ((ebox-viewport-width 240)
(ebox-viewport-height 8)
(ebox-runtime-idle-prewarm nil)
(ebox-runtime-idle-reflow-cache-prewarm nil)
(initial
(ebox-test-column
(ebox-test-box :key 'a (ebox-test-text "A"))
(ebox-test-box :key 'b (ebox-test-text "B"))))
(expanded
(ebox-test-column
(ebox-test-box :key 'a (ebox-test-text "A"))
(ebox-test-box :key 'b (ebox-test-text "B"))
(ebox-test-box :key 'c (ebox-test-text "C"))))
(native-source
(ebox-test-column
(ebox-test-box :key 'a (ebox-test-text "A"))
(ebox-test-box :key 'b (ebox-test-text "B"))
(ebox-test-box :key 'c (ebox-test-text "C"))
(ebox-test-box :key 'd (ebox-test-text "D"))))
(updated
(ebox-test-column
(ebox-test-box :key 'a (ebox-test-text "A"))
(ebox-test-box :key 'b (ebox-test-text "B"))
(ebox-test-box :key 'c (ebox-test-text "C"))
(ebox-test-box :key 'd (ebox-test-text "D"))
:bgcolor "#222222")))
;; Exercise the normal non-native mount contract; only the later
;; structural commit is allowed to bootstrap a private session.
(cl-letf (((symbol-function 'ebox-native-reflow-layout-ready-p)
(lambda () nil)))
(ebox-render-to-buffer buffer initial))
(let* ((surface
(with-current-buffer buffer ebox-surface--buffer-surface))
(ordinary-state (tp-surface-client-state surface))
(ordinary-root-id
(plist-get (plist-get ordinary-state :root-node) :node-id))
(ordinary-root-object
(gethash ordinary-root-id
(plist-get ordinary-state
:surface-node-object-table)))
(original-create
(symbol-function 'ebox-native-reflow-create-session))
(original-render
(symbol-function 'ebox-surface--render-candidate-node))
(original-project
(symbol-function 'ebox-surface--projection-start))
(render-count 0)
(projection-count 0))
(should-not (plist-get ordinary-state :native-sync-session))
(should-not (eq (plist-get ordinary-state :projection-kind)
'native-frame))
(cl-letf
(((symbol-function 'ebox-native-reflow-create-session)
(lambda (&rest arguments)
(setq rejected-session
(apply original-create arguments))))
((symbol-function 'ebox-native-reflow-execute-session-sync)
(lambda (&rest _) (error "Reject native execution")))
((symbol-function 'ebox-surface--render-candidate-node)
(lambda (&rest arguments)
(cl-incf render-count)
(apply original-render arguments)))
((symbol-function 'ebox-surface--projection-start)
(lambda (&rest arguments)
(cl-incf projection-count)
(apply original-project arguments))))
(let* ((report (ebox-commit buffer expanded))
(state (tp-surface-client-state surface))
(expected
(let ((ebox-viewport-width 240)
(ebox-viewport-height 8))
(ebox-surface-test--render-runtime state))))
(should-not (plist-get report :projection-kind))
(should-not (plist-get report :native-frame-kind))
(should (eq (plist-get report :strategy)
'ordinary-fallback))
(should (equal (plist-get report :patch-ops)
'(tp-surface)))
(should (eq (plist-get report :render-scope) 'surface))
(should-not (plist-member report :owner-ids))
(should-not (plist-member report :owner-type))
(should (eq (plist-get report :native-attempt) 'failed))
(should (equal (plist-get report :native-fallback-reason)
"Reject native execution"))
(should (= render-count 1))
(should (= projection-count 1))
(should-not (plist-get state :projection-kind))
(dolist (key ebox-native-commit--failed-render-state-keys)
(should-not (plist-member state key)))
(should-not (plist-member state :native-render-fallback))
(should (= (plist-get (plist-get state :root-node) :node-id)
ordinary-root-id))
(should (eq (gethash ordinary-root-id
(plist-get state
:surface-node-object-table))
ordinary-root-object))
(with-current-buffer buffer
(should (equal-including-properties
(buffer-substring (point-min) (point-max))
expected)))))
(should rejected-session)
(should (ebox-native-reflow-session-released-p rejected-session))
(let ((report (ebox-commit buffer native-source)))
(setq winning-session
(plist-get (tp-surface-client-state surface)
:native-sync-session))
(should (eq (plist-get report :projection-kind) 'native-frame))
;; A bootstrap begins with a full frame; session/compiler
;; continuity does not claim a cross-handle Rust patch.
(should (eq (plist-get report :native-frame-kind) 'full))
(should winning-session)
(should-not (plist-get (tp-surface-client-state surface)
:native-sync-pending))
(should (plist-get (tp-surface-client-state surface)
:native-sync-confirmed-p))
(should (stringp
(plist-get (tp-surface-client-state surface)
:native-committed-rendered)))
(should-not (ebox-native-reflow-session-released-p
winning-session)))
(let ((report (ebox-commit buffer updated)))
(setq next-session
(plist-get (tp-surface-client-state surface)
:native-sync-session))
(should (eq (plist-get report :projection-kind) 'native-frame))
;; A style-table change cannot consume the prior confirmed
;; document and therefore remains a truthful full frame.
(should (eq (plist-get report :native-frame-kind) 'full))
(should next-session)
(should-not (eq next-session winning-session))
(should (ebox-native-reflow-session-released-p winning-session))
(should-not (ebox-native-reflow-session-released-p
next-session)))))
(when (buffer-live-p buffer)
(kill-buffer buffer))
(when next-session
(should (ebox-native-reflow-session-released-p next-session))))))
(ert-deftest ebox-viewport-reflow-retains-final-sized-flex-child-fragments ()
"Viewport reflow should reuse final-sized Flex child fragments exactly."
(ebox-surface-test--reset-render-state)
@ -1731,6 +1896,63 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-text-style-owns-local-cascade-state ()
"Persist an independent style owner state for canonical Text."
(ebox-surface-test--reset-render-state)
(let ((buffer (generate-new-buffer " *ebox-shared-text-style-state*")))
(unwind-protect
(let ((ebox-viewport-width 240)
(ebox-viewport-height 6))
(ebox-render-to-buffer
buffer
(ebox-test-box
:color "#334155" :width '(viewport)
(ebox-test-box (ebox-test-text "Inherited"))))
(let* ((state (ebox--buffer-render-state buffer))
(nodes (plist-get state :node-table))
(objects (plist-get state :surface-node-object-table))
(style-states (plist-get state :style-binding-states))
(source-index (plist-get state :source-index))
text-id text-node)
(maphash
(lambda (node-id node)
(when (and (eq (plist-get node :ebox-kind) 'text)
(equal (plist-get node :content) "Inherited"))
(setq text-id node-id text-node node)))
nodes)
(should text-id)
(let* ((parent-id
(gethash text-id (plist-get state :parent-table)))
(object (gethash text-id objects))
(parent-object (gethash parent-id objects))
(style-state (gethash object style-states))
(parent-state (gethash parent-object style-states))
(candidate-node (copy-sequence text-node))
(candidate-nodes (copy-hash-table nodes))
(candidate-state (copy-sequence state)))
(should style-state)
(should parent-state)
(should-not (eq (plist-get style-state :binding)
(plist-get parent-state :binding)))
(should (eq (plist-get style-state :parent-style-state)
parent-state))
(should
(equal (ebox-tree-node-id source-index text-node)
(ecss-subject-id (plist-get style-state :subject))))
(should (ebox-surface--static-style-state-p style-state))
(plist-put candidate-node :ebox-candidate-computed-style-p t)
(puthash text-id candidate-node candidate-nodes)
(plist-put candidate-state :node-table candidate-nodes)
(let ((ebox-incremental--allocated-slot-proof-cache
(make-hash-table :test 'equal)))
(should
(ebox-surface--cascade-local-owner-proof-p
state candidate-state
(list (list :node-id text-id :dirty-kind 'geometry
:changed-keys '(:content)))))))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(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
@ -3024,6 +3246,83 @@ participant and rollback contract without creating a real frame."
(when (buffer-live-p buffer)
(kill-buffer buffer))))
(ert-deftest ebox-surface-native-patch-fast-path-requires-stable-topology ()
"Only a topology-stable native patch may bypass ordinary TP planning."
(let ((projection (list :surface-root 'surface
:objects-by-node (make-hash-table :test 'eq)))
(calls nil))
(cl-letf (((symbol-function 'ebox-surface--node-object-table)
(lambda (_) (make-hash-table :test 'equal)))
((symbol-function 'ebox-surface--region-object-table)
(lambda (&rest _) (make-hash-table :test 'equal)))
((symbol-function 'ebox-surface--object-region-table)
(lambda (_) (make-hash-table :test 'eq)))
((symbol-function 'ebox-surface--logical-id-region-table)
(lambda (&rest _) (make-hash-table :test 'equal)))
((symbol-function 'tp-object-ensure)
(lambda (_context _parent key _kind)
(push 'native-object calls)
key))
((symbol-function 'ebox-surface--native-owned-ranges)
(lambda (&rest _)
(push 'native-ownership calls)
'((:start 0 :end 3))))
((symbol-function 'tp-commit-batch-create)
(lambda (&rest _)
(push 'native-batch calls)
'native-batch))
((symbol-function 'tp-commit-batch-result-create)
(lambda (&rest _)
(push 'native-result calls)
'native-fast-path))
((symbol-function 'ebox-surface--surface-plan)
(lambda (&rest _)
(push 'ordinary-plan calls)
'(ordinary-plan "new" ((:start 0 :end 3)) nil)))
((symbol-function 'tp-surface-result-create-owned)
(lambda (&rest _)
(push 'ordinary-result calls)
'ordinary-owned-path))
((symbol-function 'tp-surface-retained-content-result-create)
(lambda (&rest _)
(push 'retained-result calls)
'retained-path)))
(let ((unstable
'(:root-node nil
:runtime-revision 4
:native-topology-stable-p nil
:native-render-fragment-template [[0 3 0 nil nil nil nil nil]]
:native-render-frame
(:native-frame t :native-patch t
:base-character-count 3 :target-character-count 3
:patches nil :coordinate-patches nil))))
(should
(eq (ebox-surface--projection-result
'context projection unstable "new" 'native-frame)
'ordinary-owned-path))
(should (equal calls '(ordinary-result ordinary-plan)))
(should-not (plist-member unstable :native-committed-rendered)))
(setq calls nil)
(let ((stable
'(:root-node nil
:runtime-revision 4
:native-topology-stable-p t
:native-render-fragment-template [[0 3 0 nil nil nil nil nil]]
:native-render-frame
(:native-frame t :native-patch t
:base-character-count 3 :target-character-count 3
:patches nil :coordinate-patches nil))))
(should
(eq (ebox-surface--projection-result
'context projection stable "new" 'native-frame)
'native-fast-path))
(should (equal calls
'(native-result native-batch native-ownership
native-object native-object)))
(should (equal (plist-get stable :native-committed-rendered) "new"))
(should-not (memq 'ordinary-plan calls))
(should-not (memq 'retained-result calls))))))
(ert-deftest ebox-surface-native-full-frame-owns-full-publication-scope ()
"A complete native frame is authoritative for the whole TP surface."
(should