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
149 lines (148 loc) • 7.97 kB
JavaScript
// 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/.
/**
* `deriveInstallations` — akm 0.9.0 Chunk 5, milestone M-a.
*
* Bridges the `SearchSource[]` model (the live indexer's source list,
* `search-source.ts`) onto the durable `BundleInstallation[]` /
* `BundleComponent[]` model (spec §1.1). LIVE: called from the indexer
* (provenance derivation), the proposal repository, and written-asset
* indexing. The originally-planned full "Step-3" `scanComponent`-driven scan
* swap was superseded by the narrower Chunk-5 F4a drain-dir engine swap; the
* shadow-parity scaffolding module (`scan/scan-installations.ts`) was deleted
* as dead in the 0.9.0 close-out.
*
* Per source → one installation:
* - `id` = `source.registryId` ?? a deterministic slug of `source.path`
* (the `<bundle>` prefix of every ref this installation emits,
* spec §1.3). Slugs are made unique WITHIN a derivation batch
* by appending a short path-hash suffix on collision, so two
* sources sharing a basename never mint the same bundle id.
* - `trusted` = `source.writable === true`. 0.9.0 ships no first-class trust
* record (spec §1.3 — "no new trust machinery"); the writable
* primary/config stashes are the user's own and count as
* trusted, read-only registry caches do not. Nothing consumes
* `trusted` in the live path yet (installation grants nothing,
* History D8) — this is the Tier-A placeholder mapping.
* - components = ONE component for the current single-root akm layout
* (spec §1.2 rule 5): `{ id, root: source.path, adapter }`. The
* akm layout's type dirs (`knowledge/`, `skills/`, …) are NOT
* separate components — they are type-derived leading path
* segments of the conceptId within the one component.
*
* ── The component id == the bundle id (transitional coupling) ──
*
* The `akm` adapter's `recognize` derives an item's ref prefix from the
* component it is handed (`ref = ${c.id}//${conceptId}`, `akm-adapter.ts`), so
* for the single-component akm layout the component id MUST equal the
* installation/bundle id for refs to be `bundle//conceptId`. This mirrors the
* `akm-adapter.test.ts` convention (`component({ id: BUNDLE_ID })`). Splitting
* component-provenance from the bundle prefix (recognize learning the bundle
* from the installation) is a downstream contract refinement — nothing in the
* live path reads `IndexDocument.component` as distinct from `bundle` today.
*
* ── Adapter selection (spec §1.2 ordered probe) ──
*
* The adapter is chosen by the ordered `looksLikeRoot` probe over the built-in
* adapters (`getAdapters()`), first match wins. The registry is now a STATIC
* FROZEN map (normative §12.6) populated at module load, so this probe runs in
* production with the full 11-family set — no registration call is required
* first. Array order (`BUILTIN_ADAPTERS`) is the probe precedence, most-specific
* first, so a `.claude`/dotenv/wiki root is claimed by its tight probe before
* the loose `akm` stash-shape probe.
*
* When NO adapter's probe fires (an empty or not-yet-materialized root), the
* fallback is **`akm`** (see {@link FALLBACK_ADAPTER_ID}), preserving workspace
* root behavior. Explicit configured ownership always wins over probing.
*/
import { isSourceWriteActivated } from "../core/activation-policy.js";
import { detectAdapterId } from "../core/adapter/detect-adapter.js";
import { stashDirFor } from "../core/asset/asset-placement.js";
import { deriveBundleId, deriveBundleIds, slugForPath } from "../core/bundle-id.js";
export { deriveBundleId, deriveBundleIds, slugForPath };
/**
* The no-probe-match fallback adapter id.
*
* The status-quo-preserving default from spec §1.2(3):
*
* - Behavior discipline (spec line 267): the AKM workspace root's own indexing
* MUST stay classified `akm`. In this transitional model there is no
* depends on either explicit adapter configuration, `akm.looksLikeRoot`
* firing (type dirs / `.stash` present), or this fallback. A genuinely empty
* root fires no probe; `akm` keeps the implicit workspace root correct.
* - Status quo: before the registry was wired, `getAdapters()` was empty in
* production so EVERY source fell back here — this preserves that exact
* fallback value while the probe now genuinely classifies the non-akm shapes.
*/
const FALLBACK_ADAPTER_ID = "akm";
/**
* Resolve the adapter id for a component root via the ordered `looksLikeRoot`
* probe (spec §1.2), first match wins; falls back to `akm` when no probe fires.
*/
export { detectAdapterId } from "../core/adapter/detect-adapter.js";
/**
* Derive the durable `BundleInstallation[]` from the transitional
* `SearchSource[]`. Order is preserved (source priority = installation
* priority). Bundle ids are unique within the returned batch.
*/
export function deriveInstallations(sources) {
const ids = deriveBundleIds(sources);
const installations = [];
for (const [index, source] of sources.entries()) {
// D-R5 rule 1: when the source carries its config bundle key (a slug-legal
// registryId), that key IS the installation id — equal by construction to
// this derivation. A non-slug-legal registryId slugs from the path instead.
const id = ids[index];
const writable = isSourceWriteActivated(source);
const adapter = source.adapterId ?? detectAdapterId(source.path, FALLBACK_ADAPTER_ID);
installations.push({
id,
trusted: writable,
components: [
{
// Single-component akm layout: the component id == the bundle id so
// the adapter's `ref = ${c.id}//${conceptId}` yields `bundle//…`.
id,
adapter,
root: source.path,
writable,
},
],
});
}
return installations;
}
/** Canonical bundle ids whose configured component policy permits mutation. */
export function deriveWritableBundleIds(sources) {
return new Set(deriveInstallations(sources)
.filter((installation) => installation.components.some((component) => component.writable === true))
.map((installation) => installation.id));
}
/**
* Derive the durable `EntryProvenance` for an indexed entry (Chunk-5 flip
* §14.4): `conceptId` is the D-R2 qualified `<stash-subdir>/<name>` spelling
* (`stashDirFor(type)` prefix; a foreign type with no placement stash-subdir
* keeps the bare name), and `item_ref` is `<bundle>//<conceptId>` — the exact
* spelling `recognize` emits as `IndexDocument.ref`. Shared by the full-index
* diff-persist writer and the write-path `indexWrittenAssets` fast path so both
* populate item_ref identically (F4a M-core-2 item 5 — no NULL-item_ref rows).
*/
export function deriveEntryProvenance(bundle, type, name,
/**
* The OWNING adapter's `doc.conceptId`, when the caller has it. Preferred
* verbatim over the akm `stashDirFor` re-derivation so a non-akm adapter's
* identity (`pages/foo`, snapshot paths, ...) survives into `item_ref`
* (D-R3: identity comes from the resolved entry, not a re-derivation).
*/
adapterConceptId) {
const typeStashDir = stashDirFor(type);
const conceptId = adapterConceptId ?? (typeStashDir !== undefined ? `${typeStashDir}/${name}` : name);
return {
itemRef: `${bundle.bundleId}//${conceptId}`,
bundleId: bundle.bundleId,
componentId: bundle.componentId,
conceptId,
adapterId: bundle.adapterId,
};
}