codrspot-processor-mcp 0.1.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.
Files changed (100) hide show
  1. codepreproc_client/__init__.py +14 -0
  2. codepreproc_client/__main__.py +9 -0
  3. codepreproc_client/layer1_business/__init__.py +0 -0
  4. codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
  5. codepreproc_client/layer1_business/api_client/__init__.py +0 -0
  6. codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
  7. codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
  8. codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
  9. codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
  10. codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
  11. codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
  12. codepreproc_client/layer1_business/cli/__init__.py +0 -0
  13. codepreproc_client/layer1_business/cli/init.py +578 -0
  14. codepreproc_client/layer1_business/cli/main.py +78 -0
  15. codepreproc_client/layer1_business/config.py +104 -0
  16. codepreproc_client/layer1_business/git_utils.py +67 -0
  17. codepreproc_client/layer1_business/license/__init__.py +0 -0
  18. codepreproc_client/layer1_business/license/fingerprint.py +61 -0
  19. codepreproc_client/layer1_business/license/jwt_client.py +60 -0
  20. codepreproc_client/layer1_business/license/project_lock.py +44 -0
  21. codepreproc_client/layer1_business/local_registry.py +40 -0
  22. codepreproc_client/layer1_business/manifest/__init__.py +0 -0
  23. codepreproc_client/layer1_business/manifest/credentials.py +35 -0
  24. codepreproc_client/layer1_business/mcp/__init__.py +0 -0
  25. codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
  26. codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
  27. codepreproc_client/layer1_business/mcp/server.py +656 -0
  28. codepreproc_client/layer1_business/mcp/tools.py +1665 -0
  29. codepreproc_client/layer1_business/project_registry.py +99 -0
  30. codepreproc_client/layer2_tooling/__init__.py +0 -0
  31. codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
  32. codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
  33. codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
  34. codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
  35. codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
  36. codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
  37. codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
  38. codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
  39. codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
  40. codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
  41. codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
  42. codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
  43. codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
  44. codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
  45. codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
  46. codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
  47. codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
  48. codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
  49. codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
  50. codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
  51. codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
  52. codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
  53. codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
  54. codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
  55. codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
  56. codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
  57. codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
  58. codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
  59. codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
  60. codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
  61. codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
  62. codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
  63. codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
  64. codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
  65. codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
  66. codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
  67. codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
  68. codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
  69. codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
  70. codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
  71. codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
  72. codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
  73. codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
  74. codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
  75. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
  76. codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
  77. codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
  78. codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
  79. codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
  80. codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
  81. codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
  82. codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
  83. codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
  84. codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
  85. codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
  86. codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
  87. codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
  88. codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
  89. codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
  90. codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
  91. codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
  92. codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
  93. codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
  94. codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
  95. codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
  96. codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
  97. codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
  98. contracts/__init__.py +0 -0
  99. contracts/dtos.py +1239 -0
  100. contracts/ir.py +102 -0
@@ -0,0 +1,313 @@
1
+ """AST-first chunking with safe fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ import xxhash
9
+
10
+ from codepreproc_client.layer2_tooling.condensation.chunking.languages import detect_language, get_chunk_query, get_language, get_parser
11
+ from codepreproc_client.layer1_business.git_utils import normalize_content
12
+ from contracts.dtos import ChunkingConfig, CodeChunk, ProjectConfig
13
+
14
+ _SYMBOL_RE = re.compile(r"\b([A-Za-z_][A-Za-z0-9_\.]{2,})\b")
15
+
16
+
17
+ class AstChunker:
18
+ """Chunk files using tree-sitter when possible, otherwise fallback to line blocks."""
19
+
20
+ def __init__(self, chunking: ChunkingConfig) -> None:
21
+ self.chunking = chunking
22
+
23
+ def chunk_file(self, file_path: Path, project: ProjectConfig, git_sha: str) -> list[CodeChunk]:
24
+ """Chunk a source file."""
25
+
26
+ try:
27
+ if file_path.stat().st_size > self.chunking.max_file_size_kb * 1024:
28
+ return []
29
+ raw = file_path.read_bytes()
30
+ except OSError:
31
+ return []
32
+
33
+ if b"\x00" in raw:
34
+ return []
35
+
36
+ try:
37
+ text = normalize_content(raw.decode("utf-8"))
38
+ except UnicodeDecodeError:
39
+ return []
40
+
41
+ relative_path = str(file_path.resolve().relative_to(project.root.resolve()))
42
+ language = detect_language(file_path) or "text"
43
+ parser = get_parser(language)
44
+ query = get_chunk_query(language)
45
+ if parser is not None and query is not None:
46
+ chunks = self._chunk_with_tree_sitter(text, relative_path, language, project, git_sha, parser, query)
47
+ if chunks:
48
+ return chunks
49
+ return self._chunk_fallback(text, relative_path, language, project, git_sha)
50
+
51
+ def _chunk_with_tree_sitter(
52
+ self,
53
+ text: str,
54
+ relative_path: str,
55
+ language: str,
56
+ project: ProjectConfig,
57
+ git_sha: str,
58
+ parser: object,
59
+ query: object,
60
+ ) -> list[CodeChunk]:
61
+ """Best-effort tree-sitter chunking."""
62
+
63
+ try:
64
+ from tree_sitter import QueryCursor
65
+
66
+ tree = parser.parse(text.encode("utf-8"))
67
+ cursor = QueryCursor(query)
68
+ raw_matches = list(cursor.matches(getattr(tree, "root_node")))
69
+ except Exception:
70
+ return []
71
+
72
+ # matches() yields (pattern_index, {capture_name: Node | [Node]}) —
73
+ # each dict entry is a single matched pattern, so chunk and symbol are
74
+ # guaranteed to correspond to each other.
75
+ matches: list[tuple[object, str | None]] = []
76
+ for _pat_idx, cap in raw_matches:
77
+ chunk = cap.get("chunk")
78
+ sym = cap.get("symbol")
79
+ if chunk is None:
80
+ continue
81
+ if isinstance(chunk, list):
82
+ chunk = chunk[0] if chunk else None
83
+ if isinstance(sym, list):
84
+ sym = sym[0] if sym else None
85
+ if chunk is None:
86
+ continue
87
+ try:
88
+ symbol_text: str | None = sym.text.decode("utf-8") if sym is not None else None
89
+ except Exception:
90
+ symbol_text = None
91
+ matches.append((chunk, symbol_text))
92
+
93
+ results: list[CodeChunk] = []
94
+ for node, symbol in matches:
95
+ try:
96
+ body = normalize_content(node.text.decode("utf-8"))
97
+ start_line = node.start_point.row + 1
98
+ except Exception:
99
+ continue
100
+ kind = self._kind_from_node_type(getattr(node, "type", "symbol"))
101
+ results.extend(
102
+ self._build_chunks(
103
+ text=body,
104
+ relative_path=relative_path,
105
+ language=language,
106
+ project_id=project.project_id,
107
+ symbol=symbol or f"{Path(relative_path).stem}:{start_line}",
108
+ kind=kind,
109
+ start_line=start_line,
110
+ git_sha=git_sha,
111
+ )
112
+ )
113
+ return results
114
+
115
+ def _chunk_fallback(
116
+ self,
117
+ text: str,
118
+ relative_path: str,
119
+ language: str,
120
+ project: ProjectConfig,
121
+ git_sha: str,
122
+ ) -> list[CodeChunk]:
123
+ """Split a file into bounded blocks."""
124
+
125
+ lines = text.splitlines()
126
+ blocks: list[list[str]] = []
127
+ current: list[str] = []
128
+ current_len = 0
129
+ for line in lines:
130
+ line_len = len(line) + 1
131
+ should_break = current and (
132
+ current_len + line_len > self.chunking.max_chunk_chars
133
+ or (not line.strip() and current_len >= self.chunking.max_chunk_chars // 2)
134
+ )
135
+ if should_break:
136
+ blocks.append(current)
137
+ current = []
138
+ current_len = 0
139
+ current.append(line)
140
+ current_len += line_len
141
+ if current:
142
+ blocks.append(current)
143
+
144
+ chunks: list[CodeChunk] = []
145
+ cursor_line = 1
146
+ for index, block in enumerate(blocks, start=1):
147
+ body = "\n".join(block).strip("\n")
148
+ if not body.strip():
149
+ cursor_line += len(block)
150
+ continue
151
+ symbol = self._derive_symbol(relative_path, body, index)
152
+ chunks.extend(
153
+ self._build_chunks(
154
+ text=body,
155
+ relative_path=relative_path,
156
+ language=language,
157
+ project_id=project.project_id,
158
+ symbol=symbol,
159
+ kind="module",
160
+ start_line=cursor_line,
161
+ git_sha=git_sha,
162
+ )
163
+ )
164
+ cursor_line += len(block)
165
+ return chunks
166
+
167
+ def _build_chunks(
168
+ self,
169
+ text: str,
170
+ relative_path: str,
171
+ language: str,
172
+ project_id: str,
173
+ symbol: str,
174
+ kind: str,
175
+ start_line: int,
176
+ git_sha: str,
177
+ ) -> list[CodeChunk]:
178
+ """Build one or more CodeChunk instances for bounded text."""
179
+
180
+ normalized = normalize_content(text)
181
+ if len(normalized) <= self.chunking.max_chunk_chars:
182
+ return [self._make_chunk(normalized, relative_path, language, project_id, symbol, kind, start_line, git_sha, 0)]
183
+
184
+ chunks: list[CodeChunk] = []
185
+ lines = normalized.splitlines()
186
+ current: list[str] = []
187
+ current_len = 0
188
+ block_start = start_line
189
+ sub_index = 0
190
+ for line in lines:
191
+ line_len = len(line) + 1
192
+ if current and current_len + line_len > self.chunking.max_chunk_chars:
193
+ body = "\n".join(current)
194
+ chunks.append(
195
+ self._make_chunk(body, relative_path, language, project_id, f"{symbol}#{sub_index}", kind, block_start, git_sha, len(current) - 1)
196
+ )
197
+ sub_index += 1
198
+ block_start += len(current)
199
+ current = []
200
+ current_len = 0
201
+ current.append(line)
202
+ current_len += line_len
203
+ if current:
204
+ body = "\n".join(current)
205
+ chunks.append(
206
+ self._make_chunk(body, relative_path, language, project_id, f"{symbol}#{sub_index}", kind, block_start, git_sha, len(current) - 1)
207
+ )
208
+ return chunks
209
+
210
+ def _make_chunk(
211
+ self,
212
+ body: str,
213
+ relative_path: str,
214
+ language: str,
215
+ project_id: str,
216
+ symbol: str,
217
+ kind: str,
218
+ start_line: int,
219
+ git_sha: str,
220
+ relative_end_offset: int,
221
+ ) -> CodeChunk:
222
+ """Create a chunk model."""
223
+
224
+ signature = next((line.strip() for line in body.splitlines() if line.strip()), None)
225
+ content_hash = xxhash.xxh64_hexdigest(f"{relative_path}\n{symbol}\n{body}")
226
+ deps = sorted({token for token in _SYMBOL_RE.findall(body) if token != symbol})[:32]
227
+ return CodeChunk(
228
+ chunk_id=content_hash,
229
+ project_id=project_id,
230
+ file_path=relative_path,
231
+ language=language,
232
+ symbol=symbol,
233
+ kind=kind,
234
+ start_line=start_line,
235
+ end_line=start_line + relative_end_offset,
236
+ body=body,
237
+ signature=signature,
238
+ deps=deps,
239
+ git_sha=git_sha,
240
+ content_hash=content_hash,
241
+ )
242
+
243
+ @staticmethod
244
+ def _kind_from_node_type(node_type: str) -> str:
245
+ _EXACT: dict[str, str] = {
246
+ # JavaScript / TypeScript
247
+ "class_declaration": "class",
248
+ "abstract_class_declaration": "class",
249
+ "interface_declaration": "class",
250
+ "enum_declaration": "class",
251
+ "type_alias_declaration": "class",
252
+ "function_declaration": "function",
253
+ "arrow_function": "function",
254
+ "function_expression": "function",
255
+ "generator_function_declaration": "function",
256
+ "lexical_declaration": "function", # const foo = () => {}
257
+ "method_definition": "method",
258
+ # Python
259
+ "function_definition": "function",
260
+ "class_definition": "class",
261
+ # Go
262
+ "method_declaration": "method",
263
+ "type_declaration": "class",
264
+ # Rust
265
+ "function_item": "function",
266
+ "struct_item": "class",
267
+ "impl_item": "class",
268
+ # C#
269
+ "record_declaration": "class",
270
+ "constructor_declaration": "method",
271
+ "namespace_declaration": "module",
272
+ "file_scoped_namespace_declaration": "module",
273
+ # C
274
+ "struct_specifier": "class",
275
+ "enum_specifier": "class",
276
+ "type_definition": "class",
277
+ "preproc_function_def": "function",
278
+ }
279
+ if node_type in _EXACT:
280
+ return _EXACT[node_type]
281
+ # Fallback: substring signals
282
+ if "class" in node_type or "struct" in node_type or "interface" in node_type or "enum" in node_type:
283
+ return "class"
284
+ if "method" in node_type:
285
+ return "method"
286
+ if "function" in node_type:
287
+ return "function"
288
+ return "module"
289
+
290
+ @staticmethod
291
+ def _derive_symbol(relative_path: str, body: str, index: int) -> str:
292
+ # Python / Go / Rust / generic: def foo, class Foo, func Foo, fn foo
293
+ m = re.search(
294
+ r"^\s*(?:export\s+(?:default\s+)?)?(?:async\s+)?(?:def|class|func|fn|function\*?)\s+([A-Za-z_$][A-Za-z0-9_$]*)",
295
+ body, flags=re.MULTILINE,
296
+ )
297
+ if m:
298
+ return m.group(1)
299
+ # JS/TS: const/let/var foo = ... (arrow fn, function expr, class expr)
300
+ m = re.search(
301
+ r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*[=:]",
302
+ body, flags=re.MULTILINE,
303
+ )
304
+ if m:
305
+ return m.group(1)
306
+ # TypeScript-specific declarations: interface, type, enum, abstract class
307
+ m = re.search(
308
+ r"^\s*(?:export\s+)?(?:interface|type|enum|abstract\s+class)\s+([A-Za-z_$][A-Za-z0-9_$]*)",
309
+ body, flags=re.MULTILINE,
310
+ )
311
+ if m:
312
+ return m.group(1)
313
+ return f"{Path(relative_path).stem}:block_{index}"
@@ -0,0 +1,238 @@
1
+ """Language detection and tree-sitter query helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ LANGUAGE_EXTENSIONS: dict[str, tuple[str, ...]] = {
10
+ "python": (".py",),
11
+ "typescript": (".ts",),
12
+ "tsx": (".tsx",),
13
+ "javascript": (".js", ".mjs", ".cjs", ".jsx"),
14
+ "go": (".go",),
15
+ "rust": (".rs",),
16
+ "c": (".c", ".h"),
17
+ "csharp": (".cs",),
18
+ "java": (".java",),
19
+ "php": (".php", ".phtml"),
20
+ "yaml": (".yaml", ".yml"),
21
+ "markdown": (".md",),
22
+ }
23
+
24
+ # Arrow-function-in-const pattern reused across JS/TS grammars.
25
+ _JS_ARROW_CONST = """
26
+ (lexical_declaration
27
+ (variable_declarator name: (identifier) @symbol value: (arrow_function))
28
+ ) @chunk
29
+ (lexical_declaration
30
+ (variable_declarator name: (identifier) @symbol value: (function_expression))
31
+ ) @chunk
32
+ """
33
+
34
+ QUERY_STRINGS: dict[str, str] = {
35
+ "python": """
36
+ (function_definition name: (identifier) @symbol) @chunk
37
+ (class_definition name: (identifier) @symbol) @chunk
38
+ """,
39
+ "typescript": """
40
+ (function_declaration name: (identifier) @symbol) @chunk
41
+ (class_declaration name: (type_identifier) @symbol) @chunk
42
+ (method_definition name: (property_identifier) @symbol) @chunk
43
+ (interface_declaration name: (type_identifier) @symbol) @chunk
44
+ (enum_declaration name: (identifier) @symbol) @chunk
45
+ (type_alias_declaration name: (type_identifier) @symbol) @chunk
46
+ """ + _JS_ARROW_CONST,
47
+ "tsx": """
48
+ (function_declaration name: (identifier) @symbol) @chunk
49
+ (class_declaration name: (type_identifier) @symbol) @chunk
50
+ (method_definition name: (property_identifier) @symbol) @chunk
51
+ (interface_declaration name: (type_identifier) @symbol) @chunk
52
+ (enum_declaration name: (identifier) @symbol) @chunk
53
+ """ + _JS_ARROW_CONST,
54
+ "javascript": """
55
+ (function_declaration name: (identifier) @symbol) @chunk
56
+ (class_declaration name: (identifier) @symbol) @chunk
57
+ (method_definition name: (property_identifier) @symbol) @chunk
58
+ """ + _JS_ARROW_CONST,
59
+ "go": """
60
+ (function_declaration name: (identifier) @symbol) @chunk
61
+ (method_declaration name: (field_identifier) @symbol) @chunk
62
+ (type_declaration (type_spec name: (type_identifier) @symbol)) @chunk
63
+ """,
64
+ "rust": """
65
+ (function_item name: (identifier) @symbol) @chunk
66
+ (struct_item name: (type_identifier) @symbol) @chunk
67
+ (impl_item type: (type_identifier) @symbol) @chunk
68
+ """,
69
+ "csharp": """
70
+ (class_declaration name: (identifier) @symbol) @chunk
71
+ (interface_declaration name: (identifier) @symbol) @chunk
72
+ (method_declaration name: (identifier) @symbol) @chunk
73
+ (constructor_declaration name: (identifier) @symbol) @chunk
74
+ (struct_declaration name: (identifier) @symbol) @chunk
75
+ (enum_declaration name: (identifier) @symbol) @chunk
76
+ (record_declaration name: (identifier) @symbol) @chunk
77
+ (namespace_declaration name: (qualified_name) @symbol) @chunk
78
+ (namespace_declaration name: (identifier) @symbol) @chunk
79
+ """,
80
+ "java": """
81
+ (class_declaration name: (identifier) @symbol) @chunk
82
+ (interface_declaration name: (identifier) @symbol) @chunk
83
+ (enum_declaration name: (identifier) @symbol) @chunk
84
+ (method_declaration name: (identifier) @symbol) @chunk
85
+ (constructor_declaration name: (identifier) @symbol) @chunk
86
+ """,
87
+ "php": """
88
+ (function_definition name: (name) @symbol) @chunk
89
+ (class_declaration name: (name) @symbol) @chunk
90
+ (method_declaration name: (name) @symbol) @chunk
91
+ (interface_declaration name: (name) @symbol) @chunk
92
+ (trait_declaration name: (name) @symbol) @chunk
93
+ (enum_declaration name: (name) @symbol) @chunk
94
+ """,
95
+ "c": """
96
+ (function_definition
97
+ declarator: (function_declarator
98
+ declarator: (identifier) @symbol)) @chunk
99
+ (function_definition
100
+ declarator: (pointer_declarator
101
+ declarator: (function_declarator
102
+ declarator: (identifier) @symbol))) @chunk
103
+ (struct_specifier
104
+ name: (type_identifier) @symbol
105
+ body: (_)) @chunk
106
+ (enum_specifier
107
+ name: (type_identifier) @symbol
108
+ body: (_)) @chunk
109
+ (type_definition
110
+ declarator: (type_identifier) @symbol) @chunk
111
+ (preproc_function_def
112
+ name: (identifier) @symbol) @chunk
113
+ """,
114
+ "yaml": "",
115
+ "markdown": "",
116
+ }
117
+
118
+
119
+ def detect_language(file_path: Path) -> str | None:
120
+ """Return a supported language name from extension."""
121
+
122
+ suffix = file_path.suffix.lower()
123
+ for language, extensions in LANGUAGE_EXTENSIONS.items():
124
+ if suffix in extensions:
125
+ return language
126
+ return None
127
+
128
+
129
+ @lru_cache(maxsize=None)
130
+ def get_parser(language: str) -> Any | None:
131
+ """Return a tree-sitter parser when available."""
132
+
133
+ if language not in QUERY_STRINGS or not QUERY_STRINGS[language]:
134
+ return None
135
+ try:
136
+ from tree_sitter_language_pack import get_parser as ts_get_parser
137
+ return ts_get_parser(language)
138
+ except Exception:
139
+ pass
140
+ try:
141
+ from tree_sitter_languages import get_parser as ts_get_parser
142
+ return ts_get_parser(language)
143
+ except Exception:
144
+ pass
145
+ # Fallback: build a parser from the DLL-loaded language when the
146
+ # tree_sitter_languages package API is incompatible with the installed
147
+ # tree-sitter binding version.
148
+ lang = _get_language_via_dll(language)
149
+ if lang is None:
150
+ return None
151
+ try:
152
+ from tree_sitter import Parser
153
+ return Parser(lang)
154
+ except Exception:
155
+ return None
156
+
157
+
158
+ @lru_cache(maxsize=None)
159
+ def _get_language_via_dll(language: str) -> Any | None:
160
+ """Load a tree-sitter Language directly from tree_sitter_languages' bundled library.
161
+
162
+ Needed when tree_sitter_languages was compiled against an older tree-sitter ABI
163
+ (pre-0.22) but the installed tree-sitter Python binding is 0.22+, which changed
164
+ the Language() constructor. We bypass tree_sitter_languages.get_language() and
165
+ call the underlying C symbol ourselves.
166
+ """
167
+ try:
168
+ import ctypes
169
+ import warnings
170
+ from pathlib import Path as _Path
171
+ import tree_sitter_languages as _tsl
172
+ pkg_dir = _Path(_tsl.__file__).parent
173
+ # Find the bundled native library (name varies by platform).
174
+ for candidate in ("languages.dll", "languages.so", "languages.dylib"):
175
+ lib_path = pkg_dir / candidate
176
+ if lib_path.exists():
177
+ break
178
+ else:
179
+ # Try .pyd / .so with version suffix (Linux wheels)
180
+ matches = list(pkg_dir.glob("languages*.so")) + list(pkg_dir.glob("languages*.pyd"))
181
+ if not matches:
182
+ return None
183
+ lib_path = matches[0]
184
+
185
+ lib = ctypes.CDLL(str(lib_path))
186
+ fn_name = f"tree_sitter_{language.replace('-', '_')}"
187
+ fn = getattr(lib, fn_name, None)
188
+ if fn is None:
189
+ return None
190
+ fn.restype = ctypes.c_void_p
191
+ ptr = fn()
192
+ if not ptr:
193
+ return None
194
+ from tree_sitter import Language
195
+ with warnings.catch_warnings():
196
+ warnings.simplefilter("ignore", DeprecationWarning)
197
+ return Language(ptr)
198
+ except Exception:
199
+ return None
200
+
201
+
202
+ @lru_cache(maxsize=None)
203
+ def get_language(language: str) -> Any | None:
204
+ """Return the tree-sitter Language object for query compilation."""
205
+
206
+ try:
207
+ from tree_sitter_language_pack import get_language as ts_get_language
208
+ return ts_get_language(language)
209
+ except Exception:
210
+ pass
211
+ try:
212
+ from tree_sitter_languages import get_language as ts_get_language
213
+ return ts_get_language(language)
214
+ except Exception:
215
+ pass
216
+ return _get_language_via_dll(language)
217
+
218
+
219
+ @lru_cache(maxsize=None)
220
+ def get_chunk_query(language: str) -> Any | None:
221
+ """Return a compiled tree-sitter Query when available."""
222
+
223
+ query_str = QUERY_STRINGS.get(language, "")
224
+ if not query_str:
225
+ return None
226
+ lang = get_language(language)
227
+ if lang is None:
228
+ return None
229
+ try:
230
+ from tree_sitter import Query
231
+ return Query(lang, query_str)
232
+ except Exception:
233
+ pass
234
+ # Fallback for older tree-sitter where lang.query() still works
235
+ try:
236
+ return lang.query(query_str)
237
+ except Exception:
238
+ return None
@@ -0,0 +1,128 @@
1
+ """Embedding subprocess worker.
2
+
3
+ Launched as ``python -u -m codepreproc_client.layer2_tooling.condensation.embed_worker``.
4
+ Communicates with the parent via stdin/stdout JSON lines.
5
+
6
+ Protocol
7
+ --------
8
+ Parent → worker (stdin):
9
+ First line: {"model_name": "...", "device": "..."}
10
+ Subsequent lines: {"type": "encode", "request_id": "...", "texts": [...], "batch_size": N}
11
+ or {"type": "shutdown"}
12
+
13
+ Worker → parent (stdout):
14
+ {"type": "worker_ready"}
15
+ {"type": "worker_failed", "error": "..."}
16
+ {"type": "batch_done", "request_id": "...", "vectors": [[...]], "start": N, "end": N, "total_texts": N}
17
+ {"type": "batch_resized", "request_id": "...", "batch_size": N, "reason": "oom"}
18
+ {"type": "completed", "request_id": "...", "total_texts": N}
19
+ {"type": "failed", "request_id": "...", "error": "..."}
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import sys
26
+
27
+ import numpy as np
28
+
29
+
30
+ def _send(event: dict) -> None:
31
+ sys.stdout.write(json.dumps(event) + "\n")
32
+ sys.stdout.flush()
33
+
34
+
35
+ def _coerce_batch_vectors(vectors, *, expected_rows: int) -> list[list[float]]:
36
+ array = np.asarray(vectors, dtype=np.float32)
37
+ if expected_rows == 0:
38
+ return []
39
+ if array.ndim == 1:
40
+ if expected_rows != 1:
41
+ raise ValueError(f"embedding_batch_shape_invalid: got {array.shape} for {expected_rows} texts")
42
+ array = array.reshape(1, -1)
43
+ if array.ndim != 2:
44
+ raise ValueError(f"embedding_batch_shape_invalid: got {array.shape} for {expected_rows} texts")
45
+ if array.shape[0] != expected_rows:
46
+ raise ValueError(f"embedding_batch_row_mismatch: got {array.shape[0]} rows for {expected_rows} texts")
47
+ return array.tolist()
48
+
49
+
50
+ def main() -> None:
51
+ config_line = sys.stdin.readline()
52
+ if not config_line:
53
+ return
54
+ try:
55
+ config = json.loads(config_line)
56
+ except json.JSONDecodeError as exc:
57
+ _send({"type": "worker_failed", "error": f"bad config line: {exc}"})
58
+ return
59
+
60
+ model_name = config["model_name"]
61
+ device = config["device"]
62
+
63
+ try:
64
+ from sentence_transformers import SentenceTransformer
65
+
66
+ model = SentenceTransformer(model_name, device=device)
67
+ _send({"type": "worker_ready"})
68
+ except Exception as exc:
69
+ _send({"type": "worker_failed", "error": str(exc)})
70
+ return
71
+
72
+ for raw_line in sys.stdin:
73
+ line = raw_line.strip()
74
+ if not line:
75
+ continue
76
+ try:
77
+ command = json.loads(line)
78
+ except json.JSONDecodeError:
79
+ continue
80
+
81
+ if command["type"] == "shutdown":
82
+ return
83
+ if command["type"] != "encode":
84
+ continue
85
+
86
+ request_id = command["request_id"]
87
+ texts = command["texts"]
88
+ current_batch = max(1, int(command["batch_size"]))
89
+ offset = 0
90
+ try:
91
+ while offset < len(texts):
92
+ upper = min(len(texts), offset + current_batch)
93
+ batch = texts[offset:upper]
94
+ try:
95
+ vectors = model.encode(
96
+ batch,
97
+ batch_size=current_batch,
98
+ show_progress_bar=False,
99
+ convert_to_numpy=True,
100
+ normalize_embeddings=True,
101
+ )
102
+ except RuntimeError as exc:
103
+ if "out of memory" not in str(exc).lower() or current_batch == 1:
104
+ raise
105
+ current_batch = max(1, current_batch // 2)
106
+ _send({
107
+ "type": "batch_resized",
108
+ "request_id": request_id,
109
+ "batch_size": current_batch,
110
+ "reason": "oom",
111
+ })
112
+ continue
113
+ _send({
114
+ "type": "batch_done",
115
+ "request_id": request_id,
116
+ "vectors": _coerce_batch_vectors(vectors, expected_rows=len(batch)),
117
+ "start": offset,
118
+ "end": upper,
119
+ "total_texts": len(texts),
120
+ })
121
+ offset = upper
122
+ _send({"type": "completed", "request_id": request_id, "total_texts": len(texts)})
123
+ except Exception as exc:
124
+ _send({"type": "failed", "request_id": request_id, "error": str(exc)})
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()