Skip to content

Commit 43c6b7e

Browse files
authored
Merge pull request #3 from grimlor/fix/windows-utf8-encoding
fix: add explicit encoding="utf-8" to all text file I/O
2 parents 6239617 + 17d21b0 commit 43c6b7e

42 files changed

Lines changed: 1324 additions & 1124 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
if: github.event_name == 'pull_request'
1616
runs-on: ubuntu-latest
1717
steps:
18-
- uses: actions/checkout@v4
18+
- uses: actions/checkout@v6
1919
with:
2020
fetch-depth: 0
2121

@@ -61,10 +61,10 @@ jobs:
6161
python-version: ["3.11", "3.12", "3.13"]
6262

6363
steps:
64-
- uses: actions/checkout@v4
64+
- uses: actions/checkout@v6
6565

6666
- name: Install uv
67-
uses: astral-sh/setup-uv@v4
67+
uses: astral-sh/setup-uv@v8.1.0
6868

6969
- name: Set up Python ${{ matrix.python-version }}
7070
run: uv python install ${{ matrix.python-version }}

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ jobs:
1616
if: "!startsWith(github.event.head_commit.message, 'chore(release):')"
1717

1818
steps:
19-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@v6
2020
with:
2121
fetch-depth: 0 # full history needed for commit analysis
2222
fetch-tags: true
2323

2424
- name: Install uv
25-
uses: astral-sh/setup-uv@v4
25+
uses: astral-sh/setup-uv@v8.1.0
2626

2727
- name: Set up Python
2828
run: uv python install 3.13

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Thumbs.db
3838

3939
# Coverage
4040
.coverage
41+
.coverage.*
4142
coverage.json
4243
htmlcov/
4344

config/settings.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ eval_history_path = "data/eval_history.jsonl"
110110
[chroma]
111111
persist_dir = "./data/chroma_db"
112112
distance_metric = "cosine"
113+
# Flush HNSW index to disk after every N writes.
114+
# 1 = immediate durability (required for Windows correctness).
115+
sync_threshold = 1
113116

114117
[security]
115118
screen_prompt = """\

pyproject.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ dev = [
4545
[project.scripts]
4646
jobsearch-rag = "jobsearch_rag.__main__:main"
4747

48+
# ---------------------------------------------------------------------------
49+
# uv
50+
# ---------------------------------------------------------------------------
51+
[tool.uv]
52+
# Ensure the lock file resolves wheels for every CI platform.
53+
# Without this, uv may pick a version that lacks wheels for some targets
54+
# (e.g. onnxruntime 1.26.0 dropped macOS ARM64 wheels for Python 3.12).
55+
required-environments = [
56+
"sys_platform == 'darwin' and platform_machine == 'arm64'",
57+
"sys_platform == 'linux' and platform_machine == 'x86_64'",
58+
"sys_platform == 'win32' and platform_machine == 'AMD64'",
59+
]
60+
61+
# onnxruntime 1.26.0 has no macOS ARM64 wheel for Python 3.12.
62+
# Cap until upstream publishes the missing wheel.
63+
constraint-dependencies = ["onnxruntime<1.26"]
64+
4865
# ---------------------------------------------------------------------------
4966
# Taskipy
5067
# ---------------------------------------------------------------------------
@@ -139,7 +156,7 @@ asyncio_mode = "auto"
139156
python_files = ["*_test.py", "test_*.py"]
140157
python_classes = ["Test*"]
141158
python_functions = ["test_*"]
142-
addopts = "-v --strict-markers -m 'not integration and not live'"
159+
addopts = "-v --strict-markers -m 'not integration and not live' -n auto --cov-fail-under=100"
143160
log_cli = true
144161
log_cli_level = "INFO"
145162
log_cli_format = "%(asctime)s %(levelname)-8s %(name)s — %(message)s"

src/jobsearch_rag/adapters/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ async def save_storage_state(self) -> Path:
417417
path = self._config.storage_state_path
418418
path.parent.mkdir(parents=True, exist_ok=True)
419419
state = await self._context.storage_state()
420-
path.write_text(json.dumps(state, indent=2))
420+
path.write_text(json.dumps(state, indent=2), encoding="utf-8")
421421
logger.info("Session state saved to %s", path)
422422
return path
423423

src/jobsearch_rag/cli.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _read_jd_text(
5959
jd_path = jd_dir / filename
6060
if not jd_path.exists():
6161
return ""
62-
content = jd_path.read_text()
62+
content = jd_path.read_text(encoding="utf-8")
6363
marker = "## Job Description\n"
6464
idx = content.find(marker)
6565
if idx == -1:
@@ -79,7 +79,7 @@ def _load_prior_csv(csv_path: Path, *, max_full_text_chars: int) -> list[RankedL
7979
return []
8080

8181
listings: list[RankedListing] = []
82-
with open(csv_path) as f:
82+
with open(csv_path, encoding="utf-8") as f:
8383
reader = csv_mod.DictReader(f)
8484
for row in reader:
8585
external_id = row.get("external_id", "")
@@ -218,6 +218,7 @@ def handle_index(args: argparse.Namespace) -> None:
218218
store = VectorStore(
219219
persist_dir=settings.chroma.persist_dir,
220220
distance_metric=settings.chroma.distance_metric,
221+
sync_threshold=settings.chroma.sync_threshold,
221222
)
222223
indexer = Indexer(store=store, embedder=embedder)
223224

@@ -376,6 +377,7 @@ def handle_decide(args: argparse.Namespace) -> None:
376377
store = VectorStore(
377378
persist_dir=settings.chroma.persist_dir,
378379
distance_metric=settings.chroma.distance_metric,
380+
sync_threshold=settings.chroma.sync_threshold,
379381
)
380382
recorder = DecisionRecorder(
381383
store=store, embedder=embedder, decisions_dir=settings.output.decisions_dir
@@ -430,6 +432,7 @@ def handle_decisions(args: argparse.Namespace) -> None:
430432
store = VectorStore(
431433
persist_dir=settings.chroma.persist_dir,
432434
distance_metric=settings.chroma.distance_metric,
435+
sync_threshold=settings.chroma.sync_threshold,
433436
)
434437
recorder = DecisionRecorder(
435438
store=store, embedder=embedder, decisions_dir=settings.output.decisions_dir
@@ -494,6 +497,7 @@ def handle_review(args: argparse.Namespace) -> None:
494497
store = VectorStore(
495498
persist_dir=settings.chroma.persist_dir,
496499
distance_metric=settings.chroma.distance_metric,
500+
sync_threshold=settings.chroma.sync_threshold,
497501
)
498502
recorder = DecisionRecorder(
499503
store=store, embedder=embedder, decisions_dir=settings.output.decisions_dir
@@ -509,7 +513,7 @@ def handle_review(args: argparse.Namespace) -> None:
509513
ranked_listings: list[RankedListing] = []
510514
jd_dir = settings.output.jd_dir
511515

512-
with open(csv_path) as f:
516+
with open(csv_path, encoding="utf-8") as f:
513517
reader = csv_mod.DictReader(f)
514518
for _i, row in enumerate(reader, 1):
515519
title = row.get("title", "")
@@ -618,6 +622,7 @@ def handle_rescore(args: argparse.Namespace) -> None:
618622
store = VectorStore(
619623
persist_dir=settings.chroma.persist_dir,
620624
distance_metric=settings.chroma.distance_metric,
625+
sync_threshold=settings.chroma.sync_threshold,
621626
)
622627
scorer = Scorer(
623628
store=store,
@@ -731,6 +736,7 @@ def _build_eval_stack(
731736
store = VectorStore(
732737
persist_dir=settings.chroma.persist_dir,
733738
distance_metric=settings.chroma.distance_metric,
739+
sync_threshold=settings.chroma.sync_threshold,
734740
)
735741
scorer = Scorer(
736742
store=store,
@@ -865,9 +871,9 @@ def handle_export(args: argparse.Namespace) -> None:
865871

866872
fmt = args.format
867873
if fmt == "markdown" and md_path.exists():
868-
print(md_path.read_text())
874+
print(md_path.read_text(encoding="utf-8"))
869875
elif fmt == "csv" and csv_path.exists():
870-
print(csv_path.read_text())
876+
print(csv_path.read_text(encoding="utf-8"))
871877
else:
872878
print(f"No {fmt} export found in {out_dir}. Run 'search' to generate results.")
873879

@@ -882,6 +888,7 @@ def handle_reset(args: argparse.Namespace) -> None:
882888
store = VectorStore(
883889
persist_dir=settings.chroma.persist_dir,
884890
distance_metric=settings.chroma.distance_metric,
891+
sync_threshold=settings.chroma.sync_threshold,
885892
)
886893

887894
collections = [args.collection] if args.collection else list(_COLLECTIONS)

src/jobsearch_rag/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class ChromaConfig:
136136

137137
persist_dir: str
138138
distance_metric: str
139+
sync_threshold: int
139140

140141

141142
@dataclass
@@ -461,8 +462,17 @@ def _validate(data: _TOMLDict, filepath: Path) -> Settings:
461462
chroma = ChromaConfig(
462463
persist_dir=str(_require_field(chroma_data, "persist_dir", "chroma")),
463464
distance_metric=str(_require_field(chroma_data, "distance_metric", "chroma")),
465+
sync_threshold=int(_require_field(chroma_data, "sync_threshold", "chroma")),
464466
)
465467

468+
# Validate sync_threshold
469+
if chroma.sync_threshold < 1:
470+
raise ActionableError.validation(
471+
field_name="chroma.sync_threshold",
472+
reason=f"is {chroma.sync_threshold} — must be >= 1",
473+
suggestion="Set [chroma].sync_threshold to a positive integer (e.g. 1)",
474+
)
475+
466476
# Validate distance_metric
467477
valid_metrics = {"cosine", "l2", "ip"}
468478
if chroma.distance_metric not in valid_metrics:

src/jobsearch_rag/export/csv_export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def export(
5454
qualified = [r for r in listings if not (r.scores.disqualified and r.final_score == 0.0)]
5555
qualified.sort(key=lambda r: r.final_score, reverse=True)
5656

57-
with open(output_path, "w", newline="") as f:
57+
with open(output_path, "w", newline="", encoding="utf-8") as f:
5858
writer = csv.writer(f)
5959
writer.writerow(_COLUMNS)
6060

src/jobsearch_rag/export/jd_files.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def export(
9292
filepath = out / filename
9393

9494
content = self._render(r)
95-
filepath.write_text(content)
95+
filepath.write_text(content, encoding="utf-8")
9696
paths.append(filepath)
9797

9898
logger.info("Exported %d JD files to %s", len(paths), out)

0 commit comments

Comments
 (0)