Migrate all repos into monorepo context folders

Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import type { Skill, Person } from '../schemas/types';
import {
getEntityDir,
getEntityPath,
readEntity,
writeEntity,
listEntities,
deleteEntity,
entityExists,
} from './entity-files';
describe('entity-files', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'kb-test-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
describe('getEntityDir', () => {
it('maps person to kb/profiles/', () => {
expect(getEntityDir('person', '/base')).toBe(join('/base', 'kb', 'profiles'));
});
it('maps experience to kb/experiences/', () => {
expect(getEntityDir('experience', '/base')).toBe(join('/base', 'kb', 'experiences'));
});
it('maps skill to kb/skills/', () => {
expect(getEntityDir('skill', '/base')).toBe(join('/base', 'kb', 'skills'));
});
it('maps organization to kb/organizations/', () => {
expect(getEntityDir('organization', '/base')).toBe(join('/base', 'kb', 'organizations'));
});
it('maps project to kb/projects/', () => {
expect(getEntityDir('project', '/base')).toBe(join('/base', 'kb', 'projects'));
});
it('maps certification to kb/certifications/', () => {
expect(getEntityDir('certification', '/base')).toBe(join('/base', 'kb', 'certifications'));
});
it('maps tandem to kb/tandems/', () => {
expect(getEntityDir('tandem', '/base')).toBe(join('/base', 'kb', 'tandems'));
});
});
describe('getEntityPath', () => {
it('constructs correct file path', () => {
expect(getEntityPath('skill', 'python', '/base')).toBe(join('/base', 'kb', 'skills', 'python.yaml'));
});
});
describe('writeEntity + readEntity round-trip', () => {
const skill: Skill = {
id: 'typescript',
type: 'skill',
created: '2025-01-15',
modified: '2025-01-15',
name: 'TypeScript',
category: 'programming-language',
level: 'expert',
years: 5,
};
it('writes and reads back an entity', async () => {
await writeEntity(skill, tempDir);
const result = await readEntity('skill', 'typescript', tempDir);
expect(result).toEqual(skill);
});
it('rejects invalid entities on write', async () => {
const invalid = { id: 'bad', type: 'skill', created: '2025-01-15', modified: '2025-01-15' } as unknown as Skill;
await expect(writeEntity(invalid, tempDir)).rejects.toThrow('Validation failed');
});
});
describe('readEntity errors', () => {
it('throws descriptive error when entity not found', async () => {
await expect(readEntity('skill', 'nonexistent', tempDir)).rejects.toThrow(
/Entity not found: skill "nonexistent"/
);
});
});
describe('listEntities', () => {
it('returns empty array for empty/missing directory', async () => {
const ids = await listEntities('skill', tempDir);
expect(ids).toEqual([]);
});
it('lists entity IDs from directory', async () => {
const skill1: Skill = {
id: 'python',
type: 'skill',
created: '2025-01-15',
modified: '2025-01-15',
name: 'Python',
category: 'programming-language',
level: 'expert',
};
const skill2: Skill = {
id: 'rust',
type: 'skill',
created: '2025-01-15',
modified: '2025-01-15',
name: 'Rust',
category: 'programming-language',
level: 'intermediate',
};
await writeEntity(skill1, tempDir);
await writeEntity(skill2, tempDir);
const ids = await listEntities('skill', tempDir);
expect(ids.sort()).toEqual(['python', 'rust']);
});
});
describe('deleteEntity', () => {
it('deletes an existing entity file', async () => {
const skill: Skill = {
id: 'go-lang',
type: 'skill',
created: '2025-01-15',
modified: '2025-01-15',
name: 'Go',
category: 'programming-language',
level: 'advanced',
};
await writeEntity(skill, tempDir);
expect(await entityExists('skill', 'go-lang', tempDir)).toBe(true);
await deleteEntity('skill', 'go-lang', tempDir);
expect(await entityExists('skill', 'go-lang', tempDir)).toBe(false);
});
it('throws descriptive error when entity not found', async () => {
await expect(deleteEntity('skill', 'nonexistent', tempDir)).rejects.toThrow(
/Entity not found: skill "nonexistent"/
);
});
});
describe('entityExists', () => {
it('returns false for non-existent entity', async () => {
expect(await entityExists('person', 'nobody', tempDir)).toBe(false);
});
it('returns true for existing entity', async () => {
const person: Person = {
id: 'andre-knie',
type: 'person',
created: '2025-01-15',
modified: '2025-09-01',
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
};
await writeEntity(person, tempDir);
expect(await entityExists('person', 'andre-knie', tempDir)).toBe(true);
});
});
describe('multiple profiles coexist (Req 1.5)', () => {
it('supports multiple person profiles in the same KB', async () => {
const person1: Person = {
id: 'andre-knie',
type: 'person',
created: '2025-01-15',
modified: '2025-09-01',
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
};
const person2: Person = {
id: 'claudia-froldi',
type: 'person',
created: '2025-01-15',
modified: '2025-09-01',
name: { first: 'Claudia', last: 'Froldi', display: 'Claudia Froldi' },
};
await writeEntity(person1, tempDir);
await writeEntity(person2, tempDir);
const ids = await listEntities('person', tempDir);
expect(ids.sort()).toEqual(['andre-knie', 'claudia-froldi']);
const read1 = await readEntity('person', 'andre-knie', tempDir);
const read2 = await readEntity('person', 'claudia-froldi', tempDir);
expect(read1.id).toBe('andre-knie');
expect(read2.id).toBe('claudia-froldi');
});
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* Entity file management functions for the knowledge base.
*
* Maps entity types to directory paths, reads/writes/lists/deletes entity YAML files.
* Uses fs/promises for all file operations.
*/
import { readFile, writeFile, readdir, unlink, access, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import type { Entity, EntityType } from '../schemas/types';
import { serializeEntity, deserializeEntity } from './yaml-utils';
import { validateEntity } from '../schemas/validate';
/**
* Maps entity types to their kb/ subdirectory names.
*/
const ENTITY_TYPE_DIRS: Record<EntityType, string> = {
person: 'profiles',
experience: 'experiences',
skill: 'skills',
organization: 'organizations',
project: 'projects',
certification: 'certifications',
tandem: 'tandems',
};
/**
* Returns the directory path for a given entity type.
*/
export function getEntityDir(type: EntityType, basePath: string = process.cwd()): string {
const dir = ENTITY_TYPE_DIRS[type];
return join(basePath, 'kb', dir);
}
/**
* Constructs the full file path for an entity given its type and ID.
*/
export function getEntityPath(type: EntityType, id: string, basePath: string = process.cwd()): string {
return join(getEntityDir(type, basePath), `${id}.yaml`);
}
/**
* Reads an entity from its YAML file.
* Throws a descriptive error if the file is not found.
*/
export async function readEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise<Entity> {
const filePath = getEntityPath(type, id, basePath);
let content: string;
try {
content = await readFile(filePath, 'utf-8');
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`);
}
throw err;
}
return deserializeEntity(content);
}
/**
* Writes an entity to its YAML file.
* Validates the entity before writing; rejects invalid entities with validation errors.
*/
export async function writeEntity(entity: Entity, basePath: string = process.cwd()): Promise<void> {
const result = validateEntity(entity);
if (!result.valid) {
throw new Error(`Validation failed for entity "${entity.id}": ${result.errors.join('; ')}`);
}
const filePath = getEntityPath(entity.type, entity.id, basePath);
const dir = getEntityDir(entity.type, basePath);
await mkdir(dir, { recursive: true });
const yaml = serializeEntity(entity);
await writeFile(filePath, yaml, 'utf-8');
}
/**
* Lists all entity IDs in a given type directory.
* Returns an array of entity IDs (file names without .yaml extension).
*/
export async function listEntities(type: EntityType, basePath: string = process.cwd()): Promise<string[]> {
const dir = getEntityDir(type, basePath);
let files: string[];
try {
files = await readdir(dir);
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
throw err;
}
return files
.filter((f) => f.endsWith('.yaml'))
.map((f) => f.replace(/\.yaml$/, ''));
}
/**
* Deletes an entity file.
* Throws a descriptive error if the file is not found.
*/
export async function deleteEntity(type: EntityType, id: string, basePath: string = process.cwd()): Promise<void> {
const filePath = getEntityPath(type, id, basePath);
try {
await unlink(filePath);
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`Entity not found: ${type} "${id}" (expected at ${filePath})`);
}
throw err;
}
}
/**
* Checks if an entity file exists.
*/
export async function entityExists(type: EntityType, id: string, basePath: string = process.cwd()): Promise<boolean> {
const filePath = getEntityPath(type, id, basePath);
try {
await access(filePath);
return true;
} catch {
return false;
}
}
+192
View File
@@ -0,0 +1,192 @@
import { describe, it, expect } from 'vitest';
import { serializeEntity, deserializeEntity } from './yaml-utils';
import type { Experience, Tandem, Skill, Person } from '../schemas/types';
describe('yaml-utils', () => {
describe('serializeEntity / deserializeEntity round-trip', () => {
it('round-trips a skill entity', () => {
const skill: Skill = {
id: 'python',
type: 'skill',
created: '2025-01-15',
modified: '2025-01-15',
name: 'Python',
category: 'programming-language',
level: 'expert',
years: 8,
};
const yaml = serializeEntity(skill);
const result = deserializeEntity(yaml);
expect(result).toEqual(skill);
});
it('round-trips an experience with null end date and skillsUsed', () => {
const exp: Experience = {
id: 'db-cargo-lead',
type: 'experience',
created: '2025-01-15',
modified: '2025-09-01',
title: 'Lead IT Innovation',
organization: 'db-cargo',
start: '2023-04',
end: null,
achievements: ['Built AI platform', 'Led team of 5'],
skillsUsed: ['python', 'machine-learning'],
};
const yaml = serializeEntity(exp);
const result = deserializeEntity(yaml);
expect(result).toEqual(exp);
});
it('round-trips a tandem entity with all kebab-case fields', () => {
const tandem: Tandem = {
id: 'froldi-knie',
type: 'tandem',
created: '2025-01-15',
modified: '2025-04-01',
partners: ['andre-knie', 'claudia-froldi'],
sharedVision: 'Shared professional vision.',
complementarySkills: 'Complementary skills description.',
collaborationModel: 'How we collaborate.',
combinedNarrative: 'Joint narrative text.',
jointCompetencies: ['leadership', 'innovation'],
};
const yaml = serializeEntity(tandem);
const result = deserializeEntity(yaml);
expect(result).toEqual(tandem);
});
it('round-trips a person entity with nested objects', () => {
const person: Person = {
id: 'andre-knie',
type: 'person',
created: '2025-01-15',
modified: '2025-09-01',
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
contact: { email: 'test@example.com', location: 'Berlin' },
summary: 'A multi-line\nsummary text.',
languages: [
{ language: 'German', level: 'native' },
{ language: 'English', level: 'fluent' },
],
skills: ['python', 'typescript'],
experiences: ['db-cargo-lead'],
};
const yaml = serializeEntity(person);
const result = deserializeEntity(yaml);
expect(result).toEqual(person);
});
});
describe('serializeEntity', () => {
it('converts camelCase keys to kebab-case in YAML output', () => {
const exp: Experience = {
id: 'test-exp',
type: 'experience',
created: '2025-01-15',
modified: '2025-01-15',
title: 'Developer',
organization: 'test-org',
start: '2023-01',
end: null,
skillsUsed: ['typescript'],
};
const yaml = serializeEntity(exp);
expect(yaml).toContain('skills-used:');
expect(yaml).not.toContain('skillsUsed');
});
it('renders null values explicitly', () => {
const exp: Experience = {
id: 'test-exp',
type: 'experience',
created: '2025-01-15',
modified: '2025-01-15',
title: 'Developer',
organization: 'test-org',
start: '2023-01',
end: null,
};
const yaml = serializeEntity(exp);
expect(yaml).toContain('end: null');
});
it('converts tandem-specific fields to kebab-case', () => {
const tandem: Tandem = {
id: 'froldi-knie',
type: 'tandem',
created: '2025-01-15',
modified: '2025-04-01',
partners: ['andre-knie', 'claudia-froldi'],
sharedVision: 'Vision text',
complementarySkills: 'Skills text',
collaborationModel: 'Model text',
};
const yaml = serializeEntity(tandem);
expect(yaml).toContain('shared-vision:');
expect(yaml).toContain('complementary-skills:');
expect(yaml).toContain('collaboration-model:');
expect(yaml).not.toContain('sharedVision');
expect(yaml).not.toContain('complementarySkills');
expect(yaml).not.toContain('collaborationModel');
});
});
describe('deserializeEntity', () => {
it('converts kebab-case YAML keys to camelCase', () => {
const yaml = `id: test-exp
type: experience
created: "2025-01-15"
modified: "2025-01-15"
title: Developer
organization: test-org
start: "2023-01"
end: null
skills-used:
- typescript
- react
`;
const result = deserializeEntity(yaml);
expect(result).toHaveProperty('skillsUsed', ['typescript', 'react']);
expect(result).not.toHaveProperty('skills-used');
});
it('parses tandem YAML with kebab-case keys', () => {
const yaml = `id: froldi-knie
type: tandem
created: "2025-01-15"
modified: "2025-04-01"
partners:
- andre-knie
- claudia-froldi
shared-vision: Vision text
complementary-skills: Skills text
collaboration-model: Model text
combined-narrative: Narrative text
joint-competencies:
- leadership
- innovation
`;
const result = deserializeEntity(yaml) as Tandem;
expect(result.sharedVision).toBe('Vision text');
expect(result.complementarySkills).toBe('Skills text');
expect(result.collaborationModel).toBe('Model text');
expect(result.combinedNarrative).toBe('Narrative text');
expect(result.jointCompetencies).toEqual(['leadership', 'innovation']);
});
it('handles null end dates', () => {
const yaml = `id: test-exp
type: experience
created: "2025-01-15"
modified: "2025-01-15"
title: Developer
organization: test-org
start: "2023-01"
end: null
`;
const result = deserializeEntity(yaml) as Experience;
expect(result.end).toBeNull();
});
});
});
+77
View File
@@ -0,0 +1,77 @@
/**
* YAML serialization/deserialization utilities for knowledge base entities.
*
* Handles conversion between TypeScript camelCase properties and YAML kebab-case keys.
* Uses the `yaml` package for parsing and stringifying.
*/
import { stringify, parse } from 'yaml';
import type { Entity } from '../schemas/types';
/**
* Map of camelCase TypeScript property names to their kebab-case YAML equivalents.
* Only includes fields that differ between the two conventions.
*/
const CAMEL_TO_KEBAB: Record<string, string> = {
skillsUsed: 'skills-used',
sharedVision: 'shared-vision',
complementarySkills: 'complementary-skills',
collaborationModel: 'collaboration-model',
combinedNarrative: 'combined-narrative',
jointCompetencies: 'joint-competencies',
};
const KEBAB_TO_CAMEL: Record<string, string> = Object.fromEntries(
Object.entries(CAMEL_TO_KEBAB).map(([camel, kebab]) => [kebab, camel])
);
/**
* Recursively converts object keys from camelCase to kebab-case (where mapped).
*/
function toKebabKeys(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(toKebabKeys);
if (typeof obj !== 'object') return obj;
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
const kebabKey = CAMEL_TO_KEBAB[key] ?? key;
result[kebabKey] = toKebabKeys(value);
}
return result;
}
/**
* Recursively converts object keys from kebab-case to camelCase (where mapped).
*/
function toCamelKeys(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(toCamelKeys);
if (typeof obj !== 'object') return obj;
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
const camelKey = KEBAB_TO_CAMEL[key] ?? key;
result[camelKey] = toCamelKeys(value);
}
return result;
}
/**
* Serializes an entity object to a YAML string.
* Converts camelCase keys to kebab-case for the YAML output.
* Null values are preserved (rendered as `null` in YAML).
*/
export function serializeEntity(entity: Entity): string {
const kebabObj = toKebabKeys(entity);
return stringify(kebabObj, { nullStr: 'null', lineWidth: 0 });
}
/**
* Deserializes a YAML string to an entity object.
* Converts kebab-case keys to camelCase for the TypeScript representation.
*/
export function deserializeEntity(yamlStr: string): Entity {
const parsed = parse(yamlStr);
return toCamelKeys(parsed) as Entity;
}