diff --git a/docs/database.md b/docs/database.md index 7c218b9..dc4ccd0 100644 --- a/docs/database.md +++ b/docs/database.md @@ -71,7 +71,7 @@ A revision number identifies a *state position between diffs*, not a diff: - Change records carry no revision of their own; the revision of a state is counted from the preceding snapshot (`snapshot.r + n` records after it), or from the start of the file. - Revisions are absolute and survive rotation: the leading snapshot of a rotated file carries the revision of its cutoff state, so `r` values keep referring to the same state positions across history rewrites. -`kanta.rev` exposes the current revision at runtime: the number of change records applied since revision 0, advanced in memory as transactions queue records. Reading it before `open()` raises `DataIntegrityError`. The CLI accepts `rN` range bounds to select records by revision (e.g. `-r r10:r20`): a bound `rN` addresses state N together with the record that produced it — the change record resulting in state N, or, when that change is no longer in the file, the snapshot materializing state N. `-r r5` and `-r r5:r6` therefore show the same record and output state 5, while `-r r5..r6` also includes the next change and outputs state 6. Ranges truncate Python-style instead of erroring: on a rotated file whose leading snapshot holds revision 3, `-r r1:r4` starts at the beginning of the file and shows that snapshot. Snapshot indicator lines show the snapshot's revision. +`kanta.rev` exposes the current revision at runtime: the number of change records applied since revision 0, advanced in memory as transactions queue records. Reading it before `open()` raises `DataIntegrityError`. The CLI accepts `rN` range bounds to select records by revision (e.g. `-r r10:r20`): a bound `rN` addresses state N together with the record that produced it — the change record resulting in state N, or, when that change is no longer in the file, the snapshot materializing state N. `-r r5` and `-r r5:r6` therefore show the same record and output state 5, while `-r r5..r6` also includes the next change and outputs state 6. Ranges truncate Python-style instead of erroring: on a rotated file whose leading snapshot holds revision 3, `-r r1:r4` starts at the beginning of the file and shows that snapshot. Change record headers show the revision of the state the record produces as `rN`. ## CLI Range Filtering diff --git a/kanta/__main__.py b/kanta/__main__.py index b74b3ca..f7ff874 100644 --- a/kanta/__main__.py +++ b/kanta/__main__.py @@ -307,6 +307,7 @@ def _print_change_log( previous: dict[str, Any], current: dict[str, Any], kanta: Kanta[Any], + revision: int, highlight: Any = None, ) -> None: """Log a single change record to stderr. @@ -314,8 +315,10 @@ def _print_change_log( The record is dispatched as a :class:`LogEvent` through the Kanta object's logemit handlers; the CLI's own rendering (with the ``l`` label and timestamp) is the fallback when no handler claims the event. - ``highlight`` is an optional :class:`kanta.grep.GrepHighlighter` with - the record's matched regions, applied to the fallback rendering. + The fallback header shows the revision of the state the record produces + as ``r``. ``highlight`` is an optional + :class:`kanta.grep.GrepHighlighter` with the record's matched regions, + applied to the fallback rendering. """ event = record_change_event(record, previous, current, kanta) @@ -328,6 +331,7 @@ def _print_change_log( lines = format_diff(ev.diff, ev.previous, ev.logfmt, highlight=highlight) else: header, lines = ev.header, ev.diff_lines + header += str(Line().target(f" r{revision}")) if not lines: _print(f"{label} {ts} {header}") elif len(lines) == 1: @@ -400,19 +404,16 @@ def _print_snapshot_indicator( label: str, snap: Snapshot, index: int, - revision: int, serializer: Serializer, ) -> None: """Print a snapshot indicator line to stderr. - ``snapshot s`` is rendered in bright white; the version, revision, - optional mtime and data size are printed in normal and dark colors - respectively. + ``snapshot s`` is rendered in bright white; the version, optional + mtime and data size are printed in normal and dark colors respectively. """ ts = _format_ts(snap.ts) line = Line().snapshot("snapshot").snapshot(f" s{index}") line.target(f" v{snap.v}") - line.target(f" r{revision}") if snap.m is not None: line.target(f" {_format_ts(snap.m)}") size = len(serializer.encode(snap.state)) @@ -554,7 +555,6 @@ async def _run(args: argparse.Namespace) -> int: record_label(snap_event.line_number, snap_event.record_index), snap_event.snap, snapshot_line_to_index[snap_event.line_number], - snap_event.revision, kanta._impl.serializer, ) _print() @@ -629,13 +629,18 @@ async def _run(args: argparse.Namespace) -> int: label, event.snap, snapshot_line_to_index[event.line_number], - event.revision, kanta._impl.serializer, ) else: assert previous is not None _print_change_log( - label, event.record, previous, current, kanta, highlight + label, + event.record, + previous, + current, + kanta, + event.revision, + highlight, ) if items: _print() diff --git a/tests/test_cli.py b/tests/test_cli.py index 6500c9f..b86fb86 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -105,9 +105,12 @@ def test_cli_snapshot_line_format(tmp_path, capsys, monkeypatch): # Snapshot line: bright white snapshot/sN, white version/mtime, dark size. assert "\x1b[97msnapshot s0" in err - assert "\x1b[38;5;250m v1 r0 2026-08-12 09:00:00" in err + assert "\x1b[38;5;250m v1 2026-08-12 09:00:00" in err assert "\x1b[38;5;242m 13 B" in err + # The change record header carries the revision it produces. + assert "\x1b[38;5;250m r1" in err + def test_cli_strips_ansi_without_color_support(tmp_path, capsys, monkeypatch): """Without a tty and with NO_COLOR set, output contains no ANSI codes.""" diff --git a/tests/test_revision.py b/tests/test_revision.py index 4be2d35..eb17a56 100644 --- a/tests/test_revision.py +++ b/tests/test_revision.py @@ -335,17 +335,19 @@ def test_cli_revision_snapshot_base_file(tmp_path, capsys): assert _run_cli(path, "-r", "r1") == 2 assert "r1" in capsys.readouterr().err - # r2 prints the snapshot line (it literally carries r: 2) and its state. + # r2 prints the snapshot line (it literally carries r: 2) and its state; + # revisions are shown on change records, not on snapshot indicators. assert _run_cli(path, "-r", "r2", "-o", "-") == 0 out = capsys.readouterr() assert "snapshot s0" in out.err - assert "r2" in out.err + assert "r2" not in out.err assert json.loads(out.out) == {"foo": 1, "bar": 1} # r3 is the change, replayed on the snapshot base; state includes baz. assert _run_cli(path, "-r", "r3", "-o", "-") == 0 out = capsys.readouterr() assert "add_baz" in out.err + assert " r3" in out.err assert json.loads(out.out) == {"foo": 1, "bar": 1, "baz": 1} # Single index and half-open range agree. @@ -380,6 +382,8 @@ def test_cli_revision_ranges(tmp_path, capsys): assert _run_cli(path, "-r", "r1") == 0 single = capsys.readouterr().err assert "bootstrap" in single + # Change record headers carry the revision of the state they produce. + assert " r1" in single assert "inc0" not in single assert _run_cli(path, "-r", "r1:r2") == 0 @@ -451,7 +455,7 @@ def test_cli_revision_after_rotation(tmp_path, capsys): assert _run_cli(path, "-r", "r2", "-o", "-") == 0 out = capsys.readouterr() assert "snapshot s0" in out.err - assert "r2" in out.err + assert "r2" not in out.err assert "day-20" not in out.err assert json.loads(out.out) == {"users": {}, "counter": 1} @@ -465,7 +469,7 @@ def test_cli_revision_after_rotation(tmp_path, capsys): assert _run_cli(path, "-r", "r1:r3") == 0 err = capsys.readouterr().err assert "snapshot s0" in err - assert "r2" in err + assert "r2" not in err assert "day-20" not in err # Absolute revisions reach the retained change records.