Type node creation and filter local memory by effective type

- nap_create_node accepts `type`; when omitted (also in nap_bulk and
  agent-node creates) the type is inferred from the folder and sent
  explicitly, so older APIs also store typed plans and decisions.
  Explicit spelling variants are canonicalized; custom types pass as is.
- Local browse, full-text and encrypted-shard searches filter on the
  effective type (canonical spelling, or the folder's type for generic
  notes; `type=note` excludes notes whose folder implies a type); server
  rows are post-filtered the same way. Rows expose `effective_type` when
  it differs from the stored type; discover offers an effective_type
  facet.
- Active-only filters also exclude deployed, released,
  released_verified_production and folded.
Mirrors napseer src/vocab.rs; decision
/decisions/2026-09-29-node-type-status-vocabulary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
HumbleTurtle
2026-09-29 00:32:36 -03:00
co-authored by Claude Opus 5.5
parent 611a4571f3
commit f201cb358b
4 changed files with 624 additions and 22 deletions
+20 -4
View File
@@ -52,18 +52,18 @@ def unlock(mod, secret=None):
cache_secret(mod, secret or memory_secret())
def plain_node(name, minute, content=None):
def plain_node(name, minute, content=None, folder="/notes"):
stamp = f"2026-09-01T12:{minute:02d}:00Z"
return {
"id": name, "project_id": PROJECT, "full_path": f"/notes/{name}", "folder_path": "/notes",
"id": name, "project_id": PROJECT, "full_path": f"{folder}/{name}", "folder_path": folder,
"name": name, "type": "note", "metadata": {}, "tags": [], "aliases": [], "links": [],
"content_text": content if content is not None else name, "updated_at": stamp, "archived_at": None,
}
def e2e_node(mod, name, minute, content, metadata=None, secret=None, archived=False):
def e2e_node(mod, name, minute, content, metadata=None, secret=None, archived=False, folder="/notes"):
secret = secret or memory_secret()
node = plain_node(name, minute)
node = plain_node(name, minute, folder=folder)
context = {"full_path": node["full_path"]}
node.update({
"content_text": "", "metadata": {}, "encryption_state": "encrypted",
@@ -229,6 +229,22 @@ def test_e2e_body_term_is_findable_with_snippet_while_unlocked(env):
assert not discovered.get("warnings")
def test_e2e_shard_hits_filter_on_effective_type(env):
mod, provider, _ = env
provider.put(e2e_node(mod, "legacyplan", 1, BODY, folder="/plans"), "node_created")
provider.put(e2e_node(mod, "scratchnote", 2, BODY), "node_created")
assert mod.reindex_project({})["e2e_index"]["status"] == "applied"
# Bodies are only in the shard, so these hits come from the E2E path.
plans = mod.search_local_index({"q": BODY_TERM, "limit": 10, "type": "plan"})
assert hit_ids(plans) == ["legacyplan"]
assert plans["items"][0]["search_source"] == "local_e2e"
assert (plans["items"][0]["type"], plans["items"][0]["effective_type"]) == ("note", "plan")
notes = mod.search_local_index({"q": BODY_TERM, "limit": 10, "type": "note"})
assert hit_ids(notes) == ["scratchnote"]
assert notes["items"][0]["search_source"] == "local_e2e"
def test_locked_secret_yields_no_e2e_hits_and_an_explicit_warning(env):
mod, provider, _ = env
seed(mod, provider)