Deduplicate kanban card slugs

This commit is contained in:
HumbleTurtle
2026-05-10 02:30:38 -03:00
parent 4a332ed413
commit dfead93d15
2 changed files with 67 additions and 0 deletions
+40
View File
@@ -7542,6 +7542,44 @@ def normalize_kanban_slug(value, fallback="untitled"):
return slug or fallback 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): def kanban_compact_card(node, view="summary", preview_chars=240):
item = summarize_node(node, view=view, preview_chars=preview_chars) item = summarize_node(node, view=view, preview_chars=preview_chars)
metadata = node_metadata(node) metadata = node_metadata(node)
@@ -7861,6 +7899,8 @@ def create_kanban_card(args):
raise RuntimeError("title is required") raise RuntimeError("title is required")
column = normalize_kanban_column(args.get("column"), "todo") column = normalize_kanban_column(args.get("column"), "todo")
slug = normalize_kanban_slug(args.get("slug") or title) 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}" path = f"/kanban/{column}/{slug}"
metadata = kanban_card_metadata(args, column=column) metadata = kanban_card_metadata(args, column=column)
metadata.setdefault("rank", next_kanban_rank(column)) metadata.setdefault("rank", next_kanban_rank(column))
@@ -260,6 +260,31 @@ def test_kanban_workflow_create_move_and_block(mod):
assert writes[2][2]["metadata"]["blocked_by"] == "waiting" 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): def test_plan_lifecycle_dry_run_and_write(mod):
plan = node("/plans/example", "in_progress", tags=["plan"]) plan = node("/plans/example", "in_progress", tags=["plan"])
outcome = node("/implementation-notes/example", "completed", node_type="implementation-note") outcome = node("/implementation-notes/example", "completed", node_type="implementation-note")
@@ -348,6 +373,8 @@ def run():
mod = load_module() mod = load_module()
test_kanban_workflow_create_move_and_block(mod) test_kanban_workflow_create_move_and_block(mod)
mod = load_module() mod = load_module()
test_kanban_create_deduplicates_overlapping_titles(mod)
mod = load_module()
test_plan_lifecycle_dry_run_and_write(mod) test_plan_lifecycle_dry_run_and_write(mod)
mod = load_module() mod = load_module()
test_plan_lifecycle_archived_idempotent(mod) test_plan_lifecycle_archived_idempotent(mod)