Files
superwork/vscode/src/codeReader/qa/qaMap.ts
T

91 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 代码问答的存法:每个工作区文件夹一份 `<workspace>/.spx/code-qa.json`,读写的规矩同文件注释(见 pathMap)。
* 形状是 `{ "相对路径": [{ anchor, entries: [{ id, question, answer, createdAt, sessionId }] }] }`:
* 一份文件里挂着问答的每一块一项(anchor 见 BlockAnchors),块下面问过的按时间排。
*/
import type { PathMap, PathMapFile } from '../pathMap'
/** 问过的一次:问题、回答(Markdown)、什么时候问的(ISO 时间)、Claude Code 的会话 id。 */
export interface QaEntry {
id: string
question: string
answer: string
createdAt: string
sessionId: string
}
/** 挂在一块上的问答。 */
export interface QaThread {
anchor: string
entries: QaEntry[]
}
export type QaMap = PathMap<QaThread[]>
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function text(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() !== '' ? value : undefined
}
function parseEntry(value: unknown): QaEntry | undefined {
if (!isRecord(value))
return undefined
const [id, question, answer, createdAt] = [text(value.id), text(value.question), text(value.answer), text(value.createdAt)]
if (!id || !question || !answer || !createdAt || Number.isNaN(Date.parse(createdAt)))
return undefined
return { id, question, answer, createdAt, sessionId: typeof value.sessionId === 'string' ? value.sessionId : '' }
}
/** 同一块的并成一项;块按记号排,问答按时间排:写出来的顺序稳定,git diff 干净。 */
function tidy(threads: QaThread[]): QaThread[] {
const byAnchor = new Map<string, QaEntry[]>()
for (const thread of threads)
byAnchor.set(thread.anchor, [...byAnchor.get(thread.anchor) ?? [], ...thread.entries])
return [...byAnchor]
.filter(([, entries]) => entries.length > 0)
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
.map(([anchor, entries]) => ({
anchor,
entries: [...entries].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
}))
}
/** 一份文件的问答:不是数组、一项都认不出的当没有;认不出的块、问答跳过。 */
function parseThreads(value: unknown): QaThread[] | undefined {
if (!Array.isArray(value))
return undefined
const threads = value.flatMap((item): QaThread[] => {
const anchor = isRecord(item) ? text(item.anchor) : undefined
if (!anchor || !isRecord(item) || !Array.isArray(item.entries))
return []
return [{ anchor, entries: item.entries.flatMap(entry => parseEntry(entry) ?? []) }]
})
const tidied = tidy(threads)
return tidied.length > 0 ? tidied : undefined
}
export const QA_FILE: PathMapFile<QaThread[]> = {
path: '.spx/code-qa.json',
label: '代码问答',
parse: parseThreads,
}
/** 在 key 这份文件的 anchor 那块下面记一次问答。 */
export function withEntry(map: QaMap, key: string, anchor: string, entry: QaEntry): QaMap {
return { ...map, [key]: tidy([...map[key] ?? [], { anchor, entries: [entry] }]) }
}
/** 删掉一次问答;块下面删光了这块就不挂了,文件里删光了这份文件就不记了。 */
export function withoutEntry(map: QaMap, key: string, anchor: string, entryId: string): QaMap {
const threads = tidy((map[key] ?? []).map(thread => thread.anchor === anchor
? { anchor, entries: thread.entries.filter(entry => entry.id !== entryId) }
: thread))
const next = { ...map }
delete next[key]
return threads.length > 0 ? { ...next, [key]: threads } : next
}