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,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
|
receipts/tui/__init__.py
ADDED
|
File without changes
|