From c5d713a7b8e91170bf34012be79a55e0accbde91 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Fri, 10 Jan 2025 19:49:00 +0100
Subject: [PATCH 01/11] feat(utils): add string helper

---
 packages/utils/src/index.ts                |  7 ++
 packages/utils/src/lib/string.ts           | 58 ++++++++++++++++
 packages/utils/src/lib/string.unit.test.ts | 81 ++++++++++++++++++++++
 packages/utils/src/lib/types.ts            |  7 ++
 4 files changed, 153 insertions(+)
 create mode 100644 packages/utils/src/lib/string.ts
 create mode 100644 packages/utils/src/lib/string.unit.test.ts

diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 58dd0dbe6..08e9ee4ec 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -95,6 +95,12 @@ export {
   formatReportScore,
 } from './lib/reports/utils.js';
 export { isSemver, normalizeSemver, sortSemvers } from './lib/semver.js';
+export {
+  camelCaseToKebabCase,
+  kebabCaseToSentence,
+  kebabCaseToCamelCase,
+  camelCaseToSentence,
+} from './lib/string.js';
 export * from './lib/text-formats/index.js';
 export {
   capitalize,
@@ -121,6 +127,7 @@ export type {
   ItemOrArray,
   Prettify,
   WithRequired,
+  CamelCaseToKebabCase,
 } from './lib/types.js';
 export { verboseUtils } from './lib/verbose-utils.js';
 export { zodErrorMessageBuilder } from './lib/zod-validation.js';
diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts
new file mode 100644
index 000000000..d183d6377
--- /dev/null
+++ b/packages/utils/src/lib/string.ts
@@ -0,0 +1,58 @@
+import type { CamelCaseToKebabCase } from './types.js';
+
+/**
+ * Converts a kebab-case string to camelCase.
+ * @param string - The kebab-case string to convert.
+ * @returns The camelCase string.
+ */
+export function kebabCaseToCamelCase(string: string) {
+  return string
+    .split('-')
+    .map((segment, index) =>
+      index === 0
+        ? segment
+        : segment.charAt(0).toUpperCase() + segment.slice(1),
+    )
+    .join('');
+}
+
+/**
+ * Converts a camelCase string to kebab-case.
+ * @param string - The camelCase string to convert.
+ * @returns The kebab-case string.
+ */
+export function camelCaseToKebabCase<T extends string>(
+  string: T,
+): CamelCaseToKebabCase<T> {
+  return string
+    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
+    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
+    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
+    .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
+    .toLowerCase() as CamelCaseToKebabCase<T>;
+}
+
+/**
+ * Formats a slug to a readable title.
+ * @param slug - The slug to format.
+ * @returns The formatted title.
+ */
+export function kebabCaseToSentence(slug: string = '') {
+  return slug
+    .replace(/-/g, ' ')
+    .replace(/\b\w/g, letter => letter.toUpperCase());
+}
+
+/**
+ * Formats a slug to a readable title.
+ * @param slug - The slug to format.
+ * @returns The formatted title.
+ */
+export function camelCaseToSentence(slug: string = '') {
+  return slug
+    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
+    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
+    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
+    .replace(/[\s_]+/g, ' ') // Replace spaces and underscores with hyphens
+    .replace(/\b\w/g, letter => letter.toUpperCase());
+}
diff --git a/packages/utils/src/lib/string.unit.test.ts b/packages/utils/src/lib/string.unit.test.ts
new file mode 100644
index 000000000..d7100c984
--- /dev/null
+++ b/packages/utils/src/lib/string.unit.test.ts
@@ -0,0 +1,81 @@
+import {
+  camelCaseToKebabCase,
+  kebabCaseToCamelCase,
+  kebabCaseToSentence,
+} from './string.js';
+
+describe('kebabCaseToCamelCase', () => {
+  it('should convert simple kebab-case to camelCase', () => {
+    expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld');
+  });
+
+  it('should handle multiple hyphens', () => {
+    expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe(
+      'thisIsALongString',
+    );
+  });
+
+  it('should preserve numbers', () => {
+    expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test');
+  });
+
+  it('should handle single word', () => {
+    expect(kebabCaseToCamelCase('hello')).toBe('hello');
+  });
+
+  it('should handle empty string', () => {
+    expect(kebabCaseToCamelCase('')).toBe('');
+  });
+});
+
+describe('camelCaseToKebabCase', () => {
+  it('should convert simple camelCase to kebab-case', () => {
+    expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world');
+  });
+
+  it('should handle multiple capital letters', () => {
+    expect(camelCaseToKebabCase('thisIsALongString')).toBe(
+      'this-is-a-long-string',
+    );
+  });
+
+  it('should handle consecutive capital letters', () => {
+    expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser');
+  });
+
+  it('should handle spaces and underscores', () => {
+    expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test');
+  });
+
+  it('should handle single word', () => {
+    expect(camelCaseToKebabCase('hello')).toBe('hello');
+  });
+
+  it('should handle empty string', () => {
+    expect(camelCaseToKebabCase('')).toBe('');
+  });
+});
+
+describe('kebabCaseToSentence', () => {
+  it('should convert simple slug to title case', () => {
+    expect(kebabCaseToSentence('hello-world')).toBe('Hello World');
+  });
+
+  it('should handle multiple hyphens', () => {
+    expect(kebabCaseToSentence('this-is-a-title')).toBe('This Is A Title');
+  });
+
+  it('should handle empty string', () => {
+    expect(kebabCaseToSentence()).toBe('');
+  });
+
+  it('should handle single word', () => {
+    expect(kebabCaseToSentence('hello')).toBe('Hello');
+  });
+
+  it('should handle numbers in slug', () => {
+    expect(kebabCaseToSentence('chapter-1-introduction')).toBe(
+      'Chapter 1 Introduction',
+    );
+  });
+});
diff --git a/packages/utils/src/lib/types.ts b/packages/utils/src/lib/types.ts
index 03b53ea77..62360f9ab 100644
--- a/packages/utils/src/lib/types.ts
+++ b/packages/utils/src/lib/types.ts
@@ -15,3 +15,10 @@ export type WithRequired<T, K extends keyof T> = Prettify<
 >;
 
 export type Prettify<T> = { [K in keyof T]: T[K] };
+
+export type CamelCaseToKebabCase<T extends string> =
+  T extends `${infer First}${infer Rest}`
+    ? Rest extends Uncapitalize<Rest>
+      ? `${Lowercase<First>}${CamelCaseToKebabCase<Rest>}`
+      : `${Lowercase<First>}-${CamelCaseToKebabCase<Rest>}`
+    : T;

From 619be589a814d75ec8237b7e58b513c9e5962129 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Fri, 10 Jan 2025 19:52:08 +0100
Subject: [PATCH 02/11] fix(utils): update docs

---
 packages/utils/src/lib/string.ts | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts
index d183d6377..c476a534c 100644
--- a/packages/utils/src/lib/string.ts
+++ b/packages/utils/src/lib/string.ts
@@ -34,22 +34,22 @@ export function camelCaseToKebabCase<T extends string>(
 
 /**
  * Formats a slug to a readable title.
- * @param slug - The slug to format.
+ * @param string - The slug to format.
  * @returns The formatted title.
  */
-export function kebabCaseToSentence(slug: string = '') {
-  return slug
+export function kebabCaseToSentence(string = '') {
+  return string
     .replace(/-/g, ' ')
     .replace(/\b\w/g, letter => letter.toUpperCase());
 }
 
 /**
  * Formats a slug to a readable title.
- * @param slug - The slug to format.
+ * @param string - The slug to format.
  * @returns The formatted title.
  */
-export function camelCaseToSentence(slug: string = '') {
-  return slug
+export function camelCaseToSentence(string = '') {
+  return string
     .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
     .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
     .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase

From 90b78f9f0a5be223eb0a32179341e3089ac811e5 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 13:20:21 +0100
Subject: [PATCH 03/11] fix(utils): refine string helper

---
 packages/utils/src/lib/string.ts | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts
index c476a534c..a20e7f7d6 100644
--- a/packages/utils/src/lib/string.ts
+++ b/packages/utils/src/lib/string.ts
@@ -33,23 +33,23 @@ export function camelCaseToKebabCase<T extends string>(
 }
 
 /**
- * Formats a slug to a readable title.
- * @param string - The slug to format.
+ * Formats a string to a readable title.
+ * @param stringToFormat - The string to format.
  * @returns The formatted title.
  */
-export function kebabCaseToSentence(string = '') {
-  return string
+export function kebabCaseToSentence(stringToFormat: string) {
+  return stringToFormat
     .replace(/-/g, ' ')
     .replace(/\b\w/g, letter => letter.toUpperCase());
 }
 
 /**
- * Formats a slug to a readable title.
- * @param string - The slug to format.
+ * Formats a string to a readable title.
+ * @param stringToFormat - The string to format.
  * @returns The formatted title.
  */
-export function camelCaseToSentence(string = '') {
-  return string
+export function camelCaseToSentence(stringToFormat : string) {
+  return stringToFormat
     .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
     .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
     .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase

From 15c4677f0341732f784610a2ff626e0af05c41fa Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 13:33:25 +0100
Subject: [PATCH 04/11] fix(utils): refactor string helper

---
 packages/utils/src/index.ts                |  2 +-
 packages/utils/src/lib/string.ts           | 26 ++++++---
 packages/utils/src/lib/string.unit.test.ts | 64 ++++++++++++++++++++--
 3 files changed, 79 insertions(+), 13 deletions(-)

diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 08e9ee4ec..acf952922 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -99,7 +99,7 @@ export {
   camelCaseToKebabCase,
   kebabCaseToSentence,
   kebabCaseToCamelCase,
-  camelCaseToSentence,
+  formatToSentenceCase,
 } from './lib/string.js';
 export * from './lib/text-formats/index.js';
 export {
diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts
index a20e7f7d6..53a175752 100644
--- a/packages/utils/src/lib/string.ts
+++ b/packages/utils/src/lib/string.ts
@@ -44,15 +44,27 @@ export function kebabCaseToSentence(stringToFormat: string) {
 }
 
 /**
- * Formats a string to a readable title.
+ * Converts a camelCase, PascalCase, kebab-case, or snake_case string into a readable sentence.
+ * It also ensures numbers are separated correctly from words.
+ *
+ * @example
+ * camelCaseToSentence('helloWorld') // 'Hello world'
+ * camelCaseToSentence('thisIsALongString') // 'This is a long string'
+ * camelCaseToSentence('my-1-word') // 'My 1 word'
+ * camelCaseToSentence('my_snake_case') // 'My snake case'
+ * camelCaseToSentence('PascalCaseExample') // 'Pascal case example'
+ * camelCaseToSentence('Chapter1Introduction') // 'Chapter 1 Introduction'
+ *
  * @param stringToFormat - The string to format.
  * @returns The formatted title.
  */
-export function camelCaseToSentence(stringToFormat : string) {
+export function formatToSentenceCase(stringToFormat: string): string {
   return stringToFormat
-    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
-    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
-    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
-    .replace(/[\s_]+/g, ' ') // Replace spaces and underscores with hyphens
-    .replace(/\b\w/g, letter => letter.toUpperCase());
+    .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase -> split before uppercase letters
+    .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // PascalCase -> split uppercase sequences
+    .replace(/(\d+)([A-Za-z])/g, '$1 $2') // Separate numbers from letters (before a letter)
+    .replace(/([A-Za-z])(\d+)/g, '$1 $2') // Separate numbers from letters (after a letter)
+    .replace(/[-_]+/g, ' ') // Convert kebab-case and snake_case to spaces
+    .trim() // Remove leading/trailing spaces
+    .replace(/\b\w/g, char => char.toUpperCase()); // Capitalize first letter
 }
diff --git a/packages/utils/src/lib/string.unit.test.ts b/packages/utils/src/lib/string.unit.test.ts
index d7100c984..c53fdf09b 100644
--- a/packages/utils/src/lib/string.unit.test.ts
+++ b/packages/utils/src/lib/string.unit.test.ts
@@ -1,5 +1,6 @@
 import {
   camelCaseToKebabCase,
+  formatToSentenceCase,
   kebabCaseToCamelCase,
   kebabCaseToSentence,
 } from './string.js';
@@ -57,7 +58,7 @@ describe('camelCaseToKebabCase', () => {
 });
 
 describe('kebabCaseToSentence', () => {
-  it('should convert simple slug to title case', () => {
+  it('should convert simple kebab-case string to title case', () => {
     expect(kebabCaseToSentence('hello-world')).toBe('Hello World');
   });
 
@@ -65,10 +66,6 @@ describe('kebabCaseToSentence', () => {
     expect(kebabCaseToSentence('this-is-a-title')).toBe('This Is A Title');
   });
 
-  it('should handle empty string', () => {
-    expect(kebabCaseToSentence()).toBe('');
-  });
-
   it('should handle single word', () => {
     expect(kebabCaseToSentence('hello')).toBe('Hello');
   });
@@ -79,3 +76,60 @@ describe('kebabCaseToSentence', () => {
     );
   });
 });
+
+describe('formatToSentenceCase', () => {
+  it('should convert camelCase to sentence case', () => {
+    expect(formatToSentenceCase('helloWorld')).toBe('Hello World');
+    expect(formatToSentenceCase('thisIsATitle')).toBe('This Is A Title');
+    expect(formatToSentenceCase('myTestString')).toBe('My Test String');
+  });
+
+  it('should convert PascalCase to sentence case', () => {
+    expect(formatToSentenceCase('HelloWorld')).toBe('Hello World');
+    expect(formatToSentenceCase('FormatToSentenceCase')).toBe(
+      'Format To Sentence Case',
+    );
+  });
+
+  it('should handle strings with numbers correctly', () => {
+    expect(formatToSentenceCase('chapter1Introduction')).toBe(
+      'Chapter 1 Introduction',
+    );
+    expect(formatToSentenceCase('version2Release')).toBe('Version 2 Release');
+    expect(formatToSentenceCase('test123String')).toBe('Test 123 String');
+  });
+
+  it('should handle kebab-case and snake_case formats', () => {
+    expect(formatToSentenceCase('hello-world')).toBe('Hello World');
+    expect(formatToSentenceCase('snake_case_example')).toBe(
+      'Snake Case Example',
+    );
+  });
+
+  it('should return capitalized single words', () => {
+    expect(formatToSentenceCase('hello')).toBe('Hello');
+    expect(formatToSentenceCase('test')).toBe('Test');
+  });
+
+  it('should handle acronyms properly', () => {
+    expect(formatToSentenceCase('APIResponse')).toBe('API Response');
+    expect(formatToSentenceCase('HTTPError')).toBe('HTTP Error');
+  });
+
+  it('should handle mixed case formats', () => {
+    expect(formatToSentenceCase('thisIs-mixed_CASE')).toBe(
+      'This Is Mixed CASE',
+    );
+  });
+
+  it('should return an empty string when given an empty input', () => {
+    expect(formatToSentenceCase('')).toBe('');
+  });
+
+  it('should not modify already formatted sentences', () => {
+    expect(formatToSentenceCase('This is a sentence')).toBe(
+      'This Is A Sentence',
+    );
+    expect(formatToSentenceCase('Hello World')).toBe('Hello World');
+  });
+});

From 28ade3713b98e7e9f07269e2850301afea3626c2 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 14:04:50 +0100
Subject: [PATCH 05/11] fix(utils): added title and sentence case functions

---
 packages/utils/src/index.ts                   |   2 +-
 packages/utils/src/lib/case-conversions.ts    | 101 ++++++++++
 .../src/lib/case-conversions.unit.test.ts     | 172 ++++++++++++++++++
 packages/utils/src/lib/string.ts              |  70 -------
 packages/utils/src/lib/string.unit.test.ts    | 135 --------------
 5 files changed, 274 insertions(+), 206 deletions(-)
 create mode 100644 packages/utils/src/lib/case-conversions.ts
 create mode 100644 packages/utils/src/lib/case-conversions.unit.test.ts
 delete mode 100644 packages/utils/src/lib/string.ts
 delete mode 100644 packages/utils/src/lib/string.unit.test.ts

diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index acf952922..d77a4fba9 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -100,7 +100,7 @@ export {
   kebabCaseToSentence,
   kebabCaseToCamelCase,
   formatToSentenceCase,
-} from './lib/string.js';
+} from './lib/case-conversions.js';
 export * from './lib/text-formats/index.js';
 export {
   capitalize,
diff --git a/packages/utils/src/lib/case-conversions.ts b/packages/utils/src/lib/case-conversions.ts
new file mode 100644
index 000000000..0b7e36857
--- /dev/null
+++ b/packages/utils/src/lib/case-conversions.ts
@@ -0,0 +1,101 @@
+import type { CamelCaseToKebabCase } from './types.js';
+
+/**
+ * Converts a kebab-case string to camelCase.
+ * @param string - The kebab-case string to convert.
+ * @returns The camelCase string.
+ */
+export function kebabCaseToCamelCase(string: string) {
+  return string
+    .split('-')
+    .map((segment, index) =>
+      index === 0
+        ? segment
+        : segment.charAt(0).toUpperCase() + segment.slice(1),
+    )
+    .join('');
+}
+
+/**
+ * Converts a camelCase string to kebab-case.
+ * @param string - The camelCase string to convert.
+ * @returns The kebab-case string.
+ */
+export function camelCaseToKebabCase<T extends string>(
+  string: T,
+): CamelCaseToKebabCase<T> {
+  return string
+    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
+    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
+    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
+    .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
+    .toLowerCase() as CamelCaseToKebabCase<T>;
+}
+
+/**
+ * Converts a string to Title Case.
+ * - Capitalizes the first letter of each major word.
+ * - Keeps articles, conjunctions, and short prepositions in lowercase unless they are the first word.
+ *
+ * @param input - The string to convert.
+ * @returns The formatted title case string.
+ */
+export function toTitleCase(input: string): string {
+  const minorWords = new Set([
+    'a',
+    'an',
+    'the',
+    'and',
+    'or',
+    'but',
+    'for',
+    'nor',
+    'on',
+    'in',
+    'at',
+    'to',
+    'by',
+    'of',
+  ]);
+
+  return input
+    .replace(/([a-z])([A-Z])/g, '$1 $2') // Split PascalCase & camelCase
+    .replace(/[_-]/g, ' ') // Replace kebab-case and snake_case with spaces
+    .replace(/(\d+)/g, ' $1 ') // Add spaces around numbers
+    .replace(/\s+/g, ' ') // Remove extra spaces
+    .trim()
+    .split(' ')
+    .map((word, index) => {
+      // Preserve uppercase acronyms (e.g., API, HTTP)
+      if (/^[A-Z]{2,}$/.test(word)) {
+        return word;
+      }
+
+      // Capitalize first word or non-minor words
+      if (index === 0 || !minorWords.has(word.toLowerCase())) {
+        return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+      }
+      return word.toLowerCase();
+    })
+    .join(' ');
+}
+
+/**
+ * Converts a string to Sentence Case.
+ * - Capitalizes only the first letter of the sentence.
+ * - Retains case of proper nouns.
+ *
+ * @param input - The string to convert.
+ * @returns The formatted sentence case string.
+ */
+export function toSentenceCase(input: string): string {
+  return input
+    .replace(/([a-z])([A-Z])/g, '$1 $2') // Split PascalCase & camelCase
+    .replace(/[_-]/g, ' ') // Replace kebab-case and snake_case with spaces
+    .replace(/(\d+)/g, ' $1 ') // Add spaces around numbers
+    .replace(/\s+/g, ' ') // Remove extra spaces
+    .trim()
+    .toLowerCase()
+    .replace(/^(\w)/, match => match.toUpperCase()) // Capitalize first letter
+    .replace(/\b([A-Z]{2,})\b/g, match => match); // Preserve uppercase acronyms
+}
diff --git a/packages/utils/src/lib/case-conversions.unit.test.ts b/packages/utils/src/lib/case-conversions.unit.test.ts
new file mode 100644
index 000000000..e64b4e719
--- /dev/null
+++ b/packages/utils/src/lib/case-conversions.unit.test.ts
@@ -0,0 +1,172 @@
+import {
+  camelCaseToKebabCase,
+  kebabCaseToCamelCase,
+  toSentenceCase,
+  toTitleCase,
+} from './case-conversions.js';
+
+describe('kebabCaseToCamelCase', () => {
+  it('should convert simple kebab-case to camelCase', () => {
+    expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld');
+  });
+
+  it('should handle multiple hyphens', () => {
+    expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe(
+      'thisIsALongString',
+    );
+  });
+
+  it('should preserve numbers', () => {
+    expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test');
+  });
+
+  it('should handle single word', () => {
+    expect(kebabCaseToCamelCase('hello')).toBe('hello');
+  });
+
+  it('should handle empty string', () => {
+    expect(kebabCaseToCamelCase('')).toBe('');
+  });
+});
+
+describe('camelCaseToKebabCase', () => {
+  it('should convert simple camelCase to kebab-case', () => {
+    expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world');
+  });
+
+  it('should handle multiple capital letters', () => {
+    expect(camelCaseToKebabCase('thisIsALongString')).toBe(
+      'this-is-a-long-string',
+    );
+  });
+
+  it('should handle consecutive capital letters', () => {
+    expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser');
+  });
+
+  it('should handle spaces and underscores', () => {
+    expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test');
+  });
+
+  it('should handle single word', () => {
+    expect(camelCaseToKebabCase('hello')).toBe('hello');
+  });
+
+  it('should handle empty string', () => {
+    expect(camelCaseToKebabCase('')).toBe('');
+  });
+});
+
+describe('toTitleCase', () => {
+  it('should capitalize each word in a simple sentence', () => {
+    expect(toTitleCase('hello world')).toBe('Hello World');
+  });
+
+  it('should capitalize each word in a longer sentence', () => {
+    expect(toTitleCase('this is a title')).toBe('This Is a Title');
+  });
+
+  it('should convert PascalCase to title case', () => {
+    expect(toTitleCase('FormatToTitleCase')).toBe('Format to Title Case');
+  });
+
+  it('should convert camelCase to title case', () => {
+    expect(toTitleCase('thisIsTest')).toBe('This Is Test');
+  });
+
+  it('should convert kebab-case to title case', () => {
+    expect(toTitleCase('hello-world-example')).toBe('Hello World Example');
+  });
+
+  it('should convert snake_case to title case', () => {
+    expect(toTitleCase('snake_case_example')).toBe('Snake Case Example');
+  });
+
+  it('should capitalize a single word', () => {
+    expect(toTitleCase('hello')).toBe('Hello');
+  });
+
+  it('should handle numbers in words correctly', () => {
+    expect(toTitleCase('chapter1Introduction')).toBe('Chapter 1 Introduction');
+  });
+
+  it('should handle numbers in slugs correctly', () => {
+    expect(toTitleCase('version2Release')).toBe('Version 2 Release');
+  });
+
+  it('should handle acronyms properly', () => {
+    expect(toTitleCase('apiResponse')).toBe('Api Response');
+  });
+
+  it('should handle mixed-case inputs correctly', () => {
+    expect(toTitleCase('thisIs-mixed_CASE')).toBe('This Is Mixed CASE');
+  });
+
+  it('should not modify already formatted title case text', () => {
+    expect(toTitleCase('Hello World')).toBe('Hello World');
+  });
+
+  it('should return an empty string when given an empty input', () => {
+    expect(toTitleCase('')).toBe('');
+  });
+});
+
+describe('toSentenceCase', () => {
+  it('should convert a simple sentence to sentence case', () => {
+    expect(toSentenceCase('hello world')).toBe('Hello world');
+  });
+
+  it('should maintain a correctly formatted sentence', () => {
+    expect(toSentenceCase('This is a test')).toBe('This is a test');
+  });
+
+  it('should convert PascalCase to sentence case', () => {
+    expect(toSentenceCase('FormatToSentenceCase')).toBe(
+      'Format to sentence case',
+    );
+  });
+
+  it('should convert camelCase to sentence case', () => {
+    expect(toSentenceCase('thisIsTest')).toBe('This is test');
+  });
+
+  it('should convert kebab-case to sentence case', () => {
+    expect(toSentenceCase('hello-world-example')).toBe('Hello world example');
+  });
+
+  it('should convert snake_case to sentence case', () => {
+    expect(toSentenceCase('snake_case_example')).toBe('Snake case example');
+  });
+
+  it('should capitalize a single word', () => {
+    expect(toSentenceCase('hello')).toBe('Hello');
+  });
+
+  it('should handle numbers in words correctly', () => {
+    expect(toSentenceCase('chapter1Introduction')).toBe(
+      'Chapter 1 introduction',
+    );
+  });
+
+  it('should handle numbers in slugs correctly', () => {
+    expect(toSentenceCase('version2Release')).toBe('Version 2 release');
+  });
+
+  it('should handle acronyms properly', () => {
+    expect(toSentenceCase('apiResponse')).toBe('Api response');
+  });
+
+  it('should handle mixed-case inputs correctly', () => {
+    expect(toSentenceCase('thisIs-mixed_CASE')).toBe('This is mixed case');
+  });
+
+  it('should not modify already formatted sentence case text', () => {
+    expect(toSentenceCase('This is a normal sentence.')).toBe(
+      'This is a normal sentence.',
+    );
+  });
+
+  it('should return an empty string when given an empty input', () => {
+    expect(toSentenceCase('')).toBe('');
+  });
+});
diff --git a/packages/utils/src/lib/string.ts b/packages/utils/src/lib/string.ts
deleted file mode 100644
index 53a175752..000000000
--- a/packages/utils/src/lib/string.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { CamelCaseToKebabCase } from './types.js';
-
-/**
- * Converts a kebab-case string to camelCase.
- * @param string - The kebab-case string to convert.
- * @returns The camelCase string.
- */
-export function kebabCaseToCamelCase(string: string) {
-  return string
-    .split('-')
-    .map((segment, index) =>
-      index === 0
-        ? segment
-        : segment.charAt(0).toUpperCase() + segment.slice(1),
-    )
-    .join('');
-}
-
-/**
- * Converts a camelCase string to kebab-case.
- * @param string - The camelCase string to convert.
- * @returns The kebab-case string.
- */
-export function camelCaseToKebabCase<T extends string>(
-  string: T,
-): CamelCaseToKebabCase<T> {
-  return string
-    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
-    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
-    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
-    .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
-    .toLowerCase() as CamelCaseToKebabCase<T>;
-}
-
-/**
- * Formats a string to a readable title.
- * @param stringToFormat - The string to format.
- * @returns The formatted title.
- */
-export function kebabCaseToSentence(stringToFormat: string) {
-  return stringToFormat
-    .replace(/-/g, ' ')
-    .replace(/\b\w/g, letter => letter.toUpperCase());
-}
-
-/**
- * Converts a camelCase, PascalCase, kebab-case, or snake_case string into a readable sentence.
- * It also ensures numbers are separated correctly from words.
- *
- * @example
- * camelCaseToSentence('helloWorld') // 'Hello world'
- * camelCaseToSentence('thisIsALongString') // 'This is a long string'
- * camelCaseToSentence('my-1-word') // 'My 1 word'
- * camelCaseToSentence('my_snake_case') // 'My snake case'
- * camelCaseToSentence('PascalCaseExample') // 'Pascal case example'
- * camelCaseToSentence('Chapter1Introduction') // 'Chapter 1 Introduction'
- *
- * @param stringToFormat - The string to format.
- * @returns The formatted title.
- */
-export function formatToSentenceCase(stringToFormat: string): string {
-  return stringToFormat
-    .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase -> split before uppercase letters
-    .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') // PascalCase -> split uppercase sequences
-    .replace(/(\d+)([A-Za-z])/g, '$1 $2') // Separate numbers from letters (before a letter)
-    .replace(/([A-Za-z])(\d+)/g, '$1 $2') // Separate numbers from letters (after a letter)
-    .replace(/[-_]+/g, ' ') // Convert kebab-case and snake_case to spaces
-    .trim() // Remove leading/trailing spaces
-    .replace(/\b\w/g, char => char.toUpperCase()); // Capitalize first letter
-}
diff --git a/packages/utils/src/lib/string.unit.test.ts b/packages/utils/src/lib/string.unit.test.ts
deleted file mode 100644
index c53fdf09b..000000000
--- a/packages/utils/src/lib/string.unit.test.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import {
-  camelCaseToKebabCase,
-  formatToSentenceCase,
-  kebabCaseToCamelCase,
-  kebabCaseToSentence,
-} from './string.js';
-
-describe('kebabCaseToCamelCase', () => {
-  it('should convert simple kebab-case to camelCase', () => {
-    expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld');
-  });
-
-  it('should handle multiple hyphens', () => {
-    expect(kebabCaseToCamelCase('this-is-a-long-string')).toBe(
-      'thisIsALongString',
-    );
-  });
-
-  it('should preserve numbers', () => {
-    expect(kebabCaseToCamelCase('user-123-test')).toBe('user123Test');
-  });
-
-  it('should handle single word', () => {
-    expect(kebabCaseToCamelCase('hello')).toBe('hello');
-  });
-
-  it('should handle empty string', () => {
-    expect(kebabCaseToCamelCase('')).toBe('');
-  });
-});
-
-describe('camelCaseToKebabCase', () => {
-  it('should convert simple camelCase to kebab-case', () => {
-    expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world');
-  });
-
-  it('should handle multiple capital letters', () => {
-    expect(camelCaseToKebabCase('thisIsALongString')).toBe(
-      'this-is-a-long-string',
-    );
-  });
-
-  it('should handle consecutive capital letters', () => {
-    expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser');
-  });
-
-  it('should handle spaces and underscores', () => {
-    expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test');
-  });
-
-  it('should handle single word', () => {
-    expect(camelCaseToKebabCase('hello')).toBe('hello');
-  });
-
-  it('should handle empty string', () => {
-    expect(camelCaseToKebabCase('')).toBe('');
-  });
-});
-
-describe('kebabCaseToSentence', () => {
-  it('should convert simple kebab-case string to title case', () => {
-    expect(kebabCaseToSentence('hello-world')).toBe('Hello World');
-  });
-
-  it('should handle multiple hyphens', () => {
-    expect(kebabCaseToSentence('this-is-a-title')).toBe('This Is A Title');
-  });
-
-  it('should handle single word', () => {
-    expect(kebabCaseToSentence('hello')).toBe('Hello');
-  });
-
-  it('should handle numbers in slug', () => {
-    expect(kebabCaseToSentence('chapter-1-introduction')).toBe(
-      'Chapter 1 Introduction',
-    );
-  });
-});
-
-describe('formatToSentenceCase', () => {
-  it('should convert camelCase to sentence case', () => {
-    expect(formatToSentenceCase('helloWorld')).toBe('Hello World');
-    expect(formatToSentenceCase('thisIsATitle')).toBe('This Is A Title');
-    expect(formatToSentenceCase('myTestString')).toBe('My Test String');
-  });
-
-  it('should convert PascalCase to sentence case', () => {
-    expect(formatToSentenceCase('HelloWorld')).toBe('Hello World');
-    expect(formatToSentenceCase('FormatToSentenceCase')).toBe(
-      'Format To Sentence Case',
-    );
-  });
-
-  it('should handle strings with numbers correctly', () => {
-    expect(formatToSentenceCase('chapter1Introduction')).toBe(
-      'Chapter 1 Introduction',
-    );
-    expect(formatToSentenceCase('version2Release')).toBe('Version 2 Release');
-    expect(formatToSentenceCase('test123String')).toBe('Test 123 String');
-  });
-
-  it('should handle kebab-case and snake_case formats', () => {
-    expect(formatToSentenceCase('hello-world')).toBe('Hello World');
-    expect(formatToSentenceCase('snake_case_example')).toBe(
-      'Snake Case Example',
-    );
-  });
-
-  it('should return capitalized single words', () => {
-    expect(formatToSentenceCase('hello')).toBe('Hello');
-    expect(formatToSentenceCase('test')).toBe('Test');
-  });
-
-  it('should handle acronyms properly', () => {
-    expect(formatToSentenceCase('APIResponse')).toBe('API Response');
-    expect(formatToSentenceCase('HTTPError')).toBe('HTTP Error');
-  });
-
-  it('should handle mixed case formats', () => {
-    expect(formatToSentenceCase('thisIs-mixed_CASE')).toBe(
-      'This Is Mixed CASE',
-    );
-  });
-
-  it('should return an empty string when given an empty input', () => {
-    expect(formatToSentenceCase('')).toBe('');
-  });
-
-  it('should not modify already formatted sentences', () => {
-    expect(formatToSentenceCase('This is a sentence')).toBe(
-      'This Is A Sentence',
-    );
-    expect(formatToSentenceCase('Hello World')).toBe('Hello World');
-  });
-});

From 15057fc5023605c8077a0ebf66f14455899c8eba Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 14:13:04 +0100
Subject: [PATCH 06/11] fix(utils): include PR feedback

---
 packages/utils/src/index.ts                      |  2 +-
 packages/utils/src/lib/case-conversions.ts       | 14 +++++++++++---
 .../utils/src/lib/case-conversions.unit.test.ts  | 16 ++++++++++++++++
 packages/utils/src/lib/text-formats/table.ts     |  2 +-
 packages/utils/src/lib/transform.ts              |  6 ------
 packages/utils/src/lib/transform.unit.test.ts    | 15 ---------------
 packages/utils/src/lib/types.ts                  |  5 +++++
 7 files changed, 34 insertions(+), 26 deletions(-)

diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index d77a4fba9..1c6a4946a 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -103,7 +103,6 @@ export {
 } from './lib/case-conversions.js';
 export * from './lib/text-formats/index.js';
 export {
-  capitalize,
   countOccurrences,
   distinct,
   factorOf,
@@ -131,3 +130,4 @@ export type {
 } from './lib/types.js';
 export { verboseUtils } from './lib/verbose-utils.js';
 export { zodErrorMessageBuilder } from './lib/zod-validation.js';
+export { capitalize } from './lib/case-conversions';
diff --git a/packages/utils/src/lib/case-conversions.ts b/packages/utils/src/lib/case-conversions.ts
index 0b7e36857..bcb32ca7f 100644
--- a/packages/utils/src/lib/case-conversions.ts
+++ b/packages/utils/src/lib/case-conversions.ts
@@ -1,11 +1,13 @@
-import type { CamelCaseToKebabCase } from './types.js';
+import type { CamelCaseToKebabCase, KebabCaseToCamelCase } from './types.js';
 
 /**
  * Converts a kebab-case string to camelCase.
  * @param string - The kebab-case string to convert.
  * @returns The camelCase string.
  */
-export function kebabCaseToCamelCase(string: string) {
+export function kebabCaseToCamelCase<T extends string>(
+  string: T,
+): KebabCaseToCamelCase<T> {
   return string
     .split('-')
     .map((segment, index) =>
@@ -13,7 +15,7 @@ export function kebabCaseToCamelCase(string: string) {
         ? segment
         : segment.charAt(0).toUpperCase() + segment.slice(1),
     )
-    .join('');
+    .join('') as KebabCaseToCamelCase<T>;
 }
 
 /**
@@ -99,3 +101,9 @@ export function toSentenceCase(input: string): string {
     .replace(/^(\w)/, match => match.toUpperCase()) // Capitalize first letter
     .replace(/\b([A-Z]{2,})\b/g, match => match); // Preserve uppercase acronyms
 }
+
+export function capitalize<T extends string>(text: T): Capitalize<T> {
+  return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
+    1,
+  )}` as Capitalize<T>;
+}
diff --git a/packages/utils/src/lib/case-conversions.unit.test.ts b/packages/utils/src/lib/case-conversions.unit.test.ts
index e64b4e719..b60829055 100644
--- a/packages/utils/src/lib/case-conversions.unit.test.ts
+++ b/packages/utils/src/lib/case-conversions.unit.test.ts
@@ -1,10 +1,26 @@
+import { describe, expect, it } from 'vitest';
 import {
   camelCaseToKebabCase,
+  capitalize,
   kebabCaseToCamelCase,
   toSentenceCase,
   toTitleCase,
 } from './case-conversions.js';
 
+describe('capitalize', () => {
+  it('should transform the first string letter to upper case', () => {
+    expect(capitalize('code PushUp')).toBe('Code PushUp');
+  });
+
+  it('should leave the first string letter in upper case', () => {
+    expect(capitalize('Code PushUp')).toBe('Code PushUp');
+  });
+
+  it('should accept empty string', () => {
+    expect(capitalize('')).toBe('');
+  });
+});
+
 describe('kebabCaseToCamelCase', () => {
   it('should convert simple kebab-case to camelCase', () => {
     expect(kebabCaseToCamelCase('hello-world')).toBe('helloWorld');
diff --git a/packages/utils/src/lib/text-formats/table.ts b/packages/utils/src/lib/text-formats/table.ts
index 6cfb4842c..14d57df0c 100644
--- a/packages/utils/src/lib/text-formats/table.ts
+++ b/packages/utils/src/lib/text-formats/table.ts
@@ -5,7 +5,7 @@ import type {
   TableColumnObject,
   TableColumnPrimitive,
 } from '@code-pushup/models';
-import { capitalize } from '../transform.js';
+import { capitalize } from '@code-pushup/utils';
 
 export function rowToStringArray({ rows, columns = [] }: Table): string[][] {
   if (Array.isArray(rows.at(0)) && typeof columns.at(0) === 'object') {
diff --git a/packages/utils/src/lib/transform.ts b/packages/utils/src/lib/transform.ts
index d3943a0c4..315c7d030 100644
--- a/packages/utils/src/lib/transform.ts
+++ b/packages/utils/src/lib/transform.ts
@@ -123,12 +123,6 @@ export function toJsonLines<T>(json: T[]) {
   return json.map(item => JSON.stringify(item)).join('\n');
 }
 
-export function capitalize<T extends string>(text: T): Capitalize<T> {
-  return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
-    1,
-  )}` as Capitalize<T>;
-}
-
 export function toNumberPrecision(
   value: number,
   decimalPlaces: number,
diff --git a/packages/utils/src/lib/transform.unit.test.ts b/packages/utils/src/lib/transform.unit.test.ts
index 1478f8d0d..6716d1e83 100644
--- a/packages/utils/src/lib/transform.unit.test.ts
+++ b/packages/utils/src/lib/transform.unit.test.ts
@@ -1,6 +1,5 @@
 import { describe, expect, it } from 'vitest';
 import {
-  capitalize,
   countOccurrences,
   deepClone,
   distinct,
@@ -273,20 +272,6 @@ describe('JSON lines format', () => {
   });
 });
 
-describe('capitalize', () => {
-  it('should transform the first string letter to upper case', () => {
-    expect(capitalize('code PushUp')).toBe('Code PushUp');
-  });
-
-  it('should leave the first string letter in upper case', () => {
-    expect(capitalize('Code PushUp')).toBe('Code PushUp');
-  });
-
-  it('should accept empty string', () => {
-    expect(capitalize('')).toBe('');
-  });
-});
-
 describe('toNumberPrecision', () => {
   it.each([
     [12.1, 0, 12],
diff --git a/packages/utils/src/lib/types.ts b/packages/utils/src/lib/types.ts
index 62360f9ab..5cbf40ac8 100644
--- a/packages/utils/src/lib/types.ts
+++ b/packages/utils/src/lib/types.ts
@@ -22,3 +22,8 @@ export type CamelCaseToKebabCase<T extends string> =
       ? `${Lowercase<First>}${CamelCaseToKebabCase<Rest>}`
       : `${Lowercase<First>}-${CamelCaseToKebabCase<Rest>}`
     : T;
+
+export type KebabCaseToCamelCase<T extends string> =
+  T extends `${infer First}-${infer Rest}`
+    ? `${First}${Capitalize<KebabCaseToCamelCase<Rest>>}`
+    : T;

From fa7485b16efdfed43ca9916591a5a6a821f92c6e Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 14:18:16 +0100
Subject: [PATCH 07/11] fix(utils): reuse capitalize

---
 packages/utils/src/index.ts                | 4 ++--
 packages/utils/src/lib/case-conversions.ts | 6 ++----
 2 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 704706889..52df761d8 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -97,10 +97,10 @@ export {
 export { isSemver, normalizeSemver, sortSemvers } from './lib/semver.js';
 export {
   camelCaseToKebabCase,
-  kebabCaseToSentence,
   kebabCaseToCamelCase,
-  formatToSentenceCase,
   capitalize,
+  toSentenceCase,
+  toTitleCase,
 } from './lib/case-conversions.js';
 export * from './lib/text-formats/index.js';
 export {
diff --git a/packages/utils/src/lib/case-conversions.ts b/packages/utils/src/lib/case-conversions.ts
index bcb32ca7f..0a99d44cc 100644
--- a/packages/utils/src/lib/case-conversions.ts
+++ b/packages/utils/src/lib/case-conversions.ts
@@ -75,7 +75,7 @@ export function toTitleCase(input: string): string {
 
       // Capitalize first word or non-minor words
       if (index === 0 || !minorWords.has(word.toLowerCase())) {
-        return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+        return capitalize(word);
       }
       return word.toLowerCase();
     })
@@ -103,7 +103,5 @@ export function toSentenceCase(input: string): string {
 }
 
 export function capitalize<T extends string>(text: T): Capitalize<T> {
-  return `${text.charAt(0).toLocaleUpperCase()}${text.slice(
-    1,
-  )}` as Capitalize<T>;
+  return `${text.charAt(0).toLocaleUpperCase()}${text.slice(1).toLowerCase()}` as Capitalize<T>;
 }

From da37aebefeb1838dd8234abd2326c041bcf078ed Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 14:29:12 +0100
Subject: [PATCH 08/11] fix(utils): reuse capitalize

---
 packages/utils/src/lib/case-conversions.ts           |  6 +-----
 packages/utils/src/lib/case-conversions.unit.test.ts | 10 +++++++---
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/utils/src/lib/case-conversions.ts b/packages/utils/src/lib/case-conversions.ts
index 0a99d44cc..3fb6a4086 100644
--- a/packages/utils/src/lib/case-conversions.ts
+++ b/packages/utils/src/lib/case-conversions.ts
@@ -10,11 +10,7 @@ export function kebabCaseToCamelCase<T extends string>(
 ): KebabCaseToCamelCase<T> {
   return string
     .split('-')
-    .map((segment, index) =>
-      index === 0
-        ? segment
-        : segment.charAt(0).toUpperCase() + segment.slice(1),
-    )
+    .map((segment, index) => (index === 0 ? segment : capitalize(segment)))
     .join('') as KebabCaseToCamelCase<T>;
 }
 
diff --git a/packages/utils/src/lib/case-conversions.unit.test.ts b/packages/utils/src/lib/case-conversions.unit.test.ts
index b60829055..7734c2177 100644
--- a/packages/utils/src/lib/case-conversions.unit.test.ts
+++ b/packages/utils/src/lib/case-conversions.unit.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it } from 'vitest';
 import {
   camelCaseToKebabCase,
   capitalize,
@@ -9,11 +9,15 @@ import {
 
 describe('capitalize', () => {
   it('should transform the first string letter to upper case', () => {
-    expect(capitalize('code PushUp')).toBe('Code PushUp');
+    expect(capitalize('code')).toBe('Code');
+  });
+
+  it('should lowercase all but the the first string letter', () => {
+    expect(capitalize('PushUp')).toBe('Pushup');
   });
 
   it('should leave the first string letter in upper case', () => {
-    expect(capitalize('Code PushUp')).toBe('Code PushUp');
+    expect(capitalize('Code')).toBe('Code');
   });
 
   it('should accept empty string', () => {

From 6bf0402c2e87c02f2fea77b1d015407a2d73c2cd Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 18:14:14 +0100
Subject: [PATCH 09/11] fix(utils): refactor tests

---
 .../src/lib/case-conversion.type.test.ts      | 63 +++++++++++++++++++
 packages/utils/src/lib/case-conversions.ts    | 12 ++--
 .../src/lib/case-conversions.unit.test.ts     | 26 ++++----
 3 files changed, 80 insertions(+), 21 deletions(-)
 create mode 100644 packages/utils/src/lib/case-conversion.type.test.ts

diff --git a/packages/utils/src/lib/case-conversion.type.test.ts b/packages/utils/src/lib/case-conversion.type.test.ts
new file mode 100644
index 000000000..230e59a59
--- /dev/null
+++ b/packages/utils/src/lib/case-conversion.type.test.ts
@@ -0,0 +1,63 @@
+import { assertType, describe, expectTypeOf, it } from 'vitest';
+import type { CamelCaseToKebabCase, KebabCaseToCamelCase } from './types.js';
+
+/* eslint-disable vitest/expect-expect */
+describe('CamelCaseToKebabCase', () => {
+  // ✅ CamelCase → kebab-case Type Tests
+
+  it('CamelCaseToKebabCase works correctly', () => {
+    expectTypeOf<
+      CamelCaseToKebabCase<'myTestString'>
+    >().toEqualTypeOf<'my-test-string'>();
+    expectTypeOf<
+      CamelCaseToKebabCase<'APIResponse'>
+    >().toEqualTypeOf<'a-p-i-response'>();
+    expectTypeOf<
+      CamelCaseToKebabCase<'myXMLParser'>
+    >().toEqualTypeOf<'my-x-m-l-parser'>();
+    expectTypeOf<
+      CamelCaseToKebabCase<'singleWord'>
+    >().toEqualTypeOf<'single-word'>();
+
+    // @ts-expect-error Ensures that non-camelCase strings do not pass
+    assertType<CamelCaseToKebabCase<'hello_world'>>();
+
+    // @ts-expect-error Numbers should not be transformed
+    assertType<CamelCaseToKebabCase<'version2Release'>>();
+  });
+
+  // ✅ kebab-case → CamelCase Type Tests
+  it('KebabCaseToCamelCase works correctly', () => {
+    expectTypeOf<
+      KebabCaseToCamelCase<'my-test-string'>
+    >().toEqualTypeOf<'myTestString'>();
+    expectTypeOf<
+      KebabCaseToCamelCase<'a-p-i-response'>
+    >().toEqualTypeOf<'aPIResponse'>();
+    expectTypeOf<
+      KebabCaseToCamelCase<'my-x-m-l-parser'>
+    >().toEqualTypeOf<'myXMLParser'>();
+    expectTypeOf<
+      KebabCaseToCamelCase<'single-word'>
+    >().toEqualTypeOf<'singleWord'>();
+
+    // @ts-expect-error Ensures that non-kebab-case inputs are not accepted
+    assertType<KebabCaseToCamelCase<'my Test String'>>();
+
+    // @ts-expect-error Numbers should not be transformed
+    assertType<KebabCaseToCamelCase<'version-2-release'>>();
+  });
+
+  // ✅ Edge Cases
+  it('Edge cases for case conversions', () => {
+    expectTypeOf<CamelCaseToKebabCase<''>>().toEqualTypeOf<''>();
+    expectTypeOf<KebabCaseToCamelCase<''>>().toEqualTypeOf<''>();
+
+    // @ts-expect-error Ensures no spaces allowed in input
+    assertType<CamelCaseToKebabCase<'this is not camelCase'>>();
+
+    // @ts-expect-error Ensures no mixed case with dashes
+    assertType<KebabCaseToCamelCase<'this-Is-Wrong'>>();
+  });
+});
+/* eslint-enable vitest/expect-expect */
diff --git a/packages/utils/src/lib/case-conversions.ts b/packages/utils/src/lib/case-conversions.ts
index 3fb6a4086..589b65b34 100644
--- a/packages/utils/src/lib/case-conversions.ts
+++ b/packages/utils/src/lib/case-conversions.ts
@@ -16,17 +16,15 @@ export function kebabCaseToCamelCase<T extends string>(
 
 /**
  * Converts a camelCase string to kebab-case.
- * @param string - The camelCase string to convert.
+ * @param input - The camelCase string to convert.
  * @returns The kebab-case string.
  */
 export function camelCaseToKebabCase<T extends string>(
-  string: T,
+  input: T,
 ): CamelCaseToKebabCase<T> {
-  return string
-    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Split between uppercase followed by uppercase+lowercase
-    .replace(/([a-z])([A-Z])/g, '$1-$2') // Split between lowercase followed by uppercase
-    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // Additional split for consecutive uppercase
-    .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens
+  return input
+    .replace(/([a-z])([A-Z])/g, '$1-$2') // Insert dash before uppercase letters
+    .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') // Handle consecutive uppercase letters
     .toLowerCase() as CamelCaseToKebabCase<T>;
 }
 
diff --git a/packages/utils/src/lib/case-conversions.unit.test.ts b/packages/utils/src/lib/case-conversions.unit.test.ts
index 7734c2177..8fdbdbba9 100644
--- a/packages/utils/src/lib/case-conversions.unit.test.ts
+++ b/packages/utils/src/lib/case-conversions.unit.test.ts
@@ -1,4 +1,4 @@
-import { beforeEach, describe, expect, it } from 'vitest';
+import { describe, expect, it } from 'vitest';
 import {
   camelCaseToKebabCase,
   capitalize,
@@ -50,30 +50,28 @@ describe('kebabCaseToCamelCase', () => {
 });
 
 describe('camelCaseToKebabCase', () => {
-  it('should convert simple camelCase to kebab-case', () => {
-    expect(camelCaseToKebabCase('helloWorld')).toBe('hello-world');
+  it('should convert camelCase to kebab-case', () => {
+    expect(camelCaseToKebabCase('myTestString')).toBe('my-test-string');
   });
 
-  it('should handle multiple capital letters', () => {
-    expect(camelCaseToKebabCase('thisIsALongString')).toBe(
-      'this-is-a-long-string',
-    );
+  it('should handle acronyms properly', () => {
+    expect(camelCaseToKebabCase('APIResponse')).toBe('api-response');
   });
 
-  it('should handle consecutive capital letters', () => {
+  it('should handle consecutive uppercase letters correctly', () => {
     expect(camelCaseToKebabCase('myXMLParser')).toBe('my-xml-parser');
   });
 
-  it('should handle spaces and underscores', () => {
-    expect(camelCaseToKebabCase('hello_world test')).toBe('hello-world-test');
+  it('should handle single-word camelCase', () => {
+    expect(camelCaseToKebabCase('singleWord')).toBe('single-word');
   });
 
-  it('should handle single word', () => {
-    expect(camelCaseToKebabCase('hello')).toBe('hello');
+  it('should not modify already kebab-case strings', () => {
+    expect(camelCaseToKebabCase('already-kebab')).toBe('already-kebab');
   });
 
-  it('should handle empty string', () => {
-    expect(camelCaseToKebabCase('')).toBe('');
+  it('should not modify non-camelCase inputs', () => {
+    expect(camelCaseToKebabCase('not_camelCase')).toBe('not_camel-case');
   });
 });
 

From ffd3b174ce4f1c9d7ee9ae3c156548be666af026 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 18:16:35 +0100
Subject: [PATCH 10/11] ci: extend consistent file name rule

---
 eslint.config.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/eslint.config.js b/eslint.config.js
index a9bff8986..6cea14bdf 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -87,7 +87,9 @@ export default tseslint.config(
     rules: {
       'vitest/consistent-test-filename': [
         'warn',
-        { pattern: String.raw`.*\.(unit|integration|e2e)\.test\.[tj]sx?$` },
+        {
+          pattern: String.raw`.*\.(bench|type|unit|integration|e2e)\.test\.[tj]sx?$`,
+        },
       ],
     },
   },

From 503bc82cb5f59e188d0700eced692f4cca1039a3 Mon Sep 17 00:00:00 2001
From: Michael <michael.hladky@push-based.io>
Date: Thu, 30 Jan 2025 18:22:50 +0100
Subject: [PATCH 11/11] fix(utils): fix cyclic dep

---
 packages/utils/src/lib/text-formats/table.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/utils/src/lib/text-formats/table.ts b/packages/utils/src/lib/text-formats/table.ts
index 14d57df0c..6ca339f16 100644
--- a/packages/utils/src/lib/text-formats/table.ts
+++ b/packages/utils/src/lib/text-formats/table.ts
@@ -5,7 +5,7 @@ import type {
   TableColumnObject,
   TableColumnPrimitive,
 } from '@code-pushup/models';
-import { capitalize } from '@code-pushup/utils';
+import { capitalize } from '../case-conversions.js';
 
 export function rowToStringArray({ rows, columns = [] }: Table): string[][] {
   if (Array.isArray(rows.at(0)) && typeof columns.at(0) === 'object') {