mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 18:21:09 +00:00
hotfix(ci): keep ai review feedback fast and deterministic
This commit is contained in:
@@ -290,6 +290,66 @@ function readExecutionMetadata(executionFile) {
|
||||
}
|
||||
}
|
||||
|
||||
function readSelectedFiles(manifestFile) {
|
||||
if (!manifestFile || !fs.existsSync(manifestFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return fs
|
||||
.readFileSync(manifestFile, 'utf8')
|
||||
.split('\n')
|
||||
.map((line) => cleanText(line))
|
||||
.filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatHotspotFiles(files) {
|
||||
if (!files.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visible = files.slice(0, 4).map(renderCode).join(', ');
|
||||
return files.length > 4 ? `${visible}, and ${files.length - 4} more` : visible;
|
||||
}
|
||||
|
||||
function formatRemainingCoverage(rendering) {
|
||||
if (
|
||||
typeof rendering.selectedFiles !== 'number' ||
|
||||
typeof rendering.reviewableFiles !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const remainingFiles = Math.max(rendering.reviewableFiles - rendering.selectedFiles, 0);
|
||||
const hasChangeCounts =
|
||||
typeof rendering.selectedChanges === 'number' &&
|
||||
typeof rendering.reviewableChanges === 'number';
|
||||
const remainingChanges = hasChangeCounts
|
||||
? Math.max(rendering.reviewableChanges - rendering.selectedChanges, 0)
|
||||
: null;
|
||||
|
||||
if (remainingFiles === 0 && (!hasChangeCounts || remainingChanges === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof remainingChanges === 'number') {
|
||||
return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}; ${remainingChanges} changed lines`;
|
||||
}
|
||||
|
||||
return `${remainingFiles} file${remainingFiles === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
function formatFallbackFollowUp(rendering) {
|
||||
if (rendering.mode === 'triage') {
|
||||
return 'Focus manual review on the hotspot files above, and use `/review` for a deeper pass when release, auth, config, or workflow paths changed.';
|
||||
}
|
||||
|
||||
return 'Use `/review` when you need a deeper maintainer rerun with more surrounding context.';
|
||||
}
|
||||
|
||||
export function normalizeStructuredOutput(raw) {
|
||||
if (!raw) {
|
||||
return { ok: false, reason: 'missing structured output' };
|
||||
@@ -461,6 +521,7 @@ export function renderIncompleteReview({
|
||||
runUrl,
|
||||
runtimeTools,
|
||||
turnsUsed,
|
||||
selectedFiles,
|
||||
rendering: renderOptions,
|
||||
status,
|
||||
}) {
|
||||
@@ -468,7 +529,7 @@ export function renderIncompleteReview({
|
||||
const lines = [
|
||||
'### ⚠️ AI Review Incomplete',
|
||||
'',
|
||||
'Claude did not return validated structured review output, so this workflow did not publish raw scratch text.',
|
||||
'Claude did not return validated structured review output, so this workflow published deterministic hotspot context instead of raw scratch text.',
|
||||
'',
|
||||
`- Outcome: ${describeIncompleteOutcome({ reason, rendering, turnsUsed, status })}`,
|
||||
];
|
||||
@@ -484,6 +545,15 @@ export function renderIncompleteReview({
|
||||
if (runtimeBudget) {
|
||||
lines.push(`- Runtime budget: ${escapeMarkdownText(runtimeBudget)}`);
|
||||
}
|
||||
const hotspotFiles = formatHotspotFiles(selectedFiles || []);
|
||||
if (hotspotFiles) {
|
||||
lines.push(`- Hotspot files in this pass: ${hotspotFiles}`);
|
||||
}
|
||||
const remainingCoverage = formatRemainingCoverage(rendering);
|
||||
if (remainingCoverage) {
|
||||
lines.push(`- Remaining reviewable scope not fully covered: ${escapeMarkdownText(remainingCoverage)}`);
|
||||
}
|
||||
lines.push(`- Manual follow-up: ${escapeMarkdownText(formatFallbackFollowUp(rendering))}`);
|
||||
if (runtimeTools?.length) {
|
||||
lines.push(`- Runtime tools: ${runtimeTools.map(renderCode).join(', ')}`);
|
||||
}
|
||||
@@ -501,6 +571,7 @@ export function writeReviewFromEnv(env = process.env) {
|
||||
const runUrl = env.AI_REVIEW_RUN_URL || '#';
|
||||
const validation = normalizeStructuredOutput(env.AI_REVIEW_STRUCTURED_OUTPUT);
|
||||
const metadata = readExecutionMetadata(env.AI_REVIEW_EXECUTION_FILE);
|
||||
const selectedFiles = readSelectedFiles(env.AI_REVIEW_SCOPE_MANIFEST_FILE);
|
||||
const status = cleanText(env.AI_REVIEW_STATUS).toLowerCase() || null;
|
||||
const rendering = normalizeRenderingMetadata({
|
||||
mode: env.AI_REVIEW_MODE,
|
||||
@@ -521,6 +592,7 @@ export function writeReviewFromEnv(env = process.env) {
|
||||
runUrl,
|
||||
runtimeTools: metadata.runtimeTools,
|
||||
turnsUsed: metadata.turnsUsed,
|
||||
selectedFiles,
|
||||
rendering,
|
||||
status,
|
||||
});
|
||||
|
||||
@@ -4,10 +4,14 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const MODE_LIMITS = {
|
||||
fast: { maxFiles: 16, maxChangedLines: 900, maxPatchLines: 90, maxPatchChars: 7000 },
|
||||
triage: { maxFiles: 10, maxChangedLines: 700, maxPatchLines: 80, maxPatchChars: 6000 },
|
||||
triage: { maxFiles: 6, maxChangedLines: 520, maxPatchLines: 60, maxPatchChars: 4500 },
|
||||
deep: { maxFiles: 20, maxChangedLines: 1600, maxPatchLines: 120, maxPatchChars: 9000 },
|
||||
};
|
||||
|
||||
const TRIAGE_SIZE_CLASS_LIMITS = {
|
||||
xlarge: { maxFiles: 4, maxChangedLines: 360, maxPatchLines: 45, maxPatchChars: 3200 },
|
||||
};
|
||||
|
||||
const MODE_LABELS = {
|
||||
fast: 'diff-focused bounded review',
|
||||
triage: 'hotspot-based bounded review (non-exhaustive)',
|
||||
@@ -35,6 +39,13 @@ function cleanText(value) {
|
||||
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
|
||||
}
|
||||
|
||||
function normalizeSizeClass(value) {
|
||||
const sizeClass = cleanText(value).toLowerCase();
|
||||
return sizeClass === 'small' || sizeClass === 'medium' || sizeClass === 'large' || sizeClass === 'xlarge'
|
||||
? sizeClass
|
||||
: null;
|
||||
}
|
||||
|
||||
function escapeMarkdown(value) {
|
||||
return cleanText(value).replace(/\\/g, '\\\\').replace(/([`*_{}[\]<>|])/g, '\\$1');
|
||||
}
|
||||
@@ -127,8 +138,17 @@ export function normalizePullFiles(files) {
|
||||
}).map((file) => ({ ...file, score: scoreFile(file) }));
|
||||
}
|
||||
|
||||
export function buildReviewScope(files, mode) {
|
||||
const limits = MODE_LIMITS[mode] || MODE_LIMITS.fast;
|
||||
function resolveModeLimits(mode, sizeClass) {
|
||||
if (mode !== 'triage') {
|
||||
return MODE_LIMITS[mode] || MODE_LIMITS.fast;
|
||||
}
|
||||
|
||||
return TRIAGE_SIZE_CLASS_LIMITS[sizeClass] || MODE_LIMITS.triage;
|
||||
}
|
||||
|
||||
export function buildReviewScope(files, mode, options = {}) {
|
||||
const sizeClass = normalizeSizeClass(options.sizeClass);
|
||||
const limits = resolveModeLimits(mode, sizeClass);
|
||||
const reviewable = files.filter((file) => file.reviewable);
|
||||
const lowSignal = files.filter((file) => !file.reviewable);
|
||||
const usingChangedFallback = reviewable.length === 0;
|
||||
@@ -260,6 +280,7 @@ export async function writeScopeFromEnv(env = process.env, request) {
|
||||
const prNumber = Number.parseInt(cleanText(env.AI_REVIEW_PR_NUMBER), 10);
|
||||
const baseRef = cleanText(env.AI_REVIEW_BASE_REF || 'dev');
|
||||
const mode = cleanText(env.AI_REVIEW_MODE || 'fast').toLowerCase();
|
||||
const sizeClass = normalizeSizeClass(env.AI_REVIEW_PR_SIZE_CLASS);
|
||||
const turnBudget = Number.parseInt(cleanText(env.AI_REVIEW_MAX_TURNS || '0'), 10) || 0;
|
||||
const timeoutMinutes = Number.parseInt(cleanText(env.AI_REVIEW_TIMEOUT_MINUTES || '0'), 10) || 0;
|
||||
const outputFile = env.AI_REVIEW_SCOPE_FILE || '.ccs-ai-review-scope.md';
|
||||
@@ -287,7 +308,7 @@ export async function writeScopeFromEnv(env = process.env, request) {
|
||||
const files = normalizePullFiles(
|
||||
await collectPullRequestFiles(`${apiUrl}/repos/${repository}/pulls/${prNumber}/files?per_page=100`, fetchPage)
|
||||
);
|
||||
const scope = buildReviewScope(files, mode);
|
||||
const scope = buildReviewScope(files, mode, { sizeClass });
|
||||
const markdown = renderReviewScope({ prNumber, baseRef, turnBudget, timeoutMinutes, scope });
|
||||
|
||||
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user