Initial open-source release

This commit is contained in:
Andrey Avtomonov 2026-05-10 23:12:26 +02:00
commit 1a42152e6f
1199 changed files with 257054 additions and 0 deletions

View file

@ -0,0 +1,168 @@
import type { MemoryFlowReplayInput } from './types.js';
function baseScenario(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowReplayInput {
return {
runId: 'run-success',
connectionId: 'warehouse',
adapter: 'metricflow',
status: 'done',
sourceDir: '/tmp/source',
syncId: 'sync-success',
reportPath: 'ingest-report.json',
errors: [],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 4 },
{ type: 'scope_detected', fingerprint: 'metricflow:demo' },
{ type: 'raw_snapshot_written', syncId: 'sync-success', rawFileCount: 4 },
{ type: 'diff_computed', added: 2, modified: 1, deleted: 0, unchanged: 1 },
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 2, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/global/orders.md' },
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
{ type: 'work_unit_started', unitKey: 'revenue', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'revenue', target: 'wiki', action: 'updated', key: 'knowledge/global/revenue.md' },
{ type: 'work_unit_finished', unitKey: 'revenue', status: 'success' },
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
{ type: 'saved', commitSha: 'abc123456789', wikiCount: 2, slCount: 1 }, // pragma: allowlist secret
{ type: 'provenance_recorded', rowCount: 4 },
{ type: 'report_created', runId: 'run-success', reportPath: 'ingest-report.json' },
],
plannedWorkUnits: [
{ unitKey: 'orders', rawFiles: ['models/orders.yml', 'models/customers.yml'], peerFileCount: 1, dependencyCount: 1 },
{ unitKey: 'revenue', rawFiles: ['docs/revenue.md'], peerFileCount: 0, dependencyCount: 0 },
],
details: {
actions: [
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/global/orders.md',
summary: 'Captured order definitions',
rawFiles: ['models/orders.yml'],
status: 'success',
},
{
unitKey: 'orders',
target: 'sl',
action: 'updated',
key: 'warehouse.orders',
summary: 'Updated orders source',
rawFiles: ['models/orders.yml'],
status: 'success',
},
{
unitKey: 'revenue',
target: 'wiki',
action: 'updated',
key: 'knowledge/global/revenue.md',
summary: 'Updated revenue notes',
rawFiles: ['docs/revenue.md'],
status: 'success',
},
],
provenance: [
{
rawPath: 'models/orders.yml',
artifactKind: 'wiki',
artifactKey: 'knowledge/global/orders.md',
actionType: 'created',
},
{ rawPath: 'models/orders.yml', artifactKind: 'sl', artifactKey: 'warehouse.orders', actionType: 'updated' },
],
transcripts: [
{
unitKey: 'orders',
path: 'transcripts/orders.json',
toolCallCount: 3,
errorCount: 0,
toolNames: ['wiki_write', 'sl_write_source'],
},
],
},
...overrides,
};
}
export function successfulReplayScenario(): MemoryFlowReplayInput {
return baseScenario();
}
export function deletedRawPathsScenario(): MemoryFlowReplayInput {
return baseScenario({
events: baseScenario().events.map((event) =>
event.type === 'diff_computed'
? { ...event, deleted: 2 }
: event.type === 'chunks_planned'
? { ...event, evictionCount: 2 }
: event,
),
});
}
export function validationRevertScenario(): MemoryFlowReplayInput {
return baseScenario({
runId: 'run-validation-failure',
status: 'error',
errors: ['semantic-layer validation failed for warehouse.orders'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 1 },
{ type: 'raw_snapshot_written', syncId: 'sync-validation', rawFileCount: 1 },
{ type: 'diff_computed', added: 1, modified: 0, deleted: 0, unchanged: 0 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
{
type: 'work_unit_finished',
unitKey: 'orders',
status: 'failed',
reason: 'semantic-layer validation failed for warehouse.orders',
},
],
plannedWorkUnits: [{ unitKey: 'orders', rawFiles: ['models/orders.yml'], peerFileCount: 0, dependencyCount: 0 }],
details: {
actions: [
{
unitKey: 'orders',
target: 'sl',
action: 'updated',
key: 'warehouse.orders',
summary: 'Invalid measure was reverted',
rawFiles: ['models/orders.yml'],
status: 'failed',
},
],
provenance: [],
transcripts: [
{
unitKey: 'orders',
path: 'transcripts/orders.json',
toolCallCount: 2,
errorCount: 1,
toolNames: ['sl_write_source'],
},
],
},
});
}
export function flaggedFallbackScenario(): MemoryFlowReplayInput {
return baseScenario({
runId: 'run-flagged-fallback',
events: baseScenario().events.map((event) =>
event.type === 'reconciliation_finished' ? { ...event, fallbackCount: 1 } : event,
),
});
}
export function postSaveSecretFailureScenario(): MemoryFlowReplayInput {
return baseScenario({
runId: 'run-post-save-failure',
status: 'error',
errors: ['index refresh failed https://example.com/private token=abc123'],
events: baseScenario().events.map((event) =>
event.type === 'saved' ? { ...event, commitSha: 'def456789012' } : event, // pragma: allowlist secret
),
});
}

View file

@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';
import {
deletedRawPathsScenario,
flaggedFallbackScenario,
postSaveSecretFailureScenario,
successfulReplayScenario,
validationRevertScenario,
} from './acceptance-fixtures.js';
import { renderMemoryFlowReplay } from './render.js';
import { buildMemoryFlowViewModel } from './view-model.js';
function renderScenario(input = successfulReplayScenario(), terminalWidth = 140): string {
return renderMemoryFlowReplay(buildMemoryFlowViewModel(input), { terminalWidth });
}
describe('memory-flow acceptance scenarios', () => {
it('renders a completed replay with a clear saved-memory completion line', () => {
const output = renderScenario(successfulReplayScenario());
expect(output).toContain('KLO memory flow warehouse/metricflow done');
expect(output).toContain('Saved 3 memories from 4 raw files: 2 wiki pages, 1 SL updates.');
expect(output).toContain('Commit: abc12345 Run: run-success Report: ingest-report.json');
});
it('renders deleted raw paths as eviction candidates without listing every raw path by default', () => {
const output = renderScenario(deletedRawPathsScenario());
expect(output).toContain('2 deletions');
expect(output).toContain('Eviction candidates: 2');
expect(output).not.toContain('/full/local/path/private/orders-2024.sql');
});
it('renders invalid semantic-layer writes as reverted, not saved', () => {
const output = renderScenario(validationRevertScenario());
expect(output).toContain('orders reverted: semantic-layer validation failed for warehouse.orders');
expect(output).toContain('Invalid semantic-layer writes were not saved.');
expect(output).not.toContain('Saved 1 memories');
});
it('renders flagged fallbacks in gates details', () => {
const output = renderScenario(flaggedFallbackScenario());
expect(output).toContain('0 conflict, 1 fallback');
expect(output).toContain('Flagged fallbacks: 1');
});
it('renders no ANSI color codes in the text fallback for terminals without color support', () => {
const output = renderScenario(successfulReplayScenario(), 80);
expect(output).toContain('KLO memory flow warehouse/metricflow done');
expect(output).not.toMatch(/\u001b\[[0-9;]*m/);
});
it('redacts secrets in visible post-save failure text', () => {
const output = renderScenario(postSaveSecretFailureScenario());
expect(output).toContain('Post-save error: index refresh failed https://[redacted] token=[redacted]');
expect(output).not.toContain('abc123');
expect(output).not.toContain('https://example.com/private');
});
});

View file

@ -0,0 +1,332 @@
import { describe, expect, it } from 'vitest';
import type { LocalIngestRunRecord } from '../local-stage-ingest.js';
import type { IngestReportSnapshot } from '../reports.js';
import { ingestReportToMemoryFlowReplay, localIngestRunToMemoryFlowReplay } from './events.js';
function localRecord(): LocalIngestRunRecord {
return {
runId: 'local-run-1',
jobId: 'local-run-1',
status: 'done',
adapter: 'metricflow',
connectionId: 'warehouse',
sourceDir: '/tmp/source',
syncId: 'sync-1',
startedAt: '2026-04-30T10:00:00.000Z',
completedAt: '2026-04-30T10:00:01.000Z',
progress: 1,
done: true,
previousRunId: null,
diffSummary: { added: 2, modified: 1, deleted: 1, unchanged: 4 },
diffPaths: {
added: ['models/orders.yml', 'models/revenue.yml'],
modified: ['models/customers.yml'],
deleted: ['models/old.yml'],
unchanged: ['models/a.yml', 'models/b.yml', 'models/c.yml', 'models/d.yml'],
},
workUnitCount: 2,
rawFileCount: 7,
workUnits: [
{
unitKey: 'orders',
rawFiles: ['models/orders.yml'],
peerFileIndex: ['models/customers.yml'],
dependencyPaths: ['models/base.yml'],
},
{
unitKey: 'revenue',
rawFiles: ['models/revenue.yml'],
peerFileIndex: [],
dependencyPaths: [],
},
],
evictionDeletedRawPaths: ['raw-sources/warehouse/metricflow/sync-1/models/old.yml'],
errors: [],
};
}
function reportSnapshot(): IngestReportSnapshot {
return {
id: 'report-1',
runId: 'run-1',
jobId: 'job-1',
connectionId: 'warehouse',
sourceKey: 'lookml',
createdAt: '2026-04-30T10:00:02.000Z',
body: {
syncId: 'sync-2',
diffSummary: { added: 1, modified: 1, deleted: 0, unchanged: 3 },
commitSha: 'abc123456789', // pragma: allowlist secret
failedWorkUnits: ['customers'],
reconciliationSkipped: false,
conflictsResolved: [
{
kind: 'near_duplicate',
artifactKey: 'warehouse.orders',
detail: 'kept candidate definition',
flaggedForHuman: false,
},
],
evictionsApplied: [],
unmappedFallbacks: [{ rawPath: 'cards/42.json', reason: 'no_connection_mapping', fallback: 'flagged' }],
evictionInputs: [],
unresolvedCards: [],
supersededBy: null,
overrideOf: null,
provenanceRows: [
{
rawPath: 'views/orders.view.lkml',
artifactKind: 'wiki',
artifactKey: 'knowledge/global/orders.md',
actionType: 'wiki_written',
},
{
rawPath: 'views/orders.view.lkml',
artifactKind: 'sl',
artifactKey: 'warehouse.orders',
actionType: 'measure_added',
},
{
rawPath: 'views/customers.view.lkml',
artifactKind: null,
artifactKey: null,
actionType: 'skipped',
},
],
toolTranscripts: [
{
unitKey: 'orders',
path: '/tmp/klo/run/wu-transcripts/job-1/orders.jsonl',
toolCallCount: 3,
errorCount: 0,
toolNames: ['read_raw_span', 'wiki_write', 'sl_write_source'],
},
{
unitKey: 'customers',
path: '/tmp/klo/run/wu-transcripts/job-1/customers.jsonl',
toolCallCount: 2,
errorCount: 1,
toolNames: ['read_raw_span', 'sl_write_source'],
},
],
workUnits: [
{
unitKey: 'orders',
rawFiles: ['views/orders.view.lkml'],
status: 'success',
actions: [
{ target: 'wiki', type: 'created', key: 'knowledge/global/orders.md', detail: 'order facts' },
{ target: 'sl', type: 'updated', key: 'warehouse.orders', detail: 'order measures' },
],
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'warehouse.orders' }],
},
{
unitKey: 'customers',
rawFiles: ['views/customers.view.lkml'],
status: 'failed',
reason: 'semantic-layer validation failed',
actions: [{ target: 'sl', type: 'created', key: 'warehouse.customers', detail: 'invalid source' }],
touchedSlSources: [{ connectionId: 'warehouse', sourceName: 'warehouse.customers' }],
},
],
},
};
}
describe('memory-flow event mapping', () => {
it('maps a local ingest run to source, snapshot, diff, chunk, and report events', () => {
const replay = localIngestRunToMemoryFlowReplay(localRecord());
expect(replay).toMatchObject({
runId: 'local-run-1',
connectionId: 'warehouse',
adapter: 'metricflow',
status: 'done',
sourceDir: '/tmp/source',
syncId: 'sync-1',
plannedWorkUnits: [
{ unitKey: 'orders', rawFiles: ['models/orders.yml'], peerFileCount: 1, dependencyCount: 1 },
{ unitKey: 'revenue', rawFiles: ['models/revenue.yml'], peerFileCount: 0, dependencyCount: 0 },
],
});
expect(replay.events).toEqual([
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 7 },
{ type: 'scope_detected', fingerprint: null },
{ type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 7 },
{ type: 'diff_computed', added: 2, modified: 1, deleted: 1, unchanged: 4 },
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 2, evictionCount: 1 },
{ type: 'report_created', runId: 'local-run-1' },
]);
});
it('maps an ingest report snapshot to work-unit, candidate, gate, saved, provenance, and report events', () => {
const replay = ingestReportToMemoryFlowReplay(reportSnapshot(), { provenanceRowCount: 5 });
expect(replay).toMatchObject({
runId: 'run-1',
connectionId: 'warehouse',
adapter: 'lookml',
status: 'error',
sourceDir: null,
syncId: 'sync-2',
reportId: 'report-1',
plannedWorkUnits: [
{ unitKey: 'orders', rawFiles: ['views/orders.view.lkml'], peerFileCount: 0, dependencyCount: 0 },
{ unitKey: 'customers', rawFiles: ['views/customers.view.lkml'], peerFileCount: 0, dependencyCount: 0 },
],
});
expect(replay.events).toContainEqual({
type: 'candidate_action',
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/global/orders.md',
});
expect(replay.events).toContainEqual({
type: 'work_unit_finished',
unitKey: 'customers',
status: 'failed',
reason: 'semantic-layer validation failed',
});
expect(replay.events).toContainEqual({ type: 'reconciliation_finished', conflictCount: 1, fallbackCount: 1 });
expect(replay.events).toContainEqual({ type: 'saved', commitSha: 'abc123456789', wikiCount: 1, slCount: 2 }); // pragma: allowlist secret
expect(replay.events).toContainEqual({ type: 'provenance_recorded', rowCount: 5 });
expect(replay.events).toContainEqual({ type: 'report_created', runId: 'run-1', reportPath: 'report-1' });
expect(replay.details.actions).toEqual([
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/global/orders.md',
summary: 'order facts',
rawFiles: ['views/orders.view.lkml'],
status: 'success',
},
{
unitKey: 'orders',
target: 'sl',
action: 'updated',
key: 'warehouse.orders',
summary: 'order measures',
rawFiles: ['views/orders.view.lkml'],
status: 'success',
},
{
unitKey: 'customers',
target: 'sl',
action: 'created',
key: 'warehouse.customers',
summary: 'invalid source',
rawFiles: ['views/customers.view.lkml'],
status: 'failed',
},
]);
expect(replay.details.provenance).toEqual([
{
rawPath: 'views/orders.view.lkml',
artifactKind: 'wiki',
artifactKey: 'knowledge/global/orders.md',
actionType: 'wiki_written',
},
{
rawPath: 'views/orders.view.lkml',
artifactKind: 'sl',
artifactKey: 'warehouse.orders',
actionType: 'measure_added',
},
{
rawPath: 'views/customers.view.lkml',
artifactKind: null,
artifactKey: null,
actionType: 'skipped',
},
]);
expect(replay.details.transcripts).toEqual([
{
unitKey: 'orders',
path: '/tmp/klo/run/wu-transcripts/job-1/orders.jsonl',
toolCallCount: 3,
errorCount: 0,
toolNames: ['read_raw_span', 'wiki_write', 'sl_write_source'],
},
{
unitKey: 'customers',
path: '/tmp/klo/run/wu-transcripts/job-1/customers.jsonl',
toolCallCount: 2,
errorCount: 1,
toolNames: ['read_raw_span', 'sl_write_source'],
},
]);
});
it('prefers captured memory-flow snapshots from report bodies', () => {
const report = reportSnapshot();
Object.assign(report.body, {
memoryFlow: {
metadata: {
schemaVersion: 1,
mode: 'full',
origin: 'captured',
timing: 'captured',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: null,
sourceReportPath: null,
fallbackReason: null,
},
runId: 'run-1',
connectionId: 'warehouse',
adapter: 'lookml',
status: 'running',
sourceDir: null,
syncId: 'sync-2',
errors: [],
plannedWorkUnits: [
{ unitKey: 'orders', rawFiles: ['views/orders.view.lkml'], peerFileCount: 1, dependencyCount: 2 },
],
details: { actions: [], provenance: [], transcripts: [] },
events: [
{
type: 'source_acquired',
adapter: 'lookml',
trigger: 'manual_resync',
fileCount: 1,
emittedAt: '2026-05-01T10:00:00.000Z',
},
],
},
});
const replay = ingestReportToMemoryFlowReplay(report);
expect(replay.metadata).toEqual({
schemaVersion: 1,
mode: 'full',
origin: 'captured',
timing: 'captured',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: 'report-1',
sourceReportPath: 'report-1',
fallbackReason: null,
});
expect(replay.status).toBe('error');
expect(replay.reportId).toBe('report-1');
expect(replay.reportPath).toBe('report-1');
expect(replay.events[0]).toMatchObject({ type: 'source_acquired', emittedAt: '2026-05-01T10:00:00.000Z' });
expect(replay.events).toContainEqual({ type: 'report_created', runId: 'run-1', reportPath: 'report-1' });
});
it('labels reconstructed report replays as synthetic when no captured snapshot exists', () => {
const replay = ingestReportToMemoryFlowReplay(reportSnapshot(), { provenanceRowCount: 5 });
expect(replay.metadata).toEqual({
schemaVersion: 1,
mode: 'full',
origin: 'synthetic-report',
timing: 'synthetic',
capturedAt: '2026-04-30T10:00:02.000Z',
sourceReportId: 'report-1',
sourceReportPath: 'report-1',
fallbackReason: 'report did not include captured memory-flow events',
});
});
});

View file

@ -0,0 +1,247 @@
import type { MemoryAction } from '../../memory/index.js';
import type { LocalIngestRunRecord } from '../local-stage-ingest.js';
import type { IngestReportSnapshot } from '../reports.js';
import type {
MemoryFlowActionDetail,
MemoryFlowDetailSections,
MemoryFlowEvent,
MemoryFlowPlannedWorkUnit,
MemoryFlowReplayInput,
} from './types.js';
interface ReportReplayOptions {
provenanceRowCount?: number;
}
function plannedWorkUnitFromLocal(
workUnit: LocalIngestRunRecord['workUnits'][number],
): MemoryFlowPlannedWorkUnit {
return {
unitKey: workUnit.unitKey,
rawFiles: workUnit.rawFiles,
peerFileCount: workUnit.peerFileIndex.length,
dependencyCount: workUnit.dependencyPaths.length,
};
}
function plannedWorkUnitFromReport(
workUnit: IngestReportSnapshot['body']['workUnits'][number],
): MemoryFlowPlannedWorkUnit {
return {
unitKey: workUnit.unitKey,
rawFiles: workUnit.rawFiles,
peerFileCount: 0,
dependencyCount: 0,
};
}
function countActions(actions: MemoryAction[], target: MemoryAction['target']): number {
return actions.filter((action) => action.target === target).length;
}
function allReportActions(report: IngestReportSnapshot): MemoryAction[] {
return report.body.workUnits.flatMap((workUnit) => workUnit.actions);
}
function rawFileCount(report: IngestReportSnapshot): number {
return new Set(report.body.workUnits.flatMap((workUnit) => workUnit.rawFiles)).size;
}
function emptyMemoryFlowDetails(): MemoryFlowDetailSections {
return { actions: [], provenance: [], transcripts: [] };
}
function fullModeMetadata(input: {
origin: 'captured' | 'synthetic-report';
timing: 'captured' | 'synthetic';
capturedAt: string | null;
sourceReportId: string | null;
sourceReportPath: string | null;
fallbackReason: string | null;
}): MemoryFlowReplayInput['metadata'] {
return {
schemaVersion: 1,
mode: 'full',
origin: input.origin,
timing: input.timing,
capturedAt: input.capturedAt,
sourceReportId: input.sourceReportId,
sourceReportPath: input.sourceReportPath,
fallbackReason: input.fallbackReason,
};
}
function reportStatus(report: IngestReportSnapshot): MemoryFlowReplayInput['status'] {
return report.body.failedWorkUnits.length > 0 ? 'error' : 'done';
}
function reportCreatedEvent(report: IngestReportSnapshot): MemoryFlowEvent {
return { type: 'report_created', runId: report.runId, reportPath: report.id };
}
function capturedReportReplay(report: IngestReportSnapshot): MemoryFlowReplayInput | null {
if (!report.body.memoryFlow) {
return null;
}
const hasReportCreated = report.body.memoryFlow.events.some((event) => event.type === 'report_created');
return {
...report.body.memoryFlow,
metadata: fullModeMetadata({
origin: 'captured',
timing: 'captured',
capturedAt: report.body.memoryFlow.metadata?.capturedAt ?? report.createdAt,
sourceReportId: report.id,
sourceReportPath: report.id,
fallbackReason: null,
}),
runId: report.runId,
connectionId: report.connectionId,
adapter: report.sourceKey,
status: reportStatus(report),
syncId: report.body.syncId,
reportId: report.id,
reportPath: report.id,
errors: report.body.failedWorkUnits,
events: hasReportCreated ? report.body.memoryFlow.events : [...report.body.memoryFlow.events, reportCreatedEvent(report)],
};
}
function actionDetailsFromReport(report: IngestReportSnapshot): MemoryFlowActionDetail[] {
return report.body.workUnits.flatMap((workUnit) =>
workUnit.actions.map((action) => ({
unitKey: workUnit.unitKey,
target: action.target,
action: action.type,
key: action.key,
summary: action.detail,
rawFiles: [...workUnit.rawFiles],
status: workUnit.status,
})),
);
}
function detailSectionsFromReport(report: IngestReportSnapshot): MemoryFlowDetailSections {
return {
actions: actionDetailsFromReport(report),
provenance: report.body.provenanceRows.map((row) => ({ ...row })),
transcripts: report.body.toolTranscripts.map((summary) => ({
...summary,
toolNames: [...summary.toolNames],
})),
};
}
export function localIngestRunToMemoryFlowReplay(record: LocalIngestRunRecord): MemoryFlowReplayInput {
const events: MemoryFlowEvent[] = [
{ type: 'source_acquired', adapter: record.adapter, trigger: 'manual_resync', fileCount: record.rawFileCount },
{ type: 'scope_detected', fingerprint: null },
{ type: 'raw_snapshot_written', syncId: record.syncId, rawFileCount: record.rawFileCount },
{ type: 'diff_computed', ...record.diffSummary },
{
type: 'chunks_planned',
chunkCount: record.workUnitCount,
workUnitCount: record.workUnitCount,
evictionCount: record.evictionDeletedRawPaths.length,
},
{ type: 'report_created', runId: record.runId },
];
return {
runId: record.runId,
connectionId: record.connectionId,
adapter: record.adapter,
status: record.status,
sourceDir: record.sourceDir,
syncId: record.syncId,
errors: record.errors,
events,
plannedWorkUnits: record.workUnits.map(plannedWorkUnitFromLocal),
details: emptyMemoryFlowDetails(),
};
}
export function ingestReportToMemoryFlowReplay(
report: IngestReportSnapshot,
options: ReportReplayOptions = {},
): MemoryFlowReplayInput {
const captured = capturedReportReplay(report);
if (captured) {
return captured;
}
const actions = allReportActions(report);
const workUnitEvents: MemoryFlowEvent[] = report.body.workUnits.flatMap((workUnit) => [
{ type: 'work_unit_started', unitKey: workUnit.unitKey, skills: [], stepBudget: 0 } satisfies MemoryFlowEvent,
...workUnit.actions.map(
(action): MemoryFlowEvent => ({
type: 'candidate_action',
unitKey: workUnit.unitKey,
target: action.target,
action: action.type,
key: action.key,
}),
),
{
type: 'work_unit_finished',
unitKey: workUnit.unitKey,
status: workUnit.status,
...(workUnit.reason ? { reason: workUnit.reason } : {}),
} satisfies MemoryFlowEvent,
]);
const events: MemoryFlowEvent[] = [
{
type: 'source_acquired',
adapter: report.sourceKey,
trigger: 'manual_resync',
fileCount: rawFileCount(report),
},
{ type: 'scope_detected', fingerprint: null },
{ type: 'raw_snapshot_written', syncId: report.body.syncId, rawFileCount: rawFileCount(report) },
{ type: 'diff_computed', ...report.body.diffSummary },
{
type: 'chunks_planned',
chunkCount: report.body.workUnits.length,
workUnitCount: report.body.workUnits.length,
evictionCount: report.body.evictionInputs.length,
},
...workUnitEvents,
{
type: 'reconciliation_finished',
conflictCount: report.body.conflictsResolved.length,
fallbackCount: report.body.unmappedFallbacks.length,
},
{
type: 'saved',
commitSha: report.body.commitSha,
wikiCount: countActions(actions, 'wiki'),
slCount: countActions(actions, 'sl'),
},
{ type: 'provenance_recorded', rowCount: options.provenanceRowCount ?? actions.length },
{ type: 'report_created', runId: report.runId, reportPath: report.id },
];
return {
metadata: fullModeMetadata({
origin: 'synthetic-report',
timing: 'synthetic',
capturedAt: report.createdAt,
sourceReportId: report.id,
sourceReportPath: report.id,
fallbackReason: 'report did not include captured memory-flow events',
}),
runId: report.runId,
connectionId: report.connectionId,
adapter: report.sourceKey,
status: reportStatus(report),
sourceDir: null,
syncId: report.body.syncId,
reportId: report.id,
reportPath: report.id,
errors: report.body.failedWorkUnits,
events,
plannedWorkUnits: report.body.workUnits.map(plannedWorkUnitFromReport),
details: detailSectionsFromReport(report),
};
}

View file

@ -0,0 +1,17 @@
export {
memoryFlowReplayInputSchema,
memoryFlowStreamEventSchema,
parseMemoryFlowReplayInput,
} from './schema.js';
export type { MemoryFlowStreamEvent } from './schema.js';
export { buildMemoryFlowViewModel } from './view-model.js';
export { renderMemoryFlowReplay } from './render.js';
export { formatMemoryFlowFinalSummary } from './summary.js';
export type {
MemoryFlowDetailSections,
MemoryFlowEvent,
MemoryFlowPlannedWorkUnit,
MemoryFlowReplayInput,
MemoryFlowRunStatus,
MemoryFlowViewModel,
} from './types.js';

View file

@ -0,0 +1,326 @@
import { describe, expect, it } from 'vitest';
import {
createInitialMemoryFlowInteractionState,
findMemoryFlowSearchMatches,
reduceMemoryFlowInteractionState,
selectMemoryFlowChip,
selectMemoryFlowColumn,
selectedMemoryFlowColumn,
selectedMemoryFlowDetails,
visibleMemoryFlowChips,
} from './interaction.js';
import type { MemoryFlowInteractionState, MemoryFlowViewModel } from './types.js';
function view(): MemoryFlowViewModel {
return {
title: 'KLO memory flow warehouse/metricflow running',
subtitle: 'Run run-1 Sync sync-1',
status: 'running',
activeLine: 'active: WorkUnit orders step 2/4',
selectedTitle: 'WORKUNITS',
selectedDetails: ['orders: 1 raw, 0 peers, 1 deps'],
completionLine: null,
trustIssues: [
{
id: 'flagged-fallbacks',
severity: 'warning',
title: 'Flagged fallbacks',
detail: '1 fallback needs review',
columnId: 'gates',
},
{
id: 'work-unit-failed:customers',
severity: 'failed',
title: 'WorkUnit failed',
detail: 'customers failed: semantic-layer validation failed',
columnId: 'workUnits',
targetLabel: 'customers',
},
],
details: {
actions: [
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/orders.md',
summary: 'order facts',
rawFiles: ['orders.yml'],
status: 'success',
},
],
provenance: [
{
rawPath: 'orders.yml',
artifactKind: 'wiki',
artifactKey: 'knowledge/orders.md',
actionType: 'wiki_written',
},
],
transcripts: [
{
unitKey: 'customers',
path: '/tmp/transcripts/customers.jsonl',
toolCallCount: 2,
errorCount: 1,
toolNames: ['read_raw_span', 'sl_write_source'],
},
],
},
columns: [
{
id: 'source',
title: 'SOURCE',
status: 'complete',
headline: '2 raw files',
counters: ['sync sync-1', 'scope none'],
chips: [{ label: 'metricflow', status: 'complete' }],
details: ['Trigger: manual_resync', 'Adapter: metricflow'],
},
{
id: 'chunks',
title: 'CHUNKS',
status: 'complete',
headline: '2 chunks',
counters: ['+1 ~1 -0 =0', '0 deletions'],
chips: [{ label: 'orders', status: 'complete' }],
details: ['Work units planned: 2', 'Eviction candidates: 0'],
},
{
id: 'workUnits',
title: 'WORKUNITS',
status: 'active',
headline: '2 WUs',
counters: ['1 done', '1 failed', '1 active'],
chips: [
{ label: 'orders', status: 'complete', detail: '1 raw span' },
{ label: 'customers', status: 'failed', detail: 'semantic-layer validation failed' },
],
details: ['orders: 1 raw, 0 peers, 1 deps', 'customers: 1 raw, 0 peers, 0 deps'],
},
{
id: 'actions',
title: 'ACTIONS',
status: 'complete',
headline: '2 candidates',
counters: ['1 wiki', '1 SL'],
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
details: ['wiki created: knowledge/orders.md', 'sl updated: warehouse.orders'],
},
{
id: 'gates',
title: 'GATES',
status: 'warning',
headline: '0 conflict, 1 fallback',
counters: ['1 failed', '1 flagged'],
chips: [{ label: 'customers', status: 'failed' }],
details: ['Failed work units: 1', 'Flagged fallbacks: 1', 'customers: semantic-layer validation failed'],
},
{
id: 'saved',
title: 'SAVED',
status: 'complete',
headline: '2 memories',
counters: ['1 wiki', '1 SL', '2 provenance'],
chips: [{ label: 'abc12345', status: 'complete' }],
details: ['Commit: abc12345', 'Run: run-1', 'Report: report-1', 'Provenance rows: 2'],
},
],
};
}
describe('memory-flow interaction reducer', () => {
it('selects the active work-unit column by default', () => {
const state = createInitialMemoryFlowInteractionState(view());
expect(state).toEqual({
selectedColumnId: 'workUnits',
selectedChipIndex: 0,
expanded: false,
pane: 'overview',
filter: 'all',
search: { editing: false, query: '', matchIndex: 0 },
shouldQuit: false,
});
expect(selectedMemoryFlowColumn(view(), state).title).toBe('WORKUNITS');
});
it('moves between columns and clamps chip selection', () => {
let state = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'down', view());
state = reduceMemoryFlowInteractionState(state, 'down', view());
expect(state.selectedChipIndex).toBe(1);
state = reduceMemoryFlowInteractionState(state, 'right', view());
expect(state.selectedColumnId).toBe('actions');
expect(state.selectedChipIndex).toBe(0);
state = reduceMemoryFlowInteractionState(state, 'left', view());
expect(state.selectedColumnId).toBe('workUnits');
expect(state.selectedChipIndex).toBe(0);
});
it('selects a column directly for mouse-driven renderers', () => {
const initial = createInitialMemoryFlowInteractionState(view());
const selected = selectMemoryFlowColumn(view(), initial, 'actions');
expect(selected).toMatchObject({
selectedColumnId: 'actions',
selectedChipIndex: 0,
expanded: true,
shouldQuit: false,
});
expect(selectedMemoryFlowColumn(view(), selected).title).toBe('ACTIONS');
expect(selectedMemoryFlowDetails(view(), selected)).toContain('wiki created: knowledge/orders.md');
});
it('selects and clamps a chip directly for mouse-driven renderers', () => {
const initial = createInitialMemoryFlowInteractionState(view());
const selected = selectMemoryFlowChip(view(), initial, 'workUnits', 99);
expect(selected).toMatchObject({
selectedColumnId: 'workUnits',
selectedChipIndex: 1,
expanded: true,
shouldQuit: false,
});
expect(selectedMemoryFlowDetails(view(), selected)).toContain(
'Selected chip: customers (semantic-layer validation failed)',
);
});
it('ignores direct selection of an unknown column', () => {
const initial = createInitialMemoryFlowInteractionState(view());
const selected = selectMemoryFlowColumn(view(), initial, 'missing' as never);
expect(selected).toEqual({ ...initial, shouldQuit: false });
});
it('toggles expansion, attention filtering, all panes, and quit', () => {
let state: MemoryFlowInteractionState = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'enter', view());
expect(state.expanded).toBe(true);
expect(selectedMemoryFlowDetails(view(), state)).toContain('orders: 1 raw, 0 peers, 1 deps');
state = reduceMemoryFlowInteractionState(state, 'filter', view());
expect(state.filter).toBe('failed_or_flagged');
expect(visibleMemoryFlowChips(selectedMemoryFlowColumn(view(), state), state)).toEqual([
{ label: 'customers', status: 'failed', detail: 'semantic-layer validation failed' },
]);
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('trust');
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('details');
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('log');
expect(selectedMemoryFlowDetails(view(), state)).toContain('WORKUNITS active: 2 WUs');
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('provenance');
expect(selectedMemoryFlowDetails(view(), state)).toContain(
'orders.yml -> wiki:knowledge/orders.md (wiki_written)',
);
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('transcript');
expect(selectedMemoryFlowDetails(view(), state)).toContain(
'customers: 2 tool calls, 1 errors, tools read_raw_span, sl_write_source',
);
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('overview');
state = reduceMemoryFlowInteractionState(state, 'provenance', view());
expect(state.pane).toBe('provenance');
expect(selectedMemoryFlowDetails(view(), state)).toContain(
'orders.yml -> wiki:knowledge/orders.md (wiki_written)',
);
state = reduceMemoryFlowInteractionState(state, 'transcript', view());
expect(state.pane).toBe('transcript');
expect(selectedMemoryFlowDetails(view(), state)).toContain(
'customers: 2 tool calls, 1 errors, tools read_raw_span, sl_write_source',
);
state = reduceMemoryFlowInteractionState(state, 'quit', view());
expect(state.shouldQuit).toBe(true);
});
it('shows trust issue details and filters chips using issue targets', () => {
let state: MemoryFlowInteractionState = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'tab', view());
expect(state.pane).toBe('trust');
expect(selectedMemoryFlowDetails(view(), state)).toEqual([
'FAILED WorkUnit failed: customers failed: semantic-layer validation failed',
'WARNING Flagged fallbacks: 1 fallback needs review',
]);
state = reduceMemoryFlowInteractionState(state, 'filter', view());
expect(visibleMemoryFlowChips(selectedMemoryFlowColumn(view(), state), state, view())).toEqual([
{ label: 'customers', status: 'failed', detail: 'semantic-layer validation failed' },
]);
});
it('searches across columns, trust issues, actions, provenance, and transcripts', () => {
const matches = findMemoryFlowSearchMatches(view(), 'customers');
expect(matches.map((match) => match.label)).toEqual([
'WORKUNITS > customers',
'GATES',
'Trust > WorkUnit failed',
'Transcript > customers',
]);
let state = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'search-start', view());
state = reduceMemoryFlowInteractionState(state, { type: 'search-input', value: 'customers' }, view());
expect(state.search).toEqual({
editing: true,
query: 'customers',
matchIndex: 0,
});
expect(state.selectedColumnId).toBe('workUnits');
expect(state.selectedChipIndex).toBe(1);
state = reduceMemoryFlowInteractionState(state, 'search-submit', view());
expect(state.search.editing).toBe(false);
});
it('cycles search matches forward and backward with wraparound', () => {
let state = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'search-start', view());
state = reduceMemoryFlowInteractionState(state, { type: 'search-input', value: 'customers' }, view());
expect(state.search).toEqual({ editing: true, query: 'customers', matchIndex: 0 });
expect(state.selectedColumnId).toBe('workUnits');
expect(state.selectedChipIndex).toBe(1);
state = reduceMemoryFlowInteractionState(state, 'search-next', view());
expect(state.search).toEqual({ editing: true, query: 'customers', matchIndex: 1 });
expect(state.selectedColumnId).toBe('gates');
expect(state.selectedChipIndex).toBe(0);
state = reduceMemoryFlowInteractionState(state, 'search-next', view());
expect(state.search).toEqual({ editing: true, query: 'customers', matchIndex: 2 });
expect(state.selectedColumnId).toBe('workUnits');
state = reduceMemoryFlowInteractionState(state, 'search-previous', view());
expect(state.search).toEqual({ editing: true, query: 'customers', matchIndex: 1 });
expect(state.selectedColumnId).toBe('gates');
state = reduceMemoryFlowInteractionState(state, 'search-previous', view());
state = reduceMemoryFlowInteractionState(state, 'search-previous', view());
expect(state.search).toEqual({ editing: true, query: 'customers', matchIndex: 3 });
expect(state.selectedColumnId).toBe('workUnits');
});
});

View file

@ -0,0 +1,450 @@
import type {
MemoryFlowChip,
MemoryFlowColumnView,
MemoryFlowFilterMode,
MemoryFlowInteractionCommand,
MemoryFlowInteractionState,
MemoryFlowPaneId,
MemoryFlowSearchMatch,
MemoryFlowViewModel,
} from './types.js';
const CYCLING_PANES: MemoryFlowPaneId[] = ['overview', 'trust', 'details', 'log', 'provenance', 'transcript'];
function attentionStatus(status: MemoryFlowChip['status']): boolean {
return status === 'failed' || status === 'warning';
}
function trustIssueTargets(view: MemoryFlowViewModel, column: MemoryFlowColumnView): Set<string> {
return new Set(
view.trustIssues
.filter((issue) => issue.columnId === column.id && issue.targetLabel)
.map((issue) => issue.targetLabel as string),
);
}
function columnIndex(view: MemoryFlowViewModel, columnId: MemoryFlowInteractionState['selectedColumnId']): number {
const index = view.columns.findIndex((column) => column.id === columnId);
return index >= 0 ? index : 0;
}
function clampChipIndex(column: MemoryFlowColumnView, state: MemoryFlowInteractionState, view?: MemoryFlowViewModel): number {
const chips = visibleMemoryFlowChips(column, state, view);
if (chips.length === 0) {
return 0;
}
return Math.max(0, Math.min(state.selectedChipIndex, chips.length - 1));
}
function withColumn(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
direction: -1 | 1,
): MemoryFlowInteractionState {
const nextIndex = Math.max(0, Math.min(columnIndex(view, state.selectedColumnId) + direction, view.columns.length - 1));
const selectedColumnId = view.columns[nextIndex]?.id ?? state.selectedColumnId;
const nextState = { ...state, selectedColumnId, selectedChipIndex: 0, expanded: false };
return { ...nextState, selectedChipIndex: clampChipIndex(selectedMemoryFlowColumn(view, nextState), nextState, view) };
}
function nextPane(current: MemoryFlowPaneId): MemoryFlowPaneId {
const currentIndex = CYCLING_PANES.indexOf(current);
if (currentIndex === -1) {
return 'overview';
}
return CYCLING_PANES[(currentIndex + 1) % CYCLING_PANES.length] ?? 'overview';
}
function toggleFilter(filter: MemoryFlowFilterMode): MemoryFlowFilterMode {
return filter === 'all' ? 'failed_or_flagged' : 'all';
}
export function visibleMemoryFlowChips(
column: MemoryFlowColumnView,
state: Pick<MemoryFlowInteractionState, 'filter'>,
view?: MemoryFlowViewModel,
): MemoryFlowChip[] {
if (state.filter === 'all') {
return column.chips;
}
const issueTargets = view ? trustIssueTargets(view, column) : new Set<string>();
return column.chips.filter((chip) => attentionStatus(chip.status) || issueTargets.has(chip.label));
}
function includesQuery(value: string, query: string): boolean {
return value.toLocaleLowerCase().includes(query.toLocaleLowerCase());
}
function pushMatch(
matches: MemoryFlowSearchMatch[],
query: string,
match: MemoryFlowSearchMatch,
values: string[],
): void {
if (values.some((value) => includesQuery(value, query))) {
matches.push(match);
}
}
export function findMemoryFlowSearchMatches(view: MemoryFlowViewModel, query: string): MemoryFlowSearchMatch[] {
const normalized = query.trim();
if (!normalized) {
return [];
}
const matches: MemoryFlowSearchMatch[] = [];
for (const column of view.columns) {
const chipMatches = column.chips
.map((chip, chipIndex) => ({ chip, chipIndex }))
.filter(({ chip }) => includesQuery(chip.label, normalized) || includesQuery(chip.detail ?? '', normalized));
for (const { chip, chipIndex } of chipMatches) {
if (column.id === 'workUnits' || column.id === 'actions') {
matches.push({
columnId: column.id,
chipIndex,
label: `${column.title} > ${chip.label}`,
detail: chip.detail ?? column.headline,
});
}
}
if (chipMatches.length === 0 || column.id !== 'workUnits') {
pushMatch(matches, normalized, { columnId: column.id, label: column.title, detail: column.headline }, [
column.title,
column.headline,
...column.counters,
...column.details,
]);
}
}
for (const issue of view.trustIssues) {
pushMatch(
matches,
normalized,
{ columnId: issue.columnId, label: `Trust > ${issue.title}`, detail: issue.detail },
[issue.title, issue.detail, issue.targetLabel ?? ''],
);
}
for (const row of view.details.provenance) {
pushMatch(
matches,
normalized,
{
columnId: 'saved',
label: `Provenance > ${row.rawPath}`,
detail: `${row.rawPath} ${row.artifactKind ?? 'none'} ${row.artifactKey ?? 'none'} ${row.actionType}`,
},
[row.rawPath, row.artifactKind ?? '', row.artifactKey ?? '', row.actionType],
);
}
for (const transcript of view.details.transcripts) {
pushMatch(
matches,
normalized,
{
columnId: 'workUnits',
label: `Transcript > ${transcript.unitKey}`,
detail: `${transcript.path} ${transcript.toolNames.join(' ')}`,
},
[transcript.unitKey, transcript.path, ...transcript.toolNames],
);
}
return matches;
}
function selectSearchMatch(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
query: string,
matchIndex: number,
): MemoryFlowInteractionState {
const matches = findMemoryFlowSearchMatches(view, query);
if (matches.length === 0) {
return {
...state,
search: { editing: state.search.editing, query, matchIndex: 0 },
shouldQuit: false,
};
}
const index = Math.max(0, Math.min(matchIndex, matches.length - 1));
const match = matches[index]!;
const nextState = {
...state,
selectedColumnId: match.columnId,
selectedChipIndex: match.chipIndex ?? 0,
expanded: true,
search: { editing: state.search.editing, query, matchIndex: index },
shouldQuit: false,
};
return {
...nextState,
selectedChipIndex: clampChipIndex(selectedMemoryFlowColumn(view, nextState), nextState, view),
};
}
function moveSearchMatch(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
direction: -1 | 1,
): MemoryFlowInteractionState {
const query = state.search.query.trim();
if (!query) {
return { ...state, search: { ...state.search, matchIndex: 0 }, shouldQuit: false };
}
const matches = findMemoryFlowSearchMatches(view, query);
if (matches.length === 0) {
return { ...state, search: { ...state.search, matchIndex: 0 }, shouldQuit: false };
}
const nextIndex = (state.search.matchIndex + direction + matches.length) % matches.length;
return selectSearchMatch(view, state, state.search.query, nextIndex);
}
export function selectedMemoryFlowColumn(
view: MemoryFlowViewModel,
state: Pick<MemoryFlowInteractionState, 'selectedColumnId'>,
): MemoryFlowColumnView {
return view.columns.find((column) => column.id === state.selectedColumnId) ?? view.columns[0]!;
}
export function createInitialMemoryFlowInteractionState(view: MemoryFlowViewModel): MemoryFlowInteractionState {
const column =
view.columns.find((candidate) => candidate.status === 'active') ??
view.columns.find((candidate) => candidate.status === 'failed' || candidate.status === 'warning') ??
view.columns.find((candidate) => candidate.details.length > 0) ??
view.columns[0]!;
return {
selectedColumnId: column.id,
selectedChipIndex: 0,
expanded: false,
pane: 'overview',
filter: 'all',
search: { editing: false, query: '', matchIndex: 0 },
shouldQuit: false,
};
}
export function selectMemoryFlowColumn(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
columnId: MemoryFlowInteractionState['selectedColumnId'],
): MemoryFlowInteractionState {
const column = view.columns.find((candidate) => candidate.id === columnId);
if (!column) {
return { ...state, shouldQuit: false };
}
const nextState = {
...state,
selectedColumnId: column.id,
selectedChipIndex: 0,
expanded: true,
shouldQuit: false,
};
return { ...nextState, selectedChipIndex: clampChipIndex(column, nextState, view) };
}
export function selectMemoryFlowChip(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
columnId: MemoryFlowInteractionState['selectedColumnId'],
chipIndex: number,
): MemoryFlowInteractionState {
const column = view.columns.find((candidate) => candidate.id === columnId);
if (!column) {
return { ...state, shouldQuit: false };
}
const nextState = {
...state,
selectedColumnId: column.id,
selectedChipIndex: Math.max(0, chipIndex),
expanded: true,
shouldQuit: false,
};
return { ...nextState, selectedChipIndex: clampChipIndex(column, nextState, view) };
}
export function reduceMemoryFlowInteractionState(
state: MemoryFlowInteractionState,
command: MemoryFlowInteractionCommand,
view: MemoryFlowViewModel,
): MemoryFlowInteractionState {
if (command === 'search-start') {
return { ...state, pane: 'details', search: { ...state.search, editing: true }, shouldQuit: false };
}
if (command === 'search-submit') {
return { ...state, search: { ...state.search, editing: false }, shouldQuit: false };
}
if (command === 'search-clear') {
return { ...state, search: { editing: false, query: '', matchIndex: 0 }, shouldQuit: false };
}
if (command === 'search-backspace') {
return selectSearchMatch(view, state, state.search.query.slice(0, -1), 0);
}
if (command === 'search-next') {
return moveSearchMatch(view, state, 1);
}
if (command === 'search-previous') {
return moveSearchMatch(view, state, -1);
}
if (typeof command === 'object' && command.type === 'search-input') {
return selectSearchMatch(view, state, `${state.search.query}${command.value}`, 0);
}
if (command === 'quit') {
return { ...state, shouldQuit: true };
}
if (command === 'left') {
return withColumn(view, { ...state, shouldQuit: false }, -1);
}
if (command === 'right') {
return withColumn(view, { ...state, shouldQuit: false }, 1);
}
if (command === 'up' || command === 'down') {
const column = selectedMemoryFlowColumn(view, state);
const visibleChips = visibleMemoryFlowChips(column, state, view);
const delta = command === 'up' ? -1 : 1;
return {
...state,
selectedChipIndex:
visibleChips.length === 0
? 0
: Math.max(0, Math.min(state.selectedChipIndex + delta, visibleChips.length - 1)),
shouldQuit: false,
};
}
if (command === 'enter') {
return { ...state, expanded: !state.expanded, shouldQuit: false };
}
if (command === 'tab') {
return { ...state, pane: nextPane(state.pane), shouldQuit: false };
}
if (command === 'filter') {
const nextState = { ...state, filter: toggleFilter(state.filter), selectedChipIndex: 0, shouldQuit: false };
return {
...nextState,
selectedChipIndex: clampChipIndex(selectedMemoryFlowColumn(view, nextState), nextState, view),
};
}
if (command === 'provenance') {
return { ...state, pane: 'provenance', expanded: true, shouldQuit: false };
}
if (command === 'transcript') {
return { ...state, pane: 'transcript', expanded: true, shouldQuit: false };
}
return { ...state, shouldQuit: false };
}
function trustIssueDetailLines(view: MemoryFlowViewModel): string[] {
if (view.trustIssues.length === 0) {
return ['No trust issues detected.'];
}
return view.trustIssues
.slice()
.sort((left, right) => {
if (left.severity === right.severity) return 0;
return left.severity === 'failed' ? -1 : 1;
})
.map((issue) => {
const label = issue.severity === 'failed' ? 'FAILED' : 'WARNING';
return `${label} ${issue.title}: ${issue.detail}`;
});
}
function provenanceDetailLines(view: MemoryFlowViewModel): string[] {
if (view.details.provenance.length === 0) {
const savedColumn = view.columns.find((candidate) => candidate.id === 'saved');
return savedColumn?.details.length ? savedColumn.details : ['Provenance rows: 0'];
}
return view.details.provenance.map((row) => {
const artifact = row.artifactKind && row.artifactKey ? `${row.artifactKind}:${row.artifactKey}` : 'no saved artifact';
return `${row.rawPath} -> ${artifact} (${row.actionType})`;
});
}
function transcriptDetailLines(view: MemoryFlowViewModel, selectedChip: MemoryFlowChip | undefined): string[] {
const selectedUnit = selectedChip?.label;
const transcripts =
selectedUnit && view.details.transcripts.some((summary) => summary.unitKey === selectedUnit)
? view.details.transcripts.filter((summary) => summary.unitKey === selectedUnit)
: view.details.transcripts;
if (transcripts.length === 0) {
const workUnitsColumn = view.columns.find((candidate) => candidate.id === 'workUnits');
return workUnitsColumn?.details.length ? workUnitsColumn.details : ['No work-unit transcript summary available.'];
}
return transcripts.map(
(summary) =>
`${summary.unitKey}: ${summary.toolCallCount} tool calls, ${summary.errorCount} errors, tools ${
summary.toolNames.join(', ') || 'none'
}`,
);
}
export function selectedMemoryFlowDetails(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string[] {
const column = selectedMemoryFlowColumn(view, state);
const chips = visibleMemoryFlowChips(column, state, view);
const selectedChip = chips[state.selectedChipIndex];
if (state.pane === 'log') {
return [
view.activeLine,
...view.columns.map((candidate) => `${candidate.title} ${candidate.status}: ${candidate.headline}`),
...(view.completionLine ? [view.completionLine] : []),
];
}
if (state.pane === 'trust') {
return trustIssueDetailLines(view);
}
if (state.pane === 'provenance') {
return provenanceDetailLines(view);
}
if (state.pane === 'transcript') {
return transcriptDetailLines(view, selectedChip);
}
const baseDetails = column.details.length ? column.details : [`${column.title}: ${column.headline}`];
if (state.pane === 'overview' && !state.expanded) {
return [
column.headline,
...column.counters,
...(selectedChip ? [`Selected chip: ${selectedChip.label}${selectedChip.detail ? ` (${selectedChip.detail})` : ''}`] : []),
];
}
return [
...baseDetails,
...(selectedChip ? [`Selected chip: ${selectedChip.label}${selectedChip.detail ? ` (${selectedChip.detail})` : ''}`] : []),
];
}

View file

@ -0,0 +1,177 @@
import { describe, expect, it } from 'vitest';
import { createInitialMemoryFlowInteractionState, reduceMemoryFlowInteractionState } from './interaction.js';
import { renderMemoryFlowInteractive } from './interactive-render.js';
import type { MemoryFlowViewModel } from './types.js';
function view(): MemoryFlowViewModel {
return {
title: 'KLO memory flow warehouse/metricflow done',
subtitle: 'Run run-1 Sync sync-1',
status: 'done',
activeLine: 'active: complete',
selectedTitle: 'WORKUNITS',
selectedDetails: ['orders: 1 raw, 0 peers, 1 deps'],
completionLine:
'Saved 2 memories from 2 raw files: 1 wiki pages, 1 SL updates. Commit: abc12345 Run: run-1 Report: report-1',
trustIssues: [
{
id: 'work-unit-failed:customers',
severity: 'failed',
title: 'WorkUnit failed',
detail: 'customers failed: validation reset',
columnId: 'workUnits',
targetLabel: 'customers',
},
{
id: 'flagged-fallbacks',
severity: 'warning',
title: 'Flagged fallbacks',
detail: '1 fallback needs review',
columnId: 'gates',
},
],
details: {
actions: [
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/orders.md',
summary: 'order facts',
rawFiles: ['orders.yml'],
status: 'success',
},
],
provenance: [
{
rawPath: 'orders.yml',
artifactKind: 'wiki',
artifactKey: 'knowledge/orders.md',
actionType: 'wiki_written',
},
],
transcripts: [
{
unitKey: 'customers',
path: '/tmp/transcripts/customers.jsonl',
toolCallCount: 2,
errorCount: 1,
toolNames: ['read_raw_span', 'sl_write_source'],
},
],
},
columns: [
{
id: 'source',
title: 'SOURCE',
status: 'complete',
headline: '2 raw files',
counters: ['sync sync-1', 'scope none'],
chips: [{ label: 'metricflow', status: 'complete' }],
details: ['Trigger: manual_resync', 'Adapter: metricflow'],
},
{
id: 'chunks',
title: 'CHUNKS',
status: 'complete',
headline: '2 chunks',
counters: ['+1 ~1 -0 =0', '0 deletions'],
chips: [{ label: 'orders', status: 'complete' }],
details: ['Work units planned: 2', 'Eviction candidates: 0'],
},
{
id: 'workUnits',
title: 'WORKUNITS',
status: 'warning',
headline: '2 WUs',
counters: ['1 done', '1 failed', '0 active'],
chips: [
{ label: 'orders', status: 'complete', detail: '1 raw span' },
{ label: 'customers', status: 'failed', detail: 'validation reset' },
],
details: ['orders: 1 raw, 0 peers, 1 deps', 'customers: 1 raw, 0 peers, 0 deps'],
},
{
id: 'actions',
title: 'ACTIONS',
status: 'complete',
headline: '2 candidates',
counters: ['1 wiki', '1 SL'],
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
details: ['wiki created: knowledge/orders.md', 'sl updated: warehouse.orders'],
},
{
id: 'gates',
title: 'GATES',
status: 'warning',
headline: '0 conflict, 1 fallback',
counters: ['1 failed', '1 flagged'],
chips: [{ label: 'customers', status: 'failed' }],
details: ['Failed work units: 1', 'Flagged fallbacks: 1'],
},
{
id: 'saved',
title: 'SAVED',
status: 'complete',
headline: '2 memories',
counters: ['1 wiki', '1 SL', '2 provenance'],
chips: [{ label: 'abc12345', status: 'complete' }],
details: ['Commit: abc12345', 'Run: run-1', 'Report: report-1', 'Provenance rows: 2'],
},
],
};
}
describe('renderMemoryFlowInteractive', () => {
it('marks the selected column and selected chip in a wide layout', () => {
const state = createInitialMemoryFlowInteractionState(view());
const output = renderMemoryFlowInteractive(view(), state, { terminalWidth: 140 });
expect(output).toContain('KLO memory flow warehouse/metricflow done');
expect(output).toContain('OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED');
expect(output).toContain('[WORKUNITS]');
expect(output).toContain('> orders');
expect(output).toContain('Selected: WORKUNITS > orders');
expect(output).toContain('Pane: overview Filter: all');
expect(output).toContain('- Selected chip: orders (1 raw span)');
expect(output).toContain(
'Saved 2 memories from 2 raw files: 1 wiki pages, 1 SL updates. Commit: abc12345 Run: run-1 Report: report-1',
);
});
it('renders attention-filtered details in a narrow layout', () => {
let state = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'filter', view());
state = reduceMemoryFlowInteractionState(state, 'enter', view());
const output = renderMemoryFlowInteractive(view(), state, { terminalWidth: 72 });
expect(output).toContain('OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED');
expect(output).toContain('[WORKUNITS]');
expect(output).toContain('Filter: failed_or_flagged');
expect(output).toContain('> customers');
expect(output).toContain('- customers: 1 raw, 0 peers, 0 deps');
});
it('renders report-backed transcript detail pane rows', () => {
let state = createInitialMemoryFlowInteractionState(view());
state = reduceMemoryFlowInteractionState(state, 'down', view());
state = reduceMemoryFlowInteractionState(state, 'transcript', view());
const output = renderMemoryFlowInteractive(view(), state, { terminalWidth: 100 });
expect(output).toContain('Pane: transcript Filter: all');
expect(output).toContain('- customers: 2 tool calls, 1 errors, tools read_raw_span, sl_write_source');
});
it('keeps trust issues visible in the interactive renderer', () => {
const state = createInitialMemoryFlowInteractionState(view());
const output = renderMemoryFlowInteractive(view(), state, { terminalWidth: 140 });
expect(output).toContain('Trust issues');
expect(output).toContain('FAILED WorkUnit failed: customers failed: validation reset');
expect(output).toContain('WARNING Flagged fallbacks: 1 fallback needs review');
});
});

View file

@ -0,0 +1,160 @@
import {
findMemoryFlowSearchMatches,
selectedMemoryFlowColumn,
selectedMemoryFlowDetails,
visibleMemoryFlowChips,
} from './interaction.js';
import type {
MemoryFlowColumnView,
MemoryFlowInteractionState,
MemoryFlowRenderOptions,
MemoryFlowViewModel,
} from './types.js';
import { renderMemoryFlowConnectorLine } from './visuals.js';
const WIDE_COLUMN_WIDTH = 18;
function cell(value: string | undefined, width = WIDE_COLUMN_WIDTH): string {
const text = value ?? '';
const normalized = text.length > width ? text.slice(0, width - 1) : text;
return normalized.padEnd(width, ' ');
}
function row(values: string[]): string {
return values.map((value) => cell(value)).join(' ').trimEnd();
}
function columnLabel(column: MemoryFlowColumnView, state: MemoryFlowInteractionState): string {
return column.id === state.selectedColumnId ? `[${column.title}]` : column.title;
}
function counterAt(column: MemoryFlowColumnView, index: number): string {
return column.counters[index] ?? '';
}
function chipLabel(view: MemoryFlowViewModel, column: MemoryFlowColumnView, state: MemoryFlowInteractionState): string {
const chips = visibleMemoryFlowChips(column, state, view);
if (chips.length === 0) {
return '-';
}
const selectedIndex = column.id === state.selectedColumnId ? state.selectedChipIndex : -1;
return chips
.slice(0, 2)
.map((chip, index) => `${index === selectedIndex ? '> ' : ''}${chip.label}`)
.join(', ');
}
function selectedLine(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string {
const column = selectedMemoryFlowColumn(view, state);
const chip = visibleMemoryFlowChips(column, state, view)[state.selectedChipIndex];
return `Selected: ${column.title}${chip ? ` > ${chip.label}` : ''}`;
}
function trustIssueLines(view: MemoryFlowViewModel): string[] {
if (view.trustIssues.length === 0) {
return [];
}
return [
'Trust issues',
...view.trustIssues.slice(0, 4).map((issue) => {
const label = issue.severity === 'failed' ? 'FAILED' : 'WARNING';
return `${label} ${issue.title}: ${issue.detail}`;
}),
...(view.trustIssues.length > 4 ? [`+${view.trustIssues.length - 4} more trust issues`] : []),
'',
];
}
function searchLine(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string | null {
if (!state.search.editing && state.search.query.length === 0) {
return null;
}
const matches = findMemoryFlowSearchMatches(view, state.search.query);
const active = state.search.editing ? 'editing' : 'locked';
return `Search: ${state.search.query || '/'} (${matches.length} matches, ${active})`;
}
function detailLines(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string[] {
const currentSearchLine = searchLine(view, state);
return [
selectedLine(view, state),
`Pane: ${state.pane} Filter: ${state.filter}`,
...(currentSearchLine ? [currentSearchLine] : []),
...selectedMemoryFlowDetails(view, state).map((detail) => `- ${detail}`),
];
}
function renderWide(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string {
const lines = [
view.title,
view.activeLine,
view.subtitle,
renderMemoryFlowConnectorLine(view),
...trustIssueLines(view),
'',
row(view.columns.map((column) => columnLabel(column, state))),
row(view.columns.map((column) => column.headline)),
row(view.columns.map((column) => counterAt(column, 0))),
row(view.columns.map((column) => counterAt(column, 1))),
row(view.columns.map((column) => chipLabel(view, column, state))),
'',
...detailLines(view, state),
];
if (view.completionLine) {
lines.push('', view.completionLine);
}
lines.push('');
return lines.join('\n');
}
function renderNarrowColumn(
view: MemoryFlowViewModel,
column: MemoryFlowColumnView,
state: MemoryFlowInteractionState,
): string[] {
return [
columnLabel(column, state),
` ${column.headline}`,
...column.counters.slice(0, 3).map((counter) => ` ${counter}`),
` ${chipLabel(view, column, state)}`,
];
}
function renderNarrow(view: MemoryFlowViewModel, state: MemoryFlowInteractionState): string {
const lines = [
view.title,
view.activeLine,
view.subtitle,
renderMemoryFlowConnectorLine(view),
...trustIssueLines(view),
'',
...view.columns.flatMap((column, index) => [
...(index > 0 ? [''] : []),
...renderNarrowColumn(view, column, state),
]),
'',
...detailLines(view, state),
];
if (view.completionLine) {
lines.push('', view.completionLine);
}
lines.push('');
return lines.join('\n');
}
export function renderMemoryFlowInteractive(
view: MemoryFlowViewModel,
state: MemoryFlowInteractionState,
options: MemoryFlowRenderOptions = {},
): string {
if ((options.terminalWidth ?? 120) < 100) {
return renderNarrow(view, state);
}
return renderWide(view, state);
}

View file

@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest';
import { createMemoryFlowLiveBuffer, sanitizeMemoryFlowError } from './live-buffer.js';
import type { MemoryFlowReplayInput } from './types.js';
function initialReplay(): MemoryFlowReplayInput {
return {
runId: 'live-run-1',
connectionId: 'warehouse',
adapter: 'fake',
status: 'running',
sourceDir: '/tmp/source',
syncId: 'pending',
errors: [],
events: [],
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
};
}
describe('createMemoryFlowLiveBuffer', () => {
it('emits immutable replay snapshots on every live change', () => {
const onChange = vi.fn();
const buffer = createMemoryFlowLiveBuffer(initialReplay(), { onChange });
buffer.emit({ type: 'source_acquired', adapter: 'fake', trigger: 'manual_resync', fileCount: 2 });
buffer.update({
syncId: 'sync-1',
plannedWorkUnits: [
{
unitKey: 'fake-orders',
rawFiles: ['orders.json'],
peerFileCount: 0,
dependencyCount: 0,
},
],
});
buffer.emit({ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 });
buffer.finish('done');
expect(onChange).toHaveBeenCalledTimes(4);
expect(buffer.snapshot()).toMatchObject({
runId: 'live-run-1',
status: 'done',
syncId: 'sync-1',
plannedWorkUnits: [{ unitKey: 'fake-orders' }],
});
expect(buffer.snapshot().events.map((event) => event.type)).toEqual(['source_acquired', 'chunks_planned']);
const staleSnapshot = onChange.mock.calls[1][0] as MemoryFlowReplayInput;
expect(staleSnapshot.details).toEqual({ actions: [], provenance: [], transcripts: [] });
staleSnapshot.events.push({ type: 'report_created', runId: 'mutated' });
expect(buffer.snapshot().events.map((event) => event.type)).toEqual(['source_acquired', 'chunks_planned']);
});
it('stamps live events with emittedAt without mutating caller events', () => {
const event = { type: 'source_acquired', adapter: 'fake', trigger: 'manual_resync', fileCount: 2 } as const;
const buffer = createMemoryFlowLiveBuffer(initialReplay(), {
now: () => new Date('2026-05-01T10:00:00.000Z'),
});
buffer.emit(event);
expect(event).not.toHaveProperty('emittedAt');
expect(buffer.snapshot().events).toEqual([
{
type: 'source_acquired',
adapter: 'fake',
trigger: 'manual_resync',
fileCount: 2,
emittedAt: '2026-05-01T10:00:00.000Z',
},
]);
});
it('marks failed runs with sanitized error messages', () => {
const onChange = vi.fn();
const buffer = createMemoryFlowLiveBuffer(initialReplay(), { onChange });
buffer.finish('error', [
sanitizeMemoryFlowError(
new Error('Connection failed for postgres://user:password@localhost:5432/db?api_key=abc password=secret'), // pragma: allowlist secret
),
]);
expect(buffer.snapshot()).toMatchObject({
status: 'error',
errors: ['Connection failed for postgres://[redacted] password=[redacted]'],
});
expect(onChange).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,74 @@
import type {
MemoryFlowEvent,
MemoryFlowEventSink,
MemoryFlowLiveBufferOptions,
MemoryFlowReplayInput,
MemoryFlowReplayPatch,
MemoryFlowRunStatus,
} from './types.js';
const URL_PATTERN = /\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/gi;
const SECRET_ASSIGNMENT_PATTERN = /\b(password|passwd|pwd|token|api[_-]?key|secret)=([^\s&]+)/gi;
function copyReplayInput(input: MemoryFlowReplayInput): MemoryFlowReplayInput {
return {
...input,
errors: [...input.errors],
events: [...input.events],
plannedWorkUnits: input.plannedWorkUnits.map((workUnit) => ({
...workUnit,
rawFiles: [...workUnit.rawFiles],
})),
details: {
actions: input.details.actions.map((action) => ({ ...action, rawFiles: [...action.rawFiles] })),
provenance: input.details.provenance.map((row) => ({ ...row })),
transcripts: input.details.transcripts.map((summary) => ({ ...summary, toolNames: [...summary.toolNames] })),
},
};
}
function notify(input: MemoryFlowReplayInput, options: MemoryFlowLiveBufferOptions): void {
options.onChange?.(copyReplayInput(input));
}
function stampEvent(event: MemoryFlowEvent, options: MemoryFlowLiveBufferOptions): MemoryFlowEvent {
if (event.emittedAt) {
return { ...event };
}
return { ...event, emittedAt: (options.now ?? (() => new Date()))().toISOString() };
}
export function sanitizeMemoryFlowError(error: unknown): string {
const raw = error instanceof Error ? error.message : String(error);
return raw
.replace(URL_PATTERN, (value) => `${value.slice(0, value.indexOf('://'))}://[redacted]`)
.replace(SECRET_ASSIGNMENT_PATTERN, '$1=[redacted]');
}
export function createMemoryFlowLiveBuffer(
initialInput: MemoryFlowReplayInput,
options: MemoryFlowLiveBufferOptions = {},
): MemoryFlowEventSink {
let input = copyReplayInput(initialInput);
return {
emit(event: MemoryFlowEvent): void {
input = { ...input, events: [...input.events, stampEvent(event, options)] };
notify(input, options);
},
update(patch: MemoryFlowReplayPatch): void {
input = copyReplayInput({ ...input, ...patch });
notify(input, options);
},
finish(status: MemoryFlowRunStatus, errors: string[] = input.errors): void {
input = copyReplayInput({ ...input, status, errors });
notify(input, options);
},
snapshot(): MemoryFlowReplayInput {
return copyReplayInput(input);
},
};
}

View file

@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
describe('@klo/context/ingest/memory-flow lightweight export', () => {
it('exports replay parsing and text rendering without the full ingest entry point', async () => {
const memoryFlow = await import('./index.js');
expect(memoryFlow.parseMemoryFlowReplayInput).toBeTypeOf('function');
expect(memoryFlow.buildMemoryFlowViewModel).toBeTypeOf('function');
expect(memoryFlow.renderMemoryFlowReplay).toBeTypeOf('function');
});
});

View file

@ -0,0 +1,114 @@
import { describe, expect, it } from 'vitest';
import type { MemoryFlowViewModel } from './types.js';
import { renderMemoryFlowReplay } from './render.js';
function view(): MemoryFlowViewModel {
return {
title: 'KLO memory flow warehouse/metricflow done',
subtitle: 'Run run-1 Sync sync-1',
status: 'done',
activeLine: 'active: complete',
selectedTitle: 'SOURCE',
selectedDetails: ['Trigger: manual_resync', 'Adapter: metricflow'],
completionLine:
'Saved 2 memories from 2 raw files: 1 wiki pages, 1 SL updates. Commit: abc12345 Run: run-1 Report: report-1',
trustIssues: [],
details: { actions: [], provenance: [], transcripts: [] },
columns: [
{
id: 'source',
title: 'SOURCE',
status: 'complete',
headline: '2 raw files',
counters: ['sync sync-1', 'scope none'],
chips: [{ label: 'metricflow', status: 'complete' }],
details: ['Trigger: manual_resync'],
},
{
id: 'chunks',
title: 'CHUNKS',
status: 'complete',
headline: '2 chunks',
counters: ['+1 ~1 -0 =3', '0 deletions'],
chips: [{ label: 'orders', status: 'complete' }],
details: ['Work units planned: 2'],
},
{
id: 'workUnits',
title: 'WORKUNITS',
status: 'warning',
headline: '2 WUs',
counters: ['1 done', '1 failed', '0 active'],
chips: [{ label: 'orders', status: 'complete' }],
details: ['orders: 1 raw, 1 peers, 1 deps'],
},
{
id: 'actions',
title: 'ACTIONS',
status: 'complete',
headline: '2 candidates',
counters: ['1 wiki', '1 SL'],
chips: [{ label: 'knowledge/orders.md', status: 'complete' }],
details: ['wiki created: knowledge/orders.md'],
},
{
id: 'gates',
title: 'GATES',
status: 'warning',
headline: '1 conflict, 1 fallback',
counters: ['1 failed', '1 flagged'],
chips: [{ label: 'customers', status: 'failed' }],
details: ['Failed work units: 1'],
},
{
id: 'saved',
title: 'SAVED',
status: 'complete',
headline: '2 memories',
counters: ['1 wiki', '1 SL', '3 provenance'],
chips: [{ label: 'abc12345', status: 'complete' }],
details: ['Commit: abc12345'],
},
],
};
}
describe('renderMemoryFlowReplay', () => {
it('renders a six-column wide terminal snapshot', () => {
expect(renderMemoryFlowReplay(view(), { terminalWidth: 140 })).toContain(
'OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED',
);
expect(renderMemoryFlowReplay(view(), { terminalWidth: 140 })).toMatchInlineSnapshot(`
"KLO memory flow warehouse/metricflow done
active: complete
Run run-1 Sync sync-1
OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED
SOURCE CHUNKS WORKUNITS ACTIONS GATES SAVED
2 raw files 2 chunks 2 WUs 2 candidates 1 conflict, 1 fallb 2 memories
sync sync-1 +1 ~1 -0 =3 1 done 1 wiki 1 failed 1 wiki
scope none 0 deletions 1 failed 1 SL 1 flagged 1 SL
Selected: SOURCE
- Trigger: manual_resync
- Adapter: metricflow
Saved 2 memories from 2 raw files: 1 wiki pages, 1 SL updates. Commit: abc12345 Run: run-1 Report: report-1
"
`);
});
it('renders a stacked narrow terminal snapshot', () => {
expect(renderMemoryFlowReplay(view(), { terminalWidth: 72 })).toContain(
'OK SOURCE -> OK CHUNKS -> !! WORKUNITS -> OK ACTIONS -> !! GATES -> OK SAVED',
);
expect(renderMemoryFlowReplay(view(), { terminalWidth: 72 })).toContain(`SOURCE
2 raw files
sync sync-1
scope none`);
expect(renderMemoryFlowReplay(view(), { terminalWidth: 72 })).toContain(`GATES
1 conflict, 1 fallback
1 failed
1 flagged`);
});
});

View file

@ -0,0 +1,99 @@
import type { MemoryFlowColumnView, MemoryFlowRenderOptions, MemoryFlowViewModel } from './types.js';
import { renderMemoryFlowConnectorLine } from './visuals.js';
const WIDE_COLUMN_WIDTH = 20;
function cell(value: string | undefined, width = WIDE_COLUMN_WIDTH): string {
const text = value ?? '';
const normalized = text.length > width ? text.slice(0, width - 1) : text;
return normalized.padEnd(width, ' ');
}
function row(values: string[]): string {
return values.map((value) => cell(value)).join(' ').trimEnd();
}
function counterAt(column: MemoryFlowColumnView, index: number): string {
return column.counters[index] ?? '';
}
function trustIssueLines(view: MemoryFlowViewModel): string[] {
if (view.trustIssues.length === 0) {
return [];
}
return [
'Trust issues',
...view.trustIssues.slice(0, 4).map((issue) => {
const label = issue.severity === 'failed' ? 'FAILED' : 'WARNING';
return `${label} ${issue.title}: ${issue.detail}`;
}),
...(view.trustIssues.length > 4 ? [`+${view.trustIssues.length - 4} more trust issues`] : []),
'',
];
}
function renderWide(view: MemoryFlowViewModel): string {
const lines = [
view.title,
view.activeLine,
view.subtitle,
renderMemoryFlowConnectorLine(view),
...trustIssueLines(view),
'',
row(view.columns.map((column) => column.title)),
row(view.columns.map((column) => column.headline)),
row(view.columns.map((column) => counterAt(column, 0))),
row(view.columns.map((column) => counterAt(column, 1))),
'',
`Selected: ${view.selectedTitle}`,
...view.selectedDetails.map((detail) => `- ${detail}`),
];
if (view.completionLine) {
lines.push('', view.completionLine);
}
lines.push('');
return lines.join('\n');
}
function renderNarrowColumn(column: MemoryFlowColumnView): string[] {
return [
column.title,
` ${column.headline}`,
...column.counters.slice(0, 3).map((counter) => ` ${counter}`),
];
}
function renderNarrow(view: MemoryFlowViewModel): string {
const lines = [
view.title,
view.activeLine,
view.subtitle,
renderMemoryFlowConnectorLine(view),
...trustIssueLines(view),
'',
...view.columns.flatMap((column, index) => [
...(index > 0 ? [''] : []),
...renderNarrowColumn(column),
]),
'',
`Selected: ${view.selectedTitle}`,
...view.selectedDetails.map((detail) => `- ${detail}`),
];
if (view.completionLine) {
lines.push('', view.completionLine);
}
lines.push('');
return lines.join('\n');
}
export function renderMemoryFlowReplay(view: MemoryFlowViewModel, options: MemoryFlowRenderOptions = {}): string {
if ((options.terminalWidth ?? 120) < 100) {
return renderNarrow(view);
}
return renderWide(view);
}

View file

@ -0,0 +1,164 @@
import { describe, expect, it } from 'vitest';
import {
memoryFlowReplayInputSchema,
memoryFlowStreamEventSchema,
parseMemoryFlowReplayInput,
} from './schema.js';
import type { MemoryFlowReplayInput } from './types.js';
function snapshot(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowReplayInput {
return {
runId: 'job-1',
connectionId: 'connection-1',
adapter: 'metabase',
status: 'running',
sourceDir: null,
syncId: 'sync-1',
errors: [],
events: [
{ type: 'source_acquired', adapter: 'metabase', trigger: 'manual_resync', fileCount: 2 },
{ type: 'scope_detected', fingerprint: 'scope-1' },
{ type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 2 },
{ type: 'diff_computed', added: 1, modified: 1, deleted: 0, unchanged: 0 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'work_unit_step', unitKey: 'orders', stepIndex: 1, stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/orders.md' },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
{ type: 'saved', commitSha: 'abc12345', wikiCount: 1, slCount: 0 },
{ type: 'provenance_recorded', rowCount: 1 },
{ type: 'report_created', runId: 'run-1', reportPath: 'ingest-report.json' },
],
plannedWorkUnits: [{ unitKey: 'orders', rawFiles: ['orders.md'], peerFileCount: 0, dependencyCount: 1 }],
details: {
actions: [
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/orders.md',
summary: 'Created orders page',
rawFiles: ['orders.md'],
status: 'success',
},
],
provenance: [
{
rawPath: 'orders.md',
artifactKind: 'wiki',
artifactKey: 'knowledge/orders.md',
actionType: 'wiki_written',
},
],
transcripts: [
{
unitKey: 'orders',
path: 'transcripts/orders.jsonl',
toolCallCount: 2,
errorCount: 0,
toolNames: ['wiki_write'],
},
],
},
...overrides,
};
}
describe('memory-flow schemas', () => {
it('parses a full replay input snapshot', () => {
expect(parseMemoryFlowReplayInput(snapshot())).toEqual(snapshot());
});
it('parses replay metadata and timestamped events', () => {
const parsed = parseMemoryFlowReplayInput(
snapshot({
metadata: {
schemaVersion: 1,
mode: 'full',
origin: 'captured',
timing: 'captured',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: 'report-1',
sourceReportPath: 'reports/report-1.json',
fallbackReason: null,
},
events: [
{
type: 'source_acquired',
adapter: 'metabase',
trigger: 'manual_resync',
fileCount: 2,
emittedAt: '2026-05-01T10:00:00.000Z',
},
],
}),
);
expect(parsed.metadata).toEqual({
schemaVersion: 1,
mode: 'full',
origin: 'captured',
timing: 'captured',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: 'report-1',
sourceReportPath: 'reports/report-1.json',
fallbackReason: null,
});
expect(parsed.events).toEqual([
{
type: 'source_acquired',
adapter: 'metabase',
trigger: 'manual_resync',
fileCount: 2,
emittedAt: '2026-05-01T10:00:00.000Z',
},
]);
});
it('parses skipped deterministic stages', () => {
const parsed = parseMemoryFlowReplayInput(
snapshot({
status: 'done',
events: [
{ type: 'source_acquired', adapter: 'live-database', trigger: 'demo_deterministic', fileCount: 7 },
{ type: 'scope_detected', fingerprint: 'sqlite' },
{ type: 'raw_snapshot_written', syncId: 'sync-demo', rawFileCount: 7 },
{ type: 'diff_computed', added: 7, modified: 0, deleted: 0, unchanged: 0 },
{ type: 'chunks_planned', chunkCount: 7, workUnitCount: 0, evictionCount: 0 },
{ type: 'stage_skipped', stage: 'workUnits', reason: 'deterministic mode' },
{ type: 'stage_skipped', stage: 'actions', reason: 'requires LLM' },
{ type: 'stage_skipped', stage: 'gates', reason: 'requires candidate actions' },
{ type: 'stage_skipped', stage: 'saved', reason: 'requires LLM memory synthesis' },
{ type: 'saved', commitSha: null, wikiCount: 0, slCount: 0 },
{ type: 'provenance_recorded', rowCount: 0 },
{
type: 'report_created',
runId: 'scan-demo',
reportPath: 'raw-sources/orbit_demo/live-database/sync-demo/scan-report.json',
},
],
}),
);
expect(parsed.events).toContainEqual({ type: 'stage_skipped', stage: 'workUnits', reason: 'deterministic mode' });
expect(parsed.events).toContainEqual({ type: 'stage_skipped', stage: 'actions', reason: 'requires LLM' });
});
it('parses snapshot and closed stream events', () => {
expect(memoryFlowStreamEventSchema.parse({ type: 'snapshot', snapshot: snapshot({ status: 'done' }) })).toEqual({
type: 'snapshot',
snapshot: snapshot({ status: 'done' }),
});
expect(memoryFlowStreamEventSchema.parse({ type: 'closed', status: 'done', errors: [] })).toEqual({
type: 'closed',
status: 'done',
errors: [],
});
});
it('rejects invalid replay status values', () => {
expect(() => memoryFlowReplayInputSchema.parse({ ...snapshot(), status: 'complete' })).toThrow();
});
});

View file

@ -0,0 +1,171 @@
import * as z from 'zod';
import type { MemoryFlowReplayInput } from './types.js';
export const memoryFlowRunStatusSchema = z.enum(['running', 'done', 'error']);
const memoryFlowEventTimestampShape = {
emittedAt: z.string().datetime().optional(),
};
function eventSchema<T extends z.ZodRawShape>(shape: T): z.ZodObject<T & typeof memoryFlowEventTimestampShape> {
return z.object({ ...shape, ...memoryFlowEventTimestampShape });
}
const memoryFlowReplayMetadataSchema = z.object({
schemaVersion: z.literal(1),
mode: z.enum(['full', 'deterministic', 'replay', 'seeded']),
origin: z.enum(['captured', 'packaged', 'synthetic-report']),
timing: z.enum(['captured', 'synthetic', 'not-captured', 'prebuilt']),
capturedAt: z.string().datetime().nullable(),
sourceReportId: z.string().min(1).nullable(),
sourceReportPath: z.string().min(1).nullable(),
fallbackReason: z.string().min(1).nullable(),
});
export const memoryFlowEventSchema = z.discriminatedUnion('type', [
eventSchema({
type: z.literal('source_acquired'),
adapter: z.string().min(1),
trigger: z.string().min(1),
fileCount: z.number().int().min(0),
}),
eventSchema({ type: z.literal('scope_detected'), fingerprint: z.string().nullable() }),
eventSchema({
type: z.literal('raw_snapshot_written'),
syncId: z.string().min(1),
rawFileCount: z.number().int().min(0),
}),
eventSchema({
type: z.literal('diff_computed'),
added: z.number().int().min(0),
modified: z.number().int().min(0),
deleted: z.number().int().min(0),
unchanged: z.number().int().min(0),
}),
eventSchema({
type: z.literal('chunks_planned'),
chunkCount: z.number().int().min(0),
workUnitCount: z.number().int().min(0),
evictionCount: z.number().int().min(0),
}),
eventSchema({
type: z.literal('stage_skipped'),
stage: z.enum(['source', 'chunks', 'workUnits', 'actions', 'gates', 'saved']),
reason: z.string().min(1),
}),
eventSchema({
type: z.literal('work_unit_started'),
unitKey: z.string().min(1),
skills: z.array(z.string().min(1)),
stepBudget: z.number().int().min(0),
}),
eventSchema({
type: z.literal('work_unit_step'),
unitKey: z.string().min(1),
stepIndex: z.number().int().min(0),
stepBudget: z.number().int().min(0),
}),
eventSchema({
type: z.literal('candidate_action'),
unitKey: z.string().min(1),
target: z.enum(['wiki', 'sl']),
action: z.string().min(1),
key: z.string().min(1),
}),
eventSchema({
type: z.literal('work_unit_finished'),
unitKey: z.string().min(1),
status: z.enum(['success', 'failed']),
reason: z.string().optional(),
}),
eventSchema({
type: z.literal('reconciliation_finished'),
conflictCount: z.number().int().min(0),
fallbackCount: z.number().int().min(0),
}),
eventSchema({
type: z.literal('saved'),
commitSha: z.string().nullable(),
wikiCount: z.number().int().min(0),
slCount: z.number().int().min(0),
}),
eventSchema({ type: z.literal('provenance_recorded'), rowCount: z.number().int().min(0) }),
eventSchema({
type: z.literal('report_created'),
runId: z.string().min(1),
reportPath: z.string().min(1).optional(),
}),
]);
export const memoryFlowPlannedWorkUnitSchema = z.object({
unitKey: z.string().min(1),
rawFiles: z.array(z.string()),
peerFileCount: z.number().int().min(0),
dependencyCount: z.number().int().min(0),
});
export const memoryFlowActionDetailSchema = z.object({
unitKey: z.string().min(1),
target: z.enum(['wiki', 'sl']),
action: z.enum(['created', 'updated', 'removed']),
key: z.string().min(1),
summary: z.string(),
rawFiles: z.array(z.string()),
status: z.enum(['success', 'failed']),
});
const memoryFlowProvenanceDetailSchema = z.object({
rawPath: z.string(),
artifactKind: z.enum(['sl', 'wiki']).nullable(),
artifactKey: z.string().nullable(),
actionType: z.string().min(1),
});
const memoryFlowTranscriptDetailSchema = z.object({
unitKey: z.string().min(1),
path: z.string().min(1),
toolCallCount: z.number().int().min(0),
errorCount: z.number().int().min(0),
toolNames: z.array(z.string()),
});
export const memoryFlowDetailSectionsSchema = z.object({
actions: z.array(memoryFlowActionDetailSchema),
provenance: z.array(memoryFlowProvenanceDetailSchema),
transcripts: z.array(memoryFlowTranscriptDetailSchema),
});
export const memoryFlowReplayInputSchema: z.ZodType<MemoryFlowReplayInput> = z.object({
metadata: memoryFlowReplayMetadataSchema.optional(),
runId: z.string().min(1),
connectionId: z.string().min(1),
adapter: z.string().min(1),
status: memoryFlowRunStatusSchema,
sourceDir: z.string().nullable(),
syncId: z.string().min(1),
reportId: z.string().min(1).optional(),
reportPath: z.string().min(1).optional(),
errors: z.array(z.string()),
events: z.array(memoryFlowEventSchema),
plannedWorkUnits: z.array(memoryFlowPlannedWorkUnitSchema),
details: memoryFlowDetailSectionsSchema,
});
export const memoryFlowStreamEventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('snapshot'), snapshot: memoryFlowReplayInputSchema }),
z.object({
type: z.literal('closed'),
status: memoryFlowRunStatusSchema,
errors: z.array(z.string()),
}),
]);
export type MemoryFlowStreamEvent = z.infer<typeof memoryFlowStreamEventSchema>;
export function parseMemoryFlowReplayInput(value: unknown): MemoryFlowReplayInput {
const result = memoryFlowReplayInputSchema.safeParse(value);
if (!result.success) {
throw new Error(`Invalid memory-flow replay input: ${z.prettifyError(result.error)}`);
}
return result.data;
}

View file

@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest';
import type { MemoryFlowReplayInput } from './types.js';
import { formatMemoryFlowFinalSummary } from './summary.js';
function input(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowReplayInput {
return {
runId: 'run-1',
connectionId: 'warehouse',
adapter: 'metricflow',
status: 'done',
sourceDir: '/tmp/source',
syncId: 'sync-1',
errors: [],
plannedWorkUnits: [{ unitKey: 'orders', rawFiles: ['orders.yml'], peerFileCount: 0, dependencyCount: 0 }],
details: { actions: [], provenance: [], transcripts: [] },
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 2 },
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
{ type: 'saved', commitSha: 'abc12345', wikiCount: 1, slCount: 1 },
{ type: 'provenance_recorded', rowCount: 2 },
{ type: 'report_created', runId: 'run-1', reportPath: 'report-1' },
],
...overrides,
};
}
describe('formatMemoryFlowFinalSummary', () => {
it('summarizes a successful full memory-flow run', () => {
expect(formatMemoryFlowFinalSummary(input())).toBe(
[
'Memory-flow summary: done',
'Connection: warehouse',
'Adapter: metricflow',
'Run: run-1',
'Sync: sync-1',
'Source files: 2',
'Table reviews: 1 total, 1 done, 0 failed',
'Saved memory: 1 wiki, 1 semantic layer',
'Provenance rows: 2',
'Report: report-1',
'',
].join('\n'),
);
});
it('includes trust issues and sanitized errors for failed runs', () => {
expect(
formatMemoryFlowFinalSummary(
input({
status: 'error',
errors: ['failed token=secret'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 2 },
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'failed', reason: 'validation failed token=secret' },
],
}),
),
).toContain('Trust issues: 3');
});
it('labels replay source metadata in final summaries', () => {
const summary = formatMemoryFlowFinalSummary({
metadata: {
schemaVersion: 1,
mode: 'replay',
origin: 'packaged',
timing: 'captured',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: 'demo-replay-report',
sourceReportPath: 'replays/replay.memory-flow.v1.json',
fallbackReason: null,
},
runId: 'demo-replay-orbit',
connectionId: 'orbit_demo',
adapter: 'live-database',
status: 'done',
sourceDir: null,
syncId: 'demo-replay-sync',
reportPath: 'replays/replay.memory-flow.v1.json',
errors: [],
events: [
{ type: 'source_acquired', adapter: 'live-database', trigger: 'demo_replay', fileCount: 7 },
{ type: 'saved', commitSha: null, wikiCount: 3, slCount: 2 },
{ type: 'provenance_recorded', rowCount: 5 },
{ type: 'report_created', runId: 'demo-replay-orbit', reportPath: 'replays/replay.memory-flow.v1.json' },
],
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
});
expect(summary).toContain('Replay source: packaged replay (captured timing)');
expect(summary).toContain('Replay captured: 2026-05-01T10:00:03.000Z');
});
it('labels synthetic report replays with the reconstruction reason', () => {
const summary = formatMemoryFlowFinalSummary({
metadata: {
schemaVersion: 1,
mode: 'full',
origin: 'synthetic-report',
timing: 'synthetic',
capturedAt: '2026-05-01T10:00:03.000Z',
sourceReportId: 'report-1',
sourceReportPath: 'report-1',
fallbackReason: 'report did not include captured memory-flow events',
},
runId: 'run-1',
connectionId: 'warehouse',
adapter: 'lookml',
status: 'done',
sourceDir: null,
syncId: 'sync-1',
reportPath: 'report-1',
errors: [],
events: [{ type: 'report_created', runId: 'run-1', reportPath: 'report-1' }],
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
});
expect(summary).toContain('Replay source: synthetic report replay (synthetic timing)');
expect(summary).toContain('Replay note: report did not include captured memory-flow events');
});
});

View file

@ -0,0 +1,93 @@
import { sanitizeMemoryFlowError } from './live-buffer.js';
import type { MemoryFlowEvent, MemoryFlowReplayInput } from './types.js';
import { buildMemoryFlowViewModel } from './view-model.js';
function latest<T extends MemoryFlowEvent['type']>(
events: MemoryFlowEvent[],
type: T,
): Extract<MemoryFlowEvent, { type: T }> | undefined {
return events.filter((event): event is Extract<MemoryFlowEvent, { type: T }> => event.type === type).at(-1);
}
function eventsOf<T extends MemoryFlowEvent['type']>(
events: MemoryFlowEvent[],
type: T,
): Array<Extract<MemoryFlowEvent, { type: T }>> {
return events.filter((event): event is Extract<MemoryFlowEvent, { type: T }> => event.type === type);
}
function replaySourceLine(input: MemoryFlowReplayInput): string | null {
const metadata = input.metadata;
if (!metadata) {
return null;
}
const origin =
metadata.origin === 'synthetic-report'
? 'synthetic report replay'
: metadata.origin === 'packaged'
? 'packaged replay'
: 'captured replay';
return `Replay source: ${origin} (${metadata.timing} timing)`;
}
function humanizeSummaryText(value: string): string {
return value
.replace(/\bWORKUNITS\b/g, 'PLAN')
.replace(/\bWorkUnit\b/g, 'Table review')
.replace(/\bwork units\b/gi, 'table reviews')
.replace(/\bWUs\b/g, 'tables')
.replace(/\braw files\b/gi, 'database files')
.replace(/\braw file\b/gi, 'database file')
.replace(/\bSL\b/g, 'semantic layer');
}
export function formatMemoryFlowFinalSummary(input: MemoryFlowReplayInput): string {
const sources = eventsOf(input.events, 'source_acquired');
const source = sources.at(-1);
const totalFiles = sources.reduce((sum, s) => sum + s.fileCount, 0);
const saved = latest(input.events, 'saved');
const provenance = latest(input.events, 'provenance_recorded');
const report = latest(input.events, 'report_created');
const finished = eventsOf(input.events, 'work_unit_finished');
const failed = finished.filter((event) => event.status === 'failed');
const view = buildMemoryFlowViewModel(input);
const lines = [
`Memory-flow summary: ${input.status}`,
`Connection: ${input.connectionId}`,
...(sources.length > 1
? [`Sources: ${[...new Set(sources.map((s) => s.adapter))].join(', ')}`]
: [`Adapter: ${input.adapter}`]),
`Run: ${input.runId}`,
`Sync: ${input.syncId}`,
`Source files: ${totalFiles}`,
`Table reviews: ${input.plannedWorkUnits.length || finished.length} total, ${finished.length - failed.length} done, ${failed.length} failed`,
`Saved memory: ${saved?.wikiCount ?? 0} wiki, ${saved?.slCount ?? 0} semantic layer`,
`Provenance rows: ${provenance?.rowCount ?? 0}`,
`Report: ${report?.reportPath ?? input.reportPath ?? 'none'}`,
];
const sourceLine = replaySourceLine(input);
if (sourceLine) {
lines.push(sourceLine);
}
if (input.metadata?.capturedAt) {
lines.push(`Replay captured: ${input.metadata.capturedAt}`);
}
if (input.metadata?.fallbackReason) {
lines.push(`Replay note: ${input.metadata.fallbackReason}`);
}
if (view.trustIssues.length > 0) {
lines.push(`Trust issues: ${view.trustIssues.length}`);
for (const issue of view.trustIssues.slice(0, 3)) {
lines.push(`- ${humanizeSummaryText(issue.title)}: ${humanizeSummaryText(issue.detail)}`);
}
}
for (const error of input.errors.slice(0, 3)) {
lines.push(`Error: ${sanitizeMemoryFlowError(error)}`);
}
lines.push('');
return lines.join('\n');
}

View file

@ -0,0 +1,246 @@
type MemoryFlowReplayMode = 'full' | 'deterministic' | 'replay' | 'seeded';
type MemoryFlowReplayOrigin = 'captured' | 'packaged' | 'synthetic-report';
type MemoryFlowReplayTiming = 'captured' | 'synthetic' | 'not-captured' | 'prebuilt';
interface MemoryFlowReplayMetadata {
schemaVersion: 1;
mode: MemoryFlowReplayMode;
origin: MemoryFlowReplayOrigin;
timing: MemoryFlowReplayTiming;
capturedAt: string | null;
sourceReportId: string | null;
sourceReportPath: string | null;
fallbackReason: string | null;
}
type MemoryFlowEventPayload =
| {
type: 'source_acquired';
adapter: string;
trigger: string;
fileCount: number;
}
| { type: 'scope_detected'; fingerprint: string | null }
| {
type: 'raw_snapshot_written';
syncId: string;
rawFileCount: number;
}
| {
type: 'diff_computed';
added: number;
modified: number;
deleted: number;
unchanged: number;
}
| {
type: 'chunks_planned';
chunkCount: number;
workUnitCount: number;
evictionCount: number;
}
| {
type: 'stage_skipped';
stage: MemoryFlowColumnId;
reason: string;
}
| {
type: 'work_unit_started';
unitKey: string;
skills: string[];
stepBudget: number;
}
| {
type: 'work_unit_step';
unitKey: string;
stepIndex: number;
stepBudget: number;
}
| {
type: 'candidate_action';
unitKey: string;
target: 'wiki' | 'sl';
action: string;
key: string;
}
| {
type: 'work_unit_finished';
unitKey: string;
status: 'success' | 'failed';
reason?: string;
}
| {
type: 'reconciliation_finished';
conflictCount: number;
fallbackCount: number;
}
| {
type: 'saved';
commitSha: string | null;
wikiCount: number;
slCount: number;
}
| { type: 'provenance_recorded'; rowCount: number }
| { type: 'report_created'; runId: string; reportPath?: string };
export type MemoryFlowEvent = MemoryFlowEventPayload & { emittedAt?: string };
export type MemoryFlowRunStatus = 'running' | 'done' | 'error';
export interface MemoryFlowPlannedWorkUnit {
unitKey: string;
rawFiles: string[];
peerFileCount: number;
dependencyCount: number;
}
export interface MemoryFlowActionDetail {
unitKey: string;
target: 'wiki' | 'sl';
action: 'created' | 'updated' | 'removed';
key: string;
summary: string;
rawFiles: string[];
status: 'success' | 'failed';
}
interface MemoryFlowProvenanceDetail {
rawPath: string;
artifactKind: 'sl' | 'wiki' | null;
artifactKey: string | null;
actionType: string;
}
interface MemoryFlowTranscriptDetail {
unitKey: string;
path: string;
toolCallCount: number;
errorCount: number;
toolNames: string[];
}
export interface MemoryFlowDetailSections {
actions: MemoryFlowActionDetail[];
provenance: MemoryFlowProvenanceDetail[];
transcripts: MemoryFlowTranscriptDetail[];
}
export interface MemoryFlowReplayInput {
metadata?: MemoryFlowReplayMetadata;
runId: string;
connectionId: string;
adapter: string;
status: MemoryFlowRunStatus;
sourceDir: string | null;
syncId: string;
reportId?: string;
reportPath?: string;
errors: string[];
events: MemoryFlowEvent[];
plannedWorkUnits: MemoryFlowPlannedWorkUnit[];
details: MemoryFlowDetailSections;
}
export type MemoryFlowReplayPatch = Partial<Omit<MemoryFlowReplayInput, 'events'>>;
export interface MemoryFlowEventSink {
emit(event: MemoryFlowEvent): void;
update(patch: MemoryFlowReplayPatch): void;
finish(status: MemoryFlowRunStatus, errors?: string[]): void;
snapshot(): MemoryFlowReplayInput;
}
export interface MemoryFlowLiveBufferOptions {
onChange?(snapshot: MemoryFlowReplayInput): void;
now?: () => Date;
}
export type MemoryFlowColumnId = 'source' | 'chunks' | 'workUnits' | 'actions' | 'gates' | 'saved';
export type MemoryFlowDisplayStatus = 'waiting' | 'active' | 'complete' | 'warning' | 'failed';
export interface MemoryFlowChip {
label: string;
status: MemoryFlowDisplayStatus;
detail?: string;
}
export interface MemoryFlowColumnView {
id: MemoryFlowColumnId;
title: string;
status: MemoryFlowDisplayStatus;
headline: string;
counters: string[];
chips: MemoryFlowChip[];
details: string[];
}
export interface MemoryFlowTrustIssue {
id: string;
severity: 'warning' | 'failed';
title: string;
detail: string;
columnId: MemoryFlowColumnId;
targetLabel?: string;
}
export interface MemoryFlowSearchMatch {
columnId: MemoryFlowColumnId;
chipIndex?: number;
label: string;
detail: string;
}
export interface MemoryFlowViewModel {
title: string;
subtitle: string;
status: MemoryFlowRunStatus;
activeLine: string;
columns: MemoryFlowColumnView[];
trustIssues: MemoryFlowTrustIssue[];
selectedTitle: string;
selectedDetails: string[];
completionLine: string | null;
details: MemoryFlowDetailSections;
}
export interface MemoryFlowRenderOptions {
terminalWidth?: number;
}
export type MemoryFlowPaneId = 'overview' | 'trust' | 'details' | 'log' | 'provenance' | 'transcript';
export type MemoryFlowFilterMode = 'all' | 'failed_or_flagged';
interface MemoryFlowSearchState {
editing: boolean;
query: string;
matchIndex: number;
}
export interface MemoryFlowInteractionState {
selectedColumnId: MemoryFlowColumnId;
selectedChipIndex: number;
expanded: boolean;
pane: MemoryFlowPaneId;
filter: MemoryFlowFilterMode;
search: MemoryFlowSearchState;
shouldQuit: boolean;
}
export type MemoryFlowInteractionCommand =
| 'left'
| 'right'
| 'up'
| 'down'
| 'enter'
| 'tab'
| 'filter'
| 'provenance'
| 'transcript'
| 'search-start'
| 'search-submit'
| 'search-backspace'
| 'search-clear'
| 'search-next'
| 'search-previous'
| 'quit'
| { type: 'search-input'; value: string };

View file

@ -0,0 +1,436 @@
import { describe, expect, it } from 'vitest';
import type { MemoryFlowReplayInput } from './types.js';
import { buildMemoryFlowViewModel } from './view-model.js';
function replayInput(): MemoryFlowReplayInput {
return {
runId: 'run-1',
connectionId: 'warehouse',
adapter: 'metricflow',
status: 'done',
sourceDir: '/tmp/source',
syncId: 'sync-1',
errors: [],
plannedWorkUnits: [
{ unitKey: 'orders', rawFiles: ['orders.yml'], peerFileCount: 1, dependencyCount: 1 },
{ unitKey: 'revenue', rawFiles: ['revenue.yml'], peerFileCount: 0, dependencyCount: 0 },
],
details: {
actions: [
{
unitKey: 'orders',
target: 'wiki',
action: 'created',
key: 'knowledge/orders.md',
summary: 'order facts',
rawFiles: ['orders.yml'],
status: 'success',
},
{
unitKey: 'orders',
target: 'sl',
action: 'updated',
key: 'warehouse.orders',
summary: 'order measures',
rawFiles: ['orders.yml'],
status: 'success',
},
],
provenance: [
{
rawPath: 'orders.yml',
artifactKind: 'wiki',
artifactKey: 'knowledge/orders.md',
actionType: 'wiki_written',
},
],
transcripts: [
{
unitKey: 'orders',
path: '/tmp/transcripts/orders.jsonl',
toolCallCount: 3,
errorCount: 0,
toolNames: ['read_raw_span', 'wiki_write', 'sl_write_source'],
},
],
},
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 2 },
{ type: 'scope_detected', fingerprint: 'scope-abc' },
{ type: 'raw_snapshot_written', syncId: 'sync-1', rawFileCount: 2 },
{ type: 'diff_computed', added: 1, modified: 1, deleted: 0, unchanged: 3 },
{ type: 'chunks_planned', chunkCount: 2, workUnitCount: 2, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'orders', target: 'wiki', action: 'created', key: 'knowledge/orders.md' },
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
{ type: 'work_unit_finished', unitKey: 'revenue', status: 'failed', reason: 'validation failed' },
{ type: 'reconciliation_finished', conflictCount: 1, fallbackCount: 1 },
{ type: 'saved', commitSha: 'abc123456789', wikiCount: 1, slCount: 1 }, // pragma: allowlist secret
{ type: 'provenance_recorded', rowCount: 3 },
{ type: 'report_created', runId: 'run-1', reportPath: 'report-1' },
],
};
}
function baseReplayInput(overrides: Partial<MemoryFlowReplayInput> = {}): MemoryFlowReplayInput {
return {
runId: 'run-errors',
connectionId: 'warehouse',
adapter: 'metricflow',
status: 'error',
sourceDir: '/tmp/source',
syncId: 'sync-errors',
errors: [],
events: [],
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
...overrides,
};
}
describe('buildMemoryFlowViewModel', () => {
it('builds six readable columns from replay events', () => {
const view = buildMemoryFlowViewModel(replayInput());
expect(view.title).toBe('KLO memory flow warehouse/metricflow done');
expect(view.activeLine).toBe('active: complete');
expect(view.columns.map((column) => column.id)).toEqual([
'source',
'chunks',
'workUnits',
'actions',
'gates',
'saved',
]);
expect(view.columns.map((column) => column.headline)).toEqual([
'2 raw files',
'2 chunks',
'2 WUs',
'2 candidates',
'1 conflict, 1 fallback',
'2 memories',
]);
expect(view.columns.find((column) => column.id === 'workUnits')?.counters).toEqual([
'1 done',
'1 failed',
'0 active',
]);
expect(view.columns.find((column) => column.id === 'actions')?.counters).toEqual(['1 wiki', '1 SL']);
expect(view.details.actions).toHaveLength(2);
expect(view.details.provenance).toEqual([
{
rawPath: 'orders.yml',
artifactKind: 'wiki',
artifactKey: 'knowledge/orders.md',
actionType: 'wiki_written',
},
]);
expect(view.details.transcripts).toEqual([
{
unitKey: 'orders',
path: '/tmp/transcripts/orders.jsonl',
toolCallCount: 3,
errorCount: 0,
toolNames: ['read_raw_span', 'wiki_write', 'sl_write_source'],
},
]);
expect(view.columns.find((column) => column.id === 'actions')?.details).toContain(
'orders wiki created knowledge/orders.md: order facts',
);
expect(view.columns.find((column) => column.id === 'saved')?.details).toContain('Commit: abc12345');
expect(view.completionLine).toBe(
'Saved 2 memories from 2 raw files: 1 wiki pages, 1 SL updates. Commit: abc12345 Run: run-1 Report: report-1',
);
});
it('shows all seeded demo source families and sums raw files in the completion line', () => {
const view = buildMemoryFlowViewModel({
runId: 'demo-seeded-orbit',
connectionId: 'orbit_demo',
adapter: 'live-database',
status: 'done',
sourceDir: null,
syncId: 'demo-seeded-sync',
errors: [],
events: [
{ type: 'source_acquired', adapter: 'live-database', trigger: 'demo_seeded', fileCount: 8 },
{ type: 'source_acquired', adapter: 'dbt_descriptions', trigger: 'demo_seeded', fileCount: 6 },
{ type: 'source_acquired', adapter: 'looker', trigger: 'demo_seeded', fileCount: 7 },
{ type: 'source_acquired', adapter: 'notion', trigger: 'demo_seeded', fileCount: 8 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'revenue-and-contracts', skills: ['knowledge_capture'], stepBudget: 40 },
{
type: 'candidate_action',
unitKey: 'revenue-and-contracts',
target: 'wiki',
action: 'created',
key: 'knowledge/global/arr-contract-first.md',
},
{ type: 'work_unit_finished', unitKey: 'revenue-and-contracts', status: 'success' },
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
{ type: 'saved', commitSha: 'demo-seeded', wikiCount: 10, slCount: 6 },
{ type: 'provenance_recorded', rowCount: 23 },
{ type: 'report_created', runId: 'demo-seeded-orbit', reportPath: 'reports/seeded-demo-report.json' },
],
plannedWorkUnits: [
{ unitKey: 'revenue-and-contracts', rawFiles: ['contracts'], peerFileCount: 1, dependencyCount: 1 },
],
details: { actions: [], provenance: [], transcripts: [] },
});
expect(view.title).toBe('KLO memory flow Warehouse + dbt + BI + Docs done');
expect(view.columns.find((column) => column.id === 'source')?.counters[0]).toBe('Warehouse, dbt, BI, Docs');
expect(view.completionLine).toContain('Saved 16 memories from 29 raw files');
});
it('derives sticky trust issues from failed work units, gates, and provenance mismatch', () => {
const input = replayInput();
const view = buildMemoryFlowViewModel({
...input,
events: [
...input.events.filter((event) => event.type !== 'provenance_recorded'),
{ type: 'provenance_recorded', rowCount: 1 },
],
});
expect(view.trustIssues).toEqual([
{
id: 'work-unit-failed:revenue',
severity: 'failed',
title: 'WorkUnit failed',
detail: 'revenue failed: validation failed',
columnId: 'workUnits',
targetLabel: 'revenue',
},
{
id: 'sl-validation-reverted:revenue',
severity: 'warning',
title: 'SL validation revert',
detail: 'revenue reverted after semantic-layer validation failure',
columnId: 'gates',
targetLabel: 'revenue',
},
{
id: 'reconciliation-conflicts',
severity: 'warning',
title: 'Reconciliation conflicts',
detail: '1 conflict resolved during reconciliation',
columnId: 'gates',
},
{
id: 'flagged-fallbacks',
severity: 'warning',
title: 'Flagged fallbacks',
detail: '1 fallback needs review',
columnId: 'gates',
},
{
id: 'provenance-mismatch',
severity: 'warning',
title: 'Provenance mismatch',
detail: '2 saved memories but 1 provenance rows recorded',
columnId: 'saved',
},
]);
expect(view.columns.find((column) => column.id === 'workUnits')?.chips).toContainEqual({
label: 'revenue',
status: 'failed',
detail: 'validation failed',
});
});
it('accepts multiple provenance rows per saved memory', () => {
const input = replayInput();
const view = buildMemoryFlowViewModel({
...input,
events: [
...input.events.filter((event) => event.type !== 'provenance_recorded'),
{ type: 'provenance_recorded', rowCount: 23 },
],
});
expect(view.trustIssues.find((issue) => issue.id === 'provenance-mismatch')).toBeUndefined();
});
it('derives deterministic mode as a degraded trust issue', () => {
const view = buildMemoryFlowViewModel({
runId: 'demo-deterministic-scan',
connectionId: 'orbit_demo',
adapter: 'live-database',
status: 'done',
sourceDir: 'raw-sources/orbit_demo/live-database/sync-demo',
syncId: 'sync-demo',
reportPath: 'raw-sources/orbit_demo/live-database/sync-demo/scan-report.json',
errors: [],
plannedWorkUnits: [],
details: { actions: [], provenance: [], transcripts: [] },
events: [
{ type: 'source_acquired', adapter: 'live-database', trigger: 'demo_deterministic', fileCount: 7 },
{ type: 'chunks_planned', chunkCount: 7, workUnitCount: 0, evictionCount: 0 },
{ type: 'stage_skipped', stage: 'workUnits', reason: 'deterministic mode' },
{ type: 'stage_skipped', stage: 'actions', reason: 'requires LLM' },
{ type: 'stage_skipped', stage: 'gates', reason: 'requires candidate actions' },
{ type: 'stage_skipped', stage: 'saved', reason: 'requires LLM memory synthesis' },
],
});
expect(view.trustIssues).toEqual([
{
id: 'degraded-mode:workUnits',
severity: 'warning',
title: 'Degraded mode',
detail: 'WORKUNITS skipped: deterministic mode',
columnId: 'workUnits',
targetLabel: 'skipped',
},
{
id: 'degraded-mode:actions',
severity: 'warning',
title: 'Degraded mode',
detail: 'ACTIONS skipped: requires LLM',
columnId: 'actions',
targetLabel: 'skipped',
},
{
id: 'degraded-mode:gates',
severity: 'warning',
title: 'Degraded mode',
detail: 'GATES skipped: requires candidate actions',
columnId: 'gates',
targetLabel: 'skipped',
},
{
id: 'degraded-mode:saved',
severity: 'warning',
title: 'Degraded mode',
detail: 'SAVED skipped: requires LLM memory synthesis',
columnId: 'saved',
targetLabel: 'skipped',
},
]);
});
it('keeps local planning-only runs honest about unsaved memory', () => {
const view = buildMemoryFlowViewModel({
runId: 'local-run-1',
connectionId: 'warehouse',
adapter: 'fake',
status: 'done',
sourceDir: '/tmp/source',
syncId: 'sync-local',
errors: [],
plannedWorkUnits: [{ unitKey: 'orders', rawFiles: ['orders.json'], peerFileCount: 0, dependencyCount: 0 }],
details: { actions: [], provenance: [], transcripts: [] },
events: [
{ type: 'source_acquired', adapter: 'fake', trigger: 'manual_resync', fileCount: 1 },
{ type: 'scope_detected', fingerprint: null },
{ type: 'raw_snapshot_written', syncId: 'sync-local', rawFileCount: 1 },
{ type: 'diff_computed', added: 1, modified: 0, deleted: 0, unchanged: 0 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'report_created', runId: 'local-run-1' },
],
});
expect(view.columns.find((column) => column.id === 'actions')?.headline).toBe('0 candidates');
expect(view.columns.find((column) => column.id === 'gates')?.headline).toBe('not run');
expect(view.columns.find((column) => column.id === 'saved')?.headline).toBe('not saved');
expect(view.completionLine).toBe(null);
});
it('surfaces a sanitized source acquisition error when no source event exists', () => {
const view = buildMemoryFlowViewModel(
baseReplayInput({
errors: ['failed to read https://example.com/source?token=abc123 password=hunter2'],
}),
);
expect(view.activeLine).toBe('active: source failed - failed to read https://[redacted] password=[redacted]');
expect(view.selectedTitle).toBe('SOURCE');
expect(view.selectedDetails).toContain('Source acquisition failed: failed to read https://[redacted] password=[redacted]');
});
it('surfaces a sanitized planning error after source acquisition but before chunks', () => {
const view = buildMemoryFlowViewModel(
baseReplayInput({
errors: ['adapter detection failed api_key=abc123'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 3 },
{ type: 'raw_snapshot_written', syncId: 'sync-errors', rawFileCount: 3 },
],
}),
);
expect(view.activeLine).toBe('active: planning failed - adapter detection failed api_key=[redacted]');
const source = view.columns.find((column) => column.id === 'source');
expect(source?.details).toContain('Error: adapter detection failed api_key=[redacted]');
});
it('labels failed semantic-layer WorkUnits as reverted in gates details', () => {
const view = buildMemoryFlowViewModel(
baseReplayInput({
status: 'error',
errors: ['semantic-layer validation failed for warehouse.orders'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 2 },
{ type: 'raw_snapshot_written', syncId: 'sync-errors', rawFileCount: 2 },
{ type: 'diff_computed', added: 2, modified: 0, deleted: 0, unchanged: 0 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'orders', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'candidate_action', unitKey: 'orders', target: 'sl', action: 'updated', key: 'warehouse.orders' },
{
type: 'work_unit_finished',
unitKey: 'orders',
status: 'failed',
reason: 'semantic-layer validation failed for warehouse.orders',
},
],
plannedWorkUnits: [{ unitKey: 'orders', rawFiles: ['orders.yml'], peerFileCount: 0, dependencyCount: 0 }],
}),
);
const gates = view.columns.find((column) => column.id === 'gates');
expect(gates?.details).toContain('orders reverted: semantic-layer validation failed for warehouse.orders');
expect(gates?.details).toContain('Invalid semantic-layer writes were not saved.');
});
it('keeps non-validation WorkUnit failures actionable', () => {
const view = buildMemoryFlowViewModel(
baseReplayInput({
status: 'error',
errors: ['agent step budget exhausted'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 1 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_started', unitKey: 'docs', skills: ['knowledge_capture'], stepBudget: 40 },
{ type: 'work_unit_finished', unitKey: 'docs', status: 'failed', reason: 'agent step budget exhausted' },
],
plannedWorkUnits: [{ unitKey: 'docs', rawFiles: ['docs.md'], peerFileCount: 0, dependencyCount: 0 }],
}),
);
const gates = view.columns.find((column) => column.id === 'gates');
expect(gates?.details).toContain('docs failed: agent step budget exhausted');
});
it('shows whether durable memory landed before a post-save failure', () => {
const view = buildMemoryFlowViewModel(
baseReplayInput({
status: 'error',
errors: ['index refresh failed token=abc123'],
events: [
{ type: 'source_acquired', adapter: 'metricflow', trigger: 'manual_resync', fileCount: 2 },
{ type: 'chunks_planned', chunkCount: 1, workUnitCount: 1, evictionCount: 0 },
{ type: 'work_unit_finished', unitKey: 'orders', status: 'success' },
{ type: 'reconciliation_finished', conflictCount: 0, fallbackCount: 0 },
{ type: 'saved', commitSha: 'abc123456789', wikiCount: 1, slCount: 1 }, // pragma: allowlist secret
],
}),
);
const saved = view.columns.find((column) => column.id === 'saved');
expect(saved?.details).toContain('Durable memory landed before failure.');
expect(saved?.details).toContain('Post-save error: index refresh failed token=[redacted]');
expect(view.activeLine).toBe('active: save failed - index refresh failed token=[redacted]');
});
});

View file

@ -0,0 +1,523 @@
import type {
MemoryFlowChip,
MemoryFlowColumnId,
MemoryFlowColumnView,
MemoryFlowDisplayStatus,
MemoryFlowEvent,
MemoryFlowReplayInput,
MemoryFlowTrustIssue,
MemoryFlowViewModel,
} from './types.js';
import { sanitizeMemoryFlowError } from './live-buffer.js';
function latest<T extends MemoryFlowEvent['type']>(
events: MemoryFlowEvent[],
type: T,
): Extract<MemoryFlowEvent, { type: T }> | undefined {
return events.filter((event): event is Extract<MemoryFlowEvent, { type: T }> => event.type === type).at(-1);
}
function eventsOf<T extends MemoryFlowEvent['type']>(
events: MemoryFlowEvent[],
type: T,
): Array<Extract<MemoryFlowEvent, { type: T }>> {
return events.filter((event): event is Extract<MemoryFlowEvent, { type: T }> => event.type === type);
}
function skippedStage(
input: MemoryFlowReplayInput,
stage: Extract<MemoryFlowEvent, { type: 'stage_skipped' }>['stage'],
): Extract<MemoryFlowEvent, { type: 'stage_skipped' }> | undefined {
return eventsOf(input.events, 'stage_skipped').find((event) => event.stage === stage);
}
function formatDiff(diff: Extract<MemoryFlowEvent, { type: 'diff_computed' }> | undefined): string {
if (!diff) return '+0 ~0 -0 =0';
return `+${diff.added} ~${diff.modified} -${diff.deleted} =${diff.unchanged}`;
}
function countCandidateActions(events: MemoryFlowEvent[], target: 'wiki' | 'sl'): number {
return eventsOf(events, 'candidate_action').filter((event) => event.target === target).length;
}
function columnStatus(input: {
hasFailures?: boolean;
hasWarnings?: boolean;
hasActivity?: boolean;
complete?: boolean;
}): MemoryFlowDisplayStatus {
if (input.hasFailures) return 'failed';
if (input.hasWarnings) return 'warning';
if (input.hasActivity) return 'active';
if (input.complete) return 'complete';
return 'waiting';
}
function firstChips(labels: string[], status: MemoryFlowDisplayStatus): Array<{ label: string; status: MemoryFlowDisplayStatus }> {
return labels.slice(0, 2).map((label) => ({ label, status }));
}
function safeErrors(input: MemoryFlowReplayInput): string[] {
return input.errors.map((error) => sanitizeMemoryFlowError(error)).filter((error) => error.length > 0);
}
function latestSafeError(input: MemoryFlowReplayInput): string | null {
return safeErrors(input)[0] ?? null;
}
function failureStage(input: MemoryFlowReplayInput): 'source' | 'planning' | 'work_unit' | 'save' | 'run' {
const hasSource = !!latest(input.events, 'source_acquired');
const hasChunks = !!latest(input.events, 'chunks_planned');
const hasFailedWorkUnit = eventsOf(input.events, 'work_unit_finished').some((event) => event.status === 'failed');
const hasSaved = !!latest(input.events, 'saved');
if (!hasSource) return 'source';
if (!hasChunks) return 'planning';
if (hasFailedWorkUnit) return 'work_unit';
if (hasSaved) return 'save';
return 'run';
}
function activeLine(input: MemoryFlowReplayInput): string {
if (input.status !== 'error') {
return input.status === 'running' ? 'active: running' : 'active: complete';
}
const error = latestSafeError(input);
if (!error) return 'active: error';
const stage = failureStage(input);
return `active: ${stage.replace('_', ' ')} failed - ${error}`;
}
function errorDetails(input: MemoryFlowReplayInput): string[] {
const errors = safeErrors(input);
if (errors.length === 0) return [];
const [first, ...rest] = errors;
const stage = failureStage(input);
const label =
stage === 'source'
? 'Source acquisition failed'
: stage === 'planning'
? 'Error'
: stage === 'save'
? 'Post-save error'
: 'Error';
return [`${label}: ${first}`, ...rest.map((error) => `Error: ${error}`)];
}
function isValidationFailure(reason: string | undefined): boolean {
return /semantic-layer|validation|invalid/i.test(reason ?? '');
}
function failedWorkUnitDetails(failed: Array<Extract<MemoryFlowEvent, { type: 'work_unit_finished' }>>): string[] {
const details = failed.map((event) => {
const reason = event.reason ?? 'failed';
const label = isValidationFailure(reason) ? 'reverted' : 'failed';
return `${event.unitKey} ${label}: ${sanitizeMemoryFlowError(reason)}`;
});
if (failed.some((event) => isValidationFailure(event.reason))) {
details.push('Invalid semantic-layer writes were not saved.');
}
return details;
}
function columnTitle(columnId: MemoryFlowColumnId): string {
if (columnId === 'workUnits') return 'WORKUNITS';
return columnId.toUpperCase();
}
function plural(value: number, singular: string, pluralLabel = `${singular}s`): string {
return `${value} ${value === 1 ? singular : pluralLabel}`;
}
function finishedWorkUnitByKey(
input: MemoryFlowReplayInput,
): Map<string, Extract<MemoryFlowEvent, { type: 'work_unit_finished' }>> {
return new Map(eventsOf(input.events, 'work_unit_finished').map((event) => [event.unitKey, event]));
}
function workUnitChips(input: MemoryFlowReplayInput): MemoryFlowChip[] {
const finishedByKey = finishedWorkUnitByKey(input);
return input.plannedWorkUnits.slice(0, 8).map((workUnit) => {
const finished = finishedByKey.get(workUnit.unitKey);
if (finished?.status === 'failed') {
return {
label: workUnit.unitKey,
status: 'failed',
detail: sanitizeMemoryFlowError(finished.reason ?? 'failed'),
};
}
return { label: workUnit.unitKey, status: finished ? 'complete' : 'active' };
});
}
function actionChips(
input: MemoryFlowReplayInput,
events: Array<Extract<MemoryFlowEvent, { type: 'candidate_action' }>>,
): MemoryFlowChip[] {
if (input.details.actions.length > 0) {
return input.details.actions.slice(0, 8).map((action) => ({
label: action.key,
status: action.status === 'failed' ? 'failed' : 'complete',
detail: action.status === 'failed' ? action.summary : undefined,
}));
}
return events.slice(0, 8).map((action) => ({ label: action.key, status: 'complete' }));
}
function buildMemoryFlowTrustIssues(input: MemoryFlowReplayInput): MemoryFlowTrustIssue[] {
const issues: MemoryFlowTrustIssue[] = [];
const failed = eventsOf(input.events, 'work_unit_finished').filter((event) => event.status === 'failed');
const reconciliation = latest(input.events, 'reconciliation_finished');
const saved = latest(input.events, 'saved');
const provenance = latest(input.events, 'provenance_recorded');
for (const event of failed) {
const reason = sanitizeMemoryFlowError(event.reason ?? 'failed');
issues.push({
id: `work-unit-failed:${event.unitKey}`,
severity: 'failed',
title: 'WorkUnit failed',
detail: `${event.unitKey} failed: ${reason}`,
columnId: 'workUnits',
targetLabel: event.unitKey,
});
if (isValidationFailure(event.reason)) {
issues.push({
id: `sl-validation-reverted:${event.unitKey}`,
severity: 'warning',
title: 'SL validation revert',
detail: `${event.unitKey} reverted after semantic-layer validation failure`,
columnId: 'gates',
targetLabel: event.unitKey,
});
}
}
if ((reconciliation?.conflictCount ?? 0) > 0) {
issues.push({
id: 'reconciliation-conflicts',
severity: 'warning',
title: 'Reconciliation conflicts',
detail: `${plural(reconciliation?.conflictCount ?? 0, 'conflict')} resolved during reconciliation`,
columnId: 'gates',
});
}
if ((reconciliation?.fallbackCount ?? 0) > 0) {
issues.push({
id: 'flagged-fallbacks',
severity: 'warning',
title: 'Flagged fallbacks',
detail: `${plural(reconciliation?.fallbackCount ?? 0, 'fallback')} needs review`,
columnId: 'gates',
});
}
const savedCount = (saved?.wikiCount ?? 0) + (saved?.slCount ?? 0);
if (savedCount > 0 && provenance && provenance.rowCount < savedCount) {
issues.push({
id: 'provenance-mismatch',
severity: 'warning',
title: 'Provenance mismatch',
detail: `${savedCount} saved memories but ${provenance.rowCount} provenance rows recorded`,
columnId: 'saved',
});
}
for (const skipped of eventsOf(input.events, 'stage_skipped')) {
issues.push({
id: `degraded-mode:${skipped.stage}`,
severity: 'warning',
title: 'Degraded mode',
detail: `${columnTitle(skipped.stage)} skipped: ${skipped.reason}`,
columnId: skipped.stage,
targetLabel: 'skipped',
});
}
for (const [index, error] of safeErrors(input).entries()) {
issues.push({
id: `run-error:${index}`,
severity: 'failed',
title: 'Run error',
detail: error,
columnId: failureStage(input) === 'source' ? 'source' : 'gates',
});
}
return issues;
}
function humanizeAdapter(adapter: string): string {
const labels: Record<string, string> = {
'live-database': 'Warehouse',
'live_database': 'Warehouse',
'dbt_descriptions': 'dbt',
'looker': 'BI',
'lookml': 'BI',
'notion': 'Docs',
'metabase': 'BI',
'metricflow': 'dbt',
'historic_sql': 'SQL',
};
return labels[adapter] ?? adapter;
}
function sourceColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const sources = eventsOf(input.events, 'source_acquired');
const source = sources.at(-1);
const snapshot = latest(input.events, 'raw_snapshot_written');
const scope = latest(input.events, 'scope_detected');
const totalFiles = sources.reduce((sum, s) => sum + s.fileCount, 0);
const adapterLabels = sources.length > 1
? [...new Set(sources.map((s) => humanizeAdapter(s.adapter)))]
: [input.adapter, input.connectionId];
return {
id: 'source',
title: 'SOURCE',
status: columnStatus({ complete: !!source }),
headline: `${totalFiles} raw files`,
counters: sources.length > 1
? [adapterLabels.join(', '), `sync ${snapshot?.syncId ?? input.syncId}`]
: [`sync ${snapshot?.syncId ?? input.syncId}`, scope?.fingerprint ? `scope ${scope.fingerprint}` : 'scope none'],
chips: adapterLabels.map((label) => ({ label, status: 'complete' as MemoryFlowDisplayStatus })),
details: [
`Trigger: ${source?.trigger ?? 'unknown'}`,
...(sources.length > 1
? sources.map((s) => `${humanizeAdapter(s.adapter)}: ${s.fileCount} files`)
: [`Adapter: ${input.adapter}`]),
`Connection: ${input.connectionId}`,
`Source: ${input.sourceDir ?? 'stored report'}`,
...errorDetails(input),
],
};
}
function chunksColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const chunks = latest(input.events, 'chunks_planned');
const diff = latest(input.events, 'diff_computed');
return {
id: 'chunks',
title: 'CHUNKS',
status: columnStatus({ hasWarnings: (chunks?.evictionCount ?? 0) > 0, complete: !!chunks }),
headline: `${chunks?.chunkCount ?? 0} chunks`,
counters: [formatDiff(diff), `${chunks?.evictionCount ?? 0} deletions`],
chips: firstChips(input.plannedWorkUnits.map((workUnit) => workUnit.unitKey), 'complete'),
details: [
`Work units planned: ${chunks?.workUnitCount ?? 0}`,
`Eviction candidates: ${chunks?.evictionCount ?? 0}`,
`Diff: ${formatDiff(diff)}`,
],
};
}
function workUnitsColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const finished = eventsOf(input.events, 'work_unit_finished');
const failed = finished.filter((event) => event.status === 'failed');
const succeeded = finished.filter((event) => event.status === 'success');
const active = eventsOf(input.events, 'work_unit_started').filter(
(started) => !finished.some((event) => event.unitKey === started.unitKey),
);
const total = input.plannedWorkUnits.length || latest(input.events, 'chunks_planned')?.workUnitCount || 0;
const skipped = skippedStage(input, 'workUnits');
if (skipped) {
return {
id: 'workUnits',
title: 'WORKUNITS',
status: 'warning',
headline: 'skipped',
counters: ['0 done', '0 failed', '0 active'],
chips: [{ label: 'skipped', status: 'warning', detail: skipped.reason }],
details: [`Skipped: ${skipped.reason}`],
};
}
return {
id: 'workUnits',
title: 'WORKUNITS',
status: columnStatus({ hasFailures: failed.length > 0, hasActivity: active.length > 0, complete: total > 0 }),
headline: `${total} WUs`,
counters: [`${succeeded.length} done`, `${failed.length} failed`, `${active.length} active`],
chips: workUnitChips(input),
details: input.plannedWorkUnits.map(
(workUnit) =>
`${workUnit.unitKey}: ${workUnit.rawFiles.length} raw, ${workUnit.peerFileCount} peers, ${workUnit.dependencyCount} deps`,
),
};
}
function actionsColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const actions = eventsOf(input.events, 'candidate_action');
const wikiCount = countCandidateActions(input.events, 'wiki');
const slCount = countCandidateActions(input.events, 'sl');
const skipped = skippedStage(input, 'actions');
if (skipped) {
return {
id: 'actions',
title: 'ACTIONS',
status: 'warning',
headline: 'skipped',
counters: ['0 wiki', '0 SL'],
chips: [{ label: 'skipped', status: 'warning', detail: skipped.reason }],
details: [`Skipped: ${skipped.reason}`],
};
}
const details = input.details.actions.length
? input.details.actions.map(
(action) => `${action.unitKey} ${action.target} ${action.action} ${action.key}: ${action.summary}`,
)
: actions.map((action) => `${action.target} ${action.action}: ${action.key}`);
return {
id: 'actions',
title: 'ACTIONS',
status: columnStatus({ complete: actions.length > 0 }),
headline: `${actions.length} candidates`,
counters: [`${wikiCount} wiki`, `${slCount} SL`],
chips: actionChips(input, actions),
details,
};
}
function gatesColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const reconciliation = latest(input.events, 'reconciliation_finished');
const failed = eventsOf(input.events, 'work_unit_finished').filter((event) => event.status === 'failed');
const headline = reconciliation
? `${reconciliation.conflictCount} conflict, ${reconciliation.fallbackCount} fallback`
: 'not run';
const skipped = skippedStage(input, 'gates');
if (skipped) {
return {
id: 'gates',
title: 'GATES',
status: 'warning',
headline: 'skipped',
counters: ['0 failed', '0 flagged'],
chips: [{ label: 'skipped', status: 'warning', detail: skipped.reason }],
details: [`Skipped: ${skipped.reason}`],
};
}
return {
id: 'gates',
title: 'GATES',
status: columnStatus({
hasFailures: failed.length > 0,
hasWarnings: (reconciliation?.conflictCount ?? 0) > 0 || (reconciliation?.fallbackCount ?? 0) > 0,
complete: !!reconciliation,
}),
headline,
counters: [`${failed.length} failed`, `${reconciliation?.fallbackCount ?? 0} flagged`],
chips: firstChips(failed.map((event) => event.unitKey), 'failed'),
details: [
`Reconciliation: ${headline}`,
`Failed work units: ${failed.length}`,
`Conflicts resolved: ${reconciliation?.conflictCount ?? 0}`,
`Flagged fallbacks: ${reconciliation?.fallbackCount ?? 0}`,
...failedWorkUnitDetails(failed),
...errorDetails(input),
],
};
}
function savedColumn(input: MemoryFlowReplayInput): MemoryFlowColumnView {
const saved = latest(input.events, 'saved');
const provenance = latest(input.events, 'provenance_recorded');
const report = latest(input.events, 'report_created');
const memoryCount = (saved?.wikiCount ?? 0) + (saved?.slCount ?? 0);
const chipLabels = [saved?.commitSha ? saved.commitSha.slice(0, 8) : '', report?.reportPath ?? ''].filter(
(label): label is string => label.length > 0,
);
const skipped = skippedStage(input, 'saved');
if (skipped) {
return {
id: 'saved',
title: 'SAVED',
status: 'warning',
headline: '0 memories',
counters: ['0 wiki', '0 SL', '0 provenance'],
chips: [{ label: 'skipped', status: 'warning', detail: skipped.reason }],
details: [
`Skipped: ${skipped.reason}`,
`Run: ${input.runId}`,
`Report: ${report?.reportPath ?? input.reportPath ?? 'none'}`,
],
};
}
return {
id: 'saved',
title: 'SAVED',
status: columnStatus({ complete: memoryCount > 0 }),
headline: memoryCount > 0 ? `${memoryCount} memories` : 'not saved',
counters: [`${saved?.wikiCount ?? 0} wiki`, `${saved?.slCount ?? 0} SL`, `${provenance?.rowCount ?? 0} provenance`],
chips: firstChips(chipLabels, 'complete'),
details: [
`Commit: ${saved?.commitSha ? saved.commitSha.slice(0, 8) : 'none'}`,
`Run: ${input.runId}`,
`Report: ${report?.reportPath ?? input.reportPath ?? 'none'}`,
`Provenance rows: ${provenance?.rowCount ?? 0}`,
...(input.status === 'error' && saved ? ['Durable memory landed before failure.'] : []),
...(input.status === 'error' && saved ? errorDetails(input) : []),
],
};
}
function completionLine(input: MemoryFlowReplayInput): string | null {
const sources = eventsOf(input.events, 'source_acquired');
const saved = latest(input.events, 'saved');
const report = latest(input.events, 'report_created');
if (sources.length === 0 || !saved || saved.wikiCount + saved.slCount === 0) {
return null;
}
const totalFiles = sources.reduce((sum, event) => sum + event.fileCount, 0);
const commit = saved.commitSha ? saved.commitSha.slice(0, 8) : 'none';
return `Saved ${saved.wikiCount + saved.slCount} memories from ${totalFiles} raw files: ${saved.wikiCount} wiki pages, ${saved.slCount} SL updates. Commit: ${commit} Run: ${input.runId} Report: ${report?.reportPath ?? input.reportPath ?? 'none'}`;
}
export function buildMemoryFlowViewModel(input: MemoryFlowReplayInput): MemoryFlowViewModel {
const columns = [
sourceColumn(input),
chunksColumn(input),
workUnitsColumn(input),
actionsColumn(input),
gatesColumn(input),
savedColumn(input),
];
const plannedWorkUnitsColumn = columns.find((column) => column.id === 'workUnits');
const errorColumn =
input.status === 'error'
? columns.find((column) => column.id === (failureStage(input) === 'source' ? 'source' : 'gates'))
: undefined;
const warningColumn = columns.find((column) => column.status === 'warning');
const firstExpandableColumn =
errorColumn ??
warningColumn ??
(input.plannedWorkUnits.length > 0 && !latest(input.events, 'saved') && plannedWorkUnitsColumn
? plannedWorkUnitsColumn
: (columns.find((column) => column.details.length > 0) ?? columns[0]));
const trustIssues = buildMemoryFlowTrustIssues(input);
const sources = eventsOf(input.events, 'source_acquired');
const titleSources = sources.length > 1
? [...new Set(sources.map((s) => humanizeAdapter(s.adapter)))].join(' + ')
: `${input.connectionId}/${input.adapter}`;
return {
title: `KLO memory flow ${titleSources} ${input.status}`,
subtitle: `Run ${input.runId} Sync ${input.syncId}`,
status: input.status,
activeLine: activeLine(input),
columns,
trustIssues,
selectedTitle: firstExpandableColumn.title,
selectedDetails: firstExpandableColumn.details,
completionLine: completionLine(input),
details: input.details,
};
}

View file

@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import {
buildMemoryFlowVisualModel,
memoryFlowStatusBadge,
renderMemoryFlowConnectorLine,
} from './visuals.js';
import type { MemoryFlowViewModel } from './types.js';
function viewWithStatuses(statuses: Array<'waiting' | 'active' | 'complete' | 'warning' | 'failed'>): MemoryFlowViewModel {
const titles = ['SOURCE', 'CHUNKS', 'WORKUNITS', 'ACTIONS', 'GATES', 'SAVED'];
const ids = ['source', 'chunks', 'workUnits', 'actions', 'gates', 'saved'] as const;
return {
title: 'KLO memory flow warehouse/metricflow running',
subtitle: 'Run run-1 Sync sync-1',
status: 'running',
activeLine: 'active: WorkUnit orders',
selectedTitle: 'WORKUNITS',
selectedDetails: ['orders: 1 raw, 0 peers, 1 deps'],
completionLine: null,
trustIssues: [],
details: { actions: [], provenance: [], transcripts: [] },
columns: statuses.map((status, index) => ({
id: ids[index],
title: titles[index],
status,
headline: `${titles[index].toLowerCase()} headline`,
counters: [],
chips: [],
details: [],
})),
};
}
describe('memory-flow visual helpers', () => {
it('uses ASCII badges with text meaning for every status', () => {
expect(memoryFlowStatusBadge('waiting')).toEqual({ label: '..', text: 'waiting' });
expect(memoryFlowStatusBadge('active')).toEqual({ label: '>>', text: 'active' });
expect(memoryFlowStatusBadge('complete')).toEqual({ label: 'OK', text: 'complete' });
expect(memoryFlowStatusBadge('warning')).toEqual({ label: '!!', text: 'warning' });
expect(memoryFlowStatusBadge('failed')).toEqual({ label: 'XX', text: 'failed' });
});
it('renders a no-color connector line with status badges and six columns', () => {
const view = viewWithStatuses(['complete', 'complete', 'active', 'waiting', 'waiting', 'waiting']);
expect(renderMemoryFlowConnectorLine(view)).toBe(
'OK SOURCE -> OK CHUNKS -> >> WORKUNITS -> .. ACTIONS -> .. GATES -> .. SAVED',
);
});
it('moves the pulse to the active column, then warnings, failures, and the last completed column', () => {
expect(
buildMemoryFlowVisualModel(viewWithStatuses(['complete', 'complete', 'active', 'waiting', 'waiting', 'waiting']))
.pulseColumnId,
).toBe('workUnits');
expect(
buildMemoryFlowVisualModel(viewWithStatuses(['complete', 'warning', 'complete', 'waiting', 'waiting', 'waiting']))
.pulseColumnId,
).toBe('chunks');
expect(
buildMemoryFlowVisualModel(viewWithStatuses(['complete', 'complete', 'failed', 'waiting', 'waiting', 'waiting']))
.pulseColumnId,
).toBe('workUnits');
expect(
buildMemoryFlowVisualModel(viewWithStatuses(['complete', 'complete', 'complete', 'complete', 'waiting', 'waiting']))
.pulseColumnId,
).toBe('actions');
});
});

View file

@ -0,0 +1,78 @@
import type {
MemoryFlowColumnId,
MemoryFlowColumnView,
MemoryFlowDisplayStatus,
MemoryFlowViewModel,
} from './types.js';
export interface MemoryFlowStatusBadge {
label: '..' | '>>' | 'OK' | '!!' | 'XX';
text: 'waiting' | 'active' | 'complete' | 'warning' | 'failed';
}
export interface MemoryFlowVisualColumn {
id: MemoryFlowColumnId;
title: string;
status: MemoryFlowDisplayStatus;
badge: MemoryFlowStatusBadge;
pulse: boolean;
}
export interface MemoryFlowVisualModel {
columns: MemoryFlowVisualColumn[];
connectorLine: string;
pulseColumnId: MemoryFlowColumnId;
}
export function memoryFlowStatusBadge(status: MemoryFlowDisplayStatus): MemoryFlowStatusBadge {
if (status === 'active') return { label: '>>', text: 'active' };
if (status === 'complete') return { label: 'OK', text: 'complete' };
if (status === 'warning') return { label: '!!', text: 'warning' };
if (status === 'failed') return { label: 'XX', text: 'failed' };
return { label: '..', text: 'waiting' };
}
function firstColumnWithStatus(
columns: MemoryFlowColumnView[],
status: MemoryFlowDisplayStatus,
): MemoryFlowColumnView | undefined {
return columns.find((column) => column.status === status);
}
function lastCompletedColumn(columns: MemoryFlowColumnView[]): MemoryFlowColumnView {
return [...columns].reverse().find((column) => column.status === 'complete') ?? columns[0];
}
function selectPulseColumn(columns: MemoryFlowColumnView[]): MemoryFlowColumnView {
return (
firstColumnWithStatus(columns, 'active') ??
firstColumnWithStatus(columns, 'warning') ??
firstColumnWithStatus(columns, 'failed') ??
lastCompletedColumn(columns)
);
}
function renderColumn(column: MemoryFlowVisualColumn): string {
return `${column.badge.label} ${column.title}`;
}
export function buildMemoryFlowVisualModel(view: MemoryFlowViewModel): MemoryFlowVisualModel {
const pulseColumn = selectPulseColumn(view.columns);
const columns = view.columns.map((column) => ({
id: column.id,
title: column.title,
status: column.status,
badge: memoryFlowStatusBadge(column.status),
pulse: column.id === pulseColumn.id,
}));
return {
columns,
connectorLine: columns.map(renderColumn).join(' -> '),
pulseColumnId: pulseColumn.id,
};
}
export function renderMemoryFlowConnectorLine(view: MemoryFlowViewModel): string {
return buildMemoryFlowVisualModel(view).connectorLine;
}