"""Vault statistics collector for the Knowtation domain. Computes lightweight, filesystem-only statistics by walking the vault and parsing frontmatter — no vector store, no network, no Node.js required. Designed to be called by ``muse plumbing domain-info`` when the active domain is ``knowtation``, and by any agent that needs a quick vault health summary without doing a full ``snapshot()``. Output shape (:class:`VaultStats`):: { "note_count": 76, "source_types": ["chatgpt-export", "slack", "telegram"], "projects": ["born-free", "dreambolt-network"], "entity_count": 14, "tag_count": 31, "index_freshness": "2026-05-12T17:30:00+00:00" // or null } ``index_freshness`` is the ISO 8601 UTC mtime of the ``data/`` directory at the vault root (where Knowtation stores its vector/sqlite index). Returns ``null`` when the directory does not exist, indicating the vault has never been indexed. """ from __future__ import annotations import os import pathlib from datetime import datetime, timezone from typing import TypedDict from muse.plugins.knowtation.parser import parse_frontmatter from muse.plugins.knowtation.plugin import _ALWAYS_IGNORE_DIRS # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- _MARKDOWN_EXTS: frozenset[str] = frozenset({".md", ".markdown", ".mdx"}) # Structural directories that hold scaffold files, not authored vault notes # (mirrors the exclusion applied by the data-integrity tests). _STRUCTURAL_DIRS: frozenset[str] = frozenset({"templates", "meta"}) _IGNORE_DIRS: frozenset[str] = _ALWAYS_IGNORE_DIRS | _STRUCTURAL_DIRS # --------------------------------------------------------------------------- # VaultStats TypedDict # --------------------------------------------------------------------------- class VaultStats(TypedDict): """Statistics snapshot for one Knowtation vault. ``note_count`` Total number of Markdown files found (excluding structural dirs). ``source_types`` Sorted list of unique ``source`` / ``source_type`` values seen across all notes. Identifies which capture/import pipelines have contributed. ``projects`` Sorted list of unique ``project`` slugs seen across all notes. ``entity_count`` Number of unique entity labels seen across all ``entity:`` fields. ``tag_count`` Number of unique tag strings seen across all ``tags:`` fields. ``index_freshness`` ISO 8601 UTC mtime of the ``data/`` index directory, or ``None`` when the vault has never been indexed. """ note_count: int source_types: list[str] projects: list[str] entity_count: int tag_count: int index_freshness: str | None # --------------------------------------------------------------------------- # Collection # --------------------------------------------------------------------------- def collect_vault_stats(root: pathlib.Path) -> VaultStats: """Walk *root* and return a :class:`VaultStats` snapshot. Parses only YAML frontmatter (via :func:`~parser.parse_frontmatter`); does not read note bodies or call any external service. Skips files and directories that are unreadable without raising. Args: root: Vault root directory (same as the Muse repo root for a ``knowtation`` repo). Returns: A :class:`VaultStats` mapping with counts and sorted lists. """ note_count: int = 0 source_types: set[str] = set() projects: set[str] = set() entities: set[str] = set() tags: set[str] = set() root_str = str(root) for dirpath, dirnames, filenames in os.walk(root_str, followlinks=False): # Prune ignored and hidden directories in-place so os.walk skips them. dirnames[:] = sorted( d for d in dirnames if d not in _IGNORE_DIRS and not d.startswith(".") ) for fname in sorted(filenames): suffix = pathlib.PurePosixPath(fname).suffix.lower() if suffix not in _MARKDOWN_EXTS: continue note_count += 1 fpath = pathlib.Path(dirpath) / fname try: content = fpath.read_bytes() except OSError: continue fm = parse_frontmatter(content) if fm is None: continue src = fm.effective_source if src: source_types.add(src) if fm.project: projects.add(fm.project) entities.update(fm.entity) tags.update(fm.tags) # Index freshness — mtime of data/ directory (Knowtation's index store). index_freshness: str | None = None data_dir = root / "data" if data_dir.exists(): try: mtime = data_dir.stat().st_mtime dt = datetime.fromtimestamp(mtime, tz=timezone.utc) index_freshness = dt.isoformat() except OSError: pass return VaultStats( note_count=note_count, source_types=sorted(source_types), projects=sorted(projects), entity_count=len(entities), tag_count=len(tags), index_freshness=index_freshness, )