Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .work/todo.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,5 @@
{"actor":"richardhightower","ev":"01M2XSTB8K77AKXWD160QM4XTJ","git":"f7134f8","item":"01M2XSTB8K5DMTPHVX3WR8QXSM","op":"create","set":{"body":"CHANGELOG.md carries two '## Unreleased' headings: the intentional placeholder at the top and a leftover from the 0.7.2 era sitting between the 0.7.3 and 0.7.2 sections. Its two bullets describe host manifests reaching 0.7.2, which shipped in 0.7.3, so they belong in the 0.7.3 section above them. A second Unreleased heading makes a release-notes reader think unreleased work is pending.","kind":"bug","level":"task","milestone":"v0.9.6","priority":"P3","status":"todo","title":"Remove the stray Unreleased heading from CHANGELOG"},"ts":"2026-09-19T21:40:35Z"}
{"actor":"richardhightower","ev":"01M2XSY20ZHBEVCZTTGZ90W0BH","item":"01M2XSTB63SP54A482P7GF95VD","op":"close","set":{"resolution":"tools/ci-local.sh parses every .github/workflows/*.yml; negative-tested against the #82 breakage","status":"done"},"ts":"2026-09-19T21:42:36Z"}
{"actor":"richardhightower","ev":"01M2XSY22P112RB524KRYMRTY6","item":"01M2XSTB8K5DMTPHVX3WR8QXSM","op":"close","set":{"resolution":"stray Unreleased heading removed; its bullets folded into 0.7.3","status":"done"},"ts":"2026-09-19T21:42:36Z"}
{"actor":"richardhightower","ev":"01M2XWFX068C9YAT53QRT3MZ8D","item":"01M2XWFX063T5QMJ7SC5ZE2R6Q","op":"create","set":{"body":"rg prints resolved paths. search() built each hit's display path against the bundle the caller passed, so when that bundle still contained a symlink the two did not match and relative_to raised. Search failed outright rather than returning a wrong answer. On macOS /var is a symlink to /private/var, so every temporary bundle reproduces it; a symlinked checkout or a container bind mount is the same shape on Linux. CI has neither, so the defect shipped. Resolve the bundle once at the top of search() and every engine agrees.","kind":"bug","level":"task","milestone":"v0.9.6","priority":"P1","status":"todo","title":"search dies on a symlinked bundle root"},"ts":"2026-09-19T22:27:18Z"}
{"actor":"richardhightower","ev":"01M2XWG1S5HFP2QQ5MHKHNHKAA","item":"01M2XWFX063T5QMJ7SC5ZE2R6Q","op":"close","set":{"resolution":"search() resolves the bundle once; regression test searches through a real symlink","status":"done"},"ts":"2026-09-19T22:27:23Z"}
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
wiki_key: roadmap
doc_type: roadmap
truth_state: current
source_hash: eb13176e
generated_at: 2026-09-19T21:42:36Z
source_hash: 81b0e6dc
generated_at: 2026-09-19T22:27:23Z
---

<!-- GENERATED by worklog roadmap-render. DO NOT EDIT. -->
Expand Down
7 changes: 7 additions & 0 deletions scripts/pkc_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ def search(
if not terms:
return [], "scan"

# rg yields resolved paths, so on a symlinked root the rel path built for
# each hit below raised ValueError against the caller's bundle (#85).
# /var -> /private/var on macOS, a symlinked checkout, a bind mount. Every
# engine derives its paths from the bundle it is handed, so resolving once
# here makes candidate_files and the loop below agree. One syscall, not one
# per file: _filter_rg_hits keeps its own guard for direct callers.
bundle = bundle.resolve()
type_filter = {t.lower() for t in (types or []) if t}
results: list[dict[str, Any]] = []
files, engine = candidate_files(
Expand Down
23 changes: 23 additions & 0 deletions tests/test_pkc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,29 @@ def test_titleless_stem_match_is_invisible_on_every_engine(self):
self.assertEqual([h["path"] for h in hits], [h["path"] for h in scan])
self.assertEqual([h["score"] for h in hits], [h["score"] for h in scan])

def test_symlinked_bundle_agrees_across_engines(self):
# Issue #85. rg prints resolved paths, so building the rel path against
# an unresolved bundle raised ValueError and search() died outright.
# /var -> /private/var on macOS does this to every tempfile bundle; a
# symlinked checkout or a bind mount is the same shape on Linux, which
# is why CI never saw it.
real = Path(tempfile.mkdtemp())
(real / "index.md").write_text('---\nokf_version: "0.2"\ntitle: t\n---\n', encoding="utf-8")
(real / "features").mkdir()
(real / "features" / "other.md").write_text(
"---\ntype: Feature\ntitle: Zebra crossing\n---\n\nplain\n", encoding="utf-8"
)
link = Path(tempfile.mkdtemp()) / "via-symlink"
link.symlink_to(real, target_is_directory=True)

scan, scan_engine = search_bundle(link, "zebra", use_rg=False, use_index=False)
accel, rg_engine = search_bundle(link, "zebra", use_rg=True, use_index=False)
indexed, idx_engine = search_bundle(link, "zebra", use_rg=False, use_index=True)
self.assertEqual((scan_engine, rg_engine, idx_engine), ("scan", "rg", "index"))
self.assertEqual([h["path"] for h in scan], ["/features/other.md"])
for hits in (accel, indexed):
self.assertEqual([h["path"] for h in hits], [h["path"] for h in scan])

def test_search_rg_matches_scan_ranking(self):
bundle = ROOT / "sample-knowledge"
scan, scan_engine = search_bundle(bundle, "JWT", limit=10, use_rg=False, use_index=False)
Expand Down
Loading