@expo/code-review-cli 0.12.1 → 0.12.2
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/README.md +42 -17
- package/build/core/claude-code.js +23 -3
- package/build/core/opencode.js +23 -4
- package/build/core/prompts.js +35 -6
- package/build/core/render.js +13 -0
- package/build/core/research.js +222 -7
- package/build/core/review.js +72 -24
- package/build/core/schema.js +14 -0
- package/build/core/tools.js +5 -0
- package/build/research-mcp/audit.js +163 -0
- package/build/research-mcp/brave-search.js +2 -11
- package/build/research-mcp/cli.js +18 -2
- package/build/research-mcp/direct-fetch.js +100 -0
- package/build/research-mcp/query-sanitizer.js +146 -0
- package/build/research-mcp/server.js +205 -107
- package/package.json +1 -1
- package/templates/coordinator.md +2 -0
- package/templates/shared.md +7 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const MAX_QUERY_CHARACTERS = 160;
|
|
2
|
+
const MAX_QUERY_TOKENS = 8;
|
|
3
|
+
const MAX_TOKEN_CHARACTERS = 64;
|
|
4
|
+
const SECRET_SHAPE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\b(?:sk|xox[baprs]|gh[opusr])[-_][A-Za-z0-9_-]{12,}\b|\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b)/i;
|
|
5
|
+
const NAMED_SECRET = /\b(?:api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|password|passwd|secret|credential)\b\s*(?::|=|is)?\s*\S+/i;
|
|
6
|
+
const QUOTED_LITERAL = /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g;
|
|
7
|
+
const URL_OR_EMAIL = /\b(?:https?:\/\/|www\.)\S+|\b[^\s@]+@[^\s@]+\.[^\s@]+\b/gi;
|
|
8
|
+
const PATH_LIKE = /(?:^|\s)(?:\.{0,2}[\\/]|[A-Za-z]:[\\/]|~[\\/])\S+/g;
|
|
9
|
+
const QUERY_TOKEN = /[A-Za-z_][A-Za-z0-9_.:$#<>()[\]-]{0,63}|\b\d{1,4}(?:\.\d{1,3}){0,2}\b/g;
|
|
10
|
+
const PROSE_STOP_WORDS = new Set([
|
|
11
|
+
"a",
|
|
12
|
+
"about",
|
|
13
|
+
"an",
|
|
14
|
+
"and",
|
|
15
|
+
"are",
|
|
16
|
+
"can",
|
|
17
|
+
"could",
|
|
18
|
+
"do",
|
|
19
|
+
"does",
|
|
20
|
+
"documentation",
|
|
21
|
+
"explain",
|
|
22
|
+
"find",
|
|
23
|
+
"for",
|
|
24
|
+
"from",
|
|
25
|
+
"how",
|
|
26
|
+
"i",
|
|
27
|
+
"in",
|
|
28
|
+
"is",
|
|
29
|
+
"it",
|
|
30
|
+
"of",
|
|
31
|
+
"on",
|
|
32
|
+
"or",
|
|
33
|
+
"please",
|
|
34
|
+
"search",
|
|
35
|
+
"show",
|
|
36
|
+
"tell",
|
|
37
|
+
"the",
|
|
38
|
+
"this",
|
|
39
|
+
"to",
|
|
40
|
+
"what",
|
|
41
|
+
"when",
|
|
42
|
+
"where",
|
|
43
|
+
"why",
|
|
44
|
+
"with",
|
|
45
|
+
"work",
|
|
46
|
+
"works",
|
|
47
|
+
]);
|
|
48
|
+
function shannonEntropy(value) {
|
|
49
|
+
const counts = new Map();
|
|
50
|
+
for (const character of value)
|
|
51
|
+
counts.set(character, (counts.get(character) ?? 0) + 1);
|
|
52
|
+
let entropy = 0;
|
|
53
|
+
for (const count of counts.values()) {
|
|
54
|
+
const probability = count / value.length;
|
|
55
|
+
entropy -= probability * Math.log2(probability);
|
|
56
|
+
}
|
|
57
|
+
return entropy;
|
|
58
|
+
}
|
|
59
|
+
function looksHighEntropy(value) {
|
|
60
|
+
if (/^[A-Fa-f0-9]{24,}$/.test(value))
|
|
61
|
+
return true;
|
|
62
|
+
if (/^[A-Za-z0-9+/]{20,}={0,2}$/.test(value) && /[+/=]/.test(value))
|
|
63
|
+
return true;
|
|
64
|
+
return (value.length >= 24 &&
|
|
65
|
+
/[a-z]/.test(value) &&
|
|
66
|
+
/[A-Z]/.test(value) &&
|
|
67
|
+
/\d/.test(value) &&
|
|
68
|
+
shannonEntropy(value) >= 4.2);
|
|
69
|
+
}
|
|
70
|
+
function isApiAnchor(value) {
|
|
71
|
+
return (/[a-z][A-Z]/.test(value) ||
|
|
72
|
+
/[A-Z][A-Za-z0-9_]{1,}/.test(value) ||
|
|
73
|
+
/[._:$#()]/.test(value) ||
|
|
74
|
+
/_[A-Z0-9]/.test(value));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert an agent-authored search into a short API-symbol query. Dangerous shapes
|
|
78
|
+
* fail closed; prose, literals, URLs, paths, and unsupported punctuation are removed.
|
|
79
|
+
*/
|
|
80
|
+
export function sanitizeDocumentationQuery(rawQuery) {
|
|
81
|
+
const visible = rawQuery
|
|
82
|
+
.normalize("NFKC")
|
|
83
|
+
// oxlint-disable-next-line no-control-regex -- outbound query containment
|
|
84
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
85
|
+
.replace(/\s+/g, " ")
|
|
86
|
+
.trim();
|
|
87
|
+
if (!visible || visible.length > 300) {
|
|
88
|
+
throw new Error("Query must contain between 1 and 300 visible characters");
|
|
89
|
+
}
|
|
90
|
+
if (SECRET_SHAPE.test(visible) || NAMED_SECRET.test(visible)) {
|
|
91
|
+
throw new Error("Query contains credential-shaped or secret-labeled material");
|
|
92
|
+
}
|
|
93
|
+
const candidates = (visible
|
|
94
|
+
.replace(QUOTED_LITERAL, " ")
|
|
95
|
+
.replace(URL_OR_EMAIL, " ")
|
|
96
|
+
.replace(PATH_LIKE, " ")
|
|
97
|
+
.match(QUERY_TOKEN) ?? []).filter((token) => {
|
|
98
|
+
if (token.length > MAX_TOKEN_CHARACTERS || looksHighEntropy(token))
|
|
99
|
+
return false;
|
|
100
|
+
return !PROSE_STOP_WORDS.has(token.toLowerCase());
|
|
101
|
+
});
|
|
102
|
+
const unique = [...new Set(candidates)].slice(0, MAX_QUERY_TOKENS);
|
|
103
|
+
if (!unique.some(isApiAnchor)) {
|
|
104
|
+
throw new Error("Query must include an API-like symbol or member name");
|
|
105
|
+
}
|
|
106
|
+
const sanitized = unique.join(" ").slice(0, MAX_QUERY_CHARACTERS).trim();
|
|
107
|
+
if (!sanitized)
|
|
108
|
+
throw new Error("Query contained no safe documentation terms");
|
|
109
|
+
return sanitized;
|
|
110
|
+
}
|
|
111
|
+
/** Reject URL decorations and path segments that could encode arbitrary outbound data. */
|
|
112
|
+
export function assertSafeDocumentationUrlShape(rawUrl) {
|
|
113
|
+
let url;
|
|
114
|
+
try {
|
|
115
|
+
url = new URL(rawUrl);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new Error("Invalid documentation URL");
|
|
119
|
+
}
|
|
120
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port) {
|
|
121
|
+
throw new Error("Documentation URL must use plain HTTPS without credentials or a port");
|
|
122
|
+
}
|
|
123
|
+
if (url.search || url.hash) {
|
|
124
|
+
throw new Error("Documentation URL must not contain a query string or fragment");
|
|
125
|
+
}
|
|
126
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
127
|
+
for (const segment of segments) {
|
|
128
|
+
let decoded;
|
|
129
|
+
try {
|
|
130
|
+
decoded = decodeURIComponent(segment);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error("Documentation URL contains an invalid encoded path segment");
|
|
134
|
+
}
|
|
135
|
+
// oxlint-disable-next-line no-control-regex -- URL path is an outbound data boundary
|
|
136
|
+
const containsControlCharacter = /[\\/\u0000-\u001f\u007f]/.test(decoded);
|
|
137
|
+
if (decoded.length > 120 ||
|
|
138
|
+
containsControlCharacter ||
|
|
139
|
+
SECRET_SHAPE.test(decoded) ||
|
|
140
|
+
NAMED_SECRET.test(decoded) ||
|
|
141
|
+
looksHighEntropy(decoded)) {
|
|
142
|
+
throw new Error("Documentation URL contains a suspicious path segment");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return url;
|
|
146
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { ResearchAudit } from "./audit.js";
|
|
5
|
+
import { sanitizeDocumentationQuery } from "./query-sanitizer.js";
|
|
6
|
+
import { fetchDocumentationUrl, resolveDirectDocumentationTarget } from "./direct-fetch.js";
|
|
4
7
|
import { searchExpoAlgolia } from "./expo-algolia.js";
|
|
5
8
|
import { searchOkHttpDocumentation } from "./okhttp-search.js";
|
|
6
9
|
import { getProvider, resolveAllowedUrl } from "./providers.js";
|
|
@@ -20,6 +23,9 @@ function defaultProviders(platform) {
|
|
|
20
23
|
}
|
|
21
24
|
export async function createDocumentationServer(options = {}) {
|
|
22
25
|
const index = options.indexPath ? await loadSearchIndex(options.indexPath) : undefined;
|
|
26
|
+
const maxCalls = Math.min(20, Math.max(1, options.maxCalls ?? 8));
|
|
27
|
+
const maxResultsPerCall = Math.min(3, Math.max(1, options.maxResultsPerCall ?? 3));
|
|
28
|
+
const audit = new ResearchAudit(options.auditPath, maxCalls);
|
|
23
29
|
const server = new McpServer({
|
|
24
30
|
name: "review-research-mcp",
|
|
25
31
|
version: "0.2.0",
|
|
@@ -58,130 +64,222 @@ export async function createDocumentationServer(options = {}) {
|
|
|
58
64
|
openWorldHint: true,
|
|
59
65
|
},
|
|
60
66
|
}, async ({ platform, query, limit, language, providers, sourceKinds }) => {
|
|
67
|
+
const sanitizedQuery = sanitizeDocumentationQuery(query);
|
|
61
68
|
const selectedProviders = (providers ?? defaultProviders(platform)).filter((provider) => platform === "all" || getProvider(provider).platform === platform);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
if (selectedProviders.length === 0) {
|
|
70
|
+
throw new Error("No selected documentation provider matches the requested platform");
|
|
71
|
+
}
|
|
72
|
+
const boundedLimit = Math.min(limit, maxResultsPerCall);
|
|
73
|
+
const auditInput = {
|
|
74
|
+
platform,
|
|
75
|
+
providers: selectedProviders,
|
|
76
|
+
query: sanitizedQuery,
|
|
77
|
+
};
|
|
78
|
+
const requestId = await audit.reserve("search_platform_docs", auditInput);
|
|
79
|
+
try {
|
|
80
|
+
const localResults = index
|
|
81
|
+
? searchDocumentation(index, sanitizedQuery, {
|
|
82
|
+
platform,
|
|
83
|
+
limit: boundedLimit,
|
|
84
|
+
providers: selectedProviders,
|
|
85
|
+
...(sourceKinds ? { sourceKinds } : {}),
|
|
86
|
+
...(language ? { language } : {}),
|
|
87
|
+
})
|
|
88
|
+
: [];
|
|
89
|
+
const warnings = [];
|
|
90
|
+
const remoteResults = [];
|
|
91
|
+
const perProviderLimit = Math.max(1, Math.ceil(boundedLimit / selectedProviders.length));
|
|
92
|
+
const indexedAt = new Date().toISOString();
|
|
93
|
+
const searched = await Promise.all(selectedProviders.map(async (provider) => {
|
|
94
|
+
if (provider === "expo") {
|
|
95
|
+
if (sourceKinds && !sourceKinds.includes("official-api")) {
|
|
96
|
+
return { results: [], warnings: [] };
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const documents = await searchExpoAlgolia(sanitizedQuery, perProviderLimit);
|
|
100
|
+
return {
|
|
101
|
+
results: documents.map((document, position) => ({
|
|
102
|
+
id: `expo-algolia:${document.url}`,
|
|
103
|
+
platform: document.platform,
|
|
104
|
+
provider: "expo",
|
|
105
|
+
sourceKind: "official-api",
|
|
106
|
+
title: document.title,
|
|
107
|
+
url: document.url,
|
|
108
|
+
passage: document.body.slice(0, 1_400),
|
|
109
|
+
indexedAt,
|
|
110
|
+
score: perProviderLimit - position,
|
|
111
|
+
})),
|
|
112
|
+
warnings: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
return {
|
|
118
|
+
results: [],
|
|
119
|
+
warnings: [`Expo documentation search unavailable: ${message}`],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
79
122
|
}
|
|
80
|
-
|
|
81
|
-
|
|
123
|
+
if (provider === "okhttp" &&
|
|
124
|
+
(!sourceKinds || sourceKinds.includes("official-guide")) &&
|
|
125
|
+
!language) {
|
|
126
|
+
try {
|
|
127
|
+
const results = await searchOkHttpDocumentation(sanitizedQuery, perProviderLimit, options.fetchImplementation ?? fetch);
|
|
128
|
+
if (results.length > 0) {
|
|
129
|
+
return { results, warnings: [] };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
warnings.push(`OkHttp documentation search unavailable: ${message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!options.braveApiKey) {
|
|
82
138
|
return {
|
|
83
|
-
results:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
sourceKind: "official-api",
|
|
88
|
-
title: document.title,
|
|
89
|
-
url: document.url,
|
|
90
|
-
passage: document.body.slice(0, 1_400),
|
|
91
|
-
indexedAt,
|
|
92
|
-
score: perProviderLimit - position,
|
|
93
|
-
})),
|
|
94
|
-
warnings: [],
|
|
139
|
+
results: [],
|
|
140
|
+
warnings: [
|
|
141
|
+
`Scoped web search unavailable for ${provider}: BRAVE_SEARCH_API_KEY is not set`,
|
|
142
|
+
],
|
|
95
143
|
};
|
|
96
144
|
}
|
|
145
|
+
try {
|
|
146
|
+
return await searchRemoteDocumentation(provider, sanitizedQuery, perProviderLimit, {
|
|
147
|
+
apiKey: options.braveApiKey,
|
|
148
|
+
...(options.fetchImplementation
|
|
149
|
+
? { fetchImplementation: options.fetchImplementation }
|
|
150
|
+
: {}),
|
|
151
|
+
...(language ? { language } : {}),
|
|
152
|
+
...(sourceKinds ? { sourceKinds } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
97
155
|
catch (error) {
|
|
98
156
|
const message = error instanceof Error ? error.message : String(error);
|
|
99
157
|
return {
|
|
100
158
|
results: [],
|
|
101
|
-
warnings: [`
|
|
159
|
+
warnings: [`Scoped web search unavailable for ${provider}: ${message}`],
|
|
102
160
|
};
|
|
103
161
|
}
|
|
162
|
+
}));
|
|
163
|
+
for (const searchedProvider of searched) {
|
|
164
|
+
remoteResults.push(...searchedProvider.results);
|
|
165
|
+
warnings.push(...searchedProvider.warnings);
|
|
104
166
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const results = [...remoteResults, ...localResults]
|
|
169
|
+
.filter((result) => {
|
|
170
|
+
if (!result.provider)
|
|
171
|
+
return false;
|
|
108
172
|
try {
|
|
109
|
-
|
|
110
|
-
if (results.length > 0) {
|
|
111
|
-
return { results, warnings: [] };
|
|
112
|
-
}
|
|
173
|
+
resolveAllowedUrl(getProvider(result.provider), result.url);
|
|
113
174
|
}
|
|
114
|
-
catch
|
|
115
|
-
|
|
116
|
-
warnings.push(`OkHttp documentation search unavailable: ${message}`);
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
117
177
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (const searchedProvider of searched) {
|
|
146
|
-
remoteResults.push(...searchedProvider.results);
|
|
147
|
-
warnings.push(...searchedProvider.warnings);
|
|
178
|
+
const key = `${result.provider}|${result.url}|${result.title}`;
|
|
179
|
+
if (seen.has(key))
|
|
180
|
+
return false;
|
|
181
|
+
seen.add(key);
|
|
182
|
+
return true;
|
|
183
|
+
})
|
|
184
|
+
.slice(0, boundedLimit);
|
|
185
|
+
const uniqueWarnings = [...new Set(warnings)].slice(0, 10);
|
|
186
|
+
const payload = {
|
|
187
|
+
notice: untrustedMaterialNotice,
|
|
188
|
+
retrieval: {
|
|
189
|
+
scopedWebSearch: Boolean(options.braveApiKey),
|
|
190
|
+
expoSearch: selectedProviders.includes("expo"),
|
|
191
|
+
localIndex: index
|
|
192
|
+
? {
|
|
193
|
+
generatedAt: index.serialized.generatedAt,
|
|
194
|
+
providers: index.serialized.providers,
|
|
195
|
+
}
|
|
196
|
+
: null,
|
|
197
|
+
},
|
|
198
|
+
...(uniqueWarnings.length > 0 ? { warnings: uniqueWarnings } : {}),
|
|
199
|
+
results,
|
|
200
|
+
};
|
|
201
|
+
await audit.complete(requestId, "search_platform_docs", auditInput, results, uniqueWarnings);
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
204
|
+
};
|
|
148
205
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
206
|
+
catch (error) {
|
|
207
|
+
await audit.fail(requestId, "search_platform_docs", auditInput, error);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
server.registerTool("fetch_platform_doc", {
|
|
212
|
+
title: "Fetch an official documentation URL",
|
|
213
|
+
description: "Fetch one caller-supplied documentation URL from the fixed Apple, Android, Expo, React Native, dependency, build-tool, release-note, or issue-source allowlist. The URL and every redirect are revalidated before download. Returns bounded extracted passages and the canonical source URL; use query only to rank passages within that page.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
url: z
|
|
216
|
+
.string()
|
|
217
|
+
.url()
|
|
218
|
+
.max(2_000)
|
|
219
|
+
.describe("Exact HTTPS documentation URL to fetch; must match a supported provider"),
|
|
220
|
+
provider: z
|
|
221
|
+
.enum(PROVIDERS)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("Optional provider hint for URLs accepted by more than one corpus"),
|
|
224
|
+
query: z
|
|
225
|
+
.string()
|
|
226
|
+
.min(1)
|
|
227
|
+
.max(300)
|
|
228
|
+
.optional()
|
|
229
|
+
.describe("Optional short phrase used only to select the most relevant page passages"),
|
|
230
|
+
limit: z.number().int().min(1).max(5).default(3),
|
|
231
|
+
},
|
|
232
|
+
annotations: {
|
|
233
|
+
readOnlyHint: true,
|
|
234
|
+
destructiveHint: false,
|
|
235
|
+
idempotentHint: true,
|
|
236
|
+
openWorldHint: true,
|
|
237
|
+
},
|
|
238
|
+
}, async ({ url, provider, query, limit }) => {
|
|
239
|
+
const sanitizedQuery = query ? sanitizeDocumentationQuery(query) : undefined;
|
|
240
|
+
// Resolve and validate before recording anything. This prevents credentials,
|
|
241
|
+
// query strings, or covert high-entropy path data from reaching either the
|
|
242
|
+
// network or the append-only audit file.
|
|
243
|
+
const target = resolveDirectDocumentationTarget(url, provider);
|
|
244
|
+
const auditInput = {
|
|
245
|
+
providers: [target.provider],
|
|
246
|
+
...(sanitizedQuery ? { query: sanitizedQuery } : {}),
|
|
247
|
+
url: target.url.href,
|
|
184
248
|
};
|
|
249
|
+
const requestId = await audit.reserve("fetch_platform_doc", auditInput);
|
|
250
|
+
try {
|
|
251
|
+
const fetched = await fetchDocumentationUrl(target.url.href, {
|
|
252
|
+
provider: target.provider,
|
|
253
|
+
...(sanitizedQuery ? { query: sanitizedQuery } : {}),
|
|
254
|
+
limit: Math.min(limit, maxResultsPerCall),
|
|
255
|
+
...(options.fetchImplementation
|
|
256
|
+
? { fetchImplementation: options.fetchImplementation }
|
|
257
|
+
: {}),
|
|
258
|
+
});
|
|
259
|
+
const payload = {
|
|
260
|
+
notice: untrustedMaterialNotice,
|
|
261
|
+
retrieval: {
|
|
262
|
+
mode: "direct-url",
|
|
263
|
+
provider: fetched.provider,
|
|
264
|
+
sourceKind: fetched.sourceKind,
|
|
265
|
+
canonicalUrl: fetched.canonicalUrl,
|
|
266
|
+
},
|
|
267
|
+
results: fetched.results,
|
|
268
|
+
};
|
|
269
|
+
await audit.complete(requestId, "fetch_platform_doc", {
|
|
270
|
+
...auditInput,
|
|
271
|
+
platform: getProvider(fetched.provider).platform,
|
|
272
|
+
providers: [fetched.provider],
|
|
273
|
+
url: fetched.canonicalUrl,
|
|
274
|
+
}, fetched.results);
|
|
275
|
+
return {
|
|
276
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
await audit.fail(requestId, "fetch_platform_doc", auditInput, error);
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
185
283
|
});
|
|
186
284
|
return server;
|
|
187
285
|
}
|
package/package.json
CHANGED
package/templates/coordinator.md
CHANGED
|
@@ -34,6 +34,8 @@ metadata. You do **not** re-review the code. You consolidate and decide.
|
|
|
34
34
|
field from the final finding after folding it into `rationale`; otherwise the
|
|
35
35
|
reporter detaches it below the collapsed block. Infer conservatively when a
|
|
36
36
|
reviewer omitted either signal. Drop low-confidence findings.
|
|
37
|
+
Preserve each kept finding's grounded `sources` array. When merging duplicates,
|
|
38
|
+
keep the union of their existing sources. Never invent or edit a source.
|
|
37
39
|
<!-- @ref LLP 0009#prompt-rules-for-adopters [implements] — the handoff is summary input only, never a reported finding and never a decision input -->
|
|
38
40
|
4. **Extract overall PR risk.** Find the internal `__overall_pr_risk__` handoff
|
|
39
41
|
from the cross-cutting reviewer, or from the full-context security reviewer
|
package/templates/shared.md
CHANGED
|
@@ -215,7 +215,8 @@ Return **only** a single fenced ```json code block, an object of this shape:
|
|
|
215
215
|
"title": "short one-line summary",
|
|
216
216
|
"rationale": "**Confidence:** High — why certainty is high.<br>**Impact if shipped:** Medium — concrete expected consequence.\\n\\n<details>\\n<summary>Evidence and reasoning</summary>\\n\\nFull failure/exploit path.\\n\\n</details>",
|
|
217
217
|
"evidence": "one contiguous line of the flagged code, copied VERBATIM",
|
|
218
|
-
"suggestion": "optional concrete fix, or omit"
|
|
218
|
+
"suggestion": "optional concrete fix, or omit",
|
|
219
|
+
"sources": [{ "title": "exact injected documentation title", "url": "exact injected URL" }]
|
|
219
220
|
}
|
|
220
221
|
],
|
|
221
222
|
"trace": {
|
|
@@ -225,6 +226,11 @@ Return **only** a single fenced ```json code block, an object of this shape:
|
|
|
225
226
|
}
|
|
226
227
|
```
|
|
227
228
|
|
|
229
|
+
`sources` is optional. Include it only when injected platform research materially
|
|
230
|
+
supports the finding. Copy the exact title and URL from that research; the engine
|
|
231
|
+
rejects sources outside the trusted result set. Omit it for findings that did not
|
|
232
|
+
use documentation research.
|
|
233
|
+
|
|
228
234
|
`line` is the start line in the new version of the file, or `null` if not
|
|
229
235
|
line-specific. `evidence` is used to help verify the finding, so make it easy to
|
|
230
236
|
locate: copy **one contiguous line** of the flagged code **verbatim** (not spanning
|