wdyt 0.1.13 → 0.1.15
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/package.json +1 -1
- package/src/commands/chat.ts +65 -3
- package/src/context/hints.test.ts +135 -0
- package/src/context/hints.ts +264 -0
- package/src/context/index.ts +48 -0
- package/src/context/references.test.ts +341 -0
- package/src/context/references.ts +232 -0
- package/src/context/rereview.test.ts +135 -0
- package/src/context/rereview.ts +204 -0
- package/src/context/symbols.test.ts +550 -0
- package/src/context/symbols.ts +234 -0
- package/src/flow/index.ts +18 -0
- package/src/flow/specs.test.ts +260 -0
- package/src/flow/specs.ts +255 -0
- package/src/git/diff.test.ts +311 -0
- package/src/git/diff.ts +205 -0
- package/src/integration.test.ts +538 -0
package/package.json
CHANGED
package/src/commands/chat.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { join, dirname, basename } from "path";
|
|
|
23
23
|
import { homedir } from "os";
|
|
24
24
|
import { $ } from "bun";
|
|
25
25
|
import { getTab, getWindow } from "../state";
|
|
26
|
+
import { processReReview, recordReview } from "../context/rereview";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Chat send payload structure (from flowctl.py build_chat_payload)
|
|
@@ -32,9 +33,17 @@ export interface ChatSendPayload {
|
|
|
32
33
|
mode: string;
|
|
33
34
|
new_chat?: boolean;
|
|
34
35
|
chat_name?: string;
|
|
36
|
+
chat_id?: string; // Continue specific chat by ID (for re-reviews)
|
|
35
37
|
selected_paths?: string[];
|
|
38
|
+
base_branch?: string; // Base branch for changed files detection
|
|
39
|
+
review_type?: string; // Type of review for preamble (e.g., "implementation", "plan")
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Verdict type for code reviews
|
|
44
|
+
*/
|
|
45
|
+
export type Verdict = "SHIP" | "NEEDS_WORK" | "MAJOR_RETHINK";
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* Chat send response
|
|
40
49
|
*/
|
|
@@ -42,6 +51,9 @@ export interface ChatSendResponse {
|
|
|
42
51
|
id: string;
|
|
43
52
|
path: string;
|
|
44
53
|
review?: string;
|
|
54
|
+
verdict?: Verdict;
|
|
55
|
+
isReReview?: boolean;
|
|
56
|
+
changedFiles?: string[];
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
@@ -207,6 +219,22 @@ async function loadSkillPrompt(skillName: string): Promise<string> {
|
|
|
207
219
|
throw new Error(`Skill not found: ${skillName}`);
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Parse verdict from Claude's response
|
|
224
|
+
* Looks for <verdict>SHIP|NEEDS_WORK|MAJOR_RETHINK</verdict> tag
|
|
225
|
+
*
|
|
226
|
+
* @param response - The raw response from Claude
|
|
227
|
+
* @returns The parsed verdict or undefined if not found
|
|
228
|
+
*/
|
|
229
|
+
function parseVerdict(response: string): Verdict | undefined {
|
|
230
|
+
const verdictMatch = response.match(/<verdict>(SHIP|NEEDS_WORK|MAJOR_RETHINK)<\/verdict>/i);
|
|
231
|
+
if (verdictMatch) {
|
|
232
|
+
// Normalize to uppercase since the regex is case-insensitive
|
|
233
|
+
return verdictMatch[1].toUpperCase() as Verdict;
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
/**
|
|
211
239
|
* Run a chat using Claude CLI
|
|
212
240
|
* Sends the prompt + context to Claude and returns the response
|
|
@@ -378,8 +406,24 @@ export async function chatSendCommand(
|
|
|
378
406
|
const tab = await getTab(windowId, tabId);
|
|
379
407
|
const window = await getWindow(windowId);
|
|
380
408
|
|
|
409
|
+
// Check for re-review scenario
|
|
410
|
+
// Re-review is detected when:
|
|
411
|
+
// - chat_id is provided (continuing a previous chat)
|
|
412
|
+
// - new_chat is explicitly false (not starting fresh)
|
|
413
|
+
const isReReviewExplicit = payload.chat_id !== undefined || payload.new_chat === false;
|
|
414
|
+
const reReviewResult = await processReReview({
|
|
415
|
+
chatId: payload.chat_id,
|
|
416
|
+
isReReview: isReReviewExplicit,
|
|
417
|
+
baseBranch: payload.base_branch,
|
|
418
|
+
reviewType: payload.review_type,
|
|
419
|
+
});
|
|
420
|
+
|
|
381
421
|
// Use message from payload as the prompt, or fall back to tab's prompt
|
|
382
|
-
|
|
422
|
+
// Prepend re-review preamble if this is a re-review
|
|
423
|
+
let prompt = payload.message || tab.prompt;
|
|
424
|
+
if (reReviewResult.isReReview && reReviewResult.preamble) {
|
|
425
|
+
prompt = reReviewResult.preamble + prompt;
|
|
426
|
+
}
|
|
383
427
|
|
|
384
428
|
// Determine which files to include
|
|
385
429
|
// Use selected_paths from payload if provided, otherwise use tab's selectedFiles
|
|
@@ -426,13 +470,26 @@ export async function chatSendCommand(
|
|
|
426
470
|
const chatPath = join(chatsDir, `${chatId}.xml`);
|
|
427
471
|
await Bun.write(chatPath, xmlContent);
|
|
428
472
|
|
|
473
|
+
// Record this review for future re-review detection
|
|
474
|
+
recordReview(chatId, files.map((f) => f.path));
|
|
475
|
+
|
|
429
476
|
// Always run Claude CLI to process the chat - that's what a drop-in rp-cli replacement does
|
|
430
477
|
if (await claudeCliAvailable()) {
|
|
431
478
|
const response = await runClaudeChat(chatPath, prompt);
|
|
432
479
|
|
|
480
|
+
// Parse verdict from response
|
|
481
|
+
const verdict = parseVerdict(response);
|
|
482
|
+
|
|
433
483
|
return {
|
|
434
484
|
success: true,
|
|
435
|
-
data: {
|
|
485
|
+
data: {
|
|
486
|
+
id: chatId,
|
|
487
|
+
path: chatPath,
|
|
488
|
+
review: response,
|
|
489
|
+
verdict,
|
|
490
|
+
isReReview: reReviewResult.isReReview,
|
|
491
|
+
changedFiles: reReviewResult.changedFiles,
|
|
492
|
+
},
|
|
436
493
|
output: `Chat: \`${chatId}\`\n\n${response}`,
|
|
437
494
|
};
|
|
438
495
|
}
|
|
@@ -440,7 +497,12 @@ export async function chatSendCommand(
|
|
|
440
497
|
// Fallback: just return the chat ID if Claude CLI isn't available
|
|
441
498
|
return {
|
|
442
499
|
success: true,
|
|
443
|
-
data: {
|
|
500
|
+
data: {
|
|
501
|
+
id: chatId,
|
|
502
|
+
path: chatPath,
|
|
503
|
+
isReReview: reReviewResult.isReReview,
|
|
504
|
+
changedFiles: reReviewResult.changedFiles,
|
|
505
|
+
},
|
|
444
506
|
output: `Chat: \`${chatId}\`\n\nContext exported to: ${chatPath}\n(Install Claude CLI for automatic LLM processing)`,
|
|
445
507
|
};
|
|
446
508
|
} catch (error) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for context hints generation module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
generateContextHints,
|
|
8
|
+
formatHints,
|
|
9
|
+
type ContextHint,
|
|
10
|
+
} from "./hints";
|
|
11
|
+
|
|
12
|
+
describe("formatHints", () => {
|
|
13
|
+
it("returns empty string for empty hints array", () => {
|
|
14
|
+
const result = formatHints([]);
|
|
15
|
+
expect(result).toBe("");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("formats single hint correctly", () => {
|
|
19
|
+
const hints: ContextHint[] = [
|
|
20
|
+
{ file: "src/auth.ts", line: 15, symbol: "validateToken", refCount: 3 },
|
|
21
|
+
];
|
|
22
|
+
const result = formatHints(hints);
|
|
23
|
+
expect(result).toBe(
|
|
24
|
+
"Consider these related files:\n- src/auth.ts:15 - references validateToken"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("formats multiple hints correctly", () => {
|
|
29
|
+
const hints: ContextHint[] = [
|
|
30
|
+
{ file: "src/auth.ts", line: 15, symbol: "validateToken", refCount: 5 },
|
|
31
|
+
{ file: "src/types.ts", line: 42, symbol: "User", refCount: 3 },
|
|
32
|
+
{ file: "src/api.ts", line: 100, symbol: "fetchUser", refCount: 2 },
|
|
33
|
+
];
|
|
34
|
+
const result = formatHints(hints);
|
|
35
|
+
const expected = [
|
|
36
|
+
"Consider these related files:",
|
|
37
|
+
"- src/auth.ts:15 - references validateToken",
|
|
38
|
+
"- src/types.ts:42 - references User",
|
|
39
|
+
"- src/api.ts:100 - references fetchUser",
|
|
40
|
+
].join("\n");
|
|
41
|
+
expect(result).toBe(expected);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("generateContextHints", () => {
|
|
46
|
+
it("returns empty array when no changed files provided", async () => {
|
|
47
|
+
const result = await generateContextHints({
|
|
48
|
+
changedFiles: [],
|
|
49
|
+
fileContents: new Map(),
|
|
50
|
+
});
|
|
51
|
+
expect(result).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns empty array for unsupported file types", async () => {
|
|
55
|
+
const result = await generateContextHints({
|
|
56
|
+
changedFiles: ["README.md", "config.json"],
|
|
57
|
+
fileContents: new Map([
|
|
58
|
+
["README.md", "# Readme"],
|
|
59
|
+
["config.json", '{"key": "value"}'],
|
|
60
|
+
]),
|
|
61
|
+
});
|
|
62
|
+
expect(result).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("extracts symbols from TypeScript files", async () => {
|
|
66
|
+
const fileContents = new Map([
|
|
67
|
+
[
|
|
68
|
+
"src/test.ts",
|
|
69
|
+
`
|
|
70
|
+
export function uniqueTestFunction123() {}
|
|
71
|
+
export interface UniqueTestInterface456 {}
|
|
72
|
+
export type UniqueTestType789 = string;
|
|
73
|
+
`,
|
|
74
|
+
],
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// This test verifies symbol extraction works
|
|
78
|
+
// References won't be found in a clean test environment,
|
|
79
|
+
// but we can verify the pipeline runs without errors
|
|
80
|
+
const result = await generateContextHints({
|
|
81
|
+
changedFiles: ["src/test.ts"],
|
|
82
|
+
fileContents,
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Should return empty since no references exist for made-up names
|
|
87
|
+
expect(Array.isArray(result)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("respects maxHints limit", async () => {
|
|
91
|
+
// Create a mock scenario where we'd have many hints
|
|
92
|
+
// In practice, the limit is applied after collecting all refs
|
|
93
|
+
const result = await generateContextHints({
|
|
94
|
+
changedFiles: ["src/test.ts"],
|
|
95
|
+
fileContents: new Map([["src/test.ts", "function test() {}"]]),
|
|
96
|
+
cwd: process.cwd(),
|
|
97
|
+
maxHints: 5,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.length).toBeLessThanOrEqual(5);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("integration", () => {
|
|
105
|
+
it("combines extraction and reference finding", async () => {
|
|
106
|
+
// Test with real project files if available
|
|
107
|
+
const fileContents = new Map([
|
|
108
|
+
[
|
|
109
|
+
"src/context/symbols.ts",
|
|
110
|
+
`
|
|
111
|
+
export function extractSymbols() {}
|
|
112
|
+
export interface Symbol {}
|
|
113
|
+
`,
|
|
114
|
+
],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// This runs the full pipeline
|
|
118
|
+
const result = await generateContextHints({
|
|
119
|
+
changedFiles: ["src/context/symbols.ts"],
|
|
120
|
+
fileContents,
|
|
121
|
+
cwd: process.cwd(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Should complete without errors
|
|
125
|
+
expect(Array.isArray(result)).toBe(true);
|
|
126
|
+
|
|
127
|
+
// Each hint should have the required fields
|
|
128
|
+
for (const hint of result) {
|
|
129
|
+
expect(hint).toHaveProperty("file");
|
|
130
|
+
expect(hint).toHaveProperty("line");
|
|
131
|
+
expect(hint).toHaveProperty("symbol");
|
|
132
|
+
expect(hint).toHaveProperty("refCount");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context hints generation module for wdyt
|
|
3
|
+
*
|
|
4
|
+
* Combines symbol extraction + reference finding to generate
|
|
5
|
+
* context hints for code reviews, matching flowctl's gather_context_hints().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractSymbols, isSupported, type Symbol } from "./symbols";
|
|
9
|
+
import { findReferences, type Reference } from "./references";
|
|
10
|
+
|
|
11
|
+
/** Maximum number of context hints to return */
|
|
12
|
+
const MAX_HINTS = 15;
|
|
13
|
+
|
|
14
|
+
/** Maximum references to fetch per symbol (before curation) */
|
|
15
|
+
const REFS_PER_SYMBOL = 5;
|
|
16
|
+
|
|
17
|
+
/** Context hint for a related file */
|
|
18
|
+
export interface ContextHint {
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
symbol: string;
|
|
22
|
+
refCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options for generating context hints */
|
|
26
|
+
export interface GenerateHintsOptions {
|
|
27
|
+
/** Changed files to analyze */
|
|
28
|
+
changedFiles: string[];
|
|
29
|
+
/** File contents (path -> content) */
|
|
30
|
+
fileContents: Map<string, string>;
|
|
31
|
+
/** Working directory (defaults to cwd) */
|
|
32
|
+
cwd?: string;
|
|
33
|
+
/** Maximum hints to return (default: 15) */
|
|
34
|
+
maxHints?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract symbols from changed files
|
|
39
|
+
*
|
|
40
|
+
* @param changedFiles - Array of changed file paths
|
|
41
|
+
* @param fileContents - Map of file path to content
|
|
42
|
+
* @returns Map of file path to extracted symbols
|
|
43
|
+
*/
|
|
44
|
+
function extractSymbolsFromFiles(
|
|
45
|
+
changedFiles: string[],
|
|
46
|
+
fileContents: Map<string, string>
|
|
47
|
+
): Map<string, Symbol[]> {
|
|
48
|
+
const result = new Map<string, Symbol[]>();
|
|
49
|
+
|
|
50
|
+
for (const filePath of changedFiles) {
|
|
51
|
+
// Skip unsupported files
|
|
52
|
+
if (!isSupported(filePath)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const content = fileContents.get(filePath);
|
|
57
|
+
if (!content) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const symbols = extractSymbols(content, filePath);
|
|
62
|
+
if (symbols.length > 0) {
|
|
63
|
+
result.set(filePath, symbols);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find references for all symbols across files
|
|
72
|
+
*
|
|
73
|
+
* @param symbolsByFile - Map of file path to symbols
|
|
74
|
+
* @param cwd - Working directory
|
|
75
|
+
* @returns Map of symbol name to references (with reference count)
|
|
76
|
+
*/
|
|
77
|
+
async function findAllReferences(
|
|
78
|
+
symbolsByFile: Map<string, Symbol[]>,
|
|
79
|
+
cwd: string
|
|
80
|
+
): Promise<Map<string, { refs: Reference[]; count: number }>> {
|
|
81
|
+
const results = new Map<string, { refs: Reference[]; count: number }>();
|
|
82
|
+
const promises: Promise<void>[] = [];
|
|
83
|
+
|
|
84
|
+
for (const [filePath, symbols] of Array.from(symbolsByFile.entries())) {
|
|
85
|
+
for (const symbol of symbols) {
|
|
86
|
+
const promise = findReferences({
|
|
87
|
+
symbol: symbol.name,
|
|
88
|
+
definitionFile: filePath,
|
|
89
|
+
cwd,
|
|
90
|
+
limit: REFS_PER_SYMBOL,
|
|
91
|
+
}).then((refs) => {
|
|
92
|
+
const existing = results.get(symbol.name);
|
|
93
|
+
if (existing) {
|
|
94
|
+
// Merge refs and update count
|
|
95
|
+
existing.refs.push(...refs);
|
|
96
|
+
existing.count += refs.length;
|
|
97
|
+
} else {
|
|
98
|
+
results.set(symbol.name, { refs, count: refs.length });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
promises.push(promise);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await Promise.all(promises);
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Curate hints to max limit, prioritizing by reference frequency
|
|
111
|
+
*
|
|
112
|
+
* @param referenceMap - Map of symbol to references
|
|
113
|
+
* @param maxHints - Maximum hints to return
|
|
114
|
+
* @returns Array of curated context hints
|
|
115
|
+
*/
|
|
116
|
+
function curateHints(
|
|
117
|
+
referenceMap: Map<string, { refs: Reference[]; count: number }>,
|
|
118
|
+
maxHints: number
|
|
119
|
+
): ContextHint[] {
|
|
120
|
+
// Flatten all references with their symbol info
|
|
121
|
+
const allHints: ContextHint[] = [];
|
|
122
|
+
const seenFileLines = new Set<string>();
|
|
123
|
+
|
|
124
|
+
for (const [symbol, { refs, count }] of Array.from(referenceMap.entries())) {
|
|
125
|
+
for (const ref of refs) {
|
|
126
|
+
// Deduplicate by file:line
|
|
127
|
+
const key = `${ref.file}:${ref.line}`;
|
|
128
|
+
if (seenFileLines.has(key)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
seenFileLines.add(key);
|
|
132
|
+
|
|
133
|
+
allHints.push({
|
|
134
|
+
file: ref.file,
|
|
135
|
+
line: ref.line,
|
|
136
|
+
symbol,
|
|
137
|
+
refCount: count,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Sort by reference count (descending) - most referenced symbols first
|
|
143
|
+
allHints.sort((a, b) => b.refCount - a.refCount);
|
|
144
|
+
|
|
145
|
+
// Take top maxHints
|
|
146
|
+
return allHints.slice(0, maxHints);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Format hints as flowctl-compatible output
|
|
151
|
+
*
|
|
152
|
+
* Output format:
|
|
153
|
+
* ```
|
|
154
|
+
* Consider these related files:
|
|
155
|
+
* - src/auth.ts:15 - references validateToken
|
|
156
|
+
* - src/types.ts:42 - references User
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @param hints - Array of context hints
|
|
160
|
+
* @returns Formatted string matching flowctl gather_context_hints() output
|
|
161
|
+
*/
|
|
162
|
+
export function formatHints(hints: ContextHint[]): string {
|
|
163
|
+
if (hints.length === 0) {
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lines = ["Consider these related files:"];
|
|
168
|
+
|
|
169
|
+
for (const hint of hints) {
|
|
170
|
+
lines.push(`- ${hint.file}:${hint.line} - references ${hint.symbol}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate context hints from changed files
|
|
178
|
+
*
|
|
179
|
+
* Combines symbol extraction + reference finding to identify
|
|
180
|
+
* related files that may be affected by changes.
|
|
181
|
+
*
|
|
182
|
+
* @param options - Generation options
|
|
183
|
+
* @returns Array of context hints (max 15 by default)
|
|
184
|
+
*/
|
|
185
|
+
export async function generateContextHints(
|
|
186
|
+
options: GenerateHintsOptions
|
|
187
|
+
): Promise<ContextHint[]> {
|
|
188
|
+
const {
|
|
189
|
+
changedFiles,
|
|
190
|
+
fileContents,
|
|
191
|
+
cwd = process.cwd(),
|
|
192
|
+
maxHints = MAX_HINTS,
|
|
193
|
+
} = options;
|
|
194
|
+
|
|
195
|
+
// Step 1: Extract symbols from changed files
|
|
196
|
+
const symbolsByFile = extractSymbolsFromFiles(changedFiles, fileContents);
|
|
197
|
+
|
|
198
|
+
if (symbolsByFile.size === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Step 2: Find references to extracted symbols
|
|
203
|
+
const referenceMap = await findAllReferences(symbolsByFile, cwd);
|
|
204
|
+
|
|
205
|
+
// Step 3: Curate to max hints, prioritized by relevance
|
|
206
|
+
return curateHints(referenceMap, maxHints);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate formatted context hints string
|
|
211
|
+
*
|
|
212
|
+
* This is the main entry point for getting context hints in flowctl format.
|
|
213
|
+
*
|
|
214
|
+
* @param options - Generation options
|
|
215
|
+
* @returns Formatted hints string (empty if no hints found)
|
|
216
|
+
*/
|
|
217
|
+
export async function getFormattedContextHints(
|
|
218
|
+
options: GenerateHintsOptions
|
|
219
|
+
): Promise<string> {
|
|
220
|
+
const hints = await generateContextHints(options);
|
|
221
|
+
return formatHints(hints);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate context hints from git diff
|
|
226
|
+
*
|
|
227
|
+
* Convenience function that reads file contents and generates hints.
|
|
228
|
+
*
|
|
229
|
+
* @param changedFiles - List of changed file paths (relative to cwd)
|
|
230
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
231
|
+
* @param maxHints - Maximum hints to return (default: 15)
|
|
232
|
+
* @returns Formatted hints string
|
|
233
|
+
*/
|
|
234
|
+
export async function generateHintsFromDiff(
|
|
235
|
+
changedFiles: string[],
|
|
236
|
+
cwd: string = process.cwd(),
|
|
237
|
+
maxHints: number = MAX_HINTS
|
|
238
|
+
): Promise<string> {
|
|
239
|
+
const { join } = await import("path");
|
|
240
|
+
|
|
241
|
+
// Read file contents
|
|
242
|
+
const fileContents = new Map<string, string>();
|
|
243
|
+
|
|
244
|
+
for (const filePath of changedFiles) {
|
|
245
|
+
const fullPath = filePath.startsWith("/") ? filePath : join(cwd, filePath);
|
|
246
|
+
const file = Bun.file(fullPath);
|
|
247
|
+
|
|
248
|
+
if (await file.exists()) {
|
|
249
|
+
try {
|
|
250
|
+
const content = await file.text();
|
|
251
|
+
fileContents.set(filePath, content);
|
|
252
|
+
} catch {
|
|
253
|
+
// Skip files we can't read
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return getFormattedContextHints({
|
|
259
|
+
changedFiles,
|
|
260
|
+
fileContents,
|
|
261
|
+
cwd,
|
|
262
|
+
maxHints,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context module exports
|
|
3
|
+
*
|
|
4
|
+
* Provides symbol extraction, reference finding, and context hints
|
|
5
|
+
* generation for code reviews.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Symbol extraction
|
|
9
|
+
export {
|
|
10
|
+
extractSymbols,
|
|
11
|
+
extractSymbolsFromFile,
|
|
12
|
+
isSupported,
|
|
13
|
+
getSupportedExtensions,
|
|
14
|
+
type Symbol,
|
|
15
|
+
type SymbolType,
|
|
16
|
+
} from "./symbols";
|
|
17
|
+
|
|
18
|
+
// Reference finding
|
|
19
|
+
export {
|
|
20
|
+
findReferences,
|
|
21
|
+
findReferencesForSymbols,
|
|
22
|
+
formatReferences,
|
|
23
|
+
isGitRepository,
|
|
24
|
+
type Reference,
|
|
25
|
+
type FindReferencesOptions,
|
|
26
|
+
} from "./references";
|
|
27
|
+
|
|
28
|
+
// Context hints generation
|
|
29
|
+
export {
|
|
30
|
+
generateContextHints,
|
|
31
|
+
getFormattedContextHints,
|
|
32
|
+
generateHintsFromDiff,
|
|
33
|
+
formatHints,
|
|
34
|
+
type ContextHint,
|
|
35
|
+
type GenerateHintsOptions,
|
|
36
|
+
} from "./hints";
|
|
37
|
+
|
|
38
|
+
// Re-review cache-busting
|
|
39
|
+
export {
|
|
40
|
+
buildReReviewPreamble,
|
|
41
|
+
getChangedFiles,
|
|
42
|
+
detectReReview,
|
|
43
|
+
recordReview,
|
|
44
|
+
getPreviousReviewState,
|
|
45
|
+
clearReviewState,
|
|
46
|
+
processReReview,
|
|
47
|
+
type ReReviewOptions,
|
|
48
|
+
} from "./rereview";
|