From b5e8b6d86323f015a87c18078562cde89f8c1234 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Mon, 31 Aug 2026 21:52:51 +0800 Subject: [PATCH] feat: publish Ebox SPI v2 provider for M2a E5 --- DESIGN.md | 1 + DESIGN.zh.md | 1 + Makefile | 10 +- ...box-current-implementation-reference.en.md | 6 +- ...box-current-implementation-reference.zh.md | 6 +- ebox-spi.el | 259 ++++++++++++ ebox.el | 40 +- tests/ebox-ci-contract-tests.el | 3 + tests/ebox-docs-contract-tests.el | 4 +- tests/ebox-package-tests.el | 5 +- tests/ebox-spi-tests.el | 381 ++++++++++++++++++ 11 files changed, 698 insertions(+), 18 deletions(-) create mode 100644 ebox-spi.el create mode 100644 tests/ebox-spi-tests.el diff --git a/DESIGN.md b/DESIGN.md index b7bd579..354ddaa 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -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 diff --git a/DESIGN.zh.md b/DESIGN.zh.md index 5dc7238..145bead 100644 --- a/DESIGN.zh.md +++ b/DESIGN.zh.md @@ -11,6 +11,7 @@ Ebox 是底层空间渲染引擎。它负责把声明式节点树转换为经过 - native 模块只是可选加速器,必须有完全等价的 Elisp fallback。 - 新抽象必须消除重复,或让已有的公共工作流更简单。 - 公共 property schema、ECSS metadata 与 used engine projection 都从同一 property definition record 派生。 +- 跨包 framework integration 只发布 additive versioned provider;consumer selection 与 fallback policy 不属于 Ebox。 ## 流水线 diff --git a/Makefile b/Makefile index 4492805..1932bcf 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/docs/maintainer/ebox-current-implementation-reference.en.md b/docs/maintainer/ebox-current-implementation-reference.en.md index f8f4fdf..1140dd7 100644 --- a/docs/maintainer/ebox-current-implementation-reference.en.md +++ b/docs/maintainer/ebox-current-implementation-reference.en.md @@ -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 diff --git a/docs/maintainer/ebox-current-implementation-reference.zh.md b/docs/maintainer/ebox-current-implementation-reference.zh.md index b99212b..44dc9ef 100644 --- a/docs/maintainer/ebox-current-implementation-reference.zh.md +++ b/docs/maintainer/ebox-current-implementation-reference.zh.md @@ -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 diff --git a/ebox-spi.el b/ebox-spi.el new file mode 100644 index 0000000..afdce63 --- /dev/null +++ b/ebox-spi.el @@ -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 diff --git a/ebox.el b/ebox.el index f736578..bc1ac0c 100644 --- a/ebox.el +++ b/ebox.el @@ -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 diff --git a/tests/ebox-ci-contract-tests.el b/tests/ebox-ci-contract-tests.el index 5300172..ede5002 100644 --- a/tests/ebox-ci-contract-tests.el +++ b/tests/ebox-ci-contract-tests.el @@ -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)))) diff --git a/tests/ebox-docs-contract-tests.el b/tests/ebox-docs-contract-tests.el index 2ba10cf..e0b8588 100644 --- a/tests/ebox-docs-contract-tests.el +++ b/tests/ebox-docs-contract-tests.el @@ -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") diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index c6a50c8..f9af626 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -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 () diff --git a/tests/ebox-spi-tests.el b/tests/ebox-spi-tests.el new file mode 100644 index 0000000..fd39f57 --- /dev/null +++ b/tests/ebox-spi-tests.el @@ -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