Audience: Contributors, LLM co-developers
Purpose: Everything you need to build a new Concept Model forlib/akasha/concepts/
Version: 1.3 — Akasha kernel seriesseeds
A Concept Model is a higher-order graph constructor that lives on top of the raw Cortex. The Cortex knows only atoms (chunks) and links — it has no understanding of "a document" or "a task list" or "a research thread". Concept Models give semantic shape to those raw nodes.
The dispatch chain is:
Shell command ──► kernel dispatch() ──► ConceptModel.dispatch(op, params)
│
op_*(self, **params)
│
cortex.put_chunk / put_link / …
Every concept model must:
- Inherit from
BaseConcept. - Expose all behaviour exclusively through
op_*methods. - Write to the Cortex only via
self.cortex.*orself.create_structured_chunk(). - Never touch the filesystem, the shell, or the network directly.
- Call
self._require_concept()at the top of everyop_*method that requires an initialized instance (i.e. all operators exceptop_new).
File: lib/akasha/concepts/base.py
BaseConcept(session, concept_id: Optional[str] = None, namespace: Optional[str] = None)| Attribute | Source | Description |
|---|---|---|
self.session |
arg | The kernel Session object. Carries client_id, active_scopes, and a context dict. |
self.cortex |
session.cortex or session.local_cortex |
AkashaEngine instance. All Cortex I/O goes here. |
self.concept_id |
arg | Stable string ID for this concept instance (a chunk hash or an alias). None until op_new sets it. |
self.set_name |
derived | f"set:concept:{self.concept_id}" — the general concept catalog set. None if concept_id not yet set. |
self.namespace |
arg | Optional string (e.g. "loom"). When set, all session context keys are prefixed with "{namespace}:". See §2.3. |
Staging state (see §2.7):
self.staged_changes: Dict[str, Dict] # key → {content, meta, action_type}
self.staged_links: List[Tuple] # (src, dst, rel, w)
self.undo_stack: List[Dict]
self.redo_stack: List[Dict]def dispatch(self, operator: str, params: Dict[str, Any]) -> Any"foo.bar" → calls self.op_foo_bar(**params).
- The kernel calls this. Never call
dispatchfrom inside a concept. - All
op_*methods must accept their parameters as keyword arguments. - If an operator is not implemented,
dispatchraisesNotImplementedError.
@property
def allowed_scopes(self) -> List[str]Returns session.active_scopes — the IAM scope list computed for the authenticated client.
Pass this whenever a Cortex method takes a scopes or requester_scopes argument.
Reading and writing session context (for stateful concepts) — always use _ctx_key():
ctx_val = getattr(self.session, "get_context", lambda k: None)(self._ctx_key("some_key"))
if hasattr(self.session, "set_context"):
self.session.set_context(self._ctx_key("some_key"), value)_ctx_key(key) returns "{namespace}:{key}" when a namespace is active, or bare key when not.
Never pass raw string literals to get_context / set_context — always route through _ctx_key.
def _require_concept(self)Raises RuntimeError if self.concept_id is None. Call at the top of every op_*
method that requires the concept to be initialized. This is defined in BaseConcept — do
not redefine it in subclasses.
def op_add(self, text: str) -> Dict[str, Any]:
self._require_concept() # ← required in every op_* except op_new
...def register_concept_node(self, key: str, subset_suffix: Optional[str] = None)Adds key to:
self.set_name(set:concept:{concept_id}) — the general concept catalog for this instancef"{self.set_name}:{subset_suffix}"(if suffix provided)
This is the canonical way to populate the concept catalog set, which serialize_concept
uses for export and which deletion sweeps scan. Note: this registers the concept-word atoms,
not the content atoms (see §3.3 for the distinction).
Human readability is the concept model's first priority. The catalog set key is
set:concept:<hash> — a bare hash. When a client warps to a concept, that set is the focus,
so every read surface (Cosmos FOCAL LOCK / node labels / hover, the cockpit wake, sim /
node.sim labels) would show set:concept:a48c51e1… unless something makes it readable.
BaseConcept fixes this at the source: it gives the catalog set a real alias.
def alias_concept_set(self, name: Optional[str] = None) -> Optional[str] # register concept:<slug(name)>
def ensure_concept_set(self) -> None # create_set + alias, one stepalias_concept_set()registersconcept:<slug(name)>onself.set_name. Whennameis omitted it is derived from the concept root's meta (name/title) or content head ("[ Survey: Foo ]"→Foo) — so callers usually pass nothing.- Two hooks apply it automatically, so a model rarely calls it directly:
register_concept_nodecalls it (idempotent) — the universal hook: it fires for every model that registers a catalog node, including those that build the set implicitly viaadd_to_set(e.g.note,log).ensure_concept_set()is the drop-in forself.cortex.create_set(self.set_name)inop_new— use it so the alias is applied at creation even for models that register no node at the root.
- Invariants: collision-safe (first-wins — never steals an alias already bound to another
key, so an ontology
concept:appleatom keeps its name) and side-effect-free (a rawcore.put_aliasrow, deliberately not theset_aliasproto-word / collection-derivation machinery — a set is not an atom). Idempotent; safe to call on everyop_new/op_open.
When authoring a new model: call self.ensure_concept_set() in op_new instead of
self.cortex.create_set(self.set_name), and (optionally) self.alias_concept_set() in
op_open to back-fill pre-existing instances. Readability then comes for free.
Beacon focal preview (Cockpit). A related readability rule: any atom persisted as a reference to a focal point should store a readable
focal_preview(the focal atom's content head), not only its key — see the Cockpitop_drop_beacon/op_wake(§10.1), so the Trace Deck showsLOC: Rome, not a hash.
def create_structured_chunk(
self,
content: str,
role: str,
author_id: str,
scopes: List[str],
parent_set_id: Optional[str] = None
) -> Dict[str, Any]Returns {"chunk_id": str, "chunk_set": f"set:chunk:{chunk_id}"}.
Internally:
- Calls
cortex.put_chunk(content, meta, author, scopes)— immediate write. - Registers the new node to
self.set_nameviaregister_concept_node. - Also registers to
parent_set_idif given. - Creates
set:chunk:{chunk_id}— a private inner-element bucket for this atom.
This is the preferred entry point for materializing content atoms; it handles all standard housekeeping automatically.
Note for NoteConcept subclasses:
create_structured_chunkcallsregister_concept_nodewhich adds the content atom toset:concept:*. In the dual- namespace design (§3.3), content atoms should not be inset:concept:*. Use_register_to_packagewithconcept_wordfor structural nodes, and callcreate_structured_chunkonly for raw content chunks where the dual-namespace rule does not apply (theparent_set_idcorrectly routes them toset:note:*).
def annotate_span(
self,
parent_chunk_key: str,
start_char: int,
end_char: int,
annotation_text: str,
role: str = "annotation",
target_concept_id: Optional[str] = None
) -> str # returns annotation atom key
def get_span_annotations(self, parent_chunk_key: str) -> List[Dict[str, Any]]Span annotations map to character offsets within a parent atom.
Links created: sys:has_annotation_span, sys:included, optionally sys:associated_with.
get_span_annotations returns results sorted by start offset.
In-memory undo/redo before a final bulk commit. Use for interactive multi-step editing.
| Method | Effect |
|---|---|
stage_change(key, content, meta, action_type) |
Queue a node mutation |
stage_link(src, dst, rel, w) |
Queue a link creation |
undo() |
Pop undo stack, revert staged state |
redo() |
Re-apply last undone action |
commit_staged() |
Flush all staged changes to Cortex; handles hash-remapping |
commit_staged returns {"status", "nodes_written", "links_woven", "mutations"}.
Boundary warning: Staging is per-concept in-process state. It is not a Harmonia workspace transaction. If a concept is used inside a JCL job and the job rolls back, staged commits that already reached the Cortex will not be reversed. Stage only within a single JCL step, or call
commit_stagedbefore the Harmonia rollback boundary.
def serialize_concept(self) -> Dict[str, Any]
def hydrate_concept(self, schema_data: Dict[str, Any]) -> str
def export_concept(self, format_type: str, **kwargs) -> Any
def import_concept(self, raw_data: Any, format_type: str, **kwargs) -> strserialize_concept produces the canonical transfer schema:
{
"specification": "akasha_concept_schema_v1.0",
"concept_type": "MyConceptClass",
"concept_id": "...",
"atoms": { "<key>": { "content": "...", "meta": {...} } },
"links": [ { "src": "...", "dst": "...", "rel": "...", "w": 1.0 } ]
}Export/import for "json" format is built-in. Other formats ("markdown", "pdf") require
a plugin registered at self.cortex._plugins["transform.export.<format>"].
File: lib/akasha/concepts/note.py
NoteConcept is the canonical reference for a stateful, structured concept with dual- namespace set management and a sequential timeline topology.
def __init__(self, session, concept_id=None):
super().__init__(session, concept_id)
if not self.concept_id:
active_root = getattr(self.session, "get_context", lambda k: None)("active_note_root")
if active_root:
self.concept_id = active_root
self.set_name = f"set:concept:{self.concept_id}"If the kernel doesn't supply a concept_id, NoteConcept reads the session context key
"active_note_root". This allows commands like n.add to work without explicitly
referencing the note — the session remembers which note is open.
Pattern: Use session context for "active resource" tracking. op_new sets it;
every subsequent operator reads it via _require_concept + auto-mount.
NoteConcept uses two orthogonal set namespaces that serve different purposes. The same physical atom must never appear in both.
| Namespace | Contents | Purpose |
|---|---|---|
set:note:{root_id} |
Content atoms — the actual sections, paragraphs, and chunks of this note | Note-model scope: reading, traversal, sequential output |
set:concept:{root_id} |
Concept-word atoms — general vocabulary terms like "section", "paragraph", "document" |
General concept catalog: serialization, cross-model vocabulary |
Note-scope set helpers:
def _get_set_name(self, suffix: str = "") -> str:
return f"set:note:{self.concept_id}:{suffix}" if suffix else f"set:note:{self.concept_id}"Sub-sets created at initialization:
set:note:{root_id}— master content membershipset:note:{root_id}:sectionsset:note:{root_id}:paragraphsset:section:{sec_id},set:paragraph:{para_id}— per-node inner buckets
General concept-word atoms represent structural roles (e.g. "section", "paragraph") as
standalone vocabulary atoms, independent of any particular note document.
content_atom("Introduction", role=section)
│
│ sys:derived_from (one-directional)
▼
concept_word_atom("section", type=concept_word)
│
aliased: concept:word:section
registered: set:concept:{root_id}
The note model's usage of "section" derives from the general concept, but may differ in meaning or scope. The reverse link is intentionally absent — the general concept does not reference back into any specific note.
Implementation:
_CONCEPT_WORD_ALIAS_PREFIX = "concept:word:"
def _get_or_create_concept_word(self, word: str) -> str:
alias = f"{_CONCEPT_WORD_ALIAS_PREFIX}{word}"
existing = self.cortex.resolve_alias(alias)
if existing:
return existing
# Create a new general concept-word atom in the user's private scope
key = self.cortex.put_chunk(
content=word,
meta={"type": "concept_word", "word": word, "concept_model": "note", ...},
author=author_id, scopes=scopes
)
self.cortex.set_alias(key, alias)
return key
def _register_to_package(self, key: str, subset_suffix=None, concept_word=None):
# 1. Content atom → note-scope only
self.cortex.add_to_set(self._get_set_name(), key)
if subset_suffix:
self.cortex.add_to_set(self._get_set_name(subset_suffix), key)
# 2. Concept-word atom → concept catalog (separate atom, one-directional link)
if concept_word:
cw_key = self._get_or_create_concept_word(concept_word)
self.register_concept_node(cw_key) # → set:concept:{root_id}
self.cortex.put_link(key, cw_key, "sys:derived_from", author=author_id)concept_word values passed by each operator:
| Operator | concept_word |
|---|---|
op_new |
"document" |
op_section |
role value (e.g. "section", "chapter", "appendix") |
op_paragraph |
"paragraph" |
op_add_chunk |
(none — content chunks use create_structured_chunk directly) |
NoteConcept maintains two orthogonal structures simultaneously:
HORIZONTAL (timeline)
root ──sys:top──► node_A ──sys:next──► node_B ──sys:next──► node_C
root ──sys:bottom────────────────────────────────────────────► node_C
(each node also carries sys:previous pointing back)
VERTICAL (hierarchy)
root ──sys:contains──► chapter_1 ──sys:contains──► section_1_1
section_1_1 ──sys:part_of──► chapter_1
The horizontal timeline enables flat sequential reading (op_get_sequential_text).
The vertical hierarchy enables TOC traversal (op_toc).
_append_to_timeline manages the sys:bottom floating pointer using the Cortex
abstraction layer — no raw SQL:
def _append_to_timeline(self, node_id: str, author_id: str):
tail_links = self.cortex.get_adjacent_links(self.concept_id, "sys:bottom")
if not tail_links:
self.cortex.put_link(self.concept_id, node_id, "sys:top", author=author_id)
else:
last_node_id = tail_links[0][0]
self.cortex.put_link(last_node_id, node_id, "sys:next", author=author_id)
self.cortex.put_link(node_id, last_node_id, "sys:previous", author=author_id)
self.cortex.remove_link(self.concept_id, last_node_id, "sys:bottom")
self.cortex.put_link(self.concept_id, node_id, "sys:bottom", author=author_id)| Shell command | Kernel method | op_* method |
Parameters |
|---|---|---|---|
n.new |
note.new |
op_new |
title: str, author?: str, isbn?: str |
n.add |
note.add |
op_add_chunk |
text: str, role?: str |
n.sec / n.chap |
note.section |
op_section |
title: str, role?: str |
n.para |
note.paragraph |
op_paragraph |
category?: str |
n.toc |
note.toc |
op_toc |
(none) |
n.read |
note.read |
op_get_sequential_text |
(none) |
n.rm |
note.rm |
op_delete |
(none) |
The template below reflects all patterns established across the bundled concept
models. Copy it verbatim and search-replace mything / MyThing / mt.
"""
MyThing Concept Model.
Brief description of what this concept models and its topology.
Namespace contract (two-namespace rule):
- Content atoms → set:mything:{concept_id} AND set:mything:{concept_id}:{subset}
- Concept-word atom → set:concept:{concept_id} (concept catalog scope)
"""
import time
import logging
from typing import List, Dict, Any, Optional
from lib.akasha.concepts.base import BaseConcept
logger = logging.getLogger("Harmonia.Concept.MyThing")
# Session context key — must be unique across all concept models
CONTEXT_KEY_ACTIVE = "active_mything_root"
# Global index set — all root IDs across all sessions
INDEX_SET = "set:mything:index"
_CONCEPT_WORD_ALIAS_PREFIX = "concept:word:"
class MyThingConcept(BaseConcept):
"""One-line description of what this concept represents."""
# ── Plugin registry contract ───────────────────────────────────────────────
CONCEPT_PREFIX = "mt"
CONCEPT_METHODS = {
"new": {"op": "op_new"},
# coerce normalises both "mything_id" (API return) and "mt_id" (shorthand)
"open": {"op": "op_open",
"coerce": lambda d: {
"mything_id": d.get("mything_id") or d.get("mt_id", ""),
}},
"ls": {"op": "op_list_all"}, # list ALL instances (scans INDEX_SET)
"add": {"op": "op_add"},
"list": {"op": "op_list"}, # list structure of the ACTIVE instance
"rm": {"op": "op_delete"},
}
# ── Constructor ───────────────────────────────────────────────────────────
def __init__(self, session: Any, concept_id: Optional[str] = None, namespace: Optional[str] = None):
super().__init__(session, concept_id, namespace=namespace)
# Auto-mount: if kernel didn't pass a concept_id, recover from session context
if not self.concept_id:
stored = getattr(self.session, "get_context", lambda k: None)(self._ctx_key(CONTEXT_KEY_ACTIVE))
if stored:
self.concept_id = stored
self.set_name = f"set:concept:{self.concept_id}"
# ── Private helpers ───────────────────────────────────────────────────────
def _mt_set(self, suffix: Optional[str] = None) -> str:
base = f"set:mything:{self.concept_id}"
return f"{base}:{suffix}" if suffix else base
def _author_and_scopes(self):
author_id = getattr(self.session, "client_id", "system")
scopes = [f"owner:user_{author_id}", f"view:user_{author_id}"]
return author_id, scopes
def _get_or_create_concept_word(self, word: str) -> str:
alias = f"{_CONCEPT_WORD_ALIAS_PREFIX}{word}"
existing = self.cortex.resolve_alias(alias)
if existing:
return existing
author_id, scopes = self._author_and_scopes()
key = self.cortex.put_chunk(
content=word,
meta={"type": "concept_word", "word": word,
"concept_model": "mything", "created_at": time.time()},
author=author_id, scopes=scopes,
)
self.cortex.set_alias(key, alias)
return key
def _register(self, key: str, subset_suffix: Optional[str] = None,
concept_word: Optional[str] = None) -> None:
"""Dual-namespace: content atom → mything-scope; concept-word → catalog."""
author_id, _ = self._author_and_scopes()
self.cortex.add_to_set(self._mt_set(), key) # main content set
if subset_suffix:
self.cortex.add_to_set(self._mt_set(subset_suffix), key)
if concept_word:
cw_key = self._get_or_create_concept_word(concept_word)
self.register_concept_node(cw_key) # concept catalog set
self.cortex.put_link(key, cw_key, "sys:derived_from", author=author_id)
def _require_access(self, atom_id: str, label: str = "Atom") -> None:
"""Access guard: raises RuntimeError if atom_id is not readable by this session."""
if not self.cortex.check_access(atom_id, self.allowed_scopes):
raise RuntimeError(f"{label} not accessible: {atom_id[:12]}")
def _visible_members(self, suffix: str) -> List[str]:
"""Return accessible members of a subset, filtered by IAM."""
return [
k for k in self.cortex.get_collection_members(self._mt_set(suffix))
if self.cortex.check_access(k, self.allowed_scopes)
]
# ── Operators ─────────────────────────────────────────────────────────────
def op_new(self, title: str) -> Dict[str, Any]:
"""[mt.new] Create a new MyThing root."""
# op_new is the only operator that does NOT call _require_concept
author_id, scopes = self._author_and_scopes()
root_id = self.cortex.put_chunk(
content=f"[ MyThing: {title} ]",
meta={
"type": "concept", # always "concept" on roots
"concept": "mything", # validated in op_open — use this field
"role": "root",
"title": title,
"created_at": time.time(),
},
author=author_id, scopes=scopes,
)
self.concept_id = root_id
self.set_name = f"set:concept:{self.concept_id}"
# ORDERING RULE: create ALL sets before any call that writes to them.
self.ensure_concept_set() # concept catalog set + readable alias (§2.5.1)
self.cortex.create_set(self._mt_set()) # main content set
self.cortex.create_set(self._mt_set("items")) # per-subset sets
self.cortex.create_set(INDEX_SET) # global index (idempotent)
self.cortex.add_to_set(INDEX_SET, root_id)
# Register AFTER sets exist (no concept_word — root lives in content set only)
self._register(root_id)
if hasattr(self.session, "set_context"):
self.session.set_context(self._ctx_key(CONTEXT_KEY_ACTIVE), root_id)
logger.info("[MyThingConcept] Created '%s' (%s)", title, root_id[:8])
return {"status": "created", "mything_id": root_id, "title": title}
def op_open(self, mything_id: str) -> Dict[str, Any]:
"""[mt.open] Mount an existing MyThing as the session's active instance."""
# Validate using "concept" field, NOT "type" (all roots have type="concept")
meta = self.cortex.get_meta(mything_id)
if not meta or meta.get("concept") != "mything":
raise RuntimeError(f"Atom '{mything_id[:12]}' is not a mything root.")
self.concept_id = mything_id
self.set_name = f"set:concept:{self.concept_id}"
if hasattr(self.session, "set_context"):
self.session.set_context(self._ctx_key(CONTEXT_KEY_ACTIVE), mything_id)
return {"status": "opened", "mything_id": mything_id, "title": meta.get("title", "")}
def op_list_all(self) -> Dict[str, Any]:
"""[mt.ls] List all MyThing roots accessible to this session."""
members = self.cortex.get_collection_members(INDEX_SET)
items = []
for key in members:
if not self.cortex.check_access(key, self.allowed_scopes):
continue
meta = self.cortex.get_meta(key) or {}
if meta.get("concept") != "mything":
continue
items.append({"mything_id": key, "title": meta.get("title", ""),
"created_at": meta.get("created_at", 0)})
items.sort(key=lambda x: x.get("created_at", 0), reverse=True)
return {"mythings": items, "count": len(items)}
def op_add(self, text: str) -> Dict[str, Any]:
"""[mt.add] Add a content item to the active MyThing."""
self._require_concept()
author_id, scopes = self._author_and_scopes()
item_id = self.cortex.put_chunk(
content=text,
meta={"type": "mt_item", "created_at": time.time()},
author=author_id, scopes=scopes,
)
# Sub-atom goes into BOTH main set and sub-set (never sub-set only)
self._register(item_id, subset_suffix="items", concept_word="item")
self.cortex.put_link(self.concept_id, item_id, "mt:item", author=author_id)
return {"status": "added", "item_id": item_id}
def op_list(self) -> Dict[str, Any]:
"""[mt.list] Return structural inventory of the active MyThing."""
self._require_concept()
return {
"mything_id": self.concept_id,
"items": self._visible_members("items"),
}
def op_delete(self) -> Dict[str, Any]:
"""[mt.rm] Delete the active MyThing root and clear session context."""
self._require_concept()
target = self.concept_id
self.cortex.drop_chunk(target, requester_scopes=self.allowed_scopes)
if hasattr(self.session, "set_context"):
self.session.set_context(CONTEXT_KEY_ACTIVE, None)
return {"status": "deleted", "mything_id": target}Key differences from a naive implementation, all enforced by the patterns above:
CONCEPT_PREFIX+CONCEPT_METHODSat class level — required for auto-discovery.__init__auto-mount — always override to restore session context.coerceonopen— normalise the param name to match what the API returns.ls→op_list_allscansINDEX_SET;list→op_listreturns the current instance's structure. Both are mandatory.- Set creation order in
op_new— ALL sets created before ANY_registerorregister_concept_nodecall. INDEX_SETcreated withcreate_set(idempotent) beforeadd_to_set._registeradds to BOTH main set AND sub-set._require_accesscalled on every input atom ID supplied by the caller._visible_membersfilters by IAM — never return rawget_collection_membersoutput.- Root meta uses
"concept": "mything"—op_openvalidates on this field. - Sub-atom
typeuses"{prefix}_{role}"naming (e.g."mt_item"). op_deleteclears session context (sets toNone).
See §4.3 for the full bug catalogue that motivated these patterns.
Use this before opening a PR or handing a concept model off for review.
Structure
- Inherits
BaseConcept; no methods copied from it. -
CONCEPT_PREFIX(str) andCONCEPT_METHODS(dict) defined at class level. -
__init__overrides and restoresCONTEXT_KEY_ACTIVEfrom session. - All public behaviour exposed as
op_*methods accepting keyword arguments only.
op_new
- Root meta has
"type": "concept","concept": "<name>","role": "root". -
self.ensure_concept_set()called (after the root atom exists) BEFORE any_registercall — creates the catalog set AND its human-readable alias (§2.5.1). -
INDEX_SETcreated withcreate_set(idempotent) beforeadd_to_set. - All content sets (
_mt_set(),_mt_set("items"), …) created before any writes. -
_registerorregister_concept_nodecalled ONLY AFTER all sets exist. - Session context written via
session.set_context(CONTEXT_KEY_ACTIVE, root_id). - Return dict includes all key fields (
title, etc.) thatop_openwould return.
op_open
- Validates with
meta.get("concept") != "<name>", notmeta.get("type"). -
get_metareturn guarded againstNone:meta = ... or {}. -
coercelambda inCONCEPT_METHODS["open"]normalises both the short ID key (e.g.mt_id) and the full API return key (e.g.mything_id). - Writes
CONTEXT_KEY_ACTIVEto session on success.
op_list_all (mapped to ls)
- Scans
INDEX_SET, notself.set_name. - Filters each member with
check_accessbefore including in results. - Skips members whose
meta.get("concept")doesn't match.
Sub-atom operators (op_add_*)
- Every input atom ID from the caller guarded with
_require_access(id, label). - Sub-atom added to BOTH main set (
_mt_set()) AND sub-set (_mt_set("items")). - Sub-atom
typeuses"{prefix}_{role}"convention (e.g."mt_item").
op_list / op_map
- Uses
_visible_members(suffix)(withcheck_accessfilter), never rawget_collection_members.
op_delete (mapped to rm)
- Calls
drop_chunkwithrequester_scopes=self.allowed_scopes. - Sets
CONTEXT_KEY_ACTIVEtoNonein session.
General
- Every
op_*exceptop_newcallsself._require_concept()as first statement. - All
put_chunkcalls passscopesderived fromsession.client_id. -
CONTEXT_KEY_ACTIVEandINDEX_SETdefined as module-level constants. - No filesystem, network, or shell access.
- No import of
kernel.pyorrouter.py. - IAM routing added to
_METHOD_TO_ACTIONinkernel.py(§7.2).
The following bugs have been encountered in real concept model implementations. Each entry gives the symptom, the root cause, and the fix.
Symptom: op_new raises a Cortex error about a missing set, or concept-word
atoms are registered into a non-existent set.
Root cause: self.cortex.create_set(self.set_name) was called AFTER
_register_to_package or register_concept_node — which write to self.set_name.
Fix: Create ALL sets (self.set_name, INDEX_SET, all content sets) before any
call that writes to them.
# ✗ WRONG — set_name not yet created when _register runs
self._register(root_id)
self.cortex.create_set(self.set_name)
# ✓ CORRECT — all sets created first
self.cortex.create_set(self.set_name)
self.cortex.create_set(self._mt_set())
self.cortex.create_set(INDEX_SET)
self._register(root_id) # safe nowSymptom: op_list / op_map returns empty results even after adding items.
The sub-set (set:mt:{id}:items) has members but set:mt:{id} is empty.
Root cause: _register only added the atom to the sub-set, skipping the main
content set.
Fix: Always add to BOTH main set and sub-set.
# ✗ WRONG — main set never updated
self.cortex.add_to_set(self._mt_set("items"), item_id)
# ✓ CORRECT
self.cortex.add_to_set(self._mt_set(), item_id) # main set
self.cortex.add_to_set(self._mt_set("items"), item_id) # sub-setSymptom: op_open raises "is not a mything root" for valid roots, OR passes
silently for the wrong atom type.
Root cause: All concept roots have "type": "concept". The discriminating field
is "concept".
Fix:
# ✗ WRONG — all roots have type "concept"; this never catches wrong roots
if meta.get("type") != "mything_root":
raise RuntimeError(...)
# ✓ CORRECT
if not meta or meta.get("concept") != "mything":
raise RuntimeError(...)Symptom: mt.open fails with a parameter error when called with the ID key
returned by mt.new (e.g. {"mything_id": "..."} fails because op_open expects
{"mt_id": "..."}).
Root cause: op_new returns "mything_id" but op_open takes mt_id as its
parameter, and no coerce lambda normalises the name.
Fix: Add a coerce lambda in CONCEPT_METHODS["open"]:
"open": {"op": "op_open",
"coerce": lambda d: {
"mything_id": d.get("mything_id") or d.get("mt_id", ""),
}},Symptom: A caller can pass any atom ID and have it silently added to the concept, bypassing IAM — a security and data-integrity issue.
Root cause: op_add_* methods do not call _require_access on the IDs
supplied by the caller.
Fix: Call _require_access(atom_id, "label") on every user-supplied atom ID
before using it.
def op_add_item(self, ref_id: str) -> Dict[str, Any]:
self._require_concept()
self._require_access(ref_id, "Item atom") # ← required
...Symptom: AttributeError: 'NoneType' object has no attribute 'get' when
accessing metadata on a root or any atom that may not exist.
Root cause: self.cortex.get_meta(key) returns None if the atom is not found.
Fix:
# ✗ WRONG
root_meta = self.cortex.get_meta(self.concept_id)
allowed = root_meta.get("context_universes", []) # crashes if None
# ✓ CORRECT
root_meta = self.cortex.get_meta(self.concept_id) or {}
allowed = root_meta.get("context_universes", [])Symptom: op_list / op_map leaks atom IDs that the current session is not
authorised to see.
Root cause: Raw get_collection_members output returned directly, without
check_access filtering.
Fix:
# ✗ WRONG
return list(self.cortex.get_collection_members(self._mt_set("items")))
# ✓ CORRECT
return [
k for k in self.cortex.get_collection_members(self._mt_set("items"))
if self.cortex.check_access(k, self.allowed_scopes)
]Symptom: op_open validation always fails (or passes incorrectly); concept catalog
queries return unexpected atoms.
Root cause: Root atom created without "concept": "<name>" in meta — relying
only on "type": "concept_root" or a custom type string.
Fix: Root put_chunk meta must always contain:
meta={
"type": "concept", # always literal "concept"
"concept": "mything", # the discriminating field
"role": "root",
...
}Symptom: A second call in the same session (e.g. mt.add) fails with "No active
concept" even though mt.new was called moments before.
Root cause: The subclass does not override __init__, so BaseConcept.__init__
never reads CONTEXT_KEY_ACTIVE from the session — the session context written by
op_new is never recovered.
Fix: Always override __init__:
def __init__(self, session, concept_id=None):
super().__init__(session, concept_id)
if not self.concept_id:
stored = getattr(self.session, "get_context", lambda k: None)(CONTEXT_KEY_ACTIVE)
if stored:
self.concept_id = stored
self.set_name = f"set:concept:{self.concept_id}"Symptom: Users cannot discover existing instances or delete them; the concept is inconsistent with other models in the shell.
Root cause: op_list_all and/or op_delete not implemented.
Fix: Always implement both, mapped as "ls" and "rm" in CONCEPT_METHODS.
Symptom: Filtering by meta.get("type") in client code is unreliable; types
from different concept models collide (e.g. two models both use "node").
Root cause: Sub-atom type field uses a generic name instead of a namespaced one.
Fix: Always prefix the type with the concept prefix:
"mt_item", "pres_frame", "agg_measure", "synth_code", etc.
Every concept model uses two categories of sets, each holding a different kind of atom:
| Category | Default naming | Populated by | Purpose |
|---|---|---|---|
| Content set | set:<concept>:{root_id} |
_register_to_package, create_structured_chunk with parent_set_id |
Stores the actual content atoms (text, structure) of this concept instance |
| Concept catalog | set:concept:{root_id} (self.set_name) |
register_concept_node |
Stores general concept-word atoms; used by serialize_concept |
Rule: A physical atom belongs to exactly one category. The same atom is never in both.
Root atom corollary: The concept root atom belongs to the content set only. Never pass concept_word= when calling _register(root_id) — the root is not a concept-word node.
Concept-word atoms are stable, reusable vocabulary atoms. They represent structural or semantic roles as general concepts, independent of any specific instance. Properties:
- Content: the role word itself (e.g.
"section") - Meta:
{"type": "concept_word", "word": "section", "concept_model": "note"} - Alias:
concept:word:section— stable lookup key across sessions - Scope: user's private scope (can be promoted to collective by LIBRARIAN)
- Lifetime: shared; one atom per word, not one per note instance
Every content atom that has a concept-word counterpart carries a sys:derived_from link:
content_atom ──sys:derived_from──► concept_word_atom
This is one-directional. The concept word does not reference back into specific instances because the general concept may be used differently across models and documents.
These are the AkashaEngine methods a concept model may call:
| Method | Signature | Notes |
|---|---|---|
put_chunk |
(content, meta, author, scopes) → key |
Primary atom write. Returns a content-addressed hash key. |
get_chunk |
(key) → str |
Read atom content. |
get_meta |
(key) → dict |
Read atom metadata. |
drop_chunk |
(key, requester_scopes) → None |
Physical deletion. Always pass requester_scopes=self.allowed_scopes. |
put_link |
(src, dst, rel, w?, author?) → None |
Create a directed link. |
remove_link |
(src, dst, rel) → None |
Remove a specific directed link. Use this — never raw SQL. |
get_adjacent_links |
(key, rel?) → List[(dst, rel)] |
Outgoing links, optionally filtered by rel. |
get_incoming_links |
(key, rel?) → List[(src, rel)] |
Incoming links, optionally filtered by rel. |
create_set |
(set_name) → None |
Initialize a set bag. |
add_to_set |
(set_name, key) → None |
Add a member to a set. |
remove_from_set |
(set_name, key) → None |
Remove a member from a set. |
get_collection_members |
(set_name) → List[str] |
List all members of a set. |
check_access |
(key, scopes) → bool |
IAM access check for a single atom. |
set_alias |
(key, alias) → None |
Assign a human-readable alias to an atom. |
resolve_alias |
(alias) → Optional[str] |
Look up a key by alias. |
| Method | Signature | Notes |
|---|---|---|
session.client_id |
str property |
The authenticated client identifier. |
session.active_scopes |
List[str] property |
IAM scope list. Same as self.allowed_scopes. |
session.get_context(key) |
(str) → Any |
Read a transient session value. |
session.set_context(key, val) |
(str, Any) → None |
Write a transient session value. |
No changes to kernel.py are required.
Drop your Python file into lib/akasha/concepts/. The Concept Model Plugin Registry
(lib/akasha/concepts/registry.py) auto-discovers all eligible classes at kernel startup.
A class is eligible if it defines both CONCEPT_PREFIX and CONCEPT_METHODS:
class MyThingConcept(BaseConcept):
CONCEPT_PREFIX = "mything"
CONCEPT_METHODS = {
# suffix → op method name, or spec dict with "op" and optional "coerce"
"new": {"op": "op_new"},
"add": {"op": "op_add"},
"ls": {"op": "op_list"},
"rm": {"op": "op_delete"},
# Example with coerce — use when JSON param names differ from op arg names:
"open": {
"op": "op_open",
"coerce": lambda d: {"mything_id": d.get("id") or d.get("mything_id", "")},
},
}At startup, ConceptRegistry.discover(concepts_dir) scans the directory, imports each
*.py file that is not a dunder file, and registers every top-level class that has both
CONCEPT_PREFIX and CONCEPT_METHODS. The full method name dispatched in JSON-RPC is
"{CONCEPT_PREFIX}.{suffix}".
Exception mapping inside the registry:
| Python exception | JSON-RPC code | Typical cause |
|---|---|---|
RuntimeError |
-32002 |
No active concept (_require_concept), not found, precondition |
TypeError, ValueError |
-32602 |
Invalid or missing parameter |
NotImplementedError |
-32601 |
Op method exists in CONCEPT_METHODS but not on class |
| Any other | -32603 |
Internal error |
Concept model commands are intentionally hidden from the main help output.
Document each model's API in docs/concept-model-spec.md (this file).
Add each method to _METHOD_TO_ACTION in kernel.py so IAM enforces read/write/drop
permissions on concept model commands:
"mything.new": "write",
"mything.add": "write",
"mything.ls": "read",
"mything.rm": "drop",Add short aliases to api/router.py:
"mt.new": "mything.new",
"mt.add": "mything.add",
"mt.ls": "mything.ls",
"mt.rm": "mything.rm",The Session Instance Layer (Layer 3, lib/akasha/session/space.py) lets a client mount
concept models as named instances inside a virtual space. Concept models do not need to
know about SpaceConcept — they only need to follow two conventions, and SpaceConcept
handles the rest.
A concept model reaches the session in one of two ways:
| Mode | Used by | How binding is set |
|---|---|---|
| Direct | Simple single-instance models (NoteConcept, etc.) | op_new / op_open call session.set_context themselves |
| Space-managed | Multi-instance models (CastConcept, etc.) | instance.mount / instance.focus call session.set_context via SpaceConcept |
A model can support both simultaneously: op_new sets context for standalone use;
SpaceConcept overwrites it on instance.focus.
Any concept model that can be managed by SpaceConcept must expose its active-instance
context key as a class attribute named CONTEXT_KEY_ACTIVE.
# 1. Module-level constant — readable by anyone importing this module
CONTEXT_KEY_ACTIVE = "active_mything_root"
class MyThingConcept(BaseConcept):
CONCEPT_PREFIX = "mything"
# 2. Class attribute — readable by SpaceConcept via cls.CONTEXT_KEY_ACTIVE
CONTEXT_KEY_ACTIVE = CONTEXT_KEY_ACTIVEBaseConcept defines this as None by default. If a model leaves it None, SpaceConcept
cannot set focus for it; instance.focus on that slot raises an error.
When instance.focus slot="bot" is called:
- SpaceConcept reads the slot's
modelandconcept_id. - Resolves the plugin class via
registry.get_class(model). - Reads
cls.CONTEXT_KEY_ACTIVEto get the key name. - Calls
session.set_context(key, concept_id).
The concept model itself is never called — only the session context key is updated.
When instance.mount model=mything slot=X name="..." is called without an id
parameter, SpaceConcept creates a new instance by dispatching mything.new. It then
reads the returned dict's "concept_id" key to know the new root atom's ID.
def op_new(self, title: str, ...) -> Dict[str, Any]:
...
return {
"status": "created",
"concept_id": root_id, # required — SpaceConcept reads this key
"mything_id": root_id, # optional alias for CLI output
"title": title,
}Missing concept_id in the return dict causes instance.mount to raise a RuntimeError.
Every model that maintains an active instance should set session context directly in
op_new and op_open. This supports standalone use (without instance.mount):
def op_new(self, title: str) -> Dict[str, Any]:
...
if hasattr(self.session, "set_context"):
self.session.set_context(CONTEXT_KEY_ACTIVE, root_id)
return {"status": "created", "concept_id": root_id, ...}When instance.mount later calls _apply_focus, it overwrites the same key with the
same value — this is a safe no-op. The hasattr guard keeps the model testable with
stub session objects.
For a model that supports multiple simultaneous instances (like CastConcept):
instance.mount model=mything slot=A name="alpha"
→ op_new() called → sets active_mything_root = A_id (auto-focus, first slot)
instance.mount model=mything slot=B name="beta"
→ op_new() called → sets active_mything_root = B_id (op_new sets it directly)
→ SpaceConcept does NOT auto-focus (slot A already focused) → restores A_id
# result: active_mything_root = A_id (slot A still focused)
instance.focus slot=B
→ SpaceConcept sets active_mything_root = B_id
instance.blur model=mything
→ SpaceConcept sets active_mything_root = None
instance.unmount slot=A
→ SlotAtom removed from space; underlying concept root preserved in Cortex
The concept model's op_* methods read CONTEXT_KEY_ACTIVE from session context at
invocation time — they do not cache it. Switching focus between slots is therefore
instantaneous with no state to clean up inside the model.
| Requirement | Where | What |
|---|---|---|
| Module constant | top of file | CONTEXT_KEY_ACTIVE = "active_{model}_root" |
| Class attribute | class body | CONTEXT_KEY_ACTIVE = CONTEXT_KEY_ACTIVE |
op_new return |
op_new |
include "concept_id": root_id |
| Session context | op_new, op_open |
session.set_context(CONTEXT_KEY_ACTIVE, id) |
| Session context | op_delete / op_close |
session.set_context(CONTEXT_KEY_ACTIVE, None) |
No changes to space.py, kernel.py, or the registry are required for a new model to
be mountable via instance.mount.
Operand-First: The dispatch operator ("note.add") names the concept before the verb.
The concept is the first-class object; the verb is a method on it.
Cortex as the only storage: Concepts never write to files, databases, or networks
directly. All persistence goes through self.cortex. This keeps concepts testable in
isolation with a mock Cortex.
Session for context, Cortex for memory: Transient state (which note is open, which container is active) lives in session context. Permanent knowledge lives in Cortex atoms.
Scope propagation: Concept methods always derive author_id from session.client_id
and build scopes as ["owner:user_{id}", "view:user_{id}"]. Every atom written by a
concept is owned by the authenticated client.
Content set vs concept catalog: Content atoms live in the concept's own namespace
(set:note:*, set:mything:*). General vocabulary lives in set:concept:*. The same
atom is never in both. Use sys:derived_from links to connect instances to vocabulary.
_require_concept everywhere: Every op_* except op_new must call
self._require_concept() as its first statement. Returning error dicts silently is wrong
— raise explicitly so the kernel can surface a clear error to the client.
No cross-concept direct calls: Concepts do not instantiate or call each other. Cross-
concept relationships are expressed as Cortex links:
cortex.put_link(a_id, b_id, "sys:associated_with").
| Relation | Direction | Meaning |
|---|---|---|
sys:top |
root → first_node | Timeline start pointer |
sys:bottom |
root → last_node | Timeline end floating pointer (updated via remove_link + put_link) |
sys:next |
node → next_node | Forward sequential link |
sys:previous |
node → prev_node | Backward sequential link |
sys:contains |
parent → child | Vertical hierarchy: parent owns child |
sys:part_of |
child → parent | Vertical hierarchy: child belongs to parent |
sys:derived_from |
content_atom → concept_word_atom | Instance derives from general concept (one-directional) |
sys:has_annotation_span |
chunk → annotation | Character-level sub-atom |
sys:included |
chunk → annotation | Containment (span alias) |
sys:associated_with |
any → any | Loose semantic association |
Custom relations for a concept must use a concept-namespaced form:
{prefix}:{relation} # e.g. agg:unit, synth:code, pres:frame, mt:item
Never use bare relation names or re-use another concept's prefix. The bundled models establish these namespaces:
| Concept | Relations used |
|---|---|
Aggregation (agg) |
agg:unit, agg:group, agg:member, agg:measure, agg:analysis_out, agg:analysis_in, agg:hierarchy, agg:child |
Synthesis (synth) |
synth:source, synth:refers_to, synth:code, synth:applies_to, synth:theme, synth:contains, synth:interp, synth:interprets, synth:supported_by, synth:claim, synth:argues, synth:evidence, synth:thread, synth:step |
Presentation (pres) |
pres:deck, pres:frame, pres:region, pres:node |
Avoid reusing sys:top/bottom/next/previous unless your concept implements a sequential
timeline. Using them for other purposes breaks op_get_sequential_text and TOC traversal
if the two concepts share atoms.
The following concept models ship with AkashicTree and are registered automatically
via the Plugin Registry. Their commands are hidden from the main help output.
Source: lib/akasha/concepts/cockpit.py
Session context key: active_cockpit_root
A stateful navigational vessel for the Semantic Cosmos. Maintains a transient instrument panel (focal point, axis, scope) in the session, and materialises observations as beacons — permanent atoms with embedded telemetry — only when explicitly commanded. The chronological sequence of beacons is the wake.
Topology:
- Root atom (
concept: "cockpit",role: "root") anchors the vessel. - Beacon atoms linked via
sys:containsand sequenced bysys:top → sys:next → sys:bottom. - Transient state lives in session context only; never written between beacon drops.
| Method | Required params | Description |
|---|---|---|
cockpit.new |
name (str) |
Commission a new cockpit deck |
cockpit.ls |
— | List all cockpits owned by this user |
cockpit.open |
cockpit_id (str) |
Mount an existing cockpit as active |
cockpit.lock |
target (str) |
Set the focal point (session only) |
cockpit.tune |
axis? (str), scope? (int) |
Adjust dimensional lens filters |
cockpit.beacon |
note (str) |
Drop a beacon at current focal point |
cockpit.wake |
— | Read the chronological beacon trail |
cockpit.status |
— | Read instrument panel state |
cockpit.rm |
— | Decommission the active cockpit |
CLI aliases: cp.new, cp.ls, cp.open, cp.lock, cp.tune, cp.beacon, cp.wake, cp.status, cp.rm
The Cockpit is the projection source for the Cosmos 3-D viewport (dive.look → cosmos
payload; services/static/cosmos/). The vessel's own operators (above) manage focus/axis/scope
and beacons; the viewport it projects now surfaces the full meaning layer, so navigation is
over real semantic structure rather than a decorative graph. These capabilities are provided by
the kernel + CosmosMapper (not new cockpit.* operators) and are reached from the cockpit
console:
Real spatial position. Each atom's Cosmos position (cosmos_nd X/Y/Z, and the per-node
x/y/z in the cosmos graph payload) is a projection of its self-owned semantic_vector
(CosmosMapper.position): near in space ⇒ near in meaning. When a learned distributional
model exists it projects onto that model's principal SVD axes (a crisp, topic-clustered
layout, fitted server-side — no client algorithm); otherwise a distance-preserving random
projection; else a stable hash. Nodes also carry a degree-based size (val) and an
emotion/sense aura color.
Instruments (kernel methods reached from the cockpit). The lens axes (structure /
emotion / context) genuinely filter relations; on top of that the console can call:
| Instrument | Method | What it surfaces |
|---|---|---|
| Semantic neighbours | sim / semantic.search id= |
atoms like this one (anchored on its meaning) |
| Structural neighbours | node.sim (node.learn builds it) |
atoms connected the same way |
| Consciousness view | view / cosmos |
signposts / resonance / cosmos_nd / aura, standalone (no dive) |
| Emotion | emotion.find / emotion.profile |
atoms that feel an emotion / an atom's emotion vector |
| Gaps | assoc / gap.scan |
missing 1-hop links / important-but-thin concepts |
| Dream bridges | dream → dream.confirm / dream.forget |
async "sleep-on-it" affinity-gap bridges (near in meaning, far in the graph), staged as tent: links for mandatory human confirmation |
Full reference: docs/developer/cosmos-frontend-requirements.md (viewport contract),
docs/for-llm/semantic-layer.md (embeddings / position), CLAUDE.md "Jataka — Narrator" (dream).
Source: lib/akasha/concepts/fieldnote.py
Session context key: active_fieldnote_root
A flat, context-tagged field observation record. Unlike the Note concept (hierarchical document), FieldNote is deliberately flat — all observations append to a single chronological timeline. Context metadata (project, region, season) lives on the root atom.
| Method | Required params | Description |
|---|---|---|
fieldnote.new |
title (str) |
Create a new FieldNote record |
fieldnote.ls |
— | List all accessible FieldNote records |
fieldnote.open |
fieldnote_id (str) |
Mount an existing record as active |
fieldnote.add |
text (str) |
Append an observation |
fieldnote.read |
— | Read all observations in order |
fieldnote.rm |
— | Delete the active FieldNote root |
Optional params for fieldnote.new: project, region, season (all strings).
Response shapes:
fieldnote.read→{"observations": [...], "count": N}- Each observation:
{"id", "content", "role", "created_at"}
Source: lib/akasha/concepts/survey.py
Session context key: active_survey_root
A pure-structural data-collection universe. Maintains five typed atom populations — root,
questions, options, respondents, responses — each cross-linked by sys:contains /
sys:part_of and registered into two namespaces (dual-namespace rule).
Namespace contract:
| Namespace | Set | Contents |
|---|---|---|
| Survey-scope | set:survey:{id}, set:survey:{id}:{subset} |
All content atoms |
| Concept catalog | set:concept:{id} |
Concept-word atoms only — NOT the root atom |
Each content atom links to its concept-word via sys:derived_from.
| Method | Required params | Description |
|---|---|---|
survey.new |
title (str) |
Create a new survey root |
survey.open |
survey_id (str) |
Mount an existing survey |
survey.ls |
— | List all accessible surveys |
survey.q.add |
text (str) |
Add a question to active survey |
survey.opt.add |
question_id, label (str) |
Add an option to a question |
survey.res.add |
respondent_id (str) |
Register a respondent |
survey.ans |
question_id, respondent_atom, answer |
Record a tri-linked response |
survey.list |
— | Structural inventory of active survey |
survey.rm |
— | Delete the active survey root |
Optional params: survey.new accepts description; survey.q.add accepts qtype and order; survey.opt.add accepts value; survey.res.add accepts attributes dict.
Source: lib/akasha/concepts/aggregation.py
Session context key: active_aggregation_root
Prefix: agg · Index set: set:agg:index
Builds statistical analysis structures over any set of Cortex atoms. The five-layer model (Unit → Group → Measure → Analysis → Hierarchy) maps raw atom collections to labelled partitions, attaches statistics to each partition, records directed relations between partitions, and organises everything into a navigable hierarchy. Source atoms are indexed as references — never copied.
| Method | Required params | Optional params | Description |
|---|---|---|---|
agg.new |
source_id |
— | Create aggregation root linked to a corpus atom |
agg.open |
agg_id / aggregation_id |
— | Mount an existing aggregation |
agg.ls |
— | — | List all accessible aggregations |
agg.unit.add |
unit_id |
— | Index an atom as an analysis unit |
agg.group.add |
label |
members (list) |
Create a labelled group of units |
agg.measure.add |
group_id, key, value |
— | Attach a statistic to a group |
agg.analysis.add |
src_group, dst_group, relation, score |
— | Record a directed relation between groups |
agg.hier.add |
label |
children (list) |
Create a hierarchy node |
agg.list |
— | — | Structural inventory of active aggregation |
agg.rm |
— | — | Delete the active aggregation root |
CLI aliases: agg.new, agg.open, agg.ls, agg.unit.add, agg.group.add, agg.measure.add, agg.analysis.add, agg.hier.add, agg.list, agg.rm
See docs/concept-extensions.md §3 for design rationale, topology, and cross-concept patterns.
Source: lib/akasha/concepts/synthesis.py
Session context key: active_synthesis_root
Prefix: synth · Index set: set:synth:index
Models the qualitative reasoning process between raw source material and a finished argument. The six layers — Source (reference), Code, Theme, Interpretation, Claim, Thread — correspond to the mental operations of qualitative analysis (Grounded Theory, thematic analysis, discourse analysis). Source atoms are indexed as references; the analysis builds interpretive meaning on top without duplicating the originals.
synth.trace walks the evidence chain backwards from any claim to its source atoms,
producing a full audit trail.
| Method | Required params | Optional params | Description |
|---|---|---|---|
synth.new |
title |
source_universes (list) |
Create synthesis root |
synth.open |
synth_id / synthesis_id |
— | Mount an existing synthesis |
synth.ls |
— | — | List all accessible syntheses |
synth.source.add |
ref_id |
ref_universe, note |
Index a source atom (with optional commentary) |
synth.code.add |
label |
source_id, confidence |
Create a qualitative code |
synth.theme.add |
title |
codes (list) |
Create a theme grouping codes |
synth.interp.add |
text |
theme_id, support (list), stance, confidence |
Record an interpretation |
synth.claim.add |
text |
interpretations (list), evidence (list), status, confidence |
Assert a claim |
synth.thread.new |
title |
— | Create a named reasoning thread |
synth.thread.add |
thread_id, node_id |
— | Append an atom as next step in a thread |
synth.map |
— | — | Structural inventory of active synthesis |
synth.trace |
claim_id |
— | Walk evidence chain from claim to sources |
synth.rm |
— | — | Delete the active synthesis root |
CLI aliases: as above (no short-form prefix defined by default).
See docs/concept-extensions.md §4 for design rationale, topology, and cross-concept patterns including the Aggregation → Synthesis mixed-methods pattern.
Source: lib/akasha/concepts/presentation.py
Session context key: active_presentation_root
Prefix: pres · Index set: set:pres:index
Models the abstract structure of a presentation (slide deck, research talk, poster, report) without coupling to any rendering format. Content always lives in upstream models (FieldNote, Survey, Aggregation, Synthesis); the Presentation only holds arrangement — which atoms appear where, in what order, and with what role.
The four-layer stack (Deck → Frame → Region → Node) maps to slide software metaphors
but is format-agnostic. A Node is a (ref_universe, ref_id) pointer to a Cortex atom
from any concept model.
| Method | Required params | Optional params | Description |
|---|---|---|---|
pres.new |
title |
context_universes (list) |
Create presentation root |
pres.open |
pres_id / presentation_id |
— | Mount an existing presentation |
pres.ls |
— | — | List all accessible presentations |
pres.deck.add |
title |
order |
Add a section deck |
pres.frame.add |
title |
deck_id, order, ref_universe, ref_id |
Add a slide/page |
pres.region.add |
frame_id, label |
order |
Add a layout zone within a frame |
pres.node.add |
parent_id, ref_universe, ref_id |
role, style |
Attach a content reference |
pres.list |
— | — | Structural inventory of active presentation |
pres.rm |
— | — | Delete the active presentation root |
CLI aliases: as above (no short-form prefix defined by default).
See docs/concept-extensions.md §5 for design rationale, topology, and cross-concept patterns including the Aggregation + Synthesis → Presentation full pipeline.
Source: lib/akasha/concepts/thesaurus.py
Prefix: thesaurus · CLI: thesaurus.<op> (canonical) · th.<op> (abbrev) · thesaurus mode. Operators: reference / explore / concept
A glossary read surface over the graph — the preparation layer for projecting
concepts onto web concept pages. Three read operators, no writes: the model reads
the thesaurus:* relations (written by ontology load / Weaver — see
ontology/thesaurus/a_thesaurus_core.csl) and the incrementally-maintained
meta['salience'] meaning-density score. It has no root lifecycle; it operates
across the whole graph.
Simplified (breaking). The former
shelf.*(ShelfScore),curation.*(CurationCollections), andseries.*(ExhibitionSeries) operators were removed. Ranking is now the singlesaliencenumber; enrichment happens at ontology/weave time, not through a write operator.
No double code. explore delegates to the shared filter-search core
lib/akasha/discovery.py:discover_atoms — the same code behind the explore
command. concept is built on consciousness.generate_view — the same dive basic
view the dive / view commands produce — and extends it. There is one search
implementation and one dive implementation.
| Method (CLI) | Required | Optional | Description |
|---|---|---|---|
thesaurus.reference (reference) |
— | order (default alpha), ns, initial, limit (default 200) |
Glossary index of named concepts, alphabetical. order= is an open axis: lang:<code> (locale collation), era (chronological), assoc (associative index) are reserved and currently fall back to alpha — the response's order_applied says which comparator ran. ns= scopes to one namespace; initial= is the glossary letter-jump. |
thesaurus.explore (lookup) |
query (or ns/type) |
ns, type, limit (default 20) |
Search for a target concept via the shared discovery core. query is a name/alias pattern (%/_ wildcards). Each match carries salience; results rank by it. |
thesaurus.concept (concept) |
name or atom_id |
— | Concept page: the dive basic view (signposts, resonance, cosmos_nd) plus the writer's view — synonyms / antonyms / broader / narrower related terms, usage examples, and external_refs — for investigating a word before using it in prose. |
reference output (alphabetical glossary):
{ "order": "alpha", "order_applied": "alpha", "total": 3,
"concepts": [
{"key": "...", "name": "word:en:memory", "term": "memory",
"initial": "M", "description": "...", "salience": 0.87}
] }explore output (glossary search):
{ "query": "mem", "count": 1,
"matches": [
{"key": "...", "name": "word:en:memory", "term": "memory",
"preview": "...", "color": "#...", "salience": 0.87}
] }concept output (dive basic view + writer's view):
{ "type": "thesaurus:concept",
"atom": {"key": "...", "name": "word:en:memory", "term": "memory",
"description": "...", "aliases": [...], "meta": {...}},
"salience": 0.87,
"synonyms": [{"key":"...","name":"word:en:recall","term":"recall","salience":...}],
"antonyms": [{"...":"word:en:oblivion"}],
"broader": [], "narrower": [],
"related": [{"key":"...","name":"...","rel":"calc:associated_with","dir":"out","salience":...}],
"examples": [{"text": "Her memory of that summer never faded.", "key": "..."}],
"external_refs": [{"label": "Wikipedia", "url": "https://..."}],
"signposts": [...], "resonance": [...], "cosmos_nd": [x,y,z,T,layer,color] }The related cloud is a flat list carrying each link's rel and dir (out/in),
deduped against the categorised buckets. synonyms/antonyms/broader/narrower
fall back to sys:*/calc:* relations when thesaurus:* links have not been
curated yet (common right after ontology load).
Source: lib/akasha/concepts/curation.py
Prefix: curation · CLI: curation.<op> (canonical) · cur.<op> (abbrev) · curation mode. Operators: new / narrate / ls · Index set: set:curation:index
Curation is interpretation as a narrative path over relationships. Where fact /
thesaurus deep-dive a single atom, curation interprets a set of atoms through
the RELATIONSHIPS among them, and its output is a narrative path: an ordered walk
Atom→Atom expressed as curation:next edges (the derived interpretation).
Two invariants:
- Relationship-centred. The operand is the relation structure, not the atom.
- No burden of proof. A curation is an interpretation from a standpoint; it shows
its GROUNDS (which relations / operation produced the path) but does not prove them.
Output atoms carry
provenance:interpretation— cross-check withfactatoms if verification is actually required.
Simplified (breaking). The former premise / input / view / fold / conclusion / dispute reconciliation engine (13 ops, 4 enums,
trace/diagnose) was removed and replaced by the 3 ops below.
Construction — two modes:
- derived — pass relation axes
rels=; the path is computed from the relationship structure. With ≥2 axes andop=intersect(default), an edge a→b survives only if b is adjacent to a under EVERY axis (e.g. later-in-time ∩ descends-from → a lineage narrative); a single axis follows that relation's chain.union/compose/preferare reserved and fall back to intersect (op_appliedreports which ran). - authored — pass
ids=as the intended ORDER (no rels, ormode=authored); the path is taken verbatim and itscuration:nextedges written — narrative-order-first.
| Method (CLI) | Required | Optional | Description |
|---|---|---|---|
curation.new (curate) |
title |
thesis, set, ids, rels, op (default intersect), mode, alias |
Create a curation: derive a path from relations, or author an order. set= targets a collection, ids= an explicit atom list (comma/space separated — CSL-friendly). Idempotent when alias= resolves to an existing curation. |
curation.narrate (narrate) |
curation_id or name |
— | Read the narrative path back: ordered steps, transitions, grounds (relation axes / op), provenance. This is the story STRUCTURE — hand to Jataka (jataka.present as=narrative) for prose. |
curation.ls (cur.ls) |
— | — | List curations. |
Relations: curation:next (path step, w = order), curation:over (root → each atom
interpreted). Examples shipped: ontology/curation/lineage_demo.ak (a patriline with
chrono:before + lineage:begat for derived auto path-discovery, autoloaded) and
curations/sky_dreamers.csl (an authored thematic narrative). See
docs/for-llm/curation-model.md.
Source: lib/akasha/concepts/recipe.py · extends FormulaConcept (§10.10)
Prefix: recipe · CLI: recipe.<op> (canonical) · rcp.<op> (abbrev) · recipe mode. Operators: new / add / step / food / nutrition / view / ls / suggest · Index set: set:recipe:index
Recipe is the cooking specialization of the formula base model (§10.10): a root plus
typed sub-groups — ingredients (materials), methods (operations), ordered steps, hints,
presentation, and constraints (specs). Its distinguishing feature is the dimensional
axes — season, ethnic, course, scene, plus the ingredients / methods / constraints it
carries — held as cross-recipe membership sets, so specifying axes RETRIEVES recipes by
weighted intersection (the composite cross_query idea applied to a menu). The file is a
thin skin: it renames the base operators to cooking vocabulary, binds the material source
to food atoms (USDA), and overrides the rollup's property hook to read nutrition — the
survey-shaped universe, ordered process, rollup, specs, and suggestion are all inherited.
Recipe is carried as a product surface (an iOS app backend), so its JSON-RPC field/operator
names are kept stable even as the base evolves.
Two points make recipe unlike curation:
- Recipes are authored data, not interpretation. A recipe asserts a structure (these
ingredients, this order); there is no "narrative from a standpoint", so no
provenance:interpretation. - Constraints are a hard, fail-closed filter — not a soft rank. Allergy / taboo /
dietary restrictions must subtract candidates from a suggestion, never merely lower
a score.
recipe.suggest … avoid=peanutdrops every recipe that uses peanut or carries the peanut constraint, regardless of how well it matches the other axes. This is enforced structurally (set subtraction overset:recipe:ing:{slug}∪set:recipe:constraint:{slug}), because getting it wrong is a safety incident.
Nutrition. recipe.nutrition accumulates (grams / basis_g) × each nutrient over a
recipe's ingredients, reading nutrition in either of two representations so both import
paths work:
- Structured meta —
recipe.foodwritesfood:{slug}withmeta.nutrition = {"basis_g": 100, "kcal": …, "protein_g": …}. - USDA content string — the ontology importer (
scripts/usda_food_import.py) can't write meta through.ak def, so it stamps"… — per 100g: 18 kcal, Protein 0.6g, Fat 0.1g, …"into the atom's content;recipe.nutritionparses that (_parse_nutrition_content, fires only on theper <N>g:marker) and maps USDA labels to the same canonical keys.
Recipes reference ingredients by slug only — they never stub the food: namespace — so
recipe.nutrition resolves each ingredient's food atom at read time from either handle a
USDA import provides: a pinned food:fdc:{id} (from fdc= on the ingredient, or an
fdc:11429-style value) → food:{slug} → a plain-name / leaf lookup. Both aliases are
adopted, not one excluded, so a food is reachable by name or by fdc id — whichever the
loader or the author used. A later USDA load always wins (recipe.food re-points with
set_alias(force=True); recipes resolve by slug/id, so the rebind is transparent). The
accumulator is schema-agnostic: it sums every numeric nutrient key present, so new USDA
fields total automatically. Only mass units (g/kg/mg/oz/lb) convert to grams; a count/portion
unit is reported unmeasured, a food with no data no_data — degradation-first, never a
silent drop. A constraint= that is a nutrient bound (kcal<=600, protein_g>=20) is
stored as a target (recipe:target) checked against the accumulated totals — not an
allergen; it never enters the avoid= subtraction set.
Loader contract (recipe ↔ USDA). Keep both aliases on each food atom —
food:fdc:{id}andfood:{slug}(a cooking-name alias, e.g.food:daikon) — alongside the descriptive key (food:{category}:{normalized_name}); do not drop the fdc id, since a recipe can pin to it withfdc=. Nutrition can live in the contentper <N>g:string or inmeta.nutrition. When a cooking name has nofood:{slug}alias, an author bridges the short-name → USDA-descriptive gap by pinningfdc=<id>; recipe.nutrition resolves by id/slug/name and reportsno_dataonly when none of them hit a food atom.
| Operator | Positional | Keyword args | Description |
|---|---|---|---|
recipe.new (rcp.new) |
title |
season, ethnic, course, scene, group, alias |
Create a recipe root and tag its discrete axes (each becomes a cross-recipe membership set). Idempotent when alias= resolves to an existing recipe. |
recipe.add (rcp.add) |
recipe |
exactly one of ingredient (+qty,unit,fdc) / method / hint / plating / constraint |
Add one operand. Ingredients are qty-carrying line atoms (recipe:uses); fdc=<id> pins the ingredient to a precise USDA food (food:fdc:<id>). Methods/allergen-constraints resolve to canonical method:/constraint: vocab; a constraint that is a nutrient bound (kcal<=600) becomes a target instead. |
recipe.step (rcp.step) |
recipe |
text, uses, by |
Append an ordered step (chained via sys:next), crossing it with the ingredients (recipe:step_uses) and methods (recipe:step_by) it touches. |
recipe.food (rcp.food) |
name |
basis_g (default 100) + open nutrient set (kcal, protein_g, fat_g, …) |
Define/refresh a food atom's nutrition (the USDA import write endpoint). Accepts an open set of nutrient fields via **kwargs. Fresh values on an existing food re-point food:{slug}. |
recipe.nutrition (rcp.nutrition) |
recipe or name |
— | Accumulate ingredient nutrition → totals, a per-ingredient table (each measured/unmeasured/no_data), and targets compliance (each actual, met). |
recipe.view (rcp.view) |
recipe or name |
— | Assemble the full card: ingredients (with qty), methods, ordered steps (with crossings), hints, presentation, axis tags, constraints, plus a nutrition summary and targets. GUI-ready. |
recipe.ls (rcp.ls) |
— | season, ethnic, course, scene, group |
List recipes, optionally filtered by discrete axis (intersection). |
recipe.suggest (rcp.suggest) |
— | season, ethnic, course, scene, group, have, avoid, mode, limit |
Rank recipes by axis intersection: score = (axes matched)/(axes requested). avoid=/allergen-constraints are a hard filter (subtract, never rank). mode=generative reserved (stage a new recipe skeleton for confirmation) → falls back to retrieval, reported as mode_applied. |
Registry note.
recipe.foodaccepts an open nutrient set via**kwargs; the registry's_filter_params(concepts/registry.py) passes all params through to any op that declares a VAR_KEYWORD parameter (params are already stripped of framework keys upstream). Ops without**kwargsare unaffected — extras are still filtered out by signature as before.
Relations: recipe:uses (→ ingredient line, w = order), recipe:by (→ method),
recipe:step (→ step, w = order), sys:next (step → step, the time axis),
recipe:step_uses / recipe:step_by (step × ingredient / method crossings),
recipe:hint, recipe:plating, recipe:constraint, recipe:target (→ a nutrient bound).
Axis index sets: set:recipe:group:{axis}:{value}, set:recipe:ing:{slug},
set:recipe:method:{slug}, set:recipe:constraint:{slug}. Nutrition: stored in the
food:{slug} atom's meta (nutrition.basis_g + per-nutrient values). Examples shipped:
ontology/recipe/pantry.ak (canonical method + constraint vocab, autoloaded) and
curations/recipe_demo.csl (three recipes + their recipe.food nutrition + a kcal<=600
target, authored via CSL — recipes carry runtime state, so they are CSL not flat .ak; run at
boot so recipe.suggest / recipe.nutrition work on a fresh install).
Source: lib/akasha/concepts/formula.py
Prefix: formula · CLI: formula.<op> · form.<op> (abbrev) · formula mode. Operators: new / material / op / step / source / rollup / spec / view / ls / suggest · Index set: set:formula:index
FormulaConcept is the domain-neutral base for "materials + operations + ordered
process". recipe (§10.9) extends it for cooking; the same structure serves pigment/dye
mixing, perfume, cosmetics, and process-industry / manufacturing (materials + procedure =
ISA-88's "recipe"), including cost/BOM rollup and procurement. A domain model subclasses
FormulaConcept, sets a few class attributes (CONCEPT_PREFIX, AXES, SOURCE_NS, the
ID_KEY/NOUN), and skins the operator names; all graph machinery is inherited —
relations and sets are parametrised by the subclass prefix, so recipe gets recipe:* /
set:recipe:* from the same code that gives formula its formula:* / set:formula:*.
Two capabilities lift it above a flat list:
- Property rollup (
_rollup, exposed asformula.rollup/recipe.nutrition) — accumulate any numeric material property, weighted by quantity. Two contributions per material: a direct line property (e.g.cost=entered on the line, summed as-is) and source-scaled properties ((amount /basis) × each per-basis property from the material's source atom, via the overridable_source_propshook). Schema-agnostic — it sums every numeric key present, so nutrition, cost, mass, VOC, CO₂ … all total with no code change. Recipe's hook reads food nutrition; the base readsmaterial:{slug}.meta.props(defined byformula.source name=… cost=… [basis=]). Degradation-first: a non-mass unit isunmeasured, a mass material with no source and no direct propno_data. - Specs, two kinds (
formula.spec) — a numeric bound (cost<=5) is a target checked against the rollup (recipe:target); anything else is a categorical constraint (a hard, fail-closed filter thatsuggest avoid=subtracts). Never conflated.step=/ccp=scope a target to a process step (the hook HACCP control points will use).
| Operator | Positional | Keyword args | Description |
|---|---|---|---|
formula.new |
title |
axis kwargs (kind, line, grade), alias |
Create a root, tag its axes. Idempotent by alias. |
formula.material |
formula |
name, qty, unit, source, + numeric props (cost=) |
Add a material line; numeric kwargs become direct line properties for the rollup. |
formula.op |
formula |
name |
Add an operation / technique. |
formula.step |
formula |
text, uses, by |
Append an ordered process step, crossing material × operation. |
formula.source |
name |
basis (default 1) + numeric props |
Define a material's per-basis properties (cost, density, …). Fresh values re-point material:{slug}. |
formula.rollup |
formula |
— | Accumulate material properties → totals + per-material table + target compliance. |
formula.spec |
formula |
value, step, unit, ccp |
Add a spec: numeric bound → target, else categorical constraint. |
formula.view / .ls / .suggest |
— | (as recipe) | Sheet assembly / axis-filtered list / axis-intersection ranking. |
PERT / critical path (formula.critical, recipe.critical). Steps form a dependency
DAG, not just a chain: formula.step … dur=20 dur_unit=min after=<step[,step]> [label=x].
after= names predecessor steps (by order index, label, or key); with no after a step
depends on the previous one (linear default), and steps with no mutual dependency run in
parallel (a second burner, the oven while you prep). formula.critical runs the Critical
Path Method (forward/backward pass) → per-step ES/EF/LS/LF + slack, the zero-slack critical
path, the makespan (accounting for parallel branches) and the naive sequential total.
Relation P:after (step → predecessor); duration on step meta (dur_min). This is
design-time process planning, distinct from the JCL/Harmonia runtime depends_on PERT.
Control points / HACCP (formula.control / .measure / .checkpoints;
recipe.control / .measure / .haccp). A control spec bounds a process parameter —
formula.control param=temp op='>=' value=75 unit=C step=<ref> ccp=yes (a step/formula-scoped
target with a ccp critical-control-point flag). formula.measure param=temp value=78 [step=]
records an observed value (an audit trail — latest per param/step wins).
formula.checkpoints checks every target against the best available actual — a recorded
measurement for its parameter (preferring the same step) if present, else the material rollup —
reporting per-target pass/fail/pending, the CCP subset, violations, and an overall safe
flag. For cooking this is hygiene management: cook temperature (a CCP), holding/storage
temperature, serve-within, shelf life. Relation P:measure; measurements in
set:P:{id}:measurements.
Relations (prefix P = the subclass's CONCEPT_PREFIX): P:uses (→ material line),
P:by (→ operation), P:step, P:after (→ predecessor step), sys:next (step order),
P:step_uses / P:step_by (step crossings), P:spec (→ constraint), P:target (→ numeric
bound / control spec), P:measure (→ measurement), plus notes. Sets:
set:P:{id}:{materials|operations|steps|notes|presentation|specs|targets|measurements},
set:P:group:{axis}:{value}, set:P:{mat|op|spec}:{slug}.
Status: Design pending. The questions below will become significant once concept count and data volume grow. The current implementation makes no guarantees beyond what is listed under "Current behaviour".
op_delete in each concept model calls cortex.drop_chunk(root_id) and removes
root_id from INDEX_SET. Nothing else is guaranteed:
- Subordinate atoms (traits, masks, items, …) are not deleted.
- Concept sets (
set:concept:{id},set:<model>:{id}, sub-sets) are not removed. - Aliases assigned to the root are not invalidated.
space:owns/space:containslinks in any SessionSpace are not cleaned up.
This is safe while data volume is small ("seeds phase"). The items below are the open design questions.
Drop policy
| Policy | Meaning |
|---|---|
shallow |
Delete root only; subordinate atoms and sets enter GC queue |
cascade |
Recursively delete all atoms reachable via sys:contains from root |
tombstone |
Rewrite root meta to {"deleted": true}; links preserved for audit |
Default policy for each concept model, and whether callers can override, TBD.
Index and set cleanup
- Is
INDEX_SETremoval automatic (triggered bydrop_chunk) or the concept's responsibility? - When does an empty concept set get reclaimed? Reference-counted, sweep-based, or manual?
- Who owns set GC — Cortex, kernel, or individual concept models?
Alias invalidation
- When
drop_chunk(root_id)is called, are aliases pointing toroot_idautomatically removed? - Or do they persist as dangling aliases until explicit cleanup?
Orphan policy
After a shallow drop, subordinate atoms have no reachable parent. Options:
- Silent orphan — atoms remain; only a sweep finds them.
- Warn on read — Cortex flags atoms whose root is gone.
- GC sweep — a maintenance job collects unreferenced atoms periodically.
SessionSpace cleanup
When a concept root is deleted, any SlotAtom in a SessionSpace that holds its
concept_id becomes stale. Whether instance.unmount is called automatically or the
slot silently holds a dead reference is TBD.
Cascading across concept boundaries
If a CastConcept root is cascade-deleted, should linked SynthesisConcept nodes that
reference it via sys:associated_with also be dropped, or only unlinked?
Session context is user-scoped, not service-scoped. Two UI services that use the same
concept class (e.g. the Note app and the Loom writing atelier both use NoteConcept) share
the same session context dictionary. Without isolation, whichever service called op_new
last overwrites active_note_root, causing the other service to load a wrong document.
BaseConcept.__init__ accepts an optional namespace: str. When provided:
self._ns = f"{namespace}:"self._ctx_key(key)returnsf"{namespace}:{key}"instead of barekey
All context reads and writes in op_new, op_open, op_delete, and __init__ must
route through self._ctx_key(). Services that share the same concept class but need
independent state simply use different namespaces.
Concept class (__init__, every set_context / get_context call):
def __init__(self, session, concept_id=None, namespace=None):
super().__init__(session, concept_id, namespace=namespace)
if not self.concept_id:
stored = getattr(self.session, "get_context", lambda k: None)(
self._ctx_key(CONTEXT_KEY_ACTIVE)
)
...
# In op_new / op_open / op_delete:
self.session.set_context(self._ctx_key(CONTEXT_KEY_ACTIVE), root_id)Kernel handler — instantiate with the service's namespace:
# Standard Note app — no namespace (backward-compatible)
concept = NoteConcept(session)
# Loom writing atelier — isolated under "loom:"
concept = NoteConcept(session, namespace="loom")RPC naming convention — prefix the RPC method with the service name so the kernel knows which namespace to use without ambiguity:
note.new / note.read / … → no namespace (Note app)
loom.note.new / loom.note.read → namespace="loom" (Loom)
| Rule | Rationale |
|---|---|
Every get_context / set_context call goes through self._ctx_key() |
Prevents raw key bleed |
| Each UI service that uses a shared concept model gets a unique namespace string | Guarantees session-level isolation |
Namespace strings are short lowercase identifiers ("loom", "fieldnote") |
Readable in session dumps |
namespace=None (default) preserves the bare key — backward-compatible |
Existing services need no change |
New RPCs for a namespaced service follow {service}.{concept}.{op} |
Kernel can dispatch to the right namespace without inspecting request body |
A concept model needs namespace support only when the same concept class is (or could be)
used by more than one UI service. If a concept class has exactly one service, its module-
level CONTEXT_KEY_ACTIVE is already unique and no namespace is required.
| Concept Class | Context Key | Namespace Needed? |
|---|---|---|
NoteConcept |
active_note_root |
Yes — Note app + Loom both use it |
FieldNoteConcept |
active_fieldnote_root |
Applied (future-proofing) |
| All others | active_*_root (unique per class) |
Only if a second service appears |
| Version | Changes |
|---|---|
| 1.0 | Initial specification |
| 1.1 | Added _require_concept to BaseConcept (§2.4). Added cortex.remove_link (§6). Documented dual-namespace design and sys:derived_from derivation pattern (§3.2, §3.3, §5). Updated _append_to_timeline to use cortex.remove_link. Fixed op_delete to pass requester_scopes. Added staging × Harmonia boundary warning (§2.8). Added §5 set namespace design as a standalone section. Updated template (§4.1) to reflect all current patterns. Added sys:derived_from to reserved relations table (§9). |
| 1.2 | Replaced manual kernel handler pattern (§7.1) with Plugin Registry auto-discovery. CONCEPT_PREFIX + CONCEPT_METHODS opt-in — no kernel.py changes needed for new models. Added §10 API reference for bundled concept models (Cockpit, FieldNote, Survey). |
| 1.3 | Rewrote §4.1 template to reflect all patterns from Aggregation, Synthesis, Presentation implementations: CONCEPT_METHODS with coerce, INDEX_SET, _require_access, _visible_members, correct set creation order. Expanded §4.2 checklist. Added §4.3 Common Implementation Bugs (B1–B11). Added concept-namespace relation convention to §9 with full table for agg/synth/pres relations. Added §10.4–10.6 API reference entries for Aggregation, Synthesis, Presentation. |
| 1.4 | Fixed Two-Namespace Rule violation in template: _register(root_id) — no concept_word on root atom (§4.1, §4.3 B1, §5.1). Added root atom corollary to §5.1. Added §7.4 Session Instance Layer Integration: two binding modes, CONTEXT_KEY_ACTIVE convention, concept_id return requirement, multi-instance lifecycle. Added §11 Root Lifecycle Guarantees stub: current behaviour, open questions on drop policy, GC, alias invalidation, orphan policy, SessionSpace cleanup. |
| 1.5 | Added namespace parameter to BaseConcept.__init__ and _ctx_key() helper (§2.1, §2.3). Updated template __init__, op_new, op_open to use _ctx_key() (§4.1). Added §12 Service Namespace Isolation: problem statement, solution, design rules, affected models table. |
| 1.6 | Added §10.7 Thesaurus: cross-graph semantic enrichment model with ShelfScore, typed relation registry, and CurationCollection management. Unlike other models, Thesaurus has no single root — it operates across the entire graph. Relation types registered in ontology/thesaurus/a_thesaurus_core.csl. |
| 1.7 | Thesaurus §10.7 extended: thesaurus:external_ref relation type added; thesaurus.shelf.link_ext operator attaches external URLs (Wikipedia, Britannica, etc.) to atoms; external_refs ShelfScore component (w=0.10) counts external refs / 4. thesaurus.view.atom and thesaurus.view.curation operators added for complete UI projection. ShelfScore weights rebalanced: link_total 0.10→0.05, chain_balance 0.10→0.05, external_refs new 0.10. Shell renderer extended to handle thesaurus:AtomView and thesaurus:CurationView result types. |
| 1.8 | Thesaurus §10.7 extended with ExhibitionSeries: thesaurus:in_series relation added (many-to-many: a CurationCollection can belong to multiple series). thesaurus.series.new, thesaurus.series.ls, thesaurus.series.add, thesaurus.view.series operators added. set:thesaurus:series index set. url_slug field added to all view projections (series, curation, collection). URL permanence convention documented: /series/{slug}, /exhibition/{slug}, /word/{alias}. Shell renderer extended with _render_thesaurus_series_view for thesaurus:SeriesView result type. |
| 1.9 | Added §10.9 Recipe: a survey-shaped cookable universe (root + ingredients/methods/steps/hints/presentation/constraints) with axis-driven suggestion. Dimensional axes (season/ethnic/course/scene + ingredients/methods/constraints) are cross-recipe membership sets; recipe.suggest ranks by weighted intersection (the cross_query idea on a menu). Constraints are a hard, fail-closed filter (avoid= subtracts candidates, never soft-ranks) — the deliberate asymmetry vs curation's no-burden-of-proof. Operators: new / add / step / view / ls / suggest. Examples: ontology/recipe/pantry.ak (method + constraint vocab, autoloaded) + curations/recipe_demo.csl (three recipes, boot-run so suggest works fresh). |
| 1.10 | Recipe §10.9 nutrition: recipe.food (open nutrient set via **kwargs) writes USDA-style nutrition into food:{slug} meta (the USDA import endpoint); recipe.nutrition accumulates (grams/basis_g)×nutrient over an ingredient's food atom (resolved by slug/name at read time — recipes never stub food:, so a USDA load wins), schema-agnostic over nutrient keys, degradation-first (unmeasured for non-mass units, no_data for missing food). Reads nutrition from either structured meta.nutrition (recipe.food) or a USDA .ak content string "… — per 100g: 18 kcal, Protein 0.6g, …" (_parse_nutrition_content), since .ak def can't write meta — reconciles with scripts/usda_food_import.py. A constraint= nutrient bound (kcal<=600) becomes a recipe:target checked against totals, separate from allergen constraints; recipe.view gains a nutrition summary + targets. Registry _filter_params now passes all params through to VAR_KEYWORD ops (others unchanged). Loader contract documented: alias each food by food:{slug} so recipes resolve. |
| 1.11 | Recipe food resolution adopts both aliases instead of excluding one: recipe.nutrition resolves an ingredient from food:fdc:{id} (pinned via fdc= on recipe.add, or an fdc:11429-style value) → food:{slug} → plain-name/leaf, so a food is reachable by name or by fdc id. recipe.add ingredient= gains fdc=; the id is stored on the ingredient line and surfaced in recipe.view. Bridges the short-cooking-name → USDA-descriptive-name gap without a synonym table. Loader contract updated: keep both food:fdc:{id} and food:{slug}. |
| 1.12 | Added §10.10 Formula base model (FormulaConcept, lib/akasha/concepts/formula.py): the domain-neutral base for "materials + operations + ordered process" — generalises recipe to dye/perfume/cosmetics/manufacturing (ISA-88-style materials + procedure + cost/BOM rollup). Machinery (relations/sets parametrised by prefix, property rollup, specs, axis suggestion) is inherited; recipe is refactored to a thin subclass (RecipeConcept(FormulaConcept)) with its recipe.* API/field names unchanged (product-surface stability). New generic formula.* model registered (S1 of the base-model plan). Property rollup generalises recipe.nutrition: direct line props (cost=) + source-scaled props (formula.source / food nutrition), schema-agnostic; specs unify numeric targets and categorical constraints (with step=/ccp= scoping reserved for HACCP). test/formula_eval.py (6/6); recipe_eval 10/10 unchanged. Reserved: PERT (dur/after/formula.critical, S2) and HACCP control specs (S3). |
| 1.13 | Formula PERT / critical path (S2): formula.step/recipe.step gain dur=/dur_unit= and after= (predecessor steps by order/label/key), forming a dependency DAG — omit after for the linear default, use it for parallel branches. formula.critical/recipe.critical run the Critical Path Method (forward/backward pass) → per-step ES/EF/LS/LF + slack, the zero-slack critical path, the makespan (parallel-aware) and the sequential total. Relation P:after; duration on step meta. Design-time planning, distinct from JCL runtime depends_on. formula_eval 7/7, recipe_eval 11/11. |
| 1.21 | Reference-recipe expansion (ontology dish → structured recipe). The recipe ontology (TheMealDB ~744) stores each dish as ONE described atom in the shared nucleus, in a labelled grammar (<Title> (<Cat> · <Cuisine> · <Country>). Ingredients: <m> <name>; …. Method: <step 1 …>. Source: <url>). Module parser _parse_dish (+ _parse_ing_line, _split_steps) turns that back into {title, axes, ingredients(+qty/unit), steps, source} deterministically (validated across all 744: 0 with zero steps/ingredients; median 9 steps / 10 ingredients; step N markers → split, else newlines, else sentence segmentation). Two operators: recipe.reference.get (READ, guest-ok) projects a dish atom into a recipe card on the fly (parsed steps + ingredients + linked ingred:* concepts + image + source) with NO materialisation — the reference library browses the shared atoms in place; recipe.reference.clone (WRITE) materialises a dish into the caller's OWN editable recipe (recipe.new + a step per instruction + an ingredient per parsed line, best-effort food pin), counts against quota — "save & customise". test/recipe_reference_eval.py 4/4 (parser on the exact grammar, guest projection, clone→editable recipe.view, step-marker vs prose fallback). Regression green (recipe/dict/food_search/tier/account/formula, cli_router, invariants 0 fail). External spec §4.2 (reference.get/clone). |
| 1.20 | Food-app surface (iOS-lead consolidated spec — the app is now a food-information app, not only recipe suggestion). A-1 recipe.food.lookup — food-dictionary read of one food (nutrition + allergens/season/categories surfaced generically from the atom's links); exact-match preferred (case-insensitive), ambiguous → {found:false, candidates:[…]}. A-2 recipe.method.list / A-3 recipe.tool.list — catalogue reads `{methods |
| 1.19 | Food catalogue search (recipe.food.search / generic formula.source.search) — the ingredient picker the app needs. When the food ontology (base pack ~380 + the archives nutrition pack ~38k) loads, food atoms carry category-namespaced keys (food:<category>:<slug>) + a food:fdc:<id> alias and live in the shared nucleus, so a bare ingredient name is ambiguous and only fdc=-pinned adds resolved nutrition. New READ op searches by name (all tokens, via get_aliases_by_pattern) across both the shared nucleus catalogue AND the caller's private foods (food:user:<uid>:…), returning each hit's display name + fdc + per-basis nutrition (parsed from meta.nutrition or the "… — per 100g: …" content string) + scope (catalog |
| 1.18 | Recipe launch-hardening (10-point iOS-lead review; all generic in FormulaConcept + the recipe skin + kernel). (1) Monotonic revision — an append-only revlog (set:{P}:{root}:revlog, uuid-fenced marker atoms, never removed) bumps on every write, so it changes even across an in-place edit (detach+recreate keeps the parts count); expected_revision is the preferred optimistic-lock guard (legacy expected_updated_at still honoured); updated_at kept for display, version (parts count) kept for back-compat but must not be locked on. (2) Stable item ids — each per-formula item atom (ingredient/step/note/target/measurement) carries a lid fenced into its content (textlid, U+2063) so identical text across formulas never collapses into one meta-clobbered atom, and an edit carries the lid forward onto the new atom key; recipe.step/recipe.add return step_id/ingredient_id (stable) alongside atom_key; after=/item= resolve the lid → current key. (3) request_key scoped to (client, rpc_method, request_key) + ~24 h retention (expired hits re-execute). (4) recipe.food gated to librarian/admin (shared catalogue food:<slug>; catalog_denied); new recipe.food.personal writes a private food:user:<uid>:<slug> that _resolve_food prefers for that user. (6) auth.account.delete — self-service (own account only; confirm required; refuses guests/admins): deregisters the identity (soft-delete = retained audit), purges the private cell (manager.purge_client_cell), and deletes the entitlement entry; mapped to capability read so it does NOT open a graph workspace on the cortex it deletes. (9) Pagination default limit=20, max 100, whole list (limit=0) admin-only (_page_limit). (10) recipe.control/recipe.measure gated with recipe.haccp (one paid HACCP feature). Docs (5/7/8): billing wording → StoreKit 2 signed transaction / store purchase credential + server-to-server notifications; session_token exceptions (auth.status/auth.register//health); recipe_quota documented as the single source of truth. recipe_eval 13/13, formula_eval 8/8, recipe_tier_eval 6/6, new recipe_account_eval 5/5. Security: account-delete only ever targets the caller; recipe.food catalogue write is librarian/admin; no invariant weakened. External spec docs/api/recipe-jsonrpc-v1.md updated end-to-end. |
| 1.17 | General self-registration + entitlement (kernel, product-neutral, for iOS/Android). auth.register — bounded self-service signup: OFF unless AKASHA_ALLOW_SELF_REGISTER=1; role FORCED to user (client-supplied role ignored — no escalation); a taken id is REJECTED (never overwritten → no hijack); passphrase ≥8 chars (raw over HTTPS); auto-logs-in (returns an akt: token) on the free plan. auth.plan (read own tier, capability read) / auth.plan.set (admin, capability iam.manage — the billing hook) store the tier in the shared nucleus vault (entitlement KV, product-neutral); recipe tiering now reads this general store (recipe.plan.set ≡ auth.plan.set). Billing wiring documented (external receipt-validation service → auth.plan.set with an admin token; client never self-upgrades). test/recipe_tier_eval.py 6/6 (adds register + general plan). Security: self-registration is a deliberate, config-gated loosening of the admin-only user-management rule — role-forced, no-overwrite, verified adversarially (no privilege escalation; bare-id-over-network invariant intact). External spec docs/api/recipe-jsonrpc-v1.md §2.4 documents auth.register + the upgrade flow. |
| 1.16 | Recipe entitlement / tiering (server-side, the akashickitchen product model): OFF by default (AKASHA_RECIPE_TIERING) so the OSS recipe model has no limits. When on, a free plan is capped at AKASHA_RECIPE_FREE_QUOTA own recipes (default 5; recipe.new → quota_reached error over quota) and the analytics in AKASHA_RECIPE_PAID_FEATURES (default nutrition/critical/haccp) return a locked result for free users (recipe.view's nutrition block too). A user's plan lives in the shared nucleus vault (recipe_plan KV — server-side, cross-session, never a client-writable atom), set by the admin-only `recipe.plan.set user= tier=paid |
| 1.15 | Recipe client-facing contract additions (for the iOS product surface): idempotent writes (request_key on recipe.add/step/measure → a retry returns status:"duplicate", no double insert); edit/delete (recipe.remove, recipe.ingredient.remove/.update, recipe.step.remove/.update — detach the immutable atom; update = detach + re-add); derived version/updated_at on the card + optimistic-lock expected_updated_at on writes (stale → -32002 conflict); limit/cursor pagination on recipe.ls/suggest (→ next_cursor/has_more). All generic in FormulaConcept. External spec docs/api/recipe-jsonrpc-v1.md documents these + the MVP policy (HTTPS-gated writes, admin-provisioned users, guest read-only). recipe_eval 13/13, formula_eval 8/8. |
| 1.14 | Formula control points / HACCP (S3): formula.control/recipe.control add a bound on a process parameter (param=temp op='>=' value=75 step=<ref> ccp=yes) — a step/formula-scoped target with a critical-control-point flag; formula.measure/recipe.measure record observed values (audit trail, latest per param/step wins); formula.checkpoints/recipe.haccp check each target against the best actual (a measurement for its param, preferring the same step, else the rollup) → per-target pass/fail/pending, the CCP subset, violations, and an overall safe flag. For cooking: cook temperature (CCP), storage temperature, serve-within, shelf life. Relation P:measure; set:P:{id}:measurements. formula_eval 8/8, recipe_eval 12/12, cli_router 5/5. Base-model plan (S1–S3) complete. |