@catheadowl/ukp 0.1.0
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/LICENSE +21 -0
- package/README.md +208 -0
- package/bun.lock +35 -0
- package/package.json +51 -0
- package/src/artifacts.ts +75 -0
- package/src/capabilities/nav.ts +734 -0
- package/src/capabilities/propose.ts +456 -0
- package/src/capabilities/qmd.ts +174 -0
- package/src/capabilities/read.ts +1040 -0
- package/src/capabilities/remote-client.ts +856 -0
- package/src/capabilities/rename-recovery.ts +294 -0
- package/src/capabilities/rg.ts +505 -0
- package/src/capabilities/search.ts +1105 -0
- package/src/capabilities/tls-identity.ts +87 -0
- package/src/capabilities/update.ts +385 -0
- package/src/cli.ts +425 -0
- package/src/commands/diagnose.ts +383 -0
- package/src/commands/flags.ts +49 -0
- package/src/commands/guide-content/client.txt +36 -0
- package/src/commands/guide-content/propose.txt +38 -0
- package/src/commands/guide-content/service-qmd.txt +44 -0
- package/src/commands/guide-content/service.txt +114 -0
- package/src/commands/guide.ts +121 -0
- package/src/commands/init.ts +245 -0
- package/src/commands/inspect.ts +182 -0
- package/src/commands/inventory.ts +570 -0
- package/src/commands/kit.ts +269 -0
- package/src/commands/nav.ts +280 -0
- package/src/commands/propose.ts +257 -0
- package/src/commands/read.ts +558 -0
- package/src/commands/rg.ts +355 -0
- package/src/commands/search.ts +325 -0
- package/src/commands/serve.ts +221 -0
- package/src/commands/update.ts +99 -0
- package/src/config/client.ts +55 -0
- package/src/config/external-tool.ts +58 -0
- package/src/config/file-native.ts +111 -0
- package/src/config/manifest.ts +188 -0
- package/src/fslock.ts +95 -0
- package/src/path-safety.ts +65 -0
- package/src/registry.ts +436 -0
- package/src/scope.ts +78 -0
- package/src/server.ts +1011 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { loadManifest } from "../config/manifest.ts";
|
|
5
|
+
import { resolveExternalToolCapability, EXTERNAL_PROVIDER } from "../config/external-tool.ts";
|
|
6
|
+
import { localPathOf, readRegistry } from "../registry.ts";
|
|
7
|
+
import { resolveScope } from "../scope.ts";
|
|
8
|
+
import { isInsideRealRoot } from "../path-safety.ts";
|
|
9
|
+
|
|
10
|
+
// rg capability (ADR-RG-001..004): an independent atomic
|
|
11
|
+
// capability parallel to `search` — base lexical search over the endpoint's
|
|
12
|
+
// own files via ripgrep. Thin shaping only (vision principle 6): UKP owns
|
|
13
|
+
// name→cwd resolution, scope boundaries, and output shaping into
|
|
14
|
+
// read/file-ready `ukp://` references; rg's own semantics pass through.
|
|
15
|
+
// Structured outcome per ADR 0021 — never rendered text, never exit codes.
|
|
16
|
+
|
|
17
|
+
export interface RgRequest {
|
|
18
|
+
query: string;
|
|
19
|
+
limit: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RgOptions {
|
|
23
|
+
explicitEndpoints?: string[];
|
|
24
|
+
global: boolean;
|
|
25
|
+
glob?: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
ignoreCase?: boolean;
|
|
28
|
+
count?: boolean;
|
|
29
|
+
/** Validated passthrough args (allowlist + scope protection, ADR-RG-002). */
|
|
30
|
+
passthrough: readonly string[];
|
|
31
|
+
/** Surface concern (propose precedent): the capability never consumes it —
|
|
32
|
+
* the adapter picks the render mode; the outcome is always structured. */
|
|
33
|
+
json?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ParsedRg {
|
|
37
|
+
request: RgRequest;
|
|
38
|
+
options: RgOptions;
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RgContext {
|
|
43
|
+
currentDirectory: string;
|
|
44
|
+
registryPath: string;
|
|
45
|
+
rgCommand?: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** ADR 0021 aggregate classification — the adapter maps it onto exit codes;
|
|
49
|
+
* D-036 semantics (skip never changes the success state; a tool failure
|
|
50
|
+
* fails the run; an interrupt is 130). */
|
|
51
|
+
export type RgAggregateStatus =
|
|
52
|
+
| "succeeded"
|
|
53
|
+
| "failed"
|
|
54
|
+
| "cancelled"
|
|
55
|
+
| "no-success";
|
|
56
|
+
|
|
57
|
+
/** One match in the contract zone: endpoint-relative path, optional line,
|
|
58
|
+
* matched-line excerpt, and the durable slot key. `ukp_uri` is emitted only
|
|
59
|
+
* when the path safely resolves inside the Service folder (ADR 0019 stance);
|
|
60
|
+
* rg results are endpoint-local scans so this is the common case. */
|
|
61
|
+
export interface RgMatch {
|
|
62
|
+
path: string;
|
|
63
|
+
line?: number;
|
|
64
|
+
text?: string;
|
|
65
|
+
ukp_uri?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RgCountEntry {
|
|
69
|
+
path: string;
|
|
70
|
+
count: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RgEndpointOutcome {
|
|
74
|
+
name: string;
|
|
75
|
+
provider: typeof EXTERNAL_PROVIDER;
|
|
76
|
+
status?:
|
|
77
|
+
| "succeeded"
|
|
78
|
+
| "no_matches"
|
|
79
|
+
| "skipped"
|
|
80
|
+
| "failed"
|
|
81
|
+
/** human mode only: this endpoint's SIGINT stopped the run — its header
|
|
82
|
+
* was already rendered, its block never is. */
|
|
83
|
+
| "interrupted"
|
|
84
|
+
/** not reached because an earlier endpoint was interrupted. */
|
|
85
|
+
| "cancelled";
|
|
86
|
+
/** Factual skip/failure message (no surface prefix). */
|
|
87
|
+
message?: string;
|
|
88
|
+
/** Match mode results (client-side capped at the request limit). */
|
|
89
|
+
matches?: RgMatch[];
|
|
90
|
+
/** Count mode results (`--count`). */
|
|
91
|
+
counts?: RgCountEntry[];
|
|
92
|
+
/** True when matches were truncated at the request limit. */
|
|
93
|
+
truncated?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RgResult {
|
|
97
|
+
query: string;
|
|
98
|
+
limit: number;
|
|
99
|
+
endpoints: RgEndpointOutcome[];
|
|
100
|
+
warnings: string[];
|
|
101
|
+
aggregate: RgAggregateStatus;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class RgUsageError extends Error {
|
|
105
|
+
constructor(message: string) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "RgUsageError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class RgPlanningError extends Error {
|
|
112
|
+
constructor(message: string) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "RgPlanningError";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const RG_DEFAULT_LIMIT = 50;
|
|
119
|
+
export const RG_MAX_LIMIT = 1000;
|
|
120
|
+
|
|
121
|
+
export function defaultRgCommand(): readonly string[] {
|
|
122
|
+
return ["rg"];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Base-tool availability probe (diagnose / provider resolver): true when a
|
|
126
|
+
* ripgrep executable starts. */
|
|
127
|
+
export function rgExecutableAvailable(): boolean {
|
|
128
|
+
const result = spawnSync("rg", ["--version"], { encoding: "utf8", windowsHide: true, timeout: 5_000 });
|
|
129
|
+
return !result.error;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function rgTimeoutMs(): number {
|
|
133
|
+
const raw = process.env.UKP_RG_TIMEOUT_MS;
|
|
134
|
+
if (raw === undefined || raw.length === 0) return 60_000;
|
|
135
|
+
const parsed = Number(raw);
|
|
136
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 60_000;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Passthrough validation (ADR-RG-002): allowlist + scope protection.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/** Value flags consume the next token (or an attached value: `-C2`). */
|
|
144
|
+
const PASSTHROUGH_VALUE_FLAGS = new Set(["-A", "-B", "-C", "-m", "--max-count", "-g", "--glob", "-t", "--type", "--max-filesize"]);
|
|
145
|
+
const PASSTHROUGH_BOOL_FLAGS = new Set(["-i", "--ignore-case", "-S", "--smart-case", "-s", "--case-sensitive", "-w", "--word-regexp", "-F", "--fixed-strings", "-v", "--invert-match", "-U", "--multiline", "--no-ignore", "--hidden", "--no-messages", "--column", "--no-heading"]);
|
|
146
|
+
|
|
147
|
+
/** Validates passthrough args: allowlist membership + scope protection.
|
|
148
|
+
* Throws `RgUsageError` naming the rejected token and the reason; returns
|
|
149
|
+
* the validated args unchanged. Exported so the parse layer can fail as
|
|
150
|
+
* exit 2 before any endpoint work starts; the capability re-validates. */
|
|
151
|
+
export function validateRgPassthrough(args: readonly string[]): readonly string[] {
|
|
152
|
+
for (let index = 0; index < args.length; index++) {
|
|
153
|
+
const token = args[index]!;
|
|
154
|
+
if (!token.startsWith("-")) {
|
|
155
|
+
throw new RgUsageError(
|
|
156
|
+
`passthrough argument '${token}' is not allowed: rg targets the Service folder — no path operands (use the endpoint selector for scope)`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (PASSTHROUGH_BOOL_FLAGS.has(token)) continue;
|
|
160
|
+
if (PASSTHROUGH_VALUE_FLAGS.has(token)) {
|
|
161
|
+
if (index + 1 >= args.length) {
|
|
162
|
+
throw new RgUsageError(`passthrough flag '${token}' requires a value`);
|
|
163
|
+
}
|
|
164
|
+
index += 1; // consume the value token
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (/^-[ABCm]=/.test(token)) continue; // long-form value on a short flag
|
|
168
|
+
if (/^-[ABCm].+$/.test(token)) continue; // attached value (-C2)
|
|
169
|
+
if (/^--[\w-]+=/.test(token)) {
|
|
170
|
+
const longFlag = token.slice(0, token.indexOf("="));
|
|
171
|
+
if (PASSTHROUGH_VALUE_FLAGS.has(longFlag) || PASSTHROUGH_BOOL_FLAGS.has(longFlag)) continue;
|
|
172
|
+
}
|
|
173
|
+
throw new RgUsageError(
|
|
174
|
+
`passthrough flag '${token}' is not on the rg allowlist (rejected: path operands, output-changing flags such as --json/-r, config/pre filters; the frozen flag set covers the common surface)`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return args;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Execution
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
interface PlannedEndpoint {
|
|
185
|
+
name: string;
|
|
186
|
+
folder: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function planRg(parsed: ParsedRg, context: RgContext): { plan: PlannedEndpoint[]; warnings: string[] } {
|
|
190
|
+
const registry = readRegistry(context.registryPath);
|
|
191
|
+
const scope = resolveScope({
|
|
192
|
+
currentDirectory: context.currentDirectory,
|
|
193
|
+
registry,
|
|
194
|
+
explicitEndpoints: parsed.options.explicitEndpoints,
|
|
195
|
+
global: parsed.options.global,
|
|
196
|
+
});
|
|
197
|
+
const warnings = [...parsed.warnings, ...scope.warnings];
|
|
198
|
+
const plan: PlannedEndpoint[] = [];
|
|
199
|
+
for (const binding of scope.bindings) {
|
|
200
|
+
// Remote bindings are split off to the remote transport by the surface
|
|
201
|
+
// adapter before this point (ukp_remote W6); reaching here with one
|
|
202
|
+
// means a programmatic caller — localPathOf refuses it with a
|
|
203
|
+
// classified error.
|
|
204
|
+
const service = loadManifest(localPathOf(binding));
|
|
205
|
+
if (service.effectiveName !== binding.name) {
|
|
206
|
+
throw new RgPlanningError(
|
|
207
|
+
`endpoint '${binding.name}' no longer matches Service effective name '${service.effectiveName}'`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
// External-tool base tier (ADR-RG-003): rg is effectively present on
|
|
211
|
+
// every registered local Service; the declaration only overrides config.
|
|
212
|
+
resolveExternalToolCapability(service.manifest, "rg");
|
|
213
|
+
plan.push({ name: binding.name, folder: service.folder });
|
|
214
|
+
}
|
|
215
|
+
return { plan, warnings };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function encodeUkpUriSegment(segment: string): string {
|
|
219
|
+
return segment.replace(/[%#? ]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** rg reports paths relative to its cwd (the Service folder); normalize
|
|
223
|
+
* separators and shape the durable slot key. */
|
|
224
|
+
function toUkpUri(endpointName: string, path: string): string {
|
|
225
|
+
const rel = path.replace(/\\/g, "/");
|
|
226
|
+
return `ukp://${endpointName}/${rel.split("/").map(encodeUkpUriSegment).join("/")}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** ADR 0019 emission rule (ADR 0023 / D-073, spec §3): the URI is a slot
|
|
230
|
+
* promise, so it is emitted only when the match path safely resolves inside
|
|
231
|
+
* the Service folder — resolved containment, same stance as search's
|
|
232
|
+
* provider-location mapping. A miss silently drops the `ukp_uri` field
|
|
233
|
+
* (the match and its `path` stay): rg output is tool output, not user
|
|
234
|
+
* error. Output-side filtering is the second line of defense; the
|
|
235
|
+
* passthrough allowlist (no path operands) is the first. */
|
|
236
|
+
function ukpUriIfInside(endpointFolder: string, endpointName: string, path: string): string | undefined {
|
|
237
|
+
const absolute = resolve(endpointFolder, ...path.replace(/\\/g, "/").split("/"));
|
|
238
|
+
let folderReal: string;
|
|
239
|
+
let targetReal: string;
|
|
240
|
+
try {
|
|
241
|
+
folderReal = realpathSync(endpointFolder);
|
|
242
|
+
targetReal = realpathSync(absolute);
|
|
243
|
+
} catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (!isInsideRealRoot(folderReal, targetReal)) return undefined;
|
|
247
|
+
return toUkpUri(endpointName, path);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface RgJsonEvent {
|
|
251
|
+
type?: string;
|
|
252
|
+
data?: {
|
|
253
|
+
path?: { text?: string };
|
|
254
|
+
line_number?: number;
|
|
255
|
+
lines?: { text?: string };
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function rgToolArgs(parsed: ParsedRg): string[] {
|
|
260
|
+
const args: string[] = ["--no-heading", "--no-messages"];
|
|
261
|
+
if (parsed.options.count) {
|
|
262
|
+
args.push("--count");
|
|
263
|
+
} else {
|
|
264
|
+
args.push("--json");
|
|
265
|
+
}
|
|
266
|
+
if (parsed.options.ignoreCase) args.push("-i");
|
|
267
|
+
if (parsed.options.glob !== undefined) args.push("--glob", parsed.options.glob);
|
|
268
|
+
if (parsed.options.type !== undefined) args.push("--type", parsed.options.type);
|
|
269
|
+
args.push(...parsed.options.passthrough);
|
|
270
|
+
args.push("-e", parsed.request.query);
|
|
271
|
+
return args;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Runs the rg capability and returns the structured outcome. Scope and
|
|
275
|
+
* planning failures throw typed errors (`RgUsageError`, `RgPlanningError`,
|
|
276
|
+
* `ScopeError`, `ManifestError`) for the surface adapter to map. */
|
|
277
|
+
export function runRg(parsed: ParsedRg, context: RgContext): RgResult {
|
|
278
|
+
validateRgPassthrough(parsed.options.passthrough);
|
|
279
|
+
const { plan, warnings } = planRg(parsed, context);
|
|
280
|
+
const command = context.rgCommand ?? defaultRgCommand();
|
|
281
|
+
const endpoints: RgEndpointOutcome[] = [];
|
|
282
|
+
let succeeded = false;
|
|
283
|
+
let failed = false;
|
|
284
|
+
let interrupted = false;
|
|
285
|
+
|
|
286
|
+
for (const endpoint of plan) {
|
|
287
|
+
const outcome: RgEndpointOutcome = { name: endpoint.name, provider: EXTERNAL_PROVIDER };
|
|
288
|
+
endpoints.push(outcome);
|
|
289
|
+
if (interrupted) {
|
|
290
|
+
outcome.status = "cancelled";
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Explicit search root: an implicit-cwd search is environment-sensitive
|
|
295
|
+
// (first Linux CI run: rg 14.1.1 walked zero bytes with no path operand
|
|
296
|
+
// while an explicit "." matched). "." keeps rg's rendered paths
|
|
297
|
+
// endpoint-relative; the "./" prefix is stripped at both intakes below.
|
|
298
|
+
const result = spawnSync(command[0]!, [...command.slice(1), ...rgToolArgs(parsed), "."], {
|
|
299
|
+
cwd: endpoint.folder,
|
|
300
|
+
encoding: "utf8",
|
|
301
|
+
windowsHide: true,
|
|
302
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
303
|
+
timeout: rgTimeoutMs(),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (result.error) {
|
|
307
|
+
// ADR-RG-003/R-7: a missing base tool is a degradation, not a fault —
|
|
308
|
+
// skipped + warning, the run continues with other endpoints.
|
|
309
|
+
warnings.push(`endpoint '${endpoint.name}' rg unavailable: rg executable is not available`);
|
|
310
|
+
outcome.status = "skipped";
|
|
311
|
+
outcome.message = "rg executable is not available";
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (result.signal === "SIGTERM") {
|
|
315
|
+
failed = true;
|
|
316
|
+
const message = `endpoint '${endpoint.name}' rg timed out after ${rgTimeoutMs() / 1000}s (set UKP_RG_TIMEOUT_MS to adjust)`;
|
|
317
|
+
warnings.push(message);
|
|
318
|
+
outcome.status = "failed";
|
|
319
|
+
outcome.message = `rg timed out after ${rgTimeoutMs() / 1000}s`;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (result.signal === "SIGINT" || result.status === 130) {
|
|
323
|
+
warnings.push(`endpoint '${endpoint.name}' rg cancelled`);
|
|
324
|
+
outcome.status = "interrupted";
|
|
325
|
+
interrupted = true;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
329
|
+
// rg exits 1 for "no matches" (a result, not a failure); anything else
|
|
330
|
+
// is a tool failure.
|
|
331
|
+
failed = true;
|
|
332
|
+
const detail = (result.stderr ?? "").trim().split(/\r?\n/).find((line) => line.length > 0);
|
|
333
|
+
warnings.push(`endpoint '${endpoint.name}' rg failed${detail ? `: ${detail}` : ""}`);
|
|
334
|
+
outcome.status = "failed";
|
|
335
|
+
outcome.message = detail ?? `rg exited with status ${result.status}`;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (parsed.options.count) {
|
|
340
|
+
const counts: RgCountEntry[] = [];
|
|
341
|
+
for (const line of (result.stdout ?? "").split(/\r?\n/)) {
|
|
342
|
+
if (line.length === 0) continue;
|
|
343
|
+
const separator = line.lastIndexOf(":");
|
|
344
|
+
if (separator <= 0) continue;
|
|
345
|
+
const count = Number(line.slice(separator + 1));
|
|
346
|
+
if (!Number.isSafeInteger(count)) continue;
|
|
347
|
+
counts.push({ path: line.slice(0, separator).replace(/\\/g, "/").replace(/^\.\//, ""), count });
|
|
348
|
+
}
|
|
349
|
+
if (counts.length === 0) {
|
|
350
|
+
succeeded = true; // no matches is a successful result (D-036 stance)
|
|
351
|
+
outcome.status = "no_matches";
|
|
352
|
+
} else {
|
|
353
|
+
succeeded = true;
|
|
354
|
+
outcome.status = "succeeded";
|
|
355
|
+
outcome.counts = counts;
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const matches: RgMatch[] = [];
|
|
361
|
+
let truncated = false;
|
|
362
|
+
for (const line of (result.stdout ?? "").split(/\r?\n/)) {
|
|
363
|
+
if (line.length === 0) continue;
|
|
364
|
+
let event: RgJsonEvent;
|
|
365
|
+
try {
|
|
366
|
+
event = JSON.parse(line) as RgJsonEvent;
|
|
367
|
+
} catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (event.type !== "match") continue;
|
|
371
|
+
if (matches.length >= parsed.request.limit) {
|
|
372
|
+
truncated = true;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const path = event.data?.path?.text;
|
|
376
|
+
if (typeof path !== "string" || path.length === 0) continue;
|
|
377
|
+
const entry: RgMatch = { path: path.replace(/\\/g, "/").replace(/^\.\//, "") };
|
|
378
|
+
const lineNumber = event.data?.line_number;
|
|
379
|
+
if (Number.isSafeInteger(lineNumber) && (lineNumber as number) > 0) entry.line = lineNumber;
|
|
380
|
+
const text = event.data?.lines?.text;
|
|
381
|
+
if (typeof text === "string") {
|
|
382
|
+
const firstLine = text.split(/\r?\n/)[0] ?? "";
|
|
383
|
+
if (firstLine.length > 0) entry.text = firstLine.length > 240 ? `${firstLine.slice(0, 240)}...` : firstLine;
|
|
384
|
+
}
|
|
385
|
+
const uri = ukpUriIfInside(endpoint.folder, endpoint.name, entry.path);
|
|
386
|
+
if (uri !== undefined) entry.ukp_uri = uri;
|
|
387
|
+
matches.push(entry);
|
|
388
|
+
}
|
|
389
|
+
if (matches.length === 0 && !truncated) {
|
|
390
|
+
// No matches is a successful result (search precedent D-036: a
|
|
391
|
+
// no-match endpoint never fails the run).
|
|
392
|
+
succeeded = true;
|
|
393
|
+
outcome.status = "no_matches";
|
|
394
|
+
} else {
|
|
395
|
+
succeeded = true;
|
|
396
|
+
outcome.status = "succeeded";
|
|
397
|
+
outcome.matches = matches;
|
|
398
|
+
if (truncated) outcome.truncated = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
query: parsed.request.query,
|
|
404
|
+
limit: parsed.request.limit,
|
|
405
|
+
endpoints,
|
|
406
|
+
warnings,
|
|
407
|
+
aggregate: interrupted
|
|
408
|
+
? "cancelled"
|
|
409
|
+
: failed
|
|
410
|
+
? "failed"
|
|
411
|
+
: succeeded
|
|
412
|
+
? "succeeded"
|
|
413
|
+
: "no-success",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Presentation (ADR 0021 two-stage form, surface-neutral view).
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
export interface RgEnvelope {
|
|
422
|
+
schema: "ukp.rg.v1";
|
|
423
|
+
command: "rg";
|
|
424
|
+
capability: "rg";
|
|
425
|
+
query: string;
|
|
426
|
+
limit: number;
|
|
427
|
+
count_mode: boolean;
|
|
428
|
+
endpoints: Array<{
|
|
429
|
+
name: string;
|
|
430
|
+
provider: string;
|
|
431
|
+
status: string;
|
|
432
|
+
match_count?: number;
|
|
433
|
+
truncated?: boolean;
|
|
434
|
+
message?: string;
|
|
435
|
+
matches?: RgMatch[];
|
|
436
|
+
counts?: RgCountEntry[];
|
|
437
|
+
}>;
|
|
438
|
+
warnings: string[];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function projectRgEnvelope(result: RgResult, countMode: boolean): RgEnvelope {
|
|
442
|
+
return {
|
|
443
|
+
schema: "ukp.rg.v1",
|
|
444
|
+
command: "rg",
|
|
445
|
+
capability: "rg",
|
|
446
|
+
query: result.query,
|
|
447
|
+
limit: result.limit,
|
|
448
|
+
count_mode: countMode,
|
|
449
|
+
endpoints: result.endpoints.map((endpoint) => ({
|
|
450
|
+
name: endpoint.name,
|
|
451
|
+
provider: endpoint.provider,
|
|
452
|
+
status: endpoint.status ?? "failed",
|
|
453
|
+
...(endpoint.matches !== undefined ? { match_count: endpoint.matches.length, matches: endpoint.matches } : {}),
|
|
454
|
+
...(endpoint.counts !== undefined ? { counts: endpoint.counts } : {}),
|
|
455
|
+
...(endpoint.truncated !== undefined ? { truncated: endpoint.truncated } : {}),
|
|
456
|
+
...(endpoint.message !== undefined ? { message: endpoint.message } : {}),
|
|
457
|
+
})),
|
|
458
|
+
warnings: result.warnings,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export interface RgHumanView {
|
|
463
|
+
body: string;
|
|
464
|
+
diagnostics: string;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function renderRgHuman(result: RgResult): RgHumanView {
|
|
468
|
+
const lines: string[] = [];
|
|
469
|
+
for (const endpoint of result.endpoints) {
|
|
470
|
+
if (endpoint.status === "skipped" || endpoint.status === "cancelled") continue;
|
|
471
|
+
if (lines.length > 0) lines.push("");
|
|
472
|
+
lines.push(`== ${endpoint.name} ==`);
|
|
473
|
+
if (endpoint.status === "interrupted") break;
|
|
474
|
+
if (endpoint.status === "failed") {
|
|
475
|
+
lines.push(`status: failed${endpoint.message ? `: ${endpoint.message}` : ""}`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (endpoint.status === "no_matches") {
|
|
479
|
+
lines.push("(no matches)");
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (endpoint.counts !== undefined) {
|
|
483
|
+
for (const entry of endpoint.counts) lines.push(`${entry.path}: ${entry.count}`);
|
|
484
|
+
if (endpoint.truncated) lines.push("(truncated at the result limit)");
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
let unitIndex = 1;
|
|
488
|
+
for (const match of endpoint.matches ?? []) {
|
|
489
|
+
if (unitIndex > 1) lines.push("");
|
|
490
|
+
const identity = match.line !== undefined ? `${match.path}:${match.line}` : match.path;
|
|
491
|
+
lines.push(`${unitIndex}. ${identity}`);
|
|
492
|
+
if (match.text !== undefined) lines.push(` ${match.text.replace(/\n/g, "\n ")}`);
|
|
493
|
+
if (match.ukp_uri !== undefined) {
|
|
494
|
+
const fragment = match.line !== undefined ? `#L${match.line}` : "";
|
|
495
|
+
lines.push(` uri: ${match.ukp_uri}${fragment}`);
|
|
496
|
+
}
|
|
497
|
+
unitIndex += 1;
|
|
498
|
+
}
|
|
499
|
+
if (endpoint.truncated) lines.push(`(truncated at the result limit ${result.limit}; raise --limit)`);
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
body: lines.length > 0 ? `${lines.join("\n")}\n` : "",
|
|
503
|
+
diagnostics: result.warnings.length > 0 ? `${result.warnings.join("\n")}\n` : "",
|
|
504
|
+
};
|
|
505
|
+
}
|