From dfead93d1588e105d5cf7b1aa3521e5af885af90 Mon Sep 17 00:00:00 2001 From: HumbleTurtle Date: Sun, 10 May 2026 02:30:38 -0300 Subject: [PATCH] Deduplicate kanban card slugs --- resources/scripts/napseer_mcp_server.py | 40 +++++++++++++++++++++ tests/test_mcp_memory_ergonomics_helpers.py | 27 ++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/resources/scripts/napseer_mcp_server.py b/resources/scripts/napseer_mcp_server.py index 13e62b5..6ed0db2 100755 --- a/resources/scripts/napseer_mcp_server.py +++ b/resources/scripts/napseer_mcp_server.py @@ -7542,6 +7542,44 @@ def normalize_kanban_slug(value, fallback="untitled"): return slug or fallback +def kanban_timestamp_token(value=None): + if value is None: + value = datetime.now(timezone.utc) + if isinstance(value, str): + try: + value = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + value = datetime.now(timezone.utc) + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + value = value.astimezone(timezone.utc) + return value.strftime("%Y%m%d%H%M%S") + + +def unique_kanban_slug(column, slug, existing_cards=None, now=None): + column = normalize_kanban_column(column) + slug = normalize_kanban_slug(slug) + cards = existing_cards + if cards is None: + cards = list_kanban_cards({"column": column, "active_only": False, "limit": DISCOVERY_COMPACT_MAX_LIMIT, "view": "metadata"}).get("items", []) + folder = f"/kanban/{column}" + existing_names = set() + for card in cards or []: + path = normalize_node_path(card.get("full_path") or "") + if path.startswith(f"{folder}/"): + existing_names.add(path[len(folder) + 1:]) + if slug not in existing_names: + return slug + stamped = f"{slug}-{kanban_timestamp_token(now)}" + if stamped not in existing_names: + return stamped + for index in range(2, 100): + candidate = f"{stamped}-{index}" + if candidate not in existing_names: + return candidate + return f"{stamped}-{secrets.token_hex(3)}" + + def kanban_compact_card(node, view="summary", preview_chars=240): item = summarize_node(node, view=view, preview_chars=preview_chars) metadata = node_metadata(node) @@ -7861,6 +7899,8 @@ def create_kanban_card(args): raise RuntimeError("title is required") column = normalize_kanban_column(args.get("column"), "todo") slug = normalize_kanban_slug(args.get("slug") or title) + if not as_bool(args.get("upsert"), False): + slug = unique_kanban_slug(column, slug, now=args.get("now")) path = f"/kanban/{column}/{slug}" metadata = kanban_card_metadata(args, column=column) metadata.setdefault("rank", next_kanban_rank(column)) diff --git a/tests/test_mcp_memory_ergonomics_helpers.py b/tests/test_mcp_memory_ergonomics_helpers.py index 4cfc740..20e77f2 100644 --- a/tests/test_mcp_memory_ergonomics_helpers.py +++ b/tests/test_mcp_memory_ergonomics_helpers.py @@ -260,6 +260,31 @@ def test_kanban_workflow_create_move_and_block(mod): assert writes[2][2]["metadata"]["blocked_by"] == "waiting" +def test_kanban_create_deduplicates_overlapping_titles(mod): + writes = [] + existing_cards = [ + node("/kanban/todo/add-mcp-commands", "todo", metadata={"column": "todo"}, tags=["kanban"], node_type="kanban_card"), + node("/kanban/todo/add-mcp-commands-20260510123456", "todo", metadata={"column": "todo"}, tags=["kanban"], node_type="kanban_card"), + node("/kanban/doing/add-mcp-commands", "doing", metadata={"column": "doing"}, tags=["kanban"], node_type="kanban_card"), + ] + + mod.list_kanban_cards = lambda args: {"items": existing_cards if args.get("column") == "todo" else []} + mod.request_project_write = lambda method, path, payload, *args, **kwargs: writes.append((method, path, payload)) or { + **existing_cards[0], + "id": "id-deduped", + "full_path": f"{payload.get('folder_path')}/{payload.get('name')}", + "folder_path": payload.get("folder_path"), + "name": payload.get("name"), + "metadata": payload.get("metadata", {}), + "tags": payload.get("tags", []), + } + mod.index_node = lambda node: None + + created = mod.create_kanban_card({"title": "Add MCP commands", "column": "todo", "now": "2026-05-10T12:34:56Z"}) + assert created["path"] == "/kanban/todo/add-mcp-commands-20260510123456-2" + assert writes[0][2]["name"] == "add-mcp-commands-20260510123456-2" + + def test_plan_lifecycle_dry_run_and_write(mod): plan = node("/plans/example", "in_progress", tags=["plan"]) outcome = node("/implementation-notes/example", "completed", node_type="implementation-note") @@ -348,6 +373,8 @@ def run(): mod = load_module() test_kanban_workflow_create_move_and_block(mod) mod = load_module() + test_kanban_create_deduplicates_overlapping_titles(mod) + mod = load_module() test_plan_lifecycle_dry_run_and_write(mod) mod = load_module() test_plan_lifecycle_archived_idempotent(mod)