feat: publish Ebox SPI v2 provider for M2a E5
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-08-31 21:52:51 +08:00
parent 918a88d95d
commit b5e8b6d863
11 changed files with 698 additions and 18 deletions

View File

@ -11,6 +11,7 @@ Ebox is the low-level spatial rendering engine. Its job is to turn a declarative
- The native module is an optional accelerator with an exact Elisp fallback.
- New abstractions must remove duplication or make an existing public workflow simpler.
- Public property schema, ECSS metadata, and used engine projection are derived from one property definition record.
- Cross-package framework integration is an additive versioned provider; consumer selection and fallback policy remain outside Ebox.
## Pipeline

View File

@ -11,6 +11,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过
- native 模块只是可选加速器,必须有完全等价的 Elisp fallback。
- 新抽象必须消除重复,或让已有的公共工作流更简单。
- 公共 property schema、ECSS metadata 与 used engine projection 都从同一 property definition record 派生。
- 跨包 framework integration 只发布 additive versioned providerconsumer selection 与 fallback policy 不属于 Ebox。
## 流水线

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 docs-contract-tests ci-contract-tests performance-evaluator visual-check native-rust-tests native-build diff-check clean package-lint package-lint-install
.PHONY: all check ci load compile test checkdoc source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests state-contract-tests layout-boundary-tests layout-boundary-performance patch-plan-tests patch-plan-performance 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 docs-contract-tests ci-contract-tests
test: source-tests font-tests core-tests child-range-tests grid-tests ebox-commit-tests surface-tests visual-check-tests package-tests selector-tests dsl-tests flex-tests state-contract-tests layout-boundary-tests patch-plan-tests 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
@ -84,6 +84,12 @@ style-schema-tests:
style-schema-performance:
$(EMACS_TEST) -l tests/ebox-style-schema-tests.el --eval "(ert-run-tests-batch-and-exit 'ebox-style-schema-performance-uses-one-lookup-per-property)"
spi-tests:
$(EMACS_TEST) -l tests/ebox-spi-tests.el -f ert-run-tests-batch-and-exit
spi-performance:
$(EMACS_TEST) -l tests/ebox-spi-tests.el --eval "(ert-run-tests-batch-and-exit 'ebox-spi-performance-builds-bounded-provider-snapshots)"
performance-evaluator: surface-tests ebox-commit-tests
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch

View File

@ -35,11 +35,12 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
| `ebox-incremental.el` | Runtime state, snapshots, dirty planning, owner escalation, pure commit preparation, and reports. |
| `ebox-dsl.el` | Data-oriented `.ebox` forms and lowering to public nodes. |
| `ebox-selector.el` | CSS-like string parsing to ECSS structured selector ASTs, indexed candidate lookup, and tree/runtime query handles. |
| `ebox-spi.el` | Additive immutable framework SPI v2 provider and initial/update operation descriptors. |
| `ebox-native-reflow.el` | Optional native module loading/build commands, ABI checks, bounded sessions, and Elisp fallback. |
The package intentionally does not include application Components, UI controls, reactive data, or a playground implementation. Those are sibling-package responsibilities.
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-tests.el`, `tests/ebox-layout-boundary-tests.el`, `tests/ebox-patch-plan-tests.el`, `tests/ebox-style-schema-tests.el`, `tests/ebox-child-range-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, `scripts/ebox-visual-check.el`, and `scripts/ebox-performance-evaluator.el`.
The active contract also covers `Makefile`, `.github/workflows/ci.yml`, `tests/ebox-core-render-tests.el`, `tests/ebox-state-contract-tests.el`, `tests/ebox-layout-boundary-tests.el`, `tests/ebox-patch-plan-tests.el`, `tests/ebox-style-schema-tests.el`, `tests/ebox-spi-tests.el`, `tests/ebox-child-range-tests.el`, `tests/ebox-grid-tests.el`, `tests/ebox-commit-tests.el`, `tests/ebox-surface-tests.el`, `tests/ebox-dsl-tests.el`, `tests/ebox-flex-tests.el`, `tests/ebox-selector-tests.el`, `tests/ebox-package-tests.el`, `tests/ebox-visual-check-tests.el`, `tests/ebox-docs-contract-tests.el`, `tests/ebox-ci-contract-tests.el`, `native/Cargo.toml`, `native/Cargo.lock`, `native/build.rs`, `native/vendor/emacs-30/emacs-module.h`, `native/src/lib.rs`, `native/src/layout.rs`, `native/c/ebox_module.c`, `scripts/ebox-package-lint.el`, `scripts/ebox-visual-check.el`, and `scripts/ebox-performance-evaluator.el`.
## Runtime model
@ -89,6 +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.
- `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.
@ -127,6 +129,8 @@ make patch-plan-tests
make patch-plan-performance
make style-schema-tests
make style-schema-performance
make spi-tests
make spi-performance
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

View File

@ -35,11 +35,12 @@
| `ebox-incremental.el` | runtime、snapshot、dirty 规划、owner 提升、纯 commit 准备和报告。 |
| `ebox-dsl.el` | 数据型 `.ebox` form以及向公共节点的 lowering。 |
| `ebox-selector.el` | 把 CSS-like 字符串解析为 ECSS structured selector AST利用索引缩小候选并返回 tree/runtime query handle。 |
| `ebox-spi.el` | Additive immutable framework SPI v2 provider 及 initial/update operation descriptor。 |
| `ebox-native-reflow.el` | 可选 native 模块加载/构建、ABI 校验、受限 session 和 Elisp fallback。 |
本包有意不包含应用 Component、UI control、响应式 data 或 playground 实现;它们属于同级包。历史应用性能记录器和 native reflow 评估器也不属于独立 Ebox 的发布边界Ebox 只保留 native 模块本身、Rust 构建输入和可重复的构建检查。
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-tests.el`、`tests/ebox-layout-boundary-tests.el`、`tests/ebox-patch-plan-tests.el`、`tests/ebox-style-schema-tests.el`、`tests/ebox-child-range-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el`、`scripts/ebox-visual-check.el` 和 `scripts/ebox-performance-evaluator.el`
active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-core-render-tests.el`、`tests/ebox-state-contract-tests.el`、`tests/ebox-layout-boundary-tests.el`、`tests/ebox-patch-plan-tests.el`、`tests/ebox-style-schema-tests.el`、`tests/ebox-spi-tests.el`、`tests/ebox-child-range-tests.el`、`tests/ebox-grid-tests.el`、`tests/ebox-commit-tests.el`、`tests/ebox-surface-tests.el`、`tests/ebox-dsl-tests.el`、`tests/ebox-flex-tests.el`、`tests/ebox-selector-tests.el`、`tests/ebox-package-tests.el`、`tests/ebox-visual-check-tests.el`、`tests/ebox-docs-contract-tests.el`、`tests/ebox-ci-contract-tests.el`、`native/Cargo.toml`、`native/Cargo.lock`、`native/build.rs`、`native/vendor/emacs-30/emacs-module.h`、`native/src/lib.rs`、`native/src/layout.rs`、`native/c/ebox_module.c`、`scripts/ebox-package-lint.el`、`scripts/ebox-visual-check.el` 和 `scripts/ebox-performance-evaluator.el`
## 运行时模型
@ -89,6 +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 保留。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。
@ -125,6 +127,8 @@ make patch-plan-tests
make patch-plan-performance
make style-schema-tests
make style-schema-performance
make spi-tests
make spi-performance
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

259
ebox-spi.el Normal file
View File

@ -0,0 +1,259 @@
;;; ebox-spi.el --- Versioned Ebox framework provider -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; Additive provider-side Ebox framework SPI v2. This module publishes an
;; immutable capability snapshot and delegates initial/update execution to the
;; existing combined Ebox participant. It does not select a consumer port,
;; probe a framework package, or remove the v1 callback surface.
;;; Code:
(require 'cl-lib)
(require 'ebox-canonical)
(require 'ebox-surface)
(require 'tp-transaction)
(declare-function ebox--render-to-buffer-internal
"ebox" (buffer-or-name input options &optional participant))
(declare-function ebox-commit
"ebox"
(buffer-or-name next-root &optional framework-stage
framework-rollback))
(declare-function tp-transaction-active-p "tp-reactive" ())
(define-error 'ebox-framework-spi-provider-error
"Invalid Ebox framework SPI provider")
(defconst ebox-framework-spi-version 2
"Version of the additive Ebox framework provider SPI.")
(defconst ebox-framework-spi-schema-version 'ebox-framework-spi-schema/v2
"Schema identity for Ebox framework provider records.")
(defconst ebox-framework-spi--required-capabilities
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)
"Capabilities guaranteed by every Ebox framework SPI v2 provider record.")
(defconst ebox-framework-spi--stage-order
'(ebox-mirror/native framework-stage)
"Owner order inside the combined Ebox participant stage.")
(defconst ebox-framework-spi--rollback-order
'(framework-rollback ebox-mirror/native tp)
"Rollback order exposed by the combined Ebox participant contract.")
(cl-defstruct
(ebox-framework-spi-operation
(:constructor ebox-framework-spi--make-operation)
(:conc-name ebox-framework-spi--operation-))
"One immutable operation descriptor in the framework SPI."
(kind nil :read-only t)
(function nil :read-only t)
(argument-schema nil :read-only t)
(result-schema nil :read-only t)
(paired-stage-rollback-p nil :read-only t))
(cl-defstruct
(ebox-framework-spi-provider
(:constructor ebox-framework-spi--make-provider)
(:conc-name ebox-framework-spi--provider-))
"One immutable Ebox framework SPI capability record."
(spi-version nil :read-only t)
(schema-version nil :read-only t)
(capabilities nil :read-only t)
(tp-protocol nil :read-only t)
(stage-order nil :read-only t)
(rollback-order nil :read-only t)
(report-semantics nil :read-only t)
(initial-operation nil :read-only t)
(update-operation nil :read-only t))
(defun ebox-framework-spi-operation-kind (operation)
"Return OPERATION's immutable kind."
(ebox-framework-spi--operation-kind operation))
(defun ebox-framework-spi-operation-function (operation)
"Return OPERATION's immutable function symbol."
(ebox-framework-spi--operation-function operation))
(defun ebox-framework-spi-operation-argument-schema (operation)
"Return a defensive copy of OPERATION's argument schema."
(copy-sequence (ebox-framework-spi--operation-argument-schema operation)))
(defun ebox-framework-spi-operation-result-schema (operation)
"Return OPERATION's immutable result schema."
(ebox-framework-spi--operation-result-schema operation))
(defun ebox-framework-spi-operation-paired-stage-rollback-p (operation)
"Return non-nil when OPERATION requires paired stage and rollback."
(ebox-framework-spi--operation-paired-stage-rollback-p operation))
(defun ebox-framework-spi-provider-spi-version (provider)
"Return PROVIDER's immutable SPI version."
(ebox-framework-spi--provider-spi-version provider))
(defun ebox-framework-spi-provider-schema-version (provider)
"Return PROVIDER's immutable schema version."
(ebox-framework-spi--provider-schema-version provider))
(defun ebox-framework-spi-provider-capabilities (provider)
"Return a defensive copy of PROVIDER's capabilities."
(copy-sequence (ebox-framework-spi--provider-capabilities provider)))
(defun ebox-framework-spi-provider-tp-protocol (provider)
"Return PROVIDER's immutable TP protocol."
(ebox-framework-spi--provider-tp-protocol provider))
(defun ebox-framework-spi-provider-stage-order (provider)
"Return a defensive copy of PROVIDER's participant stage order."
(copy-sequence (ebox-framework-spi--provider-stage-order provider)))
(defun ebox-framework-spi-provider-rollback-order (provider)
"Return a defensive copy of PROVIDER's participant rollback order."
(copy-sequence (ebox-framework-spi--provider-rollback-order provider)))
(defun ebox-framework-spi-provider-report-semantics (provider)
"Return PROVIDER's immutable report semantics."
(ebox-framework-spi--provider-report-semantics provider))
(defun ebox-framework-spi-provider-initial-operation (provider)
"Return PROVIDER's immutable initial operation descriptor."
(ebox-framework-spi--provider-initial-operation provider))
(defun ebox-framework-spi-provider-update-operation (provider)
"Return PROVIDER's immutable update operation descriptor."
(ebox-framework-spi--provider-update-operation provider))
(defun ebox-framework-spi--validate-callback-pair
(framework-stage framework-rollback)
"Validate FRAMEWORK-STAGE and FRAMEWORK-ROLLBACK as a 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)
(defun ebox-framework-spi-initial
(buffer-or-name input framework-stage framework-rollback)
"Initially mount INPUT and run the paired framework callbacks.
FRAMEWORK-STAGE runs after provisional TP state and Ebox mirrors/native state
agree. FRAMEWORK-ROLLBACK receives the same legacy report on any later
transaction failure. Return that same report object after completion."
(ebox-framework-spi--validate-callback-pair
framework-stage framework-rollback)
(unless (ebox-canonical-input-p input)
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
(when (tp-transaction-active-p)
(error "Ebox SPI initial operation cannot join an outer TP transaction"))
(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
(ebox-surface--make-framework-participant
:publish framework-stage
:rollback framework-rollback
:state 'unpublished
:diagnostics nil)))
(ebox--render-to-buffer-internal buffer input nil participant))))
(defun ebox-framework-spi-update
(buffer-or-name input framework-stage framework-rollback)
"Update BUFFER-OR-NAME from INPUT through paired framework callbacks.
Return the same completed legacy report object observed by FRAMEWORK-STAGE."
(ebox-framework-spi--validate-callback-pair
framework-stage framework-rollback)
(ebox-commit buffer-or-name input framework-stage framework-rollback))
(defun ebox-framework-spi--operation
(kind function argument-schema)
"Return a fresh KIND operation descriptor for FUNCTION and ARGUMENT-SCHEMA."
(ebox-framework-spi--make-operation
:kind kind
:function function
:argument-schema (copy-sequence argument-schema)
:result-schema 'ebox-legacy-report/same-object
:paired-stage-rollback-p t))
(defun ebox-framework-spi--validate-operation
(operation kind function argument-schema)
"Validate OPERATION against KIND, FUNCTION, and ARGUMENT-SCHEMA."
(unless (and (ebox-framework-spi-operation-p operation)
(eq (ebox-framework-spi--operation-kind operation) kind)
(eq (ebox-framework-spi--operation-function operation) function)
(equal (ebox-framework-spi--operation-argument-schema operation)
argument-schema)
(eq (ebox-framework-spi--operation-result-schema operation)
'ebox-legacy-report/same-object)
(eq (ebox-framework-spi--operation-paired-stage-rollback-p
operation)
t))
(signal 'ebox-framework-spi-provider-error
(list :malformed-operation kind operation)))
t)
(defun ebox-framework-spi-validate-provider (provider)
"Validate PROVIDER as one complete Ebox framework SPI v2 record."
(unless (ebox-framework-spi-provider-p provider)
(signal 'ebox-framework-spi-provider-error
(list :malformed-provider provider)))
(unless (and (eql (ebox-framework-spi--provider-spi-version provider)
ebox-framework-spi-version)
(eq (ebox-framework-spi--provider-schema-version provider)
ebox-framework-spi-schema-version)
(equal (ebox-framework-spi--provider-capabilities provider)
ebox-framework-spi--required-capabilities)
(eq (ebox-framework-spi--provider-tp-protocol provider)
tp-transaction-protocol)
(equal (ebox-framework-spi--provider-stage-order provider)
ebox-framework-spi--stage-order)
(equal (ebox-framework-spi--provider-rollback-order provider)
ebox-framework-spi--rollback-order)
(eq (ebox-framework-spi--provider-report-semantics provider)
'same-object-legacy-report))
(signal 'ebox-framework-spi-provider-error
(list :malformed-provider-schema provider)))
(ebox-framework-spi--validate-operation
(ebox-framework-spi--provider-initial-operation provider)
'initial 'ebox-framework-spi-initial
'(buffer canonical-input framework-stage framework-rollback))
(ebox-framework-spi--validate-operation
(ebox-framework-spi--provider-update-operation provider)
'update 'ebox-framework-spi-update
'(buffer canonical-input-or-candidate framework-stage framework-rollback))
t)
(defun ebox-framework-spi-capabilities ()
"Return a fresh immutable Ebox framework SPI v2 provider record."
(let ((provider
(ebox-framework-spi--make-provider
:spi-version ebox-framework-spi-version
:schema-version ebox-framework-spi-schema-version
:capabilities
(copy-sequence ebox-framework-spi--required-capabilities)
:tp-protocol tp-transaction-protocol
:stage-order (copy-sequence ebox-framework-spi--stage-order)
:rollback-order (copy-sequence ebox-framework-spi--rollback-order)
:report-semantics 'same-object-legacy-report
:initial-operation
(ebox-framework-spi--operation
'initial 'ebox-framework-spi-initial
'(buffer canonical-input framework-stage framework-rollback))
:update-operation
(ebox-framework-spi--operation
'update 'ebox-framework-spi-update
'(buffer canonical-input-or-candidate
framework-stage framework-rollback)))))
(ebox-framework-spi-validate-provider provider)
provider))
(provide 'ebox-framework-spi-v2)
(provide 'ebox-spi)
;;; ebox-spi.el ends here

40
ebox.el
View File

@ -32,7 +32,7 @@
"ebox-flex.el" "ebox-grid.el" "ebox-canonical.el"
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
"ebox-dsl.el" "ebox-selector.el" "ebox.el"
"ebox-dsl.el" "ebox-selector.el" "ebox-spi.el" "ebox.el"
"ebox-native-reflow.el")
"Active Ebox Lisp sources compiled by `ebox-byte-compile'.")
@ -4384,13 +4384,9 @@ Examples:
(signal 'wrong-type-argument (list 'functionp observer)))
(cons present observer)))
;;;###autoload
(defun ebox-render-to-buffer (buffer-or-name input &optional options)
"Render canonical INPUT into BUFFER-OR-NAME and return the buffer.
INPUT atomically carries one typed Text/Box root and its source generation.
OPTIONS accepts only `:observer'. Its function receives the buffer and flat
TP/Ebox reports after accepted initial publication and later commits."
(declare (indent 1))
(defun ebox--render-to-buffer-internal
(buffer-or-name input options &optional framework-participant)
"Render INPUT into BUFFER-OR-NAME with OPTIONS and FRAMEWORK-PARTICIPANT."
(unless (ebox-canonical-input-p input)
(signal 'wrong-type-argument (list 'ebox-canonical-input-p input)))
(let* ((node (ebox-canonical-input--single-root
@ -4398,6 +4394,13 @@ TP/Ebox reports after accepted initial publication and later commits."
(source-base-index (ebox-canonical-input--source-index input))
(observer-option (ebox--render-observer-option options))
(buffer (get-buffer-create buffer-or-name))
(report-base
(and framework-participant
'(:strategy initial-mount
:constraint-source declarative
:publication-scope full-root
:dirty-count 1 :patch-count 1
:patch-ops (initial-mount))))
(old-observer (ebox-surface-buffer-observer buffer))
(old-bridge (with-current-buffer buffer ebox-surface--tp-observer))
success result)
@ -4411,9 +4414,10 @@ TP/Ebox reports after accepted initial publication and later commits."
(lambda ()
(ebox--with-render-gc
(ebox-surface-mount-buffer
buffer node nil nil nil
buffer node report-base nil nil
(and source-base-index
(list :source-base-index source-base-index)))
(list :source-base-index source-base-index))
framework-participant)
(with-current-buffer buffer
(goto-char (point-min))
(add-hook 'kill-buffer-hook
@ -4423,12 +4427,24 @@ TP/Ebox reports after accepted initial publication and later commits."
(ebox--schedule-buffer-runtime-prewarm buffer nil nil)
buffer))))
success t)
result)
(if framework-participant
(ebox-surface--framework-participant-report
framework-participant)
result))
(when (and (not success) (car observer-option))
(ebox-surface-set-buffer-observer buffer old-observer)
(unless old-bridge
(ebox-surface-cleanup-buffer-observer buffer))))))
;;;###autoload
(defun ebox-render-to-buffer (buffer-or-name input &optional options)
"Render canonical INPUT into BUFFER-OR-NAME and return the buffer.
INPUT atomically carries one typed Text/Box root and its source generation.
OPTIONS accepts only `:observer'. Its function receives the buffer and flat
TP/Ebox reports after accepted initial publication and later commits."
(declare (indent 1))
(ebox--render-to-buffer-internal buffer-or-name input options))
(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))))
@ -4845,6 +4861,8 @@ and are documented separately in the public API reference.")
viewport-reflow)
"Stable high-level Ebox capability families used by conformance tooling.")
(require 'ebox-spi)
(provide 'ebox)
;;; ============================================================
;;; Public API Summary

View File

@ -25,6 +25,8 @@
"patch-plan-performance:"
"style-schema-tests:"
"style-schema-performance:"
"spi-tests:"
"spi-performance:"
"docs-contract-tests:"
"ci-contract-tests:" "native-rust-tests:"
"native-build:" "performance-evaluator:" "diff-check:"))
@ -40,6 +42,7 @@
(should (string-match-p "ebox-byte-compile" source))
(should (string-match-p "ebox-native-reflow.el" ebox))
(should (string-match-p "ebox-patch-plan.el" ebox))
(should (string-match-p "ebox-spi.el" ebox))
(should (string-match-p "ebox-child-range.el" ebox))
(should-not (string-match-p "ebox-playground.el" ebox))))

View File

@ -29,12 +29,13 @@
"ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el"
"ebox-layout.el" "ebox-flex.el" "ebox-grid.el"
"ebox-buffer-backend.el" "ebox-patch-plan.el" "ebox-incremental.el"
"ebox-surface.el" "ebox-dsl.el"
"ebox-surface.el" "ebox-dsl.el" "ebox-spi.el"
"ebox-selector.el" "ebox-native-reflow.el"
"tests/ebox-core-render-tests.el" "tests/ebox-state-contract-tests.el"
"tests/ebox-layout-boundary-tests.el"
"tests/ebox-patch-plan-tests.el"
"tests/ebox-style-schema-tests.el"
"tests/ebox-spi-tests.el"
"tests/ebox-child-range-tests.el" "tests/ebox-grid-tests.el"
"tests/ebox-commit-tests.el" "tests/ebox-surface-tests.el"
"tests/ebox-dsl-tests.el" "tests/ebox-flex-tests.el"
@ -55,6 +56,7 @@
"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" "package-lint" "diff-check")

View File

@ -59,7 +59,8 @@
ebox-buffer-backend ebox-patch-plan
ebox-incremental
ebox-surface ebox-viewport ebox-dsl
ebox-selector))
ebox-selector ebox-spi
ebox-framework-spi-v2))
(should (featurep feature))))
(ert-deftest ebox-canonical-module-loads-typed-port-without-facade ()
@ -142,7 +143,7 @@
"ebox-buffer-backend.el" "ebox-patch-plan.el"
"ebox-incremental.el"
"ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el"
"ebox-dsl.el" "ebox-selector.el" "ebox.el"
"ebox-dsl.el" "ebox-selector.el" "ebox-spi.el" "ebox.el"
"ebox-native-reflow.el"))))
(ert-deftest ebox-surface-depends-only-on-public-tp-api ()

381
tests/ebox-spi-tests.el Normal file
View File

@ -0,0 +1,381 @@
;;; ebox-spi-tests.el --- M2a framework provider SPI gates -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'benchmark)
(require 'ebox)
(require 'ebox-spi)
(require 'ebox-fixtures)
(defconst ebox-spi-test--root
(expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name)))
"Repository root used by provider SPI tests.")
(defun ebox-spi-test--source (file)
"Return repository FILE as text."
(with-temp-buffer
(insert-file-contents (expand-file-name file ebox-spi-test--root))
(buffer-string)))
(defun ebox-spi-test--reset-runtime-state ()
"Reset identities and compatibility mirrors used by SPI tests."
(setq ebox--region-id-counter 0
ebox--runtime-node-id-counter 0)
(dolist (table (list ebox--region-box-table
ebox--buffer-render-state-table
ebox--scroll-global-state))
(when (hash-table-p table) (clrhash table))))
(ert-deftest ebox-spi-provider-schema-is-complete-and-defensive ()
"The additive v2 provider exposes one complete immutable snapshot."
(let* ((first (ebox-framework-spi-capabilities))
(second (ebox-framework-spi-capabilities))
(initial (ebox-framework-spi-provider-initial-operation first))
(update (ebox-framework-spi-provider-update-operation first)))
(should (featurep 'ebox-framework-spi-v2))
(should (ebox-framework-spi-validate-provider first))
(should-not (eq first second))
(should (= (ebox-framework-spi-provider-spi-version first) 2))
(should (eq (ebox-framework-spi-provider-schema-version first)
'ebox-framework-spi-schema/v2))
(should (eq (ebox-framework-spi-provider-tp-protocol first)
'tp-transaction-protocol-v1+v2))
(should
(equal (ebox-framework-spi-provider-capabilities first)
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)))
(should
(equal (ebox-framework-spi-provider-stage-order first)
'(ebox-mirror/native framework-stage)))
(should
(equal (ebox-framework-spi-provider-rollback-order first)
'(framework-rollback ebox-mirror/native tp)))
(should (eq (ebox-framework-spi-provider-report-semantics first)
'same-object-legacy-report))
(should (eq (ebox-framework-spi-operation-kind initial) 'initial))
(should (eq (ebox-framework-spi-operation-function initial)
'ebox-framework-spi-initial))
(should
(equal (ebox-framework-spi-operation-argument-schema initial)
'(buffer canonical-input framework-stage framework-rollback)))
(should (eq (ebox-framework-spi-operation-result-schema initial)
'ebox-legacy-report/same-object))
(should (eq (ebox-framework-spi-operation-paired-stage-rollback-p initial)
t))
(should (eq (ebox-framework-spi-operation-kind update) 'update))
(should (eq (ebox-framework-spi-operation-function update)
'ebox-framework-spi-update))
(should
(equal (ebox-framework-spi-operation-argument-schema update)
'(buffer canonical-input-or-candidate
framework-stage framework-rollback)))
(should (eq (ebox-framework-spi-operation-result-schema update)
'ebox-legacy-report/same-object))
(should (eq (ebox-framework-spi-operation-paired-stage-rollback-p update)
t))
(should-error
(eval
`(setf (ebox-framework-spi-provider-spi-version ',first) 3)))
(should-error
(eval
`(setf (ebox-framework-spi-operation-kind ',initial) 'broken)))
(let ((capabilities (ebox-framework-spi-provider-capabilities first))
(stage-order (ebox-framework-spi-provider-stage-order first))
(rollback-order (ebox-framework-spi-provider-rollback-order first))
(arguments (ebox-framework-spi-operation-argument-schema initial)))
(setcar capabilities 'mutated)
(setcar stage-order 'mutated)
(setcar rollback-order 'mutated)
(setcar arguments 'mutated))
(should (ebox-framework-spi-validate-provider first))
(should (eq (car (ebox-framework-spi-provider-capabilities first))
'initial-paired-stage-rollback))
(should (eq (car (ebox-framework-spi-provider-stage-order first))
'ebox-mirror/native))
(should (eq (car (ebox-framework-spi-provider-rollback-order first))
'framework-rollback))
(should (eq (car (ebox-framework-spi-operation-argument-schema initial))
'buffer))
(let ((fresh (ebox-framework-spi-capabilities)))
(should (eq (car (ebox-framework-spi-provider-capabilities fresh))
'initial-paired-stage-rollback))
(should (eq (car (ebox-framework-spi-operation-argument-schema
(ebox-framework-spi-provider-initial-operation fresh)))
'buffer)))))
(ert-deftest ebox-spi-provider-is-additive-and-does-not-bootstrap-consumers ()
"E5 publishes no selected port, consumer probe, or v1 removal."
(let ((source (ebox-spi-test--source "ebox-spi.el"))
(facade (ebox-spi-test--source "ebox.el")))
(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 (functionp #'ebox-render-to-buffer))
(should (functionp #'ebox-commit))
(should (functionp #'ebox-framework-spi-capabilities))))
(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)
(let* ((provider (ebox-framework-spi-capabilities))
(initial-function
(ebox-framework-spi-operation-function
(ebox-framework-spi-provider-initial-operation provider)))
(update-function
(ebox-framework-spi-operation-function
(ebox-framework-spi-provider-update-operation provider)))
(buffer (generate-new-buffer " *ebox-m2a-e5-success*"))
initial-seen update-seen trace)
(unwind-protect
(let ((initial-report
(funcall
initial-function buffer
(ebox-test-box :key 'root (ebox-test-text "initial")
:width '(96))
(lambda (report)
(setq initial-seen report)
(push 'initial-stage trace)
(should (eq (plist-get report :framework-participant-state)
'published))
(let ((surface (ebox-surface--live-buffer-surface buffer)))
(should surface)
(should
(eq (gethash buffer ebox--buffer-render-state-table)
(tp-surface-client-state surface))))
(plist-put report :framework-token 'initial-token))
(lambda (_report) (push 'initial-rollback trace)))))
(should (eq initial-report initial-seen))
(should (eq (plist-get initial-report :framework-participant-state)
'completed))
(should (eq (plist-get initial-report :framework-token)
'initial-token))
(should (eq (plist-get initial-report :strategy) 'initial-mount))
(should (equal trace '(initial-stage)))
(setq trace nil)
(let ((update-report
(funcall
update-function buffer
(ebox-test-box :key 'root (ebox-test-text "updated")
:width '(96))
(lambda (report)
(setq update-seen report)
(push 'update-stage trace)
(plist-put report :framework-token 'update-token))
(lambda (_report) (push 'update-rollback trace)))))
(should (eq update-report update-seen))
(should (eq (plist-get update-report
:framework-participant-state)
'completed))
(should (eq (plist-get update-report :framework-token)
'update-token))
(should (equal trace '(update-stage)))
(should (string-match-p
"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-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)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-initial-fault*"))
(original
(symbol-function 'tp--run-transaction-precommit-functions))
stage-report rollback-report trace failure)
(unwind-protect
(progn
(with-current-buffer buffer (insert "sentinel"))
(cl-letf
(((symbol-function 'tp--run-transaction-precommit-functions)
(lambda ()
(funcall original)
(error "E5 initial later failure"))))
(setq failure
(condition-case condition
(ebox-framework-spi-initial
buffer
(ebox-test-box
:key 'root :width '(96) :height 1 :overflow 'scroll
(ebox-test-text "zero\none\ntwo\nthree"))
(lambda (report)
(setq stage-report report)
(push 'stage trace)
(should (> (hash-table-count
ebox--scroll-global-state)
0)))
(lambda (report)
(setq rollback-report report)
(push 'rollback trace)))
(error condition))))
(should (equal (cadr failure) "E5 initial later failure"))
(should (equal trace '(rollback stage)))
(should (eq stage-report rollback-report))
(should (eq (plist-get stage-report :framework-participant-state)
'rolled-back))
(should-not (ebox-surface-buffer-mounted-p buffer))
(should-not (gethash buffer ebox--buffer-render-state-table))
(should (= (hash-table-count ebox--scroll-global-state) 0))
(should
(equal (with-current-buffer buffer (buffer-string)) "sentinel")))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-initial-stage-failure-runs-its-pair ()
"An initial framework-stage error rolls back the same report and mount."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-stage-fault*"))
stage-report rollback-report trace)
(unwind-protect
(progn
(should-error
(ebox-framework-spi-initial
buffer
(ebox-test-box :key 'root (ebox-test-text "rejected"))
(lambda (report)
(setq stage-report report)
(push 'stage trace)
(error "E5 stage callback failure"))
(lambda (report)
(setq rollback-report report)
(push 'rollback trace))))
(should (equal trace '(rollback stage)))
(should (eq stage-report rollback-report))
(should (eq (plist-get stage-report :framework-participant-state)
'rolled-back))
(should-not (ebox-surface-buffer-mounted-p buffer))
(should-not (gethash buffer ebox--buffer-render-state-table))
(should (equal (with-current-buffer buffer (buffer-string)) "")))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-update-rolls-the-same-report-back ()
"A later TP failure restores update state through the v2 operation."
(ebox-spi-test--reset-runtime-state)
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-update-fault*"))
(original
(symbol-function 'tp--run-transaction-precommit-functions))
stage-report rollback-report trace)
(unwind-protect
(progn
(ebox-render-to-buffer
buffer (ebox-test-box :key 'root (ebox-test-text "old")))
(let* ((surface (ebox-surface--live-buffer-surface buffer))
(state (tp-surface-client-state surface))
(revision (tp-surface-revision surface))
(contents
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))))
(cl-letf
(((symbol-function 'tp--run-transaction-precommit-functions)
(lambda ()
(funcall original)
(error "E5 update later failure"))))
(should-error
(ebox-framework-spi-update
buffer
(ebox-test-box :key 'root (ebox-test-text "rejected"))
(lambda (report)
(setq stage-report report)
(push 'stage trace))
(lambda (report)
(setq rollback-report report)
(push 'rollback trace)))))
(should (equal trace '(rollback stage)))
(should (eq stage-report rollback-report))
(should (eq (plist-get stage-report
:framework-participant-state)
'rolled-back))
(should (eq (tp-surface-client-state surface) state))
(should (= (tp-surface-revision surface) revision))
(should
(equal-including-properties
(with-current-buffer buffer
(buffer-substring (point-min) (point-max)))
contents))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-rejects-malformed-pairs-and-provider-records ()
"Provider-side schemas reject malformed callbacks and capability records."
(let ((buffer (generate-new-buffer " *ebox-m2a-e5-invalid*"))
(input (ebox-test-box :key 'root (ebox-test-text "invalid")))
(valid (ebox-framework-spi-capabilities)))
(unwind-protect
(progn
(should-error
(ebox-framework-spi-initial buffer input nil #'ignore)
:type 'wrong-type-argument)
(should-error
(ebox-framework-spi-initial buffer input #'ignore nil)
:type 'wrong-type-argument)
(should-error
(ebox-framework-spi-update buffer input 7 #'ignore)
:type 'wrong-type-argument)
(should-error
(ebox-framework-spi-validate-provider
(ebox-framework-spi--make-provider
:spi-version 99
:schema-version 'broken
:capabilities nil
:tp-protocol 'broken))
:type 'ebox-framework-spi-provider-error)
(should-error
(ebox-framework-spi-validate-provider
(ebox-framework-spi--make-provider
:spi-version 2
:schema-version 'ebox-framework-spi-schema/v2
:capabilities
'(initial-paired-stage-rollback
update-paired-stage-rollback
combined-participant-ordering
same-object-legacy-report)
:tp-protocol 'tp-transaction-protocol-v1+v2
:stage-order '(ebox-mirror/native framework-stage)
:rollback-order '(framework-rollback ebox-mirror/native tp)
:report-semantics 'same-object-legacy-report
:initial-operation
(ebox-framework-spi--make-operation
:kind 'initial
:function 'ebox-framework-spi-initial
:argument-schema
'(buffer canonical-input framework-stage framework-rollback)
:result-schema 'broken
:paired-stage-rollback-p nil)
:update-operation
(ebox-framework-spi-provider-update-operation valid)))
:type 'ebox-framework-spi-provider-error))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-spi-performance-builds-bounded-provider-snapshots ()
"Capability access constructs one provider and two operations per call."
(let ((provider-count 0)
(operation-count 0)
(provider-constructor
(symbol-function 'ebox-framework-spi--make-provider))
(operation-factory
(symbol-function 'ebox-framework-spi--operation))
benchmark last)
(cl-letf (((symbol-function 'ebox-framework-spi--make-provider)
(lambda (&rest arguments)
(cl-incf provider-count)
(apply provider-constructor arguments)))
((symbol-function 'ebox-framework-spi--operation)
(lambda (&rest arguments)
(cl-incf operation-count)
(apply operation-factory arguments))))
(setq benchmark
(benchmark-run
1
(dotimes (_iteration 1000)
(setq last (ebox-framework-spi-capabilities))))))
(should (ebox-framework-spi-provider-p last))
(should (= provider-count 1000))
(should (= operation-count 2000))
(should (< (car benchmark) 2.0))))
(provide 'ebox-spi-tests)
;;; ebox-spi-tests.el ends here