webctx 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CONTRIBUTORS.md +5 -16
- package/README.md +10 -3
- package/internal/app/alexandria.go +102 -0
- package/internal/app/app.go +4 -3
- package/internal/app/firecrawl_fallback.go +21 -18
- package/internal/app/firecrawl_fallback_test.go +38 -0
- package/internal/app/native_markdown.go +76 -0
- package/internal/app/native_markdown_test.go +78 -0
- package/internal/app/research.go +615 -0
- package/internal/app/research_excerpt.go +149 -0
- package/internal/app/research_excerpt_test.go +47 -0
- package/internal/app/research_navigation.go +237 -0
- package/internal/app/research_navigation_test.go +72 -0
- package/internal/app/research_openapi.go +289 -0
- package/internal/app/research_openapi_test.go +140 -0
- package/internal/app/research_test.go +154 -0
- package/internal/app/retrieval_quality.go +106 -0
- package/internal/app/retrieval_quality_test.go +63 -0
- package/internal/app/scrape.go +4 -65
- package/internal/app/structured_source.go +259 -0
- package/internal/app/structured_source_test.go +70 -0
- package/internal/app/tools.go +36 -9
- package/package.json +1 -1
- package/pkg/origo/server.go +10 -3
- package/pkg/origo/server_test.go +15 -0
- package/pkg/retrieval/retrieval.go +6 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sort"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
const backtickFence = "\x60\x60\x60"
|
|
9
|
+
|
|
10
|
+
type evidenceBlock struct {
|
|
11
|
+
text string
|
|
12
|
+
code bool
|
|
13
|
+
score int
|
|
14
|
+
order int
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// A code fence is indivisible evidence: do not silently truncate a command,
|
|
18
|
+
// request body or SDK example and then invent a closing fence.
|
|
19
|
+
func splitEvidenceBlocks(markdown, question string) []evidenceBlock {
|
|
20
|
+
var blocks []evidenceBlock
|
|
21
|
+
var content strings.Builder
|
|
22
|
+
terms := queryTerms(question)
|
|
23
|
+
inCode := false
|
|
24
|
+
fence := ""
|
|
25
|
+
flush := func(code bool) {
|
|
26
|
+
text := strings.TrimSpace(content.String())
|
|
27
|
+
content.Reset()
|
|
28
|
+
if text == "" {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
score := termScore(text, terms)
|
|
32
|
+
if code {
|
|
33
|
+
score *= 3
|
|
34
|
+
if strings.Contains(strings.ToLower(question), "rest") &&
|
|
35
|
+
(strings.Contains(text, "curl") || strings.Contains(text, "POST ") || strings.Contains(text, "GET ")) {
|
|
36
|
+
score += 4
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
blocks = append(blocks, evidenceBlock{text: text, code: code, score: score, order: len(blocks)})
|
|
40
|
+
}
|
|
41
|
+
for _, line := range strings.Split(markdown, "\n") {
|
|
42
|
+
trimmed := strings.TrimSpace(line)
|
|
43
|
+
opening := !inCode && (strings.HasPrefix(trimmed, backtickFence) || strings.HasPrefix(trimmed, "~~~"))
|
|
44
|
+
if opening {
|
|
45
|
+
flush(false)
|
|
46
|
+
inCode = true
|
|
47
|
+
fence = trimmed[:3]
|
|
48
|
+
}
|
|
49
|
+
content.WriteString(line)
|
|
50
|
+
content.WriteByte('\n')
|
|
51
|
+
if inCode && !opening && strings.TrimSpace(strings.TrimPrefix(trimmed, fence)) == "" &&
|
|
52
|
+
strings.HasPrefix(trimmed, fence) {
|
|
53
|
+
flush(true)
|
|
54
|
+
inCode = false
|
|
55
|
+
fence = ""
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Unclosed code blocks are not valid examples; treat them as source prose.
|
|
59
|
+
flush(false)
|
|
60
|
+
return blocks
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Select complete, high-relevance examples and explanatory prose from long
|
|
64
|
+
// sections, preserving original order and the hard context budget.
|
|
65
|
+
func atomicFocusedSection(markdown, question string, budget int) string {
|
|
66
|
+
markdown = strings.TrimSpace(markdown)
|
|
67
|
+
if len(markdown) <= budget {
|
|
68
|
+
return markdown
|
|
69
|
+
}
|
|
70
|
+
if budget < 180 {
|
|
71
|
+
return "*[Section exceeds excerpt budget; follow the source URL.]*"
|
|
72
|
+
}
|
|
73
|
+
blocks := splitEvidenceBlocks(markdown, question)
|
|
74
|
+
const omitted = "\n\n*[Other source text omitted. Follow the original URL for full details.]*"
|
|
75
|
+
remaining := budget - len(omitted) - 8
|
|
76
|
+
if remaining < 80 {
|
|
77
|
+
return "*[Section exceeds excerpt budget; follow the source URL.]*"
|
|
78
|
+
}
|
|
79
|
+
selected := make(map[int]string)
|
|
80
|
+
var codes []evidenceBlock
|
|
81
|
+
var prose []evidenceBlock
|
|
82
|
+
for _, block := range blocks {
|
|
83
|
+
if block.code {
|
|
84
|
+
codes = append(codes, block)
|
|
85
|
+
} else {
|
|
86
|
+
prose = append(prose, block)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
sort.SliceStable(codes, func(i, j int) bool {
|
|
90
|
+
if codes[i].score == codes[j].score {
|
|
91
|
+
return codes[i].order < codes[j].order
|
|
92
|
+
}
|
|
93
|
+
return codes[i].score > codes[j].score
|
|
94
|
+
})
|
|
95
|
+
// Reserve a little space for the section explanation.
|
|
96
|
+
codeAllowance := remaining * 85 / 100
|
|
97
|
+
for _, block := range codes {
|
|
98
|
+
size := len(block.text) + 2
|
|
99
|
+
if size <= codeAllowance {
|
|
100
|
+
selected[block.order] = block.text
|
|
101
|
+
codeAllowance -= size
|
|
102
|
+
remaining -= size
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sort.SliceStable(prose, func(i, j int) bool {
|
|
106
|
+
if prose[i].score == prose[j].score {
|
|
107
|
+
return prose[i].order < prose[j].order
|
|
108
|
+
}
|
|
109
|
+
return prose[i].score > prose[j].score
|
|
110
|
+
})
|
|
111
|
+
for _, block := range prose {
|
|
112
|
+
if remaining < 100 {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
if len(block.text)+2 <= remaining {
|
|
116
|
+
selected[block.order] = block.text
|
|
117
|
+
remaining -= len(block.text) + 2
|
|
118
|
+
} else if part := completeProseLines(block.text, remaining-4); part != "" {
|
|
119
|
+
selected[block.order] = part
|
|
120
|
+
remaining -= len(part) + 2
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
var output []string
|
|
124
|
+
for _, block := range blocks {
|
|
125
|
+
if text := selected[block.order]; text != "" {
|
|
126
|
+
output = append(output, text)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if len(output) == 0 {
|
|
130
|
+
return "*[Code examples exceed the excerpt budget; inspect source URL for the full examples.]*"
|
|
131
|
+
}
|
|
132
|
+
return strings.Join(output, "\n\n") + omitted
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func completeProseLines(text string, maxBytes int) string {
|
|
136
|
+
if maxBytes <= 0 {
|
|
137
|
+
return ""
|
|
138
|
+
}
|
|
139
|
+
var lines []string
|
|
140
|
+
size := 0
|
|
141
|
+
for _, line := range strings.Split(text, "\n") {
|
|
142
|
+
if size+len(line)+1 > maxBytes {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
lines = append(lines, line)
|
|
146
|
+
size += len(line) + 1
|
|
147
|
+
}
|
|
148
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
149
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestFocusedExcerptPreservesWholeCurlExamples(t *testing.T) {
|
|
9
|
+
code := backtickFence + "bash\ncurl -X POST https://api.example.com/v2/scrape \\\n" +
|
|
10
|
+
" -H 'Content-Type: application/json' \\\n" +
|
|
11
|
+
" -d '{\"alexandria\":{\"capability\":\"find-tools\"}}'\n" + backtickFence
|
|
12
|
+
doc := "# Documentation\n\n" + strings.Repeat("Background prose unrelated to endpoints.\n", 200) +
|
|
13
|
+
"\n## Inspect tools over REST\n" + strings.Repeat("Introductory filler.\n", 70) +
|
|
14
|
+
"\n" + code + "\n" + strings.Repeat("Appendix filler.\n", 200)
|
|
15
|
+
got := focusExcerpt(doc, "How do I inspect Alexandria tools with a REST request example?", 1600)
|
|
16
|
+
if !strings.Contains(got, code) {
|
|
17
|
+
t.Fatalf("the complete REST example must survive extraction:\n%s", got)
|
|
18
|
+
}
|
|
19
|
+
if strings.Count(got, backtickFence) != 2 {
|
|
20
|
+
t.Fatalf("expected exactly one balanced code fence: %s", got)
|
|
21
|
+
}
|
|
22
|
+
if len(got) > 1800 {
|
|
23
|
+
t.Fatalf("excerpt exceeded budget: %d", len(got))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestOversizedCodeBlocksAreNeverPartiallyReturned(t *testing.T) {
|
|
28
|
+
huge := backtickFence + "json\n{\"payload\":\"" + strings.Repeat("a", 3000) + "\"}\n" + backtickFence
|
|
29
|
+
doc := "# API\n\n" + huge + "\n\n" + strings.Repeat("Further explanation.\n", 200)
|
|
30
|
+
got := atomicFocusedSection(doc, "What is the API payload?", 700)
|
|
31
|
+
if strings.Contains(got, "\"payload\"") || strings.Contains(got, backtickFence) {
|
|
32
|
+
t.Fatalf("oversized example should be omitted, not truncated:\n%s", got)
|
|
33
|
+
}
|
|
34
|
+
if !strings.Contains(got, "omitted") && !strings.Contains(got, "source URL") {
|
|
35
|
+
t.Fatalf("omission should be explicitly marked: %s", got)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func TestPlainCodeFenceOpeningNotTreatedAsClosing(t *testing.T) {
|
|
40
|
+
doc := "# API\n" + strings.Repeat("Preliminary text.\n", 80) + "\n" +
|
|
41
|
+
backtickFence + "\nPOST /v2/scrape\n{}\n" + backtickFence + "\n" +
|
|
42
|
+
strings.Repeat("End notes.\n", 150)
|
|
43
|
+
got := atomicFocusedSection(doc, "scrape request", 650)
|
|
44
|
+
if !strings.Contains(got, backtickFence+"\nPOST /v2/scrape\n{}\n"+backtickFence) {
|
|
45
|
+
t.Fatalf("plain opening fence was not handled atomically: %s", got)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/url"
|
|
5
|
+
"regexp"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
var documentationIndexPattern = regexp.MustCompile("https?://[^\\s<>\"')\\]]+/llms\\.txt")
|
|
11
|
+
|
|
12
|
+
func documentIndexURLs(base *url.URL, primary string) []string {
|
|
13
|
+
var urls []string
|
|
14
|
+
seen := map[string]bool{}
|
|
15
|
+
for _, raw := range documentationIndexPattern.FindAllString(primary, 5) {
|
|
16
|
+
target, err := url.Parse(raw)
|
|
17
|
+
if err != nil || validateSourceURL(raw) != nil ||
|
|
18
|
+
!strings.EqualFold(base.Hostname(), target.Hostname()) || target.Port() != base.Port() {
|
|
19
|
+
continue
|
|
20
|
+
}
|
|
21
|
+
if !seen[raw] {
|
|
22
|
+
urls = append(urls, raw)
|
|
23
|
+
seen[raw] = true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
fallback := base.Scheme + "://" + base.Host + "/llms.txt"
|
|
27
|
+
if !seen[fallback] {
|
|
28
|
+
urls = append(urls, fallback)
|
|
29
|
+
}
|
|
30
|
+
return urls
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func implementationQuestion(question string) bool {
|
|
34
|
+
text := strings.ToLower(question)
|
|
35
|
+
for _, signal := range []string{
|
|
36
|
+
"implement", "integrat", "runnable", "configuration",
|
|
37
|
+
"code example", "complete", "how to", "set up", "using a", "creating",
|
|
38
|
+
} {
|
|
39
|
+
if strings.Contains(text, signal) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func implementationLinkBonus(candidate sourceCandidate, question string) int {
|
|
47
|
+
lower := strings.ToLower(candidate.url + " " + candidate.label)
|
|
48
|
+
q := strings.ToLower(question)
|
|
49
|
+
score := 0
|
|
50
|
+
if implementationQuestion(question) {
|
|
51
|
+
switch {
|
|
52
|
+
case strings.Contains(lower, "/get-started/"), strings.Contains(lower, "/quickstart"),
|
|
53
|
+
strings.Contains(lower, "getting started"):
|
|
54
|
+
score += 64 // Publisher's end-to-end guide, not isolated snippets.
|
|
55
|
+
case strings.Contains(lower, "tutorial"), strings.Contains(lower, "integration"),
|
|
56
|
+
strings.Contains(lower, "how-to"):
|
|
57
|
+
score += 16
|
|
58
|
+
case strings.Contains(lower, "example"):
|
|
59
|
+
score += 6
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if strings.Contains(q, "sql") || strings.Contains(q, "sqlite") {
|
|
63
|
+
if strings.Contains(lower, "sqlite") || strings.Contains(lower, "storage") {
|
|
64
|
+
score += 26
|
|
65
|
+
} else if strings.Contains(lower, "/examples/") {
|
|
66
|
+
score -= 16
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if strings.Contains(q, "webhook") || strings.Contains(q, "fulfill") || strings.Contains(q, "fulfil") {
|
|
70
|
+
if strings.Contains(lower, "webhook") || strings.Contains(lower, "fulfill") || strings.Contains(lower, "fulfil") {
|
|
71
|
+
score += 30
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if strings.Contains(q, "checkout") && (strings.Contains(lower, "checkout") || strings.Contains(lower, "session")) {
|
|
75
|
+
score += 6
|
|
76
|
+
}
|
|
77
|
+
if strings.Contains(q, "signature") && strings.Contains(lower, "signature") {
|
|
78
|
+
score += 24
|
|
79
|
+
}
|
|
80
|
+
if strings.Contains(q, "curl") || strings.Contains(q, "rest") || strings.Contains(q, "endpoint") {
|
|
81
|
+
if strings.Contains(lower, "/api/") || strings.Contains(lower, "api-reference") {
|
|
82
|
+
score += 10
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if strings.Contains(q, "session") && strings.Contains(q, "creat") &&
|
|
86
|
+
(strings.Contains(lower, "/sessions/create") || strings.Contains(lower, "create a checkout session")) {
|
|
87
|
+
score += 38
|
|
88
|
+
}
|
|
89
|
+
if strings.Contains(q, "checkout") && strings.Contains(lower, "/checkout/quickstart") {
|
|
90
|
+
score += 20
|
|
91
|
+
}
|
|
92
|
+
if strings.Contains(q, "fulfill") || strings.Contains(q, "fulfil") {
|
|
93
|
+
if strings.Contains(lower, "/checkout/fulfillment") {
|
|
94
|
+
score += 24
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if strings.Contains(lower, "/alarms/") && !strings.Contains(q, "alarm") {
|
|
98
|
+
score -= 24
|
|
99
|
+
}
|
|
100
|
+
if strings.Contains(lower, "/concepts/") && implementationQuestion(question) {
|
|
101
|
+
score -= 12
|
|
102
|
+
}
|
|
103
|
+
if strings.Contains(lower, "legacy") && (strings.Contains(q, "new ") || strings.Contains(q, "current")) && !strings.Contains(q, "legacy") {
|
|
104
|
+
score -= 25
|
|
105
|
+
}
|
|
106
|
+
return score
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Search indices on a large documentation host often contain links for many
|
|
110
|
+
// unrelated products. Prefer links in the original section or with a product
|
|
111
|
+
// named in the question. This prevents "webhooks for Climate Orders" from
|
|
112
|
+
// outranking generic Checkout webhooks solely because both contain "webhook".
|
|
113
|
+
func sourceScopeBonus(candidate sourceCandidate, rawURL, question string) int {
|
|
114
|
+
base, err1 := url.Parse(rawURL)
|
|
115
|
+
target, err2 := url.Parse(candidate.url)
|
|
116
|
+
if err1 != nil || err2 != nil {
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
q := strings.ToLower(question)
|
|
120
|
+
sourceSegments := strings.Split(strings.Trim(base.Path, "/"), "/")
|
|
121
|
+
targetSegments := strings.Split(strings.Trim(target.Path, "/"), "/")
|
|
122
|
+
if len(targetSegments) == 0 {
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
score := 0
|
|
126
|
+
if len(sourceSegments) > 0 && targetSegments[0] == sourceSegments[0] {
|
|
127
|
+
score += 8
|
|
128
|
+
}
|
|
129
|
+
if len(targetSegments) > 0 && targetSegments[0] != "" &&
|
|
130
|
+
!strings.Contains(q, strings.TrimSuffix(targetSegments[0], "s")) &&
|
|
131
|
+
!strings.Contains(q, targetSegments[0]) &&
|
|
132
|
+
targetSegments[0] != "api" && targetSegments[0] != "webhooks" &&
|
|
133
|
+
targetSegments[0] != "events" &&
|
|
134
|
+
(len(sourceSegments) == 0 || targetSegments[0] != sourceSegments[0]) {
|
|
135
|
+
score -= 24
|
|
136
|
+
}
|
|
137
|
+
// A general Worker overview is less relevant than Durable Objects'
|
|
138
|
+
// focused setup instructions even if both mention "Worker".
|
|
139
|
+
if strings.Contains(q, "durable object") && strings.Contains(target.Path, "/workers/") &&
|
|
140
|
+
!strings.Contains(target.Path, "/durable-objects/") {
|
|
141
|
+
score -= 14
|
|
142
|
+
}
|
|
143
|
+
return score
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func normalizedDocumentationURL(raw string) string {
|
|
147
|
+
u, err := url.Parse(raw)
|
|
148
|
+
if err != nil {
|
|
149
|
+
return raw
|
|
150
|
+
}
|
|
151
|
+
p := strings.TrimSuffix(u.Path, "/index.md")
|
|
152
|
+
p = strings.TrimSuffix(p, ".md")
|
|
153
|
+
p = strings.TrimSuffix(p, "/")
|
|
154
|
+
u.Path = p
|
|
155
|
+
u.Fragment = ""
|
|
156
|
+
u.RawFragment = ""
|
|
157
|
+
u.RawPath = ""
|
|
158
|
+
return u.String()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func researchCandidates(rawURL, primary, question string, limit int) []sourceCandidate {
|
|
162
|
+
baseCandidates := relatedSourceLinks(primary, rawURL, question, limit*4)
|
|
163
|
+
indexCandidates := linksFromDocsIndexWithPrimary(rawURL, primary, question, limit*5)
|
|
164
|
+
seen := map[string]bool{normalizedDocumentationURL(rawURL): true}
|
|
165
|
+
var candidates []sourceCandidate
|
|
166
|
+
for _, c := range append(baseCandidates, indexCandidates...) {
|
|
167
|
+
key := normalizedDocumentationURL(c.url)
|
|
168
|
+
if seen[key] {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
seen[key] = true
|
|
172
|
+
c.score += implementationLinkBonus(c, question)
|
|
173
|
+
c.score += sourceScopeBonus(c, rawURL, question)
|
|
174
|
+
candidates = append(candidates, c)
|
|
175
|
+
}
|
|
176
|
+
sort.SliceStable(candidates, func(i, j int) bool {
|
|
177
|
+
if candidates[i].score == candidates[j].score {
|
|
178
|
+
return candidates[i].order < candidates[j].order
|
|
179
|
+
}
|
|
180
|
+
return candidates[i].score > candidates[j].score
|
|
181
|
+
})
|
|
182
|
+
if len(candidates) > limit {
|
|
183
|
+
return candidates[:limit]
|
|
184
|
+
}
|
|
185
|
+
return candidates
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type evidenceNeed struct {
|
|
189
|
+
name string
|
|
190
|
+
alternatives []string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check for explicit source evidence; do not claim that a plausible guess
|
|
194
|
+
// supplies missing request fields or code.
|
|
195
|
+
func evidenceNeeds(question string) []evidenceNeed {
|
|
196
|
+
q := strings.ToLower(question)
|
|
197
|
+
var needs []evidenceNeed
|
|
198
|
+
for _, literal := range []string{"line_items", "success_url", "cancel_url", "sql.exec", "wrangler", "sqlite"} {
|
|
199
|
+
if strings.Contains(q, literal) {
|
|
200
|
+
needs = append(needs, evidenceNeed{literal, []string{literal}})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if strings.Contains(q, "webhook") {
|
|
204
|
+
needs = append(needs, evidenceNeed{"webhook events", []string{"webhook", "checkout.session.completed"}})
|
|
205
|
+
if strings.Contains(q, "signature") || strings.Contains(q, "secure") {
|
|
206
|
+
needs = append(needs, evidenceNeed{"webhook signature verification", []string{"stripe-signature", "constructevent", "signature verification", "signatureverification"}})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if strings.Contains(q, "sql table") || strings.Contains(q, "creating a sql") {
|
|
210
|
+
needs = append(needs, evidenceNeed{"SQL table creation", []string{"create table"}})
|
|
211
|
+
}
|
|
212
|
+
if strings.Contains(q, "accesses the binding") || strings.Contains(q, "access the binding") {
|
|
213
|
+
needs = append(needs, evidenceNeed{"Worker binding access", []string{"getbyname", "env.my_durable_object", "env.durable_object"}})
|
|
214
|
+
}
|
|
215
|
+
if strings.Contains(q, "checkout") && (strings.Contains(q, "fulfill") || strings.Contains(q, "fulfil")) {
|
|
216
|
+
needs = append(needs, evidenceNeed{"Checkout fulfillment event", []string{"checkout.session.completed"}})
|
|
217
|
+
}
|
|
218
|
+
return needs
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func missingResearchEvidence(question, text string) []string {
|
|
222
|
+
lower := strings.ToLower(text)
|
|
223
|
+
var missing []string
|
|
224
|
+
for _, need := range evidenceNeeds(question) {
|
|
225
|
+
found := false
|
|
226
|
+
for _, candidate := range need.alternatives {
|
|
227
|
+
if strings.Contains(lower, candidate) {
|
|
228
|
+
found = true
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if !found {
|
|
233
|
+
missing = append(missing, need.name)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return missing
|
|
237
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
"strings"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestStripeFocusedLinksPreferCheckoutAndWebhookDocs(t *testing.T) {
|
|
10
|
+
old := http.DefaultClient
|
|
11
|
+
t.Cleanup(func() { http.DefaultClient = old })
|
|
12
|
+
index := "# Documentation\n\n" +
|
|
13
|
+
"- [Climate order webhooks](https://docs.stripe.com/climate/orders/webhooks.md)\n" +
|
|
14
|
+
"- [Create a Checkout Session](https://docs.stripe.com/api/checkout/sessions/create.md)\n" +
|
|
15
|
+
"- [Build a Stripe-hosted checkout page](https://docs.stripe.com/checkout/quickstart.md)\n" +
|
|
16
|
+
"- [Set up and deploy a webhook](https://docs.stripe.com/webhooks/quickstart.md)\n" +
|
|
17
|
+
"- [Fulfill orders](https://docs.stripe.com/checkout/fulfillment.md)\n" +
|
|
18
|
+
"- [Verify webhook signatures](https://docs.stripe.com/webhooks.md#verify-signature)\n"
|
|
19
|
+
http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
20
|
+
if req.URL.Path == "/llms.txt" {
|
|
21
|
+
return testHTTPResponse(req, http.StatusOK, index, nil), nil
|
|
22
|
+
}
|
|
23
|
+
return testHTTPResponse(req, http.StatusNotFound, "", nil), nil
|
|
24
|
+
})}
|
|
25
|
+
question := "Implement Stripe-hosted Checkout: create a Checkout Session REST request and securely fulfill orders using webhooks, including signature verification"
|
|
26
|
+
primary := "# Stripe Checkout\n\n[Start building your checkout integration](https://docs.stripe.com/checkout/quickstart.md)"
|
|
27
|
+
got := researchCandidates("https://docs.stripe.com/payments/checkout", primary, question, 5)
|
|
28
|
+
var links []string
|
|
29
|
+
for _, c := range got {
|
|
30
|
+
links = append(links, c.url)
|
|
31
|
+
}
|
|
32
|
+
output := strings.Join(links, "\n")
|
|
33
|
+
for _, want := range []string{
|
|
34
|
+
"/checkout/quickstart.md",
|
|
35
|
+
"/api/checkout/sessions/create.md",
|
|
36
|
+
"/webhooks/quickstart.md",
|
|
37
|
+
"/checkout/fulfillment.md",
|
|
38
|
+
} {
|
|
39
|
+
if !strings.Contains(output, want) {
|
|
40
|
+
t.Errorf("expected %q among selected source links: %s", want, output)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if strings.Contains(output, "/climate/") {
|
|
44
|
+
t.Errorf("irrelevant Climate-specific webhook outranked Checkout sources: %s", output)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestCloudflareNewSQLiteQuestionPrefersGettingStarted(t *testing.T) {
|
|
49
|
+
old := http.DefaultClient
|
|
50
|
+
t.Cleanup(func() { http.DefaultClient = old })
|
|
51
|
+
http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
52
|
+
return testHTTPResponse(req, http.StatusForbidden, "", nil), nil
|
|
53
|
+
})}
|
|
54
|
+
primary := "# Cloudflare Durable Objects\n" +
|
|
55
|
+
"[Durable Object alarms](https://developers.cloudflare.com/durable-objects/api/alarms/)\n" +
|
|
56
|
+
"[SQLite storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/)\n" +
|
|
57
|
+
"[Get started](https://developers.cloudflare.com/durable-objects/get-started/)\n" +
|
|
58
|
+
"> Documentation Index https://developers.cloudflare.com/durable-objects/llms.txt\n"
|
|
59
|
+
question := "For a new Cloudflare Worker using a SQLite-backed Durable Object, show complete wrangler configuration, Worker binding, and SQL table code."
|
|
60
|
+
got := researchCandidates("https://developers.cloudflare.com/durable-objects/", primary, question, 2)
|
|
61
|
+
if len(got) != 2 {
|
|
62
|
+
t.Fatalf("unexpected number of selected references: %+v", got)
|
|
63
|
+
}
|
|
64
|
+
var links []string
|
|
65
|
+
for _, c := range got {
|
|
66
|
+
links = append(links, c.url)
|
|
67
|
+
}
|
|
68
|
+
output := strings.Join(links, "\n")
|
|
69
|
+
if !strings.Contains(output, "/get-started/") || strings.Contains(output, "/alarms/") {
|
|
70
|
+
t.Fatalf("unrelated alarm material displaced getting-started guide: %s", output)
|
|
71
|
+
}
|
|
72
|
+
}
|