@open-cr-agent/core 0.2.0 → 0.4.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 +1 -1
- package/dist/anchor/relocate.d.ts +3 -2
- package/dist/anchor/relocate.js +10 -3
- package/dist/bundle/grouping.d.ts +1 -0
- package/dist/bundle/grouping.js +2 -2
- package/dist/contracts.d.ts +18 -0
- package/dist/domain.d.ts +26 -3
- package/dist/domain.js +8 -0
- package/dist/errors.d.ts +13 -2
- package/dist/errors.js +41 -3
- package/dist/index.d.ts +25 -17
- package/dist/index.js +16 -17
- package/dist/internal.d.ts +22 -0
- package/dist/internal.js +25 -0
- package/dist/judge/judge.d.ts +2 -1
- package/dist/judge/judge.js +10 -3
- package/dist/judge/prompt.d.ts +1 -0
- package/dist/judge/prompt.js +2 -2
- package/dist/memory/memory.js +3 -2
- package/dist/net/proxied-fetch.d.ts +10 -0
- package/dist/net/proxied-fetch.js +33 -0
- package/dist/pipeline/agents.d.ts +35 -0
- package/dist/pipeline/agents.js +55 -0
- package/dist/pipeline/budget.d.ts +2 -1
- package/dist/pipeline/budget.js +3 -2
- package/dist/pipeline/context.d.ts +3 -1
- package/dist/pipeline/context.js +6 -1
- package/dist/pipeline/coverage.d.ts +6 -0
- package/dist/pipeline/coverage.js +44 -0
- package/dist/pipeline/execute.d.ts +2 -0
- package/dist/pipeline/execute.js +10 -4
- package/dist/pipeline/findings.d.ts +2 -2
- package/dist/pipeline/findings.js +2 -1
- package/dist/pipeline/helpers.d.ts +2 -2
- package/dist/pipeline/helpers.js +11 -4
- package/dist/pipeline/imports.d.ts +12 -0
- package/dist/pipeline/imports.js +126 -0
- package/dist/pipeline/matrix.d.ts +11 -1
- package/dist/pipeline/matrix.js +8 -0
- package/dist/pipeline/output-schema.d.ts +345 -0
- package/dist/pipeline/output-schema.js +181 -0
- package/dist/pipeline/output.d.ts +7 -0
- package/dist/pipeline/output.js +7 -0
- package/dist/pipeline/plan.d.ts +3 -2
- package/dist/pipeline/plan.js +6 -2
- package/dist/pipeline/preview.d.ts +2 -0
- package/dist/pipeline/preview.js +4 -0
- package/dist/pipeline/provenance.d.ts +30 -0
- package/dist/pipeline/provenance.js +90 -0
- package/dist/pipeline/report.d.ts +13 -1
- package/dist/pipeline/report.js +2 -0
- package/dist/pipeline/run-id.d.ts +2 -0
- package/dist/pipeline/run-id.js +13 -0
- package/dist/pipeline/run.d.ts +16 -5
- package/dist/pipeline/run.js +36 -52
- package/dist/pipeline/task.d.ts +6 -1
- package/dist/pipeline/task.js +5 -2
- package/dist/plugin/registry.d.ts +5 -1
- package/dist/plugin/registry.js +12 -2
- package/dist/plugin/types.d.ts +3 -1
- package/dist/review/plan-phase.d.ts +3 -2
- package/dist/review/plan-phase.js +5 -3
- package/dist/rules/repo-rules.js +3 -2
- package/dist/runtime/attempt.d.ts +22 -0
- package/dist/runtime/attempt.js +37 -0
- package/dist/runtime/failback.d.ts +30 -0
- package/dist/runtime/failback.js +160 -0
- package/dist/runtime/models.d.ts +30 -0
- package/dist/runtime/models.js +89 -0
- package/dist/runtime/quota.d.ts +9 -0
- package/dist/runtime/quota.js +38 -0
- package/dist/runtime/tools.d.ts +21 -0
- package/dist/runtime/tools.js +113 -0
- package/dist/sarif/candidates.d.ts +27 -0
- package/dist/sarif/candidates.js +102 -0
- package/dist/sarif/schema.d.ts +144 -0
- package/dist/sarif/schema.js +71 -0
- package/dist/select/select.d.ts +11 -1
- package/dist/select/select.js +10 -0
- package/dist/session/jsonl.d.ts +0 -1
- package/dist/session/jsonl.js +4 -10
- package/dist/verify/prompt.d.ts +1 -0
- package/dist/verify/prompt.js +2 -2
- package/dist/verify/verify.d.ts +2 -1
- package/dist/verify/verify.js +4 -2
- package/package.json +12 -3
- package/dist/anchor/index.d.ts +0 -3
- package/dist/anchor/index.js +0 -3
- package/dist/bundle/index.d.ts +0 -3
- package/dist/bundle/index.js +0 -3
- package/dist/diff/index.d.ts +0 -3
- package/dist/diff/index.js +0 -3
- package/dist/judge/index.d.ts +0 -4
- package/dist/judge/index.js +0 -4
- package/dist/memory/index.d.ts +0 -2
- package/dist/memory/index.js +0 -2
- package/dist/pipeline/index.d.ts +0 -9
- package/dist/pipeline/index.js +0 -9
- package/dist/plugin/index.d.ts +0 -5
- package/dist/plugin/index.js +0 -5
- package/dist/rereview/index.d.ts +0 -4
- package/dist/rereview/index.js +0 -4
- package/dist/review/index.d.ts +0 -10
- package/dist/review/index.js +0 -10
- package/dist/rules/index.d.ts +0 -5
- package/dist/rules/index.js +0 -5
- package/dist/select/index.d.ts +0 -2
- package/dist/select/index.js +0 -2
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/verify/index.d.ts +0 -3
- package/dist/verify/index.js +0 -3
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const EFFORT_LEVELS = [
|
|
2
|
+
"none",
|
|
3
|
+
"minimal",
|
|
4
|
+
"low",
|
|
5
|
+
"medium",
|
|
6
|
+
"high",
|
|
7
|
+
];
|
|
8
|
+
export const AGENT_ROLES = ["verifier", "judge", "helper"];
|
|
9
|
+
export const ROLE_TIERS = {
|
|
10
|
+
verifier: "standard",
|
|
11
|
+
judge: "top",
|
|
12
|
+
helper: "light",
|
|
13
|
+
};
|
|
14
|
+
// A reviewer's effort covers every call made on its behalf: its review
|
|
15
|
+
// tasks and its plan call.
|
|
16
|
+
export function reviewerEffort(reviewer, settings) {
|
|
17
|
+
return settings.reviewerOverrides?.[reviewer.id]?.effort ?? settings.effort?.[reviewer.modelTier];
|
|
18
|
+
}
|
|
19
|
+
export function roleEffort(role, settings) {
|
|
20
|
+
return settings.roles?.[role]?.effort ?? settings.effort?.[ROLE_TIERS[role]];
|
|
21
|
+
}
|
|
22
|
+
// The enabled reviewers and the roles, each with its tier and effort.
|
|
23
|
+
export function resolveAgents(reviewers, settings) {
|
|
24
|
+
const agent = (id, tier, effort) => effort === undefined ? { id, tier } : { id, tier, effort };
|
|
25
|
+
return [
|
|
26
|
+
...reviewers
|
|
27
|
+
.filter((r) => settings.reviewerOverrides?.[r.id]?.enabled !== false)
|
|
28
|
+
.map((r) => agent(r.id, r.modelTier, reviewerEffort(r, settings))),
|
|
29
|
+
...AGENT_ROLES.map((role) => agent(role, ROLE_TIERS[role], roleEffort(role, settings))),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
// The request fields that name an agent and its effort.
|
|
33
|
+
export function agentCall(agent, effort) {
|
|
34
|
+
return effort === undefined ? { agent } : { agent, effort };
|
|
35
|
+
}
|
|
36
|
+
// Said once per run, so a configured effort that never reached a model is
|
|
37
|
+
// not mistaken for one that did.
|
|
38
|
+
export function effortWarnings(agents, runtime) {
|
|
39
|
+
const asked = agents.filter((a) => a.effort !== undefined);
|
|
40
|
+
if (asked.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const { appliedTo } = runtime;
|
|
43
|
+
if (!appliedTo) {
|
|
44
|
+
return [
|
|
45
|
+
`the ${runtime.name} runtime does not apply reasoning effort yet; the effort configured for ${asked.map((a) => a.id).join(", ")} was not sent`,
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const refused = asked.filter((a) => appliedTo.call(runtime, a.id)?.effort === false);
|
|
49
|
+
return refused.length === 0
|
|
50
|
+
? []
|
|
51
|
+
: [
|
|
52
|
+
`the endpoint refused the reasoning effort for ${refused.map((a) => a.id).join(", ")}; those calls were sent again without it`,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Usage } from "../contracts.js";
|
|
2
|
+
import { OcraError } from "../errors.js";
|
|
2
3
|
export declare const REVIEW_BUDGET_SHARE = 0.8;
|
|
3
|
-
export declare class SpendLimitReached extends
|
|
4
|
+
export declare class SpendLimitReached extends OcraError {
|
|
4
5
|
constructor(maxCostUsd: number);
|
|
5
6
|
}
|
|
6
7
|
export interface SpendTracker {
|
package/dist/pipeline/budget.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OcraError } from "../errors.js";
|
|
1
2
|
// Review tasks may spend this share of the limit; the rest is kept for Verify
|
|
2
3
|
// and Judge, so the run that hits the limit still gets its findings checked.
|
|
3
4
|
// Those stages are one short call per file with findings plus one top-tier
|
|
@@ -7,9 +8,9 @@
|
|
|
7
8
|
// and whatever the runtime had not reported yet) comes out of the reserve.
|
|
8
9
|
export const REVIEW_BUDGET_SHARE = 0.8;
|
|
9
10
|
// Why running review tasks were stopped.
|
|
10
|
-
export class SpendLimitReached extends
|
|
11
|
+
export class SpendLimitReached extends OcraError {
|
|
11
12
|
constructor(maxCostUsd) {
|
|
12
|
-
super(`stopped at the spend limit of $${maxCostUsd}`);
|
|
13
|
+
super("BUDGET_EXHAUSTED", `stopped at the spend limit of $${maxCostUsd}`);
|
|
13
14
|
this.name = "SpendLimitReached";
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReviewContext, VcsAdapter } from "../contracts.js";
|
|
2
2
|
import type { FileDiff } from "../domain.js";
|
|
3
|
-
|
|
3
|
+
import { OcraError } from "../errors.js";
|
|
4
|
+
export declare class AccessDeniedError extends OcraError {
|
|
5
|
+
constructor(message: string);
|
|
4
6
|
}
|
|
5
7
|
export declare function reviewContext(vcs: VcsAdapter, diffs: readonly FileDiff[]): ReviewContext;
|
|
6
8
|
//# sourceMappingURL=context.d.ts.map
|
package/dist/pipeline/context.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
+
import { OcraError } from "../errors.js";
|
|
2
3
|
import { isSecretPath } from "../select/select.js";
|
|
3
|
-
export class AccessDeniedError extends
|
|
4
|
+
export class AccessDeniedError extends OcraError {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super("ACCESS_DENIED", message);
|
|
7
|
+
this.name = "AccessDeniedError";
|
|
8
|
+
}
|
|
4
9
|
}
|
|
5
10
|
const WINDOWS = process.platform === "win32";
|
|
6
11
|
// Agents read the repository through this context only, so the policy lives
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FileDecision } from "../select/select.js";
|
|
2
|
+
import type { JobResult } from "./execute.js";
|
|
3
|
+
import type { MatrixCell } from "./matrix.js";
|
|
4
|
+
import type { CoverageEntry } from "./report.js";
|
|
5
|
+
export declare function coverageOf(decisions: readonly FileDecision[], results: readonly JobResult[], unchanged: ReadonlySet<string>, limited: readonly MatrixCell[], notStarted: ReadonlySet<string>): CoverageEntry[];
|
|
6
|
+
//# sourceMappingURL=coverage.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function coverageOf(decisions, results, unchanged, limited, notStarted) {
|
|
2
|
+
// A file is reviewed when every reviewer assigned to it finished at least
|
|
3
|
+
// one of its tasks: under --ultra one completed sample is enough. A
|
|
4
|
+
// reviewer whose task failed makes the file "failed"; one whose task never
|
|
5
|
+
// started (the task or spend limit, a cancelled run) makes it "unreviewed".
|
|
6
|
+
const done = new Map();
|
|
7
|
+
const ran = new Set();
|
|
8
|
+
const key = (reviewer, file) => `${reviewer}\0${file}`;
|
|
9
|
+
for (const { outcome } of results) {
|
|
10
|
+
const started = !notStarted.has(outcome.taskId);
|
|
11
|
+
for (const file of outcome.files) {
|
|
12
|
+
const k = key(outcome.reviewer, file);
|
|
13
|
+
if (started)
|
|
14
|
+
ran.add(k);
|
|
15
|
+
done.set(k, done.get(k) === true || outcome.status === "completed");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const cell of limited) {
|
|
19
|
+
for (const f of cell.bundle.files) {
|
|
20
|
+
const k = key(cell.reviewer.id, f.newPath);
|
|
21
|
+
if (!done.has(k))
|
|
22
|
+
done.set(k, false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const status = new Map();
|
|
26
|
+
for (const [k, completed] of done) {
|
|
27
|
+
const file = k.split("\0")[1];
|
|
28
|
+
const now = completed ? "reviewed" : ran.has(k) ? "failed" : "unreviewed";
|
|
29
|
+
const before = status.get(file);
|
|
30
|
+
// failed outranks unreviewed, which outranks reviewed.
|
|
31
|
+
if (!before || now === "failed" || (now === "unreviewed" && before === "reviewed")) {
|
|
32
|
+
status.set(file, now);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return decisions.map((d) => {
|
|
36
|
+
const path = d.diff.newPath;
|
|
37
|
+
if (!d.selected)
|
|
38
|
+
return { path, status: "excluded", reason: d.reason };
|
|
39
|
+
if (unchanged.has(path))
|
|
40
|
+
return { path, status: "unchanged" };
|
|
41
|
+
return { path, status: status.get(path) ?? "unreviewed" };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=coverage.js.map
|
|
@@ -2,6 +2,7 @@ import { type AnchorContext } from "../anchor/anchor.js";
|
|
|
2
2
|
import type { AgentRuntime, Usage } from "../contracts.js";
|
|
3
3
|
import type { Finding } from "../domain.js";
|
|
4
4
|
import { planBundle } from "../review/plan-phase.js";
|
|
5
|
+
import { type AgentSettings } from "./agents.js";
|
|
5
6
|
import type { MatrixCell } from "./matrix.js";
|
|
6
7
|
import type { ReviewPlan } from "./plan.js";
|
|
7
8
|
import type { ReviewEvent, TaskOutcome } from "./report.js";
|
|
@@ -21,6 +22,7 @@ export interface ExecuteOptions {
|
|
|
21
22
|
emit: (event: ReviewEvent) => void;
|
|
22
23
|
onUsage?: ((usage: Usage) => void) | undefined;
|
|
23
24
|
signal: AbortSignal;
|
|
25
|
+
agents?: AgentSettings;
|
|
24
26
|
}
|
|
25
27
|
type PlannedBundle = Awaited<ReturnType<typeof planBundle>>;
|
|
26
28
|
export declare const PLAN_MIN_FILES = 5;
|
package/dist/pipeline/execute.js
CHANGED
|
@@ -4,6 +4,7 @@ import { findCallers } from "../review/impact.js";
|
|
|
4
4
|
import { planBundle } from "../review/plan-phase.js";
|
|
5
5
|
import { buildReviewPrompt } from "../review/prompt.js";
|
|
6
6
|
import { resolveRules } from "../rules/resolve.js";
|
|
7
|
+
import { reviewerEffort } from "./agents.js";
|
|
7
8
|
import { toFinding } from "./findings.js";
|
|
8
9
|
import { executeTask } from "./task.js";
|
|
9
10
|
import { addUsage } from "./usage.js";
|
|
@@ -28,6 +29,7 @@ export async function runJob(job, plan, options) {
|
|
|
28
29
|
guidelines: plan.guidelines,
|
|
29
30
|
accepted: memoryFor(files, plan.memory),
|
|
30
31
|
};
|
|
32
|
+
const effort = reviewerEffort(job.reviewer, options.agents ?? {});
|
|
31
33
|
const extraUsage = [];
|
|
32
34
|
const extraWarnings = [];
|
|
33
35
|
let prompt = buildReviewPrompt(input);
|
|
@@ -36,7 +38,7 @@ export async function runJob(job, plan, options) {
|
|
|
36
38
|
const key = `${job.reviewer.id}\0${job.bundle.label}`;
|
|
37
39
|
const shared = options.plans?.get(key);
|
|
38
40
|
const planning = shared ??
|
|
39
|
-
planBundle(options.runtime, job.reviewer, buildReviewPrompt({ ...input, forPlanning: true }), options.signal);
|
|
41
|
+
planBundle(options.runtime, job.reviewer, buildReviewPrompt({ ...input, forPlanning: true }), options.signal, effort);
|
|
40
42
|
if (!shared)
|
|
41
43
|
options.plans?.set(key, planning);
|
|
42
44
|
const planned = await planning;
|
|
@@ -62,6 +64,7 @@ export async function runJob(job, plan, options) {
|
|
|
62
64
|
taskId: job.taskId,
|
|
63
65
|
reviewer: job.reviewer.id,
|
|
64
66
|
modelTier: job.reviewer.modelTier,
|
|
67
|
+
...(effort === undefined ? {} : { effort }),
|
|
65
68
|
systemPrompt: prompt.system,
|
|
66
69
|
userPrompt: prompt.user,
|
|
67
70
|
context: plan.context,
|
|
@@ -85,7 +88,7 @@ export async function runJob(job, plan, options) {
|
|
|
85
88
|
// happened to read the file.
|
|
86
89
|
const bundleFiles = new Set(files);
|
|
87
90
|
let outside = 0;
|
|
88
|
-
for (const reported of result.findings) {
|
|
91
|
+
for (const { reported, model } of result.findings) {
|
|
89
92
|
// Relocating a quote on a file outside the bundle would pay for an
|
|
90
93
|
// answer the next check throws away.
|
|
91
94
|
const context = bundleFiles.has(reported.file)
|
|
@@ -101,13 +104,15 @@ export async function runJob(job, plan, options) {
|
|
|
101
104
|
const content = anchor.lineRange
|
|
102
105
|
? await plan.context.readFile(anchor.file).catch(() => undefined)
|
|
103
106
|
: undefined;
|
|
104
|
-
const
|
|
107
|
+
const provenance = model === undefined ? { task: job.taskId } : { task: job.taskId, model };
|
|
108
|
+
const finding = toFinding(reported, job.reviewer.id, provenance, anchor, content);
|
|
105
109
|
findings.push(finding);
|
|
106
110
|
emit({ type: "finding", taskId: job.taskId, finding });
|
|
107
111
|
}
|
|
108
112
|
if (outside > 0) {
|
|
109
113
|
warnings.push(`${job.taskId}: dropped ${outside} finding(s) on files outside its bundle`);
|
|
110
114
|
}
|
|
115
|
+
const usage = extraUsage.reduce(addUsage, result.usage);
|
|
111
116
|
const outcome = {
|
|
112
117
|
taskId: job.taskId,
|
|
113
118
|
reviewer: job.reviewer.id,
|
|
@@ -116,10 +121,11 @@ export async function runJob(job, plan, options) {
|
|
|
116
121
|
status: result.status,
|
|
117
122
|
findings: findings.length,
|
|
118
123
|
durationMs: Date.now() - started,
|
|
124
|
+
usage,
|
|
119
125
|
};
|
|
120
126
|
if (result.error !== undefined)
|
|
121
127
|
outcome.error = result.error;
|
|
122
128
|
emit({ type: "task_finished", outcome });
|
|
123
|
-
return { outcome, findings, usage
|
|
129
|
+
return { outcome, findings, usage, warnings };
|
|
124
130
|
}
|
|
125
131
|
//# sourceMappingURL=execute.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Anchor } from "../anchor/anchor.js";
|
|
2
|
-
import type { Finding, ReportedFinding } from "../domain.js";
|
|
2
|
+
import type { Finding, FindingProvenance, ReportedFinding } from "../domain.js";
|
|
3
3
|
export declare function fingerprint(category: string, file: string, existingCode: string): string;
|
|
4
|
-
export declare function toFinding(reported: ReportedFinding, reviewer: string, anchor: Anchor, content?: string): Finding;
|
|
4
|
+
export declare function toFinding(reported: ReportedFinding, reviewer: string, provenance: FindingProvenance, anchor: Anchor, content?: string): Finding;
|
|
5
5
|
export declare function dedupeFindings(findings: readonly Finding[]): Finding[];
|
|
6
6
|
//# sourceMappingURL=findings.d.ts.map
|
|
@@ -7,13 +7,14 @@ export function fingerprint(category, file, existingCode) {
|
|
|
7
7
|
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
8
8
|
}
|
|
9
9
|
// content is the anchored file at head, when the anchor found lines in it.
|
|
10
|
-
export function toFinding(reported, reviewer, anchor, content) {
|
|
10
|
+
export function toFinding(reported, reviewer, provenance, anchor, content) {
|
|
11
11
|
const finding = {
|
|
12
12
|
...reported,
|
|
13
13
|
file: anchor.file,
|
|
14
14
|
id: randomUUID(),
|
|
15
15
|
fingerprint: fingerprint(reported.category, anchor.file, reported.existingCode),
|
|
16
16
|
reviewer,
|
|
17
|
+
provenance,
|
|
17
18
|
anchor: {
|
|
18
19
|
method: anchor.method,
|
|
19
20
|
inDiff: anchor.inDiff,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FileGrouper } from "../bundle/grouping.js";
|
|
2
|
-
import type { AgentRuntime, Usage } from "../contracts.js";
|
|
2
|
+
import type { AgentRuntime, Effort, Usage } from "../contracts.js";
|
|
3
3
|
export declare const HELPER_TIMEOUT_MS = 60000;
|
|
4
|
-
export declare function runtimeGrouper(runtime: AgentRuntime, signal: AbortSignal, onUsage: (usage: Usage) => void): FileGrouper | undefined;
|
|
4
|
+
export declare function runtimeGrouper(runtime: AgentRuntime, signal: AbortSignal, onUsage: (usage: Usage) => void, effort?: Effort): FileGrouper | undefined;
|
|
5
5
|
export declare function parseJsonAnswer(text: string): unknown;
|
|
6
6
|
//# sourceMappingURL=helpers.d.ts.map
|
package/dist/pipeline/helpers.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { usageSpent } from "../errors.js";
|
|
1
|
+
import { OcraError, usageSpent } from "../errors.js";
|
|
2
|
+
import { agentCall } from "./agents.js";
|
|
2
3
|
export const HELPER_TIMEOUT_MS = 60_000;
|
|
3
4
|
// Grouping runs on the runtime's cheapest tier when the runtime supports plain
|
|
4
5
|
// completions; otherwise bundling falls back to per-file review.
|
|
5
|
-
export function runtimeGrouper(runtime, signal, onUsage) {
|
|
6
|
+
export function runtimeGrouper(runtime, signal, onUsage, effort) {
|
|
6
7
|
const complete = runtime.complete?.bind(runtime);
|
|
7
8
|
if (!complete)
|
|
8
9
|
return undefined;
|
|
9
10
|
return {
|
|
10
11
|
async group(prompt) {
|
|
11
|
-
const result = await complete({
|
|
12
|
+
const result = await complete({
|
|
13
|
+
tier: "light",
|
|
14
|
+
...agentCall("helper", effort),
|
|
15
|
+
system: prompt.system,
|
|
16
|
+
user: prompt.user,
|
|
17
|
+
timeoutMs: HELPER_TIMEOUT_MS,
|
|
18
|
+
}, AbortSignal.any([signal, AbortSignal.timeout(HELPER_TIMEOUT_MS)])).catch((error) => {
|
|
12
19
|
const spent = usageSpent(error);
|
|
13
20
|
if (spent)
|
|
14
21
|
onUsage(spent);
|
|
@@ -25,7 +32,7 @@ export function parseJsonAnswer(text) {
|
|
|
25
32
|
const candidate = (fenced ?? text).trim();
|
|
26
33
|
const start = candidate.search(/[[{]/);
|
|
27
34
|
if (start < 0)
|
|
28
|
-
throw new
|
|
35
|
+
throw new OcraError("RUNTIME_INVALID_OUTPUT", "the model answered without JSON");
|
|
29
36
|
const end = Math.max(candidate.lastIndexOf("]"), candidate.lastIndexOf("}"));
|
|
30
37
|
return JSON.parse(candidate.slice(start, end + 1));
|
|
31
38
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Finding } from "../domain.js";
|
|
2
|
+
import type { SarifLog } from "../sarif/schema.js";
|
|
3
|
+
import type { ReviewPlan } from "./plan.js";
|
|
4
|
+
import type { ReviewEvent, TaskOutcome } from "./report.js";
|
|
5
|
+
export interface SarifImport {
|
|
6
|
+
findings: Finding[];
|
|
7
|
+
outcomes: TaskOutcome[];
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare const MAX_IMPORTED_PER_RUN = 200;
|
|
11
|
+
export declare function importSarif(logs: readonly SarifLog[], plan: Pick<ReviewPlan, "selected" | "context">, emit: (event: ReviewEvent) => void): Promise<SarifImport>;
|
|
12
|
+
//# sourceMappingURL=imports.d.ts.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { anchorFinding } from "../anchor/anchor.js";
|
|
2
|
+
import { sarifCandidates } from "../sarif/candidates.js";
|
|
3
|
+
import { toFinding } from "./findings.js";
|
|
4
|
+
import { boundFinding } from "./task.js";
|
|
5
|
+
import { emptyUsage } from "./usage.js";
|
|
6
|
+
// A hostile log could carry thousands of results on the change.
|
|
7
|
+
export const MAX_IMPORTED_PER_RUN = 200;
|
|
8
|
+
export async function importSarif(logs, plan, emit) {
|
|
9
|
+
const windows = changedLines(plan.selected);
|
|
10
|
+
const imported = { findings: [], outcomes: [], warnings: [] };
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
for (const log of logs) {
|
|
13
|
+
for (const run of log.runs) {
|
|
14
|
+
const { tool, candidates, skipped } = sarifCandidates(run);
|
|
15
|
+
const n = (counts.get(tool.slug) ?? 0) + 1;
|
|
16
|
+
counts.set(tool.slug, n);
|
|
17
|
+
const taskId = `sarif-${tool.slug}-${n}`;
|
|
18
|
+
const started = Date.now();
|
|
19
|
+
const onChange = candidates.filter((c) => touches(windows, c));
|
|
20
|
+
const kept = onChange.slice(0, MAX_IMPORTED_PER_RUN);
|
|
21
|
+
const files = [...new Set(kept.map((c) => c.file))];
|
|
22
|
+
emit({ type: "task_started", taskId, reviewer: tool.slug, bundle: "sarif", files });
|
|
23
|
+
let noCode = 0;
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const candidate of kept) {
|
|
26
|
+
const finding = await toImportedFinding(candidate, tool.slug, taskId, plan);
|
|
27
|
+
if (!finding) {
|
|
28
|
+
noCode += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
findings.push(finding);
|
|
32
|
+
emit({ type: "finding", taskId, finding });
|
|
33
|
+
}
|
|
34
|
+
const outcome = {
|
|
35
|
+
taskId,
|
|
36
|
+
reviewer: tool.slug,
|
|
37
|
+
bundle: "sarif",
|
|
38
|
+
files,
|
|
39
|
+
status: "completed",
|
|
40
|
+
findings: findings.length,
|
|
41
|
+
durationMs: Date.now() - started,
|
|
42
|
+
usage: emptyUsage(),
|
|
43
|
+
};
|
|
44
|
+
emit({ type: "task_finished", outcome });
|
|
45
|
+
imported.findings.push(...findings);
|
|
46
|
+
imported.outcomes.push(outcome);
|
|
47
|
+
imported.warnings.push(...importWarnings(tool.name, {
|
|
48
|
+
imported: findings.length,
|
|
49
|
+
outside: candidates.length - onChange.length,
|
|
50
|
+
overLimit: onChange.length - kept.length,
|
|
51
|
+
noCode,
|
|
52
|
+
...skipped,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return imported;
|
|
57
|
+
}
|
|
58
|
+
async function toImportedFinding(candidate, reviewer, taskId, plan) {
|
|
59
|
+
const content = await plan.context.readFile(candidate.file).catch(() => undefined);
|
|
60
|
+
const existingCode = candidate.snippet ?? quoteFrom(content, candidate.lines);
|
|
61
|
+
if (!existingCode)
|
|
62
|
+
return undefined;
|
|
63
|
+
const reported = boundFinding({
|
|
64
|
+
category: reviewer,
|
|
65
|
+
severity: candidate.severity,
|
|
66
|
+
file: candidate.file,
|
|
67
|
+
existingCode,
|
|
68
|
+
title: candidate.title,
|
|
69
|
+
body: candidate.body,
|
|
70
|
+
evidence: [],
|
|
71
|
+
});
|
|
72
|
+
// The tool's lines are not trusted over the quote: the same anchoring as
|
|
73
|
+
// for a model's finding decides where the comment lands.
|
|
74
|
+
const anchor = await anchorFinding(reported, {
|
|
75
|
+
diffs: plan.selected,
|
|
76
|
+
readNewFile: plan.context.readFile,
|
|
77
|
+
});
|
|
78
|
+
return toFinding(reported, reviewer, { task: taskId }, anchor, content);
|
|
79
|
+
}
|
|
80
|
+
function quoteFrom(content, lines) {
|
|
81
|
+
if (content === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const quote = content
|
|
84
|
+
.split("\n")
|
|
85
|
+
.slice(lines.start - 1, lines.end)
|
|
86
|
+
.join("\n")
|
|
87
|
+
.trim();
|
|
88
|
+
return quote || undefined;
|
|
89
|
+
}
|
|
90
|
+
// The new-side lines each hunk covers, context included.
|
|
91
|
+
function changedLines(diffs) {
|
|
92
|
+
const windows = new Map();
|
|
93
|
+
for (const diff of diffs) {
|
|
94
|
+
const ranges = diff.hunks
|
|
95
|
+
.filter((h) => h.newLines > 0)
|
|
96
|
+
.map((h) => ({ start: h.newStart, end: h.newStart + h.newLines - 1 }));
|
|
97
|
+
if (ranges.length > 0)
|
|
98
|
+
windows.set(diff.newPath, ranges);
|
|
99
|
+
}
|
|
100
|
+
return windows;
|
|
101
|
+
}
|
|
102
|
+
function touches(windows, candidate) {
|
|
103
|
+
const ranges = windows.get(candidate.file);
|
|
104
|
+
if (!ranges)
|
|
105
|
+
return false;
|
|
106
|
+
return ranges.some((r) => candidate.lines.start <= r.end && candidate.lines.end >= r.start);
|
|
107
|
+
}
|
|
108
|
+
function importWarnings(tool, c) {
|
|
109
|
+
const left = [];
|
|
110
|
+
if (c.outside > 0)
|
|
111
|
+
left.push(`${c.outside} outside the change`);
|
|
112
|
+
if (c.overLimit > 0)
|
|
113
|
+
left.push(`${c.overLimit} over the limit of ${MAX_IMPORTED_PER_RUN} per run`);
|
|
114
|
+
if (c.noCode > 0)
|
|
115
|
+
left.push(`${c.noCode} on lines that could not be read`);
|
|
116
|
+
if (c.noLocation > 0)
|
|
117
|
+
left.push(`${c.noLocation} without a file and lines`);
|
|
118
|
+
if (c.unsupportedUri > 0)
|
|
119
|
+
left.push(`${c.unsupportedUri} on paths outside the repository`);
|
|
120
|
+
if (c.noMessage > 0)
|
|
121
|
+
left.push(`${c.noMessage} without a message`);
|
|
122
|
+
if (left.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
return [`${tool}: imported ${c.imported} result(s); left out ${left.join(", ")}`];
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=imports.js.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import type { Bundle } from "../bundle/bundle.js";
|
|
3
|
+
import type { Effort } from "../contracts.js";
|
|
2
4
|
import type { RiskTier } from "../domain.js";
|
|
3
5
|
import type { ReviewerDefinition } from "../review/reviewer.js";
|
|
4
6
|
export declare const RISK_TIERS: readonly RiskTier[];
|
|
5
7
|
export interface ReviewerOverride {
|
|
6
8
|
enabled?: boolean | undefined;
|
|
7
9
|
minTier?: RiskTier | undefined;
|
|
10
|
+
effort?: Effort | undefined;
|
|
8
11
|
}
|
|
9
12
|
export type ReviewerOverrides = Readonly<Record<string, ReviewerOverride>>;
|
|
10
13
|
export interface MatrixCell {
|
|
@@ -12,7 +15,14 @@ export interface MatrixCell {
|
|
|
12
15
|
reviewer: ReviewerDefinition;
|
|
13
16
|
bundle: Bundle;
|
|
14
17
|
}
|
|
15
|
-
export
|
|
18
|
+
export declare const skipReasonSchema: z.ZodEnum<{
|
|
19
|
+
below_tier: "below_tier";
|
|
20
|
+
disabled: "disabled";
|
|
21
|
+
no_guidelines: "no_guidelines";
|
|
22
|
+
no_matching_files: "no_matching_files";
|
|
23
|
+
task_limit: "task_limit";
|
|
24
|
+
}>;
|
|
25
|
+
export type SkipReason = z.infer<typeof skipReasonSchema>;
|
|
16
26
|
export interface SkippedCell {
|
|
17
27
|
reviewer: string;
|
|
18
28
|
bundle: string;
|
package/dist/pipeline/matrix.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import picomatch from "picomatch";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
export const RISK_TIERS = ["trivial", "lite", "full"];
|
|
4
|
+
export const skipReasonSchema = z.enum([
|
|
5
|
+
"disabled",
|
|
6
|
+
"below_tier",
|
|
7
|
+
"no_matching_files",
|
|
8
|
+
"no_guidelines",
|
|
9
|
+
"task_limit",
|
|
10
|
+
]);
|
|
3
11
|
// Deterministic: which reviewer runs on which bundle, so cost grows with the
|
|
4
12
|
// risk of the change instead of bundles × reviewers.
|
|
5
13
|
export function planMatrix(bundles, reviewers, tier, overrides = {}, options = {}) {
|