project-nomad/admin/app/utils/eval/corpus_source.ts

39 lines
2.0 KiB
TypeScript

import { basename, join, resolve, sep } from 'node:path'
/** Where the frozen corpus lives, relative to the app root. */
export const EVAL_CORPUS_DIR = 'tests/eval/corpus'
/**
* Map a retrieved chunk's `source` payload back to its corpus document id, or
* null if the chunk did not come from the eval corpus.
*
* The eval corpus shares the `nomad_knowledge_base` Qdrant collection with the
* developer's real documents — NOMAD "collections" are a payload tag, not
* separate Qdrant collections — and is isolated by a
* `collection: __nomad_eval__` filter that Qdrant applies during search. That
* filter does hold, but a harness whose every number depends on a filter should
* be able to *prove* the filter held rather than assume it. This function is
* that proof: the caller counts every unresolved chunk and fails the run if the
* count is non-zero.
*
* The check is on the resolved **path**, not the file extension. An earlier
* version accepted any `*.md`, which was worse than useless here: NOMAD embeds
* its own `admin/docs/*.md` into the knowledge base on first run, so a leaked
* `faq.md` would have resolved to the plausible-looking document id "faq", been
* counted as a merely-irrelevant chunk, and quietly depressed precision with no
* indication that anything had gone wrong.
*
* Mirrors the resolve-then-prefix-check guard in `RagService.resolveUploadPath`.
* `corpusDir` is injectable so this stays testable without a real corpus on disk.
*/
export function docIdFromSource(source: unknown, corpusDir?: string): string | null {
if (typeof source !== 'string') return null
const dir = resolve(corpusDir ?? join(process.cwd(), EVAL_CORPUS_DIR))
const abs = resolve(source)
// The trailing separator matters: without it, a sibling directory such as
// "…/corpus-backup" shares a string prefix with "…/corpus" and would pass.
if (!abs.startsWith(dir + sep)) return null
if (!abs.endsWith('.md')) return null
return basename(abs, '.md')
}