✅ fresh
last synced 2026-06-17T22:39:35.514075+00:00 · coverage 100% (
watcher)Validation by Beadloom
doc_sync— same source assync-check.
Watcher
File watcher for automatic reindex on file system changes.
Source: src/beadloom/application/watcher.py
Specification
Purpose
The watcher module monitors project directories for file changes and automatically triggers reindex operations. It uses the watchfiles library for efficient file system monitoring with configurable debounce. Graph YAML changes trigger a full reindex; all other changes (docs, code) trigger an incremental reindex. A callback mechanism supports programmatic consumers beyond the default Rich console output.
Data Structures
WatchEvent (frozen dataclass)
| Field | Type | Description |
|---|---|---|
files_changed | int | Number of relevant files in the debounced batch |
is_graph_change | bool | True if any changed file is inside .beadloom/_graph/ |
reindex_type | str | Either "full" or "incremental" |
Constants
DEFAULT_DEBOUNCE_MS
DEFAULT_DEBOUNCE_MS = 500Default debounce interval in milliseconds. Changes within this window are batched into a single reindex.
_WATCH_EXTENSIONS
Frozen set of file extensions that the watcher considers relevant:
_WATCH_EXTENSIONS = frozenset({
".yml", ".yaml", # graph
".md", # docs
".py", ".ts", ".tsx", # code
".js", ".jsx", # code
".go", ".rs", # code
})Watched Directories
_get_watch_paths(project_root) builds the list of directories to monitor:
| Directory | Condition | Purpose |
|---|---|---|
.beadloom/_graph/ | Always (if exists) | Graph YAML definitions |
docs/ | If exists | Markdown documentation |
src/ | If exists | Source code |
lib/ | If exists | Source code |
app/ | If exists | Source code |
If no directories exist, the watcher prints an error and returns immediately.
Change Classification
Changes are classified by path to determine reindex type:
- Graph change: File path starts with
<project_root>/.beadloom/_graph/(checked by_is_graph_file). - Non-graph change: All other relevant files.
If any file in a debounced batch is a graph change, the entire batch triggers a full reindex. Otherwise, an incremental reindex is triggered.
Filtering Logic
_filter_relevant(changes, project_root) applies the following filters to raw file system events:
- Temp file exclusion: Skip files whose name starts with
~or ends with.tmp. - Extension filter: Skip files whose suffix is not in
_WATCH_EXTENSIONS. - Relative path check: Skip files that are not relative to
project_root. - Hidden directory exclusion: Skip files inside hidden directories (name starts with
.), with the explicit exception of.beadloom. Only directory components are checked, not the filename itself.
Watch Loop
watch(project_root, debounce_ms, callback) operates as follows:
- Resolve watch paths via
_get_watch_paths. - If no paths, print error and return.
- Print monitored paths and debounce configuration via Rich console.
- Enter
watchfiles.watch()loop with configured debounce. - For each batch: a. Filter to relevant changes via
_filter_relevant. b. If no relevant changes remain, skip. c. Determine if any graph file changed. d. Execute full or incremental reindex accordingly. e. Print timestamped summary (UTCHH:MM:SSformat) with reindex type and file count. f. Ifcallbackis provided, invoke it with aWatchEvent. - On
KeyboardInterrupt, print stop message and return.
CLI Interface
beadloom watch [--debounce MS] [--project DIR]| Option | Type | Default | Description |
|---|---|---|---|
--debounce | int | 500 | Debounce delay in ms |
--project | Path | None (current directory) | Path to the project root |
The CLI command (watch_cmd in src/beadloom/services/commands/dashboard.py) performs two pre-flight checks before invoking watch():
- Verifies that
watchfilesis importable; exits with error and install instructions if missing. - Verifies that the
.beadloom/_graph/directory exists; exits with error suggestingbeadloom initif missing.
The command also wraps the watch() invocation in a second try/except ImportError as a safety net, catching cases where the import succeeds at module-load time but fails at call time (e.g., partial installation).
API
Public Functions
def watch(
project_root: Path,
debounce_ms: int = DEFAULT_DEBOUNCE_MS,
callback: Callable[[WatchEvent], None] | None = None,
) -> NoneWatch project files and auto-reindex on changes. This function blocks until interrupted by KeyboardInterrupt. The callback parameter enables programmatic consumers to receive WatchEvent notifications after each reindex.
Internal Functions
def _get_watch_paths(project_root: Path) -> list[Path]Build the list of directories to watch. Always includes .beadloom/_graph/ (if it exists); conditionally includes docs/, src/, lib/, app/.
def _is_graph_file(path_str: str, project_root: Path) -> boolCheck if a file path string is inside the .beadloom/_graph/ directory by comparing against the directory prefix string.
def _filter_relevant(
changes: Iterable[tuple[object, str]],
project_root: Path,
) -> list[tuple[object, str]]Filter raw watchfiles change events to only relevant changes. Each change is a (change_type, path_str) tuple.
def _format_time() -> strReturn current UTC time as HH:MM:SS string.
Public Classes
@dataclass(frozen=True)
class WatchEvent:
files_changed: int
is_graph_change: bool
reindex_type: str # "full" | "incremental"Public Constants
DEFAULT_DEBOUNCE_MS: int = 500Invariants
- Graph YAML changes (files inside
.beadloom/_graph/) always trigger a full reindex, never incremental. - Only one reindex executes per debounced batch. Multiple file changes within the debounce window are coalesced.
- The
callbackis invoked after the reindex completes, not before. WatchEvent.reindex_typeis always either"full"or"incremental"-- no other values._filter_relevantnever passes through temp files or files with non-watched extensions.
Constraints
- Requires the
watchfilesoptional dependency. It is not installed by default; install viabeadloom[watch]extra (orpip install beadloom[watch]). The CLI command catchesImportErrorat two points (import and invocation) and provides install instructions. - The CLI command also requires
.beadloom/_graph/to exist; it exits with an error suggestingbeadloom initif the directory is missing. KeyboardInterruptis the only supported mechanism to stop the watch loop. There is no programmatic stop/cancel API.- The watcher does not recurse into hidden directories except
.beadloom. Files in.git/,.venv/, or other dotdirs are never watched. - Watch paths are resolved once at startup. New directories created after the watcher starts (e.g., a new
lib/directory) are not automatically picked up; the watcher must be restarted. - The watcher uses
watchfiles.watch()which internally uses thenotifyRust crate for platform-native file system events. Behavior on network file systems may be unreliable. - Console output uses Rich formatting. In non-TTY environments the formatting degrades gracefully but timestamps and messages are still emitted.
Testing
Test files: tests/test_watcher.py
Tests should cover the following scenarios:
_get_watch_paths: Verify correct paths are returned based on which directories exist. Verify empty list when no relevant directories exist._is_graph_file: VerifyTruefor paths inside.beadloom/_graph/andFalsefor all other paths._filter_relevant-- temp files: Verify files starting with~or ending with.tmpare excluded._filter_relevant-- extension filter: Verify files with non-watched extensions (e.g.,.pyc,.log) are excluded._filter_relevant-- hidden directories: Verify files in.git/or.venv/are excluded, but files in.beadloom/_graph/are included._filter_relevant-- outside project root: Verify files not relative toproject_rootare excluded.WatchEventconstruction: Verify correct field values for graph changes (full reindex) and non-graph changes (incremental reindex)._format_time: Verify output format matchesHH:MM:SS.- Watch loop integration: Mock
watchfiles.watchto yield controlled batches, verify correct reindex functions are called and callback receives expectedWatchEventvalues. - No watch paths: Verify
watch()returns immediately when no directories to watch exist.