# TP 1.0 Current Architecture 本文描述 TP 1.0 当前实现的模块边界、权威状态、数据流和事务模型。公共行为合同见 [API semantics](API-SEMANTICS.md),设计背景见 [retained runtime architecture](retained-runtime-target-architecture.md)。 按功能查找公共入口和用法时,使用 [API reference](API-REFERENCE.md)。 ## 1. 定位 TP 是通用 retained/reactive text runtime: ```text application state → signals/bindings → prepare context + stable objects → pure surface plan → reconcile/diff → atomic Buffer publication ``` TP 负责文本属性 contribution、响应式依赖、身份、位置、变化和提交。调用者负责业务含义以及期望显示结果。 TP 不依赖 Ebox 或 ECSS,不包含 selector、stylesheet、CSS cascade、Box/Flex/Grid、measurement、layout owner 或 viewport dirty semantics。 ## 2. 模块图 ```text tp-core ├─ tp-style │ └─ tp-reactive │ └─ tp-surface ├─ tp-layer ├─ tp-ops ├─ tp-search ├─ tp-query ├─ tp-palette └─ tp-builtins tp.el loads the public package surface ``` 真实 require 关系按源码为准;上图表达责任层次,不要求每个 consumer 经过所有中间模块。 | Module | Owns | Must not own | | --- | --- | --- | | `tp-core.el` | canonical ranges/requests/results、interval traversal、plist/face merge、native property facts | runtime identity、reactivity、publication | | `tp-style.el` | native property policies、direct declarations、explicit computed source、projection | selector、stylesheet、specificity、CSS winner | | `tp-reactive.el` | signals、bindings、dynamic dependency graph、scheduler、candidate source state、transaction participants | buffer scans、mount positions、layout impact | | `tp-surface.el` | prepare context、objects、plans、anchors、mount/index、contribution ledger、diff、publication、rollback、reports | stylesheet/cascade、consumer layout decisions | | `tp-layer.el` | `define-tp`/`define-tps` declaration recipes and registry | live layer stack、inline runtime metadata、watcher engine | | `tp-ops.el` | direct set/reset/add/get/at/remove/clear plus one-shot `tp-propertize`/`tp-apply` | retained identity、scan refresh | | `tp-search.el` | match/regexp application、property search/navigation | runtime identity | | `tp-query.el` | native lookup/change wrappers and mutation policy | retained publication | | `tp-palette.el` | theme-aware palette data | runtime scheduling | | `tp-builtins.el` | built-in direct recipes and display helpers | managed refresh hooks | | `tp.el` | package metadata and public module loading | business logic | There is no `tp-render.el` or `tp-stack.el`. The 0.3 scan renderer and managed stack runtime were deleted rather than wrapped. ## 3. Property data flow Direct declarations are native property/value pairs. `tp-style.el` resolves them through one policy pipeline: ```text native declarations → canonical text/PROPERTY ids → explicit computed-source resolution → normalize → validate → merge contributions → project to final Emacs properties ``` Ordinary functions are literal. Only a tagged `tp-computed` source runs. Explicit nil remains distinguishable from absence throughout projection and retained contribution ownership. The policy registry is generic. It knows how final Emacs properties compose; it does not decide which stylesheet declaration wins. CSS selection belongs to ECSS outside TP. ## 4. Static façade and declaration recipes `tp-propertize` and `tp-apply` use the same direct projection core but do not create live state. `tp-set/reset/add/remove` and the search/query families share canonical range, presence, and mutation primitives from `tp-core.el`. `define-tp` and `define-tps` store recipe arglists and body forms. Application expands a recipe into ordinary direct properties. Static recipes are also compiled into the named style registry. Parameterized recipes stay evaluable recipes rather than frozen declarations. Recipe and group redefinition uses candidate registry state and commits only after body expansion, generated element creation, and named-style compilation succeed. Failure restores the previous registry state. No recipe application writes `tp-name`, `tp-layers`, or `tp-meta` to text. No `$variable` parser remains. ## 5. Reactive graph The authoritative graph lives in `tp-reactive.el`: ```text signal ──subscribers──> binding ──subscribers──> binding │ └── owner object/surface ``` A binding is identified by owner plus caller-namespaced key. While its compute function runs, `tp-signal-read` and `tp-binding-read` record the exact dependencies used in that execution. On success, the new dependency set replaces the old set. A conditional branch therefore removes obsolete subscriptions automatically. Signal writes enter transaction-local candidate state. Dirty bindings are deduplicated and evaluated by dependency order. Equal signal writes and equal binding results stop propagation. Nested writes queue another stabilization pass rather than recursively mutating output. Cycle detection reports the path. The graph contains no layer-to-buffer registry. A source reaches surfaces through binding owners, not by scanning `buffer-list` or searching text properties. ## 6. Prepare context and identity Every materialize/mount/update creates a short-lived prepare context. A producer calls `tp-object-ensure` before producing the corresponding plan node. Object identity is scoped to one surface and derived from: - parent object identity; - sibling-local explicit key, or unkeyed position; - opaque kind. Candidate objects exist only inside the context. Successful publication promotes them to live objects; failed contexts dispose them and their bindings/anchors. `tp-object-resolve` queries live identity by key path without creating state. The context records touched objects/bindings. Omitted objects are removed. Omitted bindings default to deletion unless an explicit lifecycle says retain. A logical object with no direct plan node must call `tp-object-retain`; disjoint physical output is attached through `tp-object-attach-fragment`. Content ranges normally use `tp-object-attach-content-ranges`, which defensively snapshots opaque tags. A producer that freshly allocates candidate-local tags and never mutates or exposes them may use `tp-object-attach-content-ranges-owned` to transfer those snapshots without another copy. The same candidate-local ownership rule applies to `tp-surface-plan-create-owned` and `tp-surface-result-create-owned`; an owned result is bound to the active prepare context and consumed once, while normal plan/result constructors remain defensive boundaries. ## 7. Pure surface plans A plan is a defensive immutable-semantics tree of key/kind/text/props/children/tags/capability. It contains desired output only. It deliberately excludes: - buffer/position/marker; - patch operation or inverse journal; - producer/binding closure; - client continuation; - consumer-specific layout identity. TP validates sibling keys, legal text/children combinations, property shape, and capability before publication. Tags remain opaque; they are indexed for callers but never interpreted by TP. `tp-surface-materialize-string` creates an ephemeral surface/context, renders the plan, then releases all candidate runtime state. `tp-surface-mount` creates a live surface and stores the producer or plan for later reactive preparation. ## 8. Mounts and side indexes Every live surface owns: - key path to live object table; - object to bindings; - object to marker-backed mounts; - position/tag query index; - retained plan and producer; - properties contribution ledger; - opaque client state; - revision and last report. The displayed text contains only properties needed by Emacs display or interaction. Identity, provenance, dependencies, marker metadata, revisions, and client state stay in side state. `content` mounts own their text and properties. `properties` mounts attach objects to opaque range anchors and can only contribute properties to host-owned text. One object may have multiple disjoint mounts. Public queries expose numeric range/tag snapshots, never live markers. ## 9. Properties contribution ledger For every relevant anchor/property interval, the surface keeps: - host baseline presence/value; - ordered TP contributions; - last published presence/value; - contributing anchors. Candidate preparation collects interval boundaries from old ledger entries, current mounts, and current host property runs. It verifies that a previously published value has not been replaced externally, composes the baseline with current contributions through the property policy, and emits an operation only when the resulting presence/value changes. An external mismatch raises `tp-property-conflict`. `tp-range-rebase` replaces the baseline with current host state. Unmount restores a baseline only when the current value is still TP's last published value; otherwise it preserves the host edit and reports the conflict. ## 10. Reconcile and diff TP reconciles object identity by the prepare tree and compares old/new plans for: - created, removed, retained, and moved keyed objects; - minimal character replacement using common prefix/suffix; - exact property-run differences; - mount/index changes; - scoped output authorization. `tp-surface-update-scoped` maps requested objects directly through the object-to-mount index. For content surfaces it proves old/new changes stay within those mounted ranges; properties surfaces perform the equivalent contribution-range proof. A mismatch is an error unless root fallback is explicitly selected. Callers that only need scalar commit metrics may request no report snapshot and read `tp-surface-report-summary` after publication. An equal candidate produces no prepared publication. It preserves revision, report, buffer modified state, markers, and client state. ## 11. Transaction and publication The outer transaction owns candidate source values, dirty bindings, prepared surfaces, participants, inverse journals, view state, and final observer scheduling. ```text freeze candidate writes → recompute exact dependency closure → prepare every affected surface → validate all candidates → capture inverse journals → publish surfaces in stable id order → publish transaction participants → commit signals/bindings/surface state/revisions → run observers ``` Content publication edits the minimal text span and then exact property runs. Properties publication writes only prepared contribution operations. Marker mounts, indexes, plans, producer, bindings, opaque client state and report switch with the same revision. Rollback restores text, properties, marker/index state, plans, producer, client state, signal values, binding values/dependencies, dirty queues, revisions and reports. Property journals are explicit because `atomic-change-group` alone does not cover every silent property mutation path. `tp-transaction-participate` lets a consumer promote rollback-capable opaque state inside this boundary. Observers are different: they run only after the transaction commits, and observer failure is recorded rather than rolled back. If publication kills a target buffer, kill teardown is authoritative. Other surfaces and source state roll back; TP never recreates the killed buffer. ## 12. Lifecycle Surfaces are buffer-local lifecycle owners. A weak global registry supports lookup without keeping dead buffers alive. Mount installs local change/kill hooks; unmount and kill remove hooks, markers, ledger entries, objects, bindings, subscriptions, indexes, client state and weak registrations. Global signals are explicitly disposable. Buffer-scoped signals are disposed by their buffer kill hook. Owner disposal detaches both dependency directions so no downstream subscriber keeps a dead object alive. ## 13. Diagnostics Public diagnostics are defensive snapshots: - `tp-reactive-counters` reports graph work; - `tp-surface-report` reports the last publication; - `tp-surface-inspect` reports surface lifecycle/state counts; - `tp-surface-at-point` queries side indexes; - `tp-object-mounts` returns numeric range/tag snapshots. Reports use generic terms such as bindings, objects, text/property operations, touched characters, scope and rollback. They contain no Ebox paint/layout vocabulary. ## 14. Architectural invariants - TP source/tests/examples/package metadata do not require or name Ebox/ECSS runtime APIs. - TP contains no CSS selector/stylesheet/specificity/origin/winner engine. - There is one signal/binding/surface/mount/diff/transaction runtime; no embedded mode exists. - TP is the only writer for live TP surfaces. - Normal source-to-output flow is signal to binding to object to mount; it does not scan buffers or displayed text for identity. - `tp-name`, `tp-layers`, and `tp-meta` are not runtime storage. - Plans contain no raw positions or lifecycle closures. - Ordinary functions are literal; only `tp-computed` executes. - Candidate failure leaks no object, binding, anchor, subscription or revision. - Every successful publication advances Buffer state and side state together; every failure preserves the previous committed revision.