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.
123 lines
3.3 KiB
TypeScript
123 lines
3.3 KiB
TypeScript
/**
|
|
* Conflict Detection — identifies fields where a proposed update
|
|
* differs from the existing entity value.
|
|
*
|
|
* Pure function, no I/O.
|
|
*/
|
|
|
|
import type { Entity } from '../schemas/types';
|
|
|
|
// --- Types ---
|
|
|
|
export interface Conflict {
|
|
/** Dot-notation path to the conflicting field */
|
|
fieldPath: string;
|
|
/** The current value in the existing entity */
|
|
existingValue: unknown;
|
|
/** The proposed new value */
|
|
proposedValue: unknown;
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
/**
|
|
* Deep equality check for JSON-serializable values.
|
|
*/
|
|
function deepEqual(a: unknown, b: unknown): boolean {
|
|
if (a === b) return true;
|
|
if (a === null || b === null) return false;
|
|
if (a === undefined || b === undefined) return false;
|
|
if (typeof a !== typeof b) return false;
|
|
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
if (a.length !== b.length) return false;
|
|
return a.every((item, i) => deepEqual(item, b[i]));
|
|
}
|
|
|
|
if (typeof a === 'object' && typeof b === 'object') {
|
|
const aObj = a as Record<string, unknown>;
|
|
const bObj = b as Record<string, unknown>;
|
|
const aKeys = Object.keys(aObj);
|
|
const bKeys = Object.keys(bObj);
|
|
if (aKeys.length !== bKeys.length) return false;
|
|
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resolves a dot-notation field path on an object.
|
|
*/
|
|
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
const parts = path.split('.');
|
|
let current: unknown = obj;
|
|
for (const part of parts) {
|
|
if (current === null || current === undefined || typeof current !== 'object') {
|
|
return undefined;
|
|
}
|
|
current = (current as Record<string, unknown>)[part];
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Flattens a nested object into dot-notation paths with their values.
|
|
* Only flattens plain objects, not arrays.
|
|
*/
|
|
function flattenPaths(
|
|
obj: Record<string, unknown>,
|
|
prefix = ''
|
|
): Array<{ path: string; value: unknown }> {
|
|
const results: Array<{ path: string; value: unknown }> = [];
|
|
|
|
for (const key of Object.keys(obj)) {
|
|
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
const value = obj[key];
|
|
|
|
if (
|
|
value !== null &&
|
|
value !== undefined &&
|
|
typeof value === 'object' &&
|
|
!Array.isArray(value)
|
|
) {
|
|
results.push(...flattenPaths(value as Record<string, unknown>, fullPath));
|
|
} else {
|
|
results.push({ path: fullPath, value });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// --- Main Function ---
|
|
|
|
/**
|
|
* Detects conflicts between an existing entity and a proposed update.
|
|
* A conflict exists when the proposed value for a field differs from the existing value
|
|
* and the existing value is already set (not undefined/null).
|
|
*/
|
|
export function detectConflicts(
|
|
existing: Entity,
|
|
proposed: Record<string, unknown>
|
|
): Conflict[] {
|
|
const conflicts: Conflict[] = [];
|
|
const existingObj = existing as unknown as Record<string, unknown>;
|
|
const flatProposed = flattenPaths(proposed);
|
|
|
|
for (const { path, value: proposedValue } of flatProposed) {
|
|
const existingValue = getNestedValue(existingObj, path);
|
|
|
|
// Only flag as conflict if existing value is set and differs
|
|
if (existingValue !== undefined && existingValue !== null && !deepEqual(existingValue, proposedValue)) {
|
|
conflicts.push({
|
|
fieldPath: path,
|
|
existingValue,
|
|
proposedValue,
|
|
});
|
|
}
|
|
}
|
|
|
|
return conflicts;
|
|
}
|