Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion emdx/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ def _find_keyword_search(

# Combine: only show documents that match both criteria
results: list[dict[str, Any]] = [
dict(doc) for doc in search_results if doc["id"] in tag_doc_ids
dict(doc) for doc in search_results if doc.id in tag_doc_ids
][:limit]

if not results:
Expand Down
31 changes: 14 additions & 17 deletions emdx/commands/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from emdx.commands.categories import app as categories_app
from emdx.commands.epics import app as epics_app
from emdx.models import tasks
from emdx.models.types import TaskDict, TaskRef
from emdx.models.task import Task
from emdx.models.types import TaskRef
from emdx.utils.lazy_group import make_alias_group
from emdx.utils.output import console, is_non_interactive, print_json

Expand Down Expand Up @@ -58,7 +59,7 @@ def _blocker_summary(task_id: int) -> str:
return f"{names}{extra}"


def _display_id(task: TaskDict) -> str:
def _display_id(task: Task) -> str:
"""Return KEY-N display ID if available, otherwise #id."""
if task.get("epic_key") and task.get("epic_seq"):
return f"{task['epic_key']}-{task['epic_seq']}"
Expand Down Expand Up @@ -229,7 +230,7 @@ def ready(
ready_tasks = tasks.get_ready_tasks()

if json_output:
print_json(ready_tasks)
print_json([t.to_dict() if hasattr(t, "to_dict") else t for t in ready_tasks])
return

if not ready_tasks:
Expand Down Expand Up @@ -596,7 +597,7 @@ def brief(


def _assemble_brief(
task: TaskDict,
task: Task,
task_id: int,
log_limit: int,
) -> dict[str, object]:
Expand Down Expand Up @@ -681,11 +682,7 @@ def _assemble_brief(
}
)

output_id: int | None = None
try:
output_id = dict(task).get("output_doc_id") # type: ignore[assignment]
except Exception:
pass
output_id: int | None = task.get("output_doc_id")
if output_id:
output_doc = get_document(output_id)
related_docs.append(
Expand Down Expand Up @@ -898,7 +895,7 @@ def list_cmd(
)

if json_output:
print_json(task_list)
print_json([t.to_dict() if hasattr(t, "to_dict") else t for t in task_list])
return

if not task_list:
Expand All @@ -922,7 +919,7 @@ def list_cmd(
console.print(table)


def _task_label(task: TaskDict) -> str:
def _task_label(task: Task) -> str:
"""Format task label: DEBT-13 if epic, else #id."""
epic_key = task.get("epic_key")
epic_seq = task.get("epic_seq")
Expand All @@ -931,9 +928,9 @@ def _task_label(task: TaskDict) -> str:
return f"#{task['id']}"


def _display_title(task: TaskDict) -> str:
def _display_title(task: Task) -> str:
"""Strip redundant KEY-N: prefix from title since the ID column has it."""
title = task["title"]
title: str = task["title"]
epic_key = task.get("epic_key")
epic_seq = task.get("epic_seq")
if epic_key and epic_seq:
Expand Down Expand Up @@ -1112,7 +1109,7 @@ def dep_list(

if json_output:

def _dep_summary(d: TaskDict) -> dict[str, str | int]:
def _dep_summary(d: Task) -> dict[str, str | int]:
return {
"id": d["id"],
"display_id": _display_id(d),
Expand Down Expand Up @@ -1176,7 +1173,7 @@ def chain(

if json_output:

def _task_summary(t: TaskDict) -> dict[str, str | int]:
def _task_summary(t: Task) -> dict[str, str | int]:
return {
"id": t["id"],
"display_id": _display_id(t),
Expand Down Expand Up @@ -1214,11 +1211,11 @@ def _task_summary(t: TaskDict) -> dict[str, str | int]:
console.print("\n[yellow]No dependencies in either direction[/yellow]")


def _walk_deps(task_id: int, direction: str) -> list[TaskDict]:
def _walk_deps(task_id: int, direction: str) -> list[Task]:
"""BFS walk of dependency graph. Returns tasks in traversal order."""
visited: set[int] = set()
queue = [task_id]
result: list[TaskDict] = []
result: list[Task] = []

while queue:
current = queue.pop(0)
Expand Down
128 changes: 24 additions & 104 deletions emdx/database/types.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,40 @@
"""TypedDict definitions for the database layer."""
"""TypedDict definitions for the database layer.

Document-related types have been replaced by the Document dataclass
in emdx.models.document. Remaining types here are for non-document
database structures (stats, links, wiki, standing queries).
"""

from __future__ import annotations

from datetime import datetime
from typing import TypedDict

# ── Document types ─────────────────────────────────────────────────────


class DocumentRow(TypedDict):
"""Full document row from the documents table.

Datetime fields are parsed from SQLite strings to datetime objects
by ``_parse_doc_datetimes`` before being returned to callers.
"""

id: int
title: str
content: str
project: str | None
created_at: datetime | None
updated_at: datetime | None
accessed_at: datetime | None
access_count: int
deleted_at: datetime | None
is_deleted: int
parent_id: int | None
relationship: str | None
archived_at: datetime | None
stage: str | None
doc_type: str


class DocumentListItem(TypedDict):
"""Document item returned by list_documents."""

id: int
title: str
project: str | None
created_at: datetime | None
access_count: int
parent_id: int | None
relationship: str | None
archived_at: datetime | None
accessed_at: datetime | None


class RecentDocumentItem(TypedDict):
"""Document item returned by get_recent_documents."""

id: int
title: str
project: str | None
accessed_at: datetime | None
access_count: int
# ── Stats types ───────────────────────────────────────────────────────


class DeletedDocumentItem(TypedDict):
"""Document item returned by list_deleted_documents."""
class MostViewedDoc(TypedDict):
"""Most viewed document summary."""

id: int
title: str
project: str | None
deleted_at: datetime | None
access_count: int


class ChildDocumentItem(TypedDict):
"""Document item returned by get_children."""

id: int
title: str
project: str | None
created_at: datetime | None
parent_id: int | None
relationship: str | None
archived_at: datetime | None


class SupersedeCandidate(TypedDict):
"""Candidate document for supersede matching."""

id: int
title: str
content: str
project: str | None
created_at: datetime | None
parent_id: int | None


class SearchResult(TypedDict, total=False):
"""Search result from FTS5 queries."""

id: int
title: str
project: str | None
created_at: datetime | None
updated_at: datetime | None
snippet: str | None
rank: float
doc_type: str
class DatabaseStats(TypedDict, total=False):
"""Statistics returned by get_stats."""

total_documents: int
total_projects: int
total_views: int
avg_views: float
newest_doc: str | None
last_accessed: str | None
table_size: str
most_viewed: MostViewedDoc

class MostViewedDoc(TypedDict):
"""Most viewed document summary."""

id: int
title: str
access_count: int
# ── Document link types ───────────────────────────────────────────────


class DocumentLinkDetail(TypedDict):
Expand All @@ -123,6 +50,9 @@ class DocumentLinkDetail(TypedDict):
link_type: str


# ── Wiki types ────────────────────────────────────────────────────────


class WikiArticleTimingDict(TypedDict):
"""Step-level timing (milliseconds) for wiki article generation.

Expand All @@ -138,17 +68,7 @@ class WikiArticleTimingDict(TypedDict):
save_ms: float


class DatabaseStats(TypedDict, total=False):
"""Statistics returned by get_stats."""

total_documents: int
total_projects: int
total_views: int
avg_views: float
newest_doc: str | None
last_accessed: str | None
table_size: str
most_viewed: MostViewedDoc
# ── Standing query types ──────────────────────────────────────────────


class StandingQueryRow(TypedDict):
Expand Down
11 changes: 11 additions & 0 deletions emdx/models/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ class SearchHit:
snippet: str | None = None
rank: float = 0.0

# ── Attribute forwarding ──────────────────────────────────────────

def __getattr__(self, name: str) -> Any:
"""Forward attribute access to the inner Document."""
try:
return getattr(self.doc, name)
except AttributeError:
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
) from None

# ── Dict-compatibility layer ──────────────────────────────────────

def __getitem__(self, key: str) -> Any:
Expand Down
Loading
Loading