返回 DeepSeek-Reasonix
claim.go
根目录 / internal / completion / claim.go
1 package completion
2
3 import (
4 "encoding/json"
5 "strings"
6
7 "reasonix/internal/evidence"
8 )
9
10 // Claim is the model's own account of the work, as passed to update_goal. It
11 // is the only model-authored part of a report, and it can only ever add to
12 // what the host found: Verified is checked against the ledger, while
13 // Unverified and Risks are declarations the host cannot verify but has no
14 // reason to suppress.
15 type Claim struct {
16 Verified []string
17 Unverified []string
18 Risks []string
19 }
20
21 // Empty reports whether the turn made no claim at all.
22 func (c Claim) Empty() bool {
23 return len(c.Verified) == 0 && len(c.Unverified) == 0 && len(c.Risks) == 0
24 }
25
26 // LatestCompleteClaim returns the latest successful update_goal(complete)
27 // account. continue/blocked reports and failed calls claim nothing.
28 func LatestCompleteClaim(ledger *evidence.Ledger) (Claim, bool) {
29 if ledger == nil {
30 return Claim{}, false
31 }
32 var out Claim
33 found := false
34 for _, r := range ledger.Receipts() {
35 if !r.Success || r.ToolName != "update_goal" || len(r.Args) == 0 {
36 continue
37 }
38 var payload struct {
39 Status string `json:"status"`
40 Completion struct {
41 Verified []string `json:"verified"`
42 Unverified []string `json:"unverified"`
43 Risks []string `json:"risks"`
44 } `json:"completion"`
45 }
46 if json.Unmarshal(r.Args, &payload) != nil {
47 continue
48 }
49 if strings.ToLower(strings.TrimSpace(payload.Status)) != "complete" {
50 continue
51 }
52 out = Claim{
53 Verified: trimAll(payload.Completion.Verified),
54 Unverified: trimAll(payload.Completion.Unverified),
55 Risks: trimAll(payload.Completion.Risks),
56 }
57 found = true
58 }
59 return out, found
60 }
61
62 // claimOf extracts the latest successful update_goal completion claim. A
63 // failed call claims nothing: outside an active goal turn the tool fails
64 // closed, and a rejected claim must not reach the report.
65 func claimOf(receipts []evidence.Receipt) Claim {
66 var out Claim
67 for _, r := range receipts {
68 if !r.Success || r.ToolName != "update_goal" || len(r.Args) == 0 {
69 continue
70 }
71 var payload struct {
72 Completion struct {
73 Verified []string `json:"verified"`
74 Unverified []string `json:"unverified"`
75 Risks []string `json:"risks"`
76 } `json:"completion"`
77 }
78 if json.Unmarshal(r.Args, &payload) != nil {
79 continue
80 }
81 out = Claim{
82 Verified: trimAll(payload.Completion.Verified),
83 Unverified: trimAll(payload.Completion.Unverified),
84 Risks: trimAll(payload.Completion.Risks),
85 }
86 }
87 return out
88 }
89
90 func trimAll(in []string) []string {
91 var out []string
92 for _, s := range in {
93 if s = strings.TrimSpace(s); s != "" {
94 out = append(out, s)
95 }
96 }
97 return out
98 }
99
100 // reconcile folds the claim into a host-built report. Claimed verifications
101 // are matched against real receipts, and every mismatch becomes a gap; the
102 // model's own declarations are carried through untouched. Nothing here can
103 // clear a gap the host found — a claim only ever adds.
104 func reconcile(rep Report, claim Claim, receipts []evidence.Receipt) Report {
105 rep.Claimed = claim
106 rep.Risks = claim.Risks
107 var gaps []Gap
108 for _, command := range claim.Verified {
109 if why := unbackedClaim(command, receipts); why != "" {
110 gaps = append(gaps, Gap{GapUnbackedClaim, why})
111 }
112 }
113 for _, note := range claim.Unverified {
114 gaps = append(gaps, Gap{GapDeclaredUnverified, note})
115 }
116 rep.Gaps = append(gaps, rep.Gaps...)
117 return rep
118 }
119
120 // unbackedClaim returns why a claimed verification is not backed by the
121 // ledger, or "" when a successful run of it survives the latest mutation.
122 func unbackedClaim(command string, receipts []evidence.Receipt) string {
123 lastMutation := -1
124 for i, r := range receipts {
125 if r.Success && (r.Mutation || r.Write) {
126 lastMutation = i
127 }
128 }
129 matched, index, success := false, -1, false
130 for i, r := range receipts {
131 ran := strings.TrimSpace(r.Command)
132 if ran == "" {
133 continue
134 }
135 if ran != command && !evidence.CommandMatches(command, ran) {
136 continue
137 }
138 matched, index, success = true, i, r.Success
139 }
140 switch {
141 case !matched:
142 return command + " — claimed as verification, but no run of it was recorded"
143 case !success:
144 return command + " — claimed as verification, but its last run failed"
145 case index < lastMutation:
146 return command + " — claimed as verification, but it last ran before the latest change"
147 default:
148 return ""
149 }
150 }
151
151 lines GO