@open-cr-agent/core 0.1.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/LICENSE +202 -0
- package/README.md +3 -0
- package/dist/anchor/anchor.d.ts +24 -0
- package/dist/anchor/anchor.js +66 -0
- package/dist/anchor/index.d.ts +3 -0
- package/dist/anchor/index.js +3 -0
- package/dist/anchor/match.d.ts +22 -0
- package/dist/anchor/match.js +94 -0
- package/dist/anchor/relocate.d.ts +5 -0
- package/dist/anchor/relocate.js +38 -0
- package/dist/bundle/bundle.d.ts +22 -0
- package/dist/bundle/bundle.js +102 -0
- package/dist/bundle/grouping.d.ts +16 -0
- package/dist/bundle/grouping.js +25 -0
- package/dist/bundle/index.d.ts +3 -0
- package/dist/bundle/index.js +3 -0
- package/dist/contracts.d.ts +77 -0
- package/dist/contracts.js +2 -0
- package/dist/diff/hunks.d.ts +3 -0
- package/dist/diff/hunks.js +64 -0
- package/dist/diff/index.d.ts +3 -0
- package/dist/diff/index.js +3 -0
- package/dist/diff/parse.d.ts +3 -0
- package/dist/diff/parse.js +116 -0
- package/dist/diff/quoted-path.d.ts +6 -0
- package/dist/diff/quoted-path.js +46 -0
- package/dist/domain.d.ts +126 -0
- package/dist/domain.js +23 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +16 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/judge/index.d.ts +4 -0
- package/dist/judge/index.js +4 -0
- package/dist/judge/judge.d.ts +46 -0
- package/dist/judge/judge.js +122 -0
- package/dist/judge/prompt.d.ts +25 -0
- package/dist/judge/prompt.js +62 -0
- package/dist/judge/verdict.d.ts +9 -0
- package/dist/judge/verdict.js +26 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/memory.d.ts +19 -0
- package/dist/memory/memory.js +49 -0
- package/dist/pipeline/budget.d.ts +9 -0
- package/dist/pipeline/budget.js +19 -0
- package/dist/pipeline/context.d.ts +6 -0
- package/dist/pipeline/context.js +71 -0
- package/dist/pipeline/execute.d.ts +32 -0
- package/dist/pipeline/execute.js +122 -0
- package/dist/pipeline/findings.d.ts +6 -0
- package/dist/pipeline/findings.js +44 -0
- package/dist/pipeline/helpers.d.ts +6 -0
- package/dist/pipeline/helpers.js +32 -0
- package/dist/pipeline/index.d.ts +9 -0
- package/dist/pipeline/index.js +9 -0
- package/dist/pipeline/matrix.d.ts +38 -0
- package/dist/pipeline/matrix.js +93 -0
- package/dist/pipeline/output.d.ts +69 -0
- package/dist/pipeline/output.js +123 -0
- package/dist/pipeline/plan.d.ts +34 -0
- package/dist/pipeline/plan.js +68 -0
- package/dist/pipeline/pool.d.ts +2 -0
- package/dist/pipeline/pool.js +13 -0
- package/dist/pipeline/preview.d.ts +42 -0
- package/dist/pipeline/preview.js +73 -0
- package/dist/pipeline/report.d.ts +118 -0
- package/dist/pipeline/report.js +27 -0
- package/dist/pipeline/run.d.ts +36 -0
- package/dist/pipeline/run.js +301 -0
- package/dist/pipeline/task.d.ts +19 -0
- package/dist/pipeline/task.js +143 -0
- package/dist/pipeline/usage.d.ts +4 -0
- package/dist/pipeline/usage.js +13 -0
- package/dist/plugin/builtin.d.ts +14 -0
- package/dist/plugin/builtin.js +47 -0
- package/dist/plugin/host.d.ts +9 -0
- package/dist/plugin/host.js +78 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.js +5 -0
- package/dist/plugin/registry.d.ts +37 -0
- package/dist/plugin/registry.js +104 -0
- package/dist/plugin/types.d.ts +53 -0
- package/dist/plugin/types.js +2 -0
- package/dist/rereview/index.d.ts +4 -0
- package/dist/rereview/index.js +4 -0
- package/dist/rereview/presence.d.ts +3 -0
- package/dist/rereview/presence.js +27 -0
- package/dist/rereview/quote.d.ts +4 -0
- package/dist/rereview/quote.js +28 -0
- package/dist/rereview/reconcile.d.ts +20 -0
- package/dist/rereview/reconcile.js +55 -0
- package/dist/review/impact.d.ts +11 -0
- package/dist/review/impact.js +53 -0
- package/dist/review/index.d.ts +10 -0
- package/dist/review/index.js +10 -0
- package/dist/review/plan-phase.d.ts +10 -0
- package/dist/review/plan-phase.js +33 -0
- package/dist/review/prompt-text.d.ts +16 -0
- package/dist/review/prompt-text.js +90 -0
- package/dist/review/prompt.d.ts +28 -0
- package/dist/review/prompt.js +85 -0
- package/dist/review/reviewer.d.ts +17 -0
- package/dist/review/reviewer.js +2 -0
- package/dist/review/reviewers/agents-md.d.ts +3 -0
- package/dist/review/reviewers/agents-md.js +45 -0
- package/dist/review/reviewers/correctness.d.ts +3 -0
- package/dist/review/reviewers/correctness.js +48 -0
- package/dist/review/reviewers/docs.d.ts +3 -0
- package/dist/review/reviewers/docs.js +47 -0
- package/dist/review/reviewers/performance.d.ts +3 -0
- package/dist/review/reviewers/performance.js +59 -0
- package/dist/review/reviewers/scopes.d.ts +3 -0
- package/dist/review/reviewers/scopes.js +9 -0
- package/dist/review/reviewers/security.d.ts +3 -0
- package/dist/review/reviewers/security.js +52 -0
- package/dist/review/tools.d.ts +9 -0
- package/dist/review/tools.js +8 -0
- package/dist/rules/builtin/general.d.ts +2 -0
- package/dist/rules/builtin/general.js +9 -0
- package/dist/rules/builtin/go.d.ts +2 -0
- package/dist/rules/builtin/go.js +9 -0
- package/dist/rules/builtin/index.d.ts +9 -0
- package/dist/rules/builtin/index.js +16 -0
- package/dist/rules/builtin/java.d.ts +2 -0
- package/dist/rules/builtin/java.js +9 -0
- package/dist/rules/builtin/performance.d.ts +2 -0
- package/dist/rules/builtin/performance.js +9 -0
- package/dist/rules/builtin/python.d.ts +2 -0
- package/dist/rules/builtin/python.js +9 -0
- package/dist/rules/builtin/security.d.ts +2 -0
- package/dist/rules/builtin/security.js +11 -0
- package/dist/rules/builtin/typescript.d.ts +2 -0
- package/dist/rules/builtin/typescript.js +9 -0
- package/dist/rules/index.d.ts +5 -0
- package/dist/rules/index.js +5 -0
- package/dist/rules/languages.d.ts +3 -0
- package/dist/rules/languages.js +20 -0
- package/dist/rules/repo-rules.d.ts +15 -0
- package/dist/rules/repo-rules.js +22 -0
- package/dist/rules/resolve.d.ts +4 -0
- package/dist/rules/resolve.js +27 -0
- package/dist/rules/rule-set.d.ts +6 -0
- package/dist/rules/rule-set.js +2 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +2 -0
- package/dist/select/patterns.d.ts +6 -0
- package/dist/select/patterns.js +132 -0
- package/dist/select/select.d.ts +19 -0
- package/dist/select/select.js +52 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/jsonl.d.ts +11 -0
- package/dist/session/jsonl.js +57 -0
- package/dist/triage.d.ts +11 -0
- package/dist/triage.js +55 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/prompt.d.ts +24 -0
- package/dist/verify/prompt.js +71 -0
- package/dist/verify/verify.d.ts +29 -0
- package/dist/verify/verify.js +98 -0
- package/package.json +40 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { usageSpent } from "../errors.js";
|
|
2
|
+
export const HELPER_TIMEOUT_MS = 60_000;
|
|
3
|
+
// Grouping runs on the runtime's cheapest tier when the runtime supports plain
|
|
4
|
+
// completions; otherwise bundling falls back to per-file review.
|
|
5
|
+
export function runtimeGrouper(runtime, signal, onUsage) {
|
|
6
|
+
const complete = runtime.complete?.bind(runtime);
|
|
7
|
+
if (!complete)
|
|
8
|
+
return undefined;
|
|
9
|
+
return {
|
|
10
|
+
async group(prompt) {
|
|
11
|
+
const result = await complete({ tier: "light", system: prompt.system, user: prompt.user, timeoutMs: HELPER_TIMEOUT_MS }, AbortSignal.any([signal, AbortSignal.timeout(HELPER_TIMEOUT_MS)])).catch((error) => {
|
|
12
|
+
const spent = usageSpent(error);
|
|
13
|
+
if (spent)
|
|
14
|
+
onUsage(spent);
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
onUsage(result.usage);
|
|
18
|
+
return parseJsonAnswer(result.text);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Models often wrap JSON in a Markdown fence or add a sentence around it.
|
|
23
|
+
export function parseJsonAnswer(text) {
|
|
24
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text)?.[1];
|
|
25
|
+
const candidate = (fenced ?? text).trim();
|
|
26
|
+
const start = candidate.search(/[[{]/);
|
|
27
|
+
if (start < 0)
|
|
28
|
+
throw new Error("the model answered without JSON");
|
|
29
|
+
const end = Math.max(candidate.lastIndexOf("]"), candidate.lastIndexOf("}"));
|
|
30
|
+
return JSON.parse(candidate.slice(start, end + 1));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./context.js";
|
|
2
|
+
export * from "./findings.js";
|
|
3
|
+
export * from "./matrix.js";
|
|
4
|
+
export * from "./output.js";
|
|
5
|
+
export * from "./preview.js";
|
|
6
|
+
export * from "./report.js";
|
|
7
|
+
export * from "./run.js";
|
|
8
|
+
export * from "./usage.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./context.js";
|
|
2
|
+
export * from "./findings.js";
|
|
3
|
+
export * from "./matrix.js";
|
|
4
|
+
export * from "./output.js";
|
|
5
|
+
export * from "./preview.js";
|
|
6
|
+
export * from "./report.js";
|
|
7
|
+
export * from "./run.js";
|
|
8
|
+
export * from "./usage.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Bundle } from "../bundle/bundle.js";
|
|
2
|
+
import type { RiskTier } from "../domain.js";
|
|
3
|
+
import type { ReviewerDefinition } from "../review/reviewer.js";
|
|
4
|
+
export declare const RISK_TIERS: readonly RiskTier[];
|
|
5
|
+
export interface ReviewerOverride {
|
|
6
|
+
enabled?: boolean | undefined;
|
|
7
|
+
minTier?: RiskTier | undefined;
|
|
8
|
+
}
|
|
9
|
+
export type ReviewerOverrides = Readonly<Record<string, ReviewerOverride>>;
|
|
10
|
+
export interface MatrixCell {
|
|
11
|
+
taskId: string;
|
|
12
|
+
reviewer: ReviewerDefinition;
|
|
13
|
+
bundle: Bundle;
|
|
14
|
+
}
|
|
15
|
+
export type SkipReason = "disabled" | "below_tier" | "no_matching_files" | "no_guidelines" | "task_limit";
|
|
16
|
+
export interface SkippedCell {
|
|
17
|
+
reviewer: string;
|
|
18
|
+
bundle: string;
|
|
19
|
+
reason: SkipReason;
|
|
20
|
+
}
|
|
21
|
+
export interface ReviewMatrix {
|
|
22
|
+
cells: MatrixCell[];
|
|
23
|
+
skipped: SkippedCell[];
|
|
24
|
+
limited?: MatrixCell[];
|
|
25
|
+
}
|
|
26
|
+
export declare function planMatrix(bundles: readonly Bundle[], reviewers: readonly ReviewerDefinition[], tier: RiskTier, overrides?: ReviewerOverrides, options?: {
|
|
27
|
+
allTiers?: boolean;
|
|
28
|
+
hasGuidelines?: boolean;
|
|
29
|
+
}): ReviewMatrix;
|
|
30
|
+
export declare const DEFAULT_MAX_TASKS = 60;
|
|
31
|
+
export interface TaskOptions {
|
|
32
|
+
ultra?: boolean;
|
|
33
|
+
maxTasks?: number;
|
|
34
|
+
hasGuidelines?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function planTasks(bundles: readonly Bundle[], reviewers: readonly ReviewerDefinition[], tier: RiskTier, overrides?: ReviewerOverrides, options?: TaskOptions): ReviewMatrix;
|
|
37
|
+
export declare function rank(tier: RiskTier): number;
|
|
38
|
+
//# sourceMappingURL=matrix.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
export const RISK_TIERS = ["trivial", "lite", "full"];
|
|
3
|
+
// Deterministic: which reviewer runs on which bundle, so cost grows with the
|
|
4
|
+
// risk of the change instead of bundles × reviewers.
|
|
5
|
+
export function planMatrix(bundles, reviewers, tier, overrides = {}, options = {}) {
|
|
6
|
+
const cells = [];
|
|
7
|
+
const skipped = [];
|
|
8
|
+
const ignores = new Map(reviewers.map((r) => [r.id, ignoreMatcher(r)]));
|
|
9
|
+
bundles.forEach((bundle, i) => {
|
|
10
|
+
for (const reviewer of reviewers) {
|
|
11
|
+
const override = overrides[reviewer.id] ?? {};
|
|
12
|
+
const skip = (reason) => skipped.push({ reviewer: reviewer.id, bundle: bundle.label, reason });
|
|
13
|
+
if (override.enabled === false) {
|
|
14
|
+
skip("disabled");
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const minTier = options.allTiers
|
|
18
|
+
? "trivial"
|
|
19
|
+
: (override.minTier ?? reviewer.scope?.minTier ?? "trivial");
|
|
20
|
+
if (rank(tier) < rank(minTier)) {
|
|
21
|
+
skip("below_tier");
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (reviewer.scope?.requiresGuidelines && options.hasGuidelines !== true) {
|
|
25
|
+
skip("no_guidelines");
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const ignored = ignores.get(reviewer.id) ?? (() => false);
|
|
29
|
+
const files = bundle.files.filter((f) => !ignored(f.newPath));
|
|
30
|
+
if (files.length === 0) {
|
|
31
|
+
skip("no_matching_files");
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
cells.push({
|
|
35
|
+
taskId: `${reviewer.id}-${i + 1}`,
|
|
36
|
+
reviewer,
|
|
37
|
+
bundle: files.length === bundle.files.length ? bundle : { ...bundle, files },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return { cells, skipped };
|
|
42
|
+
}
|
|
43
|
+
// Every review task is a model conversation, so a change set with hundreds
|
|
44
|
+
// of bundles must not become hundreds of tasks.
|
|
45
|
+
export const DEFAULT_MAX_TASKS = 60;
|
|
46
|
+
// The matrix as it runs: --ultra reviews every cell twice, and past
|
|
47
|
+
// `maxTasks` the least important cells are skipped: second samples first,
|
|
48
|
+
// then reviewers in reverse registration order (correctness first), so
|
|
49
|
+
// every file keeps its first reviewer as long as possible.
|
|
50
|
+
export function planTasks(bundles, reviewers, tier, overrides = {}, options = {}) {
|
|
51
|
+
const planned = planMatrix(bundles, reviewers, tier, overrides, {
|
|
52
|
+
allTiers: options.ultra === true,
|
|
53
|
+
hasGuidelines: options.hasGuidelines === true,
|
|
54
|
+
});
|
|
55
|
+
const cells = options.ultra
|
|
56
|
+
? planned.cells.flatMap((cell) => [cell, { ...cell, taskId: `${cell.taskId}b` }])
|
|
57
|
+
: planned.cells;
|
|
58
|
+
const max = options.maxTasks ?? DEFAULT_MAX_TASKS;
|
|
59
|
+
if (cells.length <= max)
|
|
60
|
+
return { cells, skipped: planned.skipped };
|
|
61
|
+
const order = new Map(reviewers.map((r, i) => [r.id, i]));
|
|
62
|
+
const priority = (cell, index) => [
|
|
63
|
+
cell.taskId.endsWith("b") ? 1 : 0,
|
|
64
|
+
order.get(cell.reviewer.id) ?? reviewers.length,
|
|
65
|
+
index,
|
|
66
|
+
];
|
|
67
|
+
const ranked = cells
|
|
68
|
+
.map((cell, index) => ({ cell, key: priority(cell, index) }))
|
|
69
|
+
.sort((a, b) => a.key[0] - b.key[0] || a.key[1] - b.key[1] || a.key[2] - b.key[2]);
|
|
70
|
+
const kept = new Set(ranked.slice(0, max).map((r) => r.cell));
|
|
71
|
+
return {
|
|
72
|
+
cells: cells.filter((cell) => kept.has(cell)),
|
|
73
|
+
limited: cells.filter((cell) => !kept.has(cell)),
|
|
74
|
+
skipped: [
|
|
75
|
+
...planned.skipped,
|
|
76
|
+
...cells
|
|
77
|
+
.filter((cell) => !kept.has(cell))
|
|
78
|
+
.map((cell) => ({
|
|
79
|
+
reviewer: cell.reviewer.id,
|
|
80
|
+
bundle: cell.bundle.label,
|
|
81
|
+
reason: "task_limit",
|
|
82
|
+
})),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function rank(tier) {
|
|
87
|
+
return RISK_TIERS.indexOf(tier);
|
|
88
|
+
}
|
|
89
|
+
function ignoreMatcher(reviewer) {
|
|
90
|
+
const globs = reviewer.scope?.ignore ?? [];
|
|
91
|
+
return globs.length === 0 ? () => false : picomatch([...globs], { dot: true, nocase: true });
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=matrix.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Usage } from "../contracts.js";
|
|
2
|
+
import type { ChangeRequest, RiskTier, Severity, Verdict, Verification } from "../domain.js";
|
|
3
|
+
import type { JudgeDecisions } from "../judge/judge.js";
|
|
4
|
+
import type { MemoryEntry } from "../memory/memory.js";
|
|
5
|
+
import type { RefutedFinding } from "../verify/verify.js";
|
|
6
|
+
import type { SkippedCell } from "./matrix.js";
|
|
7
|
+
import type { ReviewPreview } from "./preview.js";
|
|
8
|
+
import type { AnchoringSummary, CoverageEntry, ReviewReport, TaskOutcome } from "./report.js";
|
|
9
|
+
export declare const REPORT_VERSION = 1;
|
|
10
|
+
export interface OutputFinding {
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
reviewer: string;
|
|
13
|
+
category: string;
|
|
14
|
+
severity: Severity;
|
|
15
|
+
verification: Verification;
|
|
16
|
+
file: string;
|
|
17
|
+
lines?: {
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
};
|
|
21
|
+
inDiff: boolean;
|
|
22
|
+
status: "new" | "unfixed";
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
suggestion?: string;
|
|
26
|
+
evidence: string[];
|
|
27
|
+
code: string;
|
|
28
|
+
lowConfidence?: true;
|
|
29
|
+
}
|
|
30
|
+
export interface OutputPriorFinding {
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
title: string;
|
|
33
|
+
file: string;
|
|
34
|
+
severity: Severity;
|
|
35
|
+
verification: Verification;
|
|
36
|
+
}
|
|
37
|
+
export interface ReportOutput {
|
|
38
|
+
version: typeof REPORT_VERSION;
|
|
39
|
+
changeRequest: ChangeRequest;
|
|
40
|
+
tier: RiskTier;
|
|
41
|
+
verdict: Verdict;
|
|
42
|
+
summary: string;
|
|
43
|
+
scope?: ReviewReport["scope"];
|
|
44
|
+
coverage: CoverageEntry[];
|
|
45
|
+
findings: OutputFinding[];
|
|
46
|
+
unverifiedCriticals: number;
|
|
47
|
+
refuted: RefutedFinding[];
|
|
48
|
+
remembered: MemoryEntry[];
|
|
49
|
+
judgement?: JudgeDecisions;
|
|
50
|
+
rereview?: Record<"fixed" | "notReproduced" | "notRechecked" | "unchanged" | "dismissed", OutputPriorFinding[]>;
|
|
51
|
+
tasks: TaskOutcome[];
|
|
52
|
+
skipped: SkippedCell[];
|
|
53
|
+
bundles: {
|
|
54
|
+
label: string;
|
|
55
|
+
files: string[];
|
|
56
|
+
}[];
|
|
57
|
+
anchoring?: AnchoringSummary;
|
|
58
|
+
usage: Usage;
|
|
59
|
+
warnings: string[];
|
|
60
|
+
}
|
|
61
|
+
export declare function toReportOutput(report: ReviewReport): ReportOutput;
|
|
62
|
+
export declare const PLAN_VERSION = 1;
|
|
63
|
+
export type PlanOutput = {
|
|
64
|
+
version: typeof PLAN_VERSION;
|
|
65
|
+
} & ReviewPreview;
|
|
66
|
+
export declare function toPlanOutput(preview: ReviewPreview): PlanOutput;
|
|
67
|
+
export declare function isUnsafeCodePoint(code: number): boolean;
|
|
68
|
+
export declare function serializeOutput(value: unknown, indent?: number): string;
|
|
69
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// The published shape of a review: `--format json` and a session's
|
|
2
|
+
// report.json. It is a contract with scripts and CI, so it carries a version
|
|
3
|
+
// and only what a reader can rely on; internal fields (per-run ids, how each
|
|
4
|
+
// finding was anchored, code signatures, platform state) stay out, though the
|
|
5
|
+
// anchoring counts of the whole run are in. Change it
|
|
6
|
+
// only by adding optional fields, or by a new version.
|
|
7
|
+
export const REPORT_VERSION = 1;
|
|
8
|
+
export function toReportOutput(report) {
|
|
9
|
+
const output = {
|
|
10
|
+
version: REPORT_VERSION,
|
|
11
|
+
changeRequest: report.changeRequest,
|
|
12
|
+
tier: report.tier,
|
|
13
|
+
verdict: report.verdict,
|
|
14
|
+
summary: report.summary,
|
|
15
|
+
coverage: report.coverage,
|
|
16
|
+
findings: report.findings.map(outputFinding),
|
|
17
|
+
unverifiedCriticals: report.unverifiedCriticals,
|
|
18
|
+
refuted: report.refuted,
|
|
19
|
+
remembered: report.remembered,
|
|
20
|
+
tasks: report.tasks,
|
|
21
|
+
skipped: report.skipped,
|
|
22
|
+
bundles: report.bundles,
|
|
23
|
+
usage: report.usage,
|
|
24
|
+
warnings: report.warnings,
|
|
25
|
+
};
|
|
26
|
+
if (report.scope)
|
|
27
|
+
output.scope = report.scope;
|
|
28
|
+
if (report.judgement)
|
|
29
|
+
output.judgement = report.judgement;
|
|
30
|
+
if (report.anchoring)
|
|
31
|
+
output.anchoring = report.anchoring;
|
|
32
|
+
if (report.rereview) {
|
|
33
|
+
const r = report.rereview;
|
|
34
|
+
output.rereview = {
|
|
35
|
+
fixed: r.fixed.map(outputPrior),
|
|
36
|
+
notReproduced: r.notReproduced.map(outputPrior),
|
|
37
|
+
notRechecked: r.notRechecked.map(outputPrior),
|
|
38
|
+
unchanged: r.unchanged.map(outputPrior),
|
|
39
|
+
dismissed: r.dismissed.map(outputPrior),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
function outputFinding(f) {
|
|
45
|
+
const output = {
|
|
46
|
+
fingerprint: f.fingerprint,
|
|
47
|
+
reviewer: f.reviewer,
|
|
48
|
+
category: f.category,
|
|
49
|
+
severity: f.severity,
|
|
50
|
+
verification: f.verification ?? "unchecked",
|
|
51
|
+
file: f.file,
|
|
52
|
+
inDiff: f.anchor.inDiff,
|
|
53
|
+
status: f.status === "unfixed" ? "unfixed" : "new",
|
|
54
|
+
title: f.title,
|
|
55
|
+
body: f.body,
|
|
56
|
+
evidence: f.evidence,
|
|
57
|
+
code: f.existingCode,
|
|
58
|
+
};
|
|
59
|
+
if (f.lineRange)
|
|
60
|
+
output.lines = { start: f.lineRange.start, end: f.lineRange.end };
|
|
61
|
+
if (f.suggestion !== undefined)
|
|
62
|
+
output.suggestion = f.suggestion;
|
|
63
|
+
if (f.lowConfidence)
|
|
64
|
+
output.lowConfidence = true;
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
function outputPrior(f) {
|
|
68
|
+
return {
|
|
69
|
+
fingerprint: f.fingerprint,
|
|
70
|
+
title: f.title,
|
|
71
|
+
file: f.file,
|
|
72
|
+
severity: f.severity,
|
|
73
|
+
verification: f.verification ?? "unchecked",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// The published shape of `ocra review --plan --format json`, versioned like
|
|
77
|
+
// the report. The preview is already free of internal fields; the version
|
|
78
|
+
// makes the contract explicit.
|
|
79
|
+
export const PLAN_VERSION = 1;
|
|
80
|
+
export function toPlanOutput(preview) {
|
|
81
|
+
return {
|
|
82
|
+
version: PLAN_VERSION,
|
|
83
|
+
changeRequest: preview.changeRequest,
|
|
84
|
+
tier: preview.tier,
|
|
85
|
+
selected: preview.selected,
|
|
86
|
+
excluded: preview.excluded,
|
|
87
|
+
bundles: preview.bundles,
|
|
88
|
+
groupingSkipped: preview.groupingSkipped,
|
|
89
|
+
tasks: preview.tasks,
|
|
90
|
+
skipped: preview.skipped,
|
|
91
|
+
promptTokens: preview.promptTokens,
|
|
92
|
+
// Added in version 1 without a bump: a new field older readers ignore.
|
|
93
|
+
planCalls: preview.planCalls,
|
|
94
|
+
warnings: preview.warnings,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Characters a terminal or an editor acts on: C0 controls but tab and
|
|
98
|
+
// newline, DEL and C1 controls (ANSI and OSC escapes), line and paragraph
|
|
99
|
+
// separators, and bidirectional marks and overrides ("Trojan Source"). One
|
|
100
|
+
// definition for every output: terminal text replaces them, JSON escapes them.
|
|
101
|
+
export function isUnsafeCodePoint(code) {
|
|
102
|
+
return ((code <= 0x1f && code !== 0x09 && code !== 0x0a) ||
|
|
103
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
104
|
+
code === 0x061c ||
|
|
105
|
+
code === 0x200e ||
|
|
106
|
+
code === 0x200f ||
|
|
107
|
+
code === 0x2028 ||
|
|
108
|
+
code === 0x2029 ||
|
|
109
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
110
|
+
(code >= 0x2066 && code <= 0x2069));
|
|
111
|
+
}
|
|
112
|
+
// JSON for files and stdout. JSON.stringify escapes C0 controls but not the
|
|
113
|
+
// rest; model and pull request text reaches these files.
|
|
114
|
+
export function serializeOutput(value, indent = 2) {
|
|
115
|
+
let out = "";
|
|
116
|
+
for (const char of JSON.stringify(value, null, indent)) {
|
|
117
|
+
const code = char.codePointAt(0) ?? 0;
|
|
118
|
+
out +=
|
|
119
|
+
code > 0x1f && isUnsafeCodePoint(code) ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Bundle } from "../bundle/bundle.js";
|
|
2
|
+
import type { AgentRuntime, ReviewContext, Usage } from "../contracts.js";
|
|
3
|
+
import type { ChangeRequest, FileDiff, RiskTier } from "../domain.js";
|
|
4
|
+
import { type MemoryEntry } from "../memory/memory.js";
|
|
5
|
+
import { type RepoRule } from "../rules/repo-rules.js";
|
|
6
|
+
import { type FileDecision } from "../select/select.js";
|
|
7
|
+
import type { ReviewEvent } from "./report.js";
|
|
8
|
+
import type { ReviewOptions } from "./run.js";
|
|
9
|
+
export declare const GUIDELINES_PATH = "AGENTS.md";
|
|
10
|
+
export interface ReviewPlan {
|
|
11
|
+
changeRequest: ChangeRequest;
|
|
12
|
+
decisions: FileDecision[];
|
|
13
|
+
selected: FileDiff[];
|
|
14
|
+
tier: RiskTier;
|
|
15
|
+
bundles: Bundle[];
|
|
16
|
+
unchanged: ReadonlySet<string>;
|
|
17
|
+
widened?: {
|
|
18
|
+
from: RiskTier;
|
|
19
|
+
to: RiskTier;
|
|
20
|
+
};
|
|
21
|
+
context: ReviewContext;
|
|
22
|
+
guidelines: string | undefined;
|
|
23
|
+
repoRules: RepoRule[];
|
|
24
|
+
memory: MemoryEntry[];
|
|
25
|
+
usage: Usage[];
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
export type PlanOptions = Pick<ReviewOptions, "vcs" | "rules" | "readTrusted" | "selection" | "bundling" | "grouper"> & {
|
|
29
|
+
runtime?: AgentRuntime;
|
|
30
|
+
reviewOnly?: ReadonlySet<string>;
|
|
31
|
+
priorTier?: RiskTier;
|
|
32
|
+
};
|
|
33
|
+
export declare function planReview(options: PlanOptions, emit: (event: ReviewEvent) => void, signal: AbortSignal): Promise<ReviewPlan>;
|
|
34
|
+
//# sourceMappingURL=plan.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { bundleFiles, defaultBundlePolicy } from "../bundle/bundle.js";
|
|
2
|
+
import { MEMORY_PATH, parseMemory } from "../memory/memory.js";
|
|
3
|
+
import { parseRepoRules, REPO_RULES_PATH } from "../rules/repo-rules.js";
|
|
4
|
+
import { defaultSelectionPolicy, selectFiles } from "../select/select.js";
|
|
5
|
+
import { triage } from "../triage.js";
|
|
6
|
+
import { reviewContext } from "./context.js";
|
|
7
|
+
import { runtimeGrouper } from "./helpers.js";
|
|
8
|
+
import { rank } from "./matrix.js";
|
|
9
|
+
export const GUIDELINES_PATH = "AGENTS.md";
|
|
10
|
+
export async function planReview(options, emit, signal) {
|
|
11
|
+
const { vcs } = options;
|
|
12
|
+
const readTrusted = options.readTrusted ?? ((path) => vcs.readFile(path));
|
|
13
|
+
const changeRequest = await vcs.getChangeRequest();
|
|
14
|
+
emit({ type: "run_started", changeRequest });
|
|
15
|
+
const diffs = await vcs.getDiff();
|
|
16
|
+
const decisions = selectFiles(diffs, options.selection ?? defaultSelectionPolicy);
|
|
17
|
+
const selected = decisions.filter((d) => d.selected).map((d) => d.diff);
|
|
18
|
+
const tier = triage(selected);
|
|
19
|
+
emit({
|
|
20
|
+
type: "files_selected",
|
|
21
|
+
selected: selected.length,
|
|
22
|
+
excluded: diffs.length - selected.length,
|
|
23
|
+
tier,
|
|
24
|
+
});
|
|
25
|
+
const [guidelines, fileRules, memoryText] = await Promise.all([
|
|
26
|
+
readTrusted(GUIDELINES_PATH),
|
|
27
|
+
loadRepoRules(readTrusted),
|
|
28
|
+
readTrusted(MEMORY_PATH),
|
|
29
|
+
]);
|
|
30
|
+
const usage = [];
|
|
31
|
+
const grouper = options.grouper ??
|
|
32
|
+
(options.runtime ? runtimeGrouper(options.runtime, signal, (u) => usage.push(u)) : undefined);
|
|
33
|
+
const widened = options.reviewOnly && options.priorTier && rank(tier) > rank(options.priorTier)
|
|
34
|
+
? { from: options.priorTier, to: tier }
|
|
35
|
+
: undefined;
|
|
36
|
+
const only = widened ? undefined : options.reviewOnly;
|
|
37
|
+
const inScope = only
|
|
38
|
+
? selected.filter((d) => only.has(d.newPath) || only.has(d.oldPath))
|
|
39
|
+
: selected;
|
|
40
|
+
const unchanged = new Set(selected.filter((d) => !inScope.includes(d)).map((d) => d.newPath));
|
|
41
|
+
const bundled = await bundleFiles(inScope, options.bundling ?? defaultBundlePolicy, grouper);
|
|
42
|
+
emit({
|
|
43
|
+
type: "files_bundled",
|
|
44
|
+
strategy: bundled.strategy,
|
|
45
|
+
bundles: bundled.bundles.length,
|
|
46
|
+
warnings: bundled.warnings,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
changeRequest,
|
|
50
|
+
decisions,
|
|
51
|
+
selected,
|
|
52
|
+
tier,
|
|
53
|
+
bundles: bundled.bundles,
|
|
54
|
+
unchanged,
|
|
55
|
+
...(widened ? { widened } : {}),
|
|
56
|
+
context: reviewContext(vcs, diffs),
|
|
57
|
+
guidelines,
|
|
58
|
+
repoRules: [...(options.rules ?? []), ...fileRules],
|
|
59
|
+
memory: memoryText === undefined ? [] : parseMemory(memoryText),
|
|
60
|
+
usage,
|
|
61
|
+
warnings: bundled.warnings,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function loadRepoRules(read) {
|
|
65
|
+
const text = await read(REPO_RULES_PATH);
|
|
66
|
+
return text === undefined ? [] : parseRepoRules(text);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function mapWithConcurrency(items, limit, worker) {
|
|
2
|
+
const results = new Array(items.length);
|
|
3
|
+
let next = 0;
|
|
4
|
+
const runners = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
5
|
+
while (next < items.length) {
|
|
6
|
+
const index = next++;
|
|
7
|
+
results[index] = await worker(items[index]);
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
await Promise.all(runners);
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChangeRequest, RiskTier } from "../domain.js";
|
|
2
|
+
import type { ReviewerDefinition } from "../review/reviewer.js";
|
|
3
|
+
import type { FileDecision } from "../select/select.js";
|
|
4
|
+
import { type ReviewerOverrides, type SkippedCell } from "./matrix.js";
|
|
5
|
+
import { type PlanOptions } from "./plan.js";
|
|
6
|
+
export interface PreviewTask {
|
|
7
|
+
taskId: string;
|
|
8
|
+
reviewer: string;
|
|
9
|
+
bundle: string;
|
|
10
|
+
files: string[];
|
|
11
|
+
promptTokens: number;
|
|
12
|
+
planPromptTokens?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ReviewPreview {
|
|
15
|
+
changeRequest: ChangeRequest;
|
|
16
|
+
tier: RiskTier;
|
|
17
|
+
selected: string[];
|
|
18
|
+
excluded: {
|
|
19
|
+
path: string;
|
|
20
|
+
reason: Exclude<FileDecision, {
|
|
21
|
+
selected: true;
|
|
22
|
+
}>["reason"];
|
|
23
|
+
}[];
|
|
24
|
+
bundles: {
|
|
25
|
+
label: string;
|
|
26
|
+
files: string[];
|
|
27
|
+
}[];
|
|
28
|
+
groupingSkipped: boolean;
|
|
29
|
+
tasks: PreviewTask[];
|
|
30
|
+
skipped: SkippedCell[];
|
|
31
|
+
promptTokens: number;
|
|
32
|
+
planCalls: number;
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
export type PreviewOptions = Omit<PlanOptions, "runtime"> & {
|
|
36
|
+
reviewers?: readonly ReviewerDefinition[];
|
|
37
|
+
reviewerOverrides?: ReviewerOverrides;
|
|
38
|
+
ultra?: boolean;
|
|
39
|
+
maxTasks?: number;
|
|
40
|
+
};
|
|
41
|
+
export declare function previewReview(options: PreviewOptions): Promise<ReviewPreview>;
|
|
42
|
+
//# sourceMappingURL=preview.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defaultBundlePolicy } from "../bundle/bundle.js";
|
|
2
|
+
import { memoryFor } from "../memory/memory.js";
|
|
3
|
+
import { buildReviewPrompt } from "../review/prompt.js";
|
|
4
|
+
import { correctnessReviewer } from "../review/reviewers/correctness.js";
|
|
5
|
+
import { resolveRules } from "../rules/resolve.js";
|
|
6
|
+
import { isLargeBundle } from "./execute.js";
|
|
7
|
+
import { planTasks } from "./matrix.js";
|
|
8
|
+
import { planReview } from "./plan.js";
|
|
9
|
+
// Everything a review would do before its first model call, for free: which
|
|
10
|
+
// files, which tasks, and how large each first prompt is.
|
|
11
|
+
export async function previewReview(options) {
|
|
12
|
+
const plan = await planReview(options, () => { }, new AbortController().signal);
|
|
13
|
+
const reviewers = options.reviewers ?? [correctnessReviewer];
|
|
14
|
+
const planned = planTasks(plan.bundles, reviewers, plan.tier, options.reviewerOverrides, {
|
|
15
|
+
ultra: options.ultra === true,
|
|
16
|
+
...(options.maxTasks !== undefined ? { maxTasks: options.maxTasks } : {}),
|
|
17
|
+
hasGuidelines: Boolean(plan.guidelines?.trim()),
|
|
18
|
+
});
|
|
19
|
+
const { cells } = planned;
|
|
20
|
+
const plannedBundles = new Set();
|
|
21
|
+
const tasks = cells.map((cell) => {
|
|
22
|
+
const files = cell.bundle.files.map((f) => f.newPath);
|
|
23
|
+
const input = {
|
|
24
|
+
reviewer: cell.reviewer,
|
|
25
|
+
changeRequest: plan.changeRequest,
|
|
26
|
+
changedFiles: plan.selected,
|
|
27
|
+
bundle: cell.bundle.files,
|
|
28
|
+
rules: resolveRules(files, plan.repoRules, cell.reviewer.rules),
|
|
29
|
+
guidelines: plan.guidelines,
|
|
30
|
+
accepted: memoryFor(files, plan.memory),
|
|
31
|
+
};
|
|
32
|
+
const prompt = buildReviewPrompt(input);
|
|
33
|
+
const task = {
|
|
34
|
+
taskId: cell.taskId,
|
|
35
|
+
reviewer: cell.reviewer.id,
|
|
36
|
+
bundle: cell.bundle.label,
|
|
37
|
+
files,
|
|
38
|
+
promptTokens: tokens(prompt),
|
|
39
|
+
};
|
|
40
|
+
const key = `${cell.reviewer.id}\0${cell.bundle.label}`;
|
|
41
|
+
if ((options.ultra || isLargeBundle(cell.bundle.files)) && !plannedBundles.has(key)) {
|
|
42
|
+
plannedBundles.add(key);
|
|
43
|
+
task.planPromptTokens = tokens(buildReviewPrompt({ ...input, forPlanning: true }));
|
|
44
|
+
}
|
|
45
|
+
return task;
|
|
46
|
+
});
|
|
47
|
+
const policy = options.bundling ?? defaultBundlePolicy;
|
|
48
|
+
return {
|
|
49
|
+
changeRequest: plan.changeRequest,
|
|
50
|
+
tier: plan.tier,
|
|
51
|
+
selected: plan.selected.map((d) => d.newPath),
|
|
52
|
+
excluded: plan.decisions.flatMap((d) => d.selected ? [] : [{ path: d.diff.newPath, reason: d.reason }]),
|
|
53
|
+
bundles: plan.bundles.map((b) => ({ label: b.label, files: b.files.map((f) => f.newPath) })),
|
|
54
|
+
groupingSkipped: options.grouper === undefined && plan.selected.length >= policy.groupingMinFiles,
|
|
55
|
+
tasks,
|
|
56
|
+
skipped: planned.skipped,
|
|
57
|
+
promptTokens: tasks.reduce((sum, t) => sum + t.promptTokens + (t.planPromptTokens ?? 0), 0),
|
|
58
|
+
planCalls: tasks.filter((t) => t.planPromptTokens !== undefined).length,
|
|
59
|
+
warnings: [
|
|
60
|
+
...plan.warnings,
|
|
61
|
+
...(planned.limited?.length
|
|
62
|
+
? [
|
|
63
|
+
`task limit reached: ${planned.limited.length} review task(s) would be skipped and their files reported as not reviewed`,
|
|
64
|
+
]
|
|
65
|
+
: []),
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Four characters per token: an estimate, not a tokenizer.
|
|
70
|
+
function tokens(prompt) {
|
|
71
|
+
return Math.ceil((prompt.system.length + prompt.user.length) / 4);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=preview.js.map
|