code-context-control 2.123.0__py3-none-any.whl → 2.124.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.
- cli/c3.py +1 -1
- cli/mcp_server.py +5 -18
- cli/tools/compress.py +3 -2
- cli/tools/read.py +4 -1
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/METADATA +1 -1
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/RECORD +13 -13
- services/file_map.py +61 -2
- services/file_memory.py +107 -10
- services/parser.py +41 -19
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/WHEEL +0 -0
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/entry_points.txt +0 -0
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/licenses/LICENSE +0 -0
- {code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/top_level.txt +0 -0
cli/c3.py
CHANGED
cli/mcp_server.py
CHANGED
|
@@ -47,7 +47,6 @@ C3_VERSION = _read_version()
|
|
|
47
47
|
# Tool handlers
|
|
48
48
|
from cli.tools._helpers import maybe_related_facts, validate_file_path
|
|
49
49
|
from cli.tools.agent import handle_agent
|
|
50
|
-
from cli.tools.compress import handle_compress
|
|
51
50
|
from cli.tools.delegate import handle_delegate
|
|
52
51
|
from cli.tools.edit import handle_edit
|
|
53
52
|
from cli.tools.filter import handle_filter
|
|
@@ -85,7 +84,7 @@ def _build_instructions(ide_name: str) -> str:
|
|
|
85
84
|
return (
|
|
86
85
|
"C3 — local code intelligence. Route by goal:\n"
|
|
87
86
|
" FIND candidates → c3_search\n"
|
|
88
|
-
" MAP a file → c3_read(file_path) with no symbols, then READ → c3_read(symbols=…)
|
|
87
|
+
" MAP a file/dir → c3_read(file_path) with no symbols (comma paths batch), then READ → c3_read(symbols=…)\n"
|
|
89
88
|
" EDIT code → c3_edit (always; it logs to the ledger automatically)\n"
|
|
90
89
|
" VALIDATE after every edit → c3_validate\n"
|
|
91
90
|
" BLAST RADIUS before shared-symbol edits → c3_impact\n"
|
|
@@ -598,22 +597,10 @@ async def c3_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
|
598
597
|
return await asyncio.to_thread(handle_read, file_path, symbols, lines, include_docstrings, svc, finalize)
|
|
599
598
|
|
|
600
599
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
use this form for a comma-separated batch (up to 10). mode: map (default); other modes
|
|
606
|
-
are legacy."""
|
|
607
|
-
path_err = validate_file_path(file_path)
|
|
608
|
-
if path_err:
|
|
609
|
-
return f"[c3_compress:error] {path_err}"
|
|
610
|
-
svc = _svc(ctx)
|
|
611
|
-
|
|
612
|
-
def finalize(name, args, resp, summ, **kw):
|
|
613
|
-
return _finalize_response(ctx, name, args, resp, summ, **kw)
|
|
614
|
-
|
|
615
|
-
return await asyncio.to_thread(handle_compress, file_path, mode, svc, finalize, maybe_related_facts)
|
|
616
|
-
|
|
600
|
+
# c3_compress left the MCP surface in 2.124.0: c3_read(file_path) serves the
|
|
601
|
+
# same map, c3_read('a.py,b.py') the same batch, c3_read('<dir>') a directory.
|
|
602
|
+
# handle_compress stays importable for c3_project, the Oracle bridge and the
|
|
603
|
+
# CLI (docs/file-map.md § Retired modes).
|
|
617
604
|
|
|
618
605
|
@mcp.tool()
|
|
619
606
|
async def c3_validate(file_path: str, ctx: Context = None) -> str:
|
cli/tools/compress.py
CHANGED
|
@@ -102,7 +102,7 @@ def _build_map(svc, rel: str) -> str:
|
|
|
102
102
|
svc.file_memory.complete_updates(completed)
|
|
103
103
|
if failed:
|
|
104
104
|
svc.file_memory.complete_updates(failed, failed=True)
|
|
105
|
-
return svc.file_memory.get_or_build_map(rel)
|
|
105
|
+
return svc.file_memory.get_or_build_map(rel, max_tokens=svc.file_memory.MAP_TOKEN_BUDGET)
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def _compress_single(file_path: str, requested: str, note: str, svc, finalize) -> str:
|
|
@@ -197,7 +197,8 @@ def _compress_batch(paths: list, requested: str, note: str, svc, finalize) -> st
|
|
|
197
197
|
return fp, None, None, None, "not found"
|
|
198
198
|
rel = str(full.resolve().relative_to(
|
|
199
199
|
Path(svc.project_path).resolve())).replace("\\", "/")
|
|
200
|
-
res = svc.file_memory.get_or_build_map(
|
|
200
|
+
res = svc.file_memory.get_or_build_map(
|
|
201
|
+
rel, max_tokens=svc.file_memory.MAP_TOKEN_BUDGET)
|
|
201
202
|
try:
|
|
202
203
|
raw_tok = count_tokens(full.read_text(encoding="utf-8", errors="replace"))
|
|
203
204
|
map_tok = count_tokens(res)
|
cli/tools/read.py
CHANGED
|
@@ -331,7 +331,8 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
|
331
331
|
|
|
332
332
|
if not ranges:
|
|
333
333
|
map_cached = not svc.file_memory.needs_update(rel_path)
|
|
334
|
-
file_map = svc.file_memory.get_or_build_map(
|
|
334
|
+
file_map = svc.file_memory.get_or_build_map(
|
|
335
|
+
rel_path, max_tokens=svc.file_memory.MAP_TOKEN_BUDGET)
|
|
335
336
|
if count_tokens(file_map) >= full_file_tokens():
|
|
336
337
|
# A map that costs more than the file is worth nothing: serve the
|
|
337
338
|
# file (docs/file-map.md § Small files).
|
|
@@ -341,6 +342,8 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
|
|
|
341
342
|
+ "\n[map only — pass lines=[start,end] or symbols=[...] for exact source]"
|
|
342
343
|
+ maybe_related_facts(svc, rel_path, top_k=3, context="read"))
|
|
343
344
|
map_tok = count_tokens(resp)
|
|
345
|
+
if finalize is None:
|
|
346
|
+
return resp
|
|
344
347
|
return finalize_with_tokens(
|
|
345
348
|
finalize, svc, "c3_read", {"file": file_path},
|
|
346
349
|
resp, f"{full_file_tokens()}->{map_tok}tok",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.124.0
|
|
4
4
|
Summary: Local MCP code-intelligence for AI coding tools: surgical search/read/edit, agent-config version history, path-level access + masking guards, and a multi-project hub.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
|
|
2
2
|
cli/_hook_utils.py,sha256=e48F6FQloFReUIMtmWsih2t54j9SROzAaHtMkEuttDs,20489
|
|
3
3
|
cli/_shell_writes.py,sha256=XzUNlUDljmg-wZxK03xHnRe36WeQxe4tpO9HKVFwP5o,5090
|
|
4
|
-
cli/c3.py,sha256=
|
|
4
|
+
cli/c3.py,sha256=TRLSfHOBi-9AxQUAaXYq46tFgaBd4WMKw0WIBnFijHI,416843
|
|
5
5
|
cli/docs.html,sha256=X9M7qC7LWi8pPJNF-4yEM57jhuZ3WeTESECbdDOeNaA,143172
|
|
6
6
|
cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
|
|
7
7
|
cli/hook_access_guard.py,sha256=ba8-un6G9Ap8HCzx7nyhVq3c_G4sOtiVXRy0BzzX8-Y,23349
|
|
@@ -23,7 +23,7 @@ cli/hook_terse_advisor.py,sha256=W_zaTfX2akgX-ifuUTsLu2H1atM8XmWt6jpPoeU7Pm0,625
|
|
|
23
23
|
cli/hub_server.py,sha256=4BxvRHCGNc87jOtWankOVjQLYNk-GhWI2OY1harC8ro,184548
|
|
24
24
|
cli/hub_ui.html,sha256=nnGqWhWGCLfNisGUnoAlQFcBr6wPNsq-AEOR7uFtVkE,2942
|
|
25
25
|
cli/mcp_proxy.py,sha256=r-Qhlie7gCtotex8yQUudCyBLRUK-IW6cNIW5RhBW0o,17648
|
|
26
|
-
cli/mcp_server.py,sha256=
|
|
26
|
+
cli/mcp_server.py,sha256=3oxd0sQisBVnWRVix9TDBPgv_w-9dc-MIClJEd6igws,63714
|
|
27
27
|
cli/progress.py,sha256=qTkNy0howOD3hXraMo4ok3dYTpBW5yG_tenKfF4UDM0,1470
|
|
28
28
|
cli/server.py,sha256=bsHvKGPXsBJi_MDLiw0IAEq8RWKIG56q72c7OKKTLug,173324
|
|
29
29
|
cli/ui.html,sha256=xcdt74nlFEXx-0Bx6-Okw-WSVZPAXL0iukxU0ytI6CA,5694
|
|
@@ -78,7 +78,7 @@ cli/tools/agent.py,sha256=RmEEh5HhCZLvYu0gSRcwA_NENpd4iIYJW3exBzT8kVs,47983
|
|
|
78
78
|
cli/tools/artifacts.py,sha256=SjazShDFIzOpAZW7uMv9itBktXnc6pqQKZeaen8GgzI,9379
|
|
79
79
|
cli/tools/bitbucket.py,sha256=cni8Y1EoiNm9jcHEpzi2dgKFIT1-4ct0WOhpTs7ac3k,35812
|
|
80
80
|
cli/tools/ci.py,sha256=fQ03RENywS5gqHjH26Fa4YiraBTvchlPcPSnVYwwKBs,19444
|
|
81
|
-
cli/tools/compress.py,sha256=
|
|
81
|
+
cli/tools/compress.py,sha256=YjYl0ariAAtIAyG2O-iSgweQXY2-THnl7JcNL83HXbI,10926
|
|
82
82
|
cli/tools/credentials.py,sha256=R8Errpa41vsq0Z_sb7K-fmJxVovzXDYUyniITLrFC24,19033
|
|
83
83
|
cli/tools/delegate.py,sha256=yA2-ph53Y8XAJMpz7R5LwqXshRRD8bYljgN244RbHWA,62012
|
|
84
84
|
cli/tools/edit.py,sha256=xnRRM2UUybvGvfSzXzIjWmt6_zNaCbkupiK1z-8K4jE,30127
|
|
@@ -92,7 +92,7 @@ cli/tools/locks.py,sha256=FvuNxToU-5ZE2m_C6fTrn1jKERoxR4cbtxNb_b8mXqA,5882
|
|
|
92
92
|
cli/tools/memory.py,sha256=yDDRsEngeFcjb6nXUrhRZXuboasXfixJeyltKhDbZD4,24935
|
|
93
93
|
cli/tools/override.py,sha256=LiZFgoDN-35NXVwldnYrcCzfBl8MhQQ3tKVVkOG5d_o,9217
|
|
94
94
|
cli/tools/project.py,sha256=MWzi7tBTQ03p_6PavmG9ZLXZHO0IhjKEJNyB0oZWTFc,24248
|
|
95
|
-
cli/tools/read.py,sha256=
|
|
95
|
+
cli/tools/read.py,sha256=yLpJuxQnGRnEvX5lyVeQn2L56tz90dmilIaj1f4KmSk,18532
|
|
96
96
|
cli/tools/search.py,sha256=BZh3E7YggjCWOC1lJz0YNcRsdDLWsJiEmONtgUW2rEo,32397
|
|
97
97
|
cli/tools/session.py,sha256=ailQiRqXhi9NcBB3XS3Dv72GEkEZaYivB0bqDlhmgTA,5202
|
|
98
98
|
cli/tools/shell.py,sha256=Zcsocl8IXeL9DSQNewq_fXECSufTccUdwc-H3S2Alto,58818
|
|
@@ -123,7 +123,7 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
|
|
|
123
123
|
cli/ui/components/settings.js,sha256=ATbAjBlVIwCNpxq7s191b49a_INQV38iwmySqtJLYwY,79066
|
|
124
124
|
cli/ui/components/sidebar.js,sha256=K2ym2kUgpbyG-EBA_wBIIiqQY8qTatsiBZwzseMWuIQ,9939
|
|
125
125
|
cli/ui/components/tasks.js,sha256=vyKQ3uwoppMwvdEaHlhWXW4oWcAisx4NveqzMhsYqHo,38438
|
|
126
|
-
code_context_control-2.
|
|
126
|
+
code_context_control-2.124.0.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
|
|
127
127
|
core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
|
|
128
128
|
core/codex_patch.py,sha256=CxvT_rQIwPNTRJZj9EHJmx0ZdOeSLxjiixuLI3Z7lPc,2107
|
|
129
129
|
core/config.py,sha256=YmkcZwedz_lfDM0ZuI_f4xpe98ixPLcSQFcTCHgRiRg,19206
|
|
@@ -218,8 +218,8 @@ services/edit_ledger.py,sha256=Tj8Q14o19VHQHpofRHdbZKKcVs17v-ctZ3LOeVTqvdQ,29362
|
|
|
218
218
|
services/embedding_index.py,sha256=c85kAkekL7r-liiFEwqi7nJAQr4ZbJA36h0oMdiTurc,25156
|
|
219
219
|
services/enforcement_policy.py,sha256=bB-lI7EZwWT5MhoNtkBjjixN4h7zAYUtVBWPMJ6aONo,17196
|
|
220
220
|
services/error_reporting.py,sha256=HZ3ru8i5RLf8nq2R4iRnTs5sm1blUxknSbv5hdxuxs0,4139
|
|
221
|
-
services/file_map.py,sha256=
|
|
222
|
-
services/file_memory.py,sha256=
|
|
221
|
+
services/file_map.py,sha256=j37DTR-UE_PMNj7vG2HiHucpwpJNaKgiD94UNUU-UXE,17091
|
|
222
|
+
services/file_memory.py,sha256=4n2y6Sci4ThP1ob8dh68bEq80ui5I50iWpQjoIbGI84,38565
|
|
223
223
|
services/git_context.py,sha256=FFdA8Y46op4DYsUnZsXLTqxIjgxb0q_d-hkFTbosC8Q,9518
|
|
224
224
|
services/hub_service.py,sha256=z2MZ2Rg9NLNXxQ17e05XGF_f6qSTDWLYtoC3WpuQfGU,2634
|
|
225
225
|
services/index_store.py,sha256=SWHHKQB_wYww1ubtKgkLo5FFEqaWhPbw0OAN8x3gk9Q,16453
|
|
@@ -251,7 +251,7 @@ services/override_grants.py,sha256=BQVwD6WCxVORsUaqDWNToIalz2mStnJRMIR-SOxQzuQ,3
|
|
|
251
251
|
services/override_policy.py,sha256=VC5H-FETUz4u6f4JM_q5jqT2HIb1MiFzhpdJyuBG72I,22369
|
|
252
252
|
services/override_requests.py,sha256=H9lSUbfOjkjE1muztaQ58InqqsqhtnKIT-pF2VzvWkg,31148
|
|
253
253
|
services/override_wake.py,sha256=HjSOXQTDi0irgeMyrTqngL7xLcSOOIgYsVW9yxod4ps,10363
|
|
254
|
-
services/parser.py,sha256=
|
|
254
|
+
services/parser.py,sha256=S_YoKLS3r1xCUVHApXb3BTEfoRAzFhuo1_8XsG0qxVE,63505
|
|
255
255
|
services/project_manager.py,sha256=uQh-GUvH5cgV5Ce9XYPINlQeqH6NvNdW8zrNFYRbBkw,37855
|
|
256
256
|
services/project_runtime.py,sha256=yyftX48BFjq4B2KcR4KGszDAAa4xR6gn6j0SgTyro1s,11603
|
|
257
257
|
services/protocol.py,sha256=CWZ0BWWd-jPui-11hZlvGgkNk18gMFhhLWbp-BP35Kk,12861
|
|
@@ -306,8 +306,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
|
|
|
306
306
|
tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
|
|
307
307
|
tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
|
|
308
308
|
tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
|
|
309
|
-
code_context_control-2.
|
|
310
|
-
code_context_control-2.
|
|
311
|
-
code_context_control-2.
|
|
312
|
-
code_context_control-2.
|
|
313
|
-
code_context_control-2.
|
|
309
|
+
code_context_control-2.124.0.dist-info/METADATA,sha256=aoNZXvOHSJK4ylPra3DMz2K2FMUEkKIKiAYLOUYOWcg,27067
|
|
310
|
+
code_context_control-2.124.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
311
|
+
code_context_control-2.124.0.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
|
|
312
|
+
code_context_control-2.124.0.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
|
|
313
|
+
code_context_control-2.124.0.dist-info/RECORD,,
|
services/file_map.py
CHANGED
|
@@ -56,7 +56,7 @@ _PY_DEF = re.compile(r"^(?:async\s+)?def\s+[\w.]+\s*\((?P<params>.*)\)\s*(?:->\s
|
|
|
56
56
|
_PY_CLASS = re.compile(r"^class\s+\w+\s*(?:\((?P<bases>.*)\))?\s*:?$")
|
|
57
57
|
_GO_FUNC = re.compile(r"^func\s+(?:\((?P<recv>[^)]*)\)\s*)?\w+\s*\((?P<params>.*?)\)\s*(?P<ret>[^{]*?)\s*\{?$")
|
|
58
58
|
_RS_FN = re.compile(r"^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+|const\s+|unsafe\s+)*fn\s+\w+(?:<[^>]*>)?\s*\((?P<params>.*)\)\s*(?:->\s*(?P<ret>[^{]+?))?\s*(?:where\b.*)?\{?$")
|
|
59
|
-
_JS_FUNC = re.compile(r"^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s
|
|
59
|
+
_JS_FUNC = re.compile(r"^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*[\w$]*\s*(?:<[^>]*>)?\s*\((?P<params>.*)\)\s*(?::\s*(?P<ret>[^{]+?))?\s*\{?\s*$")
|
|
60
60
|
_JS_ARROW = re.compile(r"^(?:export\s+)?(?:const|let|var)\s+\w+\s*(?::[^=]+)?=\s*(?:async\s+)?\((?P<params>.*)\)\s*(?::\s*(?P<ret>[^=]+?))?\s*=>")
|
|
61
61
|
_JS_ARROW1 = re.compile(r"^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?P<params>\w+)\s*=>")
|
|
62
62
|
_JS_CLASS = re.compile(r"^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+\w+(?:<[^>]*>)?\s*(?:extends\s+(?P<base>[\w.<>, ]+?))?\s*(?:implements\s+(?P<impl>[\w.<>, ]+?))?\s*\{?$")
|
|
@@ -67,6 +67,40 @@ def _norm(text: str) -> str:
|
|
|
67
67
|
return _WS.sub(" ", str(text or "")).strip()
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def _declaration_head(sig: str) -> str:
|
|
71
|
+
"""Cut a brace-language signature after its parameter list and return
|
|
72
|
+
type: the text up to the body's `{`, an arrow, or a `;`. Parentheses
|
|
73
|
+
are depth-matched, so a 400-char minified chunk holding several
|
|
74
|
+
declarations yields only the first one's head."""
|
|
75
|
+
i = sig.find("(")
|
|
76
|
+
if i < 0:
|
|
77
|
+
j = sig.find("{")
|
|
78
|
+
return sig if j < 0 else sig[:j].rstrip()
|
|
79
|
+
depth = 0
|
|
80
|
+
close = -1
|
|
81
|
+
for k in range(i, len(sig)):
|
|
82
|
+
ch = sig[k]
|
|
83
|
+
if ch == "(":
|
|
84
|
+
depth += 1
|
|
85
|
+
elif ch == ")":
|
|
86
|
+
depth -= 1
|
|
87
|
+
if depth == 0:
|
|
88
|
+
close = k
|
|
89
|
+
break
|
|
90
|
+
if close < 0:
|
|
91
|
+
return sig
|
|
92
|
+
rest = sig[close + 1:]
|
|
93
|
+
cut = len(rest)
|
|
94
|
+
for stop in ("{", ";"):
|
|
95
|
+
j = rest.find(stop)
|
|
96
|
+
if 0 <= j < cut:
|
|
97
|
+
cut = j
|
|
98
|
+
j = rest.find("=>")
|
|
99
|
+
if 0 <= j < cut:
|
|
100
|
+
cut = j + 2 # keep the arrow: the arrow-function grammar needs it
|
|
101
|
+
return (sig[:close + 1] + rest[:cut]).rstrip()
|
|
102
|
+
|
|
103
|
+
|
|
70
104
|
def parse_signature(signature: str, language: str) -> dict:
|
|
71
105
|
"""Split a raw signature into params / ret / receiver / bases.
|
|
72
106
|
|
|
@@ -78,6 +112,10 @@ def parse_signature(signature: str, language: str) -> dict:
|
|
|
78
112
|
if not sig:
|
|
79
113
|
return {}
|
|
80
114
|
lang = (language or "").lower()
|
|
115
|
+
if lang in ("javascript", "typescript", "go", "rust") and not sig.lstrip().startswith(
|
|
116
|
+
("class ", "export class", "export default class", "abstract class",
|
|
117
|
+
"struct ", "pub struct", "enum ", "pub enum", "trait ", "pub trait", "impl ", "type ")):
|
|
118
|
+
sig = _declaration_head(sig)
|
|
81
119
|
out: dict = {}
|
|
82
120
|
if lang == "python":
|
|
83
121
|
m = _PY_DEF.match(sig)
|
|
@@ -290,6 +328,25 @@ def render_map(record: dict, *, include_docs: bool = False,
|
|
|
290
328
|
header = f"# {path} ({total_lines}L {language})".rstrip()
|
|
291
329
|
body: list = []
|
|
292
330
|
|
|
331
|
+
shape = record.get("shape") or {}
|
|
332
|
+
parser = str(record.get("parser") or "")
|
|
333
|
+
minified = bool(shape.get("minified"))
|
|
334
|
+
if parser == "skipped" or shape.get("oversized"):
|
|
335
|
+
body.append(f"[map:not mapped] {shape.get('bytes', 0):,} bytes on {total_lines} lines, "
|
|
336
|
+
f"longest line {shape.get('longest_line', 0):,} chars — read with lines=[a,b]")
|
|
337
|
+
return "\n".join([header] + body)
|
|
338
|
+
if parser == "lexical":
|
|
339
|
+
body.append("[map:lexical-fallback] the parse overran its deadline; "
|
|
340
|
+
"symbols come from a line scan and may be incomplete")
|
|
341
|
+
if minified:
|
|
342
|
+
# A minified bundle's `var` soup is not structure; named declarations are.
|
|
343
|
+
soup = [s for s in sections if s.get("type") in ("variable", "constant")]
|
|
344
|
+
sections = [s for s in sections if s.get("type") not in ("variable", "constant")]
|
|
345
|
+
body.append(f"[map:minified] longest line {shape.get('longest_line', 0):,} chars; "
|
|
346
|
+
f"{len(soup)} var bindings omitted")
|
|
347
|
+
elif shape.get("generated"):
|
|
348
|
+
body.append("[map:generated] the file says it is generated")
|
|
349
|
+
|
|
293
350
|
imports = [s for s in sections if s.get("type") == "import"]
|
|
294
351
|
others = [s for s in sections if s.get("type") != "import"]
|
|
295
352
|
if imports:
|
|
@@ -329,7 +386,9 @@ def render_map(record: dict, *, include_docs: bool = False,
|
|
|
329
386
|
while kept and count_tokens("\n".join([header] + kept)) > max_tokens:
|
|
330
387
|
kept.pop()
|
|
331
388
|
dropped = len(body) - len(kept)
|
|
332
|
-
text = "\n".join([header] + kept + [
|
|
389
|
+
text = "\n".join([header] + kept + [
|
|
390
|
+
f"… {dropped} more symbols (map budget {max_tokens} tokens; "
|
|
391
|
+
f"pass symbols=[…] or lines=[a,b] for the part you need)"])
|
|
333
392
|
return text
|
|
334
393
|
|
|
335
394
|
|
services/file_memory.py
CHANGED
|
@@ -195,6 +195,10 @@ class FileMemoryStore:
|
|
|
195
195
|
"parser": parser,
|
|
196
196
|
"sections": sections,
|
|
197
197
|
}
|
|
198
|
+
shape = getattr(self, "_last_shape", None) or {}
|
|
199
|
+
if shape.get("minified") or shape.get("generated") or shape.get("oversized"):
|
|
200
|
+
record["shape"] = {k: shape[k] for k in ("minified", "generated", "oversized",
|
|
201
|
+
"longest_line", "bytes") if k in shape}
|
|
198
202
|
|
|
199
203
|
self._save(rel_path, record)
|
|
200
204
|
self._cache_map(rel_path, record)
|
|
@@ -213,17 +217,31 @@ class FileMemoryStore:
|
|
|
213
217
|
return None
|
|
214
218
|
return self._cache_map(rel_path, record)
|
|
215
219
|
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
#: Default token budget for a map served to the model (C4, 2.124.0). A
|
|
221
|
+
#: 250 KB generated module renders 117k tokens unbounded; the MCP
|
|
222
|
+
#: response cap is 25k. Over budget the map keeps its first lines and
|
|
223
|
+
#: says how many symbols it left out.
|
|
224
|
+
MAP_TOKEN_BUDGET = 6000
|
|
225
|
+
|
|
226
|
+
def get_or_build_map(self, rel_path: str, max_tokens: Optional[int] = None) -> str:
|
|
227
|
+
"""Get map if cached, otherwise build it on-demand.
|
|
228
|
+
|
|
229
|
+
`max_tokens` renders a budgeted map (never cached); None serves the
|
|
230
|
+
cached full map.
|
|
231
|
+
"""
|
|
218
232
|
record = self.get(rel_path)
|
|
219
233
|
|
|
220
234
|
# Check staleness
|
|
221
235
|
if record and not self.needs_update(rel_path):
|
|
236
|
+
if max_tokens:
|
|
237
|
+
return render_map(record, max_tokens=max_tokens)
|
|
222
238
|
return self._cache_map(rel_path, record)
|
|
223
239
|
|
|
224
240
|
# Build fresh
|
|
225
241
|
updated = self.update(rel_path)
|
|
226
242
|
if updated:
|
|
243
|
+
if max_tokens:
|
|
244
|
+
return render_map(updated, max_tokens=max_tokens)
|
|
227
245
|
return self._cache_map(rel_path, updated)
|
|
228
246
|
|
|
229
247
|
return f"[file_map] Could not build map for {rel_path} — file not found or unreadable."
|
|
@@ -556,6 +574,39 @@ class FileMemoryStore:
|
|
|
556
574
|
PARSER_TREE_SITTER = "tree_sitter"
|
|
557
575
|
PARSER_REGEX = "regex"
|
|
558
576
|
PARSER_GENERIC = "generic"
|
|
577
|
+
PARSER_LEXICAL = "lexical" # deadline or parse failure -> regex scan
|
|
578
|
+
PARSER_SKIPPED = "skipped" # minified / oversized: not parsed
|
|
579
|
+
|
|
580
|
+
#: Bounds for large files (C4, 2.124.0). A parse that overruns the
|
|
581
|
+
#: deadline is abandoned for the regex scan; a minified or oversized
|
|
582
|
+
#: file is not parsed at all.
|
|
583
|
+
PARSE_DEADLINE_S = 1.5
|
|
584
|
+
MAX_PARSE_BYTES = 2 * 1024 * 1024
|
|
585
|
+
MINIFIED_LINE_CHARS = 2000
|
|
586
|
+
MINIFIED_AVG_LINE_CHARS = 400
|
|
587
|
+
GENERATED_MARKERS = ("do not edit", "autogenerated", "auto-generated",
|
|
588
|
+
"@generated", "generated by", "generated file")
|
|
589
|
+
|
|
590
|
+
@classmethod
|
|
591
|
+
def classify_large(cls, content: str) -> dict:
|
|
592
|
+
"""Cheap shape facts before any parse: {minified, generated, longest_line, oversized}."""
|
|
593
|
+
size = len(content)
|
|
594
|
+
longest = 0
|
|
595
|
+
n_lines = 0
|
|
596
|
+
for line in content.split("\n"):
|
|
597
|
+
n_lines += 1
|
|
598
|
+
if len(line) > longest:
|
|
599
|
+
longest = len(line)
|
|
600
|
+
avg = size / max(1, n_lines)
|
|
601
|
+
head = "\n".join(content.split("\n", 6)[:6]).lower()
|
|
602
|
+
return {
|
|
603
|
+
"bytes": size,
|
|
604
|
+
"lines": n_lines,
|
|
605
|
+
"longest_line": longest,
|
|
606
|
+
"minified": longest >= cls.MINIFIED_LINE_CHARS and avg >= cls.MINIFIED_AVG_LINE_CHARS,
|
|
607
|
+
"generated": any(m in head for m in cls.GENERATED_MARKERS),
|
|
608
|
+
"oversized": size >= cls.MAX_PARSE_BYTES,
|
|
609
|
+
}
|
|
559
610
|
|
|
560
611
|
def _extract_sections(self, filepath: Path, content: str) -> list:
|
|
561
612
|
"""Extract structural sections with line ranges from source code."""
|
|
@@ -570,18 +621,56 @@ class FileMemoryStore:
|
|
|
570
621
|
older single-value call keeps working.
|
|
571
622
|
"""
|
|
572
623
|
ext = filepath.suffix.lower()
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
624
|
+
shape = self.classify_large(content)
|
|
625
|
+
self._last_shape = shape
|
|
626
|
+
if shape["oversized"]:
|
|
627
|
+
# Not worth a parse: the map says so and stops (docs/file-map.md
|
|
628
|
+
# § Large files). Minified files ARE parsed — bounded signature
|
|
629
|
+
# capture made that cheap — and the renderer drops their
|
|
630
|
+
# binding soup.
|
|
631
|
+
return [{"type": "content", "name": "(not mapped)",
|
|
632
|
+
"line_start": 1, "line_end": max(1, shape["lines"]),
|
|
633
|
+
"note": "oversized"}], self.PARSER_SKIPPED
|
|
578
634
|
|
|
579
635
|
lines = content.splitlines()
|
|
580
636
|
patterns = STRUCTURE_PATTERNS.get(ext, {})
|
|
581
637
|
|
|
638
|
+
# Try AST parser first, under a deadline. tree-sitter 0.26 has no
|
|
639
|
+
# parse timeout, so the parse runs on a worker and is abandoned
|
|
640
|
+
# (not killed) when it overruns; the regex scan answers instead.
|
|
641
|
+
ast_sections, overran = self._extract_ast_with_deadline(content, ext)
|
|
642
|
+
if ast_sections is not None:
|
|
643
|
+
return ast_sections, self.PARSER_TREE_SITTER
|
|
644
|
+
if overran and patterns:
|
|
645
|
+
secs, _ = self._extract_regex_sections(lines, ext, patterns)
|
|
646
|
+
return secs, self.PARSER_LEXICAL
|
|
647
|
+
|
|
582
648
|
if not patterns:
|
|
583
649
|
return self._extract_generic_sections(lines), self.PARSER_GENERIC
|
|
584
650
|
|
|
651
|
+
secs, _ = self._extract_regex_sections(lines, ext, patterns)
|
|
652
|
+
return secs, self.PARSER_REGEX
|
|
653
|
+
|
|
654
|
+
def _extract_ast_with_deadline(self, content: str, ext: str):
|
|
655
|
+
"""(sections | None, overran). Runs extract_sections_ast on a worker
|
|
656
|
+
thread bounded by PARSE_DEADLINE_S; the thread is daemonic so an
|
|
657
|
+
abandoned parse cannot hold the process open."""
|
|
658
|
+
import concurrent.futures as _cf
|
|
659
|
+
pool = _cf.ThreadPoolExecutor(max_workers=1, thread_name_prefix="c3-parse")
|
|
660
|
+
try:
|
|
661
|
+
fut = pool.submit(extract_sections_ast, content, ext)
|
|
662
|
+
try:
|
|
663
|
+
return fut.result(timeout=self.PARSE_DEADLINE_S), False
|
|
664
|
+
except _cf.TimeoutError:
|
|
665
|
+
self._record_diag("parse_deadline", ext, f"{len(content)} bytes")
|
|
666
|
+
return None, True
|
|
667
|
+
except Exception:
|
|
668
|
+
return None, True
|
|
669
|
+
finally:
|
|
670
|
+
pool.shutdown(wait=False)
|
|
671
|
+
|
|
672
|
+
def _extract_regex_sections(self, lines: list, ext: str, patterns: dict) -> tuple:
|
|
673
|
+
"""The regex/indentation extractor (the pre-tree-sitter path)."""
|
|
585
674
|
sections = []
|
|
586
675
|
i = 0
|
|
587
676
|
current_class = None # Track current class for method nesting
|
|
@@ -674,6 +763,14 @@ class FileMemoryStore:
|
|
|
674
763
|
|
|
675
764
|
base_indent = len(lines[start]) - len(lines[start].lstrip())
|
|
676
765
|
|
|
766
|
+
def _last_code_line(upto: int) -> int:
|
|
767
|
+
# 1-indexed line number of the last non-blank line before `upto`
|
|
768
|
+
# (0-indexed exclusive): blank lines belong to nobody.
|
|
769
|
+
j = upto
|
|
770
|
+
while j > start + 1 and not lines[j - 1].strip():
|
|
771
|
+
j -= 1
|
|
772
|
+
return j
|
|
773
|
+
|
|
677
774
|
for i in range(start + 1, len(lines)):
|
|
678
775
|
line = lines[i]
|
|
679
776
|
stripped = line.strip()
|
|
@@ -681,11 +778,11 @@ class FileMemoryStore:
|
|
|
681
778
|
continue
|
|
682
779
|
current_indent = len(line) - len(line.lstrip())
|
|
683
780
|
if current_indent <= base_indent:
|
|
684
|
-
return i # 1-indexed
|
|
781
|
+
return _last_code_line(i) # 1-indexed
|
|
685
782
|
# Check for decorators at same level (next function)
|
|
686
783
|
if current_indent == base_indent and stripped.startswith('@'):
|
|
687
|
-
return i
|
|
688
|
-
return len(lines)
|
|
784
|
+
return _last_code_line(i)
|
|
785
|
+
return _last_code_line(len(lines))
|
|
689
786
|
|
|
690
787
|
def _find_brace_block_end(self, lines: list, start: int) -> int:
|
|
691
788
|
"""Find end of a brace-delimited block.
|
services/parser.py
CHANGED
|
@@ -57,6 +57,28 @@ PARSER_VERSION = "4" # 4: records carry parser attribution, no summary, posix p
|
|
|
57
57
|
|
|
58
58
|
_WS_RUN = re.compile(r"\s+")
|
|
59
59
|
|
|
60
|
+
#: A signature is never longer than this. A 680 KB minified line used to be
|
|
61
|
+
#: copied into every one of its 20,000 symbols (176 s, an 11 MB record).
|
|
62
|
+
SIGNATURE_MAX_CHARS = 400
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sig_line(lines: List[str], node) -> str:
|
|
66
|
+
"""The node's first line from its start column, capped at SIGNATURE_MAX_CHARS."""
|
|
67
|
+
row, col = node.start_point
|
|
68
|
+
line = lines[row] if row < len(lines) else ""
|
|
69
|
+
chunk = line[col:col + SIGNATURE_MAX_CHARS] if len(line) - col > SIGNATURE_MAX_CHARS else line[col:]
|
|
70
|
+
if not chunk.strip():
|
|
71
|
+
chunk = line[:SIGNATURE_MAX_CHARS]
|
|
72
|
+
return chunk.strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _sig_span(lines: List[str], start_row: int, end_row: int) -> str:
|
|
76
|
+
"""Lines start_row..end_row joined, capped at SIGNATURE_MAX_CHARS."""
|
|
77
|
+
text = "\n".join(lines[start_row:end_row + 1]).strip()
|
|
78
|
+
if len(text) > SIGNATURE_MAX_CHARS:
|
|
79
|
+
text = text[:SIGNATURE_MAX_CHARS].rstrip() + "…"
|
|
80
|
+
return text
|
|
81
|
+
|
|
60
82
|
|
|
61
83
|
def _node_text(node, lines: List[str]) -> str:
|
|
62
84
|
"""The source text of a node, joined across lines, whitespace-normalized."""
|
|
@@ -157,7 +179,7 @@ def _walk_python(node, lines: List[str], sections: List[Dict[str, Any]], parent_
|
|
|
157
179
|
# Find the colon
|
|
158
180
|
colon_node = next((c for c in child.children if c.type == ':'), None)
|
|
159
181
|
sig_end = colon_node.end_point[0] if colon_node else sig_start
|
|
160
|
-
signature =
|
|
182
|
+
signature = _sig_span(lines, sig_start, sig_end)
|
|
161
183
|
|
|
162
184
|
section = {
|
|
163
185
|
"type": "class",
|
|
@@ -186,7 +208,7 @@ def _walk_python(node, lines: List[str], sections: List[Dict[str, Any]], parent_
|
|
|
186
208
|
sig_start = child.start_point[0]
|
|
187
209
|
colon_node = next((c for c in child.children if c.type == ':'), None)
|
|
188
210
|
sig_end = colon_node.end_point[0] if colon_node else sig_start
|
|
189
|
-
signature =
|
|
211
|
+
signature = _sig_span(lines, sig_start, sig_end)
|
|
190
212
|
|
|
191
213
|
is_async = any(c.type == 'async' for c in child.children)
|
|
192
214
|
|
|
@@ -232,7 +254,7 @@ def _walk_python(node, lines: List[str], sections: List[Dict[str, Any]], parent_
|
|
|
232
254
|
"name": name,
|
|
233
255
|
"line_start": child.start_point[0] + 1,
|
|
234
256
|
"line_end": child.end_point[0] + 1,
|
|
235
|
-
"signature": lines
|
|
257
|
+
"signature": _sig_line(lines, child)
|
|
236
258
|
})
|
|
237
259
|
|
|
238
260
|
elif child.type == 'comment':
|
|
@@ -249,10 +271,10 @@ def _walk_python(node, lines: List[str], sections: List[Dict[str, Any]], parent_
|
|
|
249
271
|
elif child.type in ('import_statement', 'import_from_statement'):
|
|
250
272
|
section = {
|
|
251
273
|
"type": "import",
|
|
252
|
-
"name": lines
|
|
274
|
+
"name": _sig_line(lines, child),
|
|
253
275
|
"line_start": child.start_point[0] + 1,
|
|
254
276
|
"line_end": child.end_point[0] + 1,
|
|
255
|
-
"signature": lines
|
|
277
|
+
"signature": _sig_line(lines, child)
|
|
256
278
|
}
|
|
257
279
|
if not parent_section:
|
|
258
280
|
sections.append(section)
|
|
@@ -294,7 +316,7 @@ def _walk_js_ts(node, lines: List[str], sections: List[Dict[str, Any]], parent_s
|
|
|
294
316
|
name = lines[name_node.start_point[0]][name_node.start_point[1]:name_node.end_point[1]] if name_node else 'Unknown'
|
|
295
317
|
|
|
296
318
|
# Simple signature heuristic: first line
|
|
297
|
-
signature = lines
|
|
319
|
+
signature = _sig_line(lines, child)
|
|
298
320
|
|
|
299
321
|
t = child.type.split('_')[0]
|
|
300
322
|
section = {
|
|
@@ -321,7 +343,7 @@ def _walk_js_ts(node, lines: List[str], sections: List[Dict[str, Any]], parent_s
|
|
|
321
343
|
name_node = next((c for c in child.children if c.type in ('property_identifier', 'identifier', 'private_property_identifier')), None)
|
|
322
344
|
name = lines[name_node.start_point[0]][name_node.start_point[1]:name_node.end_point[1]] if name_node else 'Unknown'
|
|
323
345
|
|
|
324
|
-
signature = lines
|
|
346
|
+
signature = _sig_line(lines, child)
|
|
325
347
|
|
|
326
348
|
is_async = any(c.type == 'async' for c in child.children)
|
|
327
349
|
|
|
@@ -373,7 +395,7 @@ def _walk_js_ts(node, lines: List[str], sections: List[Dict[str, Any]], parent_s
|
|
|
373
395
|
"name": name,
|
|
374
396
|
"line_start": child.start_point[0] + 1,
|
|
375
397
|
"line_end": child.end_point[0] + 1,
|
|
376
|
-
"signature": lines
|
|
398
|
+
"signature": _sig_line(lines, child)
|
|
377
399
|
}
|
|
378
400
|
if is_async: section["async"] = True
|
|
379
401
|
doc = _extract_docstring_js(child, lines)
|
|
@@ -391,7 +413,7 @@ def _walk_js_ts(node, lines: List[str], sections: List[Dict[str, Any]], parent_s
|
|
|
391
413
|
"name": name,
|
|
392
414
|
"line_start": child.start_point[0] + 1,
|
|
393
415
|
"line_end": child.end_point[0] + 1,
|
|
394
|
-
"signature": lines
|
|
416
|
+
"signature": _sig_line(lines, child)
|
|
395
417
|
})
|
|
396
418
|
|
|
397
419
|
elif child.type == 'comment':
|
|
@@ -408,10 +430,10 @@ def _walk_js_ts(node, lines: List[str], sections: List[Dict[str, Any]], parent_s
|
|
|
408
430
|
elif child.type == 'import_statement':
|
|
409
431
|
section = {
|
|
410
432
|
"type": "import",
|
|
411
|
-
"name": lines
|
|
433
|
+
"name": _sig_line(lines, child),
|
|
412
434
|
"line_start": child.start_point[0] + 1,
|
|
413
435
|
"line_end": child.end_point[0] + 1,
|
|
414
|
-
"signature": lines
|
|
436
|
+
"signature": _sig_line(lines, child)
|
|
415
437
|
}
|
|
416
438
|
if not parent_section:
|
|
417
439
|
sections.append(section)
|
|
@@ -446,7 +468,7 @@ def _walk_html(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
446
468
|
"name": f"{tag_name}: {text_content.strip()[:60]}",
|
|
447
469
|
"line_start": child.start_point[0] + 1,
|
|
448
470
|
"line_end": child.end_point[0] + 1,
|
|
449
|
-
"signature": lines
|
|
471
|
+
"signature": _sig_line(lines, child)
|
|
450
472
|
})
|
|
451
473
|
|
|
452
474
|
# Check for ID attribute
|
|
@@ -474,7 +496,7 @@ def _walk_html(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
474
496
|
"tag": tag_name,
|
|
475
497
|
"line_start": child.start_point[0] + 1,
|
|
476
498
|
"line_end": child.end_point[0] + 1,
|
|
477
|
-
"signature": lines
|
|
499
|
+
"signature": _sig_line(lines, child)
|
|
478
500
|
})
|
|
479
501
|
|
|
480
502
|
# Recurse into element children
|
|
@@ -504,7 +526,7 @@ def _walk_markdown(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
504
526
|
"name": f"{level}: {content_text.strip()[:60]}",
|
|
505
527
|
"line_start": child.start_point[0] + 1,
|
|
506
528
|
"line_end": child.end_point[0] + 1,
|
|
507
|
-
"signature": lines
|
|
529
|
+
"signature": _sig_line(lines, child)
|
|
508
530
|
})
|
|
509
531
|
|
|
510
532
|
# Recurse if needed (atx headings are top level usually, but just in case)
|
|
@@ -582,7 +604,7 @@ def _walk_go(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
582
604
|
name_node = next((c for c in child.children if c.type == 'identifier' or c.type == 'field_identifier'), None)
|
|
583
605
|
name = lines[name_node.start_point[0]][name_node.start_point[1]:name_node.end_point[1]] if name_node else 'Unknown'
|
|
584
606
|
|
|
585
|
-
signature = lines
|
|
607
|
+
signature = _sig_line(lines, child)
|
|
586
608
|
|
|
587
609
|
receiver = ""
|
|
588
610
|
if child.type == 'method_declaration':
|
|
@@ -609,7 +631,7 @@ def _walk_go(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
609
631
|
"name": _node_text(ident, lines),
|
|
610
632
|
"line_start": spec.start_point[0] + 1,
|
|
611
633
|
"line_end": spec.end_point[0] + 1,
|
|
612
|
-
"signature": lines
|
|
634
|
+
"signature": _sig_line(lines, spec)
|
|
613
635
|
})
|
|
614
636
|
elif child.type == 'type_declaration':
|
|
615
637
|
# Drill into type specs
|
|
@@ -625,7 +647,7 @@ def _walk_go(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
625
647
|
"name": name,
|
|
626
648
|
"line_start": child.start_point[0] + 1,
|
|
627
649
|
"line_end": child.end_point[0] + 1,
|
|
628
|
-
"signature": lines
|
|
650
|
+
"signature": _sig_line(lines, child)
|
|
629
651
|
})
|
|
630
652
|
_walk_go(child, lines, sections)
|
|
631
653
|
|
|
@@ -656,9 +678,9 @@ def _walk_rust(node, lines: List[str], sections: List[Dict[str, Any]]):
|
|
|
656
678
|
sig.append(ln)
|
|
657
679
|
if '{' in ln or ln.rstrip().endswith(';'):
|
|
658
680
|
break
|
|
659
|
-
signature = " ".join(x.strip() for x in sig)
|
|
681
|
+
signature = " ".join(x.strip() for x in sig)[:SIGNATURE_MAX_CHARS]
|
|
660
682
|
else:
|
|
661
|
-
signature = lines
|
|
683
|
+
signature = _sig_line(lines, child)
|
|
662
684
|
sections.append({
|
|
663
685
|
"type": stype,
|
|
664
686
|
"name": name,
|
|
File without changes
|
{code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{code_context_control-2.123.0.dist-info → code_context_control-2.124.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|