@verevoir/review 0.2.0 → 0.3.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.
Files changed (53) hide show
  1. package/README.md +132 -2
  2. package/dist/cli/bin.d.ts +2 -0
  3. package/dist/cli/bin.js +46 -0
  4. package/dist/cli/footer.d.ts +9 -0
  5. package/dist/cli/footer.js +59 -0
  6. package/dist/cli/review.d.ts +45 -0
  7. package/dist/cli/review.js +507 -0
  8. package/dist/context-assembler.d.ts +9 -0
  9. package/dist/context-assembler.js +118 -0
  10. package/dist/context-files.d.ts +33 -0
  11. package/dist/context-files.js +174 -0
  12. package/dist/context-types.d.ts +28 -0
  13. package/dist/context-types.js +5 -0
  14. package/dist/dedup.d.ts +40 -0
  15. package/dist/dedup.js +151 -0
  16. package/dist/diff-hunks.d.ts +17 -0
  17. package/dist/diff-hunks.js +73 -0
  18. package/dist/fs-source-access.d.ts +7 -0
  19. package/dist/fs-source-access.js +39 -0
  20. package/dist/parseReply.js +58 -7
  21. package/dist/practice-lenses.d.ts +69 -0
  22. package/dist/practice-lenses.js +218 -0
  23. package/dist/prompt.d.ts +23 -0
  24. package/dist/prompt.js +18 -0
  25. package/dist/references.d.ts +44 -0
  26. package/dist/references.js +312 -0
  27. package/dist/report.d.ts +17 -1
  28. package/dist/report.js +56 -3
  29. package/dist/run-review.d.ts +16 -0
  30. package/dist/run-review.js +37 -2
  31. package/dist/source-access.d.ts +8 -0
  32. package/dist/source-access.js +1 -0
  33. package/dist/subject-context-render.d.ts +8 -0
  34. package/dist/subject-context-render.js +29 -0
  35. package/dist/tier.d.ts +20 -0
  36. package/dist/tier.js +88 -0
  37. package/dist/warm-first.d.ts +2 -0
  38. package/dist/warm-first.js +21 -0
  39. package/lenses.default.json +30 -0
  40. package/package.json +5 -1
  41. package/src/cli/bin.ts +61 -0
  42. package/src/cli/footer.ts +78 -0
  43. package/src/cli/review.ts +647 -0
  44. package/src/context-files.ts +194 -0
  45. package/src/dedup.ts +187 -0
  46. package/src/parseReply.ts +58 -7
  47. package/src/practice-lenses.ts +265 -0
  48. package/src/prompt.ts +44 -0
  49. package/src/references.ts +381 -0
  50. package/src/report.ts +71 -3
  51. package/src/run-review.ts +55 -7
  52. package/src/tier.ts +103 -0
  53. package/src/warm-first.ts +27 -0
@@ -0,0 +1,2 @@
1
+ import type { Reviewer } from './panel.js';
2
+ export declare function warmFirst(reviewer: Reviewer): Reviewer;
@@ -0,0 +1,21 @@
1
+ // Cache warm-up for the fan. runPanel dispatches every lens at once, so with a
2
+ // provider prompt cache each lens writes the same shared prefix and none reads
3
+ // it. `warmFirst` wraps a Reviewer so the FIRST call made runs alone, and every
4
+ // later call waits for it to settle (success or failure) before starting: the
5
+ // rest then read the prefix the first one wrote.
6
+ //
7
+ // The seam is the Reviewer itself, so runPanel is untouched. The first caller
8
+ // is whichever lens runPanel invokes first (the first lens in the set); the
9
+ // check-and-set below runs synchronously within that first call, before any
10
+ // await, so no later call can also believe it is first.
11
+ export function warmFirst(reviewer) {
12
+ let first;
13
+ return (lensId, prompt) => {
14
+ if (first === undefined) {
15
+ const call = reviewer(lensId, prompt);
16
+ first = call.then(() => undefined, () => undefined);
17
+ return call;
18
+ }
19
+ return first.then(() => reviewer(lensId, prompt));
20
+ };
21
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "version": 1,
3
+ "lenses": [
4
+ {
5
+ "id": "correctness",
6
+ "title": "Correctness",
7
+ "focus": "Does the change do what the diff, its comments and its description say it does? Read the claim and the code as two independent assertions and look for the gap between them. Prefer a concrete failing input over a description of a risk. This dimension judges work performed against a claim about it, not a description of a requirement: where the change under review is itself a description of a requirement, there is no behaviour yet for this lens to check — answer out_of_scope rather than manufacturing a gap between a description and work that does not exist yet."
8
+ },
9
+ {
10
+ "id": "honesty",
11
+ "title": "Honesty of the record",
12
+ "focus": "Anything that reports a state it did not observe: a comment claiming a property the code does not have, a log line or return value describing work that did not happen, a success reported over an empty result, documentation asserting a behaviour the implementation lacks. This still applies in full where a description of a requirement makes a false claim about the system's CURRENT state — that is exactly as dishonest there as anywhere else. But a description of a requirement is not dishonest merely for reporting no success — the absence of a completion claim is not itself a finding, and must not be written up as one."
13
+ },
14
+ {
15
+ "id": "failure-modes",
16
+ "title": "Failure distinguishable from silence",
17
+ "focus": "Where the change can fail, can a reader tell which failure happened? Missing, empty and broken must stay three answers. A swallowed error, a catch that returns a default, or a single word covering two outcomes belongs here. This judges error handling that exists in the change. A description of a requirement has no catch, return or logged outcome yet — answer out_of_scope, not a review of prose about intended handling as though it were the handling itself."
18
+ },
19
+ {
20
+ "id": "verification",
21
+ "title": "Verification that verifies",
22
+ "focus": "Would each new or changed test fail if the code it covers were wrong? Look for assertions that hold vacuously, suites that pass because they found nothing to run, and guards whose only test exercises the pure function underneath rather than the guard. This judges a test that exists in the change. A description of a requirement — including one describing what a future test must prove — has no test yet for this lens to check against; answer out_of_scope, not a verdict on a stated intention."
23
+ },
24
+ {
25
+ "id": "blast-radius",
26
+ "title": "Scope, collateral and credentials",
27
+ "focus": "What does this change touch that it did not need to touch? Reformatting, drive-by renames, widened permissions, a credential reaching a wider process than before, or a bound that moved without being named."
28
+ }
29
+ ]
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verevoir/review",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The review panel: lens-based diff review. One implementation, consumed by pump-internal review, local preflight, eval-review, and CI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -12,8 +12,12 @@
12
12
  "import": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "bin": {
16
+ "aigency-review": "./dist/cli/bin.js"
17
+ },
15
18
  "files": [
16
19
  "dist",
20
+ "lenses.default.json",
17
21
  "src",
18
22
  "tsconfig.json",
19
23
  "tsconfig.build.json",
package/src/cli/bin.ts ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ // The process boundary for `aigency-review`: construct the real adapters and
4
+ // hand them to `main` (review.ts). No decision lives here.
5
+
6
+ import { execFile } from 'node:child_process';
7
+ import { appendFile, readFile, readdir, realpath, stat } from 'node:fs/promises';
8
+ import process from 'node:process';
9
+ import { promisify } from 'node:util';
10
+
11
+ import { main } from './review.js';
12
+
13
+ const execFileAsync = promisify(execFile);
14
+
15
+ async function readAll(stream: NodeJS.ReadableStream): Promise<string> {
16
+ const chunks: string[] = [];
17
+ stream.setEncoding('utf8');
18
+ for await (const chunk of stream) chunks.push(chunk as string);
19
+ return chunks.join('');
20
+ }
21
+
22
+ main(
23
+ process.argv.slice(2),
24
+ process.env,
25
+ {
26
+ readStdin: () => readAll(process.stdin),
27
+ stdout: (text) => void process.stdout.write(text),
28
+ stderr: (text) => void process.stderr.write(text),
29
+ readFile: (path) => readFile(path, 'utf8'),
30
+ appendFile: (path, text) => appendFile(path, text, 'utf8'),
31
+ readDir: async (path) =>
32
+ (await readdir(path, { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name),
33
+ listDir: async (path) =>
34
+ (await readdir(path, { withFileTypes: true })).map((e) => ({
35
+ name: e.name,
36
+ isDir: e.isDirectory(),
37
+ })),
38
+ realpath: (path) => realpath(path),
39
+ statFile: async (path) => {
40
+ const s = await stat(path);
41
+ return { size: s.size, isFile: s.isFile() };
42
+ },
43
+ },
44
+ {
45
+ // execFile, no shell: a ref is an argument, never parsed as a command.
46
+ git: async (args) =>
47
+ (await execFileAsync('git', [...args], { maxBuffer: 512 * 1024 * 1024 })).stdout,
48
+ // dist/cli/bin.js and src/cli/bin.ts both sit two levels under the package root.
49
+ defaultLenses: () => readFile(new URL('../../lenses.default.json', import.meta.url), 'utf8'),
50
+ },
51
+ ).then(
52
+ (code) => {
53
+ process.exitCode = code;
54
+ },
55
+ (cause: unknown) => {
56
+ const message = cause instanceof Error ? cause.message : String(cause);
57
+ process.stderr.write('RESULT: could_not_run\n');
58
+ process.stderr.write(`Unexpected failure: ${message}\n`);
59
+ process.exitCode = 1;
60
+ },
61
+ );
@@ -0,0 +1,78 @@
1
+ // What judged a review: the model, the route (by NAME only -- never a key or a
2
+ // path), and per-lens token usage and wall time, from runReviewNode's `onCall`
3
+ // records. Pure formatting.
4
+
5
+ import type { RawCall } from '../raw-call.js';
6
+
7
+ export type RouteName = 'env' | 'key-file' | 'claude-cli';
8
+
9
+ function seconds(ms: number): string {
10
+ return `${(ms / 1000).toFixed(1)}s`;
11
+ }
12
+
13
+ function byLens(calls: readonly RawCall[]): RawCall[] {
14
+ return calls.slice().sort((a, b) => (a.lens < b.lens ? -1 : a.lens > b.lens ? 1 : 0));
15
+ }
16
+
17
+ function tokens(call: RawCall): string {
18
+ const u = call.usage;
19
+ if (u === undefined) return 'usage not reported';
20
+ const cache =
21
+ u.cacheReadInputTokens > 0 || u.cacheCreationInputTokens > 0
22
+ ? ` (cache read ${String(u.cacheReadInputTokens)}, cache write ` +
23
+ `${String(u.cacheCreationInputTokens)})`
24
+ : '';
25
+ return `${String(u.inputTokens)} input / ${String(u.outputTokens)} output tokens${cache}`;
26
+ }
27
+
28
+ /** The model(s) the transport reported, else the requested one, said as such. */
29
+ export function modelText(requested: string, calls: readonly RawCall[]): string {
30
+ const resolved = [
31
+ ...new Set(calls.map((c) => c.resolved).filter((m): m is string => m !== null)),
32
+ ].sort();
33
+ if (resolved.length === 0) {
34
+ return `model \`${requested}\` (as requested; the transport reported none)`;
35
+ }
36
+ const names = resolved.map((m) => `\`${m}\``).join(', ');
37
+ return resolved.length === 1 && resolved[0] === requested
38
+ ? `model ${names}`
39
+ : `model ${names} (requested \`${requested}\`)`;
40
+ }
41
+
42
+ /** The one line saying how many [blocking] findings tier enforcement downgraded; none for 0. */
43
+ export function tierLine(downgraded: number): string | undefined {
44
+ return downgraded > 0
45
+ ? `Tier: ${String(downgraded)} blocking finding(s) downgraded: no Reject/Expect signal quoted`
46
+ : undefined;
47
+ }
48
+
49
+ export function renderFooter(
50
+ requested: string,
51
+ route: RouteName,
52
+ calls: readonly RawCall[],
53
+ downgraded = 0,
54
+ ): string {
55
+ const lines = ['---', '', `Judged with ${modelText(requested, calls)}, route: ${route}.`];
56
+ const tier = tierLine(downgraded);
57
+ if (tier !== undefined) lines.push('', tier);
58
+ if (calls.length > 0) {
59
+ lines.push('', 'Per-lens usage and wall time:', '');
60
+ for (const call of byLens(calls)) {
61
+ lines.push(
62
+ call.failure === undefined
63
+ ? `- \`${call.lens}\`: ${tokens(call)}, ${seconds(call.durationMs)}`
64
+ : `- \`${call.lens}\`: failed after ${seconds(call.durationMs)}`,
65
+ );
66
+ }
67
+ }
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /** One stderr line per lens. */
72
+ export function usageLines(calls: readonly RawCall[]): string[] {
73
+ return byLens(calls).map((call) =>
74
+ call.failure === undefined
75
+ ? `lens ${call.lens}: ${tokens(call)}, ${seconds(call.durationMs)}\n`
76
+ : `lens ${call.lens}: failed after ${seconds(call.durationMs)}\n`,
77
+ );
78
+ }