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

292 lines (242 loc) • 19.6 kB
# AKM 0.9.0 — Ref-grammar decision + revised Chunk-5 flip plan **Status:** DECIDED — binding for the Chunk-5 flip and Chunk 8. Amends the adapter spec (`akm-0.9.0-bundle-adapter-spec.md` §1.3/§5.1) and the normative spec (`akm-format-neutral-bundle-workspace-spec.md` §11.1) only where §"Spec amendments" below says so; everything else in both documents stands. **Date:** 2026-07-18. **Context:** PR #719, branch `claude/akm-architecture-refactor-fubvd7` (Chunk 5 ~70% banked; the flip stalled six times on the test-codemod problem). **Question:** one canonical ref grammar (`[bundle//]conceptId`) vs. multiple ref formats — and, downstream of that, how the deferred Chunk-5 flip + script-only test codemod actually executes. --- ## 1. The decision in one paragraph **Keep a single canonical grammar and a single stored spelling (Option A's end-state), and take the workable half of Option B as the abstraction: refs are `parse → resolve → serialize`, and the short (bundle-omitted) form is a first-class *accepted input* at every boundary — CLI, repository lookups, and tests — resolved by a deterministic, behavior-preserving rule.** There is no second grammar: the short form is the same production with the optional prefix omitted, not a different format. `type:name` is not an accepted form anywhere after the flip (it survives only inside the frozen `scripts/akm-migrate/migrate/legacy/` copy). This dissolves the codemod problem — the measured blocker was never the grammar, it was the assumption that tests must *spell the bundle*; they don't, because resolution supplies it, exactly the way origin-less `type:name` refs already work today. Full Option B (a plural-format ref abstraction with a `type:name` compatibility spelling) is **rejected**: normative §11.4 prohibits a permanent dual-parser, §11.2 prohibits `type` in refs, and the measured test suite shows the compatibility form buys nothing — only ~225 of the ~4,700 ref literals are origin-qualified; the rest are already short refs relying on resolution. --- ## 2. Sub-decisions (each binding) ### D-R1 — Grammar model: one grammar, resolver-first abstraction ``` ref := [ <bundle> "//" ] <concept-id> [ "#" <fragment> ] (unchanged, §11.1) ``` The ref abstraction is three functions with a type-level distinction between *maybe-short* and *resolved*: - **parse** — pure syntax, no I/O, no workspace knowledge. Already built: `parseBundleRef` / `BUNDLE_REF_RE` (`src/core/asset/asset-ref.ts:258,205`). Returns `BundleRef { bundle?: string; conceptId; fragment? }`. - **resolve** — `resolveRef(input: string | BundleRef, ctx: RefContext): ResolvedRef` where `ResolvedRef` is `BundleRef & { bundle: string }` (branded or structurally narrowed). `ctx` carries the resolution surface (see D-R4). This is the one new module the flip adds (small, pure over an injected bundle list + an index lookup callback). - **serialize** — `bundleRefToString` (`asset-ref.ts:248`), unchanged. Serializing a `ResolvedRef` always emits the fully-qualified form. **The core rule:** *only `ResolvedRef` crosses a storage boundary; only unresolved input crosses an input boundary.* Repositories, state writers, and durable keys take/emit fully-qualified refs exclusively; parsers and CLI/API entrypoints are the only place short forms exist. This is §11.1's "short form is input sugar" made structural instead of conventional. ### D-R2 — conceptId spelling: the qualified `<stash-subdir>/<canonical-name>` form This resolves the open recognize/placeNew spelling split documented at `src/core/adapter/adapters/akm-adapter.ts:397-403` ("owned downstream (Chunk 3/5)" — i.e., here). The `akm` adapter's conceptId is the **qualified form**: the placement bundle-subdir followed by the per-type canonical name — `knowledge/http-caching`, `skills/code-review`, `scripts/db/migrate/run.sh`, `workflows/release`. Not the bare canonical name that `recognize()` currently emits (`akm-adapter.ts:198,224`), and not the raw file path (a skill's id is `skills/<dir>`, not `skills/<dir>/SKILL`; per normative §11.2 the adapter owns both stripping directions, so "path within the bundle" means *the item's path as the adapter defines it* — a directory-item's path is its directory). Why qualified beats the banked bare-name spelling: 1. **It makes the codemod purely textual.** `type:name → stashDirFor(type)/name` is a static 13-row table (`asset-placement.ts:89-165`: skill→skills, command→commands, agent→agents, knowledge→knowledge, workflow→workflows, script→scripts, memory→memories, env→env, secret→secrets, lesson→lessons, task→tasks, session→sessions, fact→facts). No runtime helper, no semantic transform — the script-only gate holds. 2. **No cross-type identity collisions.** Bare names make `skill:deploy` and `workflow:deploy` both `bundle//deploy` — a behavior change (today's `entry_key = stashDir:type:name` keeps them distinct) and an unfixable ambiguity for the ~4,700 test literals. Qualified ids keep them distinct (`skills/deploy` vs `workflows/deploy`) with zero new machinery. 3. **It is the spelling `placeNew` already consumes** (`akm-adapter.ts:405`) and the spelling the adapter spec's §1.3 examples already show (`personal//knowledge/http-caching`, `team-catalog//workflows/release`). Only `recognize()` and the additive `item_ref` writer disagree, and both are one-line derivations. 4. **It preserves §11.2's reclassification invariant** far better than bare names: the qualified id is anchored on where the file *is*; a bare canonical name depends on per-type ext-keep/strip rules and can change when only the `type` changes. **Search surfaces do not move.** `entry.name` / the FTS `name` column keep the *bare* canonical name exactly as today (identity ≠ search text); `bm25(entries_fts,0,10,5,3,2,1)` and the §12.3 goldens are untouched by construction. Only `IndexDocument.conceptId` / `ref` / `item_ref` change spelling. Cost: the two banked derivation sites change (`akm-adapter.ts` recognize; `indexer.ts:1032` `itemRef = ${bundle}//${entry.name}` → the qualified spelling) and the shadow-parity harness + byte-identity proof re-run. The harness exists and is exactly the tool for this — that proof is an afternoon, not a redesign. This is the *only* piece of banked Chunk-5 work this decision revises. ### D-R3 — Canonical storage-key rule and accepted input forms | Surface | Accepted form(s) | Stored/emitted form | |---|---|---| | Index identity (`entries.item_ref`, UNIQUE post-flip) | — (writer-derived) | fully-qualified `bundle//conceptId` | | Durable state keys (`usage_events.entry_ref`, utility, feedback, proposal targets, `.derived-twin` bases) | — (derived from a resolved entry, never from raw input) | fully-qualified | | Refs inside bundle content (frontmatter fields, workflow/task target refs) | short or fully-qualified | resolved against the **containing** bundle at index/parse time; stored fully-qualified | | Prose body-refs (lint missing-ref scan, `akm mv` xref rewrite, search ref-prefix) | fully-qualified anchored form ONLY (`BUNDLE_REF_RE`); bare short tokens in prose are not refs | n/a (recognition, not storage) | | CLI args / programmatic API / **tests** | short or fully-qualified (+ `#fragment`) | resolved per D-R4 before any lookup or write | | `type:name` (old grammar) | **nowhere** after the flip; frozen migrator only | migrated once per §11.4 | Unchanged from normative §11.1 except one amendment (D-R4's fallback order). The "three legacy spellings per ref" probing that §11.1 calls out as the cost of leaving this open (`rekeyStateDbForMove`) is exactly what this table retires. ### D-R4 — Resolution rule for short input refs (the §11.1 amendment) §11.1 currently says CLI sugar resolves "against the workspace `defaultBundle`" — full stop. That is a silent behavior regression: today an origin-less ref searches **all** sources in priority order (`asset-ref.ts:22` — "primary → search paths → installed"), and 182 test files plus real CLI muscle memory depend on it. Amended rule: > A short ref from CLI/API input resolves to the **defaultBundle** if the conceptId exists there, > otherwise to the first bundle containing the conceptId in **installation priority order** > (the `deriveInstallations` / config order — the same order origin-less lookups walk today). > First match wins, deterministically. No match → not-found error naming the forms tried. Content-internal short refs resolve to the **containing** bundle, never defaultBundle (unchanged, §11.1). The old `local//` scoping (primary-only) is replaced by an explicit `resolveRef(input, { only: bundleId })` API option, not a ref spelling. A later lint MAY warn when a short ref shadows the same conceptId in a lower-priority bundle; it is not a 0.9.0 gate. ### D-R5 — Bundle identity (the Chunk-8 coupling, pinned now) A bundle id is a **workspace-assigned slug** (charset per §11.1: no `/ : . #` or whitespace), chosen by this precedence: 1. **Config key** — the key in the Chunk-8 `bundles` map. Authoritative once Chunk 8 lands. 2. **`registryId`** — for registry-installed sources (today's behavior, `installations.ts:115`). 3. **`slugForPath(sourcePath)`** — basename slug, batch-unique via path-hash suffix (`installations.ts:73,147`) — the fallback for unconfigured ad-hoc sources only. Stability semantics: - The id is **workspace-scoped and stable under path moves once configured**: Chunk 8's config migration writes the `bundles` map keyed by exactly what rule 2/3 derives today (`registryId ?? slugForPath(path)`), so **no second identity migration happens at Chunk 8** — the §11.4 `type:name → conceptId` re-key is the only one. After migration, editing a bundle's `path` doesn't change its key; changing the key is `akm bundle rename` (§11.5 rekey transaction, with the §11.5 startup guard against silently re-minted state). - `defaultBundle` = the primary bundle's derived id, emitted by the migrator. - **mkdtemp instability is confined and irrelevant**: an unconfigured tmp-dir fixture gets a random slug, but under D-R1/D-R4 tests never need to spell it — they speak short refs. The few tests that assert fully-qualified *stored* keys pin identity explicitly (set `registryId`, or mount the source at a fixed-basename child of the tmp dir); that is the hand-edit bucket, not the codemod. --- ## 3. Why this dissolves the forcing problem The stall analysis framed the choice as (a) inject runtime `bundleFor(dir)` helpers into 182 test files (semantic transform — violates the script-only gate) or (b) use the short form for content-internal refs only. Both assume API boundaries demand fully-qualified refs. They don't — and never did in the old grammar either: `AssetRef.origin` is optional, `findEntryIdByRef` takes an optional `stashDir`, and the suite's literals are overwhelmingly origin-less (~4,700 total, ~225 origin-qualified, 8 files setting `registryId`). **Today's tests already exercise a resolve-at-lookup model.** Making short-form resolution first-class in the new grammar (D-R1/D-R4) means the codemod maps short ref → short ref: ``` "skill:code-review" → "skills/code-review" (static table, D-R2) "script:db/migrate/run.sh" → "scripts/db/migrate/run.sh" "knowledge:guide.md" → "knowledge/guide" (markdown ext-strip, same table) "npm:@scope/pkg//skill:x" → hand bucket if asserted as a stored key; else registryId is the bundle id and the mapping is textual (D-R5 rule 2) "local//skill:x" → hand bucket (scoped-resolve API or fixture registryId), ~subset of 225 ``` No literal ever needs the random `akm-<hash>` slug. The codemod is a table-driven regex over `.ts` + JSON fixtures — squarely inside the plan §15 rule-2 "script-only, zero hand-edited hunks, ≥20-literal mutation spot-check" gate. **Why not full Option B anyway?** A retained `type:name` parser is (1) prohibited as a permanent dual-parser (§11.4), (2) reintroduces `type` into identity against §11.2 and D28, (3) keeps ~216 src parse sites alive that the flip exists to delete, and (4) solves only the same problem the resolver already solves. Multiple *stored* spellings are strictly worse: every reader grows N-way probing (the `rekeyStateDbForMove` disease §11.1 already diagnoses), and durable-state survival across rebuilds (usage_events relink, derived-twin base lookup) would need spelling normalization at every join forever. --- ## 4. Revised Chunk-5 flip execution plan Every step lands green on the integration branch; the dual-input window opens at F1 and closes at F5 **inside the same chunk**, satisfying §11.4's no-permanent-dual-parser rule. Chunk-8 scope (three-DB merge, config migration, journaled cutover) is untouched. - **F0 — Pin the conceptId spelling (additive, ~2 sites).** Change `recognize()`'s conceptId and the `item_ref` writer (`indexer.ts:1032`) to the D-R2 qualified form; `entry.name`/FTS untouched. Re-run the shadow-parity harness (`tests/integration/shadow-scan-parity.test.ts`) and the byte-identity proof for the `item_ref` derivation; delete the spelling-tension NOTE at `akm-adapter.ts:397`. Gate: parity green, §12.3 suites green (they must be unaffected — FTS inputs unchanged). - **F1 — Resolution layer + dual-input readers repointed onto `item_ref`.** Add `resolveRef`/`RefContext`/`ResolvedRef` (D-R1, D-R4). Repoint the seven readers (`index-entries-repository.ts:41,178,245,386,534,727,755`) to key on `item_ref`, accepting BOTH grammars at the input edge (old `type:name` translated via the static D-R2 table + source→bundle map; new refs resolved via D-R4). `getEntryByRef(db, type, name)` gains a ref-shaped overload; `.derived-twin` suffix logic moves to conceptId-suffix on `item_ref`. Additive; the whole existing suite stays green on the old literals. Gate: full suite green pre-codemod; readers' old-grammar path is a thin shim marked for F5 deletion. - **F2 — The script-only codemod (the ~4,700 literals, ~150+ files).** One script in `scripts/`: the D-R2 static table over quoted/template `[origin//]type:name` literals in `tests/**` (.ts + fixture JSON), per-type name normalization (markdown `±.md`), `registryId` origins carried as bundle ids, `DESIGNATIONS.json` path/hash regeneration. Lands atomically with nothing else in the commit. Gate: suite green behind the F1 dual readers; mutation spot-check — revert ≥20 random re-keyed literals, suite must go red, recorded in the chunk ledger; grep ratchet arms `type-prefix literal in tests/` → shrink-only. - **F3 — The hand bucket (separate, individually reviewable commits; ~40–60 files as already ledgered in plan §12.1).** parseAssetRef unit tests ported to parseBundleRef/resolveRef; `local//` scoping tests → `{only}` resolution option; tests asserting stored-key spellings pin bundle identity via `registryId` or fixed-basename fixture roots; goldens re-baselined where the *asserted key* (not ranking) changed. - **F4 — The flip proper.** Swap the live scan loop: `generateMetadataFlat` (`indexer.ts:873`) → `scanComponent` (`scan-component.ts:136`) over `deriveInstallations`; `StashEntry` → `IndexDocument` rename; `entry_key/stash_dir/entry_type` demoted, `item_ref` becomes THE key with the UNIQUE constraint (`index-schema.ts:242` note); diff persistence per plan; the shadow-parity harness flips to assert the **persisted index** against the legacy stream one last time, then the legacy stream is deleted. Index-db-resident state (`usage_events`, utility/feedback) re-keys onto `item_ref` via the §11.4 join-against-last-good-index mapping with orphan quarantine. `REF_RE` at `base-linter.ts:182` retargets to `BUNDLE_REF_RE`. Gate: §12.3 full parity (nDCG/MRR/recall + filter + whyMatched), durable-state survival suite (relink across full rebuild, derived-twin base lookup, content-hash skip), fn-size + cycle ratchets (db trio leaves the baseline per DoD 11). - **F5 — Delete the old grammar.** `parseAssetRef`/`makeAssetRef`/`refToString`/`AssetRef`/ `TYPE_ALIASES` deleted (~216 sites already repointed by F1/F4); the F1 old-grammar reader shim deleted; frozen `scripts/akm-migrate/migrate/legacy/legacy-layout.ts` untouched. Gate: grep `parseAssetRef` → 0, `StashEntry` → 0 (declared scopes); tests type-prefix ratchet → 0 and flipped absolute. **Chunk 8 then consumes D-R5 as settled:** the config migrator emits `bundles` keys equal to the already-derived ids (no re-key), `defaultBundle`, the §11.5 startup guard, and the three-DB merge re-key runs over refs that are already in final spelling. ### D-R6 — Adapter-owned reserved filenames (BINDING) Verified against upstream OKF v0.1 §3.1/§6/§7 (2026-07-19 audit): `index.md` (directory listing, progressive disclosure) and `log.md` (update history) are reserved structural files at every level of an **OKF bundle** and "MUST NOT be used for concept documents." This is an OKF adapter rule, not a cross-format rule derived from OKF. 1. **OKF.** The `okf` adapter never indexes those paths as concepts and refuses concept writes to them. Structural generation remains an OKF-specific bundle-maintenance operation. 2. **AKM and LLM Wiki.** These adapters independently reserve the same names under their own format contracts. Their recognition and write paths enforce those rules without treating the files as OKF. 3. **Other adapters.** Each adapter declares its own structural and reserved names. The ref grammar does not prohibit a concept ID solely because its final segment is `index` or `log`. ## 5. Spec amendments (exhaustive) 1. Normative §11.1: CLI/API short-ref resolution — replace "resolved … against the workspace `defaultBundle`" with the D-R4 defaultBundle-then-priority-order rule. 2. Normative §7.8/§11.2 (clarifying note, no rule change): "path within the bundle" is the item's path *as the adapter defines it*; a directory-item's path is its directory (skills). 3. Adapter spec §5.1: pin the akm adapter's conceptId to the D-R2 qualified spelling; drop the recognize/place spelling-tension note. 4. Plan §11 Chunk 5: insert F0/F1 (spelling pin + resolver/dual-reader step) ahead of the codemod; codemod and grep gates otherwise unchanged. 5. Adapter spec §5.1/§6 (+ Chunk-8 checklist): the D-R6 reserved-filename rule — `index.md`/ `log.md` are never recognized as items and never valid write targets; the akm adapter's exclusion + existing-file handling land with F4/Chunk 8. ## 6. Invariants check - **FTS5 schema + bm25 weights unchanged** — D-R2 keeps `name`/search fields on the bare canonical name; identity-only change (spec line 245 holds). - **§12.3 parity** — gated at F0 (must be no-op) and F4 (full run). - **Durable-state survival** — single stored spelling + F4 re-key with §11.4 orphan quarantine; relink/derived-twin/content-hash suites gate F4. - **Cycle ratchet ≤10 / fn-size shrink-only** — resolver is a new pure leaf; db trio exits the baseline at F4 per DoD 11. - **No new trust machinery** — resolution is name lookup only; `trusted` untouched. - **Frozen migrator** — never edited; it is where `type:name` parsing lives forever. - **Script-only codemod gate** — preserved by construction (D-R2 static table); hand edits are segregated in F3 exactly as plan §15 rule 2 requires.