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,140 @@
1
+ """Score-aware rewrite optimization — never trade honesty for score.
2
+
3
+ After the honest rewriter runs, each rewrite is checked for ATS-dimension
4
+ regressions the rewrite itself introduced: losing every concrete number
5
+ the original had (quantification) or losing a strong opening verb
6
+ (action verbs). Offending bullets get ONE constrained re-prompt that pins
7
+ the regression and supplies the provable numbers the model may use. If
8
+ the retry still regresses — or weakens honesty in any way — the first
9
+ honest rewrite is kept. Max one retry per bullet, and the loop reports
10
+ what happened instead of hiding it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+
17
+ from receipts.ats.scorer import _NUMBER_RE, _STRONG_VERBS
18
+ from receipts.llm.provider import LLMProvider
19
+ from receipts.verify.rewriter import RewriteResult, rewrite_bullet
20
+ from receipts.verify.verifier import VerificationResult
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class OptimizedRewrite:
25
+ result: RewriteResult
26
+ regressions_found: list[str] = field(default_factory=list)
27
+ retried: bool = False
28
+ retry_fixed: bool = False
29
+
30
+
31
+ def _has_number(text: str) -> bool:
32
+ return bool(_NUMBER_RE.search(text))
33
+
34
+
35
+ def _starts_with_strong_verb(text: str) -> bool:
36
+ words = text.strip().split()
37
+ if not words:
38
+ return False
39
+ return words[0].lower().rstrip(",.:;") in _STRONG_VERBS
40
+
41
+
42
+ def check_regressions(rewrite: RewriteResult) -> list[str]:
43
+ """ATS dimensions where the rewrite is measurably weaker than the original."""
44
+ if rewrite.original == rewrite.rewritten:
45
+ return []
46
+
47
+ regressions: list[str] = []
48
+ if _has_number(rewrite.original) and not _has_number(rewrite.rewritten):
49
+ regressions.append("quantification")
50
+ if _starts_with_strong_verb(rewrite.original) and not _starts_with_strong_verb(
51
+ rewrite.rewritten
52
+ ):
53
+ regressions.append("action_verbs")
54
+ return regressions
55
+
56
+
57
+ def _retry_constraint(regressions: list[str], grounded_facts: str | None) -> str:
58
+ parts = ["Your previous rewrite regressed on ATS dimensions:"]
59
+ if "quantification" in regressions:
60
+ if grounded_facts:
61
+ parts.append(
62
+ "- It lost all concrete numbers. Include at least one number,"
63
+ " but ONLY from the provable facts list or verified claims."
64
+ )
65
+ else:
66
+ parts.append(
67
+ "- It lost all concrete numbers. Keep any number that was"
68
+ " part of a verified claim; do not invent new ones."
69
+ )
70
+ if "action_verbs" in regressions:
71
+ parts.append(
72
+ "- It no longer starts with a strong action verb"
73
+ " (architected, built, engineered, optimized, ...). Fix that."
74
+ )
75
+ parts.append("Honesty rules still apply and always win over score.")
76
+ return "\n".join(parts)
77
+
78
+
79
+ def optimize_rewrites(
80
+ rewrites: list[RewriteResult],
81
+ results_by_bullet: dict[str, list[VerificationResult]],
82
+ provider: LLMProvider,
83
+ *,
84
+ grounded_facts: str | None = None,
85
+ addable_keywords: list[str] | None = None,
86
+ ) -> list[OptimizedRewrite]:
87
+ """Check each rewrite for ATS regressions; one constrained retry each.
88
+
89
+ The retry is accepted only if it clears the regressions it was asked to
90
+ fix — otherwise the original honest rewrite stands.
91
+ """
92
+ optimized: list[OptimizedRewrite] = []
93
+
94
+ for rw in rewrites:
95
+ regressions = check_regressions(rw)
96
+ if not regressions:
97
+ optimized.append(OptimizedRewrite(result=rw))
98
+ continue
99
+
100
+ bullet_results = results_by_bullet.get(rw.original, [])
101
+ retry = rewrite_bullet(
102
+ rw.original,
103
+ bullet_results,
104
+ provider,
105
+ grounded_facts=grounded_facts,
106
+ addable_keywords=addable_keywords,
107
+ extra_constraint=_retry_constraint(regressions, grounded_facts),
108
+ original_raw=getattr(rw, "original_raw", ""),
109
+ )
110
+
111
+ still_regressed = set(check_regressions(retry)) & set(regressions)
112
+ if retry.rewritten != rw.original and not still_regressed:
113
+ optimized.append(
114
+ OptimizedRewrite(
115
+ result=retry,
116
+ regressions_found=regressions,
117
+ retried=True,
118
+ retry_fixed=True,
119
+ )
120
+ )
121
+ else:
122
+ optimized.append(
123
+ OptimizedRewrite(
124
+ result=rw,
125
+ regressions_found=regressions,
126
+ retried=True,
127
+ retry_fixed=False,
128
+ )
129
+ )
130
+
131
+ return optimized
132
+
133
+
134
+ def group_results_by_bullet(
135
+ results: list[VerificationResult],
136
+ ) -> dict[str, list[VerificationResult]]:
137
+ by_bullet: dict[str, list[VerificationResult]] = {}
138
+ for r in results:
139
+ by_bullet.setdefault(r.claim.bullet_text, []).append(r)
140
+ return by_bullet
@@ -0,0 +1,227 @@
1
+ """Surgical TeX bullet replacement.
2
+
3
+ Copies the original .tex file verbatim and replaces only the bullet
4
+ points that have rewrites — everything else (formatting, macros,
5
+ headers, education, skills) is preserved exactly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from receipts.resume.tex_parser import clean_tex
15
+ from receipts.verify.rewriter import RewriteResult
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class TexRewriteReport:
20
+ original_path: Path
21
+ output_path: Path
22
+ replacements: int
23
+ skipped: int
24
+
25
+
26
+ def _normalize(text: str) -> str:
27
+ """Collapse whitespace for fuzzy matching."""
28
+ return re.sub(r"\s+", " ", text).strip().lower()
29
+
30
+
31
+ def _find_item_spans(tex: str) -> list[tuple[int, int, str]]:
32
+ r"""Find all \item blocks in the TeX source.
33
+
34
+ Returns (start, end, cleaned_text) for each \item.
35
+ The span covers from ``\item`` to just before the next ``\item``,
36
+ ``\end{highlights}``, or ``\end{``.
37
+ """
38
+ pattern = re.compile(r"\\item\s+", re.DOTALL)
39
+ matches = list(pattern.finditer(tex))
40
+ spans: list[tuple[int, int, str]] = []
41
+
42
+ for i, m in enumerate(matches):
43
+ start = m.start()
44
+ if i + 1 < len(matches):
45
+ end = matches[i + 1].start()
46
+ else:
47
+ end_hl = tex.find(r"\end{highlights}", m.end())
48
+ end_env = tex.find(r"\end{", m.end())
49
+ if end_hl != -1:
50
+ end = end_hl
51
+ elif end_env != -1:
52
+ end = end_env
53
+ else:
54
+ end = len(tex)
55
+
56
+ raw = tex[m.end() : end].strip()
57
+ cleaned = clean_tex(raw)
58
+ spans.append((start, end, cleaned))
59
+
60
+ return spans
61
+
62
+
63
+ def _match_rewrite(
64
+ cleaned_item: str, rewrites: list[RewriteResult]
65
+ ) -> RewriteResult | None:
66
+ """Find the rewrite whose original matches this item's cleaned text."""
67
+ norm_item = _normalize(cleaned_item)
68
+ for rw in rewrites:
69
+ if rw.original == rw.rewritten:
70
+ continue
71
+ norm_orig = _normalize(rw.original)
72
+ if norm_orig == norm_item:
73
+ return rw
74
+ if len(norm_orig) > 20 and norm_orig in norm_item:
75
+ return rw
76
+ if len(norm_item) > 20 and norm_item in norm_orig:
77
+ return rw
78
+ return None
79
+
80
+
81
+ def _plain_to_tex_bullet(text: str) -> str:
82
+ """Convert a plain-text rewritten bullet into minimal LaTeX.
83
+
84
+ Wraps technology names and numbers in \\textbf{} to match typical
85
+ resume styling. Escapes LaTeX-special characters.
86
+ """
87
+ s = text
88
+ s = s.replace("&", "\\&")
89
+ s = s.replace("%", "\\%")
90
+ s = s.replace("#", "\\#")
91
+ s = s.replace("~", "\\textasciitilde{}")
92
+ return s
93
+
94
+
95
+ def rewrite_tex(
96
+ original_path: Path,
97
+ rewrites: list[RewriteResult],
98
+ output_path: Path | None = None,
99
+ *,
100
+ section_filter: str | None = None,
101
+ ) -> TexRewriteReport:
102
+ """Copy the .tex file and surgically replace rewritten bullets.
103
+
104
+ Parameters
105
+ ----------
106
+ original_path:
107
+ Path to the original .tex resume file.
108
+ rewrites:
109
+ List of RewriteResult from the verification pipeline.
110
+ output_path:
111
+ Where to write the modified .tex. Defaults to
112
+ ``<original_stem>_optimized.tex`` next to the original.
113
+ section_filter:
114
+ If given, only replace bullets inside \\section{} blocks whose
115
+ name contains this substring (case-insensitive). Useful for
116
+ limiting changes to just the Projects section.
117
+ """
118
+ if output_path is None:
119
+ output_path = original_path.with_stem(original_path.stem + "_optimized")
120
+
121
+ tex = original_path.read_text(encoding="utf-8")
122
+ actionable = [rw for rw in rewrites if rw.original != rw.rewritten]
123
+
124
+ if not actionable:
125
+ output_path.write_text(tex, encoding="utf-8")
126
+ return TexRewriteReport(
127
+ original_path=original_path,
128
+ output_path=output_path,
129
+ replacements=0,
130
+ skipped=0,
131
+ )
132
+
133
+ if section_filter:
134
+ section_ranges = _section_ranges(tex, section_filter)
135
+ else:
136
+ section_ranges = None
137
+
138
+ replacements: list[tuple[int, int, str]] = []
139
+ matched_rewrites: set[int] = set()
140
+
141
+ def _in_section(pos: int) -> bool:
142
+ if section_ranges is None:
143
+ return True
144
+ return any(s <= pos < e for s, e in section_ranges)
145
+
146
+ def _overlaps_claimed(start: int, end: int) -> bool:
147
+ return any(start < e and s < end for s, e, _ in replacements)
148
+
149
+ # Pass 1 — replace by identity: the bullet's raw TeX (carried through
150
+ # the claim → verification → rewrite pipeline) is a verbatim substring
151
+ # of this file, so an exact find pinpoints it. This also covers bullets
152
+ # that aren't \item lines at all (e.g. onecolentry skills rows).
153
+ for rw in actionable:
154
+ raw = getattr(rw, "original_raw", "")
155
+ if not raw or len(raw.strip()) < 10:
156
+ continue
157
+ idx = tex.find(raw)
158
+ if idx == -1 or tex.find(raw, idx + 1) != -1:
159
+ continue # absent or ambiguous — leave it to the fuzzy pass
160
+ if not _in_section(idx) or _overlaps_claimed(idx, idx + len(raw)):
161
+ continue
162
+ matched_rewrites.add(id(rw))
163
+ replacements.append((idx, idx + len(raw), _plain_to_tex_bullet(rw.rewritten)))
164
+
165
+ # Pass 2 — legacy fuzzy match over \item spans for anything left
166
+ # (externally supplied rewrites, or raw text that didn't resolve).
167
+ spans = _find_item_spans(tex)
168
+ remaining = [rw for rw in actionable if id(rw) not in matched_rewrites]
169
+
170
+ for start, end, cleaned in spans:
171
+ if not _in_section(start) or _overlaps_claimed(start, end):
172
+ continue
173
+
174
+ rw = _match_rewrite(cleaned, remaining)
175
+ if rw is not None:
176
+ rw_idx = id(rw)
177
+ if rw_idx not in matched_rewrites:
178
+ matched_rewrites.add(rw_idx)
179
+ new_item = f"\\item {_plain_to_tex_bullet(rw.rewritten)}"
180
+ ws_after = ""
181
+ raw_after = tex[end - 1 : end]
182
+ if raw_after == "\n":
183
+ ws_after = "\n"
184
+ replacements.append((start, end, new_item + ws_after))
185
+
186
+ result_tex = tex
187
+ for start, end, replacement in sorted(replacements, reverse=True):
188
+ result_tex = result_tex[:start] + replacement + result_tex[end:]
189
+
190
+ output_path.write_text(result_tex, encoding="utf-8")
191
+
192
+ return TexRewriteReport(
193
+ original_path=original_path,
194
+ output_path=output_path,
195
+ replacements=len(replacements),
196
+ skipped=len(actionable) - len(replacements),
197
+ )
198
+
199
+
200
+ def _section_ranges(tex: str, filter_str: str) -> list[tuple[int, int]]:
201
+ r"""Find byte ranges of \section{} blocks matching the filter."""
202
+ section_re = re.compile(r"\\section\{", re.DOTALL)
203
+ hits = list(section_re.finditer(tex))
204
+ ranges: list[tuple[int, int]] = []
205
+
206
+ lower_filter = filter_str.lower()
207
+
208
+ for idx, m in enumerate(hits):
209
+ brace_start = m.start() + len("\\section")
210
+ depth = 0
211
+ i = brace_start
212
+ while i < len(tex):
213
+ if tex[i] == "{":
214
+ depth += 1
215
+ elif tex[i] == "}":
216
+ depth -= 1
217
+ if depth == 0:
218
+ break
219
+ i += 1
220
+ section_name = clean_tex(tex[brace_start + 1 : i])
221
+
222
+ if lower_filter in section_name.lower():
223
+ sec_start = m.start()
224
+ sec_end = hits[idx + 1].start() if idx + 1 < len(hits) else len(tex)
225
+ ranges.append((sec_start, sec_end))
226
+
227
+ return ranges
File without changes