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). ReturnsBundleRef { bundle?: string; conceptId; fragment? }. - resolve —
resolveRef(input: string | BundleRef, ctx: RefContext): ResolvedRefwhereResolvedRefisBundleRef & { bundle: string }(branded or structurally narrowed).ctxcarries 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 aResolvedRefalways 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:
- It makes the codemod purely textual.
type:name → stashDirFor(type)/nameis 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. - No cross-type identity collisions. Bare names make
skill:deployandworkflow:deploybothbundle//deploy— a behavior change (today'sentry_key = stashDir:type:namekeeps them distinct) and an unfixable ambiguity for the ~4,700 test literals. Qualified ids keep them distinct (skills/deployvsworkflows/deploy) with zero new machinery. - It is the spelling
placeNewalready 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). Onlyrecognize()and the additiveitem_refwriter disagree, and both are one-line derivations. - 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
typechanges.
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:
- Config key — the key in the Chunk-8
bundlesmap. Authoritative once Chunk 8 lands. registryId— for registry-installed sources (today's behavior,installations.ts:115).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
bundlesmap keyed by exactly what rule 2/3 derives today (registryId ?? slugForPath(path)), so no second identity migration happens at Chunk 8 — the §11.4type:name → conceptIdre-key is the only one. After migration, editing a bundle'spathdoesn't change its key; changing the key isakm 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 theitem_refwriter (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 theitem_refderivation; delete the spelling-tension NOTE atakm-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. AddresolveRef/RefContext/ResolvedRef(D-R1, D-R4). Repoint the seven readers (index-entries-repository.ts:41,178,245,386,534,727,755) to key onitem_ref, accepting BOTH grammars at the input edge (oldtype:nametranslated 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-twinsuffix logic moves to conceptId-suffix onitem_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:nameliterals intests/**(.ts + fixture JSON), per-type name normalization (markdown±.md),registryIdorigins carried as bundle ids,DESIGNATIONS.jsonpath/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 armstype-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 viaregistryIdor 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) overderiveInstallations;StashEntry→IndexDocumentrename;entry_key/stash_dir/entry_typedemoted,item_refbecomes THE key with the UNIQUE constraint (index-schema.ts:242note); 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 ontoitem_refvia the §11.4 join-against-last-good-index mapping with orphan quarantine.REF_REatbase-linter.ts:182retargets toBUNDLE_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_ALIASESdeleted (~216 sites already repointed by F1/F4); the F1 old-grammar reader shim deleted; frozenscripts/akm-migrate/migrate/legacy/legacy-layout.tsuntouched. Gate: grepparseAssetRef→ 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.
- OKF. The
okfadapter never indexes those paths as concepts and refuses concept writes to them. Structural generation remains an OKF-specific bundle-maintenance operation. - 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.
- 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
indexorlog.
5. Spec amendments (exhaustive)
- Normative §11.1: CLI/API short-ref resolution — replace "resolved … against the workspace
defaultBundle" with the D-R4 defaultBundle-then-priority-order rule. - 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).
- Adapter spec §5.1: pin the akm adapter's conceptId to the D-R2 qualified spelling; drop the recognize/place spelling-tension note.
- Plan §11 Chunk 5: insert F0/F1 (spelling pin + resolver/dual-reader step) ahead of the codemod; codemod and grep gates otherwise unchanged.
- Adapter spec §5.1/§6 (+ Chunk-8 checklist): the D-R6 reserved-filename rule —
index.md/log.mdare 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;
trusteduntouched. - Frozen migrator — never edited; it is where
type:nameparsing 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.