# Changelog All notable changes to the tp library are documented here. ## Unreleased ### Added - `tp-transaction-participate` lets a client promote rollback-capable opaque side state after all affected surfaces publish but before source values commit. Participant keys are unique per outer transaction, failure rolls participants back in reverse publication order, and observers still run only after the transaction exits. - `tp-propertize`, `tp-apply`, and `tp-watch` now provide simple one-shot string, one-shot buffer-range, and reactive existing-text entry points over the same schema/cascade/projector and retained properties-surface core. - TP 1.0 retained surfaces now provide defensive pure plans, prepare-scoped object identity, keyed/positional reconciliation, `content` and `properties` capabilities, marker-backed range anchors, same-surface overlapping property contributions, compare-before-write conflicts and explicit rebase, common-prefix/suffix text edits, property-run diffs, side indexes, opaque client state, generic reports, lifecycle cleanup, and atomic multi-buffer publication with exact rollback. Pure materialization uses the same plan semantics without leaving live handles or subscriptions. - TP 1.0 signals and bindings now form an exact source→binding and binding→binding dependency graph with conditional rewiring, memoized equality cutoffs, transaction-local candidate signal values, deduplicated topological flushing, nested-write stabilization, rollback, cycle paths, owner disposal, buffer-scoped sources, variable adapters, and public scheduler counters. The legacy layer scanner remains isolated only until the retained-surface cutover. - The first TP 1.0 runtime slice: `tp-style.el` provides atomic namespaced property schemas, structured subject selectors and combinators, deterministic origin/importance/layer/specificity/scope/source-order cascade, property-specific inheritance, tagged CSS-wide values, custom-property fallback/cycle handling, explicit `tp-computed` value sources, named declarations, provenance, and final Emacs-property projection. Ordinary function values remain literal. - Internal Stage 2 canonical façade records and dataflow: `tp--native-range`, `tp--presence`, `tp--request`, `tp--match`, and `tp--result`. Public entry points and historical return shapes remain compatible. - Stage 3 text-only native façade in `tp-query.el`: `tp-lookup-result`, `tp-lookup`, `tp-property-change`, `tp-property-any`, `tp-property-not-all`, and `tp-with-mutation-policy`. - Stage 4 managed lifecycle APIs: `tp-attach-managed-layers`, `tp-detach-managed-layers`, `tp-managed-layer-diagnostics`, `tp-managed-buffer-diagnostics`, `tp-managed-diagnostics`, and `tp-layer-transaction`. - Stage 5 overlay-aware lookup modes for `tp-lookup`: `:char` and `:char-source` report Emacs character-property values and the winning overlay identity when an overlay wins. - `docs/API-SEMANTICS.md` centralizes the current object/coordinate, mutation, presence/nil, search-result, layer-ownership, `tp-text`, error, and hidden-stack conflict contracts. - `tp-any-value`, a unique public sentinel for property-search wildcard matching when later positional arguments also need to be supplied. - `tp-unresolved-layer` and `tp-layer-conflict` error types. - `tp-reactive-observer-errors`, a newest-first structured record of isolated watcher callback failures. ### Fixed - Initial `tp-text` rendering now preserves every embedded property interval for strings and buffers instead of spreading position-zero properties across the replacement. - `tp-text` application preserves the caller's `tp-set`, `tp-reset`, or `tp-add` write semantics even when replacement text is unchanged. - Explicit nil properties override embedded `tp-text` values. - Non-parameterized layer redefinition refreshes managed regions with old/new ownership reconciliation: removed keys disappear, new keys replace them, and unrelated or externally changed values survive. - Search APIs distinguish omitted VALUE (any directly present value) from explicit nil (a present nil value), with one presence-aware run scanner shared by strings and buffers. - Removing the last nested sub-property removes the empty parent key consistently for strings and buffers. - Stack NOERROR catches only unresolved layer specifications; errors from layer bodies and internal operations propagate. - When hidden-layer full-stack storage detects an external direct property edit, stack decoding now signals `tp-layer-conflict` before any managed write instead of silently discarding the external value. - Buffer transaction rollback tracks its live range with markers, so insertions and deletions inside the range are removed or restored together with the original text-property snapshot. ### Changed - Transform and compute failures now propagate as business-output errors. A transform returning a non-string also signals. Watcher failures remain isolated observers, but are recorded structurally while the managed update continues. - Managed layer entries now store `tp-meta` in authoritative `tp-layers` storage, including parameterized args and definition versions. Direct rendered properties and public stack queries strip `tp-meta`; historical returns remain unchanged. - Parameterized mounted layer entries refresh from stored args after redefinition. - Insert/copy/yank/stickiness/narrowing/indirect-buffer behavior is now documented as direct Emacs delegation with no tp wrapper. - Overlay creation, movement, deletion, priority management, and lifecycle remain native Emacs responsibilities; tp only reports overlay-aware lookup results. - `tp-with-mutation-policy` accepts only ordinary+respect, ordinary+inhibit, and silent+inhibit; silent+respect is rejected. - Theme enable/disable events now increment theme generation and expose conservative refresh diagnostics. Reproducible benchmark evidence is recorded in `docs/BENCHMARKS.md`; timings are advisory baseline data, not release thresholds. ## 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).