_query.py python
205 lines 6.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 33 days ago
1 """Shared query helpers for the knowtation-domain CLI commands.
2
3 This module provides the low-level primitives that multiple knowtation-domain
4 commands need — note classification, vault-wide file inspection, commit-graph
5 walking (reused from ``muse.plugins.code._query``), and project grouping —
6 so each CLI command can stay thin.
7
8 None of these functions are part of the public ``KnowtationPlugin`` API. They
9 are internal helpers for the CLI layer and must not be imported by any core
10 module.
11
12 Phase notes
13 -----------
14 Phase 1.1 provides classification and vault-wide helpers.
15 Phase 1.4 (symbol model and parser) will add ``symbols_for_note`` once the
16 parser for frontmatter + sections exists.
17 Phase 3.1 (link indexer) will add ``links_for_note``.
18 """
19
20 from __future__ import annotations
21
22 import pathlib
23 import logging
24
25 from muse.core._types import Manifest
26
27 logger = logging.getLogger(__name__)
28
29
30 # ---------------------------------------------------------------------------
31 # Note file classification
32 # ---------------------------------------------------------------------------
33
34 #: File extensions that receive Markdown-level semantic parsing.
35 NOTE_EXTENSIONS: frozenset[str] = frozenset({".md", ".markdown", ".mdx"})
36
37 #: File extensions that are tracked at raw-bytes level only.
38 #: Kept here so CLI display commands can label files accurately.
39 _OTHER_TRACKED_EXTENSIONS: dict[str, str] = {
40 ".txt": "Text",
41 ".rst": "reStructuredText",
42 ".yaml": "YAML",
43 ".yml": "YAML",
44 ".json": "JSON",
45 ".toml": "TOML",
46 ".html": "HTML",
47 ".htm": "HTML",
48 ".css": "CSS",
49 ".svg": "SVG",
50 ".png": "Image",
51 ".jpg": "Image",
52 ".jpeg": "Image",
53 ".gif": "Image",
54 ".webp": "Image",
55 ".pdf": "PDF",
56 ".mp3": "Audio",
57 ".m4a": "Audio",
58 ".wav": "Audio",
59 ".mp4": "Video",
60 ".mov": "Video",
61 ".sh": "Shell",
62 ".zsh": "Shell",
63 ".bash": "Shell",
64 }
65
66
67 def file_type_of(file_path: str) -> str:
68 """Return a display type label for *file_path* based on its extension.
69
70 Markdown files return ``"Markdown"``. Other known types return their
71 category name. Unknown extensions return the raw suffix string (or
72 ``"(no ext)"`` when there is none).
73
74 Args:
75 file_path: Workspace-relative POSIX path.
76
77 Returns:
78 Human-readable file type string, e.g. ``"Markdown"``, ``"YAML"``,
79 ``"Image"``, etc.
80 """
81 suffix = pathlib.PurePosixPath(file_path).suffix.lower()
82 if suffix in NOTE_EXTENSIONS:
83 return "Markdown"
84 return _OTHER_TRACKED_EXTENSIONS.get(suffix, suffix or "(no ext)")
85
86
87 def is_note(file_path: str) -> bool:
88 """Return ``True`` when *file_path* has a Markdown extension.
89
90 This is the gate for semantic (frontmatter + section) processing. Files
91 for which this returns ``False`` are tracked at raw-bytes level only.
92
93 Args:
94 file_path: Workspace-relative POSIX path.
95
96 Returns:
97 ``True`` when the extension is in ``NOTE_EXTENSIONS``.
98 """
99 suffix = pathlib.PurePosixPath(file_path).suffix.lower()
100 return suffix in NOTE_EXTENSIONS
101
102
103 # ---------------------------------------------------------------------------
104 # Vault-wide note enumeration
105 # ---------------------------------------------------------------------------
106
107
108 def notes_in_manifest(manifest: Manifest) -> dict[str, str]:
109 """Return the subset of *manifest* that contains only Markdown notes.
110
111 Args:
112 manifest: Full snapshot manifest mapping path → content_hash.
113
114 Returns:
115 A new dict containing only paths where :func:`is_note` is ``True``.
116 """
117 return {path: oid for path, oid in manifest.items() if is_note(path)}
118
119
120 def non_notes_in_manifest(manifest: Manifest) -> dict[str, str]:
121 """Return the subset of *manifest* that contains non-Markdown files.
122
123 Args:
124 manifest: Full snapshot manifest mapping path → content_hash.
125
126 Returns:
127 A new dict containing only paths where :func:`is_note` is ``False``.
128 """
129 return {path: oid for path, oid in manifest.items() if not is_note(path)}
130
131
132 def vault_note_count(manifest: Manifest) -> int:
133 """Return the number of Markdown notes in *manifest*.
134
135 Args:
136 manifest: Full snapshot manifest.
137
138 Returns:
139 Integer count of ``.md`` / ``.markdown`` / ``.mdx`` paths.
140 """
141 return sum(1 for path in manifest if is_note(path))
142
143
144 # ---------------------------------------------------------------------------
145 # Project grouping helpers
146 # ---------------------------------------------------------------------------
147
148
149 def group_by_project(
150 manifest: Manifest,
151 ) -> dict[str, list[str]]:
152 """Group note paths in *manifest* by their POSIX directory prefix.
153
154 This is a fast heuristic grouping used by ``muse plumbing domain-info``
155 (Phase 1.5) before the full frontmatter parser (Phase 1.3) is available.
156 Notes at the vault root are placed under the ``"__root__"`` key.
157
158 Phase 1.5 will replace this with a frontmatter-aware grouper once
159 ``FrontmatterSchema`` is wired.
160
161 Args:
162 manifest: Full snapshot manifest mapping path → content_hash.
163
164 Returns:
165 ``{project_name: [note_paths]}`` dict, sorted by project name.
166 """
167 groups: dict[str, list[str]] = {}
168 for path in sorted(manifest):
169 if not is_note(path):
170 continue
171 parts = path.split("/")
172 project = parts[0] if len(parts) > 1 else "__root__"
173 groups.setdefault(project, []).append(path)
174 return dict(sorted(groups.items()))
175
176
177 # ---------------------------------------------------------------------------
178 # Mist ID validation
179 # ---------------------------------------------------------------------------
180
181 #: Mist attachment IDs are 12-char base58 SHA-256 prefixes.
182 #: Valid character set: Bitcoin base58 (no 0, O, I, l).
183 _BASE58_CHARS: frozenset[str] = frozenset(
184 "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
185 )
186
187 _MIST_ID_LENGTH = 12
188
189
190 def is_valid_mist_id(mist_id: str) -> bool:
191 """Return ``True`` when *mist_id* looks like a valid mist attachment ID.
192
193 A valid mist ID is exactly 12 characters from the Bitcoin base58 alphabet
194 (no 0, O, I, or lowercase l). This mirrors the mist domain manifest which
195 defines ``artifacts`` with ``id = first 12 chars of base58 SHA-256``.
196
197 Args:
198 mist_id: The candidate mist attachment ID string.
199
200 Returns:
201 ``True`` when the ID is exactly 12 valid base58 characters.
202 """
203 if len(mist_id) != _MIST_ID_LENGTH:
204 return False
205 return all(c in _BASE58_CHARS for c in mist_id)
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 33 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 33 days ago