返回 DeepSeek-Reasonix
commandmatch.go
根目录 / internal / evidence / commandmatch.go
1 package evidence
2
3 import (
4 "strings"
5
6 "reasonix/internal/shellparse"
7 )
8
9 // CommandMatches reports whether a cited verification command is proven by a
10 // command that actually ran. Models paraphrase commands when citing them
11 // (dropping a `cd` prefix, changing quote style, omitting flags), so byte
12 // equality rejects real verifications; instead both sides are split into
13 // shell segments and each cited segment must be covered by some ran segment.
14 func CommandMatches(cited, ran string) bool {
15 citedSegs := commandSegments(cited)
16 if len(citedSegs) == 0 {
17 return false
18 }
19 ranSegs := commandSegments(ran)
20 for _, c := range citedSegs {
21 if !segmentCovered(c, ranSegs) {
22 return false
23 }
24 }
25 return true
26 }
27
28 func segmentCovered(cited string, ranSegs []string) bool {
29 for _, r := range ranSegs {
30 if segmentMatches(cited, r) {
31 return true
32 }
33 }
34 return false
35 }
36
37 // segmentMatches accepts normalized equality, or a token subset with the same
38 // head token (e.g. cited "ls x 2>&1" against ran "ls -la x 2>&1"). One-token
39 // citations only match exactly, so a bare "ls" can't claim an unrelated run.
40 func segmentMatches(cited, ran string) bool {
41 ct, rt := segmentTokens(cited), segmentTokens(ran)
42 if len(ct) == 0 || len(rt) == 0 {
43 return false
44 }
45 if strings.Join(ct, " ") == strings.Join(rt, " ") {
46 return true
47 }
48 if len(ct) < 2 || ct[0] != rt[0] {
49 return false
50 }
51 have := make(map[string]bool, len(rt))
52 for _, t := range rt {
53 have[t] = true
54 }
55 for _, t := range ct {
56 if !have[t] {
57 return false
58 }
59 }
60 return true
61 }
62
63 var segmentSeparators = []string{"&&", "||", ";", "|", "\n"}
64
65 func commandSegments(s string) []string {
66 if segs, _, ok := shellparse.SplitTopLevel(s); ok {
67 return segs
68 }
69 parts := []string{s}
70 for _, sep := range segmentSeparators {
71 var next []string
72 for _, p := range parts {
73 next = append(next, strings.Split(p, sep)...)
74 }
75 parts = next
76 }
77 var segs []string
78 for _, p := range parts {
79 p = strings.TrimSpace(p)
80 if p == "" || strings.HasPrefix(p, "#") {
81 continue
82 }
83 segs = append(segs, p)
84 }
85 return segs
86 }
87
88 func segmentTokens(s string) []string {
89 if fields, malformed := shellparse.StaticFields(s); malformed == "" {
90 return fields
91 }
92 fields := strings.Fields(s)
93 tokens := make([]string, 0, len(fields))
94 for _, f := range fields {
95 f = strings.ReplaceAll(f, `"`, "")
96 f = strings.ReplaceAll(f, "'", "")
97 if f != "" {
98 tokens = append(tokens, f)
99 }
100 }
101 return tokens
102 }
103
103 lines GO