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
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