@usepipr/runtime 0.3.2 → 0.3.3

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.
@@ -2398,6 +2398,14 @@ type CategorizedFinding = {
2398
2398
  suggestedFix?: string;
2399
2399
  };
2400
2400
 
2401
+ type ReviewSummary = {
2402
+ headline: string;
2403
+ changeSummary: string[];
2404
+ riskLevel: "low" | "medium" | "high";
2405
+ riskSummary: string;
2406
+ reviewerFocus: string[];
2407
+ };
2408
+
2401
2409
  const categorizedFindingSchema = z.strictObject({
2402
2410
  title: z.string(),
2403
2411
  severity: z.enum(["critical", "high", "medium", "low", "nit"]),
@@ -2420,6 +2428,14 @@ const categorizedFindingSchema = z.strictObject({
2420
2428
  suggestedFix: z.string().optional(),
2421
2429
  });
2422
2430
 
2431
+ const reviewSummarySchema = z.strictObject({
2432
+ headline: z.string(),
2433
+ changeSummary: z.array(z.string()).min(1).max(4),
2434
+ riskLevel: z.enum(["low", "medium", "high"]),
2435
+ riskSummary: z.string(),
2436
+ reviewerFocus: z.array(z.string()).max(4),
2437
+ });
2438
+
2423
2439
  export default definePipr((pipr) => {
2424
2440
  const model = pipr.model({
2425
2441
  provider: "deepseek",
@@ -2433,7 +2449,7 @@ export default definePipr((pipr) => {
2433
2449
  const reviewOutput = pipr.schema({
2434
2450
  id: "review/categorized-findings",
2435
2451
  schema: z.strictObject({
2436
- summary: z.string(),
2452
+ summary: reviewSummarySchema,
2437
2453
  findings: z.array(categorizedFindingSchema),
2438
2454
  }),
2439
2455
  });
@@ -2449,6 +2465,11 @@ export default definePipr((pipr) => {
2449
2465
  medium for important follow-up, low for minor actionable improvements,
2450
2466
  and nit only for tiny but concrete issues. Include suggestedFix only when
2451
2467
  there is an exact replacement for the selected range.
2468
+
2469
+ Make summary maintainer-facing and scannable: one concrete headline, one
2470
+ to four behavior-focused change bullets, a risk level with rationale, and
2471
+ reviewer focus only for useful human follow-up. Put actionable defects in
2472
+ findings, not only in summary.
2452
2473
  \`,
2453
2474
  output: reviewOutput,
2454
2475
  tools: pipr.tools.readOnly,
@@ -2477,18 +2498,24 @@ export default definePipr((pipr) => {
2477
2498
  });
2478
2499
  await ctx.comment({
2479
2500
  main: [
2480
- result.summary,
2501
+ "## Summary",
2481
2502
  "",
2482
- "## Findings",
2503
+ \`**\${result.summary.headline}**\`,
2483
2504
  "",
2484
- findingsTable(result.findings),
2505
+ summaryTable(result.summary, result.findings.length),
2506
+ "",
2507
+ "## What Changed",
2508
+ "",
2509
+ bulletList(result.summary.changeSummary, "No changed behavior summarized."),
2510
+ "",
2511
+ "## Reviewer Focus",
2485
2512
  "",
2486
- "<details>",
2487
- "<summary>Finding rationales</summary>",
2513
+ bulletList(result.summary.reviewerFocus, "No special reviewer focus."),
2488
2514
  "",
2489
- findingRationales(result.findings),
2515
+ "## Findings",
2490
2516
  "",
2491
- "</details>",
2517
+ findingsTable(result.findings),
2518
+ ...(result.findings.length > 0 ? ["", findingRationalesBlock(result.findings)] : []),
2492
2519
  ].join("\\n"),
2493
2520
  inlineFindings,
2494
2521
  });
@@ -2499,6 +2526,23 @@ export default definePipr((pipr) => {
2499
2526
  pipr.command({ pattern: "@pipr review", permission: "write", task });
2500
2527
  });
2501
2528
 
2529
+ function summaryTable(summary: ReviewSummary, findingCount: number): string {
2530
+ return [
2531
+ "| Outcome | Risk | Risk summary |",
2532
+ "| --- | --- | --- |",
2533
+ \`| \${findingOutcome(findingCount)} | \${labelValue(summary.riskLevel)} | \${tableCell(
2534
+ summary.riskSummary,
2535
+ )} |\`,
2536
+ ].join("\\n");
2537
+ }
2538
+
2539
+ function findingOutcome(findingCount: number): string {
2540
+ if (findingCount === 0) {
2541
+ return "No findings";
2542
+ }
2543
+ return findingCount === 1 ? "1 finding" : \`\${findingCount} findings\`;
2544
+ }
2545
+
2502
2546
  function findingsTable(findings: CategorizedFinding[]): string {
2503
2547
  if (findings.length === 0) {
2504
2548
  return [
@@ -2519,22 +2563,48 @@ function findingsTable(findings: CategorizedFinding[]): string {
2519
2563
  ].join("\\n");
2520
2564
  }
2521
2565
 
2522
- function findingRationales(findings: CategorizedFinding[]): string {
2566
+ function findingRationalesBlock(findings: CategorizedFinding[]): string {
2523
2567
  if (findings.length === 0) {
2524
- return "No findings.";
2568
+ return "";
2525
2569
  }
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");
2570
+ return [
2571
+ "<details>",
2572
+ "<summary>Finding rationales</summary>",
2573
+ "",
2574
+ findings
2575
+ .map((finding, index) =>
2576
+ [
2577
+ \`### \${index + 1}. \${finding.title}\`,
2578
+ "",
2579
+ \`**Severity:** \${labelValue(finding.severity)}\`,
2580
+ \`**Category:** \${labelValue(finding.category)}\`,
2581
+ "",
2582
+ finding.rationale,
2583
+ ].join("\\n"),
2584
+ )
2585
+ .join("\\n\\n"),
2586
+ "",
2587
+ "</details>",
2588
+ ].join("\\n");
2589
+ }
2590
+
2591
+ function bulletList(items: string[], emptyText: string): string {
2592
+ if (items.length === 0) {
2593
+ return emptyText;
2594
+ }
2595
+ return items.map((item) => \`- \${lineText(item)}\`).join("\\n");
2596
+ }
2597
+
2598
+ function labelValue(value: string): string {
2599
+ return value.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase());
2600
+ }
2601
+
2602
+ function lineText(value: string): string {
2603
+ return value.replaceAll("\\n", " ").trim();
2604
+ }
2605
+
2606
+ function tableCell(value: string): string {
2607
+ return lineText(value).replaceAll("|", "\\\\|");
2538
2608
  }
2539
2609
  `
2540
2610
  };
@@ -2552,15 +2622,25 @@ const securitySastRecipe = {
2552
2622
  configTs: `import { definePipr } from "@usepipr/sdk";
2553
2623
  import type { ReviewFinding } from "@usepipr/sdk";
2554
2624
 
2625
+ type SecuritySummary = {
2626
+ headline: string;
2627
+ riskLevel: "low" | "medium" | "high";
2628
+ riskSummary: string;
2629
+ reviewerFocus: string[];
2630
+ };
2631
+
2632
+ type SecurityRisk = {
2633
+ title: string;
2634
+ category: "auth" | "injection" | "secret" | "crypto" | "data-exposure" | "other";
2635
+ severity: "low" | "medium" | "high" | "critical";
2636
+ rationale: string;
2637
+ finding?: ReviewFinding;
2638
+ };
2639
+
2555
2640
  type SecurityReview = {
2556
- summary: string;
2557
- risks: Array<{
2558
- title: string;
2559
- category: "auth" | "injection" | "secret" | "crypto" | "data-exposure" | "other";
2560
- severity: "low" | "medium" | "high" | "critical";
2561
- rationale: string;
2562
- finding?: ReviewFinding;
2563
- }>;
2641
+ summary: SecuritySummary;
2642
+ risks: SecurityRisk[];
2643
+ diagramMermaid?: string;
2564
2644
  };
2565
2645
 
2566
2646
  export default definePipr((pipr) => {
@@ -2578,7 +2658,20 @@ export default definePipr((pipr) => {
2578
2658
  additionalProperties: false,
2579
2659
  required: ["summary", "risks"],
2580
2660
  properties: {
2581
- summary: { type: "string" },
2661
+ summary: {
2662
+ type: "object",
2663
+ additionalProperties: false,
2664
+ required: ["headline", "riskLevel", "riskSummary", "reviewerFocus"],
2665
+ properties: {
2666
+ headline: { type: "string" },
2667
+ riskLevel: { type: "string", enum: ["low", "medium", "high"] },
2668
+ riskSummary: { type: "string" },
2669
+ reviewerFocus: {
2670
+ type: "array",
2671
+ items: { type: "string" },
2672
+ },
2673
+ },
2674
+ },
2582
2675
  risks: {
2583
2676
  type: "array",
2584
2677
  items: {
@@ -2610,6 +2703,7 @@ export default definePipr((pipr) => {
2610
2703
  },
2611
2704
  },
2612
2705
  },
2706
+ diagramMermaid: { type: "string" },
2613
2707
  },
2614
2708
  },
2615
2709
  });
@@ -2621,6 +2715,11 @@ export default definePipr((pipr) => {
2621
2715
  Review for exploitable security issues only. Focus on auth bypasses,
2622
2716
  injection, unsafe deserialization, secret exposure, cryptography misuse,
2623
2717
  authorization gaps, and data exposure. Do not report hypothetical style issues.
2718
+ Make summary maintainer-facing and scannable with a concrete headline,
2719
+ risk level, risk rationale, and only useful security follow-up. Set
2720
+ diagramMermaid only when a high or critical risk has a concrete source-to-sink
2721
+ path and a small Mermaid flowchart clarifies it. Do not include Markdown
2722
+ fences in diagramMermaid.
2624
2723
  \`,
2625
2724
  output: securityOutput,
2626
2725
  tools: pipr.tools.readOnly,
@@ -2636,14 +2735,40 @@ export default definePipr((pipr) => {
2636
2735
  async run(ctx) {
2637
2736
  const manifest = await ctx.change.diffManifest({ compressed: true });
2638
2737
  const result = await ctx.pi.run(security, { manifest });
2639
- const inlineFindings = result.risks.flatMap((risk) => (risk.finding ? [risk.finding] : []));
2640
- if (result.risks.some((risk) => risk.severity === "critical" || risk.severity === "high")) {
2738
+ const inlineFindings: ReviewFinding[] = result.risks.flatMap((risk) =>
2739
+ risk.finding ? [risk.finding] : [],
2740
+ );
2741
+ const hasHighOrCriticalRisk = result.risks.some(isHighOrCriticalRisk);
2742
+ const hasConcreteHighOrCriticalRisk = result.risks.some(
2743
+ (risk) => isHighOrCriticalRisk(risk) && risk.finding,
2744
+ );
2745
+ if (hasHighOrCriticalRisk) {
2641
2746
  ctx.check.fail("High or critical security risk found.");
2642
2747
  } else {
2643
2748
  ctx.check.pass("No high or critical security risks found.");
2644
2749
  }
2645
2750
  await ctx.comment({
2646
- main: result.summary,
2751
+ main: [
2752
+ "## Summary",
2753
+ "",
2754
+ \`**\${result.summary.headline}**\`,
2755
+ "",
2756
+ securityStatusTable(result.summary, result.risks),
2757
+ "",
2758
+ result.summary.riskSummary,
2759
+ "",
2760
+ "## Reviewer Focus",
2761
+ "",
2762
+ bulletList(result.summary.reviewerFocus, "No special security follow-up."),
2763
+ "",
2764
+ "## Security Risks",
2765
+ "",
2766
+ securityRisksTable(result.risks),
2767
+ ...(result.risks.length > 0 ? ["", riskRationalesBlock(result.risks)] : []),
2768
+ ...(hasConcreteHighOrCriticalRisk && result.diagramMermaid?.trim()
2769
+ ? ["", attackPathDiagramBlock(result.diagramMermaid, hasConcreteHighOrCriticalRisk)]
2770
+ : []),
2771
+ ].join("\\n"),
2647
2772
  inlineFindings,
2648
2773
  });
2649
2774
  },
@@ -2652,6 +2777,122 @@ export default definePipr((pipr) => {
2652
2777
  pipr.on.changeRequest({ actions: ["opened", "updated", "reopened", "ready"], task });
2653
2778
  pipr.command({ pattern: "@pipr security", permission: "write", task });
2654
2779
  });
2780
+
2781
+ function securityStatusTable(summary: SecuritySummary, risks: SecurityRisk[]): string {
2782
+ return [
2783
+ "| Status | Summary risk | Max severity | Risks |",
2784
+ "| --- | --- | --- | ---: |",
2785
+ \`| \${risks.some(isHighOrCriticalRisk) ? "Fail" : "Pass"} | \${labelValue(
2786
+ summary.riskLevel,
2787
+ )} | \${maxSeverity(risks)} | \${risks.length} |\`,
2788
+ ].join("\\n");
2789
+ }
2790
+
2791
+ function securityRisksTable(risks: SecurityRisk[]): string {
2792
+ if (risks.length === 0) {
2793
+ return [
2794
+ "| Severity | Category | Title |",
2795
+ "| --- | --- | --- |",
2796
+ "| - | - | No security risks found. |",
2797
+ ].join("\\n");
2798
+ }
2799
+ return [
2800
+ "| Severity | Category | Title |",
2801
+ "| --- | --- | --- |",
2802
+ ...risks.map(
2803
+ (risk) =>
2804
+ \`| \${labelValue(risk.severity)} | \${tableCell(risk.category)} | \${tableCell(risk.title)} |\`,
2805
+ ),
2806
+ ].join("\\n");
2807
+ }
2808
+
2809
+ function riskRationalesBlock(risks: SecurityRisk[]): string {
2810
+ if (risks.length === 0) {
2811
+ return "";
2812
+ }
2813
+ return [
2814
+ "<details>",
2815
+ "<summary>Risk rationales</summary>",
2816
+ "",
2817
+ risks
2818
+ .map((risk, index) =>
2819
+ [
2820
+ \`### \${index + 1}. \${risk.title}\`,
2821
+ "",
2822
+ \`**Severity:** \${labelValue(risk.severity)}\`,
2823
+ \`**Category:** \${labelValue(risk.category)}\`,
2824
+ "",
2825
+ risk.rationale,
2826
+ ].join("\\n"),
2827
+ )
2828
+ .join("\\n\\n"),
2829
+ "",
2830
+ "</details>",
2831
+ ].join("\\n");
2832
+ }
2833
+
2834
+ function attackPathDiagramBlock(
2835
+ diagramMermaid: string | undefined,
2836
+ hasConcreteHighOrCriticalRisk: boolean,
2837
+ ): string {
2838
+ const diagram = diagramMermaid?.trim();
2839
+ if (!diagram || !hasConcreteHighOrCriticalRisk) {
2840
+ return "";
2841
+ }
2842
+ const fence = markdownFenceFor(diagram);
2843
+ return [
2844
+ "<details>",
2845
+ "<summary>Attack path diagram</summary>",
2846
+ "",
2847
+ \`\${fence}mermaid\`,
2848
+ diagram,
2849
+ fence,
2850
+ "",
2851
+ "</details>",
2852
+ ].join("\\n");
2853
+ }
2854
+
2855
+ function maxSeverity(risks: SecurityRisk[]): string {
2856
+ const order: SecurityRisk["severity"][] = ["low", "medium", "high", "critical"];
2857
+ const severity = risks.reduce<SecurityRisk["severity"] | undefined>((current, risk) => {
2858
+ if (!current || order.indexOf(risk.severity) > order.indexOf(current)) {
2859
+ return risk.severity;
2860
+ }
2861
+ return current;
2862
+ }, undefined);
2863
+ return severity ? labelValue(severity) : "None";
2864
+ }
2865
+
2866
+ function isHighOrCriticalRisk(risk: SecurityRisk): boolean {
2867
+ return risk.severity === "high" || risk.severity === "critical";
2868
+ }
2869
+
2870
+ function bulletList(items: string[], emptyText: string): string {
2871
+ if (items.length === 0) {
2872
+ return emptyText;
2873
+ }
2874
+ return items.map((item) => \`- \${lineText(item)}\`).join("\\n");
2875
+ }
2876
+
2877
+ function labelValue(value: string): string {
2878
+ return value.replaceAll("-", " ").replace(/^./, (char) => char.toUpperCase());
2879
+ }
2880
+
2881
+ function lineText(value: string): string {
2882
+ return value.replaceAll("\\n", " ").trim();
2883
+ }
2884
+
2885
+ function tableCell(value: string): string {
2886
+ return lineText(value).replaceAll("|", "\\\\|");
2887
+ }
2888
+
2889
+ function markdownFenceFor(value: string): string {
2890
+ const longestBacktickRun = Math.max(
2891
+ 0,
2892
+ ...[...value.matchAll(/\`+/g)].map((match) => match[0].length),
2893
+ );
2894
+ return "\`".repeat(Math.max(3, longestBacktickRun + 1));
2895
+ }
2655
2896
  `
2656
2897
  };
2657
2898
  //#endregion
@@ -2713,8 +2954,8 @@ function isOfficialInitRecipeId(recipe) {
2713
2954
  //#endregion
2714
2955
  //#region src/config/init.ts
2715
2956
  const supportedOfficialInitAdapters = ["github"];
2716
- const defaultWorkflowActionRef = "somus/pipr@v0.3.2";
2717
- const defaultSdkVersion = "0.3.2";
2957
+ const defaultWorkflowActionRef = "somus/pipr@v0.3.3";
2958
+ const defaultSdkVersion = "0.3.3";
2718
2959
  const defaultTypesBunVersion = "1.3.14";
2719
2960
  function resolveOfficialInitAdapters(adapters) {
2720
2961
  if (adapters === void 0) return [...supportedOfficialInitAdapters];
@@ -4799,10 +5040,10 @@ function buildRepairPrompt(options) {
4799
5040
  }
4800
5041
  //#endregion
4801
5042
  //#region package.json
4802
- var version = "0.3.2";
5043
+ var version = "0.3.3";
4803
5044
  //#endregion
4804
5045
  //#region src/review/comment-branding.ts
4805
- const piprLogoUrl = "https://pipr.run/apple-touch-icon.png";
5046
+ const piprLogoUrl = "https://pipr.run/images/pipr/pipr-mark.svg";
4806
5047
  const piprRepositoryUrl = "https://github.com/somus/pipr";
4807
5048
  const mainCommentTitle = `# <img src="${piprLogoUrl}" width="22" height="22" alt=""> Pipr Review`;
4808
5049
  const mainCommentTitles = new Set([
@@ -8311,4 +8552,4 @@ async function runActionCommandWithDependencies(options) {
8311
8552
  //#endregion
8312
8553
  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 };
8313
8554
 
8314
- //# sourceMappingURL=commands-CQg9QVZt.mjs.map
8555
+ //# sourceMappingURL=commands-C1sVsbEj.mjs.map