codmap 0.0.3__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.
- codemap/__init__.py +10 -0
- codemap/apidiff.py +208 -0
- codemap/arch.py +190 -0
- codemap/cli.py +718 -0
- codemap/diagnostics.py +256 -0
- codemap/extract/__init__.py +10 -0
- codemap/extract/attrflow.py +230 -0
- codemap/extract/behavior.py +771 -0
- codemap/extract/dataflow.py +97 -0
- codemap/extract/dispatch.py +248 -0
- codemap/extract/griffe_extractor.py +496 -0
- codemap/extract/gsource.py +83 -0
- codemap/extract/roots.py +427 -0
- codemap/freshness.py +94 -0
- codemap/incremental.py +195 -0
- codemap/integrations/__init__.py +51 -0
- codemap/integrations/base.py +196 -0
- codemap/integrations/cocoindex.py +78 -0
- codemap/integrations/gate.py +58 -0
- codemap/integrations/gitnexus.py +93 -0
- codemap/integrations/registry.py +69 -0
- codemap/integrations/transport.py +46 -0
- codemap/model.py +178 -0
- codemap/provenance.py +248 -0
- codemap/query.py +1164 -0
- codemap/scope.py +212 -0
- codemap/serve/__init__.py +26 -0
- codemap/serve/_scip_pb2.py +100 -0
- codemap/serve/api_surface.py +60 -0
- codemap/serve/apidiff.py +83 -0
- codemap/serve/architecture.py +101 -0
- codemap/serve/audit.py +176 -0
- codemap/serve/check.py +80 -0
- codemap/serve/ctags.py +203 -0
- codemap/serve/impact.py +84 -0
- codemap/serve/livingdocs.py +174 -0
- codemap/serve/mcp_server.py +278 -0
- codemap/serve/mermaid.py +120 -0
- codemap/serve/pack.py +93 -0
- codemap/serve/rag.py +142 -0
- codemap/serve/review.py +197 -0
- codemap/serve/scip.py +183 -0
- codemap/serve/semantic.py +71 -0
- codemap/serve/server.py +43 -0
- codemap/serve/session.py +482 -0
- codemap/serve/subsystems.py +85 -0
- codemap/serve/vault.py +156 -0
- codemap/store.py +28 -0
- codemap/tomlio.py +59 -0
- codmap-0.0.3.dist-info/METADATA +245 -0
- codmap-0.0.3.dist-info/RECORD +55 -0
- codmap-0.0.3.dist-info/WHEEL +5 -0
- codmap-0.0.3.dist-info/entry_points.txt +2 -0
- codmap-0.0.3.dist-info/licenses/LICENSE +21 -0
- codmap-0.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Dataflow-by-string-key pass — column/key flow (DESIGN §7, M12, gap-doc F6).
|
|
2
|
+
|
|
3
|
+
The behavioral layer follows *symbols*; but bquant threads its data through
|
|
4
|
+
string-keyed DataFrame columns — ``df['macd_hist'] = …`` here, ``macd_data['macd_hist']``
|
|
5
|
+
there — and the call-graph sees none of it (dogfood F6: querying ``macd_hist``
|
|
6
|
+
returned nothing, worse than grep). This pass makes those keys first-class:
|
|
7
|
+
|
|
8
|
+
- a ``column`` node per distinct string key (``column:macd_hist``);
|
|
9
|
+
- a ``writes`` edge (function → column) for ``x['key'] = …`` (Store subscript) and
|
|
10
|
+
for a dict-literal producer ``{'key': value}`` (how bquant returns computed columns);
|
|
11
|
+
- a ``reads`` edge (function → column) for ``x['key']`` used as a value (Load).
|
|
12
|
+
|
|
13
|
+
**Honesty.** This is *string-keyed access* flow, an **over-set** of DataFrame
|
|
14
|
+
columns: ``config['path']`` and ``d['k']`` land here too — statically we cannot
|
|
15
|
+
tell a frame from a dict without type inference. Querying a *specific* key
|
|
16
|
+
(``macd_hist``) is precise regardless; treat the node set as "keys", not "columns".
|
|
17
|
+
Embedded sample-data modules (literal datasets) are skipped — they are data, not code.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
|
|
24
|
+
from codemap.extract.behavior import _index_modules, _named_functions, _node_id
|
|
25
|
+
from codemap.extract.gsource import module_file
|
|
26
|
+
from codemap.model import Edge, Node
|
|
27
|
+
|
|
28
|
+
_COLUMN_PREFIX = "column:"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def add_dataflow(graph, griffe_root, target_pkg: str) -> None:
|
|
32
|
+
"""Add column nodes + reads/writes edges for string-keyed subscripts.
|
|
33
|
+
|
|
34
|
+
Each column node records ``extras.subscripted`` (M14/F15): True iff the key was
|
|
35
|
+
ever accessed as ``x['k']`` somewhere, not only used as a dict-literal payload
|
|
36
|
+
key. Each edge records ``extras.access`` (``subscript`` | ``dict-literal``). The
|
|
37
|
+
B1 dogfood found 71% of keys were dict-literal-only (result dicts, config,
|
|
38
|
+
rcParams) — the flag lets aggregates surface the real column-like set.
|
|
39
|
+
"""
|
|
40
|
+
modules = _index_modules(griffe_root)
|
|
41
|
+
subscripted: dict[str, bool] = {} # key -> ever accessed as a subscript
|
|
42
|
+
edges: set[tuple[str, str, str, str]] = set() # (type, func_id, col_id, access)
|
|
43
|
+
for modpath in sorted(modules):
|
|
44
|
+
if "samples.embedded" in modpath:
|
|
45
|
+
continue # embedded datasets are data, not code
|
|
46
|
+
mod = modules[modpath]
|
|
47
|
+
fp = module_file(mod) # None for a namespace dir (R1-C21)
|
|
48
|
+
if fp is None:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
tree = ast.parse(fp.read_text(encoding="utf-8"))
|
|
52
|
+
except (OSError, SyntaxError):
|
|
53
|
+
continue
|
|
54
|
+
for fnode, class_stack in _named_functions(tree):
|
|
55
|
+
node_id = _node_id(modpath, class_stack, fnode.name)
|
|
56
|
+
if node_id not in graph.nodes:
|
|
57
|
+
continue
|
|
58
|
+
for key, is_write, is_subscript in _own_key_uses(fnode):
|
|
59
|
+
col_id = _COLUMN_PREFIX + key
|
|
60
|
+
subscripted[key] = subscripted.get(key, False) or is_subscript
|
|
61
|
+
access = "subscript" if is_subscript else "dict-literal"
|
|
62
|
+
edges.add(("writes" if is_write else "reads", node_id, col_id, access))
|
|
63
|
+
|
|
64
|
+
for key in sorted(subscripted):
|
|
65
|
+
col_id = _COLUMN_PREFIX + key
|
|
66
|
+
if col_id not in graph.nodes:
|
|
67
|
+
graph.add_node(Node(id=col_id, kind="column",
|
|
68
|
+
extras={"key": key, "root": "core",
|
|
69
|
+
"subscripted": subscripted[key]}))
|
|
70
|
+
for etype, src, col_id, access in sorted(edges):
|
|
71
|
+
graph.add_edge(Edge(etype, src, col_id,
|
|
72
|
+
extras={"resolution": "string-key", "access": access}))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _own_key_uses(fnode):
|
|
76
|
+
"""Yield (key, is_write, is_subscript) for string-keyed use in a function body.
|
|
77
|
+
|
|
78
|
+
Skips nested defs/classes (their own scope). ``is_write`` is set for a Store
|
|
79
|
+
subscript (``x['k'] = …`` / ``x['k'] += …``) and for a dict-literal key
|
|
80
|
+
(``{'k': value}`` — a producer of that key). A Load subscript is a read.
|
|
81
|
+
``is_subscript`` distinguishes container access (``x['k']``) from a dict-literal
|
|
82
|
+
payload key (``{'k': …}``) — the F15 precision discriminator.
|
|
83
|
+
"""
|
|
84
|
+
def visit(node):
|
|
85
|
+
for child in ast.iter_child_nodes(node):
|
|
86
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
87
|
+
continue
|
|
88
|
+
if isinstance(child, ast.Subscript) and isinstance(child.slice, ast.Constant) \
|
|
89
|
+
and isinstance(child.slice.value, str):
|
|
90
|
+
yield child.slice.value, isinstance(child.ctx, ast.Store), True
|
|
91
|
+
elif isinstance(child, ast.Dict):
|
|
92
|
+
for k in child.keys:
|
|
93
|
+
if isinstance(k, ast.Constant) and isinstance(k.value, str):
|
|
94
|
+
yield k.value, True, False # dict-literal key = producer (write)
|
|
95
|
+
yield from visit(child)
|
|
96
|
+
|
|
97
|
+
yield from visit(fnode)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Registry-aware call bridging — reconnect the dispatch seams (DESIGN §7, M7).
|
|
2
|
+
|
|
3
|
+
The behavioral pass (M4/M5) leaves calls that go through a factory/registry
|
|
4
|
+
*unresolved*: bquant wires plugins by string key, not import —
|
|
5
|
+
``self.swing_strategy = create_swing_strategy(name)`` then
|
|
6
|
+
``self.swing_strategy.calculate_global(data)``. So the call chain from
|
|
7
|
+
``analyze_zones`` dies exactly at the plugin seams — the swing/shape/… strategies
|
|
8
|
+
and the zone detector, which is *where the real work happens* (dogfood F5).
|
|
9
|
+
|
|
10
|
+
But codemap already holds the dispatch table: M1.5 recorded every
|
|
11
|
+
``@Registry.register('key')`` binding in ``extras.registry`` ({key → class}). This
|
|
12
|
+
pass uses it to bridge the seams with **candidate** edges — an honest
|
|
13
|
+
over-approximation ("dispatches to one of {zigzag, find_peaks, …}") flagged
|
|
14
|
+
``resolution="registry-candidate"``; a literal key resolves to a single class
|
|
15
|
+
(``resolution="registry"``). Concrete strategies do NOT inherit their Protocol,
|
|
16
|
+
so families are grouped by the *registrar*, not by inheritance.
|
|
17
|
+
|
|
18
|
+
Bridged forms:
|
|
19
|
+
- ``self.attr = create_X(...)`` in a class → ``self.attr.method(...)`` elsewhere in
|
|
20
|
+
that class → edges to ``{impl}.method`` for every impl of family X;
|
|
21
|
+
- a direct factory/getter call → edges to the family's implementation classes
|
|
22
|
+
(exact class when the key argument is a string literal).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import ast
|
|
28
|
+
|
|
29
|
+
from codemap.extract.behavior import _index_modules, _named_functions, _node_id, _own_calls
|
|
30
|
+
from codemap.extract.gsource import module_file
|
|
31
|
+
from codemap.model import Edge
|
|
32
|
+
|
|
33
|
+
_DISPATCH_VERBS = ("get", "create", "make", "build", "new")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_dispatch(graph, griffe_root, target_pkg: str) -> None:
|
|
37
|
+
"""Bridge factory/registry dispatch seams using the M1.5 registry table."""
|
|
38
|
+
families = _build_families(graph)
|
|
39
|
+
if not families:
|
|
40
|
+
return
|
|
41
|
+
recognizer = _Recognizer(families)
|
|
42
|
+
|
|
43
|
+
modules = _index_modules(griffe_root)
|
|
44
|
+
edges: dict[tuple[str, str, str], str] = {} # (src, tgt, kind) -> resolution (best)
|
|
45
|
+
for modpath in sorted(modules):
|
|
46
|
+
mod = modules[modpath]
|
|
47
|
+
fp = module_file(mod) # None for a namespace dir (R1-C21)
|
|
48
|
+
if fp is None:
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
tree = ast.parse(fp.read_text(encoding="utf-8"))
|
|
52
|
+
except (OSError, SyntaxError):
|
|
53
|
+
continue
|
|
54
|
+
funcs = _named_functions(tree)
|
|
55
|
+
# pass A: bind class attributes to families (self.attr = create_X(...)).
|
|
56
|
+
attr_family: dict[str, dict[str, tuple]] = {}
|
|
57
|
+
for fnode, class_stack in funcs:
|
|
58
|
+
if not class_stack:
|
|
59
|
+
continue
|
|
60
|
+
class_id = ".".join([modpath, *class_stack])
|
|
61
|
+
for attr, fam in _attr_bindings(fnode, recognizer):
|
|
62
|
+
attr_family.setdefault(class_id, {})[attr] = fam
|
|
63
|
+
# pass B: bridge dispatch call-sites to family members.
|
|
64
|
+
for fnode, class_stack in funcs:
|
|
65
|
+
node_id = _node_id(modpath, class_stack, fnode.name)
|
|
66
|
+
if node_id not in graph.nodes:
|
|
67
|
+
continue
|
|
68
|
+
class_id = ".".join([modpath, *class_stack]) if class_stack else ""
|
|
69
|
+
binds = attr_family.get(class_id, {})
|
|
70
|
+
for call in _own_calls(fnode):
|
|
71
|
+
for tgt, resolution in _bridge_call(call, recognizer, binds, families, graph):
|
|
72
|
+
key = (node_id, tgt, "calls")
|
|
73
|
+
# prefer the exact "registry" resolution over "registry-candidate".
|
|
74
|
+
if key not in edges or resolution == "registry":
|
|
75
|
+
edges[key] = resolution
|
|
76
|
+
|
|
77
|
+
for (src, tgt, _kind), resolution in edges.items():
|
|
78
|
+
graph.add_edge(Edge("calls", src, tgt, extras={"resolution": resolution}))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_family_links(graph) -> None:
|
|
82
|
+
"""Link a registry family's members to the Protocol they satisfy (F4, M9).
|
|
83
|
+
|
|
84
|
+
Concrete strategies use *structural* typing — they never inherit their
|
|
85
|
+
Protocol — so the dogfood (F4) found the family invisible to ``query`` and to
|
|
86
|
+
the class diagram even though every datum is in the graph. This synthesises an
|
|
87
|
+
``implements`` edge from each registered impl to its Protocol, matched
|
|
88
|
+
data-driven: the family token (``swing``) against the Protocol name
|
|
89
|
+
(``SwingCalculationStrategy``); for a token-less registrar the registry class
|
|
90
|
+
name drives it (``ZoneDetectionRegistry`` → ``ZoneDetectionStrategy``). No
|
|
91
|
+
hardcoded package names — a foreign convention just yields no link.
|
|
92
|
+
"""
|
|
93
|
+
families = _build_families(graph)
|
|
94
|
+
if not families:
|
|
95
|
+
return
|
|
96
|
+
protocols = _protocols(graph) # {name_lower: protocol_id}
|
|
97
|
+
for fid, fam in sorted(families.items()):
|
|
98
|
+
proto_id = _match_protocol(fid, protocols)
|
|
99
|
+
if proto_id is None:
|
|
100
|
+
continue
|
|
101
|
+
for class_id in sorted(set(fam["members"].values())):
|
|
102
|
+
if class_id in graph.nodes:
|
|
103
|
+
graph.add_edge(Edge("implements", class_id, proto_id,
|
|
104
|
+
extras={"via": "registry"}))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _protocols(graph) -> dict[str, str]:
|
|
108
|
+
"""Protocol classes (inherit ``typing.Protocol``) keyed by lowercased name."""
|
|
109
|
+
out: dict[str, str] = {}
|
|
110
|
+
for e in graph.edges:
|
|
111
|
+
if e.type == "inherits" and e.target == "typing.Protocol":
|
|
112
|
+
out[e.source.rsplit(".", 1)[-1].lower()] = e.source
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _match_protocol(fid: tuple, protocols: dict[str, str]) -> str | None:
|
|
117
|
+
"""Best Protocol for a family: token (or registry-class stem) ⊂ Protocol name."""
|
|
118
|
+
reg_class, token = fid
|
|
119
|
+
tokens = [token] if token else []
|
|
120
|
+
stem = reg_class[:-len("Registry")] if reg_class.endswith("Registry") else reg_class
|
|
121
|
+
if stem:
|
|
122
|
+
tokens.append(stem.lower())
|
|
123
|
+
for tok in tokens:
|
|
124
|
+
if not tok:
|
|
125
|
+
continue
|
|
126
|
+
matches = sorted((name, pid) for name, pid in protocols.items() if tok in name)
|
|
127
|
+
if matches:
|
|
128
|
+
return matches[0][1] # deterministic: shortest/alphabetical name
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# -- family table (from extras.registry) -------------------------------------
|
|
133
|
+
|
|
134
|
+
def _build_families(graph) -> dict:
|
|
135
|
+
"""family_id (reg_class, token) -> {'members': {key: class_id}, 'token': str}."""
|
|
136
|
+
families: dict = {}
|
|
137
|
+
for node in graph.nodes.values():
|
|
138
|
+
reg = node.extras.get("registry")
|
|
139
|
+
if not reg:
|
|
140
|
+
continue
|
|
141
|
+
fid = _family_of(reg.get("decorator", ""))
|
|
142
|
+
fam = families.setdefault(fid, {"members": {}, "token": fid[1]})
|
|
143
|
+
fam["members"][reg.get("key")] = node.id
|
|
144
|
+
return families
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _family_of(decorator_path: str) -> tuple[str, str]:
|
|
148
|
+
"""(registry_class, family_token) from a register decorator path.
|
|
149
|
+
|
|
150
|
+
``StrategyRegistry.register_swing_strategy`` -> ("StrategyRegistry", "swing")
|
|
151
|
+
``ZoneDetectionRegistry.register`` -> ("ZoneDetectionRegistry", "")
|
|
152
|
+
"""
|
|
153
|
+
parts = decorator_path.split(".")
|
|
154
|
+
reg_class = parts[-2] if len(parts) >= 2 else ""
|
|
155
|
+
method = parts[-1]
|
|
156
|
+
token = method
|
|
157
|
+
for pre in ("register_", "register"):
|
|
158
|
+
if token.startswith(pre):
|
|
159
|
+
token = token[len(pre):]
|
|
160
|
+
break
|
|
161
|
+
token = token.replace("_strategy", "").replace("strategy", "").strip("_")
|
|
162
|
+
return reg_class, token
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _Recognizer:
|
|
166
|
+
"""Decide which family (if any) a call-site dispatches."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, families: dict):
|
|
169
|
+
self.by_regclass: dict[str, list] = {}
|
|
170
|
+
self.token_families: list = [] # (token, fid)
|
|
171
|
+
for fid in families:
|
|
172
|
+
reg_class, token = fid
|
|
173
|
+
self.by_regclass.setdefault(reg_class, []).append(fid)
|
|
174
|
+
if token:
|
|
175
|
+
self.token_families.append((token, fid))
|
|
176
|
+
|
|
177
|
+
def family_of_call(self, call) -> tuple | None:
|
|
178
|
+
f = call.func
|
|
179
|
+
# <RegistryClass>.get_x(...) / .get(...) / .create(...)
|
|
180
|
+
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name):
|
|
181
|
+
recv, meth = f.value.id, f.attr
|
|
182
|
+
if recv in self.by_regclass and _is_dispatch_verb(meth):
|
|
183
|
+
for fid in self.by_regclass[recv]:
|
|
184
|
+
token = fid[1]
|
|
185
|
+
if token == "" or token in meth:
|
|
186
|
+
return fid
|
|
187
|
+
# create_x(...) module factory — matched by family token in the name.
|
|
188
|
+
name = f.id if isinstance(f, ast.Name) else None
|
|
189
|
+
if name and _is_dispatch_verb(name):
|
|
190
|
+
for token, fid in self.token_families:
|
|
191
|
+
if token and token in name:
|
|
192
|
+
return fid
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _is_dispatch_verb(name: str) -> bool:
|
|
197
|
+
return name.split("_", 1)[0] in _DISPATCH_VERBS
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# -- ast bridging -------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def _attr_bindings(fnode, recognizer):
|
|
203
|
+
"""Yield (attr, family) for ``self.attr = <dispatch call>`` in a method body."""
|
|
204
|
+
for node in ast.walk(fnode):
|
|
205
|
+
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
|
|
206
|
+
continue
|
|
207
|
+
fam = recognizer.family_of_call(node.value)
|
|
208
|
+
if fam is None:
|
|
209
|
+
continue
|
|
210
|
+
for tgt in node.targets:
|
|
211
|
+
if isinstance(tgt, ast.Attribute) and isinstance(tgt.value, ast.Name) \
|
|
212
|
+
and tgt.value.id == "self":
|
|
213
|
+
yield tgt.attr, fam
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _bridge_call(call, recognizer, binds, families, graph):
|
|
217
|
+
"""Yield (target_id, resolution) candidate edges for one call-site."""
|
|
218
|
+
f = call.func
|
|
219
|
+
# self.attr.method(...) where self.attr is bound to a family -> impl.method
|
|
220
|
+
if isinstance(f, ast.Attribute) and isinstance(f.value, ast.Attribute) \
|
|
221
|
+
and isinstance(f.value.value, ast.Name) and f.value.value.id == "self":
|
|
222
|
+
fam = binds.get(f.value.attr)
|
|
223
|
+
if fam is not None:
|
|
224
|
+
method = f.attr
|
|
225
|
+
for class_id in families[fam]["members"].values():
|
|
226
|
+
target = f"{class_id}.{method}"
|
|
227
|
+
if target in graph.nodes:
|
|
228
|
+
yield target, "registry-candidate"
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# direct factory/getter call -> implementation class(es)
|
|
232
|
+
fam = recognizer.family_of_call(call)
|
|
233
|
+
if fam is not None:
|
|
234
|
+
members = families[fam]["members"]
|
|
235
|
+
key = _literal_key(call)
|
|
236
|
+
if key is not None and key in members:
|
|
237
|
+
yield members[key], "registry" # exact — literal key
|
|
238
|
+
else:
|
|
239
|
+
for class_id in members.values():
|
|
240
|
+
yield class_id, "registry-candidate"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _literal_key(call):
|
|
244
|
+
"""First positional arg if it's a string literal, else None."""
|
|
245
|
+
if call.args and isinstance(call.args[0], ast.Constant) \
|
|
246
|
+
and isinstance(call.args[0].value, str):
|
|
247
|
+
return call.args[0].value
|
|
248
|
+
return None
|