From b5f314edff62f94b7404bcf007ae0cd075dd28cb Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sat, 5 Sep 2026 05:25:29 +0800 Subject: [PATCH] refactor: require SPI v2 for runtime mounts and Host attachment --- Makefile | 7 +- README.md | 16 +- README.zh-CN.md | 14 +- docs/architecture.en.md | 14 +- docs/architecture.zh.md | 11 +- docs/migration-0.2.en.md | 16 + docs/migration-0.2.zh.md | 14 + etaf-host.el | 56 ++- etaf-render-port.el | 392 ++---------------- etaf-runtime.el | 9 +- etaf.el | 2 +- tests/etaf-docs-tests.el | 2 + tests/etaf-g1-cross-layer-tests.el | 132 +++--- tests/etaf-host-tests.el | 4 +- tests/etaf-render-port-tests.el | 243 +++++------ .../etaf-m0a-condition-consumers.sexp | 2 - 16 files changed, 291 insertions(+), 643 deletions(-) create mode 100644 docs/migration-0.2.en.md create mode 100644 docs/migration-0.2.zh.md diff --git a/Makefile b/Makefile index c76edad..664df2c 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ SOURCES = etaf-view.el etaf-compiler.el etaf-component.el etaf-scheduler.el etaf EXAMPLES = examples/etaf-counter-example.el examples/etaf-data-example.el examples/etaf-resource-example.el TESTS = tests/etaf-tests.el tests/etaf-compiler-tests.el tests/etaf-component-frontends-tests.el tests/etaf-resource-tests.el tests/etaf-data-tests.el tests/etaf-theme-tp-tests.el tests/etaf-examples-tests.el tests/etaf-observer-tests.el tests/etaf-performance-tests.el tests/etaf-gui-verifier-tests.el tests/etaf-m0a-current-characterization-tests.el tests/etaf-interaction-contract-tests.el tests/etaf-m0b-component-manifest-tests.el tests/etaf-render-port-tests.el tests/etaf-generation-tests.el tests/etaf-host-tests.el tests/etaf-retirement-tests.el tests/etaf-scheduler-tests.el tests/etaf-g1-cross-layer-tests.el -.PHONY: test compile load checkdoc docs-check scheduler-benchmark check clean +.PHONY: test compile load checkdoc docs-check metadata-check scheduler-benchmark check clean test: compile $(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" \ @@ -24,6 +24,9 @@ docs-check: $(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" -l tests/etaf-docs-tests.el \ -f ert-run-tests-batch-and-exit +metadata-check: + $(EMACS) -Q --batch --eval "(progn (require 'package) (with-temp-buffer (insert-file-contents \"etaf.el\") (let ((desc (package-buffer-info))) (unless (and (equal (package-desc-version desc) '(0 2 1)) (equal (package-desc-reqs desc) '((emacs (29 1)) (ebox (3 0 0)) (tp (2 0 0))))) (error \"Unexpected ETAF package metadata: %S\" desc)))))" + scheduler-benchmark: $(EMACS) -Q --batch $(LOAD_PATH) --eval "(setq load-prefer-newer t)" \ -l scripts/benchmark-scheduler-context.el \ @@ -32,7 +35,7 @@ scheduler-benchmark: checkdoc: $(EMACS) -Q --batch --eval '(progn (require (quote checkdoc)) (dolist (directory (list "." "examples" "scripts")) (dolist (file (directory-files directory t)) (when (string-suffix-p ".el" file) (checkdoc-file file)))))' -check: checkdoc compile test docs-check scheduler-benchmark +check: checkdoc metadata-check compile test docs-check scheduler-benchmark clean: rm -f *.elc examples/*.elc scripts/*.elc tests/*.elc diff --git a/README.md b/README.md index 1e9ad75..f67bb15 100644 --- a/README.md +++ b/README.md @@ -112,15 +112,15 @@ There is no separate `etaf-data` install: Data is a core ETAF capability. There ## Load and verify -ECSS 0.1.0 and TP 1.0.1 are independent packages and may be installed in either order. Install both before Ebox 2.0.1, then install ETAF 0.1.1. ETAF declares TP directly because Host final-accept authority uses the TP transaction contract; rendering still consumes only the Ebox 2.0 public contract. +Install ECSS 0.1.0 and TP 2.0.0 before Ebox 3.0.0, then install ETAF 0.2.1. +ETAF declares TP directly because Host final-accept authority uses the TP +transaction contract. Rendering requires Ebox framework SPI v2; a missing, +malformed, or incompatible provider fails during ETAF bootstrap. -ETAF selects the Ebox framework SPI v2 port by default. For an immediate -process-wide rollback to the complete legacy render/Host port, set -`etaf-render-port-selection-policy` to `v1` before loading ETAF. The selection -is immutable for that Emacs process; restart Emacs to change it. This rollback -does not create a second generation owner: semantic CAS, projected compatibility -stores, retirement, and scheduler authority stay unified, preserving the same -generation, token, and store-version outcomes on both render ports. +ETAF snapshots one immutable v2 render port for the Emacs process. During an +ordered upgrade it also accepts Ebox's transitional dual-capability TP manifest +because that manifest contains the required v2 protocol. ETAF never dispatches +through the retired v1 capability. During development, load the sibling Ebox checkout before ETAF: diff --git a/README.zh-CN.md b/README.zh-CN.md index ad428fc..8e849ea 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -106,14 +106,14 @@ operation 的 flat 阶段。`etaf-performance-records` 返回 operation/stage ## 加载与验证 -ECSS 0.1.0 与 TP 1.0.1 是互相独立的包,安装顺序任意;两者都安装后再安装 Ebox 2.0.1,最后安装 ETAF 0.1.1。ETAF 会直接声明 TP 依赖,因为 Host final-accept authority 使用 TP transaction contract;渲染仍只使用 Ebox 2.0 的公共契约。 +先安装 ECSS 0.1.0 与 TP 2.0.0,再安装 Ebox 3.0.0,最后安装 ETAF 0.2.1。 +ETAF 会直接声明 TP 依赖,因为 Host final-accept authority 使用 TP transaction +contract。渲染要求 Ebox framework SPI v2;provider 缺失、格式错误或不兼容时, +ETAF 会在 bootstrap 阶段 fail closed。 -ETAF 默认选择 Ebox framework SPI v2 port。若要立即按进程完整回退旧 -render/Host port,必须在加载 ETAF 前把 -`etaf-render-port-selection-policy` 设为 `v1`。该选择在当前 Emacs 进程中不可 -变;切换时需要重启 Emacs。这个回退不会创建第二个 generation owner:semantic -CAS、单向投影的兼容 stores、retirement 和 scheduler authority 仍保持统一,因此 -两个 render port 的 generation、token 与 store-version outcome 完全一致。 +ETAF 为当前 Emacs 进程 snapshot 一个不可变的 v2 render port。按依赖顺序升级时, +也接受 Ebox 过渡期的 TP 双能力 manifest,因为其中包含所需的 v2 协议;ETAF +不会调用已经退役的 v1 capability。 开发时先把同级 Ebox 检出目录加入 `load-path`: diff --git a/docs/architecture.en.md b/docs/architecture.en.md index 5319aa1..c8311b5 100644 --- a/docs/architecture.en.md +++ b/docs/architecture.en.md @@ -458,14 +458,12 @@ back into it. The migration-only `legacy`, `project`, and `shadow` routes prove projection equivalence and rollback safety without introducing a second committed truth. -At process bootstrap, `etaf-render-port-selection-policy` selects exactly one -immutable Ebox port. Its default `v2` value uses the compatible framework SPI; -`v1`, when set before ETAF is loaded, bypasses provider probing and selects the -complete legacy render/Host port. The rollback changes only that inter-package -publication route. Generation/store CAS, compatibility projection, retirement, -and scheduler authority remain single-owner so the old and new render ports -produce identical generation, token, and store-version outcomes. A mounted or -in-flight Runtime is never switched between ports. +At process bootstrap, `etaf-render-port` requires Ebox framework SPI v2 and +snapshots exactly one immutable port. A missing, malformed, or incompatible +provider fails closed before any Runtime is mounted. The ordered dependency +upgrade may expose either a transitional TP v1+v2 manifest or the final v2-only +manifest; both prove the required v2 capability, and ETAF never calls a v1 +operation. A mounted or in-flight Runtime never switches ports. Each semantic candidate captures the expected generation, semantic token, and instance/resource/artifact/route store versions. The render path stages one CAS diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 03725ab..78e91c8 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -447,12 +447,11 @@ generation;Runtime 中同名 hash table 只是由 generation 单向重建的 `legacy`/`project`/`shadow` 路由只用于证明投影等价与安全回退,不增加第二份 committed truth。 -进程 bootstrap 时,`etaf-render-port-selection-policy` 只选择一个不可变的 Ebox -port。默认值 `v2` 使用兼容的 framework SPI;若在 ETAF 加载前设为 `v1`,则跳过 -provider 探测并选择完整的旧 render/Host port。这个回退只改变跨包 publication -route;generation/store CAS、兼容投影、retirement 与 scheduler authority 始终只有 -一个 owner,因此新旧 render port 会产生完全一致的 generation、token 与 -store-version outcome。mounted 或 in-flight Runtime 绝不会在两个 port 之间切换。 +进程 bootstrap 时,`etaf-render-port` 要求 Ebox framework SPI v2,并 snapshot +唯一的不可变 port。provider 缺失、格式错误或不兼容都会在 Runtime mount 前 fail +closed。按依赖顺序升级时,Ebox 可暂时报告 TP v1+v2 双能力 manifest,也可报告最终 +的 v2-only manifest;两者都证明所需的 v2 capability,ETAF 不会调用 v1 operation。 +mounted 或 in-flight Runtime 绝不会切换 port。 每个 semantic candidate 同时捕获 expected generation、semantic token,以及 instance/resource/artifact/route store versions。render 路径在 Ebox framework diff --git a/docs/migration-0.2.en.md b/docs/migration-0.2.en.md new file mode 100644 index 0000000..45413ee --- /dev/null +++ b/docs/migration-0.2.en.md @@ -0,0 +1,16 @@ +# Migrating to ETAF 0.2 + +ETAF 0.2.1 requires Ebox 3.0.0 and TP 2.0.0. Install TP, then Ebox, then ETAF. +The renderer bootstrap now requires Ebox framework SPI v2 and fails closed when +the provider is absent, malformed, or incompatible. + +Remove any configuration of `etaf-render-port-selection-policy`. The v1 render +port and its manual initial-publication cleanup path have been retired. Runtime +mounts and updates always use the immutable SPI v2 port, including its paired +framework stage and rollback, same-object report, observation replay, revision, +and Host final-marker contracts. + +During an ordered source upgrade, ETAF accepts Ebox providers that report either +`tp-transaction-protocol-v1+v2` or `tp-transaction-protocol-v2`. The former is a +temporary capability declaration that still contains the required v2 protocol; +ETAF never invokes a v1 transaction operation. diff --git a/docs/migration-0.2.zh.md b/docs/migration-0.2.zh.md new file mode 100644 index 0000000..ad960ea --- /dev/null +++ b/docs/migration-0.2.zh.md @@ -0,0 +1,14 @@ +# 迁移到 ETAF 0.2 + +ETAF 0.2.1 要求 Ebox 3.0.0 与 TP 2.0.0。安装顺序为 TP、Ebox、ETAF。 +renderer bootstrap 现在强制要求 Ebox framework SPI v2;provider 缺失、格式错误 +或不兼容时会 fail closed。 + +请删除所有 `etaf-render-port-selection-policy` 配置。v1 render port 及其 initial +publication 手工清理路径已经退役。Runtime mount 与 update 始终使用不可变的 SPI +v2 port,并保留配对 framework stage/rollback、同对象 report、observation replay、 +revision 与 Host final marker 契约。 + +按依赖顺序从源码升级时,ETAF 接受 Ebox 报告 +`tp-transaction-protocol-v1+v2` 或 `tp-transaction-protocol-v2`。前者只是包含所需 +v2 协议的过渡期能力声明;ETAF 不会调用 v1 transaction operation。 diff --git a/etaf-host.el b/etaf-host.el index bb47f5f..0c8ce22 100644 --- a/etaf-host.el +++ b/etaf-host.el @@ -5,7 +5,7 @@ ;;; Commentary: ;; Owns one mounted Runtime's fixed Host authority token and state machine. -;; Initial v2 attachment registers a bounded TP final marker so attached +;; Initial attachment registers a bounded TP final marker so attached ;; authority and buffer publication share one final-accept boundary. Detach ;; invalidates authority in O(1) before any unbounded retirement work. @@ -83,10 +83,8 @@ :index index :value value)) -(defun etaf-host-authority-stage-attach (authority &optional legacy-p) - "Stage AUTHORITY attachment and register its final marker. -When LEGACY-P is non-nil, publish attached state directly after the legacy Ebox -initial operation and rely on manual framework rollback." +(defun etaf-host-authority-stage-attach (authority) + "Stage AUTHORITY attachment and register its final marker." (unless (eq (etaf-host-authority-state authority) 'attaching) (signal 'etaf-host-authority-error (list :stage-attach (etaf-host-authority-state authority)))) @@ -94,32 +92,28 @@ initial operation and rely on manual framework rollback." (token (etaf-host-authority-token authority)) (version (etaf-host-authority-version authority))) (aset slots etaf-host--state-slot 'provisionally-attached) - (if legacy-p - (let ((inhibit-quit t)) - (aset slots etaf-host--state-slot 'attached) - (aset slots etaf-host--version-slot (1+ version))) - (tp-transaction-register-final-marker - :owner-key - (list 'etaf-host - (etaf-host-authority-host-id authority) - (etaf-host-authority-mount-epoch authority)) - :expected-token - (tp-final-marker-expectation-create - :target slots :index etaf-host--token-slot :value token) - :expected-version - (tp-final-marker-expectation-create - :target slots :index etaf-host--version-slot :value version) - :next-values - (vector - (etaf-host--slot-write authority etaf-host--state-slot 'attached) - (etaf-host--slot-write authority etaf-host--version-slot (1+ version))) - :inverse-values - (vector - (etaf-host--slot-write - authority etaf-host--state-slot 'provisionally-attached) - (etaf-host--slot-write authority etaf-host--version-slot version)) - :slot-write-count 2 - :operation-key 'tp-vector-slots/v1)) + (tp-transaction-register-final-marker + :owner-key + (list 'etaf-host + (etaf-host-authority-host-id authority) + (etaf-host-authority-mount-epoch authority)) + :expected-token + (tp-final-marker-expectation-create + :target slots :index etaf-host--token-slot :value token) + :expected-version + (tp-final-marker-expectation-create + :target slots :index etaf-host--version-slot :value version) + :next-values + (vector + (etaf-host--slot-write authority etaf-host--state-slot 'attached) + (etaf-host--slot-write authority etaf-host--version-slot (1+ version))) + :inverse-values + (vector + (etaf-host--slot-write + authority etaf-host--state-slot 'provisionally-attached) + (etaf-host--slot-write authority etaf-host--version-slot version)) + :slot-write-count 2 + :operation-key 'tp-vector-slots/v1) authority)) (defun etaf-host-authority-rollback-attach (authority) diff --git a/etaf-render-port.el b/etaf-render-port.el index d864f7f..f3680e1 100644 --- a/etaf-render-port.el +++ b/etaf-render-port.el @@ -4,11 +4,9 @@ ;;; Commentary: -;; This file is ETAF's only Ebox framework-SPI bootstrap owner. It probes the -;; Ebox v2 provider once, selects one immutable port, and exposes that -;; selection to downstream ETAF code without repeated `featurep' or `fboundp' -;; protocol guesses. A completely absent v2 provider receives a complete v1 -;; fallback; a present but broken or incompatible provider fails closed. +;; This file is ETAF's only Ebox framework-SPI bootstrap owner. It requires +;; one compatible Ebox v2 provider, snapshots it once, and exposes one immutable +;; port to downstream ETAF code without repeated protocol guesses. ;;; Code: @@ -22,17 +20,6 @@ "Incompatible Ebox framework SPI provider" 'etaf-spi-bootstrap-error) -(defcustom etaf-render-port-selection-policy 'v2 - "ETAF render authority selected during process bootstrap. -`v2' selects the compatible Ebox SPI v2 port. `v1' is the complete legacy -render/Host rollback switch and bypasses provider probing. Generation stores, -retirement, and scheduler keep their unified authority so v1 and v2 preserve -the same generation/token/store-version outcomes. Set this before loading -ETAF; the selected port is process-wide and immutable." - :type '(choice (const :tag "ETAF v2 authorities" v2) - (const :tag "Complete ETAF v1 route" v1)) - :group 'etaf) - (defconst etaf-render-port--required-spi-version 2 "Ebox framework SPI version consumed by this ETAF build.") @@ -48,9 +35,11 @@ ETAF; the selected port is process-wide and immutable." initial-observation-replay) "Capabilities required from an Ebox framework SPI v2 provider.") -(defconst etaf-render-port--required-tp-protocol - 'tp-transaction-protocol-v1+v2 - "TP transaction protocol required by the Ebox framework SPI.") +(defconst etaf-render-port--accepted-tp-protocols + '(tp-transaction-protocol-v1+v2 tp-transaction-protocol-v2) + "TP protocols accepted from an Ebox SPI v2 provider. +The dual-capability manifest is accepted during dependency-order migration +because it contains v2; ETAF never dispatches through its v1 capability.") (defconst etaf-render-port--required-stage-order '(ebox-mirror/native framework-stage) @@ -77,7 +66,7 @@ ETAF; the selected port is process-wide and immutable." (provider nil :read-only t)) (defun etaf-render-port-route (port) - "Return selected PORT route, either `v1' or `v2'." + "Return selected PORT route, always `v2'." (etaf-render-port--route port)) (defun etaf-render-port-spi-version (port) @@ -232,8 +221,8 @@ ETAF; the selected port is process-wide and immutable." (dolist (capability etaf-render-port--required-capabilities) (unless (memq capability capabilities) (push (list :missing-capability capability) failures)))) - (unless (eq (plist-get snapshot :tp-protocol) - etaf-render-port--required-tp-protocol) + (unless (memq (plist-get snapshot :tp-protocol) + etaf-render-port--accepted-tp-protocols) (push (list :tp-protocol (plist-get snapshot :tp-protocol)) failures)) (unless (equal (plist-get snapshot :stage-order) etaf-render-port--required-stage-order) @@ -266,292 +255,6 @@ ETAF; the selected port is process-wide and immutable." (push (list slot operation) failures)))) (nreverse failures))) -(defun etaf-render-port--validate-framework-pair - (framework-stage framework-rollback) - "Validate FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK as one required pair." - (unless (functionp framework-stage) - (signal 'wrong-type-argument (list 'functionp framework-stage))) - (unless (functionp framework-rollback) - (signal 'wrong-type-argument (list 'functionp framework-rollback))) - t) - -(defvar-local etaf-render-port--v1-cleanup-diagnostics nil - "Contained cleanup failures from the latest failed v1 initial operation.") - -(defvar-local etaf-render-port--v1-committed-revision nil - "ETAF-owned revision evidence for a mounted legacy Ebox v1 surface.") - -(defun etaf-render-port-v1-cleanup-diagnostics (buffer) - "Return a defensive copy of BUFFER's latest v1 cleanup diagnostics." - (and (buffer-live-p (get-buffer buffer)) - (with-current-buffer (get-buffer buffer) - (copy-tree etaf-render-port--v1-cleanup-diagnostics)))) - -(defun etaf-render-port--v1-buffer-snapshot (buffer) - "Return BUFFER content and editor state needed by v1 manual cleanup." - (with-current-buffer buffer - (let ((narrowed-p (buffer-narrowed-p)) - (start (point-min)) - (end (point-max))) - (save-restriction - (widen) - (list :contents (buffer-substring (point-min) (point-max)) - :point (point) - :mark-marker (mark-marker) - :mark-position (mark t) - :mark-insertion-type - (marker-insertion-type (mark-marker)) - :mark-active mark-active - :narrowed-p narrowed-p - :narrow-start start - :narrow-end end - :read-only buffer-read-only - :modified-p (buffer-modified-p) - :undo-list buffer-undo-list - :overlays - (mapcar - (lambda (overlay) - (list :overlay overlay - :start (overlay-start overlay) - :end (overlay-end overlay))) - (delete-dups - (append (car (overlay-lists)) (cdr (overlay-lists)))))))))) - -(defun etaf-render-port--v1-restore-buffer - (buffer snapshot &optional restore-contents-p) - "Restore BUFFER editor state from a pre-render v1 SNAPSHOT. -When RESTORE-CONTENTS-P is non-nil, also re-materialize text as a last-resort -fallback after change-group cancellation itself failed." - (unless (buffer-live-p buffer) - (error "Legacy Ebox target died during manual cleanup")) - (with-current-buffer buffer - (let ((inhibit-read-only t) - (inhibit-modification-hooks t)) - (widen) - (when restore-contents-p - (let ((buffer-undo-list t)) - (erase-buffer) - (insert (plist-get snapshot :contents)))) - (let* ((saved-overlays (plist-get snapshot :overlays)) - (saved-identities - (mapcar (lambda (entry) (plist-get entry :overlay)) - saved-overlays))) - (dolist - (overlay - (delete-dups - (append (car (overlay-lists)) (cdr (overlay-lists))))) - (unless (memq overlay saved-identities) - (delete-overlay overlay))) - (dolist (entry saved-overlays) - (let ((overlay (plist-get entry :overlay))) - (move-overlay overlay - (plist-get entry :start) - (plist-get entry :end) - buffer)))) - (when (plist-get snapshot :narrowed-p) - (narrow-to-region - (min (point-max) (plist-get snapshot :narrow-start)) - (min (point-max) (plist-get snapshot :narrow-end)))) - (goto-char (min (point-max) - (max (point-min) (plist-get snapshot :point)))) - (let ((mark-marker (plist-get snapshot :mark-marker))) - (set-marker mark-marker (plist-get snapshot :mark-position) buffer) - (set-marker-insertion-type - mark-marker (plist-get snapshot :mark-insertion-type))) - (setq mark-active (plist-get snapshot :mark-active)) - (setq buffer-read-only (plist-get snapshot :read-only)) - (set-buffer-modified-p (plist-get snapshot :modified-p)) - (setq buffer-undo-list (plist-get snapshot :undo-list)))) - buffer) - -(defun etaf-render-port--v1-cleanup-failed-initial - (buffer snapshot change-group change-group-active-p stage-entered - observer framework-rollback) - "Clean one failed legacy initial operation and return diagnostics. -BUFFER and SNAPSHOT identify editor custody. CHANGE-GROUP-ACTIVE-P says -whether CHANGE-GROUP still needs cancellation. STAGE-ENTERED controls the -paired FRAMEWORK-ROLLBACK. OBSERVER is detached before Ebox unmount." - (let (cancel-failed-p) - (when (buffer-live-p buffer) - (with-current-buffer buffer - (setq-local etaf-render-port--v1-cleanup-diagnostics nil))) - (cl-labels - ((record - (diagnostic) - (when (buffer-live-p buffer) - (with-current-buffer buffer - (setq-local - etaf-render-port--v1-cleanup-diagnostics - (append etaf-render-port--v1-cleanup-diagnostics - (list diagnostic)))))) - (run-phase - (entry) - (let ((phase (nth 0 entry)) - (function (nth 1 entry)) - (failure-function (nth 2 entry)) - completed-p) - (unwind-protect - (let ((inhibit-quit t) (quit-flag nil)) - (condition-case condition - (progn (funcall function) (setq completed-p t)) - ((error quit) - (setq completed-p t) - (when failure-function (funcall failure-function)) - (record - (list :phase phase - :condition (copy-tree condition)))))) - (unless completed-p - (when failure-function (funcall failure-function)) - (record (list :phase phase :nonlocal-exit t)))))) - (run-phases - (entries) - (when entries - ;; A cleanup callback may perform an arbitrary nonlocal exit. - ;; Nested unwind cleanup guarantees every later phase still runs. - (unwind-protect - (run-phase (car entries)) - (run-phases (cdr entries)))))) - (run-phases - `((framework-rollback - ,(lambda () - (when stage-entered - (funcall framework-rollback nil)))) - (observer-detach - ,(lambda () - (when (and observer (buffer-live-p buffer) - (ebox-surface-buffer-mounted-p buffer)) - (ebox-buffer-set-observer buffer nil)))) - (ebox-unmount - ,(lambda () - (when (and (buffer-live-p buffer) - (ebox-surface-buffer-mounted-p buffer)) - (ebox-unmount-buffer buffer)))) - (revision-reset - ,(lambda () - (when (buffer-live-p buffer) - (with-current-buffer buffer - (setq-local - etaf-render-port--v1-committed-revision nil))))) - (change-group-cancel - ,(lambda () - (when change-group-active-p - (with-current-buffer buffer - (cancel-change-group change-group)))) - ,(lambda () (setq cancel-failed-p t))) - (buffer-restore - ,(lambda () - (etaf-render-port--v1-restore-buffer - buffer snapshot cancel-failed-p)))))) - (and (buffer-live-p buffer) - (etaf-render-port-v1-cleanup-diagnostics buffer)))) - -(defun etaf-render-port--v1-record-revision (buffer revision) - "Record committed v1 REVISION for BUFFER without postaccept failure." - (when (buffer-live-p buffer) - (with-current-buffer buffer - (setq-local etaf-render-port--v1-committed-revision - (if (and (integerp revision) (> revision 0)) - revision - 'unavailable))))) - -(defun etaf-render-port--v1-revision (buffer) - "Return ETAF's committed revision evidence for legacy v1 BUFFER." - (let ((revision - (and (buffer-live-p buffer) - (buffer-local-value - 'etaf-render-port--v1-committed-revision buffer)))) - (unless (and (integerp revision) (> revision 0)) - (error "Mounted legacy Ebox surface has no committed revision: %S" - revision)) - revision)) - -(defun etaf-render-port--v1-initial - (buffer input framework-stage framework-rollback &optional observer) - "Publish INPUT initially to BUFFER through legacy Ebox. -FRAMEWORK-STAGE runs after publication; FRAMEWORK-ROLLBACK performs contained -manual framework cleanup if staging fails. OBSERVER, when non-nil, is passed -through Ebox's legacy initial option." - (etaf-render-port--validate-framework-pair - framework-stage framework-rollback) - (let* ((buffer (get-buffer-create buffer)) - (snapshot (etaf-render-port--v1-buffer-snapshot buffer)) - (change-group (with-current-buffer buffer (prepare-change-group))) - result stage-entered operation-started-p change-group-active-p - cleanup-ran-p) - (cl-labels - ((cleanup - () - (unless cleanup-ran-p - (setq cleanup-ran-p t) - (when operation-started-p - (let ((active-p change-group-active-p)) - (setq change-group-active-p nil) - (etaf-render-port--v1-cleanup-failed-initial - buffer snapshot change-group active-p stage-entered - observer framework-rollback)))))) - (unwind-protect - (condition-case primary - (progn - (when (ebox-surface-buffer-mounted-p buffer) - (error - "Legacy Ebox initial operation requires an unmounted buffer")) - (with-current-buffer buffer - (setq-local etaf-render-port--v1-cleanup-diagnostics nil - etaf-render-port--v1-committed-revision nil) - (activate-change-group change-group) - (setq operation-started-p t - change-group-active-p t) - (save-restriction - (widen) - (setq result - (ebox-render-to-buffer - buffer input - (and observer (list :observer observer)))))) - (setq stage-entered t) - (funcall framework-stage nil) - (with-current-buffer buffer - (accept-change-group change-group)) - (setq change-group-active-p nil) - ;; TP surfaces start at committed revision one. Legacy Ebox - ;; does not expose its surface handle, so ETAF owns this - ;; compatibility evidence and advances it from update reports. - (etaf-render-port--v1-record-revision buffer 1) - result) - ((error quit) - (cleanup) - (signal (car primary) (cdr primary)))) - (when change-group-active-p - (cleanup)))))) - -(defun etaf-render-port--v1-update - (buffer input framework-stage framework-rollback) - "Update BUFFER from INPUT through the legacy Ebox callback pair. -FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK retain their existing Ebox meanings." - (etaf-render-port--validate-framework-pair - framework-stage framework-rollback) - (let ((report - (ebox-commit buffer input framework-stage framework-rollback))) - ;; Publication is accepted here. Missing compatibility metadata must not - ;; become a rollback-capable error after commit; a later read fails closed. - (etaf-render-port--v1-record-revision - (get-buffer buffer) (plist-get report :surface-revision)) - report)) - -(defun etaf-render-port--v1-fallback (&optional bootstrap-outcome) - "Return the complete immutable v1 port tagged with BOOTSTRAP-OUTCOME." - (etaf-render-port--create - :route 'v1 - :spi-version 1 - :schema-version 'etaf-ebox-v1-fallback/v1 - :capabilities - '(initial-manual-cleanup update-paired-stage-rollback - same-object-update-report) - :tp-protocol 'tp-transaction-protocol-v1 - :initial-function 'etaf-render-port--v1-initial - :update-function 'etaf-render-port--v1-update - :revision-function 'etaf-render-port--v1-revision - :bootstrap-outcome (or bootstrap-outcome 'v2-absent-v1-selected))) - (defun etaf-render-port--v2-port (snapshot) "Return an immutable selected v2 port from compatible SNAPSHOT." (let ((failures (etaf-render-port--incompatibilities snapshot))) @@ -574,32 +277,25 @@ FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK retain their existing Ebox meanings." (defun etaf-render-port--bootstrap () "Probe Ebox exactly once and return one immutable selected render port." - (pcase etaf-render-port-selection-policy - ('v1 - (etaf-render-port--v1-fallback 'v1-kill-switch-selected)) - ('v2 - (let ((feature-present-p (featurep 'ebox-framework-spi-v2)) - (predicate-present-p (fboundp 'ebox-framework-spi-capabilities))) - (cond - ((and (not feature-present-p) (not predicate-present-p)) - (etaf-render-port--v1-fallback)) - ((not feature-present-p) - (etaf-render-port--bootstrap-error 'predicate-without-v2-feature)) - ((not predicate-present-p) - (etaf-render-port--bootstrap-error 'v2-feature-without-predicate)) - (t - (condition-case condition - (etaf-render-port--v2-port - (etaf-render-port--provider-snapshot - (ebox-framework-spi-capabilities))) - ((etaf-spi-incompatible-error etaf-spi-bootstrap-error) - (signal (car condition) (cdr condition))) - ((error quit) - (etaf-render-port--bootstrap-error - 'provider-predicate-failure condition))))))) - (_ - (etaf-render-port--bootstrap-error - 'invalid-render-port-selection etaf-render-port-selection-policy)))) + (let ((feature-present-p (featurep 'ebox-framework-spi-v2)) + (predicate-present-p (fboundp 'ebox-framework-spi-capabilities))) + (cond + ((and (not feature-present-p) (not predicate-present-p)) + (etaf-render-port--bootstrap-error 'v2-provider-missing)) + ((not feature-present-p) + (etaf-render-port--bootstrap-error 'predicate-without-v2-feature)) + ((not predicate-present-p) + (etaf-render-port--bootstrap-error 'v2-feature-without-predicate)) + (t + (condition-case condition + (etaf-render-port--v2-port + (etaf-render-port--provider-snapshot + (ebox-framework-spi-capabilities))) + ((etaf-spi-incompatible-error etaf-spi-bootstrap-error) + (signal (car condition) (cdr condition))) + ((error quit) + (etaf-render-port--bootstrap-error + 'provider-predicate-failure condition))))))) (defconst etaf-render-port--selected-port (etaf-render-port--bootstrap) @@ -615,19 +311,15 @@ FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK retain their existing Ebox meanings." FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair. OBSERVER, when non-nil, receives TP and Ebox snapshots measured during the initial v2 publication and replayed only after successful final accept." - (if (eq (etaf-render-port-route etaf-render-port--selected-port) 'v1) - (funcall (etaf-render-port-initial-function - etaf-render-port--selected-port) - buffer input framework-stage framework-rollback observer) - (let ((report - (funcall - (etaf-render-port-initial-function etaf-render-port--selected-port) - buffer input framework-stage framework-rollback))) - (when observer - (dolist (provider-report - (ebox-framework-spi-initial-observation-reports report)) - (funcall observer buffer provider-report))) - report))) + (let ((report + (funcall + (etaf-render-port-initial-function etaf-render-port--selected-port) + buffer input framework-stage framework-rollback))) + (when observer + (dolist (provider-report + (ebox-framework-spi-initial-observation-reports report)) + (funcall observer buffer provider-report))) + report)) (defun etaf-render-port-update (buffer input framework-stage framework-rollback) @@ -639,11 +331,7 @@ FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK are one required callback pair." (defun etaf-render-port-unmount (buffer) "Release the retained Ebox surface owned by mounted BUFFER." - (let ((buffer (get-buffer buffer))) - (prog1 (ebox-unmount-buffer buffer) - (when (buffer-live-p buffer) - (with-current-buffer buffer - (setq-local etaf-render-port--v1-committed-revision nil)))))) + (ebox-unmount-buffer (get-buffer buffer))) (defun etaf-render-port-mounted-p (buffer) "Return non-nil when BUFFER owns a live retained Ebox surface." diff --git a/etaf-runtime.el b/etaf-runtime.el index de92b7c..7bbab59 100644 --- a/etaf-runtime.el +++ b/etaf-runtime.el @@ -6176,17 +6176,12 @@ RENDERED-IDENTITIES names the Component render participants." (etaf--runtime-participant-publish participant)) (lambda (_report) (etaf--runtime-participant-rollback participant)))) - (let* ((host-authority (etaf-runtime-host-authority runtime)) - (legacy-p - (eq (etaf-render-port-route - (etaf-render-port-selected)) - 'v1))) + (let ((host-authority (etaf-runtime-host-authority runtime))) (etaf-render-port-initial (etaf-runtime-buffer runtime) next-root (lambda (_report) (etaf--runtime-participant-publish participant) - (etaf-host-authority-stage-attach - host-authority legacy-p)) + (etaf-host-authority-stage-attach host-authority)) (lambda (_report) (etaf-host-authority-rollback-attach host-authority) (etaf--runtime-participant-rollback participant)) diff --git a/etaf.el b/etaf.el index dc2a768..d5a783f 100644 --- a/etaf.el +++ b/etaf.el @@ -3,7 +3,7 @@ ;; SPDX-License-Identifier: GPL-3.0-or-later ;; Author: ETAF contributors -;; Version: 0.1.1 +;; Version: 0.2.0 ;; Package-Requires: ((emacs "29.1") (ebox "2.0.1") (tp "1.0.1")) ;; Keywords: ui, tools, convenience ;; URL: https://github.com/ginqi7/etaf diff --git a/tests/etaf-docs-tests.el b/tests/etaf-docs-tests.el index a5f7d13..ae57828 100644 --- a/tests/etaf-docs-tests.el +++ b/tests/etaf-docs-tests.el @@ -103,6 +103,8 @@ "docs/user-guide.zh.md" "docs/implementation-plan.en.md" "docs/implementation-plan.zh.md" + "docs/migration-0.2.en.md" + "docs/migration-0.2.zh.md" "postmortem/2026-08-05-executable-core-examples.en.md" "postmortem/2026-08-05-executable-core-examples.zh.md")) (should (file-exists-p (expand-file-name file etaf-docs-test--root))))) diff --git a/tests/etaf-g1-cross-layer-tests.el b/tests/etaf-g1-cross-layer-tests.el index 831352d..f798dc9 100644 --- a/tests/etaf-g1-cross-layer-tests.el +++ b/tests/etaf-g1-cross-layer-tests.el @@ -33,7 +33,7 @@ (combined-participant . etaf-g1-ebox-etaf-combined-participant-same-report) (spi-branches . etaf-g1-spi-four-branches-and-selected-port-immutability) (generation-host-cas . etaf-g1-generation-cas-and-host-lifecycle-guards) - (v1-v2-equivalence . etaf-g1-v1-v2-runtime-equivalence) + (v2-runtime-contract . etaf-g1-v2-runtime-contract) (runtime-fault-rollback . etaf-g1-runtime-fault-restores-authorities) (postcommit-diagnostics . etaf-g1-postcommit-report-fault-keeps-accepted-state) (host-unmount-kill . etaf-g1-host-unmount-and-kill-inflight-route) @@ -47,7 +47,7 @@ (defconst etaf-g1--required-fault-keys '(tp-order multi-surface combined-participant spi-branches - generation-host-cas v1-v2-equivalence runtime-fault-rollback + generation-host-cas v2-runtime-contract runtime-fault-rollback postcommit-diagnostics host-unmount-kill nested-runtime-event multi-context native-fallback retirement load-path-harness gui-recovery-harness) "Required unique behavior keys for the G1 cross-layer gate.") @@ -192,14 +192,15 @@ (tp--transaction-precommit-functions nil)) (should-error (tp-with-transaction - (tp-transaction-participate - 'tp-first (lambda () (push 'first-stage etaf-g1--tp-trace)) - (lambda () (push 'first-rollback etaf-g1--tp-trace))) - (tp-transaction-participate - 'tp-second - (lambda () (push 'second-stage etaf-g1--tp-trace) - (error "G1 injected partial apply")) - (lambda () (push 'second-rollback etaf-g1--tp-trace)))))) + (tp-transaction-participate-v2 + :key 'tp-first + :stage (lambda () (push 'first-stage etaf-g1--tp-trace)) + :rollback (lambda () (push 'first-rollback etaf-g1--tp-trace))) + (tp-transaction-participate-v2 + :key 'tp-second + :stage (lambda () (push 'second-stage etaf-g1--tp-trace) + (error "G1 injected partial apply")) + :rollback (lambda () (push 'second-rollback etaf-g1--tp-trace)))))) (should (equal (nreverse etaf-g1--tp-trace) '(first-stage second-stage second-rollback first-rollback)))) @@ -212,12 +213,14 @@ '(etaf-g1--tp-precommit-probe))) (should-error (tp-with-transaction - (tp-transaction-participate - 'tp-first (lambda () (push 'first-stage etaf-g1--tp-trace)) - (lambda () (push 'first-rollback etaf-g1--tp-trace))) - (tp-transaction-participate - 'tp-second (lambda () (push 'second-stage etaf-g1--tp-trace)) - (lambda () (push 'second-rollback etaf-g1--tp-trace))))) + (tp-transaction-participate-v2 + :key 'tp-first + :stage (lambda () (push 'first-stage etaf-g1--tp-trace)) + :rollback (lambda () (push 'first-rollback etaf-g1--tp-trace))) + (tp-transaction-participate-v2 + :key 'tp-second + :stage (lambda () (push 'second-stage etaf-g1--tp-trace)) + :rollback (lambda () (push 'second-rollback etaf-g1--tp-trace))))) (should (equal (nreverse etaf-g1--tp-trace) '(first-stage second-stage precommit @@ -632,7 +635,8 @@ (and (not (eq feature 'ebox-framework-spi-v2)) (funcall original-featurep feature)))) ((symbol-function 'ebox-framework-spi-capabilities) nil)) - (should (eq (etaf-render-port-route (etaf-render-port--bootstrap)) 'v1))) + (should-error (etaf-render-port--bootstrap) + :type 'etaf-spi-bootstrap-error)) (should (eq (etaf-render-port-route (etaf-render-port--bootstrap)) 'v2)) (cl-letf (((symbol-function 'ebox-framework-spi-capabilities) (lambda () (error "G1 malformed provider")))) @@ -661,19 +665,20 @@ authority generation token versions (etaf--generation-create :generation-id 2) 2 versions) :type 'etaf-generation-error)) - (let ((buffer (generate-new-buffer " *g1-host*"))) + (let ((buffer (generate-new-buffer " *g1-host*")) + (source (etaf-ref 0)) runtime) (unwind-protect - (let ((host (etaf-host-authority-create 'g1-host 1 buffer))) - (etaf-host-authority-begin-attach host) - ;; Legacy attach is the explicit non-transactional compatibility - ;; path; v2 uses the same state transition behind a TP marker. - (etaf-host-authority-stage-attach host t) - (let ((token (etaf-host-authority-token host))) + (progn + (etaf-mount buffer (etaf-g1--view source)) + (setq runtime (etaf-runtime-for-buffer buffer)) + (let* ((host (etaf-runtime-host-authority runtime)) + (token (etaf-host-authority-token host))) (should (etaf-host-authority-accepts-token-p host token)) - (etaf-host-authority-begin-detach host) - (etaf-host-authority-invalidate host) + (etaf-unmount runtime) + (setq runtime nil) (should-not (etaf-host-authority-accepts-token-p host token)))) - (kill-buffer buffer)))) + (when runtime (etaf-unmount runtime)) + (when (buffer-live-p buffer) (kill-buffer buffer))))) (defun etaf-g1--run-selected-render-port-route () "Run one lifecycle through the process-selected port and return evidence." @@ -750,7 +755,6 @@ (list :selected-route (etaf-render-port-route selected) :bootstrap-outcome (etaf-render-port-bootstrap-outcome selected) - :render-port-selection-policy etaf-render-port-selection-policy :generation-mirror-route etaf-generation-mirror-route :semantic-commit-route etaf-semantic-commit-route :v2-feature-present (featurep 'ebox-framework-spi-v2) @@ -771,8 +775,8 @@ (executable-find invocation-name) (error "Cannot resolve current Emacs executable: %S" invocation-name))) -(defun etaf-g1--probe-route-in-fresh-emacs (route) - "Bootstrap ROUTE in a fresh Emacs process and return its route evidence." +(defun etaf-g1--probe-v2-in-fresh-emacs () + "Bootstrap the required v2 port in a fresh Emacs and return its evidence." (let* ((etaf-root (expand-file-name "etaf" etaf-g1--workspace-root)) (test-file (expand-file-name "tests/etaf-g1-cross-layer-tests.el" etaf-root)) @@ -781,15 +785,6 @@ (expand-file-name directory etaf-g1--workspace-root)) '("etaf" "etaf/examples" "etaf/scripts" "ebox" "tp" "ecss"))) - (route-setup - (pcase route - ('v1 - (list - "--eval" - "(setq etaf-render-port-selection-policy 'v1)")) - ('v2 - (list "--eval" "(setq etaf-render-port-selection-policy 'v2)")) - (_ (error "Unknown G1 render route: %S" route)))) (arguments (append '("-Q" "--batch") @@ -797,7 +792,6 @@ (mapcar (lambda (directory) (list "-L" directory)) load-directories)) '("--eval" "(setq load-prefer-newer t)") - route-setup (list "-l" test-file "--eval" @@ -813,60 +807,46 @@ (list (current-buffer) t) nil arguments) output (buffer-string)) (unless (and (integerp status) (zerop status)) - (ert-fail (format "Fresh %S route probe failed (%S):\n%s" - route status output))) + (ert-fail (format "Fresh v2 route probe failed (%S):\n%s" + status output))) (goto-char (point-min)) (unless (re-search-forward "^ETAF_G1_ROUTE_EVIDENCE:\\([^[:space:]]+\\)$" nil t) - (ert-fail (format "Fresh %S route probe emitted no evidence:\n%s" - route output))) + (ert-fail (format "Fresh v2 route probe emitted no evidence:\n%s" + output))) (setq encoded (match-string-no-properties 1))) (read (base64-decode-string encoded)))) -(ert-deftest etaf-g1-v1-v2-runtime-equivalence () - "Fresh v1 and v2 bootstraps preserve lifecycle and rollback equivalence." - (let* ((v1 (etaf-g1--probe-route-in-fresh-emacs 'v1)) - (v2 (etaf-g1--probe-route-in-fresh-emacs 'v2)) - (v1-lifecycle (plist-get v1 :lifecycle-evidence)) - (v2-lifecycle (plist-get v2 :lifecycle-evidence))) - (should (eq (plist-get v1 :selected-route) 'v1)) +(ert-deftest etaf-g1-v2-runtime-contract () + "A fresh v2 bootstrap preserves lifecycle, identity and failed-update state." + (let* ((v2 (etaf-g1--probe-v2-in-fresh-emacs)) + (lifecycle (plist-get v2 :lifecycle-evidence)) + (initial (plist-get lifecycle :initial)) + (updated (plist-get lifecycle :updated)) + (rolled-back (plist-get lifecycle :rolled-back)) + (host-lifecycle (plist-get lifecycle :lifecycle))) (should (eq (plist-get v2 :selected-route) 'v2)) - (should (eq (plist-get v1 :bootstrap-outcome) - 'v1-kill-switch-selected)) (should (eq (plist-get v2 :bootstrap-outcome) 'valid-v2-selected)) - (should (plist-get v1 :v2-feature-present)) - (should (plist-get v1 :v2-predicate-present)) (should (plist-get v2 :v2-feature-present)) (should (plist-get v2 :v2-predicate-present)) - (should (eq (plist-get v1 :render-port-selection-policy) 'v1)) - (should (eq (plist-get v1 :generation-mirror-route) 'project)) - (should (eq (plist-get v1 :semantic-commit-route) 'cas)) - (should (eq (plist-get v2 :render-port-selection-policy) 'v2)) (should (eq (plist-get v2 :generation-mirror-route) 'project)) (should (eq (plist-get v2 :semantic-commit-route) 'cas)) - (should (plist-get v1 :selected-stable)) (should (plist-get v2 :selected-stable)) - (should (eq (plist-get v1 :initial-dispatch) - 'etaf-render-port--v1-initial)) - (should (eq (plist-get v1 :update-dispatch) - 'etaf-render-port--v1-update)) (should (eq (plist-get v2 :initial-dispatch) 'ebox-framework-spi-initial)) (should (eq (plist-get v2 :update-dispatch) 'ebox-framework-spi-update)) - (dolist (evidence (list v1 v2)) - (should (= 1 (plist-get evidence :initial-dispatch-count))) - (should (= 2 (plist-get evidence :update-dispatch-count)))) - (dolist (key '(:initial :updated :rolled-back :update-report :lifecycle)) - (should (equal-including-properties - (plist-get v1-lifecycle key) (plist-get v2-lifecycle key)))) - ;; The v2 Host attaches with one generic final marker; legacy v1 performs - ;; its compatible attach inside the reversible manual stage. - (should (= 0 (plist-get v1-lifecycle :initial-marker-count))) - (should (= 1 (plist-get v2-lifecycle :initial-marker-count))) - (should (= 0 (plist-get v1-lifecycle :update-marker-count))) - (should (= 0 (plist-get v2-lifecycle :update-marker-count))))) + (should (= 1 (plist-get v2 :initial-dispatch-count))) + (should (= 2 (plist-get v2 :update-dispatch-count))) + (should (equal (plist-get initial :semantic-text) "value=0")) + (should (equal (plist-get updated :semantic-text) "value=1")) + (should (equal-including-properties updated rolled-back)) + (should (= 4 (length host-lifecycle))) + (should (eq (plist-get (car host-lifecycle) :host-state) 'attached)) + (should (eq (plist-get (car (last host-lifecycle)) :host-state) 'terminal)) + (should (= 1 (plist-get lifecycle :initial-marker-count))) + (should (= 0 (plist-get lifecycle :update-marker-count))))) (provide 'etaf-g1-cross-layer-tests) ;;; etaf-g1-cross-layer-tests.el ends here diff --git a/tests/etaf-host-tests.el b/tests/etaf-host-tests.el index 3cae5ef..47164a9 100644 --- a/tests/etaf-host-tests.el +++ b/tests/etaf-host-tests.el @@ -30,10 +30,10 @@ (unwind-protect (progn (cl-letf (((symbol-function 'etaf-host-authority-stage-attach) - (lambda (authority &optional legacy-p) + (lambda (authority) (setq lookup-during-stage (etaf-runtime-for-buffer buffer-name)) - (funcall original-stage authority legacy-p)))) + (funcall original-stage authority)))) (etaf-mount buffer-name (etaf--view-call 'etaf-host-test-component diff --git a/tests/etaf-render-port-tests.el b/tests/etaf-render-port-tests.el index d82b0a1..b45bf2c 100644 --- a/tests/etaf-render-port-tests.el +++ b/tests/etaf-render-port-tests.el @@ -12,11 +12,17 @@ (file-name-directory (or load-file-name buffer-file-name)))) "ETAF package root used by static bootstrap-owner checks.") -(ert-deftest etaf-render-port-selection-policy-defaults-to-v2 () - "The package's declared bootstrap authority profile defaults to v2." - (should - (eq (eval (car (get 'etaf-render-port-selection-policy 'standard-value)) t) - 'v2))) +(ert-deftest etaf-render-port-package-metadata-requires-final-v2-stack () + "ETAF 0.2.1 declares the Ebox 3 and TP 2 runtime requirements." + (require 'package) + (with-temp-buffer + (insert-file-contents + (expand-file-name "etaf.el" etaf-render-port-test--root)) + (let ((description (package-buffer-info))) + (should (equal (package-desc-version description) '(0 2 1))) + (should + (equal (package-desc-reqs description) + '((emacs (29 1)) (ebox (3 0 0)) (tp (2 0 0)))))))) (ert-deftest etaf-render-port-selects-valid-v2-immutably () "A valid provider produces one immutable v2 selected port." @@ -27,8 +33,8 @@ (should (= (etaf-render-port-spi-version port) 2)) (should (eq (etaf-render-port-schema-version port) 'ebox-framework-spi-schema/v2)) - (should (eq (etaf-render-port-tp-protocol port) - 'tp-transaction-protocol-v1+v2)) + (should (memq (etaf-render-port-tp-protocol port) + etaf-render-port--accepted-tp-protocols)) (should (eq (etaf-render-port-initial-function port) 'ebox-framework-spi-initial)) (should (eq (etaf-render-port-update-function port) @@ -43,47 +49,30 @@ (should-error (eval `(setf (etaf-render-port--route ',port) 'broken))))) -(ert-deftest etaf-render-port-falls-back-only-when-v2-is-fully-absent () - "Only complete feature/predicate absence selects the immutable v1 port." +(ert-deftest etaf-render-port-requires-v2-provider () + "Complete provider absence fails closed instead of selecting a legacy port." (let ((original-featurep (symbol-function 'featurep))) (cl-letf (((symbol-function 'featurep) (lambda (feature) (and (not (eq feature 'ebox-framework-spi-v2)) (funcall original-featurep feature)))) ((symbol-function 'ebox-framework-spi-capabilities) nil)) - (let ((port (etaf-render-port--bootstrap))) - (should (eq (etaf-render-port-route port) 'v1)) - (should (= (etaf-render-port-spi-version port) 1)) - (should (eq (etaf-render-port-initial-function port) - 'etaf-render-port--v1-initial)) - (should (eq (etaf-render-port-update-function port) - 'etaf-render-port--v1-update)) - (should (eq (etaf-render-port-revision-function port) - 'etaf-render-port--v1-revision)) - (should (eq (etaf-render-port-bootstrap-outcome port) - 'v2-absent-v1-selected)))))) + (should-error (etaf-render-port--bootstrap) + :type 'etaf-spi-bootstrap-error)))) -(ert-deftest etaf-render-port-v1-kill-switch-bypasses-present-provider () - "The explicit v1 profile selects old authority without probing Ebox v2." - (let ((etaf-render-port-selection-policy 'v1) - (provider-calls 0)) - (cl-letf (((symbol-function 'ebox-framework-spi-capabilities) - (lambda () - (cl-incf provider-calls) - (error "kill switch must bypass provider")))) - (let ((port (etaf-render-port--bootstrap))) - (should (featurep 'ebox-framework-spi-v2)) - (should (fboundp 'ebox-framework-spi-capabilities)) - (should (= provider-calls 0)) - (should (eq (etaf-render-port-route port) 'v1)) - (should (eq (etaf-render-port-bootstrap-outcome port) - 'v1-kill-switch-selected)))))) - -(ert-deftest etaf-render-port-rejects-invalid-selection-policy () - "A malformed render-port selection policy fails before provider probing." - (let ((etaf-render-port-selection-policy 'invalid)) - (should-error (etaf-render-port--bootstrap) - :type 'etaf-spi-bootstrap-error))) +(ert-deftest etaf-render-port-accepts-each-v2-capable-tp-manifest () + "Both transitional dual-capability and final v2-only providers are valid." + (let ((provider (ebox-framework-spi-capabilities))) + (dolist (protocol '(tp-transaction-protocol-v1+v2 + tp-transaction-protocol-v2)) + (cl-letf + (((symbol-function 'ebox-framework-spi-capabilities) + (lambda () provider)) + ((symbol-function 'ebox-framework-spi-provider-tp-protocol) + (lambda (_provider) protocol))) + (should (eq (etaf-render-port-tp-protocol + (etaf-render-port--bootstrap)) + protocol)))))) (ert-deftest etaf-render-port-rejects-half-present-v2 () "Feature-only and predicate-only providers fail instead of downgrading." @@ -147,27 +136,31 @@ (should-error (etaf-render-port--bootstrap) :type 'etaf-spi-incompatible-error)))) -(ert-deftest etaf-render-port-v1-initial-runs-manual-framework-cleanup () - "A failed legacy initial stage invokes its paired cleanup exactly once." - (let ((buffer (generate-new-buffer " *etaf-v1-manual-cleanup*")) trace) +(ert-deftest etaf-render-port-v2-initial-delegates-paired-rollback () + "The selected SPI operation owns stage failure and paired rollback." + (let ((buffer (generate-new-buffer " *etaf-v2-paired-rollback*")) trace) (unwind-protect - (cl-letf (((symbol-function 'ebox-render-to-buffer) - (lambda (&rest _arguments) + (cl-letf (((symbol-function 'ebox-framework-spi-initial) + (lambda (_buffer _input stage rollback) (push 'render trace) - buffer))) + (condition-case condition + (funcall stage nil) + (error + (funcall rollback nil) + (signal (car condition) (cdr condition))))))) (should-error - (etaf-render-port--v1-initial + (etaf-render-port-initial buffer 'input (lambda (_report) (push 'stage trace) - (error "injected v1 initial failure")) + (error "injected v2 initial failure")) (lambda (_report) (push 'rollback trace)))) (should (equal (nreverse trace) '(render stage rollback)))) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-initial-restores-real-ebox-surface () - "A failed legacy stage removes Ebox authority and restores buffer content." - (let ((buffer (generate-new-buffer " *etaf-v1-real-cleanup*")) +(ert-deftest etaf-render-port-v2-initial-restores-real-ebox-surface () + "A failed SPI stage removes Ebox authority and restores buffer content." + (let ((buffer (generate-new-buffer " *etaf-v2-real-cleanup*")) (input (ebox-build '(box "committed"))) (rollback-count 0) captured) @@ -175,28 +168,26 @@ (progn (with-current-buffer buffer (insert "sentinel")) (condition-case condition - (etaf-render-port--v1-initial + (etaf-render-port-initial buffer input (lambda (_report) - (error "injected real v1 stage failure")) + (error "injected real v2 stage failure")) (lambda (_report) (cl-incf rollback-count))) (error (setq captured condition))) - (should (equal captured '(error "injected real v1 stage failure"))) + (should (equal captured '(error "injected real v2 stage failure"))) (should (= rollback-count 1)) (should-not (ebox-surface-buffer-mounted-p buffer)) (should-not (ebox-surface-buffer-observer buffer)) (should (equal "sentinel" - (with-current-buffer buffer (buffer-string)))) - (should-not - (etaf-render-port-v1-cleanup-diagnostics buffer))) + (with-current-buffer buffer (buffer-string))))) (when (and (buffer-live-p buffer) (ebox-surface-buffer-mounted-p buffer)) (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-failure-restores-exact-editor-custody () - "Legacy stage rollback preserves Emacs-owned editor identities and undo." - (let ((buffer (generate-new-buffer " *etaf-v1-editor-custody*")) +(ert-deftest etaf-render-port-v2-failure-restores-exact-editor-custody () + "SPI stage rollback preserves Emacs-owned editor identities and undo." + (let ((buffer (generate-new-buffer " *etaf-v2-editor-custody*")) (input (ebox-build '(box "committed"))) overlay left-marker right-marker snapshot captured) (unwind-protect @@ -238,7 +229,7 @@ :undo-list (copy-tree buffer-undo-list) :modified-p (buffer-modified-p)))) (condition-case condition - (etaf-render-port--v1-initial + (etaf-render-port-initial buffer input (lambda (_report) (error "editor custody primary")) #'ignore) @@ -292,9 +283,9 @@ (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-nonlocal-exit-runs-exact-cleanup () - "A legacy stage throw cannot escape with mounted or editor state retained." - (let ((buffer (generate-new-buffer " *etaf-v1-nonlocal-cleanup*")) +(ert-deftest etaf-render-port-v2-nonlocal-exit-runs-exact-cleanup () + "A SPI stage throw cannot escape with mounted or editor state retained." + (let ((buffer (generate-new-buffer " *etaf-v2-nonlocal-cleanup*")) (input (ebox-build '(box "committed"))) (rollback-count 0)) (unwind-protect @@ -302,11 +293,11 @@ (with-current-buffer buffer (insert "before")) (should (eq - (catch 'etaf-v1-test-escape - (etaf-render-port--v1-initial + (catch 'etaf-v2-test-escape + (etaf-render-port-initial buffer input (lambda (_report) - (throw 'etaf-v1-test-escape 'escaped)) + (throw 'etaf-v2-test-escape 'escaped)) (lambda (_report) (cl-incf rollback-count))) 'not-escaped) 'escaped)) @@ -314,15 +305,13 @@ (should-not (ebox-surface-buffer-mounted-p buffer)) (should-not (ebox-surface-buffer-observer buffer)) (should (equal "before" - (with-current-buffer buffer (buffer-string)))) - (should-not - (etaf-render-port-v1-cleanup-diagnostics buffer))) + (with-current-buffer buffer (buffer-string))))) (when (and (buffer-live-p buffer) (ebox-surface-buffer-mounted-p buffer)) (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-precondition-keeps-existing-mount () +(ert-deftest etaf-render-port-v2-precondition-keeps-existing-mount () "Rejecting an already mounted target must not clean up foreign authority." (let ((buffer (generate-new-buffer " *etaf-v1-existing-mount*"))) (unwind-protect @@ -330,7 +319,7 @@ (ebox-render-to-buffer buffer (ebox-build '(box "existing"))) (let ((revision (ebox-surface-buffer-revision buffer))) (should-error - (etaf-render-port--v1-initial + (etaf-render-port-initial buffer (ebox-build '(box "replacement")) #'ignore #'ignore) :type 'error) (should (ebox-surface-buffer-mounted-p buffer)) @@ -342,48 +331,42 @@ (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-rollback-throw-cannot-stop-cleanup-tail () - "A framework rollback throw still unmounts and restores legacy editor state." - (let ((buffer (generate-new-buffer " *etaf-v1-rollback-throw*")) +(ert-deftest etaf-render-port-v2-rollback-throw-cannot-stop-cleanup-tail () + "A framework rollback throw propagates only after SPI cleanup completes." + (let ((buffer (generate-new-buffer " *etaf-v2-rollback-throw*")) (input (ebox-build '(box "committed")))) (unwind-protect (progn (with-current-buffer buffer (insert "before")) (should (eq - (catch 'etaf-v1-rollback-escape - (etaf-render-port--v1-initial + (catch 'etaf-v2-rollback-escape + (etaf-render-port-initial buffer input (lambda (_report) (error "primary before rollback throw")) (lambda (_report) - (throw 'etaf-v1-rollback-escape 'rollback-escaped))) + (throw 'etaf-v2-rollback-escape 'rollback-escaped))) 'not-escaped) 'rollback-escaped)) (should-not (ebox-surface-buffer-mounted-p buffer)) (should-not (ebox-surface-buffer-observer buffer)) (should (equal "before" - (with-current-buffer buffer (buffer-string)))) - (let ((diagnostic - (cl-find 'framework-rollback - (etaf-render-port-v1-cleanup-diagnostics buffer) - :key (lambda (entry) (plist-get entry :phase))))) - (should diagnostic) - (should (plist-get diagnostic :nonlocal-exit)))) + (with-current-buffer buffer (buffer-string))))) (when (and (buffer-live-p buffer) (ebox-surface-buffer-mounted-p buffer)) (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-cleanup-fault-keeps-primary-and-continues () - "A secondary v1 cleanup fault is diagnosed while Ebox cleanup continues." - (let ((buffer (generate-new-buffer " *etaf-v1-cleanup-fault*")) +(ert-deftest etaf-render-port-v2-cleanup-fault-keeps-primary-and-continues () + "A secondary rollback fault is contained while Ebox cleanup continues." + (let ((buffer (generate-new-buffer " *etaf-v2-cleanup-fault*")) (input (ebox-build '(box "committed"))) captured) (unwind-protect (progn (with-current-buffer buffer (insert "before")) (condition-case condition - (etaf-render-port--v1-initial + (etaf-render-port-initial buffer input (lambda (_report) (error "primary stage failure")) (lambda (_report) (error "secondary rollback failure"))) @@ -391,14 +374,7 @@ (should (equal captured '(error "primary stage failure"))) (should-not (ebox-surface-buffer-mounted-p buffer)) (should (equal "before" - (with-current-buffer buffer (buffer-string)))) - (let ((diagnostics - (etaf-render-port-v1-cleanup-diagnostics buffer))) - (should (= 1 (length diagnostics))) - (should (eq (plist-get (car diagnostics) :phase) - 'framework-rollback)) - (should (equal (plist-get (car diagnostics) :condition) - '(error "secondary rollback failure"))))) + (with-current-buffer buffer (buffer-string))))) (when (and (buffer-live-p buffer) (ebox-surface-buffer-mounted-p buffer)) (ebox-unmount-buffer buffer)) @@ -423,46 +399,25 @@ (ebox-unmount-buffer buffer)) (when (buffer-live-p buffer) (kill-buffer buffer))))) -(ert-deftest etaf-render-port-v1-revision-needs-no-v2-ebox-accessor () - "A v1-only Ebox retains revisions without a v2-only query symbol." - (let ((buffer (generate-new-buffer " *etaf-v1-revision*")) - (etaf-render-port--selected-port (etaf-render-port--v1-fallback))) +(ert-deftest etaf-render-port-v2-preserves-provider-report-identity () + "Initial and update return the exact report seen by framework staging." + (let ((buffer (generate-new-buffer " *etaf-v2-report-identity*")) + initial-stage-report update-stage-report) (unwind-protect - (cl-letf (((symbol-function 'ebox-surface-buffer-revision) nil)) - (etaf-render-port-initial - buffer (ebox-build '(box "one")) #'ignore #'ignore) - (should (= (etaf-render-port-revision buffer) 1)) - (let ((report + (let ((initial-report + (etaf-render-port-initial + buffer (ebox-build '(box "one")) + (lambda (report) (setq initial-stage-report report)) + #'ignore))) + (should (eq initial-report initial-stage-report)) + (let ((update-report (etaf-render-port-update - buffer (ebox-build '(box "two")) #'ignore #'ignore))) + buffer (ebox-build '(box "two")) + (lambda (report) (setq update-stage-report report)) + #'ignore))) + (should (eq update-report update-stage-report)) (should (= (etaf-render-port-revision buffer) - (plist-get report :surface-revision)))) - (with-current-buffer buffer - (setq-local etaf-render-port--v1-committed-revision nil)) - (should-error (etaf-render-port-revision buffer) :type 'error)) - (when (and (buffer-live-p buffer) - (ebox-surface-buffer-mounted-p buffer)) - (ebox-unmount-buffer buffer)) - (when (buffer-live-p buffer) (kill-buffer buffer))))) - -(ert-deftest etaf-render-port-v1-missing-update-revision-fails-later () - "A committed v1 update may finish, but missing revision evidence fails closed." - (let ((buffer (generate-new-buffer " *etaf-v1-missing-revision*")) - (etaf-render-port--selected-port (etaf-render-port--v1-fallback)) - (report (list :strategy 'injected-without-revision))) - (unwind-protect - (progn - (etaf-render-port-initial - buffer (ebox-build '(box "one")) #'ignore #'ignore) - (cl-letf (((symbol-function 'ebox-commit) - (lambda (_buffer _input framework-stage _rollback) - (funcall framework-stage report) - report))) - (should - (eq (etaf-render-port-update - buffer (ebox-build '(box "two")) #'ignore #'ignore) - report))) - (should-error (etaf-render-port-revision buffer) :type 'error)) + (plist-get update-report :surface-revision))))) (when (and (buffer-live-p buffer) (ebox-surface-buffer-mounted-p buffer)) (ebox-unmount-buffer buffer)) @@ -499,6 +454,15 @@ "(ebox-surface-")) (should-not (string-match-p (regexp-quote call) source)))))))) +(ert-deftest etaf-render-port-production-has-no-v1-render-route () + "Production ETAF contains no retired render-port policy or v1 helper." + (dolist (file (directory-files etaf-render-port-test--root t "\\.el\\'")) + (with-temp-buffer + (insert-file-contents file) + (let ((source (buffer-string))) + (should-not (string-match-p "etaf-render-port-selection-policy" source)) + (should-not (string-match-p "etaf-render-port--v1" source)))))) + (ert-deftest etaf-render-port-routes-standalone-renderer-mount () "The no-Runtime compatibility mount uses the immutable selected port." (require 'etaf-renderer) @@ -539,12 +503,9 @@ (ert-deftest etaf-render-port-selected-port-is-process-stable () "Every downstream read returns the one bootstrap-selected port identity." - (let* ((selected (etaf-render-port-selected)) - (route (etaf-render-port-route selected))) - (let ((etaf-render-port-selection-policy (if (eq route 'v1) 'v2 'v1))) - (should (eq selected (etaf-render-port-selected))) - (should (eq route - (etaf-render-port-route (etaf-render-port-selected))))))) + (let ((selected (etaf-render-port-selected))) + (should (eq selected (etaf-render-port-selected))) + (should (eq (etaf-render-port-route selected) 'v2)))) (provide 'etaf-render-port-tests) diff --git a/tests/fixtures/etaf-m0a-condition-consumers.sexp b/tests/fixtures/etaf-m0a-condition-consumers.sexp index f6925f9..8fdf920 100644 --- a/tests/fixtures/etaf-m0a-condition-consumers.sexp +++ b/tests/fixtures/etaf-m0a-condition-consumers.sexp @@ -11,8 +11,6 @@ (:file "etaf-reactive.el" :form condition-case :conditions (error quit) :owner etaf-reactive :policy generic-containment) (:file "etaf-reactive.el" :form condition-case :conditions (error quit) :owner etaf-reactive :policy generic-containment) (:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment) - (:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment) - (:file "etaf-render-port.el" :form condition-case :conditions (error quit) :owner etaf-render-port :policy generic-containment) (:file "etaf-render-port.el" :form condition-case :conditions (etaf-spi-incompatible-error etaf-spi-bootstrap-error) :owner etaf-render-port :policy specific-compatibility) (:file "etaf-resource.el" :form condition-case :conditions (error) :owner etaf-resource :policy generic-containment) (:file "etaf-resource.el" :form condition-case :conditions (error) :owner etaf-resource :policy generic-containment)