From 958132f604272c5ac98d6a6d998ad1a100ca810f Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Tue, 1 Sep 2026 01:07:23 +0800 Subject: [PATCH] feat: support framework Host teardown --- ...box-current-implementation-reference.en.md | 2 +- ...box-current-implementation-reference.zh.md | 2 +- docs/user/ebox-api-reference.en.md | 4 +- docs/user/ebox-api-reference.zh.md | 5 +- ebox-spi.el | 34 +++++++++++- ebox.el | 22 ++++++++ tests/ebox-spi-tests.el | 55 ++++++++++++++++++- 7 files changed, 117 insertions(+), 7 deletions(-) diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index 1140dd7..a98d9d8 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -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. diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index 44dc9ef..7f14830 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -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,正确性不变。 diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 5309cd2..114f91f 100644 --- a/docs/user/ebox-api-reference.en.md +++ b/docs/user/ebox-api-reference.en.md @@ -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`, diff --git a/docs/user/ebox-api-reference.zh.md b/docs/user/ebox-api-reference.zh.md index 480d8a4..f249e13 100644 --- a/docs/user/ebox-api-reference.zh.md +++ b/docs/user/ebox-api-reference.zh.md @@ -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`; diff --git a/ebox-spi.el b/ebox-spi.el index afdce63..f1141e7 100644 --- a/ebox-spi.el +++ b/ebox-spi.el @@ -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." diff --git a/ebox.el b/ebox.el index bc1ac0c..2b0bfa1 100644 --- a/ebox.el +++ b/ebox.el @@ -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) diff --git a/tests/ebox-spi-tests.el b/tests/ebox-spi-tests.el index fd39f57..94c8015 100644 --- a/tests/ebox-spi-tests.el +++ b/tests/ebox-spi-tests.el @@ -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)