ktx/packages/context/src/ingest/ingest-trace.ts
Andrey Avtomonov e64da5a85d
feat(ingest): default local ingest to isolated diffs (#128)
* docs: add isolated-diff ingestion design

* Refine isolated-diff ingestion design after adversarial review iteration 1

* Refine isolated-diff ingestion design after adversarial review iteration 2

* Refine isolated-diff ingestion design after adversarial review iteration 3

* feat: persist ingest trace events

* feat: add isolated ingest patch helpers

* feat: validate wiki body semantic references

* feat: add final ingest artifact gates

* feat: execute ingest work units in child worktrees

* feat: integrate isolated work unit patches

* feat: route selected ingest sources through isolated diffs

* test: cover isolated diff ingestion regressions

* feat: add isolated diff ingestion v1 core

* docs: document ingest trace inspection

* docs: add isolated diff ingestion v1 core plan

* fix(ingest): tighten final artifact gates

* fix(ingest): gate isolated final integration tree

* fix(ingest): persist postmortem failure traces

* fix(ingest): trace policy conflicts and cleanup child worktrees

* test(ingest): verify isolated diff postmortem coverage

* docs: add isolated diff ingestion gates and trace closure plan

* fix(ingest): gate provenance before isolated diff squash

* docs: add isolated diff ingestion provenance gate closure plan

* fix(ingest): gate final wiki references

* fix(ingest): enforce SL target connection scope

* fix(ingest): trace isolated SL target policy gates

* test(ingest): cover isolated diff reference and target gates

* chore(ingest): verify isolated diff gate closure

* docs: add isolated diff ingestion reference and target gate closure plan

* fix(ingest): gate global wiki references

* docs: add isolated diff ingestion global wiki reference gate closure plan

* fix(ingest): validate scan sources and wiki refs

* test(ingest): cover isolated diff textual conflict resolver

* test(ingest): cover isolated diff resolver integration

* feat(ingest): repair isolated diff textual conflicts

* feat(ingest): report isolated diff resolver outcomes

* test(ingest): verify isolated diff textual conflict repair

* test(ingest): align textual conflict failure coverage

* docs: add isolated diff textual conflict resolver plan

* test(ingest): cover isolated diff gate repair

* feat(ingest): add isolated diff gate repair agent

* feat(ingest): repair isolated diff semantic gate failures

* feat(ingest): wire isolated diff gate repair

* test(ingest): verify isolated diff final gate repair

* chore(ingest): verify isolated diff gate repair

* docs: add isolated diff gate repair plan

* Improve ingest progress updates

* feat(ingest): route direct-write connectors through isolated diffs

* test(ingest): cover non-metabase isolated diff routing

* feat(ingest): project metricflow semantic models before work units

* test(ingest): verify metricflow isolated projection path

* chore(ingest): verify isolated diff connector migration

* docs: add isolated diff connector migration plan

* feat(ingest): make isolated diff routing the private default

* feat(ingest): promote isolated diff to default runner path

* feat(ingest): default local ingest to isolated diffs

* chore(ingest): remove isolated diff allowlist references

* fix(ingest): preserve transient evidence for isolated work units

* docs: add isolated diff default promotion plan

* refactor(ingest): remove shared worktree WorkUnit path

* docs(ingest): align WorkUnit prompts with isolated diffs

* test(ingest): drop unused runner import

* docs: add isolated diff shared worktree removal plan

* docs: add isolated diff gate repair classification plan

* fix: restrict claude-code mcp servers

* docs: align ingest trace guidance with public CLI

---------

Co-authored-by: Andrey Avtomonov <7889985+andreybavt@users.noreply.github.com>
2026-05-18 13:38:06 +02:00

158 lines
4.2 KiB
TypeScript

import { appendFile, mkdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
export type IngestTraceLevel = 'info' | 'debug' | 'trace' | 'error';
const TRACE_LEVEL_RANK: Record<IngestTraceLevel, number> = {
error: 0,
info: 1,
debug: 2,
trace: 3,
};
export interface IngestTraceContext {
tracePath: string;
jobId: string;
connectionId: string;
sourceKey: string;
runId?: string;
syncId?: string;
level?: IngestTraceLevel;
}
export interface IngestTraceEvent {
schemaVersion: 1;
at: string;
level: IngestTraceLevel;
jobId: string;
connectionId: string;
sourceKey: string;
runId?: string;
syncId?: string;
phase: string;
event: string;
durationMs?: number;
data?: Record<string, unknown>;
error?: {
name: string;
message: string;
stack?: string;
};
}
export interface IngestTraceWriter {
readonly tracePath: string;
readonly context: IngestTraceContext;
withContext(context: Partial<Pick<IngestTraceContext, 'runId' | 'syncId'>>): IngestTraceWriter;
event(
level: IngestTraceLevel,
phase: string,
event: string,
data?: Record<string, unknown>,
error?: unknown,
durationMs?: number,
): Promise<void>;
}
export function ingestTracePathForJob(homeDir: string, jobId: string): string {
return join(homeDir, 'ingest-traces', jobId, 'trace.jsonl');
}
function serializeError(error: unknown): IngestTraceEvent['error'] | undefined {
if (error === undefined || error === null) {
return undefined;
}
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
...(error.stack ? { stack: error.stack } : {}),
};
}
return { name: 'Error', message: String(error) };
}
function shouldWrite(configured: IngestTraceLevel, incoming: IngestTraceLevel): boolean {
return TRACE_LEVEL_RANK[incoming] <= TRACE_LEVEL_RANK[configured];
}
export class FileIngestTraceWriter implements IngestTraceWriter {
readonly tracePath: string;
readonly context: IngestTraceContext;
constructor(context: IngestTraceContext) {
this.context = { ...context, level: context.level ?? 'debug' };
this.tracePath = context.tracePath;
}
withContext(context: Partial<Pick<IngestTraceContext, 'runId' | 'syncId'>>): IngestTraceWriter {
return new FileIngestTraceWriter({ ...this.context, ...context, tracePath: this.tracePath });
}
async event(
level: IngestTraceLevel,
phase: string,
event: string,
data?: Record<string, unknown>,
error?: unknown,
durationMs?: number,
): Promise<void> {
if (!shouldWrite(this.context.level ?? 'debug', level)) {
return;
}
const serializedError = serializeError(error);
const payload: IngestTraceEvent = {
schemaVersion: 1,
at: new Date().toISOString(),
level,
jobId: this.context.jobId,
connectionId: this.context.connectionId,
sourceKey: this.context.sourceKey,
...(this.context.runId ? { runId: this.context.runId } : {}),
...(this.context.syncId ? { syncId: this.context.syncId } : {}),
phase,
event,
...(durationMs !== undefined ? { durationMs } : {}),
...(data ? { data } : {}),
...(serializedError ? { error: serializedError } : {}),
};
await mkdir(dirname(this.tracePath), { recursive: true });
await appendFile(this.tracePath, `${JSON.stringify(payload)}\n`, 'utf-8');
}
}
export class NoopIngestTraceWriter implements IngestTraceWriter {
readonly tracePath = '';
readonly context: IngestTraceContext = {
tracePath: '',
jobId: '',
connectionId: '',
sourceKey: '',
level: 'error',
};
withContext(): IngestTraceWriter {
return this;
}
async event(): Promise<void> {}
}
export async function traceTimed<T>(
trace: IngestTraceWriter,
phase: string,
event: string,
data: Record<string, unknown>,
fn: () => Promise<T>,
): Promise<T> {
await trace.event('debug', phase, `${event}_started`, data);
const started = Date.now();
try {
const result = await fn();
await trace.event('debug', phase, `${event}_finished`, data, undefined, Date.now() - started);
return result;
} catch (error) {
await trace.event('error', phase, `${event}_failed`, data, error, Date.now() - started);
throw error;
}
}