Skip to content

render: verbosity-projected block model + check-aware satisfied rows #437

Description

@pskry

Problem

The stream block renderer (internal/render/cli/cli.go) decides verbosity
last-second with if v >= signal.V / if v >= signal.VV scattered across
renderStepBlock, driftRows, and satisfiedRows. The two *Rows functions
are near-duplicates, each re-deriving its own op-column alignment, and the
block-vs-row gating is smeared across all three. Adding a verbosity level or a
new detail column means touching every one of them.

Why the obvious fix (a decorator chain) doesn't work

The instinct is "silent renderer, then a -v renderer that calls down and
appends, then -vv on top." That breaks on the op-id column: going -v -> -vv
doesn't append to a row, it inserts a left column and re-aligns the
survivors:

-v:    satisfied
-vv:   ensure_service_active   satisfied

A super().render() + append chain can't express "add a left column and re-pad"
without re-parsing the finished string the lower layer produced. Same story for
the ok-step header (gated wholesale, not base + delta) and the index hints
(replaced across levels, not added). The information is monotonic; the rendering
is not append-only.

Proposed shape: project a full model down

Represent a finished step as a header + rows, where each row is cells and each
cell carries a min signal.Verbosity:

// drift row:      [opID min=VV] [field min=V] ["cur → des" min=V]
// satisfied row:  [opID min=V]  ["satisfied"  min=V]

One projector does the whole verbosity job: drop cells below the target v,
compute column widths over the survivors, pad + join. The header carries its
own visibility min (ok -> V, changed/failed -> Quiet).

Result: driftRows/satisfiedRows collapse into "build []row" with zero
verbosity logic inside them, the op-column drops-and-realigns for free, and the
scattered if v >= gates become data on the model.

Scope

  • Stream block path only: renderStepBlock + driftRows + satisfiedRows.
  • Leave RenderIndexStep's verbosity switch alone -- its hints ("run -v" /
    "run -vv") are genuinely mutually exclusive, not additive, and a switch is
    the honest shape there.

Safety net

The stream and region goldens (stream_*.golden, region_*.golden) pin the
current output. A correct refactor should regenerate byte-identical (or with a
reviewed, intentional diff). just test update regenerates them.

Followup: satisfied rows should describe the check, not name the op

Once the projection model is in place, fix the satisfied-row content. Today a
converged run step renders:

✓ [3] run › publish shared state
    └─ run  satisfied

The run label is the op display-id -- apply-centric (run: <apply>) and
colliding with the [3] run step kind. It's misleading: in check mode nothing
apply-side ran; the check was satisfied. Expected:

-v:   check  satisfied
-vv:  check  sleep 3  satisfied

i.e. label the row by what was verified (check), and at -vv show the command
that verified it (sleep 3, truncated). The op-id is drift attribution (which
op reported a change); a satisfied op has nothing to attribute, so the useful
detail is what it checked, not who checked.

The data isn't in the event today: on a satisfied check runOp.Check returns
(CheckSatisfied, nil, nil), discarding the field and command (they only exist
on the drift path and in Inspect). So this needs threading:

  • New optional op interface, spec.CheckSummarizer { CheckSummary() (label, detail string) }.
    runOp returns ("check", op.check). Ops that don't implement it fall back to
    (OpDisplayID(op), ""), so copy/service ops keep ensure_owner satisfied
    etc. until taught otherwise.
  • event.Result.Ops becomes []event.OpLine{Label, Detail} instead of []string.
  • engine/exec.go builds the OpLines; satisfiedRows renders Label at -v,
    Label Detail at -vv (detail column aligned, dropped where empty). This
    slots cleanly into the cell-min-verbosity model above.

Known asymmetry to accept (or revisit): a changed run still shows
run check exit 1 -> exit 0 (op-id prefix at -vv) while a satisfied run
shows check sleep 3 satisfied (no op-id). They carry different info -- fine
to leave split, or reconsider the drift row's redundant op-id prefix for
single-op steps at the same time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions