Two output surfaces, one report shape. In proseprobe, our open-source project, neither the command line nor the batch runner gets to build its own report dictionary.
The shape lives in report.py and nowhere else.
report() takes text, scores it with score() at DEFAULT_CONFIDENCE, counts words with split_words, and hands the Score to report_score. That function is the only place the dictionary gets built: report_version from REPORT_VERSION, then name, path, words, score from _score_dict, and explanation from explain(...).text(), where excerpt_limit defaults to EXCERPT_LIMIT and limit defaults to None, so nothing is trimmed unless a caller asks.
_score_dict walks the score in a fixed order (summary, value, interval, band, spanned_bands, contributions, guard, notes), delegating to _range_dict, _contribution_dict, _guard_dict and _reading_dict, with every float passed through _round at DIGITS places.
Both callers go through that one door.
DocumentReport.to_dict in batch.py returns report_score with its own name, path and words, and batch.py imports DIGITS and REPORT_VERSION from report instead of defining them again. In cli.py, --json appends report_score, while the human path renders sentences through _render, which prints explain(...).text() followed by result.guard.summary() and computes no numbers of its own. One file prints one object, several print a list. That is the only difference between the two surfaces.
Our take: output drift between a CLI and a batch job is rarely a disagreement about logic. It is two callers each owning a little bit of formatting, a rounding here, a key order there, a version constant copied once and then forgotten. Consumers read the difference as a data problem, and you debug the wrong layer.
The cheap fix is making the shape a function with one implementation, and letting the version stamp live next to it. Then rounding and key order get decided before any caller exists, and a new output mode is a new caller rather than a new contract.
When you have more than one way to emit the same record, it pays to know which code owns the shape, a serializer both paths call or each path on its own. We think that is the thing worth checking first.