From 30ac302d478e8048ce9d7e972b88fe5e5e979f05 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Wed, 26 Aug 2026 17:12:02 +0800 Subject: [PATCH] docs: freeze typed Text and Box architecture contract --- docs/proposals/module-boundaries.en.md | 731 ++++++++++++++++++------- docs/proposals/module-boundaries.zh.md | 365 +++++++++--- 2 files changed, 824 insertions(+), 272 deletions(-) diff --git a/docs/proposals/module-boundaries.en.md b/docs/proposals/module-boundaries.en.md index cd04523..f959fb9 100644 --- a/docs/proposals/module-boundaries.en.md +++ b/docs/proposals/module-boundaries.en.md @@ -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 + 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) + +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. diff --git a/docs/proposals/module-boundaries.zh.md b/docs/proposals/module-boundaries.zh.md index dc34695..5a6c6f8 100644 --- a/docs/proposals/module-boundaries.zh.md +++ b/docs/proposals/module-boundaries.zh.md @@ -13,23 +13,70 @@ Text、Box、Component;Fragment、布局细节和底层包通过渐进披露 ## 1. 最终模型 -ETAF 的表示链只有四层: +ETAF/Ebox 的表示链按职责分层: ```text -Component 语义与所有权 - ↓ -View:Text / Box / Fragment - ↓ -Ebox:测量、布局、几何 patch - ↓ -TP + Emacs adapter:paint 与最终提交 +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 + 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/layout;BoxNode 没有 content/value。participation 字段 +存储在 BoxNode,但只有直接父 formatting context 可以验证和解释它。 + +`sourceHandle` 是来源层拥有的 opaque handle:ETAF 路径指向 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) + +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 +schema;ETAF 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 participation(grow/shrink/basis/order/self + alignment)只允许在直接父 Flex 的 child Box,Grid placement/self alignment + 只允许在直接父 Grid 的 child Box;root 或错误父级报错。 + +同一个 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 经测量产生 LayoutPlan;plan 只包含本次候选所需的测量结果、 +坐标、尺寸、布局 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 paint;Ebox 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 不依赖 ETAF;TP 不解析 View;ECSS 不写 buffer;UI 不调用 @@ -354,14 +545,16 @@ Elisp / Emacs: ``` Component identity、依赖所有权、TP 优先级和 generation authority 不因 Rust 化而 -转移。Elisp 不重复 Rust 已经完成的 diff、cascade、布局或文字扫描。 +转移。Rust 最多计算无副作用的候选 paint operations;TP 继续拥有 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 不构造 LayoutPlan,geometry 与 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 或可选短写法;