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
+213
View File
@@ -0,0 +1,213 @@
/**
* Personal Knowledge Base - Entity Type Definitions
*
* TypeScript interfaces for all knowledge graph entity types.
* Properties use camelCase (the YAML files use kebab-case; these represent the parsed/deserialized form).
*/
// --- Entity Types ---
/**
* Union type of all supported entity types in the knowledge graph.
*/
export type EntityType =
| 'person'
| 'experience'
| 'skill'
| 'organization'
| 'project'
| 'certification'
| 'tandem';
/**
* Relationship types between entities in the knowledge graph.
*/
export type RelationshipType =
| 'has_skill'
| 'worked_at'
| 'collaborated_with'
| 'applied_for'
| 'partners_with';
// --- Common Header ---
/**
* Common header present on all entity files.
*/
export interface EntityHeader {
/** Unique identifier in kebab-case */
id: string;
/** The entity type discriminator */
type: EntityType;
/** ISO-8601 date when the entity was created */
created: string;
/** ISO-8601 date when the entity was last modified */
modified: string;
}
// --- Person ---
export interface PersonName {
first: string;
last: string;
display: string;
}
export interface PersonContact {
email?: string;
linkedin?: string;
location?: string;
}
export interface Language {
language: string;
level: string;
}
export interface Education {
institution: string;
degree: string;
field: string;
start: string;
end: string;
}
export interface Person extends EntityHeader {
type: 'person';
name: PersonName;
contact?: PersonContact;
summary?: string;
languages?: Language[];
education?: Education[];
/** References to Skill entity IDs */
skills?: string[];
/** References to Experience entity IDs */
experiences?: string[];
/** References to Certification entity IDs */
certifications?: string[];
publications?: string[];
}
// --- Experience ---
export interface Experience extends EntityHeader {
type: 'experience';
title: string;
/** Reference to Organization entity ID */
organization: string;
start: string;
end: string | null;
description?: string;
achievements?: string[];
/** References to Skill entity IDs */
skillsUsed?: string[];
/** References to Project entity IDs */
projects?: string[];
}
// --- Skill ---
export type SkillCategory =
| 'programming-language'
| 'framework'
| 'methodology'
| 'soft-skill'
| 'domain';
export type SkillLevel =
| 'beginner'
| 'intermediate'
| 'advanced'
| 'expert';
export interface Skill extends EntityHeader {
type: 'skill';
name: string;
category: SkillCategory;
level: SkillLevel;
years?: number;
context?: string;
}
// --- Organization ---
export interface Organization extends EntityHeader {
type: 'organization';
name: string;
industry: string;
/** Reference to parent Organization entity ID */
parent?: string;
website?: string;
}
// --- Project ---
export interface Project extends EntityHeader {
type: 'project';
name: string;
description?: string;
/** Reference to Organization entity ID */
organization: string;
start: string;
end?: string;
/** References to Skill entity IDs */
skillsUsed?: string[];
/** References to Person entity IDs */
persons?: string[];
}
// --- Certification ---
export interface Certification extends EntityHeader {
type: 'certification';
name: string;
issuer: string;
date: string;
expires?: string;
/** Reference to Person entity ID */
person: string;
}
// --- Tandem ---
export interface Tandem extends EntityHeader {
type: 'tandem';
/** Exactly 2 Person entity IDs */
partners: [string, string];
sharedVision: string;
complementarySkills: string;
collaborationModel: string;
combinedNarrative?: string;
jointCompetencies?: string[];
}
// --- Graph Index ---
export interface GraphEntity {
id: string;
type: EntityType;
name: string;
}
export interface GraphRelationship {
from: string;
to: string;
type: RelationshipType;
context?: string;
}
export interface GraphIndex {
entities: GraphEntity[];
relationships: GraphRelationship[];
}
// --- Union type for any entity ---
export type Entity =
| Person
| Experience
| Skill
| Organization
| Project
| Certification
| Tandem;
+661
View File
@@ -0,0 +1,661 @@
import { describe, it, expect } from 'vitest';
import {
validateEntity,
validatePerson,
validateExperience,
validateSkill,
validateOrganization,
validateProject,
validateCertification,
validateTandem,
isKebabCase,
isValidDate,
isTemporallyConsistent,
ValidationResult,
} from './validate';
// --- Helper: minimal valid entity headers ---
const baseHeader = (type: string) => ({
id: 'test-entity',
type,
created: '2025-01-15',
modified: '2025-01-15',
});
describe('isKebabCase', () => {
it('accepts valid kebab-case strings', () => {
expect(isKebabCase('hello')).toBe(true);
expect(isKebabCase('hello-world')).toBe(true);
expect(isKebabCase('a-b-c')).toBe(true);
expect(isKebabCase('test123')).toBe(true);
expect(isKebabCase('my-skill-2')).toBe(true);
});
it('rejects invalid kebab-case strings', () => {
expect(isKebabCase('')).toBe(false);
expect(isKebabCase('-leading')).toBe(false);
expect(isKebabCase('trailing-')).toBe(false);
expect(isKebabCase('UPPER')).toBe(false);
expect(isKebabCase('camelCase')).toBe(false);
expect(isKebabCase('has space')).toBe(false);
expect(isKebabCase('double--hyphen')).toBe(false);
expect(isKebabCase('under_score')).toBe(false);
});
});
describe('isValidDate', () => {
it('accepts valid ISO dates', () => {
expect(isValidDate('2025-01-15')).toBe(true);
expect(isValidDate('2025-01')).toBe(true);
expect(isValidDate('2020-12-31')).toBe(true);
expect(isValidDate('1999-06')).toBe(true);
});
it('rejects invalid dates', () => {
expect(isValidDate('')).toBe(false);
expect(isValidDate('2025')).toBe(false);
expect(isValidDate('2025-13')).toBe(false);
expect(isValidDate('2025-00')).toBe(false);
expect(isValidDate('not-a-date')).toBe(false);
expect(isValidDate('2025/01/15')).toBe(false);
expect(isValidDate('01-2025')).toBe(false);
});
});
describe('isTemporallyConsistent', () => {
it('returns true when end is null or undefined', () => {
expect(isTemporallyConsistent('2025-01', null)).toBe(true);
expect(isTemporallyConsistent('2025-01', undefined)).toBe(true);
});
it('returns true when start <= end', () => {
expect(isTemporallyConsistent('2020-01', '2025-01')).toBe(true);
expect(isTemporallyConsistent('2025-01', '2025-01')).toBe(true);
expect(isTemporallyConsistent('2025-01-01', '2025-01-02')).toBe(true);
});
it('returns false when start > end', () => {
expect(isTemporallyConsistent('2025-06', '2025-01')).toBe(false);
expect(isTemporallyConsistent('2025-01-15', '2025-01-01')).toBe(false);
});
});
describe('validateEntity', () => {
it('rejects null/undefined', () => {
expect(validateEntity(null).valid).toBe(false);
expect(validateEntity(undefined).valid).toBe(false);
});
it('rejects non-objects', () => {
expect(validateEntity('string').valid).toBe(false);
expect(validateEntity(42).valid).toBe(false);
});
it('rejects missing type', () => {
const result = validateEntity({ id: 'test' });
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: type');
});
it('rejects invalid type', () => {
const result = validateEntity({ ...baseHeader('invalid-type') });
expect(result.valid).toBe(false);
});
it('dispatches to correct validator', () => {
const person = {
...baseHeader('person'),
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
};
expect(validateEntity(person).valid).toBe(true);
});
});
describe('validatePerson', () => {
it('accepts a valid person', () => {
const person = {
...baseHeader('person'),
name: { first: 'Andre', last: 'Knie', display: 'Andre Knie' },
};
expect(validatePerson(person).valid).toBe(true);
});
it('rejects missing name', () => {
const result = validatePerson(baseHeader('person'));
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: name');
});
it('rejects missing name subfields', () => {
const person = { ...baseHeader('person'), name: { first: 'Andre' } };
const result = validatePerson(person);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: name.last');
expect(result.errors).toContain('Missing required field: name.display');
});
});
describe('validateExperience', () => {
it('accepts a valid experience', () => {
const exp = {
...baseHeader('experience'),
title: 'Lead Developer',
organization: 'db-cargo',
start: '2023-04',
end: null,
};
expect(validateExperience(exp).valid).toBe(true);
});
it('rejects missing required fields', () => {
const result = validateExperience(baseHeader('experience'));
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: title');
expect(result.errors).toContain('Missing required field: organization');
expect(result.errors).toContain('Missing required field: start');
});
it('detects temporal inconsistency', () => {
const exp = {
...baseHeader('experience'),
title: 'Dev',
organization: 'org',
start: '2025-06',
end: '2025-01',
};
const result = validateExperience(exp);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
});
});
describe('validateSkill', () => {
it('accepts a valid skill', () => {
const skill = {
...baseHeader('skill'),
name: 'Python',
category: 'programming-language',
level: 'expert',
};
expect(validateSkill(skill).valid).toBe(true);
});
it('rejects invalid category', () => {
const skill = {
...baseHeader('skill'),
name: 'Python',
category: 'invalid-cat',
level: 'expert',
};
const result = validateSkill(skill);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('category'))).toBe(true);
});
it('rejects invalid level', () => {
const skill = {
...baseHeader('skill'),
name: 'Python',
category: 'framework',
level: 'master',
};
const result = validateSkill(skill);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('level'))).toBe(true);
});
});
describe('validateOrganization', () => {
it('accepts a valid organization', () => {
const org = {
...baseHeader('organization'),
name: 'DB Cargo AG',
industry: 'logistics',
};
expect(validateOrganization(org).valid).toBe(true);
});
it('rejects missing fields', () => {
const result = validateOrganization(baseHeader('organization'));
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: name');
expect(result.errors).toContain('Missing required field: industry');
});
});
describe('validateProject', () => {
it('accepts a valid project', () => {
const project = {
...baseHeader('project'),
name: 'AI Platform',
organization: 'db-cargo',
start: '2024-01',
};
expect(validateProject(project).valid).toBe(true);
});
it('detects temporal inconsistency', () => {
const project = {
...baseHeader('project'),
name: 'AI Platform',
organization: 'db-cargo',
start: '2025-06',
end: '2024-01',
};
const result = validateProject(project);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
});
});
describe('validateCertification', () => {
it('accepts a valid certification', () => {
const cert = {
...baseHeader('certification'),
name: 'AWS Solutions Architect',
issuer: 'Amazon Web Services',
date: '2024-03',
person: 'andre-knie',
};
expect(validateCertification(cert).valid).toBe(true);
});
it('rejects missing fields', () => {
const result = validateCertification(baseHeader('certification'));
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: name');
expect(result.errors).toContain('Missing required field: issuer');
expect(result.errors).toContain('Missing required field: date');
expect(result.errors).toContain('Missing required field: person');
});
});
describe('validateTandem', () => {
it('accepts a valid tandem', () => {
const tandem = {
...baseHeader('tandem'),
partners: ['andre-knie', 'claudia-froldi'],
sharedVision: 'Shared vision text',
complementarySkills: 'Complementary skills text',
collaborationModel: 'Collaboration model text',
};
expect(validateTandem(tandem).valid).toBe(true);
});
it('rejects partners with wrong count', () => {
const tandem = {
...baseHeader('tandem'),
partners: ['only-one'],
sharedVision: 'Vision',
complementarySkills: 'Skills',
collaborationModel: 'Model',
};
const result = validateTandem(tandem);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('exactly 2'))).toBe(true);
});
it('rejects non-array partners', () => {
const tandem = {
...baseHeader('tandem'),
partners: 'not-an-array',
sharedVision: 'Vision',
complementarySkills: 'Skills',
collaborationModel: 'Model',
};
const result = validateTandem(tandem);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('must be an array'))).toBe(true);
});
it('rejects missing tandem-specific fields', () => {
const tandem = {
...baseHeader('tandem'),
partners: ['a', 'b'],
};
const result = validateTandem(tandem);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Missing required field: sharedVision');
expect(result.errors).toContain('Missing required field: complementarySkills');
expect(result.errors).toContain('Missing required field: collaborationModel');
});
it('rejects invalid id format in common fields', () => {
const tandem = {
id: 'Invalid-ID',
type: 'tandem',
created: '2025-01-15',
modified: '2025-01-15',
partners: ['a', 'b'],
sharedVision: 'Vision',
complementarySkills: 'Skills',
collaborationModel: 'Model',
};
const result = validateTandem(tandem);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('kebab-case'))).toBe(true);
});
});
// --- Property-Based Tests (fast-check) ---
import fc from 'fast-check';
// --- Arbitraries ---
/** Generates a valid kebab-case ID */
const kebabCaseId = fc
.array(
fc.stringMatching(/^[a-z0-9]{1,8}$/),
{ minLength: 1, maxLength: 4 }
)
.map(parts => parts.join('-'));
/** Generates a valid ISO date (YYYY-MM or YYYY-MM-DD) */
const isoDate = fc.oneof(
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')}`),
fc.record({
year: fc.integer({ min: 1990, max: 2030 }),
month: fc.integer({ min: 1, max: 12 }),
day: fc.integer({ min: 1, max: 28 }),
}).map(({ year, month, day }) => `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`)
);
/** Generates a valid entity header */
const entityHeader = (type: string) =>
fc.record({
id: kebabCaseId,
type: fc.constant(type),
created: isoDate,
modified: isoDate,
});
/** Generates a valid Person entity */
const personArb = entityHeader('person').chain(header =>
fc.record({
first: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
last: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
display: fc.string({ minLength: 1, maxLength: 40 }).filter(s => s.trim().length > 0),
}).map(name => ({
...header,
name: { first: name.first, last: name.last, display: name.display },
}))
);
/** Generates a valid Skill entity */
const skillArb = entityHeader('skill').chain(header =>
fc.record({
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
category: fc.constantFrom('programming-language', 'framework', 'methodology', 'soft-skill', 'domain'),
level: fc.constantFrom('beginner', 'intermediate', 'advanced', 'expert'),
}).map(fields => ({ ...header, ...fields }))
);
/** Generates a valid Organization entity */
const orgArb = entityHeader('organization').chain(header =>
fc.record({
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
industry: fc.string({ minLength: 1, maxLength: 20 }).filter(s => s.trim().length > 0),
}).map(fields => ({ ...header, ...fields }))
);
/** Generates a valid Experience entity */
const experienceArb = entityHeader('experience').chain(header =>
fc.record({
title: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
organization: kebabCaseId,
start: isoDate,
}).map(fields => ({ ...header, ...fields, end: null }))
);
/** Generates a valid Project entity */
const projectArb = entityHeader('project').chain(header =>
fc.record({
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
organization: kebabCaseId,
start: isoDate,
}).map(fields => ({ ...header, ...fields }))
);
/** Generates a valid Certification entity */
const certArb = entityHeader('certification').chain(header =>
fc.record({
name: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
issuer: fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
date: isoDate,
person: kebabCaseId,
}).map(fields => ({ ...header, ...fields }))
);
/** Generates a valid Tandem entity */
const tandemArb = entityHeader('tandem').chain(header =>
fc.record({
partner1: kebabCaseId,
partner2: kebabCaseId,
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),
}).map(fields => ({
...header,
partners: [fields.partner1, fields.partner2] as [string, string],
sharedVision: fields.sharedVision,
complementarySkills: fields.complementarySkills,
collaborationModel: fields.collaborationModel,
}))
);
/** Generates any valid entity */
const anyValidEntity = fc.oneof(
personArb,
skillArb,
orgArb,
experienceArb,
projectArb,
certArb,
tandemArb,
);
// --- Property 1: Entity serialization round-trip ---
describe('Property 1: Entity serialization round-trip', () => {
it('JSON.parse(JSON.stringify(entity)) produces an equivalent entity that passes validation', () => {
fc.assert(
fc.property(anyValidEntity, (entity) => {
const roundTripped = JSON.parse(JSON.stringify(entity));
// All attributes preserved
expect(roundTripped).toEqual(entity);
// Still passes validation
const result = validateEntity(roundTripped);
expect(result.valid).toBe(true);
}),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 2.1, 8.2
*/
});
// --- Property 2: Entity schema validation ---
describe('Property 2: Entity schema validation', () => {
it('accepts any valid entity with all required attributes present and correctly typed', () => {
fc.assert(
fc.property(anyValidEntity, (entity) => {
const result = validateEntity(entity);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
}),
{ numRuns: 100 }
);
});
it('rejects entities missing required fields', () => {
const entityTypes = ['person', 'experience', 'skill', 'organization', 'project', 'certification', 'tandem'] as const;
fc.assert(
fc.property(
fc.constantFrom(...entityTypes),
kebabCaseId,
isoDate,
isoDate,
(type, id, created, modified) => {
// Entity with only header fields — missing type-specific required fields
const entity = { id, type, created, modified };
const result = validateEntity(entity);
// Person needs name, experience needs title/org/start, etc.
// All types have additional required fields beyond the header
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
}
),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 1.1, 1.3
*/
});
// --- Property 3: Temporal consistency ---
describe('Property 3: Temporal consistency', () => {
it('validation passes when start <= end for time-bound entities', () => {
fc.assert(
fc.property(
kebabCaseId,
isoDate,
isoDate,
fc.constantFrom('experience', 'project'),
(id, date1, date2, type) => {
const [start, end] = date1 <= date2 ? [date1, date2] : [date2, date1];
const entity: Record<string, unknown> = {
id,
type,
created: start,
modified: start,
start,
end,
};
if (type === 'experience') {
entity.title = 'Test Role';
entity.organization = 'test-org';
} else {
entity.name = 'Test Project';
entity.organization = 'test-org';
}
const result = validateEntity(entity);
// Should not have temporal inconsistency errors
const temporalErrors = result.errors.filter(e => e.includes('Temporal inconsistency'));
expect(temporalErrors).toHaveLength(0);
}
),
{ numRuns: 100 }
);
});
it('validation fails when start > end for time-bound entities', () => {
fc.assert(
fc.property(
kebabCaseId,
fc.record({
year: fc.integer({ min: 1990, max: 2025 }),
month: fc.integer({ min: 1, max: 12 }),
}),
fc.integer({ min: 1, max: 60 }),
fc.constantFrom('experience', 'project'),
(id, startParts, monthsLater, type) => {
const startStr = `${startParts.year}-${String(startParts.month).padStart(2, '0')}`;
// Compute an end date that is strictly before start
const endYear = startParts.year - Math.ceil(monthsLater / 12);
const endMonth = ((startParts.month - (monthsLater % 12) - 1 + 12) % 12) + 1;
const endStr = `${endYear}-${String(endMonth).padStart(2, '0')}`;
// Only test when end is actually before start (string comparison)
fc.pre(endStr < startStr);
const entity: Record<string, unknown> = {
id,
type,
created: startStr,
modified: startStr,
start: startStr,
end: endStr,
};
if (type === 'experience') {
entity.title = 'Test Role';
entity.organization = 'test-org';
} else {
entity.name = 'Test Project';
entity.organization = 'test-org';
}
const result = validateEntity(entity);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Temporal inconsistency'))).toBe(true);
}
),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 1.4
*/
});
// --- Property 15: File naming convention ---
describe('Property 15: File naming convention', () => {
it('any valid entity ID produces a file name matching <entity-id>.yaml with kebab-case', () => {
fc.assert(
fc.property(kebabCaseId, (id) => {
const fileName = `${id}.yaml`;
// File name matches pattern: lowercase letters, numbers, hyphens, ending in .yaml
expect(fileName).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*\.yaml$/);
// The ID itself is valid kebab-case
expect(isKebabCase(id)).toBe(true);
}),
{ numRuns: 100 }
);
});
it('non-kebab-case IDs are rejected by validation', () => {
const invalidIds = fc.oneof(
fc.constant(''),
fc.constant('-leading'),
fc.constant('trailing-'),
fc.constant('UPPERCASE'),
fc.constant('camelCase'),
fc.constant('has space'),
fc.constant('double--hyphen'),
fc.constant('under_score'),
fc.stringMatching(/^[A-Z]{1,10}$/),
);
fc.assert(
fc.property(invalidIds, isoDate, isoDate, (id, created, modified) => {
const entity = {
id,
type: 'skill',
created,
modified,
name: 'Test',
category: 'framework',
level: 'expert',
};
const result = validateEntity(entity);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('kebab-case') || e.includes('id'))).toBe(true);
}),
{ numRuns: 100 }
);
});
/**
* Validates: Requirements 9.2
*/
});
+357
View File
@@ -0,0 +1,357 @@
/**
* Personal Knowledge Base - Entity Validation Functions
*
* Validates entity objects against their schema definitions.
* Returns descriptive errors listing missing or invalid fields.
*/
import type {
EntityType,
SkillCategory,
SkillLevel,
} from './types';
// --- Validation Result ---
export interface ValidationResult {
valid: boolean;
errors: string[];
}
// --- Constants ---
const VALID_ENTITY_TYPES: EntityType[] = [
'person',
'experience',
'skill',
'organization',
'project',
'certification',
'tandem',
];
const VALID_SKILL_CATEGORIES: SkillCategory[] = [
'programming-language',
'framework',
'methodology',
'soft-skill',
'domain',
];
const VALID_SKILL_LEVELS: SkillLevel[] = [
'beginner',
'intermediate',
'advanced',
'expert',
];
// --- Helper Functions ---
/**
* Validates kebab-case format: lowercase letters, numbers, and hyphens only.
* No leading or trailing hyphens allowed.
*/
export function isKebabCase(id: string): boolean {
if (typeof id !== 'string' || id.length === 0) return false;
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(id);
}
/**
* Validates ISO date format: YYYY-MM-DD or YYYY-MM.
*/
export function isValidDate(date: string): boolean {
if (typeof date !== 'string') return false;
return /^\d{4}-(0[1-9]|1[0-2])(-([0-2]\d|3[01]))?$/.test(date);
}
/**
* Validates temporal consistency: start <= end when both are present.
* Returns true if end is null/undefined (open-ended) or start <= end.
*/
export function isTemporallyConsistent(
start: string,
end: string | null | undefined
): boolean {
if (end === null || end === undefined) return true;
return start <= end;
}
// --- Common Validation ---
function validateCommonFields(entity: Record<string, unknown>): string[] {
const errors: string[] = [];
if (!entity.id) {
errors.push('Missing required field: id');
} else if (typeof entity.id !== 'string') {
errors.push('Field "id" must be a string');
} else if (!isKebabCase(entity.id)) {
errors.push('Field "id" must be in kebab-case format (lowercase letters, numbers, hyphens only, no leading/trailing hyphens)');
}
if (!entity.type) {
errors.push('Missing required field: type');
} else if (typeof entity.type !== 'string') {
errors.push('Field "type" must be a string');
} else if (!VALID_ENTITY_TYPES.includes(entity.type as EntityType)) {
errors.push(`Field "type" must be one of: ${VALID_ENTITY_TYPES.join(', ')}`);
}
if (!entity.created) {
errors.push('Missing required field: created');
} else if (typeof entity.created !== 'string') {
errors.push('Field "created" must be a string');
} else if (!isValidDate(entity.created)) {
errors.push('Field "created" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
}
if (!entity.modified) {
errors.push('Missing required field: modified');
} else if (typeof entity.modified !== 'string') {
errors.push('Field "modified" must be a string');
} else if (!isValidDate(entity.modified)) {
errors.push('Field "modified" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
}
return errors;
}
// --- Entity-Specific Validators ---
export function validatePerson(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.name) {
errors.push('Missing required field: name');
} else if (typeof entity.name !== 'object' || entity.name === null) {
errors.push('Field "name" must be an object');
} else {
const name = entity.name as Record<string, unknown>;
if (!name.first || typeof name.first !== 'string') {
errors.push('Missing required field: name.first');
}
if (!name.last || typeof name.last !== 'string') {
errors.push('Missing required field: name.last');
}
if (!name.display || typeof name.display !== 'string') {
errors.push('Missing required field: name.display');
}
}
return { valid: errors.length === 0, errors };
}
export function validateExperience(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.title || typeof entity.title !== 'string') {
errors.push('Missing required field: title');
}
if (!entity.organization || typeof entity.organization !== 'string') {
errors.push('Missing required field: organization');
}
if (!entity.start) {
errors.push('Missing required field: start');
} else if (typeof entity.start !== 'string') {
errors.push('Field "start" must be a string');
} else if (!isValidDate(entity.start)) {
errors.push('Field "start" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
}
// Temporal consistency check
if (
entity.start &&
typeof entity.start === 'string' &&
entity.end !== null &&
entity.end !== undefined &&
typeof entity.end === 'string'
) {
if (!isValidDate(entity.end)) {
errors.push('Field "end" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
} else if (!isTemporallyConsistent(entity.start, entity.end)) {
errors.push('Temporal inconsistency: start date must be less than or equal to end date');
}
}
return { valid: errors.length === 0, errors };
}
export function validateSkill(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.name || typeof entity.name !== 'string') {
errors.push('Missing required field: name');
}
if (!entity.category) {
errors.push('Missing required field: category');
} else if (typeof entity.category !== 'string') {
errors.push('Field "category" must be a string');
} else if (!VALID_SKILL_CATEGORIES.includes(entity.category as SkillCategory)) {
errors.push(`Field "category" must be one of: ${VALID_SKILL_CATEGORIES.join(', ')}`);
}
if (!entity.level) {
errors.push('Missing required field: level');
} else if (typeof entity.level !== 'string') {
errors.push('Field "level" must be a string');
} else if (!VALID_SKILL_LEVELS.includes(entity.level as SkillLevel)) {
errors.push(`Field "level" must be one of: ${VALID_SKILL_LEVELS.join(', ')}`);
}
return { valid: errors.length === 0, errors };
}
export function validateOrganization(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.name || typeof entity.name !== 'string') {
errors.push('Missing required field: name');
}
if (!entity.industry || typeof entity.industry !== 'string') {
errors.push('Missing required field: industry');
}
return { valid: errors.length === 0, errors };
}
export function validateProject(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.name || typeof entity.name !== 'string') {
errors.push('Missing required field: name');
}
if (!entity.organization || typeof entity.organization !== 'string') {
errors.push('Missing required field: organization');
}
if (!entity.start) {
errors.push('Missing required field: start');
} else if (typeof entity.start !== 'string') {
errors.push('Field "start" must be a string');
} else if (!isValidDate(entity.start)) {
errors.push('Field "start" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
}
// Temporal consistency check
if (
entity.start &&
typeof entity.start === 'string' &&
entity.end !== null &&
entity.end !== undefined &&
typeof entity.end === 'string'
) {
if (!isValidDate(entity.end)) {
errors.push('Field "end" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
} else if (!isTemporallyConsistent(entity.start, entity.end)) {
errors.push('Temporal inconsistency: start date must be less than or equal to end date');
}
}
return { valid: errors.length === 0, errors };
}
export function validateCertification(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.name || typeof entity.name !== 'string') {
errors.push('Missing required field: name');
}
if (!entity.issuer || typeof entity.issuer !== 'string') {
errors.push('Missing required field: issuer');
}
if (!entity.date) {
errors.push('Missing required field: date');
} else if (typeof entity.date !== 'string') {
errors.push('Field "date" must be a string');
} else if (!isValidDate(entity.date)) {
errors.push('Field "date" must be a valid ISO date (YYYY-MM-DD or YYYY-MM)');
}
if (!entity.person || typeof entity.person !== 'string') {
errors.push('Missing required field: person');
}
return { valid: errors.length === 0, errors };
}
export function validateTandem(entity: Record<string, unknown>): ValidationResult {
const errors = validateCommonFields(entity);
if (!entity.partners) {
errors.push('Missing required field: partners');
} else if (!Array.isArray(entity.partners)) {
errors.push('Field "partners" must be an array');
} else if (entity.partners.length !== 2) {
errors.push('Field "partners" must contain exactly 2 entries');
} else if (
typeof entity.partners[0] !== 'string' ||
typeof entity.partners[1] !== 'string'
) {
errors.push('Field "partners" must contain exactly 2 strings');
}
if (!entity.sharedVision || typeof entity.sharedVision !== 'string') {
errors.push('Missing required field: sharedVision');
}
if (!entity.complementarySkills || typeof entity.complementarySkills !== 'string') {
errors.push('Missing required field: complementarySkills');
}
if (!entity.collaborationModel || typeof entity.collaborationModel !== 'string') {
errors.push('Missing required field: collaborationModel');
}
return { valid: errors.length === 0, errors };
}
// --- Main Dispatch Function ---
/**
* Validates an entity by dispatching to the appropriate validator based on the `type` field.
* Returns a ValidationResult with descriptive errors if validation fails.
*/
export function validateEntity(entity: unknown): ValidationResult {
if (entity === null || entity === undefined) {
return { valid: false, errors: ['Entity must not be null or undefined'] };
}
if (typeof entity !== 'object') {
return { valid: false, errors: ['Entity must be an object'] };
}
const obj = entity as Record<string, unknown>;
if (!obj.type || typeof obj.type !== 'string') {
return { valid: false, errors: ['Missing required field: type'] };
}
switch (obj.type) {
case 'person':
return validatePerson(obj);
case 'experience':
return validateExperience(obj);
case 'skill':
return validateSkill(obj);
case 'organization':
return validateOrganization(obj);
case 'project':
return validateProject(obj);
case 'certification':
return validateCertification(obj);
case 'tandem':
return validateTandem(obj);
default:
return {
valid: false,
errors: [`Field "type" must be one of: ${VALID_ENTITY_TYPES.join(', ')}`],
};
}
}