Coverage for src/prosemark/app/materialize_node.py: 100%
36 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-24 18:08 +0000
1"""MaterializeNode use case for converting placeholders to actual nodes."""
3from pathlib import Path
4from typing import TYPE_CHECKING
6from prosemark.domain.models import BinderItem, NodeId
7from prosemark.exceptions import PlaceholderNotFoundError
9if TYPE_CHECKING: # pragma: no cover
10 from prosemark.ports.binder_repo import BinderRepo
11 from prosemark.ports.clock import Clock
12 from prosemark.ports.console_port import ConsolePort
13 from prosemark.ports.id_generator import IdGenerator
14 from prosemark.ports.logger import Logger
15 from prosemark.ports.node_repo import NodeRepo
18class MaterializeNode:
19 """Convert placeholder items in the binder to actual content nodes."""
21 def __init__(
22 self,
23 *,
24 binder_repo: 'BinderRepo',
25 node_repo: 'NodeRepo',
26 id_generator: 'IdGenerator',
27 clock: 'Clock',
28 console: 'ConsolePort',
29 logger: 'Logger',
30 ) -> None:
31 """Initialize the MaterializeNode use case.
33 Args:
34 binder_repo: Repository for binder operations.
35 node_repo: Repository for node operations.
36 id_generator: Generator for unique node IDs.
37 clock: Clock for timestamps.
38 console: Console output port.
39 logger: Logger port.
41 """
42 self.binder_repo = binder_repo
43 self.node_repo = node_repo
44 self.id_generator = id_generator
45 self.clock = clock
46 self.console = console
47 self.logger = logger
49 def execute(
50 self,
51 *,
52 title: str,
53 project_path: Path | None = None,
54 ) -> NodeId:
55 """Materialize a placeholder into a real node.
57 Args:
58 title: Title of the placeholder to materialize.
59 project_path: Project directory path.
61 Returns:
62 The ID of the newly created node.
64 Raises:
65 PlaceholderNotFoundError: If no placeholder with the given title is found.
67 """
68 project_path = project_path or Path.cwd()
69 self.logger.info('Materializing placeholder: %s', title)
71 # Load existing binder
72 binder = self.binder_repo.load()
74 # Find the placeholder item
75 placeholder = self._find_placeholder(binder.roots, title)
76 if not placeholder:
77 msg = f"Placeholder '{title}' not found"
78 raise PlaceholderNotFoundError(msg)
80 # Check if already materialized
81 if placeholder.node_id: # pragma: no cover
82 self.console.print_warning(f"'{title}' is already materialized") # pragma: no cover
83 return placeholder.node_id # pragma: no cover
85 # Generate new node ID
86 node_id = self.id_generator.new()
88 # Create the node files
89 self.node_repo.create(node_id, title, None)
91 # Update the placeholder with the node ID
92 placeholder.node_id = node_id
94 # Save updated binder
95 self.binder_repo.save(binder)
97 self.console.print_success(f'Materialized "{title}" ({node_id.value})')
98 self.console.print_info(f'Created files: {node_id.value}.md, {node_id.value}.notes.md')
99 self.logger.info('Placeholder materialized: %s -> %s', title, node_id.value)
101 return node_id
103 def _find_placeholder(self, items: list[BinderItem], title: str) -> BinderItem | None:
104 """Find a placeholder item by title in the hierarchy.
106 Args:
107 items: List of binder items to search.
108 title: Title to search for.
110 Returns:
111 The placeholder item if found, None otherwise.
113 """
114 for item in items:
115 if item.display_title == title and not item.node_id:
116 return item
117 found = self._find_placeholder(item.children, title)
118 if found:
119 return found
120 return None