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/config.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""BYOK configuration.
|
|
2
|
+
|
|
3
|
+
All configuration comes from environment variables, optionally loaded from
|
|
4
|
+
``.env`` files (never committed). Resolution order (first hit wins):
|
|
5
|
+
|
|
6
|
+
1. real environment variables
|
|
7
|
+
2. ``.env`` in the current working directory (per-project overrides)
|
|
8
|
+
3. ``~/.receipts/.env`` (user-level config, written by ``receipts setup``)
|
|
9
|
+
|
|
10
|
+
No key is ever hardcoded, and no default points at a paid API — the default
|
|
11
|
+
provider is local Ollama.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
|
|
23
|
+
GLOBAL_CONFIG_PATH = Path.home() / ".receipts" / ".env"
|
|
24
|
+
|
|
25
|
+
DEFAULT_PROVIDER = "ollama"
|
|
26
|
+
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
|
|
27
|
+
# Defaults chosen from what is actually pulled on the dev machine (see
|
|
28
|
+
# SETUP.md for how to change them or pull alternatives).
|
|
29
|
+
DEFAULT_OLLAMA_CHAT_MODEL = "gpt-oss:20b"
|
|
30
|
+
DEFAULT_OLLAMA_EMBED_MODEL = "nomic-embed-text"
|
|
31
|
+
# Current free-tier Gemini models (verified against ai.google.dev, July 2026).
|
|
32
|
+
DEFAULT_GEMINI_CHAT_MODEL = "gemini-3.5-flash"
|
|
33
|
+
DEFAULT_GEMINI_EMBED_MODEL = "gemini-embedding-2"
|
|
34
|
+
DEFAULT_ANTHROPIC_CHAT_MODEL = "claude-sonnet-4-20250514"
|
|
35
|
+
DEFAULT_OPENAI_CHAT_MODEL = "gpt-4o-mini"
|
|
36
|
+
DEFAULT_OPENAI_EMBED_MODEL = "text-embedding-3-small"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Settings:
|
|
41
|
+
provider: str = DEFAULT_PROVIDER
|
|
42
|
+
# Optional separate provider for embeddings (RECEIPTS_EMBED_PROVIDER).
|
|
43
|
+
# Needed for chat providers without an embeddings API (Anthropic).
|
|
44
|
+
# None = embeddings use the same provider as chat.
|
|
45
|
+
embed_provider: str | None = None
|
|
46
|
+
ollama_host: str = DEFAULT_OLLAMA_HOST
|
|
47
|
+
ollama_chat_model: str = DEFAULT_OLLAMA_CHAT_MODEL
|
|
48
|
+
ollama_embed_model: str = DEFAULT_OLLAMA_EMBED_MODEL
|
|
49
|
+
gemini_api_key: str | None = None
|
|
50
|
+
gemini_chat_model: str = DEFAULT_GEMINI_CHAT_MODEL
|
|
51
|
+
gemini_embed_model: str = DEFAULT_GEMINI_EMBED_MODEL
|
|
52
|
+
anthropic_api_key: str | None = None
|
|
53
|
+
anthropic_chat_model: str = DEFAULT_ANTHROPIC_CHAT_MODEL
|
|
54
|
+
openai_api_key: str | None = None
|
|
55
|
+
openai_chat_model: str = DEFAULT_OPENAI_CHAT_MODEL
|
|
56
|
+
openai_embed_model: str = DEFAULT_OPENAI_EMBED_MODEL
|
|
57
|
+
# Optional OpenAI-compatible endpoint (Groq, OpenRouter, local servers).
|
|
58
|
+
# No default — leaving it unset means the official OpenAI API.
|
|
59
|
+
openai_base_url: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_settings(env: Mapping[str, str] | None = None) -> Settings:
|
|
63
|
+
"""Build Settings from environment variables.
|
|
64
|
+
|
|
65
|
+
When ``env`` is not injected (normal runtime), a local ``.env`` file is
|
|
66
|
+
loaded first; real environment variables always win over ``.env`` values.
|
|
67
|
+
Tests pass an explicit ``env`` mapping and never touch the process
|
|
68
|
+
environment or the filesystem.
|
|
69
|
+
"""
|
|
70
|
+
if env is None:
|
|
71
|
+
# dotenv never overrides variables that are already set, so loading
|
|
72
|
+
# the project .env first makes it win over the user-level file.
|
|
73
|
+
load_dotenv()
|
|
74
|
+
if GLOBAL_CONFIG_PATH.is_file():
|
|
75
|
+
load_dotenv(GLOBAL_CONFIG_PATH)
|
|
76
|
+
env = os.environ
|
|
77
|
+
return Settings(
|
|
78
|
+
provider=env.get("RECEIPTS_PROVIDER", DEFAULT_PROVIDER).strip().lower(),
|
|
79
|
+
embed_provider=(env.get("RECEIPTS_EMBED_PROVIDER", "").strip().lower() or None),
|
|
80
|
+
ollama_host=env.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).strip(),
|
|
81
|
+
ollama_chat_model=env.get(
|
|
82
|
+
"OLLAMA_CHAT_MODEL", DEFAULT_OLLAMA_CHAT_MODEL
|
|
83
|
+
).strip(),
|
|
84
|
+
ollama_embed_model=env.get(
|
|
85
|
+
"OLLAMA_EMBED_MODEL", DEFAULT_OLLAMA_EMBED_MODEL
|
|
86
|
+
).strip(),
|
|
87
|
+
gemini_api_key=env.get("GEMINI_API_KEY") or None,
|
|
88
|
+
gemini_chat_model=env.get(
|
|
89
|
+
"GEMINI_CHAT_MODEL", DEFAULT_GEMINI_CHAT_MODEL
|
|
90
|
+
).strip(),
|
|
91
|
+
gemini_embed_model=env.get(
|
|
92
|
+
"GEMINI_EMBED_MODEL", DEFAULT_GEMINI_EMBED_MODEL
|
|
93
|
+
).strip(),
|
|
94
|
+
anthropic_api_key=env.get("ANTHROPIC_API_KEY") or None,
|
|
95
|
+
anthropic_chat_model=env.get(
|
|
96
|
+
"ANTHROPIC_CHAT_MODEL", DEFAULT_ANTHROPIC_CHAT_MODEL
|
|
97
|
+
).strip(),
|
|
98
|
+
openai_api_key=env.get("OPENAI_API_KEY") or None,
|
|
99
|
+
openai_chat_model=env.get(
|
|
100
|
+
"OPENAI_CHAT_MODEL", DEFAULT_OPENAI_CHAT_MODEL
|
|
101
|
+
).strip(),
|
|
102
|
+
openai_embed_model=env.get(
|
|
103
|
+
"OPENAI_EMBED_MODEL", DEFAULT_OPENAI_EMBED_MODEL
|
|
104
|
+
).strip(),
|
|
105
|
+
openai_base_url=(env.get("OPENAI_BASE_URL") or "").strip() or None,
|
|
106
|
+
)
|
receipts/errors.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Shared exception types for receipts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReceiptsError(Exception):
|
|
7
|
+
"""Base class for all receipts errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderConfigError(ReceiptsError):
|
|
11
|
+
"""The configured provider is missing something it needs (e.g. an API key).
|
|
12
|
+
|
|
13
|
+
The message must always tell the user exactly what to do next.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProviderUnavailableError(ReceiptsError):
|
|
18
|
+
"""The configured provider exists but cannot be reached right now."""
|
receipts/export.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Export documents to PDF — markdown via pure Python, TeX via a compiler.
|
|
2
|
+
|
|
3
|
+
Markdown (dossiers, reports) converts with zero external programs:
|
|
4
|
+
``markdown`` renders HTML, ``xhtml2pdf`` renders the PDF. Both are plain
|
|
5
|
+
pip installs (the ``[pdf]`` extra). TeX genuinely requires a TeX engine —
|
|
6
|
+
we detect latexmk/tectonic/pdflatex and say exactly what to install when
|
|
7
|
+
none exists (or point at Overleaf).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from receipts.errors import ReceiptsError
|
|
16
|
+
|
|
17
|
+
_PDF_EXTRA_HELP = """\
|
|
18
|
+
PDF export for markdown needs the optional [pdf] extra:
|
|
19
|
+
pip install codebase-receipts-cli[pdf]
|
|
20
|
+
(installs 'markdown' and 'xhtml2pdf' — pure Python, no system packages)"""
|
|
21
|
+
|
|
22
|
+
_STYLE = """
|
|
23
|
+
@page { size: a4; margin: 1.6cm; }
|
|
24
|
+
body { font-family: Helvetica, Arial, sans-serif; font-size: 10pt; color: #1a1a1a; }
|
|
25
|
+
h1 { font-size: 17pt; border-bottom: 2px solid #333; padding-bottom: 4px; }
|
|
26
|
+
h2 { font-size: 13pt; margin-top: 14pt; border-bottom: 1px solid #999; }
|
|
27
|
+
h3 { font-size: 11pt; margin-top: 10pt; }
|
|
28
|
+
code { font-family: Courier, monospace; font-size: 9pt; background: #f2f2f2; }
|
|
29
|
+
pre { background: #f2f2f2; padding: 6px; font-size: 8.5pt; }
|
|
30
|
+
blockquote { border-left: 3px solid #888; margin-left: 0;
|
|
31
|
+
padding-left: 10px; color: #333; }
|
|
32
|
+
table { border-collapse: collapse; }
|
|
33
|
+
th, td { border: 1px solid #999; padding: 4px 8px; font-size: 9pt; }
|
|
34
|
+
hr { border: 0; border-top: 1px solid #bbb; }
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ExportResult:
|
|
40
|
+
success: bool
|
|
41
|
+
pdf_path: Path | None
|
|
42
|
+
tool_used: str
|
|
43
|
+
error: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def export_markdown_pdf(md_path: Path, output: Path | None = None) -> ExportResult:
|
|
47
|
+
"""Render a markdown file to PDF with pure-Python tooling."""
|
|
48
|
+
try:
|
|
49
|
+
import markdown
|
|
50
|
+
from xhtml2pdf import pisa
|
|
51
|
+
except ImportError as exc:
|
|
52
|
+
raise ReceiptsError(_PDF_EXTRA_HELP) from exc
|
|
53
|
+
|
|
54
|
+
if output is None:
|
|
55
|
+
output = md_path.with_suffix(".pdf")
|
|
56
|
+
|
|
57
|
+
text = md_path.read_text(encoding="utf-8")
|
|
58
|
+
body = markdown.markdown(text, extensions=["tables", "fenced_code", "sane_lists"])
|
|
59
|
+
html = (
|
|
60
|
+
"<html><head><style>"
|
|
61
|
+
+ _STYLE
|
|
62
|
+
+ "</style></head><body>"
|
|
63
|
+
+ body
|
|
64
|
+
+ "</body></html>"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
with open(output, "wb") as fh:
|
|
68
|
+
status = pisa.CreatePDF(src=html, dest=fh, encoding="utf-8")
|
|
69
|
+
|
|
70
|
+
if status.err:
|
|
71
|
+
return ExportResult(
|
|
72
|
+
success=False,
|
|
73
|
+
pdf_path=None,
|
|
74
|
+
tool_used="xhtml2pdf",
|
|
75
|
+
error=f"PDF rendering reported {status.err} error(s).",
|
|
76
|
+
)
|
|
77
|
+
return ExportResult(success=True, pdf_path=output, tool_used="xhtml2pdf")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def export_tex_pdf(tex_path: Path, output: Path | None = None) -> ExportResult:
|
|
81
|
+
"""Compile a .tex file to PDF via whatever TeX engine is installed."""
|
|
82
|
+
from receipts.rewrite.compiler import compile_tex
|
|
83
|
+
|
|
84
|
+
result = compile_tex(tex_path)
|
|
85
|
+
if not result.success:
|
|
86
|
+
error = result.error or "compilation failed"
|
|
87
|
+
if result.tool_used is None:
|
|
88
|
+
error += (
|
|
89
|
+
"\n Easiest single-binary install: tectonic"
|
|
90
|
+
" (https://tectonic-typesetting.github.io) — or paste the"
|
|
91
|
+
" .tex into Overleaf."
|
|
92
|
+
)
|
|
93
|
+
return ExportResult(
|
|
94
|
+
success=False,
|
|
95
|
+
pdf_path=None,
|
|
96
|
+
tool_used=result.tool_used or "none",
|
|
97
|
+
error=error,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
pdf_path = result.pdf_path
|
|
101
|
+
if output is not None and pdf_path is not None and output != pdf_path:
|
|
102
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
output.write_bytes(pdf_path.read_bytes())
|
|
104
|
+
pdf_path = output
|
|
105
|
+
|
|
106
|
+
return ExportResult(
|
|
107
|
+
success=True, pdf_path=pdf_path, tool_used=result.tool_used or "tex"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def export_to_pdf(source: Path, output: Path | None = None) -> ExportResult:
|
|
112
|
+
"""Dispatch on extension: .md → xhtml2pdf, .tex → TeX compiler."""
|
|
113
|
+
suffix = source.suffix.lower()
|
|
114
|
+
if suffix in (".md", ".markdown"):
|
|
115
|
+
return export_markdown_pdf(source, output)
|
|
116
|
+
if suffix == ".tex":
|
|
117
|
+
return export_tex_pdf(source, output)
|
|
118
|
+
raise ReceiptsError(
|
|
119
|
+
f"Don't know how to export '{source.suffix}' to PDF —" " supported: .md, .tex"
|
|
120
|
+
)
|
|
File without changes
|