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.
Files changed (78) hide show
  1. codebase_receipts_cli-1.0.0.dist-info/METADATA +268 -0
  2. codebase_receipts_cli-1.0.0.dist-info/RECORD +78 -0
  3. codebase_receipts_cli-1.0.0.dist-info/WHEEL +4 -0
  4. codebase_receipts_cli-1.0.0.dist-info/entry_points.txt +2 -0
  5. codebase_receipts_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. receipts/__init__.py +0 -0
  7. receipts/ama/__init__.py +0 -0
  8. receipts/ama/interviewer.py +415 -0
  9. receipts/ats/__init__.py +1 -0
  10. receipts/ats/comparator.py +98 -0
  11. receipts/ats/scorer.py +550 -0
  12. receipts/ats/stats.py +164 -0
  13. receipts/cli.py +2062 -0
  14. receipts/config.py +106 -0
  15. receipts/errors.py +18 -0
  16. receipts/export.py +120 -0
  17. receipts/ingest/__init__.py +0 -0
  18. receipts/ingest/artifact_extractor.py +679 -0
  19. receipts/ingest/git_source.py +159 -0
  20. receipts/ingest/manifest.py +114 -0
  21. receipts/ingest/scanner.py +141 -0
  22. receipts/ingest/secrets_scanner.py +225 -0
  23. receipts/interactive/__init__.py +1 -0
  24. receipts/interactive/repl.py +755 -0
  25. receipts/ledger/__init__.py +0 -0
  26. receipts/ledger/pricing_table.py +54 -0
  27. receipts/ledger/token_ledger.py +226 -0
  28. receipts/llm/__init__.py +0 -0
  29. receipts/llm/anthropic_provider.py +95 -0
  30. receipts/llm/factory.py +124 -0
  31. receipts/llm/fake_provider.py +79 -0
  32. receipts/llm/gemini_provider.py +205 -0
  33. receipts/llm/json_utils.py +46 -0
  34. receipts/llm/metered.py +62 -0
  35. receipts/llm/ollama_provider.py +117 -0
  36. receipts/llm/openai_provider.py +118 -0
  37. receipts/llm/provider.py +42 -0
  38. receipts/llm/split.py +78 -0
  39. receipts/llm/token_estimate.py +35 -0
  40. receipts/mine/__init__.py +1 -0
  41. receipts/mine/code_metrics.py +246 -0
  42. receipts/mine/git_evidence.py +109 -0
  43. receipts/prep/__init__.py +1 -0
  44. receipts/prep/dossier.py +313 -0
  45. receipts/prep/readiness.py +227 -0
  46. receipts/resume/__init__.py +0 -0
  47. receipts/resume/claim_extractor.py +338 -0
  48. receipts/resume/loader.py +35 -0
  49. receipts/resume/pdf_parser.py +259 -0
  50. receipts/resume/tex_parser.py +394 -0
  51. receipts/rewrite/__init__.py +0 -0
  52. receipts/rewrite/compiler.py +180 -0
  53. receipts/rewrite/optimizer.py +140 -0
  54. receipts/rewrite/tex_rewriter.py +227 -0
  55. receipts/tui/__init__.py +0 -0
  56. receipts/tui/ama_app.py +454 -0
  57. receipts/tui/app.py +316 -0
  58. receipts/tui/dossier_app.py +170 -0
  59. receipts/tui/hub.py +367 -0
  60. receipts/tui/ingest_app.py +183 -0
  61. receipts/tui/rewrite_app.py +463 -0
  62. receipts/tui/score_app.py +237 -0
  63. receipts/tui/widgets/__init__.py +0 -0
  64. receipts/tui/widgets/claims_table.py +50 -0
  65. receipts/tui/widgets/diff_view.py +38 -0
  66. receipts/tui/widgets/status_bar.py +38 -0
  67. receipts/verify/__init__.py +0 -0
  68. receipts/verify/bm25.py +112 -0
  69. receipts/verify/enrich.py +128 -0
  70. receipts/verify/jd_fetch.py +101 -0
  71. receipts/verify/kb.py +379 -0
  72. receipts/verify/keyword_gap.py +127 -0
  73. receipts/verify/query_expand.py +88 -0
  74. receipts/verify/rerank.py +74 -0
  75. receipts/verify/rewriter.py +172 -0
  76. receipts/verify/router.py +211 -0
  77. receipts/verify/summaries.py +258 -0
  78. 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