tp/CHANGELOG.md
Kinneyzhang 8d108403ad feat(tp): add rollback-capable transaction participants
Publish client-owned side state after retained surfaces and roll it back in reverse order on any transaction failure.\n\nVerified: 715 ERT tests, 92 doctests, WERROR compile-all, checkdoc, git diff --check.
2026-08-06 03:40:40 +08:00

427 lines
22 KiB
Markdown

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