Skip to content

Commit 5b712da

Browse files
Add dow --version flag and fix man-page control-line text loss (v2.0.3)
Two defects surfaced during a fresh-venv install verification of v2.0.2: - There was no `dow --version` / `dow -V`, even though the version already fed the man-page header. Added a top-level eager Typer option. - The roff generator did not guard wrapped prose lines that begin with `'` or `.` (a roff control-line prefix), so troff silently dropped such sentences (e.g. init's "'dow commit' to capture v1 ...") and warned `macro 'dow' not defined`. `_roff` now protects them with a zero-width `\&`. Regenerated the packaged man page (its header was a stale 0.1.0). Adds tests/test_manpage.py to pin both fixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4dcc23b commit 5b712da

5 files changed

Lines changed: 95 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ All notable changes to dow are documented here. The format follows
44
[Keep a Changelog](https://keepachangelog.com/), and dow adheres to
55
[Semantic Versioning](https://semver.org/).
66

7+
## [2.0.3] - 2026-07-11
8+
9+
### Added
10+
- **`dow --version` / `dow -V`.** A top-level flag now prints the installed
11+
version and exits. The version was already computed for the man-page header
12+
but was not reachable from the command line.
13+
14+
### Fixed
15+
- **Man page dropped any prose line that began with `'` or `.`.** A wrapped
16+
documentation sentence starting with an apostrophe or period (e.g. init's
17+
"'dow commit' to capture v1 ...") is a roff *control* line, so troff silently
18+
swallowed it and warned `macro 'dow' not defined`. The roff generator now
19+
guards such lines with a leading zero-width `\&`, so every sentence renders.
20+
Regression tests added (`tests/test_manpage.py`).
21+
- **Regenerated the packaged man page** (`man/dow.1`): its header showed a stale
22+
`dow 0.1.0` and predated the escaping fix.
23+
724
## [2.0.2] - 2026-07-11
825

926
### Changed

dow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.2"
1+
__version__ = "2.0.3"

dow/cli.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,16 @@ def _dow_version() -> str:
435435

436436

437437
def _roff(text: str) -> str:
438-
"""Escape dynamic text for a roff man page (backslashes and hyphens)."""
439-
return text.replace("\\", "\\\\").replace("-", "\\-")
438+
"""Escape dynamic text for a roff man page.
439+
440+
Doubles backslashes and escapes hyphens, and protects any line that begins
441+
with ``.`` or ``'`` - which roff treats as a control line - with a leading
442+
zero-width ``\\&`` so wrapped prose (e.g. a sentence starting with
443+
``'dow commit'``) is not silently swallowed by troff.
444+
"""
445+
text = text.replace("\\", "\\\\").replace("-", "\\-")
446+
lines = text.split("\n")
447+
return "\n".join("\\&" + ln if ln[:1] in (".", "'") else ln for ln in lines)
440448

441449

442450
def _command_usage(cmd, name: str) -> str:
@@ -591,6 +599,26 @@ def _print_banner() -> None:
591599
pass # decorative only; never let a legacy console break the command
592600

593601

602+
def _version_callback(value: bool) -> None:
603+
if value:
604+
report.console.print(f"dow {_dow_version()}")
605+
raise typer.Exit()
606+
607+
608+
@app.callback()
609+
def _main(
610+
version: bool = typer.Option(
611+
None,
612+
"--version",
613+
"-V",
614+
callback=_version_callback,
615+
is_eager=True,
616+
help="Show the dow version and exit.",
617+
),
618+
) -> None:
619+
"""Drift Observation Workbench - track how your AI's behavior changes across versions."""
620+
621+
594622
def main() -> None:
595623
# UTF-8 output so the banner and box-drawing art survive redirection on Windows
596624
# (a piped stdout otherwise defaults to cp1252 and cannot encode the glyphs).

man/dow.1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.TH DOW 1 "2026-07-10" "dow 0.1.0" "Drift Observation Workbench"
1+
.TH DOW 1 "2026-07-11" "dow 2.0.3" "Drift Observation Workbench"
22
.SH NAME
33
dow \- track how your AI's behavior changes across versions
44
.SH SYNOPSIS
@@ -18,7 +18,7 @@ Creates specs/<name>.yaml from a starter template and, if it does not exist yet,
1818
an evals.py with a couple of example evaluators. Pass a name to choose the spec
1919
(the default is 'summarization'); the spec's name and the file are kept in sync.
2020
Edit the spec to describe your prompt, model, sampling, and evaluation, then run
21-
'dow commit' to capture v1. This only writes starter files \- there is no staging
21+
\&'dow commit' to capture v1. This only writes starter files \- there is no staging
2222
area, index, or refs, and it refuses to overwrite an existing spec.
2323
.PP
2424
Arguments:
@@ -457,4 +457,3 @@ A vLLM OpenAI-compatible server, local or remote. Set VLLM_BASE_URL (default htt
457457
.PP
458458
.SH SEE ALSO
459459
Run \fBdow help\fR or \fBdow help \fICOMMAND\fR for interactive help.
460-

tests/test_manpage.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Man page + version surface: guard the roff generator and `dow --version`.
2+
3+
Regression coverage for two defects found during a v2.0.2 install verification:
4+
5+
1. A wrapped doc line beginning with ``'`` or ``.`` is a roff control line, so
6+
troff silently dropped it (e.g. init.txt's "'dow commit' to capture v1 ..."
7+
sentence vanished from `man dow`). `_roff` now protects such lines with a
8+
leading zero-width ``\\&``.
9+
2. There was no ``dow --version`` flag even though the version feeds the man
10+
page header; it is now a top-level eager option.
11+
"""
12+
from typer.testing import CliRunner
13+
14+
from dow.cli import _dow_version, _render_manpage, _roff, app
15+
16+
runner = CliRunner()
17+
18+
19+
def test_roff_protects_leading_control_characters():
20+
# Lines that begin with a roff control char get a zero-width guard.
21+
assert _roff("'dow commit' to capture v1").startswith("\\&'")
22+
assert _roff(".PP is not a real macro here").startswith("\\&.")
23+
# Ordinary prose is untouched (apart from hyphen/backslash escaping).
24+
assert _roff("ordinary line") == "ordinary line"
25+
# Multi-line text is guarded line by line.
26+
out = _roff("first line\n'second starts with quote")
27+
assert out.splitlines()[1].startswith("\\&'")
28+
29+
30+
def test_manpage_has_no_unprotected_control_line_from_prose():
31+
page = _render_manpage()
32+
# The init sentence that troff used to swallow must survive, guarded.
33+
assert "'dow commit' to capture v1" in page
34+
assert "\\&'dow commit' to capture v1" in page
35+
# No line may start with an apostrophe followed by a letter (that is the
36+
# exact shape troff misreads as an undefined macro call).
37+
for line in page.splitlines():
38+
assert not (line[:1] == "'" and line[1:2].isalpha()), line
39+
40+
41+
def test_version_flag_prints_version():
42+
for flag in ("--version", "-V"):
43+
result = runner.invoke(app, [flag])
44+
assert result.exit_code == 0
45+
assert _dow_version() in result.stdout

0 commit comments

Comments
 (0)