tp/docs/retained-runtime-target-architecture-en.md
Kinneyzhang db5864141a update
2026-08-13 14:40:44 +08:00

16 KiB

TP Retained/Reactive Text Runtime Target Architecture

Chinese version: TP Retained/Reactive Text Runtime 目标架构.

For the public entry index, see API-REFERENCE.md; for behavior contracts, see API-SEMANTICS.md.

Status: implemented TP 1.0 architecture contract. This document records the target boundaries now implemented and protected by tests; ARCHITECTURE.md and API-SEMANTICS.md are authoritative for current module and public-behavior facts. The target-architecture filename remains stable for existing links.

1. Product position

TP 1.0 is an independently installable, testable, releasable, and usable retained/reactive text runtime for Emacs. It projects declarative properties, reactive data, and stable text objects onto strings and buffers while owning final text-property diff, marker/index state, transactions, and buffer publication.

TP does not depend on Ebox and contains no spatial concepts such as Box, Flex, Grid, padding, layout owners, or Ebox regions. Ebox, ETAF, dashboards, diagnostics, interactive lists, and ordinary buffer annotations can all consume the same public TP APIs.

TP is not a CSS engine either. Complete stylesheets, selectors, specificity, origin/importance, CSS cascade layers, CSS-wide values, and custom properties belong to the independent ECSS package. ECSS may hand final computed declarations to TP for publication, but TP neither requires, detects, nor names ECSS.

The boundary in one sentence is:

TP owns objects, dependencies, positions, changes, and publication; an upper consumer owns business meaning and the desired display result.

2. Design invariants

  • TP source, tests, examples, and package metadata neither require, detect, nor name Ebox.
  • TP owns no CSS stylesheet, selector matching, specificity, or cascade winner; those capabilities belong to the independent ECSS package.
  • One property, binding, surface, mount, diff, and transaction core serves every live use; there is no embedded mode.
  • TP is the sole final buffer committer. Producer and consumer prepare phases produce candidate values only.
  • One live object has one TP identity. A consumer may store its opaque handle but cannot maintain a parallel identity requiring long-term synchronization.
  • Signal updates travel from source to binding and object to marker-backed mount. Normal hot paths never scan buffer-list or search text by tp-name/tp-layers.
  • tp-name, tp-layers, and tp-meta are not a live runtime database. Definitions, provenance, identity, dependencies, and mount metadata live in side state.
  • Capabilities have exactly two values, content and properties. They share one compute and commit semantics and differ only in write permission.
  • Ordinary function values are literal. Only an explicit tp-computed value source executes and records dependencies.
  • Signal sources, bindings, plans, marker/index state, and opaque client state use one transaction revision and roll back together.

3. Independent data flow

application state / theme / host edit
                 │
                 ▼
           signals + bindings
                 │ exact dependency graph
                 ▼
          TP prepare context
                 │ candidate objects/bindings/anchors
                 ▼
       ordinary or client producer
                 │ object → direct property contributions
                 │ → generic keyed surface plan
                 ▼
         reconcile + prepare diff
                 │ prepared text/property operations
                 ▼
       atomic multi-surface publication
                 │
                 ▼
        strings / Emacs buffers

Prepare creates candidate state without publication. A producer returns an ordinary surface plan plus optional opaque client state. TP never interprets client state but promotes it atomically with the plan, dependencies, mounts, and revision.

4. Property contributions, composition, and value sources

4.1 Property policies and contribution composition

TP maintains generic property policies for the Emacs text properties it ultimately writes: presence, normalization, validation, equality, merge, and projection. A policy answers only how TP contributions compose, whether a value truly changed, and how the result becomes an Emacs property; it defines no selector, stylesheet, or CSS winner.

Every properties mount contributes only the properties it declares. TP composes the host baseline and all TP contributions per property in stable mount/contribution order, strictly distinguishes explicit nil from absence, and retains enough side state to remove one contribution, detect an external conflict, and roll back a transaction. This is text-property ownership and composition, not CSS cascade.

ECSS is an independent pure style-computation package: it consumes property schemas, a subject tree/adapter, and a stylesheet and returns computed declarations plus provenance. It neither mutates buffers nor owns markers and depends on neither TP nor Ebox. Ebox or an ordinary text UI that needs complete CSS runs ECSS first and then hands the final text/property plan to TP; callers that do not need CSS use TP directly.

4.2 Literal and computed values

Every ordinary Elisp value is literal, including function objects. TP never invokes a help-echo function, keymap command, or callback implicitly.

A declaration that must be evaluated uses tp-computed to wrap a compute function. It executes in the owning object's binding during prepare; tp-signal-read and tp-binding-read record dependencies automatically. The result then passes through the property policy's normalization and validation and is treated as a literal, with no implicit second invocation. Errors abort the whole candidate transaction, and equal results produce no surface change.

An upper DSL may accept evaluable Elisp, but its constructor/compiler boundary must distinguish quoted lists, literal functions, and computed sources and compile reactive computation to tp-computed rather than creating a second watcher.

5. Stable objects and prepare contexts

Object identity is valid in one surface and retained by parent identity, sibling key, and opaque kind. Explicit keys are unique under one parent. An unkeyed object may retain identity only by position plus kind. Handles never cross surfaces.

TP creates a short-lived prepare context at the start of mount/update. A producer calls:

(tp-object-ensure context parent key kind)

A matching live object returns its retained handle. A new object exists only in candidate state. Duplicate keys, stale parents, orphan objects, and cross-surface handles fail during prepare. Candidate identity becomes live only after successful publication; a failed handle is permanently unresolvable.

tp-surface-materialize-string gives a producer the same semantics through an ephemeral context. Its objects and bindings exist only for that pure computation, are released after the string returns, and never enter a live registry.

tp-object-resolve performs read-only lookup of a live handle by surface and key path for public selection/interaction entry points. It never creates an object.

6. Bindings and real reactive dependencies

A definition is a reusable recipe; a binding is a concrete computation installed on one object. tp-bind installs idempotently by object handle plus caller-namespaced binding key and stores at least compute, last successful value, dependencies, dirty state, revision, and lifecycle policy.

tp-binding-read returns another binding's memoized value while registering a binding-to-binding dependency. Signals, computed properties, measurements, and ordinary derived values can therefore form a real dependency graph rather than scanning a buffer after variables change.

Before recomputation TP removes old dependencies; after success the actual set read this time becomes authoritative, so conditional branches release unused sources. A transaction dirty queue deduplicates invalidations and recomputes one binding at most once per batch. Cycles report the complete path.

Prepare records touched objects and bindings. A logical object with no visible characters must be retained explicitly; one logical object may use prepare-only attachments to own several disjoint plan fragments without putting those attachments in the plan. Removing an object cleans up its bindings, subscriptions, anchors, and mounts in the same publication. A surviving binding omitted from the current declaration is removed by default unless an explicit lifecycle policy retains it. Ordinary bindings may return arbitrary client values; only designated producer bindings return plans or subplans.

7. Surface plans

A surface plan is immutable-semantic pure data with these node fields:

Field Contract
key Sibling-local stable key
kind Opaque comparable discriminator
text Optional plain/propertized string leaf
props Final direct Emacs text properties
children Ordered child plans
tags Opaque side metadata indexed but not interpreted by TP
capability content or properties

A plan contains no marker, buffer position, patch operation, producer closure, binding closure, or client continuation. Valid keymap/help-echo/callback functions may remain final property values, and reconciliation never invokes them.

Public constructors defensively copy caller-owned plists/strings or otherwise provide immutable semantics. Mutating the original after commit cannot change a published surface.

8. Mounts, range anchors, and property ownership

8.1 Content capability

A content mount owns text and properties in its span. It may insert, delete, move, or replace text and update properties. It serves dashboards, menus, special UI buffers, and advanced renderer output.

8.2 Properties capability

A properties mount contributes declared properties without changing host text. tp-range-anchor-create constructs an opaque marker-backed anchor from buffer, start/end, and marker insertion policy. During prepare the producer calls tp-object-attach-range to attach an object to that anchor. The plan itself still contains no position.

For every anchor/property interval, TP side state records the host baseline, every TP contribution, and the last published value. Overlapping contributions from one surface compose per property through its property policy.

If external code changes the same property to a value different from TP's last publication, the next prepare raises tp-property-conflict by default. TP neither overwrites the external value nor restores an old snapshot; the caller explicitly invokes tp-range-rebase or unmounts. Unmount removes only the TP contribution: it restores the baseline only while the current value still equals TP's last publication, otherwise preserving the external value and recording the conflict.

Live markers follow host text editing. An edit crossing or deleting an anchor boundary follows an explicit shorten/remove/stale policy; TP never scans nearby text to guess a new location.

9. Reconciliation and transactions

TP reconciles object identity by parent/key and computes child insert/remove/reorder, text diff, and property-run diff between old and new plans. A normal update resolves one or more ranges directly through the object-to-mount index; public queries expose numeric range/tag snapshots rather than live markers.

A transaction freezes candidate signal writes; creates prepare contexts; prepares every actually affected surface; validates capabilities, conflicts, and lifecycle; creates per-surface operations and inverse journals; publishes in stable surface-id order; then atomically swaps source values, bindings, plans, indexes, client state, and revisions. Observers run only after every surface succeeds.

One global signal may reach several surfaces. TP prepares all before publishing any. If a compute, buffer write, marker/index step, or transaction participant fails, TP rolls back already published surfaces and restores signal values, binding values/dependencies, dirty queues, and revisions. An observer failure occurs after commit and is reported without rollback.

If a buffer is killed during publication, kill-buffer teardown is authoritative and irreversible. TP rolls back other surfaces and source state and never resurrects the killed buffer.

10. Public API levels

10.1 Simple one-shot properties

(tp-propertize string declarations)
(tp-apply buffer start end declarations)

Both use the same direct-declaration validation/projection and mutation primitives without identity, bindings, or live mounts. Existing tp-set, tp-reset, tp-add, tp-remove, lookup, and search APIs reuse the same lower semantics.

10.2 Reactive existing text

(tp-watch buffer start end compute)

tp-watch creates a properties range anchor, surface object, and binding. COMPUTE returns declarations, and the result is an opaque update/unmount handle. Ordinary users do not manually build objects, bindings, or surfaces.

10.3 Complete retained UI

(tp-surface-mount buffer plan-or-producer options)
(tp-surface-update surface plan-or-producer)
(tp-surface-update-scoped surface objects plan-or-producer options)
(tp-surface-unmount surface)

tp-surface-update-scoped is a one-transaction authorization over retained object identity. TP resolves each object through its mount index, supports one object with several disjoint mounts, verifies that the full candidate changes no output outside the authorized ranges, and then publishes text, direct properties, mounts, indexes, client state, and revision in the same rollback boundary. Scope is not stored in text and is not a second reactive runtime. Mismatch signals by default; an explicit option may request a full-root fallback.

The full API also includes tp-computed, signals, prepare/object operations, bindings, range anchors, transactions, materialize-string, and at-point/inspect/report diagnostics. TP adds no overlapping tp-mount/tp-update aliases and exposes no raw marker, patch, change-set, domain dirty kind, or :impact API.

11. Standalone acceptance scenarios

With no Ebox on load-path, TP 1.0 continuously verifies:

  1. static string/buffer properties: nested faces, keymaps, help-echo functions, and explicit nil;
  2. reactive status: tp-watch and a retained content producer, conditional/binding dependencies, and batching;
  3. retained dashboard: keyed add/remove/reorder, buttons, signal-driven theme declarations, and rollback;
  4. existing-text diagnostics: range anchors, host edits, overlap, external property conflict/rebase/unmount;
  5. two-buffer producer: isolated identity/state plus global-signal multi-surface atomicity;
  6. performance: sparse updates proportional to the actual dependency closure, zero buffer operations for equal values, and no marker/subscription/weak-reference leaks after unmount.

12. Compatibility, versioning, and deletion

TP 1.0 is a major-version transition. Stateless public APIs that map directly to the unified core remain. Managed behavior that requires the inline tp-layers/tp-meta database or scan renderer is explicitly removed or replaced rather than retained in a hidden compatibility engine.

There is no legacy import API in TP 1.0. Callers must provide a plan or producer explicitly; TP never scans historical propertized text to reconstruct surface identity.

The current implementation has deleted the layer-to-buffer registry, scan-driven refresh hooks, old inline managed codec, duplicate transactions, and old batch renderer. The TP 1.0 package, tests, examples, and this document all work without an Ebox repository.

13. Implemented, frozen contracts

TP 1.0 executable contract tests define and continuously protect prepare-context/object timing; binding identity and lifecycle; literal/computed values; range anchors and property conflicts; single/multi-surface rollback; the three convenience levels; error taxonomy; report shape; explicit nil versus absence; and read-only, undo, narrowing, indirect-buffer, and kill-buffer behavior.

If implementation requires an Ebox/ECSS-specific branch, a CSS selector/cascade winner, a post-commit identity scan, raw position/closure in a plan, a second renderer, or cannot safely remove a properties contribution, integration stops for ownership review rather than adding an adapter mode.