@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 CHANGED
@@ -1,6 +1,4 @@
1
- export * from "./platform.js";
2
- export * from "./render.js";
3
- export * from "./retry.js";
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 * from "./platform.js";
2
- export * from "./render.js";
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
@@ -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,5 @@
1
+ export interface Placement {
2
+ startsLine?: boolean;
3
+ }
4
+ export declare function safeMarkdown(text: string, { startsLine }?: Placement): string;
5
+ //# sourceMappingURL=neutralize.d.ts.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("<!--", "&lt;!--")
104
+ .replace(/<\/?[a-zA-Z][^>]*>/g, (tag) => tag.replaceAll("<", "&lt;"))
105
+ .replaceAll("@", "@​")
106
+ .replace(/!\[/g, "!​[")
107
+ .replace(/\/(?=ocra)/gi, "/​")
108
+ .replace(/:(?=\/\/)/g, ":​")
109
+ .replace(/\bwww(?=\.)/gi, "www​")
110
+ .replaceAll("](", "]\\(")
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
- // (`&#64;all`); unlike `&amp;`, it keeps `&lt;` 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("<!--", "&lt;!--")
78
- .replace(/<\/?[a-zA-Z][^>]*>/g, (tag) => tag.replaceAll("<", "&lt;"))
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).replaceAll("\n", " ")}`);
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 { type ChangeRequest, type CodeMatch, type FileDiff, type PriorReview, type ReviewReport, type VcsAdapter } from "@open-cr-agent/core";
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, } from "@open-cr-agent/core";
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.1.2",
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.1.2",
41
+ "@open-cr-agent/core": "0.3.0",
34
42
  "zod": "^4.6.5"
35
43
  },
36
44
  "publishConfig": {