|
| 1 | +import { readJsonDiagnostic, updateJsonAtomic } from "../core/atomic-store.js"; |
| 2 | +import path from "node:path"; |
| 3 | +import { hashContextText } from "./hash.js"; |
| 4 | +import { createContextFeedback, type CreateContextFeedbackInput } from "./feedback.js"; |
| 5 | +import type { ContextFeedback, ContextFeedbackStore } from "./types.js"; |
| 6 | + |
| 7 | +const FEEDBACK_DIRECTORY = path.join(".agent-context", "context-registry", "feedback"); |
| 8 | + |
| 9 | +export function contextFeedbackStorePath(repository: string): string { |
| 10 | + const root = normalizeRepository(repository); |
| 11 | + return path.join(root, FEEDBACK_DIRECTORY, `${hashContextText(root.toLowerCase()).slice(0, 32)}.json`); |
| 12 | +} |
| 13 | + |
| 14 | +export function recordContextFeedback(input: CreateContextFeedbackInput): ContextFeedback { |
| 15 | + const root = normalizeRepository(input.repository); |
| 16 | + const feedback = createContextFeedback({ ...input, repository: root }); |
| 17 | + let result = feedback; |
| 18 | + updateJsonAtomic<ContextFeedbackStore>(contextFeedbackStorePath(root), (current) => { |
| 19 | + const store = current ?? emptyStore(root); |
| 20 | + validateFeedbackStore(store, root); |
| 21 | + const existing = store.feedback.find((item) => item.feedbackId === feedback.feedbackId); |
| 22 | + if (existing) { |
| 23 | + result = existing; |
| 24 | + return store; |
| 25 | + } |
| 26 | + result = feedback; |
| 27 | + return { |
| 28 | + ...store, |
| 29 | + revision: store.revision + 1, |
| 30 | + feedback: [...store.feedback, feedback].sort(compareFeedback) |
| 31 | + }; |
| 32 | + }); |
| 33 | + return result; |
| 34 | +} |
| 35 | + |
| 36 | +export function readContextFeedback(repository: string): ContextFeedback[] { |
| 37 | + return readContextFeedbackStore(repository).feedback; |
| 38 | +} |
| 39 | + |
| 40 | +export function readContextFeedbackStore(repository: string): ContextFeedbackStore { |
| 41 | + const root = normalizeRepository(repository); |
| 42 | + const filePath = contextFeedbackStorePath(root); |
| 43 | + const result = readJsonDiagnostic<ContextFeedbackStore>(filePath); |
| 44 | + if (result.status === "missing") return emptyStore(root); |
| 45 | + if (result.status === "corrupt") throw new Error(`Unable to read Context feedback store ${filePath}: ${result.error}`); |
| 46 | + validateFeedbackStore(result.value, root); |
| 47 | + return { |
| 48 | + ...result.value, |
| 49 | + feedback: [...result.value.feedback].sort(compareFeedback) |
| 50 | + }; |
| 51 | +} |
| 52 | + |
| 53 | +function emptyStore(repository: string): ContextFeedbackStore { |
| 54 | + return { schemaVersion: 1, revision: 0, repository, feedback: [] }; |
| 55 | +} |
| 56 | + |
| 57 | +function validateFeedbackStore(store: ContextFeedbackStore, repository: string): void { |
| 58 | + if (store.schemaVersion !== 1) throw new Error(`Unsupported Context feedback schemaVersion ${String(store.schemaVersion)}.`); |
| 59 | + if (!Number.isInteger(store.revision) || store.revision < 0) throw new Error("Context feedback store revision must be non-negative."); |
| 60 | + if (path.resolve(store.repository) !== repository) throw new Error("Context feedback store repository does not match the current repository."); |
| 61 | + if (!Array.isArray(store.feedback)) throw new Error("Context feedback store feedback must be an array."); |
| 62 | + const ids = new Set<string>(); |
| 63 | + for (const item of store.feedback) { |
| 64 | + if (!item || item.schemaVersion !== 1 || typeof item.feedbackId !== "string") throw new Error("Context feedback store contains an invalid feedback record."); |
| 65 | + if (ids.has(item.feedbackId)) throw new Error(`Context feedback store contains duplicate feedbackId ${item.feedbackId}.`); |
| 66 | + ids.add(item.feedbackId); |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +function compareFeedback(left: ContextFeedback, right: ContextFeedback): number { |
| 71 | + return left.createdAt.localeCompare(right.createdAt) || left.feedbackId.localeCompare(right.feedbackId); |
| 72 | +} |
| 73 | + |
| 74 | +function normalizeRepository(repository: string): string { |
| 75 | + if (!repository.trim()) throw new Error("Context feedback repository must not be empty."); |
| 76 | + return path.resolve(repository); |
| 77 | +} |
0 commit comments