@denisvieiradev/gitwise-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +840 -0
- package/dist/index.js +3077 -0
- package/dist/index.js.map +1 -0
- package/dist/testing/index.d.ts +58 -0
- package/dist/testing/index.js +83 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-DnMpR1qf.d.ts +29 -0
- package/package.json +58 -0
- package/templates/.gitkeep +0 -0
- package/templates/commit.md +1 -0
- package/templates/pr.md +11 -0
- package/templates/release-changelog.md +19 -0
- package/templates/release-notes.md +13 -0
- package/templates/release-version.md +11 -0
- package/templates/review.md +19 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { L as LLMProvider, a as LLMChatRequest, b as LLMChatResponse } from '../types-DnMpR1qf.js';
|
|
2
|
+
|
|
3
|
+
interface MockResponse {
|
|
4
|
+
content: string;
|
|
5
|
+
tokens?: {
|
|
6
|
+
input: number;
|
|
7
|
+
output: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
interface PrefixMatcher {
|
|
11
|
+
prefix: string;
|
|
12
|
+
response: MockResponse;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* MockLLMProvider — deterministic LLM stub for tests.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* const mock = new MockLLMProvider();
|
|
19
|
+
* mock.queueByIndex({ content: "result" }); // first call
|
|
20
|
+
* mock.queueByPrefix("Analyze", { content: "analysis" }); // by prompt prefix
|
|
21
|
+
* await someFunction(mock);
|
|
22
|
+
* mock.assertCallCount(2);
|
|
23
|
+
* mock.assertTotalTokens({ input: 10, output: 5 });
|
|
24
|
+
*/
|
|
25
|
+
declare class MockLLMProvider implements LLMProvider {
|
|
26
|
+
private readonly indexedResponses;
|
|
27
|
+
private readonly prefixResponses;
|
|
28
|
+
private callCount;
|
|
29
|
+
private totalInputTokens;
|
|
30
|
+
private totalOutputTokens;
|
|
31
|
+
private readonly calls;
|
|
32
|
+
/** Queue a response to return for the N-th call (0-indexed). */
|
|
33
|
+
queueByIndex(response: MockResponse): this;
|
|
34
|
+
/** Queue a response to return when userMessage starts with the given prefix. */
|
|
35
|
+
queueByPrefix(prefix: string, response: MockResponse): this;
|
|
36
|
+
chat(req: LLMChatRequest): Promise<LLMChatResponse>;
|
|
37
|
+
private buildResponse;
|
|
38
|
+
/** Returns all recorded calls. */
|
|
39
|
+
getCalls(): readonly LLMChatRequest[];
|
|
40
|
+
/** Returns the number of chat() calls made. */
|
|
41
|
+
getCallCount(): number;
|
|
42
|
+
/** Assert the provider was called exactly N times. */
|
|
43
|
+
assertCallCount(expected: number): void;
|
|
44
|
+
/** Assert accumulated token totals. */
|
|
45
|
+
assertTotalTokens(expected: {
|
|
46
|
+
input?: number;
|
|
47
|
+
output?: number;
|
|
48
|
+
}): void;
|
|
49
|
+
getTotalTokens(): {
|
|
50
|
+
input: number;
|
|
51
|
+
output: number;
|
|
52
|
+
};
|
|
53
|
+
reset(): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare const __testingPlaceholder__: unique symbol;
|
|
57
|
+
|
|
58
|
+
export { MockLLMProvider, type MockResponse, type PrefixMatcher, __testingPlaceholder__ };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/testing/mock-llm-provider.ts
|
|
2
|
+
var MockLLMProvider = class {
|
|
3
|
+
indexedResponses = [];
|
|
4
|
+
prefixResponses = [];
|
|
5
|
+
callCount = 0;
|
|
6
|
+
totalInputTokens = 0;
|
|
7
|
+
totalOutputTokens = 0;
|
|
8
|
+
calls = [];
|
|
9
|
+
/** Queue a response to return for the N-th call (0-indexed). */
|
|
10
|
+
queueByIndex(response) {
|
|
11
|
+
this.indexedResponses.push(response);
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
/** Queue a response to return when userMessage starts with the given prefix. */
|
|
15
|
+
queueByPrefix(prefix, response) {
|
|
16
|
+
this.prefixResponses.push({ prefix, response });
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
async chat(req) {
|
|
20
|
+
this.calls.push(req);
|
|
21
|
+
const idx = this.callCount++;
|
|
22
|
+
for (const matcher of this.prefixResponses) {
|
|
23
|
+
if (req.userMessage.startsWith(matcher.prefix) || req.systemPrompt.startsWith(matcher.prefix)) {
|
|
24
|
+
return this.buildResponse(matcher.response);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const indexed = this.indexedResponses[idx];
|
|
28
|
+
if (indexed) {
|
|
29
|
+
return this.buildResponse(indexed);
|
|
30
|
+
}
|
|
31
|
+
return this.buildResponse({ content: `mock-response-${idx}` });
|
|
32
|
+
}
|
|
33
|
+
buildResponse(r) {
|
|
34
|
+
const tokens = r.tokens ?? { input: 10, output: 5 };
|
|
35
|
+
this.totalInputTokens += tokens.input;
|
|
36
|
+
this.totalOutputTokens += tokens.output;
|
|
37
|
+
return { content: r.content, tokens };
|
|
38
|
+
}
|
|
39
|
+
/** Returns all recorded calls. */
|
|
40
|
+
getCalls() {
|
|
41
|
+
return this.calls;
|
|
42
|
+
}
|
|
43
|
+
/** Returns the number of chat() calls made. */
|
|
44
|
+
getCallCount() {
|
|
45
|
+
return this.callCount;
|
|
46
|
+
}
|
|
47
|
+
/** Assert the provider was called exactly N times. */
|
|
48
|
+
assertCallCount(expected) {
|
|
49
|
+
if (this.callCount !== expected) {
|
|
50
|
+
throw new Error(`Expected ${expected} LLM call(s), got ${this.callCount}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Assert accumulated token totals. */
|
|
54
|
+
assertTotalTokens(expected) {
|
|
55
|
+
if (expected.input !== void 0 && this.totalInputTokens !== expected.input) {
|
|
56
|
+
throw new Error(`Expected ${expected.input} input tokens, got ${this.totalInputTokens}`);
|
|
57
|
+
}
|
|
58
|
+
if (expected.output !== void 0 && this.totalOutputTokens !== expected.output) {
|
|
59
|
+
throw new Error(`Expected ${expected.output} output tokens, got ${this.totalOutputTokens}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getTotalTokens() {
|
|
63
|
+
return { input: this.totalInputTokens, output: this.totalOutputTokens };
|
|
64
|
+
}
|
|
65
|
+
reset() {
|
|
66
|
+
this.indexedResponses.length = 0;
|
|
67
|
+
this.prefixResponses.length = 0;
|
|
68
|
+
this.calls.length = 0;
|
|
69
|
+
this.callCount = 0;
|
|
70
|
+
this.totalInputTokens = 0;
|
|
71
|
+
this.totalOutputTokens = 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/testing/index.ts
|
|
76
|
+
var __testingPlaceholder__ = /* @__PURE__ */ Symbol.for(
|
|
77
|
+
"@denisvieiradev/gitwise-core/testing#placeholder"
|
|
78
|
+
);
|
|
79
|
+
export {
|
|
80
|
+
MockLLMProvider,
|
|
81
|
+
__testingPlaceholder__
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/mock-llm-provider.ts","../../src/testing/index.ts"],"sourcesContent":["import type { LLMProvider, LLMChatRequest, LLMChatResponse } from \"../providers/types.js\";\n\nexport interface MockResponse {\n content: string;\n tokens?: { input: number; output: number };\n}\n\nexport interface PrefixMatcher {\n prefix: string;\n response: MockResponse;\n}\n\n/**\n * MockLLMProvider — deterministic LLM stub for tests.\n *\n * Usage:\n * const mock = new MockLLMProvider();\n * mock.queueByIndex({ content: \"result\" }); // first call\n * mock.queueByPrefix(\"Analyze\", { content: \"analysis\" }); // by prompt prefix\n * await someFunction(mock);\n * mock.assertCallCount(2);\n * mock.assertTotalTokens({ input: 10, output: 5 });\n */\nexport class MockLLMProvider implements LLMProvider {\n private readonly indexedResponses: MockResponse[] = [];\n private readonly prefixResponses: PrefixMatcher[] = [];\n private callCount = 0;\n private totalInputTokens = 0;\n private totalOutputTokens = 0;\n private readonly calls: LLMChatRequest[] = [];\n\n /** Queue a response to return for the N-th call (0-indexed). */\n queueByIndex(response: MockResponse): this {\n this.indexedResponses.push(response);\n return this;\n }\n\n /** Queue a response to return when userMessage starts with the given prefix. */\n queueByPrefix(prefix: string, response: MockResponse): this {\n this.prefixResponses.push({ prefix, response });\n return this;\n }\n\n async chat(req: LLMChatRequest): Promise<LLMChatResponse> {\n this.calls.push(req);\n const idx = this.callCount++;\n\n // Prefix matching takes precedence\n for (const matcher of this.prefixResponses) {\n if (req.userMessage.startsWith(matcher.prefix) || req.systemPrompt.startsWith(matcher.prefix)) {\n return this.buildResponse(matcher.response);\n }\n }\n\n // Index-based fallback\n const indexed = this.indexedResponses[idx];\n if (indexed) {\n return this.buildResponse(indexed);\n }\n\n // Default response\n return this.buildResponse({ content: `mock-response-${idx}` });\n }\n\n private buildResponse(r: MockResponse): LLMChatResponse {\n const tokens = r.tokens ?? { input: 10, output: 5 };\n this.totalInputTokens += tokens.input;\n this.totalOutputTokens += tokens.output;\n return { content: r.content, tokens };\n }\n\n /** Returns all recorded calls. */\n getCalls(): readonly LLMChatRequest[] {\n return this.calls;\n }\n\n /** Returns the number of chat() calls made. */\n getCallCount(): number {\n return this.callCount;\n }\n\n /** Assert the provider was called exactly N times. */\n assertCallCount(expected: number): void {\n if (this.callCount !== expected) {\n throw new Error(`Expected ${expected} LLM call(s), got ${this.callCount}`);\n }\n }\n\n /** Assert accumulated token totals. */\n assertTotalTokens(expected: { input?: number; output?: number }): void {\n if (expected.input !== undefined && this.totalInputTokens !== expected.input) {\n throw new Error(`Expected ${expected.input} input tokens, got ${this.totalInputTokens}`);\n }\n if (expected.output !== undefined && this.totalOutputTokens !== expected.output) {\n throw new Error(`Expected ${expected.output} output tokens, got ${this.totalOutputTokens}`);\n }\n }\n\n getTotalTokens(): { input: number; output: number } {\n return { input: this.totalInputTokens, output: this.totalOutputTokens };\n }\n\n reset(): void {\n this.indexedResponses.length = 0;\n this.prefixResponses.length = 0;\n this.calls.length = 0;\n this.callCount = 0;\n this.totalInputTokens = 0;\n this.totalOutputTokens = 0;\n }\n}\n","export const __testingPlaceholder__ = Symbol.for(\n \"@denisvieiradev/gitwise-core/testing#placeholder\",\n);\n\nexport { MockLLMProvider } from \"./mock-llm-provider.js\";\nexport type { MockResponse, PrefixMatcher } from \"./mock-llm-provider.js\";\n"],"mappings":";AAuBO,IAAM,kBAAN,MAA6C;AAAA,EACjC,mBAAmC,CAAC;AAAA,EACpC,kBAAmC,CAAC;AAAA,EAC7C,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACX,QAA0B,CAAC;AAAA;AAAA,EAG5C,aAAa,UAA8B;AACzC,SAAK,iBAAiB,KAAK,QAAQ;AACnC,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,cAAc,QAAgB,UAA8B;AAC1D,SAAK,gBAAgB,KAAK,EAAE,QAAQ,SAAS,CAAC;AAC9C,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,KAAK,KAA+C;AACxD,SAAK,MAAM,KAAK,GAAG;AACnB,UAAM,MAAM,KAAK;AAGjB,eAAW,WAAW,KAAK,iBAAiB;AAC1C,UAAI,IAAI,YAAY,WAAW,QAAQ,MAAM,KAAK,IAAI,aAAa,WAAW,QAAQ,MAAM,GAAG;AAC7F,eAAO,KAAK,cAAc,QAAQ,QAAQ;AAAA,MAC5C;AAAA,IACF;AAGA,UAAM,UAAU,KAAK,iBAAiB,GAAG;AACzC,QAAI,SAAS;AACX,aAAO,KAAK,cAAc,OAAO;AAAA,IACnC;AAGA,WAAO,KAAK,cAAc,EAAE,SAAS,iBAAiB,GAAG,GAAG,CAAC;AAAA,EAC/D;AAAA,EAEQ,cAAc,GAAkC;AACtD,UAAM,SAAS,EAAE,UAAU,EAAE,OAAO,IAAI,QAAQ,EAAE;AAClD,SAAK,oBAAoB,OAAO;AAChC,SAAK,qBAAqB,OAAO;AACjC,WAAO,EAAE,SAAS,EAAE,SAAS,OAAO;AAAA,EACtC;AAAA;AAAA,EAGA,WAAsC;AACpC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,eAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,gBAAgB,UAAwB;AACtC,QAAI,KAAK,cAAc,UAAU;AAC/B,YAAM,IAAI,MAAM,YAAY,QAAQ,qBAAqB,KAAK,SAAS,EAAE;AAAA,IAC3E;AAAA,EACF;AAAA;AAAA,EAGA,kBAAkB,UAAqD;AACrE,QAAI,SAAS,UAAU,UAAa,KAAK,qBAAqB,SAAS,OAAO;AAC5E,YAAM,IAAI,MAAM,YAAY,SAAS,KAAK,sBAAsB,KAAK,gBAAgB,EAAE;AAAA,IACzF;AACA,QAAI,SAAS,WAAW,UAAa,KAAK,sBAAsB,SAAS,QAAQ;AAC/E,YAAM,IAAI,MAAM,YAAY,SAAS,MAAM,uBAAuB,KAAK,iBAAiB,EAAE;AAAA,IAC5F;AAAA,EACF;AAAA,EAEA,iBAAoD;AAClD,WAAO,EAAE,OAAO,KAAK,kBAAkB,QAAQ,KAAK,kBAAkB;AAAA,EACxE;AAAA,EAEA,QAAc;AACZ,SAAK,iBAAiB,SAAS;AAC/B,SAAK,gBAAgB,SAAS;AAC9B,SAAK,MAAM,SAAS;AACpB,SAAK,YAAY;AACjB,SAAK,mBAAmB;AACxB,SAAK,oBAAoB;AAAA,EAC3B;AACF;;;AC9GO,IAAM,yBAAyB,uBAAO;AAAA,EAC3C;AACF;","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type ModelTier = "fast" | "balanced" | "powerful";
|
|
2
|
+
interface LLMChatRequest {
|
|
3
|
+
systemPrompt: string;
|
|
4
|
+
userMessage: string;
|
|
5
|
+
tier: ModelTier;
|
|
6
|
+
}
|
|
7
|
+
interface LLMChatResponse {
|
|
8
|
+
content: string;
|
|
9
|
+
tokens: {
|
|
10
|
+
input: number;
|
|
11
|
+
output: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface LLMProvider {
|
|
15
|
+
chat(req: LLMChatRequest): Promise<LLMChatResponse>;
|
|
16
|
+
}
|
|
17
|
+
interface ModelConfig {
|
|
18
|
+
fast: string;
|
|
19
|
+
balanced: string;
|
|
20
|
+
powerful: string;
|
|
21
|
+
}
|
|
22
|
+
interface ProviderConfig {
|
|
23
|
+
kind: "api" | "claude-code";
|
|
24
|
+
models: ModelConfig;
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
claudeCliPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type { LLMProvider as L, ModelTier as M, ProviderConfig as P, LLMChatRequest as a, LLMChatResponse as b, ModelConfig as c };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@denisvieiradev/gitwise-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared logic for gitwise: non-interactive commit/review/pr/release commands, LLM providers, git/github primitives, prompt templates.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./testing": {
|
|
14
|
+
"types": "./dist/testing/index.d.ts",
|
|
15
|
+
"import": "./dist/testing/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"templates",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"test": "node --experimental-vm-modules ../../node_modules/.bin/jest --passWithNoTests",
|
|
28
|
+
"lint": "tsc --noEmit",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"gitwise",
|
|
33
|
+
"git",
|
|
34
|
+
"ai",
|
|
35
|
+
"claude",
|
|
36
|
+
"commit",
|
|
37
|
+
"pull-request",
|
|
38
|
+
"release",
|
|
39
|
+
"code-review"
|
|
40
|
+
],
|
|
41
|
+
"author": "Denis Vieira <denisvieira05@gmail.com> (https://github.com/denisvieiradev)",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/denisvieiradev/gitwise.git",
|
|
46
|
+
"directory": "packages/core"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/denisvieiradev/gitwise/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/denisvieiradev/gitwise#readme",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.12.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@anthropic-ai/sdk": "^0.109.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{type}}({{scope}}): {{description}}
|
package/templates/pr.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
You are a technical writer generating a changelog entry following the Keep a Changelog format (https://keepachangelog.com).
|
|
2
|
+
|
|
3
|
+
Group changes under these sections (omit empty sections):
|
|
4
|
+
### Added
|
|
5
|
+
### Changed
|
|
6
|
+
### Fixed
|
|
7
|
+
### Removed
|
|
8
|
+
### Deprecated
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- Each entry is a bullet point starting with a verb in past tense
|
|
13
|
+
- Be concise but specific; include scope when relevant (e.g., "Added `release` command for automated versioning")
|
|
14
|
+
- Do NOT include commit hashes, author names, or dates
|
|
15
|
+
- Do NOT include a version header — it will be added programmatically
|
|
16
|
+
- Do NOT wrap the output in markdown fences
|
|
17
|
+
- Collapse minor related changes into single entries when appropriate
|
|
18
|
+
|
|
19
|
+
Project: {{projectName}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
You are a product communications specialist writing user-facing release notes for version {{version}} of {{projectName}}.
|
|
2
|
+
|
|
3
|
+
Write entirely in {{language}}.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Write for non-technical users (product managers, stakeholders, end users)
|
|
7
|
+
- Focus on WHAT changed and WHY it matters, not HOW it was implemented
|
|
8
|
+
- Use a friendly, professional tone
|
|
9
|
+
- Start with a brief summary paragraph (2-3 sentences)
|
|
10
|
+
- Then list highlights as bullet points
|
|
11
|
+
- End with a brief closing note if appropriate
|
|
12
|
+
- Do NOT include code snippets, file paths, commit hashes, or technical jargon
|
|
13
|
+
- Do NOT wrap the output in markdown fences
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
You are a release engineer analyzing git commits to determine the appropriate semantic version bump.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- MAJOR: breaking changes, removed public APIs, incompatible interface changes
|
|
5
|
+
- MINOR: new features, new commands, new public APIs (backward compatible)
|
|
6
|
+
- PATCH: bug fixes, documentation updates, refactoring, dependency updates, chore tasks
|
|
7
|
+
|
|
8
|
+
Current version: {{currentVersion}}
|
|
9
|
+
|
|
10
|
+
Analyze the commits below and respond with JSON only (no markdown fences, no extra text):
|
|
11
|
+
{"suggestion": "major|minor|patch", "reasoning": "one sentence explaining why"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
You are a senior code reviewer. Analyze the diff and produce a code review with findings in these categories:
|
|
2
|
+
|
|
3
|
+
## Critical
|
|
4
|
+
Issues that must be fixed before merging (bugs, security, data loss).
|
|
5
|
+
|
|
6
|
+
## Suggestions
|
|
7
|
+
Improvements worth considering (performance, readability, patterns).
|
|
8
|
+
|
|
9
|
+
## Nitpicks
|
|
10
|
+
Minor style or convention issues.
|
|
11
|
+
|
|
12
|
+
For each finding, include:
|
|
13
|
+
- File and line reference
|
|
14
|
+
- Description of the issue
|
|
15
|
+
- Suggested fix
|
|
16
|
+
|
|
17
|
+
End with a summary: total findings count per category and overall recommendation (approve, request changes).
|
|
18
|
+
|
|
19
|
+
{{diff}}
|