diff --git a/docs/architecture.en.md b/docs/architecture.en.md index 1587ac1..4856b9f 100644 --- a/docs/architecture.en.md +++ b/docs/architecture.en.md @@ -64,7 +64,7 @@ Properties must be complete before the first child. A child can be a string, a n (text :font-weight 'bold "Hello") (text :color "#687386" - (expr :value (if ready "Ready" "Waiting"))))) + (expr (if ready "Ready" "Waiting"))))) ``` An attribute appearing after a child is invalid because the two regions may @@ -77,18 +77,17 @@ not be interleaved. The rule is simple: 1. Structural View positions are not quoted. This includes `etaf-view`, Hosts, Component calls, children, slot forms, and static Component styles. -2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr :value`, `:setup`, Context values, Behavior constructors, and Actions. +2. Elisp expression positions follow normal Elisp evaluation. This includes attribute values, `:key`, `:on-*`, `:use`, `expr`, `:setup`, Context values, Behavior constructors, and Actions. ```elisp (etaf-view (text :color (if dark "#F4F6FB" "#1F2328") - (expr :value label))) + (expr label))) (etaf-view (column (expr - :value (when open (etaf-view (text :font-weight 'bold "Details")))))) @@ -103,30 +102,37 @@ Attribute values do not need an `expr` wrapper. `expr` exists only because the c `expr` accepts exactly one property and no children: ```elisp -(expr :value ELISP-EXPRESSION) +(expr ELISP-EXPRESSION) ``` -It evaluates the expression, then accepts a string, View, sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other Elisp forms remain ordinary Elisp inside `:value`. +It evaluates the expression, then accepts a string, typed View, proper typed +View sequence, or `nil`. It creates no Ebox wrapper, identity, lifecycle, +watcher, or effect. `if`, `when`, `cond`, `let`, `mapcar`, `cl-loop`, and other +Elisp forms remain ordinary Elisp inside the form. The current core has no file-facing `.etaf` pair loader. `etaf-define-component` is the structure/style/behavior unit: its View defines structure, `:styles` owns presentation rules, and `:setup` owns retained state, events, and lifecycle behavior. A future `.etaf` single-file component format belongs in a compiler layer that lowers into this same public View and Component contract; it is not a second runtime grammar. ## 4. Components -The public definition macro has exactly three keywords: +The public definition macro has four keywords. Choose exactly one frontend; +the other two clauses are optional: -```elisp +```text (etaf-define-component NAME (&key PROPS) DOCSTRING? + :setup OPAQUE-STATE-FORM :view VIEW :styles (styles RULE...)) (etaf-define-component NAME (&key PROPS) DOCSTRING? - :setup SETUP + :setup OPAQUE-STATE-FORM + :render ORDINARY-ELISP :styles (styles RULE...)) ``` -`:view` and `:setup` are mutually exclusive. `:styles` is optional and may appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection. +`:view` and `:render` are mutually exclusive and exactly one is required. +`:setup` and `:styles` are optional and may each appear once. Props are the only declared business inputs; ordinary trailing children and named slots are normalized separately into the Component's slot collection. The Component definition is the current structure/style/behavior boundary. Keep dynamic state, Action callbacks, and lifecycle work in `:setup`; keep static presentation in `:styles`. A future `.etaf` SFC compiler may produce these definitions, but the Runtime does not load `.etaf` files directly. @@ -138,31 +144,34 @@ The Component definition is the current structure/style/behavior boundary. Keep :view (text :font-weight 'bold - (expr :value label))) + (expr label))) ``` ```elisp (etaf-define-component disclosure (&key title) "Render a retained disclosure." :setup - (let ((open (etaf-ref nil))) - (lambda () - (etaf-view - (column - (text - :role 'button - :on-press - (lambda () - (setf (etaf-value open) - (not (etaf-value open)))) - (expr :value (if (etaf-value open) "Hide" "Show"))) - (expr - :value - (when (etaf-value open) - (etaf-view (text (expr :value title)))))))))) + (etaf-ref nil) + :view + (column + (text + :role 'button + :on-press + (let ((open (etaf-state))) + (lambda () + (setf (etaf-value open) + (not (etaf-value open))))) + (expr (if (etaf-value (etaf-state)) "Hide" "Show"))) + (expr + (when (etaf-value (etaf-state)) + (etaf-view (text (expr title))))))) ``` -`:setup` runs once for a retained Component instance and must return a zero-argument render function. Re-render reads current props and refs without rerunning setup. Setup is the owner for local refs, computed values, watches, Effects, and cleanup registration. +`:setup` runs once for a retained Component instance and returns one opaque state +value. `etaf-state` returns that exact value during `:view` or `:render`. +Re-render reads current props and state without rerunning setup. Setup is the +owner for local refs, computed values, watches, Effects, and cleanup +registration; it never returns a render function. `:key` is stable identity metadata, not a business prop. On a Component call it selects the retained Component instance within the sibling scope; on a Host it is forwarded as the Ebox node key. If a render candidate fails, the Runtime restores the previous instance, handlers, behaviors, and buffer. @@ -296,6 +305,19 @@ Use `etaf-action-define` and `etaf-dispatch` when the mutation is named and shar `etaf-behavior-create` accepts the reserved `:install` attribute for an optional zero-argument installer. The installer may return a cleanup function; `etaf-current-behavior-context` exposes the current Runtime, structural path, and Host props while it runs. Installer state is disposed when the Behavior is replaced or its owner is unmounted. +Interaction composition is deterministic. An explicit Host `:on-*` callback +runs first, followed by Behavior callbacks in declaration order; an error +short-circuits the remaining callbacks. For non-event attributes the explicit +Host value wins, otherwise the first declaring Behavior wins. Behavior names +must be unique on one Host before any installer runs. Stable installer identity +is reused, and every installed cleanup runs exactly once. Events dispatch only +to the exact Host reference: ETAF has no capture or bubble phase. + +Action names should be application- or feature-prefixed symbols. Duplicate +registration is an error by default. `etaf-action-redefine-run` is the explicit +authoring/reload boundary; replacement affects future name-based dispatch and +does not flush a mounted Runtime. + Runtime events are dispatched through `etaf-dispatch-event`, and focus/hit testing use public Ebox Host-reference queries through `etaf-activate`, `etaf-focus`, `etaf-focus-next`, `etaf-host-ref-bounds`, and `etaf-host-ref-position`. ## 9. Context, Theme, Data, and Resource @@ -310,14 +332,13 @@ Context is an inherited Component Scope environment: :setup (let ((service (etaf-ref "demo-service"))) (etaf-provide 'service service) - (lambda () (etaf-view (slot))))) + service) + :view (slot)) (etaf-define-component service-consumer () "Read the inherited service." - :setup - (let ((service (etaf-inject 'service nil t))) - (lambda () - (etaf-view (text (expr :value (etaf-value service))))))) + :setup (etaf-inject 'service nil t) + :view (text (expr (etaf-value (etaf-state))))) (etaf-view (service-provider (service-consumer))) ``` @@ -328,10 +349,9 @@ Keys are stable ordinary symbols. The nearest ancestor wins; a missing required (etaf-define-component themed-shell () "Provide default text colors to a subtree." :setup - (progn - (etaf-theme-provide - '(:color "#F4F6FB" :bgcolor "#202634")) - (lambda () (etaf-view (slot))))) + (etaf-theme-provide + '(:color "#F4F6FB" :bgcolor "#202634")) + :view (slot)) ``` Palette resolution remains a Theme concern, not a UI catalog concern. Core diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 6c4acc9..5164d24 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -64,7 +64,7 @@ ATTRIBUTE = :KEY VALUE (text :font-weight 'bold "Hello") (text :color "#687386" - (expr :value (if ready "Ready" "Waiting"))))) + (expr (if ready "Ready" "Waiting"))))) ``` 属性出现在子节点之后时,属性区和子节点区被交错,属于非法结构。 @@ -76,18 +76,17 @@ ATTRIBUTE = :KEY VALUE 规则只有两条: 1. 结构性 View 位置不需要 quote,包括 `etaf-view`、Host、Component 调用、子节点、slot 和静态 Component styles。 -2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr :value`、`:setup`、Context 值、Behavior 构造器和 Action。 +2. Elisp 表达式位置遵循普通 Elisp 求值,包括属性值、`:key`、`:on-*`、`:use`、`expr`、`:setup`、Context 值、Behavior 构造器和 Action。 ```elisp (etaf-view (text :color (if dark "#F4F6FB" "#1F2328") - (expr :value label))) + (expr label))) (etaf-view (column (expr - :value (when open (etaf-view (text :font-weight 'bold "Details")))))) @@ -102,30 +101,34 @@ ATTRIBUTE = :KEY VALUE `expr` 只接受一个属性且不能有子节点: ```elisp -(expr :value ELISP-EXPRESSION) +(expr ELISP-EXPRESSION) ``` -它执行表达式,然后接受字符串、View、View 序列或 `nil`。它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、`let`、`mapcar`、`cl-loop` 等 Elisp 形式仍然只是 `:value` 中的普通 Elisp。 +它执行表达式,然后接受字符串、typed View、typed View proper sequence 或 `nil`。 +它不创建 Ebox wrapper、identity、生命周期、watch 或 effect。`if`、`when`、`cond`、 +`let`、`mapcar`、`cl-loop` 等仍是 form 中的普通 Elisp。 当前 core 没有面向文件的 `.etaf` pair loader。`etaf-define-component` 才是结构/样式/行为单元:View 定义结构,`:styles` 负责 presentation,`:setup` 负责 retained state、事件和生命周期行为。未来的 `.etaf` SFC 属于把结果 lowering 到同一套公共 View/Component 契约的 compiler layer,而不是第二套 Runtime 语法。 ## 4. Component -公共定义宏只有三个关键字: +公共定义宏有四个关键字。两个 frontend 必须二选一,另外两个 clause 可选: -```elisp +```text (etaf-define-component NAME (&key PROPS) DOCSTRING? + :setup OPAQUE-STATE-FORM :view VIEW :styles (styles RULE...)) (etaf-define-component NAME (&key PROPS) DOCSTRING? - :setup SETUP + :setup OPAQUE-STATE-FORM + :render ORDINARY-ELISP :styles (styles RULE...)) ``` -`:view` 和 `:setup` 互斥;`:styles` 可选且最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。 +`:view` 和 `:render` 互斥且必须恰好出现一个;`:setup` 与 `:styles` 可选且各最多出现一次。Props 是唯一需要声明的业务输入;普通尾部子节点和命名 slot 会被规范化为 Component 的 slot 集合。 Component definition 是当前结构/样式/行为边界:动态状态、Action callback 和生命周期工作放进 `:setup`,静态 presentation 放进 `:styles`。未来 `.etaf` SFC compiler 可以生成这些 definition,但 Runtime 不会直接加载 `.etaf` 文件。 @@ -137,31 +140,33 @@ Component definition 是当前结构/样式/行为边界:动态状态、Action :view (text :font-weight 'bold - (expr :value label))) + (expr label))) ``` ```elisp (etaf-define-component disclosure (&key title) "Render a retained disclosure." :setup - (let ((open (etaf-ref nil))) - (lambda () - (etaf-view - (column - (text - :role 'button - :on-press - (lambda () - (setf (etaf-value open) - (not (etaf-value open)))) - (expr :value (if (etaf-value open) "Hide" "Show"))) - (expr - :value - (when (etaf-value open) - (etaf-view (text (expr :value title)))))))))) + (etaf-ref nil) + :view + (column + (text + :role 'button + :on-press + (let ((open (etaf-state))) + (lambda () + (setf (etaf-value open) + (not (etaf-value open))))) + (expr (if (etaf-value (etaf-state)) "Hide" "Show"))) + (expr + (when (etaf-value (etaf-state)) + (etaf-view (text (expr title))))))) ``` -`:setup` 对一个 retained Component instance 只运行一次,并且必须返回零参数 render 函数。重新渲染读取当前 props 和 ref,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup 的创建。 +`:setup` 对一个 retained Component instance 只运行一次,返回一个 opaque 状态值。 +`:view` 或 `:render` 中用 `etaf-state` 取得这个准确值。重新渲染读取当前 props 和 +state,不重新运行 setup。setup 负责局部 ref、computed、watch、Effect 和 cleanup, +不返回 render 函数。 `:key` 是稳定的 identity metadata,不是业务 prop。放在 Component 调用上时,它选择同级作用域内要保留的 Component instance;放在 Host 上时,它会作为 Ebox node key 向下传递。候选渲染失败时,Runtime 恢复旧 instance、handlers、Behaviors 和 buffer。 @@ -295,6 +300,16 @@ Behavior = 安装多个非视觉能力的可复用 bundle `etaf-behavior-create` 接受保留的 `:install` 属性,用于可选的零参数 installer。Installer 可以返回 cleanup;运行期间可通过 `etaf-current-behavior-context` 读取当前 Runtime、结构路径和 Host props。Behavior 被替换或 owner 卸载时,installer 状态会被释放。 +交互组合是确定的:显式 Host `:on-*` callback 先运行,然后按声明顺序运行 Behavior +callback;任一错误会 short-circuit 后续 callback。非事件属性由显式 Host 值优先, +否则第一个声明该属性的 Behavior 获胜(first-wins)。同一 Host 的 Behavior name 必须在任何 +installer 运行前保持唯一。稳定 installer identity 会复用,每个已安装 cleanup +exactly-once。事件只 dispatch 给准确 Host ref;ETAF 没有 capture 或 bubble 阶段。 + +Action name 应使用 application/feature-prefixed symbol。重复注册默认报错。 +`etaf-action-redefine-run` 是显式 authoring/reload 边界;替换只影响未来按 name 的 +dispatch,不会 flush 已挂载 Runtime。 + Runtime 事件通过 `etaf-dispatch-event` 进入;命中测试和 focus 通过 Ebox Host 引用查询,并由 `etaf-activate`、`etaf-focus`、`etaf-focus-next`、`etaf-host-ref-bounds` 和 `etaf-host-ref-position` 提供公共入口。 ## 9. Context、Theme、Data 与 Resource @@ -309,14 +324,13 @@ Context 是继承的 Component Scope 环境: :setup (let ((service (etaf-ref "demo-service"))) (etaf-provide 'service service) - (lambda () (etaf-view (slot))))) + service) + :view (slot)) (etaf-define-component service-consumer () "Read the inherited service." - :setup - (let ((service (etaf-inject 'service nil t))) - (lambda () - (etaf-view (text (expr :value (etaf-value service))))))) + :setup (etaf-inject 'service nil t) + :view (text (expr (etaf-value (etaf-state))))) (etaf-view (service-provider (service-consumer))) ``` @@ -327,10 +341,9 @@ key 使用稳定的普通 symbol。最近的祖先优先,缺失的 required ke (etaf-define-component themed-shell () "Provide default text colors to a subtree." :setup - (progn - (etaf-theme-provide - '(:color "#F4F6FB" :bgcolor "#202634")) - (lambda () (etaf-view (slot))))) + (etaf-theme-provide + '(:color "#F4F6FB" :bgcolor "#202634")) + :view (slot)) ``` Palette 解析属于 Theme,而不是 UI 目录。core 提供 diff --git a/docs/implementation-plan.en.md b/docs/implementation-plan.en.md index fe6c545..73d742b 100644 --- a/docs/implementation-plan.en.md +++ b/docs/implementation-plan.en.md @@ -18,7 +18,7 @@ The repository is complete for the agreed unified architecture when the mandator | Milestone | Delivered responsibility | Evidence | | --- | --- | --- | -| P0 grammar | Unified View shape, property-first parsing, `etaf-view`, `expr :value`, core Hosts, aliases | `tests/etaf-tests.el` structural and syntax tests | +| P0 grammar | Unified View shape, property-first parsing, `etaf-view`, `(expr FORM)`, core Hosts, aliases | `tests/etaf-tests.el` structural and syntax tests | | P1 Components | `:view`, `:setup`, props, default/named slots, retained instances, lifecycle, `:key`, raw Ebox escape | Component, slot, mount, prop-update, raw-node, and rollback tests | | P2 Runtime | refs, computed, effects, watches, Scope cleanup, Context, Theme, Behaviors, events, focus, Actions | Mounted event/focus tests, reactive failure rollback, cleanup tests | | P3 presentation | scoped styles, selector matching, Theme precedence, inline text runs, Resource and error boundary | Style, Theme, text-surface, Resource, and error tests | @@ -55,16 +55,19 @@ Do not split a file merely to create a shorter name. Split only when a stable re - The only structural form is `(NAME :PROPERTY VALUE ... CHILD ...)`. - All properties precede children. - Attribute values are ordinary Elisp expressions. -- The only child computation bridge is `(expr :value ELISP-EXPRESSION)`. +- The only child computation bridge is `(expr ELISP-EXPRESSION)`; its result + may be text, a typed View/proper typed View sequence, or `nil`. - A dynamic View returned by Elisp is constructed with `(etaf-view ...)` inside `expr`; quoted structural data is never executed. - The six visible Host forms are `text`, `box`, `row`, `column`, `flex`, and `grid`; `fragment`, `slot`, and `expr` are transparent framework structure. - There is no raw Ebox View escape hatch. Framework integrations use Ebox's evaluated typed-constructor port outside the author DSL. ### 4.2 Component and slots -- `etaf-define-component` accepts only `:view`, `:setup`, and optional `:styles`. -- `:view` and `:setup` are mutually exclusive. -- `:setup` runs once per retained identity and returns a zero-argument render function. +- `etaf-define-component` chooses exactly one of `:view` or `:render`; `:setup` + and `:styles` are optional. +- `:view` and `:render` are mutually exclusive frontends. +- `:setup` runs once per retained identity and returns opaque state read with + `etaf-state` during either frontend. - Props update render without rerunning setup. - Trailing children are `slots.default`; named slot input uses `(slot :name 'NAME CHILD...)`. - Default outlet shorthand is `(slot)` or `(slot FALLBACK...)`. diff --git a/docs/implementation-plan.zh.md b/docs/implementation-plan.zh.md index 3b8b17e..448fad0 100644 --- a/docs/implementation-plan.zh.md +++ b/docs/implementation-plan.zh.md @@ -18,7 +18,7 @@ | 里程碑 | 已交付职责 | 证据 | | --- | --- | --- | -| P0 grammar | 统一 View 形状、属性优先解析、`etaf-view`、`expr :value`、核心 Host、alias | `tests/etaf-tests.el` 结构和语法测试 | +| P0 grammar | 统一 View 形状、属性优先解析、`etaf-view`、`(expr FORM)`、核心 Host、alias | `tests/etaf-tests.el` 结构和语法测试 | | P1 Component | `:view`、`:setup`、props、默认/命名 slot、retained instance、生命周期、`:key`、raw Ebox 出口 | Component、slot、mount、prop 更新、raw node、rollback 测试 | | P2 Runtime | ref、computed、effect、watch、Scope cleanup、Context、Theme、Behavior、事件、focus、Action | 挂载事件/focus、响应式失败回滚和 cleanup 测试 | | P3 presentation | 作用域样式、selector、Theme 优先级、inline text runs、Resource 和 error boundary | 样式、Theme、文本 surface、Resource、error 测试 | @@ -55,16 +55,19 @@ - 唯一结构形式是 `(NAME :PROPERTY VALUE ... CHILD ...)`。 - 所有属性必须先于所有子节点。 - 属性值是普通 Elisp 表达式。 -- 唯一的子节点计算桥接是 `(expr :value ELISP-EXPRESSION)`。 +- 唯一的子节点计算桥接是 `(expr ELISP-EXPRESSION)`;结果可以是文本、typed View、 + typed View proper sequence 或 `nil`。 - Elisp 返回动态 View 时,在 `expr` 中用 `(etaf-view ...)` 构造;被 quote 的结构数据永远不会被执行。 - 六个可见 Host form 是 `text`、`box`、`row`、`column`、`flex`、`grid`;`fragment`、`slot`、`expr` 是透明的框架结构。 - View 作者 DSL 不提供 raw Ebox 逃生口;框架集成在作者 DSL 之外使用 Ebox evaluated typed-constructor 端口。 ### 4.2 Component 与 slot -- `etaf-define-component` 只接受 `:view`、`:setup` 和可选 `:styles`。 -- `:view` 与 `:setup` 互斥。 -- `:setup` 对每个 retained identity 只运行一次,返回零参数 render 函数。 +- `etaf-define-component` 必须在 `:view` 与 `:render` 中恰好选择一个;`:setup` 与 + `:styles` 可选。 +- `:view` 与 `:render` 是互斥 frontend。 +- `:setup` 对每个 retained identity 只运行一次,返回 opaque 状态,并由两个 frontend + 中的 `etaf-state` 读取。 - Props 更新只重新 render,不重新运行 setup。 - 尾部 children 是 `slots.default`;命名 slot 输入使用 `(slot :name 'NAME CHILD...)`。 - 默认 outlet 简写是 `(slot)` 或 `(slot FALLBACK...)`。 diff --git a/docs/proposals/component-definition.zh.md b/docs/proposals/component-definition.zh.md index 0fe59e6..353210b 100644 --- a/docs/proposals/component-definition.zh.md +++ b/docs/proposals/component-definition.zh.md @@ -1,10 +1,10 @@ -# ETAF Component 定义:目标设计 +# ETAF Component 定义:历史目标设计 -> 状态:已完成架构审查的目标设计,尚未实现。 +> **SUPERSEDED / historical:本文不再是当前公共合同。** > -> 本文只定义目标公共契约,不代表当前版本已经支持这些写法。当前实现差距见 -> “实现状态与迁移”一节。审查通过并完成代码、测试、GUI 与性能门禁后,稳定内容 -> 才能进入用户指南。 +> 当前公共合同以 [docs/architecture.zh.md](../architecture.zh.md) 与 +> [docs/user-guide.zh.md](../user-guide.zh.md) 为准。本文保留为设计决策历史,不再以 +> 创建时的“当前状态”表覆盖已经交付的实现。 本文只拥有 **Component 作者语法和作者可见语义**。属性 owner、跨包依赖、 transaction 权限、Ebox/TP/Rust 边界仍以 @@ -1016,11 +1016,12 @@ grammar,也不要求 IR 保存 frontend provenance。 | topology compiler | 是 | 否 | | Runtime/identity/lifecycle | 同一套 | 同一套 | -## 21. 实现状态与迁移 +## 21. 历史实现快照(非当前状态) -截至本文创建时,当前代码的真实状态是: +下表只记录本文创建时的历史差距,不能用于判断当前版本。当前实现与迁移状态见 +[implementation-plan.zh.md](../implementation-plan.zh.md)。 -| 能力 | 当前状态 | 目标动作 | +| 能力 | 历史状态 | 当时目标动作 | | --- | --- | --- | | `etaf-define-component :view` | 已实现 | 按新 DSL grammar 收敛。 | | `:setup` | 已实现,但返回零参数 render function | 删除旧语义;改为只返回 state。 | diff --git a/docs/proposals/module-boundaries.en.md b/docs/proposals/module-boundaries.en.md index 702c075..d0d3c6d 100644 --- a/docs/proposals/module-boundaries.en.md +++ b/docs/proposals/module-boundaries.en.md @@ -824,7 +824,10 @@ complete chain, for example: `etaf-component`: -- defines Component props, `:view`, `:setup`, and `:styles`; +- defines Component props, mutually exclusive `:view`/`:render` frontends, + optional `:setup` returning opaque state, the sole `etaf-state` accessor, + and `:styles`; +- defines the `etaf-node` code frontend at the same typed View ABI boundary; - does not schedule Runtime or call Ebox. `etaf-runtime`: diff --git a/docs/user-guide.en.md b/docs/user-guide.en.md index 56a36b1..2d06445 100644 --- a/docs/user-guide.en.md +++ b/docs/user-guide.en.md @@ -80,7 +80,7 @@ Attribute values are ordinary Elisp expressions. They do not need an extra `expr (text :color (if dark "#F4F6FB" "#1F2328") :background-color "#20242B" - (expr :value label)))) + (expr label)))) ``` The child region is structural. `expr` is the one explicit bridge for ordinary Elisp computation: @@ -88,14 +88,15 @@ The child region is structural. `expr` is the one explicit bridge for ordinary E ```elisp (etaf-view (column - (expr :value (if loading "Loading..." "Ready")) + (expr (if loading "Loading..." "Ready")) (expr - :value (when open (etaf-view (text :font-style 'italic "Details")))))) ``` -`expr` accepts exactly `:value` and no children. Its result can be a string, View, sequence, or `nil`. `if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp inside the value. +`expr` accepts exactly one ordinary Elisp form and no structural children. Its +result can be a string, typed View, proper typed View sequence, or `nil`. +`if`, `when`, `cond`, `let`, `mapcar`, and `cl-loop` remain normal Elisp. Quote has one ordinary Elisp meaning: @@ -134,7 +135,7 @@ The beginner form is a stateless `:view` Component: :view (text :font-weight 'bold - (expr :value label))) + (expr label))) (etaf-mount "*etaf-status*" @@ -154,8 +155,9 @@ The definition macro accepts only these keywords: | Keyword | Meaning | | --- | --- | -| `:view` | The stateless View producer; mutually exclusive with `:setup` | -| `:setup` | One-time Component initialization returning a zero-argument render function | +| `:view` | Declarative View frontend; mutually exclusive with `:render` | +| `:render` | Ordinary-Elisp frontend returning one typed View through `etaf-node` | +| `:setup` | Optional one-time initialization returning opaque state read with `etaf-state` | | `:styles` | Optional static scoped style declaration | There is no separate declaration block for children, slots, events, state, or variants. Props are declared with `(&key ...)`; children and slots are implicit content. @@ -173,18 +175,24 @@ Use `:setup` when the Component owns local state: (lambda () (message "%s mounted" title))) (etaf-on-unmounted (lambda () (message "%s unmounted" title))) - (lambda () - (etaf-view - (column - (text :font-weight 'bold (expr :value title)) - (text (expr :value (format "Count: %d" (etaf-value count)))) - (text - :role 'button - :on-press (lambda () (cl-incf (etaf-value count))) - "Increment")))))) + count) + :view + (column + (text :font-weight 'bold (expr title)) + (text (expr (format "Count: %d" (etaf-value (etaf-state))))) + (text + :role 'button + :on-press + (let ((count (etaf-state))) + (lambda () (cl-incf (etaf-value count)))) + "Increment"))) ``` -Setup runs once for the retained instance. Its returned render function runs on each update. `etaf-on-mounted`, `etaf-on-updated`, and `etaf-on-unmounted` register lifecycle callbacks for that Component instance. Scope disposal automatically stops reactive effects and cleanup. +Setup runs once for the retained instance and returns one opaque state value. +The selected `:view` or `:render` frontend runs on each update and reads that +exact value with `etaf-state`. `etaf-on-mounted`, `etaf-on-updated`, and +`etaf-on-unmounted` register lifecycle callbacks for that Component instance. +Scope disposal automatically stops reactive effects and cleanup. The small reactive API is: @@ -219,7 +227,7 @@ Trailing children are the anonymous/default slot: "Render a titled panel." :view (column - (text :font-weight 'bold (expr :value title)) + (text :font-weight 'bold (expr title)) (slot (text :color "#687386" "No content")))) (etaf-view @@ -236,7 +244,7 @@ Named slots use `:name` and must use a stable non-keyword symbol: :view (column (slot :name 'header - (text :font-weight 'bold (expr :value title))) + (text :font-weight 'bold (expr title))) (slot (text :color "#687386" "No body")))) (etaf-view @@ -248,6 +256,22 @@ Named slots use `:name` and must use a stable non-keyword symbol: The two default-slot shorthands are `(slot)` and `(slot FALLBACK...)`. The normalized spelling is `(slot :name 'default FALLBACK...)`. At a call site, ordinary children fill `default`; a named input uses `(slot :name 'header CHILD...)`. An explicit empty `(slot :name 'header)` suppresses the fallback. Strings, numbers, variables, and runtime expressions are not valid slot names. +`expr` may return a typed View or a proper sequence of typed Views at a +structural boundary. It never exposes or accepts ETAF's private structs. The +same Component can combine a keyed `:for`, a structural expression, and a +named footer slot: + +```elisp +(etaf-define-component etaf-docs-collection-card (&key items footer-view) + "Render keyed rows, one dynamic typed View, and a footer slot." + :view + (column + (row :for (item items) :key (car item) + (text (expr (cdr item)))) + (expr footer-view) + (slot :name 'footer (text "No footer")))) +``` + ## 7. Styles and themes Static Component styles use one declaration form: @@ -291,10 +315,9 @@ Theme is a Context convenience, not another runtime object: (etaf-define-component themed-shell () "Provide default text colors to a subtree." :setup - (progn - (etaf-theme-provide - '(:color "#F4F6FB" :bgcolor "#202634")) - (lambda () (etaf-view (slot))))) + (etaf-theme-provide + '(:color "#F4F6FB" :bgcolor "#202634")) + :view (slot)) ``` For a light/dark application palette, keep the semantic roles in one palette @@ -372,6 +395,18 @@ For a reusable installer, reserve `:install` for the cleanup-producing part of t The installer can call `etaf-current-behavior-context` when it needs the current Runtime or Host path. Replacing the Behavior runs the old cleanup before the new state becomes current. Behavior equality keeps function and reactive-value identity with `eq`; a newly-created installer closure is therefore a deliberate replacement, not an accidental reuse. The replacement is staged under the mounted resource registry and becomes authoritative only when its generation commits. +Composition rules are fixed: the Host callback runs before Behavior callbacks, +Behaviors follow declaration order, and a callback error short-circuits the +rest. For non-event attributes the Host wins, then the first Behavior wins. +Duplicate Behavior names fail before installation. Stable installers are reused +and each installed cleanup runs exactly once. Dispatch targets one exact Host; +there is no capture or bubble phase. + +Use application- or feature-prefixed Action names. Duplicate Action +registration is an error. During deliberate reload, wrap the replacement in +`etaf-action-redefine-run`; it changes future name-based dispatch without +flushing the mounted Runtime. + Focus and hit testing are Runtime operations: ```elisp @@ -394,15 +429,14 @@ Use Context for a dependency shared across component depth, not for ordinary pro :setup (let ((service (etaf-ref "demo-service"))) (etaf-provide 'service service) - (lambda () (etaf-view (slot))))) + service) + :view (slot)) (etaf-define-component service-label () "Read the inherited service." - :setup - (let ((service (etaf-inject 'service nil t))) - (lambda () - (etaf-view - (text (expr :value (format "Service: %s" (etaf-value service))))))) + :setup (etaf-inject 'service nil t) + :view + (text (expr (format "Service: %s" (etaf-value (etaf-state)))))) (etaf-mount "*etaf-context*" @@ -469,8 +503,9 @@ updates only when that identity enters or leaves the main selection, including when application code writes `etaf-data-selection` directly. The default DataGrid path uses these refs with keyed retained row owners, so a single-select change invalidates the old and new rows rather than the complete visible page. -Custom `:row-selected-p` and `:selected-key` contracts remain available when -selection is owned outside the Controller. +Custom `:row-selected-p` remains available when selection is owned outside the +Controller. Stable identity still comes from the required `:row-key`; there is +no second selection-key DataGrid prop. `etaf-data-controller` accepts `:item-key` for stable selected-row lookup. When created inside a Component setup, its internal effect Scope is owned by the @@ -662,7 +697,7 @@ Most applications need only `etaf-view`, `etaf-mount`, `etaf-define-component`, - Put every property before the first child. - Use `:font-weight 'bold`, not `:font-weight :bold`; the weight is an Elisp symbol value, not a property keyword. - Do not quote a structural View form. -- Use `expr :value` for `if`, `when`, `let`, `mapcar`, or a View returned by ordinary Elisp. +- Use `(expr FORM)` for `if`, `when`, `let`, `mapcar`, or a typed View returned by ordinary Elisp. - Use `(slot)` or `(slot FALLBACK...)` for the default outlet; use `:name 'header` for named slot content. - Keep writes out of rendering; use an event, Action, watch callback, or Effect. - Use `etaf-ui` Components for product controls; core Hosts are the structural foundation. diff --git a/docs/user-guide.zh.md b/docs/user-guide.zh.md index 0a33b17..665b83c 100644 --- a/docs/user-guide.zh.md +++ b/docs/user-guide.zh.md @@ -80,7 +80,7 @@ View 含有状态型 Component、响应式数据、事件或生命周期时, (text :color (if dark "#F4F6FB" "#1F2328") :background-color "#20242B" - (expr :value label)))) + (expr label)))) ``` 子节点区是结构语法。`expr` 是执行普通 Elisp 的唯一明确桥接: @@ -88,14 +88,15 @@ View 含有状态型 Component、响应式数据、事件或生命周期时, ```elisp (etaf-view (column - (expr :value (if loading "Loading..." "Ready")) + (expr (if loading "Loading..." "Ready")) (expr - :value (when open (etaf-view (text :font-style 'italic "Details")))))) ``` -`expr` 只接受 `:value`,不能有子节点。返回值可以是字符串、View、序列或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和 `cl-loop` 仍然是 value 中的普通 Elisp。 +`expr` 只接受一个普通 Elisp form,不能有结构子节点。返回值可以是字符串、typed +View、typed View proper sequence 或 `nil`。`if`、`when`、`cond`、`let`、`mapcar` 和 +`cl-loop` 仍是普通 Elisp。 quote 只有普通 Elisp 的含义: @@ -134,7 +135,7 @@ quote 只有普通 Elisp 的含义: :view (text :font-weight 'bold - (expr :value label))) + (expr label))) (etaf-mount "*etaf-status*" @@ -154,8 +155,9 @@ Component 的规范名称可以带 `etaf-` 前缀: | 关键字 | 作用 | | --- | --- | -| `:view` | 无状态 Component 的 View 生产者,与 `:setup` 互斥 | -| `:setup` | 一次性初始化,返回零参数 render 函数 | +| `:view` | 声明式 View frontend,与 `:render` 互斥 | +| `:render` | 普通 Elisp frontend,通过 `etaf-node` 返回一个 typed View | +| `:setup` | 可选的一次性初始化,返回由 `etaf-state` 读取的 opaque 状态 | | `:styles` | 可选的静态作用域样式声明 | 没有单独的 children、slot、event、state 或 variant 声明块。业务 props 通过 `(&key ...)` 声明,children 和 slot 是隐式内容。 @@ -173,18 +175,23 @@ Component 自己拥有状态时使用 `:setup`: (lambda () (message "%s mounted" title))) (etaf-on-unmounted (lambda () (message "%s unmounted" title))) - (lambda () - (etaf-view - (column - (text :font-weight 'bold (expr :value title)) - (text (expr :value (format "Count: %d" (etaf-value count)))) - (text - :role 'button - :on-press (lambda () (cl-incf (etaf-value count))) - "Increment")))))) + count) + :view + (column + (text :font-weight 'bold (expr title)) + (text (expr (format "Count: %d" (etaf-value (etaf-state))))) + (text + :role 'button + :on-press + (let ((count (etaf-state))) + (lambda () (cl-incf (etaf-value count)))) + "Increment"))) ``` -Setup 对 retained instance 只执行一次,返回的 render 函数在更新时重复运行。`etaf-on-mounted`、`etaf-on-updated` 和 `etaf-on-unmounted` 注册该 Component 的生命周期 callback。Scope 释放时会自动停止响应式 effect 并运行 cleanup。 +Setup 对 retained instance 只执行一次,返回一个 opaque 状态值。选定的 `:view` 或 +`:render` frontend 在更新时运行,并通过 `etaf-state` 读取这个准确值。 +`etaf-on-mounted`、`etaf-on-updated` 和 `etaf-on-unmounted` 注册该 Component 的生命周期 +callback。Scope 释放时会自动停止响应式 effect 并运行 cleanup。 响应式 API 只有一套模型: @@ -219,7 +226,7 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新 "Render a titled panel." :view (column - (text :font-weight 'bold (expr :value title)) + (text :font-weight 'bold (expr title)) (slot (text :color "#687386" "No content")))) (etaf-view @@ -236,7 +243,7 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新 :view (column (slot :name 'header - (text :font-weight 'bold (expr :value title))) + (text :font-weight 'bold (expr title))) (slot (text :color "#687386" "No body")))) (etaf-view @@ -248,6 +255,21 @@ Setup 对 retained instance 只执行一次,返回的 render 函数在更新 默认 slot 的两个用户简写是 `(slot)` 和 `(slot FALLBACK...)`。内部统一形式是 `(slot :name 'default FALLBACK...)`。调用处的普通子节点填充 `default`;命名内容写成 `(slot :name 'header CHILD...)`。显式空的 `(slot :name 'header)` 会抑制 fallback。字符串、数字、变量和运行时表达式都不是合法 slot name。 +在结构边界,`expr` 可以返回 typed View 或 typed View 的 proper sequence,但它不暴露、 +也不接受 ETAF 私有 struct。同一个 Component 可以组合 keyed `:for`、结构表达式和 +命名 footer slot: + +```elisp +(etaf-define-component etaf-docs-collection-card (&key items footer-view) + "Render keyed rows, one dynamic typed View, and a footer slot." + :view + (column + (row :for (item items) :key (car item) + (text (expr (cdr item)))) + (expr footer-view) + (slot :name 'footer (text "No footer")))) +``` + ## 7. 样式与 Theme 静态 Component 样式只有一种声明形式: @@ -290,10 +312,9 @@ Theme 是 Context 的便捷形式,不是另一个 Runtime 对象: (etaf-define-component themed-shell () "Provide default text colors to a subtree." :setup - (progn - (etaf-theme-provide - '(:color "#F4F6FB" :bgcolor "#202634")) - (lambda () (etaf-view (slot))))) + (etaf-theme-provide + '(:color "#F4F6FB" :bgcolor "#202634")) + :view (slot)) ``` 如果应用有亮/暗两套 palette,应把语义 role 集中放在一份 palette plist 中, @@ -369,6 +390,15 @@ Behavior 用来打包可复用的非视觉属性和 cleanup: Installer 需要 Runtime 或 Host path 时,可以调用 `etaf-current-behavior-context`。Behavior 被替换时,旧 cleanup 会在新状态成为当前状态前运行。Behavior equality 对 function 和 reactive value 使用 `eq`;因此新建的 installer closure 会被视为有意替换,而不是错误复用。替换状态先以 mounted resource registry 的 staged resource 保存,generation commit 后才成为 authority。 +组合规则固定:Host callback 先于 Behavior callback,Behavior 按声明顺序运行,callback +error 会 short-circuit 剩余 callback。非事件属性先由 Host 获胜,否则由第一个 +Behavior 获胜(first-wins)。重复 Behavior name 在 installer 前失败;稳定 installer 会复用,每个 +已安装 cleanup exactly-once。dispatch 只命中准确 Host,不存在 capture 或 bubble。 + +Action name 使用 application/feature-prefixed symbol;重复 Action 注册默认报错。显式 +reload 用 `etaf-action-redefine-run` 包住替换,它只改变未来按 name 的 dispatch,不会 +flush 已挂载 Runtime。 + Focus 和 hit testing 是 Runtime 操作: ```elisp @@ -391,15 +421,14 @@ Context 适合跨多层共享依赖,不适合普通 label: :setup (let ((service (etaf-ref "demo-service"))) (etaf-provide 'service service) - (lambda () (etaf-view (slot))))) + service) + :view (slot)) (etaf-define-component service-label () "Read the inherited service." - :setup - (let ((service (etaf-inject 'service nil t))) - (lambda () - (etaf-view - (text (expr :value (format "Service: %s" (etaf-value service))))))) + :setup (etaf-inject 'service nil t) + :view + (text (expr (format "Service: %s" (etaf-value (etaf-state)))))) (etaf-mount "*etaf-context*" @@ -465,8 +494,8 @@ Controller 通过 `etaf-data-items`、`etaf-data-status`、`etaf-data-error`、` identity 进入或离开主 selection 时它才会变化,应用直接写 `etaf-data-selection` 时也一样。DataGrid 默认把这些 ref 与 keyed retained row owner 配合使用,因此单选变化只会使旧行和新行失效,而不是整个可见页。 -selection 由 Controller 外部拥有时,仍可使用自定义 `:row-selected-p` 与 -`:selected-key` 契约。 +selection 由 Controller 外部拥有时,仍可使用自定义 `:row-selected-p`。稳定 identity +继续来自必选的 `:row-key`;DataGrid 没有第二个 selection-key prop。 `etaf-data-controller` 支持用于稳定选中行查找的 `:item-key`。在 Component setup 中创建时,它的内部 effect Scope 会自动归当前 Component Scope 所有;如果 @@ -650,7 +679,7 @@ observer,不修改任何函数。 - 所有属性必须放在第一个子节点之前。 - 使用 `:font-weight 'bold`,不要使用 `:font-weight :bold`;weight 是 Elisp symbol 值,不是属性 keyword。 - 结构性 View form 不要 quote。 -- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 View 时,使用 `expr :value`。 +- `if`、`when`、`let`、`mapcar` 或 Elisp 返回 typed View 时,使用 `(expr FORM)`。 - 默认 outlet 使用 `(slot)` 或 `(slot FALLBACK...)`;命名内容使用 `:name 'header`。 - 不要在 render 中写状态;使用事件、Action、watch callback 或 Effect。 - 产品级控件使用 `etaf-ui` Component;core Host 只是结构基础。 diff --git a/scripts/etaf-m0b-component-manifest.el b/scripts/etaf-m0b-component-manifest.el new file mode 100644 index 0000000..5f5985b --- /dev/null +++ b/scripts/etaf-m0b-component-manifest.el @@ -0,0 +1,111 @@ +;;; etaf-m0b-component-manifest.el --- Observed Component contract manifest -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; Emit a machine-readable M0b manifest that keeps Component business props, +;; forwarded Host attributes, and the single-root forwarding guarantee in +;; separate fields. The manifest is emitted only after a real mounted probe. + +;;; Code: + +(require 'cl-lib) +(require 'json) +(require 'etaf) + +(defconst etaf-m0b-component-manifest--dsl-name + 'etaf-m0b-manifest-dsl-card) + +(defconst etaf-m0b-component-manifest--render-name + 'etaf-m0b-manifest-render-card) + +(defun etaf-m0b-component-manifest--define-fixtures () + "Define the two manifest probe Components through the public authoring API." + (etaf-component-redefine-run + (lambda () + (eval + '(etaf-define-component etaf-m0b-manifest-dsl-card (&key title) + :view + (box :class "definition" (text (expr title))))) + (eval + '(etaf-define-component etaf-m0b-manifest-render-card (&key label) + :render + (etaf-node 'box (list :class "definition") + (list (etaf-node 'text nil (list label))))))))) + +(defun etaf-m0b-component-manifest--spec-props (name) + "Return declared business props for Component NAME." + (let ((spec (gethash name etaf--view-registry))) + (unless (etaf--component-spec-p spec) + (error "Manifest fixture is not a Component: %S" name)) + (mapcar #'symbol-name (etaf--component-spec-props spec)))) + +(defun etaf-m0b-component-manifest--mounted-probe (name prop value ref text) + "Mount NAME with PROP VALUE and verify forwarded attrs at REF and TEXT." + (let ((buffer (generate-new-buffer-name " *etaf-m0b-manifest*"))) + (unwind-protect + (progn + (etaf-mount + buffer + (etaf-node name + (list prop value :class "caller" + :aria-label "manifest-probe" :ref ref) + nil)) + (let* ((runtime (etaf-runtime-for-buffer buffer)) + (props (etaf-runtime-host-props-for runtime ref))) + (unless (and (string-match-p (regexp-quote text) + (with-current-buffer buffer + (buffer-string))) + (member "caller" (plist-get props :class)) + (equal "manifest-probe" + (plist-get props :aria-label))) + (error "Mounted Component forwarding probe failed: %S" name))) + t) + (when-let* ((runtime (etaf-runtime-for-buffer buffer))) + (etaf-unmount runtime)) + (when-let* ((live (get-buffer buffer))) + (kill-buffer live))))) + +(defun etaf-m0b-component-manifest-data () + "Return the observed M0b Component manifest as an alist." + (etaf-m0b-component-manifest--define-fixtures) + (let ((dsl-ok + (etaf-m0b-component-manifest--mounted-probe + etaf-m0b-component-manifest--dsl-name :title "DSL" 'manifest-dsl + "DSL")) + (render-ok + (etaf-m0b-component-manifest--mounted-probe + etaf-m0b-component-manifest--render-name :label "Render" + 'manifest-render "Render")) + (attrs '("class" "style/presentation" "layout" "ref" + "role/tab-index" "aria-*" "on-*" "use"))) + `((schema-version . 1) + (evidence-mode . "observed") + (components + . (((name . ,(symbol-name etaf-m0b-component-manifest--dsl-name)) + (frontend . "view") + (declared-business-props + . ,(etaf-m0b-component-manifest--spec-props + etaf-m0b-component-manifest--dsl-name)) + (forwarded-host-attrs . ,attrs) + (root-shape-forwarding-guarantee + . "single attr-capable root only; fragment, text, and multi-root results reject forwarded attrs") + (mounted-validation . ,dsl-ok)) + ((name . ,(symbol-name etaf-m0b-component-manifest--render-name)) + (frontend . "render") + (declared-business-props + . ,(etaf-m0b-component-manifest--spec-props + etaf-m0b-component-manifest--render-name)) + (forwarded-host-attrs . ,attrs) + (root-shape-forwarding-guarantee + . "single attr-capable root only; fragment, text, and multi-root results reject forwarded attrs") + (mounted-validation . ,render-ok))))))) + +(defun etaf-m0b-component-manifest-write-json () + "Write the observed M0b Component manifest as JSON." + (princ (json-encode (etaf-m0b-component-manifest-data)))) + +(provide 'etaf-m0b-component-manifest) + +;;; etaf-m0b-component-manifest.el ends here diff --git a/tests/etaf-docs-tests.el b/tests/etaf-docs-tests.el index 7fd7722..a5f7d13 100644 --- a/tests/etaf-docs-tests.el +++ b/tests/etaf-docs-tests.el @@ -6,6 +6,12 @@ (require 'ert) (require 'cl-lib) +(require 'json) +(require 'macroexp) +(require 'etaf) + +(declare-function etaf-m0b-component-manifest-write-json + "etaf-m0b-component-manifest" ()) (defconst etaf-docs-test--root (file-name-directory @@ -30,7 +36,7 @@ (unless (re-search-forward "^```[[:space:]]*$" nil t) (error "Unclosed Elisp documentation block")) (push (buffer-substring-no-properties start - (line-beginning-position)) + (match-beginning 0)) blocks)))) (nreverse blocks))) @@ -47,6 +53,42 @@ (end-of-file count) (error (error "Cannot read documentation form: %S" err)))))) +(defun etaf-docs-test--forms (source) + "Read and return every form in documentation SOURCE." + (with-temp-buffer + (insert source) + (goto-char (point-min)) + (let (forms) + (condition-case nil + (while t (push (read (current-buffer)) forms)) + (end-of-file (nreverse forms)))))) + +(defun etaf-docs-test--mounted-collection-probe (forms) + "Load exact documentation FORMS and mount the collection composition." + (etaf-component-redefine-run + (lambda () + (dolist (form forms) + (eval form t)))) + (let ((buffer (generate-new-buffer " *etaf-docs-m0b*"))) + (unwind-protect + (progn + (let ((view + (eval + '(etaf-view + (etaf-docs-collection-card + :items '((1 . "One") (2 . "Two")) + :footer-view (etaf-view (text "Dynamic")) + (slot :name 'footer (text "Footer")))) + t))) + (etaf-mount buffer view)) + (let ((text (with-current-buffer buffer (buffer-string)))) + (dolist (expected '("One" "Two" "Dynamic" "Footer")) + (should (string-match-p expected text))))) + (when-let* ((runtime (etaf-runtime-for-buffer buffer))) + (etaf-unmount runtime)) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + (ert-deftest etaf-docs-have-paired-long-lived-files () "Keep the public architecture, guide, and plan in both languages." (dolist (file '("README.md" @@ -95,6 +137,119 @@ (etaf-docs-test--read file))) (should (> (etaf-docs-test--read-all block) 0))))) +(ert-deftest etaf-docs-component-contract-is-current () + "Keep public Component prose aligned with the authoritative macro contract." + (dolist (file '("docs/architecture.en.md" "docs/architecture.zh.md" + "docs/user-guide.en.md" "docs/user-guide.zh.md" + "docs/implementation-plan.en.md" + "docs/implementation-plan.zh.md")) + (let ((contents (etaf-docs-test--read file))) + (should (string-match-p (regexp-quote ":view") contents)) + (should (string-match-p (regexp-quote ":render") contents)) + (should (string-match-p (regexp-quote ":setup") contents)) + (should (string-match-p (regexp-opt '("opaque state" "opaque 状态")) + contents)) + (should-not + (string-match-p + (regexp-opt '("setup returns a zero-argument render function" + ":setup` runs once per retained identity and returns a zero-argument render function" + ":setup` runs once for a retained Component instance and must return a zero-argument render function" + "返回零参数 render 函数" + "必须返回零参数 render 函数")) + contents))))) + +(ert-deftest etaf-docs-data-grid-selection-contract-is-current () + "Document only selection inputs actually declared by `etaf-data-grid'." + (dolist (file '("docs/user-guide.en.md" "docs/user-guide.zh.md")) + (let ((contents (etaf-docs-test--read file))) + (should (string-match-p (regexp-quote ":row-selected-p") contents)) + (should-not (string-match-p (regexp-quote ":selected-key") contents))))) + +(ert-deftest etaf-docs-superseded-proposals-not-current-contract () + "Keep the old Component proposal explicitly historical." + (let ((contents + (etaf-docs-test--read "docs/proposals/component-definition.zh.md"))) + (should (string-match-p "SUPERSEDED" contents)) + (should (string-match-p "historical" contents)) + (should (string-match-p (regexp-quote "docs/architecture.zh.md") contents)) + (should-not (string-match-p "尚未实现" contents)))) + +(ert-deftest etaf-docs-module-boundaries-target-vocabulary-is-paired () + "Keep the English and Chinese target Component vocabulary equivalent." + (dolist (file '("docs/proposals/module-boundaries.en.md" + "docs/proposals/module-boundaries.zh.md")) + (let ((contents (etaf-docs-test--read file))) + (dolist (token '(":view" ":render" ":setup" "etaf-state" "etaf-node" + "opaque")) + (should (string-match-p (regexp-quote token) contents)))))) + +(ert-deftest etaf-docs-interaction-contract-is-current () + "Keep the public Action/Behavior/event composition contract explicit." + (dolist (file '("docs/architecture.en.md" "docs/architecture.zh.md" + "docs/user-guide.en.md" "docs/user-guide.zh.md")) + (let ((contents (etaf-docs-test--read file))) + (dolist (token '("Host" "Behavior" "short-circuit" "first" + "capture" "bubble" "application" "feature" + "etaf-action-redefine-run" "dispatch")) + (should (string-match-p (regexp-quote token) contents)))))) + +(ert-deftest etaf-docs-component-manifest-is-machine-readable () + "Validate the three-column Component manifest and observed evidence." + (let* ((script (expand-file-name "scripts/etaf-m0b-component-manifest.el" + etaf-docs-test--root)) + (json-object-type 'alist) + (json-array-type 'list) + (manifest + (with-temp-buffer + (let ((standard-output (current-buffer))) + (load script nil t) + (etaf-m0b-component-manifest-write-json)) + (goto-char (point-min)) + (json-read)))) + (should (equal 1 (alist-get 'schema-version manifest))) + (should (equal "observed" (alist-get 'evidence-mode manifest))) + (dolist (entry (alist-get 'components manifest)) + (should (alist-get 'declared-business-props entry)) + (should (alist-get 'forwarded-host-attrs entry)) + (should (alist-get 'root-shape-forwarding-guarantee entry)) + (should (eq t (alist-get 'mounted-validation entry)))))) + +(ert-deftest etaf-docs-executable-suite-is-fail-closed () + "Classify exact blocks before macroexpansion, loading, or mounted smoke." + (let ((fixture (expand-file-name + "tests/fixtures/etaf-m0b-doc-examples.sexp" + etaf-docs-test--root))) + (should (file-readable-p fixture)) + (let ((records (with-temp-buffer + (insert-file-contents fixture) + (goto-char (point-min)) + (read (current-buffer))))) + (should records) + (dolist (record records) + (let* ((file (plist-get record :file)) + (index (plist-get record :block)) + (classification (plist-get record :classification)) + (blocks (etaf-docs-test--elisp-blocks + (etaf-docs-test--read file))) + (source (nth (1- index) blocks))) + (should source) + (should (equal (secure-hash 'sha256 source) + (plist-get record :sha256))) + (should (memq classification + '(macroexpand-only load-safe mounted-smoke))) + (pcase classification + ('macroexpand-only + (should (> (etaf-docs-test--read-all source) 0))) + ((or 'load-safe 'mounted-smoke) + (let ((forms (etaf-docs-test--forms source))) + (dolist (form forms) + (should (macroexpand-all (copy-tree form)))) + (pcase (plist-get record :probe) + ('collection-composition + (etaf-docs-test--mounted-collection-probe forms)) + ('nil (dolist (form forms) (eval form t))) + (_ (ert-fail "Unknown safe documentation probe"))))))))))) + (ert-deftest etaf-source-uses-only-public-ebox-names () "Keep the ETAF implementation independent of Ebox private functions." (dolist (file diff --git a/tests/etaf-m0b-component-manifest-tests.el b/tests/etaf-m0b-component-manifest-tests.el new file mode 100644 index 0000000..493a013 --- /dev/null +++ b/tests/etaf-m0b-component-manifest-tests.el @@ -0,0 +1,26 @@ +;;; etaf-m0b-component-manifest-tests.el --- M0b manifest probes -*- lexical-binding: t; -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Code: + +(require 'ert) +(require 'etaf-m0b-component-manifest) + +(ert-deftest etaf-m0b-component-manifest-is-derived-and-mounted () + "Derive business props and prove Host-attribute forwarding by mounting." + (let* ((manifest (etaf-m0b-component-manifest-data)) + (components (alist-get 'components manifest))) + (should (= 2 (length components))) + (should (equal '("title") + (alist-get 'declared-business-props (car components)))) + (should (equal '("label") + (alist-get 'declared-business-props (cadr components)))) + (dolist (entry components) + (should (alist-get 'forwarded-host-attrs entry)) + (should (string-match-p "single attr-capable root" + (alist-get 'root-shape-forwarding-guarantee + entry))) + (should (eq t (alist-get 'mounted-validation entry)))))) + +;;; etaf-m0b-component-manifest-tests.el ends here diff --git a/tests/fixtures/etaf-m0a-document-examples.sexp b/tests/fixtures/etaf-m0a-document-examples.sexp new file mode 100644 index 0000000..af3253e --- /dev/null +++ b/tests/fixtures/etaf-m0a-document-examples.sexp @@ -0,0 +1 @@ +((:file "README.md" :blocks ((1 "90788f843ad1f654c31d7317e9a853a2b90a40d961b6c494dc5dc4b5d1d8deec" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (2 "2685e06fdf8da0d8bad106cd7bec45b7e3cff4fa70c95df519dfa908b68507e4" ok 1 error etaf-view-syntax-error skipped-unsafe arbitrary-document-code macroexpand-error) (3 "4a15a53cbb5f28620fe3fbcf779fc7ec24828efff72fcb780b29cb14d61d8690" ok 2 error etaf-view-syntax-error skipped-unsafe arbitrary-document-code macroexpand-error) (4 "cdbc6593afa8eb1a55d1ebc3b7b436c5c7a73dfdb47bca7f2e4d627e2b5efc3b" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (5 "170887ddf9784e93c815c26982fa27ada7e251b4e91d084c00e508428adf2576" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (6 "03be4130f1485ae08b5d2d5e951fbc243e13db7ab43a269d2eb4fd31a6f022ba" ok 3 ok 3 skipped-unsafe arbitrary-document-code none))) (:file "README.zh-CN.md" :blocks ((1 "90788f843ad1f654c31d7317e9a853a2b90a40d961b6c494dc5dc4b5d1d8deec" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (2 "2685e06fdf8da0d8bad106cd7bec45b7e3cff4fa70c95df519dfa908b68507e4" ok 1 error etaf-view-syntax-error skipped-unsafe arbitrary-document-code macroexpand-error) (3 "4a15a53cbb5f28620fe3fbcf779fc7ec24828efff72fcb780b29cb14d61d8690" ok 2 error etaf-view-syntax-error skipped-unsafe arbitrary-document-code macroexpand-error) (4 "a23afe4a6d314d6e2f6482ff2b20809c6d00a1eb419545286601908fd4276f7e" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (5 "170887ddf9784e93c815c26982fa27ada7e251b4e91d084c00e508428adf2576" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (6 "03be4130f1485ae08b5d2d5e951fbc243e13db7ab43a269d2eb4fd31a6f022ba" ok 3 ok 3 skipped-unsafe arbitrary-document-code none))) (:file "examples/README.md" :blocks ((1 "0668b4bd330b19e78fa26c19e36ec39b3dffbe1da9bbf0b918110e4968d92c15" ok 5 ok 5 skipped-unsafe arbitrary-document-code none))) (:file "examples/README.zh-CN.md" :blocks ((1 "0668b4bd330b19e78fa26c19e36ec39b3dffbe1da9bbf0b918110e4968d92c15" ok 5 ok 5 skipped-unsafe arbitrary-document-code none))) (:file "docs/architecture.en.md" :blocks ((1 "1b540d9726c3cf0f064baa0bf8fa6e0f7bdd1bc552adb2032d76534243c691f8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (2 "aca28b3eee51e12db137d1e1f53fb70ffb7b5bb757829d93572c238b8aad4e3b" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (3 "b0c46b25b40bd95ddeadf502d19689c4068945a7ab7e415ac52f2aacbabe5f96" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (4 "37eadf35f976fcc795ef264ebe4481bf9c62576e9d879aa7f87809a912763ad8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (5 "ba85666e995b31ed155432504b66ca82aca4b173cee242906e51df1687c0b0b0" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (6 "820db03998e5d9b72defb2149f981f0a47142a374c34fd7d06894d2a46bd4e6d" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (7 "497e5a3bf314a96a61428b217870886e20cd110b65fb1e2fa5d7e53907a1603f" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (8 "2486da6efc52edf44c791ec3931ec1b6b2261283c73893581a7dfb462f08e77e" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (9 "f930e82bef8a415d67afbc9f68bfc0d4c901840137ba1cc51696a022f2efde77" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (10 "57d8239f1aca6c75bd46d3d666e730a46b232347841f37034af66efd14e14200" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (11 "0b4a7062e7591a3c49efa40750ecf00e52380b444811e476e2413ba84f21dc24" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (12 "1eaf9b6d6df02ae197d4126314d56997a9956d1f115e51d0e172bc5d53d8b31a" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (13 "2b3627845c5667b4dde0dbf1944cfbb9e7213926f40818e644502c328bd12251" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (14 "1c406360dfdc23b26ff331468db47dcb93d710168a9b14b97ff663f637e556c8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (15 "a739e2986b545a6c49528de3f515480b99bb51a99c7c950291639ef50758088a" ok 1 ok 1 skipped-unsafe arbitrary-document-code none))) (:file "docs/architecture.zh.md" :blocks ((1 "1b540d9726c3cf0f064baa0bf8fa6e0f7bdd1bc552adb2032d76534243c691f8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (2 "aca28b3eee51e12db137d1e1f53fb70ffb7b5bb757829d93572c238b8aad4e3b" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (3 "b0c46b25b40bd95ddeadf502d19689c4068945a7ab7e415ac52f2aacbabe5f96" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (4 "37eadf35f976fcc795ef264ebe4481bf9c62576e9d879aa7f87809a912763ad8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (5 "ba85666e995b31ed155432504b66ca82aca4b173cee242906e51df1687c0b0b0" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (6 "820db03998e5d9b72defb2149f981f0a47142a374c34fd7d06894d2a46bd4e6d" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (7 "497e5a3bf314a96a61428b217870886e20cd110b65fb1e2fa5d7e53907a1603f" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (8 "2486da6efc52edf44c791ec3931ec1b6b2261283c73893581a7dfb462f08e77e" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (9 "f930e82bef8a415d67afbc9f68bfc0d4c901840137ba1cc51696a022f2efde77" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (10 "57d8239f1aca6c75bd46d3d666e730a46b232347841f37034af66efd14e14200" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (11 "0b4a7062e7591a3c49efa40750ecf00e52380b444811e476e2413ba84f21dc24" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (12 "1eaf9b6d6df02ae197d4126314d56997a9956d1f115e51d0e172bc5d53d8b31a" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (13 "2b3627845c5667b4dde0dbf1944cfbb9e7213926f40818e644502c328bd12251" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (14 "1c406360dfdc23b26ff331468db47dcb93d710168a9b14b97ff663f637e556c8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (15 "a739e2986b545a6c49528de3f515480b99bb51a99c7c950291639ef50758088a" ok 1 ok 1 skipped-unsafe arbitrary-document-code none))) (:file "docs/user-guide.en.md" :blocks ((1 "03be4130f1485ae08b5d2d5e951fbc243e13db7ab43a269d2eb4fd31a6f022ba" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (2 "3ea1c9ff2e1ca4a31ef46ac55ef3c78e7a630346f385c12621504a5470f24f56" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (3 "ebbbee1c31e6f89aca772b7ebba9756d555a108e97197cd8ee6db74f09e5f6b9" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (4 "d8963a39d3ca02c524661bf0ccc05eef1331f4305e6e568b6ed4cd021e5b2e4f" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (5 "14fb850d1f28bf6970aeb9ca0d15fa9ab4bed747b5a0671e07642cc7ff6da9f4" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (6 "947b25b44a74005f416e9556caeae6f0b6cbc95f69cc248ad4a375b321d983ba" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (7 "072830fd4b5b0cecbe51808bf1cbd39a06586f43d5729c65ac09f3a33ebdc220" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (8 "c94fcd1e44d8ef044d6372d01bef058b05581569ba3a499530ba85040f238c9c" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (9 "023c30adcfd14fc330ab07529ce32856e27209aa87b36e0cc1a50aae5f20d283" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (10 "f19f26fead0e2efc8ab26631f010518cf5a6b477990598ae20c0dd911908c838" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (11 "216d25cee8b364bcc5dc39d8458ce08db07c40fa8ce8894ae10fdde94bc52906" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (12 "eb7850ac84d549db58ea7c4c1c3b47d4eae25957be77f6e0ae2a196bbbead2f1" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (13 "4113f7b5f7f7b95ace9fae0b92b91ce33b8dfb85d534480b7cc1994b4e30187a" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (14 "502cc98bd5ec20f24764a935ca60ceb8c0de61d4507fe76530070b0516e9c016" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (15 "d5e1633dd8fa0b5edfba0bc31314fcc97e446ffa0577f3d3c9465e887d5b7903" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (16 "0944e77a00d76e6462cca3e117c2b36c3611863128321c2307dd1ff8b3696368" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (17 "1d5c4895dfaaa595e6ea821158f0bfb11d7777dd697e5660bfc00cf14f82a684" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (18 "79390863a3fdf604969cc4f424b36a9a466e569bd0ac740e61c580b2518cb2da" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (19 "d76fc85649564ade8955902b14bf562617c5aa46bbae899c970816e26edacb58" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (20 "2b3627845c5667b4dde0dbf1944cfbb9e7213926f40818e644502c328bd12251" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (21 "9f81f0ca552349f409a8125e06243f41b6a53c1704827a74d9e305383c5f2f6e" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (22 "47844b8b47eb7fe2403b4a93919bd2885fc505cf1937d60de3455f9a6a252e09" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (23 "9b6376756bbfd168285a847ea1bc908ec2e71948a3f5193a06b6b8c8b77310ed" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (24 "803de6e620863e9e0697e3b97305bd7a4f84696a0ab521f4d889c6718f33fc29" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (25 "2105a901604efb117020716ba735764c75312391b0af6094b103808163281b6c" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (26 "ecdda8ab15432792c2a879a8ae942609fa9884f8f1c0a9c1ce008d199119aba2" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (27 "1e22b36b7c2601697a08c378c1480d434bff34e2cdfea19b3c01eedac13585b9" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (28 "dd7229d4443b5ae338ae78e3b9007ab25f1c7abfc028224c4af549a320e355d2" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (29 "c0d6fac9480353ea5d474510a6179c117a61f9658b3a60e19b45c5bcd8e28771" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (30 "58d5a6f2347b7ce1e9769acba9405d484c47cc2fa4ace19b73457d2cc99e4a2c" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (31 "c00135e9766c2e2e274917db624e9642e950d2f24e1aef6e9d1f4f4ee349ca71" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (32 "d3898d77f2d252873e0cb11a27de6737d36a8146b8bbdbc667769e8a116a85e1" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (33 "67dd1415cb71b8d8efbaacaf3cf88502611ace7132d9fa192b3471d9c75c2115" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (34 "67fa1ca41788eebc5efe50add5e7245d60c0f2d6b84a8023d02c80c39089b1c8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (35 "170887ddf9784e93c815c26982fa27ada7e251b4e91d084c00e508428adf2576" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (36 "691a4c3ef48dad301e35e7fb439810dd6e00395dab3094538e0ed9ead465b252" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (37 "fc6559abfcc19506f1caf7ac89bcb8115af37aab75d36c8da3a050fa2df0527e" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (38 "60c8119e975c2518c63fa18c345cb294dd41640f6f94b98b3eda677c3eb01b3e" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (39 "7443880f4f6bec722a9cca875228183fb8ea323edb5253e73dbaca32a7d228da" ok 3 ok 3 skipped-unsafe arbitrary-document-code none))) (:file "docs/user-guide.zh.md" :blocks ((1 "03be4130f1485ae08b5d2d5e951fbc243e13db7ab43a269d2eb4fd31a6f022ba" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (2 "3ea1c9ff2e1ca4a31ef46ac55ef3c78e7a630346f385c12621504a5470f24f56" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (3 "ebbbee1c31e6f89aca772b7ebba9756d555a108e97197cd8ee6db74f09e5f6b9" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (4 "d8963a39d3ca02c524661bf0ccc05eef1331f4305e6e568b6ed4cd021e5b2e4f" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (5 "14fb850d1f28bf6970aeb9ca0d15fa9ab4bed747b5a0671e07642cc7ff6da9f4" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (6 "947b25b44a74005f416e9556caeae6f0b6cbc95f69cc248ad4a375b321d983ba" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (7 "072830fd4b5b0cecbe51808bf1cbd39a06586f43d5729c65ac09f3a33ebdc220" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (8 "c94fcd1e44d8ef044d6372d01bef058b05581569ba3a499530ba85040f238c9c" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (9 "023c30adcfd14fc330ab07529ce32856e27209aa87b36e0cc1a50aae5f20d283" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (10 "f19f26fead0e2efc8ab26631f010518cf5a6b477990598ae20c0dd911908c838" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (11 "216d25cee8b364bcc5dc39d8458ce08db07c40fa8ce8894ae10fdde94bc52906" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (12 "eb7850ac84d549db58ea7c4c1c3b47d4eae25957be77f6e0ae2a196bbbead2f1" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (13 "4113f7b5f7f7b95ace9fae0b92b91ce33b8dfb85d534480b7cc1994b4e30187a" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (14 "502cc98bd5ec20f24764a935ca60ceb8c0de61d4507fe76530070b0516e9c016" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (15 "d5e1633dd8fa0b5edfba0bc31314fcc97e446ffa0577f3d3c9465e887d5b7903" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (16 "0944e77a00d76e6462cca3e117c2b36c3611863128321c2307dd1ff8b3696368" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (17 "1d5c4895dfaaa595e6ea821158f0bfb11d7777dd697e5660bfc00cf14f82a684" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (18 "79390863a3fdf604969cc4f424b36a9a466e569bd0ac740e61c580b2518cb2da" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (19 "d76fc85649564ade8955902b14bf562617c5aa46bbae899c970816e26edacb58" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (20 "2b3627845c5667b4dde0dbf1944cfbb9e7213926f40818e644502c328bd12251" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (21 "9f81f0ca552349f409a8125e06243f41b6a53c1704827a74d9e305383c5f2f6e" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (22 "47844b8b47eb7fe2403b4a93919bd2885fc505cf1937d60de3455f9a6a252e09" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (23 "9b6376756bbfd168285a847ea1bc908ec2e71948a3f5193a06b6b8c8b77310ed" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (24 "803de6e620863e9e0697e3b97305bd7a4f84696a0ab521f4d889c6718f33fc29" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (25 "2105a901604efb117020716ba735764c75312391b0af6094b103808163281b6c" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (26 "ecdda8ab15432792c2a879a8ae942609fa9884f8f1c0a9c1ce008d199119aba2" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (27 "1e22b36b7c2601697a08c378c1480d434bff34e2cdfea19b3c01eedac13585b9" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (28 "dd7229d4443b5ae338ae78e3b9007ab25f1c7abfc028224c4af549a320e355d2" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (29 "c0d6fac9480353ea5d474510a6179c117a61f9658b3a60e19b45c5bcd8e28771" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (30 "58d5a6f2347b7ce1e9769acba9405d484c47cc2fa4ace19b73457d2cc99e4a2c" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (31 "c00135e9766c2e2e274917db624e9642e950d2f24e1aef6e9d1f4f4ee349ca71" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (32 "d3898d77f2d252873e0cb11a27de6737d36a8146b8bbdbc667769e8a116a85e1" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (33 "67dd1415cb71b8d8efbaacaf3cf88502611ace7132d9fa192b3471d9c75c2115" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (34 "67fa1ca41788eebc5efe50add5e7245d60c0f2d6b84a8023d02c80c39089b1c8" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (35 "170887ddf9784e93c815c26982fa27ada7e251b4e91d084c00e508428adf2576" ok 3 ok 3 skipped-unsafe arbitrary-document-code none) (36 "691a4c3ef48dad301e35e7fb439810dd6e00395dab3094538e0ed9ead465b252" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (37 "fc6559abfcc19506f1caf7ac89bcb8115af37aab75d36c8da3a050fa2df0527e" ok 1 ok 1 skipped-unsafe arbitrary-document-code none) (38 "60c8119e975c2518c63fa18c345cb294dd41640f6f94b98b3eda677c3eb01b3e" ok 2 ok 2 skipped-unsafe arbitrary-document-code none) (39 "3b2dd2ce38340418d7c2bdbc71675a97ce696e091aa98f2e9c750150dc2b71c7" ok 3 ok 3 skipped-unsafe arbitrary-document-code none))) (:file "docs/implementation-plan.en.md" :blocks nil) (:file "docs/implementation-plan.zh.md" :blocks nil)) diff --git a/tests/fixtures/etaf-m0b-doc-examples.sexp b/tests/fixtures/etaf-m0b-doc-examples.sexp new file mode 100644 index 0000000..3c6200e --- /dev/null +++ b/tests/fixtures/etaf-m0b-doc-examples.sexp @@ -0,0 +1,5 @@ +((:file "docs/user-guide.en.md" + :block 18 + :sha256 "79390863a3fdf604969cc4f424b36a9a466e569bd0ac740e61c580b2518cb2da" + :classification mounted-smoke + :probe collection-composition))