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.
Files changed (55) hide show
  1. codemap/__init__.py +10 -0
  2. codemap/apidiff.py +208 -0
  3. codemap/arch.py +190 -0
  4. codemap/cli.py +718 -0
  5. codemap/diagnostics.py +256 -0
  6. codemap/extract/__init__.py +10 -0
  7. codemap/extract/attrflow.py +230 -0
  8. codemap/extract/behavior.py +771 -0
  9. codemap/extract/dataflow.py +97 -0
  10. codemap/extract/dispatch.py +248 -0
  11. codemap/extract/griffe_extractor.py +496 -0
  12. codemap/extract/gsource.py +83 -0
  13. codemap/extract/roots.py +427 -0
  14. codemap/freshness.py +94 -0
  15. codemap/incremental.py +195 -0
  16. codemap/integrations/__init__.py +51 -0
  17. codemap/integrations/base.py +196 -0
  18. codemap/integrations/cocoindex.py +78 -0
  19. codemap/integrations/gate.py +58 -0
  20. codemap/integrations/gitnexus.py +93 -0
  21. codemap/integrations/registry.py +69 -0
  22. codemap/integrations/transport.py +46 -0
  23. codemap/model.py +178 -0
  24. codemap/provenance.py +248 -0
  25. codemap/query.py +1164 -0
  26. codemap/scope.py +212 -0
  27. codemap/serve/__init__.py +26 -0
  28. codemap/serve/_scip_pb2.py +100 -0
  29. codemap/serve/api_surface.py +60 -0
  30. codemap/serve/apidiff.py +83 -0
  31. codemap/serve/architecture.py +101 -0
  32. codemap/serve/audit.py +176 -0
  33. codemap/serve/check.py +80 -0
  34. codemap/serve/ctags.py +203 -0
  35. codemap/serve/impact.py +84 -0
  36. codemap/serve/livingdocs.py +174 -0
  37. codemap/serve/mcp_server.py +278 -0
  38. codemap/serve/mermaid.py +120 -0
  39. codemap/serve/pack.py +93 -0
  40. codemap/serve/rag.py +142 -0
  41. codemap/serve/review.py +197 -0
  42. codemap/serve/scip.py +183 -0
  43. codemap/serve/semantic.py +71 -0
  44. codemap/serve/server.py +43 -0
  45. codemap/serve/session.py +482 -0
  46. codemap/serve/subsystems.py +85 -0
  47. codemap/serve/vault.py +156 -0
  48. codemap/store.py +28 -0
  49. codemap/tomlio.py +59 -0
  50. codmap-0.0.3.dist-info/METADATA +245 -0
  51. codmap-0.0.3.dist-info/RECORD +55 -0
  52. codmap-0.0.3.dist-info/WHEEL +5 -0
  53. codmap-0.0.3.dist-info/entry_points.txt +2 -0
  54. codmap-0.0.3.dist-info/licenses/LICENSE +21 -0
  55. 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