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
+338
View File
@@ -0,0 +1,338 @@
import { describe, it, expect } from 'vitest';
import fc from 'fast-check';
import { selectRelevant } from './relevance-selection';
import { generateTandemData } from './tandem-generator';
import type { Experience, Skill, Project, Person, Tandem } from '../schemas/types';
// --- Arbitraries ---
const kebabCaseId = fc
.array(
fc.stringMatching(/^[a-z0-9]{1,8}$/),
{ minLength: 1, maxLength: 4 }
)
.map(parts => parts.join('-'));
const isoDate = fc
.record({
year: fc.integer({ min: 1990, max: 2030 }),
month: fc.integer({ min: 1, max: 12 }),
})
.map(({ year, month }) => `${year}-${String(month).padStart(2, '0')}`);
const skillCategory = fc.constantFrom(
'programming-language' as const,
'framework' as const,
'methodology' as const,
'soft-skill' as const,
'domain' as const
);
const skillLevel = fc.constantFrom(
'beginner' as const,
'intermediate' as const,
'advanced' as const,
'expert' as const
);
/** Generates a Skill entity */
const skillArb = fc.record({
id: kebabCaseId,
created: isoDate,
modified: isoDate,
name: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
category: skillCategory,
level: skillLevel,
}).map(f => ({
id: f.id,
type: 'skill' as const,
created: f.created,
modified: f.modified,
name: f.name,
category: f.category,
level: f.level,
}));
/** Generates an Experience entity */
const experienceArb = fc.record({
id: kebabCaseId,
created: isoDate,
modified: isoDate,
title: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
organization: kebabCaseId,
start: isoDate,
description: fc.string({ minLength: 0, maxLength: 50 }),
skillsUsed: fc.array(fc.string({ minLength: 1, maxLength: 15 }).filter(s => s.trim().length > 0), { minLength: 0, maxLength: 5 }),
}).map(f => ({
id: f.id,
type: 'experience' as const,
created: f.created,
modified: f.modified,
title: f.title,
organization: f.organization,
start: f.start,
end: null,
description: f.description,
skillsUsed: f.skillsUsed,
}));
/** Generates a Project entity */
const projectArb = fc.record({
id: kebabCaseId,
created: isoDate,
modified: isoDate,
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
organization: kebabCaseId,
start: isoDate,
description: fc.string({ minLength: 0, maxLength: 50 }),
skillsUsed: fc.array(fc.string({ minLength: 1, maxLength: 15 }).filter(s => s.trim().length > 0), { minLength: 0, maxLength: 5 }),
}).map(f => ({
id: f.id,
type: 'project' as const,
created: f.created,
modified: f.modified,
name: f.name,
organization: f.organization,
start: f.start,
description: f.description,
skillsUsed: f.skillsUsed,
}));
/** Generates a Person entity */
const personArb = fc.record({
id: kebabCaseId,
created: isoDate,
modified: isoDate,
firstName: fc.string({ minLength: 1, maxLength: 15 }).filter(s => s.trim().length > 0),
lastName: fc.string({ minLength: 1, maxLength: 15 }).filter(s => s.trim().length > 0),
summary: fc.string({ minLength: 0, maxLength: 50 }),
}).map(f => ({
id: f.id,
type: 'person' as const,
created: f.created,
modified: f.modified,
name: { first: f.firstName, last: f.lastName, display: `${f.firstName} ${f.lastName}` },
summary: f.summary || undefined,
skills: [] as string[],
experiences: [] as string[],
}));
/** Generates a Tandem entity referencing two partner IDs */
const tandemArb = (partner1Id: string, partner2Id: string) => fc.record({
id: kebabCaseId,
created: isoDate,
modified: isoDate,
sharedVision: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
complementarySkills: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
collaborationModel: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
combinedNarrative: fc.string({ minLength: 0, maxLength: 50 }),
jointCompetencies: fc.array(fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0), { minLength: 0, maxLength: 3 }),
}).map(f => ({
id: f.id,
type: 'tandem' as const,
created: f.created,
modified: f.modified,
partners: [partner1Id, partner2Id] as [string, string],
sharedVision: f.sharedVision,
complementarySkills: f.complementarySkills,
collaborationModel: f.collaborationModel,
combinedNarrative: f.combinedNarrative || undefined,
jointCompetencies: f.jointCompetencies.length > 0 ? f.jointCompetencies : undefined,
}));
// --- Property 9: CV relevance selection ordering ---
describe('Property 9: CV relevance selection ordering', () => {
it('matching items have score > 0 and appear before score-0 items in the output', () => {
fc.assert(
fc.property(
fc.array(fc.string({ minLength: 1, maxLength: 15 }).filter(s => s.trim().length > 0), { minLength: 1, maxLength: 5 }),
fc.array(experienceArb, { minLength: 0, maxLength: 5 }),
fc.array(skillArb, { minLength: 0, maxLength: 5 }),
fc.array(projectArb, { minLength: 0, maxLength: 5 }),
(requirements, experiences, skills, projects) => {
const results = selectRelevant(requirements, experiences, skills, projects);
// All items with matchedRequirements.length > 0 must have score > 0
for (const r of results) {
if (r.matchedRequirements.length > 0) {
expect(r.score).toBeGreaterThan(0);
}
}
// Items are sorted: all score > 0 items appear before score === 0 items
let seenZero = false;
for (const r of results) {
if (r.score === 0) {
seenZero = true;
} else if (seenZero) {
// A non-zero score item appeared after a zero-score item — ordering violated
expect(seenZero && r.score > 0).toBe(false);
}
}
// Results are sorted by score descending
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
}
}
),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 5.1, 5.2
*/
});
// --- Property 10: Tandem application document set (exactly 3 docs) ---
describe('Property 10: Tandem application document set', () => {
it('generateTandemData produces data with exactly 2 partner profiles referencing both partners', () => {
fc.assert(
fc.property(
personArb,
personArb,
fc.array(experienceArb, { minLength: 0, maxLength: 3 }),
fc.array(experienceArb, { minLength: 0, maxLength: 3 }),
fc.array(skillArb, { minLength: 0, maxLength: 3 }),
fc.array(skillArb, { minLength: 0, maxLength: 3 }),
(partner1, partner2, exp1, exp2, skills1, skills2) => {
// Ensure distinct partner IDs
const p1 = { ...partner1, id: partner1.id + '-p1' };
const p2 = { ...partner2, id: partner2.id + '-p2' };
const tandem: Tandem = {
id: 'tandem-test',
type: 'tandem',
created: '2025-01-01',
modified: '2025-01-01',
partners: [p1.id, p2.id],
sharedVision: 'Shared vision text',
complementarySkills: 'Complementary skills text',
collaborationModel: 'Collaboration model text',
};
const result = generateTandemData(
tandem,
p1 as Person,
p2 as Person,
exp1,
exp2,
skills1,
skills2
);
// Exactly 2 partner profiles in the output
expect(result.partnerProfiles).toHaveLength(2);
// The tandem CV references both partners
expect(result.partners).toContain(p1.id);
expect(result.partners).toContain(p2.id);
// Each partner profile corresponds to one of the partners
const profileIds = result.partnerProfiles.map(p => p.personId);
expect(profileIds).toContain(p1.id);
expect(profileIds).toContain(p2.id);
// The tandem data contains shared information from both profiles
expect(result.sharedVision).toBeTruthy();
expect(result.complementarySkills).toBeTruthy();
expect(result.collaborationModel).toBeTruthy();
}
),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 5.3, 5.4
*/
});
// --- Property 11: CV output validity and traceability ---
describe('Property 11: CV output validity and traceability', () => {
it('all data in TandemDocumentData traces back to input entities', () => {
fc.assert(
fc.property(
personArb,
personArb,
fc.array(experienceArb, { minLength: 1, maxLength: 4 }),
fc.array(experienceArb, { minLength: 1, maxLength: 4 }),
fc.array(skillArb, { minLength: 1, maxLength: 4 }),
fc.array(skillArb, { minLength: 1, maxLength: 4 }),
(partner1, partner2, exp1, exp2, skills1, skills2) => {
const p1 = { ...partner1, id: partner1.id + '-p1' };
const p2 = { ...partner2, id: partner2.id + '-p2' };
const tandem: Tandem = {
id: 'tandem-trace',
type: 'tandem',
created: '2025-01-01',
modified: '2025-01-01',
partners: [p1.id, p2.id],
sharedVision: 'Vision',
complementarySkills: 'Skills complement',
collaborationModel: 'Model',
combinedNarrative: 'Narrative',
jointCompetencies: ['competency-1', 'competency-2'],
};
const result = generateTandemData(
tandem,
p1 as Person,
p2 as Person,
exp1,
exp2,
skills1,
skills2
);
// All skill names in partner profiles must come from input skills
const allInputSkillNames = [...skills1, ...skills2].map(s => s.name);
for (const profile of result.partnerProfiles) {
for (const skillName of profile.topSkills) {
expect(allInputSkillNames).toContain(skillName);
}
}
// All experience titles in partner profiles must come from input experiences
const allInputExpTitles = [...exp1, ...exp2].map(e => e.title);
for (const profile of result.partnerProfiles) {
for (const exp of profile.topExperiences) {
expect(allInputExpTitles).toContain(exp.title);
}
}
// All organization references in experiences must come from input experiences
const allInputOrgs = [...exp1, ...exp2].map(e => e.organization);
for (const profile of result.partnerProfiles) {
for (const exp of profile.topExperiences) {
expect(allInputOrgs).toContain(exp.organization);
}
}
// Display names must come from input persons
const inputDisplayNames = [p1.name.display, p2.name.display];
for (const profile of result.partnerProfiles) {
expect(inputDisplayNames).toContain(profile.displayName);
}
// Tandem-level data must trace back to the tandem entity
expect(result.sharedVision).toBe(tandem.sharedVision);
expect(result.complementarySkills).toBe(tandem.complementarySkills);
expect(result.collaborationModel).toBe(tandem.collaborationModel);
expect(result.combinedNarrative).toBe(tandem.combinedNarrative);
expect(result.jointCompetencies).toEqual(tandem.jointCompetencies);
}
),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 5.5, 5.6
*/
});
+78
View File
@@ -0,0 +1,78 @@
import type { Experience, Skill, Project } from '../schemas/types';
export interface RelevanceResult {
entityId: string;
entityType: 'experience' | 'skill' | 'project';
score: number;
matchedRequirements: string[];
}
/**
* Scores and ranks experiences, skills, and projects by relevance to job posting requirements.
* Items with matches appear before non-matching items, sorted by score descending.
*/
export function selectRelevant(
requirements: string[],
experiences: Experience[],
skills: Skill[],
projects: Project[]
): RelevanceResult[] {
const lowerReqs = requirements.map(r => r.toLowerCase());
const results: RelevanceResult[] = [];
for (const skill of skills) {
const matched = lowerReqs.filter(req => skill.name.toLowerCase().includes(req) || req.includes(skill.name.toLowerCase()));
results.push({
entityId: skill.id,
entityType: 'skill',
score: matched.length,
matchedRequirements: matched,
});
}
for (const exp of experiences) {
const matched = findMatchingRequirements(lowerReqs, exp.skillsUsed, exp.title, exp.description);
results.push({
entityId: exp.id,
entityType: 'experience',
score: matched.length,
matchedRequirements: matched,
});
}
for (const project of projects) {
const matched = findMatchingRequirements(lowerReqs, project.skillsUsed, project.name, project.description);
results.push({
entityId: project.id,
entityType: 'project',
score: matched.length,
matchedRequirements: matched,
});
}
results.sort((a, b) => b.score - a.score);
return results;
}
function findMatchingRequirements(
lowerReqs: string[],
skillsUsed: string[] | undefined,
title: string,
description: string | undefined
): string[] {
const matched = new Set<string>();
const lowerTitle = title.toLowerCase();
const lowerDesc = (description ?? '').toLowerCase();
const lowerSkills = (skillsUsed ?? []).map(s => s.toLowerCase());
for (const req of lowerReqs) {
if (lowerSkills.some(s => s.includes(req) || req.includes(s))) {
matched.add(req);
} else if (lowerTitle.includes(req) || lowerDesc.includes(req)) {
matched.add(req);
}
}
return [...matched];
}
+67
View File
@@ -0,0 +1,67 @@
import type { Tandem, Person, Experience, Skill } from '../schemas/types';
export interface PartnerProfile {
personId: string;
displayName: string;
summary?: string;
topSkills: string[];
topExperiences: Array<{ title: string; organization: string; start: string; end: string | null }>;
}
export interface TandemDocumentData {
tandemId: string;
partners: [string, string];
sharedVision: string;
complementarySkills: string;
collaborationModel: string;
combinedNarrative?: string;
jointCompetencies?: string[];
partnerProfiles: PartnerProfile[];
}
/**
* Produces a structured tandem document from a tandem entity and both partners' data.
* Returns data only — formatting is handled by templates.
*/
export function generateTandemData(
tandem: Tandem,
partner1Profile: Person,
partner2Profile: Person,
partner1Experiences: Experience[],
partner2Experiences: Experience[],
partner1Skills: Skill[],
partner2Skills: Skill[]
): TandemDocumentData {
return {
tandemId: tandem.id,
partners: tandem.partners,
sharedVision: tandem.sharedVision,
complementarySkills: tandem.complementarySkills,
collaborationModel: tandem.collaborationModel,
combinedNarrative: tandem.combinedNarrative,
jointCompetencies: tandem.jointCompetencies,
partnerProfiles: [
buildPartnerProfile(partner1Profile, partner1Experiences, partner1Skills),
buildPartnerProfile(partner2Profile, partner2Experiences, partner2Skills),
],
};
}
function buildPartnerProfile(
person: Person,
experiences: Experience[],
skills: Skill[]
): PartnerProfile {
return {
personId: person.id,
displayName: person.name.display,
summary: person.summary,
topSkills: skills.map(s => s.name),
topExperiences: experiences.map(exp => ({
title: exp.title,
organization: exp.organization,
start: exp.start,
end: exp.end,
})),
};
}