Files
claude-mem/src/services/transcripts/field-utils.ts
T
Alex Newman 0b214a59a1 chore: bump version to 10.0.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:25:50 -05:00

152 lines
3.8 KiB
TypeScript

import type { FieldSpec, MatchRule, TranscriptSchema, WatchTarget } from './types.js';
interface ResolveContext {
watch: WatchTarget;
schema: TranscriptSchema;
session?: Record<string, unknown>;
}
function parsePath(path: string): Array<string | number> {
const cleaned = path.trim().replace(/^\$\.?/, '');
if (!cleaned) return [];
const tokens: Array<string | number> = [];
const parts = cleaned.split('.');
for (const part of parts) {
const regex = /([^[\]]+)|\[(\d+)\]/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(part)) !== null) {
if (match[1]) {
tokens.push(match[1]);
} else if (match[2]) {
tokens.push(parseInt(match[2], 10));
}
}
}
return tokens;
}
export function getValueByPath(input: unknown, path: string): unknown {
if (!path) return undefined;
const tokens = parsePath(path);
let current: any = input;
for (const token of tokens) {
if (current === null || current === undefined) return undefined;
current = current[token as any];
}
return current;
}
function isEmptyValue(value: unknown): boolean {
return value === undefined || value === null || value === '';
}
function resolveFromContext(path: string, ctx: ResolveContext): unknown {
if (path.startsWith('$watch.')) {
const key = path.slice('$watch.'.length);
return (ctx.watch as any)[key];
}
if (path.startsWith('$schema.')) {
const key = path.slice('$schema.'.length);
return (ctx.schema as any)[key];
}
if (path.startsWith('$session.')) {
const key = path.slice('$session.'.length);
return ctx.session ? (ctx.session as any)[key] : undefined;
}
if (path === '$cwd') return ctx.watch.workspace;
if (path === '$project') return ctx.watch.project;
return undefined;
}
export function resolveFieldSpec(
spec: FieldSpec | undefined,
entry: unknown,
ctx: ResolveContext
): unknown {
if (spec === undefined) return undefined;
if (typeof spec === 'string') {
const fromContext = resolveFromContext(spec, ctx);
if (fromContext !== undefined) return fromContext;
return getValueByPath(entry, spec);
}
if (spec.coalesce && Array.isArray(spec.coalesce)) {
for (const candidate of spec.coalesce) {
const value = resolveFieldSpec(candidate, entry, ctx);
if (!isEmptyValue(value)) return value;
}
}
if (spec.path) {
const fromContext = resolveFromContext(spec.path, ctx);
if (fromContext !== undefined) return fromContext;
const value = getValueByPath(entry, spec.path);
if (!isEmptyValue(value)) return value;
}
if (spec.value !== undefined) return spec.value;
if (spec.default !== undefined) return spec.default;
return undefined;
}
export function resolveFields(
fields: Record<string, FieldSpec> | undefined,
entry: unknown,
ctx: ResolveContext
): Record<string, unknown> {
const resolved: Record<string, unknown> = {};
if (!fields) return resolved;
for (const [key, spec] of Object.entries(fields)) {
resolved[key] = resolveFieldSpec(spec, entry, ctx);
}
return resolved;
}
export function matchesRule(
entry: unknown,
rule: MatchRule | undefined,
schema: TranscriptSchema
): boolean {
if (!rule) return true;
const path = rule.path || schema.eventTypePath || 'type';
const value = path ? getValueByPath(entry, path) : undefined;
if (rule.exists) {
if (value === undefined || value === null || value === '') return false;
}
if (rule.equals !== undefined) {
return value === rule.equals;
}
if (rule.in && Array.isArray(rule.in)) {
return rule.in.includes(value);
}
if (rule.contains !== undefined) {
return typeof value === 'string' && value.includes(rule.contains);
}
if (rule.regex) {
try {
const regex = new RegExp(rule.regex);
return regex.test(String(value ?? ''));
} catch {
return false;
}
}
return true;
}