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,35 @@
1
+ """Local token-count estimation for providers that don't report usage.
2
+
3
+ Uses tiktoken's ``cl100k_base`` as a reasonable cross-model approximation.
4
+ Counts produced here are ESTIMATES and must be labeled as such in the ledger
5
+ (``CompletionResult.tokens_estimated=True``).
6
+
7
+ tiktoken downloads its encoding file on first use, so this module is only
8
+ imported by providers that already require network/daemon access (Ollama) —
9
+ never by the fake provider or the test suite.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import functools
15
+
16
+ _CHARS_PER_TOKEN_FALLBACK = 4
17
+
18
+
19
+ @functools.lru_cache(maxsize=1)
20
+ def _encoding():
21
+ import tiktoken
22
+
23
+ return tiktoken.get_encoding("cl100k_base")
24
+
25
+
26
+ def estimate_tokens(text: str) -> int:
27
+ """Estimated token count for ``text`` — never a measured value."""
28
+ if not text:
29
+ return 0
30
+ # tiktoken 0.13.x has a known Windows crash (access violation in the Rust
31
+ # backend) on certain inputs when called from a large scan loop. The
32
+ # chars/4 heuristic is accurate enough for the token-budget gate in the
33
+ # artifact extractor, and tiktoken is still used by Ollama for per-call
34
+ # ledger estimates where individual calls are short and isolated.
35
+ return max(1, len(text) // _CHARS_PER_TOKEN_FALLBACK)
@@ -0,0 +1 @@
1
+ """Evidence mining — provable facts from code and git history, zero LLM calls."""
@@ -0,0 +1,246 @@
1
+ """Static code metrics miner — counts things the codebase can PROVE.
2
+
3
+ Zero LLM calls. Walks the scanned file tree (respecting .gitignore) and
4
+ counts concrete, checkable artifacts: API routes, database models,
5
+ migrations, UI components, tests, service modules, and lines of code per
6
+ language. Every count carries file-level provenance so a number in a
7
+ rewritten resume bullet can always be traced back to the files that
8
+ produced it.
9
+
10
+ Regex-based on purpose: it must work identically on Windows (where
11
+ tree-sitter support is spotty) and the patterns only need to be precise
12
+ enough to count, not to parse.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ from receipts.ingest import scanner
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Data model
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class MetricCount:
30
+ key: str # machine name, e.g. "api_routes"
31
+ label: str # human name, e.g. "API route definitions"
32
+ count: int
33
+ files: list[str] = field(default_factory=list) # provenance (rel paths)
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class CodeFacts:
38
+ metrics: list[MetricCount]
39
+ languages: dict[str, int] # language -> lines of code
40
+ total_files: int
41
+
42
+ def metric(self, key: str) -> MetricCount | None:
43
+ for m in self.metrics:
44
+ if m.key == key:
45
+ return m
46
+ return None
47
+
48
+ def summary_lines(self) -> list[str]:
49
+ """Human-readable one-liners for CLI/TUI output."""
50
+ lines = [f"{m.label}: {m.count}" for m in self.metrics if m.count > 0]
51
+ if self.languages:
52
+ top = sorted(self.languages.items(), key=lambda kv: -kv[1])[:5]
53
+ lang_str = ", ".join(f"{name} ({loc:,} LOC)" for name, loc in top)
54
+ lines.append(f"Languages: {lang_str}")
55
+ return lines
56
+
57
+ def prompt_facts(self) -> str:
58
+ """Facts formatted for a rewrite prompt — numbers with provenance."""
59
+ lines: list[str] = []
60
+ for m in self.metrics:
61
+ if m.count == 0:
62
+ continue
63
+ examples = ", ".join(m.files[:3])
64
+ suffix = f" (e.g. {examples})" if examples else ""
65
+ lines.append(f"- {m.count} {m.label.lower()}{suffix}")
66
+ if self.languages:
67
+ top = sorted(self.languages.items(), key=lambda kv: -kv[1])[:5]
68
+ lines.append(
69
+ "- languages by size: "
70
+ + ", ".join(f"{name} ({loc:,} lines)" for name, loc in top)
71
+ )
72
+ return "\n".join(lines)
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Pattern-based counters
77
+ # ---------------------------------------------------------------------------
78
+
79
+ # FastAPI/Flask decorators and Express-style route registrations.
80
+ _PY_ROUTE_RE = re.compile(
81
+ r"@(?:\w+\.)?(?:app|router|api_router|blueprint|bp)\."
82
+ r"(?:get|post|put|delete|patch|route|websocket)\s*\(",
83
+ re.IGNORECASE,
84
+ )
85
+ _JS_ROUTE_RE = re.compile(
86
+ r"\b(?:app|router)\.(?:get|post|put|delete|patch)\s*\(\s*['\"`]/"
87
+ )
88
+
89
+ # SQLAlchemy declarative / Django / generic ORM model classes.
90
+ _PY_MODEL_RE = re.compile(
91
+ r"^class\s+\w+\s*\((?:[^)]*\b(?:Base|db\.Model|models\.Model)\b)[^)]*\)\s*:",
92
+ re.MULTILINE,
93
+ )
94
+
95
+ # React/Vue-style component definitions in .tsx/.jsx files.
96
+ _COMPONENT_RE = re.compile(
97
+ r"(?:export\s+(?:default\s+)?)?(?:function|const)\s+([A-Z]\w+)\s*(?:=|\()"
98
+ )
99
+
100
+ _PY_TEST_RE = re.compile(r"^def\s+test_\w+", re.MULTILINE)
101
+ _JS_TEST_RE = re.compile(r"^\s*(?:it|test)\s*\(\s*['\"`]", re.MULTILINE)
102
+
103
+ _EXT_LANGUAGE = {
104
+ ".py": "Python",
105
+ ".ts": "TypeScript",
106
+ ".tsx": "TypeScript",
107
+ ".js": "JavaScript",
108
+ ".jsx": "JavaScript",
109
+ ".java": "Java",
110
+ ".kt": "Kotlin",
111
+ ".go": "Go",
112
+ ".rs": "Rust",
113
+ ".rb": "Ruby",
114
+ ".sql": "SQL",
115
+ ".html": "HTML",
116
+ ".css": "CSS",
117
+ ".sh": "Shell",
118
+ ".yaml": "YAML",
119
+ ".yml": "YAML",
120
+ }
121
+
122
+
123
+ def _count_matches(pattern: re.Pattern[str], content: str) -> int:
124
+ return len(pattern.findall(content))
125
+
126
+
127
+ def _is_migration(rel_path: str) -> bool:
128
+ lowered = rel_path.lower()
129
+ return (
130
+ (
131
+ "alembic/versions/" in lowered
132
+ or "/migrations/" in lowered
133
+ or lowered.startswith("migrations/")
134
+ )
135
+ and lowered.endswith(".py")
136
+ and not lowered.endswith("__init__.py")
137
+ )
138
+
139
+
140
+ def _is_service_module(rel_path: str) -> bool:
141
+ lowered = rel_path.lower()
142
+ name = lowered.rsplit("/", 1)[-1]
143
+ return name.endswith("_service.py") or (
144
+ "/services/" in lowered and name.endswith(".py") and name != "__init__.py"
145
+ )
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Public API
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def mine_code_facts(root: Path) -> CodeFacts:
154
+ """Mine provable counts from a codebase folder. No network, no LLM."""
155
+ result = scanner.scan(root)
156
+
157
+ routes = 0
158
+ route_files: list[str] = []
159
+ models = 0
160
+ model_files: list[str] = []
161
+ components = 0
162
+ component_files: list[str] = []
163
+ tests = 0
164
+ test_files: list[str] = []
165
+ migration_files: list[str] = []
166
+ service_files: list[str] = []
167
+ languages: dict[str, int] = {}
168
+
169
+ for file in result.text_files:
170
+ rel = file.rel_path
171
+ ext = "." + rel.rsplit(".", 1)[-1].lower() if "." in rel else ""
172
+
173
+ language = _EXT_LANGUAGE.get(ext)
174
+ content: str | None = None
175
+ if language or ext in (".py", ".ts", ".tsx", ".js", ".jsx"):
176
+ content = scanner.read_text(root, rel)
177
+
178
+ if language and content is not None:
179
+ loc = sum(1 for line in content.splitlines() if line.strip())
180
+ languages[language] = languages.get(language, 0) + loc
181
+
182
+ if content is None:
183
+ continue
184
+
185
+ if ext == ".py":
186
+ n = _count_matches(_PY_ROUTE_RE, content)
187
+ if n:
188
+ routes += n
189
+ route_files.append(rel)
190
+ n = _count_matches(_PY_MODEL_RE, content)
191
+ if n:
192
+ models += n
193
+ model_files.append(rel)
194
+ n = _count_matches(_PY_TEST_RE, content)
195
+ if n:
196
+ tests += n
197
+ test_files.append(rel)
198
+ if _is_migration(rel):
199
+ migration_files.append(rel)
200
+ if _is_service_module(rel):
201
+ service_files.append(rel)
202
+
203
+ elif ext in (".ts", ".js"):
204
+ n = _count_matches(_JS_ROUTE_RE, content)
205
+ if n:
206
+ routes += n
207
+ route_files.append(rel)
208
+ n = _count_matches(_JS_TEST_RE, content)
209
+ if n:
210
+ tests += n
211
+ test_files.append(rel)
212
+
213
+ elif ext in (".tsx", ".jsx"):
214
+ n = _count_matches(_COMPONENT_RE, content)
215
+ if n:
216
+ components += n
217
+ component_files.append(rel)
218
+ n = _count_matches(_JS_TEST_RE, content)
219
+ if n:
220
+ tests += n
221
+ test_files.append(rel)
222
+
223
+ metrics = [
224
+ MetricCount("api_routes", "API route definitions", routes, route_files),
225
+ MetricCount("db_models", "Database models", models, model_files),
226
+ MetricCount(
227
+ "migrations",
228
+ "Schema migrations",
229
+ len(migration_files),
230
+ migration_files,
231
+ ),
232
+ MetricCount("ui_components", "UI components", components, component_files),
233
+ MetricCount(
234
+ "service_modules",
235
+ "Service modules",
236
+ len(service_files),
237
+ service_files,
238
+ ),
239
+ MetricCount("tests", "Test functions", tests, test_files),
240
+ ]
241
+
242
+ return CodeFacts(
243
+ metrics=metrics,
244
+ languages=languages,
245
+ total_files=len(result.files),
246
+ )
@@ -0,0 +1,109 @@
1
+ """Git history evidence — proves what code content can't.
2
+
3
+ Duration claims ("2 years"), collaboration claims ("team of 5"), and
4
+ delivery-volume claims ("shipped 40+ features") are invisible to code
5
+ content but provable from commit history. Zero LLM calls; everything is
6
+ read from the local repository. If the folder isn't a git repo (or is a
7
+ shallow clone with no useful history), the result says so honestly —
8
+ never guess.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ _MAX_COMMITS = 5000 # backstop against pathological histories
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class GitEvidence:
22
+ available: bool
23
+ reason: str = "" # why unavailable, when available is False
24
+ first_commit_date: str = "" # ISO date
25
+ last_commit_date: str = ""
26
+ active_months: int = 0
27
+ commit_count: int = 0
28
+ contributor_count: int = 0
29
+ contributors: list[str] = field(default_factory=list)
30
+ merge_count: int = 0
31
+
32
+ def summary_lines(self) -> list[str]:
33
+ if not self.available:
34
+ return [f"Git history: unavailable ({self.reason})"]
35
+ span = (
36
+ f"{self.first_commit_date} to {self.last_commit_date}"
37
+ f" (~{self.active_months} month(s))"
38
+ )
39
+ return [
40
+ f"Git history: {self.commit_count} commit(s) over {span}",
41
+ f"Contributors: {self.contributor_count}",
42
+ f"Merges: {self.merge_count}",
43
+ ]
44
+
45
+ def prompt_facts(self) -> str:
46
+ """Facts formatted for a rewrite prompt."""
47
+ if not self.available:
48
+ return ""
49
+ lines = [
50
+ f"- development span: {self.first_commit_date} to"
51
+ f" {self.last_commit_date} (~{self.active_months} months)",
52
+ f"- {self.commit_count} commits by"
53
+ f" {self.contributor_count} contributor(s)",
54
+ ]
55
+ if self.merge_count:
56
+ lines.append(f"- {self.merge_count} merge commits")
57
+ return "\n".join(lines)
58
+
59
+
60
+ def mine_git_evidence(root: Path) -> GitEvidence:
61
+ """Read commit history from a local repo. Never raises for non-repos."""
62
+ try:
63
+ from git import InvalidGitRepositoryError, NoSuchPathError, Repo
64
+ except ImportError: # pragma: no cover - GitPython is a hard dep
65
+ return GitEvidence(available=False, reason="GitPython not installed")
66
+
67
+ try:
68
+ repo = Repo(root, search_parent_directories=False)
69
+ except (InvalidGitRepositoryError, NoSuchPathError):
70
+ return GitEvidence(available=False, reason="not a git repository")
71
+
72
+ try:
73
+ commits = list(repo.iter_commits(max_count=_MAX_COMMITS))
74
+ except Exception as exc: # empty repo, detached corruption, etc.
75
+ return GitEvidence(available=False, reason=f"no readable history: {exc}")
76
+
77
+ if not commits:
78
+ return GitEvidence(available=False, reason="repository has no commits")
79
+
80
+ # iter_commits yields newest first.
81
+ newest = commits[0]
82
+ oldest = commits[-1]
83
+
84
+ def _date(commit) -> datetime:
85
+ return datetime.fromtimestamp(commit.committed_date, tz=timezone.utc)
86
+
87
+ first_date = _date(oldest)
88
+ last_date = _date(newest)
89
+ span_days = max(0, (last_date - first_date).days)
90
+ active_months = max(1, round(span_days / 30.4))
91
+
92
+ authors: set[str] = set()
93
+ merge_count = 0
94
+ for c in commits:
95
+ name = (c.author.name or "").strip() or (c.author.email or "unknown")
96
+ authors.add(name)
97
+ if len(c.parents) > 1:
98
+ merge_count += 1
99
+
100
+ return GitEvidence(
101
+ available=True,
102
+ first_commit_date=first_date.date().isoformat(),
103
+ last_commit_date=last_date.date().isoformat(),
104
+ active_months=active_months,
105
+ commit_count=len(commits),
106
+ contributor_count=len(authors),
107
+ contributors=sorted(authors),
108
+ merge_count=merge_count,
109
+ )
@@ -0,0 +1 @@
1
+ """Interview-prep artifacts — defense dossier and readiness tracking."""