package engine import ( "errors" "io" "strconv" "strings" ) var ( errExplanationUnavailable = errors.New("configkit: configuration explanation is unavailable") errExplanationUnknownPath = errors.New("configkit: configuration explanation path is unknown") ) // WriteExplanation writes a deterministic human-readable rendering of Explain. // Unknown paths produce no output. Writer failures may leave partial output. func (r *Inspection) WriteExplanation(output io.Writer, path Path) error { if r == nil || output == nil { return errExplanationUnavailable } explanation, ok := r.Explain(path) if !ok { return errExplanationUnknownPath } var rendered strings.Builder labels := make(map[string]string, len(r.sources)) for _, source := range r.sources { labels[source.ID] = source.Label } rendered.WriteString("path: ") rendered.WriteString(strconv.Quote(explanation.Path.String())) rendered.WriteByte('\n') writeExplanationOrigin(&rendered, "effective source", explanation.Origin, labels) writeExplanationSnapshot(&rendered, "effective (canonical typed)", explanation.Effective, 0) if explanation.SecretReference != nil { rendered.WriteString("secret reference: ") rendered.WriteString(strconv.Quote(explanation.SecretReference.Name)) rendered.WriteString(" (resolved from process environment)\n") } rendered.WriteString("history:\n") for _, change := range explanation.History { rendered.WriteString(" - representation: ") if change.Origin.Operation == ChangeDecoding { rendered.WriteString("input to canonical typed\n") } else if change.Origin.Operation == ChangeInheritance { rendered.WriteString("structural inheritance\n") } else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerUnderlay || change.Origin.Layer == LayerOverlay || change.Origin.Layer == LayerCLI { rendered.WriteString("policy-validated input\n") } else { rendered.WriteString("canonical typed\n") } writeExplanationOrigin(&rendered, " source", change.Origin, labels) writeExplanationSnapshot(&rendered, " before", change.Before, 4) writeExplanationSnapshot(&rendered, " after", change.After, 4) for _, reason := range change.Reasons { rendered.WriteString(" reason: ") rendered.WriteString(strconv.Quote(reason)) rendered.WriteByte('\n') } for _, dependency := range change.Dependencies { rendered.WriteString(" dependency: ") rendered.WriteString(strconv.Quote(dependency.String())) rendered.WriteByte('\n') } } data := []byte(rendered.String()) written, err := output.Write(data) if err == nil && written != len(data) { return io.ErrShortWrite } return err } func writeExplanationOrigin(output *strings.Builder, label string, origin Origin, labels map[string]string) { output.WriteString(label) output.WriteString(": layer=") output.WriteString(strconv.Quote(string(origin.Layer))) if origin.Operation != "" { output.WriteString(" operation=") output.WriteString(strconv.Quote(string(origin.Operation))) } output.WriteByte('\n') for _, location := range origin.Locations { output.WriteString(" location: source=") output.WriteString(strconv.Quote(location.SourceID)) if label := labels[location.SourceID]; label != "" { output.WriteString(" label=") output.WriteString(strconv.Quote(label)) } output.WriteString(" line=") output.WriteString(strconv.Itoa(location.Line)) output.WriteString(" column=") output.WriteString(strconv.Itoa(location.Column)) output.WriteByte('\n') } for _, step := range origin.Inheritance { output.WriteString(" inherited: from=") output.WriteString(strconv.Quote(step.From.String())) output.WriteString(" to=") output.WriteString(strconv.Quote(step.To.String())) output.WriteString(" reference=") output.WriteString(strconv.Quote(step.Reference.String())) output.WriteByte('\n') } } func writeExplanationSnapshot(output *strings.Builder, label string, snapshot ValueSnapshot, indent int) { if !snapshot.Present { output.WriteString(label) output.WriteString(": absent\n") return } output.WriteString(label) if snapshot.Redacted { output.WriteString(" (redacted)") } output.WriteString(":\n") prefix := strings.Repeat(" ", indent+2) for _, line := range strings.SplitAfter(snapshot.YAML, "\n") { if line == "" { continue } output.WriteString(prefix) output.WriteString(line) if !strings.HasSuffix(line, "\n") { output.WriteByte('\n') } } }