codebase-receipts-cli 1.0.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.
- codebase_receipts_cli-1.0.0.dist-info/METADATA +268 -0
- codebase_receipts_cli-1.0.0.dist-info/RECORD +78 -0
- codebase_receipts_cli-1.0.0.dist-info/WHEEL +4 -0
- codebase_receipts_cli-1.0.0.dist-info/entry_points.txt +2 -0
- codebase_receipts_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- receipts/__init__.py +0 -0
- receipts/ama/__init__.py +0 -0
- receipts/ama/interviewer.py +415 -0
- receipts/ats/__init__.py +1 -0
- receipts/ats/comparator.py +98 -0
- receipts/ats/scorer.py +550 -0
- receipts/ats/stats.py +164 -0
- receipts/cli.py +2062 -0
- receipts/config.py +106 -0
- receipts/errors.py +18 -0
- receipts/export.py +120 -0
- receipts/ingest/__init__.py +0 -0
- receipts/ingest/artifact_extractor.py +679 -0
- receipts/ingest/git_source.py +159 -0
- receipts/ingest/manifest.py +114 -0
- receipts/ingest/scanner.py +141 -0
- receipts/ingest/secrets_scanner.py +225 -0
- receipts/interactive/__init__.py +1 -0
- receipts/interactive/repl.py +755 -0
- receipts/ledger/__init__.py +0 -0
- receipts/ledger/pricing_table.py +54 -0
- receipts/ledger/token_ledger.py +226 -0
- receipts/llm/__init__.py +0 -0
- receipts/llm/anthropic_provider.py +95 -0
- receipts/llm/factory.py +124 -0
- receipts/llm/fake_provider.py +79 -0
- receipts/llm/gemini_provider.py +205 -0
- receipts/llm/json_utils.py +46 -0
- receipts/llm/metered.py +62 -0
- receipts/llm/ollama_provider.py +117 -0
- receipts/llm/openai_provider.py +118 -0
- receipts/llm/provider.py +42 -0
- receipts/llm/split.py +78 -0
- receipts/llm/token_estimate.py +35 -0
- receipts/mine/__init__.py +1 -0
- receipts/mine/code_metrics.py +246 -0
- receipts/mine/git_evidence.py +109 -0
- receipts/prep/__init__.py +1 -0
- receipts/prep/dossier.py +313 -0
- receipts/prep/readiness.py +227 -0
- receipts/resume/__init__.py +0 -0
- receipts/resume/claim_extractor.py +338 -0
- receipts/resume/loader.py +35 -0
- receipts/resume/pdf_parser.py +259 -0
- receipts/resume/tex_parser.py +394 -0
- receipts/rewrite/__init__.py +0 -0
- receipts/rewrite/compiler.py +180 -0
- receipts/rewrite/optimizer.py +140 -0
- receipts/rewrite/tex_rewriter.py +227 -0
- receipts/tui/__init__.py +0 -0
- receipts/tui/ama_app.py +454 -0
- receipts/tui/app.py +316 -0
- receipts/tui/dossier_app.py +170 -0
- receipts/tui/hub.py +367 -0
- receipts/tui/ingest_app.py +183 -0
- receipts/tui/rewrite_app.py +463 -0
- receipts/tui/score_app.py +237 -0
- receipts/tui/widgets/__init__.py +0 -0
- receipts/tui/widgets/claims_table.py +50 -0
- receipts/tui/widgets/diff_view.py +38 -0
- receipts/tui/widgets/status_bar.py +38 -0
- receipts/verify/__init__.py +0 -0
- receipts/verify/bm25.py +112 -0
- receipts/verify/enrich.py +128 -0
- receipts/verify/jd_fetch.py +101 -0
- receipts/verify/kb.py +379 -0
- receipts/verify/keyword_gap.py +127 -0
- receipts/verify/query_expand.py +88 -0
- receipts/verify/rerank.py +74 -0
- receipts/verify/rewriter.py +172 -0
- receipts/verify/router.py +211 -0
- receipts/verify/summaries.py +258 -0
- receipts/verify/verifier.py +552 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
"""tree-sitter based extraction of key code artifacts within a token budget.
|
|
2
|
+
|
|
3
|
+
API facts verified against the installed tree-sitter 0.26.0 and
|
|
4
|
+
tree-sitter-language-pack 1.12.2: parsers come from
|
|
5
|
+
``tree_sitter.Parser(get_language(name))`` (the pack's own ``get_parser``
|
|
6
|
+
returns an incompatible native class), ``parse()`` requires bytes, and
|
|
7
|
+
decorated Python defs appear as ``decorated_definition`` wrapping the actual
|
|
8
|
+
``function_definition``/``class_definition``.
|
|
9
|
+
|
|
10
|
+
Every artifact's code is redacted with the secrets scanner before it leaves
|
|
11
|
+
this module — content headed for the knowledge base must never carry a
|
|
12
|
+
credential. Budget overruns are counted and reported, never silent.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections import deque
|
|
19
|
+
from collections.abc import Callable, Sequence
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import tree_sitter
|
|
25
|
+
from tree_sitter_language_pack import get_language
|
|
26
|
+
except (ImportError, OSError) as _tree_sitter_err:
|
|
27
|
+
import platform as _platform
|
|
28
|
+
import sys as _sys
|
|
29
|
+
|
|
30
|
+
_arch = _platform.machine()
|
|
31
|
+
_os = _platform.system()
|
|
32
|
+
_py = _sys.version.split()[0]
|
|
33
|
+
raise SystemExit(
|
|
34
|
+
f"\n[receipts] tree-sitter failed to load on {_os}/{_arch} (Python {_py}).\n"
|
|
35
|
+
"\n"
|
|
36
|
+
"This usually means a prebuilt wheel isn't available for your\n"
|
|
37
|
+
"architecture. To fix it you need a C compiler so pip can build\n"
|
|
38
|
+
"the extension from source:\n"
|
|
39
|
+
"\n"
|
|
40
|
+
" Linux/ARM64 (e.g. Raspberry Pi, Graviton):\n"
|
|
41
|
+
" sudo apt install build-essential python3-dev\n"
|
|
42
|
+
" pip install tree-sitter tree-sitter-language-pack\n"
|
|
43
|
+
"\n"
|
|
44
|
+
" macOS:\n"
|
|
45
|
+
" xcode-select --install\n"
|
|
46
|
+
" pip install tree-sitter tree-sitter-language-pack\n"
|
|
47
|
+
"\n"
|
|
48
|
+
" Windows (no C compiler):\n"
|
|
49
|
+
" Install Visual Studio Build Tools from:\n"
|
|
50
|
+
" https://visualstudio.microsoft.com/visual-cpp-build-tools/\n"
|
|
51
|
+
" then re-run: pip install tree-sitter tree-sitter-language-pack\n"
|
|
52
|
+
"\n"
|
|
53
|
+
"If your platform is genuinely unsupported, open an issue at:\n"
|
|
54
|
+
"https://github.com/Ishaan-1606/receipts-cli/issues\n"
|
|
55
|
+
"\n"
|
|
56
|
+
f"Original error: {_tree_sitter_err}\n"
|
|
57
|
+
) from None
|
|
58
|
+
|
|
59
|
+
from receipts.ingest import scanner
|
|
60
|
+
from receipts.ingest.secrets_scanner import redact
|
|
61
|
+
|
|
62
|
+
_EXTENSION_LANGUAGES = {
|
|
63
|
+
".py": "python",
|
|
64
|
+
".js": "javascript",
|
|
65
|
+
".jsx": "javascript",
|
|
66
|
+
".ts": "typescript",
|
|
67
|
+
".tsx": "tsx",
|
|
68
|
+
# Phase 20 — extraction parity across mainstream languages, chosen by
|
|
69
|
+
# usage share. Every language here has BOTH a tree-sitter map (verified
|
|
70
|
+
# against the installed tree-sitter-language-pack by probing each
|
|
71
|
+
# grammar's actual node types) and a regex signature fallback (the path
|
|
72
|
+
# Windows uses, where tree-sitter is disabled).
|
|
73
|
+
".java": "java",
|
|
74
|
+
".kt": "kotlin",
|
|
75
|
+
".kts": "kotlin",
|
|
76
|
+
".go": "go",
|
|
77
|
+
".rs": "rust",
|
|
78
|
+
".c": "c",
|
|
79
|
+
".h": "c",
|
|
80
|
+
".cpp": "cpp",
|
|
81
|
+
".cc": "cpp",
|
|
82
|
+
".cxx": "cpp",
|
|
83
|
+
".hpp": "cpp",
|
|
84
|
+
".hh": "cpp",
|
|
85
|
+
".cs": "csharp",
|
|
86
|
+
".php": "php",
|
|
87
|
+
".rb": "ruby",
|
|
88
|
+
".swift": "swift",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_PY_DEF_TYPES = {"function_definition": "function", "class_definition": "class"}
|
|
92
|
+
_JS_DEF_TYPES = {
|
|
93
|
+
"function_declaration": "function",
|
|
94
|
+
"class_declaration": "class",
|
|
95
|
+
"interface_declaration": "interface",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Top-level definition node types per grammar → artifact kind. Every entry
|
|
99
|
+
# was verified by parsing real snippets with the installed language pack —
|
|
100
|
+
# never assumed from memory (several grammars surprised: Kotlin has no
|
|
101
|
+
# `name` field, Go type names live on the inner type_spec, Swift parses
|
|
102
|
+
# extensions as class_declaration, C#/C++ nest types inside namespaces).
|
|
103
|
+
_TS_DEF_TYPES: dict[str, dict[str, str]] = {
|
|
104
|
+
"python": _PY_DEF_TYPES,
|
|
105
|
+
"javascript": _JS_DEF_TYPES,
|
|
106
|
+
"typescript": _JS_DEF_TYPES,
|
|
107
|
+
"tsx": _JS_DEF_TYPES,
|
|
108
|
+
"java": {
|
|
109
|
+
"class_declaration": "class",
|
|
110
|
+
"interface_declaration": "interface",
|
|
111
|
+
"enum_declaration": "class",
|
|
112
|
+
"record_declaration": "class",
|
|
113
|
+
},
|
|
114
|
+
"kotlin": {
|
|
115
|
+
"function_declaration": "function",
|
|
116
|
+
"class_declaration": "class",
|
|
117
|
+
"object_declaration": "class",
|
|
118
|
+
},
|
|
119
|
+
"go": {
|
|
120
|
+
"function_declaration": "function",
|
|
121
|
+
"method_declaration": "function",
|
|
122
|
+
"type_declaration": "class",
|
|
123
|
+
},
|
|
124
|
+
"rust": {
|
|
125
|
+
"function_item": "function",
|
|
126
|
+
"struct_item": "class",
|
|
127
|
+
"enum_item": "class",
|
|
128
|
+
"trait_item": "interface",
|
|
129
|
+
"impl_item": "class",
|
|
130
|
+
},
|
|
131
|
+
"c": {
|
|
132
|
+
"function_definition": "function",
|
|
133
|
+
"struct_specifier": "class",
|
|
134
|
+
"enum_specifier": "class",
|
|
135
|
+
},
|
|
136
|
+
"cpp": {
|
|
137
|
+
"function_definition": "function",
|
|
138
|
+
"class_specifier": "class",
|
|
139
|
+
"struct_specifier": "class",
|
|
140
|
+
"namespace_definition": "class",
|
|
141
|
+
"enum_specifier": "class",
|
|
142
|
+
},
|
|
143
|
+
"csharp": {
|
|
144
|
+
# With block namespaces the whole namespace becomes one artifact
|
|
145
|
+
# (like a Python class with its methods); file-scoped namespaces
|
|
146
|
+
# leave types at top level where they match individually.
|
|
147
|
+
"namespace_declaration": "class",
|
|
148
|
+
"class_declaration": "class",
|
|
149
|
+
"interface_declaration": "interface",
|
|
150
|
+
"struct_declaration": "class",
|
|
151
|
+
"enum_declaration": "class",
|
|
152
|
+
"record_declaration": "class",
|
|
153
|
+
},
|
|
154
|
+
"php": {
|
|
155
|
+
"class_declaration": "class",
|
|
156
|
+
"interface_declaration": "interface",
|
|
157
|
+
"trait_declaration": "class",
|
|
158
|
+
"function_definition": "function",
|
|
159
|
+
},
|
|
160
|
+
"ruby": {
|
|
161
|
+
"class": "class",
|
|
162
|
+
"module": "class",
|
|
163
|
+
"method": "function",
|
|
164
|
+
},
|
|
165
|
+
"swift": {
|
|
166
|
+
"class_declaration": "class", # covers struct/enum/extension too
|
|
167
|
+
"protocol_declaration": "interface",
|
|
168
|
+
"function_declaration": "function",
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#: Fallback excerpt size for files with no extractable definitions
|
|
173
|
+
#: (README, config, unsupported languages).
|
|
174
|
+
_FALLBACK_MAX_LINES = 150
|
|
175
|
+
|
|
176
|
+
#: Lines that are import/package boilerplate — the fallback excerpt skips a
|
|
177
|
+
#: contiguous run of these (plus blanks) at the top of the file so the
|
|
178
|
+
#: excerpt captures logic instead of headers. Deliberately narrow: comment
|
|
179
|
+
#: markers are NOT skipped (a markdown title or license header may be the
|
|
180
|
+
#: most informative content a doc file has).
|
|
181
|
+
_BOILERPLATE_LINE_RE = re.compile(
|
|
182
|
+
r"^\s*(?:import\b|from\s+[\w.]+\s+import\b|package\b|using\s+\w|"
|
|
183
|
+
r"#include\b|require\s*[('\"]|require_relative\b|use\s+[A-Za-z\\]|"
|
|
184
|
+
r"<\?php\s*$|\s*$)"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
#: Ceiling for the auto-scaled budget (token_budget=None). Corpora under it
|
|
188
|
+
#: are ingested whole; beyond it, importance allocation decides what stays.
|
|
189
|
+
_AUTO_BUDGET_CEILING = 1_000_000
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True)
|
|
193
|
+
class Artifact:
|
|
194
|
+
rel_path: str
|
|
195
|
+
language: str # "python", "javascript", ... or "text" for fallbacks
|
|
196
|
+
kind: str # "function" | "class" | "interface" | "file"
|
|
197
|
+
name: str
|
|
198
|
+
start_line: int # 1-based
|
|
199
|
+
end_line: int
|
|
200
|
+
code: str
|
|
201
|
+
token_estimate: int
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class ExtractionResult:
|
|
206
|
+
budget: int
|
|
207
|
+
artifacts: list[Artifact] = field(default_factory=list)
|
|
208
|
+
used_tokens: int = 0
|
|
209
|
+
dropped_over_budget: int = 0
|
|
210
|
+
redaction_count: int = 0
|
|
211
|
+
auto_budget: bool = False # True when the budget was auto-scaled
|
|
212
|
+
corpus_tokens: int = 0 # total tokens across ALL candidates, pre-budget
|
|
213
|
+
dropped_tokens: int = 0
|
|
214
|
+
dropped_by_dir: dict[str, int] = field(default_factory=dict)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_parsers: dict[str, tree_sitter.Parser] = {}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _parser_for(language: str) -> tree_sitter.Parser:
|
|
221
|
+
if language not in _parsers:
|
|
222
|
+
_parsers[language] = tree_sitter.Parser(get_language(language))
|
|
223
|
+
return _parsers[language]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
#: Node types that ARE the identifier, for grammars without a `name` field
|
|
227
|
+
#: (verified: kotlin uses simple_identifier/type_identifier, rust impl uses
|
|
228
|
+
#: type_identifier, go type_declaration wraps a type_spec, ruby uses
|
|
229
|
+
#: constant/identifier).
|
|
230
|
+
_IDENTIFIER_TYPES = {
|
|
231
|
+
"identifier",
|
|
232
|
+
"simple_identifier",
|
|
233
|
+
"type_identifier",
|
|
234
|
+
"field_identifier",
|
|
235
|
+
"namespace_identifier",
|
|
236
|
+
"constant",
|
|
237
|
+
"name",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _node_name(node: tree_sitter.Node, depth: int = 0) -> str:
|
|
242
|
+
name_node = node.child_by_field_name("name")
|
|
243
|
+
if name_node is not None and name_node.text is not None:
|
|
244
|
+
return name_node.text.decode("utf-8", errors="replace")
|
|
245
|
+
# Grammars without a `name` field: take the first identifier-ish child,
|
|
246
|
+
# recursing a little for wrapper nodes (go type_spec, c declarators).
|
|
247
|
+
for child in node.named_children:
|
|
248
|
+
if child.type in _IDENTIFIER_TYPES and child.text is not None:
|
|
249
|
+
return child.text.decode("utf-8", errors="replace")
|
|
250
|
+
if depth < 2:
|
|
251
|
+
for child in node.named_children:
|
|
252
|
+
found = _node_name(child, depth + 1)
|
|
253
|
+
if found != "<anonymous>":
|
|
254
|
+
return found
|
|
255
|
+
return "<anonymous>"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
#: Namespace containers whose members should be extracted individually —
|
|
259
|
+
#: one verified level of recursion into their declaration_list, so a C#
|
|
260
|
+
#: block namespace yields its classes instead of one namespace-sized blob.
|
|
261
|
+
_NAMESPACE_TYPES = {
|
|
262
|
+
"csharp": "namespace_declaration",
|
|
263
|
+
"cpp": "namespace_definition",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _definition_nodes(
|
|
268
|
+
root: tree_sitter.Node, language: str
|
|
269
|
+
) -> list[tuple[str, str, tree_sitter.Node]]:
|
|
270
|
+
"""Yield (kind, name, node) for top-level defs; node spans decorators/export."""
|
|
271
|
+
defs: list[tuple[str, str, tree_sitter.Node]] = []
|
|
272
|
+
kind_map = _TS_DEF_TYPES.get(language, {})
|
|
273
|
+
for outer in root.named_children:
|
|
274
|
+
inner = outer
|
|
275
|
+
if language == "python" and outer.type == "decorated_definition":
|
|
276
|
+
definition = outer.child_by_field_name("definition")
|
|
277
|
+
if definition is not None:
|
|
278
|
+
inner = definition
|
|
279
|
+
elif (
|
|
280
|
+
language in ("javascript", "typescript", "tsx")
|
|
281
|
+
and outer.type == "export_statement"
|
|
282
|
+
):
|
|
283
|
+
wrapped = next(
|
|
284
|
+
(c for c in outer.named_children if c.type in kind_map), None
|
|
285
|
+
)
|
|
286
|
+
if wrapped is not None:
|
|
287
|
+
inner = wrapped
|
|
288
|
+
elif outer.type == _NAMESPACE_TYPES.get(language):
|
|
289
|
+
body = next(
|
|
290
|
+
(c for c in outer.named_children if c.type == "declaration_list"),
|
|
291
|
+
None,
|
|
292
|
+
)
|
|
293
|
+
members = (
|
|
294
|
+
[
|
|
295
|
+
(kind_map[c.type], _node_name(c), c)
|
|
296
|
+
for c in body.named_children
|
|
297
|
+
if c.type in kind_map
|
|
298
|
+
]
|
|
299
|
+
if body is not None
|
|
300
|
+
else []
|
|
301
|
+
)
|
|
302
|
+
if members:
|
|
303
|
+
defs.extend(members)
|
|
304
|
+
continue
|
|
305
|
+
# Empty or unrecognized body — keep the namespace itself.
|
|
306
|
+
kind = kind_map.get(inner.type)
|
|
307
|
+
if kind is not None:
|
|
308
|
+
defs.append((kind, _node_name(inner), outer))
|
|
309
|
+
return defs
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _ts_code_artifacts(
|
|
313
|
+
rel_path: str, language: str, text: str
|
|
314
|
+
) -> list[tuple[str, str, int, int, str]]:
|
|
315
|
+
"""tree-sitter extraction — may crash on Windows with large batches."""
|
|
316
|
+
source = text.encode("utf-8")
|
|
317
|
+
tree = _parser_for(language).parse(source)
|
|
318
|
+
results = []
|
|
319
|
+
for kind, name, node in _definition_nodes(tree.root_node, language):
|
|
320
|
+
code = source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
321
|
+
results.append(
|
|
322
|
+
(kind, name, node.start_point.row + 1, node.end_point.row + 1, code)
|
|
323
|
+
)
|
|
324
|
+
return results
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
_PY_DEF_RE = re.compile(
|
|
328
|
+
r"^(?P<deco>(?:@\w[\w.]*(?:\(.*?\))?\s*\n)*)?"
|
|
329
|
+
r"^(?P<kw>(?:async\s+)?def|class)\s+(?P<name>\w+)",
|
|
330
|
+
re.MULTILINE,
|
|
331
|
+
)
|
|
332
|
+
_JS_DEF_RE = re.compile(
|
|
333
|
+
r"^(?:export\s+(?:default\s+)?)?(?P<kw>function|class|interface)"
|
|
334
|
+
r"\s+(?P<name>\w+)",
|
|
335
|
+
re.MULTILINE,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
_KIND_MAP = {
|
|
339
|
+
"def": "function",
|
|
340
|
+
"async def": "function",
|
|
341
|
+
"class": "class",
|
|
342
|
+
"function": "function",
|
|
343
|
+
"interface": "interface",
|
|
344
|
+
"fun": "function",
|
|
345
|
+
"fn": "function",
|
|
346
|
+
"func": "function",
|
|
347
|
+
"object": "class",
|
|
348
|
+
"record": "class",
|
|
349
|
+
"enum": "class",
|
|
350
|
+
"struct": "class",
|
|
351
|
+
"union": "class",
|
|
352
|
+
"trait": "class",
|
|
353
|
+
"impl": "class",
|
|
354
|
+
"type": "class",
|
|
355
|
+
"module": "class",
|
|
356
|
+
"protocol": "interface",
|
|
357
|
+
"namespace": "class",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# Regex signature fallback per language: (pattern, default_kind) pairs. The
|
|
361
|
+
# type-level keywords allow a little indentation because C#/Java/PHP types
|
|
362
|
+
# usually sit inside a namespace/class body; the slicer below cuts from one
|
|
363
|
+
# match to the next, so a class artifact naturally spans its methods.
|
|
364
|
+
_TYPE_MODS = (
|
|
365
|
+
r"(?:(?:public|private|protected|internal|open|abstract|sealed|static"
|
|
366
|
+
r"|final|partial|data|inline|suspend|operator|export|readonly)\s+)*"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
_REGEX_DEF_PATTERNS: dict[str, list[tuple[re.Pattern, str]]] = {
|
|
370
|
+
"python": [(_PY_DEF_RE, "function")],
|
|
371
|
+
"javascript": [(_JS_DEF_RE, "function")],
|
|
372
|
+
"typescript": [(_JS_DEF_RE, "function")],
|
|
373
|
+
"tsx": [(_JS_DEF_RE, "function")],
|
|
374
|
+
"java": [
|
|
375
|
+
(
|
|
376
|
+
re.compile(
|
|
377
|
+
r"^(?:@\w+(?:\([^)]*\))?[ \t]*\n)*[ \t]{0,8}"
|
|
378
|
+
+ _TYPE_MODS
|
|
379
|
+
+ r"(?P<kw>class|interface|enum|record)\s+(?P<name>\w+)",
|
|
380
|
+
re.MULTILINE,
|
|
381
|
+
),
|
|
382
|
+
"class",
|
|
383
|
+
),
|
|
384
|
+
],
|
|
385
|
+
"kotlin": [
|
|
386
|
+
(
|
|
387
|
+
re.compile(
|
|
388
|
+
r"^[ \t]{0,8}(?:@\w+[ \t]*)*"
|
|
389
|
+
+ _TYPE_MODS
|
|
390
|
+
+ r"(?P<kw>fun|class|object|interface)\s+(?P<name>`?\w+`?)",
|
|
391
|
+
re.MULTILINE,
|
|
392
|
+
),
|
|
393
|
+
"function",
|
|
394
|
+
),
|
|
395
|
+
],
|
|
396
|
+
"go": [
|
|
397
|
+
(
|
|
398
|
+
re.compile(r"^func\s+(?:\([^)]*\)\s*)?(?P<name>\w+)", re.MULTILINE),
|
|
399
|
+
"function",
|
|
400
|
+
),
|
|
401
|
+
(
|
|
402
|
+
re.compile(r"^type\s+(?P<name>\w+)\s+(?:struct|interface)\b", re.MULTILINE),
|
|
403
|
+
"class",
|
|
404
|
+
),
|
|
405
|
+
],
|
|
406
|
+
"rust": [
|
|
407
|
+
(
|
|
408
|
+
re.compile(
|
|
409
|
+
r"^[ \t]{0,4}(?:pub(?:\([^)]*\))?\s+)?"
|
|
410
|
+
r"(?:(?:async|unsafe|const|extern)\s+)*"
|
|
411
|
+
r"(?P<kw>fn|struct|enum|trait|impl)\s+(?P<name>\w+)",
|
|
412
|
+
re.MULTILINE,
|
|
413
|
+
),
|
|
414
|
+
"function",
|
|
415
|
+
),
|
|
416
|
+
],
|
|
417
|
+
"c": [
|
|
418
|
+
(
|
|
419
|
+
re.compile(
|
|
420
|
+
r"^(?P<kw>struct|enum|union)\s+(?P<name>\w+)\s*\{", re.MULTILINE
|
|
421
|
+
),
|
|
422
|
+
"class",
|
|
423
|
+
),
|
|
424
|
+
(
|
|
425
|
+
# Column-0 "returntype name(" — top-level function definitions.
|
|
426
|
+
r"^(?!\s)(?:[A-Za-z_][\w \t\*]*[ \t\*])(?P<name>[A-Za-z_]\w*)\s*\(",
|
|
427
|
+
"function",
|
|
428
|
+
),
|
|
429
|
+
],
|
|
430
|
+
"cpp": [
|
|
431
|
+
(
|
|
432
|
+
re.compile(
|
|
433
|
+
r"^[ \t]{0,8}(?:template\s*<[^>\n]*>[ \t]*\n)?"
|
|
434
|
+
r"(?P<kw>class|struct|namespace)\s+(?P<name>\w+)",
|
|
435
|
+
re.MULTILINE,
|
|
436
|
+
),
|
|
437
|
+
"class",
|
|
438
|
+
),
|
|
439
|
+
(
|
|
440
|
+
r"^(?!\s)(?:[A-Za-z_][\w:<> \t\*&]*[ \t\*&])(?P<name>[A-Za-z_]\w*)\s*\(",
|
|
441
|
+
"function",
|
|
442
|
+
),
|
|
443
|
+
],
|
|
444
|
+
"csharp": [
|
|
445
|
+
(
|
|
446
|
+
re.compile(
|
|
447
|
+
r"^[ \t]{0,8}(?:\[[^\]\n]+\][ \t]*\n)*[ \t]{0,8}"
|
|
448
|
+
+ _TYPE_MODS
|
|
449
|
+
+ r"(?P<kw>class|interface|struct|enum|record)\s+(?P<name>\w+)",
|
|
450
|
+
re.MULTILINE,
|
|
451
|
+
),
|
|
452
|
+
"class",
|
|
453
|
+
),
|
|
454
|
+
],
|
|
455
|
+
"php": [
|
|
456
|
+
(
|
|
457
|
+
re.compile(
|
|
458
|
+
r"^[ \t]{0,4}(?:(?:final|abstract)\s+)?"
|
|
459
|
+
r"(?P<kw>class|interface|trait|function)\s+(?P<name>\w+)",
|
|
460
|
+
re.MULTILINE,
|
|
461
|
+
),
|
|
462
|
+
"function",
|
|
463
|
+
),
|
|
464
|
+
],
|
|
465
|
+
"ruby": [
|
|
466
|
+
(
|
|
467
|
+
re.compile(
|
|
468
|
+
r"^[ \t]{0,8}(?P<kw>class|module|def)\s+(?P<name>[\w.]+[?!]?)",
|
|
469
|
+
re.MULTILINE,
|
|
470
|
+
),
|
|
471
|
+
"function",
|
|
472
|
+
),
|
|
473
|
+
],
|
|
474
|
+
"swift": [
|
|
475
|
+
(
|
|
476
|
+
re.compile(
|
|
477
|
+
r"^[ \t]{0,8}"
|
|
478
|
+
+ _TYPE_MODS
|
|
479
|
+
+ r"(?P<kw>func|class|struct|enum|protocol|extension)"
|
|
480
|
+
+ r"\s+(?P<name>\w+)",
|
|
481
|
+
re.MULTILINE,
|
|
482
|
+
),
|
|
483
|
+
"function",
|
|
484
|
+
),
|
|
485
|
+
],
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# Compile the string patterns left raw above (kept raw for line length).
|
|
489
|
+
_REGEX_DEF_PATTERNS = {
|
|
490
|
+
lang: [
|
|
491
|
+
(re.compile(p, re.MULTILINE) if isinstance(p, str) else p, kind)
|
|
492
|
+
for p, kind in specs
|
|
493
|
+
]
|
|
494
|
+
for lang, specs in _REGEX_DEF_PATTERNS.items()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _regex_code_artifacts(
|
|
499
|
+
rel_path: str, language: str, text: str
|
|
500
|
+
) -> list[tuple[str, str, int, int, str]]:
|
|
501
|
+
"""Regex-based fallback that never crashes. Less precise than tree-sitter
|
|
502
|
+
but catches top-level definitions reliably — and it's the ONLY code path
|
|
503
|
+
on Windows, where tree-sitter is disabled."""
|
|
504
|
+
specs = _REGEX_DEF_PATTERNS.get(language)
|
|
505
|
+
if not specs:
|
|
506
|
+
return []
|
|
507
|
+
|
|
508
|
+
found: dict[int, tuple[str, str]] = {} # start_pos -> (kind, name)
|
|
509
|
+
for pattern, default_kind in specs:
|
|
510
|
+
for m in pattern.finditer(text):
|
|
511
|
+
kw = (m.groupdict().get("kw") or "").strip()
|
|
512
|
+
kind = _KIND_MAP.get(kw, default_kind)
|
|
513
|
+
found.setdefault(m.start(), (kind, m.group("name")))
|
|
514
|
+
|
|
515
|
+
if not found:
|
|
516
|
+
return []
|
|
517
|
+
|
|
518
|
+
starts = sorted(found)
|
|
519
|
+
results = []
|
|
520
|
+
for i, start in enumerate(starts):
|
|
521
|
+
kind, name = found[start]
|
|
522
|
+
end_pos = starts[i + 1] if i + 1 < len(starts) else len(text)
|
|
523
|
+
start_line = text[:start].count("\n") + 1
|
|
524
|
+
end_line = text[:end_pos].count("\n") + 1
|
|
525
|
+
code = text[start:end_pos].rstrip()
|
|
526
|
+
results.append((kind, name, start_line, end_line, code))
|
|
527
|
+
return results
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
import platform as _platform # noqa: E402 (deliberately after the fallback defs)
|
|
531
|
+
|
|
532
|
+
_USE_TREE_SITTER = _platform.system() != "Windows"
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _code_artifacts(
|
|
536
|
+
rel_path: str, language: str, text: str
|
|
537
|
+
) -> list[tuple[str, str, int, int, str]]:
|
|
538
|
+
"""Extract code artifacts. Uses tree-sitter on Linux/macOS, regex on
|
|
539
|
+
Windows (tree-sitter-language-pack has native crashes on Windows when
|
|
540
|
+
parsing many files sequentially)."""
|
|
541
|
+
if _USE_TREE_SITTER:
|
|
542
|
+
try:
|
|
543
|
+
return _ts_code_artifacts(rel_path, language, text)
|
|
544
|
+
except Exception:
|
|
545
|
+
pass
|
|
546
|
+
return _regex_code_artifacts(rel_path, language, text)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _fallback_artifact(rel_path: str, text: str) -> tuple[str, str, int, int, str]:
|
|
550
|
+
lines = text.splitlines()
|
|
551
|
+
|
|
552
|
+
# Skip the contiguous import/package block at the file head — in most
|
|
553
|
+
# compiled languages the first 30 lines are boilerplate, and an excerpt
|
|
554
|
+
# of boilerplate produces "Unsupported" verdicts for the wrong reason.
|
|
555
|
+
start = 0
|
|
556
|
+
for i, line in enumerate(lines[:80]):
|
|
557
|
+
if not _BOILERPLATE_LINE_RE.match(line):
|
|
558
|
+
start = i
|
|
559
|
+
break
|
|
560
|
+
else:
|
|
561
|
+
start = 0 if len(lines) <= 80 else 80
|
|
562
|
+
|
|
563
|
+
end = min(len(lines), start + _FALLBACK_MAX_LINES)
|
|
564
|
+
excerpt = "\n".join(lines[start:end])
|
|
565
|
+
name = rel_path.rsplit("/", 1)[-1]
|
|
566
|
+
return ("file", name, start + 1, max(end, 1), excerpt)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _top_dir(rel_path: str) -> str:
|
|
570
|
+
"""Top-level directory of a repo-relative path ('.' for root files)."""
|
|
571
|
+
return rel_path.split("/", 1)[0] if "/" in rel_path else "."
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def extract_artifacts(
|
|
575
|
+
root: Path,
|
|
576
|
+
files: Sequence[scanner.ScannedFile],
|
|
577
|
+
*,
|
|
578
|
+
token_budget: int | None = None,
|
|
579
|
+
count_tokens: Callable[[str], int] | None = None,
|
|
580
|
+
) -> ExtractionResult:
|
|
581
|
+
"""Extract artifacts from text files within a token budget.
|
|
582
|
+
|
|
583
|
+
``token_budget=None`` auto-scales: the whole corpus is kept up to a
|
|
584
|
+
1M-token ceiling. When the corpus exceeds the budget, artifacts are
|
|
585
|
+
allocated by importance instead of directory-walk order: parsed code
|
|
586
|
+
(functions/classes) beats whole-file excerpts, and the budget is spread
|
|
587
|
+
round-robin across top-level directories so one big folder can't starve
|
|
588
|
+
the rest. Drops are counted per directory — never silent.
|
|
589
|
+
"""
|
|
590
|
+
if count_tokens is None:
|
|
591
|
+
from receipts.llm.token_estimate import estimate_tokens
|
|
592
|
+
|
|
593
|
+
count_tokens = estimate_tokens
|
|
594
|
+
|
|
595
|
+
# Phase 1 — collect every candidate artifact (no budget gate yet).
|
|
596
|
+
candidates: list[Artifact] = []
|
|
597
|
+
redaction_count = 0
|
|
598
|
+
for file in files:
|
|
599
|
+
if not file.is_text:
|
|
600
|
+
continue
|
|
601
|
+
text = scanner.read_text(root, file.rel_path)
|
|
602
|
+
if not text.strip():
|
|
603
|
+
continue
|
|
604
|
+
|
|
605
|
+
suffix = "." + file.rel_path.rsplit(".", 1)[-1] if "." in file.rel_path else ""
|
|
606
|
+
language = _EXTENSION_LANGUAGES.get(suffix)
|
|
607
|
+
raw: list[tuple[str, str, int, int, str]] = []
|
|
608
|
+
if language is not None:
|
|
609
|
+
try:
|
|
610
|
+
raw = _code_artifacts(file.rel_path, language, text)
|
|
611
|
+
except Exception:
|
|
612
|
+
raw = []
|
|
613
|
+
if not raw:
|
|
614
|
+
# Unsupported language, parse failure, or a script with no
|
|
615
|
+
# top-level defs — keep an excerpt rather than dropping the file.
|
|
616
|
+
raw = [_fallback_artifact(file.rel_path, text)]
|
|
617
|
+
language = language or "text"
|
|
618
|
+
|
|
619
|
+
for kind, name, start_line, end_line, code in raw:
|
|
620
|
+
clean_code, redactions = redact(code)
|
|
621
|
+
redaction_count += redactions
|
|
622
|
+
candidates.append(
|
|
623
|
+
Artifact(
|
|
624
|
+
rel_path=file.rel_path,
|
|
625
|
+
language=language,
|
|
626
|
+
kind=kind,
|
|
627
|
+
name=name,
|
|
628
|
+
start_line=start_line,
|
|
629
|
+
end_line=end_line,
|
|
630
|
+
code=clean_code,
|
|
631
|
+
token_estimate=count_tokens(clean_code),
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
corpus_tokens = sum(a.token_estimate for a in candidates)
|
|
636
|
+
auto = token_budget is None
|
|
637
|
+
budget = min(corpus_tokens, _AUTO_BUDGET_CEILING) if auto else token_budget
|
|
638
|
+
|
|
639
|
+
result = ExtractionResult(
|
|
640
|
+
budget=budget,
|
|
641
|
+
auto_budget=auto,
|
|
642
|
+
corpus_tokens=corpus_tokens,
|
|
643
|
+
redaction_count=redaction_count,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if corpus_tokens <= budget:
|
|
647
|
+
result.artifacts = candidates
|
|
648
|
+
result.used_tokens = corpus_tokens
|
|
649
|
+
return result
|
|
650
|
+
|
|
651
|
+
# Phase 2 — importance allocation. Tier 0: parsed code artifacts.
|
|
652
|
+
# Tier 1: whole-file excerpts (config, docs, unsupported languages).
|
|
653
|
+
# Within a tier, round-robin across top-level directories.
|
|
654
|
+
queues: dict[tuple[int, str], deque[Artifact]] = {}
|
|
655
|
+
dir_order: dict[int, list[str]] = {0: [], 1: []}
|
|
656
|
+
for a in candidates:
|
|
657
|
+
tier = 0 if a.kind != "file" else 1
|
|
658
|
+
key = (tier, _top_dir(a.rel_path))
|
|
659
|
+
if key not in queues:
|
|
660
|
+
queues[key] = deque()
|
|
661
|
+
dir_order[tier].append(_top_dir(a.rel_path))
|
|
662
|
+
queues[key].append(a)
|
|
663
|
+
|
|
664
|
+
for tier in (0, 1):
|
|
665
|
+
active = [queues[(tier, d)] for d in dir_order[tier]]
|
|
666
|
+
while any(active):
|
|
667
|
+
for q in active:
|
|
668
|
+
if not q:
|
|
669
|
+
continue
|
|
670
|
+
a = q.popleft()
|
|
671
|
+
if result.used_tokens + a.token_estimate > budget:
|
|
672
|
+
result.dropped_over_budget += 1
|
|
673
|
+
result.dropped_tokens += a.token_estimate
|
|
674
|
+
d = _top_dir(a.rel_path)
|
|
675
|
+
result.dropped_by_dir[d] = result.dropped_by_dir.get(d, 0) + 1
|
|
676
|
+
continue
|
|
677
|
+
result.used_tokens += a.token_estimate
|
|
678
|
+
result.artifacts.append(a)
|
|
679
|
+
return result
|