Dump the state after the last printed record for all -r/--grep/--context filters

This commit is contained in:
LeoVasanko committed 2026-09-28 04:36:41 +00:00
1 parent ecf85445ea
commit c2821dedb5
3 files changed
+45 -11

No files matched your search

+3 -1
View File
@@ -78,12 +78,14 @@ A revision number identifies a *state position between diffs*, not a diff:
`-r` is repeatable and its specs fall into two classes:
- **Unit specs** — any spec with a letter-prefixed bound (`l`, `s`, `v`, `r`, including mixed forms like `2:s3`) — index into the database file and resolve to a line window. Multiple unit specs intersect restrictively: only the common line range is processed.
- **Plain-number specs** — both bounds (or the single index) are plain numbers — index into the *filtered output*: the records left after all unit specs and all `--grep` patterns have been applied. They are a pure display filter and do not affect replay, migrations, or the `-o` output state. Negative values count from the end of the filtered output.
- **Plain-number specs** — both bounds (or the single index) are plain numbers — index into the *filtered output*: the records left after all unit specs and all `--grep` patterns have been applied. Negative values count from the end of the filtered output.
Like repeated `--grep` patterns (which must all match), repeated ranges are restrictive: `-r r1:r5 -r 0:2` prints only the first two records of those in the revision window, regardless of flag order. Plain ranges truncate Python-style and print nothing when the intersection is empty; a single plain index must exist and fails with exit code 2 otherwise.
`--context N` (default 0) adds GNU grep-style context as the final step: each printed item is expanded by N surrounding records drawn from the records in the processed range, free of the `--grep` and plain-number filters, with overlapping expansions merged so nearby matches do not duplicate context records.
The JSON dump (`-o`) and migrations operate on the state after the **last printed record**, whatever filtering produced the printout — so a plain `-r 2:5` dump ends at the 5th filtered record, and `--grep` or `--context` shift the dump to their last printed record as well. When nothing is printed (an empty filter result or `-q`), the dump falls back to the state at the end of the unit-spec window (or the end of the file).
## Meta Format Migrations
The snapshot `k` field carries an integer meta format version (`kanta.structs.META_FORMAT`, currently 1), bumped manually whenever the record-level format around the payloads changes. k1 introduced revision numbers.
+11 -4
View File
@@ -244,9 +244,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
" vN = version, rN = revision (rN is the record that produced"
" state N, with its state). Plain numbers instead index into"
" the filtered output: they select among the records left after"
" all unit ranges and --grep patterns, like a display filter,"
" without affecting the output state. Repeated ranges are"
" restrictive: only the common subset is printed. Negative"
" all unit ranges and --grep patterns. The -o dump and"
" migrations operate on the state after the last printed"
" record. Repeated ranges are restrictive: only the common"
" subset is printed. Negative"
" snapshot values count from the end (s-1 is the last"
" snapshot); negative plain numbers count from the end of the"
" filtered output. Use ':' for"
@@ -622,6 +623,12 @@ async def _run(args: argparse.Namespace) -> int:
expanded.update(range(lo, hi))
matched = sorted(expanded)
items = [pool[idx] for idx in matched]
# The dump and migrations operate on the state after the last
# printed item; an empty display keeps the window-end state.
if items:
last_event, _, last_current, _ = items[-1]
state = last_current
version = last_event.version
for event, previous, current, highlight in items:
label = record_label(event.line_number, event.record_index)
if isinstance(event, SnapshotEvent):
@@ -645,7 +652,7 @@ async def _run(args: argparse.Namespace) -> int:
if items:
_print()
# Apply optional migrations to the range-end state.
# Apply optional migrations to the selected end state.
if kanta._impl.migrations is not None:
try:
previous_version = version
+31 -6
View File
@@ -28,8 +28,8 @@ def _build(path, increments=3):
asyncio.run(build())
def test_plain_range_filters_output_not_state(tmp_path, capsys):
"""Plain numbers slice the printed items; -o state is unaffected."""
def test_plain_range_restricts_dump_to_last_printed(tmp_path, capsys):
"""The JSON dump holds the state after the last printed item."""
path = tmp_path / "data.kantadb"
_build(path)
capsys.readouterr()
@@ -41,8 +41,8 @@ def test_plain_range_filters_output_not_state(tmp_path, capsys):
assert "inc0" in out.err
assert "inc1" in out.err
assert "inc2" not in out.err
# Display filter only: the output state is the full replayed state.
assert json.loads(out.out) == {"users": {}, "counter": 3}
# The dump is the state after inc1, the last printed record.
assert json.loads(out.out) == {"users": {}, "counter": 2}
# Inclusive end.
assert _run_cli(path, "-r", "0..1") == 0
@@ -143,8 +143,8 @@ def test_unit_and_plain_combine(tmp_path, capsys):
out = capsys.readouterr()
assert "inc0" in out.err
assert "inc1" not in out.err
# The plain spec does not narrow the output state.
assert json.loads(out.out) == {"users": {}, "counter": 2}
# The dump is the state after the last printed item (inc0).
assert json.loads(out.out) == {"users": {}, "counter": 1}
def test_context_expands_after_grep(tmp_path, capsys):
@@ -191,3 +191,28 @@ def test_context_merges_overlaps(tmp_path, capsys):
assert err.count(action) == 1
assert "bootstrap" not in err
assert "inc4" not in err
def test_grep_and_context_shift_dump_to_last_printed(tmp_path, capsys):
"""Grep and --context are display filters, but the dump follows the
last printed record, so they move it too."""
path = tmp_path / "data.kantadb"
_build(path)
capsys.readouterr()
# Only inc1 prints; the dump is the state after inc1.
assert _run_cli(path, "--grep", "inc1", "-o", "-") == 0
out = capsys.readouterr()
assert "inc0" not in out.err
assert json.loads(out.out) == {"users": {}, "counter": 2}
# Context extends the printout past the match; the dump follows it.
assert _run_cli(path, "--grep", "inc1", "--context", "1", "-o", "-") == 0
out = capsys.readouterr()
assert "inc2" in out.err
assert json.loads(out.out) == {"users": {}, "counter": 3}
# Nothing printed: the dump falls back to the range-end state.
assert _run_cli(path, "--grep", "zzz", "-o", "-") == 0
out = capsys.readouterr()
assert json.loads(out.out) == {"users": {}, "counter": 3}