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.
Problem
The stream block renderer (
internal/render/cli/cli.go) decides verbositylast-second with
if v >= signal.V/if v >= signal.VVscattered acrossrenderStepBlock,driftRows, andsatisfiedRows. The two*Rowsfunctionsare 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
-vrenderer that calls down andappends, then
-vvon top." That breaks on the op-id column: going-v->-vvdoesn't append to a row, it inserts a left column and re-aligns the
survivors:
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
indexhints(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: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/satisfiedRowscollapse into "build[]row" with zeroverbosity logic inside them, the op-column drops-and-realigns for free, and the
scattered
if v >=gates become data on the model.Scope
renderStepBlock+driftRows+satisfiedRows.RenderIndexStep's verbosity switch alone -- its hints ("run-v" /"run
-vv") are genuinely mutually exclusive, not additive, and a switch isthe honest shape there.
Safety net
The stream and region goldens (
stream_*.golden,region_*.golden) pin thecurrent output. A correct refactor should regenerate byte-identical (or with a
reviewed, intentional diff).
just test updateregenerates 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
runstep renders:The
runlabel is the op display-id -- apply-centric (run: <apply>) andcolliding with the
[3] runstep kind. It's misleading: in check mode nothingapply-side ran; the check was satisfied. Expected:
i.e. label the row by what was verified (
check), and at-vvshow the commandthat verified it (
sleep 3, truncated). The op-id is drift attribution (whichop 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.Checkreturns(CheckSatisfied, nil, nil), discarding the field and command (they only existon the drift path and in
Inspect). So this needs threading:spec.CheckSummarizer { CheckSummary() (label, detail string) }.runOpreturns("check", op.check). Ops that don't implement it fall back to(OpDisplayID(op), ""), socopy/serviceops keepensure_owner satisfiedetc. until taught otherwise.
event.Result.Opsbecomes[]event.OpLine{Label, Detail}instead of[]string.engine/exec.gobuilds theOpLines;satisfiedRowsrendersLabelat-v,Label Detailat-vv(detail column aligned, dropped where empty). Thisslots 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 runshows
check sleep 3 satisfied(no op-id). They carry different info -- fineto leave split, or reconsider the drift row's redundant op-id prefix for
single-op steps at the same time.