ghagga-core 2.0.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.
Files changed (82) hide show
  1. package/README.md +51 -0
  2. package/dist/agents/consensus.d.ts +68 -0
  3. package/dist/agents/consensus.d.ts.map +1 -0
  4. package/dist/agents/consensus.js +216 -0
  5. package/dist/agents/consensus.js.map +1 -0
  6. package/dist/agents/prompts.d.ts +18 -0
  7. package/dist/agents/prompts.d.ts.map +1 -0
  8. package/dist/agents/prompts.js +194 -0
  9. package/dist/agents/prompts.js.map +1 -0
  10. package/dist/agents/simple.d.ts +49 -0
  11. package/dist/agents/simple.d.ts.map +1 -0
  12. package/dist/agents/simple.js +135 -0
  13. package/dist/agents/simple.js.map +1 -0
  14. package/dist/agents/workflow.d.ts +40 -0
  15. package/dist/agents/workflow.d.ts.map +1 -0
  16. package/dist/agents/workflow.js +127 -0
  17. package/dist/agents/workflow.js.map +1 -0
  18. package/dist/index.d.ts +19 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +21 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/memory/context.d.ts +23 -0
  23. package/dist/memory/context.d.ts.map +1 -0
  24. package/dist/memory/context.js +36 -0
  25. package/dist/memory/context.js.map +1 -0
  26. package/dist/memory/persist.d.ts +22 -0
  27. package/dist/memory/persist.d.ts.map +1 -0
  28. package/dist/memory/persist.js +103 -0
  29. package/dist/memory/persist.js.map +1 -0
  30. package/dist/memory/privacy.d.ts +19 -0
  31. package/dist/memory/privacy.d.ts.map +1 -0
  32. package/dist/memory/privacy.js +77 -0
  33. package/dist/memory/privacy.js.map +1 -0
  34. package/dist/memory/search.d.ts +20 -0
  35. package/dist/memory/search.d.ts.map +1 -0
  36. package/dist/memory/search.js +76 -0
  37. package/dist/memory/search.js.map +1 -0
  38. package/dist/pipeline.d.ts +30 -0
  39. package/dist/pipeline.d.ts.map +1 -0
  40. package/dist/pipeline.js +267 -0
  41. package/dist/pipeline.js.map +1 -0
  42. package/dist/providers/fallback.d.ts +46 -0
  43. package/dist/providers/fallback.d.ts.map +1 -0
  44. package/dist/providers/fallback.js +84 -0
  45. package/dist/providers/fallback.js.map +1 -0
  46. package/dist/providers/index.d.ts +40 -0
  47. package/dist/providers/index.d.ts.map +1 -0
  48. package/dist/providers/index.js +76 -0
  49. package/dist/providers/index.js.map +1 -0
  50. package/dist/tools/cpd.d.ts +24 -0
  51. package/dist/tools/cpd.d.ts.map +1 -0
  52. package/dist/tools/cpd.js +130 -0
  53. package/dist/tools/cpd.js.map +1 -0
  54. package/dist/tools/runner.d.ts +19 -0
  55. package/dist/tools/runner.d.ts.map +1 -0
  56. package/dist/tools/runner.js +61 -0
  57. package/dist/tools/runner.js.map +1 -0
  58. package/dist/tools/semgrep.d.ts +12 -0
  59. package/dist/tools/semgrep.d.ts.map +1 -0
  60. package/dist/tools/semgrep.js +97 -0
  61. package/dist/tools/semgrep.js.map +1 -0
  62. package/dist/tools/trivy.d.ts +11 -0
  63. package/dist/tools/trivy.d.ts.map +1 -0
  64. package/dist/tools/trivy.js +74 -0
  65. package/dist/tools/trivy.js.map +1 -0
  66. package/dist/types.d.ts +168 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +24 -0
  69. package/dist/types.js.map +1 -0
  70. package/dist/utils/diff.d.ts +53 -0
  71. package/dist/utils/diff.d.ts.map +1 -0
  72. package/dist/utils/diff.js +103 -0
  73. package/dist/utils/diff.js.map +1 -0
  74. package/dist/utils/stack-detect.d.ts +15 -0
  75. package/dist/utils/stack-detect.d.ts.map +1 -0
  76. package/dist/utils/stack-detect.js +54 -0
  77. package/dist/utils/stack-detect.js.map +1 -0
  78. package/dist/utils/token-budget.d.ts +31 -0
  79. package/dist/utils/token-budget.d.ts.map +1 -0
  80. package/dist/utils/token-budget.js +62 -0
  81. package/dist/utils/token-budget.js.map +1 -0
  82. package/package.json +68 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Privacy-safe text sanitization.
3
+ *
4
+ * Strips sensitive data (API keys, tokens, passwords) from text
5
+ * before it gets persisted to memory. This ensures that even if
6
+ * a diff contains credentials, they won't be stored in the database.
7
+ */
8
+ // ─── Patterns ───────────────────────────────────────────────────
9
+ /**
10
+ * Regex patterns for common secret formats.
11
+ * Each pattern is paired with a human-readable replacement label.
12
+ */
13
+ const SENSITIVE_PATTERNS = [
14
+ // Anthropic API keys
15
+ { pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
16
+ // OpenAI API keys (classic sk-... and newer sk-proj-... with internal hyphens)
17
+ { pattern: /sk-[a-zA-Z0-9_-]{20,}/g, replacement: '[REDACTED_OPENAI_KEY]' },
18
+ // AWS Access Key IDs
19
+ { pattern: /AKIA[0-9A-Z]{16}/g, replacement: '[REDACTED_AWS_KEY]' },
20
+ // AWS Secret Access Keys (typically 40 chars, base64-ish)
21
+ { pattern: /(?<=AWS_SECRET_ACCESS_KEY\s*=\s*)[A-Za-z0-9/+=]{40}/g, replacement: '[REDACTED_AWS_SECRET]' },
22
+ // GitHub tokens (classic and fine-grained)
23
+ { pattern: /ghp_[a-zA-Z0-9]{36,}/g, replacement: '[REDACTED_GITHUB_PAT]' },
24
+ { pattern: /gho_[a-zA-Z0-9]{36,}/g, replacement: '[REDACTED_GITHUB_OAUTH]' },
25
+ { pattern: /ghs_[a-zA-Z0-9]{36,}/g, replacement: '[REDACTED_GITHUB_APP]' },
26
+ { pattern: /ghr_[a-zA-Z0-9]{36,}/g, replacement: '[REDACTED_GITHUB_REFRESH]' },
27
+ { pattern: /github_pat_[a-zA-Z0-9_]{22,}/g, replacement: '[REDACTED_GITHUB_FINE_PAT]' },
28
+ // Google API keys
29
+ { pattern: /AIza[0-9A-Za-z_-]{35}/g, replacement: '[REDACTED_GOOGLE_KEY]' },
30
+ // Slack tokens
31
+ { pattern: /xox[bpors]-[0-9a-zA-Z-]{10,}/g, replacement: '[REDACTED_SLACK_TOKEN]' },
32
+ // Generic Bearer tokens in headers
33
+ { pattern: /Bearer\s+[a-zA-Z0-9._-]{20,}/gi, replacement: 'Bearer [REDACTED_TOKEN]' },
34
+ // Generic "password" / "secret" / "token" assignments
35
+ // Matches: password = "...", PASSWORD: "...", secret: '...', token='...'
36
+ {
37
+ pattern: /(?<=(password|secret|token|api_key|apikey|api-key)\s*[:=]\s*['"])[^'"]{8,}(?=['"])/gi,
38
+ replacement: '[REDACTED]',
39
+ },
40
+ // Base64-encoded strings that look like they could be secrets (64+ chars)
41
+ // Only match when preceded by common secret-related variable names
42
+ {
43
+ pattern: /(?<=(SECRET|KEY|TOKEN|CREDENTIAL|PASSWORD)\s*[:=]\s*['"]?)[A-Za-z0-9+/]{64,}={0,2}(?=['"]?)/gi,
44
+ replacement: '[REDACTED_BASE64]',
45
+ },
46
+ // Private keys (PEM format)
47
+ {
48
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
49
+ replacement: '[REDACTED_PRIVATE_KEY]',
50
+ },
51
+ // JWT tokens (three base64url segments separated by dots)
52
+ {
53
+ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g,
54
+ replacement: '[REDACTED_JWT]',
55
+ },
56
+ ];
57
+ // ─── Main Function ──────────────────────────────────────────────
58
+ /**
59
+ * Strip sensitive data from text before persisting to memory.
60
+ *
61
+ * Applies all known secret patterns and replaces matches with
62
+ * human-readable redaction labels. The patterns are applied in
63
+ * order, so more specific patterns take precedence.
64
+ *
65
+ * @param text - The text to sanitize
66
+ * @returns Sanitized text with secrets replaced by redaction labels
67
+ */
68
+ export function stripPrivateData(text) {
69
+ let sanitized = text;
70
+ for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
71
+ // Reset regex lastIndex for each application (since we use /g flag)
72
+ pattern.lastIndex = 0;
73
+ sanitized = sanitized.replace(pattern, replacement);
74
+ }
75
+ return sanitized;
76
+ }
77
+ //# sourceMappingURL=privacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.js","sourceRoot":"","sources":["../../src/memory/privacy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,mEAAmE;AAEnE;;;GAGG;AACH,MAAM,kBAAkB,GAAoD;IAC1E,qBAAqB;IACrB,EAAE,OAAO,EAAE,4BAA4B,EAAE,WAAW,EAAE,0BAA0B,EAAE;IAElF,+EAA+E;IAC/E,EAAE,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAE3E,qBAAqB;IACrB,EAAE,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,oBAAoB,EAAE;IAEnE,0DAA0D;IAC1D,EAAE,OAAO,EAAE,sDAAsD,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAEzG,2CAA2C;IAC3C,EAAE,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAC1E,EAAE,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAC5E,EAAE,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAC1E,EAAE,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,2BAA2B,EAAE;IAC9E,EAAE,OAAO,EAAE,+BAA+B,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAEvF,kBAAkB;IAClB,EAAE,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,uBAAuB,EAAE;IAE3E,eAAe;IACf,EAAE,OAAO,EAAE,+BAA+B,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAEnF,mCAAmC;IACnC,EAAE,OAAO,EAAE,gCAAgC,EAAE,WAAW,EAAE,yBAAyB,EAAE;IAErF,sDAAsD;IACtD,yEAAyE;IACzE;QACE,OAAO,EAAE,sFAAsF;QAC/F,WAAW,EAAE,YAAY;KAC1B;IAED,0EAA0E;IAC1E,mEAAmE;IACnE;QACE,OAAO,EAAE,+FAA+F;QACxG,WAAW,EAAE,mBAAmB;KACjC;IAED,4BAA4B;IAC5B;QACE,OAAO,EAAE,mGAAmG;QAC5G,WAAW,EAAE,wBAAwB;KACtC;IAED,0DAA0D;IAC1D;QACE,OAAO,EAAE,mEAAmE;QAC5E,WAAW,EAAE,gBAAgB;KAC9B;CACF,CAAC;AAEF,mEAAmE;AAEnE;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,IAAI,SAAS,GAAG,IAAI,CAAC;IAErB,KAAK,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,kBAAkB,EAAE,CAAC;QAC1D,oEAAoE;QACpE,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC;QACtB,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Memory search — retrieves relevant past observations for prompt injection.
3
+ *
4
+ * Builds a search query from the file paths in the current diff,
5
+ * then uses PostgreSQL full-text search (via ghagga-db) to find
6
+ * observations from past reviews of the same project.
7
+ */
8
+ /**
9
+ * Search past review observations for context relevant to the current diff.
10
+ *
11
+ * Returns a formatted string suitable for injection into agent prompts,
12
+ * or null if no relevant observations are found (or if db is unavailable).
13
+ *
14
+ * @param db - Database instance (from ghagga-db). Typed as unknown for loose coupling.
15
+ * @param project - Project identifier (e.g., "owner/repo")
16
+ * @param fileList - List of file paths in the current diff
17
+ * @returns Formatted memory context string, or null
18
+ */
19
+ export declare function searchMemoryForContext(db: unknown, project: string, fileList: string[]): Promise<string | null>;
20
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/memory/search.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqDH;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,OAAO,EACX,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Memory search — retrieves relevant past observations for prompt injection.
3
+ *
4
+ * Builds a search query from the file paths in the current diff,
5
+ * then uses PostgreSQL full-text search (via ghagga-db) to find
6
+ * observations from past reviews of the same project.
7
+ */
8
+ import { searchObservations } from 'ghagga-db';
9
+ import { formatMemoryContext } from './context.js';
10
+ // ─── Helpers ────────────────────────────────────────────────────
11
+ /**
12
+ * Build a search query from file paths.
13
+ *
14
+ * Extracts meaningful segments from file paths (directory names,
15
+ * file names without extensions) to use as search terms.
16
+ *
17
+ * @example
18
+ * ["src/auth/login.ts", "lib/db/pool.ts"]
19
+ * → "auth login db pool"
20
+ */
21
+ function buildSearchQuery(fileList) {
22
+ const terms = new Set();
23
+ for (const filePath of fileList) {
24
+ // Split path into segments
25
+ const segments = filePath.split('/').filter(Boolean);
26
+ for (const segment of segments) {
27
+ // Skip common uninformative directories
28
+ if (['src', 'lib', 'dist', 'build', 'node_modules', 'test', 'tests'].includes(segment)) {
29
+ continue;
30
+ }
31
+ // Remove file extension and add as a search term
32
+ const name = segment.replace(/\.[^.]+$/, '');
33
+ if (name.length > 2) {
34
+ terms.add(name);
35
+ }
36
+ }
37
+ }
38
+ return [...terms].slice(0, 10).join(' ');
39
+ }
40
+ // ─── Main Function ──────────────────────────────────────────────
41
+ /**
42
+ * Search past review observations for context relevant to the current diff.
43
+ *
44
+ * Returns a formatted string suitable for injection into agent prompts,
45
+ * or null if no relevant observations are found (or if db is unavailable).
46
+ *
47
+ * @param db - Database instance (from ghagga-db). Typed as unknown for loose coupling.
48
+ * @param project - Project identifier (e.g., "owner/repo")
49
+ * @param fileList - List of file paths in the current diff
50
+ * @returns Formatted memory context string, or null
51
+ */
52
+ export async function searchMemoryForContext(db, project, fileList) {
53
+ try {
54
+ if (!db)
55
+ return null;
56
+ const query = buildSearchQuery(fileList);
57
+ if (!query)
58
+ return null;
59
+ // Search with a reasonable limit — we don't want to flood the prompt
60
+ const observations = await searchObservations(db, project, query, { limit: 8 });
61
+ if (!observations || observations.length === 0)
62
+ return null;
63
+ // Format observations for prompt injection
64
+ return formatMemoryContext(observations.map((obs) => ({
65
+ type: obs.type,
66
+ title: obs.title,
67
+ content: obs.content,
68
+ })));
69
+ }
70
+ catch (error) {
71
+ // Memory is optional — never let it break the review pipeline
72
+ console.warn('[ghagga] Memory search failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
73
+ return null;
74
+ }
75
+ }
76
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/memory/search.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAYnD,mEAAmE;AAEnE;;;;;;;;;GASG;AACH,SAAS,gBAAgB,CAAC,QAAkB;IAC1C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAEhC,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,CAAC;QAChC,2BAA2B;QAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAErD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,wCAAwC;YACxC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBACvF,SAAS;YACX,CAAC;YAED,iDAAiD;YACjD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC7C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED,mEAAmE;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,EAAW,EACX,OAAe,EACf,QAAkB;IAElB,IAAI,CAAC;QACH,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAErB,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,qEAAqE;QACrE,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAA8C,EAC9C,OAAO,EACP,KAAK,EACL,EAAE,KAAK,EAAE,CAAC,EAAE,CACb,CAAC;QAEF,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAE5D,2CAA2C;QAC3C,OAAO,mBAAmB,CACxB,YAAY,CAAC,GAAG,CAAC,CAAC,GAAgB,EAAE,EAAE,CAAC,CAAC;YACtC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,OAAO,EAAE,GAAG,CAAC,OAAO;SACrB,CAAC,CAAC,CACJ,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,8DAA8D;QAC9D,OAAO,CAAC,IAAI,CACV,uDAAuD,EACvD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Main review pipeline orchestrator.
3
+ *
4
+ * Coordinates the entire review flow:
5
+ * 1. Validate input
6
+ * 2. Parse and filter the diff
7
+ * 3. Detect tech stacks
8
+ * 4. Run static analysis tools
9
+ * 5. Search memory for past context
10
+ * 6. Execute the selected agent mode
11
+ * 7. Persist new observations to memory
12
+ * 8. Return the final result
13
+ *
14
+ * Each step degrades gracefully — if static analysis fails, or
15
+ * memory is unavailable, the pipeline continues with what it has.
16
+ */
17
+ import type { ReviewInput, ReviewResult } from './types.js';
18
+ /**
19
+ * Run the full review pipeline.
20
+ *
21
+ * This is the primary entry point for all review operations.
22
+ * It orchestrates parsing, analysis, agent execution, and
23
+ * memory operations in a resilient pipeline that degrades
24
+ * gracefully when optional components fail.
25
+ *
26
+ * @param input - Complete review input with diff, config, and settings
27
+ * @returns ReviewResult with status, findings, and metadata
28
+ */
29
+ export declare function reviewPipeline(input: ReviewInput): Promise<ReviewResult>;
30
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAgB,MAAM,YAAY,CAAC;AA4B1E;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAiK9E"}
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Main review pipeline orchestrator.
3
+ *
4
+ * Coordinates the entire review flow:
5
+ * 1. Validate input
6
+ * 2. Parse and filter the diff
7
+ * 3. Detect tech stacks
8
+ * 4. Run static analysis tools
9
+ * 5. Search memory for past context
10
+ * 6. Execute the selected agent mode
11
+ * 7. Persist new observations to memory
12
+ * 8. Return the final result
13
+ *
14
+ * Each step degrades gracefully — if static analysis fails, or
15
+ * memory is unavailable, the pipeline continues with what it has.
16
+ */
17
+ import { parseDiffFiles, filterIgnoredFiles, truncateDiff } from './utils/diff.js';
18
+ import { detectStacks } from './utils/stack-detect.js';
19
+ import { calculateTokenBudget } from './utils/token-budget.js';
20
+ import { runStaticAnalysis, formatStaticAnalysisContext } from './tools/runner.js';
21
+ import { searchMemoryForContext } from './memory/search.js';
22
+ import { persistReviewObservations } from './memory/persist.js';
23
+ import { buildStackHints } from './agents/prompts.js';
24
+ import { runSimpleReview } from './agents/simple.js';
25
+ import { runWorkflowReview } from './agents/workflow.js';
26
+ import { runConsensusReview } from './agents/consensus.js';
27
+ // ─── Validation ─────────────────────────────────────────────────
28
+ /**
29
+ * Validate the review input for required fields.
30
+ * Throws descriptive errors for misconfiguration.
31
+ */
32
+ function validateInput(input) {
33
+ if (!input.diff || input.diff.trim().length === 0) {
34
+ throw new Error('Review input must include a non-empty diff');
35
+ }
36
+ if (!input.apiKey) {
37
+ throw new Error('Review input must include an API key');
38
+ }
39
+ if (!input.provider) {
40
+ throw new Error('Review input must specify an LLM provider');
41
+ }
42
+ if (!input.model) {
43
+ throw new Error('Review input must specify a model');
44
+ }
45
+ }
46
+ // ─── Pipeline ───────────────────────────────────────────────────
47
+ /**
48
+ * Run the full review pipeline.
49
+ *
50
+ * This is the primary entry point for all review operations.
51
+ * It orchestrates parsing, analysis, agent execution, and
52
+ * memory operations in a resilient pipeline that degrades
53
+ * gracefully when optional components fail.
54
+ *
55
+ * @param input - Complete review input with diff, config, and settings
56
+ * @returns ReviewResult with status, findings, and metadata
57
+ */
58
+ export async function reviewPipeline(input) {
59
+ const startTime = Date.now();
60
+ const emit = input.onProgress ?? (() => { });
61
+ // ── Step 1: Validate ───────────────────────────────────────
62
+ validateInput(input);
63
+ emit({ step: 'validate', message: 'Input validated' });
64
+ // ── Step 2: Parse and filter the diff ──────────────────────
65
+ const allFiles = parseDiffFiles(input.diff);
66
+ const filteredFiles = filterIgnoredFiles(allFiles, input.settings.ignorePatterns);
67
+ emit({
68
+ step: 'parse-diff',
69
+ message: `Parsed ${allFiles.length} files from diff, ${filteredFiles.length} after filtering`,
70
+ detail: filteredFiles.map((f) => ` ${f.path}`).join('\n'),
71
+ });
72
+ // If all files were filtered out, skip the review
73
+ if (filteredFiles.length === 0) {
74
+ return createSkippedResult(input, startTime);
75
+ }
76
+ // Reconstruct filtered diff and get file list
77
+ const filteredDiff = filteredFiles.map((f) => f.content).join('\n');
78
+ const fileList = filteredFiles.map((f) => f.path);
79
+ // ── Step 3: Detect tech stacks ─────────────────────────────
80
+ const stacks = detectStacks(fileList);
81
+ const stackHints = buildStackHints(stacks);
82
+ emit({
83
+ step: 'detect-stacks',
84
+ message: `Detected ${stacks.length} tech stack(s)`,
85
+ detail: stacks.length > 0 ? stacks.map((s) => ` ${s}`).join('\n') : ' (none detected)',
86
+ });
87
+ // ── Step 4: Truncate diff to fit token budget ──────────────
88
+ const { diffBudget } = calculateTokenBudget(input.model);
89
+ const { truncated: truncatedDiff } = truncateDiff(filteredDiff, diffBudget);
90
+ emit({
91
+ step: 'token-budget',
92
+ message: `Token budget: ${diffBudget.toLocaleString()} tokens for diff`,
93
+ });
94
+ // ── Step 5: Run static analysis (in parallel with memory) ──
95
+ emit({ step: 'static-analysis', message: 'Running static analysis & memory search...' });
96
+ const [staticResult, memoryContext] = await Promise.all([
97
+ runStaticAnalysisSafe(fileList, input),
98
+ searchMemorySafe(input, fileList),
99
+ ]);
100
+ const staticContext = formatStaticAnalysisContext(staticResult);
101
+ {
102
+ const toolsSummary = Object.entries(staticResult)
103
+ .map(([name, result]) => ` ${name}: ${result.status} (${result.findings.length} findings)`)
104
+ .join('\n');
105
+ emit({
106
+ step: 'static-results',
107
+ message: 'Static analysis complete',
108
+ detail: toolsSummary + (memoryContext ? '\n memory: loaded' : '\n memory: disabled'),
109
+ });
110
+ }
111
+ // ── Step 6: Execute agent mode ─────────────────────────────
112
+ emit({ step: 'agent-start', message: `Running ${input.mode} agent...` });
113
+ let result;
114
+ switch (input.mode) {
115
+ case 'simple':
116
+ result = await runSimpleReview({
117
+ diff: truncatedDiff,
118
+ provider: input.provider,
119
+ model: input.model,
120
+ apiKey: input.apiKey,
121
+ staticContext,
122
+ memoryContext,
123
+ stackHints,
124
+ onProgress: input.onProgress,
125
+ });
126
+ break;
127
+ case 'workflow':
128
+ result = await runWorkflowReview({
129
+ diff: truncatedDiff,
130
+ provider: input.provider,
131
+ model: input.model,
132
+ apiKey: input.apiKey,
133
+ staticContext,
134
+ memoryContext,
135
+ stackHints,
136
+ onProgress: input.onProgress,
137
+ });
138
+ break;
139
+ case 'consensus':
140
+ // Consensus mode uses the primary model with different stances
141
+ // In production, the caller would configure multiple models via the context
142
+ result = await runConsensusReview({
143
+ diff: truncatedDiff,
144
+ models: [
145
+ { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'for' },
146
+ { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'against' },
147
+ { provider: input.provider, model: input.model, apiKey: input.apiKey, stance: 'neutral' },
148
+ ],
149
+ staticContext,
150
+ memoryContext,
151
+ stackHints,
152
+ onProgress: input.onProgress,
153
+ });
154
+ break;
155
+ default: {
156
+ const _exhaustive = input.mode;
157
+ throw new Error(`Unknown review mode: ${_exhaustive}`);
158
+ }
159
+ }
160
+ // ── Step 7: Merge static analysis into result ──────────────
161
+ result.staticAnalysis = staticResult;
162
+ result.memoryContext = memoryContext;
163
+ // Add static analysis findings to the result's findings array
164
+ const staticFindings = [
165
+ ...staticResult.semgrep.findings,
166
+ ...staticResult.trivy.findings,
167
+ ...staticResult.cpd.findings,
168
+ ];
169
+ result.findings = [...result.findings, ...staticFindings];
170
+ // Track which tools ran successfully
171
+ result.metadata.toolsRun = [];
172
+ result.metadata.toolsSkipped = [];
173
+ for (const [name, tool] of Object.entries(staticResult)) {
174
+ if (tool.status === 'success') {
175
+ result.metadata.toolsRun.push(name);
176
+ }
177
+ else {
178
+ result.metadata.toolsSkipped.push(name);
179
+ }
180
+ }
181
+ // Update execution time to cover the full pipeline
182
+ result.metadata.executionTimeMs = Date.now() - startTime;
183
+ // ── Step 8: Persist to memory (fire-and-forget) ────────────
184
+ if (input.settings.enableMemory && input.db && input.context) {
185
+ // Don't await — memory persistence shouldn't block the response
186
+ persistReviewObservations(input.db, input.context.repoFullName, input.context.prNumber, result).catch((error) => {
187
+ console.warn('[ghagga] Memory persist failed (non-fatal):', error instanceof Error ? error.message : String(error));
188
+ });
189
+ }
190
+ return result;
191
+ }
192
+ // ─── Helpers ────────────────────────────────────────────────────
193
+ /**
194
+ * Run static analysis with graceful degradation.
195
+ * Returns a result with all tools skipped if anything goes wrong.
196
+ */
197
+ async function runStaticAnalysisSafe(fileList, input) {
198
+ try {
199
+ // Build a file map for static analysis (paths only, content from diff)
200
+ const files = new Map();
201
+ for (const path of fileList) {
202
+ files.set(path, ''); // Content is extracted from diff by the tool runner
203
+ }
204
+ return await runStaticAnalysis(files, '.', {
205
+ enableSemgrep: input.settings.enableSemgrep,
206
+ enableTrivy: input.settings.enableTrivy,
207
+ enableCpd: input.settings.enableCpd,
208
+ customRules: input.settings.customRules,
209
+ });
210
+ }
211
+ catch (error) {
212
+ console.warn('[ghagga] Static analysis failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
213
+ const errorResult = {
214
+ status: 'error',
215
+ findings: [],
216
+ error: error instanceof Error ? error.message : String(error),
217
+ executionTimeMs: 0,
218
+ };
219
+ return {
220
+ semgrep: errorResult,
221
+ trivy: errorResult,
222
+ cpd: errorResult,
223
+ };
224
+ }
225
+ }
226
+ /**
227
+ * Search memory with graceful degradation.
228
+ * Returns null if memory is disabled or unavailable.
229
+ */
230
+ async function searchMemorySafe(input, fileList) {
231
+ if (!input.settings.enableMemory || !input.db || !input.context) {
232
+ return null;
233
+ }
234
+ try {
235
+ return await searchMemoryForContext(input.db, input.context.repoFullName, fileList);
236
+ }
237
+ catch (error) {
238
+ console.warn('[ghagga] Memory search failed (degrading gracefully):', error instanceof Error ? error.message : String(error));
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * Create a SKIPPED result when all files are filtered out.
244
+ */
245
+ function createSkippedResult(input, startTime) {
246
+ return {
247
+ status: 'SKIPPED',
248
+ summary: 'All files in the diff matched ignore patterns. No review was performed.',
249
+ findings: [],
250
+ staticAnalysis: {
251
+ semgrep: { status: 'skipped', findings: [], executionTimeMs: 0 },
252
+ trivy: { status: 'skipped', findings: [], executionTimeMs: 0 },
253
+ cpd: { status: 'skipped', findings: [], executionTimeMs: 0 },
254
+ },
255
+ memoryContext: null,
256
+ metadata: {
257
+ mode: input.mode,
258
+ provider: input.provider,
259
+ model: input.model,
260
+ tokensUsed: 0,
261
+ executionTimeMs: Date.now() - startTime,
262
+ toolsRun: [],
263
+ toolsSkipped: ['semgrep', 'trivy', 'cpd'],
264
+ },
265
+ };
266
+ }
267
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Provider fallback chain.
3
+ *
4
+ * Tries each provider/model pair in order. If one fails with a
5
+ * server error (5xx) or times out, it moves to the next provider.
6
+ * Client errors (4xx) are NOT retried — they indicate misconfiguration.
7
+ */
8
+ import type { LLMProvider } from '../types.js';
9
+ export interface FallbackProvider {
10
+ provider: LLMProvider;
11
+ model: string;
12
+ apiKey: string;
13
+ }
14
+ export interface FallbackOptions {
15
+ /** Ordered list of providers to try */
16
+ providers: FallbackProvider[];
17
+ /** System prompt */
18
+ system: string;
19
+ /** User prompt */
20
+ prompt: string;
21
+ /** Temperature for generation (default: 0.3 for review consistency) */
22
+ temperature?: number;
23
+ }
24
+ export interface FallbackResult {
25
+ /** Generated text */
26
+ text: string;
27
+ /** Provider that succeeded */
28
+ provider: LLMProvider;
29
+ /** Model that succeeded */
30
+ model: string;
31
+ /** Approximate tokens used (prompt + completion) */
32
+ tokensUsed: number;
33
+ }
34
+ /**
35
+ * Generate text with automatic provider fallback.
36
+ *
37
+ * Tries each provider in order. On retryable errors (5xx, timeout,
38
+ * rate limit), moves to the next provider. Throws the last error
39
+ * if all providers fail.
40
+ *
41
+ * @param options - Providers, system prompt, and user prompt
42
+ * @returns Generated text with metadata about which provider succeeded
43
+ * @throws The last encountered error if all providers fail
44
+ */
45
+ export declare function generateWithFallback(options: FallbackOptions): Promise<FallbackResult>;
46
+ //# sourceMappingURL=fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../../src/providers/fallback.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAE9B,oBAAoB;IACpB,MAAM,EAAE,MAAM,CAAC;IAEf,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IAEf,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IAEb,8BAA8B;IAC9B,QAAQ,EAAE,WAAW,CAAC;IAEtB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IAEd,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;CACpB;AAkCD;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Provider fallback chain.
3
+ *
4
+ * Tries each provider/model pair in order. If one fails with a
5
+ * server error (5xx) or times out, it moves to the next provider.
6
+ * Client errors (4xx) are NOT retried — they indicate misconfiguration.
7
+ */
8
+ import { generateText } from 'ai';
9
+ import { createModel } from './index.js';
10
+ // ─── Helpers ────────────────────────────────────────────────────
11
+ /**
12
+ * Check if an error is a retryable server-side error (5xx or timeout).
13
+ */
14
+ function isRetryableError(error) {
15
+ if (error instanceof Error) {
16
+ const message = error.message.toLowerCase();
17
+ // Network/timeout errors
18
+ if (message.includes('timeout') || message.includes('econnreset') || message.includes('econnrefused')) {
19
+ return true;
20
+ }
21
+ // Check for HTTP status codes in error (provider SDKs often include them)
22
+ const statusMatch = /status[:\s]*(\d{3})/i.exec(message);
23
+ if (statusMatch) {
24
+ const status = parseInt(statusMatch[1], 10);
25
+ return status >= 500;
26
+ }
27
+ // Rate limit (429) — also retryable with a different provider
28
+ if (message.includes('rate limit') || message.includes('429')) {
29
+ return true;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ // ─── Main Function ──────────────────────────────────────────────
35
+ /**
36
+ * Generate text with automatic provider fallback.
37
+ *
38
+ * Tries each provider in order. On retryable errors (5xx, timeout,
39
+ * rate limit), moves to the next provider. Throws the last error
40
+ * if all providers fail.
41
+ *
42
+ * @param options - Providers, system prompt, and user prompt
43
+ * @returns Generated text with metadata about which provider succeeded
44
+ * @throws The last encountered error if all providers fail
45
+ */
46
+ export async function generateWithFallback(options) {
47
+ const { providers, system, prompt, temperature = 0.3 } = options;
48
+ if (providers.length === 0) {
49
+ throw new Error('No providers configured for fallback chain');
50
+ }
51
+ let lastError;
52
+ for (const { provider, model, apiKey } of providers) {
53
+ try {
54
+ const languageModel = createModel(provider, model, apiKey);
55
+ const result = await generateText({
56
+ model: languageModel,
57
+ system,
58
+ prompt,
59
+ temperature,
60
+ });
61
+ // Calculate tokens used from the response
62
+ const tokensUsed = (result.usage?.promptTokens ?? 0) + (result.usage?.completionTokens ?? 0);
63
+ return {
64
+ text: result.text,
65
+ provider,
66
+ model,
67
+ tokensUsed,
68
+ };
69
+ }
70
+ catch (error) {
71
+ lastError = error;
72
+ if (isRetryableError(error)) {
73
+ // Log and continue to next provider
74
+ console.warn(`[ghagga] Provider ${provider}/${model} failed with retryable error, trying next...`, error instanceof Error ? error.message : String(error));
75
+ continue;
76
+ }
77
+ // Non-retryable error (4xx, auth, etc.) — throw immediately
78
+ throw error;
79
+ }
80
+ }
81
+ // All providers failed with retryable errors
82
+ throw lastError;
83
+ }
84
+ //# sourceMappingURL=fallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/providers/fallback.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAuCzC,mEAAmE;AAEnE;;GAEG;AACH,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAE5C,yBAAyB;QACzB,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YACtG,OAAO,IAAI,CAAC;QACd,CAAC;QAED,0EAA0E;QAC1E,MAAM,WAAW,GAAG,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzD,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC;YAC7C,OAAO,MAAM,IAAI,GAAG,CAAC;QACvB,CAAC;QAED,8DAA8D;QAC9D,IAAI,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9D,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mEAAmE;AAEnE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAwB;IAExB,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC;IAEjE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,SAAkB,CAAC;IAEvB,KAAK,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,aAAa,GAAG,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;YAE3D,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;gBAChC,KAAK,EAAE,aAAa;gBACpB,MAAM;gBACN,MAAM;gBACN,WAAW;aACZ,CAAC,CAAC;YAEH,0CAA0C;YAC1C,MAAM,UAAU,GACd,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,gBAAgB,IAAI,CAAC,CAAC,CAAC;YAE5E,OAAO;gBACL,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,QAAQ;gBACR,KAAK;gBACL,UAAU;aACX,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAC;YAElB,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,oCAAoC;gBACpC,OAAO,CAAC,IAAI,CACV,qBAAqB,QAAQ,IAAI,KAAK,8CAA8C,EACpF,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;gBACF,SAAS;YACX,CAAC;YAED,4DAA4D;YAC5D,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,6CAA6C;IAC7C,MAAM,SAAS,CAAC;AAClB,CAAC"}