Migrate ConfigKit reports to the reporter

This commit is contained in:
2026-10-26 03:20:17 +00:00
parent 5f37aac70f
commit c0e0010683
9 changed files with 177 additions and 129 deletions

View File

@@ -1,6 +1,6 @@
# Invocation reporting implementation plan
Status: Stages 1–11 complete. Stage 12 is next.
Status: Stages 1–12 complete. Stage 13 is next.
## Objective and authority
@@ -460,7 +460,7 @@ assembled suites.
## Stage 12: Migrate configuration sources, explain, and diff
Status: pending.
Status: complete.
Primary files: [sources.go](../../integrations/ckconfig/sources.go),
[explain.go](../../integrations/ckconfig/explain.go),
@@ -484,6 +484,16 @@ Validation: retain comparison preflight, selector, safe-report, and native
forwarding tests. Cover a representative failure on each destination and quiet
rendering without duplicating core's complete visibility matrix in every command.
Configuration sources, explanation, diff, and scope inventory now submit
provider messages through the shared reporter and render through managed output.
Native explanation rendering retains already-written partial output, while
renderer and destination failures are recorded in `Presentation` independently
of operation causes. Focused ckconfig package tests pass, including comparison
preflight, source evidence, safe reports, native forwarding, warning/context
ordering, quiet rendering, and result/diagnostic destination failures. The full
ckconfig module command still fails to compile `examples/simple`, which uses the
old invocation API and is scheduled for migration in Stage 17.
## Stage 13: Migrate PromptKit acquisition adapters
Status: pending.

View File

@@ -3,41 +3,44 @@ package ckconfig
import (
"errors"
"fmt"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
func runConfigDiff[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
if plan.comparison == nil || plan.comparison.scope == nil {
return configOperationFailure(errors.New("configuration comparison was not prepared"), nil, false)
cause := errors.New("configuration comparison was not prepared")
return configFailure("Configuration comparison was not prepared.", commandkit.Explanation{}, cause)
}
selected := plan.comparison
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, nil, false)
return configFailure("Configuration comparison was canceled.", commandkit.Explanation{}, err)
}
compare := selected.scope.scope.Compare
if compare == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no comparison callback", selected.scope.scope.Name), nil, false)
cause := fmt.Errorf("configuration scope %q has no comparison callback", selected.scope.scope.Name)
return configFailure("Configuration scope does not support comparison.", commandkit.Explanation{}, cause)
}
acquired, err := compare(invocation.Context, selected.request, state)
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
reporter := invocation.Reporter()
queueConfigDiagnostics(reporter, acquired.Diagnostics)
if err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure(fmt.Sprintf("Unable to compare configuration scope %q.", selected.scope.scope.Name), acquired.Explanation, err)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration comparison was canceled.", acquired.Explanation, err)
}
report, err := renderDiffReport(acquired.Value)
if err != nil {
return configOperationFailure(err, diagnostics, false)
err = reporter.RenderOutput(func(writer io.Writer) error {
report, err := renderDiffReport(acquired.Value)
if err != nil {
return err
}
_, err = writer.Write(report)
return err
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration comparison was canceled.", acquired.Explanation, invocation.Context.Err())
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
}
if err := writeConfigReport(invocation.Streams.Out, report); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
return commandkit.Success()
}

View File

@@ -101,21 +101,30 @@ func TestConfigDiffRunsComparisonOnlyScopeAndRejectsUnsupportedScopeBeforeWork(t
func TestConfigDiffProviderFailureRetainsDiagnosticsAndCause(t *testing.T) {
wantErr := errors.New("provider unavailable")
diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityError, Write: func(writer io.Writer) error {
diagnostic := commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(writer io.Writer) error {
_, err := io.WriteString(writer, "provider warning\n")
return err
}}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app", Operations: []Operation{Diff},
Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: []commandkit.Diagnostic{diagnostic}}, fmt.Errorf("comparison setup: %w", wantErr)
return commandkit.Acquired[[]configkit.Difference]{
Diagnostics: []commandkit.Diagnostic{diagnostic},
Explanation: commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error {
_, err := io.WriteString(writer, "native comparison context\n")
return err
}}},
}, fmt.Errorf("comparison setup: %w", wantErr)
},
}}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, wantErr) || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityError {
var output, diagnosticOutput bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnosticOutput})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, wantErr) || output.Len() != 0 {
t.Fatalf("provider failure outcome/output = %#v / %q", outcome, output.String())
}
if got := diagnosticOutput.String(); !strings.Contains(got, "provider warning") || !strings.Contains(got, "native comparison context") || strings.Index(got, "provider warning") > strings.Index(got, "native comparison context") || strings.Contains(got, wantErr.Error()) {
t.Fatalf("provider messages and terminal context = %q", got)
}
}
func TestConfigDiffReportPreparationFailureWritesNoReport(t *testing.T) {
@@ -128,8 +137,8 @@ func TestConfigDiffReportPreparationFailureWritesNoReport(t *testing.T) {
},
}}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.ReportIncomplete || outcome.DeliveryError != nil || output.Len() != 0 {
outcome := app.Execute(context.Background(), []string{"config", "diff", "--quiet"}, commandkit.Streams{Out: failingOutput{err: errors.New("quiet destination should not be called")}, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete || outcome.Presentation.Output.RenderError == nil || output.Len() != 0 {
t.Fatalf("report preparation failure outcome/output = %#v / %q", outcome, output.String())
}
}
@@ -146,8 +155,8 @@ func TestConfigDiffCancellationAfterAcquisitionRetainsDiagnosticsWithoutWriting(
}}}))
defer cancel()
var output bytes.Buffer
outcome := app.Execute(ctx, []string{"config", "diff"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) || outcome.ReportIncomplete || output.Len() != 0 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != commandkit.SeverityWarning {
outcome := app.Execute(ctx, []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, context.Canceled) || output.Len() != 0 {
t.Fatalf("cancelled comparison outcome/output = %#v / %q", outcome, output.String())
}
}
@@ -170,13 +179,36 @@ func TestConfigDiffOutputErrorsCarryIncompleteDeliveryOutcome(t *testing.T) {
} {
t.Run(test.name, func(t *testing.T) {
outcome := app.Execute(context.Background(), []string{"config", "diff"}, commandkit.Streams{Out: test.writer, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.DeliveryError, test.want) || !errors.Is(outcome.Cause, test.want) || !outcome.ReportIncomplete {
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.WriteError, test.want) || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete {
t.Fatalf("delivery failure outcome = %#v", outcome)
}
})
}
}
func TestConfigDiffDiagnosticDestinationFailureIsPresentationEvidence(t *testing.T) {
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app", Operations: []Operation{Diff},
Compare: func(context.Context, ComparisonRequest[struct{}], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
return commandkit.Acquired[[]configkit.Difference]{
Diagnostics: []commandkit.Diagnostic{{Severity: commandkit.SeverityWarning, Write: func(writer io.Writer) error {
_, err := io.WriteString(writer, "provider warning\n")
return err
}}},
}, nil
},
}}}))
writeErr := errors.New("diagnostic destination failed")
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "diff", "--verbose"}, commandkit.Streams{Out: &output, Err: failingOutput{err: writeErr}})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Diagnostics.WriteError, writeErr) || outcome.Presentation.Diagnostics.State != commandkit.DeliveryIncomplete {
t.Fatalf("diagnostic delivery failure outcome = %#v", outcome)
}
if output.Len() == 0 {
t.Fatal("healthy result destination did not receive the diff report")
}
}
func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) {
type input struct {
ID int

View File

@@ -2,7 +2,6 @@ package ckconfig
import (
"bytes"
"errors"
"fmt"
"io"
@@ -54,70 +53,59 @@ type secretReferenceDTO struct {
func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
selected := plan.scopes[0]
name := selected.scope.scope.Name
reporter := invocation.Reporter()
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, nil, false)
return configFailure("Configuration explanation was canceled.", commandkit.Explanation{}, err)
}
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
cause := fmt.Errorf("configuration scope %q has no acquisition callback", name)
return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), commandkit.Explanation{}, cause)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Explain,
Selection: plan.selection,
}, selected.input, state)
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
queueConfigDiagnostics(reporter, acquired.Diagnostics)
if acquireErr != nil {
return configOperationFailure(acquireErr, diagnostics, false)
return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
}
if acquired.Value.Explanation == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name)
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause)
}
explanation, found := acquired.Value.Explanation.Explain(plan.path)
if !found {
return configOperationFailure(fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name), diagnostics, false)
cause := fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name)
return configFailure("The requested configuration path is not available.", acquired.Explanation, cause)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
}
if plan.format != "yaml" {
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, plan.path, diagnostics)
}
var report bytes.Buffer
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
return configOperationFailure(err, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
}
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []commandkit.Diagnostic) commandkit.Outcome {
checked := &checkedWriter{writer: writer}
renderErr := reader.WriteExplanation(checked, path)
if checked.failure != nil {
if renderErr == nil {
renderErr = checked.failure
} else {
renderErr = errors.Join(renderErr, checked.failure)
err := reporter.RenderOutput(func(writer io.Writer) error {
return acquired.Value.Explanation.WriteExplanation(writer, plan.path)
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
}
return commandkit.Success()
}
if renderErr != nil {
outcome := configOperationFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
err := reporter.RenderOutput(func(writer io.Writer) error {
var report bytes.Buffer
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
return err
}
_, err := writer.Write(report.Bytes())
return err
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
return commandkit.Success()
}
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {

View File

@@ -1,6 +1,7 @@
package ckconfig
import (
"bytes"
"context"
"errors"
"fmt"
@@ -10,7 +11,6 @@ import (
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"go.yaml.in/yaml/v4"
)
type scopeMetadata struct {
@@ -265,19 +265,18 @@ func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.
)},
CheckInput: func(input scopeInventoryInput) error { return checkConfigFormat(input.Format) },
Run: func(invocation commandkit.Invocation, input scopeInventoryInput) commandkit.Outcome {
var output []byte
var err error
if input.Format == "yaml" {
output, err = yaml.Marshal(prepared.inventory)
} else {
output = []byte(formatScopeInventory(prepared.inventory))
}
if err != nil {
return commandkit.Failure(err)
}
if err := writeConfigReport(invocation.Streams.Out, output); err != nil {
return incompleteConfigReport(err)
}
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
var report bytes.Buffer
if input.Format == "yaml" {
if err := encodeYAML(&report, prepared.inventory); err != nil {
return err
}
} else {
_, _ = report.WriteString(formatScopeInventory(prepared.inventory))
}
_, err := writer.Write(report.Bytes())
return err
})
return commandkit.Success()
},
}
@@ -314,21 +313,3 @@ func checkConfigFormat(format string) error {
}
func parseConfigString(value string) (string, error) { return value, nil }
func writeConfigReport(writer io.Writer, content []byte) error {
written, err := writer.Write(content)
if err != nil {
return err
}
if written != len(content) {
return io.ErrShortWrite
}
return nil
}
func incompleteConfigReport(err error) commandkit.Outcome {
outcome := commandkit.Failure(err)
outcome.ReportIncomplete = true
outcome.DeliveryError = err
return outcome
}

View File

@@ -472,13 +472,28 @@ func TestExplainParsesBeforeAcquisitionAndPreservesNativeSafeHistory(t *testing.
}
}
func TestReportWriteFailureTracksIncompleteDeliveryAndCancellation(t *testing.T) {
func TestExplainTracksNativeRenderFailureWithoutReplacingOperationCause(t *testing.T) {
renderErr := errors.New("native explanation renderer failed")
reader := &failingNativeExplanation{err: renderErr}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Explanation: reader}}, nil
},
}}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", "/token"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.RenderError, renderErr) || output.String() != "partial native explanation\n" {
t.Fatalf("native explanation failure outcome/output = %#v / %q", outcome, output.String())
}
}
func TestReportWriteFailureIsRecordedByReporterAndPreservesCancellation(t *testing.T) {
writeErr := errors.New("output failed")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Sources: &sourceInventory{values: []configkit.SourceInfo{}}}}, nil
}}}}))
outcome := app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
if !errors.Is(outcome.Cause, writeErr) || outcome.DeliveryError != writeErr || !outcome.ReportIncomplete {
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != nil || !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || outcome.Presentation.Output.State != commandkit.DeliveryIncomplete {
t.Fatalf("source delivery failure outcome = %#v", outcome)
}
@@ -531,6 +546,19 @@ type nativeExplanation struct {
found bool
}
type failingNativeExplanation struct{ err error }
func (reader *failingNativeExplanation) Explain(configkit.Path) (configkit.Explanation, bool) {
return configkit.Explanation{}, true
}
func (reader *failingNativeExplanation) WriteExplanation(writer io.Writer, _ configkit.Path) error {
if _, err := io.WriteString(writer, "partial native explanation\n"); err != nil {
return err
}
return reader.err
}
func (reader *nativeExplanation) Explain(path configkit.Path) (configkit.Explanation, bool) {
reader.path = path
return reader.value, reader.found

View File

@@ -70,14 +70,14 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
}
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
if index > 0 && hasDirective {
if err := writeConfigReport(writer, []byte("...\n")); err != nil {
if _, err := io.WriteString(writer, "...\n"); err != nil {
return err
}
}
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
framed = append(framed, '\n')
}
if err := writeConfigReport(writer, framed); err != nil {
if _, err := writer.Write(framed); err != nil {
return err
}
}

View File

@@ -52,25 +52,28 @@ type versionDTO struct {
func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
selected := plan.scopes[0]
name := selected.scope.scope.Name
reporter := invocation.Reporter()
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, nil, false)
return configFailure("Configuration source listing was canceled.", commandkit.Explanation{}, err)
}
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
cause := fmt.Errorf("configuration scope %q has no acquisition callback", name)
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), commandkit.Explanation{}, cause)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Sources,
Selection: plan.selection,
}, selected.input, state)
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
queueConfigDiagnostics(reporter, acquired.Diagnostics)
if acquireErr != nil {
return configOperationFailure(acquireErr, diagnostics, false)
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
}
if acquired.Value.Sources == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
cause := fmt.Errorf("configuration scope %q did not provide a source reader", name)
return configFailure("Configuration scope did not provide source information.", acquired.Explanation, cause)
}
sources := acquired.Value.Sources.Sources()
@@ -111,23 +114,25 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
}
document.Sources = append(document.Sources, item)
}
var report bytes.Buffer
if plan.format == "yaml" {
if err := encodeYAML(&report, document); err != nil {
return configOperationFailure(err, diagnostics, false)
}
} else {
writeSourceReportText(&report, document)
}
if err := invocation.Context.Err(); err != nil {
return configOperationFailure(err, diagnostics, false)
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
}
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
return configDeliveryFailure(err, diagnostics, true)
err := reporter.RenderOutput(func(writer io.Writer) error {
var report bytes.Buffer
if plan.format == "yaml" {
if err := encodeYAML(&report, document); err != nil {
return err
}
} else {
writeSourceReportText(&report, document)
}
_, err := writer.Write(report.Bytes())
return err
})
if err != nil && invocation.Context.Err() != nil {
return configFailure("Configuration source listing was canceled.", acquired.Explanation, invocation.Context.Err())
}
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
return commandkit.Success()
}
func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {

View File

@@ -39,11 +39,12 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi
return err
}
if plan.format == "yaml" && !writtenRecord {
if err := writeConfigReport(writer, []byte("scopes:\n")); err != nil {
if _, err := io.WriteString(writer, "scopes:\n"); err != nil {
return err
}
}
return writeConfigReport(writer, record)
_, err = writer.Write(record)
return err
})
if err != nil {
if ctxErr := invocation.Context.Err(); ctxErr != nil {