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

162 lines (161 loc) • 7.77 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/. /** * `indexDocumentToStashEntry` — the inverse of the `akm` adapter's * `indexDocumentFromEntry` (`adapters/akm-adapter.ts`). akm 0.9.0 Chunk 5, * milestone F4a (M2 groundwork). * * The indexer drains a single `IndexDocument` stream (`drainDirDocuments` × the * dispatched adapter's `recognize`) and persists its canonical projection in * `entries.document_json`. Every reader (`rowToIndexedEntry` → * `DbIndexedEntry.entry`) consumes that same projection, and parity tests pin * the boundary. * * That reconstruction is lossless by construction: the akm adapter's `recognize` * assembles a full `IndexDocument` (P1/P2 → fold → P4) and then maps it onto the * `IndexDocument` via `indexDocumentFromEntry` — first-class search/signal fields * onto named members, every other search-surface/provenance field onto * `documentJson` (the `DOCUMENT_JSON_CARRIED_FIELDS` set). This function reverses * that mapping exactly, so `indexDocumentToStashEntry(recognize(file))` deep- * equals the current persisted document for the same file (the * `tests/integration/shadow-scan-parity.test.ts` gate pins this boundary). * * Two deliberate non-round-trip fields: * - `filename` — dropped by `indexDocumentFromEntry` (no IndexDocument home), * recovered here as `basename(doc.path)`. The old pipeline set it to * `basename(file)` in `applyPostContributorFields`, and `doc.path` IS that * file, so the value is identical. * - `fileSize` — never set by `recognize` NOR by the legacy flat-walk pass; * both pipelines attach it at PERSIST time (`withFileSize`). It is therefore * intentionally absent here and added by the persist layer, exactly as * before. * * Pure, type-only imports (no cycle participation). */ import path from "node:path"; import { getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "../passes/metadata.js"; /** * Reconstruct the `IndexDocument` an `IndexDocument` was mapped from. First-class * IndexDocument members and the `documentJson`-carried extras are both restored; * every field is set only when present, matching the old pipeline's * "assign-only-when-defined" assembly so the reconstruction deep-equals it. */ export function indexDocumentToStashEntry(doc) { const dj = (doc.documentJson ?? {}); const entry = { name: doc.name, // `type` is a required member of the merged IndexDocument (M-core-1), so no // `?? ""` fallback is needed — `recognize` always sets it. type: doc.type, // Dropped by indexDocumentFromEntry — recovered from the read path (= the // basename the old pipeline stored). Always present on a recognized doc. filename: path.basename(doc.path ?? ""), }; // ── First-class IndexDocument members (spec §3) ── if (doc.description !== undefined) entry.description = doc.description; if (doc.tags !== undefined) entry.tags = doc.tags; if (doc.content !== undefined) entry.content = doc.content; if (doc.contentTruncated !== undefined) entry.contentTruncated = doc.contentTruncated; if (hasMarkdownFragmentContent(doc)) setMarkdownFragmentContent(entry, getMarkdownFragmentContent(doc)); if (doc.ownsPresentation !== undefined) entry.ownsPresentation = doc.ownsPresentation; if (doc.updated !== undefined) entry.updated = doc.updated; if (doc.links !== undefined) entry.links = doc.links; if (doc.adapterId === "okf") { if (doc.documentJson !== undefined) entry.documentJson = doc.documentJson; // OKF v0.2 trust/provenance/lifecycle family (okf-support.md v0.2 note). // These are first-class IndexDocument members the okf adapter sets // directly (never routed through documentJson), so — unlike the akm // adapter's DOCUMENT_JSON_CARRIED_FIELDS reconstruction below — they need // an explicit carry here too, or persistence silently drops them even // though `recognize()` itself returns them correctly. if (doc.provenance !== undefined) entry.provenance = doc.provenance; if (doc.lifecycleStatus !== undefined) entry.lifecycleStatus = doc.lifecycleStatus; if (doc.staleAfter !== undefined) entry.staleAfter = doc.staleAfter; if (doc.okfVersion !== undefined) entry.okfVersion = doc.okfVersion; return entry; } if (doc.aliases !== undefined) entry.aliases = doc.aliases; if (doc.searchHints !== undefined) entry.searchHints = doc.searchHints; if (doc.hints !== undefined) entry.hints = doc.hints; if (doc.quality !== undefined) entry.quality = doc.quality; if (doc.confidence !== undefined) entry.confidence = doc.confidence; if (doc.beliefState !== undefined) entry.beliefState = doc.beliefState; if (doc.currentBeliefRefs !== undefined) entry.currentBeliefRefs = doc.currentBeliefRefs; if (doc.scope !== undefined) entry.scope = doc.scope; if (doc.captureMode !== undefined) entry.captureMode = doc.captureMode; if (doc.lessonStrength !== undefined) entry.lessonStrength = doc.lessonStrength; if (doc.derivedFrom !== undefined) entry.derivedFrom = doc.derivedFrom; if (doc.pinned !== undefined) entry.pinned = doc.pinned; // ── documentJson-carried extras (DOCUMENT_JSON_CARRIED_FIELDS) ── // The `renderer` key on documentJson is adapter-internal (WI-C presentation), // NOT a IndexDocument field — deliberately not restored. assignStringList(entry, "examples", dj.examples); assignStringList(entry, "usage", dj.usage); if (isIntent(dj.intent)) entry.intent = dj.intent; assignStringList(entry, "xrefs", dj.xrefs); assignString(entry, "pageKind", dj.pageKind); assignString(entry, "whenToUse", dj.whenToUse); if (dj.toc !== undefined) entry.toc = dj.toc; if (dj.parameters !== undefined) entry.parameters = dj.parameters; if (dj.source !== undefined) entry.source = dj.source; assignString(entry, "category", dj.category); assignStringList(entry, "supersededBy", dj.supersededBy); assignStringList(entry, "contradictedBy", dj.contradictedBy); assignString(entry, "run", dj.run); assignString(entry, "setup", dj.setup); assignString(entry, "cwd", dj.cwd); if (dj.wikiRole !== undefined) entry.wikiRole = dj.wikiRole; assignStringList(entry, "sources", dj.sources); assignStringList(entry, "evidenceSources", dj.evidenceSources); assignStringList(entry, "uses", dj.uses); // D2 (#730): OKF v0.2 provenance promoteProposal stamps onto AKM-native // writes, carried via DOCUMENT_JSON_CARRIED_FIELDS (akm-adapter.ts) — // unpacked back to a first-class member here exactly like `sources`/ // `generation`/`evidenceSources` above, so it round-trips to the top level // of the persisted entry rather than staying nested under `documentJson`. if (dj.provenance !== undefined) entry.provenance = dj.provenance; return entry; } function assignStringList(entry, key, value) { if (Array.isArray(value)) entry[key] = value; } function assignString(entry, key, value) { if (typeof value === "string") entry[key] = value; } function isIntent(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }