返回 DeepSeek-Reasonix
render.go
根目录 / internal / plancontract / render.go
1 package plancontract
2
3 import (
4 "fmt"
5 "strings"
6 )
7
8 // Render turns a plan into the markdown a user reads. Its only list items are
9 // the steps — phases numbered, sub-steps indented — in their stable order.
10 // RequiresApproval is absent on purpose: it is a routing request the host answers with its
11 // approval surface, not plan content.
12 func Render(p Plan) string {
13 p = p.Normalize()
14 var b strings.Builder
15 if p.Objective != "" {
16 fmt.Fprintf(&b, "**Objective** — %s\n", p.Objective)
17 }
18 renderAssumptions(&b, p.Assumptions)
19 if len(p.NonGoals) > 0 {
20 section(&b, "Non-goals")
21 for _, goal := range p.NonGoals {
22 fmt.Fprintf(&b, " %s\n", continuation(goal))
23 }
24 }
25 renderSteps(&b, p.Ordered())
26 return strings.TrimSpace(b.String())
27 }
28
29 func renderAssumptions(b *strings.Builder, assumptions []Assumption) {
30 if len(assumptions) == 0 {
31 return
32 }
33 section(b, "Assumptions")
34 for _, a := range assumptions {
35 if a.Confirm == "" {
36 fmt.Fprintf(b, " %s\n", continuation(a.Text))
37 continue
38 }
39 fmt.Fprintf(b, " %s (confirm: %s)\n", continuation(a.Text), a.Confirm)
40 }
41 }
42
43 // continuation strips a leading list marker from text that renders without a
44 // label, so a planner that writes its assumptions as bullets cannot smuggle a
45 // line into the step list.
46 func continuation(s string) string {
47 for _, marker := range []string{"- ", "* ", "+ "} {
48 if rest, ok := strings.CutPrefix(s, marker); ok {
49 return strings.TrimSpace(rest)
50 }
51 }
52 digits := 0
53 for digits < len(s) && s[digits] >= '0' && s[digits] <= '9' {
54 digits++
55 }
56 if digits > 0 && digits+1 < len(s) && (s[digits] == '.' || s[digits] == ')') && s[digits+1] == ' ' {
57 return strings.TrimSpace(s[digits+2:])
58 }
59 return s
60 }
61
62 func renderSteps(b *strings.Builder, steps []Step) {
63 if len(steps) == 0 {
64 return
65 }
66 section(b, "Plan")
67 phase := 0
68 for _, step := range steps {
69 if step.ParentID == "" {
70 phase++
71 fmt.Fprintf(b, "%d. %s\n", phase, step.Title)
72 renderDetail(b, step, " ")
73 continue
74 }
75 fmt.Fprintf(b, " - %s\n", step.Title)
76 renderDetail(b, step, " ")
77 }
78 }
79
80 // renderDetail writes a step's evidence and checks as indented continuation
81 // lines. They carry no list marker on purpose: a reader parsing the plan for its
82 // task list must see steps and nothing else.
83 func renderDetail(b *strings.Builder, step Step, indent string) {
84 if len(step.VerifiedFiles) > 0 {
85 fmt.Fprintf(b, "%sverified: %s\n", indent, strings.Join(step.VerifiedFiles, ", "))
86 }
87 if len(step.CandidateFiles) > 0 {
88 fmt.Fprintf(b, "%scandidate: %s\n", indent, strings.Join(step.CandidateFiles, ", "))
89 }
90 for _, c := range step.Acceptance {
91 label := "accept"
92 if c.Regression {
93 label = "regression"
94 }
95 if c.Optional {
96 label += " (optional)"
97 }
98 // The id is rendered because a proof has to cite it: a criterion the
99 // executor cannot name is one it cannot satisfy.
100 fmt.Fprintf(b, "%s%s [%s]: %s\n", indent, label, c.ID, c.Text)
101 }
102 for _, v := range step.Verification {
103 command := v.Command
104 if command == "" {
105 command = "any verification command"
106 }
107 if v.Expect != "" {
108 command += " — " + v.Expect
109 }
110 fmt.Fprintf(b, "%sverify: %s\n", indent, command)
111 }
112 for _, risk := range step.Risks {
113 fmt.Fprintf(b, "%srisk: %s\n", indent, risk)
114 }
115 }
116
117 func section(b *strings.Builder, title string) {
118 if b.Len() > 0 {
119 b.WriteString("\n")
120 }
121 fmt.Fprintf(b, "**%s**\n", title)
122 }
123
123 lines GO