@open-cr-agent/vcs-platform 0.1.2 → 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.
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -5
- package/dist/internal.d.ts +5 -0
- package/dist/internal.js +7 -0
- package/dist/neutralize.d.ts +5 -0
- package/dist/neutralize.js +117 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +5 -37
- package/dist/review.d.ts +1 -1
- package/dist/review.js +1 -1
- package/dist/state.d.ts +1 -2
- package/dist/state.js +1 -1
- package/package.json +11 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export * from "./review.js";
|
|
5
|
-
export * from "./state.js";
|
|
1
|
+
export type { Bot, InlineFinding, PlatformChangeRequest, PlatformComment, PlatformThread, PublishedFindings, ReviewPlatform, } from "./platform.js";
|
|
2
|
+
export { MAX_ATTEMPTS, type RetryDecision, retryDecision } from "./retry.js";
|
|
3
|
+
export { type CodeSource, type History, PlatformReview, type PlatformReviewOptions, } from "./review.js";
|
|
6
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export * from "./retry.js";
|
|
4
|
-
export * from "./review.js";
|
|
5
|
-
export * from "./state.js";
|
|
1
|
+
export { MAX_ATTEMPTS, retryDecision } from "./retry.js";
|
|
2
|
+
export { PlatformReview, } from "./review.js";
|
|
6
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { safeMarkdown } from "./neutralize.js";
|
|
2
|
+
export { renderSummary } from "./render.js";
|
|
3
|
+
export { declinesFinding } from "./review.js";
|
|
4
|
+
export { MAX_WRITTEN_STATE_CHARS, readState, SUMMARY_MARKER, writeState } from "./state.js";
|
|
5
|
+
//# sourceMappingURL=internal.d.ts.map
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// "@open-cr-agent/vcs-platform/internal": what the platform adapters' tests
|
|
2
|
+
// check against. Not a contract: any release may change it.
|
|
3
|
+
export { safeMarkdown } from "./neutralize.js";
|
|
4
|
+
export { renderSummary } from "./render.js";
|
|
5
|
+
export { declinesFinding } from "./review.js";
|
|
6
|
+
export { MAX_WRITTEN_STATE_CHARS, readState, SUMMARY_MARKER, writeState } from "./state.js";
|
|
7
|
+
//# sourceMappingURL=internal.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Model text is untrusted: it may not close our markup, mention people,
|
|
2
|
+
// pull in images, spell one of ocra's commands (`/ocra …`, which would count
|
|
3
|
+
// if ocra posted as a person with write access), start a line with a slash
|
|
4
|
+
// (GitLab runs a line such as `/merge` or `/approve` in a comment as a quick
|
|
5
|
+
// action, with the rights of the token that posted it), or become a link at
|
|
6
|
+
// all. A pull request can plant an address for a reviewer to repeat (the
|
|
7
|
+
// adversarial probe saw one), and a bot comment lends it credibility.
|
|
8
|
+
// Character references get a zero-width space after their `&` and stay as
|
|
9
|
+
// typed, since both platforms look for mentions after decoding them
|
|
10
|
+
// (`@all`); unlike `&`, it keeps `<` quoted in code readable.
|
|
11
|
+
// Every `@` gets one too, which no mention survives (GitLab usernames may
|
|
12
|
+
// start with `_` or `.`), nor an email address. Inline links lose their `](`,
|
|
13
|
+
// and every `]:` is escaped, so no link reference definition (`[1]: https://…`)
|
|
14
|
+
// can form, at line start or inside a blockquote or list item, and no
|
|
15
|
+
// reference-style link (`[x][1]`, `[x][]`, `[1]`) has anything to resolve to.
|
|
16
|
+
// Addresses of every scheme get a zero-width space after the colon (GitLab
|
|
17
|
+
// links `smb://` and `vscode://` too), and `www.` one before its dot, so they
|
|
18
|
+
// read the same and stay text.
|
|
19
|
+
//
|
|
20
|
+
// Code is left as written, so that a reader who copies what a finding quotes
|
|
21
|
+
// gets the code and not our zero-width spaces: nothing inside a code span
|
|
22
|
+
// renders as a mention, link, image or HTML on either platform, and GitLab's
|
|
23
|
+
// quick-action extractor skips code blocks and inline code. Code is what
|
|
24
|
+
// CommonMark would render as code where the text is posted, and only what
|
|
25
|
+
// both platforms agree on: a backtick fence that starts a line of the posted
|
|
26
|
+
// comment and is closed, and an inline backtick run closed on the same line by
|
|
27
|
+
// a run of the same length. A `~~~` fence, an indented block and a span across
|
|
28
|
+
// lines keep the text rules. An unbalanced backtick run is text; it is escaped
|
|
29
|
+
// so that it cannot pair with a backtick in a later piece of the same comment
|
|
30
|
+
// and open a span that we did not neutralize. Two things stay neutralized in
|
|
31
|
+
// code because ocra reads them from the raw body, not the rendering: an HTML
|
|
32
|
+
// comment opener (ocra's markers) and `/ocra` (its commands).
|
|
33
|
+
const CHARACTER_REFERENCE = /&(?=#\d{1,7};|#[xX][\da-fA-F]{1,6};|[A-Za-z][A-Za-z\d]{1,31};)/g;
|
|
34
|
+
const FENCE_OPENER = /^ {0,3}(`{3,})[^`]*$/;
|
|
35
|
+
// A backtick string; `\`` is an escaped backtick, so never part of one.
|
|
36
|
+
const BACKTICK_RUN = /\\`|`+/g;
|
|
37
|
+
export function safeMarkdown(text, { startsLine = false } = {}) {
|
|
38
|
+
const lines = text.split("\n");
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
const line = lines[i];
|
|
42
|
+
const opener = i > 0 || startsLine ? FENCE_OPENER.exec(line) : null;
|
|
43
|
+
const end = opener ? closingFence(lines, i + 1, opener[1].length) : -1;
|
|
44
|
+
if (end === -1) {
|
|
45
|
+
out.push(neutralizeInline(line));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (let j = i; j <= end; j++)
|
|
49
|
+
out.push(neutralizeCode(lines[j]));
|
|
50
|
+
i = end;
|
|
51
|
+
}
|
|
52
|
+
return out.join("\n");
|
|
53
|
+
}
|
|
54
|
+
function closingFence(lines, from, length) {
|
|
55
|
+
const closer = new RegExp(`^ {0,3}\`{${length},}[ \\t]*$`);
|
|
56
|
+
for (let j = from; j < lines.length; j++)
|
|
57
|
+
if (closer.test(lines[j]))
|
|
58
|
+
return j;
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
// One line: code spans per CommonMark's pairing of backtick strings, the
|
|
62
|
+
// text between them neutralized, unpaired strings escaped.
|
|
63
|
+
function neutralizeInline(line) {
|
|
64
|
+
const runs = [...line.matchAll(BACKTICK_RUN)]
|
|
65
|
+
.filter((m) => m[0] !== "\\`")
|
|
66
|
+
.map((m) => ({ start: m.index, end: m.index + m[0].length }));
|
|
67
|
+
const out = [];
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
let text = "";
|
|
70
|
+
const flushText = (upTo) => {
|
|
71
|
+
text += line.slice(cursor, upTo);
|
|
72
|
+
cursor = upTo;
|
|
73
|
+
};
|
|
74
|
+
const emitText = () => {
|
|
75
|
+
if (text !== "")
|
|
76
|
+
out.push(neutralizeText(text, out.length === 0));
|
|
77
|
+
text = "";
|
|
78
|
+
};
|
|
79
|
+
for (let k = 0; k < runs.length; k++) {
|
|
80
|
+
const open = runs[k];
|
|
81
|
+
const length = open.end - open.start;
|
|
82
|
+
const closeAt = runs.findIndex((r, j) => j > k && r.end - r.start === length);
|
|
83
|
+
if (closeAt === -1) {
|
|
84
|
+
flushText(open.start);
|
|
85
|
+
text += "\\`".repeat(length);
|
|
86
|
+
cursor = open.end;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const close = runs[closeAt];
|
|
90
|
+
flushText(open.start);
|
|
91
|
+
emitText();
|
|
92
|
+
out.push(neutralizeCode(line.slice(open.start, close.end)));
|
|
93
|
+
cursor = close.end;
|
|
94
|
+
k = closeAt;
|
|
95
|
+
}
|
|
96
|
+
flushText(line.length);
|
|
97
|
+
emitText();
|
|
98
|
+
return out.join("");
|
|
99
|
+
}
|
|
100
|
+
function neutralizeText(text, atLineStart) {
|
|
101
|
+
const safe = text
|
|
102
|
+
.replace(CHARACTER_REFERENCE, "&")
|
|
103
|
+
.replaceAll("<!--", "<!--")
|
|
104
|
+
.replace(/<\/?[a-zA-Z][^>]*>/g, (tag) => tag.replaceAll("<", "<"))
|
|
105
|
+
.replaceAll("@", "@")
|
|
106
|
+
.replace(/!\[/g, "
|
|
111
|
+
.replaceAll("]:", "]\\:");
|
|
112
|
+
return atLineStart ? safe.replace(/^([ \t]*)\//, "$1/") : safe;
|
|
113
|
+
}
|
|
114
|
+
function neutralizeCode(code) {
|
|
115
|
+
return code.replaceAll("<!--", "<!--").replace(/\/(?=ocra)/gi, "/");
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=neutralize.js.map
|
package/dist/render.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export interface PlatformText {
|
|
|
4
4
|
changeRequest: string;
|
|
5
5
|
authority: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function safeMarkdown(text: string): string;
|
|
8
7
|
export declare function codeSpan(text: string): string;
|
|
9
8
|
export declare const FINDING_MARKER: RegExp;
|
|
10
9
|
export declare function inlineBody(f: Finding): string;
|
package/dist/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { coverageGaps, } from "@open-cr-agent/core";
|
|
2
|
+
import { safeMarkdown } from "./neutralize.js";
|
|
2
3
|
import { SUMMARY_MARKER, writeState } from "./state.js";
|
|
3
4
|
// GitHub rejects comments over 65,536 characters; GitLab takes 1,000,000,
|
|
4
5
|
// but a summary that long is no use to anyone.
|
|
@@ -52,39 +53,6 @@ const VERDICT = {
|
|
|
52
53
|
minor_issues: "⚠️ Minor issues",
|
|
53
54
|
significant_concerns: "🛑 Significant concerns",
|
|
54
55
|
};
|
|
55
|
-
// Model text is untrusted: it may not close our markup, mention people,
|
|
56
|
-
// pull in images, spell one of ocra's commands (`/ocra …`, which would count
|
|
57
|
-
// if ocra posted as a person with write access), start a line with a slash
|
|
58
|
-
// (GitLab runs a line such as `/merge` or `/approve` in a comment as a quick
|
|
59
|
-
// action, with the rights of the token that posted it), or become a link at
|
|
60
|
-
// all. A pull request can plant an address for a reviewer to repeat (the
|
|
61
|
-
// adversarial probe saw one), and a bot comment lends it credibility.
|
|
62
|
-
// Character references get a zero-width space after their `&` and stay as
|
|
63
|
-
// typed, since both platforms look for mentions after decoding them
|
|
64
|
-
// (`@all`); unlike `&`, it keeps `<` quoted in code readable.
|
|
65
|
-
// Every `@` gets one too, which no mention survives (GitLab usernames may
|
|
66
|
-
// start with `_` or `.`), nor an email address. Inline links lose their `](`,
|
|
67
|
-
// and every `]:` is escaped, so no link reference definition (`[1]: https://…`)
|
|
68
|
-
// can form, at line start or inside a blockquote or list item, and no
|
|
69
|
-
// reference-style link (`[x][1]`, `[x][]`, `[1]`) has anything to resolve to.
|
|
70
|
-
// Addresses of every scheme get a zero-width space after the colon (GitLab
|
|
71
|
-
// links `smb://` and `vscode://` too), and `www.` one before its dot, so they
|
|
72
|
-
// read the same and stay text.
|
|
73
|
-
const CHARACTER_REFERENCE = /&(?=#\d{1,7};|#[xX][\da-fA-F]{1,6};|[A-Za-z][A-Za-z\d]{1,31};)/g;
|
|
74
|
-
export function safeMarkdown(text) {
|
|
75
|
-
return text
|
|
76
|
-
.replace(CHARACTER_REFERENCE, "&\u200b")
|
|
77
|
-
.replaceAll("<!--", "<!--")
|
|
78
|
-
.replace(/<\/?[a-zA-Z][^>]*>/g, (tag) => tag.replaceAll("<", "<"))
|
|
79
|
-
.replaceAll("@", "@\u200b")
|
|
80
|
-
.replace(/!\[/g, "!\u200b[")
|
|
81
|
-
.replace(/\/(?=ocra)/gi, "/\u200b")
|
|
82
|
-
.replace(/^([ \t]*)\//gm, "$1\u200b/")
|
|
83
|
-
.replace(/:(?=\/\/)/g, ":\u200b")
|
|
84
|
-
.replace(/\bwww(?=\.)/gi, "www\u200b")
|
|
85
|
-
.replaceAll("](", "]\\(")
|
|
86
|
-
.replaceAll("]:", "]\\:");
|
|
87
|
-
}
|
|
88
56
|
// File paths come from the diff, so the author controls them: a backtick or
|
|
89
57
|
// newline must not end the code span and let markup through, and angle
|
|
90
58
|
// brackets must not form ocra's HTML-comment markers in the raw body.
|
|
@@ -108,7 +76,7 @@ export function inlineBody(f) {
|
|
|
108
76
|
`<!-- ocra:finding ${f.fingerprint} -->`,
|
|
109
77
|
`${ICON[f.severity]} **${safeMarkdown(f.title)}** · ${f.severity} · ${verification(f)} · ${f.reviewer}${f.lowConfidence ? " · low confidence" : ""}`,
|
|
110
78
|
"",
|
|
111
|
-
safeMarkdown(f.body),
|
|
79
|
+
safeMarkdown(f.body, { startsLine: true }),
|
|
112
80
|
];
|
|
113
81
|
if (f.suggestion)
|
|
114
82
|
parts.push("", `**Suggestion:** ${safeMarkdown(f.suggestion)}`);
|
|
@@ -122,7 +90,7 @@ export function renderSummary({ report, commented, state, text, unattributed = 0
|
|
|
122
90
|
SUMMARY_MARKER,
|
|
123
91
|
`## ocra review · ${headline(report)}`,
|
|
124
92
|
"",
|
|
125
|
-
safeMarkdown(report.summary),
|
|
93
|
+
safeMarkdown(report.summary, { startsLine: true }),
|
|
126
94
|
...overrideNote(report, text),
|
|
127
95
|
"",
|
|
128
96
|
`**${report.findings.length} finding(s)** (${counts}) · risk tier \`${report.tier}\``,
|
|
@@ -158,7 +126,7 @@ export function renderSummary({ report, commented, state, text, unattributed = 0
|
|
|
158
126
|
if (inSummary.length > 0) {
|
|
159
127
|
lines.push("", "### Findings outside the diff");
|
|
160
128
|
for (const f of inSummary) {
|
|
161
|
-
lines.push(`- ${ICON[f.severity]} ${location(f)} **${safeMarkdown(f.title)}** _(${verification(f)}${f.lowConfidence ? ", low confidence" : ""})_: ${safeMarkdown(f.body
|
|
129
|
+
lines.push(`- ${ICON[f.severity]} ${location(f)} **${safeMarkdown(f.title)}** _(${verification(f)}${f.lowConfidence ? ", low confidence" : ""})_: ${safeMarkdown(f.body.replaceAll("\n", " "))}`);
|
|
162
130
|
}
|
|
163
131
|
}
|
|
164
132
|
if (report.remembered.length > 0) {
|
|
@@ -192,7 +160,7 @@ export function renderSummary({ report, commented, state, text, unattributed = 0
|
|
|
192
160
|
}
|
|
193
161
|
const failed = report.coverage.filter((c) => c.status === "failed" || c.status === "unreviewed");
|
|
194
162
|
const { costUsd, inputTokens, outputTokens } = report.usage;
|
|
195
|
-
lines.push("", "_The verdict is advice from language models that read the change itself, and can be swayed by text in it. Do not use it as a security gate._", "", "<details><summary>Coverage and cost</summary>", "", `${report.coverage.filter((c) => c.status === "reviewed").length} reviewed · ${report.coverage.filter((c) => c.status === "unchanged").length} unchanged since the last review · ${failed.length} not reviewed · ${report.coverage.filter((c) => c.status === "excluded").length} excluded · ${inputTokens} in / ${outputTokens} out tokens · $${costUsd.toFixed(4)}${spendLimitNote(report)}`, ...failed.map((c) => `- not reviewed: ${codeSpan(c.path)}`), "", "</details>");
|
|
163
|
+
lines.push("", "_The verdict is advice from language models that read the change itself, and can be swayed by text in it. Do not use it as a security gate._", "", "<details><summary>Coverage and cost</summary>", "", `${report.coverage.filter((c) => c.status === "reviewed").length} reviewed · ${report.coverage.filter((c) => c.status === "unchanged").length} unchanged since the last review · ${failed.length} not reviewed · ${report.coverage.filter((c) => c.status === "excluded").length} excluded · ${inputTokens} in / ${outputTokens} out tokens · $${costUsd.toFixed(4)}${spendLimitNote(report)} · run ${codeSpan(report.runId)}`, ...failed.map((c) => `- not reviewed: ${codeSpan(c.path)}`), "", "</details>");
|
|
196
164
|
const markdown = lines.join("\n");
|
|
197
165
|
const footer = `\n\n${writeState(state)}`;
|
|
198
166
|
const room = MAX_SUMMARY_CHARS - footer.length;
|
package/dist/review.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChangeRequest, CodeMatch, FileDiff, PriorReview, ReviewReport, VcsAdapter } from "@open-cr-agent/core";
|
|
2
2
|
import type { ReviewPlatform } from "./platform.js";
|
|
3
3
|
export type CodeSource = Pick<VcsAdapter, "getDiff" | "readFile" | "searchCode">;
|
|
4
4
|
export interface History {
|
package/dist/review.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorMessage
|
|
1
|
+
import { errorMessage } from "@open-cr-agent/core/internal";
|
|
2
2
|
import { FINDING_MARKER, inlineBody, renderSummary } from "./render.js";
|
|
3
3
|
import { lostProgress, readState, SUMMARY_MARKER } from "./state.js";
|
|
4
4
|
const OVERRIDE = /^\/ocra override ([0-9a-fA-F]{40}(?:[0-9a-fA-F]{24})?)\s+(\S.*)$/m;
|
package/dist/state.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { PriorFinding } from "@open-cr-agent/core";
|
|
2
|
-
import { type RiskTier } from "@open-cr-agent/core";
|
|
1
|
+
import type { PriorFinding, RiskTier } from "@open-cr-agent/core";
|
|
3
2
|
export declare const SUMMARY_MARKER = "<!-- ocra:review -->";
|
|
4
3
|
export declare const MAX_WRITTEN_STATE_CHARS = 30000;
|
|
5
4
|
export interface ReviewState {
|
package/dist/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RISK_TIERS, severitySchema, verificationSchema } from "@open-cr-agent/core";
|
|
1
|
+
import { RISK_TIERS, severitySchema, verificationSchema } from "@open-cr-agent/core/internal";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
export const SUMMARY_MARKER = "<!-- ocra:review -->";
|
|
4
4
|
// Only a state block that ends the comment counts: author-controlled text
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-cr-agent/vcs-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "What Open-CR-Agent's code review platform adapters share: the review summary, its state, and who may command ocra",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -19,18 +19,26 @@
|
|
|
19
19
|
"node": ">=22.19"
|
|
20
20
|
},
|
|
21
21
|
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
22
23
|
"exports": {
|
|
23
24
|
".": {
|
|
25
|
+
"@open-cr-agent/source": "./src/index.ts",
|
|
24
26
|
"types": "./dist/index.d.ts",
|
|
25
27
|
"default": "./dist/index.js"
|
|
26
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"./internal": {
|
|
30
|
+
"@open-cr-agent/source": "./src/internal.ts",
|
|
31
|
+
"types": "./dist/internal.d.ts",
|
|
32
|
+
"default": "./dist/internal.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
27
35
|
},
|
|
28
36
|
"files": [
|
|
29
37
|
"dist",
|
|
30
38
|
"!dist/**/*.map"
|
|
31
39
|
],
|
|
32
40
|
"dependencies": {
|
|
33
|
-
"@open-cr-agent/core": "0.
|
|
41
|
+
"@open-cr-agent/core": "0.3.0",
|
|
34
42
|
"zod": "^4.6.5"
|
|
35
43
|
},
|
|
36
44
|
"publishConfig": {
|