docs: freeze typed Text and Box architecture contract

This commit is contained in:
Kinneyzhang 2026-08-26 17:12:02 +08:00
parent 44f70a6f61
commit 30ac302d47
2 changed files with 824 additions and 272 deletions

View File

@ -1,34 +1,90 @@
# ETAF Module Responsibilities and Target Architecture (Unimplemented Proposal)
# ETAF Module Responsibilities and Target Architecture (Design Proposal, Not Implemented)
> Status: design proposal. This is not the delivered API. Current behavior remains
> defined by [`architecture.en.md`](../architecture.en.md),
> Status: design proposal. This document does not describe the currently delivered API.
> Current behavior remains defined by
> [`architecture.en.md`](../architecture.en.md),
> [`user-guide.en.md`](../user-guide.en.md), and passing tests.
This document defines only the target model, module owners, dependency direction, and
completion criteria. Delivery sequencing belongs in a separate implementation plan;
historical names and compatibility do not shape the target architecture.
completion criteria. Implementation order belongs in a separate delivery plan;
historical names and compatibility policy do not shape the target architecture.
User mental cost is a hard constraint: one capability has one canonical name and one
recommended spelling. Internal algorithms, backend nodes, wrappers, and compatibility
aliases never become peer public concepts. Ordinary applications learn Text, Box, and
Component first; Fragment, layout detail, and lower packages are progressively disclosed.
User mental cost is a hard constraint of this proposal: one capability has one canonical
name and one recommended spelling. Internal algorithms, backend nodes, wrappers, and
compatibility aliases must not become peer public concepts. Ordinary applications need
to learn only Text, Box, and Component; Fragment, layout details, and lower-level
packages enter through progressive disclosure.
## 1. Final model
## 1. Final Model
The ETAF/Ebox representation pipeline is layered by responsibility:
```text
Component semantics and ownership
View: Text / Box / Fragment
Ebox: measurement, layout, geometry patches
TP + Emacs adapter: paint and final submission
AuthorView
↓ parse / desugar / string normalization
NormalizedView = Text | Box | Fragment | ComponentCall
↓ ETAF Runtime resolves ComponentCall
Resolved candidate + computed properties
├─ structure/geometry impact
│ ↓ Fragment projection preserves RangeAnchors
│ CanonicalEboxInput = Forest<TextNode | BoxNode> + RangeAnchors
│ ↓ Ebox measure / layout
│ LayoutPlan → GeometryPatch
└─ paint impact
↓ source-specific contribution adapter
(ETAF: etaf-theme-tp; standalone Ebox: ebox-surface-tp)
TP ContributionPlan → PaintPatch
GeometryPatch and PaintPatch join the existing TP transaction
↓ Emacs adapter commit / rollback
Published generation
```
The main public concepts are `Text`, `Box`, and `Component`. `Fragment` is advanced
structure, `Host` is an internal Renderer term, and an Ebox Node is a backend object.
These layers are not competing trees, nor do they repeatedly materialize the same tree.
Each lowering step consumes the identity, computed properties, and child order already
retained by the preceding layer; no fact is parsed or derived twice. LayoutPlan and
ContributionPlan are independent derivations of the same candidate transaction. A
paint-only path neither constructs CanonicalEboxInput nor runs layout. The two paths
join at the existing transaction participant. Plans own neither author identity nor DSL
nodes.
The complete normalized View shape is:
Illegal canonical node states must be unrepresentable:
```text
TextNode = {
value: String,
measurementProps,
sourceHandle,
eboxNodeId
}
BoxNode = {
outer: inline | block,
layout: Normal | RowConfig | ColumnConfig | FlexConfig | GridConfig,
geometryProps,
parentParticipationProps,
children: [TextNode | BoxNode],
sourceHandle,
eboxNodeId
}
```
TextNode has no children, outer, or layout. BoxNode has no content or value. A BoxNode
stores its participation fields, but only its direct parent formatting context may
validate and interpret them.
`sourceHandle` is an opaque handle owned by the source layer: on the ETAF path it points
to a Runtime Text/Box/Range source; on the standalone Ebox DSL path it points to an Ebox
author source. `eboxNodeId` identifies only a retained Ebox geometry node. The
Renderer/surface retains the mapping between them. Ebox reconciliation must not assume
ownership of Component, Fragment, Range, or lifecycle identity.
The primary concepts a user needs to understand are only `Text`, `Box`, and `Component`.
`Fragment` is advanced structural syntax; `Host` is an internal Renderer term; and
`Ebox Node` is a backend object. None constitutes a second component model.
The complete NormalizedView shape is:
```text
View = Text
@ -37,226 +93,477 @@ View = Text
| ComponentCall
```
Author syntax and normalized View are separate layers. ETAF and Ebox share this visual
author syntax:
```text
AuthorNode = String
| TextForm
| BoxForm
| RowSugar
| ColumnSugar
| FlexSugar
| GridSugar
```
`String` normalizes to Text. The five Box forms normalize to Box with the corresponding
typed Layout. Form names do not enter Runtime identity, diff, style, layout, or paint
protocols. ETAF additionally provides Fragment, ComponentCall, `expr`, and `slot`; the
Ebox DSL does not own those semantics.
The two author syntaxes share structural vocabulary and normalized results, but need not
share an evaluation environment: ETAF properties are Elisp expressions; the `.ebox` DSL
uses data forms. Quoting and expression differences belong only to their parsers and do
not create two canonical schemas.
A Flex/Grid item is the role a material child Box acquires in its parent formatting
context. It is neither another View nor a parent-child edge type. Participation
properties are stored on the child Box; their direct parent Flex/Grid Box owns
validation, computation, and dirty propagation.
`expr` and `slot` are computation/projection mechanisms, not visual nodes. The target
public View does not accept raw Ebox Nodes. Missing rendering capability must become a
typed View/Ebox feature with explicit identity and impact contracts rather than an
opaque escape.
public View does not accept raw Ebox nodes. A missing rendering capability must first
become a typed View/Ebox capability with identity and impact contracts; it must not
bypass framework semantics through an opaque escape hatch.
## 2. Text, Box, and Fragment
### 2.1 Text
Text is a leaf that owns strings and inline Text runs, typography/paint, text
measurement and wrapping input, and optional identity/semantic/event properties.
`Text` is a text leaf responsible for:
Text defaults to `:outer 'inline`. It cannot own Box/Component children or establish
row, column, flex, or grid layout. Use an outer Box for padding, border, dimensions, or
child layout.
- exactly one payload that evaluates to a string;
- text paint such as font, foreground/background, and underline;
- text measurement, wrapping, and text-alignment input;
- optional identity, semantic, and event properties.
### 2.2 Box axes
`Text` is always inline and does not expose `:outer`. It has no View children, cannot
nest Text, and cannot establish any layout context. Multiple styled runs are adjacent
Text nodes, not a Text subtree. Use an outer Box when block participation, padding,
border, dimensions, or child layout is required.
The Text grammars for the two authoring entry points differ precisely in evaluation
capability:
```text
ETAF TextForm = (text TEXT-PROP VALUE)
VALUE = String | (expr :value Expr<String>)
Ebox TextForm = (text TEXT-PROP String)
```
All property key-value pairs must precede the one payload. Zero payloads, multiple
payloads, nested Text, or a non-string payload are errors. A bare string is only sugar
for an unstyled Text and has this one interpretation in every position.
### 2.2 The Two Orthogonal Box Axes
`Box` has both an external participation mode and an internal layout mode:
```text
:outer = inline | block
:layout = normal | row | column | flex | grid
layout = Normal | RowConfig | ColumnConfig | FlexConfig | GridConfig
```
`:outer` controls participation in a parent normal layout. `:layout` controls child
layout. Defaults are `Box: block/normal` and `Text: inline`.
- `:outer`: how this Box participates in its parent's `normal` layout;
- `layout` variant: how this Box arranges its own children.
The defaults are:
```text
Box :outer block :layout Normal
Text :fixed inline
```
The author layer does not expose a `:layout` property. Instead, exactly one layout is
selected by the form name:
```elisp
(box :outer 'inline :layout 'flex ...)
(box ...) ; Normal
(row ...) ; RowConfig
(column ...) ; ColumnConfig
(flex ...) ; FlexConfig
(grid ...) ; GridConfig
```
This is inline-flex: an orthogonal combination, not another node type. ETAF exposes
`:outer` and `:layout`, not `:inner` or Ebox's raw display-pair representation.
Inline flex is therefore written `(flex :outer 'inline ...)`, not represented as a new
node type. `:outer` is a common property of every Box author form; each form's closed
property set constructs its corresponding layout config. To switch layout dynamically,
use `expr` to choose between View forms rather than treating `:layout` as an ordinary
dynamic style. Canonical Box uses a typed Layout variant; it does not expose `:inner` or
Ebox's backend representation `(:display (block flow))`.
### 2.3 Bounded normal layout
### 2.3 Explicit Scope of normal
Normal layout supports only:
`normal` is a real but deliberately bounded layout context:
- consecutive inline Text/Box nodes in one wrapping line flow;
- block Boxes starting a new independent block;
- stable child order and block boundaries ending adjacent inline lines.
- consecutive inline `Text` or inline `Box` nodes enter one line flow and wrap to the
available width;
- a block `Box` begins on a new line and establishes an independent block;
- inline lines before and after a block end at the block boundary;
- child order remains stable.
It does not implement full browser CSS: no floats, tables, absolute/fixed positioning,
run-in, list-item, or arbitrary anonymous-box rules. Ebox may map `normal` to an
internal flow algorithm; `flow` is not ETAF vocabulary.
The target does not implement full browser CSS: it has no floats, tables,
absolute/fixed positioning, run-in, list-item, or arbitrary anonymous-box rules. Ebox
may map `normal` to an internal flow algorithm, but `flow` is not public ETAF vocabulary.
For row, column, flex, or grid parents, the parent algorithm owns item placement and a
child's `:outer` does not change ordering. `:outer` is consumed only by a normal parent.
When the parent Layout is Row, Column, Flex, or Grid, the parent algorithm directly owns
child placement; a child's `:outer` does not change ordering. `:outer` determines inline
or block participation only when a node enters a `normal` parent.
### 2.4 Other layout modes
### 2.4 Other Layout Modes
| Mode | Responsibility |
| --- | --- |
| `row` | simple horizontal order without Flex distribution |
| `column` | simple vertical order without Flex distribution |
| `row` | simple horizontal sequential layout without Flex space distribution |
| `column` | simple vertical sequential layout without Flex space distribution |
| `flex` | Flex sizing, direction, wrapping, alignment, and gaps |
| `grid` | two-dimensional tracks, placement, spans, and gaps |
They are Box modes, not Components or alternate Runtime node names. The only canonical
surface is `(box :layout 'MODE ...)`; core provides no layout-name aliases.
These are `Box` Layout variants, not Components, and they do not register a second set
of Runtime node names for `row`, `column`, `flex`, or `grid`. Author forms have exactly
one normalization mapping:
### 2.5 Ebox DSL primitives and sugar
ETAF View and Ebox DSL are different layers: ETAF has Text/Box/Fragment/Component,
while Ebox receives lowered content and layout. The target Ebox DSL has one structural
tag:
```elisp
(box :layout MODE ...)
```text
box → Box(layout = Normal)
row → Box(layout = RowConfig)
column → Box(layout = ColumnConfig)
flex → Box(layout = FlexConfig)
grid → Box(layout = GridConfig)
```
An Ebox box may be a content leaf or own children and a layout context. A Range
descriptor is an incremental backend protocol, not an authoring node. A bare string is
content-literal shorthand, not a node type.
ETAF and Ebox share these five Box author forms. The term “layout form” denotes only an
input spelling and its config schema; it does not imply a retained Host, node type, or
runtime dispatch.
Current tags classify as follows:
The table above is the only normative lowering table. Ebox owns the canonical Layout
variants and config schemas. The ETAF parser evaluates Elisp and selects the variant
with the same name; it does not duplicate value rules. Cross-interface conformance tests
must prove that the two entry points produce the same canonical shape, computed defaults,
and errors for the same structure.
### 2.5 Ebox DSL Primitives and Sugar
ETAF View and the Ebox DSL are separate interfaces, but they share one visual author
syntax: Text is the text leaf, and Box is the structural/geometric container. The
complete visual syntax of the Ebox DSL is:
```elisp
"plain text"
(text TEXT-PROPS "styled text")
(box BOX-PROPS CHILD...)
(row BOX-PROPS CHILD...)
(column BOX-PROPS CHILD...)
(flex BOX-PROPS CHILD...)
(grid BOX-PROPS CHILD...)
```
A string always normalizes to Text. `text` accepts only textual content and Text
properties; `box` accepts only children and Box properties. The Ebox DSL does not accept
`:content`, so a string is never ambiguous between “Box content” and “child.” An empty
`(box :width ... :height ...)` is a valid empty geometry container.
`ebox-build` is the parse/desugar entry point for the author DSL. The public
programmatic port provides two typed constructors: `ebox-text-create` constructs a
TextNode, while `ebox-box-create` accepts a typed Layout variant and constructs a
BoxNode. Constructors accept neither author forms nor a flat bag of properties spanning
multiple layouts. The ETAF Renderer calls them directly; it does not reconstruct DSL.
The target public port does not retain untyped `(ebox-create :content ...)`. If the
runtime fuses a Text payload into adjacent Box storage for performance, that optimization
may occur only after canonical Text identity, range, and impact have been established,
and it must reversibly preserve those facts. A private storage encoding must not become
an author or public programmatic API. A Range descriptor is likewise an incremental
backend protocol, not a DSL node.
At the DSL boundary, each of the five Box author forms must immediately and statelessly
construct its typed Layout variant. They must not establish distinct node types,
identities, caches, validation paths, or rendering branches. Every author form rejects
`:layout`; layout-specific properties are accepted only by the corresponding form.
Other current DSL tags must be classified explicitly rather than presented beside the
primitives:
| Current tag | Classification | Target expression |
| --- | --- | --- |
| `ebox` | compatibility alias for `box` | remove; use `box` |
| `ebox` | compatibility alias for `box` | remove; always write `box` |
| `spacer` | empty-Box convenience sugar | remove; use a childless `box` |
| `row` | layout-tag sugar | `(box :layout 'row ...)` |
| `column` | layout-tag sugar | `(box :layout 'column ...)` |
| `flex` | layout-tag sugar | `(box :layout 'flex ...)` |
| `grid` | layout-tag sugar | `(box :layout 'grid ...)` |
| `item` | Flex-child participation sugar | put flex/order/align-self properties on the child Box |
| `grid-item` | Grid-child placement sugar | put grid placement properties on the child Box |
| `row` | Box layout form | retain and construct RowConfig |
| `column` | Box layout form | retain and construct ColumnConfig |
| `flex` | Box layout form | retain and construct FlexConfig |
| `grid` | Box layout form | retain and construct GridConfig |
| `item` | Flex wrapper/sugar | remove; put participation properties directly on the child Box |
| `grid-item` | Grid wrapper/sugar | remove; put placement properties directly on the child Box |
Item wrappers are therefore unnecessary. Parent-participation properties belong to
the child Box and are validated by the parent layout mode:
A direct child of Flex/Grid automatically acquires the item role; item is not a node
type. The only recommended path for nondefault participation is an explicit child Box:
```elisp
(box :layout 'flex
(box :flex-grow 1 :content "A"))
(flex
(box :flex-grow 1 "Flexible")
"Fixed")
(box :layout 'grid
(box :grid-column '(1 :span 2) :content "Header"))
(grid
(box :grid-column '(1 :span 2) "Header")
(box "Body"))
```
These are low-level Ebox examples, so Ebox `:content` is valid. ETAF View continues to
use Text/children and does not expose `:content`.
Participation/placement properties are stored on the child Box, but only its direct
parent Flex/Grid validates and consumes them; they must be rejected under the wrong
parent. A property change sends the parent layout owner through the geometry path, but
does not change the child Component/Text identity, lifecycle, or paint ownership. A
direct Text uses default item parameters; wrap it in an explicit Box when nondefault
parameters are needed.
The target Ebox DSL retains none of those aliases or sugar tags. Ebox still implements
the layout algorithms independently, but algorithm kinds are not authoring node kinds.
The target Ebox DSL removes `ebox`, `spacer`, `item`, and `grid-item`, retaining Text,
Box, and the four layout sugars. Ebox still implements the layout algorithms separately,
but the canonical visual nodes remain only TextNode and BoxNode. Author syntax, canonical
tree, layout plan, and runtime storage are separate layers.
### 2.6 Fragment
Fragment is a nonvisual structural Range. It may contain zero, one, or many sibling
Views, creates no Box or layout context, and may carry only a stable `:key` plus
children. Runtime may retain and replace its Range independently.
`Fragment` is a nonvisual structural Range:
A Component call has no fixed outer/layout properties. Its root Text/Box determines
participation. A transparent Component returning a Fragment may have multiple roots.
- it may carry zero, one, or many sibling Views;
- it creates no Box, dimensions, background, or layout context;
- Runtime may retain a stable Range identity for it and replace it locally;
- it accepts only children and an optional stable `:key`, not visual or event properties.
### 2.7 Strings, content, and empty Boxes
Fragment resolution does not discard Range identity. It splices material children into
the CanonicalEboxInput forest in order while producing a `{range-id, before, after}`
RangeAnchor. An empty Fragment still has an insertion anchor; a root Fragment may map to
zero or multiple forest roots. ETAF Runtime owns Range identity. Ebox consumes only the
descriptor to produce a local geometry patch. RangeAnchor is not a visual node and does
not participate in measurement.
Strings under a Box normalize to Text, so `(box "text")` equals
`(box (text "text"))`. ETAF does not expose `(box :content ...)`; `:content` remains an
Ebox backend field. ETAF also has no spacer type: a childless Box is an empty Box.
A Component call itself has neither `:outer` nor Layout. Its root Text/Box determines
how it participates in parent layout. A transparent Component that returns a Fragment
may produce multiple siblings, so there is no single “Component outer box.”
## 3. Property contract
### 2.7 Strings, Content, and Empty Boxes
Properties form closed, owner-specific schemas. Unknown or inapplicable combinations
fail explicitly.
A string child of a Box normalizes to Text:
| Owner | Category | Representative properties |
```elisp
(box "this is text")
```
is equivalent to:
```elisp
(box (text "this is text"))
```
Neither the ETAF nor Ebox author DSL provides `(box :content "...")`. `:content` belongs
only to the existing untyped runtime storage; the target public programmatic port does
not retain it either. Exposing `:content` through the author DSL or a canonical
constructor would recreate two models—content and Text children.
ETAF also provides no `spacer` type. A Box with no children is an empty Box.
## 3. Property Contract
Properties form closed sets and are validated by owner. Unknown properties and
inapplicable combinations are errors.
| Owner | Property category | Representative properties |
| --- | --- | --- |
| Text | outer participation | `:outer` |
| Text | text/paint | `:face`, `:color`, `:bgcolor`, `:wrap-mode`, `:text-align` |
| Box | outer/inner layout | `:outer`, `:layout` |
| Box | geometry/surface | width/height/min/max, margin, padding, border, box sizing, overflow |
| Flex container/item | layout/participation | direction, wrap, alignment, gap, grow/shrink/basis/order |
| Grid container/item | layout/participation | tracks, auto flow, placement, spans, alignment, gap |
| Text/Box | identity/semantics | `:key`, `:ref`, class/id/role/ARIA/events/Behaviors |
| ETAF Runtime metadata | identity/semantics/events/Behavior | `:key`, `:ref`, `:role`, `:aria-*`, `:on-*`, `:use` |
| ECSS author style source | selector/inline declarations | `:class`, `:id`, style declarations |
| Ebox TextNode | canonical text measurement | font metrics, wrap policy, intrinsic constraints |
| Ebox BoxNode | external/internal layout | `:outer`, typed Layout variant |
| Ebox BoxNode | geometry | width/height/min/max, margin, padding, border widths, box sizing, overflow |
| Flex container | layout | `:flex-direction`, `:flex-wrap`, `:justify-content`, `:align-items`, `:align-content`, `:gap` |
| Flex child Box | parent participation | `:order`, `:flex-grow`, `:flex-shrink`, `:flex-basis`, `:align-self` |
| Grid container | layout | track templates, auto flow, gaps, item/content alignment |
| Grid child Box | parent participation | `:grid-row`, `:grid-column`, row/column spans, self alignment |
| TP contribution | paint | color/background, underline/overline, border paint, visibility paint |
Key rules:
Rules:
- `:key` is identity metadata, not paint;
- outer/layout/layout-specific properties have structure/geometry impact;
- color/background/typography have paint impact;
- ECSS preserves that impact classification;
- TP consumes resolved paint contributions only;
- structural changes transactionally replace the required layout context.
- impact is a set, not a mutually exclusive enumeration;
- `:key` belongs to identity, not visual properties;
- `:class` enters only the ECSS selector; `:id` is one author input that may project to
both the ECSS selector and semantic metadata, but two modules must not define its
meaning independently;
- contributions such as color/background that do not alter metrics are paint-only;
- metric properties such as font family/size/weight are geometry + paint;
- padding, border width, and similar properties are geometry + paint;
- `:outer` and the Layout variant are structure + geometry;
- Flex/Grid participation is stored on the child Box, but a change dirties only the
corresponding parent layout owner for geometry;
- ECSS cascade output must retain the impact set above;
- the ETAF/Ebox candidate normalization pipeline parses each property fact once, then
passes it by owner/impact to Runtime metadata, ECSS, Ebox geometry, and the appropriate
TP adapter; owners do not reparse the original plist;
- a multi-impact fact such as border or font may project to both geometry and paint
fields, but its computed value materializes only once and paint-only state is not
duplicated in the Ebox node;
- TP consumes only already-parsed paint contributions and does not determine layout
validity;
- a structural property change must transactionally rebuild the corresponding layout
context and cannot take the paint-only fast path.
## 4. Module owners
Validation has two deterministic phases:
### ETAF Core
1. Normalization performs form-local schema validation. `box` accepts only
Normal/Common Box properties; only `flex` accepts Flex container config; only `grid`
accepts Grid container config. Other layout-specific properties fail immediately.
2. After Component/Fragment resolution and ECSS cascade, Ebox performs parent-context
validation on the candidate final tree. Flex participation
(grow/shrink/basis/order/self alignment) is allowed only on a child Box whose direct
parent is Flex. Grid placement/self alignment is allowed only on a child Box whose
direct parent is Grid. A root or wrong parent is an error.
`etaf-view` owns normalized View shapes, string-to-Text normalization, closed property
schemas, slots, keys, and expression boundaries. It does not measure, lay out, or write
buffers.
The same Box may establish one Layout internally while participating in a different
parent Layout externally—for example, `(flex :grid-column ... )` under Grid. These are
two orthogonal fields, not a property conflict. When a dynamic transaction changes both
parent Layout and child participation, only the final candidate is validated; any
failure preserves the previous generation. Unlike browsers, which commonly treat an
inapplicable CSS property as having no effect, this framework does not silently ignore
invalid combinations.
`etaf-component` owns Component props and `:view`/`:setup`/`:styles` definitions. It
does not schedule Runtime or call Ebox.
## 4. Module Owners
`etaf-runtime` owns Component/Fragment/Range identity, reactive dependencies, Context,
Theme, Actions, Behaviors, Data, Resources, lifecycle, candidate generations, commit
authority, rollback, and disposal. It does not compute pixels.
### 4.1 ETAF Core
`etaf-renderer` is the only ETAF-to-Ebox lowering adapter. It consumes normalized View
and computed properties without accessing Ebox private state.
`etaf-view`:
Theme token meaning and inheritance belong to ETAF Context, not TP.
- defines and normalizes Text, Box, Fragment, and ComponentCall;
- normalizes strings to Text;
- normalizes the `row`, `column`, `flex`, and `grid` author sugars to Box;
- validates closed property sets, slots, `:key`, and expression boundaries;
- does not measure, lay out, or write buffers.
### Ebox
`etaf-component`:
Ebox owns Text measurement/wrapping/line layout, normal/row/column/flex/grid geometry,
the box model, overflow/scroll/viewport behavior, stable Ebox node identity, geometry
snapshots, and structural patch plans. It does not know Component semantics or paint
contribution priority.
- defines Component props, `:view`, `:setup`, and `:styles`;
- does not schedule Runtime or call Ebox.
`etaf-runtime`:
- owns Component/Fragment/Range identity, reactive dependencies, Context, Theme, Action,
Behavior, Data, Resource, and lifecycle;
- owns candidate generation, transactions, commit authority, rollback, and disposal;
- decides which Component, Range, structural property, or paint contribution a change
affects;
- does not compute pixels or layout.
`etaf-renderer`:
- is the only ETAF → Ebox lowering adapter;
- converts the structure/geometry projection of ResolvedView into a
CanonicalEboxInput forest, RangeAnchors, and typed measurement/geometry properties;
- does not execute Component lifecycle or access Ebox private state.
`etaf-theme-tp`:
- is the only adapter from ETAF/ECSS computed paint contributions to TP;
- produces ContributionPlan without measuring, laying out, or owning TP priority/commit
authority;
- does not call `etaf-renderer` geometry lowering for a paint-only candidate.
Theme semantic tokens and inheritance belong to ETAF Context. TP does not own business
meaning such as “dark theme.”
### 4.2 Ebox
Ebox owns:
- normalization of String/Text/Box and layout-sugar expansion in the Ebox DSL;
- contextual validation, parent-layout consumption, and dirty propagation of child Box
participation;
- typed TextNode/BoxNode constructors and canonical property schemas;
- text measurement, wrapping, and line layout;
- the `normal`, `row`, `column`, `flex`, and `grid` geometry algorithms;
- width/height, the box model, overflow, scrolling, and viewport behavior;
- stable Ebox node identity, geometry snapshots, and structural patch plans.
Ebox does not understand Component, slot, Context, Action, Behavior, Data, or Resource,
and does not determine paint-contribution priority.
Author paint from the standalone Ebox DSL is projected by the `ebox-surface` TP adapter
into an inline ContributionPlan. It and ETAF's `etaf-theme-tp` are two source adapters
that consume the same public TP contribution contract. Neither puts paint-only
properties back into CanonicalEboxInput, nor owns TP priority, journal, or commit
authority.
`ebox-build` exclusively owns parsing/desugaring author forms. `ebox-text-create` and
`ebox-box-create` accept already evaluated canonical parameters and validate only typed
node invariants. All three ultimately consume the same canonical property schema and
enter the same layout implementation. The ETAF Renderer calls the typed programmatic
port directly; it must not reconstruct DSL and trigger a second parse. Existing untyped
`ebox-create :content` is not part of the target public port.
Logical boundaries:
```text
ebox-core pure measurement, layout, snapshots, structural patches
ebox-emacs font/window capabilities, buffer positions, structural submission
ebox-core pure measurement, layout, snapshots, and structural patches
ebox-emacs font/window capabilities, buffer positions, and structural commit
```
These need not become separate distribution packages immediately, but must remain
independently testable.
These boundaries need not immediately become two distribution packages, but pure
layout and Emacs side effects must be independently testable.
### ECSS
Measuring the CanonicalEboxInput forest produces a LayoutPlan. The plan contains only
measurement results, coordinates, dimensions, layout owners, and geometry deltas needed
by the candidate. It introduces no author-visible node type and owns no
Component/Text/Box identity. After commit or rollback, it may be retained as input to
the next incremental update or released.
ECSS owns selectors, cascade, inheritance, and computed properties. Its output retains
structure/geometry/paint impact. ECSS does not run layout or write buffers.
### 4.3 ECSS
### TP and commit boundary
ECSS owns selectors, cascade, inheritance, and computed properties. Its output must
carry each property's structure/geometry/paint impact. ECSS neither performs layout nor
writes buffers.
TP owns paint-contribution priority, paint-operation merging, Emacs text-property
journaling, application, and rollback.
### 4.4 TP and the Commit Boundary
TP owns:
- the priority of paint contributions from Theme, Component style, state, inline style,
and other sources;
- paint-operation merging;
- the Emacs text-property journal, apply, and rollback.
The authority order for one update is fixed:
```text
ETAF Runtime creates a candidate and owns commit authority
ETAF Runtime creates the candidate and owns commit authority
Ebox stages structural/geometry patches
Ebox candidate stage produces the structure/geometry patch
TP stages paint patches
Ebox surface adds a rollback-capable participant to the existing TP transaction
Emacs adapter atomically applies both
TP precommits paint; Ebox commit publishes the Emacs surface
ETAF Runtime promotes the generation; failure rolls participants back
Only after all succeed does ETAF Runtime promote the generation
Any failure rolls back through the existing Ebox/TP/Runtime journals
```
Ebox does not own TP priority, TP does not own layout, and neither participant may
promote a Runtime generation.
Ebox does not own TP paint priority; TP does not own node layout; neither may promote a
Runtime generation independently. The target introduces no second cross-package
coordinator. The existing transaction participant is the sole mechanism aligning the
Ebox surface with TP/Emacs commit.
### Upper packages
### 4.5 Upper-Level Packages
| Package | Owns | Does not own |
| --- | --- | --- |
| `etaf-ui` | ordinary reusable Components | another Runtime or layout engine |
| `etaf-sqlite` | SQLite Data Source and its transactions/disposal | Data Controller or UI |
| `etaf-performance` | application-neutral operations/stages/statistics/reports | example semantics or scheduling authority |
| `ebox-playground` | public Ebox examples/verification | ETAF |
| `etaf-playground` | ETAF/UI/SQLite composition examples and tooling | core protocols |
| `etaf-ui` | ordinary Components such as Button, Checkbox, Label, Panel, and DataGrid | a second Widget Runtime or layout engine |
| `etaf-sqlite` | SQLite Data Source, queries, mutations, transactions, and disposal | Data Controller, View, or UI |
| `etaf-performance` | application-neutral operations/stages, environment, statistics, and reports | example semantics, layout rules, or scheduling authority |
| `ebox-playground` | examples and verification of the public Ebox layout API | ETAF |
| `etaf-playground` | ETAF/UI/SQLite composition examples, loading commands, and status display | Core syntax, Runtime, or performance protocols |
`etaf-performance` is a separate optional package. No measured package depends on it;
it observes only public boundaries. A Playground `.etaf` manifest belongs to the
Playground. Any future general compiler must lower to the same View/Component contract.
`etaf-performance` is an independent optional package. ETAF, Ebox, TP, and SQLite do not
depend on it; it observes and correlates stages only through public boundaries.
## 5. Dependency direction
The Playground `.etaf` manifest belongs to the Playground. If a general compiler exists
in the future, it must lower to the same View/Component contract and must not create a
second Runtime.
## 5. Dependency Direction
```text
etaf-ui ─────────────▶ ETAF Core ─────────────▶ Ebox public port
@ -266,72 +573,110 @@ ebox-playground ─────▶ Ebox only
etaf-performance ────▶ public observation boundaries only
ETAF Renderer ───────▶ ECSS computed properties
Ebox style pipeline ─▶ ECSS computed-properties port
ETAF Runtime ───────▶ Ebox/TP transaction participants
etaf-theme-tp ───────▶ TP contribution contract
Ebox surface ────────▶ TP inline contribution + transaction participant
```
Reverse dependencies are forbidden: Ebox never depends on ETAF, TP never parses View,
ECSS never writes buffers, UI never calls Ebox private APIs, SQLite never knows UI, and
Playgrounds never inject protocols into core.
Reverse dependencies are forbidden: Ebox does not depend on ETAF; TP does not parse
View; ECSS does not write buffers; UI does not call Ebox private functions; SQLite does
not know UI; and Playground does not inject protocols into Core.
## 6. Rust and Elisp
Boundaries precede implementation language. Deterministic kernels may move to Rust only
after contracts and equivalence tests are stable:
Module boundaries precede implementation language. Deterministic computation moves to
Rust only after interfaces and equivalence tests are frozen:
```text
Rust candidates:
normalized View validation and structural diff over opaque stable IDs
normalized View validation and structural diff (over opaque stable IDs only)
ECSS cascade
Text-measurement input processing and Ebox layout
pure geometry/paint patch computation
Text measurement input processing and Ebox layout
pure-data computation of geometry/paint patches
Elisp / Emacs:
user Components, refs, Context, Actions, Data, Resources, lifecycle
execute user Components, refs, Context, Action, Data, Resource, and lifecycle
database and external I/O
Emacs events, font/window capabilities, and final submission
Emacs events, font/window capabilities, and final commit
```
Component identity, dependency ownership, TP priority, and generation authority do not
move merely because a kernel is written in Rust. Elisp must not repeat Rust diff,
cascade, layout, or text scans.
move merely because implementation moves to Rust. Rust may at most compute side-effect-
free candidate paint operations; TP continues to own contribution slots, priority,
journaling, and commit/rollback. Elisp does not repeat diff, cascade, layout, or text
scans already completed by Rust.
## 7. Correctness and performance invariants
## 7. Correctness and Performance Invariants
- View, computed properties, geometry snapshots, and paint contributions materialize
once per transaction;
- Text paint changes do not execute unrelated Components or layout;
- outer/layout/geometry changes affect only the required layout owner;
- Fragment/slot/list changes replace only their Range;
- Ebox/TP/Emacs participant failure preserves the last committed generation;
- resize uses current window facts immediately, without hidden debouncing semantics;
- instrumentation does not change the measured path.
- View, computed properties, geometry snapshots, and paint contributions each
materialize once per transaction;
- a Text paint-only contribution change neither executes unrelated Components nor runs
layout;
- changes to `:outer`, the Layout variant, or geometry run only the affected layout
owner;
- Fragment/slot/list changes replace only the corresponding Range;
- failure of any Ebox/TP/Emacs participant preserves the previous committed generation;
- resize reflows immediately from current window facts and does not change interaction
semantics through hidden debouncing;
- the performance recorder does not change the measured path.
Final gates cover structural equivalence, legal/illegal outer-layout combinations,
normal inline/block behavior, row/column/flex/grid GUI behavior, Component roots and
Fragments, style/Theme/TP priority and rollback, continuous Research Shelf/Flex
reference resize, and p95/max at or below 50ms in fixed real scenarios.
The final gates must cover:
## 8. Current gap and non-goals
- structural equivalence of Text/Box/Fragment;
- strings normalize only to Text, and both author DSLs reject `box :content`;
- Text is always inline and has exactly one string payload; nested, empty, or multiple
payloads are errors;
- canonical shape, defaults, and errors of the five ETAF/Ebox Box forms agree across
interfaces;
- the five Box author forms each produce their one corresponding Layout, and every
author `:layout` is an error;
- Flex/Grid participation is permitted only on a child Box under the matching parent;
the wrong context reports a precise error;
- a participation change runs only the parent layout and does not rebuild child identity
or lifecycle;
- typed Text/Box constructors reject mutually exclusive fields and layout sugar, and
share their result with `ebox-build`;
- CanonicalEboxInput contains no paint-only fields; the ETAF and standalone Ebox paint
adapters each project exactly once;
- sourceHandle and eboxNodeId have independent lifetimes, and reconciliation does not
change source identity;
- legal and failing `outer × layout` combinations;
- GUI behavior of normal inline/block, row, column, flex, and grid;
- layout participation of Component roots and transparent Fragments;
- empty, single-root, and multi-root Fragments all retain RangeAnchor and support local
replacement;
- a paint-only candidate does not construct LayoutPlan, while geometry and paint patches
join in the existing transaction;
- style/Theme/TP priority and transactional rollback;
- continuous resize of Research Shelf and Flex reference;
- both p95 and max at or below 50 ms in fixed real scenarios.
Current code still registers `text`, `fragment`, `container`, `row`, `column`, `stack`,
`flex`, `grid`, and `spacer`, and exposes `raw-ebox`. Target `box`, `:outer`, and
`:layout 'normal` are not implemented, and the target public View does not retain
## 8. Current Implementation Gaps and Non-Goals
The current code still registers `text`, `fragment`, `container`, `row`, `column`,
`stack`, `flex`, `grid`, and `spacer`, and provides `raw-ebox`. Target `box`, `:outer`,
and typed Layout are not yet implemented; the target public View also does not retain
`raw-ebox`.
Current Ebox DSL still accepts `box`, `ebox`, `row`, `column`, `flex`, `item`, `grid`,
`grid-item`, and `spacer`. The target Ebox DSL keeps only `box`; all other tags are
removed or represented as Box properties according to the table above.
The current Ebox DSL still encodes `box :content` and bare strings directly as untyped
`ebox-create :content`, and constructs `item` and `grid-item` as runtime wrappers. The
target adds an explicit TextNode/BoxNode canonical IR and typed constructors; retains
Text, Box, and the five Box author forms; and removes `:content` from author/public
constructor syntax as well as removing `ebox`, `spacer`, `item`, and `grid-item`.
This is a clean redesign: old Host names and old `.etaf` sources need not continue to
run, and no long-lived compatibility layer is added. Formal architecture/user docs must
not present target syntax as delivered before implementation is complete.
This is a clean redesign. It does not require old Host names or old `.etaf` files to
continue working, and it adds no long-lived compatibility layer. Until implementation
is complete, the formal architecture and user guide must not describe target syntax as
the current API.
Non-goals:
- full browser CSS;
- spacer, public flow, or `(box :content ...)`;
- injecting raw Ebox Nodes into View;
- layout modes as Components or alternate Runtime nodes;
- old Host aliases or optional layout shorthand in core;
- core protocols owned by Playground/UI/database packages;
- App precompilation without measured compile cost and runtime benefit.
- implementing full browser CSS;
- providing `spacer`, public `flow`, or `(box :content ...)` in the author DSL;
- making Flex/Grid item an author node, visual wrapper, or independent identity;
- allowing applications to inject raw Ebox Nodes into View;
- making layout modes Components or a second set of Runtime nodes;
- retaining old Host aliases or optional shorthand spellings in Core;
- allowing Playground, UI, or database packages to own Core protocols;
- adding App precompilation artifacts before real compile cost and runtime benefit exist.

View File

@ -13,23 +13,70 @@ Text、Box、ComponentFragment、布局细节和底层包通过渐进披露
## 1. 最终模型
ETAF 的表示链只有四层:
ETAF/Ebox 的表示链按职责分层:
```text
Component 语义与所有权
ViewText / Box / Fragment
Ebox测量、布局、几何 patch
TP + Emacs adapterpaint 与最终提交
AuthorView
↓ parse / desugar / string normalization
NormalizedView = Text | Box | Fragment | ComponentCall
↓ ETAF Runtime resolves ComponentCall
Resolved candidate + computed properties
├─ structure/geometry impact
│ ↓ Fragment projection preserves RangeAnchors
│ CanonicalEboxInput = Forest<TextNode | BoxNode> + RangeAnchors
│ ↓ Ebox measure / layout
│ LayoutPlan → GeometryPatch
└─ paint impact
↓ source-specific contribution adapter
(ETAF: etaf-theme-tp; standalone Ebox: ebox-surface-tp)
TP ContributionPlan → PaintPatch
GeometryPatch and PaintPatch join the existing TP transaction
↓ Emacs adapter commit / rollback
Published generation
```
这些层不是相互竞争或重复 materialize 的树。每次 lowering 只消费上层已经保留的
identity、computed properties 和 child 顺序;同一事实不重复解析或推导。
LayoutPlan 与 ContributionPlan 是同一候选事务的独立派生产物paint-only path 不
构造 CanonicalEboxInput 或运行 layout。两条路径在现有 transaction participant
汇合。plan 不拥有作者 identity也不是 DSL 节点。
Canonical node 的非法状态必须不可表达:
```text
TextNode = {
value: String,
measurementProps,
sourceHandle,
eboxNodeId
}
BoxNode = {
outer: inline | block,
layout: Normal | RowConfig | ColumnConfig | FlexConfig | GridConfig,
geometryProps,
parentParticipationProps,
children: [TextNode | BoxNode],
sourceHandle,
eboxNodeId
}
```
TextNode 没有 children/outer/layoutBoxNode 没有 content/value。participation 字段
存储在 BoxNode但只有直接父 formatting context 可以验证和解释它。
`sourceHandle` 是来源层拥有的 opaque handleETAF 路径指向 Runtime
Text/Box/Range source独立 Ebox DSL 路径指向 Ebox author source。`eboxNodeId` 只
标识 Ebox retained geometry node。Renderer/surface 保留二者映射Ebox reconciliation
不得接管 Component、Fragment、Range 或 lifecycle identity。
用户需要理解的主要概念只有 `Text`、`Box` 和 `Component`。`Fragment` 是高级
结构语法;`Host` 是 Renderer 内部术语;`Ebox Node` 是后端对象,都不构成第二套
组件模型。
规范化 View 的完整形状是:
NormalizedView 的完整形状是:
```text
View = Text
@ -38,6 +85,31 @@ View = Text
| ComponentCall
```
作者语法和规范化 View 不是同一层。ETAF 与 Ebox 的视觉作者语法共享:
```text
AuthorNode = String
| TextForm
| BoxForm
| RowSugar
| ColumnSugar
| FlexSugar
| GridSugar
```
`String` 规范化为 Text五个 Box form 规范化为带对应 typed Layout 的 Box。form
名称不进入
Runtime identity、diff、style、layout 或 paint 协议。ETAF 另外增加 Fragment、
ComponentCall、`expr` 和 `slot`Ebox DSL 不拥有这些语义。
两层作者语法共享结构词汇和规范化结果不要求共享求值环境ETAF property 是
Elisp expression`.ebox` DSL 是数据 form。quote 和 expression 的差异只属于
parser不产生两套 canonical schema。
Flex/Grid item 是 material child Box 在父 formatting context 中获得的角色,不是
另一种 View 或父子边类型。participation 属性存储在 child Box合法性、计算和
dirty propagation 由直接父 Flex/Grid Box 拥有。
`expr``slot` 是计算/投影机制,不是视觉节点。目标公共 View 不接受原始 Ebox
节点;缺失的渲染能力必须先形成有类型、有 identity/impact 契约的 View/Ebox 能力,
不能通过 opaque escape 绕过框架语义。
@ -48,14 +120,26 @@ View = Text
`Text` 是文本叶子,负责:
- 字符串和 inline Text runs
- 恰好一个求值后为字符串的 payload
- 字体、前景/背景、下划线等文本 paint
- 文字测量、换行和文本对齐的输入;
- 可选 identity、语义和事件属性。
`Text` 默认 `:outer 'inline`。它不能拥有 Box、Component 或任意布局子节点,也
不能建立 `row`、`column`、`flex` 或 `grid` 上下文。需要 padding、border、尺寸
或子布局时,在外层使用 `Box`
`Text` 固定为 inline不公开 `:outer`。它没有 View children不能嵌套 Text
也不能建立任何布局上下文。多个 styled runs 是相邻的多个 Text而不是一棵 Text
子树。需要 block participation、padding、border、尺寸或子布局时使用外层 Box。
两个作者入口的 Text grammar 精确区分求值能力:
```text
ETAF TextForm = (text TEXT-PROP VALUE)
VALUE = String | (expr :value Expr<String>)
Ebox TextForm = (text TEXT-PROP String)
```
所有属性键值对必须位于唯一 payload 之前。零个、多个、嵌套 Text 或非字符串
payload 都报错。裸字符串只是无属性 Text 的语法糖,在所有位置只有这一种解释。
### 2.2 Box 的两个正交轴
@ -63,27 +147,34 @@ View = Text
```text
:outer = inline | block
:layout = normal | row | column | flex | grid
layout = Normal | RowConfig | ColumnConfig | FlexConfig | GridConfig
```
- `:outer`:这个 Box 如何参与父级的 `normal` 布局;
- `:layout`:这个 Box 如何排列自己的子节点。
- `layout` variant:这个 Box 如何排列自己的子节点。
默认值是:
```text
Box :outer block :layout normal
Text :outer inline
Box :outer block :layout Normal
Text :fixed inline
```
`inline-flex` 是两个轴的组合,不是新的节点类型
作者层不公开 `:layout` 属性,而由 form 名唯一选择 layout
```elisp
(box :outer 'inline :layout 'flex ...)
(box ...) ; Normal
(row ...) ; RowConfig
(column ...) ; ColumnConfig
(flex ...) ; FlexConfig
(grid ...) ; GridConfig
```
公开 API 使用 `:outer``:layout`。不公开 `:inner`,也不要求用户写 Ebox 的
`(:display (block flow))` 一类后端表示。
因此 inline-flex 写为 `(flex :outer 'inline ...)`,不是新节点类型。`:outer` 是所有
Box author form 的公共属性layout config 由对应 form 的闭集属性构造。需要动态
切换 layout 时,由 `expr` 选择不同 View form而不是把 `:layout` 当普通动态样式。
Canonical Box 使用 typed Layout variant不公开 `:inner` 或 Ebox
`(:display (block flow))` 后端表示。
### 2.3 normal 的明确范围
@ -98,7 +189,7 @@ Text :outer inline
run-in、list-item 或任意匿名盒规则。Ebox 可以把 `normal` 映射为内部 flow
算法,但 `flow` 不是 ETAF 公共词汇。
当父级 `:layout``row`、`column`、`flex` 或 `grid` 时,父算法直接拥有子项
当父级 Layout 是 Row/Column/Flex/Grid 时,父算法直接拥有子项
位置;子项的 `:outer` 不改变排列顺序。`:outer` 只决定节点进入 `normal` 父级时
是 inline 还是 block。
@ -111,28 +202,58 @@ run-in、list-item 或任意匿名盒规则。Ebox 可以把 `normal` 映射为
| `flex` | Flex sizing、direction、wrap、alignment 和 gap |
| `grid` | 二维轨道、放置、跨度和 gap |
这些都是 `Box`layout mode,不是 Component也不注册 `row`、`column`、
`flex``grid` 的第二套 Runtime 节点名称。目标 API 只有规范写法
这些都是 `Box`Layout variant,不是 Component也不注册 `row`、`column`、
`flex``grid` 的第二套 Runtime 节点名称。作者 form 的规范化映射唯一
```elisp
(box :layout 'row ...)
(box :layout 'column ...)
(box :layout 'flex ...)
(box :layout 'grid ...)
```text
box → Box(layout = Normal)
row → Box(layout = RowConfig)
column → Box(layout = ColumnConfig)
flex → Box(layout = FlexConfig)
grid → Box(layout = GridConfig)
```
ETAF 与 Ebox 共享这五个 Box author form。所谓“layout form”只表示输入拼写和
对应 config schema不表示保留 Host、node type 或运行时分派。
上表是唯一规范性 lowering 表。Ebox 拥有 canonical Layout variant 和 config
schemaETAF parser 只处理自己的 Elisp 求值并选择同名 variant不复制取值规则。
两个入口必须通过跨接口 conformance 测试证明同一结构产生相同 canonical shape、
computed defaults 和错误。
### 2.5 Ebox DSL 的基础节点与语法糖
ETAF View 和 Ebox DSL 是两层不同接口ETAF 有 Text/Box/Fragment/Component
Ebox 只接收已经降低的内容与布局。目标 Ebox DSL 只有一个基础结构 tag
ETAF View 和 Ebox DSL 是两层不同接口但共享同一个视觉作者语法Text 是文本
叶子Box 是结构/几何容器。Ebox DSL 的完整视觉语法是
```elisp
(box :layout MODE ...)
"plain text"
(text TEXT-PROPS "styled text")
(box BOX-PROPS CHILD...)
(row BOX-PROPS CHILD...)
(column BOX-PROPS CHILD...)
(flex BOX-PROPS CHILD...)
(grid BOX-PROPS CHILD...)
```
Ebox `box` 可以是 content leaf也可以包含 children 并建立布局上下文。Range
descriptor 是增量后端协议,不是作者 DSL 节点;裸字符串只是 content literal
简写,也不是节点类型。
字符串始终规范化为 Text`text` 只接受文本内容和 Text 属性;`box` 只接受
children 和 Box 属性。Ebox DSL 不接受 `:content`,因此不存在“字符串有时是
Box content、有时又是 child”的双重解释。空 `(box :width ... :height ...)`
合法的空几何容器。
`ebox-build` 是作者 DSL 的 parse/desugar 入口。程序化公共端口使用两个 typed
constructor`ebox-text-create` 构造 TextNode`ebox-box-create` 接收一个 typed
Layout variant 并构造 BoxNode。constructor 不接受作者 form 或扁平的跨 layout
属性袋ETAF Renderer 直接调用它们,不重新拼 DSL。
目标公共端口不保留无 kind 的 `(ebox-create :content ...)`。若运行时为了性能把
Text payload 融合进相邻 Box 存储,该优化只能发生在 canonical Text identity、
range 和 impact 已建立之后,并且必须可逆地保留这些事实;私有存储编码不能成为
作者或程序化公共 API。Range descriptor 同样是增量后端协议,不是 DSL 节点。
五个 Box author form 必须在 DSL 边界立即、无状态地构造对应 typed Layout variant
不能建立不同的 node type、identity、缓存、验证或渲染分支。任何作者 form 都拒绝
`:layout`layout-specific 属性只由对应 form 接受。
当前 DSL 的其他 tag 必须明确分类,不能与基础节点并列:
@ -140,29 +261,34 @@ descriptor 是增量后端协议,不是作者 DSL 节点;裸字符串只是
| --- | --- | --- |
| `ebox` | `box` 的兼容别名 | 删除,统一写 `box` |
| `spacer` | 空 Box 便捷糖 | 删除,使用无 children 的 `box` |
| `row` | layout tag 糖 | `(box :layout 'row ...)` |
| `column` | layout tag 糖 | `(box :layout 'column ...)` |
| `flex` | layout tag 糖 | `(box :layout 'flex ...)` |
| `grid` | layout tag 糖 | `(box :layout 'grid ...)` |
| `item` | Flex child participation 糖 | 把 `:flex-*`、`:order`、`:align-self` 直接写在 child Box |
| `grid-item` | Grid child placement 糖 | 把 `:grid-*` placement 直接写在 child Box |
| `row` | Box layout form | 保留并构造 RowConfig |
| `column` | Box layout form | 保留并构造 ColumnConfig |
| `flex` | Box layout form | 保留并构造 FlexConfig |
| `grid` | Box layout form | 保留并构造 GridConfig |
| `item` | Flex wrapper/糖 | 删除participation 属性直接写在 child Box |
| `grid-item` | Grid wrapper/糖 | 删除placement 属性直接写在 child Box |
因此 `item``grid-item` 不是必要 wrapper。父级参与属性属于 child Box并由
父级 layout mode 验证
Flex/Grid 直接 child 自动取得 item 角色item 不是节点类型。需要非默认参与方式时,
唯一推荐路径是显式 child Box
```elisp
(box :layout 'flex
(box :flex-grow 1 :content "A"))
(flex
(box :flex-grow 1 "Flexible")
"Fixed")
(box :layout 'grid
(box :grid-column '(1 :span 2) :content "Header"))
(grid
(box :grid-column '(1 :span 2) "Header")
(box "Body"))
```
以上示例是 Ebox 低层 DSL因此可以使用 Ebox 的 `:content`ETAF View 仍然只用
Text/children不暴露 `:content`
participation/placement 属性存储在 child Box但只由直接父 Flex/Grid 验证和消费;
出现在错误父级时必须报错。属性变化使父布局 owner 进入 geometry path但不改变
child Component/Text 的 identity、lifecycle 或 paint ownership。直接 Text 使用
默认 item 参数;需要非默认参数时显式包 Box。
目标 Ebox DSL 不保留上述兼容别名或糖。布局算法仍由 Ebox 分别实现,但节点
分类只保留 `box`;算法种类不等于作者节点种类。
目标 Ebox DSL 删除 `ebox`、`spacer`、`item` 和 `grid-item`,保留 Text、Box 与
四个布局糖。布局算法仍由 Ebox 分别实现,但 canonical 视觉节点只保留
TextNode/BoxNode作者语法、canonical tree、layout plan 和运行时存储是不同层次。
### 2.6 Fragment
@ -173,7 +299,13 @@ Text/children不暴露 `:content`。
- Runtime 可以为其保留稳定 Range identity 并局部替换;
- 只接受子节点和可选稳定 `:key`,不接受视觉/事件属性。
Component call 本身没有 `:outer``:layout`。它的根 Text/Box 决定如何参与父级
Fragment resolution 不把 Range identity 丢掉。它把 material children 按顺序 splice
进 CanonicalEboxInput forest同时产生 `{range-id, before, after}` RangeAnchor
Fragment 仍有可插入锚点,根 Fragment 可以对应零个或多个 forest roots。Range
identity 由 ETAF Runtime 拥有Ebox 只消费 descriptor 来形成局部 geometry patch
RangeAnchor 不是视觉 node也不参与测量。
Component call 本身没有 `:outer` 或 Layout。它的根 Text/Box 决定如何参与父级
布局;返回 Fragment 的透明 Component 可以产生多个兄弟节点,因此不存在单一的
“Component 外部盒子”。
@ -191,8 +323,9 @@ Box 子节点中的字符串规范化为 Text
(box (text "this is text"))
```
ETAF 不提供 `(box :content "...")`。`:content` 只属于 Ebox 后端构造器;暴露它
会制造 content 与 children 两套结构模型。
ETAF 与 Ebox 作者 DSL 都不提供 `(box :content "...")`。`:content` 只属于
现有未类型化运行时存储,目标公共程序化端口也不保留它。把 `:content` 暴露给
作者 DSL 或 canonical constructor 都会重新制造 content 与 Text children 两套模型。
ETAF 也不提供 `spacer` 类型。没有子节点的 Box 就是空 Box。
@ -202,25 +335,52 @@ ETAF 也不提供 `spacer` 类型。没有子节点的 Box 就是空 Box。
| Owner | 属性类别 | 代表属性 |
| --- | --- | --- |
| Text | 外部参与 | `:outer` |
| Text | 文本/paint | `:face`、`:color`、`:bgcolor`、`:wrap-mode`、`:text-align` |
| Box | 外部/内部布局 | `:outer`、`:layout` |
| Box | 几何/表面 | `:width`、`:height`、`:min-*`、`:max-*`、`:margin`、`:padding`、`:border`、`:box-sizing`、`:overflow` |
| ETAF Runtime metadata | identity/语义/事件/Behavior | `:key`、`:ref`、`:role`、`:aria-*`、`:on-*`、`:use` |
| ECSS author style source | selector/inline declarations | `:class`、`:id`、style declarations |
| Ebox TextNode | canonical 文本测量 | font metrics、wrap policy、intrinsic constraints |
| Ebox BoxNode | 外部/内部布局 | `:outer`、typed Layout variant |
| Ebox BoxNode | 几何 | width/height/min/max、margin、padding、border widths、box sizing、overflow |
| Flex container | 布局 | `:flex-direction`、`:flex-wrap`、`:justify-content`、`:align-items`、`:align-content`、`:gap` |
| Flex item | 父级参与 | `:order`、`:flex-grow`、`:flex-shrink`、`:flex-basis`、`:align-self` |
| Flex child Box | 父级参与 | `:order`、`:flex-grow`、`:flex-shrink`、`:flex-basis`、`:align-self` |
| Grid container | 布局 | track templates、auto flow、gap、item/content alignment |
| Grid item | 父级参与 | `:grid-row`、`:grid-column`、row/column span |
| Text/Box | identity/语义 | `:key`、`:ref`、`:class`、`:id`、`:role`、`:aria-*`、`:on-*`、`:use` |
| Grid child Box | 父级参与 | `:grid-row`、`:grid-column`、row/column span、self alignment |
| TP contribution | paint | color/background、underline/overline、border paint、visibility paint |
规则:
- `:key` 是 identity metadata不是视觉属性
- `:outer`、`:layout` 和 layout-specific 属性属于 structure/geometry impact
- color、background、typography 等属于 paint impact
- ECSS 可以解析上述属性,但必须保留 impact 分类;
- impact 是集合,不是互斥枚举;
- `:key` 属于 identity不是视觉属性
- `:class` 只进入 ECSS selector`:id` 是一个 author input可同时投影到 ECSS
selector 和语义 metadata但不能由两个模块分别定义含义
- color/background 等不改变 metrics 的贡献属于 paint-only
- font family/size/weight 等 metric 属性属于 geometry + paint
- padding、border width 等属于 geometry + paint
- `:outer`、Layout variant 属于 structure + geometry
- Flex/Grid participation 存储在 child Box但变化时只使对应父布局 owner geometry dirty
- ECSS cascade 输出必须保留上述 impact set
- ETAF/Ebox candidate normalization pipeline 只解析一次 property fact再按 owner/impact
交给 Runtime metadata、ECSS、Ebox geometry 和对应 TP adapter各 owner 不重新解析
原 plist
- border/font 等 multi-impact fact 可以同时投影 geometry 与 paint 字段,但 computed
value 只 materialize 一次,不在 Ebox node 中复制 paint-only 状态;
- TP 只消费已经解析的 paint contribution不判断布局合法性
- 结构属性变化必须事务性重建对应布局上下文,不能走 paint-only 快速路径。
验证分成两个确定阶段:
1. normalization 做 form-local schema 校验。`box` 只接受 Normal/Common Box 属性;
`flex` 才接受 Flex container config`grid` 才接受 Grid container config其他
layout-specific 属性立即报错。
2. Component/Fragment resolution 与 ECSS cascade 完成后Ebox 在 candidate final
tree 上做 parent-context 校验。Flex participationgrow/shrink/basis/order/self
alignment只允许在直接父 Flex 的 child BoxGrid placement/self alignment
只允许在直接父 Grid 的 child Boxroot 或错误父级报错。
同一个 Box 可以内部建立一种 Layout、外部参与另一种父 Layout例如 Grid 下的
`(flex :grid-column ... )`;这是两个正交字段,不是属性混用。动态事务同时改变父
Layout 和 child participation 时只验证最终 candidate任何失败保留上一代。与
浏览器对不适用 CSS 属性常采取 no-effect 不同,本框架不静默忽略无效组合。
## 4. 模块 owner
### 4.1 ETAF Core
@ -229,6 +389,7 @@ ETAF 也不提供 `spacer` 类型。没有子节点的 Box 就是空 Box。
- 定义并规范化 Text、Box、Fragment 和 ComponentCall
- 将字符串规范化为 Text
- 将 `row`、`column`、`flex`、`grid` 作者糖规范化为 Box
- 验证属性闭集、slot、`:key` 和表达式边界;
- 不测量、不布局、不写 buffer。
@ -248,15 +409,25 @@ ETAF 也不提供 `spacer` 类型。没有子节点的 Box 就是空 Box。
`etaf-renderer`
- 是唯一 ETAF → Ebox 的 lowering adapter
- 把规范化 View 和 computed properties 转换为 Ebox 输入;
- 把 ResolvedView 的 structure/geometry projection 转换为 CanonicalEboxInput forest、
RangeAnchors 和 typed measurement/geometry props
- 不执行 Component lifecycle不访问 Ebox 私有状态。
`etaf-theme-tp`
- 是 ETAF/ECSS computed paint contribution → TP 的唯一 adapter
- 产生 ContributionPlan不测量、不布局、不拥有 TP priority/commit authority
- paint-only candidate 不调用 `etaf-renderer` 的 geometry lowering。
Theme 的语义 token 和继承属于 ETAF Context。TP 不拥有“暗色主题”等业务意义。
### 4.2 Ebox
Ebox 拥有:
- Ebox DSL 的 String/Text/Box 规范化和布局糖展开;
- child Box participation 的上下文验证、父布局消费和 dirty propagation
- typed TextNode/BoxNode constructor 及 canonical 属性 schema
- Text 测量、换行和行布局;
- `normal`、`row`、`column`、`flex`、`grid` 几何算法;
- width/height、box model、overflow、scroll、viewport
@ -265,6 +436,17 @@ Ebox 拥有:
Ebox 不理解 Component、slot、Context、Action、Behavior、Data 或 Resource也不
决定 paint contribution 的优先级。
独立 Ebox DSL 的 author paint 由 `ebox-surface` 的 TP adapter 投影为 inline
ContributionPlan它与 ETAF 的 `etaf-theme-tp` 是两条来源 adapter共同消费 TP
公开 contribution contract。两者都不把 paint-only 属性塞回 CanonicalEboxInput
也不拥有 TP priority、journal 或 commit authority。
`ebox-build` 独占作者 form 的 parse/desugar`ebox-text-create` 与
`ebox-box-create` 接收已经求值的 canonical 参数,只验证 typed node invariant。
三者最终消费同一个 canonical property schema 并进入同一条布局实现。ETAF
Renderer 直接调用 typed programmatic port不能重新拼 DSL 再触发一次解析。
现有无 kind 的 `ebox-create :content` 不属于目标公共端口。
逻辑边界:
```text
@ -274,6 +456,10 @@ ebox-emacs 字体/窗口能力、buffer 位置和结构提交
这不要求立即拆成两个发行包,但纯布局和 Emacs 副作用必须能独立测试。
CanonicalEboxInput forest 经测量产生 LayoutPlanplan 只包含本次候选所需的测量结果、
坐标、尺寸、布局 owner 和 geometry delta。它不新增作者可见 node type不拥有
Component/Text/Box identity提交或回滚结束后可被保留为下一次增量输入或释放。
### 4.3 ECSS
ECSS 拥有 selector、cascade、继承和 computed properties。输出必须携带属性的
@ -292,17 +478,19 @@ TP 拥有:
```text
ETAF Runtime 创建 candidate 并拥有提交授权
Ebox stage 结构/几何 patch
Ebox candidate stage 结构/几何 patch
TP stage paint patch
Ebox surface 把 rollback-capable participant 加入现有 TP transaction
Emacs adapter 原子应用两个 patch
TP precommit paintEbox commit 发布 Emacs surface
ETAF Runtime 提升 generation失败则回滚参与者
全部成功后 ETAF Runtime 提升 generation
任一步失败则沿现有 Ebox/TP/Runtime journal 回滚
```
Ebox 不拥有 TP paint 优先级TP 不拥有节点布局;两者都不能自行提升 Runtime
generation。
generation。目标不新增第二个跨包 coordinator现有 transaction participant 就是
Ebox surface 与 TP/Emacs 提交对齐的唯一机制。
### 4.5 上层包
@ -330,7 +518,10 @@ ebox-playground ─────▶ Ebox only
etaf-performance ────▶ public observation boundaries only
ETAF Renderer ───────▶ ECSS computed properties
Ebox style pipeline ─▶ ECSS computed-properties port
ETAF Runtime ───────▶ Ebox/TP transaction participants
etaf-theme-tp ───────▶ TP contribution contract
Ebox surface ────────▶ TP inline contribution + transaction participant
```
禁止反向依赖Ebox 不依赖 ETAFTP 不解析 ViewECSS 不写 bufferUI 不调用
@ -354,14 +545,16 @@ Elisp / Emacs
```
Component identity、依赖所有权、TP 优先级和 generation authority 不因 Rust 化而
转移。Elisp 不重复 Rust 已经完成的 diff、cascade、布局或文字扫描。
转移。Rust 最多计算无副作用的候选 paint operationsTP 继续拥有 contribution
slot、优先级、journal 和 commit/rollback。Elisp 不重复 Rust 已经完成的 diff、
cascade、布局或文字扫描。
## 7. 正确性与性能不变量
- 同一事务中的 View、computed properties、geometry snapshot 和 paint contribution
各 materialize 一次;
- Text paint 改变不执行无关 Component也不运行布局
- `:outer``:layout` 或几何改变只运行受影响的布局 owner
- Text paint-only contribution 改变不执行无关 Component也不运行布局
- `:outer`Layout variant 或几何改变只运行受影响的布局 owner
- Fragment/slot/list 变化只替换对应 Range
- Ebox/TP/Emacs 任一 participant 失败时保留上一代已提交状态;
- resize 使用当前窗口事实立即重排,不以隐藏 debounce 改变交互语义;
@ -370,9 +563,20 @@ Component identity、依赖所有权、TP 优先级和 generation authority 不
最终门禁必须覆盖:
- Text/Box/Fragment 结构等价;
- 字符串只规范化为 Text两个作者 DSL 都拒绝 `box :content`
- Text 固定 inline 且恰好一个字符串 payload嵌套/空/多 payload 均报错;
- ETAF/Ebox 五个 Box form 的 canonical shape、defaults 和错误跨接口一致;
- 五个 Box author form 产生唯一对应 Layout任何作者 `:layout` 均报错;
- Flex/Grid participation 只允许在匹配父级的 child Box错误上下文精确报错
- participation 变化只运行父布局,不重建 child identity/lifecycle
- typed Text/Box constructors 拒绝互斥字段和布局糖,并与 `ebox-build` 共享结果;
- CanonicalEboxInput 不含 paint-only 字段ETAF 与独立 Ebox paint adapter 各自只投影一次;
- sourceHandle 与 eboxNodeId 生命周期独立reconciliation 不改变来源 identity
- `outer × layout` 合法组合和失败组合;
- normal inline/block、row、column、flex、grid 的 GUI 行为;
- Component root 与透明 Fragment 的布局参与;
- 空/单根/多根 Fragment 都保留 RangeAnchor 并可局部替换;
- paint-only candidate 不构造 LayoutPlangeometry 与 paint patch 在现有事务汇合;
- style/Theme/TP 优先级和事务回滚;
- Research Shelf 与 Flex reference 的连续 resize
- 固定真实场景 p95 和 max 均不超过 50ms。
@ -381,11 +585,13 @@ Component identity、依赖所有权、TP 优先级和 generation authority 不
当前代码仍注册 `text`、`fragment`、`container`、`row`、`column`、`stack`、
`flex`、`grid` 和 `spacer`,并提供 `raw-ebox`;目标 `box`、`:outer`、
`:layout 'normal` 尚未实现,目标公共 View 也不保留 `raw-ebox`
typed Layout 尚未实现,目标公共 View 也不保留 `raw-ebox`
当前 Ebox DSL 仍接受 `box`、`ebox`、`row`、`column`、`flex`、`item`、`grid`、
`grid-item``spacer`。目标 Ebox DSL 只保留 `box`;其他 tag 按上表删除或改为
Box 属性。
当前 Ebox DSL 仍把 `box :content` 和裸字符串直接编码为无类型
`ebox-create :content`,并把 `item`、`grid-item` 构造成运行时 wrapper。目标新增
明确的 TextNode/BoxNode canonical IR 与 typed constructors保留 Text、Box 和
五个 Box author form删除 `:content` 作者/公共 constructor 语法以及 `ebox`、`spacer`、
`item`、`grid-item`。
这是干净重设计,不要求旧 Host 名称或旧 `.etaf` 文件继续运行,也不增加长期
兼容层。实现完成前,正式架构和用户指南不得把目标语法描述成当前 API。
@ -393,7 +599,8 @@ Box 属性。
非目标:
- 不实现完整浏览器 CSS
- 不提供 `spacer`、公共 `flow``(box :content ...)`
- 作者 DSL 不提供 `spacer`、公共 `flow``(box :content ...)`
- 不把 Flex/Grid item 做成作者节点、视觉 wrapper 或独立 identity
- 不允许应用把原始 Ebox Node 注入 View
- 不把布局模式做成 Component 或第二套 Runtime 节点;
- 不在 Core 中保留旧 Host alias 或可选短写法;