# Changelog All notable changes to the tp library are documented here. ## 2.0.0 (Unreleased) ### Added - A standalone retained surface runtime with pure defensive plans, prepare-scoped stable objects, keyed/positional reconciliation, content and properties capabilities, marker-backed range anchors, object/mount indexes, scoped updates, opaque client state, generic reports, and lifecycle cleanup. - Exact signal-to-binding and binding-to-binding dependency tracking with conditional rewiring, equality cutoffs, batched transactions, nested-write stabilization, owner disposal, buffer-scoped sources, variable adapters, cycle paths, and public structural counters. - Atomic single- and multi-surface publication with candidate source values, prepare-all/publish-all ordering, explicit property journals, rollback-capable transaction participants, observer isolation, and authoritative kill-buffer cleanup. - Transaction protocol v2: transaction-scoped publication batches, structured participants, bounded opaque final-accept markers using the closed `tp-vector-slots/v1` primitive, immutable tagged outcomes, and one live writer. - `tp-runtime-manifest`, advertising `tp-transaction-protocol-v2` and package version 2.0.0. - Public `tp-transaction-participate-v2` registration for cross-package structured participants. - Native property policies and contribution composition with explicit nil/absence, normalization, validation, equality, merge, projection, named direct styles, and explicit `tp-computed` value sources. - `tp-propertize`, `tp-apply`, and `tp-watch` as the one-shot string, one-shot buffer-range, and reactive existing-text conveniences over the same direct property/surface core. - Retained logical objects with `tp-object-retain` and `tp-object-attach-fragment`, allowing one object to own multiple disjoint physical fragments without placing handles or positions in plans. - Standalone examples for static properties, reactive status decoration, retained dashboards, and editable diagnostics. ### Changed - Structured publication batches own the single live surface-entry loop, participant stage/precommit/commit vector, and candidate-bound final accept. - Package metadata now identifies the v2-only breaking transaction contract. - `define-tp` and `define-tps` now define static or parameterized direct declaration recipes. Applying a recipe produces ordinary properties and never publishes runtime identity metadata. - TP is no longer a CSS engine. Selector, stylesheet, specificity, origin/importance, cascade layer, CSS-wide value, custom property, winner, and provenance behavior belongs to the independent ECSS package. - Function-valued properties are always literal. Only values wrapped by `tp-computed` execute and participate in dependency collection. - Runtime identity and provenance live exclusively in side state. Normal updates follow source to binding to object to marker-backed mount without scanning buffers or displayed text. - Package documentation, API semantics, architecture, doctests, and tests now describe the single TP 1.0 runtime rather than the transitional 0.3 managed model. ### Removed - The public `tp-transaction-participate` v1 facade. Replace `(tp-transaction-participate KEY PUBLISH ROLLBACK)` with `(tp-transaction-participate-v2 :key KEY :stage PUBLISH :rollback ROLLBACK)`. - The v1 publication writer, execution-route kill switch, artifact-mode switch, and their runtime manifest claims. - `tp-render.el`, `tp-stack.el`, the scan-driven renderer, layer-to-buffer registry, and duplicate managed transaction path. - Managed stack mutation, attach/detach, diagnostics, and lifecycle APIs that depended on inline stack storage. - `tp-text`, `$variable` declaration syntax, automatic layer refresh, and character-level `tp-name`/`tp-layers`/`tp-meta` runtime storage. - TP-owned CSS selector/stylesheet/cascade APIs and compatibility aliases. - The unused Dash runtime dependency. - Unused global theme lifecycle advice and managed-refresh bookkeeping from the deleted renderer. ## 0.3.0 (2026-07-27) ### Added Layer stack: - **Layer visibility**: `tp-hide-layer` / `tp-show-layer` — a hidden layer stays in the stack (and keeps receiving reactive updates) but does not render; hiding the visible top reveals the next visible layer, and with every layer hidden the text renders bare. `tp-flatten-layers` merges only visible layers; `tp-merge-layers` excludes hidden matched layers' props. - `tp-lower-layer` (mirror of `tp-raise-layer`) and a family-consistent `tp-rotate-layer` calling order `(START END DIRECTION [COUNT] [OBJECT])`, selected unambiguously by the symbols `up` / `down`; the legacy order keeps working. - `tp-layer-stack-at` — the full ordered stack at one position as `(NAME . PROPS)` conses, hidden layers marked by a `tp-hidden` entry. - Stack mutators return the number of property runs they modified (including `tp-merge-layers` / `tp-flatten-layers`), and layer-name lookups gained optional NOERROR arguments where they previously signaled. - `tp-describe-layer` — interactive help-buffer description of a layer: storage format, arglist, stored body, expanded props, reactive deps, transform, owning group. Reactive engine: - **Layer→buffer registry**: reactive updates now visit only the buffers registered as showing the affected layer instead of scanning the whole `(buffer-list)`; every buffer-mutating write path registers (tp-set family, stack mutators, match/regexp appliers), killed buffers are pruned, and an unknown layer falls back to one learning full scan. `tp-reactive-layer-buffers` exposes the registry; `tp-reactive-track-buffer` closes the insert-a-propertized-string gap. - **Minimal-diff `tp-text` re-render**: only the differing span is edited (insert-before-delete), so point and markers in unchanged text stay put and identical-text updates no longer touch the buffer at all (buffer-modified flag preserved). - `tp-gc-anonymous-layers` — collects interned anonymous layers that no registered live buffer still shows (stack-aware: buried and hidden layers count as alive; string-only layers are conservatively kept). Search and matching: - `tp-regexp-set/reset/add` accept SUBEXP: properties apply to that capture group per match (non-participating groups contribute nothing); SUBEXP beyond the pattern's group count signals a clear error. - `tp-match-*` / `tp-regexp-*` accept START/END bounds with as-if-only-that-portion semantics; reversed bounds are swapped. - `tp-forward` / `tp-backward` / `tp-forward-do` / `tp-backward-do` accept PREDICATE and NOT-CURRENT, passed through to the text-property-search machinery; defaults keep the 0.2.0 symmetric equal-matching contract exactly. Layer definitions: - **Multi-argument parameterized layers**: `define-tp` / `define-tps` arglists may declare any number of parameters; `(LAYER ARG1 ... ARGN)` and wrapped `(LAYER (ARG1 ... ARGN))` specs work in `tp-set` and `tp-put-layer`; new `tp-layer-props-with-args` / `tp-group-props-with-args` / `tp-layer-arglist`. Wrong-arity calls signal clear errors naming the layer and both counts. - Prefix-conforming aliases `tp-define-layer` / `tp-define-group` / `tp-define-palette` for discoverability (`C-h f tp-…`). Core and palette: - `tp-intervals` / `tp-intervals-map` accept an optional ABSOLUTE argument returning native buffer coordinates (feedable straight back into `tp-set`); the range-relative default is unchanged. - `tp-palette-color` (generic theme-resolved accessor) and `tp-palette-has-p` consolidate the palette query surface; all existing query functions remain. ### Fixed All six were found by an adversarial architecture/API review of the new 0.3.0 code and confirmed with minimal reproductions before fixing: - The reactive buffer registry only registered `tp-set`-family writes; layers applied via `tp-push-layer`, `tp-match-set`, etc. never re-rendered on variable updates. - Reactive updates wrote only the rendered top layer; hidden or buried layers kept stale props (visible again on `tp-show-layer`). - `tp-gc-anonymous-layers` and `tp-reactive-track-buffer` scanned only direct `tp-name` properties, so a layer buried in a stack (or hidden) could be wrongly collected / missed. - `tp-flatten-layers` / `tp-merge-layers` rendered hidden layers' properties despite `tp-hide-layer`'s documented contract. - Minimal-diff `tp-text` edits deleted before inserting, so markers at the suffix boundary drifted to the wrong character. - An error escaping a reactive update could strand queued batch entries (now drained under `unwind-protect`; `tp-reactive-reset` clears the queue). ### Changed - **Module boundaries tightened** (behavior identical under `(require 'tp)`): the `tp-text` handler chain moved from tp-render into tp-ops — partial loads now get working `tp-text` replacement — and `tp-with-batch-updates` moved up into tp-render; two of the four upward hook variables are gone (`tp--tp-text-handler-function`, `tp--reactive-flush-function`). The layer-stack storage codec and the anonymous-layer machinery now live in tp-layer; tp-stack's phantom dependency on tp-ops is gone; 67 lines of dead code deleted. tp-core holds no mutable state. - String forms of all 16 stack mutators document that they modify the string in place (unlike `tp-set`'s copy semantics); unifying this is on the 0.4 ledger. ### Deprecated - `tp-search-forward` / `tp-search-backward` (0.3.0) — thin wrappers whose nil-PREDICATE default contradicts the rest of the library's equal-matching; use `tp-forward` / `tp-backward`, or the Emacs primitives for raw access. - `tp-suffix-symbol` (0.3.0) — internal helper now private as `tp--suffix-symbol`; a compatibility alias remains. ### Infrastructure - GitHub Actions CI: Emacs 28.1 / 29.4 / 30.1 matrix running byte-compilation with warnings-as-errors, the full ERT suite, a shuffled-order rerun of every test (`make test-shuffled`, `tp-run-shuffled.el`; `SHUFFLE_SEED=N` reproduces an order), and the README doctests. - The whole tree byte-compiles with zero warnings (57 fixed: docstring rewraps and quoting, `defvar` declarations for reactive test variables, prefixed doctest counters, one impossible `eq` comparison corrected to `equal`). - Autoload cookies for the interactive commands (`tp-debug-show`, `tp-debug-clear`, `tp-reactive-reset`, `tp-layer-reset`, `tp-palette-show`, `tp-clear`) and the `define-tp` / `define-tps` macros. - Two doctest assertions made property-order-insensitive (Emacs 28 prints text-property plists in a different order than 29+). ## 0.2.0 (2026-07-26) ### Architecture - **tp.el was split into layered modules.** `(require 'tp)` still loads everything; nothing changes for users. Each module depends only on the ones before it, and the byte compiler enforces the order: | Module | Responsibility | |---|---| | `tp-core.el` | Intervals, plist/face merge engine, debug logging, `$var` utilities | | `tp-reactive.el` | Reactive dependency registry, variable watchers, batching queue | | `tp-layer.el` | `define-tp` / `define-tps`, layer registry and resolution | | `tp-ops.el` | `tp-set` / `tp-reset` / `tp-add` / `tp-get` / `tp-at` / `tp-remove` / `tp-clear` | | `tp-search.el` | `tp-match-*`, `tp-regexp-*`, `tp-search`, navigation | | `tp-render.el` | Reactive re-rendering engine (installs itself into lower modules) | | `tp-stack.el` | Layer stack operations (push/pop/move/merge/flatten/...) | | `tp-palette.el` | Light/dark color palette data | | `tp-builtins.el` | Built-in layers, palette gallery, display-buffer helpers | - The library now byte-compiles cleanly (previously `define-tp` macro-expansion failed at compile time). - New shared engine `tp--map-intervals`: a clipping interval walker that underlies region operations; property edits can no longer bleed outside the requested region. - New public constant `tp-face-properties` (`'(face font-lock-face mouse-face)`): the property family that gets face-aware merging. ### Fixed Core operations: - `(require 'text-property-search)` was missing; `tp-backward` signaled `void-function` in batch/fresh sessions. - `tp-remove` string form silently dropped the 3rd and later properties. - String-form removal helpers sampled properties at position 0 and smeared them across the range, destroying neighboring intervals; they now work per-interval. - `tp-clear` computed default bounds from the current buffer even when clearing a string (silent no-op or range error). - `(tp-get STRING START END ...)` returned nil silently; it now behaves like the buffer region form. - `tp-intervals` returned unclipped intervals (including negative offsets); it now clips to `[START, END)`. - Region-form calls with flat prop/val arguments — `(tp-set 1 4 'face 'bold)` — silently discarded the value and failed later; they now signal a clear error immediately. - Face-family prepend semantics in `tp-add` covered only `face`; they now cover `font-lock-face` and `mouse-face` too. - `tp--parse-face-list` no longer invents a `(:key nil)` pair for a trailing bare keyword. Built-ins and palette: - Emacs 28.1 compatibility restored (`plistp` is Emacs 29+; a compat shim is used, and `subr-x` is required where needed). - `tp-pop-to-buffer` / `tp-switch-to-buffer` no longer bind `q` in the shared major-mode keymap (a buffer-local minor-mode keymap is used) and no longer capture a `buffer` variable from the caller. - `tp-link` resolved its palette color once at load time; the color is now resolved at application time, so theme switches are honored. - `define-tp-palette` no longer generates per-palette defvars; `tp-palette-alist` is the single source of truth and palette redefinition takes effect immediately. - `tp-headline` emitted invalid `(:height nil)` for integer heights. - `tp-space` now produces the documented pixel `(space :width (N))` spec. - `tp-parse-color` accepts one-sided cons colors like `("white" . nil)`. Layer definitions (tp-layer): - Parameterized `define-tps` groups: the documented format returned nil props via `tp-group-props-with-arg`; all documented element shapes now resolve correctly. - Cyclic layer references signal a clear error naming the cycle (previously crashed with `excessive-lisp-nesting`); diamond-shaped reuse is not a false positive. - `define-tp` errors at macro-expansion time when extra body forms are present (previously silently discarded all but the first). - `$`-symbols in parameterized layer bodies resolve to their variables' current values at evaluation time (previously leaked literally into the output props); parameterized layers remain non-reactive, and the choice is documented. - `tp-layer-props` / `tp-group-props` and their `-with-arg` variants return copies; mutating a returned plist can no longer corrupt the registry. - `:transform` in `define-tps` group elements is honored (was silently dropped). - Group redefinition and `tp-undefine-group` clean up the layers the group generated, including their reactive deps and transforms (previously orphaned). - The group-element parser errors on unknown keywords instead of advancing by one and re-reading a value as a key. - Anonymous reactive layers are interned: an `equal` `$var` props spec reuses the existing registry entry instead of minting a new one on every `tp-set` (unbounded leak fixed). Layer stacks (tp-stack): - All stack mutators were rewritten onto a shared clipped region walker; region ops no longer alter text outside `[START, END)`, and `tp-put-layer` is region-local instead of switching behavior on whole-object emptiness. - The documented inline-plist spec (`(face bold ...)`) and list-of-layer-names spec (`'(layer-a layer-b)`) for `tp-put-layer` work (previously errored), handled at the call site. - `tp-region-layer-props` no longer double-offsets string positions. - `tp-merge-layers` / `tp-flatten-layers` no longer drop explicitly-nil values (presence is checked with `plist-member`). - Single-layer stacks no longer carry a garbage `(tp-layers nil)` property, and its absence is tolerated everywhere. - `tp-layer-top` respects the requested region instead of reading only the first interval. Search and navigation (tp-search): - `tp-backward` buffer paths passed no predicate to `text-property-search-backward`, so matching was inverted relative to `tp-forward`; backward now mirrors forward's equal-matching semantics. The legacy test that codified the inverted behavior (`tp-test-backward`) was updated to the symmetric contract. - Empty and zero-width patterns no longer loop forever in the match/regexp apply engines. - Length-changing replacements work in buffers in `tp-forward-do` / `tp-backward-do` / `tp-search-map` (previously signaled `args-out-of-range` via `store-substring`). On strings — which cannot change length in place — a length-changing replacement signals a clear error instead of silently truncating or leaving residue; same-length string replacements are unchanged. - `tp-search-map` with a non-current buffer OBJECT operates on that buffer (previously read and mutated the current buffer) and no longer corrupts buffers on length-changing replacements. - `tp-match-add` buffer path uses face-family-aware merging like the string path, so existing faces are preserved. - `tp-search-map` can remove properties on strings (nil-props ranges were previously skipped). - The triplicated ~38-line replacement lambda was extracted into one shared helper. - `tp-forward-do` / `tp-backward-do` shortfall is now all-or-nothing on both paths: TIMES targets the TIMES-th match specifically, so when fewer matches exist nothing is applied and the available count is returned. String paths previously acted on the last available match — the wrong target; the two legacy tests codifying that (`tp-test-forward-do-on-string-with-range` and its backward twin) were updated. Reactive rendering (tp-reactive / tp-render): - Sub-region `tp-text` on a string no longer discards the rest of the string. - Computed-variable updates deep-merge resolved props with the layer definition, preserving sibling static attributes. - Reactive refresh replaces the re-rendered layer's own property keys instead of accumulating (bold→italic no longer yields `(italic bold)`), while preserving other layers' properties. - `setq-local` re-renders the buffer without leaking buffer-local values into the global layer definition. - Reactive `tp-text` replacement preserves unrelated existing properties. - Computed values of nil propagate (nil was conflated with the error sentinel). - Variable-watcher reentrancy: nested `set` calls inside the update path queue their re-render through the batch queue instead of recursing. - Batched updates union their WHERE and tp-text flags at flush time instead of freezing the first change's. - Reactive strings keep per-interval props on re-render (previously only position-0 props survived and were smeared). - `:transform` applies on the first render too, not only on updates. Test infrastructure: - The test fixture now tears down with `unwind-protect` and resets all registries including `tp-layer-transforms` (previously leaked across tests); the suite passes in randomized order. - `tp-tests.el` header and `provide` renamed to match its file name. ### Added - `tp-member`: like `tp-at`, but distinguishes "property present with value nil" from "property absent" (plist-member-style result). - `Makefile` with `test` / `doctest` / `compile` / `clean` targets. - `tp-doctest.el`: executable documentation tests — 63 assertions reproducing README examples and comparing against their exact documented outputs (`make doctest`). - Per-module regression test suites: `tp-core-tests.el`, `tp-ops-tests.el`, `tp-builtins-tests.el`, `tp-layer-tests.el`, `tp-stack-tests.el`, `tp-search-tests.el`, `tp-render-tests.el` — the combined suite grew from 280 to 439 tests. ### Changed - License clarified to GPLv3+ in file headers, matching the shipped LICENSE file (headers previously said v2+). ## 0.1.0 Initial release (monolithic tp.el).