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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdyt",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "description": "Code review context builder for LLMs - what do you think?",
6
6
  "license": "MIT",
@@ -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
- const prompt = payload.message || tab.prompt;
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: { id: chatId, path: chatPath, review: response },
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: { id: chatId, path: chatPath },
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";