@usepipr/runtime 0.3.0 → 0.3.2
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/commands-4n3t4t5P.d.mts.map +1 -1
- package/dist/{commands-CYh8Au5C.mjs → commands-CQg9QVZt.mjs} +1030 -89
- package/dist/commands-CQg9QVZt.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/internal/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/commands-CYh8Au5C.mjs.map +0 -1
|
@@ -768,13 +768,11 @@ export default definePipr((pipr) => {
|
|
|
768
768
|
const result = await ctx.pi.run(changelog, { manifest });
|
|
769
769
|
await ctx.comment(
|
|
770
770
|
[
|
|
771
|
-
"## Changelog Draft",
|
|
772
|
-
"",
|
|
773
771
|
\`**Category:** \${result.category}\`,
|
|
774
772
|
"",
|
|
775
773
|
result.entry,
|
|
776
774
|
"",
|
|
777
|
-
"
|
|
775
|
+
"## Rationale",
|
|
778
776
|
result.rationale,
|
|
779
777
|
].join("\\n"),
|
|
780
778
|
);
|
|
@@ -869,23 +867,36 @@ export default definePipr((pipr) => {
|
|
|
869
867
|
Return only actionable findings that target valid diff ranges.
|
|
870
868
|
\`,
|
|
871
869
|
timeout: "10m",
|
|
872
|
-
comment: (result, context) =>
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
?
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
870
|
+
comment: (result, context) => {
|
|
871
|
+
const inlineFindingSummary =
|
|
872
|
+
result.inlineFindings.length === 0
|
|
873
|
+
? "No inline findings."
|
|
874
|
+
: "See inline comments in the diff.";
|
|
875
|
+
const localInlineFindingSummary = [
|
|
876
|
+
"## Inline Findings",
|
|
877
|
+
"",
|
|
878
|
+
result.inlineFindings.length === 0
|
|
879
|
+
? "No inline findings."
|
|
880
|
+
: result.inlineFindings.map((finding) => \`- \${finding.body}\`).join("\\n"),
|
|
881
|
+
].join("\\n");
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
main: [
|
|
885
|
+
"## Summary",
|
|
886
|
+
"",
|
|
887
|
+
result.summary.body,
|
|
888
|
+
"",
|
|
889
|
+
"## Review Result",
|
|
890
|
+
"",
|
|
891
|
+
"| Signal | Result |",
|
|
892
|
+
"| --- | ---: |",
|
|
893
|
+
\`| Inline findings | \${result.inlineFindings.length} |\`,
|
|
894
|
+
"",
|
|
895
|
+
context.platform.id === "local" ? localInlineFindingSummary : inlineFindingSummary,
|
|
896
|
+
].join("\\n"),
|
|
897
|
+
inlineFindings: result.inlineFindings,
|
|
898
|
+
};
|
|
899
|
+
},
|
|
889
900
|
});
|
|
890
901
|
});
|
|
891
902
|
`
|
|
@@ -948,20 +959,18 @@ export default definePipr((pipr) => {
|
|
|
948
959
|
};
|
|
949
960
|
const manifest = await ctx.change.diffManifest({ compressed: true, paths });
|
|
950
961
|
if (manifest.files.length === 0) {
|
|
951
|
-
await ctx.comment("
|
|
962
|
+
await ctx.comment("No dependency files changed.");
|
|
952
963
|
return;
|
|
953
964
|
}
|
|
954
965
|
const result = await ctx.pi.run(dependencyReviewer, { manifest }, { paths });
|
|
955
966
|
await ctx.comment(
|
|
956
967
|
[
|
|
957
|
-
"## Dependency Risk",
|
|
958
|
-
"",
|
|
959
968
|
result.summary,
|
|
960
969
|
"",
|
|
961
|
-
"
|
|
970
|
+
"## Risks",
|
|
962
971
|
...result.risks.map((risk) => \`- \${risk}\`),
|
|
963
972
|
"",
|
|
964
|
-
"
|
|
973
|
+
"## Follow-ups",
|
|
965
974
|
...result.followUps.map((followUp) => \`- \${followUp}\`),
|
|
966
975
|
].join("\\n"),
|
|
967
976
|
);
|
|
@@ -1033,7 +1042,7 @@ export default definePipr((pipr) => {
|
|
|
1033
1042
|
...(diagnostic.suggestedFix ? { suggestedFix: diagnostic.suggestedFix } : {}),
|
|
1034
1043
|
}));
|
|
1035
1044
|
await ctx.comment({
|
|
1036
|
-
main:
|
|
1045
|
+
main: result.summary,
|
|
1037
1046
|
inlineFindings,
|
|
1038
1047
|
});
|
|
1039
1048
|
},
|
|
@@ -1045,6 +1054,268 @@ export default definePipr((pipr) => {
|
|
|
1045
1054
|
`
|
|
1046
1055
|
};
|
|
1047
1056
|
//#endregion
|
|
1057
|
+
//#region src/config/recipes/fix-suggestions.ts
|
|
1058
|
+
const fixSuggestionsRecipe = {
|
|
1059
|
+
id: "fix-suggestions",
|
|
1060
|
+
title: "Fix Suggestions",
|
|
1061
|
+
description: "Command-first exact suggested fixes for actionable review improvements.",
|
|
1062
|
+
sourceTools: [
|
|
1063
|
+
"Qodo Merge /improve",
|
|
1064
|
+
"GitHub Copilot code review",
|
|
1065
|
+
"Cursor Bugbot"
|
|
1066
|
+
],
|
|
1067
|
+
configTs: `import { definePipr, z } from "@usepipr/sdk";
|
|
1068
|
+
import type { CommentableRange, DiffManifest, ReviewFinding } from "@usepipr/sdk";
|
|
1069
|
+
|
|
1070
|
+
export default definePipr((pipr) => {
|
|
1071
|
+
const model = pipr.model({
|
|
1072
|
+
provider: "deepseek",
|
|
1073
|
+
model: "deepseek-v4-pro",
|
|
1074
|
+
apiKey: pipr.secret({ name: "DEEPSEEK_API_KEY" }),
|
|
1075
|
+
options: { thinking: "high" },
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
pipr.config({ publication: { maxInlineComments: 6 } });
|
|
1079
|
+
|
|
1080
|
+
const fixSuggestionSchema = z.strictObject({
|
|
1081
|
+
title: z.string(),
|
|
1082
|
+
category: z.enum(["correctness", "tests", "maintainability", "typing", "documentation"]),
|
|
1083
|
+
body: z.string(),
|
|
1084
|
+
path: z.string(),
|
|
1085
|
+
rangeId: z.string(),
|
|
1086
|
+
side: z.enum(["RIGHT", "LEFT"]),
|
|
1087
|
+
startLine: z.number().int().positive(),
|
|
1088
|
+
endLine: z.number().int().positive(),
|
|
1089
|
+
suggestedFix: z.string().min(1),
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
type FixSuggestion = z.infer<typeof fixSuggestionSchema>;
|
|
1093
|
+
|
|
1094
|
+
const fixSuggestionOutput = pipr.schema({
|
|
1095
|
+
id: "review/fix-suggestions",
|
|
1096
|
+
schema: z.strictObject({
|
|
1097
|
+
summary: z.string(),
|
|
1098
|
+
suggestions: z.array(fixSuggestionSchema),
|
|
1099
|
+
}),
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const fixer = pipr.agent({
|
|
1103
|
+
name: "fix-suggestions",
|
|
1104
|
+
model,
|
|
1105
|
+
instructions: \`
|
|
1106
|
+
Find exact suggested changes for this pull request. Return a suggestion only
|
|
1107
|
+
when suggestedFix is a precise replacement for the selected diff range and
|
|
1108
|
+
the reviewer can apply it directly. Prioritize correctness, missing tests,
|
|
1109
|
+
type safety, and small maintainability improvements. Do not report broad
|
|
1110
|
+
refactors, style preferences, or issues without an exact patch.
|
|
1111
|
+
\`,
|
|
1112
|
+
output: fixSuggestionOutput,
|
|
1113
|
+
tools: pipr.tools.readOnly,
|
|
1114
|
+
retry: { invalidOutput: 1, transientFailure: 1 },
|
|
1115
|
+
timeout: "7m",
|
|
1116
|
+
prompt: () => "Find exact suggested changes for this pull request.",
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const task = pipr.task({
|
|
1120
|
+
name: "fix-suggestions",
|
|
1121
|
+
async run(ctx) {
|
|
1122
|
+
if (!ctx.command) {
|
|
1123
|
+
throw new Error("fix-suggestions is a command-only task");
|
|
1124
|
+
}
|
|
1125
|
+
const manifest = await ctx.change.diffManifest({ compressed: true });
|
|
1126
|
+
const result = await ctx.pi.run(fixer, { manifest });
|
|
1127
|
+
const publishableSuggestions = result.suggestions.filter(
|
|
1128
|
+
(suggestion) => isPublishableSuggestion(suggestion, manifest),
|
|
1129
|
+
);
|
|
1130
|
+
const inlineFindings: ReviewFinding[] = publishableSuggestions.map((suggestion) => {
|
|
1131
|
+
const category = suggestion.category
|
|
1132
|
+
.replaceAll("-", " ")
|
|
1133
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
1134
|
+
return {
|
|
1135
|
+
body: \`**\${category}:** \${suggestion.title}. \${suggestion.body}\`,
|
|
1136
|
+
path: suggestion.path,
|
|
1137
|
+
rangeId: suggestion.rangeId,
|
|
1138
|
+
side: suggestion.side,
|
|
1139
|
+
startLine: suggestion.startLine,
|
|
1140
|
+
endLine: suggestion.endLine,
|
|
1141
|
+
suggestedFix: suggestion.suggestedFix,
|
|
1142
|
+
};
|
|
1143
|
+
});
|
|
1144
|
+
await ctx.comment({
|
|
1145
|
+
main: [
|
|
1146
|
+
result.summary,
|
|
1147
|
+
"",
|
|
1148
|
+
"## Exact Suggested Changes",
|
|
1149
|
+
"",
|
|
1150
|
+
suggestionsTable(publishableSuggestions),
|
|
1151
|
+
].join("\\n"),
|
|
1152
|
+
inlineFindings,
|
|
1153
|
+
});
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
pipr.command({
|
|
1158
|
+
pattern: "@pipr improve",
|
|
1159
|
+
permission: "write",
|
|
1160
|
+
description: "Find exact suggested fixes for this pull request.",
|
|
1161
|
+
task,
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
type FindingAnchor = Pick<ReviewFinding, "path" | "rangeId" | "side" | "startLine" | "endLine">;
|
|
1166
|
+
|
|
1167
|
+
function isPublishableSuggestion(suggestion: FixSuggestion, manifest: DiffManifest): boolean {
|
|
1168
|
+
if (suggestion.suggestedFix.trim().length === 0) {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
const range = commentableRangeForFinding(suggestion, manifest);
|
|
1172
|
+
return Boolean(
|
|
1173
|
+
range &&
|
|
1174
|
+
isPublishableSuggestedFixSelection({
|
|
1175
|
+
side: suggestion.side,
|
|
1176
|
+
kind: range.kind,
|
|
1177
|
+
rangeStartLine: range.startLine,
|
|
1178
|
+
startLine: suggestion.startLine,
|
|
1179
|
+
endLine: suggestion.endLine,
|
|
1180
|
+
preview: range.preview,
|
|
1181
|
+
suggestedFix: suggestion.suggestedFix,
|
|
1182
|
+
}),
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function commentableRangeForFinding(
|
|
1187
|
+
finding: FindingAnchor,
|
|
1188
|
+
manifest: DiffManifest,
|
|
1189
|
+
): CommentableRange | undefined {
|
|
1190
|
+
for (const file of manifest.files) {
|
|
1191
|
+
const range = file.commentableRanges.find((candidate) => candidate.id === finding.rangeId);
|
|
1192
|
+
if (!range) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
return finding.rangeId === range.id &&
|
|
1196
|
+
finding.path === range.path &&
|
|
1197
|
+
finding.side === range.side &&
|
|
1198
|
+
finding.startLine <= finding.endLine &&
|
|
1199
|
+
finding.startLine >= range.startLine &&
|
|
1200
|
+
finding.endLine <= range.endLine
|
|
1201
|
+
? range
|
|
1202
|
+
: undefined;
|
|
1203
|
+
}
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function isPublishableSuggestedFixSelection(selection: {
|
|
1208
|
+
side: "RIGHT" | "LEFT";
|
|
1209
|
+
kind: "added" | "deleted" | "context" | "mixed";
|
|
1210
|
+
rangeStartLine: number;
|
|
1211
|
+
startLine: number;
|
|
1212
|
+
endLine: number;
|
|
1213
|
+
preview?: string;
|
|
1214
|
+
suggestedFix: string;
|
|
1215
|
+
}): boolean {
|
|
1216
|
+
const suggestedLines = normalizedSuggestedFixLines(selection.suggestedFix);
|
|
1217
|
+
const selectedLineCount = selection.endLine - selection.startLine + 1;
|
|
1218
|
+
if (
|
|
1219
|
+
selection.side !== "RIGHT" ||
|
|
1220
|
+
selection.kind === "deleted" ||
|
|
1221
|
+
selectedLineCount > 12 ||
|
|
1222
|
+
suggestedLines.length > 20
|
|
1223
|
+
) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const originalLines = selectedPreviewLines(selection, selectedLineCount);
|
|
1228
|
+
return Boolean(
|
|
1229
|
+
originalLines &&
|
|
1230
|
+
!hasUnchangedSelectionEdge(originalLines, suggestedLines) &&
|
|
1231
|
+
!suggestionIncludesUnselectedContext(selection, selectedLineCount, suggestedLines),
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function normalizedSuggestedFixLines(value: string): string[] {
|
|
1236
|
+
const normalized = value.replace(/\\r\\n/g, "\\n").replace(/\\r/g, "\\n");
|
|
1237
|
+
const withoutFinalNewline = normalized.endsWith("\\n") ? normalized.slice(0, -1) : normalized;
|
|
1238
|
+
return withoutFinalNewline.length === 0 ? [] : withoutFinalNewline.split("\\n");
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function selectedPreviewLines(
|
|
1242
|
+
selection: {
|
|
1243
|
+
rangeStartLine: number;
|
|
1244
|
+
startLine: number;
|
|
1245
|
+
preview?: string;
|
|
1246
|
+
},
|
|
1247
|
+
selectedLineCount: number,
|
|
1248
|
+
): string[] | undefined {
|
|
1249
|
+
if (!selection.preview) {
|
|
1250
|
+
return undefined;
|
|
1251
|
+
}
|
|
1252
|
+
const offset = selection.startLine - selection.rangeStartLine;
|
|
1253
|
+
if (offset < 0) {
|
|
1254
|
+
return undefined;
|
|
1255
|
+
}
|
|
1256
|
+
const previewLines = selection.preview.replace(/\\r\\n/g, "\\n").replace(/\\r/g, "\\n").split("\\n");
|
|
1257
|
+
if (offset + selectedLineCount > previewLines.length) {
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
return previewLines.slice(offset, offset + selectedLineCount);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function suggestionIncludesUnselectedContext(
|
|
1264
|
+
selection: {
|
|
1265
|
+
rangeStartLine: number;
|
|
1266
|
+
startLine: number;
|
|
1267
|
+
preview?: string;
|
|
1268
|
+
},
|
|
1269
|
+
selectedLineCount: number,
|
|
1270
|
+
suggestedLines: string[],
|
|
1271
|
+
): boolean {
|
|
1272
|
+
if (!selection.preview || suggestedLines.length <= selectedLineCount) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
const offset = selection.startLine - selection.rangeStartLine;
|
|
1276
|
+
if (offset < 0) {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
const previewLines = selection.preview.replace(/\\r\\n/g, "\\n").replace(/\\r/g, "\\n").split("\\n");
|
|
1280
|
+
const contextLines = [
|
|
1281
|
+
offset > 0 ? previewLines[offset - 1] : undefined,
|
|
1282
|
+
previewLines[offset + selectedLineCount],
|
|
1283
|
+
].filter((line): line is string => Boolean(line?.trim()));
|
|
1284
|
+
return contextLines.some((line) => suggestedLines.includes(line));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function hasUnchangedSelectionEdge(originalLines: string[], suggestedLines: string[]): boolean {
|
|
1288
|
+
const firstLineUnchanged = originalLines[0] === suggestedLines[0];
|
|
1289
|
+
const lastLineUnchanged = originalLines.at(-1) === suggestedLines.at(-1);
|
|
1290
|
+
if (originalLines.length === suggestedLines.length || originalLines.length === 1) {
|
|
1291
|
+
return firstLineUnchanged || lastLineUnchanged;
|
|
1292
|
+
}
|
|
1293
|
+
return firstLineUnchanged && lastLineUnchanged;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function suggestionsTable(suggestions: FixSuggestion[]): string {
|
|
1297
|
+
if (suggestions.length === 0) {
|
|
1298
|
+
return [
|
|
1299
|
+
"| Category | Title |",
|
|
1300
|
+
"| --- | --- |",
|
|
1301
|
+
"| - | No exact suggested fixes found. |",
|
|
1302
|
+
].join("\\n");
|
|
1303
|
+
}
|
|
1304
|
+
return [
|
|
1305
|
+
"| Category | Title |",
|
|
1306
|
+
"| --- | --- |",
|
|
1307
|
+
...suggestions.map((suggestion) => {
|
|
1308
|
+
const category = suggestion.category
|
|
1309
|
+
.replaceAll("-", " ")
|
|
1310
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
1311
|
+
const title = suggestion.title.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1312
|
+
return \`| \${category} | \${title} |\`;
|
|
1313
|
+
}),
|
|
1314
|
+
].join("\\n");
|
|
1315
|
+
}
|
|
1316
|
+
`
|
|
1317
|
+
};
|
|
1318
|
+
//#endregion
|
|
1048
1319
|
//#region src/config/recipes/interactive-ask.ts
|
|
1049
1320
|
const interactiveAskRecipe = {
|
|
1050
1321
|
id: "interactive-ask",
|
|
@@ -1483,7 +1754,7 @@ const prBriefingRecipe = {
|
|
|
1483
1754
|
title: "PR Briefing",
|
|
1484
1755
|
description: "PR-Agent describe-style overview, risk summary, and walkthrough.",
|
|
1485
1756
|
sourceTools: ["PR-Agent /describe", "CodeRabbit PR summaries"],
|
|
1486
|
-
configTs: `import { definePipr } from "@usepipr/sdk";
|
|
1757
|
+
configTs: `import { definePipr, z } from "@usepipr/sdk";
|
|
1487
1758
|
|
|
1488
1759
|
export default definePipr((pipr) => {
|
|
1489
1760
|
const model = pipr.model({
|
|
@@ -1495,33 +1766,178 @@ export default definePipr((pipr) => {
|
|
|
1495
1766
|
|
|
1496
1767
|
pipr.config({ publication: { maxInlineComments: 0 } });
|
|
1497
1768
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1769
|
+
const briefingSchema = z.strictObject({
|
|
1770
|
+
summary: z.string(),
|
|
1771
|
+
prType: z.enum(["feature", "bugfix", "refactor", "docs", "tests", "dependency", "infra", "mixed"]),
|
|
1772
|
+
riskLevel: z.enum(["low", "medium", "high"]),
|
|
1773
|
+
riskSummary: z.string(),
|
|
1774
|
+
changeMap: z.array(z.strictObject({
|
|
1775
|
+
area: z.string(),
|
|
1776
|
+
files: z.array(z.string()).max(6),
|
|
1777
|
+
change: z.string(),
|
|
1778
|
+
})).max(8),
|
|
1779
|
+
reviewerFocus: z.array(z.string()).max(6),
|
|
1780
|
+
notableFiles: z.array(z.strictObject({
|
|
1781
|
+
path: z.string(),
|
|
1782
|
+
reason: z.string(),
|
|
1783
|
+
})).max(8),
|
|
1784
|
+
walkthrough: z.array(z.string()).max(8),
|
|
1785
|
+
diagramMermaid: z.string().optional(),
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
type Briefing = z.infer<typeof briefingSchema>;
|
|
1789
|
+
|
|
1790
|
+
const briefingOutput = pipr.schema({
|
|
1791
|
+
id: "briefing/pr-reviewer",
|
|
1792
|
+
schema: briefingSchema,
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
const briefing = pipr.agent({
|
|
1796
|
+
name: "pr-briefing",
|
|
1500
1797
|
model,
|
|
1501
1798
|
instructions: \`
|
|
1502
1799
|
Produce a maintainer briefing instead of a defect hunt. Summarize what changed,
|
|
1503
|
-
classify the PR type, explain review risk,
|
|
1504
|
-
|
|
1800
|
+
classify the PR type, explain review risk, list notable files, and include
|
|
1801
|
+
a concise reviewer walkthrough. Use reviewerFocus for what humans should
|
|
1802
|
+
inspect first. Use diagramMermaid only when a small flowchart clarifies
|
|
1803
|
+
multi-step control flow, data flow, or package boundaries; omit it for
|
|
1804
|
+
straightforward changes. Do not report inline findings.
|
|
1505
1805
|
\`,
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1806
|
+
output: briefingOutput,
|
|
1807
|
+
tools: pipr.tools.readOnly,
|
|
1808
|
+
retry: { invalidOutput: 1, transientFailure: 1 },
|
|
1809
|
+
timeout: "7m",
|
|
1810
|
+
prompt: () => "Prepare a maintainer briefing for this pull request.",
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
const task = pipr.task({
|
|
1814
|
+
name: "pr-briefing",
|
|
1815
|
+
async run(ctx) {
|
|
1816
|
+
const manifest = await ctx.change.diffManifest({ compressed: true });
|
|
1817
|
+
const result = await ctx.pi.run(briefing, { manifest, change: ctx.change });
|
|
1818
|
+
const reviewerFocus =
|
|
1819
|
+
result.reviewerFocus.length === 0
|
|
1820
|
+
? "No special reviewer focus called out."
|
|
1821
|
+
: result.reviewerFocus.map((item) => \`- \${item}\`).join("\\n");
|
|
1822
|
+
const walkthrough =
|
|
1823
|
+
result.walkthrough.length === 0
|
|
1824
|
+
? "No walkthrough notes."
|
|
1825
|
+
: result.walkthrough.map((item) => \`- \${item}\`).join("\\n");
|
|
1826
|
+
await ctx.comment([
|
|
1827
|
+
overviewTable(result, ctx.change.title),
|
|
1828
|
+
"",
|
|
1829
|
+
"## Summary",
|
|
1830
|
+
"",
|
|
1831
|
+
result.summary,
|
|
1832
|
+
"",
|
|
1833
|
+
"## Change Map",
|
|
1834
|
+
"",
|
|
1835
|
+
changeMapTable(result.changeMap),
|
|
1836
|
+
"",
|
|
1837
|
+
"## Reviewer Focus",
|
|
1838
|
+
"",
|
|
1839
|
+
reviewerFocus,
|
|
1840
|
+
"",
|
|
1841
|
+
"## Notable Files",
|
|
1842
|
+
"",
|
|
1843
|
+
notableFilesTable(result.notableFiles),
|
|
1844
|
+
"",
|
|
1845
|
+
"## Walkthrough",
|
|
1846
|
+
"",
|
|
1847
|
+
walkthrough,
|
|
1848
|
+
"",
|
|
1849
|
+
diagramBlock(result.diagramMermaid),
|
|
1850
|
+
].filter(Boolean).join("\\n"));
|
|
1522
1851
|
},
|
|
1523
1852
|
});
|
|
1853
|
+
|
|
1854
|
+
pipr.on.changeRequest({ actions: ["opened", "updated", "reopened", "ready"], task });
|
|
1855
|
+
pipr.command({
|
|
1856
|
+
pattern: "@pipr describe",
|
|
1857
|
+
permission: "read",
|
|
1858
|
+
description: "Generate a reviewer briefing for this pull request.",
|
|
1859
|
+
task,
|
|
1860
|
+
});
|
|
1524
1861
|
});
|
|
1862
|
+
|
|
1863
|
+
function overviewTable(briefing: Briefing, title: string): string {
|
|
1864
|
+
const titleCell = title.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1865
|
+
const prType = briefing.prType.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase());
|
|
1866
|
+
const riskLevel = briefing.riskLevel
|
|
1867
|
+
.replaceAll("-", " ")
|
|
1868
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
1869
|
+
const riskSummary = briefing.riskSummary.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1870
|
+
return [
|
|
1871
|
+
"| Change | Type | Risk | Risk summary |",
|
|
1872
|
+
"| --- | --- | --- | --- |",
|
|
1873
|
+
\`| \${titleCell} | \${prType} | \${riskLevel} | \${riskSummary} |\`,
|
|
1874
|
+
].join("\\n");
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function changeMapTable(changeMap: Briefing["changeMap"]): string {
|
|
1878
|
+
if (changeMap.length === 0) {
|
|
1879
|
+
return [
|
|
1880
|
+
"| Area | Files | Change |",
|
|
1881
|
+
"| --- | --- | --- |",
|
|
1882
|
+
"| - | - | No changed areas summarized. |",
|
|
1883
|
+
].join("\\n");
|
|
1884
|
+
}
|
|
1885
|
+
return [
|
|
1886
|
+
"| Area | Files | Change |",
|
|
1887
|
+
"| --- | --- | --- |",
|
|
1888
|
+
...changeMap.map((item) => {
|
|
1889
|
+
const area = item.area.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1890
|
+
const files = item.files.join("<br>").replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1891
|
+
const change = item.change.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1892
|
+
return \`| \${area} | \${files} | \${change} |\`;
|
|
1893
|
+
}),
|
|
1894
|
+
].join("\\n");
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function notableFilesTable(files: Briefing["notableFiles"]): string {
|
|
1898
|
+
if (files.length === 0) {
|
|
1899
|
+
return [
|
|
1900
|
+
"| File | Why it matters |",
|
|
1901
|
+
"| --- | --- |",
|
|
1902
|
+
"| - | No notable files called out. |",
|
|
1903
|
+
].join("\\n");
|
|
1904
|
+
}
|
|
1905
|
+
return [
|
|
1906
|
+
"| File | Why it matters |",
|
|
1907
|
+
"| --- | --- |",
|
|
1908
|
+
...files.map((file) => {
|
|
1909
|
+
const filePath = file.path.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1910
|
+
const reason = file.reason.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
1911
|
+
return \`| \${filePath} | \${reason} |\`;
|
|
1912
|
+
}),
|
|
1913
|
+
].join("\\n");
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function diagramBlock(diagramMermaid: string | undefined): string {
|
|
1917
|
+
const diagram = diagramMermaid?.trim();
|
|
1918
|
+
if (!diagram) {
|
|
1919
|
+
return "";
|
|
1920
|
+
}
|
|
1921
|
+
const fence = markdownFenceFor(diagram);
|
|
1922
|
+
return [
|
|
1923
|
+
"<details>",
|
|
1924
|
+
"<summary>Flow diagram</summary>",
|
|
1925
|
+
"",
|
|
1926
|
+
\`\${fence}mermaid\`,
|
|
1927
|
+
diagram,
|
|
1928
|
+
fence,
|
|
1929
|
+
"",
|
|
1930
|
+
"</details>",
|
|
1931
|
+
].join("\\n");
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function markdownFenceFor(value: string): string {
|
|
1935
|
+
const longestBacktickRun = Math.max(
|
|
1936
|
+
0,
|
|
1937
|
+
...[...value.matchAll(/\`+/g)].map((match) => match[0].length),
|
|
1938
|
+
);
|
|
1939
|
+
return "\`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
1940
|
+
}
|
|
1525
1941
|
`
|
|
1526
1942
|
};
|
|
1527
1943
|
//#endregion
|
|
@@ -1531,7 +1947,8 @@ const prHygieneRecipe = {
|
|
|
1531
1947
|
title: "PR Hygiene",
|
|
1532
1948
|
description: "Danger-style PR hygiene checks for tests, docs, lockfiles, and size.",
|
|
1533
1949
|
sourceTools: ["Danger JS"],
|
|
1534
|
-
configTs: `import { definePipr } from "@usepipr/sdk";
|
|
1950
|
+
configTs: `import { definePipr, z } from "@usepipr/sdk";
|
|
1951
|
+
import type { ReviewFinding } from "@usepipr/sdk";
|
|
1535
1952
|
|
|
1536
1953
|
export default definePipr((pipr) => {
|
|
1537
1954
|
const model = pipr.model({
|
|
@@ -1541,18 +1958,60 @@ export default definePipr((pipr) => {
|
|
|
1541
1958
|
options: { thinking: "medium" },
|
|
1542
1959
|
});
|
|
1543
1960
|
|
|
1961
|
+
pipr.config({ publication: { maxInlineComments: 5 } });
|
|
1962
|
+
|
|
1963
|
+
const hygienePolicySchema = z.enum([
|
|
1964
|
+
"tests",
|
|
1965
|
+
"docs",
|
|
1966
|
+
"lockfiles",
|
|
1967
|
+
"generated-files",
|
|
1968
|
+
"change-size",
|
|
1969
|
+
]);
|
|
1970
|
+
|
|
1971
|
+
const policyCheckSchema = z.strictObject({
|
|
1972
|
+
policy: hygienePolicySchema,
|
|
1973
|
+
status: z.enum(["pass", "attention", "not-applicable"]),
|
|
1974
|
+
evidence: z.string(),
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
const hygieneFindingSchema = z.strictObject({
|
|
1978
|
+
title: z.string(),
|
|
1979
|
+
policy: hygienePolicySchema,
|
|
1980
|
+
body: z.string(),
|
|
1981
|
+
path: z.string(),
|
|
1982
|
+
rangeId: z.string(),
|
|
1983
|
+
side: z.enum(["RIGHT", "LEFT"]),
|
|
1984
|
+
startLine: z.number().int().positive(),
|
|
1985
|
+
endLine: z.number().int().positive(),
|
|
1986
|
+
suggestedFix: z.string().optional(),
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
type PolicyCheck = z.infer<typeof policyCheckSchema>;
|
|
1990
|
+
type HygieneFinding = z.infer<typeof hygieneFindingSchema>;
|
|
1991
|
+
|
|
1992
|
+
const hygieneOutput = pipr.schema({
|
|
1993
|
+
id: "review/pr-hygiene",
|
|
1994
|
+
schema: z.strictObject({
|
|
1995
|
+
summary: z.string(),
|
|
1996
|
+
checks: z.array(policyCheckSchema),
|
|
1997
|
+
findings: z.array(hygieneFindingSchema),
|
|
1998
|
+
}),
|
|
1999
|
+
});
|
|
2000
|
+
|
|
1544
2001
|
const hygiene = pipr.agent({
|
|
1545
2002
|
name: "pr-hygiene",
|
|
1546
2003
|
model,
|
|
1547
2004
|
instructions: \`
|
|
1548
|
-
Review pull request hygiene
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
output: pipr.schemas.review,
|
|
1553
|
-
prompt: (input: { manifest: unknown; changedFiles: unknown }) => pipr.prompt\`
|
|
1554
|
-
\${pipr.section("Changed files", pipr.json(input.changedFiles, { maxCharacters: 20000 }))}
|
|
2005
|
+
Review pull request hygiene, not code correctness. Evaluate tests, docs,
|
|
2006
|
+
lockfiles, generated files, and change size. Return one policy check per
|
|
2007
|
+
relevant policy. Return inline findings only for concrete hygiene gaps
|
|
2008
|
+
that maintainers can act on in the changed lines.
|
|
1555
2009
|
\`,
|
|
2010
|
+
output: hygieneOutput,
|
|
2011
|
+
tools: pipr.tools.readOnly,
|
|
2012
|
+
retry: { invalidOutput: 1, transientFailure: 1 },
|
|
2013
|
+
timeout: "6m",
|
|
2014
|
+
prompt: () => "Check this pull request for repository hygiene and merge readiness.",
|
|
1556
2015
|
});
|
|
1557
2016
|
|
|
1558
2017
|
const task = pipr.task({
|
|
@@ -1563,10 +2022,34 @@ export default definePipr((pipr) => {
|
|
|
1563
2022
|
ctx.log.info(\`Checking PR hygiene for \${changedFiles.length} changed file(s).\`);
|
|
1564
2023
|
const manifest = await ctx.change.diffManifest({ compressed: true, maxPreviewLines: 80 });
|
|
1565
2024
|
const result = await ctx.pi.run(hygiene, { manifest, changedFiles });
|
|
2025
|
+
const inlineFindings: ReviewFinding[] = result.findings.map((finding) => {
|
|
2026
|
+
const policy = finding.policy
|
|
2027
|
+
.replaceAll("-", " ")
|
|
2028
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2029
|
+
return {
|
|
2030
|
+
body: \`**\${policy}:** \${finding.title}. \${finding.body}\`,
|
|
2031
|
+
path: finding.path,
|
|
2032
|
+
rangeId: finding.rangeId,
|
|
2033
|
+
side: finding.side,
|
|
2034
|
+
startLine: finding.startLine,
|
|
2035
|
+
endLine: finding.endLine,
|
|
2036
|
+
...(finding.suggestedFix ? { suggestedFix: finding.suggestedFix } : {}),
|
|
2037
|
+
};
|
|
2038
|
+
});
|
|
1566
2039
|
ctx.check.pass("PR hygiene review completed.");
|
|
1567
2040
|
await ctx.comment({
|
|
1568
|
-
main:
|
|
1569
|
-
|
|
2041
|
+
main: [
|
|
2042
|
+
result.summary,
|
|
2043
|
+
"",
|
|
2044
|
+
"## Policy Checks",
|
|
2045
|
+
"",
|
|
2046
|
+
policyTable(result.checks),
|
|
2047
|
+
"",
|
|
2048
|
+
"## Selected Findings",
|
|
2049
|
+
"",
|
|
2050
|
+
findingsTable(result.findings),
|
|
2051
|
+
].join("\\n"),
|
|
2052
|
+
inlineFindings,
|
|
1570
2053
|
});
|
|
1571
2054
|
},
|
|
1572
2055
|
});
|
|
@@ -1574,6 +2057,51 @@ export default definePipr((pipr) => {
|
|
|
1574
2057
|
pipr.on.changeRequest({ actions: ["opened", "updated", "reopened", "ready"], task });
|
|
1575
2058
|
pipr.command({ pattern: "@pipr hygiene", permission: "write", task });
|
|
1576
2059
|
});
|
|
2060
|
+
|
|
2061
|
+
function policyTable(checks: PolicyCheck[]): string {
|
|
2062
|
+
if (checks.length === 0) {
|
|
2063
|
+
return [
|
|
2064
|
+
"| Policy | Status | Evidence |",
|
|
2065
|
+
"| --- | --- | --- |",
|
|
2066
|
+
"| - | Not applicable | No policy checks were relevant. |",
|
|
2067
|
+
].join("\\n");
|
|
2068
|
+
}
|
|
2069
|
+
return [
|
|
2070
|
+
"| Policy | Status | Evidence |",
|
|
2071
|
+
"| --- | --- | --- |",
|
|
2072
|
+
...checks.map((check) => {
|
|
2073
|
+
const policy = check.policy
|
|
2074
|
+
.replaceAll("-", " ")
|
|
2075
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2076
|
+
const status = check.status
|
|
2077
|
+
.replaceAll("-", " ")
|
|
2078
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2079
|
+
const evidence = check.evidence.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
2080
|
+
return \`| \${policy} | \${status} | \${evidence} |\`;
|
|
2081
|
+
}),
|
|
2082
|
+
].join("\\n");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function findingsTable(findings: HygieneFinding[]): string {
|
|
2086
|
+
if (findings.length === 0) {
|
|
2087
|
+
return [
|
|
2088
|
+
"| Policy | Finding |",
|
|
2089
|
+
"| --- | --- |",
|
|
2090
|
+
"| - | No selected hygiene findings. |",
|
|
2091
|
+
].join("\\n");
|
|
2092
|
+
}
|
|
2093
|
+
return [
|
|
2094
|
+
"| Policy | Finding |",
|
|
2095
|
+
"| --- | --- |",
|
|
2096
|
+
...findings.map((finding) => {
|
|
2097
|
+
const policy = finding.policy
|
|
2098
|
+
.replaceAll("-", " ")
|
|
2099
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2100
|
+
const title = finding.title.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
2101
|
+
return \`| \${policy} | \${title} |\`;
|
|
2102
|
+
}),
|
|
2103
|
+
].join("\\n");
|
|
2104
|
+
}
|
|
1577
2105
|
`
|
|
1578
2106
|
};
|
|
1579
2107
|
//#endregion
|
|
@@ -1582,8 +2110,9 @@ const qualityGateRecipe = {
|
|
|
1582
2110
|
id: "quality-gate",
|
|
1583
2111
|
title: "Quality Gate",
|
|
1584
2112
|
description: "Required review check that fails on blocking correctness and test risks.",
|
|
1585
|
-
sourceTools: ["SonarQube"],
|
|
1586
|
-
configTs: `import { definePipr } from "@usepipr/sdk";
|
|
2113
|
+
sourceTools: ["SonarQube", "Snyk"],
|
|
2114
|
+
configTs: `import { definePipr, z } from "@usepipr/sdk";
|
|
2115
|
+
import type { CommentableRange, DiffManifest, ReviewFinding } from "@usepipr/sdk";
|
|
1587
2116
|
|
|
1588
2117
|
export default definePipr((pipr) => {
|
|
1589
2118
|
const model = pipr.model({
|
|
@@ -1616,21 +2145,397 @@ export default definePipr((pipr) => {
|
|
|
1616
2145
|
},
|
|
1617
2146
|
});
|
|
1618
2147
|
|
|
1619
|
-
|
|
1620
|
-
|
|
2148
|
+
const blockerSchema = z.strictObject({
|
|
2149
|
+
title: z.string(),
|
|
2150
|
+
category: z.enum(["correctness", "security", "reliability", "test-coverage"]),
|
|
2151
|
+
impact: z.string(),
|
|
2152
|
+
body: z.string(),
|
|
2153
|
+
path: z.string(),
|
|
2154
|
+
rangeId: z.string(),
|
|
2155
|
+
side: z.enum(["RIGHT", "LEFT"]),
|
|
2156
|
+
startLine: z.number().int().positive(),
|
|
2157
|
+
endLine: z.number().int().positive(),
|
|
2158
|
+
suggestedFix: z.string().optional(),
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
type QualityBlocker = z.infer<typeof blockerSchema>;
|
|
2162
|
+
|
|
2163
|
+
const qualityGateOutput = pipr.schema({
|
|
2164
|
+
id: "review/quality-gate",
|
|
2165
|
+
schema: z.strictObject({
|
|
2166
|
+
summary: z.string(),
|
|
2167
|
+
blockers: z.array(blockerSchema),
|
|
2168
|
+
}),
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
const reviewer = pipr.agent({
|
|
2172
|
+
name: "quality-gate",
|
|
1621
2173
|
model,
|
|
1622
2174
|
instructions: \`
|
|
1623
|
-
Act as a merge quality gate. Report only blocking correctness,
|
|
1624
|
-
|
|
1625
|
-
|
|
2175
|
+
Act as a merge quality gate. Report only blocking correctness, security,
|
|
2176
|
+
reliability, or test coverage issues that must prevent merge. A blocker
|
|
2177
|
+
must have a concrete changed-code range and an impact that maintainers can
|
|
2178
|
+
verify. If no blocking issue exists, return an empty blockers array.
|
|
1626
2179
|
\`,
|
|
2180
|
+
output: qualityGateOutput,
|
|
2181
|
+
tools: pipr.tools.readOnly,
|
|
2182
|
+
retry: { invalidOutput: 1, transientFailure: 1 },
|
|
2183
|
+
timeout: "7m",
|
|
2184
|
+
prompt: () => "Run the required quality gate for this pull request.",
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
const task = pipr.task({
|
|
2188
|
+
name: "quality-gate",
|
|
1627
2189
|
check: { enabled: true, name: "quality gate", required: true },
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2190
|
+
async run(ctx) {
|
|
2191
|
+
const manifest = await ctx.change.diffManifest({ compressed: true });
|
|
2192
|
+
const result = await ctx.pi.run(reviewer, { manifest });
|
|
2193
|
+
const commentableBlockers = result.blockers.filter((blocker) =>
|
|
2194
|
+
commentableRangeForFinding(blocker, manifest) !== undefined,
|
|
2195
|
+
);
|
|
2196
|
+
const droppedBlockerCount = result.blockers.length - commentableBlockers.length;
|
|
2197
|
+
const inlineFindings: ReviewFinding[] = commentableBlockers.map((blocker) => {
|
|
2198
|
+
const category = blocker.category
|
|
2199
|
+
.replaceAll("-", " ")
|
|
2200
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2201
|
+
return {
|
|
2202
|
+
body: \`**\${category} blocker:** \${blocker.title}. \${blocker.body}\`,
|
|
2203
|
+
path: blocker.path,
|
|
2204
|
+
rangeId: blocker.rangeId,
|
|
2205
|
+
side: blocker.side,
|
|
2206
|
+
startLine: blocker.startLine,
|
|
2207
|
+
endLine: blocker.endLine,
|
|
2208
|
+
...(blocker.suggestedFix ? { suggestedFix: blocker.suggestedFix } : {}),
|
|
2209
|
+
};
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
if (commentableBlockers.length > 0) {
|
|
2213
|
+
const issueNoun = commentableBlockers.length === 1 ? "issue" : "issues";
|
|
2214
|
+
ctx.check.fail(\`\${commentableBlockers.length} blocking quality \${issueNoun} found.\`);
|
|
2215
|
+
} else {
|
|
2216
|
+
ctx.check.pass("No blocking quality issues found.");
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
await ctx.comment({
|
|
2220
|
+
main: [
|
|
2221
|
+
statusTable(commentableBlockers),
|
|
2222
|
+
"",
|
|
2223
|
+
droppedBlockersNote(droppedBlockerCount),
|
|
2224
|
+
"",
|
|
2225
|
+
result.summary,
|
|
2226
|
+
"",
|
|
2227
|
+
"## Blocking Findings",
|
|
2228
|
+
"",
|
|
2229
|
+
blockersTable(commentableBlockers),
|
|
2230
|
+
"",
|
|
2231
|
+
"## Category Breakdown",
|
|
2232
|
+
"",
|
|
2233
|
+
categoryBreakdownTable(commentableBlockers),
|
|
2234
|
+
].join("\\n"),
|
|
2235
|
+
inlineFindings,
|
|
2236
|
+
});
|
|
2237
|
+
},
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
pipr.on.changeRequest({ actions: ["opened", "updated", "reopened", "ready"], task });
|
|
2241
|
+
pipr.command({ pattern: "@pipr quality", permission: "write", task });
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
type FindingAnchor = Pick<ReviewFinding, "path" | "rangeId" | "side" | "startLine" | "endLine">;
|
|
2245
|
+
|
|
2246
|
+
function commentableRangeForFinding(
|
|
2247
|
+
finding: FindingAnchor,
|
|
2248
|
+
manifest: DiffManifest,
|
|
2249
|
+
): CommentableRange | undefined {
|
|
2250
|
+
for (const file of manifest.files) {
|
|
2251
|
+
const range = file.commentableRanges.find((candidate) => candidate.id === finding.rangeId);
|
|
2252
|
+
if (!range) {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
return finding.rangeId === range.id &&
|
|
2256
|
+
finding.path === range.path &&
|
|
2257
|
+
finding.side === range.side &&
|
|
2258
|
+
finding.startLine <= finding.endLine &&
|
|
2259
|
+
finding.startLine >= range.startLine &&
|
|
2260
|
+
finding.endLine <= range.endLine
|
|
2261
|
+
? range
|
|
2262
|
+
: undefined;
|
|
2263
|
+
}
|
|
2264
|
+
return undefined;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
function statusTable(blockers: QualityBlocker[]): string {
|
|
2268
|
+
return [
|
|
2269
|
+
"| Status | Blocking findings | Categories |",
|
|
2270
|
+
"| --- | ---: | --- |",
|
|
2271
|
+
\`| \${blockers.length === 0 ? "Pass" : "Fail"} | \${blockers.length} | \${categorySummary(
|
|
2272
|
+
blockers,
|
|
2273
|
+
)} |\`,
|
|
2274
|
+
].join("\\n");
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function blockersTable(blockers: QualityBlocker[]): string {
|
|
2278
|
+
if (blockers.length === 0) {
|
|
2279
|
+
return [
|
|
2280
|
+
"| Category | Title | Impact |",
|
|
2281
|
+
"| --- | --- | --- |",
|
|
2282
|
+
"| - | No blocking findings. | - |",
|
|
2283
|
+
].join("\\n");
|
|
2284
|
+
}
|
|
2285
|
+
return [
|
|
2286
|
+
"| Category | Title | Impact |",
|
|
2287
|
+
"| --- | --- | --- |",
|
|
2288
|
+
...blockers.map((blocker) => {
|
|
2289
|
+
const category = blocker.category
|
|
2290
|
+
.replaceAll("-", " ")
|
|
2291
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2292
|
+
const title = blocker.title.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
2293
|
+
const impact = blocker.impact.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
2294
|
+
return \`| \${category} | \${title} | \${impact} |\`;
|
|
2295
|
+
}),
|
|
2296
|
+
].join("\\n");
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function droppedBlockersNote(count: number): string {
|
|
2300
|
+
if (count === 0) {
|
|
2301
|
+
return "";
|
|
2302
|
+
}
|
|
2303
|
+
const blockerNoun = count === 1 ? "blocker" : "blockers";
|
|
2304
|
+
const verb = count === 1 ? "was" : "were";
|
|
2305
|
+
const pronoun = count === 1 ? "it does" : "they do";
|
|
2306
|
+
return [
|
|
2307
|
+
"<sub>",
|
|
2308
|
+
count,
|
|
2309
|
+
" model-reported ",
|
|
2310
|
+
blockerNoun,
|
|
2311
|
+
" ",
|
|
2312
|
+
verb,
|
|
2313
|
+
" ignored because ",
|
|
2314
|
+
pronoun,
|
|
2315
|
+
" not match a commentable diff range.",
|
|
2316
|
+
"</sub>",
|
|
2317
|
+
].join("");
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function categoryBreakdownTable(blockers: QualityBlocker[]): string {
|
|
2321
|
+
const counts = categoryCounts(blockers);
|
|
2322
|
+
if (counts.length === 0) {
|
|
2323
|
+
return [
|
|
2324
|
+
"| Category | Count |",
|
|
2325
|
+
"| --- | ---: |",
|
|
2326
|
+
"| - | 0 |",
|
|
2327
|
+
].join("\\n");
|
|
2328
|
+
}
|
|
2329
|
+
return [
|
|
2330
|
+
"| Category | Count |",
|
|
2331
|
+
"| --- | ---: |",
|
|
2332
|
+
...counts.map(([category, count]) => {
|
|
2333
|
+
const categoryLabel = category
|
|
2334
|
+
.replaceAll("-", " ")
|
|
2335
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2336
|
+
return \`| \${categoryLabel} | \${count} |\`;
|
|
1631
2337
|
}),
|
|
2338
|
+
].join("\\n");
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function categorySummary(blockers: QualityBlocker[]): string {
|
|
2342
|
+
const counts = categoryCounts(blockers);
|
|
2343
|
+
if (counts.length === 0) {
|
|
2344
|
+
return "None";
|
|
2345
|
+
}
|
|
2346
|
+
return counts
|
|
2347
|
+
.map(([category, count]) => {
|
|
2348
|
+
const categoryLabel = category
|
|
2349
|
+
.replaceAll("-", " ")
|
|
2350
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
2351
|
+
return \`\${categoryLabel} (\${count})\`;
|
|
2352
|
+
})
|
|
2353
|
+
.join(", ");
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function categoryCounts(blockers: QualityBlocker[]): Array<[string, number]> {
|
|
2357
|
+
const counts = new Map<string, number>();
|
|
2358
|
+
for (const blocker of blockers) {
|
|
2359
|
+
counts.set(blocker.category, (counts.get(blocker.category) ?? 0) + 1);
|
|
2360
|
+
}
|
|
2361
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
`
|
|
2365
|
+
};
|
|
2366
|
+
//#endregion
|
|
2367
|
+
//#region src/config/recipes/rich-review.ts
|
|
2368
|
+
const structuredReviewRecipe = {
|
|
2369
|
+
id: "rich-review",
|
|
2370
|
+
title: "Structured Review",
|
|
2371
|
+
description: "General pull request review with severity and category metadata.",
|
|
2372
|
+
sourceTools: [
|
|
2373
|
+
"CodeRabbit",
|
|
2374
|
+
"Qodo Merge",
|
|
2375
|
+
"Greptile"
|
|
2376
|
+
],
|
|
2377
|
+
configTs: `import { definePipr, z } from "@usepipr/sdk";
|
|
2378
|
+
import type { ReviewFinding } from "@usepipr/sdk";
|
|
2379
|
+
|
|
2380
|
+
type CategorizedFinding = {
|
|
2381
|
+
title: string;
|
|
2382
|
+
severity: "critical" | "high" | "medium" | "low" | "nit";
|
|
2383
|
+
category:
|
|
2384
|
+
| "correctness"
|
|
2385
|
+
| "security"
|
|
2386
|
+
| "reliability"
|
|
2387
|
+
| "performance"
|
|
2388
|
+
| "test-coverage"
|
|
2389
|
+
| "maintainability"
|
|
2390
|
+
| "documentation";
|
|
2391
|
+
rationale: string;
|
|
2392
|
+
body: string;
|
|
2393
|
+
path: string;
|
|
2394
|
+
rangeId: string;
|
|
2395
|
+
side: "RIGHT" | "LEFT";
|
|
2396
|
+
startLine: number;
|
|
2397
|
+
endLine: number;
|
|
2398
|
+
suggestedFix?: string;
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const categorizedFindingSchema = z.strictObject({
|
|
2402
|
+
title: z.string(),
|
|
2403
|
+
severity: z.enum(["critical", "high", "medium", "low", "nit"]),
|
|
2404
|
+
category: z.enum([
|
|
2405
|
+
"correctness",
|
|
2406
|
+
"security",
|
|
2407
|
+
"reliability",
|
|
2408
|
+
"performance",
|
|
2409
|
+
"test-coverage",
|
|
2410
|
+
"maintainability",
|
|
2411
|
+
"documentation",
|
|
2412
|
+
]),
|
|
2413
|
+
rationale: z.string(),
|
|
2414
|
+
body: z.string(),
|
|
2415
|
+
path: z.string(),
|
|
2416
|
+
rangeId: z.string(),
|
|
2417
|
+
side: z.enum(["RIGHT", "LEFT"]),
|
|
2418
|
+
startLine: z.number().int().positive(),
|
|
2419
|
+
endLine: z.number().int().positive(),
|
|
2420
|
+
suggestedFix: z.string().optional(),
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
export default definePipr((pipr) => {
|
|
2424
|
+
const model = pipr.model({
|
|
2425
|
+
provider: "deepseek",
|
|
2426
|
+
model: "deepseek-v4-pro",
|
|
2427
|
+
apiKey: pipr.secret({ name: "DEEPSEEK_API_KEY" }),
|
|
2428
|
+
options: { thinking: "high" },
|
|
1632
2429
|
});
|
|
2430
|
+
|
|
2431
|
+
pipr.config({ publication: { maxInlineComments: 8 } });
|
|
2432
|
+
|
|
2433
|
+
const reviewOutput = pipr.schema({
|
|
2434
|
+
id: "review/categorized-findings",
|
|
2435
|
+
schema: z.strictObject({
|
|
2436
|
+
summary: z.string(),
|
|
2437
|
+
findings: z.array(categorizedFindingSchema),
|
|
2438
|
+
}),
|
|
2439
|
+
});
|
|
2440
|
+
|
|
2441
|
+
const reviewer = pipr.agent({
|
|
2442
|
+
name: "reviewer",
|
|
2443
|
+
model,
|
|
2444
|
+
instructions: \`
|
|
2445
|
+
Review the pull request diff for correctness, security, reliability,
|
|
2446
|
+
performance, test coverage, maintainability, and documentation risks.
|
|
2447
|
+
Return only actionable findings that target valid diff ranges. Assign
|
|
2448
|
+
severity by merge impact: critical and high for merge-blocking defects,
|
|
2449
|
+
medium for important follow-up, low for minor actionable improvements,
|
|
2450
|
+
and nit only for tiny but concrete issues. Include suggestedFix only when
|
|
2451
|
+
there is an exact replacement for the selected range.
|
|
2452
|
+
\`,
|
|
2453
|
+
output: reviewOutput,
|
|
2454
|
+
tools: pipr.tools.readOnly,
|
|
2455
|
+
retry: { invalidOutput: 1, transientFailure: 1 },
|
|
2456
|
+
timeout: "10m",
|
|
2457
|
+
prompt: () => "Review this change with severity and category metadata.",
|
|
2458
|
+
});
|
|
2459
|
+
|
|
2460
|
+
const task = pipr.task({
|
|
2461
|
+
name: "review",
|
|
2462
|
+
async run(ctx) {
|
|
2463
|
+
const manifest = await ctx.change.diffManifest({ compressed: true });
|
|
2464
|
+
const result = await ctx.pi.run(reviewer, { manifest });
|
|
2465
|
+
const inlineFindings: ReviewFinding[] = result.findings.map((finding) => {
|
|
2466
|
+
const severity = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
|
|
2467
|
+
const category = finding.category.replaceAll("-", " ");
|
|
2468
|
+
return {
|
|
2469
|
+
body: \`**\${severity} \${category}:** \${finding.title}. \${finding.body}\`,
|
|
2470
|
+
path: finding.path,
|
|
2471
|
+
rangeId: finding.rangeId,
|
|
2472
|
+
side: finding.side,
|
|
2473
|
+
startLine: finding.startLine,
|
|
2474
|
+
endLine: finding.endLine,
|
|
2475
|
+
...(finding.suggestedFix ? { suggestedFix: finding.suggestedFix } : {}),
|
|
2476
|
+
};
|
|
2477
|
+
});
|
|
2478
|
+
await ctx.comment({
|
|
2479
|
+
main: [
|
|
2480
|
+
result.summary,
|
|
2481
|
+
"",
|
|
2482
|
+
"## Findings",
|
|
2483
|
+
"",
|
|
2484
|
+
findingsTable(result.findings),
|
|
2485
|
+
"",
|
|
2486
|
+
"<details>",
|
|
2487
|
+
"<summary>Finding rationales</summary>",
|
|
2488
|
+
"",
|
|
2489
|
+
findingRationales(result.findings),
|
|
2490
|
+
"",
|
|
2491
|
+
"</details>",
|
|
2492
|
+
].join("\\n"),
|
|
2493
|
+
inlineFindings,
|
|
2494
|
+
});
|
|
2495
|
+
},
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
pipr.on.changeRequest({ actions: ["opened", "updated", "reopened", "ready"], task });
|
|
2499
|
+
pipr.command({ pattern: "@pipr review", permission: "write", task });
|
|
1633
2500
|
});
|
|
2501
|
+
|
|
2502
|
+
function findingsTable(findings: CategorizedFinding[]): string {
|
|
2503
|
+
if (findings.length === 0) {
|
|
2504
|
+
return [
|
|
2505
|
+
"| Severity | Category | Title |",
|
|
2506
|
+
"| --- | --- | --- |",
|
|
2507
|
+
"| - | - | No findings. |",
|
|
2508
|
+
].join("\\n");
|
|
2509
|
+
}
|
|
2510
|
+
return [
|
|
2511
|
+
"| Severity | Category | Title |",
|
|
2512
|
+
"| --- | --- | --- |",
|
|
2513
|
+
...findings.map((finding) => {
|
|
2514
|
+
const severity = finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1);
|
|
2515
|
+
const category = finding.category.replaceAll("-", " ").replaceAll("|", "\\\\|");
|
|
2516
|
+
const title = finding.title.replaceAll("\\n", " ").replaceAll("|", "\\\\|");
|
|
2517
|
+
return \`| \${severity} | \${category} | \${title} |\`;
|
|
2518
|
+
}),
|
|
2519
|
+
].join("\\n");
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function findingRationales(findings: CategorizedFinding[]): string {
|
|
2523
|
+
if (findings.length === 0) {
|
|
2524
|
+
return "No findings.";
|
|
2525
|
+
}
|
|
2526
|
+
return findings
|
|
2527
|
+
.map((finding, index) =>
|
|
2528
|
+
[
|
|
2529
|
+
\`### \${index + 1}. \${finding.title}\`,
|
|
2530
|
+
"",
|
|
2531
|
+
\`**Severity:** \${finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1)}\`,
|
|
2532
|
+
\`**Category:** \${finding.category.replaceAll("-", " ")}\`,
|
|
2533
|
+
"",
|
|
2534
|
+
finding.rationale,
|
|
2535
|
+
].join("\\n"),
|
|
2536
|
+
)
|
|
2537
|
+
.join("\\n\\n");
|
|
2538
|
+
}
|
|
1634
2539
|
`
|
|
1635
2540
|
};
|
|
1636
2541
|
//#endregion
|
|
@@ -1738,7 +2643,7 @@ export default definePipr((pipr) => {
|
|
|
1738
2643
|
ctx.check.pass("No high or critical security risks found.");
|
|
1739
2644
|
}
|
|
1740
2645
|
await ctx.comment({
|
|
1741
|
-
main:
|
|
2646
|
+
main: result.summary,
|
|
1742
2647
|
inlineFindings,
|
|
1743
2648
|
});
|
|
1744
2649
|
},
|
|
@@ -1754,6 +2659,8 @@ export default definePipr((pipr) => {
|
|
|
1754
2659
|
const supportedOfficialInitRecipes = [
|
|
1755
2660
|
"default-review",
|
|
1756
2661
|
"bug-hunter",
|
|
2662
|
+
"rich-review",
|
|
2663
|
+
"fix-suggestions",
|
|
1757
2664
|
"security-sast",
|
|
1758
2665
|
"quality-gate",
|
|
1759
2666
|
"diff-diagnostics",
|
|
@@ -1769,6 +2676,8 @@ const supportedOfficialInitRecipes = [
|
|
|
1769
2676
|
const officialInitRecipeRegistry = {
|
|
1770
2677
|
"default-review": defaultReviewRecipe,
|
|
1771
2678
|
"bug-hunter": bugHunterRecipe,
|
|
2679
|
+
"rich-review": structuredReviewRecipe,
|
|
2680
|
+
"fix-suggestions": fixSuggestionsRecipe,
|
|
1772
2681
|
"security-sast": securitySastRecipe,
|
|
1773
2682
|
"quality-gate": qualityGateRecipe,
|
|
1774
2683
|
"diff-diagnostics": diffDiagnosticsRecipe,
|
|
@@ -1804,8 +2713,8 @@ function isOfficialInitRecipeId(recipe) {
|
|
|
1804
2713
|
//#endregion
|
|
1805
2714
|
//#region src/config/init.ts
|
|
1806
2715
|
const supportedOfficialInitAdapters = ["github"];
|
|
1807
|
-
const defaultWorkflowActionRef = "somus/pipr@v0.3.
|
|
1808
|
-
const defaultSdkVersion = "0.3.
|
|
2716
|
+
const defaultWorkflowActionRef = "somus/pipr@v0.3.2";
|
|
2717
|
+
const defaultSdkVersion = "0.3.2";
|
|
1809
2718
|
const defaultTypesBunVersion = "1.3.14";
|
|
1810
2719
|
function resolveOfficialInitAdapters(adapters) {
|
|
1811
2720
|
if (adapters === void 0) return [...supportedOfficialInitAdapters];
|
|
@@ -3242,6 +4151,12 @@ function killProcessGroup(child, signal) {
|
|
|
3242
4151
|
}
|
|
3243
4152
|
}
|
|
3244
4153
|
//#endregion
|
|
4154
|
+
//#region src/shared/redaction.ts
|
|
4155
|
+
const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
|
|
4156
|
+
function redactPotentialSecrets(value) {
|
|
4157
|
+
return value.replace(secretLikeTokenPattern, "[redacted secret]");
|
|
4158
|
+
}
|
|
4159
|
+
//#endregion
|
|
3245
4160
|
//#region src/shared/logging.ts
|
|
3246
4161
|
const sensitiveEnvNamePattern = /(TOKEN|SECRET|PASSWORD|PASS|KEY|AUTH|CREDENTIAL|COOKIE)/i;
|
|
3247
4162
|
function createRuntimeActionLog(options) {
|
|
@@ -3305,7 +4220,7 @@ function addSecret(secrets, value) {
|
|
|
3305
4220
|
function redact(message, secrets) {
|
|
3306
4221
|
let redacted = message;
|
|
3307
4222
|
for (const secret of secrets) redacted = redacted.split(secret).join("***");
|
|
3308
|
-
return redacted;
|
|
4223
|
+
return redactPotentialSecrets(redacted);
|
|
3309
4224
|
}
|
|
3310
4225
|
function compactFields(fields, secrets) {
|
|
3311
4226
|
const compact = {};
|
|
@@ -3863,8 +4778,10 @@ function parseAgentOutput(output, agent) {
|
|
|
3863
4778
|
}
|
|
3864
4779
|
function jsonPayloadCandidates(output) {
|
|
3865
4780
|
const trimmed = output.trim();
|
|
3866
|
-
const match = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/i.exec(
|
|
4781
|
+
const match = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/i.exec(trimmed);
|
|
3867
4782
|
if (match?.[1]) return [match[1].trim()];
|
|
4783
|
+
const embeddedMatches = [...trimmed.matchAll(/```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
4784
|
+
if (embeddedMatches.length === 1 && embeddedMatches[0]?.[1]) return [trimmed, embeddedMatches[0][1].trim()];
|
|
3868
4785
|
return [trimmed];
|
|
3869
4786
|
}
|
|
3870
4787
|
function buildRepairPrompt(options) {
|
|
@@ -3882,7 +4799,19 @@ function buildRepairPrompt(options) {
|
|
|
3882
4799
|
}
|
|
3883
4800
|
//#endregion
|
|
3884
4801
|
//#region package.json
|
|
3885
|
-
var version = "0.3.
|
|
4802
|
+
var version = "0.3.2";
|
|
4803
|
+
//#endregion
|
|
4804
|
+
//#region src/review/comment-branding.ts
|
|
4805
|
+
const piprLogoUrl = "https://pipr.run/apple-touch-icon.png";
|
|
4806
|
+
const piprRepositoryUrl = "https://github.com/somus/pipr";
|
|
4807
|
+
const mainCommentTitle = `# <img src="${piprLogoUrl}" width="22" height="22" alt=""> Pipr Review`;
|
|
4808
|
+
const mainCommentTitles = new Set([
|
|
4809
|
+
"# pipr Review",
|
|
4810
|
+
"# Pipr Review",
|
|
4811
|
+
mainCommentTitle
|
|
4812
|
+
]);
|
|
4813
|
+
const escapedRepositoryUrl = piprRepositoryUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4814
|
+
const mainCommentAttributionPattern = new RegExp(`^<sub>Review generated by \\[Pipr\\]\\(${escapedRepositoryUrl}\\) for commit \`[^\`]+\`\\.</sub>$`);
|
|
3886
4815
|
//#endregion
|
|
3887
4816
|
//#region src/review/inline-publication-policy.ts
|
|
3888
4817
|
const maxSuggestedFixSelectedLines = 12;
|
|
@@ -4257,7 +5186,6 @@ const publicationPlanSchema = z.strictObject({
|
|
|
4257
5186
|
});
|
|
4258
5187
|
const maxInlineFindingBodyCharacters = 700;
|
|
4259
5188
|
const maxInlineFindingBodyLines = 4;
|
|
4260
|
-
const secretLikeTokenPattern = /\b[A-Za-z0-9][A-Za-z0-9_.:/+=-]*(?:secret|token|api[_-]?key|apikey)[A-Za-z0-9_.:/+=-]{8,}\b/gi;
|
|
4261
5189
|
function buildPublicationPlan(options) {
|
|
4262
5190
|
const reviewState = options.reviewState ?? buildPriorReviewState({
|
|
4263
5191
|
findings: options.inlineItems.map((item) => item.finding),
|
|
@@ -4273,7 +5201,8 @@ function buildPublicationPlan(options) {
|
|
|
4273
5201
|
mainComment: renderMainComment({
|
|
4274
5202
|
event: options.event,
|
|
4275
5203
|
reviewState,
|
|
4276
|
-
main: options.main
|
|
5204
|
+
main: options.main,
|
|
5205
|
+
reviewedHeadSha: metadata.reviewedHeadSha
|
|
4277
5206
|
}),
|
|
4278
5207
|
mainMarker: mainCommentMarker,
|
|
4279
5208
|
changeNumber: options.event.change.number,
|
|
@@ -4357,29 +5286,39 @@ function renderMainComment(options) {
|
|
|
4357
5286
|
reviewState: options.reviewState
|
|
4358
5287
|
}),
|
|
4359
5288
|
"",
|
|
4360
|
-
|
|
5289
|
+
mainCommentTitle,
|
|
4361
5290
|
"",
|
|
4362
5291
|
redactPotentialSecrets(options.main),
|
|
5292
|
+
"",
|
|
5293
|
+
`<sub>Review generated by [Pipr](${piprRepositoryUrl}) for commit \`${options.reviewedHeadSha.slice(0, 7)}\`.</sub>`,
|
|
4363
5294
|
""
|
|
4364
5295
|
].join("\n");
|
|
4365
5296
|
}
|
|
4366
5297
|
function renderInlineBody(finding, findingId, reviewedHeadSha) {
|
|
5298
|
+
const findingBody = startsWithStructuredMarkdown(finding.body) ? finding.body : [
|
|
5299
|
+
"**Issue**",
|
|
5300
|
+
"",
|
|
5301
|
+
finding.body
|
|
5302
|
+
].join("\n");
|
|
4367
5303
|
return [
|
|
4368
5304
|
renderInlineFindingMarker(findingId, reviewedHeadSha),
|
|
4369
|
-
|
|
4370
|
-
finding.suggestedFix ?
|
|
5305
|
+
findingBody,
|
|
5306
|
+
finding.suggestedFix ? [
|
|
5307
|
+
"**Suggested change**",
|
|
5308
|
+
"",
|
|
5309
|
+
renderSuggestedChange(finding.suggestedFix)
|
|
5310
|
+
].join("\n") : ""
|
|
4371
5311
|
].filter(Boolean).join("\n");
|
|
4372
5312
|
}
|
|
5313
|
+
function startsWithStructuredMarkdown(value) {
|
|
5314
|
+
const body = value.trimStart();
|
|
5315
|
+
return /^#{1,6}\s/.test(body) || /^>/.test(body) || /^(\d+[.)]|[-*+])\s/.test(body) || /^\|/.test(body) || /^(```|~~~)/.test(body) || /^<\s*[a-z][\w:-]*(\s|>|\/>)/i.test(body) || /^\*\*[^*\n]+\*\*/.test(body);
|
|
5316
|
+
}
|
|
4373
5317
|
function renderSuggestedChange(suggestedFix) {
|
|
4374
|
-
const
|
|
5318
|
+
const longestBacktickRun = Math.max(0, ...[...suggestedFix.matchAll(/`+/g)].map((match) => match[0].length));
|
|
5319
|
+
const fence = "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
4375
5320
|
return `${fence}suggestion\n${suggestedFix}${suggestedFix.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
4376
5321
|
}
|
|
4377
|
-
function longestBacktickRun(value) {
|
|
4378
|
-
return Math.max(0, ...[...value.matchAll(/`+/g)].map((match) => match[0].length));
|
|
4379
|
-
}
|
|
4380
|
-
function redactPotentialSecrets(value) {
|
|
4381
|
-
return value.replace(secretLikeTokenPattern, "[redacted secret]");
|
|
4382
|
-
}
|
|
4383
5322
|
//#endregion
|
|
4384
5323
|
//#region src/review/comment-publishing.ts
|
|
4385
5324
|
function buildCommentPublishingPlan(options) {
|
|
@@ -4767,9 +5706,9 @@ function priorReviewForTask(priorMainComment, priorReviewState) {
|
|
|
4767
5706
|
};
|
|
4768
5707
|
}
|
|
4769
5708
|
function visibleMainComment(body) {
|
|
4770
|
-
const lines = body.split("\n").filter((line) => !line.startsWith("<!-- pipr:main-comment "));
|
|
5709
|
+
const lines = body.split("\n").filter((line) => !line.startsWith("<!-- pipr:main-comment ") && !mainCommentAttributionPattern.test(line));
|
|
4771
5710
|
while (lines[0] === "") lines.shift();
|
|
4772
|
-
if (lines[0]
|
|
5711
|
+
if (lines[0] && mainCommentTitles.has(lines[0])) lines.shift();
|
|
4773
5712
|
while (lines[0] === "") lines.shift();
|
|
4774
5713
|
return lines.join("\n").trim();
|
|
4775
5714
|
}
|
|
@@ -6150,9 +7089,10 @@ async function publishInlineCommentItem(options) {
|
|
|
6150
7089
|
error: error instanceof Error ? error.message : String(error)
|
|
6151
7090
|
};
|
|
6152
7091
|
}
|
|
7092
|
+
const publicationLocation = inlinePublicationLocationFromGitHub(location);
|
|
6153
7093
|
if (inlinePublicationDecision({
|
|
6154
7094
|
marker: options.item.marker,
|
|
6155
|
-
location:
|
|
7095
|
+
location: publicationLocation,
|
|
6156
7096
|
existing: options.existing
|
|
6157
7097
|
}) === "skip") return { status: "skipped" };
|
|
6158
7098
|
try {
|
|
@@ -6163,6 +7103,7 @@ async function publishInlineCommentItem(options) {
|
|
|
6163
7103
|
...location
|
|
6164
7104
|
});
|
|
6165
7105
|
options.existing.markers.add(options.item.marker);
|
|
7106
|
+
options.existing.locations.push(publicationLocation);
|
|
6166
7107
|
return { status: "posted" };
|
|
6167
7108
|
} catch (error) {
|
|
6168
7109
|
return {
|
|
@@ -7370,4 +8311,4 @@ async function runActionCommandWithDependencies(options) {
|
|
|
7370
8311
|
//#endregion
|
|
7371
8312
|
export { runInspectCommand as a, PublicationError as c, listOfficialInitRecipes as d, supportedOfficialInitRecipes as f, piThinkingLevels as g, piRequiredCliFlags as h, runInitCommand as i, isPublishableSuggestedFixSelection as l, piReadOnlyToolNames as m, runActionCommandWithDependencies as n, runLocalReviewCommand as o, piBuiltinToolNames as p, runDryRunCommand as r, runValidateCommand as s, runActionCommand as t, supportedOfficialInitAdapters as u };
|
|
7372
8313
|
|
|
7373
|
-
//# sourceMappingURL=commands-
|
|
8314
|
+
//# sourceMappingURL=commands-CQg9QVZt.mjs.map
|