@dreki-gg/pi-code-reviewer 0.1.0 → 0.2.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/README.md CHANGED
@@ -50,8 +50,8 @@ Evaluates changes for correctness and adherence to project standards.
50
50
  - Does the change follow naming conventions?
51
51
 
52
52
  ## Tools
53
- - `bun run typecheck`
54
- - `bun run lint`
53
+ - `npm run typecheck`
54
+ - `npm run lint`
55
55
 
56
56
  ## Severity
57
57
  - blocker: Type errors, unresolved imports
@@ -67,8 +67,8 @@ The package ships with four example lenses:
67
67
  | --- | --- |
68
68
  | `code-quality` | Correctness, lint, types, dead code |
69
69
  | `maintainability` | Coupling, complexity, readability |
70
- | `product-vision` | Alignment with product direction |
71
- | `ux-design` | Accessibility, responsiveness, interaction quality |
70
+ | `product-vision` | Traces changes back to their originating issue or design doc, checks goal alignment |
71
+ | `accessibility` | Semantic HTML, keyboard navigation, ARIA, screen reader compatibility |
72
72
 
73
73
  Run `/review-init` to scaffold these (customized for your project's tools) into `.code-review/lenses/`.
74
74
 
@@ -38,7 +38,7 @@ export function registerReviewTool(pi: ExtensionAPI) {
38
38
  ),
39
39
  }),
40
40
 
41
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
41
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
42
42
  const cwd = ctx.cwd;
43
43
  const config = loadConfig(cwd);
44
44
  const lensDir = getLensDir(cwd, config);
@@ -58,12 +58,14 @@ export function registerReviewTool(pi: ExtensionAPI) {
58
58
 
59
59
  const lensNames = resolveLensNames(params.lenses, config, available);
60
60
 
61
+ ctx.ui.setStatus('code-review', '🔍 Collecting diff...');
61
62
  const diff = await collectDiff(pi, cwd, {
62
63
  base: params.base,
63
64
  staged: params.staged,
64
65
  });
65
66
 
66
67
  if (!diff.diff.trim()) {
68
+ ctx.ui.setStatus('code-review', undefined);
67
69
  return {
68
70
  content: [{ type: 'text', text: 'No changes to review.' }],
69
71
  details: {},
@@ -71,15 +73,25 @@ export function registerReviewTool(pi: ExtensionAPI) {
71
73
  }
72
74
 
73
75
  const results: LensResult[] = [];
74
- for (const name of lensNames) {
76
+ for (let i = 0; i < lensNames.length; i++) {
75
77
  if (signal?.aborted) break;
76
78
 
79
+ const name = lensNames[i];
80
+ const progressMsg = `Lens ${i + 1}/${lensNames.length}: ${name}`;
81
+ ctx.ui.setStatus('code-review', `🔍 ${progressMsg}`);
82
+ onUpdate?.({
83
+ content: [{ type: 'text', text: progressMsg }],
84
+ details: { currentLens: name, lensIndex: i + 1, totalLenses: lensNames.length },
85
+ });
86
+
77
87
  const lens = available.get(name)!;
78
88
  const content = getLensContent(lensDir, name) ?? '';
79
89
  const result = await reviewWithLens(pi, ctx, cwd, lens, content, diff, signal);
80
90
  results.push(result);
81
91
  }
82
92
 
93
+ ctx.ui.setStatus('code-review', undefined);
94
+
83
95
  const report: ReviewReport = {
84
96
  diff: diff.diff,
85
97
  diffStat: diff.stat,
@@ -3,7 +3,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
3
  import { loadConfig, getLensDir } from '../config';
4
4
  import { collectDiff } from '../diff';
5
5
  import { discoverLenses, getLensContent } from '../lenses';
6
- import { reviewWithLens } from '../reviewer';
6
+ import { reviewWithLens, buildDiffSection } from '../reviewer';
7
7
  import { parseReviewArgs } from '../parse-args';
8
8
 
9
9
  export function registerReviewCommand(pi: ExtensionAPI) {
@@ -34,36 +34,61 @@ export function registerReviewCommand(pi: ExtensionAPI) {
34
34
  return;
35
35
  }
36
36
 
37
+ ctx.ui.setStatus('code-review', '🔍 Collecting diff...');
37
38
  const diff = await collectDiff(pi, cwd, {
38
39
  base: parsed.base,
39
40
  staged: parsed.staged,
40
41
  });
41
42
 
42
43
  if (!diff.diff.trim()) {
44
+ ctx.ui.setStatus('code-review', undefined);
43
45
  ctx.ui.notify('No changes to review', 'info');
44
46
  return;
45
47
  }
46
48
 
47
49
  ctx.ui.notify(`Reviewing ${diff.label} through ${lensNames.length} lens(es)...`, 'info');
50
+ ctx.ui.setStatus('code-review', `🔍 Reviewing (0/${lensNames.length})...`);
51
+
52
+ const lensSections: string[] = [];
53
+ for (let i = 0; i < lensNames.length; i++) {
54
+ const name = lensNames[i];
55
+ ctx.ui.setStatus('code-review', `🔍 Lens ${i + 1}/${lensNames.length}: ${name}`);
48
56
 
49
- const lensPrompts: string[] = [];
50
- for (const name of lensNames) {
51
57
  const lens = available.get(name)!;
52
58
  const content = getLensContent(lensDir, name) ?? '';
53
59
  const result = await reviewWithLens(pi, ctx, cwd, lens, content, diff);
54
60
 
55
- if (result._prompt) {
56
- lensPrompts.push(result._prompt);
61
+ if (result._lensSection) {
62
+ lensSections.push(result._lensSection);
57
63
  }
58
64
  }
59
65
 
66
+ ctx.ui.setStatus('code-review', undefined);
67
+
60
68
  const combinedPrompt = [
61
69
  `Review the following changes through ${lensNames.length} lens(es): ${lensNames.join(', ')}.`,
62
70
  '',
63
71
  'For each lens, evaluate the diff against its criteria and produce findings.',
64
72
  'Output your review as a structured report with sections per lens.',
65
73
  '',
66
- ...lensPrompts,
74
+ buildDiffSection(diff),
75
+ '',
76
+ '## Lenses',
77
+ '',
78
+ ...lensSections,
79
+ '',
80
+ '## Instructions',
81
+ '',
82
+ 'For each lens above, review the diff and output a JSON array of findings:',
83
+ '',
84
+ '```json',
85
+ '[',
86
+ ' { "file": "path/to/file.ts", "line": 42, "severity": "warning", "message": "Description" }',
87
+ ']',
88
+ '```',
89
+ '',
90
+ 'After each lens JSON array, write a 2-3 sentence summary.',
91
+ 'If there are no findings for a lens, return an empty array `[]` and note the code looks good.',
67
92
  ].join('\n');
68
93
 
69
94
  pi.sendUserMessage(combinedPrompt, { deliverAs: 'followUp' });
@@ -33,23 +33,13 @@ async function runLensTools(
33
33
  return outputs;
34
34
  }
35
35
 
36
- /** Build the review prompt for a single lens. */
37
- function buildReviewPrompt(
38
- lens: LensConfig,
39
- lensContent: string,
40
- diff: DiffSource,
41
- toolOutputs: Record<string, string>,
42
- ): string {
36
+ /** Build the shared diff section of the review prompt (included once). */
37
+ export function buildDiffSection(diff: DiffSource): string {
43
38
  const parts: string[] = [];
44
-
45
- parts.push(`You are reviewing code changes through the "${lens.name}" lens.`);
46
- parts.push('');
47
- parts.push('## Lens Definition');
48
- parts.push(lensContent);
49
- parts.push('');
50
- parts.push(`## Diff (${diff.label})`);
51
39
  const maxDiffLen = 50_000;
52
40
  const diffTruncated = diff.diff.length > maxDiffLen;
41
+
42
+ parts.push(`## Diff (${diff.label})`);
53
43
  parts.push('```diff');
54
44
  parts.push(diff.diff.slice(0, maxDiffLen));
55
45
  parts.push('```');
@@ -64,21 +54,60 @@ function buildReviewPrompt(
64
54
  parts.push(diff.stat);
65
55
  parts.push('```');
66
56
 
57
+ return parts.join('\n');
58
+ }
59
+
60
+ /** Build the lens-specific section of the review prompt (no diff duplication). */
61
+ function buildLensSection(
62
+ lens: LensConfig,
63
+ lensContent: string,
64
+ toolOutputs: Record<string, string>,
65
+ ): string {
66
+ const parts: string[] = [];
67
+
68
+ parts.push(`### Lens: ${lens.name}`);
69
+ parts.push('');
70
+ parts.push('#### Lens Definition');
71
+ parts.push(lensContent);
72
+
67
73
  if (Object.keys(toolOutputs).length > 0) {
68
74
  parts.push('');
69
- parts.push('## Tool Outputs');
75
+ parts.push('#### Tool Outputs');
70
76
  for (const [cmd, output] of Object.entries(toolOutputs)) {
71
- parts.push(`### \`${cmd}\``);
77
+ parts.push(`##### \`${cmd}\``);
72
78
  parts.push('```');
73
79
  parts.push(output.slice(0, 20_000));
74
80
  parts.push('```');
75
81
  }
76
82
  }
77
83
 
84
+ parts.push('');
85
+ parts.push('#### Severity levels');
86
+ if (lens.severityRules.blocker) parts.push(`- **blocker**: ${lens.severityRules.blocker}`);
87
+ if (lens.severityRules.warning) parts.push(`- **warning**: ${lens.severityRules.warning}`);
88
+ if (lens.severityRules.note) parts.push(`- **note**: ${lens.severityRules.note}`);
89
+
90
+ return parts.join('\n');
91
+ }
92
+
93
+ /** Build the full review prompt for a single lens (includes diff — used by the tool path). */
94
+ function buildReviewPrompt(
95
+ lens: LensConfig,
96
+ lensContent: string,
97
+ diff: DiffSource,
98
+ toolOutputs: Record<string, string>,
99
+ ): string {
100
+ const parts: string[] = [];
101
+
102
+ parts.push(`You are reviewing code changes through the "${lens.name}" lens.`);
103
+ parts.push('');
104
+ parts.push(buildDiffSection(diff));
105
+ parts.push('');
106
+ parts.push(buildLensSection(lens, lensContent, toolOutputs));
78
107
  parts.push('');
79
108
  parts.push('## Instructions');
80
109
  parts.push('');
81
- parts.push('Review the diff through this lens. For each finding, output a JSON array:');
110
+ parts.push('Review the diff above through this lens. For each finding, output a JSON array:');
82
111
  parts.push('');
83
112
  parts.push('```json');
84
113
  parts.push('[');
@@ -88,11 +117,6 @@ function buildReviewPrompt(
88
117
  parts.push(']');
89
118
  parts.push('```');
90
119
  parts.push('');
91
- parts.push('Severity levels:');
92
- if (lens.severityRules.blocker) parts.push(`- **blocker**: ${lens.severityRules.blocker}`);
93
- if (lens.severityRules.warning) parts.push(`- **warning**: ${lens.severityRules.warning}`);
94
- if (lens.severityRules.note) parts.push(`- **note**: ${lens.severityRules.note}`);
95
- parts.push('');
96
120
  parts.push(
97
121
  'After the JSON array, write a 2-3 sentence summary of your review through this lens.',
98
122
  );
@@ -116,15 +140,14 @@ export async function reviewWithLens(
116
140
 
117
141
  // Build the prompt
118
142
  const prompt = buildReviewPrompt(lens, lensContent, diff, toolOutputs);
143
+ const lensSection = buildLensSection(lens, lensContent, toolOutputs);
119
144
 
120
- // Use sendMessage to delegate to the agent for review
121
- // For now, we return the prompt and let the command handler
122
- // pass it to a subagent via the subagent tool
123
145
  return {
124
146
  lens: lens.name,
125
147
  findings: [],
126
148
  summary: '',
127
149
  toolOutputs,
128
150
  _prompt: prompt,
151
+ _lensSection: lensSection,
129
152
  };
130
153
  }
@@ -22,6 +22,8 @@ export type LensResult = {
22
22
  toolOutputs?: Record<string, string>;
23
23
  /** Review prompt built for this lens, used internally to delegate to the agent. */
24
24
  _prompt?: string;
25
+ /** Lens-specific section (without diff), used by /review command to avoid diff duplication. */
26
+ _lensSection?: string;
25
27
  };
26
28
 
27
29
  export type ReviewConfig = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-code-reviewer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Multi-lens code review extension for pi — configurable review criteria per project",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-review
3
- description: Multi-lens code review against working directory changes. Evaluates diffs through configurable criteria like code-quality, maintainability, product-vision, and ux-design. Use when user says "review my changes", "review this code", "check before committing", or "code review".
3
+ description: Multi-lens code review against working directory changes. Evaluates diffs through configurable criteria like code-quality, maintainability, product-vision, and accessibility. Use when user says "review my changes", "review this code", "check before committing", or "code review".
4
4
  ---
5
5
 
6
6
  # Code Review
@@ -36,8 +36,8 @@ Description of what this lens evaluates.
36
36
  - Evaluation point 2
37
37
 
38
38
  ## Tools
39
- - `bun run typecheck`
40
- - `bun run codeql:fallow -- --changed-since HEAD`
39
+ - `npm run typecheck`
40
+ - `npm run lint`
41
41
 
42
42
  ## Severity
43
43
  - blocker: What constitutes a blocking issue
@@ -0,0 +1,20 @@
1
+ # Accessibility
2
+
3
+ Evaluates UI changes for accessibility compliance — semantic markup, keyboard support, screen reader compatibility, and inclusive design patterns.
4
+
5
+ ## Criteria
6
+ - Are interactive elements accessible via keyboard (tab order, Enter/Space activation, Escape to dismiss)?
7
+ - Do custom components use appropriate ARIA roles, states, and properties?
8
+ - Are images and icons accompanied by meaningful alt text or aria-label?
9
+ - Are form inputs associated with visible labels (not just placeholder text)?
10
+ - Does the change use semantic HTML elements (button, nav, main, dialog) instead of generic divs with click handlers?
11
+ - Is focus managed correctly after dynamic content changes (modals, route transitions, toast notifications)?
12
+ - Are error and validation messages announced to screen readers (aria-live, role="alert")?
13
+ - Does the color usage rely solely on color to convey meaning, or is there a secondary indicator (icon, text)?
14
+
15
+ ## Tools
16
+
17
+ ## Severity
18
+ - blocker: Interactive elements unreachable by keyboard, missing labels on form inputs, click handlers on non-interactive elements without ARIA roles
19
+ - warning: Missing alt text, placeholder-only labels, missing aria-live on dynamic content, focus not managed after modals
20
+ - note: Semantic HTML improvements, ARIA attribute refinements, color contrast suggestions
@@ -1,19 +1,19 @@
1
1
  # Product Vision
2
2
 
3
- Evaluates whether changes align with the product direction and serve real user needs.
3
+ Evaluates whether changes fulfill the intent behind the work traces the diff back to its originating issue, PR description, or design doc and checks alignment.
4
4
 
5
5
  ## Criteria
6
- - Does this change serve existing users or is it speculative?
7
- - Does it introduce UI concepts or flows that don't exist elsewhere in the app?
8
- - Does it add unnecessary complexity to user-facing functionality?
9
- - Does it respect the established information architecture?
10
- - Is the feature discoverable and intuitive?
11
- - Does it follow the project's domain language (check CONTEXT.md if it exists)?
12
- - Are there simpler alternatives that would achieve the same user goal?
6
+ - Is there a linked issue, PR description, or design document that explains why this change was requested?
7
+ - Does the diff actually accomplish the stated goal, or does it drift into unrelated concerns?
8
+ - Are there parts of the requirement left unaddressed by the change?
9
+ - Does the change introduce scope creep beyond what was requested?
10
+ - Does it follow the project's domain language (check CONTEXT.md, glossary, or naming conventions)?
11
+ - Are user-facing strings consistent with the existing product vocabulary?
12
+ - If the change modifies a public API or user flow, is that justified by the original request?
13
13
 
14
14
  ## Tools
15
15
 
16
16
  ## Severity
17
- - blocker: Changes that contradict the product direction or break existing user workflows
18
- - warning: Speculative features, unnecessary complexity, inconsistent UX patterns
19
- - note: Opportunities to better align with product vision
17
+ - blocker: Change does not accomplish the stated goal, or contradicts the originating requirement
18
+ - warning: Scope creep beyond the request, unaddressed requirements, inconsistent domain language
19
+ - note: Opportunities to better align naming or structure with the stated intent
@@ -1,20 +0,0 @@
1
- # UX Design
2
-
3
- Evaluates the user experience quality of UI changes — accessibility, responsiveness, and interaction design.
4
-
5
- ## Criteria
6
- - Are interactive elements accessible (keyboard navigation, ARIA labels, focus management)?
7
- - Are loading and error states handled visually?
8
- - Is the layout responsive or does it break at common viewport sizes?
9
- - Are animations purposeful and not distracting (100-200ms, no bouncy effects)?
10
- - Is text readable (minimum 12px body, sufficient contrast)?
11
- - Are form inputs properly labeled with clear validation feedback?
12
- - Does the change follow the project's existing component patterns?
13
- - Are click targets large enough for touch (minimum 44px)?
14
-
15
- ## Tools
16
-
17
- ## Severity
18
- - blocker: Missing keyboard accessibility on interactive elements, broken layout, no error states on data-fetching UI
19
- - warning: Missing ARIA labels, tiny text (<12px), missing loading states, non-responsive layout
20
- - note: Animation polish, spacing consistency, component reuse opportunities