graphite-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Deterministic natural-language front-end for canonical queries.
|
|
2
|
+
|
|
3
|
+
`graphite query --natural "<question>"` runs a fixed, grammar-based intent
|
|
4
|
+
parser: an ordered table of anchored regular expressions, first match wins.
|
|
5
|
+
Recognized question shapes translate into canonical query plans (executed
|
|
6
|
+
through the same validated path as structured queries) or into a suggestion
|
|
7
|
+
for the canonical command that answers them (impact/context/tests). Anything
|
|
8
|
+
else falls back to deterministic ranked search, whose results serve as
|
|
9
|
+
clarification candidates.
|
|
10
|
+
|
|
11
|
+
Canonical-safe by construction: no providers, no network, no configuration
|
|
12
|
+
knobs — the grammar below is the entire capability, and it is published via
|
|
13
|
+
`graphite capabilities` so agents never have to guess phrasings.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import networkx as nx
|
|
22
|
+
|
|
23
|
+
from .query import _VERB_INDEX, execute_plan, search_graph
|
|
24
|
+
from .query_plan import make_plan
|
|
25
|
+
|
|
26
|
+
RESULT_SCHEMA_VERSION = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class NaturalRule:
|
|
31
|
+
"""One grammar rule: intent plus the anchored pattern that recognizes it."""
|
|
32
|
+
|
|
33
|
+
intent: str # query operation name, or "impact" | "tests" | "context"
|
|
34
|
+
kind: str # "operation" (translates to a plan) | "command" (suggests a CLI command)
|
|
35
|
+
template: str # human-readable form published via capabilities
|
|
36
|
+
pattern: re.Pattern[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _rule(intent: str, kind: str, template: str, pattern: str) -> NaturalRule:
|
|
40
|
+
return NaturalRule(intent, kind, template, re.compile(pattern))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Ordered, first match wins; every pattern is fullmatched against the
|
|
44
|
+
# normalized (lowercased, squashed, unpunctuated) question.
|
|
45
|
+
NATURAL_RULES: tuple[NaturalRule, ...] = (
|
|
46
|
+
_rule("callers", "operation", "who calls <symbol>", r"(?:who|what) calls (?P<node>.+)"),
|
|
47
|
+
_rule("callers", "operation", "callers of <symbol>", r"(?:list |show )?callers of (?P<node>.+)"),
|
|
48
|
+
_rule("callers", "operation", "who uses <symbol>", r"(?:who|what) uses (?P<node>.+)"),
|
|
49
|
+
_rule("calls", "operation", "what does <symbol> call", r"what does (?P<node>.+?) call"),
|
|
50
|
+
_rule("calls", "operation", "callees of <symbol>", r"(?:list |show )?callees of (?P<node>.+)"),
|
|
51
|
+
_rule(
|
|
52
|
+
"depends-on", "operation", "what does <node> depend on",
|
|
53
|
+
r"what does (?P<node>.+?) depend on",
|
|
54
|
+
),
|
|
55
|
+
_rule(
|
|
56
|
+
"depends-on", "operation", "dependencies of <node>",
|
|
57
|
+
r"(?:list |show )?dependencies of (?P<node>.+)",
|
|
58
|
+
),
|
|
59
|
+
_rule(
|
|
60
|
+
"depends-on", "operation", "what does <node> import",
|
|
61
|
+
r"what does (?P<node>.+?) import",
|
|
62
|
+
),
|
|
63
|
+
_rule("imported-by", "operation", "what depends on <node>", r"what depends on (?P<node>.+)"),
|
|
64
|
+
_rule("imported-by", "operation", "who imports <node>", r"(?:who|what) imports (?P<node>.+)"),
|
|
65
|
+
_rule(
|
|
66
|
+
"imported-by", "operation", "dependents of <node>",
|
|
67
|
+
r"(?:list |show )?dependents of (?P<node>.+)",
|
|
68
|
+
),
|
|
69
|
+
_rule(
|
|
70
|
+
"imported-by", "operation", "where is <node> imported",
|
|
71
|
+
r"where is (?P<node>.+?) (?:imported|used)",
|
|
72
|
+
),
|
|
73
|
+
_rule(
|
|
74
|
+
"reaches", "operation", "can <a> reach <b>",
|
|
75
|
+
r"can (?P<source>.+?) reach (?P<target>.+)",
|
|
76
|
+
),
|
|
77
|
+
_rule(
|
|
78
|
+
"reaches", "operation", "is there a call path from <a> to <b>",
|
|
79
|
+
r"is there a call path from (?P<source>.+?) to (?P<target>.+)",
|
|
80
|
+
),
|
|
81
|
+
_rule(
|
|
82
|
+
"reaches", "operation", "does <a> call <b>",
|
|
83
|
+
r"does (?P<source>.+?) (?:eventually |transitively )?call (?P<target>.+)",
|
|
84
|
+
),
|
|
85
|
+
_rule(
|
|
86
|
+
"path", "operation", "path from <a> to <b>",
|
|
87
|
+
r"(?:shortest |show |find )*path from (?P<source>.+?) to (?P<target>.+)",
|
|
88
|
+
),
|
|
89
|
+
_rule(
|
|
90
|
+
"path", "operation", "is there a path from <a> to <b>",
|
|
91
|
+
r"is there a path from (?P<source>.+?) to (?P<target>.+)",
|
|
92
|
+
),
|
|
93
|
+
_rule(
|
|
94
|
+
"path", "operation", "how does <a> reach <b>",
|
|
95
|
+
r"how does (?P<source>.+?) reach (?P<target>.+)",
|
|
96
|
+
),
|
|
97
|
+
_rule(
|
|
98
|
+
"stats", "operation", "graph stats",
|
|
99
|
+
r"(?:graph )?(?:stats|statistics|overview|summary)",
|
|
100
|
+
),
|
|
101
|
+
_rule("stats", "operation", "how big is the graph", r"how big is the graph"),
|
|
102
|
+
_rule(
|
|
103
|
+
"impact", "command", "what breaks if i change <path>",
|
|
104
|
+
r"what (?:breaks|is affected|gets affected) (?:if|when) (?:i )?(?:change|modify|edit|touch) (?P<node>.+)",
|
|
105
|
+
),
|
|
106
|
+
_rule(
|
|
107
|
+
"impact", "command", "impact of changing <path>",
|
|
108
|
+
r"impact of (?:changing |modifying |editing )?(?P<node>.+)",
|
|
109
|
+
),
|
|
110
|
+
_rule("impact", "command", "blast radius of <path>", r"blast radius (?:of|for) (?P<node>.+)"),
|
|
111
|
+
_rule(
|
|
112
|
+
"tests", "command", "what tests cover <path>",
|
|
113
|
+
r"(?:what|which) tests (?:cover|exercise|test) (?P<node>.+)",
|
|
114
|
+
),
|
|
115
|
+
_rule(
|
|
116
|
+
"tests", "command", "tests for <path>",
|
|
117
|
+
r"(?:what |which )?tests (?:for|to run for) (?P<node>.+)",
|
|
118
|
+
),
|
|
119
|
+
_rule(
|
|
120
|
+
"context", "command", "context for <node>",
|
|
121
|
+
r"(?:show |give |get )?(?:me )?context (?:for|of|about) (?P<node>.+)",
|
|
122
|
+
),
|
|
123
|
+
_rule("context", "command", "tell me about <node>", r"tell me about (?P<node>.+)"),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
_COMMAND_SUGGESTIONS: dict[str, tuple[str, str]] = {
|
|
127
|
+
"impact": ("impact", "impact analysis runs as its own canonical command"),
|
|
128
|
+
"tests": ("impact", "run graphite impact; its likely_tests field lists tests to run"),
|
|
129
|
+
"context": ("context", "context runs as its own canonical command"),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_ARTICLES = ("the ", "a ", "an ", "this ", "my ", "our ")
|
|
133
|
+
_DESCRIPTORS = (" function", " method", " class", " file", " module", " node", " symbol")
|
|
134
|
+
|
|
135
|
+
_STOPWORDS = frozenset(
|
|
136
|
+
"a an and are can could did do does for from give how i in is it list me my of on or our "
|
|
137
|
+
"please should show tell that the this to was we what when where which who whom whose why "
|
|
138
|
+
"will with would you your about find".split()
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _normalize(question: str) -> str:
|
|
143
|
+
text = re.sub(r"\s+", " ", question.strip().lower()).strip("\"'")
|
|
144
|
+
return re.sub(r"[?!.]+$", "", text).strip()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _clean_target(text: str) -> str:
|
|
148
|
+
cleaned = text.strip().strip("`\"'")
|
|
149
|
+
if cleaned in {article.strip() for article in _ARTICLES}:
|
|
150
|
+
return ""
|
|
151
|
+
for article in _ARTICLES:
|
|
152
|
+
if cleaned.startswith(article):
|
|
153
|
+
cleaned = cleaned[len(article):]
|
|
154
|
+
break
|
|
155
|
+
for descriptor in _DESCRIPTORS:
|
|
156
|
+
if cleaned.endswith(descriptor):
|
|
157
|
+
cleaned = cleaned[: -len(descriptor)]
|
|
158
|
+
break
|
|
159
|
+
return cleaned.strip().strip("`\"'")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def natural_catalog() -> list[dict[str, Any]]:
|
|
163
|
+
"""Machine-readable grammar listing, grouped by intent in rule order."""
|
|
164
|
+
catalog: list[dict[str, Any]] = []
|
|
165
|
+
for rule in NATURAL_RULES:
|
|
166
|
+
entry = next((e for e in catalog if e["intent"] == rule.intent), None)
|
|
167
|
+
if entry is None:
|
|
168
|
+
entry = {"intent": rule.intent, "kind": rule.kind, "templates": []}
|
|
169
|
+
catalog.append(entry)
|
|
170
|
+
entry["templates"].append(rule.template)
|
|
171
|
+
return catalog
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _search_fallback(question: str, normalized: str) -> dict[str, Any]:
|
|
175
|
+
terms = " ".join(w for w in normalized.split() if w not in _STOPWORDS)
|
|
176
|
+
if not terms:
|
|
177
|
+
return {
|
|
178
|
+
"schema_version": RESULT_SCHEMA_VERSION,
|
|
179
|
+
"error": "could not extract search terms from question",
|
|
180
|
+
"error_code": "natural_no_terms",
|
|
181
|
+
"suggestions": [
|
|
182
|
+
"Ask about a concrete symbol, file, or path",
|
|
183
|
+
"Run `graphite capabilities --json` to list the supported question grammar",
|
|
184
|
+
],
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
"schema_version": RESULT_SCHEMA_VERSION,
|
|
188
|
+
"natural": {"question": question, "intent": "search", "pattern": None},
|
|
189
|
+
"suggestion": {
|
|
190
|
+
"command": ["graphite", "search", terms],
|
|
191
|
+
"reason": "no grammar pattern matched; ranked search results serve as clarification candidates",
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def translate_natural(question: str) -> dict[str, Any]:
|
|
197
|
+
"""Deterministically translate a question, without touching any graph.
|
|
198
|
+
|
|
199
|
+
Returns one of: {natural, plan} for verb intents; {natural, suggestion} for
|
|
200
|
+
command intents and the search fallback; or an error dict with a stable
|
|
201
|
+
error_code. Never raises, never does inference.
|
|
202
|
+
"""
|
|
203
|
+
normalized = _normalize(question)
|
|
204
|
+
if not normalized:
|
|
205
|
+
return {
|
|
206
|
+
"schema_version": RESULT_SCHEMA_VERSION,
|
|
207
|
+
"error": "empty query",
|
|
208
|
+
"error_code": "empty_query",
|
|
209
|
+
}
|
|
210
|
+
for rule in NATURAL_RULES:
|
|
211
|
+
m = rule.pattern.fullmatch(normalized)
|
|
212
|
+
if m is None:
|
|
213
|
+
continue
|
|
214
|
+
captured = {key: _clean_target(value) for key, value in m.groupdict().items()}
|
|
215
|
+
if any(not value for value in captured.values()):
|
|
216
|
+
break # degenerate capture (e.g. "who calls the"); fail closed to search
|
|
217
|
+
natural = {"question": question, "intent": rule.intent, "pattern": rule.template}
|
|
218
|
+
if rule.kind == "command":
|
|
219
|
+
command, reason = _COMMAND_SUGGESTIONS[rule.intent]
|
|
220
|
+
return {
|
|
221
|
+
"schema_version": RESULT_SCHEMA_VERSION,
|
|
222
|
+
"natural": natural,
|
|
223
|
+
"suggestion": {
|
|
224
|
+
"command": ["graphite", command, captured["node"]],
|
|
225
|
+
"reason": reason,
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
spec = _VERB_INDEX[rule.intent]
|
|
229
|
+
extracted = [captured[key] for key in ("node", "source", "target") if key in captured]
|
|
230
|
+
targets = list(zip(spec.roles, extracted))
|
|
231
|
+
plan = make_plan(spec.name, targets, dict(spec.limits))
|
|
232
|
+
return {"schema_version": RESULT_SCHEMA_VERSION, "natural": natural, "plan": plan}
|
|
233
|
+
return _search_fallback(question, normalized)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def answer_natural(g: nx.DiGraph, question: str) -> dict[str, Any]:
|
|
237
|
+
"""Translate and answer: execute the plan, or run the search fallback.
|
|
238
|
+
|
|
239
|
+
Verb-intent errors (node_not_found with candidates, no_path, ...) pass
|
|
240
|
+
through the normal query envelope with the natural interpretation and plan
|
|
241
|
+
attached, so a failed resolution still explains itself.
|
|
242
|
+
"""
|
|
243
|
+
translated = translate_natural(question)
|
|
244
|
+
if "error" in translated:
|
|
245
|
+
return translated
|
|
246
|
+
if "plan" in translated:
|
|
247
|
+
result = execute_plan(g, translated["plan"])
|
|
248
|
+
return {**result, "plan": translated["plan"], "natural": translated["natural"]}
|
|
249
|
+
if translated["natural"]["intent"] == "search":
|
|
250
|
+
terms = translated["suggestion"]["command"][2]
|
|
251
|
+
return {**translated, "search": search_graph(g, terms)}
|
|
252
|
+
return translated
|