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
receipts/tui/app.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Textual TUI for the receipts verify flow.
|
|
2
|
+
|
|
3
|
+
Layout: left tree pane (ingested files), right tabbed content
|
|
4
|
+
(claims table, rewrites diff, activity log), persistent status bar.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from textual.app import App, ComposeResult
|
|
12
|
+
from textual.containers import Horizontal
|
|
13
|
+
from textual.widgets import (
|
|
14
|
+
Footer,
|
|
15
|
+
Header,
|
|
16
|
+
RichLog,
|
|
17
|
+
TabbedContent,
|
|
18
|
+
TabPane,
|
|
19
|
+
Tree,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from receipts.tui.widgets.claims_table import ClaimsTable
|
|
23
|
+
from receipts.tui.widgets.diff_view import DiffView
|
|
24
|
+
from receipts.tui.widgets.status_bar import StatusBar
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReceiptsApp(App):
|
|
28
|
+
"""Panelled TUI for resume claim verification."""
|
|
29
|
+
|
|
30
|
+
TITLE = "Receipts"
|
|
31
|
+
SUB_TITLE = "resume claim verifier"
|
|
32
|
+
|
|
33
|
+
CSS = """
|
|
34
|
+
#tree-pane {
|
|
35
|
+
width: 30;
|
|
36
|
+
dock: left;
|
|
37
|
+
border-right: solid $primary;
|
|
38
|
+
overflow-y: auto;
|
|
39
|
+
}
|
|
40
|
+
#main-area {
|
|
41
|
+
width: 1fr;
|
|
42
|
+
}
|
|
43
|
+
#activity-log {
|
|
44
|
+
height: 1fr;
|
|
45
|
+
}
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
BINDINGS = [
|
|
49
|
+
("q", "quit", "Quit"),
|
|
50
|
+
("c", "show_tab('claims-pane')", "Claims"),
|
|
51
|
+
("r", "show_tab('rewrites-pane')", "Rewrites"),
|
|
52
|
+
("l", "show_tab('log-pane')", "Log"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
resume_path: Path,
|
|
58
|
+
source_path: Path,
|
|
59
|
+
*,
|
|
60
|
+
threshold: float = 0.5,
|
|
61
|
+
sections: list[str] | None = None,
|
|
62
|
+
entries: list[str] | None = None,
|
|
63
|
+
kb_dir: Path | None = None,
|
|
64
|
+
**kwargs,
|
|
65
|
+
) -> None:
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
self.resume_path = resume_path
|
|
68
|
+
self.source_path = source_path
|
|
69
|
+
self.threshold = threshold
|
|
70
|
+
self.sections = sections
|
|
71
|
+
self.entries = entries
|
|
72
|
+
# Pre-resolved KB dir (git URLs / manifest aliases); None → derive
|
|
73
|
+
# from source_path like always.
|
|
74
|
+
self.kb_dir = kb_dir
|
|
75
|
+
|
|
76
|
+
def compose(self) -> ComposeResult:
|
|
77
|
+
yield Header()
|
|
78
|
+
with Horizontal():
|
|
79
|
+
yield Tree("Files", id="tree-pane")
|
|
80
|
+
with TabbedContent(id="main-area"):
|
|
81
|
+
with TabPane("Claims", id="claims-pane"):
|
|
82
|
+
yield ClaimsTable(id="claims-table")
|
|
83
|
+
with TabPane("Rewrites", id="rewrites-pane"):
|
|
84
|
+
yield DiffView(id="diff-view")
|
|
85
|
+
with TabPane("Log", id="log-pane"):
|
|
86
|
+
yield RichLog(id="activity-log", markup=True, wrap=True)
|
|
87
|
+
yield StatusBar(id="status-bar")
|
|
88
|
+
yield Footer()
|
|
89
|
+
|
|
90
|
+
def on_mount(self) -> None:
|
|
91
|
+
self.run_worker(self._run_verify, thread=True)
|
|
92
|
+
|
|
93
|
+
async def _run_verify(self) -> None:
|
|
94
|
+
log = self.query_one("#activity-log", RichLog)
|
|
95
|
+
status = self.query_one("#status-bar", StatusBar)
|
|
96
|
+
table = self.query_one("#claims-table", ClaimsTable)
|
|
97
|
+
diff = self.query_one("#diff-view", DiffView)
|
|
98
|
+
tree = self.query_one("#tree-pane", Tree)
|
|
99
|
+
|
|
100
|
+
log.write("[bold]Starting verification...[/bold]")
|
|
101
|
+
|
|
102
|
+
from receipts.config import load_settings
|
|
103
|
+
from receipts.llm.factory import get_provider
|
|
104
|
+
from receipts.resume.claim_extractor import extract_claims
|
|
105
|
+
from receipts.resume.loader import load_resume
|
|
106
|
+
from receipts.verify.kb import KnowledgeBase, default_kb_dir
|
|
107
|
+
from receipts.verify.rewriter import rewrite_weak_bullets
|
|
108
|
+
from receipts.verify.verifier import (
|
|
109
|
+
group_claims_by_bullet,
|
|
110
|
+
verify_bullet_claims,
|
|
111
|
+
verify_claim,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# --- Load provider ---
|
|
115
|
+
settings = load_settings()
|
|
116
|
+
try:
|
|
117
|
+
provider = get_provider(settings, verify=True)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
log.write(f"[red]Provider error: {exc}[/red]")
|
|
120
|
+
status.status_text = "Error"
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
status.provider_name = provider.name
|
|
124
|
+
log.write(f"Provider: [green]{provider.name}[/green]")
|
|
125
|
+
|
|
126
|
+
# Surface rate-limit waits in the status bar instead of hanging silently.
|
|
127
|
+
if hasattr(provider, "wait_callback"):
|
|
128
|
+
|
|
129
|
+
def _on_wait(reason: str, wait: int) -> None:
|
|
130
|
+
status.status_text = f"Rate-limited — waiting {wait}s..."
|
|
131
|
+
log.write(
|
|
132
|
+
f"[yellow]{reason} — waiting {wait}s before retrying...[/yellow]"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
provider.wait_callback = _on_wait
|
|
136
|
+
|
|
137
|
+
# --- Open KB ---
|
|
138
|
+
kb_dir = self.kb_dir or default_kb_dir(self.source_path)
|
|
139
|
+
kb = KnowledgeBase(kb_dir)
|
|
140
|
+
if kb.count() == 0:
|
|
141
|
+
msg = "Knowledge base is empty. Run 'receipts ingest' first."
|
|
142
|
+
log.write(f"[red]{msg}[/red]")
|
|
143
|
+
status.status_text = "Error — empty KB"
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
log.write(f"Knowledge base: {kb.count()} artifact(s) in {kb_dir}")
|
|
147
|
+
|
|
148
|
+
# --- Populate file tree from KB ---
|
|
149
|
+
_populate_tree(tree, kb)
|
|
150
|
+
|
|
151
|
+
# --- Parse resume ---
|
|
152
|
+
status.status_text = "Parsing resume..."
|
|
153
|
+
log.write(f"Parsing [bold]{self.resume_path.name}[/bold] ...")
|
|
154
|
+
resume = load_resume(self.resume_path)
|
|
155
|
+
log.write(f" Name: {resume.name}")
|
|
156
|
+
log.write(f" Sections: {len(resume.sections)}")
|
|
157
|
+
|
|
158
|
+
claims = extract_claims(resume, sections=self.sections, entries=self.entries)
|
|
159
|
+
if self.entries:
|
|
160
|
+
log.write(f" Scoped to entries: {', '.join(self.entries)} (+ skills)")
|
|
161
|
+
log.write(f" Claims: {len(claims)}")
|
|
162
|
+
|
|
163
|
+
# --- Verify claims, batching per bullet on API providers ---
|
|
164
|
+
status.status_text = "Verifying claims..."
|
|
165
|
+
use_batch = provider.name != "ollama"
|
|
166
|
+
groups = group_claims_by_bullet(claims) if use_batch else [[c] for c in claims]
|
|
167
|
+
log.write("")
|
|
168
|
+
log.write(f"[bold]Verifying {len(claims)} claim(s)...[/bold]")
|
|
169
|
+
if use_batch:
|
|
170
|
+
log.write("[dim]Claims from the same bullet share one LLM call.[/dim]")
|
|
171
|
+
|
|
172
|
+
results = []
|
|
173
|
+
errors = 0
|
|
174
|
+
done = 0
|
|
175
|
+
for group in groups:
|
|
176
|
+
done += len(group)
|
|
177
|
+
status.status_text = f"Verifying {done}/{len(claims)}..."
|
|
178
|
+
try:
|
|
179
|
+
if use_batch:
|
|
180
|
+
group_results = verify_bullet_claims(
|
|
181
|
+
group, kb, provider, distance_threshold=self.threshold
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
group_results = [
|
|
185
|
+
verify_claim(
|
|
186
|
+
group[0],
|
|
187
|
+
kb,
|
|
188
|
+
provider,
|
|
189
|
+
distance_threshold=self.threshold,
|
|
190
|
+
)
|
|
191
|
+
]
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
from receipts.errors import ProviderConfigError
|
|
194
|
+
|
|
195
|
+
if isinstance(exc, ProviderConfigError):
|
|
196
|
+
log.write(f"[red]Provider configuration error: {exc}[/red]")
|
|
197
|
+
log.write(
|
|
198
|
+
"[red]Aborting run — fix your .env / environment"
|
|
199
|
+
" (run 'receipts check') and retry.[/red]"
|
|
200
|
+
)
|
|
201
|
+
status.status_text = "Error — provider config"
|
|
202
|
+
return
|
|
203
|
+
errors += len(group)
|
|
204
|
+
log.write(
|
|
205
|
+
f" [red]ERROR verifying '{group[0].bullet_text[:50]}':"
|
|
206
|
+
f" {exc}[/red]"
|
|
207
|
+
)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
for result in group_results:
|
|
211
|
+
results.append(result)
|
|
212
|
+
self.call_from_thread(table.add_result, result)
|
|
213
|
+
|
|
214
|
+
if result.completion:
|
|
215
|
+
status.add_tokens(
|
|
216
|
+
result.completion.input_tokens,
|
|
217
|
+
result.completion.output_tokens,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if result.verdict == "unsupported":
|
|
221
|
+
log.write(
|
|
222
|
+
f" [red]✗[/red] {result.claim.claim_type}:"
|
|
223
|
+
f" {result.claim.value}"
|
|
224
|
+
)
|
|
225
|
+
elif result.verdict == "plausible":
|
|
226
|
+
log.write(
|
|
227
|
+
f" [yellow]?[/yellow] {result.claim.claim_type}:"
|
|
228
|
+
f" {result.claim.value}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if errors:
|
|
232
|
+
log.write(f"[red]{errors} claim(s) failed due to API errors.[/red]")
|
|
233
|
+
|
|
234
|
+
verified = sum(1 for r in results if r.verdict == "verified")
|
|
235
|
+
plausible = sum(1 for r in results if r.verdict == "plausible")
|
|
236
|
+
unsupported = sum(1 for r in results if r.verdict == "unsupported")
|
|
237
|
+
|
|
238
|
+
log.write("")
|
|
239
|
+
log.write(
|
|
240
|
+
f"[bold]Results:[/bold] "
|
|
241
|
+
f"[green]{verified} verified[/green], "
|
|
242
|
+
f"[yellow]{plausible} plausible[/yellow], "
|
|
243
|
+
f"[red]{unsupported} unsupported[/red]"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# --- Rewrite weak bullets ---
|
|
247
|
+
status.status_text = "Rewriting weak bullets..."
|
|
248
|
+
log.write("")
|
|
249
|
+
log.write("[bold]Generating rewrites for weak bullets...[/bold]")
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
rewrites = rewrite_weak_bullets(results, provider)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
log.write(f"[red]Rewrite failed: {exc}[/red]")
|
|
255
|
+
rewrites = []
|
|
256
|
+
|
|
257
|
+
if not rewrites:
|
|
258
|
+
self.call_from_thread(diff.show_no_rewrites)
|
|
259
|
+
log.write("[green]No rewrites needed.[/green]")
|
|
260
|
+
else:
|
|
261
|
+
for rw in rewrites:
|
|
262
|
+
self.call_from_thread(diff.add_rewrite, rw)
|
|
263
|
+
if rw.completion:
|
|
264
|
+
status.add_tokens(
|
|
265
|
+
rw.completion.input_tokens,
|
|
266
|
+
rw.completion.output_tokens,
|
|
267
|
+
)
|
|
268
|
+
log.write(f"Generated {len(rewrites)} rewrite(s).")
|
|
269
|
+
|
|
270
|
+
status.status_text = "Done"
|
|
271
|
+
log.write("")
|
|
272
|
+
log.write("[bold green]Verification complete.[/bold green]")
|
|
273
|
+
|
|
274
|
+
def action_show_tab(self, tab_id: str) -> None:
|
|
275
|
+
tc = self.query_one("#main-area", TabbedContent)
|
|
276
|
+
tc.active = tab_id
|
|
277
|
+
|
|
278
|
+
def on_data_table_row_selected(self, event) -> None:
|
|
279
|
+
"""Selecting a claims row shows its FULL details in the Log tab."""
|
|
280
|
+
table = self.query_one("#claims-table", ClaimsTable)
|
|
281
|
+
result = table.result_at(event.cursor_row)
|
|
282
|
+
if result is None:
|
|
283
|
+
return
|
|
284
|
+
log = self.query_one("#activity-log", RichLog)
|
|
285
|
+
log.write("")
|
|
286
|
+
log.write(f"[bold]━━━ Claim details ({result.verdict}) ━━━[/bold]")
|
|
287
|
+
log.write(
|
|
288
|
+
f"[bold]Claim:[/bold] {result.claim.claim_type} — {result.claim.value}"
|
|
289
|
+
)
|
|
290
|
+
log.write(f"[bold]Bullet:[/bold] {result.claim.bullet_text}")
|
|
291
|
+
log.write(f"[bold]Why:[/bold] {result.explanation}")
|
|
292
|
+
for e in result.evidence[:3]:
|
|
293
|
+
log.write(f" evidence: {e.rel_path} :: {e.name}")
|
|
294
|
+
self.action_show_tab("log-pane")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _populate_tree(tree: Tree, kb: object) -> None:
|
|
298
|
+
"""Build a tree of file paths from KB metadata."""
|
|
299
|
+
raw = kb._collection.get(include=["metadatas"])
|
|
300
|
+
paths: set[str] = set()
|
|
301
|
+
for meta in raw.get("metadatas", []) or []:
|
|
302
|
+
if meta and "rel_path" in meta:
|
|
303
|
+
paths.add(meta["rel_path"])
|
|
304
|
+
|
|
305
|
+
tree.root.expand()
|
|
306
|
+
dirs: dict[str, object] = {}
|
|
307
|
+
|
|
308
|
+
for path in sorted(paths):
|
|
309
|
+
parts = path.replace("\\", "/").split("/")
|
|
310
|
+
parent = tree.root
|
|
311
|
+
for i, part in enumerate(parts[:-1]):
|
|
312
|
+
key = "/".join(parts[: i + 1])
|
|
313
|
+
if key not in dirs:
|
|
314
|
+
dirs[key] = parent.add(part, expand=True)
|
|
315
|
+
parent = dirs[key]
|
|
316
|
+
parent.add_leaf(parts[-1])
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Textual TUI for the dossier flow — verify, build, preview the study doc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.widgets import Footer, Header, Markdown, RichLog, TabbedContent, TabPane
|
|
9
|
+
|
|
10
|
+
from receipts.tui.widgets.status_bar import StatusBar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DossierApp(App):
|
|
14
|
+
"""Full-screen dossier builder with an in-app markdown preview."""
|
|
15
|
+
|
|
16
|
+
TITLE = "Receipts"
|
|
17
|
+
SUB_TITLE = "defense dossier"
|
|
18
|
+
|
|
19
|
+
CSS = """
|
|
20
|
+
#dossier-preview {
|
|
21
|
+
height: 1fr;
|
|
22
|
+
padding: 0 1;
|
|
23
|
+
}
|
|
24
|
+
#activity-log {
|
|
25
|
+
height: 1fr;
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BINDINGS = [
|
|
30
|
+
("q", "quit", "Quit"),
|
|
31
|
+
("p", "show_tab('preview-pane')", "Preview"),
|
|
32
|
+
("l", "show_tab('log-pane')", "Log"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
resume_path: Path,
|
|
38
|
+
source_path: Path,
|
|
39
|
+
*,
|
|
40
|
+
threshold: float = 0.5,
|
|
41
|
+
sections: list[str] | None = None,
|
|
42
|
+
entries: list[str] | None = None,
|
|
43
|
+
output_path: Path | None = None,
|
|
44
|
+
**kwargs,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(**kwargs)
|
|
47
|
+
self.resume_path = resume_path
|
|
48
|
+
self.source_path = source_path
|
|
49
|
+
self.threshold = threshold
|
|
50
|
+
self.sections = sections
|
|
51
|
+
self.entries = entries
|
|
52
|
+
self.output_path = output_path or Path("dossier.md")
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
yield Header()
|
|
56
|
+
with TabbedContent(id="main-area"):
|
|
57
|
+
with TabPane("Preview", id="preview-pane"):
|
|
58
|
+
yield Markdown("*Building dossier...*", id="dossier-preview")
|
|
59
|
+
with TabPane("Log", id="log-pane"):
|
|
60
|
+
yield RichLog(id="activity-log", markup=True, wrap=True)
|
|
61
|
+
yield StatusBar(id="status-bar")
|
|
62
|
+
yield Footer()
|
|
63
|
+
|
|
64
|
+
def on_mount(self) -> None:
|
|
65
|
+
self.run_worker(self._run_dossier, thread=True)
|
|
66
|
+
|
|
67
|
+
async def _run_dossier(self) -> None:
|
|
68
|
+
log = self.query_one("#activity-log", RichLog)
|
|
69
|
+
status = self.query_one("#status-bar", StatusBar)
|
|
70
|
+
preview = self.query_one("#dossier-preview", Markdown)
|
|
71
|
+
|
|
72
|
+
from datetime import date
|
|
73
|
+
|
|
74
|
+
from receipts.config import load_settings
|
|
75
|
+
from receipts.llm.factory import get_provider
|
|
76
|
+
from receipts.prep.dossier import build_dossier, render_markdown
|
|
77
|
+
from receipts.prep.readiness import ReadinessStore
|
|
78
|
+
from receipts.resume.claim_extractor import extract_claims
|
|
79
|
+
from receipts.resume.loader import load_resume
|
|
80
|
+
from receipts.verify.kb import KnowledgeBase, default_kb_dir
|
|
81
|
+
from receipts.verify.verifier import verify_claims
|
|
82
|
+
|
|
83
|
+
settings = load_settings()
|
|
84
|
+
try:
|
|
85
|
+
provider = get_provider(settings, verify=True)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
log.write(f"[red]Provider error: {exc}[/red]")
|
|
88
|
+
status.status_text = "Error — provider"
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
status.provider_name = provider.name
|
|
92
|
+
if hasattr(provider, "wait_callback"):
|
|
93
|
+
|
|
94
|
+
def _on_wait(reason: str, wait: int) -> None:
|
|
95
|
+
status.status_text = f"Rate-limited — waiting {wait}s..."
|
|
96
|
+
log.write(f"[yellow]{reason} — waiting {wait}s...[/yellow]")
|
|
97
|
+
|
|
98
|
+
provider.wait_callback = _on_wait
|
|
99
|
+
|
|
100
|
+
kb = KnowledgeBase(default_kb_dir(self.source_path))
|
|
101
|
+
if kb.count() == 0:
|
|
102
|
+
log.write(
|
|
103
|
+
"[red]Knowledge base is empty. Run 'receipts ingest' first.[/red]"
|
|
104
|
+
)
|
|
105
|
+
status.status_text = "Error — empty KB"
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
status.status_text = "Parsing resume..."
|
|
109
|
+
resume = load_resume(self.resume_path)
|
|
110
|
+
claims = extract_claims(resume, sections=self.sections, entries=self.entries)
|
|
111
|
+
log.write(f"Claims: {len(claims)}")
|
|
112
|
+
|
|
113
|
+
status.status_text = "Verifying claims..."
|
|
114
|
+
log.write(f"Verifying {len(claims)} claim(s)...")
|
|
115
|
+
try:
|
|
116
|
+
results = verify_claims(
|
|
117
|
+
claims,
|
|
118
|
+
kb,
|
|
119
|
+
provider,
|
|
120
|
+
distance_threshold=self.threshold,
|
|
121
|
+
batch=provider.name != "ollama",
|
|
122
|
+
)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
log.write(f"[red]Verification failed: {exc}[/red]")
|
|
125
|
+
status.status_text = "Error — verification"
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
store = ReadinessStore()
|
|
129
|
+
try:
|
|
130
|
+
weak_spots = store.weak_spots()
|
|
131
|
+
finally:
|
|
132
|
+
store.close()
|
|
133
|
+
if weak_spots:
|
|
134
|
+
log.write(f"Including {len(weak_spots)} weak spot(s) from past sessions.")
|
|
135
|
+
|
|
136
|
+
log.write("")
|
|
137
|
+
log.write("[bold]Building dossier (one LLM call per bullet)...[/bold]")
|
|
138
|
+
|
|
139
|
+
def _progress(done: int, total: int) -> None:
|
|
140
|
+
status.status_text = f"Dossier bullet {done}/{total}..."
|
|
141
|
+
log.write(f" bullet {done}/{total}")
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
entries_built = build_dossier(
|
|
145
|
+
results, provider, weak_spots=weak_spots, on_progress=_progress
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
log.write(f"[red]Dossier build failed: {exc}[/red]")
|
|
149
|
+
status.status_text = "Error — dossier"
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
doc = render_markdown(
|
|
153
|
+
entries_built,
|
|
154
|
+
resume_name=resume.name,
|
|
155
|
+
source_name=self.source_path.name,
|
|
156
|
+
generated=date.today().isoformat(),
|
|
157
|
+
)
|
|
158
|
+
self.output_path.write_text(doc, encoding="utf-8")
|
|
159
|
+
|
|
160
|
+
self.call_from_thread(preview.update, doc)
|
|
161
|
+
|
|
162
|
+
log.write("")
|
|
163
|
+
log.write(f"[bold green]Dossier written: {self.output_path}[/bold green]")
|
|
164
|
+
log.write(f" Bullets covered: {len(entries_built)}")
|
|
165
|
+
log.write("[dim]Press p to read it here, or open the .md file.[/dim]")
|
|
166
|
+
status.status_text = "Done"
|
|
167
|
+
|
|
168
|
+
def action_show_tab(self, tab_id: str) -> None:
|
|
169
|
+
tc = self.query_one("#main-area", TabbedContent)
|
|
170
|
+
tc.active = tab_id
|