返回 DeepSeek-Reasonix
report.go
根目录 / internal / completion / report.go
1 package completion
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/evidence"
8 )
9
10 // Verdict is the report's headline. Partial is terminal: the work is proven
11 // against its criteria, and the gaps it still carries are declared rather
12 // than hidden.
13 type Verdict uint8
14
15 const (
16 VerdictUnknown Verdict = iota
17 VerdictIncomplete
18 VerdictPartial
19 VerdictDone
20 )
21
22 func (v Verdict) String() string {
23 switch v {
24 case VerdictIncomplete:
25 return "incomplete"
26 case VerdictPartial:
27 return "partial"
28 case VerdictDone:
29 return "done"
30 default:
31 return "unknown"
32 }
33 }
34
35 // Change is one path the turn mutated. Reviewed reports whether the changed
36 // result was inspected after the last write to it.
37 type Change struct {
38 Path string
39 Reviewed bool
40 }
41
42 // Verification is a delivery-verification command's latest outcome. Stale
43 // means it last ran before the newest mutation, so it proves nothing about
44 // the current tree.
45 type Verification struct {
46 Command string
47 Passed bool
48 Stale bool
49 ToolCallID string
50 Interrupted bool
51 ExitCode *int
52 }
53
54 // GapKind classifies one thing the report refuses to present as verified.
55 type GapKind uint8
56
57 const (
58 // GapUnbackedClaim is first because it is the worst: the turn asserted a
59 // verification the ledger does not support.
60 GapUnbackedClaim GapKind = iota
61 GapUnprovenCriterion
62 GapMissingCheck
63 GapFailedVerification
64 GapStaleVerification
65 GapUnverifiedChange
66 GapUnreviewedChange
67 GapDeclaredUnverified
68 )
69
70 func (k GapKind) String() string {
71 switch k {
72 case GapUnbackedClaim:
73 return "unbacked_claim"
74 case GapDeclaredUnverified:
75 return "declared_unverified"
76 case GapUnprovenCriterion:
77 return "unproven_criterion"
78 case GapMissingCheck:
79 return "missing_check"
80 case GapFailedVerification:
81 return "failed_verification"
82 case GapStaleVerification:
83 return "stale_verification"
84 case GapUnverifiedChange:
85 return "unverified_change"
86 case GapUnreviewedChange:
87 return "unreviewed_change"
88 default:
89 return "unknown"
90 }
91 }
92
93 // Gap is one unproven thing, in the report's own words.
94 type Gap struct {
95 Kind GapKind
96 Detail string
97 }
98
99 // Report is the host's completion record for one turn.
100 type Report struct {
101 // AssessmentKind distinguishes observed facts from historical quality assessments.
102 AssessmentKind string
103 Verdict Verdict
104 // Mutations counts every successful mutating receipt, including ones that
105 // named no path; Changes lists only the paths.
106 Mutations int
107 Changes []Change
108 Verifications []Verification
109 Gaps []Gap
110 // Claimed is what the turn said about itself; Risks is its declared risk
111 // list. Both are model-authored and never clear a host-found gap.
112 Claimed Claim
113 Risks []string
114 }
115
116 // BuildFacts records observations and model declarations without assigning a
117 // quality verdict or inventing checks that were never run.
118 func BuildFacts(ledger *evidence.Ledger, workspaceRoot string, scratchRoots []string) Report {
119 receipts := ledger.Receipts()
120 rep := Report{
121 AssessmentKind: "facts",
122 Verdict: VerdictUnknown,
123 Mutations: mutationsOf(receipts, workspaceRoot, scratchRoots),
124 Changes: changesOf(ledger, receipts, workspaceRoot, scratchRoots),
125 Verifications: verificationsOf(receipts, workspaceRoot, scratchRoots),
126 }
127 return reconcile(rep, claimOf(receipts), receipts)
128 }
129
130 // changesOf lists mutated paths in first-write order and asks the ledger
131 // whether each one was inspected after its own latest write, so a review that
132 // covered one file never vouches for another.
133 func changesOf(ledger *evidence.Ledger, receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) []Change {
134 var out []Change
135 at := map[string]int{}
136 lastWrite := map[string]int{}
137 for i, r := range receipts {
138 if !evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) {
139 continue
140 }
141 for _, p := range r.Paths {
142 if p == "" || evidence.ClassifyWriteScope(p, workspaceRoot, scratchRoots) == evidence.WriteScopeScratch {
143 continue
144 }
145 if _, seen := at[p]; !seen {
146 at[p] = len(out)
147 out = append(out, Change{Path: p})
148 }
149 lastWrite[p] = i
150 }
151 }
152 for i := range out {
153 out[i].Reviewed = ledger.HasHostReviewCoverageAfter(lastWrite[out[i].Path], []string{out[i].Path})
154 }
155 return out
156 }
157
158 // mutationsOf counts successful mutating receipts, path-named or not: a
159 // `sed -i` or `rm` that named nothing still changed the workspace, and must
160 // not escape the unverified-change gap by leaving no path behind.
161 func mutationsOf(receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) int {
162 count := 0
163 for _, r := range receipts {
164 if evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) {
165 count++
166 }
167 }
168 return count
169 }
170
171 // verificationsOf keeps each delivery-verification command's latest run, in
172 // first-run order, and marks the ones that predate the newest mutation.
173 func verificationsOf(receipts []evidence.Receipt, workspaceRoot string, scratchRoots []string) []Verification {
174 lastMutation := -1
175 for i, r := range receipts {
176 if evidence.IsDeliveryMutation(r, workspaceRoot, scratchRoots) {
177 lastMutation = i
178 }
179 }
180 var out []Verification
181 at := map[string]int{}
182 for i, r := range receipts {
183 command := strings.TrimSpace(r.Command)
184 if command == "" || r.Verification == evidence.VerificationNotVerification || r.Verification == evidence.VerificationNotRun || !evidence.IsVerificationCommand(command) {
185 continue
186 }
187 if _, seen := at[command]; !seen {
188 at[command] = len(out)
189 out = append(out, Verification{Command: command})
190 }
191 out[at[command]].Passed = r.Success && (r.ExitCode == nil || *r.ExitCode == 0) && r.Verification != evidence.VerificationFailed
192 out[at[command]].Stale = i < lastMutation
193 out[at[command]].ToolCallID = r.ToolCallID
194 out[at[command]].Interrupted = r.Interrupted
195 out[at[command]].ExitCode = r.ExitCode
196 }
197 return out
198 }
199
200 // Summary describes observed quantities without assigning quality.
201 func (r Report) Summary() string {
202 return fmt.Sprintf("%d changes, %d checks", len(r.Changes), len(r.Verifications))
203 }
204
205 func (r Report) GapKinds() []string {
206 seen := map[GapKind]bool{}
207 var out []string
208 for _, kind := range []GapKind{GapUnbackedClaim, GapUnprovenCriterion, GapMissingCheck, GapFailedVerification, GapStaleVerification, GapUnverifiedChange, GapUnreviewedChange, GapDeclaredUnverified} {
209 for _, gap := range r.Gaps {
210 if gap.Kind == kind && !seen[kind] {
211 seen[kind] = true
212 out = append(out, kind.String())
213 }
214 }
215 }
216 return out
217 }
218
218 lines GO