alt/apps/cli/internal/operator/output.go
toki 6ad82e2417 feat(cli): add operator market data workflow support
- Add client, output, parser_map, runner for headless operator workflow
- Support market data status query scenario
- Archive completed G07 subtask documents (02+01_api_market)
- Update CLI and scenario implementations
2026-06-01 10:45:09 +09:00

117 lines
2.9 KiB
Go

package operator
import (
"encoding/json"
"fmt"
"io"
)
// OutputFormat selects how runner events are rendered to stdout.
type OutputFormat string
const (
// OutputText emits grep-friendly key=value lines (the default).
OutputText OutputFormat = "text"
// OutputJSONL emits one JSON object per line.
OutputJSONL OutputFormat = "jsonl"
)
// noCount marks a step event that has no market result count (for example a
// hello handshake or a transport failure).
const noCount = -1
// StepEvent is one machine-readable record describing a single executed step.
type StepEvent struct {
Scenario string
Step string
Action string
Status string
Count int
ErrorCode string
ErrorMessage string
}
// RunSummary is the final record describing a whole scenario run.
type RunSummary struct {
Scenario string
Status string
Steps int
Passed int
}
// Writer renders runner events to an io.Writer in the selected format. stdout
// carries these machine-readable lines; scripts grep them for status keys.
type Writer struct {
w io.Writer
format OutputFormat
}
// NewWriter returns a Writer for the given format, defaulting to text.
func NewWriter(w io.Writer, format OutputFormat) *Writer {
if format == "" {
format = OutputText
}
return &Writer{w: w, format: format}
}
// WriteStep renders one executed step.
func (o *Writer) WriteStep(ev StepEvent) {
if o.format == OutputJSONL {
fields := map[string]any{
"type": "step",
"scenario": ev.Scenario,
"step": ev.Step,
"action": ev.Action,
"status": ev.Status,
}
if ev.Count >= 0 {
fields["count"] = ev.Count
}
if ev.ErrorCode != "" {
fields["error_code"] = ev.ErrorCode
}
if ev.ErrorMessage != "" {
fields["error_message"] = ev.ErrorMessage
}
o.writeJSON(fields)
return
}
line := fmt.Sprintf("scenario=%s step=%s action=%s status=%s", ev.Scenario, ev.Step, ev.Action, ev.Status)
if ev.Count >= 0 {
line += fmt.Sprintf(" count=%d", ev.Count)
}
if ev.ErrorCode != "" {
line += fmt.Sprintf(" error.code=%s", ev.ErrorCode)
}
if ev.ErrorMessage != "" {
line += fmt.Sprintf(" error.message=%q", ev.ErrorMessage)
}
fmt.Fprintln(o.w, line)
}
// WriteSummary renders the final run summary.
func (o *Writer) WriteSummary(s RunSummary) {
if o.format == OutputJSONL {
o.writeJSON(map[string]any{
"type": "summary",
"scenario": s.Scenario,
"status": s.Status,
"steps": s.Steps,
"passed": s.Passed,
})
return
}
fmt.Fprintf(o.w, "scenario=%s status=%s steps=%d passed=%d\n", s.Scenario, s.Status, s.Steps, s.Passed)
}
func (o *Writer) writeJSON(fields map[string]any) {
data, err := json.Marshal(fields)
if err != nil {
// Marshalling a map of strings/ints cannot realistically fail; fall back
// to a minimal line so a step result is never silently dropped.
fmt.Fprintf(o.w, "{\"type\":\"error\",\"error_message\":%q}\n", err.Error())
return
}
fmt.Fprintln(o.w, string(data))
}