Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/api",
"version": "0.10.37",
"version": "0.10.38",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@naturalcycles/diffity",
"version": "0.10.37",
"version": "0.10.38",
"description": "Agent-agnostic, GitHub-style diff viewer and code review tool with a live agent loop",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/git",
"version": "0.10.37",
"version": "0.10.38",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/github/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/github",
"version": "0.10.37",
"version": "0.10.38",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/parser",
"version": "0.10.37",
"version": "0.10.38",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/ui",
"version": "0.10.37",
"version": "0.10.38",
"type": "module",
"private": true,
"scripts": {
Expand Down
17 changes: 17 additions & 0 deletions scripts/inbox-compare.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest';
import type { BundleThread } from '@diffity/api';
import type { PrSnapshot } from '@diffity/github';
import {
UsageError,
atHead,
bundleNamesFor,
candidateLabel,
compareFindings,
Expand Down Expand Up @@ -384,3 +386,18 @@ describe('the JSON shape', () => {
});
});
});

describe('atHead', () => {
const now = {
owner: 'o', repo: 'r', number: 7, headSha: 'newest', body: 'rewritten since',
checks: [{ name: 'test', status: 'success' }], additions: 90, deletions: 9, changedFiles: 6,
} as PrSnapshot;

it('describes the pinned head, not the newest, and drops the description', () => {
const facts = { checks: [{ name: 'test', status: 'failure' as const }], additions: 40, deletions: 2, changedFiles: 3 };

expect(atHead(now, 'pinned', facts)).toMatchObject({
owner: 'o', repo: 'r', number: 7, headSha: 'pinned', body: '', ...facts,
});
});
});
61 changes: 53 additions & 8 deletions scripts/inbox-compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
* what it costs. One agent run per invocation; everything it writes stays in a scratch directory.
*/

import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { GENERAL_THREAD_FILE_PATH, parseReviewBundle, type BundleThread, type ReviewBundle } from '@diffity/api';
import { viewPr } from '@diffity/github';
import { parseChecks, viewPr, type PrSnapshot } from '@diffity/github';
import type { RunStats } from '../packages/cli/src/inbox/agent-output.js';
import { DEFAULT_INBOX_CONFIG, expandHome, type InboxConfig } from '../packages/cli/src/inbox/config.js';
import { preparePr, type PrepareResult } from '../packages/cli/src/inbox/prepare.js';
import { realPrepareDeps } from '../packages/cli/src/inbox/runtime.js';
import { realPrepareDeps, startDiffityServer } from '../packages/cli/src/inbox/runtime.js';
import { severityOf } from '../packages/cli/src/inbox/summary.js';
import { cloneDir, removeWorktree } from '../packages/cli/src/inbox/worktree.js';

Expand Down Expand Up @@ -384,6 +385,44 @@ function readBaseline(options: Options): { path: string; bundle: ReviewBundle }
return newest;
}

/** What GitHub says about one commit of a pull request, as opposed to the pull request as it is now. */
export interface HeadFacts {
checks: PrSnapshot['checks'];
additions: number;
deletions: number;
changedFiles: number;
}

/**
* The pull request as a reviewer saw it at `head`. `gh pr view` describes it as it is now: the
* newest head's CI and size, and a description that may have been rewritten since. The
* description is dropped rather than guessed at, which also keeps runs comparable with the ones
* made before the prompt carried it.
*/
export function atHead(snapshot: PrSnapshot, head: string, facts: HeadFacts): PrSnapshot {
return { ...snapshot, headSha: head, body: '', ...facts };
}

function ghJson(args: string[]): unknown {
return JSON.parse(execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }));
}

function headFacts(snapshot: PrSnapshot, head: string): HeadFacts {
const base = `repos/${snapshot.owner}/${snapshot.repo}`;
const runs = ghJson(['api', `${base}/commits/${head}/check-runs?per_page=100`]) as { check_runs?: unknown[] };
const statuses = ghJson(['api', `${base}/commits/${head}/status`]) as { statuses?: unknown[] };
const compare = ghJson(['api', `${base}/compare/${snapshot.baseRef}...${head}`]) as {
files?: { additions?: number; deletions?: number }[];
};
const files = compare.files ?? [];
return {
checks: parseChecks([...(runs.check_runs ?? []), ...(statuses.statuses ?? [])]),
additions: files.reduce((sum, file) => sum + (file.additions ?? 0), 0),
deletions: files.reduce((sum, file) => sum + (file.deletions ?? 0), 0),
changedFiles: files.length,
};
}

function prName(ref: PrRefSpec): string {
return `${ref.owner}/${ref.repo}#${ref.number}`;
}
Expand Down Expand Up @@ -445,19 +484,25 @@ async function main(): Promise<number> {
validate: { ...DEFAULT_INBOX_CONFIG.validate, model: null },
};
const candidate = candidateLabel(options.model, options.effort);
const deps = realPrepareDeps(
process.execPath, entry,
worktree => join(scratch, 'data', basename(worktree)),
config, message => console.error(` ${message}`),
);
const dataDirFor = (worktree: string) => join(scratch, 'data', basename(worktree));
const deps = {
...realPrepareDeps(process.execPath, entry, dataDirFor, config, message => console.error(` ${message}`)),
// Comparisons run back to back on a busy machine, where a large clone can take longer than the
// daemon's 30s to serve; a missed start would be scored as the candidate failing.
startServer: (worktree: string, diffRef: string) =>
startDiffityServer(process.execPath, entry, worktree, diffRef, dataDirFor(worktree), 120_000),
// The discussion as it stands now holds the reviews of this very head, the baseline's findings
// among them, so a candidate given it would be told what it is being scored on finding.
prContext: async () => null,
};

console.error(`🤖 preparing ${prName(options.ref)} at ${short} with ${candidate} — agent running…`);
const startedAt = Date.now();
const elapsed = () => (Date.now() - startedAt) / 60_000;
const ticker = setInterval(() => console.error(` … ${elapsed().toFixed(0)} min`), 60_000);
let result: PrepareResult;
try {
result = await preparePr({ ...snapshot, headSha: head }, config, deps, { bumped: true, pinHead: head });
result = await preparePr(atHead(snapshot, head, headFacts(snapshot, head)), config, deps, { bumped: true, pinHead: head });
} finally {
clearInterval(ticker);
}
Expand Down
Loading