feat: support framework Host teardown
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-01 01:07:23 +08:00
parent 17d67c063a
commit 958132f604
7 changed files with 117 additions and 7 deletions

View File

@ -90,7 +90,7 @@ Caller-owned Source Tree
- `ebox-layout.el` has no load or call edge to `ebox-surface.el` or TP. Candidate isolation, inherited-cascade detection, and one-shot materialization enter layout only through the validated port owned by `ebox-render-context.el`; `ebox.el` wires the surface implementation after both sides load.
- `ebox-patch-plan.el` consumes only tentative operation plists plus the immutable generation parent table and returns a deterministic plan artifact. The incremental adapter owns all live-fact preparation. E3 defaults to the pure route while retaining explicit `legacy` and equality-checking `shadow` routes; none of the three routes publishes.
- `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, and `tp-transaction-protocol-v1+v2`. 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`. 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.
- `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.

View File

@ -90,7 +90,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- `ebox-layout.el` 不再 load 或调用 `ebox-surface.el`/TP。candidate isolation、继承 cascade 判定和一次性 materialization 只能通过 `ebox-render-context.el` 拥有的已校验 port 进入 layout`ebox.el` 在两侧加载后接线 surface 实现。
- `ebox-patch-plan.el` 只消费 tentative operation plist 与不可变 generation parent table并返回 deterministic plan artifact所有 live fact 准备仍由 incremental adapter 拥有。E3 默认使用 pure route同时保留显式 `legacy` 与执行等价检查的 `shadow` route三条 route 都不能发布。
- `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 以及 `tp-transaction-protocol-v1+v2`。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`。replay 只从 completed initial report 构造 defensive TP/Ebox provider reports不带 mutate authority。Ebox 不创建 selected port 或 consumer bootstrap现有 v1 callback API 保留。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。

View File

@ -175,6 +175,7 @@ though the facade inventory below focuses on application entry points.
| --- | --- |
| `ebox-render` | Accept a single-root `CanonicalEboxInput` and return a propertized string without publishing a live buffer. |
| `ebox-render-to-buffer` | Accept a single-root `CanonicalEboxInput`, mount a retained TP surface, enable `ebox-buffer-mode`, and return the buffer. Its optional plist accepts only `:observer FUNCTION`. |
| `ebox-unmount-buffer` | Release a mounted Ebox/TP surface, observers, retained indexes, and runtime authority from a live buffer. |
| `ebox-display-buffer` | Accept a single-root `CanonicalEboxInput`, render through the retained path, delete other windows, and switch to the result. |
| `ebox-commit` | Atomically publish a single-root `CanonicalEboxInput` or a one-shot logical candidate returned by `ebox-candidate-begin`. |
| `ebox-buffer-set-observer` | Set or remove one function-valued observer on an already mounted buffer. |
@ -288,7 +289,8 @@ names every entry:
`ebox-column-layout-create`, `ebox-flex-layout-create`,
`ebox-grid-layout-create`, `ebox-box-create`.
- Render/publication: `ebox-render`, `ebox-render-to-buffer`,
`ebox-display-buffer`, `ebox-commit`, `ebox-buffer-set-observer`,
`ebox-unmount-buffer`, `ebox-display-buffer`, `ebox-commit`,
`ebox-buffer-set-observer`,
`ebox-buffer-update-report`,
`ebox-rerender-buffer-with-context`, `ebox-viewport-window-width`,
`ebox-call-with-render-burst`, `ebox-render-burst-begin`,

View File

@ -163,6 +163,7 @@ typed 模块还公开 `ebox-node-kind`、`ebox-text-node-p`、`ebox-box-node-p`
| --- | --- |
| `ebox-render` | 接收 single-root `CanonicalEboxInput`,返回带属性字符串,不发布 live buffer。 |
| `ebox-render-to-buffer` | 接收 single-root `CanonicalEboxInput`,挂载 retained TP surface启用 `ebox-buffer-mode` 并返回 buffer可选 plist 只接受 `:observer FUNCTION`。 |
| `ebox-unmount-buffer` | 从 live buffer 释放 mounted Ebox/TP surface、observer、retained indexes 与 runtime authority。 |
| `ebox-display-buffer` | 接收 single-root `CanonicalEboxInput`,走 retained 路径渲染,删除其他 window 后切换到结果。 |
| `ebox-commit` | 原子发布 single-root `CanonicalEboxInput``ebox-candidate-begin` 返回的 one-shot logical candidate。 |
| `ebox-buffer-set-observer` | 给已经 mounted 的 buffer 设置或移除一个 function observer。 |
@ -264,8 +265,8 @@ native 模块只是加速器,不是正确性依赖。加载包不会构建它
- 构造:`ebox-build`、`ebox-text-create`、`ebox-normal-layout-create`、
`ebox-row-layout-create`、`ebox-column-layout-create`、
`ebox-flex-layout-create`、`ebox-grid-layout-create`、`ebox-box-create`
- 渲染/发布:`ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer`、
`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
- 渲染/发布:`ebox-render`、`ebox-render-to-buffer`、`ebox-unmount-buffer`、
`ebox-display-buffer`、`ebox-commit`、`ebox-buffer-set-observer`、`ebox-buffer-update-report`、
`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width`、
`ebox-call-with-render-burst`、`ebox-render-burst-begin`、
`ebox-render-burst-end`

View File

@ -37,7 +37,8 @@
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)
same-object-legacy-report
initial-observation-replay)
"Capabilities guaranteed by every Ebox framework SPI v2 provider record.")
(defconst ebox-framework-spi--stage-order
@ -171,6 +172,37 @@ Return the same completed legacy report object observed by FRAMEWORK-STAGE."
framework-stage framework-rollback)
(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.
The reports are defensive observational snapshots; they expose no publication
or promotion capability."
(unless (and (proper-list-p report)
(eq (plist-get report :framework-participant-state) 'completed)
(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)))
(defun ebox-framework-spi--operation
(kind function argument-schema)
"Return a fresh KIND operation descriptor for FUNCTION and ARGUMENT-SCHEMA."

22
ebox.el
View File

@ -4445,6 +4445,27 @@ TP/Ebox reports after accepted initial publication and later commits."
(declare (indent 1))
(ebox--render-to-buffer-internal buffer-or-name input options))
;;;###autoload
(defun ebox-unmount-buffer (buffer-or-name)
"Unmount Ebox's retained surface from BUFFER-OR-NAME and return the buffer.
The operation releases TP surface authority, observers, retained indexes,
scroll/native runtime state, and the buffer-local Ebox surface handle."
(let ((buffer (get-buffer buffer-or-name)))
(unless (buffer-live-p buffer)
(user-error "Ebox unmount target is not a live buffer: %S"
buffer-or-name))
(let ((surface (ebox-surface--live-buffer-surface buffer)))
(unless surface
(user-error "Ebox buffer has no mounted TP surface: %S" buffer-or-name))
(tp-surface-unmount surface)
(ebox-surface-cleanup-buffer-observer buffer)
(ebox--clear-buffer-runtime-state buffer)
(with-current-buffer buffer
(setq-local ebox-surface--buffer-surface nil)
(remove-hook 'kill-buffer-hook #'ebox--cleanup-current-buffer t)
(ebox-buffer-mode -1))
buffer)))
(defun ebox--host-ref-buffer (buffer-or-name)
"Return the live buffer named by BUFFER-OR-NAME, or nil."
(let ((buffer (and buffer-or-name (get-buffer buffer-or-name))))
@ -4829,6 +4850,7 @@ through dirty-set and patch-set execution before falling back to root rerender."
ebox-selector-update-buffer
ebox-string-pixel-width
ebox-update-selector
ebox-unmount-buffer
ebox-viewport-window-width
ebox-wheel-scroll-down
ebox-wheel-scroll-up)

View File

@ -47,7 +47,8 @@
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)))
same-object-legacy-report
initial-observation-replay)))
(should
(equal (ebox-framework-spi-provider-stage-order first)
'(ebox-mirror/native framework-stage)))
@ -181,6 +182,58 @@
(point-min) (point-max)))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-initial-replays-observational-provider-reports ()
"Completed initial reports replay TP then Ebox without mutable authority."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-observation*")))
(unwind-protect
(let* ((report
(ebox-framework-spi-initial
buffer
(ebox-test-box :key 'root (ebox-test-text "observed"))
#'ignore #'ignore))
(reports
(ebox-framework-spi-initial-observation-reports report)))
(should (equal (mapcar (lambda (item) (plist-get item :provider))
reports)
'(tp ebox)))
(should (equal (mapcar (lambda (item) (plist-get item :stage))
reports)
'(publication mount)))
(should
(apply #'=
(mapcar (lambda (item)
(plist-get item :correlation-id))
reports)))
(dolist (item reports)
(should (numberp (plist-get item :duration-ms))))
(plist-put (car reports) :provider 'mutated)
(should-not (plist-member report :provider)))
(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)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-remount*")))
(unwind-protect
(progn
(ebox-framework-spi-initial
buffer (ebox-test-box :key 'root (ebox-test-text "first"))
#'ignore #'ignore)
(should (ebox-surface-buffer-mounted-p buffer))
(should (eq (ebox-unmount-buffer buffer) buffer))
(should-not (ebox-surface-buffer-mounted-p buffer))
(ebox-framework-spi-initial
buffer (ebox-test-box :key 'root (ebox-test-text "second"))
#'ignore #'ignore)
(should (ebox-surface-buffer-mounted-p buffer))
(should (string-match-p
"second"
(with-current-buffer buffer
(buffer-substring-no-properties
(point-min) (point-max))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-initial-rolls-the-same-report-back ()
"A later TP failure rolls initial framework and Ebox state back in order."
(ebox-spi-test--reset-runtime-state)