refactor: classify Ebox retained state for M2a E1
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 20:11:16 +08:00
parent 54b3514668
commit e55798e0e9
9 changed files with 566 additions and 10 deletions

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 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 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 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 docs-contract-tests ci-contract-tests
source-tests:
$(EMACS_BATCH) -l tests/ebox-source-tests.el -f ert-run-tests-batch-and-exit
@ -63,6 +63,9 @@ dsl-tests:
flex-tests:
$(EMACS_TEST) -l tests/ebox-flex-tests.el -f ert-run-tests-batch-and-exit
state-contract-tests:
$(EMACS_TEST) -l tests/ebox-state-contract-tests.el -f ert-run-tests-batch-and-exit
performance-evaluator: surface-tests ebox-commit-tests
$(EMACS_BATCH) -l scripts/ebox-performance-evaluator.el -f ebox-performance-evaluator-batch

View File

@ -19,6 +19,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
| `ebox.el` | Public facade, construction helpers, rendering, TP-backed buffer entry points, scrolling, commit, and byte compilation. |
| `ebox-cache.el` | Measurement/render cache records, invalidation, and cache reports. |
| `ebox-source.el` | Opaque source handles, immutable author source records, and candidate source indexes. |
| `ebox-state-contract.el` | Closed retained-state ownership inventory and read-only compatibility-mirror rebuild probes. |
| `ebox-style.el` | ECSS property schemas, declarations and cascade, shorthand expansion, computed style, colors, borders, and dirty effects. |
| `ebox-tree.el` | Node traversal, logical child access, ECSS subject adaptation, identity, parent paths, keys, and tree snapshots. |
| `ebox-child-range.el` | Immutable weighted segment trie, sparse persistent key trie, Range replacement, and Gate A metrics. |
@ -37,7 +38,7 @@ This is the maintainer entry point for the standalone Ebox repository. It descri
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-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-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
@ -61,6 +62,7 @@ Caller-owned Source Tree
| Model | Owner | Must not own |
| --- | --- | --- |
| Source/Element Tree | `ebox-child-range.el`, `ebox-tree.el`, `ebox-dsl.el` | Published buffer mutation. |
| State Ownership Contract | `ebox-state-contract.el` | Live publication or mutable authority. |
| Computed Style | `ebox-style.el` | Layout identity or patch execution. |
| Measurement | `ebox-measure.el` | Application state or dirty policy. |
| Formatting Context | `ebox-layout.el`, `ebox-flex.el`, `ebox-grid.el` | Buffer edits. |
@ -81,6 +83,7 @@ Caller-owned Source Tree
- Keys are local to siblings; visible strings are never used as identity.
- Ebox owns logical node-to-subject adaptation and id/class/type candidate indexes; ECSS's public structured matcher is the only selector truth source. Subjects expose built-in id/key plus explicit `:selector-attributes`, never visible content, layout state, or runtime containers.
- `ebox-style--property-definitions` is the single Ebox author-property source. Load time derives Ebox's read-only lookup index and one immutable ECSS package schema from it; the surface schema is composed once, and node construction or updates never re-register or copy the whole schema domain.
- E1 freezes the closed M2a target classification for retained state: immutable generation facts; generation-bound scroll/native authority; opaque TP client-state custody; one-way compatibility mirrors; and disposable caches. Every inventory row separately records the current v1 storage shape plus its target owner, mutation API, generation binding, rollback, rebuild proof, and cleanup. Current v1 TP client state still holds the whole Ebox state plist until a later checkpoint changes that storage shape. The E1 mirror probes only rebuild and compare projections; they never mutate live state.
- `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.
@ -112,6 +115,7 @@ make package-tests
make selector-tests
make dsl-tests
make flex-tests
make state-contract-tests
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

View File

@ -19,6 +19,7 @@
| `ebox.el` | 公共门面、构造辅助函数、渲染、基于 TP 的 buffer 入口、滚动、commit 与 byte compile。 |
| `ebox-cache.el` | 测量/渲染缓存记录、失效和缓存报告。 |
| `ebox-source.el` | opaque source handle、不可变 author source record 与 candidate source index。 |
| `ebox-state-contract.el` | 封闭的 retained-state 所有权清单与只读 compatibility-mirror 重建探针。 |
| `ebox-style.el` | ECSS property schema、declaration 与 cascade、shorthand 展开、computed style、颜色、border 和 dirty effect。 |
| `ebox-tree.el` | 节点遍历、逻辑子节点访问、ECSS subject 适配、identity、父路径、key 和树 snapshot。 |
| `ebox-child-range.el` | 不可变 weighted segment trie、稀疏持久 key trie、Range replacement 与 Gate A metrics。 |
@ -37,7 +38,7 @@
本包有意不包含应用 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-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-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`
## 运行时模型
@ -61,6 +62,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
| 模型 | Owner | 不得拥有 |
| --- | --- | --- |
| Source/Element Tree | `ebox-child-range.el`、`ebox-tree.el`、`ebox-dsl.el` | 已发布 buffer 的变更。 |
| 状态所有权合同 | `ebox-state-contract.el` | Live 发布或可变 authority。 |
| Computed Style | `ebox-style.el` | 布局 identity 或 patch 执行。 |
| Measurement | `ebox-measure.el` | 应用状态或 dirty 策略。 |
| Formatting Context | `ebox-layout.el`、`ebox-flex.el`、`ebox-grid.el` | Buffer 编辑。 |
@ -81,6 +83,7 @@ active 合同还覆盖 `Makefile`、`.github/workflows/ci.yml`、`tests/ebox-cor
- Key 只在兄弟节点中有效;不能用可见字符串作为 identity。
- Ebox 只拥有逻辑 node-to-subject 适配与 id/class/type 候选索引ECSS 的公共结构化 matcher 是唯一 selector 真相源。subject 只暴露内建 id/key 和显式 `:selector-attributes`,绝不暴露可见 content、布局状态或 runtime 容器。
- `ebox-style--property-definitions` 是 Ebox 作者属性的唯一来源:加载时一次生成 Ebox 自己的只读查询索引与一个不可变 ECSS package schemasurface schema 只组合一次node 构造和更新不重复注册或复制整份 schema。
- E1 固化封闭的 M2a 目标分类:不可变 generation fact、绑定 generation 的 scroll/native authority、opaque TP client-state custody、单向 compatibility mirror以及 disposable cache。每一行分别记录当前 v1 存储形态,以及目标 owner、mutation API、generation binding、rollback、rebuild proof 与 cleanup。在后续 checkpoint 改变存储形态前,当前 v1 TP client state 仍保存完整 Ebox state plist。E1 mirror 探针只重建并比较投影,不修改 live state。
- `owner-rerender` 范围大于 `span-patch``span-patch` 大于 `paint-patch`
- Buffer 坐标属于生成它的 generation变更后必须重新获取。
- Grid 使用普通测量与渲染流水线native reflow 可以拒绝不适合的树并回退到 Elisp正确性不变。
@ -110,6 +113,7 @@ make package-tests
make selector-tests
make dsl-tests
make flex-tests
make state-contract-tests
make docs-contract-tests
make ci-contract-tests
make performance-evaluator

277
ebox-state-contract.el Normal file
View File

@ -0,0 +1,277 @@
;;; ebox-state-contract.el --- Ebox retained-state ownership contract -*- lexical-binding: t; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; Records the M2a target ownership classification for retained Ebox state and
;; the distinct current v1 storage shape. It also offers read-only
;; compatibility-mirror rebuild probes. This module does not publish a
;; generation, mutate live runtime state, or select a transaction route.
;;; Code:
(require 'cl-lib)
(require 'subr-x)
(define-error 'ebox-state-contract-error
"Invalid Ebox retained-state contract")
(defconst ebox-state-contract-categories
'(generation-fact generation-bound-mutable-authority
tp-storage-custody compatibility-mirror disposable-cache)
"Closed set of retained-state categories used by the M2a inventory.")
(defconst ebox-state-contract-required-fields
'(:id :storage :current-contract :target-contract :category :owner
:mutation-api :generation-binding :rollback :rebuild-proof :cleanup)
"Fields required on every M2a retained-state inventory record.")
(defconst ebox-state-contract--inventory
'((:id runtime-generation-indexes
:storage (:source-index ebox-source-index/records
ebox-source-index/handle-records
ebox-source-index/host-ref-table
:root-node :node-table :parent-table
:runtime-type-count-table :range-ref-table
:selector-id-table :selector-class-table :selector-type-table)
:current-contract mixed-candidate-and-committed-state
:target-contract immutable-generation-value
:category generation-fact
:owner ebox
:mutation-api candidate-construction-only
:generation-binding immutable-generation-token
:rollback discard-candidate
:rebuild-proof source-and-runtime-index-rebuild
:cleanup generation-replacement)
(:id region-generation-indexes
:storage (:region-id-set :region-node-table :region-box-count-table
:region-box-table :layout-snapshots)
:current-contract candidate-state-plus-global-projection
:target-contract immutable-generation-value
:category generation-fact
:owner ebox
:mutation-api candidate-layout-materialization-only
:generation-binding immutable-generation-token
:rollback discard-candidate
:rebuild-proof layout-and-region-index-rebuild
:cleanup generation-replacement)
(:id scroll-membership
:storage (:scroll-region-ids)
:current-contract committed-state-membership-list
:target-contract immutable-generation-value
:category generation-fact
:owner ebox
:mutation-api candidate-layout-materialization-only
:generation-binding immutable-generation-token
:rollback discard-candidate
:rebuild-proof layout-scroll-membership-rebuild
:cleanup generation-replacement)
(:id scroll-runtime-authority
:storage (:scroll-state-table :scroll-offset :scroll-window
ebox--scroll-global-state
ebox--smooth-scroll-state-table
ebox--scroll-idle-prefetch-timers)
:current-contract global-and-state-table-mutable-handles
:target-contract stable-id-plus-generation-token-authority
:category generation-bound-mutable-authority
:owner ebox-scroll
:mutation-api stable-scroll-id-and-generation-token
:generation-binding required
:rollback participant-journal-restores-prior-authority
:rebuild-proof not-rebuildable-from-cache
:cleanup cancel-timers-and-retire-generation)
(:id native-runtime-authority
:storage (:native-sync-session :native-sync-pending :native-session
ebox-native-reflow-preparation ebox-native-reflow-session)
:current-contract candidate-preparation-and-confirmed-session-handles
:target-contract generation-token-authority
:category generation-bound-mutable-authority
:owner ebox-native
:mutation-api native-candidate-confirm-or-abort
:generation-binding required
:rollback abort-candidate-and-keep-confirmed-session
:rebuild-proof not-rebuildable-from-cache
:cleanup release-losing-session)
(:id tp-client-state-custody
:storage (tp-surface-client-state)
:current-contract entire-ebox-state-plist
:target-contract opaque-generation-correlation-only
:category tp-storage-custody
:owner tp
:mutation-api opaque-ebox-generation-correlation-only
:generation-binding opaque-correlation
:rollback tp-restores-client-state
:rebuild-proof ebox-generation-remains-source-of-truth
:cleanup tp-surface-unmount)
(:id buffer-render-state-mirror
:storage (ebox--buffer-render-state-table)
:current-contract same-object-alias-of-tp-client-state
:target-contract one-way-generation-projection
:category compatibility-mirror
:owner ebox-compatibility
:mutation-api project-from-committed-generation
:generation-binding committed-generation-token
:rollback participant-restores-prior-projection
:rebuild-proof project-buffer-mirror-from-committed-states
:cleanup remove-buffer-entry)
(:id region-box-lookup-mirror
:storage (ebox--region-box-table)
:current-contract participant-maintained-global-projection
:target-contract rebuildable-generation-projection
:category compatibility-mirror
:owner ebox-compatibility
:mutation-api project-from-committed-generation
:generation-binding committed-generation-token
:rollback participant-restores-prior-projection
:rebuild-proof project-region-mirror-from-generation-indexes
:cleanup remove-retired-generation-entries)
(:id derived-caches
:storage (ebox--char-width-cache ebox--face-height-width-cache
ebox--display-signature-cache
ebox--render-cache-signature-cache)
:current-contract process-or-render-local-derived-values
:target-contract discardable-derived-values
:category disposable-cache
:owner ebox-cache
:mutation-api cache-fill-and-evict
:generation-binding cache-key-or-display-signature
:rollback discard
:rebuild-proof recompute-with-identical-semantic-result
:cleanup bounded-eviction-or-clear))
"M2a retained-state inventory.
The records classify every state family named by the architecture plan. They
do not claim that later M2a target storage is already active. The inventory is
fail-closed: adding a retained-state family requires adding a complete record
rather than relying on an implicit default.")
(defun ebox-state-contract-inventory ()
"Return a detached copy of the retained-state inventory."
(copy-tree ebox-state-contract--inventory))
(defun ebox-state-contract-record (id)
"Return a detached inventory record identified by ID, or nil."
(when-let* ((record
(cl-find id ebox-state-contract--inventory
:key (lambda (item) (plist-get item :id)))))
(copy-tree record)))
(defun ebox-state-contract-validate ()
"Validate and return a detached retained-state inventory.
Signal `ebox-state-contract-error' when a record is incomplete, duplicated, or
uses a category outside `ebox-state-contract-categories'."
(let ((seen (make-hash-table :test #'eq)))
(dolist (record ebox-state-contract--inventory)
(dolist (field ebox-state-contract-required-fields)
(unless (plist-get record field)
(signal 'ebox-state-contract-error
(list :missing-field field :record record))))
(let ((id (plist-get record :id))
(category (plist-get record :category)))
(when (gethash id seen)
(signal 'ebox-state-contract-error
(list :duplicate-id id)))
(puthash id t seen)
(unless (memq category ebox-state-contract-categories)
(signal 'ebox-state-contract-error
(list :unknown-category category :id id)))))
(ebox-state-contract-inventory)))
(defun ebox-state-contract--project-buffer-mirror (entries)
"Project BUFFER . STATE ENTRIES into a fresh compatibility mirror."
(let ((table (make-hash-table :test #'eq))
(missing (make-symbol "missing")))
(dolist (entry entries)
(unless (and (consp entry) (bufferp (car entry)) (listp (cdr entry)))
(signal 'ebox-state-contract-error
(list :malformed-committed-state-entry entry)))
(unless (eq (gethash (car entry) table missing) missing)
(signal 'ebox-state-contract-error
(list :duplicate-buffer (car entry))))
(puthash (car entry) (cdr entry) table))
table))
(defun ebox-state-contract--project-region-mirror (entries)
"Project BUFFER . STATE ENTRIES into a fresh region compatibility mirror."
(let ((table (make-hash-table :test #'equal))
(missing (make-symbol "missing")))
(dolist (entry entries)
(when-let* ((regions (plist-get (cdr entry) :region-box-table)))
(unless (hash-table-p regions)
(signal 'ebox-state-contract-error
(list :malformed-region-index (car entry))))
(maphash
(lambda (region-id box)
(let ((existing (gethash region-id table missing)))
(when (and (not (eq existing missing)) (not (eq existing box)))
(signal 'ebox-state-contract-error
(list :duplicate-region-id region-id)))
(puthash region-id box table)))
regions)))
table))
(defun ebox-state-contract-rebuild-compatibility-mirrors (entries)
"Return fresh buffer and region mirrors projected from committed ENTRIES.
ENTRIES is a list of `(BUFFER . STATE)' pairs. STATE is the current v1 Ebox
state plist held by TP client-state custody; a later checkpoint may narrow that
custody to opaque generation correlation. The returned plist contains
`:buffer-table' and `:region-table'; neither table mutates live Ebox state."
(list :buffer-table
(ebox-state-contract--project-buffer-mirror entries)
:region-table
(ebox-state-contract--project-region-mirror entries)))
(defun ebox-state-contract--hash-diff-report (expected actual)
"Return deterministic differences and work count for two hash tables."
(unless (and (hash-table-p expected) (hash-table-p actual))
(signal 'wrong-type-argument (list 'hash-table-p expected actual)))
(let ((missing (make-symbol "missing")) differences keys)
(maphash (lambda (key _value) (push key keys)) expected)
(maphash (lambda (key _value) (push key keys)) actual)
(dolist (key (delete-dups keys))
(let ((left (gethash key expected missing))
(right (gethash key actual missing)))
(unless (or (eq left right)
(and (not (eq left missing))
(not (eq right missing))
(equal left right)))
(push key differences))))
(list
:differences
(sort differences
(lambda (left right)
(string< (prin1-to-string left) (prin1-to-string right))))
:comparisons (length (delete-dups keys)))))
(defun ebox-state-contract-probe-compatibility-mirrors
(entries buffer-mirror region-mirror)
"Compare live mirrors with fresh projections from committed ENTRIES.
BUFFER-MIRROR and REGION-MIRROR are observed compatibility tables. The return
value is a read-only report with deterministic mismatch lists and a boolean
`:consistent-p'."
(let* ((rebuilt
(ebox-state-contract-rebuild-compatibility-mirrors entries))
(buffer-report
(ebox-state-contract--hash-diff-report
(plist-get rebuilt :buffer-table) buffer-mirror))
(region-report
(ebox-state-contract--hash-diff-report
(plist-get rebuilt :region-table) region-mirror))
(buffer-differences (plist-get buffer-report :differences))
(region-differences (plist-get region-report :differences)))
(list :consistent-p (and (null buffer-differences)
(null region-differences))
:buffer-differences buffer-differences
:region-differences region-differences
:entry-count (length entries)
:projected-region-count
(hash-table-count (plist-get rebuilt :region-table))
:buffer-comparisons (plist-get buffer-report :comparisons)
:region-comparisons (plist-get region-report :comparisons))))
(provide 'ebox-state-contract)
;;; ebox-state-contract.el ends here

View File

@ -23,7 +23,8 @@
"Directory containing the active Ebox Lisp sources.")
(defconst ebox--compile-sources
'("ebox-cache.el" "ebox-source.el" "ebox-font.el" "ebox-style.el"
'("ebox-cache.el" "ebox-source.el" "ebox-state-contract.el"
"ebox-font.el" "ebox-style.el"
"ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-child-range.el" "ebox-tree.el" "ebox-measure.el"
@ -43,6 +44,7 @@
(require 'subr-x)
(require 'ebox-cache)
(require 'ebox-source)
(require 'ebox-state-contract)
(require 'ebox-font)
(require 'ebox-style)
(require 'ebox-layout-config)

View File

@ -18,6 +18,7 @@
(dolist (target '("check:" "ci:" "load:" "compile:" "test:"
"core-tests:" "child-range-tests:" "grid-tests:" "ebox-commit-tests:"
"surface-tests:"
"state-contract-tests:"
"docs-contract-tests:"
"ci-contract-tests:" "native-rust-tests:"
"native-build:" "performance-evaluator:" "diff-check:"))

View File

@ -24,12 +24,14 @@
(defconst ebox-docs-test--active-files
'("Makefile" ".github/workflows/ci.yml"
"ebox.el" "ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el"
"ebox.el" "ebox-cache.el" "ebox-state-contract.el" "ebox-style.el"
"ebox-child-range.el" "ebox-tree.el"
"ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el"
"ebox-layout.el" "ebox-flex.el" "ebox-grid.el"
"ebox-buffer-backend.el" "ebox-incremental.el" "ebox-surface.el" "ebox-dsl.el"
"ebox-selector.el" "ebox-native-reflow.el"
"tests/ebox-core-render-tests.el" "tests/ebox-child-range-tests.el" "tests/ebox-grid-tests.el"
"tests/ebox-core-render-tests.el" "tests/ebox-state-contract-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"
@ -45,7 +47,7 @@
(defconst ebox-docs-test--targets
'("check" "ci" "load" "compile" "core-tests" "child-range-tests" "grid-tests"
"ebox-commit-tests" "surface-tests" "visual-check-tests" "package-tests"
"selector-tests" "dsl-tests" "flex-tests"
"selector-tests" "dsl-tests" "flex-tests" "state-contract-tests"
"docs-contract-tests" "ci-contract-tests" "performance-evaluator"
"visual-check"
"native-rust-tests" "native-build" "package-lint" "diff-check")

View File

@ -50,7 +50,8 @@
(ert-deftest ebox-facade-loads-internal-model-modules ()
"The public ebox facade should load all internal foundation modules."
(require 'ebox)
(dolist (feature '(ebox-cache ebox-source ebox-style ebox-tree ebox-measure
(dolist (feature '(ebox-cache ebox-source ebox-state-contract
ebox-style ebox-tree ebox-measure
ebox-layout-config
ebox-node-factory
ebox-fragment ebox-layout ebox-flex ebox-grid
@ -129,7 +130,8 @@
(should (commandp 'ebox-byte-compile))
(should
(equal ebox--compile-sources
'("ebox-cache.el" "ebox-source.el" "ebox-font.el" "ebox-style.el"
'("ebox-cache.el" "ebox-source.el" "ebox-state-contract.el"
"ebox-font.el" "ebox-style.el"
"ebox-layout-config.el"
"ebox-node-factory.el"
"ebox-child-range.el"

View File

@ -0,0 +1,261 @@
;;; ebox-state-contract-tests.el --- M2a state ownership contracts -*- lexical-binding: t; -*-
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'ebox)
(require 'ebox-state-contract)
(require 'ebox-fixtures)
(defconst ebox-state-contract-test--plan-expectations
'((runtime-generation-indexes
(:source-index ebox-source-index/records
ebox-source-index/handle-records ebox-source-index/host-ref-table
:root-node :node-table :parent-table :runtime-type-count-table
:range-ref-table :selector-id-table :selector-class-table
:selector-type-table)
generation-fact ebox candidate-construction-only
immutable-generation-token
discard-candidate source-and-runtime-index-rebuild
generation-replacement)
(region-generation-indexes
(:region-id-set :region-node-table :region-box-count-table
:region-box-table :layout-snapshots)
generation-fact ebox candidate-layout-materialization-only
immutable-generation-token
discard-candidate layout-and-region-index-rebuild
generation-replacement)
(scroll-membership (:scroll-region-ids)
generation-fact ebox candidate-layout-materialization-only
immutable-generation-token
discard-candidate layout-scroll-membership-rebuild
generation-replacement)
(scroll-runtime-authority
(:scroll-state-table :scroll-offset :scroll-window
ebox--scroll-global-state ebox--smooth-scroll-state-table
ebox--scroll-idle-prefetch-timers)
generation-bound-mutable-authority ebox-scroll
stable-scroll-id-and-generation-token required
participant-journal-restores-prior-authority
not-rebuildable-from-cache cancel-timers-and-retire-generation)
(native-runtime-authority
(:native-sync-session :native-sync-pending :native-session
ebox-native-reflow-preparation ebox-native-reflow-session)
generation-bound-mutable-authority ebox-native
native-candidate-confirm-or-abort required
abort-candidate-and-keep-confirmed-session
not-rebuildable-from-cache release-losing-session)
(tp-client-state-custody (tp-surface-client-state)
tp-storage-custody tp opaque-ebox-generation-correlation-only
opaque-correlation tp-restores-client-state
ebox-generation-remains-source-of-truth tp-surface-unmount)
(buffer-render-state-mirror (ebox--buffer-render-state-table)
compatibility-mirror ebox-compatibility
project-from-committed-generation committed-generation-token
participant-restores-prior-projection
project-buffer-mirror-from-committed-states remove-buffer-entry)
(region-box-lookup-mirror (ebox--region-box-table)
compatibility-mirror ebox-compatibility
project-from-committed-generation committed-generation-token
participant-restores-prior-projection
project-region-mirror-from-generation-indexes
remove-retired-generation-entries)
(derived-caches
(ebox--char-width-cache ebox--face-height-width-cache
ebox--display-signature-cache ebox--render-cache-signature-cache)
disposable-cache ebox-cache cache-fill-and-evict
cache-key-or-display-signature
discard recompute-with-identical-semantic-result
bounded-eviction-or-clear))
"Exact M2a section 7.2 lifecycle expectations used by the E1 gate.")
(ert-deftest ebox-state-contract-inventory-is-complete-and-closed ()
"Every retained-state family has one complete closed classification."
(let* ((inventory (ebox-state-contract-validate))
(ids (mapcar (lambda (record) (plist-get record :id)) inventory)))
(should (= (length inventory) 9))
(should (= (length ids) (length (delete-dups (copy-sequence ids)))))
(dolist (record inventory)
(dolist (field ebox-state-contract-required-fields)
(should (plist-get record field)))
(should (memq (plist-get record :category)
ebox-state-contract-categories)))
(should
(equal
ids
'(runtime-generation-indexes region-generation-indexes
scroll-membership scroll-runtime-authority native-runtime-authority
tp-client-state-custody buffer-render-state-mirror
region-box-lookup-mirror derived-caches)))))
(ert-deftest ebox-state-contract-inventory-matches-plan-table-exactly ()
"Every plan-named storage family keeps its exact lifecycle classification."
(dolist (expected ebox-state-contract-test--plan-expectations)
(let ((record (ebox-state-contract-record (nth 0 expected))))
(should (equal (plist-get record :storage) (nth 1 expected)))
(should (eq (plist-get record :category) (nth 2 expected)))
(should (eq (plist-get record :owner) (nth 3 expected)))
(should (eq (plist-get record :mutation-api) (nth 4 expected)))
(should (eq (plist-get record :generation-binding) (nth 5 expected)))
(should (eq (plist-get record :rollback) (nth 6 expected)))
(should (eq (plist-get record :rebuild-proof) (nth 7 expected)))
(should (eq (plist-get record :cleanup) (nth 8 expected))))))
(ert-deftest ebox-state-contract-inventory-is-detached ()
"Callers cannot mutate the retained package inventory."
(let ((copy (ebox-state-contract-inventory)))
(setf (plist-get (car copy) :category) 'disposable-cache)
(should (eq (plist-get (ebox-state-contract-record
'runtime-generation-indexes)
:category)
'generation-fact))))
(ert-deftest ebox-state-contract-never-classifies-live-authority-as-cache ()
"Scroll and native handles retain generation-bound mutable authority."
(dolist (id '(scroll-runtime-authority native-runtime-authority))
(let ((record (ebox-state-contract-record id)))
(should (eq (plist-get record :category)
'generation-bound-mutable-authority))
(should (eq (plist-get record :generation-binding) 'required))
(should (eq (plist-get record :rebuild-proof)
'not-rebuildable-from-cache)))))
(ert-deftest ebox-state-contract-tp-custody-is-opaque ()
"The inventory separates current whole-state custody from its M2a target."
(let ((record (ebox-state-contract-record 'tp-client-state-custody)))
(should (eq (plist-get record :owner) 'tp))
(should (eq (plist-get record :current-contract)
'entire-ebox-state-plist))
(should (eq (plist-get record :target-contract)
'opaque-generation-correlation-only))
(should (eq (plist-get record :mutation-api)
'opaque-ebox-generation-correlation-only))
(should (eq (plist-get record :rebuild-proof)
'ebox-generation-remains-source-of-truth))))
(ert-deftest ebox-state-contract-rebuilds-current-compatibility-mirrors ()
"Committed client state rebuilds the current buffer and region mirrors."
(let ((buffer (generate-new-buffer " *ebox-m2a-e1-mirror*")))
(unwind-protect
(let* ((_rendered
(ebox-render-to-buffer
buffer
(ebox-test-column
(ebox-test-box :key 'first (ebox-test-text "first"))
(ebox-test-box :key 'second (ebox-test-text "second")))))
(surface (ebox-surface--live-buffer-surface buffer))
(state (tp-surface-client-state surface))
(before-text
(with-current-buffer buffer
(buffer-substring (point-min) (point-max))))
(before-revision (tp-surface-revision surface))
(entries (list (cons buffer state)))
(rebuilt
(ebox-state-contract-rebuild-compatibility-mirrors entries))
(report
(ebox-state-contract-probe-compatibility-mirrors
entries ebox--buffer-render-state-table
ebox--region-box-table)))
(should (plist-get report :consistent-p))
(should-not (plist-get report :buffer-differences))
(should-not (plist-get report :region-differences))
(should-not (eq (plist-get rebuilt :buffer-table)
ebox--buffer-render-state-table))
(should-not (eq (plist-get rebuilt :region-table)
ebox--region-box-table))
(should (eq (gethash buffer (plist-get rebuilt :buffer-table))
state))
(should
(equal (hash-table-count (plist-get rebuilt :region-table))
(hash-table-count (plist-get state :region-box-table))))
(should (eq (tp-surface-client-state surface) state))
(should (= (tp-surface-revision surface) before-revision))
(should
(equal (with-current-buffer buffer
(buffer-substring (point-min) (point-max)))
before-text)))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-state-contract-probe-detects-mirror-drift-without-mutation ()
"The read-only probe reports stale compatibility entries."
(let* ((buffer (generate-new-buffer " *ebox-m2a-e1-drift*"))
(region-id 'ebox/m2a-e1-region)
(box (list :node-id 'ebox/m2a-e1-box))
(regions (make-hash-table :test #'equal))
(state (list :region-box-table regions))
(entries (list (cons buffer state)))
(buffer-mirror (make-hash-table :test #'eq))
(region-mirror (make-hash-table :test #'equal))
(stale (list :node-id 'stale)))
(unwind-protect
(progn
(puthash region-id box regions)
(puthash buffer state buffer-mirror)
(puthash region-id stale region-mirror)
(let ((report
(ebox-state-contract-probe-compatibility-mirrors
entries buffer-mirror region-mirror)))
(should-not (plist-get report :consistent-p))
(should (equal (plist-get report :region-differences)
(list region-id)))
(should (eq (gethash region-id region-mirror) stale))))
(when (buffer-live-p buffer)
(kill-buffer buffer)))))
(ert-deftest ebox-state-contract-rejects-cross-generation-region-collision ()
"Two committed generations cannot project different boxes for one region."
(let ((first-buffer (generate-new-buffer " *ebox-m2a-e1-first*"))
(second-buffer (generate-new-buffer " *ebox-m2a-e1-second*"))
(first-regions (make-hash-table :test #'equal))
(second-regions (make-hash-table :test #'equal)))
(unwind-protect
(progn
(puthash 'shared (list :node-id 'first) first-regions)
(puthash 'shared (list :node-id 'second) second-regions)
(should-error
(ebox-state-contract-rebuild-compatibility-mirrors
(list (cons first-buffer (list :region-box-table first-regions))
(cons second-buffer (list :region-box-table second-regions))))
:type 'ebox-state-contract-error))
(when (buffer-live-p first-buffer) (kill-buffer first-buffer))
(when (buffer-live-p second-buffer) (kill-buffer second-buffer)))))
(ert-deftest ebox-state-contract-probe-work-is-bounded-by-projection-size ()
"The E1 probe performs bounded table work and never traverses a node tree."
(let (buffers entries)
(unwind-protect
(progn
(dotimes (buffer-index 64)
(let ((buffer
(generate-new-buffer
(format " *ebox-m2a-e1-perf-%d*" buffer-index)))
(regions (make-hash-table :test #'equal)))
(push buffer buffers)
(dotimes (region-index 16)
(puthash (cons buffer-index region-index)
(list :node-id (cons buffer-index region-index))
regions))
(push (cons buffer (list :region-box-table regions)) entries)))
(setq entries (nreverse entries))
(let* ((rebuilt
(ebox-state-contract-rebuild-compatibility-mirrors entries))
(report
(cl-letf (((symbol-function 'ebox-tree-map)
(lambda (&rest _arguments)
(error "E1 probe traversed the node tree"))))
(ebox-state-contract-probe-compatibility-mirrors
entries (plist-get rebuilt :buffer-table)
(plist-get rebuilt :region-table)))))
(should (plist-get report :consistent-p))
(should (= (plist-get report :entry-count) 64))
(should (= (plist-get report :projected-region-count) 1024))
(should (= (plist-get report :buffer-comparisons) 64))
(should (= (plist-get report :region-comparisons) 1024))))
(dolist (buffer buffers)
(when (buffer-live-p buffer) (kill-buffer buffer))))))
(provide 'ebox-state-contract-tests)
;;; ebox-state-contract-tests.el ends here