UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

138 lines (137 loc) • 7.46 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. import { searchLocal } from "../../../indexer/search/db-search.js"; import { stripFrontmatterBody } from "../content-hash.js"; import { stripBundle } from "../ledger.js"; import { loadRetrievalQueries } from "../retrieval-gate.js"; /** At most this many of the retired asset's most recent queries are replayed (plan §5.4, §7). */ export const CONTINUITY_MAX_QUERIES = 5; /** "Top 10" per the plan's rule — both the pass/fail cutoff and the search `limit`. */ export const CONTINUITY_TOP_N = 10; /** * Builds the real search call the continuity check uses, stateful across * every query asked of ONE instance (S2): the first time a query falls back * to keyword-only ranking (`mode: "fts-fallback"` — most often a down or * unreachable embedding endpoint), every later call through THIS instance * forces `semanticSearchMode: "off"` instead of attempting semantic search * again, so a dead endpoint costs one failed attempt per run, not one per * remaining query (a hanging endpoint at ~3s/query, 300 proposals x 5 * queries, would otherwise cost on the order of an hour). Construct exactly * one instance per pair-pass run and reuse it for every proposal judged, so * the throttle covers the whole run, not just one proposal's own queries — * and every call made once it has switched, across every remaining proposal * in the run, reports `forcedKeywordOnly: true`, not just the one call that * discovered the fallback (round-3 review: the first fix only marked THAT * call unverified, so a second proposal checked while the endpoint was * still down came back with a clean, `mode: "keyword"` — and therefore * bulk-acceptable — result). */ export function createContinuitySearch(stashDir, config) { const base = { searchType: "any", limit: CONTINUITY_TOP_N, stashDir, sources: [{ path: stashDir, isDefault: true }], }; let keywordOnly = false; return async (query) => { const forcedKeywordOnly = keywordOnly; const callConfig = keywordOnly ? { ...config, semanticSearchMode: "off" } : config; const result = await searchLocal({ ...base, query, config: callConfig }); if (result.mode === "fts-fallback") keywordOnly = true; return { hits: result.hits, mode: result.mode, forcedKeywordOnly }; }; } /** 1-indexed position of `conceptId` in `hits`, or `undefined` if it is not among them. */ function rankOf(hits, conceptId) { const index = hits.findIndex((hit) => stripBundle(hit.ref) === conceptId); return index === -1 ? undefined : index + 1; } /** Body only, whitespace collapsed — the same shape `db-search.ts`'s own content-dedupe compares (S3b). */ function normalizedBody(raw) { return stripFrontmatterBody(raw).replace(/\s+/g, " ").trim(); } /** * Replay `retiredRef`'s own past queries and check that `successorRef` ranks * in the top {@link CONTINUITY_TOP_N} for every one where the retired asset * did. Returns `undefined` when there is nothing to flag: the two bodies are * content-identical, no recorded queries, or every query ran on the real * ranking and either the retired asset never ranked top 10 for it, or the * survivor always did too. Never throws. * * S3: two fixes against false flags measured on a real night-1 admission * (300 pairs, 6 flags, half spurious): * - queries are the SAME cleaned set `loadRetrievalQueries` replays for the * retrieval regression gate (`../retrieval-gate.ts`) — stash-README * boilerplate, harness/tool envelopes, pastes over 2,000 characters, and * duplicates are dropped before replay, not just capped at 5 raw entries; * - when the retired and successor bodies normalize identical, the check * never runs at all: search's own content-dedupe (`db-search.ts`) already * hides the successor behind the retired asset for every such query, so a * "successor missing from the top 10" finding here would not be a real * risk, just that dedupe working as designed. * * S2: a query that never ran (the search call threw), ran on the * keyword-only fallback (`mode: "fts-fallback"` — the real ranking was * attempted and failed, most often a down or unreachable embedding * endpoint), or ran after the shared search instance had already switched * to forced keyword-only because an EARLIER query in the same run fell back * (`forcedKeywordOnly`) is "unverified" — it is dropped from the rank * comparison below (its hits cannot be trusted as "the ranking a user * actually gets"), but unlike a query the retired asset simply did not rank * for, it can never by itself lead to a silent `undefined` — at least one * unverified query always produces a `continuityRisk`, so a dead endpoint * reads as "risk unknown" for every proposal it touches that run, never as * "no risk found" for the ones checked after the first failure. */ export async function checkRetirementContinuity(args) { // S3b: identical bodies — search's own content-dedupe already hides the // successor for every query that would rank the retired asset, so there is // no real risk here to check for. if (normalizedBody(args.retiredRaw) === normalizedBody(args.successorRaw)) return undefined; // S3a: the SAME cleaned queries the retrieval regression gate replays — // boilerplate, envelopes, pastes and duplicates dropped before replay. const queries = loadRetrievalQueries(args.ledgerAccess, args.retiredRef).slice(0, CONTINUITY_MAX_QUERIES); if (queries.length === 0) return undefined; // no queries recorded: no check, no flag const search = args.search ?? createContinuitySearch(args.stashDir, args.config); // N2: no rank-change-report abstraction — a query only ever needs "did the // retired asset rank top 10, and if so, did the successor too?", and // `rankOf` (search itself returning at most CONTINUITY_TOP_N hits) already // answers both directly. const ranks = []; let unverifiedQueries = 0; for (const query of queries) { let hits; try { const result = await search(query); if (result.mode === "fts-fallback" || result.forcedKeywordOnly) { unverifiedQueries++; // S2: never silently compare keyword-only ranks continue; } hits = result.hits; } catch { unverifiedQueries++; // S2: a query that never ran cannot be "no risk" continue; } const retiredRank = rankOf(hits, args.retiredRef); if (retiredRank === undefined) continue; // the retired asset itself did not rank top 10 here — nothing to protect const successorRank = rankOf(hits, args.successorRef); if (successorRank === undefined) ranks.push({ query, retiredRank, successorRank: null }); } // Every query verified, and either the retired asset never ranked top 10 // for any of them, or the survivor always did too: nothing to flag. if (unverifiedQueries === 0 && ranks.length === 0) return undefined; return { failingQueries: ranks.length, ranks, ...(unverifiedQueries > 0 ? { unverifiedQueries } : {}), }; }