Files
fusion/packages/dashboard/src/knowledge-index-refresh.ts
gsxdsm 0a87890bd8 feat(knowledge): U14 — persistent knowledge index
Adds knowledge_pages (db migration 118→119) + a deterministic, model-free
keyword index of task/PR history in packages/dashboard/src, incrementally
refreshed on task completion (task:moved→done listener) and queryable via an
auth-gated, project-scoped API. Complements the LLM-extracted insights/memory
surfaces rather than duplicating them.

Follow-ups: no React view yet; PR-history page population attaches via U18.
2026-06-15 20:34:52 -07:00

68 lines
2.1 KiB
TypeScript

import type { TaskStore } from "@fusion/core";
import { refreshKnowledgeForTask } from "./knowledge-index.js";
/**
* Task-completion refresh hook for the persistent knowledge index (U14).
*
* Listens for `task:moved` and, when a task reaches `done`, incrementally
* re-indexes just that task as a knowledge page (one upsert, never a full
* re-index). Mirrors the attach/detach/start/stop lifecycle of
* `GitHubSourceIssueCloseService` so it can be wired the same way alongside the
* other `task:moved` listeners. All refresh work is fail-soft (see
* {@link refreshKnowledgeForTask}) so it can never disrupt task completion.
*/
interface TaskMovedEvent {
task: { id: string };
// store's `task:moved` carries `ColumnId`; this handler only literal-compares
// legacy ids, so the widened string field is safe.
from: string;
to: string;
}
export class KnowledgeIndexRefreshService {
private readonly defaultStore: TaskStore;
private readonly listeners = new Map<TaskStore, { onTaskMoved: (event: TaskMovedEvent) => void }>();
private started = false;
constructor(store: TaskStore) {
this.defaultStore = store;
}
start(): void {
if (this.started) return;
this.started = true;
this.attach(this.defaultStore);
}
stop(): void {
if (!this.started) return;
this.started = false;
for (const store of this.listeners.keys()) {
this.detach(store);
}
}
attach(store: TaskStore): void {
if (this.listeners.has(store)) return;
const onTaskMoved = (event: TaskMovedEvent): void => {
void this.handleTaskMoved(store, event);
};
this.listeners.set(store, { onTaskMoved });
if (this.started) {
store.on("task:moved", onTaskMoved);
}
}
detach(store: TaskStore): void {
const handlers = this.listeners.get(store);
if (!handlers) return;
store.off("task:moved", handlers.onTaskMoved);
this.listeners.delete(store);
}
private async handleTaskMoved(store: TaskStore, event: TaskMovedEvent): Promise<void> {
if (event.to !== "done") return;
await refreshKnowledgeForTask(store, event.task.id);
}
}