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

430 lines (429 loc) • 19.4 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/. /** * Index-time metadata fold for the `akm` adapter's `recognize` — akm 0.9.0 * chunk-2, WI-C, implementing spec §2 ("the nine index-time metadata * contributors move into recognize") + the workflow contributor. * * This leaf reproduces the 11 `registerMetadataContributor` sites keyed on the * winning RENDERER name (exactly how each contributor's `appliesTo({ rendererName })` * selects): * * - `output/renderers.ts` (9): toc-metadata (knowledge-md), * lesson-frontmatter-metadata, memory-frontmatter-metadata, * script-comment-metadata, env-file-metadata, secret-file-metadata, * task-yaml-metadata, session-md-metadata, fact-md-metadata; * - `workflows/renderer.ts` (1): workflow-document-metadata (the historical * `workflow-md` renderer id now presents both peer workflow sources). * * ── Cycle-safety (chunk-2 ratchet, baseline 18) ── * * Imported ONLY by `akm-adapter.ts` (itself imported by nothing in `src/` — see * that file's header), so this leaf can never gain an inbound edge from a cycle * participant and therefore can never JOIN a cycle. It VALUE-imports the pure * parsers each contributor already uses so the fold cannot drift from them: * `parseFrontmatter`, `parseMarkdownToc`, the peer-source * `compileWorkflowSource` frontend/projection, and env key-name scanning. * `output/renderers.ts` / `workflows/renderer.ts` are the * cycle-sensitive modules whose contributor bodies live in this port — their * ONE non-importable helper (`metadata.ts#extractDescriptionFromComments`, a * heavy cycle-participant module) is copied verbatim below as * {@link extractDescriptionFromComments} rather than imported. Verified: * `bun scripts/lint-import-cycles.ts` stays at 18. * * ── What is folded, and where it lands on the IndexDocument ── * * The contributors mutate a `IndexDocument`; {@link foldRecognizedMetadata} returns * the same fields as a plain {@link FoldedMetadata}, which `recognize` maps onto * `IndexDocument` first-class fields (`tags`/`searchHints`/`description`/ * `confidence`) and, for the fields without a first-class home * (`toc`/`parameters`/`source`), onto `documentJson` (opaque adapter extras). * The seed is a MINIMAL entry (name+type only): this isolates exactly what the * contributors add, which is what the parity test compares against * `applyMetadataContributors` run on the same minimal seed. * * ── Tolerance note ── * * Every contributor that reads/parses content is reproduced with the SAME * try/catch tolerance it has today (toc/memory/session/fact/task swallow parse * errors). Workflow source compilation fails closed in the drain (a broken * workflow is dropped); here the compile/projection is wrapped * so a broken workflow yields no folded metadata instead of throwing out of the * synchronous `recognize` — the drop-the-entry behavior is an index-drain * concern (Chunk 4/5), not recognition's. On the valid Chunk-0b fixture this * distinction never triggers. */ import { parseBuiltinCommandAction } from "../../../commands/command/builtin-action.js"; import { scanEnvKeyNames } from "../../../commands/env/env.js"; import { parseTaskSource } from "../../../tasks/source/parse-task-source.js"; import { compileWorkflowSource, workflowStepInstructions } from "../../../workflows/compile.js"; import { parseFrontmatter } from "../../asset/frontmatter.js"; import { parseMarkdownToc } from "../../asset/markdown.js"; import { nonEmptyString } from "./shared.js"; /** * Verbatim copy of `indexer/passes/metadata.ts#extractDescriptionFromComments` * (`:1510-1552`) — `metadata.ts` is a heavy cycle-participant module, so the * one non-importable helper `applyScriptMetadata` needs is copied here (the * others are imported). Reads a leading JSDoc block or hash-comment run from the * script file; returns `null` on read/parse miss. */ function extractDescriptionFromComments(content) { const lines = content.split(/\r?\n/).slice(0, 50); const blockStart = lines.findIndex((l) => /^\s*\/\*\*/.test(l)); if (blockStart >= 0) { const desc = []; for (let i = blockStart; i < lines.length; i++) { const line = lines[i]; if (i > blockStart && /\*\//.test(line)) break; const cleaned = line .replace(/^\s*\/?\*\*?\s?/, "") .replace(/\*\/\s*$/, "") .trim(); if (cleaned) desc.push(cleaned); } if (desc.length > 0) return desc.join(" "); } let start = 0; if (lines[0]?.startsWith("#!")) start = 1; const hashLines = []; for (let i = start; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith("#") && !line.startsWith("#!")) { hashLines.push(line.replace(/^#+\s*/, "").trim()); } else if (line === "") { } else { break; } } if (hashLines.length > 0) return hashLines.join(" "); return null; } /** * Reproduce `applyFrontmatterDescriptionAndTags` (`output/renderers.ts:668-684`): * set description/source/confidence from frontmatter (only when not already set) * and merge frontmatter `tags:` onto the folded tags. Returns nothing — mutates * `out`. */ function applyFrontmatterDescriptionAndTags(fm, out) { const desc = nonEmptyString(fm.description); if (desc && !out.description) { out.description = desc; out.source = "frontmatter"; out.confidence = 0.9; } if (Array.isArray(fm.tags) && fm.tags.length > 0) { const fmTags = fm.tags.filter((t) => typeof t === "string" && t.trim().length > 0); if (fmTags.length > 0) { out.tags = Array.from(new Set([...(out.tags ?? []), ...fmTags])); } } } /** The command a stored `akm/command` action runs; inline content names no asset. */ function storedCommandRef(action) { return action?.kind === "stored" ? action.ref : undefined; } /** The asset a workflow step targets: its `uses:` ref, or the ref of a stored `akm/command`. Prose and `run:` steps target none. */ function workflowStepTarget(spec) { if (!spec?.uses) return undefined; if (spec.uses !== "akm/command") return spec.uses; if (spec.commandMode !== "stored-ref") return undefined; try { return storedCommandRef(parseBuiltinCommandAction(spec.with)); } catch { return undefined; } } /** Collect + finalize a searchHints set the way every contributor does (`Array.from(hints).filter(Boolean)`, assigned only when non-empty). */ function finalizeHints(out, hints) { if (hints.size > 0) out.searchHints = Array.from(hints).filter(Boolean); } /** * The metadata fold, keyed on the winning renderer NAME. Returns the fields the * 11 contributors would add to a MINIMAL (name+type) `IndexDocument`. `recognize` * maps the result onto the `IndexDocument`. Reads the live file (index-time, so * disk reads are legitimate — unlike `validate`). */ export function foldRecognizedMetadata(rendererName, file) { const out = {}; switch (rendererName) { // ── toc-metadata (knowledge-md) ── case "knowledge-md": { try { const toc = parseMarkdownToc(file.content()); if (toc.headings.length > 0) out.toc = toc.headings; } catch { // Non-fatal: skip TOC if file can't be read } return out; } // ── lesson-frontmatter-metadata (lesson-md) ── case "lesson-md": { try { const fm = parseFrontmatter(file.content()).data; applyFrontmatterDescriptionAndTags(fm, out); const whenToUse = nonEmptyString(fm.when_to_use); if (whenToUse) finalizeHints(out, new Set([`when_to_use:${whenToUse}`])); } catch { // Non-fatal: skip metadata extraction on parse error } return out; } // ── memory-frontmatter-metadata (memory-md) ── case "memory-md": { try { const fm = parseFrontmatter(file.content()).data; applyFrontmatterDescriptionAndTags(fm, out); const hints = new Set(); const source = nonEmptyString(fm.source); if (source) hints.add(source); const fmObservedAt = nonEmptyString(fm.observed_at); if (fmObservedAt) { hints.add(`observed_at:${fmObservedAt}`); } else { try { const isoDate = file.stat().mtime.toISOString().slice(0, 10); hints.add(`observed_at:${isoDate}`); } catch { // Non-fatal: skip mtime fallback on stat error } } const expires = nonEmptyString(fm.expires); if (expires) hints.add(`expires:${expires}`); if (fm.subjective === true) hints.add("subjective"); finalizeHints(out, hints); } catch { // Non-fatal: skip metadata extraction on error } return out; } // ── script-comment-metadata (script-source) ── case "script-source": { if (file.ext === ".md") return out; const commentDesc = extractDescriptionFromComments(file.content()); if (commentDesc && !out.description) { out.description = commentDesc; out.source = "comments"; out.confidence = 0.7; } return out; } // ── env-file-metadata (env-file) ── case "env-file": { const keys = scanEnvKeyNames(file.content()); if (keys.length > 0) out.searchHints = keys; out.tags = Array.from(new Set([...(out.tags ?? []), "env", "secrets"])); return out; } // ── secret-file-metadata (secret-file) — tags only, NEVER reads the body ── case "secret-file": { out.tags = Array.from(new Set([...(out.tags ?? []), "secret", "sensitive"])); return out; } // ── task-yaml-metadata (task-yaml) ── case "task-yaml": { out.tags = Array.from(new Set([...(out.tags ?? []), "task", "scheduled"])); try { const parsed = parseTaskSource({ yaml: file.content(), filePath: file.absPath, workspaceRoot: file.stashRoot }); const v4 = parsed.v4; const target = v4.target; const hints = new Set(); for (const binding of v4.schedule) hints.add(`schedule:${binding.cron}`); if (target.kind === "uses") { if (target.uses.kind === "workflow") hints.add(`workflow:${target.uses.ref}`); else if (target.uses.kind === "command") hints.add(`prompt:${target.uses.ref}`); else if (target.command?.kind === "inline") hints.add(`prompt:${target.command.content}`); else if (target.command?.kind === "stored") hints.add(`prompt:${target.command.ref}`); else hints.add(`uses:${target.uses.ref}`); const used = target.uses.kind !== "builtin-command" ? target.uses.ref : storedCommandRef(target.command); if (used) out.uses = [used]; } else { hints.add(`run:${target.run}`); } if (v4.description && !out.description) { out.description = v4.description; out.source = "task-source"; out.confidence = 0.9; } if (v4.tags && v4.tags.length > 0) { out.tags = Array.from(new Set([...(out.tags ?? []), ...v4.tags])); } if (v4.when_to_use) hints.add(`when_to_use:${v4.when_to_use}`); finalizeHints(out, hints); } catch { // Non-fatal: skip metadata extraction on parse error } return out; } // ── session-md-metadata (session-md) ── case "session-md": { try { const fm = parseFrontmatter(file.content()).data; applyFrontmatterDescriptionAndTags(fm, out); out.tags = Array.from(new Set([...(out.tags ?? []), "session"])); const hints = new Set(); const harness = nonEmptyString(fm.harness); if (harness) hints.add(`harness:${harness}`); const project = nonEmptyString(fm.project); if (project) hints.add(`project:${project}`); const logPath = nonEmptyString(fm.log_path); if (logPath) hints.add(`log_path:${logPath}`); finalizeHints(out, hints); } catch { // Non-fatal: skip metadata extraction on parse error } return out; } // ── fact-md-metadata (fact-md) ── case "fact-md": { try { const fm = parseFrontmatter(file.content()).data; applyFrontmatterDescriptionAndTags(fm, out); const tags = new Set([...(out.tags ?? []), "fact"]); const hints = new Set(); const category = nonEmptyString(fm.category); if (category) { tags.add(category); hints.add(`category:${category}`); } if (fm.pinned === true) { tags.add("pinned"); hints.add("pinned"); } out.tags = Array.from(tags).filter(Boolean); finalizeHints(out, hints); } catch { // Non-fatal: skip metadata extraction on parse error } return out; } // ── workflow-document-metadata (historical renderer id `workflow-md`, peer sources) ── case "workflow-md": { try { const result = compileWorkflowSource(file.content(), { path: file.relPath, workspaceRoot: file.stashRoot }); if (!result.ok) return out; const plan = result.plan; const hints = new Set(); const uses = new Set(); if (plan.preamble) hints.add(plan.preamble); for (const step of plan.steps) { hints.add(step.stepId); hints.add(workflowStepInstructions(step)); if (step.gate.criteria[0]) hints.add(step.gate.criteria[0]); const used = workflowStepTarget(step.spec); if (used) uses.add(used); } out.searchHints = Array.from(hints).filter(Boolean); if (uses.size > 0) out.uses = [...uses]; if (plan.paramSchemas) { const parameters = Object.entries(plan.paramSchemas).map(([name, schema]) => { const description = schema.description; return { name, ...(typeof description === "string" && description ? { description } : {}) }; }); if (parameters.length) out.parameters = parameters; } } catch { // See file header: broken workflows drop at drain time, not here. } return out; } default: // No contributor applies to this renderer (e.g. skill-md/command-md/ // agent-md — the static-only mappings carry no metadata contributor). return out; } } /** * Apply {@link foldRecognizedMetadata}'s output onto a `IndexDocument` with the * SAME precedence the live in-place renderer contributors use (Chunk 5 M-b), so * `entry.applyPreContributorFields → applyFoldedMetadata → applyPostContributorFields` * reproduces `buildEntryFromFile`'s `P1/P2 → contributors → P4` byte-for-byte. * * Precedence, verified against every case in {@link foldRecognizedMetadata}: * - description/source/confidence travel together and only when the entry has * no description yet (contributors gate on `!entry.description`); a * contributor that sets description WITHOUT source/confidence leaves those * untouched — mirrored by the * `folded.source`/`folded.confidence` guards below; * - tags UNION into the existing set (order-preserving Set dedup), matching the * contributors' `Array.from(new Set([...entry.tags, ...added]))`; * - searchHints / toc / parameters are SET (overwrite), matching the * contributors' direct assignment. * * The fold computes each field unconditionally from an empty seed; re-gating on * `entry` state here is what recovers the in-place precedence (the fold ≡ * contributors-on-a-minimal-seed is pinned by the akm-adapter fold-parity test). */ export function applyFoldedMetadata(entry, folded) { if (folded.description && !entry.description) { entry.description = folded.description; if (folded.source) entry.source = folded.source; if (folded.confidence !== undefined) entry.confidence = folded.confidence; } if (folded.tags && folded.tags.length > 0) { entry.tags = Array.from(new Set([...(entry.tags ?? []), ...folded.tags])); } // searchHints MERGE (not overwrite): every live contributor seeds its hints // from `new Set(entry.searchHints ?? [])` before adding, so a frontmatter // `searchHints:` set by P2 (applyCuratedFrontmatter) survives and the // contributor's hints append to it. The fold computes only the contributor's // additions (from an empty seed), so unioning them onto the existing set — // existing first, deduped, truthy-filtered — reproduces the live merge exactly. if (folded.searchHints) { entry.searchHints = Array.from(new Set([...(entry.searchHints ?? []), ...folded.searchHints])).filter(Boolean); } if (folded.toc) entry.toc = folded.toc; if (folded.parameters) entry.parameters = folded.parameters; if (folded.uses) entry.uses = folded.uses; }