gabriel / muse public
code_query.py python
200 lines 7.1 KB
Raw
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago
1 """``muse code code-query`` — predicate query over code commit history.
2
3 Search the commit graph for code changes matching a structured predicate::
4
5 muse code code-query "symbol == 'my_function' and change == 'added'"
6 muse code code-query "language == 'Python' and author == 'agent-x'"
7 muse code code-query "agent_id == 'claude' and sem_ver_bump == 'major'"
8 muse code code-query "file == 'src/core.py'"
9 muse code code-query "change == 'removed' and kind == 'class'"
10 muse code code-query "model_id contains 'claude'"
11 muse code code-query "symbol endswith _handler"
12 muse code code-query "author == 'gabriel'" --since 2026-01-01
13 muse code code-query "sem_ver_bump == 'major'" --count
14
15 Fields
16 ------
17
18 ``symbol`` Qualified symbol name (e.g. ``"MyClass.method"``).
19 ``file`` Workspace-relative file path.
20 ``language`` Language name (``"Python"``, ``"TypeScript"``…).
21 ``kind`` Symbol kind (``"function"``, ``"class"``, ``"method"``…).
22 ``change`` ``"added"``, ``"removed"``, or ``"modified"``.
23 ``author`` Commit author string.
24 ``agent_id`` Agent identity from commit provenance.
25 ``model_id`` Model ID from commit provenance.
26 ``toolchain_id`` Toolchain string from commit provenance.
27 ``sem_ver_bump`` ``"none"``, ``"patch"``, ``"minor"``, or ``"major"``.
28 ``branch`` Branch name.
29
30 Operators: ``==``, ``!=``, ``contains``, ``startswith``, ``endswith``
31
32 Usage::
33
34 muse code code-query QUERY
35 muse code code-query QUERY --branch dev --max 100
36 muse code code-query QUERY --since 2026-01-01 --until 2026-06-30
37 muse code code-query QUERY --limit 10
38 muse code code-query QUERY --count
39 muse code code-query QUERY --json
40 """
41
42 import argparse
43 import datetime
44 import json
45 import logging
46 import pathlib
47 import sys
48 from typing import TypedDict
49
50 from muse.core.query_engine import QueryMatch, format_matches, walk_history
51 from muse.core.repo import parse_date_arg, require_repo
52 from muse.core.refs import read_current_branch
53 from muse.core.timing import start_timer
54 from muse.core.envelope import EnvelopeJson, make_envelope
55 from muse.plugins.code._code_query import build_evaluator
56 from muse.core.validation import clamp_int
57
58 logger = logging.getLogger(__name__)
59
60 class _CodeQueryOutputJson(EnvelopeJson):
61 """Top-level JSON envelope emitted by ``muse code code-query --json``.
62
63 Fields
64 ------
65 total Total number of matching commits/symbols found.
66 results List of match dicts (commit_id, message, author, …).
67 exit_code Always 0 — errors raise SystemExit before JSON is emitted,
68 so agents can treat this as a reliable success indicator.
69 duration_ms Wall-clock time for the full query in milliseconds
70 (non-negative float).
71 """
72
73 total: int
74 results: list[QueryMatch]
75
76 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
77 """Register the code-query subcommand."""
78 parser = subparsers.add_parser(
79 "code-query",
80 help="Query the code commit history using a structured predicate.",
81 description=__doc__,
82 formatter_class=argparse.RawDescriptionHelpFormatter,
83 )
84 parser.add_argument(
85 "query",
86 help="Query expression (see muse code code-query --help).",
87 )
88 parser.add_argument(
89 "--branch", default=None,
90 help="Branch to search (default: HEAD branch).",
91 )
92 parser.add_argument(
93 "--max", type=int, default=200, dest="max_commits",
94 help="Maximum commits to inspect (walk depth). Default: 200.",
95 )
96 parser.add_argument(
97 "--limit", type=int, default=None, dest="limit",
98 help="Maximum matches to display. Does not affect walk depth (see --max).",
99 )
100 parser.add_argument(
101 "--since", default=None, metavar="DATE",
102 help="Only include commits on or after DATE (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).",
103 )
104 parser.add_argument(
105 "--until", default=None, metavar="DATE",
106 help="Only include commits on or before DATE (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).",
107 )
108 parser.add_argument(
109 "--count", action="store_true",
110 help="Print only the total match count, not the matches themselves.",
111 )
112 parser.add_argument(
113 "--json", "-j", action="store_true", dest="json_out",
114 help="Emit JSON array of matches.",
115 )
116 parser.set_defaults(func=run)
117
118 def run(args: argparse.Namespace) -> None:
119 """Query the code commit history using a structured predicate.
120
121 Walks up to ``--max`` commits from HEAD on the specified branch and returns
122 all commits (and symbol-level changes) matching the predicate. Predicates
123 can filter by symbol name, agent ID, semantic-version bump, author, file, or
124 commit date. Use ``--count`` to just count matching commits.
125
126 Agent quickstart
127 ----------------
128 ::
129
130 muse code query "symbol == 'parse_query' and change == 'added'" --json
131 muse code query "agent_id contains 'claude'" --since 2026-01-01 --json
132 muse code query "sem_ver_bump == 'major'" --count --json
133
134 JSON fields
135 -----------
136 query The predicate string as passed.
137 branch Branch walked.
138 total Total number of matching commits.
139 truncated ``true`` if ``--max`` was reached before the full history.
140 commits List of matching commit objects: ``commit_id``, ``message``,
141 ``committed_at``, ``author``, ``agent_id``, ``symbol_changes``
142 (list of ``{symbol, change, file}``).
143
144 Exit codes
145 ----------
146 0 Query completed.
147 1 Query parse error or invalid arguments.
148 2 Not inside a Muse repository.
149 """
150 elapsed = start_timer()
151 query: str = args.query
152 branch: str | None = args.branch
153 max_commits: int = clamp_int(args.max_commits, 1, 100_000, 'max_commits')
154 limit: int | None = args.limit
155 json_out: bool = args.json_out
156 count_only: bool = args.count
157
158 since: datetime.datetime | None = (
159 parse_date_arg(args.since, "--since") if args.since else None
160 )
161 until: datetime.datetime | None = (
162 parse_date_arg(args.until, "--until") if args.until else None
163 )
164
165 root: pathlib.Path = require_repo()
166
167 try:
168 evaluator = build_evaluator(query)
169 except ValueError as exc:
170 print(f"❌ Query parse error: {exc}", file=sys.stderr)
171 raise SystemExit(1) from exc
172
173 resolved_branch = branch or read_current_branch(root)
174
175 # The code evaluator reads commit.structured_delta only — it never touches
176 # the snapshot manifest. Skipping manifest I/O cuts one file-read per commit.
177 matches = walk_history(
178 root,
179 resolved_branch,
180 evaluator,
181 max_commits=max_commits,
182 load_manifest=False,
183 since=since,
184 until=until,
185 )
186
187 if count_only:
188 print(len(matches))
189 return
190
191 if json_out:
192 print(json.dumps(_CodeQueryOutputJson(
193 **make_envelope(elapsed),
194 total=len(matches),
195 results=matches,
196 )))
197 return
198
199 display_limit = limit if limit is not None else 50
200 print(format_matches(matches, max_results=display_limit))
File History 1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40 docs: add | jq convention to --json section of agent-guide Sonnet 4.6 1 day ago