etaf/docs/proposals/module-boundaries.en.md

46 KiB
Raw Blame History

ETAF Module Responsibilities and Target Architecture (Design Proposal, Not Implemented)

Status: design proposal. This document does not describe the currently delivered API. Current behavior remains defined by architecture.en.md, user-guide.en.md, and passing tests.

This document defines only the target model, module owners, dependency direction, and 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 of this proposal: one capability has one canonical identity, canonical name, and recommended spelling. A small set of aliases may exist only as parse-time input sugar and must not become peer public concepts. 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.

0. Design Decision Summary

Ordinary ETAF users learn only Text, Box, Component, and these structural forms:

"plain text"
(text PROPS "styled text")
(box PROPS CHILD...)
(row PROPS CHILD...)
(column PROPS CHILD...)
(flex PROPS CHILD...)
(grid PROPS CHILD...)

row/column/flex/grid are only layout forms of Box. User properties fall into metadata, semantics/events, visual style, and parent participation. They use semantically aligned standard CSS names, a small set of explicit ETAF/Ebox-specific properties, and a small set of parse-time aliases. Users do not interact with the Ebox, TP, or Emacs backend.

Standalone Ebox users access the same visual forms as inert data syntax through ebox-build, but have no Component, Fragment, expr, slot, or lifecycle.

Framework integrators use only typed Text/Box constructors, LayoutConfig, and the render and commit boundaries. They do not reconstruct DSL, rerun cascade, or generate Emacs text properties.

Internal execution has only four layers:

Author API
  → Canonical Model
  → Render Plan (Ebox geometry + TP paint + ETAF semantics)
  → Emacs Adapter commit

Core ownership remains unchanged: ETAF Runtime owns identity/lifecycle/reactivity; ECSS owns selectors/cascade/computed style; Ebox owns measurement/geometry; TP owns paint priority/transactions; only the final adapter owns Emacs text properties such as face, display, and keymap.

Ordinary user documentation does not mention NormalizedView, PropertyDefinition, ProjectionPlan, node IDs, RangeAnchor, or TP slots. The framework integration reference describes typed constructors; only internal architecture sections describe facts, plans, patches, and journals.

1. Final Model

The ETAF/Ebox representation pipeline is layered by responsibility:

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

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.

Illegal canonical node states must be unrepresentable:

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:

View = Text
     | Box
     | Fragment
     | ComponentCall

Author syntax and normalized View are separate layers. ETAF and Ebox share this visual author syntax:

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. 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 text leaf responsible for:

  • 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.

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:

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:

:outer  = inline | block
layout = Normal | RowConfig | ColumnConfig | FlexConfig | GridConfig
  • :outer: how this Box participates in its parent's normal layout;
  • layout variant: how this Box arranges its own children.

The defaults are:

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:

(box ...)                 ; Normal
(row ...)                 ; RowConfig
(column ...)              ; ColumnConfig
(flex ...)                ; FlexConfig
(grid ...)                ; GridConfig

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 Explicit Scope of normal

normal is a real but deliberately bounded layout context:

  • 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.

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.

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

Mode Responsibility
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

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:

box    → Box(layout = Normal)
row    → Box(layout = RowConfig)
column → Box(layout = ColumnConfig)
flex   → Box(layout = FlexConfig)
grid   → Box(layout = GridConfig)

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.

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:

"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; always write box
spacer empty-Box convenience sugar remove; use a childless 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

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:

(flex
  (box :flex-grow 1 "Flexible")
  "Fixed")

(grid
  (box :grid-column '(1 :span 2) "Header")
  (box "Body"))

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 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 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.

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.

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.”

2.7 Strings, Content, and Empty Boxes

A string child of a Box normalizes to Text:

(box "this is text")

is equivalent to:

(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
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

Rules:

  • 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;
  • ETAF/Ebox candidate normalization parses the author plist once and produces canonical declarations/metadata facts; ECSS computes the computed facts once; projection then passes them by owner/impact to Runtime, Ebox, and TP, and no owner reparses 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.

Validation has two deterministic phases:

  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.

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.

3.1 Canonical Property Composition of Author Forms

The following compositions are the normative public schemas. Shorthands expand immediately upon entering the canonical registry and do not create a second set of owner or impact rules.

SharedSourceMetadata = :key :class :id
ETAFOnlyMetadata     = :ref :role :aria-* :on-* :use
ETAFSourceMetadata   = SharedSourceMetadata + ETAFOnlyMetadata
EboxSourceMetadata   = SharedSourceMetadata

TextStyle =
  :font-family :font-size :font-weight :font-style
  :color :background-color
  :text-decoration-line :text-decoration-color :text-decoration-style
  :wrap-mode (word | grapheme | none)

BoxFrame =
  :outer
  :width/:min-width/:max-width
  :height/:min-height/:max-height
  :box-sizing
  margin/padding shorthands and logical/physical longhands
  border shorthands and width/style/color longhands
  :overflow :visibility
  inherited standard text style source such as font/color
  non-inherited surface source such as :background-color

NormalConfig = :text-align

RowConfig = :item-gap :cross-align
ColumnConfig = :item-gap :cross-align

FlexConfig =
  :flex-direction :flex-wrap :flex-flow
  :justify-content :align-items :align-content
  :gap :row-gap :column-gap

GridConfig =
  :grid-template-columns :grid-template-rows
  :grid-auto-columns :grid-auto-rows :grid-auto-flow
  :justify-items :align-items :justify-content :align-content
  :gap :row-gap :column-gap

FlexParticipation =
  :order :flex :flex-grow :flex-shrink :flex-basis :align-self

GridParticipation =
  :order :grid-column :grid-row :align-self :justify-self

The six visual forms are produced only through composition:

text   = SurfaceSourceMetadata + TextStyle + one String
box    = SurfaceSourceMetadata + BoxFrame + NormalConfig + ParentParticipation + children
row    = SurfaceSourceMetadata + BoxFrame + RowConfig + ParentParticipation + children
column = SurfaceSourceMetadata + BoxFrame + ColumnConfig + ParentParticipation + children
flex   = SurfaceSourceMetadata + BoxFrame + FlexConfig + ParentParticipation + children
grid   = SurfaceSourceMetadata + BoxFrame + GridConfig + ParentParticipation + children

SurfaceSourceMetadata resolves to ETAFSourceMetadata on an ETAF surface and to EboxSourceMetadata on a standalone Ebox surface. :item-gap and :cross-align are Ebox Row/Column extensions, not CSS gap/align-items: the former inserts fixed spacing along the main axis; the latter accepts start|center|end|stretch and controls the cross axis. Flex/Grid :gap retains standard CSS shorthand semantics and expands to both row-gap and column-gap.

The final direct parent selects ParentParticipation as Default, FlexParticipation, or GridParticipation. A candidate cannot carry mutually exclusive Flex-only and Grid-only fields simultaneously. Text accepts neither BoxFrame, LayoutConfig, nor Participation; wrap it in an explicit Box when those capabilities are required. The Ebox DSL does not accept ETAFOnlyMetadata.

3.2 Naming Layers and Backend Projection

Names in the author layer belong to four namespaces: CSS, ETAF, Ebox, and the optional Emacs adapter. A standard CSS name is adopted only when its applicable objects, value grammar, defaults/inheritance, impact, and observable behavior match CSS or form an explicit subset. A capability that is conceptually similar but semantically different retains a precise ETAF/Ebox name; familiarity does not justify an incorrect standard name.

The target public API classifies historical and backend names as follows:

:bgcolor          → exact alias of :background-color
:font-height      → removed from core; optional Emacs adapter source only
:font-slant       → exact alias of :font-style with identical value grammar
:border-top-p     → retained Ebox boolean shorthand for top width/style
:grid-column-span → Ebox positive-integer shorthand for :grid-column (span N)
:grid-row-span    → Ebox positive-integer shorthand for :grid-row (span N)
:face             → not a core canonical property
:display          → private Ebox/Emacs lowering field
:content          → private legacy runtime storage field

:wrap-mode is not equivalent to CSS white-space, overflow-wrap, word-break, or line-break. It therefore remains the sole public name in the Ebox namespace, with its value restricted to word, grapheme, or none. If complete, semantically equivalent CSS properties are implemented in the future, one explicit migration must replace it; the two naming systems must not be retained indefinitely.

Standard CSS shorthands have exactly one expansion rule, including :margin, :padding, :border, :gap, :flex, :flex-flow, :grid-column, and :grid-row. The canonical registry uses namespaced IDs such as ebox/font-size and ebox/background-color; authors write only keywords and never see internal IDs.

Every definition records its naming domain and relationship to standards explicitly:

PropertyDefinition {
  authorName
  canonicalId
  aliases
  namespace: css | etaf | ebox | emacs-adapter
  standardReference
  semanticSubset
  acceptedContexts
  valueGrammar
  initial
  inherits
  impactSet
  owner
  projections
}

An alias must have exactly the same contexts, value grammar, owner, impact, and projections as its canonical property, and it cannot transform the value. A shorthand performs one-to-many expansion; an adapter source resolves a composite backend value. An alias resolves to canonicalId immediately at the parse entry point, so computed style, caches, diff, Ebox, TP, and Rust never see it. PropertyDefinition registration rejects canonical/alias collisions. Writing both the canonical name and an alias—or repeating an alias—in the same declaration is an error rather than an implicit last-wins rule. The source spelling may be retained only in diagnostic provenance.

Property references list aliases under the canonical property, for example:

background-color
  Canonical: :background-color
  Aliases:   :bgcolor

They do not list bgcolor as a second capability. In the first phase, aliases are registered by the owning package's schema; process-global user aliases are not exposed. An upper-level package that needs an alias must scope it to its own schema and must not pollute the global Ebox visual-property domain.

:border-top-p is not an alias but a retained Ebox boolean shorthand: t expands to :border-top-width 1 + :border-top-style solid, while nil expands to width 0 + style none. Color comes independently from :border-top-color, :border-color, or the default foreground. Use standard :border-top or its longhands when full control is required. Mixing :border-top-p with its width/style outputs in the same declaration is an error; different cascade sources still override one another normally through ECSS.

:grid-column-span and :grid-row-span are likewise not aliases. They accept only a positive integer and respectively expand to the corresponding Grid shorthand's span N value; mixing either with :grid-column or :grid-row in the same declaration is an error. :font-slant is an exact alias only when its value grammar is identical to :font-style. The Emacs face-height semantics of :font-height must not masquerade as CSS font-size.

The Emacs text property names face, display, keymap, mouse-face, help-echo, invisible, line-prefix, wrap-prefix, and sticky properties belong only to the final adapter. Ebox core, ETAF Runtime, ECSS, and Rust neither produce nor consume these names directly. Reuse of a named Emacs face, when needed, is available only through an optional adapter's explicit :emacs-face source input. That input immediately resolves to standard font/paint facts and computes their impact; the raw face symbol does not enter the canonical IR.

A typical projection is:

:font-weight bold
  → ebox/font-weight, impact {geometry, paint}
  → Ebox Text measurement weight
  + TP face-weight contribution
  → Emacs adapter emits face :weight

:width 120
  → ebox/width, impact {geometry}
  → Ebox GeometryPatch
  → Emacs adapter may emit display space width

3.3 Unified Property Facts and Projection Layer

The unified layer is not a direct author keyword → Emacs text property dictionary. One property may affect both measurement and paint; several properties may merge into one Emacs face; and a display produced by layout may have no direct author property. The internal stages must therefore distinguish:

CanonicalDeclaration {
  propertyId
  specifiedValue
  sourceSpelling
  sourceProvenance
}

SelectorSubjectFact {
  classList
  id
  semanticSourceHandle
}

RuntimeMetadataFact {
  propertyId
  value
  sourceHandle
}

ComputedPropertyFact {
  propertyId
  computedValue
  winningProvenance
  impactSet
  sourceHandle
}

ProjectionPlan {
  metadataFacts
  measurementFacts
  geometryFacts
  paintFacts
}

Normalization parses author input once and simultaneously produces immutable CanonicalDeclaration, SelectorSubjectFact, and RuntimeMetadataFact artifacts. :class/:id do not enter CSS declarations; SelectorSubjectFact constructs the ECSS subject directly before cascade. Selector and semantic projection share the same parsed source value for :id rather than rereading the plist independently. ECSS cascade/inheritance receives SelectorSubjectFact + declarations and produces immutable ComputedPropertyFact artifacts. :key, :ref, :on-*, and similar properties do not enter ECSS and appear only in RuntimeMetadataFact. “Materialize once” means that each stage produces its own immutable artifact once, not that one object mutates across stages.

A pure property projector consumes computed-style facts and metadata facts and generates ProjectionPlan according to the PropertyDefinition owner/impact/projections fields. Every consumer references the same fact rather than reparsing or copying its computed value. The TP paint projector is the only module that converts paintFacts into ContributionPlan. The ETAF semantic projector converts metadataFacts into Runtime/SemanticPlan.

A registry is not one process-global universal table. Each owning package supplies an immutable SchemaRegistry conforming to the same schema protocol; a surface composes a SurfaceSchemaSet:

ETAF SurfaceSchemaSet = ETAF registry + Ebox registry
Ebox SurfaceSchemaSet = Ebox registry
ETAF + Emacs adapter  = ETAF registry + Ebox registry + adapter registry

Each canonicalId has exactly one owning definition. Canonical/alias collisions are checked when composing the SchemaSet. This avoids both reverse dependencies and duplicate sources of truth.

Logical modules remain small and complete:

  • package property schema: a package-scoped PropertyDefinition registry;
  • SurfaceSchemaSet: conflict-free composition of package registries;
  • property normalizer: author names, shorthands, and value grammar;
  • property projector: computed/metadata facts → backend-neutral ProjectionPlan;
  • Ebox layout projector: measurement/geometry facts → LayoutPlan;
  • TP paint projector: paint facts → ContributionPlan;
  • ETAF semantic projector: metadata facts → Runtime/SemanticPlan;
  • Emacs adapter: GeometryPatch + PaintPatch + SemanticPlan → text-property operations.

Consequently, an Ebox canonical node stores no paint-only field; TP stores no geometry; the Emacs adapter does not reinterpret CSS; and Rust receives only canonical facts/plans, not face, display, or an Elisp plist. Debugging and performance tools must expose the complete chain, for example:

:font-weight
→ ebox/font-weight
→ {geometry, paint}
→ measurement/font-weight + paint/font-weight
→ face :weight

4. Module Owners

4.1 ETAF Core

etaf-view:

  • 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.

etaf-component:

  • 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:

ebox-core   pure measurement, layout, snapshots, and structural patches
ebox-emacs  font/window capabilities, buffer positions, and structural commit

These boundaries need not immediately become two distribution packages, but pure layout and Emacs side effects must be independently testable.

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.

4.3 ECSS

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.

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:

ETAF Runtime creates the candidate and owns commit authority
    ↓
Ebox candidate stage produces the structure/geometry patch
    ↓
Ebox surface adds a rollback-capable participant to the existing TP transaction
    ↓
TP precommits paint; Ebox commit publishes the Emacs surface
    ↓
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 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.

4.5 Upper-Level Packages

Package Owns Does not own
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 an independent optional package. ETAF, Ebox, TP, and SQLite do not depend on it; it observes and correlates stages only through public boundaries.

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.

4.6 Public Surfaces Layered by Consumer

  • Ordinary ETAF users use only etaf-view, Component, Text/Box forms, ETAF metadata/ events, canonical CSS-aligned properties, explicitly listed ETAF/Ebox extensions such as :outer and :wrap-mode, and parse-time aliases; they do not call Ebox, TP, or the Emacs adapter.
  • Standalone Ebox users use inert data forms with the same visual structural vocabulary through ebox-build; they do not own Component, slot, expr, or lifecycle.
  • Framework integrators use ebox-text-create, ebox-box-create, typed LayoutConfig, RangeAnchor, and the TP contribution contract; they do not reconstruct DSL, pass shorthands, or run a second cascade.
  • The optional Emacs adapter is an explicit opt-in, independent schema namespace. Only a surface that loads the adapter accepts :emacs-face; otherwise normalization reports the unknown namespace/property precisely.
  • ebox--*, etaf--*, tp--*, :ebox-type, :node-id, :region-id, Emacs text properties, and runtime storage fields are all private.

The user guide describes only the first layer; the Ebox DSL guide describes the second; the integration reference describes the third; only architecture documentation describes canonical IR, identity owners, impact projection, and the commit protocol.

5. Dependency Direction

etaf-ui ─────────────▶ ETAF Core ─────────────▶ Ebox public port
etaf-sqlite ─────────▶ ETAF Data contract
etaf-playground ─────▶ ETAF + optional UI/SQLite
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 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

Module boundaries precede implementation language. Deterministic computation moves to Rust only after interfaces and equivalence tests are frozen:

Rust candidates:
  normalized View validation and structural diff (over opaque stable IDs only)
  ECSS cascade
  Text measurement input processing and Ebox layout
  pure-data computation of geometry/paint patches

Elisp / Emacs:
  execute user Components, refs, Context, Action, Data, Resource, and lifecycle
  database and external I/O
  Emacs events, font/window capabilities, and final commit

Component identity, dependency ownership, TP priority, and generation authority do not 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

  • 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.

The final gates must cover:

  • structural equivalence of Text/Box/Fragment;
  • rejection of canonical/alias collisions when package registries compose a SurfaceSchemaSet;
  • exact aliases disappear after normalization, while each shorthand expands exactly once and conflicts with its owned outputs in the same declaration;
  • the optional adapter namespace is accepted only by explicitly opted-in surfaces and reports a precise error when the adapter is not loaded;
  • CanonicalDeclaration, SelectorSubjectFact, RuntimeMetadataFact, and ComputedPropertyFact remain immutable across stages, and the selector subject is produced exactly once before cascade;
  • ProjectionPlan contains only paintFacts, and only the TP projector may produce ContributionPlan;
  • Row/Column accept only the :item-gap/:cross-align extensions rather than borrowing the different semantics of CSS :gap/:align-items;
  • a multi-impact computed value such as geometry + paint materializes once and is referenced by both paths;
  • 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.

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.

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. 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:

  • 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/layout-form aliases in Core; property aliases follow only the unified mechanism in Section 3.2;
  • allowing Playground, UI, or database packages to own Core protocols;
  • adding App precompilation artifacts before real compile cost and runtime benefit exist.