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:
co-authored by
Claude Opus 5.5
parent
611a4571f3
commit
f201cb358b
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user